@deksden-com/dd-flow-cli 0.9.0-beta.150 → 0.9.0-beta.152
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 +62 -10
- package/dist/harness-runtime/lib/dd-opencode.mjs +32 -14
- 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 +54 -309
- package/dist/services/check-runner-proof.js +373 -0
- package/dist/services/code-checks.js +31 -12
- 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/repair-intents.js +4 -1
- 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-code-review.js +1 -1
- package/dist/services/vnext-code.js +3 -2
- package/dist/services/vnext-fanout.js +1 -1
- package/dist/services/vnext-merge.js +1 -1
- package/dist/services/work-registry.js +1 -1
- package/dist/verification-components.json +299 -0
- package/package.json +7 -6
- package/tools/verification-jobs.mjs +317 -0
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.152",
|
|
4
|
+
"cli_commit": "70ed6da341845d1b7f262b74f6731ac4787aff73",
|
|
5
|
+
"built_at": "2026-10-26T03:31:35.940Z",
|
|
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
|
|
|
@@ -14,7 +14,7 @@ import { durableDaemonDispatch, completeDaemonShutdown, commitDaemonShutdown, tr
|
|
|
14
14
|
import { waitForSettlement } from "./session-settlement.mjs";
|
|
15
15
|
import { nativeExecutionReceipt } from "./native-children.mjs";
|
|
16
16
|
import { errorRecord, isProvenShellCorrection, reportedError } from "./operation-errors.mjs";
|
|
17
|
-
import { checkObservedProfile, observeModel } from "./model-observations.mjs";
|
|
17
|
+
import { checkObservedProfile, observeModel, observedFields, readModelObservations } from "./model-observations.mjs";
|
|
18
18
|
import os from "node:os";
|
|
19
19
|
import path from "node:path";
|
|
20
20
|
import { parse as parseJsonc } from "jsonc-parser";
|
|
@@ -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) {
|
|
@@ -199,6 +202,15 @@ export class Runtime {
|
|
|
199
202
|
const record = { provider_session_id: id, adapter_session_id: id, parent_provider_session_id: parent, root_provider_session_id: root, cwd: info.directory };
|
|
200
203
|
this.sessions.set(id, record); return record;
|
|
201
204
|
}
|
|
205
|
+
registerObservedChild(info, root) {
|
|
206
|
+
const owner = this.sessions.get(root);
|
|
207
|
+
if (!owner || owner.parent_provider_session_id || owner.root_provider_session_id !== root
|
|
208
|
+
|| info.id === root || info.parentID !== root || info.fork)
|
|
209
|
+
throw new DaemonError("session_identity_mismatch", "OpenCode child is not a direct member of the owned root");
|
|
210
|
+
if (typeof info.directory !== "string" || !path.isAbsolute(info.directory))
|
|
211
|
+
throw new OpenCodeError("opencode_unavailable", "OpenCode child directory binding is not yet observable");
|
|
212
|
+
return this.retainSession(info, root);
|
|
213
|
+
}
|
|
202
214
|
async journal(operation, result) { const line = `${JSON.stringify({ schema_id: "dd-opencode/daemon-event@1", event_id: randomUUID(), observed_at: new Date().toISOString(), operation, active_tree: this.state.active_tree, session_id: result?.provider_session_id ?? null })}\n`; for (const journal of new Set([this.paths.journal, this.state.config.journal].filter(Boolean))) { await mkdir(path.dirname(journal), { recursive: true, mode: 0o700 }); await appendFile(journal, line, { mode: 0o600 }); } }
|
|
203
215
|
async childTree(id, seen = new Set()) {
|
|
204
216
|
if (seen.has(id)) throw new DaemonError("session_identity_mismatch", "OpenCode native topology contains a cycle");
|
|
@@ -296,15 +308,52 @@ export class Runtime {
|
|
|
296
308
|
return nativeExecutionReceipt(receipt, { daemonId: this.state.daemon_id, revision: before.get(id) ?? 0 });
|
|
297
309
|
}
|
|
298
310
|
async observeModels(id, messages, parent = null) {
|
|
299
|
-
this.
|
|
311
|
+
const journal = this.state.config.journal ?? this.paths.journal;
|
|
312
|
+
if (!this.modelMessages) {
|
|
313
|
+
this.modelMessages = new Map();
|
|
314
|
+
for (const event of journal ? await readModelObservations(journal) : []) {
|
|
315
|
+
if (event.harness !== 'opencode-server' || event.source?.method !== 'session/message' || !event.source.message_id) continue;
|
|
316
|
+
const key = `${event.session_id}:${event.source.message_id}`;
|
|
317
|
+
if (event.evidence_conflict) { const prior = this.modelMessages.get(key); if (prior) prior.conflicted = true; continue; }
|
|
318
|
+
if (!this.modelMessages.has(key)) this.modelMessages.set(key, { source: event.source,
|
|
319
|
+
known: { parent: event.parent_session_id, root: event.native_agent_role?.launch?.root_session_id,
|
|
320
|
+
role: event.native_agent_role ?? null, requested: observedFields(event.requested), observed: observedFields(event.observed) } });
|
|
321
|
+
}
|
|
322
|
+
}
|
|
300
323
|
for (const message of messages ?? []) {
|
|
301
324
|
const info = message.info;
|
|
302
|
-
if (info?.role !== "assistant" || !info.id
|
|
325
|
+
if (info?.role !== "assistant" || !info.id) continue;
|
|
303
326
|
if (info.sessionID && info.sessionID !== id) throw new DaemonError("session_identity_mismatch", "native message belongs to another Session");
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
327
|
+
const record = this.sessions.get(id);
|
|
328
|
+
if (record && (record.parent_provider_session_id ?? null) !== parent)
|
|
329
|
+
throw new DaemonError("session_identity_mismatch", "OpenCode observed message parent changed");
|
|
330
|
+
if (!record?.root_provider_session_id || !info.mode) {
|
|
331
|
+
await observeModel({ journal, harness: "opencode-server", sessionId: id, parentSessionId: parent,
|
|
332
|
+
source: { channel: "native.message_observer", scope: "response", non_asserting: true }, evidence: "unavailable", reason: "native_role_binding_unavailable" });
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
const requested = { ...this.state.config, reasoning: this.state.config.variant, mode: parent ? "dd-flow-worker" : this.state.config.agent };
|
|
336
|
+
const nativeAgentRole = this.state.config.workerLaunchConfig ? { name: info.mode, kind: parent ? 'subagent' : 'primary', parent_session_id: parent,
|
|
337
|
+
launch: { ...this.state.config.workerLaunchConfig, root_session_id: record.root_provider_session_id } } : null;
|
|
338
|
+
const observed = observedFields({ provider: info.providerID, model: info.modelID, reasoning: info.variant, mode: info.mode });
|
|
339
|
+
const tuple = { parent, root: record.root_provider_session_id, role: nativeAgentRole, requested: observedFields(requested), observed };
|
|
340
|
+
const key = `${id}:${info.id}`, previous = this.modelMessages.get(key);
|
|
341
|
+
if (previous?.conflicted) throw new DaemonError("model_observation_identity_conflict", "OpenCode native message has a retained identity/profile conflict", false, { session_id: id, message_id: info.id });
|
|
342
|
+
const conflicts = (before, after) => before != null && after != null && (typeof before === "object" && typeof after === "object"
|
|
343
|
+
? [...new Set([...Object.keys(before), ...Object.keys(after)])].some(field => conflicts(before[field], after[field])) : before !== after);
|
|
344
|
+
if (previous && !conflicts(previous.known, tuple)) {
|
|
345
|
+
// Optional disclosure can arrive late. Do not rewrite the old native fact;
|
|
346
|
+
// remember known values only to detect a later positive contradiction.
|
|
347
|
+
for (const [field, value] of Object.entries(observed)) if (value != null) previous.known.observed[field] = value;
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
const source = previous?.source ?? { method: "session/message", message_id: info.id, event_sha256: createHash("sha256").update(`${id}:${info.id}`).digest("hex") };
|
|
351
|
+
const published = await observeModel({ journal, harness: "opencode-server", sessionId: id, parentSessionId: parent, requested, nativeAgentRole, observed, source, evidence: "response" });
|
|
352
|
+
if (previous || published?.evidence_conflict) {
|
|
353
|
+
if (previous) previous.conflicted = true;
|
|
354
|
+
throw new DaemonError("model_observation_identity_conflict", "OpenCode native message changed its published identity/profile", false, { session_id: id, message_id: info.id });
|
|
355
|
+
}
|
|
356
|
+
this.modelMessages.set(key, { known: structuredClone(tuple), source });
|
|
308
357
|
}
|
|
309
358
|
}
|
|
310
359
|
async requireSettled(id) {
|
|
@@ -457,6 +506,9 @@ export class Runtime {
|
|
|
457
506
|
assertDispatch, checkHookFailures, livenessTimeoutMs: params.livenessTimeoutMs ?? this.client.timeoutMs,
|
|
458
507
|
onDispatched: () => this.journal("native_dispatch", { operation_id: operationId, provider_session_id: params.sessionId }),
|
|
459
508
|
onModelMessages: (id, messages, parent) => this.observeModels(id, messages, parent),
|
|
509
|
+
onNativeSession: (info, root) => this.registerObservedChild(info, root),
|
|
510
|
+
rootSessionId: this.sessions.get(params.sessionId)?.root_provider_session_id ?? params.sessionId,
|
|
511
|
+
parentSessionId: this.sessions.get(params.sessionId)?.parent_provider_session_id ?? null,
|
|
460
512
|
onObservationError: error => observeModel({ journal: this.state.config.journal ?? this.paths.journal, harness: "opencode-server", sessionId: params.sessionId, requested: this.state.config, source: "native.message_observer", evidence: "unavailable", reason: error.code ?? "observation_unavailable" })
|
|
461
513
|
}); await checkHookFailures(); }
|
|
462
514
|
finally { current.requests.clear(); if (this.nativeHookOperation === current) this.nativeHookOperation = null; }
|
|
@@ -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),
|
|
@@ -169,13 +169,32 @@ export class OpenCodeClient {
|
|
|
169
169
|
import(data) { return this.call("session.import", data, { productive: true }); }
|
|
170
170
|
|
|
171
171
|
async prompt(id, { prompt, messageId = `msg_${randomUUID().replaceAll("-", "")}`, retained,
|
|
172
|
-
save, onModelMessages,
|
|
172
|
+
save, onModelMessages, onNativeSession, rootSessionId = id, parentSessionId = null,
|
|
173
|
+
onObservationError, assertDispatch, onDispatched, checkHookFailures, livenessTimeoutMs = this.timeoutMs }) {
|
|
173
174
|
validateObservationDuration(livenessTimeoutMs);
|
|
174
175
|
const request = { sessionID: id, id: messageId, text: prompt, delivery: "queue", resume: true }, digest = requestDigest(request);
|
|
175
176
|
if (retained && (retained.request_sha256 !== digest || retained.message_id !== messageId)) throw new OpenCodeError("operation_request_mismatch", "OpenCode operation already binds another prompt", false, { effect: "no_effect" });
|
|
176
177
|
let state = retained ?? { message_id: messageId, request_sha256: digest, phase: "prepared", cursor: 0 };
|
|
177
|
-
const previous = await this.messages(id), baseline = new Map([[id, new Set(previous.map(item => item.info.id))]]), parents = new Map();
|
|
178
|
-
|
|
178
|
+
const previous = await this.messages(id), baseline = new Map([[id, new Set(previous.map(item => item.info.id))]]), parents = new Map([[id, parentSessionId]]);
|
|
179
|
+
// The same native lineage guard applies before dispatch and while polling.
|
|
180
|
+
// Registration precedes message publication; a list ID alone is not ownership.
|
|
181
|
+
const childrenOf = async (session, visited) => {
|
|
182
|
+
const children = await this.children(session);
|
|
183
|
+
for (const child of children) {
|
|
184
|
+
if (!child?.id || child.parentID !== session || child.fork || session !== rootSessionId || visited.has(child.id)
|
|
185
|
+
|| parents.has(child.id) && parents.get(child.id) !== session || visited.size >= 1024)
|
|
186
|
+
throw new OpenCodeError("session_identity_mismatch", "OpenCode native child lineage is invalid", false, { session_id: child?.id, parent_session_id: session });
|
|
187
|
+
visited.add(child.id); parents.set(child.id, session);
|
|
188
|
+
await onNativeSession?.(child, rootSessionId);
|
|
189
|
+
}
|
|
190
|
+
return children;
|
|
191
|
+
};
|
|
192
|
+
const capture = async (session, visited = new Set([id])) => {
|
|
193
|
+
for (const child of await childrenOf(session, visited)) {
|
|
194
|
+
baseline.set(child.id, new Set((await this.messages(child.id)).map(item => item.info.id)));
|
|
195
|
+
await capture(child.id, visited);
|
|
196
|
+
}
|
|
197
|
+
};
|
|
179
198
|
await capture(id);
|
|
180
199
|
if (retained) {
|
|
181
200
|
const index = previous.findIndex(item => item.info.id === messageId);
|
|
@@ -258,8 +277,8 @@ export class OpenCodeClient {
|
|
|
258
277
|
}
|
|
259
278
|
if (state.cursor !== watermark) { state = { ...state, cursor: watermark }; await save?.(state); }
|
|
260
279
|
let runningTools = false;
|
|
261
|
-
const observe = async (session, visited = new Set()) => {
|
|
262
|
-
|
|
280
|
+
const observe = async (session, visited = new Set([id])) => {
|
|
281
|
+
known.add(session);
|
|
263
282
|
const messages = await this.messages(session);
|
|
264
283
|
await onModelMessages?.(session, messages, parents.get(session) ?? null);
|
|
265
284
|
const floor = baseline.get(session) ?? new Set(), current = messages.filter(item => item.info.role === "assistant" && !floor.has(item.info.id));
|
|
@@ -291,10 +310,7 @@ export class OpenCodeClient {
|
|
|
291
310
|
}
|
|
292
311
|
}
|
|
293
312
|
}
|
|
294
|
-
for (const child of await
|
|
295
|
-
if (child.parentID !== session) throw lost("OpenCode child has a foreign parent");
|
|
296
|
-
parents.set(child.id, session); await observe(child.id, visited);
|
|
297
|
-
}
|
|
313
|
+
for (const child of await childrenOf(session, visited)) await observe(child.id, visited);
|
|
298
314
|
return current;
|
|
299
315
|
};
|
|
300
316
|
const current = await observe(id), rootMessages = await this.messages(id);
|
|
@@ -309,8 +325,9 @@ export class OpenCodeClient {
|
|
|
309
325
|
// Native messages and topology are separate reads: a child can
|
|
310
326
|
// finish between them. Reconcile its ancestry before rejecting
|
|
311
327
|
// the notification, never trust the notification alone.
|
|
312
|
-
const
|
|
313
|
-
|
|
328
|
+
const visited = new Set([id]);
|
|
329
|
+
const child = (await childrenOf(id, visited)).find(child => child.id === item.info.metadata.childID);
|
|
330
|
+
if (child) await observe(child.id, visited);
|
|
314
331
|
}
|
|
315
332
|
if (item.info.role === "user" || item.info.role === "synthetic" && !(
|
|
316
333
|
item.info.metadata?.source === "subagent" && parents.get(item.info.metadata.childID) === id
|
|
@@ -353,7 +370,8 @@ export class OpenCodeClient {
|
|
|
353
370
|
}
|
|
354
371
|
} catch (error) {
|
|
355
372
|
await checkHookFailures?.();
|
|
356
|
-
if (error instanceof OpenCodeError && !["opencode_unavailable", "operation_observation_lost"].includes(error.code)
|
|
373
|
+
if (error instanceof OpenCodeError && !["opencode_unavailable", "operation_observation_lost"].includes(error.code)
|
|
374
|
+
|| ["session_identity_mismatch", "model_observation_identity_conflict"].includes(error.code)) throw error;
|
|
357
375
|
await onObservationError?.(error);
|
|
358
376
|
}
|
|
359
377
|
if (clock.sample(marker)) throw lost("OpenCode native progress stopped or observation could not recover", { session_id: id, message_id: messageId, observation: clock.state(), ...(observationErrors.count ? { observation_errors: observationErrors } : {}) });
|
|
@@ -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);
|