@deksden-com/dd-flow-cli 0.9.0-beta.124 → 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 +16 -0
- package/README.md +10 -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/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/daemon-observation.d.mts +2 -0
- package/dist/harness-runtime/lib/daemon-observation.mjs +163 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +60 -13
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +151 -72
- package/dist/harness-runtime/lib/dd-agy.mjs +11 -23
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +9 -14
- package/dist/harness-runtime/lib/dd-codex.mjs +252 -80
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +14 -19
- package/dist/harness-runtime/lib/dd-droid.mjs +117 -31
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +56 -30
- package/dist/harness-runtime/lib/dd-grok.mjs +15 -13
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +18 -27
- package/dist/harness-runtime/lib/dd-opencode.mjs +44 -14
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +56 -53
- package/dist/harness-runtime/lib/dd-zcode.mjs +200 -111
- 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 +10 -1
- package/dist/harness-runtime/lib/managed-daemon.mjs +228 -40
- 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/run-control-receipt.schema.json +19 -0
- package/dist/services/code-checks.js +79 -9
- package/dist/services/codex-capacity.js +117 -41
- package/dist/services/codex-hook-delivery.js +2 -2
- package/dist/services/compatibility-preflight.js +4 -1
- package/dist/services/controller-fanout.js +9 -76
- package/dist/services/engines.js +70 -20
- package/dist/services/eval-snapshots.js +179 -40
- package/dist/services/external-work-launch.js +13 -8
- package/dist/services/harness-adapter.js +175 -11
- 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 +103 -47
- package/dist/services/merge-server.js +58 -21
- package/dist/services/recovery-observation-budget.js +67 -7
- package/dist/services/recovery-snapshot-database.js +7 -4
- 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 +169 -46
- package/dist/services/run-controller-capture.js +14 -2
- package/dist/services/run-controller-process.js +11 -4
- package/dist/services/run-controller-state.js +68 -7
- package/dist/services/run-controller.js +166 -94
- package/dist/services/run-fork.js +8 -3
- package/dist/services/run-recovery-guard.js +75 -0
- package/dist/services/run-recovery-runtime.js +1 -56
- package/dist/services/run-recovery.js +17 -13
- package/dist/services/runs.js +39 -6
- 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/vnext-code-review.js +10 -9
- 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 +10 -9
- package/dist/services/vnext-plan-review.js +21 -17
- 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 +34 -4
- 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,15 +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 readline from "node:readline";
|
|
10
9
|
import { closeBridgeProcess } from "./managed-daemon.mjs";
|
|
11
|
-
import { errorRecord } from "./operation-errors.mjs";
|
|
10
|
+
import { errorRecord, isProvenShellCorrection } from "./operation-errors.mjs";
|
|
12
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";
|
|
13
15
|
|
|
14
16
|
import { readZcodeUsage } from "./zcode-usage.mjs";
|
|
15
17
|
const ZCODE_HARNESS_CONTRACT = "dd-zcode-harness@1";
|
|
@@ -18,11 +20,11 @@ export const supportedZcodeHarnessContract = contract => [ZCODE_HARNESS_CONTRACT
|
|
|
18
20
|
// request. Keep the original correlation slot briefly so a slow native
|
|
19
21
|
// response (especially cold-start/session creation) can still settle it.
|
|
20
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();
|
|
21
26
|
|
|
22
27
|
function fail(message, code) { const error = new Error(message); if (code) error.code = code; throw error; }
|
|
23
|
-
function fatalLifecycleError(error) {
|
|
24
|
-
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);
|
|
25
|
-
}
|
|
26
28
|
function absolute(value, label) { if (!value || !path.isAbsolute(value)) fail(`${label} must be an absolute path`); return path.resolve(value); }
|
|
27
29
|
function text(value, label) { if (typeof value !== "string" || !value.trim()) fail(`${label} is required`); return value.trim(); }
|
|
28
30
|
function now() { return new Date().toISOString(); }
|
|
@@ -39,27 +41,12 @@ function executable(command, args = []) {
|
|
|
39
41
|
|
|
40
42
|
async function commandOutput(command, args) {
|
|
41
43
|
const target = executable(command, args);
|
|
42
|
-
return await
|
|
43
|
-
const child = spawn(target.command, target.args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
44
|
-
let stdout = ""; let stderr = "";
|
|
45
|
-
child.stdout.setEncoding("utf8").on("data", (chunk) => (stdout += chunk));
|
|
46
|
-
child.stderr.setEncoding("utf8").on("data", (chunk) => (stderr += chunk));
|
|
47
|
-
child.on("error", reject);
|
|
48
|
-
child.on("close", (code) => code === 0 ? resolve(stdout.trim()) : reject(new Error(stderr.trim() || `${target.command} exited ${code}`)));
|
|
49
|
-
});
|
|
44
|
+
return await commandText(target.command, target.args, { phase: "native-version" });
|
|
50
45
|
}
|
|
51
46
|
|
|
52
47
|
async function runWithStdin(command, args, stdin, env = {}) {
|
|
53
48
|
const target = executable(command, args);
|
|
54
|
-
return await
|
|
55
|
-
const child = spawn(target.command, target.args, { env: { ...process.env, ...env }, stdio: ["pipe", "pipe", "pipe"] });
|
|
56
|
-
let stdout = ""; let stderr = "";
|
|
57
|
-
child.stdout.setEncoding("utf8").on("data", (chunk) => (stdout += chunk));
|
|
58
|
-
child.stderr.setEncoding("utf8").on("data", (chunk) => (stderr += chunk));
|
|
59
|
-
child.on("error", reject);
|
|
60
|
-
child.on("close", (code) => code === 0 ? resolve(stdout.trim()) : reject(new Error(stderr.trim() || `${target.command} exited ${code}`)));
|
|
61
|
-
child.stdin.end(stdin);
|
|
62
|
-
});
|
|
49
|
+
return await commandText(target.command, target.args, { input: stdin, env, phase: "usage-ingest" });
|
|
63
50
|
}
|
|
64
51
|
|
|
65
52
|
class Journal {
|
|
@@ -73,6 +60,7 @@ class Journal {
|
|
|
73
60
|
if (!this.file) return;
|
|
74
61
|
const line = JSON.stringify({ order: ++this.order, observed_at: now(), kind, payload });
|
|
75
62
|
this.pending = this.pending.then(() => appendFile(this.file, `${line}\n`));
|
|
63
|
+
void this.pending.catch(() => {});
|
|
76
64
|
return this.pending;
|
|
77
65
|
}
|
|
78
66
|
writeInbound(payload, method) {
|
|
@@ -110,6 +98,7 @@ class Journal {
|
|
|
110
98
|
}
|
|
111
99
|
await appendFile(this.file, `${line}\n`);
|
|
112
100
|
});
|
|
101
|
+
void this.pending.catch(() => {});
|
|
113
102
|
return this.pending;
|
|
114
103
|
}
|
|
115
104
|
async flush() { await this.pending; }
|
|
@@ -158,8 +147,11 @@ export class AcpBridge {
|
|
|
158
147
|
}
|
|
159
148
|
|
|
160
149
|
configure(options) { this.options = { ...this.options, ...options }; }
|
|
161
|
-
registerModelSession(adapterId, sessionId, requested, parentSessionId = null, armed = true) {
|
|
162
|
-
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 });
|
|
163
155
|
}
|
|
164
156
|
async recordModel(adapterId, observed, source) {
|
|
165
157
|
const identity = this.modelSessions.get(adapterId); if (!identity) return;
|
|
@@ -183,15 +175,15 @@ export class AcpBridge {
|
|
|
183
175
|
const target = executable(this.options.bin, this.options.commandArgs ?? ["server"]);
|
|
184
176
|
const cwd = this.options.cwd ? absolute(this.options.cwd, "--cwd") : process.cwd();
|
|
185
177
|
this.child = spawn(target.command, target.args, { cwd, detached: process.platform !== "win32", stdio: ["pipe", "pipe", "pipe"], env: { ...process.env, ...(this.options.env ?? {}) } });
|
|
186
|
-
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));
|
|
187
179
|
this.child.stdin?.on("error", (error) => {
|
|
188
180
|
error.details = { system_code: error.code ?? null };
|
|
189
181
|
error.code = "bridge_pipe_broken";
|
|
190
182
|
this.rejectAll(error);
|
|
191
183
|
});
|
|
192
184
|
this.child.on("error", (error) => { error.code ??= "bridge_exited"; this.rejectAll(error); });
|
|
193
|
-
this.child.on("
|
|
194
|
-
|
|
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));
|
|
195
187
|
if (this.options.onSpawn) await this.options.onSpawn(this.child);
|
|
196
188
|
return await this.request("initialize", {
|
|
197
189
|
protocolVersion: 1,
|
|
@@ -206,15 +198,23 @@ export class AcpBridge {
|
|
|
206
198
|
const request = message.id !== undefined ? this.pending.get(message.id) : null;
|
|
207
199
|
const expired = message.id !== undefined ? this.expiredRequests.get(message.id) : null;
|
|
208
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
|
+
}
|
|
209
209
|
if (!isResponse || request) this.journal.writeInbound(message, request?.method);
|
|
210
|
-
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);
|
|
211
211
|
const update = message.params?.update;
|
|
212
212
|
// ZCode emits this as `session/update`; Grok Build wraps the same ACP
|
|
213
213
|
// payload in `_x.ai/session_notification`. Preserve either provider's
|
|
214
214
|
// native error before its generic JSON-RPC terminal response arrives.
|
|
215
215
|
if (typeof message.params?.sessionId === "string" && update?.sessionUpdate === "retry_state" && update.type === "failed") {
|
|
216
216
|
for (const pending of this.pending.values()) {
|
|
217
|
-
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 };
|
|
218
218
|
}
|
|
219
219
|
}
|
|
220
220
|
if (message.method === "session/update" && message.params?.update?.sessionUpdate === "agent_message_chunk" && message.params.update.content?.type === "text") {
|
|
@@ -236,17 +236,21 @@ export class AcpBridge {
|
|
|
236
236
|
const pending = Promise.resolve().then(() => observer(message)).catch(async error => {
|
|
237
237
|
// A validated no-effect rejection is returned through the invocation
|
|
238
238
|
// ledger. It is evidence, not a poisoned ACP Session.
|
|
239
|
-
const fatal = fatalLifecycleError(error);
|
|
240
239
|
const current = this.options.onLifecycleNotification === observer;
|
|
241
|
-
|
|
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
|
+
}
|
|
242
245
|
// A missing/foreign authority is not a recoverable CLI validation
|
|
243
246
|
// result. Stop admitting productive requests immediately; the managed
|
|
244
247
|
// controller still owns physical stop/drain and evidence capture.
|
|
245
|
-
if (
|
|
248
|
+
if (current && !correctable) this.rejectAll(this.notificationError);
|
|
246
249
|
try {
|
|
247
|
-
await this.journal.write("notification_failure", {
|
|
248
|
-
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");
|
|
249
252
|
} catch (persistenceError) {
|
|
253
|
+
if (current) this.notificationError ??= persistenceError;
|
|
250
254
|
process.stderr.write(`${JSON.stringify({ code: error.code, message: error.message, persistence_error: persistenceError.message })}\n`);
|
|
251
255
|
}
|
|
252
256
|
});
|
|
@@ -276,6 +280,7 @@ export class AcpBridge {
|
|
|
276
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";
|
|
277
281
|
error.retryable = error.code === "provider_rate_limited";
|
|
278
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,
|
|
279
284
|
terminal_response: { method: pending.method, session_id: pending.sessionId, request_id: message.id },
|
|
280
285
|
...(data ? { native: data } : {}), ...(details ? { provider_details: details } : {}), ...(providerDetails ? { provider_update: provider } : {}) };
|
|
281
286
|
pending.reject(error);
|
|
@@ -300,6 +305,60 @@ export class AcpBridge {
|
|
|
300
305
|
this.toolObservations.add(acpToolObservation(message, { harness: this.options.observationHarness ?? "zcode-acp", rootSessionId }));
|
|
301
306
|
}
|
|
302
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
|
+
|
|
303
362
|
toolSummary(sessionId) {
|
|
304
363
|
return this.toolObservations.summary(this.modelSessions.get(sessionId)?.sessionId ?? sessionId);
|
|
305
364
|
}
|
|
@@ -330,7 +389,9 @@ export class AcpBridge {
|
|
|
330
389
|
}
|
|
331
390
|
|
|
332
391
|
request(method, params = {}, timeoutMs = 30_000, { idleSessionId = null, idleTimeoutMs = null, inactivityMode = "reject" } = {}) {
|
|
333
|
-
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);
|
|
334
395
|
if (this.inactivityError && ["session/new", "session/prompt", "session/fork"].includes(method)) return Promise.reject(this.inactivityError);
|
|
335
396
|
const id = this.nextId++;
|
|
336
397
|
return new Promise((resolve, reject) => {
|
|
@@ -354,39 +415,32 @@ export class AcpBridge {
|
|
|
354
415
|
this.expiredRequests.set(id, { method, sessionId: params.sessionId ?? null });
|
|
355
416
|
while (this.expiredRequests.size > 128) this.expiredRequests.delete(this.expiredRequests.keys().next().value);
|
|
356
417
|
finish(timeoutError);
|
|
357
|
-
}, LATE_RESPONSE_GRACE_MS);
|
|
358
|
-
}, timeoutMs)
|
|
418
|
+
}, LATE_RESPONSE_GRACE_MS, { progress: () => pending.progress });
|
|
419
|
+
}, timeoutMs, { progress: () => this.pending.get(id)?.progress })
|
|
359
420
|
: null;
|
|
360
421
|
const validIdleTimeout = Number.isFinite(idleTimeoutMs) && idleTimeoutMs > 0 ? idleTimeoutMs : null;
|
|
361
422
|
if (idleSessionId) this.sessionActivity.set(idleSessionId, Date.now());
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
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;
|
|
365
426
|
const checkIdle = async () => {
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
const
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
inactivityRecorded = true;
|
|
384
|
-
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() });
|
|
385
|
-
if (inactivityMode !== "diagnostic") {
|
|
386
|
-
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 } }));
|
|
387
|
-
return;
|
|
388
|
-
}
|
|
389
|
-
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; }
|
|
390
444
|
};
|
|
391
445
|
const idleTimer = validIdleTimeout ? setInterval(() => {
|
|
392
446
|
void checkIdle().catch((error) => {
|
|
@@ -397,8 +451,9 @@ export class AcpBridge {
|
|
|
397
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(() => {});
|
|
398
452
|
});
|
|
399
453
|
}, Math.max(10, Math.min(1_000, Math.floor(validIdleTimeout / 4)))) : null;
|
|
400
|
-
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() });
|
|
401
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 });
|
|
402
457
|
});
|
|
403
458
|
}
|
|
404
459
|
|
|
@@ -414,8 +469,18 @@ export class AcpBridge {
|
|
|
414
469
|
try { stdin.write(`${JSON.stringify(message)}\n`); }
|
|
415
470
|
catch (error) { error.details = { system_code: error.code ?? null }; error.code = "bridge_pipe_broken"; this.rejectAll(error); throw error; }
|
|
416
471
|
}
|
|
417
|
-
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
|
+
}
|
|
418
482
|
async flush() {
|
|
483
|
+
if (this.notificationError) throw this.notificationError;
|
|
419
484
|
try { await Promise.all([...this.lifecycleNotifications]); await this.notifications; await this.journal.flush(); }
|
|
420
485
|
catch (error) { throw this.notificationError ?? error; }
|
|
421
486
|
if (this.notificationError) throw this.notificationError;
|
|
@@ -425,8 +490,10 @@ export class AcpBridge {
|
|
|
425
490
|
// Productive notification failures belong to flush()/the original Turn.
|
|
426
491
|
// Draining them must not relabel a successful physical shutdown as failed.
|
|
427
492
|
return await closeBridgeProcess(this, async () => {
|
|
493
|
+
await Promise.all([this.nativeDrain, this.diagnostics]);
|
|
428
494
|
await Promise.allSettled([...this.lifecycleNotifications, this.notifications]);
|
|
429
|
-
await this.journal.flush();
|
|
495
|
+
if (this.notificationError) await hookBudget(1_000, null).run(() => this.journal.flush(), "failure_drain");
|
|
496
|
+
else await this.journal.flush();
|
|
430
497
|
});
|
|
431
498
|
}
|
|
432
499
|
}
|
|
@@ -439,8 +506,8 @@ function isProductiveSessionNotification(message) {
|
|
|
439
506
|
// synthetic flag through the ACP wire contract.
|
|
440
507
|
if (update?.sessionUpdate === "heartbeat" || update?.type === "heartbeat") return false;
|
|
441
508
|
if (update?.sessionUpdate === "agent_thought_chunk" && typeof update?.messageId === "string" && update.messageId.startsWith("thinking_")) return false;
|
|
442
|
-
if ((update?.sessionUpdate
|
|
443
|
-
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);
|
|
444
511
|
}
|
|
445
512
|
|
|
446
513
|
// The qualified bridge encodes exactly providerId\\modelId; no other suffix or
|
|
@@ -472,16 +539,14 @@ async function resume(bridge, sessionId, cwd) {
|
|
|
472
539
|
/** Ended children are paginated by the native API (20 by default). A partial
|
|
473
540
|
* directory is never settlement evidence, including during teardown. */
|
|
474
541
|
export async function readZcodeSubagents(bridge, sessionId, timeoutMs = 30_000, retained = false) {
|
|
475
|
-
const
|
|
542
|
+
const clock = new ObservationClock({ timeoutMs });
|
|
476
543
|
const cursors = new Set(), ended = new Map();
|
|
477
544
|
let first, cursor;
|
|
478
545
|
for (;;) {
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
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);
|
|
482
548
|
if (retained && response?.sessionId !== sessionId) fail("Retained ZCode child directory has a foreign Session identity", "native_topology_incomplete");
|
|
483
549
|
const page = retained ? response.topology : response;
|
|
484
|
-
if (performance.now() >= deadline) fail("ZCode child observation deadline expired", "native_topology_incomplete");
|
|
485
550
|
if (!page || !Array.isArray(page.running)) fail("ZCode child directory is unavailable", "native_topology_incomplete");
|
|
486
551
|
if (!first) first = page;
|
|
487
552
|
if (!page.ended) {
|
|
@@ -503,6 +568,8 @@ export async function readZcodeSubagents(bridge, sessionId, timeoutMs = 30_000,
|
|
|
503
568
|
}
|
|
504
569
|
if (typeof cursor !== "string" || cursors.has(cursor) || !Number.isSafeInteger(first.revision)) fail("ZCode child pagination has no advancing, versioned cursor", "native_topology_incomplete");
|
|
505
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);
|
|
506
573
|
}
|
|
507
574
|
}
|
|
508
575
|
|
|
@@ -564,12 +631,11 @@ export function zcodeChildSessionIds(snapshot) {
|
|
|
564
631
|
// No resolve/read/subagents extension here: those may implicitly resume.
|
|
565
632
|
export async function observeClosedZcodeTree(bridge, root, retainedChildren = [], neverPromptedRootProof = null) {
|
|
566
633
|
const pending = new Set([root, ...retainedChildren]), residents = [], snapshots = [];
|
|
567
|
-
const
|
|
634
|
+
const clock = new ObservationClock({ timeoutMs: 30_000 });
|
|
568
635
|
try {
|
|
569
636
|
for (const sessionId of pending) {
|
|
570
|
-
if (pending.size > 1024 ||
|
|
571
|
-
const
|
|
572
|
-
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);
|
|
573
639
|
if (resident?.sessionId !== sessionId || typeof resident.resident !== "boolean") throw new Error("Invalid residency identity or state");
|
|
574
640
|
residents.push(resident);
|
|
575
641
|
// A newly allocated root is not durable in ZCode until its first prompt.
|
|
@@ -581,7 +647,7 @@ export async function observeClosedZcodeTree(bridge, root, retainedChildren = []
|
|
|
581
647
|
snapshots.push({ sessionId, childSessionIds: [], running: [], ended: {total:0,items:[]}, provenance: neverPromptedRootProof });
|
|
582
648
|
continue;
|
|
583
649
|
}
|
|
584
|
-
const tree = await readZcodeSubagents(bridge, sessionId,
|
|
650
|
+
const tree = await readZcodeSubagents(bridge, sessionId, 30_000, true);
|
|
585
651
|
if (!Array.isArray(tree?.childSessionIds)
|
|
586
652
|
|| tree.childSessionIds.some(id => typeof id !== "string" || !id || id === sessionId)
|
|
587
653
|
|| !Array.isArray(tree?.running) || !Array.isArray(tree?.ended?.items)
|
|
@@ -589,11 +655,12 @@ export async function observeClosedZcodeTree(bridge, root, retainedChildren = []
|
|
|
589
655
|
|| tree.ended.total > tree.childSessionIds.length
|
|
590
656
|
|| [...tree.running, ...tree.ended.items].some(child => !tree.childSessionIds.includes(child?.childSessionId))) throw new Error("Incomplete retained native topology");
|
|
591
657
|
snapshots.push({ sessionId, ...tree });
|
|
658
|
+
clock.sample(sessionId);
|
|
592
659
|
for (const child of tree.childSessionIds) pending.add(child);
|
|
593
660
|
}
|
|
594
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) };
|
|
595
662
|
} catch (error) {
|
|
596
|
-
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) };
|
|
597
664
|
}
|
|
598
665
|
}
|
|
599
666
|
|
|
@@ -608,7 +675,7 @@ async function cancelTree(bridge, sessionId, before, providerSessionId, retained
|
|
|
608
675
|
const taskId = child.taskId ?? child.agentId;
|
|
609
676
|
if (taskId) {
|
|
610
677
|
try { cancellations.push(await bridge.request("session/cancelBackgroundTask", { sessionId, taskId })); }
|
|
611
|
-
catch (error) { cancellations.push({ taskId, cancelled: false, error:
|
|
678
|
+
catch (error) { cancellations.push({ taskId, cancelled: false, error: errorRecord(error) }); }
|
|
612
679
|
}
|
|
613
680
|
}
|
|
614
681
|
// ZCode's stop is deliberately idempotent. A stop acknowledgement is not a
|
|
@@ -621,8 +688,8 @@ async function cancelTree(bridge, sessionId, before, providerSessionId, retained
|
|
|
621
688
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
622
689
|
const timeout = Math.max(1, deadline - Date.now());
|
|
623
690
|
[after, read] = await Promise.all([
|
|
624
|
-
readZcodeSubagents(bridge, sessionId, timeout).catch(error => ({ observation_error:
|
|
625
|
-
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) })),
|
|
626
693
|
]);
|
|
627
694
|
root_status = read?.projection?.status ?? read?.session?.status ?? null;
|
|
628
695
|
remember(after);
|
|
@@ -635,7 +702,7 @@ async function cancelTree(bridge, sessionId, before, providerSessionId, retained
|
|
|
635
702
|
}
|
|
636
703
|
let close = null;
|
|
637
704
|
try { close = await bridge.request("zcode/session/close", { sessionId }); }
|
|
638
|
-
catch (error) { close = { closed: false, error:
|
|
705
|
+
catch (error) { close = { closed: false, error: errorRecord(error) }; }
|
|
639
706
|
const observation = close?.closed === true ? await observeClosedZcodeTree(bridge, providerSessionId, [...knownChildren], neverPromptedRootProof ? {...neverPromptedRootProof,close_confirmed:true} : null)
|
|
640
707
|
: { topology: { root_session_id: providerSessionId, child_session_ids: [...knownChildren], complete: false }, settled: false };
|
|
641
708
|
return { cancellations, before, after, root_status, cancellation_requested: true, close_requested: true, close, ...observation };
|
|
@@ -719,10 +786,10 @@ function requireJournal(options) { absolute(options.journal, "--journal"); }
|
|
|
719
786
|
function receipt(options, value) { return options.daemonId ? { daemon_id: options.daemonId, ...value } : value; }
|
|
720
787
|
|
|
721
788
|
async function withBridge(options, operation) {
|
|
722
|
-
const bridge = new AcpBridge(options); let primary;
|
|
723
|
-
try { const initialized = await bridge.start();
|
|
724
|
-
catch (error) { primary = error; throw
|
|
725
|
-
finally { try { await bridge.close(); } catch (error) { if (!primary) throw error; primary.cleanup_error = errorRecord(error); primary.details = { ...primary.details, cleanup_error: primary.cleanup_error }; } }
|
|
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 }; } }
|
|
726
793
|
}
|
|
727
794
|
|
|
728
795
|
function flowForwarder(options, rootProviderSessionId, bridge) {
|
|
@@ -744,16 +811,22 @@ function flowForwarder(options, rootProviderSessionId, bridge) {
|
|
|
744
811
|
export async function zcodeInvocationObserver(options, rootProviderSessionId, send = invokeNativeHook) {
|
|
745
812
|
if (!options.ddFlowBin) return async () => {};
|
|
746
813
|
const owners = new Map();
|
|
747
|
-
const remember = message => {
|
|
814
|
+
const remember = (message, mandatory = false) => {
|
|
748
815
|
const update = message.params?.update;
|
|
749
816
|
if (message.method !== "session/update" || update?.sessionUpdate !== "tool_call") return null;
|
|
750
817
|
if (options.adapterSessionId && message.params?.sessionId !== options.adapterSessionId) return null;
|
|
751
818
|
const native = update._meta?.zcodeRuntime ?? {};
|
|
752
819
|
const session = zcodePhysicalSession(update, rootProviderSessionId);
|
|
753
|
-
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
|
+
}
|
|
754
824
|
const key = update.toolCallId;
|
|
755
825
|
if (!key || typeof session !== "string") return null;
|
|
756
|
-
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
|
+
}
|
|
757
830
|
owners.set(key, session);
|
|
758
831
|
return { update, native, session };
|
|
759
832
|
};
|
|
@@ -766,21 +839,35 @@ export async function zcodeInvocationObserver(options, rootProviderSessionId, se
|
|
|
766
839
|
}
|
|
767
840
|
} catch (error) { if (error.code !== "ENOENT") throw error; }
|
|
768
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());
|
|
769
845
|
return async message => {
|
|
770
|
-
|
|
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);
|
|
771
852
|
if (!facts || !options.ddFlowBin) return;
|
|
772
|
-
const {
|
|
773
|
-
|
|
774
|
-
const toolName = update._meta?.claudeCode?.toolName;
|
|
775
|
-
if (!["Bash", "bash", "execute"].includes(toolName) || typeof command !== "string") return;
|
|
776
|
-
const analysis = parseLifecycleCommand(command);
|
|
777
|
-
if (analysis.kind === "none" && !analysis.unresolved && !continuationCommand(command)) return;
|
|
853
|
+
const { native, session } = facts;
|
|
854
|
+
if (!mandatory) return;
|
|
778
855
|
const parent = session === rootProviderSessionId ? null : owners.get(native.parentToolCallId);
|
|
779
856
|
if (session !== rootProviderSessionId && !parent) throw Object.assign(new Error("Native child has no confirmed immediate parent tool owner"), { code: "zcode_parent_identity_missing" });
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
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
|
+
}
|
|
784
871
|
};
|
|
785
872
|
}
|
|
786
873
|
|
|
@@ -918,19 +1005,20 @@ export async function promptSessionWithBridge(bridge, options) {
|
|
|
918
1005
|
catch (error) { initial_usage_error = { code: error.code ?? "usage_observation_failed", message: error.message ?? String(error) }; }
|
|
919
1006
|
// This is the request budget. ACP silence is observed separately and must
|
|
920
1007
|
// never turn a long native child request into a fabricated provider failure.
|
|
921
|
-
const timeoutMs =
|
|
1008
|
+
const timeoutMs = null;
|
|
922
1009
|
const textCursor = bridge.assistantTextCursor(identity.adapterSessionId);
|
|
923
1010
|
options.assertDispatch?.();
|
|
1011
|
+
bridge.dispatchOperationId = options.operationId;
|
|
924
1012
|
const turn = await bridge.request("session/prompt", { sessionId: identity.adapterSessionId, prompt: [{ type: "text", text: prompt }] }, timeoutMs, {
|
|
925
1013
|
idleSessionId: identity.adapterSessionId,
|
|
926
|
-
idleTimeoutMs: options.livenessTimeoutMs ??
|
|
1014
|
+
idleTimeoutMs: options.livenessTimeoutMs ?? 600_000,
|
|
927
1015
|
inactivityMode: "diagnostic",
|
|
928
1016
|
});
|
|
929
1017
|
const stopReason = typeof turn?.stopReason === "string" ? turn.stopReason : null;
|
|
930
1018
|
const completionEvidence = turn?._meta?.zcodeCompletionEvidence;
|
|
931
1019
|
if (completionEvidence === "inferred" || completionEvidence === "failed") {
|
|
932
1020
|
let observationError = null;
|
|
933
|
-
try { await bridge.flush(); } catch (error) { observationError =
|
|
1021
|
+
try { await bridge.flush(); } catch (error) { observationError = errorRecord(error); }
|
|
934
1022
|
const error = new Error(`ZCode completion is ${completionEvidence}, not a confirmed successful native turn`);
|
|
935
1023
|
error.code = completionEvidence === "inferred" ? "native_outcome_unknown" : "zcode_turn_not_completed";
|
|
936
1024
|
error.retryable = false;
|
|
@@ -942,7 +1030,7 @@ export async function promptSessionWithBridge(bridge, options) {
|
|
|
942
1030
|
// collect live evidence first: ordinary inspection is allowed to recover
|
|
943
1031
|
// an evicted Session, which would undo the just-confirmed close.
|
|
944
1032
|
let observationError = null;
|
|
945
|
-
try { await bridge.flush(); } catch (error) { observationError =
|
|
1033
|
+
try { await bridge.flush(); } catch (error) { observationError = errorRecord(error); }
|
|
946
1034
|
const error = new Error(`ZCode ended the provider request with ${stopReason}, not end_turn`);
|
|
947
1035
|
error.code = stopReason === "cancelled" ? "zcode_turn_cancelled" : "zcode_turn_not_completed";
|
|
948
1036
|
error.details = { stop_reason: stopReason, provider_session_id: identity.providerSessionId, adapter_session_id: identity.adapterSessionId, ...(observationError ? { observation_error: observationError } : {}) };
|
|
@@ -954,7 +1042,8 @@ export async function promptSessionWithBridge(bridge, options) {
|
|
|
954
1042
|
const error = new Error("ZCode completed the native turn, but post-turn observation failed");
|
|
955
1043
|
error.code = "native_outcome_observation_failed";
|
|
956
1044
|
error.retryable = false;
|
|
957
|
-
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) };
|
|
958
1047
|
throw error;
|
|
959
1048
|
}
|
|
960
1049
|
evidence.tool_calls = bridge.toolSummary(identity.adapterSessionId);
|
|
@@ -987,7 +1076,7 @@ export async function cancelSession(options) {
|
|
|
987
1076
|
export async function cancelSessionWithBridge(bridge, options) {
|
|
988
1077
|
requireJournal(options);
|
|
989
1078
|
const identity = await controlledIdentity(bridge, options);
|
|
990
|
-
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) }));
|
|
991
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) });
|
|
992
1081
|
}
|
|
993
1082
|
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export const RENEWAL_POLICY: Readonly<{ budgetMs: number; attemptMs: number }>;
|
|
2
|
+
export type RenewalState = { uncertainSince: number | null; attempt: number; exhausted: boolean };
|
|
3
|
+
export function createRenewalState(): RenewalState;
|
|
4
|
+
export function renewalRemaining(state: RenewalState, now?: number): number;
|
|
5
|
+
export function renewalFailure(state: RenewalState, now?: number): number;
|
|
6
|
+
export function renewalDelay(state: RenewalState): number;
|
|
7
|
+
export function renewalConfirmed(state: RenewalState, now?: number): boolean;
|
|
8
|
+
export function maintenanceRetryable(error: unknown): boolean;
|