refactor(agent): complete inbox lifecycle migration

This commit is contained in:
_Kerman
2026-08-03 12:25:33 +08:00
parent dc1d542092
commit 49e90695cc
214 changed files with 6019 additions and 4235 deletions

View File

@@ -48,6 +48,7 @@ function sessionAgent(session: Session, id = 'agent'): Agent {
steer: () => {},
inject: () => { throw new Error('time-context must append directly to the open step') },
cancel() {},
runMaintenance: task => task(new AbortController().signal),
whenIdle: () => Promise.resolve(),
}
}

View File

@@ -104,6 +104,7 @@ function sessionAgent(session: Session, id = 'agent'): Agent {
steer: () => {},
inject: () => { throw new Error('tmux-context must append directly to the open step') },
cancel() {},
runMaintenance: task => task(new AbortController().signal),
whenIdle: () => Promise.resolve(),
}
}

View File

@@ -201,12 +201,8 @@ export function apply(ctx: Context, config: Config): void {
}
const waitForProjections = async (agent: Agent): Promise<void> => {
while (true) {
const projection = projectionTails.get(agent)
if (projection === undefined) return
await projection
if (projectionTails.get(agent) === projection) return
}
let projection: Promise<void> | undefined
while ((projection = projectionTails.get(agent)) !== undefined) await projection
}
ctx.on('agent/pre-step', async (

View File

@@ -182,6 +182,7 @@ function stubAgent(cwd?: string, seed: SessionEvent[] = []): Agent {
steer: () => {},
inject: () => { throw new Error('workspace-context must append directly to the open step') },
cancel() {},
runMaintenance: task => task(new AbortController().signal),
whenIdle: () => Promise.resolve(),
}
}
@@ -2640,7 +2641,13 @@ describe('dynamic nested workspace context injection', () => {
warmCache.set(agent.session, new Map(loaded.versions))
const options = {
authorityMessages,
scopeMessages: [],
scopeMessages: [createUserMessage({
content: [{ type: 'text', text: 'pending baseline duplicate' }],
source: {
kind: 'workspace-instructions',
changes: [{ action: 'set', scope: sk('.', 'AGENTS.md'), path: 'AGENTS.md' }],
},
})],
touchedPaths: [],
includeBaselineScopes: false,
signal: testToolSignal,
@@ -3497,6 +3504,14 @@ describe('dynamic nested workspace context injection', () => {
signal: testToolSignal,
callId: CallId('null-arguments'), name: 'read', arguments: null, agent,
}), plainResult)
ctx.emit('tools/result', stubToolExecution({
signal: testToolSignal,
callId: CallId('missing-path'), name: 'read', arguments: {}, agent,
}), plainResult)
ctx.emit('tools/result', stubToolExecution({
signal: testToolSignal,
callId: CallId('non-string-path'), name: 'read', arguments: { file_path: 1 }, agent,
}), plainResult)
ctx.emit('tools/result', stubToolExecution({
signal: testToolSignal,
callId: CallId('blank-path'), name: 'read', arguments: { file_path: ' ' }, agent,
@@ -3514,6 +3529,35 @@ describe('dynamic nested workspace context injection', () => {
}
})
it('warns when an asynchronous file-result projection fails', async () => {
const ctx = new Context()
try {
await ctx.plugin(RecordingFileSystem)
await ctx.plugin(workspaceContext, { maxBytes: 65536 })
const fs = ctx.fs as RecordingFileSystem
const agent = stubAgent('/')
const failure = new Error('projection failed')
const warn = vi.spyOn(ctx.logger, 'warn').mockImplementation(() => undefined)
fs.entries.set('/.git', { type: 'directory' })
fs.entries.set('/AGENTS.md', { type: 'file', content: 'workspace rule' })
vi.spyOn(agent.inbox, 'prepend').mockImplementationOnce(() => { throw failure })
ctx.emit('tools/result', stubToolExecution({
signal: testToolSignal,
callId: CallId('projection-failure'),
name: 'read',
arguments: { file_path: 'file.txt' },
agent,
}), { content: [], isError: false, value: null })
await vi.waitFor(() => {
expect(warn).toHaveBeenCalledWith('workspace instruction refresh failed: %o', failure)
})
} finally {
await ctx.fiber.dispose()
}
})
it('does not attach nested instructions when the byte budget is disabled', async () => {
const root = await tempRepo()
const home = await tempRepo()