@deksden-com/dd-flow-cli 0.9.0-beta.150 → 0.9.0-beta.151
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +17 -22
- package/dist/build-info.json +6 -6
- package/dist/harness-runtime/lib/agy-error-boundary.mjs +26 -5
- package/dist/harness-runtime/lib/agy-terminal-envelope.mjs +67 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +10 -14
- package/dist/harness-runtime/lib/dd-agy.mjs +3 -3
- package/dist/harness-runtime/lib/dd-droid.mjs +2 -2
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +8 -3
- package/dist/harness-runtime/lib/dd-opencode.mjs +2 -2
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +1 -1
- package/dist/harness-runtime/lib/dd-zcode.mjs +37 -12
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +1 -1
- package/dist/harness-runtime/lib/delegation-instructions.mjs +13 -4
- package/dist/harness-runtime/lib/model-observations.mjs +19 -7
- package/dist/harness-runtime/lib/native-children.d.mts +12 -1
- package/dist/harness-runtime/lib/native-children.mjs +47 -1
- package/dist/harness-runtime/lib/provider-overload-policy.d.mts +9 -1
- package/dist/harness-runtime/lib/provider-overload-policy.mjs +97 -6
- package/dist/services/check-runner-evidence.js +17 -302
- package/dist/services/check-runner-proof.js +305 -0
- package/dist/services/code-checks.js +4 -1
- package/dist/services/controller-child-capacity.js +58 -0
- package/dist/services/controller-child-observer.js +9 -3
- package/dist/services/memory-init-native-capacity.js +1 -1
- package/dist/services/provider-capacity.js +15 -7
- package/dist/services/run-controller-adapter.js +1 -1
- package/dist/services/run-controller.js +87 -18
- package/dist/services/run-recovery-guard.js +1 -1
- package/dist/services/run-recovery-runtime.js +1 -1
- package/dist/services/stage-context.js +56 -8
- package/dist/services/vnext-fanout.js +1 -1
- package/dist/verification-components.json +299 -0
- package/package.json +7 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @deksden-com/dd-flow-cli
|
|
2
2
|
|
|
3
|
+
## 0.9.0-beta.151
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Preserve causal AGY completion and model/role evidence, share bounded recursive
|
|
8
|
+
runner proof, and continue eligible provider failures with effect-fenced native
|
|
9
|
+
child admission. Admit the reviewed CP253 ZCode bridge and bind browser facts
|
|
10
|
+
to stage context without turning an unavailable probe into a baseline gate.
|
|
11
|
+
- Replace blanket release checks with scoped contract receipts and package
|
|
12
|
+
delivery verification; keep provider, Memory Bank and CLI ownership separate
|
|
13
|
+
so harness fixes do not launch unrelated acceptance suites.
|
|
14
|
+
|
|
3
15
|
## 0.9.0-beta.150
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -510,28 +510,23 @@ remain invalid and receive a standalone retry.
|
|
|
510
510
|
|
|
511
511
|
## Verification
|
|
512
512
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
`pnpm test:integration --no-file-parallelism --maxWorkers=1`.
|
|
531
|
-
The [test-suite verification runbook](https://github.com/deksden-com/dd-eval/blob/main/runbooks/flow-test-suite-verification.md)
|
|
532
|
-
owns group qualification, complete reports and cross-repository commands.
|
|
533
|
-
|
|
534
|
-
Manual smoke checks should use a temporary `DD_FLOW_HOME` when they do not intentionally inspect the operator's real local state.
|
|
513
|
+
Checks follow changed contracts, not a merge/release/EVAL event. Inspect
|
|
514
|
+
`pnpm verify:plan`, then run `pnpm test` (or `pnpm gate:merge`) for only missing
|
|
515
|
+
affected contracts. `scripts/verification-policy.mjs` owns impact selection;
|
|
516
|
+
existing Node/Vitest runners retain complete selected-file/case evidence.
|
|
517
|
+
Unknown impact requires an explicit policy mapping, never a full-suite fallback.
|
|
518
|
+
|
|
519
|
+
There is no mandatory full release gate. Freeze source/canon, build and pack the
|
|
520
|
+
exact artifact, then accept with `DD_FLOW_VERIFICATION_RECEIPT` pointing to the
|
|
521
|
+
scoped verification receipt. Preserve receipts outside the checkout. Unchanged
|
|
522
|
+
contract inputs reuse intact reports; commit/version/build time alone do not
|
|
523
|
+
invalidate behavior evidence. Failed/unknown attempts require investigation,
|
|
524
|
+
not automatic retries. Package identity and registry readback remain mandatory.
|
|
525
|
+
GitHub CI is disabled. Publication does not repeat the accepted package's smoke
|
|
526
|
+
after identical registry tar readback. EVAL preparation is resource readiness,
|
|
527
|
+
not another engine or product test suite.
|
|
528
|
+
The [verification runbook](https://github.com/deksden-com/dd-eval/blob/main/runbooks/flow-test-suite-verification.md)
|
|
529
|
+
owns the local procedure. A scoped PASS is not a claim that every test ran.
|
|
535
530
|
|
|
536
531
|
### RUN status during recovery
|
|
537
532
|
|
package/dist/build-info.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"cli_package": "@deksden-com/dd-flow-cli",
|
|
3
|
-
"cli_version": "0.9.0-beta.
|
|
4
|
-
"cli_commit": "
|
|
5
|
-
"built_at": "2026-10-
|
|
3
|
+
"cli_version": "0.9.0-beta.151",
|
|
4
|
+
"cli_commit": "89cad0d654601de59b12929240eee4e958b74207",
|
|
5
|
+
"built_at": "2026-10-25T10:59:05.638Z",
|
|
6
6
|
"built_with_canon": {
|
|
7
7
|
"version": "4.1.2",
|
|
8
8
|
"commit": "925ef10ecc0b683f7feba6f74913e15560842500",
|
|
9
9
|
"flow_contract": "dd-flow-canonical-2026-08",
|
|
10
|
-
"repo_root": "/Users/deksden/Documents/_Projects/_worktrees/dd-
|
|
11
|
-
"memorybank_root": "/Users/deksden/Documents/_Projects/_worktrees/dd-
|
|
12
|
-
"flow_root": "/Users/deksden/Documents/_Projects/_worktrees/dd-
|
|
10
|
+
"repo_root": "/Users/deksden/Documents/_Projects/_worktrees/dd-memorybank-cp253",
|
|
11
|
+
"memorybank_root": "/Users/deksden/Documents/_Projects/_worktrees/dd-memorybank-cp253/.memory-bank",
|
|
12
|
+
"flow_root": "/Users/deksden/Documents/_Projects/_worktrees/dd-memorybank-cp253/.memory-bank/dd-flow",
|
|
13
13
|
"layout": "dot_memory_bank"
|
|
14
14
|
}
|
|
15
15
|
}
|
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
import { parseLifecycleCommand, commandOption, commandPosition } from "./lifecycle-command.mjs";
|
|
2
2
|
import { entityReferenceVariants } from "./entity-references.mjs";
|
|
3
|
+
import { agyTerminalEnvelope, ownedAgyOutput } from "./agy-terminal-envelope.mjs";
|
|
3
4
|
|
|
4
5
|
/** Ordered native transcript evidence, never assistant text as a success signal. */
|
|
5
|
-
export function recoveredAgyErrorBoundary(text, error, floor = -1) {
|
|
6
|
+
export function recoveredAgyErrorBoundary(text, error, floor = -1, source = null, sessionId = null) {
|
|
6
7
|
let rows;
|
|
7
8
|
try { rows = text.trim().split("\n").filter(Boolean).map(JSON.parse); } catch { return null; }
|
|
8
|
-
let previous = -1, lastError = null, boundary = null, invocation = null;
|
|
9
|
+
let previous = -1, lastError = null, boundary = null, invocation = null, call = null;
|
|
10
|
+
const launches = new Map();
|
|
9
11
|
for (const row of rows) {
|
|
10
12
|
if (!Number.isInteger(row.step_index) || row.step_index <= previous) return null;
|
|
11
13
|
previous = row.step_index;
|
|
12
14
|
if (row.step_index <= (floor ?? -1)) continue;
|
|
13
|
-
if (row.type === "USER_INPUT") { lastError = null; boundary = null; invocation = null; continue; }
|
|
15
|
+
if (row.type === "USER_INPUT") { lastError = null; boundary = null; invocation = null; call = null; launches.clear(); continue; }
|
|
14
16
|
if (row.type === "ERROR_MESSAGE") { lastError = row; boundary = null; invocation = null; continue; }
|
|
15
17
|
if (Array.isArray(row.tool_calls)) {
|
|
16
18
|
invocation = null;
|
|
19
|
+
call = row.tool_calls.length === 1 ? row.tool_calls[0] : null;
|
|
17
20
|
if (row.tool_calls.length === 1 && row.tool_calls[0]?.name === "run_command") {
|
|
18
21
|
let command = row.tool_calls[0]?.args?.CommandLine;
|
|
19
22
|
if (typeof command === "string") {
|
|
@@ -23,8 +26,22 @@ export function recoveredAgyErrorBoundary(text, error, floor = -1) {
|
|
|
23
26
|
}
|
|
24
27
|
}
|
|
25
28
|
}
|
|
26
|
-
if (
|
|
27
|
-
|
|
29
|
+
if (row.type === 'GENERIC' && row.status === 'RUNNING') {
|
|
30
|
+
const task = row.content?.match(/^Created At: [^\n]+\nTool is running as a background task with task id: ([\w-]+\/task-\d+)\n/);
|
|
31
|
+
if (task && invocation && (!sessionId || task[1].startsWith(`${sessionId}/`))) launches.set(task[1], launches.has(task[1]) ? null : { invocation, step: row.step_index });
|
|
32
|
+
call = null; invocation = null; continue;
|
|
33
|
+
}
|
|
34
|
+
const envelope = agyTerminalEnvelope(row, call);
|
|
35
|
+
let launchStep = null;
|
|
36
|
+
if (envelope?.task_id) {
|
|
37
|
+
const launch = launches.get(envelope.task_id);
|
|
38
|
+
invocation = launch && launch.step < row.step_index ? launch.invocation : null;
|
|
39
|
+
launchStep = launch?.step ?? null;
|
|
40
|
+
}
|
|
41
|
+
if (!lastError || envelope?.exit_code !== 0) continue;
|
|
42
|
+
const retained = envelope.output_reference && source && sessionId ? ownedAgyOutput(envelope.output_reference, source, sessionId, envelope.task_id) : null;
|
|
43
|
+
const full = retained ? agyTerminalEnvelope({ type: 'GENERIC', status: 'DONE', content: retained.text }) : null;
|
|
44
|
+
const output = (retained ? full?.exit_code === 0 ? full.output : null : envelope.output)?.replaceAll("\r", "");
|
|
28
45
|
if (!output) continue;
|
|
29
46
|
for (const match of output.matchAll(/^\s*\{/gm)) {
|
|
30
47
|
let receipt;
|
|
@@ -51,6 +68,10 @@ export function recoveredAgyErrorBoundary(text, error, floor = -1) {
|
|
|
51
68
|
&& [receipt.run_id, receipt.stage, receipt.work_id].every(value => typeof value === "string" && value)
|
|
52
69
|
&& (receipt.outcome !== "paused" || typeof receipt.pause?.id === "string")) boundary = { receipt: { ok: true, outcome: receipt.outcome, run_id: receipt.run_id, stage: receipt.stage, work_id: receipt.work_id,
|
|
53
70
|
lifecycle_operation: "stage_pause", pause: { id: receipt.pause.id } }, error_step: lastError.step_index, boundary_step: row.step_index };
|
|
71
|
+
if (boundary?.boundary_step === row.step_index) {
|
|
72
|
+
if (launchStep !== null) boundary.invocation_step = launchStep;
|
|
73
|
+
if (retained) boundary.output_source = { locator: retained.locator, sha256: retained.sha256 };
|
|
74
|
+
}
|
|
54
75
|
}
|
|
55
76
|
}
|
|
56
77
|
return boundary && lastError?.error === error ? boundary : null;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { constants, openSync, closeSync, fstatSync, lstatSync, readSync, realpathSync } from 'node:fs';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
|
|
6
|
+
const taskId = '[\\w-]+/task-\\d+';
|
|
7
|
+
|
|
8
|
+
export function ownedAgyOutput(reference, source, sessionId, ownerTask = null) {
|
|
9
|
+
let fd;
|
|
10
|
+
try {
|
|
11
|
+
const transcript = fileURLToPath(source.locator), file = fileURLToPath(reference);
|
|
12
|
+
const system = path.dirname(path.dirname(transcript));
|
|
13
|
+
if (path.basename(path.dirname(system)) !== sessionId || path.basename(system) !== '.system_generated') return null;
|
|
14
|
+
const relative = path.relative(system, file);
|
|
15
|
+
if (!/^(?:steps\/\d+\/output\.txt|tasks\/task-\d+\.log)$/.test(relative) || realpathSync(file) !== file || realpathSync(system) !== system) return null;
|
|
16
|
+
if (ownerTask) {
|
|
17
|
+
const number = ownerTask.match(new RegExp(`^${sessionId}/task-(\\d+)$`))?.[1];
|
|
18
|
+
if (!number || ![`steps/${number}/output.txt`, `tasks/task-${number}.log`].includes(relative)) return null;
|
|
19
|
+
}
|
|
20
|
+
const before = lstatSync(file);
|
|
21
|
+
if (!before.isFile() || before.size > 2 * 1024 * 1024) return null;
|
|
22
|
+
fd = openSync(file, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
23
|
+
const stat = fstatSync(fd);
|
|
24
|
+
if (!stat.isFile() || stat.ino !== before.ino || stat.dev !== before.dev) return null;
|
|
25
|
+
const bytes = Buffer.alloc(stat.size);
|
|
26
|
+
let offset = 0;
|
|
27
|
+
while (offset < bytes.length) { const count = readSync(fd, bytes, offset, bytes.length - offset, offset); if (!count) return null; offset += count; }
|
|
28
|
+
const after = fstatSync(fd);
|
|
29
|
+
if (stat.size !== after.size || stat.mtimeMs !== after.mtimeMs) return null;
|
|
30
|
+
return { text: bytes.toString('utf8'), sha256: createHash('sha256').update(bytes).digest('hex'), locator: reference };
|
|
31
|
+
} catch { return null; }
|
|
32
|
+
finally { if (fd !== undefined) closeSync(fd); }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Only a native envelope is decoded here; stdout cannot introduce terminal authority. */
|
|
36
|
+
export function agyTerminalEnvelope(row, call = null) {
|
|
37
|
+
if (row?.status !== 'DONE' || typeof row.content !== 'string') return null;
|
|
38
|
+
let body = row.content.replaceAll('\r\n', '\n'), id = null, cancelled = false;
|
|
39
|
+
if (row.source === 'SYSTEM' && row.type === 'SYSTEM_MESSAGE') {
|
|
40
|
+
const match = body.match(new RegExp(`(?:^|\\n)\\[Message\\] timestamp=[^\\n]+ sender=(${taskId}) priority=MESSAGE_PRIORITY_(?:HIGH|LOW) content=Task id "(${taskId})" (finished with result:|was canceled with result:)\\n`));
|
|
41
|
+
if (!match || match[1] !== match[2]) return null;
|
|
42
|
+
id = match[1]; cancelled = match[3].startsWith('was canceled');
|
|
43
|
+
body = body.slice(match.index + match[0].length).replace(/\n<\/SYSTEM_MESSAGE>\s*$/, '');
|
|
44
|
+
} else if (row.type !== 'GENERIC' || row.source && row.source !== 'MODEL') return null;
|
|
45
|
+
else {
|
|
46
|
+
body = body.replace(/^Created At: [^\n]+\nCompleted At: [^\n]+\n/, '');
|
|
47
|
+
if (call?.name === 'manage_task') {
|
|
48
|
+
const decode = value => { try { return JSON.parse(value); } catch { return value; } };
|
|
49
|
+
id = decode(call.args?.TaskId);
|
|
50
|
+
const action = decode(call.args?.Action);
|
|
51
|
+
const status = body.match(new RegExp(`^Task: (${taskId})\\nStatus: (DONE|COMPLETED|FAILED|CANCELLED)(?:\\n|$)`));
|
|
52
|
+
if (action === 'status' && status?.[1] === id) return { task_id: id, task_terminal: true, exit_code: null, command_outcome: status[2] === 'DONE' ? null : status[2].toLowerCase(), output: null };
|
|
53
|
+
if (action === 'kill' && body.match(new RegExp(`^Task "(${taskId})" cancelled\\.(?:\\n|$)`))?.[1] === id) return { task_id: id, task_terminal: true, exit_code: null, command_outcome: 'cancelled', output: null };
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// This exact wrapper preserves the beginning of the native result, including its exit header.
|
|
58
|
+
const wrapper = body.match(/^The tool's output was truncated due to being too long\. You can find the full output in a file at (file:\/\/[^\n]+)\.\n\nHere is a preview with the middle of the output truncated:\n/);
|
|
59
|
+
if (wrapper) body = body.slice(wrapper[0].length);
|
|
60
|
+
if (cancelled && /^Tool execution was canceled(?:\n|$)/.test(body)) return { task_id: id, task_terminal: true, exit_code: null, command_outcome: 'cancelled', output: null };
|
|
61
|
+
const header = body.match(/^Command exited with code (-?\d+)\.(?:\n|$)/);
|
|
62
|
+
if (!header) return null;
|
|
63
|
+
const exit = Number(header[1]);
|
|
64
|
+
if (!Number.isSafeInteger(exit)) return null;
|
|
65
|
+
return { task_id: id, task_terminal: true, exit_code: exit, command_outcome: exit === 0 ? 'completed' : 'failed', output: wrapper ? null : body.split('Command output:\n')[1] ?? null,
|
|
66
|
+
...(wrapper ? { output_reference: wrapper[1] } : {}) };
|
|
67
|
+
}
|
|
@@ -24,6 +24,7 @@ import path from "node:path";
|
|
|
24
24
|
import { observeModel } from "./model-observations.mjs";
|
|
25
25
|
import { AGY_VERSION_TIMEOUT_MS, AgyError, agyTerminalFailure, agyModelArgs, assertProfile, executable, observedProfile, runAgy, usageSnapshot, managedAgyEnv } from "./dd-agy.mjs";
|
|
26
26
|
import { recoveredAgyErrorBoundary } from "./agy-error-boundary.mjs";
|
|
27
|
+
import { agyTerminalEnvelope } from "./agy-terminal-envelope.mjs";
|
|
27
28
|
import { settledNativeTree, nativeExecutionReceipt } from "./native-children.mjs";
|
|
28
29
|
import { retainStartupFailure, prepareRuntimeOwner, cleanupFailedStart, cleanupFailedDaemonStart, confirmDaemonProcess, confirmDaemonStopped, finishDaemonProcess, scheduleDaemonProcessHeartbeat, registerDaemonProcess, stopProcessGroup } from "./managed-daemon.mjs";
|
|
29
30
|
import { readNativeLines, streamNativeDiagnostics, nativeObservationError, admitNativeRequestBuffer } from "./native-streams.mjs";
|
|
@@ -51,10 +52,10 @@ export function agyTranscriptFacts(text, source, sessionId) {
|
|
|
51
52
|
const content = typeof event.content === 'string' ? event.content : '';
|
|
52
53
|
const native_source = { ...source, step_index: event.step_index };
|
|
53
54
|
if (event.source === 'SYSTEM' && event.type === 'SYSTEM_MESSAGE' && event.status === 'DONE') {
|
|
54
|
-
const terminal =
|
|
55
|
-
if (terminal
|
|
56
|
-
const prior = resources.get(terminal
|
|
57
|
-
if (prior) resources.set(prior.resource_id, mergeAgyResource(prior, { ...prior, state: 'stopped',
|
|
55
|
+
const terminal = agyTerminalEnvelope(event);
|
|
56
|
+
if (terminal) {
|
|
57
|
+
const prior = resources.get(terminal.task_id);
|
|
58
|
+
if (prior) resources.set(prior.resource_id, mergeAgyResource(prior, { ...prior, state: 'stopped', ...(terminal.command_outcome ? { native_outcome: terminal.command_outcome } : {}), terminal_source: native_source }));
|
|
58
59
|
}
|
|
59
60
|
continue;
|
|
60
61
|
}
|
|
@@ -63,13 +64,8 @@ export function agyTranscriptFacts(text, source, sessionId) {
|
|
|
63
64
|
if (event.type !== 'GENERIC') continue;
|
|
64
65
|
const call = calls.shift();
|
|
65
66
|
if (call?.name === 'manage_task' && event.status === 'DONE') {
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
const prior = resources.get(id);
|
|
69
|
-
if (prior && action === 'kill' && content.match(/^Created At: [^\n]+\nCompleted At: [^\n]+\nTask "([\w-]+\/task-\d+)" cancelled\.(?:\n|$)/)?.[1] === id)
|
|
70
|
-
resources.set(id, mergeAgyResource(prior, { ...prior, state: 'stopped', native_outcome: 'cancelled', terminal_source: native_source }));
|
|
71
|
-
const status = action === 'status' && content.match(/^Created At: [^\n]+\nCompleted At: [^\n]+\nTask: ([\w-]+\/task-\d+)\nStatus: (COMPLETED|FAILED|CANCELLED)\n/);
|
|
72
|
-
if (prior && status?.[1] === id) resources.set(id, mergeAgyResource(prior, { ...prior, state: 'stopped', native_outcome: status[2].toLowerCase(), terminal_source: native_source }));
|
|
67
|
+
const terminal = agyTerminalEnvelope(event, call), prior = resources.get(terminal?.task_id);
|
|
68
|
+
if (prior) resources.set(prior.resource_id, mergeAgyResource(prior, { ...prior, state: 'stopped', ...(terminal.command_outcome ? { native_outcome: terminal.command_outcome } : {}), terminal_source: native_source }));
|
|
73
69
|
}
|
|
74
70
|
if (call?.name === 'invoke_subagent' && event.status === 'DONE') {
|
|
75
71
|
const match = content.match(/^Created At: [^\n]+\nCompleted At: [^\n]+\nCreated the following subagent:\n(\{[\s\S]*?\})\nThe subagent will send you a message/);
|
|
@@ -98,7 +94,7 @@ export function mergeAgyResource(prior, next) {
|
|
|
98
94
|
return { ...prior, state: 'unknown', observation_conflict: true };
|
|
99
95
|
if (prior.observation_conflict || next.observation_conflict || prior.native_outcome && next.native_outcome && prior.native_outcome !== next.native_outcome)
|
|
100
96
|
return { ...prior, state: 'unknown', observation_conflict: true };
|
|
101
|
-
if (next.
|
|
97
|
+
if (next.terminal_source && (!Number.isSafeInteger(next.native_source?.step_index) || !Number.isSafeInteger(next.terminal_source?.step_index) || next.terminal_source.step_index <= next.native_source.step_index))
|
|
102
98
|
return { ...prior, state: 'unknown', observation_conflict: true };
|
|
103
99
|
if (prior.state === 'stopped' && next.state !== 'stopped') {
|
|
104
100
|
if (next.native_source?.step_index > (prior.terminal_source?.step_index ?? prior.native_source?.step_index))
|
|
@@ -578,8 +574,8 @@ export class Runtime {
|
|
|
578
574
|
if (result.status === "ERROR" && settled && rootStop?.fullyIdle === true && !this.state.last_hook_rejection && current.operationId && this.paths.runtime) {
|
|
579
575
|
const root = this.init.conversation_id, transcript = path.join(this.paths.runtime, "brain", root, ".system_generated", "logs", "transcript.jsonl");
|
|
580
576
|
await captureAgyTranscript(this.paths.runtime, { provider_session_id: root, parent_provider_session_id: root, log_uri: new URL(`file://${transcript}`).href }, root, [root], (text, source) => {
|
|
581
|
-
const boundary = recoveredAgyErrorBoundary(text, result.error, current.stepFloor);
|
|
582
|
-
const hook = current.nativeHookEvents?.get(boundary?.boundary_step);
|
|
577
|
+
const boundary = recoveredAgyErrorBoundary(text, result.error, current.stepFloor, source, root);
|
|
578
|
+
const hook = current.nativeHookEvents?.get(boundary?.invocation_step ?? boundary?.boundary_step);
|
|
583
579
|
if (boundary && hook) terminalMetadata.recovered_error_boundary = { ...boundary, receipt: { ...boundary.receipt, native_hook_event_key: hook, daemon_instance: this.config.daemonId }, source };
|
|
584
580
|
});
|
|
585
581
|
}
|
|
@@ -4,7 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import { SETUP_STARTUP_MS } from './setup-progress.mjs';
|
|
5
5
|
|
|
6
6
|
import { checkObservedProfile } from "./model-observations.mjs";
|
|
7
|
-
import { classifyProviderError, safeProviderError, streamRequestKey } from "./provider-overload-policy.mjs";
|
|
7
|
+
import { classifyRecoveryError as classifyProviderError, safeProviderError, streamRequestKey } from "./provider-overload-policy.mjs";
|
|
8
8
|
|
|
9
9
|
const DEFAULT_MODEL = "gemini-3.1-pro-high";
|
|
10
10
|
export const AGY_VERSION_TIMEOUT_MS = 30_000;
|
|
@@ -31,9 +31,9 @@ export function agyTerminalFailure(result, metadata = {}) {
|
|
|
31
31
|
const identity = result.turn_id ?? result.request_id;
|
|
32
32
|
const turn = typeof identity === "string" && identity ? identity : null;
|
|
33
33
|
const stream = streamRequestKey(metadata.request_proof);
|
|
34
|
-
const proofMissing = result.status === "ERROR" && ["provider_capacity_transient", "provider_rate_limited"].includes(category) && !turn && !stream;
|
|
34
|
+
const proofMissing = result.status === "ERROR" && ["provider_capacity_transient", "provider_rate_limited", "provider_network_transient"].includes(category) && !turn && !stream;
|
|
35
35
|
if (proofMissing) code = "provider_overload_evidence_unavailable";
|
|
36
|
-
return new AgyError(code, String(native.message ?? "Antigravity returned ERROR"),
|
|
36
|
+
return new AgyError(code, String(native.message ?? "Antigravity returned ERROR"), ["provider_capacity_transient", "provider_rate_limited", "provider_network_transient"].includes(code), {
|
|
37
37
|
provider_result: result, ...metadata, provider_error: safeProviderError(native), native_source: "agy.result", terminal_status: result.status === "ERROR" ? "failed" : "cancelled",
|
|
38
38
|
...(turn ? { turn_id: turn, native_turn_id: turn } : {}),
|
|
39
39
|
...(proofMissing ? { provider_category: category, reason: "native_turn_identity_unavailable" } : {}),
|
|
@@ -21,7 +21,7 @@ const metadataReaders = new Map();
|
|
|
21
21
|
function metadataReader(factory) { if (!metadataReaders.has(factory)) metadataReaders.set(factory, new DroidMetadataReader(factory)); return metadataReaders.get(factory); }
|
|
22
22
|
import { confirmDaemonProcess, finishDaemonProcess, scheduleDaemonProcessHeartbeat, registerDaemonProcess, stopProcessGroup } from "./managed-daemon.mjs";
|
|
23
23
|
import { renderAdapterPolicy, lifecycleRetryInstruction } from "./delegation-instructions.mjs";
|
|
24
|
-
import { classifyProviderError, safeProviderError } from "./provider-overload-policy.mjs";
|
|
24
|
+
import { classifyRecoveryError as classifyProviderError, safeProviderError } from "./provider-overload-policy.mjs";
|
|
25
25
|
import { settledNativeTree, nativeExecutionReceipt } from "./native-children.mjs";
|
|
26
26
|
|
|
27
27
|
export const DROID_CONTRACT = "dd-droid-harness@1";
|
|
@@ -108,7 +108,7 @@ export function droidTerminalFailure(outcome, sessionId, settled) {
|
|
|
108
108
|
terminal_status: "failed", native_source: "droid.agent_turn_outcome", observed_at: new Date().toISOString(),
|
|
109
109
|
provider_error: safeProviderError(outcome.error), will_retry: outcome.error.willRetry === true, native_retry_active: outcome.error.willRetry === true,
|
|
110
110
|
native_turn_items: { observed: settled === true, possible_effects: true, pending: settled !== true }
|
|
111
|
-
}), { retryable:
|
|
111
|
+
}), { retryable: ["provider_capacity_transient", "provider_rate_limited", "provider_network_transient"].includes(code) });
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
|
|
@@ -91,9 +91,12 @@ async function prepare(paths, config, authSource) {
|
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
93
|
catch (error) { if (error.code !== "ENOENT") throw new DaemonError("opencode_config_invalid", "Portable OpenCode provider config must be valid JSON"); }
|
|
94
|
-
|
|
95
|
-
agents: { ...agentRestrictions.agents, "dd-flow-worker": { mode: "subagent", system: "Execute the assigned dd-flow Work. Follow the issued lifecycle instructions. Do not spawn descendants.", model: `${config.provider}/${config.model}${config.variant !== "default" ? `#${config.variant}` : ""}` } },
|
|
96
|
-
experimental: { subagent_depth: 1, ...(policies ? { policies } : {}) } }
|
|
94
|
+
const generatedConfig = { ...portable, ...agentRestrictions, permissions,
|
|
95
|
+
agents: { ...agentRestrictions.agents, "dd-flow-worker": { ...agentRestrictions.agent?.[config.agent], ...agentRestrictions.agents?.[config.agent], mode: "subagent", system: "Execute the assigned dd-flow Work. Follow the issued lifecycle instructions. Do not spawn descendants.", model: `${config.provider}/${config.model}${config.variant !== "default" ? `#${config.variant}` : ""}` } },
|
|
96
|
+
experimental: { subagent_depth: 1, ...(policies ? { policies } : {}) } };
|
|
97
|
+
const configPath = path.join(paths.config, "opencode", "opencode.json");
|
|
98
|
+
await writeJson(configPath, generatedConfig);
|
|
99
|
+
config.workerLaunchConfig = { locator: configPath, sha256: createHash('sha256').update(await readFile(configPath)).digest('hex'), primary_agent: config.agent, subagent: 'dd-flow-worker', max_depth: 1 };
|
|
97
100
|
config.pluginSha256 = await writePlugin(paths, config);
|
|
98
101
|
}
|
|
99
102
|
async function bootstrapCredentials(config, client) {
|
|
@@ -303,6 +306,8 @@ export class Runtime {
|
|
|
303
306
|
if (info.sessionID && info.sessionID !== id) throw new DaemonError("session_identity_mismatch", "native message belongs to another Session");
|
|
304
307
|
await observeModel({ journal: this.state.config.journal ?? this.paths.journal, harness: "opencode-server", sessionId: id, parentSessionId: parent,
|
|
305
308
|
requested: { ...this.state.config, reasoning: this.state.config.variant, mode: parent ? "dd-flow-worker" : this.state.config.agent },
|
|
309
|
+
nativeAgentRole: this.state.config.workerLaunchConfig ? { name: info.mode ?? null, kind: parent ? 'subagent' : 'primary', parent_session_id: parent,
|
|
310
|
+
launch: { ...this.state.config.workerLaunchConfig, root_session_id: this.sessions.get(id)?.root_provider_session_id ?? null } } : null,
|
|
306
311
|
observed: { provider: info.providerID, model: info.modelID, reasoning: info.variant, mode: info.mode }, source: { method: "session/message", message_id: info.id, event_sha256: createHash("sha256").update(`${id}:${info.id}:${info.modelID}`).digest("hex") }, evidence: "response" });
|
|
307
312
|
this.modelMessages.add(info.id);
|
|
308
313
|
}
|
|
@@ -4,7 +4,7 @@ import { ToolObservations, openCodeToolObservation } from "./tool-observations.m
|
|
|
4
4
|
import { ObservationClock, observedTimeout, validateObservationDuration } from "./observation-clock.mjs";
|
|
5
5
|
import { assertNativeLine } from "./native-streams.mjs";
|
|
6
6
|
import { operationProgressReporter } from "./operation-progress.mjs";
|
|
7
|
-
import { classifyProviderError, safeProviderError } from "./provider-overload-policy.mjs";
|
|
7
|
+
import { classifyRecoveryError as classifyProviderError, safeProviderError } from "./provider-overload-policy.mjs";
|
|
8
8
|
|
|
9
9
|
export const OPENCODE_PROTOCOL = 2;
|
|
10
10
|
export class OpenCodeError extends Error {
|
|
@@ -59,7 +59,7 @@ export function providerFailure(native, sessionId, turnId) {
|
|
|
59
59
|
const category = classifyProviderError(native), code = category === "provider_failed" ? "opencode_provider_failed" : category;
|
|
60
60
|
// Never forward response.body or provider credentials into controller/Judge.
|
|
61
61
|
return new OpenCodeError(code, `OpenCode native execution failed (${type}${status ? `, HTTP ${status}` : ""})`,
|
|
62
|
-
["provider_capacity_transient", "provider_rate_limited"].includes(code), {
|
|
62
|
+
["provider_capacity_transient", "provider_rate_limited", "provider_network_transient"].includes(code), {
|
|
63
63
|
provider_session_id: sessionId, session_id: sessionId, turn_id: turnId, native_error_type: type,
|
|
64
64
|
native_turn_id: turnId, terminal_status: "failed",
|
|
65
65
|
provider_error: safeProviderError(native),
|
|
@@ -459,7 +459,7 @@ export class DaemonRuntime {
|
|
|
459
459
|
if (active && active.session_id !== params.sessionId) throw new DaemonError("session_identity_mismatch", "Retained observation has a foreign active root");
|
|
460
460
|
const cacheScope = `${params.sessionId}:${generation}`;
|
|
461
461
|
if (this.childObservationScope !== cacheScope) { this.childObservationScope = cacheScope; this.childTerminalCache = new Map(); this.childObservationKeys = new Set(); }
|
|
462
|
-
const subagents = await readZcodeSubagents(this.bridge, root.adapter_session_id, 30_000, true, this.childTerminalCache);
|
|
462
|
+
const subagents = await readZcodeSubagents(this.bridge, root.adapter_session_id, 30_000, true, this.childTerminalCache, root.provider_session_id);
|
|
463
463
|
if (generation !== (this.controlGeneration ?? 0)) throw new DaemonError("operation_cancelled", "Observation generation was retired");
|
|
464
464
|
if (activeIdentity(this.state.active_request) !== capturedIdentity) throw new DaemonError("operation_cancelled", "Observation belongs to a retired active Turn");
|
|
465
465
|
assertDaemonDispatch(this.paths.dir);
|
|
@@ -15,7 +15,7 @@ import { commandText } from "./process-json.mjs";
|
|
|
15
15
|
import { readNativeLines, streamNativeDiagnostics, nativeObservationError } from "./native-streams.mjs";
|
|
16
16
|
import { operationProgressReporter } from "./operation-progress.mjs";
|
|
17
17
|
import { safeNativeFailure } from "./native-children.mjs";
|
|
18
|
-
import { classifyProviderError, safeProviderError } from "./provider-overload-policy.mjs";
|
|
18
|
+
import { classifyRecoveryError as classifyProviderError, safeProviderError } from "./provider-overload-policy.mjs";
|
|
19
19
|
|
|
20
20
|
import { readZcodeUsage } from "./zcode-usage.mjs";
|
|
21
21
|
const ZCODE_HARNESS_CONTRACT = "dd-zcode-harness@1";
|
|
@@ -200,6 +200,7 @@ export class AcpBridge {
|
|
|
200
200
|
clientInfo: this.options.clientInfo ?? { name: "dd-zcode", version: "0.1.0" },
|
|
201
201
|
}, this.options.initializeTimeoutMs ?? SETUP_STARTUP_MS);
|
|
202
202
|
publishSetup('native_initialized', 'native-initialize');
|
|
203
|
+
this.recoveryCapability = initialized?._meta?.zcode?.ddFlowRecovery;
|
|
203
204
|
return initialized;
|
|
204
205
|
}
|
|
205
206
|
|
|
@@ -306,7 +307,7 @@ export class AcpBridge {
|
|
|
306
307
|
code: [provider?.code, data?.code, data?.native_code].filter(value => typeof value === "string").join(" "), message: messageText };
|
|
307
308
|
const category = pending.method === "session/prompt" ? classifyProviderError(nativeFailure) : "provider_failed";
|
|
308
309
|
error.code = unknown ? nativeCode : category === "provider_failed" ? "acp_request_failed" : category;
|
|
309
|
-
error.retryable = ["provider_rate_limited", "provider_capacity_transient"].includes(error.code);
|
|
310
|
+
error.retryable = ["provider_rate_limited", "provider_capacity_transient", "provider_network_transient"].includes(error.code);
|
|
310
311
|
error.details = { acp_code: message.error.code ?? null,
|
|
311
312
|
observed_at: provider?.observed_at ?? now(), provider_session_id: pending.sessionId ?? null, source: "acp.error_response", request_id: message.id,
|
|
312
313
|
terminal_response: { method: pending.method, session_id: pending.sessionId, request_id: message.id },
|
|
@@ -581,13 +582,13 @@ async function resume(bridge, sessionId, cwd) {
|
|
|
581
582
|
|
|
582
583
|
/** Ended children are paginated by the native API (20 by default). A partial
|
|
583
584
|
* directory is never settlement evidence, including during teardown. */
|
|
584
|
-
export async function readZcodeSubagents(bridge, sessionId, timeoutMs = 30_000, retained = false, terminalCache = new Map()) {
|
|
585
|
+
export async function readZcodeSubagents(bridge, sessionId, timeoutMs = 30_000, retained = false, terminalCache = new Map(), parentSessionId = sessionId) {
|
|
585
586
|
const clock = new ObservationClock({ timeoutMs });
|
|
586
587
|
const cursors = new Set(), ended = new Map();
|
|
587
588
|
let first, cursor;
|
|
588
589
|
for (;;) {
|
|
589
590
|
if (clock.sample()) fail("ZCode child observation is unavailable", "operation_observation_lost");
|
|
590
|
-
const response = await bridge.request(retained ? "zcode/session/retainedSubagents" : "zcode/session/subagents", { sessionId, ...(cursor ? { endedCursor: cursor } : {}) }, timeoutMs);
|
|
591
|
+
const response = await bridge.request(retained ? "zcode/session/retainedSubagents" : "zcode/session/subagents", { sessionId, proofContract: "zcode/retained-child-effects@1", ...(cursor ? { endedCursor: cursor } : {}) }, timeoutMs);
|
|
591
592
|
if (retained && response?.sessionId !== sessionId) fail("Retained ZCode child directory has a foreign Session identity", "native_topology_incomplete");
|
|
592
593
|
const page = retained ? response.topology : response;
|
|
593
594
|
if (!page || !Array.isArray(page.running)) fail("ZCode child directory is unavailable", "native_topology_incomplete");
|
|
@@ -615,7 +616,7 @@ export async function readZcodeSubagents(bridge, sessionId, timeoutMs = 30_000,
|
|
|
615
616
|
const cacheKey = JSON.stringify([child.childSessionId, child.startedAt, child.endedAt]);
|
|
616
617
|
if (terminalCache.has(cacheKey)) { items.push(terminalCache.get(cacheKey)); continue; }
|
|
617
618
|
let terminal;
|
|
618
|
-
try { terminal = await bridge.request("zcode/session/retainedTerminal", { sessionId: child.childSessionId, startedAt: child.startedAt, endedAt: child.endedAt }, timeoutMs); }
|
|
619
|
+
try { terminal = await bridge.request("zcode/session/retainedTerminal", { sessionId: child.childSessionId, parentSessionId, startedAt: child.startedAt, endedAt: child.endedAt, proofContract: "zcode/retained-child-effects@1" }, timeoutMs); }
|
|
619
620
|
catch (error) { items.push({ ...child, terminal_metadata_unavailable: error.code ?? "terminal_metadata_unavailable" }); continue; }
|
|
620
621
|
if (terminal?.available !== true) { items.push({ ...child, terminal_metadata_unavailable: "terminal_metadata_unavailable" }); continue; }
|
|
621
622
|
const observedAt = Date.parse(terminal.terminal_observed_at);
|
|
@@ -623,9 +624,23 @@ export async function readZcodeSubagents(bridge, sessionId, timeoutMs = 30_000,
|
|
|
623
624
|
|| terminal.startedAt !== child.startedAt || terminal.endedAt !== child.endedAt
|
|
624
625
|
|| !Number.isFinite(observedAt) || observedAt < child.startedAt || observedAt > child.endedAt) fail("ZCode terminal failure metadata has foreign identity", "native_child_turn_conflict");
|
|
625
626
|
const providerError = safeNativeFailure(terminal.provider_error);
|
|
626
|
-
const
|
|
627
|
+
const proof = terminal.effects_proof;
|
|
628
|
+
const ref = terminal.continuation_ref;
|
|
629
|
+
const effectsBound = proof?.schema_id === "zcode/retained-child-effects@1" && proof.session_id === child.childSessionId
|
|
630
|
+
&& proof.started_at === child.startedAt && proof.ended_at === child.endedAt && proof.terminal_turn_id === terminal.terminal_turn_id
|
|
631
|
+
&& proof.settled === true && proof.native_turn_items?.observed === true && proof.native_turn_items?.pending === false;
|
|
632
|
+
const refBound = effectsBound && ref?.schema_id === "zcode/child-continuation@1" && ref.child_session_id === child.childSessionId
|
|
633
|
+
&& ref.parent_session_id === parentSessionId && ref.started_at === child.startedAt && ref.ended_at === child.endedAt && ref.terminal_turn_id === terminal.terminal_turn_id
|
|
634
|
+
&& typeof ref.agent_id === "string" && /^agent_[\w-]+$/.test(ref.agent_id) && typeof ref.parent_tool_call_id === "string" && ref.parent_tool_call_id
|
|
635
|
+
&& ref.capability === "dd-flow/managed-provider-recovery@1";
|
|
636
|
+
const item = { ...child, terminal_turn_id: terminal.terminal_turn_id, terminal_observed_at: terminal.terminal_observed_at, native_source: "zcode.retained-terminal",
|
|
637
|
+
...(effectsBound ? { native_turn_items: proof.native_turn_items, effects_proof: proof } : {}),
|
|
638
|
+
...(refBound ? { continuation_ref: ref } : {}),
|
|
639
|
+
...(!effectsBound || !refBound ? { recovery_unavailable_reason: typeof (terminal.continuation_unavailable_reason ?? proof?.reason) === "string" && /^[a-z][a-z0-9_]{0,127}$/.test(terminal.continuation_unavailable_reason ?? proof?.reason) ? terminal.continuation_unavailable_reason ?? proof.reason : !effectsBound ? "native_effects_unproven" : "native_continuation_unproven" } : {}),
|
|
627
640
|
...(providerError ? { provider_error: providerError } : {}) };
|
|
628
|
-
|
|
641
|
+
// Effects/topology proof is admission-time authority: never reuse it
|
|
642
|
+
// across a later inspection merely because the old terminal is cached.
|
|
643
|
+
if (!proof && !ref) terminalCache.set(cacheKey, item);
|
|
629
644
|
items.push(item);
|
|
630
645
|
clock.sample(child.childSessionId);
|
|
631
646
|
}
|
|
@@ -638,10 +653,10 @@ export async function readZcodeSubagents(bridge, sessionId, timeoutMs = 30_000,
|
|
|
638
653
|
}
|
|
639
654
|
}
|
|
640
655
|
|
|
641
|
-
async function inspect(bridge, sessionId) {
|
|
656
|
+
async function inspect(bridge, sessionId, parentSessionId = sessionId) {
|
|
642
657
|
const [read, subagents] = await Promise.all([
|
|
643
658
|
bridge.request("zcode/session/read", { sessionId }),
|
|
644
|
-
readZcodeSubagents(bridge, sessionId),
|
|
659
|
+
readZcodeSubagents(bridge, sessionId, 30_000, false, new Map(), parentSessionId),
|
|
645
660
|
]);
|
|
646
661
|
let usage = null, usage_observation_error = null;
|
|
647
662
|
try { usage = await readZcodeUsage(bridge, sessionId); }
|
|
@@ -984,6 +999,8 @@ export function zcodeLifecycleQualification(sha256, bridgeCommit, harnessContrac
|
|
|
984
999
|
["636b14181d5c9a95476055079836fd6b58169f58", "dd-zcode-harness@2"],
|
|
985
1000
|
["4621fabd3f517ecdd349dfebebae9077c01d2f2b", "dd-zcode-harness@2"],
|
|
986
1001
|
["9c5ca6cc81c4eb5065e43f8b7ab6b7cc5ab7ae47", "dd-zcode-harness@2"],
|
|
1002
|
+
// CP253 installed artifact: exact same-child Read continuation and retained proof.
|
|
1003
|
+
["b18dfdbedaa9330e765315bce0f1a609792d6c91", "dd-zcode-harness@2"],
|
|
987
1004
|
]);
|
|
988
1005
|
const qualified = qualifiedBridgeCommits.get(bridgeCommit) === harnessContract;
|
|
989
1006
|
return { status: qualified ? "qualified" : "unqualified", mechanism: "cli-invocation@1", sha256,
|
|
@@ -1082,14 +1099,22 @@ export async function promptSessionWithBridge(bridge, options) {
|
|
|
1082
1099
|
const textCursor = bridge.assistantTextCursor(identity.adapterSessionId);
|
|
1083
1100
|
options.assertDispatch?.();
|
|
1084
1101
|
bridge.dispatchOperationId = options.operationId;
|
|
1085
|
-
const
|
|
1102
|
+
const managed = options.operationId ? { schema_id: "dd-flow/managed-provider-recovery@1", owner: "controller", operation_id: options.operationId, root_operation_id: options.operationId.replace(/:capacity:\d+:session\.prompt$/, "") } : null;
|
|
1103
|
+
const acknowledged = value => managed && value && Object.keys(managed).every(key => value[key] === managed[key]);
|
|
1104
|
+
if (managed && (bridge.recoveryCapability?.schema_id !== managed.schema_id || bridge.recoveryCapability?.owner !== "controller")) fail("ZCode bridge does not acknowledge controller-owned provider recovery", "provider_recovery_unavailable");
|
|
1105
|
+
const turn = await bridge.request("session/prompt", { sessionId: identity.adapterSessionId, prompt: [{ type: "text", text: prompt }], ...(managed ? { _meta: { zcode: { ddFlowRecovery: managed } } } : {}) }, timeoutMs, {
|
|
1086
1106
|
idleSessionId: identity.adapterSessionId,
|
|
1087
1107
|
idleTimeoutMs: options.livenessTimeoutMs ?? 600_000,
|
|
1088
1108
|
inactivityMode: "diagnostic",
|
|
1089
1109
|
}).catch(error => {
|
|
1090
1110
|
if (error.details?.terminal_response?.session_id === identity.adapterSessionId) error.details.provider_session_id = identity.providerSessionId;
|
|
1111
|
+
if (managed && !acknowledged(error.details?.native?._meta?.zcode?.ddFlowRecovery)) {
|
|
1112
|
+
const unknown = new Error("ZCode provider failure omitted managed recovery acknowledgement");
|
|
1113
|
+
unknown.code = "native_outcome_unknown"; unknown.details = { primary_error: errorRecord(error) }; throw unknown;
|
|
1114
|
+
}
|
|
1091
1115
|
throw error;
|
|
1092
1116
|
});
|
|
1117
|
+
if (managed && !acknowledged(turn?._meta?.zcode?.ddFlowRecovery)) fail("ZCode bridge omitted the managed recovery acknowledgement", "native_outcome_unknown");
|
|
1093
1118
|
const stopReason = typeof turn?.stopReason === "string" ? turn.stopReason : null;
|
|
1094
1119
|
const completionEvidence = turn?._meta?.zcodeCompletionEvidence;
|
|
1095
1120
|
if (completionEvidence === "inferred" || completionEvidence === "failed") {
|
|
@@ -1113,7 +1138,7 @@ export async function promptSessionWithBridge(bridge, options) {
|
|
|
1113
1138
|
throw error;
|
|
1114
1139
|
}
|
|
1115
1140
|
let evidence;
|
|
1116
|
-
try { await bridge.flush(); evidence = await inspect(bridge, identity.adapterSessionId); }
|
|
1141
|
+
try { await bridge.flush(); evidence = await inspect(bridge, identity.adapterSessionId, identity.providerSessionId); }
|
|
1117
1142
|
catch (cause) {
|
|
1118
1143
|
const error = new Error("ZCode completed the native turn, but post-turn observation failed");
|
|
1119
1144
|
error.code = "native_outcome_observation_failed";
|
|
@@ -1141,7 +1166,7 @@ export async function inspectSession(options) {
|
|
|
1141
1166
|
export async function inspectSessionWithBridge(bridge, options) {
|
|
1142
1167
|
requireJournal(options);
|
|
1143
1168
|
const identity = await controlledIdentity(bridge, options);
|
|
1144
|
-
return receipt(options, { harness: "zcode-acp", provider_session_id: identity.providerSessionId, adapter_session_id: identity.adapterSessionId, ...publicEvidence(await inspect(bridge, identity.adapterSessionId)) });
|
|
1169
|
+
return receipt(options, { harness: "zcode-acp", provider_session_id: identity.providerSessionId, adapter_session_id: identity.adapterSessionId, ...publicEvidence(await inspect(bridge, identity.adapterSessionId, identity.providerSessionId)) });
|
|
1145
1170
|
}
|
|
1146
1171
|
|
|
1147
1172
|
export async function cancelSession(options) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export type DelegationWork = { work_id: string; start_command: string; launch_policy?: string };
|
|
2
2
|
export type DelegationInput = { harness: string; stage: string; capacity: number; works?: DelegationWork[]; totalWorks?: number; remainingWorks?: number };
|
|
3
3
|
export function delegationAdapter(harness: string): string | null;
|
|
4
|
-
export function renderNativeChildFollowupInstruction(harness: string, childId: string): string | null;
|
|
4
|
+
export function renderNativeChildFollowupInstruction(harness: string, childId: string | { session_id?: unknown; parent_session_id?: unknown; terminal_turn_id?: unknown; continuation_ref?: unknown }): string | null;
|
|
5
5
|
export function delegationContract(harness: string): string;
|
|
6
6
|
export const lifecycleRetryInstruction: string;
|
|
7
7
|
export const lifecycleCommandCompletionInstruction: string;
|
|
@@ -39,12 +39,21 @@ export function delegationAdapter(harness) {
|
|
|
39
39
|
|
|
40
40
|
/** Only documented same-child APIs may continue an existing native worker. */
|
|
41
41
|
export function renderNativeChildFollowupInstruction(harness, childId) {
|
|
42
|
+
const child = childId && typeof childId === "object" ? childId : null;
|
|
43
|
+
childId = child?.session_id ?? childId;
|
|
42
44
|
if (typeof childId !== "string" || !childId) return null;
|
|
43
45
|
const adapter = delegationAdapter(harness), target = JSON.stringify(childId);
|
|
44
|
-
|
|
45
|
-
if (adapter === "
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
const message = JSON.stringify("Continue the same assigned Work after the transient provider failure; do not restart it or repeat committed lifecycle commands.");
|
|
47
|
+
if (adapter === "zcode") {
|
|
48
|
+
const ref = child?.continuation_ref;
|
|
49
|
+
if (ref?.schema_id !== "zcode/child-continuation@1" || ref.capability !== "dd-flow/managed-provider-recovery@1" || ref.child_session_id !== childId
|
|
50
|
+
|| ref.parent_session_id !== child.parent_session_id || ref.terminal_turn_id !== child.terminal_turn_id || !/^agent_[\w-]{1,200}$/.test(ref.agent_id ?? "")) return null;
|
|
51
|
+
return `Use the native SendMessage tool exactly once with to: ${JSON.stringify(ref.agent_id)}, summary: "Continue existing Work", and message: ${message}. Retain messageId and delivery only when the native result supplies them; never invent missing IDs. The adapter verifies typed native completion and successor evidence. Wait for this same agent with TaskOutput. If admission is uncertain, do not resend. Do not spawn a replacement or override model/reasoning.`;
|
|
52
|
+
}
|
|
53
|
+
if (adapter === "codex") return `Use collaboration.followup_task with target: ${target} and message: ${message}. Do not spawn a replacement.`;
|
|
54
|
+
if (adapter === "opencode") return `Use the native subagent tool with sessionID: ${target} and prompt: ${message}. Keep the same model and reasoning. Do not spawn a replacement.`;
|
|
55
|
+
if (adapter === "droid") return `Use the native Task tool with resume: ${target}, subagent_type: "dd-flow-worker", and prompt: ${message}. Omit model and complexity overrides. Do not spawn a replacement.`;
|
|
56
|
+
if (adapter === "grok") return `Use the native send_subagent_message tool with subagent_id: ${target}, delivery: "queue", and text: ${message}. Retain its accepted message_id and wait on the same subagent. If admission is uncertain, do not resend. Do not spawn a replacement.`;
|
|
48
57
|
return null;
|
|
49
58
|
}
|
|
50
59
|
|