fix(session): persist the delegation depth in the session header

A subagent child's recursion depth lived only in runtime AgentOptions,
so a persisted child came back from resume counted as top-level and
maxDepth stopped binding after every restart. Add
SessionHeader.delegationDepth, round-trip it through the JSONL and
SQLite backends (SQLite schema v5), restore it on agent-loop resume,
and write it when the in-process backends create a child. The seam now
owns the shared depth vocabulary (delegationDepthOf): the persisted
header is authoritative and monotone — runtime options may deepen it
but never lower it.
This commit is contained in:
Yichen Jiang
2026-07-19 17:20:36 +08:00
parent e05e7a0870
commit 0d00106fa8
29 changed files with 229 additions and 106 deletions

View File

@@ -106,6 +106,27 @@ export function runCoordinatorContract(name: string, makeFixture: () => Promise<
}
})
it('round-trips the delegation depth through persistence', async () => {
// A subagent child's recursion budget lives in its header; a reload that
// dropped it would reset the child to top-level and un-bound maxDepth
// (JSONL stores it in the header line; SQLite uses `delegation_depth`).
const fix = await makeFixture()
const { ctx, fiber } = await freshCtx(fix)
try {
const session = ctx.sessions.create(SessionId('delegated-child'), {
meta: { cwd: WORK, parentSession: SessionId('root'), delegationDepth: 2 },
})
send(session, oneTurnLog())
await ctx.parallel('session/flush', session)
const loaded = await ctx.sessionPersistence.load(SessionId('delegated-child'))
expect(loaded.meta.delegationDepth).toBe(2)
} finally {
await fiber.dispose()
await fix.cleanup()
}
})
it('source-frozen events cannot be mutated after buffering and persist unchanged', async () => {
const fix = await makeFixture()
const { ctx, fiber } = await freshCtx(fix)