fix(subagent): harden continuable persistence

This commit is contained in:
Dudu-0223
2026-07-24 12:39:07 +08:00
committed by imccyu
parent bb8ea2be51
commit 1ab3cbf673
23 changed files with 412 additions and 56 deletions

View File

@@ -0,0 +1,45 @@
# Keyless counterpart to subagent-durability-failure.cordis.yml: replace the
# live adapter with replay and fail the provider-owned final child checkpoint.
- id: base
name: '@cordisjs/plugin-include'
config:
path: ./cordis.yml
patches:
- id: llm-deepseek
name: '@deepseek-ai/dsh-llm-deepseek'
disabled: true
- id: acp-agent
name: '@deepseek-ai/dsh-acp-demo'
config:
provider: deepseek-official
model: deepseek-v4-flash
persistenceRoot: !!js process.env.DSH_SNAPSHOT_SESSIONS_ROOT ?? './.sessions'
persistenceCompression: none
workspaceContext:
maxBytes: 65536
persona: |
You are a coding assistant powered by the {{model}} model. Your working directory is {{cwd}}. Your bash tool runs under a file sandbox — a `[sandbox: file access denied …]` result is policy, not a command bug.
Verify your work by running the code or tests. Keep answers brief and factual.
- id: sandbox
name: '@deepseek-ai/dsh-sandbox-local'
config:
runnerCommand:
- bash
- -c
- while [ "$1" != "--" ]; do shift; done; shift; exec "$@"
- passthrough-runner
runnerFailureSignatures:
- 'passthrough-runner: profile rejected'
- insert:
- id: llm-replay
name: '@deepseek-ai/dsh-llm-replay'
config:
providers:
- id: deepseek-official
name: DeepSeek
models:
- id: deepseek-v4-flash
- id: deepseek-v4-pro
- id: subagent-durability-failure
name: './tests/fixtures/subagent-durability-failure.ts'

View File

@@ -0,0 +1,10 @@
# Snapshot-only durability-failure overlay. The child turn's ordinary flush
# succeeds; the provider-owned final confirmation fails deterministically.
- id: base
name: '@cordisjs/plugin-include'
config:
path: ./cordis.yml
patches:
- insert:
- id: subagent-durability-failure
name: './tests/fixtures/subagent-durability-failure.ts'

View File

@@ -41,6 +41,9 @@ const DEPTH_TWO_CONFIG = fileURLToPath(new URL('../depth-two.cordis.yml', import
const SESSION_SANDBOX_ROOT_CONFIG = fileURLToPath(new URL('../session-sandbox-root.cordis.yml', import.meta.url))
const RETRY_CONFIG = fileURLToPath(new URL('../retry.cordis.yml', import.meta.url))
const SESSION_TITLE_CONFIG = fileURLToPath(new URL('../session-title.cordis.yml', import.meta.url))
const SUBAGENT_DURABILITY_FAILURE_CONFIG = fileURLToPath(
new URL('../subagent-durability-failure.cordis.yml', import.meta.url),
)
const LSP_CONFIG = fileURLToPath(new URL('./lsp.cordis.yml', import.meta.url))
const WEB_CONFIG = fileURLToPath(new URL('../web.cordis.yml', import.meta.url))
const FS_SEARCH_CONFIG = fileURLToPath(new URL('./fs-search.cordis.yml', import.meta.url))
@@ -214,10 +217,15 @@ const SCENARIOS: Scenario[] = [
{ name: 'subagent-fork', hasModelTurn: true, recorded: true },
{ name: 'subagent-mixed', hasModelTurn: true, recorded: true },
// Authored continuable-subagent transcript: a background delegation returns
// both the durable subagent id and its task id, task_output collects the
// child result after settlement, and send_message to an unknown subagent id
// starts a follow-up task that settles failed with the id unavailable.
{ name: 'subagent-continuable', hasModelTurn: true, recorded: false },
// both the durable subagent id and its task id, a failed final durability
// confirmation reaches task_output with its diagnosis, and send_message to
// an unknown subagent id starts a follow-up task that settles unavailable.
{
name: 'subagent-continuable',
hasModelTurn: true,
recorded: false,
configPath: SUBAGENT_DURABILITY_FAILURE_CONFIG,
},
{
name: 'subagent-depth-two-rejection',
hasModelTurn: true,

View File

@@ -0,0 +1,14 @@
import type { Context } from 'cordis'
export const name = 'subagent-durability-failure'
/** Fail a continuable child's provider-owned final durability confirmation. */
export function apply(ctx: Context): void {
const flushedTurnEnds = new WeakSet<object>()
ctx.on('session/flush', (session) => {
if (session.header.parentSession === undefined) return
if (session.events.at(-1)?.type !== 'turn/end') return
if (flushedTurnEnds.has(session)) throw new Error('snapshot disk full')
flushedTurnEnds.add(session)
})
}

View File

@@ -23,7 +23,7 @@
{"type":"assistant/chunk","seq":21,"time":1785517567391,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
{"type":"assistant/message","seq":22,"time":1785517567392,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_collect_1","name":"task_output","arguments":"{\"task_id\": \"subagent-1\", \"wait\": true}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"3fcea712-0e14-4f2d-909c-f7de70018053"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[17,18,19,20,21],"surfaceOp":"append"}
{"type":"tool/call","seq":23,"time":1785517567392,"data":{"turn":1,"step":2,"callId":"call_collect_1","name":"task_output","arguments":"{\"task_id\": \"subagent-1\", \"wait\": true}"}}
{"type":"tool/result","seq":24,"time":1785517567419,"data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"call_collect_1"},"content":[{"type":"tool-result","toolCallId":"call_collect_1","content":[{"type":"text","text":"CHILD_OK\n[status: completed]"}],"isError":false}],"role":"user","id":"ae79571a-fa78-4de0-9614-a10b5223230c"}},"sourceEventSeqs":[23],"surfaceOp":"append"}
{"type":"tool/result","seq":24,"time":1785517567419,"data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"call_collect_1"},"content":[{"type":"tool-result","toolCallId":"call_collect_1","content":[{"type":"text","text":"(no new output)\n[status: failed, subagent \"33333333-3333-4333-8333-333333333333\" durability checkpoint failed; the latest child state was not confirmed persisted and may be unavailable or stale on resume: snapshot disk full]"}],"isError":false}],"role":"user","id":"ae79571a-fa78-4de0-9614-a10b5223230c"}},"sourceEventSeqs":[23],"surfaceOp":"append"}
{"type":"step/end","seq":25,"time":1785517567419,"data":{"turn":1,"step":2}}
{"type":"step/start","seq":26,"time":1785517567425,"data":{"turn":1,"step":3}}
{"type":"assistant/chunk","seq":27,"time":1789000000026,"data":{"turn":1,"step":3,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}