@deksden-com/dd-flow-cli 0.9.0-beta.123 → 0.9.0-beta.127
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 +26 -0
- package/README.md +33 -1
- package/dist/build-info.json +7 -7
- package/dist/cli/argument-values.js +38 -0
- package/dist/cli/command-inputs.js +4 -3
- package/dist/cli/help.js +1 -0
- package/dist/cli/hook-ingress.js +1 -1
- package/dist/cli/run-cli.js +126 -193
- package/dist/cli/runtime-maintenance-diagnostics.js +46 -0
- package/dist/cli/runtime-maintenance-ingress.js +51 -0
- package/dist/cli/runtime-process.js +128 -0
- package/dist/cli.js +8 -5
- package/dist/domain/stage-catalog.js +1 -1
- package/dist/harness-runtime/bin/dd-agy.mjs +3 -2
- package/dist/harness-runtime/bin/dd-codex.mjs +3 -2
- package/dist/harness-runtime/bin/dd-droid.mjs +3 -2
- package/dist/harness-runtime/bin/dd-grok.mjs +16 -6
- package/dist/harness-runtime/bin/dd-opencode.mjs +3 -2
- package/dist/harness-runtime/bin/dd-zcode.mjs +20 -16
- package/dist/harness-runtime/lib/adapter-timeouts.d.mts +5 -0
- package/dist/harness-runtime/lib/adapter-timeouts.mjs +34 -0
- package/dist/harness-runtime/lib/codex-capacity-policy.d.mts +10 -0
- package/dist/harness-runtime/lib/codex-capacity-policy.mjs +89 -0
- package/dist/harness-runtime/lib/daemon-observation.d.mts +2 -0
- package/dist/harness-runtime/lib/daemon-observation.mjs +163 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +159 -15
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +215 -86
- package/dist/harness-runtime/lib/dd-agy.mjs +11 -13
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +47 -23
- package/dist/harness-runtime/lib/dd-codex.mjs +257 -99
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +32 -30
- package/dist/harness-runtime/lib/dd-droid.mjs +117 -31
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +96 -38
- package/dist/harness-runtime/lib/dd-grok.mjs +15 -13
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +54 -20
- package/dist/harness-runtime/lib/dd-opencode.mjs +44 -14
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +81 -76
- package/dist/harness-runtime/lib/dd-zcode.mjs +204 -116
- package/dist/harness-runtime/lib/driver-recovery.mjs +12 -2
- package/dist/harness-runtime/lib/lease-renewal.d.mts +8 -0
- package/dist/harness-runtime/lib/lease-renewal.mjs +23 -0
- package/dist/harness-runtime/lib/lifecycle-command.d.mts +3 -3
- package/dist/harness-runtime/lib/lifecycle-command.mjs +74 -20
- package/dist/harness-runtime/lib/managed-daemon.d.mts +11 -0
- package/dist/harness-runtime/lib/managed-daemon.mjs +321 -65
- package/dist/harness-runtime/lib/native-children.d.mts +13 -0
- package/dist/harness-runtime/lib/native-children.mjs +78 -0
- package/dist/harness-runtime/lib/native-hook-command.d.mts +4 -0
- package/dist/harness-runtime/lib/native-hook-command.mjs +115 -11
- package/dist/harness-runtime/lib/native-streams.mjs +45 -0
- package/dist/harness-runtime/lib/observation-clock.d.mts +18 -0
- package/dist/harness-runtime/lib/observation-clock.mjs +73 -17
- package/dist/harness-runtime/lib/operation-errors.d.mts +14 -0
- package/dist/harness-runtime/lib/operation-errors.mjs +105 -8
- package/dist/harness-runtime/lib/operation-progress.d.mts +7 -0
- package/dist/harness-runtime/lib/operation-progress.mjs +112 -0
- package/dist/harness-runtime/lib/process-json.mjs +136 -38
- package/dist/harness-runtime/lib/tool-observations.d.mts +1 -0
- package/dist/harness-runtime/lib/tool-observations.mjs +5 -2
- package/dist/harness-runtime/lib/zcode-usage.mjs +3 -2
- package/dist/runtime/context.js +8 -4
- package/dist/schemas/flow-run.schema.json +1 -0
- package/dist/schemas/plan-review-decision.schema.json +3 -3
- package/dist/schemas/run-control-receipt.schema.json +19 -0
- package/dist/services/code-checks.js +93 -27
- package/dist/services/codex-capacity.js +251 -74
- package/dist/services/codex-hook-delivery.js +2 -2
- package/dist/services/compatibility-preflight.js +4 -1
- package/dist/services/controller-fanout.js +9 -75
- package/dist/services/engines.js +70 -20
- package/dist/services/eval-snapshots.js +179 -40
- package/dist/services/external-work-launch.js +21 -9
- package/dist/services/harness-adapter.js +195 -25
- package/dist/services/hooks.js +65 -21
- package/dist/services/lifecycle-invocations.js +404 -32
- package/dist/services/managed-lease-monitor.js +128 -0
- package/dist/services/managed-processes.js +117 -53
- package/dist/services/merge-server.js +66 -23
- package/dist/services/recovery-observation-budget.js +67 -7
- package/dist/services/recovery-snapshot-database.js +7 -4
- package/dist/services/review-deferral.js +23 -0
- package/dist/services/run-control-receipt.js +1 -1
- package/dist/services/run-control-worker.js +177 -118
- package/dist/services/run-control.js +99 -8
- package/dist/services/run-controller-adapter.js +177 -51
- package/dist/services/run-controller-capture.js +14 -2
- package/dist/services/run-controller-process.js +11 -4
- package/dist/services/run-controller-recovery.js +17 -0
- package/dist/services/run-controller-state.js +68 -7
- package/dist/services/run-controller.js +207 -107
- package/dist/services/run-fork.js +8 -3
- package/dist/services/run-recovery-guard.js +75 -0
- package/dist/services/run-recovery-runtime.js +21 -30
- package/dist/services/run-recovery.js +17 -13
- package/dist/services/runs.js +40 -7
- package/dist/services/runtime-budget.js +32 -15
- package/dist/services/runtime-scope-control.js +8 -0
- package/dist/services/runtime-scope-resume.js +30 -8
- package/dist/services/runtime-scope-worker.js +118 -73
- package/dist/services/runtime-service.js +6 -7
- package/dist/services/scope-writer-cleanup.js +3 -2
- package/dist/services/stage-context.js +135 -0
- package/dist/services/stage-lifecycle.js +3 -15
- package/dist/services/stage-pause.js +3 -2
- package/dist/services/usage.js +14 -3
- package/dist/services/verification-matrix.js +30 -1
- package/dist/services/vnext-code-review.js +26 -13
- package/dist/services/vnext-code.js +10 -8
- package/dist/services/vnext-contracts.js +10 -1
- package/dist/services/vnext-execution-profile.js +75 -2
- package/dist/services/vnext-fanout.js +5 -0
- package/dist/services/vnext-merge.js +30 -11
- package/dist/services/vnext-plan-review.js +102 -35
- package/dist/services/vnext-plan.js +9 -7
- package/dist/services/vnext-protocolize.js +15 -9
- package/dist/services/vnext-specify.js +6 -6
- package/dist/services/work-registry.js +43 -6
- package/dist/services/workspace-bootstrap.js +14 -3
- package/dist/shared/errors.js +11 -6
- package/dist/storage/agent-output.js +26 -0
- package/dist/storage/database.js +80 -21
- package/package.json +7 -4
|
@@ -1,14 +1,17 @@
|
|
|
1
|
-
import { observedTimeout } from "./observation-clock.mjs";
|
|
1
|
+
import { ObservationClock, observedTimeout, validateObservationDuration } from "./observation-clock.mjs";
|
|
2
2
|
import { ToolObservations, acpToolObservation, zcodePhysicalSession } from "./tool-observations.mjs";
|
|
3
|
-
import { invokeNativeHook } from "./native-hook-command.mjs";
|
|
4
|
-
import { parseLifecycleCommand, continuationCommand } from "./lifecycle-command.mjs";
|
|
3
|
+
import { hookBudget, invokeNativeHook } from "./native-hook-command.mjs";
|
|
4
|
+
import { parseLifecycleCommand, continuationCommand, commandOption } from "./lifecycle-command.mjs";
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
6
6
|
import { createHash } from "node:crypto";
|
|
7
7
|
import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
8
8
|
import path from "node:path";
|
|
9
|
-
import
|
|
10
|
-
import {
|
|
9
|
+
import { closeBridgeProcess } from "./managed-daemon.mjs";
|
|
10
|
+
import { errorRecord, isProvenShellCorrection } from "./operation-errors.mjs";
|
|
11
11
|
import { checkObservedProfile, observeModel, observedFields } from "./model-observations.mjs";
|
|
12
|
+
import { commandText } from "./process-json.mjs";
|
|
13
|
+
import { readNativeLines, streamNativeDiagnostics, nativeObservationError } from "./native-streams.mjs";
|
|
14
|
+
import { operationProgressReporter } from "./operation-progress.mjs";
|
|
12
15
|
|
|
13
16
|
import { readZcodeUsage } from "./zcode-usage.mjs";
|
|
14
17
|
const ZCODE_HARNESS_CONTRACT = "dd-zcode-harness@1";
|
|
@@ -17,11 +20,11 @@ export const supportedZcodeHarnessContract = contract => [ZCODE_HARNESS_CONTRACT
|
|
|
17
20
|
// request. Keep the original correlation slot briefly so a slow native
|
|
18
21
|
// response (especially cold-start/session creation) can still settle it.
|
|
19
22
|
const LATE_RESPONSE_GRACE_MS = 5_000;
|
|
23
|
+
// Only the owned lifecycle transport can attest a persisted CLI barrier.
|
|
24
|
+
// Model text and arbitrary callback error.details cannot grant correction.
|
|
25
|
+
const correctableLifecycleErrors = new WeakSet();
|
|
20
26
|
|
|
21
27
|
function fail(message, code) { const error = new Error(message); if (code) error.code = code; throw error; }
|
|
22
|
-
function fatalLifecycleError(error) {
|
|
23
|
-
return ["invocation_unknown", "invocation_identity_mismatch", "invocation_identity_conflict", "invocation_receipt_missing", "invocation_command_mismatch", "invocation_scope_mismatch", "invocation_generation_stale", "invocation_expired", "zcode_parent_identity_missing", "zcode_tool_identity_conflict", "native_hook_timeout", "native_hook_failed", "native_hook_response_invalid", "hook_storage_unprepared", "storage_write_failed"].includes(error?.code);
|
|
24
|
-
}
|
|
25
28
|
function absolute(value, label) { if (!value || !path.isAbsolute(value)) fail(`${label} must be an absolute path`); return path.resolve(value); }
|
|
26
29
|
function text(value, label) { if (typeof value !== "string" || !value.trim()) fail(`${label} is required`); return value.trim(); }
|
|
27
30
|
function now() { return new Date().toISOString(); }
|
|
@@ -38,27 +41,12 @@ function executable(command, args = []) {
|
|
|
38
41
|
|
|
39
42
|
async function commandOutput(command, args) {
|
|
40
43
|
const target = executable(command, args);
|
|
41
|
-
return await
|
|
42
|
-
const child = spawn(target.command, target.args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
43
|
-
let stdout = ""; let stderr = "";
|
|
44
|
-
child.stdout.setEncoding("utf8").on("data", (chunk) => (stdout += chunk));
|
|
45
|
-
child.stderr.setEncoding("utf8").on("data", (chunk) => (stderr += chunk));
|
|
46
|
-
child.on("error", reject);
|
|
47
|
-
child.on("close", (code) => code === 0 ? resolve(stdout.trim()) : reject(new Error(stderr.trim() || `${target.command} exited ${code}`)));
|
|
48
|
-
});
|
|
44
|
+
return await commandText(target.command, target.args, { phase: "native-version" });
|
|
49
45
|
}
|
|
50
46
|
|
|
51
47
|
async function runWithStdin(command, args, stdin, env = {}) {
|
|
52
48
|
const target = executable(command, args);
|
|
53
|
-
return await
|
|
54
|
-
const child = spawn(target.command, target.args, { env: { ...process.env, ...env }, stdio: ["pipe", "pipe", "pipe"] });
|
|
55
|
-
let stdout = ""; let stderr = "";
|
|
56
|
-
child.stdout.setEncoding("utf8").on("data", (chunk) => (stdout += chunk));
|
|
57
|
-
child.stderr.setEncoding("utf8").on("data", (chunk) => (stderr += chunk));
|
|
58
|
-
child.on("error", reject);
|
|
59
|
-
child.on("close", (code) => code === 0 ? resolve(stdout.trim()) : reject(new Error(stderr.trim() || `${target.command} exited ${code}`)));
|
|
60
|
-
child.stdin.end(stdin);
|
|
61
|
-
});
|
|
49
|
+
return await commandText(target.command, target.args, { input: stdin, env, phase: "usage-ingest" });
|
|
62
50
|
}
|
|
63
51
|
|
|
64
52
|
class Journal {
|
|
@@ -72,6 +60,7 @@ class Journal {
|
|
|
72
60
|
if (!this.file) return;
|
|
73
61
|
const line = JSON.stringify({ order: ++this.order, observed_at: now(), kind, payload });
|
|
74
62
|
this.pending = this.pending.then(() => appendFile(this.file, `${line}\n`));
|
|
63
|
+
void this.pending.catch(() => {});
|
|
75
64
|
return this.pending;
|
|
76
65
|
}
|
|
77
66
|
writeInbound(payload, method) {
|
|
@@ -109,6 +98,7 @@ class Journal {
|
|
|
109
98
|
}
|
|
110
99
|
await appendFile(this.file, `${line}\n`);
|
|
111
100
|
});
|
|
101
|
+
void this.pending.catch(() => {});
|
|
112
102
|
return this.pending;
|
|
113
103
|
}
|
|
114
104
|
async flush() { await this.pending; }
|
|
@@ -157,8 +147,11 @@ export class AcpBridge {
|
|
|
157
147
|
}
|
|
158
148
|
|
|
159
149
|
configure(options) { this.options = { ...this.options, ...options }; }
|
|
160
|
-
registerModelSession(adapterId, sessionId, requested, parentSessionId = null, armed = true) {
|
|
161
|
-
this.modelSessions.
|
|
150
|
+
registerModelSession(adapterId, sessionId, requested, parentSessionId = null, armed = true, bindCurrentRequest = false) {
|
|
151
|
+
const parentAdapter = parentSessionId ? [...this.modelSessions].find(([key, value]) => key === parentSessionId || value.sessionId === parentSessionId)?.[0] : null;
|
|
152
|
+
const parentRequest = this.activityOwner(parentAdapter);
|
|
153
|
+
const prior = this.modelSessions.get(adapterId);
|
|
154
|
+
this.modelSessions.set(adapterId, { sessionId, requested, parentSessionId, armed, parentRequestId: prior && !bindCurrentRequest ? prior.parentRequestId ?? null : parentRequest?.id ?? null });
|
|
162
155
|
}
|
|
163
156
|
async recordModel(adapterId, observed, source) {
|
|
164
157
|
const identity = this.modelSessions.get(adapterId); if (!identity) return;
|
|
@@ -182,15 +175,15 @@ export class AcpBridge {
|
|
|
182
175
|
const target = executable(this.options.bin, this.options.commandArgs ?? ["server"]);
|
|
183
176
|
const cwd = this.options.cwd ? absolute(this.options.cwd, "--cwd") : process.cwd();
|
|
184
177
|
this.child = spawn(target.command, target.args, { cwd, detached: process.platform !== "win32", stdio: ["pipe", "pipe", "pipe"], env: { ...process.env, ...(this.options.env ?? {}) } });
|
|
185
|
-
this.child.stderr
|
|
178
|
+
this.diagnostics = streamNativeDiagnostics(this.child.stderr, async chunk => { this.bridgeStderr = (this.bridgeStderr + chunk).slice(-65_536); await this.journal.write("bridge_stderr", { text: chunk }); }, error => this.rejectAll(error));
|
|
186
179
|
this.child.stdin?.on("error", (error) => {
|
|
187
180
|
error.details = { system_code: error.code ?? null };
|
|
188
181
|
error.code = "bridge_pipe_broken";
|
|
189
182
|
this.rejectAll(error);
|
|
190
183
|
});
|
|
191
184
|
this.child.on("error", (error) => { error.code ??= "bridge_exited"; this.rejectAll(error); });
|
|
192
|
-
this.child.on("
|
|
193
|
-
|
|
185
|
+
this.child.on("close", code => { void Promise.all([this.nativeDrain, this.diagnostics]).then(() => { const error = new Error(`zcode-acp exited ${code}${this.bridgeStderr.trim() ? `: ${this.bridgeStderr.trim()}` : ""}`); error.code = "bridge_exited"; this.rejectAll(error); }); });
|
|
186
|
+
this.nativeDrain = readNativeLines(this.child.stdout, async line => { this.receive(line); await this.journal.flush(); }, error => this.rejectAll(error));
|
|
194
187
|
if (this.options.onSpawn) await this.options.onSpawn(this.child);
|
|
195
188
|
return await this.request("initialize", {
|
|
196
189
|
protocolVersion: 1,
|
|
@@ -205,15 +198,23 @@ export class AcpBridge {
|
|
|
205
198
|
const request = message.id !== undefined ? this.pending.get(message.id) : null;
|
|
206
199
|
const expired = message.id !== undefined ? this.expiredRequests.get(message.id) : null;
|
|
207
200
|
const isResponse = message.id !== undefined && !message.method;
|
|
201
|
+
const rejected = isResponse && this.rejectedHookRequests?.get(message.id);
|
|
202
|
+
if (rejected) {
|
|
203
|
+
this.rejectedHookRequests.delete(message.id);
|
|
204
|
+
const terminal = { request_id: message.id, method: rejected.method, session_id: rejected.sessionId, terminal_response: message };
|
|
205
|
+
rejected.error.details = { ...rejected.error.details, native_terminal_after_hook_failure: terminal };
|
|
206
|
+
try { void Promise.resolve(this.journal.write("native_terminal_after_hook_failure", terminal)).catch(storage => { rejected.error.cleanup_error ??= errorRecord(storage); }); }
|
|
207
|
+
catch (storage) { rejected.error.cleanup_error ??= errorRecord(storage); }
|
|
208
|
+
}
|
|
208
209
|
if (!isResponse || request) this.journal.writeInbound(message, request?.method);
|
|
209
|
-
if (typeof message.params?.sessionId === "string" && isProductiveSessionNotification(message)) this.
|
|
210
|
+
if (typeof message.params?.sessionId === "string" && isProductiveSessionNotification(message)) this.observeActivity(message.params.sessionId, message.params.update);
|
|
210
211
|
const update = message.params?.update;
|
|
211
212
|
// ZCode emits this as `session/update`; Grok Build wraps the same ACP
|
|
212
213
|
// payload in `_x.ai/session_notification`. Preserve either provider's
|
|
213
214
|
// native error before its generic JSON-RPC terminal response arrives.
|
|
214
215
|
if (typeof message.params?.sessionId === "string" && update?.sessionUpdate === "retry_state" && update.type === "failed") {
|
|
215
216
|
for (const pending of this.pending.values()) {
|
|
216
|
-
if (pending.method === "session/prompt" && pending.sessionId === message.params.sessionId) pending.providerError = update;
|
|
217
|
+
if (pending.method === "session/prompt" && pending.sessionId === message.params.sessionId && !pending.providerError) pending.providerError = { ...update, observed_at: now(), provider_session_id: pending.sessionId, source: message.method };
|
|
217
218
|
}
|
|
218
219
|
}
|
|
219
220
|
if (message.method === "session/update" && message.params?.update?.sessionUpdate === "agent_message_chunk" && message.params.update.content?.type === "text") {
|
|
@@ -235,17 +236,21 @@ export class AcpBridge {
|
|
|
235
236
|
const pending = Promise.resolve().then(() => observer(message)).catch(async error => {
|
|
236
237
|
// A validated no-effect rejection is returned through the invocation
|
|
237
238
|
// ledger. It is evidence, not a poisoned ACP Session.
|
|
238
|
-
const fatal = fatalLifecycleError(error);
|
|
239
239
|
const current = this.options.onLifecycleNotification === observer;
|
|
240
|
-
|
|
240
|
+
const correctable = current && correctableLifecycleErrors.has(error);
|
|
241
|
+
if (current && !correctable) {
|
|
242
|
+
this.lifecycleFailure ??= this.notificationError?.code === "profile_integrity_violation" ? this.notificationError : error;
|
|
243
|
+
this.notificationError = this.lifecycleFailure;
|
|
244
|
+
}
|
|
241
245
|
// A missing/foreign authority is not a recoverable CLI validation
|
|
242
246
|
// result. Stop admitting productive requests immediately; the managed
|
|
243
247
|
// controller still owns physical stop/drain and evidence capture.
|
|
244
|
-
if (
|
|
248
|
+
if (current && !correctable) this.rejectAll(this.notificationError);
|
|
245
249
|
try {
|
|
246
|
-
await this.journal.write("notification_failure", {
|
|
247
|
-
session_id: message.params?.sessionId ?? null, tool_call_id: update?.toolCallId ?? null, event_time: now(), delivery_stage: "lifecycle_receipt", completeness: "incomplete" });
|
|
250
|
+
await hookBudget(1_000, null).run(() => this.journal.write("notification_failure", { ...errorRecord(error),
|
|
251
|
+
session_id: message.params?.sessionId ?? null, tool_call_id: update?.toolCallId ?? null, event_time: now(), delivery_stage: "lifecycle_receipt", completeness: "incomplete" }), "failure_persistence");
|
|
248
252
|
} catch (persistenceError) {
|
|
253
|
+
if (current) this.notificationError ??= persistenceError;
|
|
249
254
|
process.stderr.write(`${JSON.stringify({ code: error.code, message: error.message, persistence_error: persistenceError.message })}\n`);
|
|
250
255
|
}
|
|
251
256
|
});
|
|
@@ -275,6 +280,7 @@ export class AcpBridge {
|
|
|
275
280
|
error.code = unknown ? nativeCode : /payment required|quota|usage balance exhausted|insufficient.?credit|billing/i.test(messageText) ? "provider_quota_exhausted" : /rate.?limit|too many requests/i.test(messageText) ? "provider_rate_limited" : /\b429\b/.test(messageText) ? "provider_limit_unknown" : "acp_request_failed";
|
|
276
281
|
error.retryable = error.code === "provider_rate_limited";
|
|
277
282
|
error.details = { acp_code: message.error.code ?? null,
|
|
283
|
+
observed_at: provider?.observed_at ?? now(), provider_session_id: pending.sessionId ?? null, source: "acp.error_response", request_id: message.id,
|
|
278
284
|
terminal_response: { method: pending.method, session_id: pending.sessionId, request_id: message.id },
|
|
279
285
|
...(data ? { native: data } : {}), ...(details ? { provider_details: details } : {}), ...(providerDetails ? { provider_update: provider } : {}) };
|
|
280
286
|
pending.reject(error);
|
|
@@ -299,6 +305,60 @@ export class AcpBridge {
|
|
|
299
305
|
this.toolObservations.add(acpToolObservation(message, { harness: this.options.observationHarness ?? "zcode-acp", rootSessionId }));
|
|
300
306
|
}
|
|
301
307
|
|
|
308
|
+
observeActivity(sessionId, update) {
|
|
309
|
+
const observedAt = Date.now(), seen = new Set();
|
|
310
|
+
let adapterId = sessionId;
|
|
311
|
+
// ACP has no Turn ID: scope direct notifications to the active request on
|
|
312
|
+
// this connection, and descendants to their parent's dispatch generation.
|
|
313
|
+
const owner = this.activityOwner(adapterId);
|
|
314
|
+
if (!owner) return;
|
|
315
|
+
if (["tool_call", "tool_call_update"].includes(update?.sessionUpdate)) {
|
|
316
|
+
const toolId = update.toolCallId;
|
|
317
|
+
if (!toolId) return;
|
|
318
|
+
owner.toolProgress ??= new Map();
|
|
319
|
+
const signature = JSON.stringify({ status: update.status, kind: update.kind, title: update.title, content: update.content, rawInput: update.rawInput, rawOutput: update.rawOutput });
|
|
320
|
+
const key = `${sessionId}:${toolId}`, previous = owner.toolProgress.get(key);
|
|
321
|
+
if (previous === signature) return;
|
|
322
|
+
// An empty update is only a replay/metadata snapshot, not tool work.
|
|
323
|
+
if (update.sessionUpdate === "tool_call_update" && !update.status && !update.content?.length && update.rawInput === undefined && update.rawOutput === undefined) return;
|
|
324
|
+
if (owner.toolProgress.size >= 1_024 && !owner.toolProgress.has(key)) return;
|
|
325
|
+
owner.toolProgress.set(key, signature);
|
|
326
|
+
}
|
|
327
|
+
while (adapterId && !seen.has(adapterId)) {
|
|
328
|
+
seen.add(adapterId);
|
|
329
|
+
const pending = [...this.pending.values()].find(value => value.method === "session/prompt" && value.sessionId === adapterId);
|
|
330
|
+
if (pending) {
|
|
331
|
+
pending.progress = { cursor: (pending.progress?.cursor ?? 0) + 1, observed_at: observedAt };
|
|
332
|
+
const rejectObservation = cause => {
|
|
333
|
+
if (this.pending.get(pending.id) !== pending) return;
|
|
334
|
+
clearTimeout(pending.timer); if (pending.idleTimer) clearInterval(pending.idleTimer);
|
|
335
|
+
this.pending.delete(pending.id); pending.reject(nativeObservationError(cause));
|
|
336
|
+
};
|
|
337
|
+
void pending.publish("native_progress", { native_request_id: String(pending.id), session_id: adapterId, observed_at: new Date(observedAt).toISOString() }).catch(rejectObservation);
|
|
338
|
+
this.sessionActivity.set(adapterId, observedAt);
|
|
339
|
+
void Promise.resolve(this.options.onActivity?.({ sessionId: adapterId, requestId: pending.id, progress: pending.progress })).catch(rejectObservation);
|
|
340
|
+
}
|
|
341
|
+
const child = this.modelSessions.get(adapterId), parent = child?.parentSessionId;
|
|
342
|
+
const next = parent ? [...this.modelSessions].find(([key, value]) => key === parent || value.sessionId === parent)?.[0] : null;
|
|
343
|
+
const parentPending = this.activityOwner(next);
|
|
344
|
+
adapterId = parentPending?.id === child?.parentRequestId ? next : null;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
activityOwner(sessionId) {
|
|
349
|
+
const path = [], seen = new Set();
|
|
350
|
+
while (sessionId && !seen.has(sessionId)) {
|
|
351
|
+
seen.add(sessionId);
|
|
352
|
+
const pending = [...this.pending.values()].find(value => value.method === "session/prompt" && value.sessionId === sessionId);
|
|
353
|
+
if (pending) return path.every(child => child.parentRequestId === pending.id) ? pending : null;
|
|
354
|
+
const identity = this.modelSessions.get(sessionId);
|
|
355
|
+
if (!identity?.parentSessionId) return null;
|
|
356
|
+
path.push(identity);
|
|
357
|
+
sessionId = [...this.modelSessions].find(([key, value]) => key === identity.parentSessionId || value.sessionId === identity.parentSessionId)?.[0];
|
|
358
|
+
}
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
|
|
302
362
|
toolSummary(sessionId) {
|
|
303
363
|
return this.toolObservations.summary(this.modelSessions.get(sessionId)?.sessionId ?? sessionId);
|
|
304
364
|
}
|
|
@@ -329,7 +389,9 @@ export class AcpBridge {
|
|
|
329
389
|
}
|
|
330
390
|
|
|
331
391
|
request(method, params = {}, timeoutMs = 30_000, { idleSessionId = null, idleTimeoutMs = null, inactivityMode = "reject" } = {}) {
|
|
332
|
-
if (
|
|
392
|
+
if (timeoutMs !== null) validateObservationDuration(timeoutMs);
|
|
393
|
+
if (idleTimeoutMs !== null) validateObservationDuration(idleTimeoutMs);
|
|
394
|
+
if ((this.lifecycleFailure || this.notificationError?.code === "profile_integrity_violation") && ["session/new", "session/prompt", "session/fork"].includes(method)) return Promise.reject(this.lifecycleFailure ?? this.notificationError);
|
|
333
395
|
if (this.inactivityError && ["session/new", "session/prompt", "session/fork"].includes(method)) return Promise.reject(this.inactivityError);
|
|
334
396
|
const id = this.nextId++;
|
|
335
397
|
return new Promise((resolve, reject) => {
|
|
@@ -353,39 +415,32 @@ export class AcpBridge {
|
|
|
353
415
|
this.expiredRequests.set(id, { method, sessionId: params.sessionId ?? null });
|
|
354
416
|
while (this.expiredRequests.size > 128) this.expiredRequests.delete(this.expiredRequests.keys().next().value);
|
|
355
417
|
finish(timeoutError);
|
|
356
|
-
}, LATE_RESPONSE_GRACE_MS);
|
|
357
|
-
}, timeoutMs)
|
|
418
|
+
}, LATE_RESPONSE_GRACE_MS, { progress: () => pending.progress });
|
|
419
|
+
}, timeoutMs, { progress: () => this.pending.get(id)?.progress })
|
|
358
420
|
: null;
|
|
359
421
|
const validIdleTimeout = Number.isFinite(idleTimeoutMs) && idleTimeoutMs > 0 ? idleTimeoutMs : null;
|
|
360
422
|
if (idleSessionId) this.sessionActivity.set(idleSessionId, Date.now());
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
let
|
|
423
|
+
const clock = validIdleTimeout ? new ObservationClock({ timeoutMs: validIdleTimeout,
|
|
424
|
+
onGap: gap => this.journal.write("observation_gap", { ...gap, method, request_id: id, session_id: idleSessionId }) }) : null;
|
|
425
|
+
let checking = false, quietCursor = null;
|
|
364
426
|
const checkIdle = async () => {
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
const
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
inactivityRecorded = true;
|
|
383
|
-
this.journal.write("inactivity_detected", { method, request_id: id, session_id: idleSessionId, last_activity_at: new Date(lastActivity).toISOString(), idle_timeout_ms: validIdleTimeout, observed_at: new Date(observedAt).toISOString() });
|
|
384
|
-
if (inactivityMode !== "diagnostic") {
|
|
385
|
-
finish(Object.assign(new Error(`${method} received no session update for ${validIdleTimeout}ms`), { code: "subject_liveness_timeout", details: { session_id: idleSessionId, last_activity_at: new Date(lastActivity).toISOString(), idle_timeout_ms: validIdleTimeout } }));
|
|
386
|
-
return;
|
|
387
|
-
}
|
|
388
|
-
await this.options.onInactivity?.({ method, requestId: id, sessionId: idleSessionId, lastActivityAt: lastActivity, idleTimeoutMs: validIdleTimeout });
|
|
427
|
+
const pending = this.pending.get(id);
|
|
428
|
+
if (!pending || checking || !clock.sample(pending.progress)) return;
|
|
429
|
+
const cursor = pending.progress?.cursor ?? 0;
|
|
430
|
+
if (quietCursor === cursor) return;
|
|
431
|
+
quietCursor = cursor; checking = true;
|
|
432
|
+
const lastActivity = this.sessionActivity.get(idleSessionId) ?? pending.dispatchedAt;
|
|
433
|
+
try {
|
|
434
|
+
this.journal.write("inactivity_detected", { method, request_id: id, session_id: idleSessionId, last_activity_at: new Date(lastActivity).toISOString(), idle_timeout_ms: validIdleTimeout, observed_at: now() });
|
|
435
|
+
if (inactivityMode === "diagnostic") await this.options.onInactivity?.({ method, requestId: id, sessionId: idleSessionId, lastActivityAt: lastActivity, idleTimeoutMs: validIdleTimeout, cursor });
|
|
436
|
+
const current = this.pending.get(id);
|
|
437
|
+
if (current !== pending || (current.progress?.cursor ?? 0) !== cursor || !clock.sample(current.progress)) return;
|
|
438
|
+
this.expiredRequests.set(id, { method, sessionId: pending.sessionId });
|
|
439
|
+
finish(Object.assign(new Error(`${method} native outcome cannot be confirmed after ${validIdleTimeout}ms of silence`), { code: clock.observationLost ? "operation_observation_lost" : "subject_liveness_timeout", details: { request_id: id, session_id: idleSessionId, last_activity_at: new Date(lastActivity).toISOString(), idle_timeout_ms: validIdleTimeout, native_outcome: "unknown" } }));
|
|
440
|
+
} catch (error) {
|
|
441
|
+
const current = this.pending.get(id);
|
|
442
|
+
if (current === pending && (current.progress?.cursor ?? 0) === cursor) finish(Object.assign(new Error("ACP inactivity reconciliation could not confirm the native outcome"), { code: "operation_observation_lost", details: { request_id: id, session_id: idleSessionId, native_outcome: "unknown", observation_error: errorRecord(error) }, cause: error }));
|
|
443
|
+
} finally { checking = false; }
|
|
389
444
|
};
|
|
390
445
|
const idleTimer = validIdleTimeout ? setInterval(() => {
|
|
391
446
|
void checkIdle().catch((error) => {
|
|
@@ -396,8 +451,9 @@ export class AcpBridge {
|
|
|
396
451
|
void this.journal.write("inactivity_observation_failure", { method, request_id: id, session_id: idleSessionId, error: { code: error?.code ?? "inactivity_observation_failed", message: error?.message ?? String(error) } }).catch(() => {});
|
|
397
452
|
});
|
|
398
453
|
}, Math.max(10, Math.min(1_000, Math.floor(validIdleTimeout / 4)))) : null;
|
|
399
|
-
this.pending.set(id, { resolve, reject, timer, idleTimer, method, sessionId: params.sessionId ?? null });
|
|
454
|
+
this.pending.set(id, { id, resolve, reject, timer, idleTimer, method, sessionId: params.sessionId ?? null, dispatchedAt: Date.now(), publish: operationProgressReporter() });
|
|
400
455
|
this.send({ jsonrpc: "2.0", id, method, params });
|
|
456
|
+
if (method === "session/prompt") this.journal.write("native_dispatch", { operation_id: this.dispatchOperationId ?? process.env.DD_EVAL_OPERATION_ID ?? null, session_id: params.sessionId, native_request_id: id });
|
|
401
457
|
});
|
|
402
458
|
}
|
|
403
459
|
|
|
@@ -413,8 +469,18 @@ export class AcpBridge {
|
|
|
413
469
|
try { stdin.write(`${JSON.stringify(message)}\n`); }
|
|
414
470
|
catch (error) { error.details = { system_code: error.code ?? null }; error.code = "bridge_pipe_broken"; this.rejectAll(error); throw error; }
|
|
415
471
|
}
|
|
416
|
-
rejectAll(error) {
|
|
472
|
+
rejectAll(error) {
|
|
473
|
+
for (const [id, pending] of this.pending) {
|
|
474
|
+
if (this.notificationError === error && pending.method === "session/prompt") {
|
|
475
|
+
this.rejectedHookRequests ??= new Map(); this.rejectedHookRequests.set(id, { method: pending.method, sessionId: pending.sessionId, error });
|
|
476
|
+
if (this.rejectedHookRequests.size > 32) this.rejectedHookRequests.delete(this.rejectedHookRequests.keys().next().value);
|
|
477
|
+
}
|
|
478
|
+
clearTimeout(pending.timer); if (pending.idleTimer) clearInterval(pending.idleTimer); pending.reject(error);
|
|
479
|
+
}
|
|
480
|
+
this.pending.clear();
|
|
481
|
+
}
|
|
417
482
|
async flush() {
|
|
483
|
+
if (this.notificationError) throw this.notificationError;
|
|
418
484
|
try { await Promise.all([...this.lifecycleNotifications]); await this.notifications; await this.journal.flush(); }
|
|
419
485
|
catch (error) { throw this.notificationError ?? error; }
|
|
420
486
|
if (this.notificationError) throw this.notificationError;
|
|
@@ -423,13 +489,12 @@ export class AcpBridge {
|
|
|
423
489
|
async close() {
|
|
424
490
|
// Productive notification failures belong to flush()/the original Turn.
|
|
425
491
|
// Draining them must not relabel a successful physical shutdown as failed.
|
|
426
|
-
await
|
|
427
|
-
|
|
428
|
-
this.
|
|
429
|
-
|
|
430
|
-
await
|
|
431
|
-
}
|
|
432
|
-
await this.journal.flush();
|
|
492
|
+
return await closeBridgeProcess(this, async () => {
|
|
493
|
+
await Promise.all([this.nativeDrain, this.diagnostics]);
|
|
494
|
+
await Promise.allSettled([...this.lifecycleNotifications, this.notifications]);
|
|
495
|
+
if (this.notificationError) await hookBudget(1_000, null).run(() => this.journal.flush(), "failure_drain");
|
|
496
|
+
else await this.journal.flush();
|
|
497
|
+
});
|
|
433
498
|
}
|
|
434
499
|
}
|
|
435
500
|
|
|
@@ -441,8 +506,8 @@ function isProductiveSessionNotification(message) {
|
|
|
441
506
|
// synthetic flag through the ACP wire contract.
|
|
442
507
|
if (update?.sessionUpdate === "heartbeat" || update?.type === "heartbeat") return false;
|
|
443
508
|
if (update?.sessionUpdate === "agent_thought_chunk" && typeof update?.messageId === "string" && update.messageId.startsWith("thinking_")) return false;
|
|
444
|
-
if ((update?.sessionUpdate
|
|
445
|
-
return
|
|
509
|
+
if (["agent_message_chunk", "agent_thought_chunk"].includes(update?.sessionUpdate)) return update.content?.type === "text" && typeof update.content.text === "string" && update.content.text.length > 0;
|
|
510
|
+
return ["agent_message_chunk", "agent_thought_chunk", "tool_call", "tool_call_update"].includes(update?.sessionUpdate);
|
|
446
511
|
}
|
|
447
512
|
|
|
448
513
|
// The qualified bridge encodes exactly providerId\\modelId; no other suffix or
|
|
@@ -474,16 +539,14 @@ async function resume(bridge, sessionId, cwd) {
|
|
|
474
539
|
/** Ended children are paginated by the native API (20 by default). A partial
|
|
475
540
|
* directory is never settlement evidence, including during teardown. */
|
|
476
541
|
export async function readZcodeSubagents(bridge, sessionId, timeoutMs = 30_000, retained = false) {
|
|
477
|
-
const
|
|
542
|
+
const clock = new ObservationClock({ timeoutMs });
|
|
478
543
|
const cursors = new Set(), ended = new Map();
|
|
479
544
|
let first, cursor;
|
|
480
545
|
for (;;) {
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
const response = await bridge.request(retained ? "zcode/session/retainedSubagents" : "zcode/session/subagents", { sessionId, ...(cursor ? { endedCursor: cursor } : {}) }, remaining);
|
|
546
|
+
if (clock.sample()) fail("ZCode child observation is unavailable", "operation_observation_lost");
|
|
547
|
+
const response = await bridge.request(retained ? "zcode/session/retainedSubagents" : "zcode/session/subagents", { sessionId, ...(cursor ? { endedCursor: cursor } : {}) }, timeoutMs);
|
|
484
548
|
if (retained && response?.sessionId !== sessionId) fail("Retained ZCode child directory has a foreign Session identity", "native_topology_incomplete");
|
|
485
549
|
const page = retained ? response.topology : response;
|
|
486
|
-
if (performance.now() >= deadline) fail("ZCode child observation deadline expired", "native_topology_incomplete");
|
|
487
550
|
if (!page || !Array.isArray(page.running)) fail("ZCode child directory is unavailable", "native_topology_incomplete");
|
|
488
551
|
if (!first) first = page;
|
|
489
552
|
if (!page.ended) {
|
|
@@ -505,6 +568,8 @@ export async function readZcodeSubagents(bridge, sessionId, timeoutMs = 30_000,
|
|
|
505
568
|
}
|
|
506
569
|
if (typeof cursor !== "string" || cursors.has(cursor) || !Number.isSafeInteger(first.revision)) fail("ZCode child pagination has no advancing, versioned cursor", "native_topology_incomplete");
|
|
507
570
|
cursors.add(cursor);
|
|
571
|
+
if (cursors.size > 1_024) fail("ZCode child pagination exceeds its bounded directory limit", "native_topology_incomplete");
|
|
572
|
+
clock.sample(cursor);
|
|
508
573
|
}
|
|
509
574
|
}
|
|
510
575
|
|
|
@@ -566,12 +631,11 @@ export function zcodeChildSessionIds(snapshot) {
|
|
|
566
631
|
// No resolve/read/subagents extension here: those may implicitly resume.
|
|
567
632
|
export async function observeClosedZcodeTree(bridge, root, retainedChildren = [], neverPromptedRootProof = null) {
|
|
568
633
|
const pending = new Set([root, ...retainedChildren]), residents = [], snapshots = [];
|
|
569
|
-
const
|
|
634
|
+
const clock = new ObservationClock({ timeoutMs: 30_000 });
|
|
570
635
|
try {
|
|
571
636
|
for (const sessionId of pending) {
|
|
572
|
-
if (pending.size > 1024 ||
|
|
573
|
-
const
|
|
574
|
-
const resident = await bridge.request("zcode/session/resident", { sessionId }, timeout());
|
|
637
|
+
if (pending.size > 1024 || clock.sample()) throw new Error("Closed tree observation limit exceeded");
|
|
638
|
+
const resident = await bridge.request("zcode/session/resident", { sessionId }, 5_000);
|
|
575
639
|
if (resident?.sessionId !== sessionId || typeof resident.resident !== "boolean") throw new Error("Invalid residency identity or state");
|
|
576
640
|
residents.push(resident);
|
|
577
641
|
// A newly allocated root is not durable in ZCode until its first prompt.
|
|
@@ -583,7 +647,7 @@ export async function observeClosedZcodeTree(bridge, root, retainedChildren = []
|
|
|
583
647
|
snapshots.push({ sessionId, childSessionIds: [], running: [], ended: {total:0,items:[]}, provenance: neverPromptedRootProof });
|
|
584
648
|
continue;
|
|
585
649
|
}
|
|
586
|
-
const tree = await readZcodeSubagents(bridge, sessionId,
|
|
650
|
+
const tree = await readZcodeSubagents(bridge, sessionId, 30_000, true);
|
|
587
651
|
if (!Array.isArray(tree?.childSessionIds)
|
|
588
652
|
|| tree.childSessionIds.some(id => typeof id !== "string" || !id || id === sessionId)
|
|
589
653
|
|| !Array.isArray(tree?.running) || !Array.isArray(tree?.ended?.items)
|
|
@@ -591,11 +655,12 @@ export async function observeClosedZcodeTree(bridge, root, retainedChildren = []
|
|
|
591
655
|
|| tree.ended.total > tree.childSessionIds.length
|
|
592
656
|
|| [...tree.running, ...tree.ended.items].some(child => !tree.childSessionIds.includes(child?.childSessionId))) throw new Error("Incomplete retained native topology");
|
|
593
657
|
snapshots.push({ sessionId, ...tree });
|
|
658
|
+
clock.sample(sessionId);
|
|
594
659
|
for (const child of tree.childSessionIds) pending.add(child);
|
|
595
660
|
}
|
|
596
661
|
return { topology: { root_session_id: root, child_session_ids: [...pending].filter(id => id !== root), complete: true }, residents, snapshots, settled: residents.every(item => item.resident === false) };
|
|
597
662
|
} catch (error) {
|
|
598
|
-
return { topology: { root_session_id: root, child_session_ids: [...pending].filter(id => id !== root), complete: false }, residents, snapshots, settled: false, observation_error:
|
|
663
|
+
return { topology: { root_session_id: root, child_session_ids: [...pending].filter(id => id !== root), complete: false }, residents, snapshots, settled: false, observation_error: errorRecord(error) };
|
|
599
664
|
}
|
|
600
665
|
}
|
|
601
666
|
|
|
@@ -610,7 +675,7 @@ async function cancelTree(bridge, sessionId, before, providerSessionId, retained
|
|
|
610
675
|
const taskId = child.taskId ?? child.agentId;
|
|
611
676
|
if (taskId) {
|
|
612
677
|
try { cancellations.push(await bridge.request("session/cancelBackgroundTask", { sessionId, taskId })); }
|
|
613
|
-
catch (error) { cancellations.push({ taskId, cancelled: false, error:
|
|
678
|
+
catch (error) { cancellations.push({ taskId, cancelled: false, error: errorRecord(error) }); }
|
|
614
679
|
}
|
|
615
680
|
}
|
|
616
681
|
// ZCode's stop is deliberately idempotent. A stop acknowledgement is not a
|
|
@@ -623,8 +688,8 @@ async function cancelTree(bridge, sessionId, before, providerSessionId, retained
|
|
|
623
688
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
624
689
|
const timeout = Math.max(1, deadline - Date.now());
|
|
625
690
|
[after, read] = await Promise.all([
|
|
626
|
-
readZcodeSubagents(bridge, sessionId, timeout).catch(error => ({ observation_error:
|
|
627
|
-
bridge.request("zcode/session/read", { sessionId }, timeout).catch(error => ({ observation_error:
|
|
691
|
+
readZcodeSubagents(bridge, sessionId, timeout).catch(error => ({ observation_error: errorRecord(error) })),
|
|
692
|
+
bridge.request("zcode/session/read", { sessionId }, timeout).catch(error => ({ observation_error: errorRecord(error) })),
|
|
628
693
|
]);
|
|
629
694
|
root_status = read?.projection?.status ?? read?.session?.status ?? null;
|
|
630
695
|
remember(after);
|
|
@@ -637,7 +702,7 @@ async function cancelTree(bridge, sessionId, before, providerSessionId, retained
|
|
|
637
702
|
}
|
|
638
703
|
let close = null;
|
|
639
704
|
try { close = await bridge.request("zcode/session/close", { sessionId }); }
|
|
640
|
-
catch (error) { close = { closed: false, error:
|
|
705
|
+
catch (error) { close = { closed: false, error: errorRecord(error) }; }
|
|
641
706
|
const observation = close?.closed === true ? await observeClosedZcodeTree(bridge, providerSessionId, [...knownChildren], neverPromptedRootProof ? {...neverPromptedRootProof,close_confirmed:true} : null)
|
|
642
707
|
: { topology: { root_session_id: providerSessionId, child_session_ids: [...knownChildren], complete: false }, settled: false };
|
|
643
708
|
return { cancellations, before, after, root_status, cancellation_requested: true, close_requested: true, close, ...observation };
|
|
@@ -721,9 +786,10 @@ function requireJournal(options) { absolute(options.journal, "--journal"); }
|
|
|
721
786
|
function receipt(options, value) { return options.daemonId ? { daemon_id: options.daemonId, ...value } : value; }
|
|
722
787
|
|
|
723
788
|
async function withBridge(options, operation) {
|
|
724
|
-
const bridge = new AcpBridge(options);
|
|
725
|
-
try { const initialized = await bridge.start();
|
|
726
|
-
|
|
789
|
+
const bridge = new AcpBridge(options); let primary, result;
|
|
790
|
+
try { const initialized = await bridge.start(); result = await operation(bridge, initialized); await bridge.flush(); return result; }
|
|
791
|
+
catch (error) { primary = result === undefined ? error : nativeObservationError(error, { result }); throw primary; }
|
|
792
|
+
finally { try { await bridge.close(); } catch (error) { if (!primary) throw nativeObservationError(error, result === undefined ? {} : { result }); primary.cleanup_error = errorRecord(error); primary.details = { ...primary.details, cleanup_error: primary.cleanup_error }; } }
|
|
727
793
|
}
|
|
728
794
|
|
|
729
795
|
function flowForwarder(options, rootProviderSessionId, bridge) {
|
|
@@ -745,16 +811,22 @@ function flowForwarder(options, rootProviderSessionId, bridge) {
|
|
|
745
811
|
export async function zcodeInvocationObserver(options, rootProviderSessionId, send = invokeNativeHook) {
|
|
746
812
|
if (!options.ddFlowBin) return async () => {};
|
|
747
813
|
const owners = new Map();
|
|
748
|
-
const remember = message => {
|
|
814
|
+
const remember = (message, mandatory = false) => {
|
|
749
815
|
const update = message.params?.update;
|
|
750
816
|
if (message.method !== "session/update" || update?.sessionUpdate !== "tool_call") return null;
|
|
751
817
|
if (options.adapterSessionId && message.params?.sessionId !== options.adapterSessionId) return null;
|
|
752
818
|
const native = update._meta?.zcodeRuntime ?? {};
|
|
753
819
|
const session = zcodePhysicalSession(update, rootProviderSessionId);
|
|
754
|
-
if (!session)
|
|
820
|
+
if (!session) {
|
|
821
|
+
if (mandatory) throw Object.assign(new Error("Native subagent event has no physical child identity"), { code: "zcode_child_identity_missing" });
|
|
822
|
+
return null;
|
|
823
|
+
}
|
|
755
824
|
const key = update.toolCallId;
|
|
756
825
|
if (!key || typeof session !== "string") return null;
|
|
757
|
-
if (owners.has(key) && owners.get(key) !== session)
|
|
826
|
+
if (owners.has(key) && owners.get(key) !== session) {
|
|
827
|
+
if (mandatory) throw Object.assign(new Error("Native tool id has conflicting Session ownership"), { code: "zcode_tool_identity_conflict" });
|
|
828
|
+
return null;
|
|
829
|
+
}
|
|
758
830
|
owners.set(key, session);
|
|
759
831
|
return { update, native, session };
|
|
760
832
|
};
|
|
@@ -767,21 +839,35 @@ export async function zcodeInvocationObserver(options, rootProviderSessionId, se
|
|
|
767
839
|
}
|
|
768
840
|
} catch (error) { if (error.code !== "ENOENT") throw error; }
|
|
769
841
|
}
|
|
842
|
+
// The same native Session can host several Turns. Retained tool IDs belong
|
|
843
|
+
// to earlier Turns; a delayed duplicate cannot reject a new prompt waiter.
|
|
844
|
+
const priorTools = new Set(owners.keys());
|
|
770
845
|
return async message => {
|
|
771
|
-
|
|
846
|
+
if (priorTools.has(message.params?.update?.toolCallId)) return;
|
|
847
|
+
const candidate = message.params?.update, command = candidate?.rawInput?.command ?? candidate?.rawInput?.cmd;
|
|
848
|
+
const shell = ["Bash", "bash", "execute"].includes(candidate?._meta?.claudeCode?.toolName) && typeof command === "string";
|
|
849
|
+
const analysis = shell ? parseLifecycleCommand(command) : null;
|
|
850
|
+
const mandatory = shell && (analysis.kind !== "none" || ["lifecycle", "auxiliary"].includes(analysis.participation) || continuationCommand(command));
|
|
851
|
+
const facts = remember(message, mandatory);
|
|
772
852
|
if (!facts || !options.ddFlowBin) return;
|
|
773
|
-
const {
|
|
774
|
-
|
|
775
|
-
const toolName = update._meta?.claudeCode?.toolName;
|
|
776
|
-
if (!["Bash", "bash", "execute"].includes(toolName) || typeof command !== "string") return;
|
|
777
|
-
const analysis = parseLifecycleCommand(command);
|
|
778
|
-
if (analysis.kind === "none" && !analysis.unresolved && !continuationCommand(command)) return;
|
|
853
|
+
const { native, session } = facts;
|
|
854
|
+
if (!mandatory) return;
|
|
779
855
|
const parent = session === rootProviderSessionId ? null : owners.get(native.parentToolCallId);
|
|
780
856
|
if (session !== rootProviderSessionId && !parent) throw Object.assign(new Error("Native child has no confirmed immediate parent tool owner"), { code: "zcode_parent_identity_missing" });
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
857
|
+
try {
|
|
858
|
+
await send({ bin: options.ddFlowBin, home: options.ddFlowHome,
|
|
859
|
+
args: ["zcode", "event", "handle", "--project-root", options.projectRoot ?? options.cwd, "--json"],
|
|
860
|
+
payload: { ...message, _meta: { ddZcode: { daemonId: options.daemonId, rootProviderSessionId, parentProviderSessionId: parent } } },
|
|
861
|
+
});
|
|
862
|
+
} catch (error) {
|
|
863
|
+
const details = error.details;
|
|
864
|
+
// Issued-ID commands cannot cross the CLI CAS without the receipt.
|
|
865
|
+
// This proof concerns lifecycle effects only, never shell-prefix replay.
|
|
866
|
+
if (analysis.kind !== "none" && commandOption(analysis.invocation, "invocation-id")
|
|
867
|
+
&& isProvenShellCorrection(error)
|
|
868
|
+
&& details.issued_invocation_id === commandOption(analysis.invocation, "invocation-id")) correctableLifecycleErrors.add(error);
|
|
869
|
+
throw error;
|
|
870
|
+
}
|
|
785
871
|
};
|
|
786
872
|
}
|
|
787
873
|
|
|
@@ -919,19 +1005,20 @@ export async function promptSessionWithBridge(bridge, options) {
|
|
|
919
1005
|
catch (error) { initial_usage_error = { code: error.code ?? "usage_observation_failed", message: error.message ?? String(error) }; }
|
|
920
1006
|
// This is the request budget. ACP silence is observed separately and must
|
|
921
1007
|
// never turn a long native child request into a fabricated provider failure.
|
|
922
|
-
const timeoutMs =
|
|
1008
|
+
const timeoutMs = null;
|
|
923
1009
|
const textCursor = bridge.assistantTextCursor(identity.adapterSessionId);
|
|
924
1010
|
options.assertDispatch?.();
|
|
1011
|
+
bridge.dispatchOperationId = options.operationId;
|
|
925
1012
|
const turn = await bridge.request("session/prompt", { sessionId: identity.adapterSessionId, prompt: [{ type: "text", text: prompt }] }, timeoutMs, {
|
|
926
1013
|
idleSessionId: identity.adapterSessionId,
|
|
927
|
-
idleTimeoutMs: options.livenessTimeoutMs ??
|
|
1014
|
+
idleTimeoutMs: options.livenessTimeoutMs ?? 600_000,
|
|
928
1015
|
inactivityMode: "diagnostic",
|
|
929
1016
|
});
|
|
930
1017
|
const stopReason = typeof turn?.stopReason === "string" ? turn.stopReason : null;
|
|
931
1018
|
const completionEvidence = turn?._meta?.zcodeCompletionEvidence;
|
|
932
1019
|
if (completionEvidence === "inferred" || completionEvidence === "failed") {
|
|
933
1020
|
let observationError = null;
|
|
934
|
-
try { await bridge.flush(); } catch (error) { observationError =
|
|
1021
|
+
try { await bridge.flush(); } catch (error) { observationError = errorRecord(error); }
|
|
935
1022
|
const error = new Error(`ZCode completion is ${completionEvidence}, not a confirmed successful native turn`);
|
|
936
1023
|
error.code = completionEvidence === "inferred" ? "native_outcome_unknown" : "zcode_turn_not_completed";
|
|
937
1024
|
error.retryable = false;
|
|
@@ -943,7 +1030,7 @@ export async function promptSessionWithBridge(bridge, options) {
|
|
|
943
1030
|
// collect live evidence first: ordinary inspection is allowed to recover
|
|
944
1031
|
// an evicted Session, which would undo the just-confirmed close.
|
|
945
1032
|
let observationError = null;
|
|
946
|
-
try { await bridge.flush(); } catch (error) { observationError =
|
|
1033
|
+
try { await bridge.flush(); } catch (error) { observationError = errorRecord(error); }
|
|
947
1034
|
const error = new Error(`ZCode ended the provider request with ${stopReason}, not end_turn`);
|
|
948
1035
|
error.code = stopReason === "cancelled" ? "zcode_turn_cancelled" : "zcode_turn_not_completed";
|
|
949
1036
|
error.details = { stop_reason: stopReason, provider_session_id: identity.providerSessionId, adapter_session_id: identity.adapterSessionId, ...(observationError ? { observation_error: observationError } : {}) };
|
|
@@ -955,7 +1042,8 @@ export async function promptSessionWithBridge(bridge, options) {
|
|
|
955
1042
|
const error = new Error("ZCode completed the native turn, but post-turn observation failed");
|
|
956
1043
|
error.code = "native_outcome_observation_failed";
|
|
957
1044
|
error.retryable = false;
|
|
958
|
-
error.
|
|
1045
|
+
error.cause = cause;
|
|
1046
|
+
error.details = { native_outcome: { status: "completed", stop_reason: stopReason ?? "end_turn", completion_evidence: completionEvidence ?? "native" }, provider_session_id: identity.providerSessionId, adapter_session_id: identity.adapterSessionId, observation_error: errorRecord(cause) };
|
|
959
1047
|
throw error;
|
|
960
1048
|
}
|
|
961
1049
|
evidence.tool_calls = bridge.toolSummary(identity.adapterSessionId);
|
|
@@ -988,7 +1076,7 @@ export async function cancelSession(options) {
|
|
|
988
1076
|
export async function cancelSessionWithBridge(bridge, options) {
|
|
989
1077
|
requireJournal(options);
|
|
990
1078
|
const identity = await controlledIdentity(bridge, options);
|
|
991
|
-
const before = await readZcodeSubagents(bridge, identity.adapterSessionId).catch(error => ({ observation_error:
|
|
1079
|
+
const before = await readZcodeSubagents(bridge, identity.adapterSessionId).catch(error => ({ observation_error: errorRecord(error) }));
|
|
992
1080
|
return receipt(options, { harness: "zcode-acp", provider_session_id: identity.providerSessionId, adapter_session_id: identity.adapterSessionId, ...await cancelTree(bridge, identity.adapterSessionId, before, identity.providerSessionId, options.knownChildSessionIds, options.neverPromptedRootProof) });
|
|
993
1081
|
}
|
|
994
1082
|
|