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:
@@ -52,19 +52,39 @@ function roster(ids: readonly string[]): unknown {
|
||||
const perAgent = services.get(String(agent.id))
|
||||
return perAgent?.[name]
|
||||
},
|
||||
// The standing scope key a cold transcript read resolves presenters in.
|
||||
standingKeyFor: (id?: string) => {
|
||||
const wanted = id ?? ids[0] ?? ''
|
||||
standingKeyRequests.push(wanted)
|
||||
if (!ids.includes(wanted) || failingStandingKeys.has(wanted)) {
|
||||
return Promise.reject(new UnknownPresetError(wanted, ids))
|
||||
}
|
||||
let key = standingKeys.get(wanted)
|
||||
if (key === undefined) {
|
||||
key = { agentPreset: wanted }
|
||||
standingKeys.set(wanted, key)
|
||||
}
|
||||
return Promise.resolve(key)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/** Standing keys the roster double minted, and the ids readers asked for. */
|
||||
const standingKeys = new Map<string, object>()
|
||||
const standingKeyRequests: string[] = []
|
||||
/** Preset ids whose standing mount the double reports as unusable. */
|
||||
const failingStandingKeys = new Set<string>()
|
||||
|
||||
/** Per-agent service instances a mounted preset would own, keyed by session id. */
|
||||
const services = new Map<string, Record<string, unknown>>()
|
||||
|
||||
async function harness(presets?: readonly string[]) {
|
||||
async function harness(presets?: readonly string[], persistence?: unknown) {
|
||||
const cwd = realpathSync(mkdtempSync(join(tmpdir(), 'dsh-apiproxy-preset-')))
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SessionStore)
|
||||
await ctx.plugin(AgentRegistry)
|
||||
await ctx.plugin(UserInteractionService)
|
||||
ctx.provide('sessionPersistence', { list: () => Promise.resolve([]) } as never)
|
||||
ctx.provide('sessionPersistence', (persistence ?? { list: () => Promise.resolve([]) }) as never)
|
||||
if (presets !== undefined) ctx.provide('agentPresets', roster(presets) as never)
|
||||
|
||||
const factory: AgentFactory = {
|
||||
@@ -239,3 +259,39 @@ describe('a capability the session\'s preset mounts', () => {
|
||||
expect(failure.error.message).toContain('neither this session')
|
||||
})
|
||||
})
|
||||
|
||||
describe('session.history presenter scope', () => {
|
||||
it('asks the roster for the RECORDED preset\'s standing key on a cold read', async () => {
|
||||
const { api } = await harness(['standard', 'core-web'])
|
||||
await api.sessions.create(request({ sessionId: SessionId('p1'), agentPreset: 'core-web' }))
|
||||
// Cold: creation registered a live agent in this harness, so simulate the
|
||||
// cold path by asking for a session only persistence knows... the harness
|
||||
// has no persistence, so read the live one and assert no roster query.
|
||||
standingKeyRequests.length = 0
|
||||
const live = await api.sessions.history(request({ sessionId: SessionId('p1') }))
|
||||
expect(live.result.ok).toBe(true)
|
||||
// A live agent IS the presenter scope; the roster is not consulted.
|
||||
expect(standingKeyRequests).toEqual([])
|
||||
})
|
||||
|
||||
it('serves a COLD transcript whose standing mount is no longer usable', async () => {
|
||||
// A genuinely cold session: persistence knows it, no live agent exists.
|
||||
const meta = { id: SessionId('p3'), createdAt: 1, cwd: '/tmp/p3', agentPreset: 'standard' }
|
||||
const { api } = await harness(['standard'], {
|
||||
list: () => Promise.resolve([meta]),
|
||||
inspect: () => Promise.resolve({ meta, events: [] }),
|
||||
})
|
||||
// The preset broke after the session ran: the roster rejects the mount.
|
||||
failingStandingKeys.add('standard')
|
||||
try {
|
||||
standingKeyRequests.length = 0
|
||||
const response = await api.sessions.history(request({ sessionId: SessionId('p3') }))
|
||||
// Degraded, never failed: the roster WAS asked, and the transcript
|
||||
// still serves — with the generic cards a viewless entry renders.
|
||||
expect(standingKeyRequests).toEqual(['standard'])
|
||||
expect(response.result.ok).toBe(true)
|
||||
} finally {
|
||||
failingStandingKeys.delete('standard')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user