Merge branch 'stack/agent-profiles-1-seam' into stack/agent-profiles-3-wire
Rewires this layer onto the standing-mount model: - serviceForAgent roots its search at the agent's standing mount (parent scope key → live mount fiber) — the composition no longer lives under the agent's own fiber, and two agents on one preset now address ONE instance, which the sharing test asserts instead of distinctness. - viewFor/historyPage take a registry view SCOPE. A live agent is that scope; a cold read uses the recorded preset's standing key via standingKeyFor — composing plugins but starting no agent, session, or turn. A header without a preset (a pre-roster log) renders through the DEFAULT preset's standing layer; an unusable preset degrades the read to generic cards, never fails it. This turns produced-files and chat-scroll green structurally, with the token counts untouched (no resume, so the projections fold stays detached). - The detached projections baseline now includes every standing unit's key at its empty fold (todos: null): the standing mount registers units deterministically, which is what makes the client's "omitted key = capability absence → clear" rule safe again. seeded-history's contract test asserts the new shape. - The standard preset's realm preamble no longer claims a shared label pools instances — provide() throws on the second registration under one realm symbol; labels join REALMS.
This commit is contained in:
@@ -18,7 +18,7 @@ import { pathToFileURL } from 'node:url'
|
||||
import { Context, type Fiber } from 'cordis'
|
||||
import { Include } from '@cordisjs/plugin-include'
|
||||
import type { EntryTree } from '@cordisjs/plugin-loader'
|
||||
import { scopeOf } from '@deepseek-ai/dsh-scope'
|
||||
import { scopeOf, scopeParentOf, type ScopeKey } from '@deepseek-ai/dsh-scope'
|
||||
import { PresetMountError, type AgentPreset } from './types.ts'
|
||||
|
||||
/** What one mounted subtree publishes about itself for the audit to read. */
|
||||
@@ -77,6 +77,8 @@ export interface PresetMount {
|
||||
readonly presetId: string
|
||||
/** The mounted subtree's fiber. */
|
||||
readonly fiber: Fiber
|
||||
/** The standing scope key agents are parented to (undefined only in torn-down records). */
|
||||
readonly key: ScopeKey | undefined
|
||||
}
|
||||
|
||||
const mounts = new Set<PresetMount>()
|
||||
@@ -189,14 +191,22 @@ export function serviceForAgent<K extends string & keyof Context>(
|
||||
agent: { ctx: Context },
|
||||
name: K,
|
||||
): Context[K] | undefined {
|
||||
const root = agent.ctx.fiber
|
||||
// The agent's own key is parented to its preset's standing key; the mount
|
||||
// is no longer under the agent's fiber, so the search roots at the standing
|
||||
// mount instead of walking up from the agent.
|
||||
const agentKey = scopeOf(agent.ctx)
|
||||
if (agentKey === undefined) return undefined
|
||||
const standingKey = scopeParentOf(agentKey)
|
||||
if (standingKey === undefined) return undefined
|
||||
const mount = livePresetMounts().find(candidate => candidate.key === standingKey)
|
||||
if (mount === undefined) return undefined
|
||||
const store = ctx.reflect.store
|
||||
for (const key of Object.getOwnPropertySymbols(store)) {
|
||||
const impl = store[key]
|
||||
/* v8 ignore next -- cordis deletes a store slot on disposal rather than clearing it */
|
||||
if (impl === undefined) continue
|
||||
if (impl.name !== name) continue
|
||||
if (withinFiber(impl.fiber, root)) return impl.value as Context[K]
|
||||
if (withinFiber(impl.fiber, mount.fiber)) return impl.value as Context[K]
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
@@ -248,8 +258,9 @@ export async function mountPreset(agentCtx: Context, preset: AgentPreset): Promi
|
||||
)
|
||||
}
|
||||
const config: Include.Config = { path: pathToFileURL(preset.path).href }
|
||||
// Before the record this mount is about to add: every session takes this
|
||||
// path, so it is what keeps the set bounded on a host that never reads it.
|
||||
// Before the record this mount is about to add: standing mounts are one per
|
||||
// preset and live until whole-tree teardown, so pruning here only sweeps
|
||||
// records of torn-down runtimes (tests; an HMR reload of the roster).
|
||||
pruneDisposedMounts()
|
||||
const handle = agentCtx.plugin(PresetTree, config)
|
||||
try {
|
||||
@@ -269,7 +280,7 @@ export async function mountPreset(agentCtx: Context, preset: AgentPreset): Promi
|
||||
+ 'a preset service must sit behind an `isolate` realm or move to the host composition',
|
||||
)
|
||||
}
|
||||
mounts.add({ presetId: preset.id, fiber })
|
||||
mounts.add({ presetId: preset.id, fiber, key: scopeOf(agentCtx) })
|
||||
} catch (error) {
|
||||
try {
|
||||
await handle.dispose()
|
||||
|
||||
Reference in New Issue
Block a user