| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110 |
- import type { Context } from 'cordis'
- import { SessionId } from '@deepseek-ai/dsh-session'
- export const name = 'subagent-durability-failure'
- export const inject = ['agents', 'sessionPersistence', 'subagents']
- /**
- * The authored parent transcript names the background child by a stable
- * placeholder id, but the live continuable child is minted with a fresh random
- * session id at run time. This snapshot-only overlay bridges that gap and forces
- * deterministic ordering plus authored child durability failures:
- *
- * - `PLACEHOLDER_CHILD_ID` in a scripted `send_message` is remapped to the real
- * child so both follow-ups queue onto the same live inbox in FIFO order.
- * - The unknown-id `send_message` (`UNKNOWN_CHILD_ID`) resolves through a
- * persistence load fenced behind both accepted follow-ups, so the transcript
- * records the same order on every runner.
- * - The child's final continuation turn fails its durability checkpoint with a
- * fixed message, so the scenario proves child-first disposal survives a failed
- * last flush.
- * - Under `DSH_SUBAGENT_PUBLISHED_FAILURE`, a one-shot child's first
- * follow-up fails after publication, so its model prompt never runs; its
- * published handle then also fails disposal, so the parent observes both
- * independent failures.
- */
- const PLACEHOLDER_CHILD_ID = '33333333-3333-4333-8333-333333333333'
- const UNKNOWN_CHILD_ID = '22222222-2222-4222-8222-222222222222'
- /** The child continuation turn whose durability checkpoint is forced to fail. */
- const FAILED_CHECKPOINT_TURN = 3
- /** Fail the child checkpoint and stabilize the authored follow-up failure ordering. */
- export function apply(ctx: Context): void {
- const followupsAccepted = Promise.withResolvers<undefined>()
- const publishedFailure = process.env.DSH_SUBAGENT_PUBLISHED_FAILURE === '1'
- const persistence = ctx.sessionPersistence
- const load = persistence.load.bind(persistence)
- const agents = ctx.agents
- const create = agents.create.bind(agents)
- agents.create = async (options) => {
- const handle = await create(options)
- if (!publishedFailure || options.meta?.parentSession === undefined) return handle
- handle.agent.followup = () => {
- throw new Error('snapshot published run failed')
- }
- return {
- ...handle,
- async dispose() {
- await handle.dispose()
- throw new Error('snapshot published handle disposal failed')
- },
- }
- }
- // The unavailable-child lookup is real asynchronous I/O. Fence it behind both
- // authored follow-ups so runner speed cannot reorder the exact log.
- persistence.load = async (id) => {
- if (id === UNKNOWN_CHILD_ID) await followupsAccepted.promise
- return load.call(persistence, id)
- }
- ctx.effect(() => () => {
- agents.create = create
- persistence.load = load
- followupsAccepted.resolve(undefined)
- }, 'subagent snapshot ordering')
- // Remap the placeholder child id in a follow-up to the live child. The child
- // id the model "knows" is authored into the transcript, while the running
- // child is minted with a random id, so without this the follow-ups would
- // never reach the live inbox.
- let realChildId: string | undefined
- const subagents = ctx.subagents as unknown as {
- followup: (authority: unknown, childId: SessionId, content: unknown, options: unknown) => Promise<unknown>
- }
- const deliver = subagents.followup.bind(subagents)
- subagents.followup = (authority, childId, content, options) => {
- const mapped = childId === PLACEHOLDER_CHILD_ID && realChildId !== undefined
- ? SessionId(realChildId)
- : childId
- return deliver(authority, mapped, content, options)
- }
- // Both authored follow-ups reach the child inbox before the unknown-id lookup
- // runs, so the queued FIFO order is what the transcript records. The first
- // child enqueue is the initial delegation, which also pins the real child id.
- let accepted = 0
- ctx.on('agent/inbox/inserted', (agent) => {
- if (agent.session.header.parentSession === undefined) return
- if (realChildId === undefined) realChildId = agent.session.header.id
- accepted += 1
- if (accepted >= 3) followupsAccepted.resolve(undefined)
- })
- ctx.on('agent/pre-step', async (agent, _messages, _context, next) => {
- if (agent.session.header.parentSession !== undefined) await followupsAccepted.promise
- return next()
- })
- // The child's ordinary per-turn flushes succeed; only the final continuation
- // turn's durability checkpoint fails, turning that turn/end into a durable
- // error the parent never sees.
- const childTurn = new WeakMap<object, number>()
- ctx.on('session/event', (session, event) => {
- if (session.header.parentSession === undefined || event.type !== 'turn/start') return
- childTurn.set(session, event.data.turn)
- })
- ctx.on('session/flush', (session) => {
- if (session.header.parentSession === undefined) return
- if (childTurn.get(session) === FAILED_CHECKPOINT_TURN) throw new Error('snapshot disk full')
- })
- }
|