Files
deepseek-harness/packages/host/plugin-inventory/src/index.ts
T
Yichen Jiang 0caceb2ced fix(boot): commit a recomposition only once the root include accepts it
`ProfileRuntime.recompose({ reloadBundles: true })` replaced its profile
before the Loader had accepted the update, so a rejected recomposition left
`current`, `layers`, and `originOf` describing a stack that never mounted.
The user patch watchers took a second path through the launcher's
`composeLive`, which wrote the new conflicts into the failure registry
before the update and kept composing from the runtime's stale profile.
Conflict records shared the registry with row failures under a fabricated
entry id and a group id that meant three different things, so the plugin
inventory attributed a conflict to the layer that won the id, and a bundle
disabled after failing kept its import and apply records forever.

The runtime now holds one committed composition — profile, stack ownership,
conflicts — and publishes a candidate only after `entry.update` resolves;
`originOf` reads the committed owners instead of recomputing them. The
watchers call `recompose` through a `reapply` callback, so every live change
goes through the one entry point. Conflicts live on the runtime with their
messages and never enter `pluginFailures`, whose records name rows that
reached the Loader; the inventory lists them under the layer that lost. A
contained group clears its rows' records when it unmounts.
2026-09-05 22:19:36 +08:00

152 lines
6.1 KiB
TypeScript

/** Read-only projection of the current Cordis Loader plugin entries. */
import type { Context, FiberState } from '@deepseek-ai/cordis'
import type {} from '@deepseek-ai/cordis-plugin-loader'
// Type-only: the optional agent-preset roster resolved through `ctx.get`.
import type {} from '@deepseek-ai/dsh-agent-presets'
// Type-only: the optional profile runtime and contained-failure registry the
// boot glue provides, both resolved through `ctx.get`.
import type {} from '@deepseek-ai/dsh-app-boot'
import { TypertRemoteService, Remote } from '@deepseek-ai/dsh-typert-protocol'
// Typert-generated ./typert and ./remote artifacts import Zod at runtime.
import type {} from 'zod'
import type {
AgentPresetPluginGroup,
PluginEntryId,
PluginFiberPhase,
PluginInventoryEntry,
PluginInventorySnapshot,
PluginPackageRef,
} from './types.ts'
export type * from './types.ts'
/** Brand an existing Loader-tree entry id at the owning boundary. */
function pluginEntryId(value: string): PluginEntryId {
return value as PluginEntryId
}
/** The wire view of a row's package origin. */
function packageRef(origin: { packageName: string; version?: string }): PluginPackageRef {
return {
name: origin.packageName,
...origin.version === undefined ? {} : { version: origin.version },
}
}
/** Runtime mirror: FiberState is a cross-package const enum. */
const FIBER_STATE = {
PENDING: 0 as FiberState.PENDING,
LOADING: 1 as FiberState.LOADING,
ACTIVE: 2 as FiberState.ACTIVE,
FAILED: 3 as FiberState.FAILED,
DISPOSED: 4 as FiberState.DISPOSED,
UNLOADING: 5 as FiberState.UNLOADING,
} as const
/** Complete public projection of Cordis Fiber states. */
const FIBER_PHASE = {
[FIBER_STATE.PENDING]: 'pending',
[FIBER_STATE.LOADING]: 'loading',
[FIBER_STATE.ACTIVE]: 'active',
[FIBER_STATE.FAILED]: 'failed',
[FIBER_STATE.DISPOSED]: null,
[FIBER_STATE.UNLOADING]: 'unloading',
} as const satisfies Record<FiberState, PluginFiberPhase>
/** Remote-only service exposing the Loader's current non-group entry state. */
export class PluginInventoryGateway extends TypertRemoteService {
static inject = ['loader']
constructor(ctx: Context) {
super(ctx, 'pluginInventory')
}
/**
* Read the Loader directly on every call. Cordis's internal plugin/status
* events already maintain Entry.fiber and Fiber.state, so a second cache
* would only add another lifecycle truth to keep synchronized.
*
* When an agent-preset roster is composed, the snapshot also carries each
* preset's composition rows, because those rows — not the Loader's own
* entries — are where a deployment that mounts the roster runs its
* model-facing plugins.
* @returns Current non-group Loader entries in Loader order, with per-preset
* compositions when a roster is composed.
*/
@Remote('list')
async list(): Promise<PluginInventorySnapshot> {
const entries: PluginInventoryEntry[] = []
const runtime = this.ctx.get('profileRuntime')
const failures = this.ctx.get('pluginFailures')
const userDisabled = runtime?.userDisabledRowIds() ?? new Set<string>()
const listed = new Set<string>()
for (const entry of this.ctx.loader.entries()) {
if (entry.options.group) continue
listed.add(entry.id)
// Provenance and user patches address rows by the id the composition
// declares; the tree-wide `entry.id` adds the owning include's prefix.
const origin = runtime?.originOf(entry.options.id)
const enabled = !entry.disabled
const failure = failures?.get(entry.id)
entries.push({
entryId: pluginEntryId(entry.id),
moduleName: entry.options.name,
enabled,
fiberPhase: entry.fiber === undefined ? null : FIBER_PHASE[entry.fiber.state],
trust: origin?.trust ?? 'builtin',
...origin === undefined ? {} : { package: packageRef(origin) },
...enabled ? {} : { disabledBy: userDisabled.has(entry.options.id) ? 'user' as const : 'composition' as const },
...failure === undefined ? {} : { failure: { stage: failure.stage, message: failure.message } },
})
}
// A row an isolated bundle failed to start is gone from the tree; the
// registry is its only record, and the list must still show it.
for (const failure of failures?.list() ?? []) {
if (listed.has(failure.entryId)) continue
const origin = runtime?.originOf(failure.rowId)
entries.push({
entryId: pluginEntryId(failure.entryId),
moduleName: failure.moduleName,
enabled: true,
fiberPhase: 'failed',
trust: origin?.trust ?? 'external',
...origin === undefined ? {} : { package: packageRef(origin) },
failure: { stage: failure.stage, message: failure.message },
})
}
// A row the composition left out never reached the tree; the conflict
// names the layer that lost, so no lookup of the id's owner is needed.
if (runtime !== undefined) {
for (const conflict of runtime.conflicts) {
const version = runtime.layers.find(candidate => candidate.packageName === conflict.packageName)?.version
entries.push({
entryId: pluginEntryId(`conflict:${conflict.layer}:${conflict.rowId}`),
moduleName: conflict.moduleName,
enabled: true,
fiberPhase: 'failed',
trust: conflict.packageName === undefined ? 'builtin' : 'external',
...conflict.packageName === undefined
? {}
: { package: packageRef({ packageName: conflict.packageName, ...version === undefined ? {} : { version } }) },
failure: { stage: 'conflict', message: conflict.message },
})
}
}
const presets = this.ctx.get('agentPresets')
if (presets === undefined) return { entries }
const agentPresets: AgentPresetPluginGroup[] = (await presets.compositionInventory()).map(
composition => ({
...composition,
rows: composition.rows.map(({ fiberState, ...row }) => ({
...row,
fiberPhase: fiberState === undefined ? null : FIBER_PHASE[fiberState],
})),
}),
)
return { entries, agentPresets }
}
}
export default PluginInventoryGateway