fix(session-query): avoid deep lineage recursion (round 2)

This commit is contained in:
Hypatia May
2026-07-13 13:53:03 +08:00
parent 320de5466a
commit 8e019f2a65
2 changed files with 41 additions and 4 deletions

View File

@@ -248,10 +248,26 @@ function buildDescendants(
childrenByParent: ReadonlyMap<SessionId, readonly SessionRecord[]>,
sessionId: SessionId,
): SessionLineageNode[] {
return (childrenByParent.get(sessionId) ?? []).map(child => ({
session: cloneRecord(child),
descendants: buildDescendants(childrenByParent, child.header.id),
}))
const descendants: SessionLineageNode[] = []
const stack = [{ sessionId, descendants }]
while (stack.length > 0) {
// The length guard proves a frame exists.
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const frame = stack.pop()!
const nodes: SessionLineageNode[] = []
for (const child of childrenByParent.get(frame.sessionId) ?? []) {
const node = { session: cloneRecord(child), descendants: [] }
nodes.push(node)
frame.descendants.push(node)
}
for (let index = nodes.length - 1; index >= 0; index -= 1) {
// The loop bounds prove this indexed node exists.
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const node = nodes[index]!
stack.push({ sessionId: node.session.header.id, descendants: node.descendants })
}
}
return descendants
}
function compareSessionsAscending(a: SessionRecord, b: SessionRecord): number {

View File

@@ -195,6 +195,27 @@ describe('session lineage tracing', () => {
await expect(ctx.sessionQuery.traceSession(durable.id))
.rejects.toThrow(expectCode('SESSION_QUERY_PERSISTENCE_FAILED'))
})
it('constructs deeply nested descendants without consuming the JavaScript call stack', async () => {
const ctx = await queryContext()
const root = ctx.sessions.create(SessionId('deep-0'), { meta: { createdAt: 0 } })
let parent = root
for (let depth = 1; depth < 3_000; depth += 1) {
parent = ctx.sessions.create(SessionId(`deep-${depth}`), {
meta: { createdAt: depth, parentSession: parent.id },
})
}
const trace = await ctx.sessionQuery.traceSession(root.id)
expect(trace.complete).toBe(true)
let node = trace.descendants[0]
for (let depth = 1; depth < 3_000; depth += 1) {
if (node === undefined) throw new Error(`lineage ended before depth ${depth}`)
if (depth === 2_999) expect(node.session.header.id).toBe(SessionId('deep-2999'))
node = node.descendants[0]
}
expect(node).toBeUndefined()
})
})
describe('session event tracing', () => {