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

@@ -625,6 +625,7 @@ export class AgentLoop extends Service implements AgentFactory {
...loaded.meta.cwd === undefined ? {} : { cwd: loaded.meta.cwd },
...loaded.meta.parentSession === undefined ? {} : { parentSession: loaded.meta.parentSession },
...loaded.meta.seedLength === undefined ? {} : { seedLength: loaded.meta.seedLength },
...loaded.meta.delegationDepth === undefined ? {} : { delegationDepth: loaded.meta.delegationDepth },
},
})
const agent = transaction.prepare(agentOptions, session, this.maxParallelToolCalls)

View File

@@ -411,7 +411,7 @@ describe('the session-persistence RFC: AgentLoop factory create/resume', () => {
await ctx.fiber.dispose()
})
it('resume of a forked session preserves the parentSession lineage and seed boundary in the header', async () => {
it('resume of a forked session preserves the lineage, seed boundary, and delegation depth in the header', async () => {
// Lifecycle 1: persist a FORKED session (carries parentSession + seedLength
// in its header) by creating it with a complete-turn seed — the write path
// materializes the fork (header + seed) on disk.
@@ -423,7 +423,7 @@ describe('the session-persistence RFC: AgentLoop factory create/resume', () => {
const { ctx: ctx1, root } = await persistentHarness(adapter1)
const forked = ctx1.sessions.create(SessionId('forked-sess'), {
seed,
meta: { cwd: '/w', parentSession: SessionId('parent-sess'), seedLength: seed.length },
meta: { cwd: '/w', parentSession: SessionId('parent-sess'), seedLength: seed.length, delegationDepth: 1 },
})
await ctx1.parallel('session/flush', forked)
await ctx1.fiber.dispose()
@@ -447,6 +447,9 @@ describe('the session-persistence RFC: AgentLoop factory create/resume', () => {
expect(a2.session.header.parentSession).toBe('parent-sess')
expect(a2.session.header.cwd).toBe('/w')
expect(a2.session.header.seedLength).toBe(seed.length)
// The recursion budget survives resume — a dropped depth would let a
// resumed child delegate as if it were top-level.
expect(a2.session.header.delegationDepth).toBe(1)
await ctx2.fiber.dispose()
})