@deksden-com/dd-flow-cli 0.9.0-beta.64 → 0.9.0-beta.74
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/dist/build-info.json +3 -3
- package/dist/cli/help.js +2 -0
- package/dist/cli/run-cli.js +15 -0
- package/dist/harness-runtime/bin/dd-agy.mjs +4 -2
- package/dist/harness-runtime/bin/dd-codex.mjs +4 -2
- package/dist/harness-runtime/bin/dd-droid.mjs +4 -2
- package/dist/harness-runtime/bin/dd-grok.mjs +4 -2
- package/dist/harness-runtime/bin/dd-opencode.mjs +4 -2
- package/dist/harness-runtime/bin/dd-zcode.mjs +15 -7
- package/dist/harness-runtime/lib/daemon-operations.mjs +108 -10
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +5 -4
- package/dist/harness-runtime/lib/dd-codex.mjs +38 -4
- package/dist/harness-runtime/lib/dd-droid.mjs +55 -20
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +3 -2
- package/dist/harness-runtime/lib/dd-grok.mjs +14 -8
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +43 -8
- package/dist/harness-runtime/lib/dd-opencode.mjs +5 -4
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +182 -16
- package/dist/harness-runtime/lib/dd-zcode.mjs +162 -63
- package/dist/harness-runtime/lib/driver-recovery.mjs +2 -2
- package/dist/harness-runtime/lib/managed-daemon.mjs +27 -5
- package/dist/harness-runtime/lib/operation-errors.mjs +1 -1
- package/dist/harness-runtime/lib/tool-observations.d.mts +17 -0
- package/dist/harness-runtime/lib/tool-observations.mjs +153 -0
- package/dist/services/code-checks.js +120 -19
- package/dist/services/controller-fanout.js +11 -0
- package/dist/services/engines.js +40 -9
- package/dist/services/eval-snapshots.js +30 -47
- package/dist/services/harness-adapter.js +12 -6
- package/dist/services/lifecycle-invocations.js +12 -8
- package/dist/services/native-session-control.js +6 -0
- package/dist/services/projects.js +6 -2
- package/dist/services/recovery-observation-budget.js +9 -1
- package/dist/services/run-control-worker.js +19 -0
- package/dist/services/run-control.js +87 -13
- package/dist/services/run-controller-adapter.js +134 -7
- package/dist/services/run-controller.js +11 -3
- package/dist/services/run-engine-bindings.js +1 -1
- package/dist/services/run-fork.js +124 -0
- package/dist/services/run-observations.js +112 -0
- package/dist/services/runs.js +1 -11
- package/dist/services/schema-validation.js +1 -1
- package/dist/services/stage-lifecycle.js +14 -2
- package/dist/services/usage.js +20 -87
- package/dist/services/vnext-code-review.js +35 -27
- package/dist/services/vnext-code.js +62 -26
- package/dist/services/vnext-merge.js +8 -8
- package/dist/services/vnext-plan-review.js +16 -16
- package/dist/services/vnext-plan.js +51 -11
- package/dist/services/work-registry.js +48 -27
- package/dist/shared/errors.js +12 -0
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { observedTimeout } from "./observation-clock.mjs";
|
|
2
|
+
import { ToolObservations, acpToolObservation, zcodePhysicalSession } from "./tool-observations.mjs";
|
|
2
3
|
import { invokeNativeHook } from "./native-hook-command.mjs";
|
|
3
4
|
import { spawn } from "node:child_process";
|
|
4
5
|
import { createHash } from "node:crypto";
|
|
@@ -9,6 +10,10 @@ import { stopProcessGroup } from "./managed-daemon.mjs";
|
|
|
9
10
|
import { checkObservedProfile, observeModel, observedFields } from "./model-observations.mjs";
|
|
10
11
|
|
|
11
12
|
const ZCODE_HARNESS_CONTRACT = "dd-zcode-harness@1";
|
|
13
|
+
// A deadline is an observation boundary, not permission to replay an ACP
|
|
14
|
+
// request. Keep the original correlation slot briefly so a slow native
|
|
15
|
+
// response (especially cold-start/session creation) can still settle it.
|
|
16
|
+
const LATE_RESPONSE_GRACE_MS = 5_000;
|
|
12
17
|
|
|
13
18
|
function fail(message, code) { const error = new Error(message); if (code) error.code = code; throw error; }
|
|
14
19
|
function fatalLifecycleError(error) {
|
|
@@ -17,6 +22,11 @@ function fatalLifecycleError(error) {
|
|
|
17
22
|
function absolute(value, label) { if (!value || !path.isAbsolute(value)) fail(`${label} must be an absolute path`); return path.resolve(value); }
|
|
18
23
|
function text(value, label) { if (typeof value !== "string" || !value.trim()) fail(`${label} is required`); return value.trim(); }
|
|
19
24
|
function now() { return new Date().toISOString(); }
|
|
25
|
+
function responseSummary(message, method = null) {
|
|
26
|
+
const result = message?.result;
|
|
27
|
+
return { request_id: message?.id ?? null, method, has_result: result !== undefined,
|
|
28
|
+
result_keys: result && typeof result === "object" && !Array.isArray(result) ? Object.keys(result).slice(0, 32) : [], error: message?.error ?? null };
|
|
29
|
+
}
|
|
20
30
|
|
|
21
31
|
function executable(command, args = []) {
|
|
22
32
|
const resolved = command ?? process.env.DD_ZCODE_ACP_BIN ?? "zcode-acp";
|
|
@@ -129,13 +139,15 @@ export class AcpBridge {
|
|
|
129
139
|
this.options = options;
|
|
130
140
|
this.nextId = 1;
|
|
131
141
|
this.pending = new Map();
|
|
142
|
+
this.allocationObservers = new Map();
|
|
143
|
+
this.lateResponses = new Map();
|
|
144
|
+
this.expiredRequests = new Map();
|
|
132
145
|
this.sessionActivity = new Map();
|
|
133
146
|
this.notifications = Promise.resolve();
|
|
134
147
|
this.lifecycleNotifications = new Set();
|
|
135
148
|
this.notificationError = null;
|
|
136
149
|
this.bridgeStderr = "";
|
|
137
|
-
this.
|
|
138
|
-
this.failedToolCalls = new Set();
|
|
150
|
+
this.toolObservations = new ToolObservations();
|
|
139
151
|
this.assistantText = new Map();
|
|
140
152
|
this.journal = new Journal(options.journal);
|
|
141
153
|
this.modelSessions = new Map(); this.modelProfiles = new Map();
|
|
@@ -166,6 +178,11 @@ export class AcpBridge {
|
|
|
166
178
|
const cwd = this.options.cwd ? absolute(this.options.cwd, "--cwd") : process.cwd();
|
|
167
179
|
this.child = spawn(target.command, target.args, { cwd, detached: process.platform !== "win32", stdio: ["pipe", "pipe", "pipe"], env: { ...process.env, ...(this.options.env ?? {}) } });
|
|
168
180
|
this.child.stderr.setEncoding("utf8").on("data", (chunk) => { this.bridgeStderr += String(chunk); this.journal.write("bridge_stderr", { text: String(chunk) }); });
|
|
181
|
+
this.child.stdin?.on("error", (error) => {
|
|
182
|
+
error.details = { system_code: error.code ?? null };
|
|
183
|
+
error.code = "bridge_pipe_broken";
|
|
184
|
+
this.rejectAll(error);
|
|
185
|
+
});
|
|
169
186
|
this.child.on("error", (error) => { error.code ??= "bridge_exited"; this.rejectAll(error); });
|
|
170
187
|
this.child.on("exit", (code) => { const error = new Error(`zcode-acp exited ${code}${this.bridgeStderr.trim() ? `: ${this.bridgeStderr.trim()}` : ""}`); error.code = "bridge_exited"; this.rejectAll(error); });
|
|
171
188
|
readline.createInterface({ input: this.child.stdout }).on("line", (line) => this.receive(line));
|
|
@@ -180,7 +197,9 @@ export class AcpBridge {
|
|
|
180
197
|
let message;
|
|
181
198
|
try { message = JSON.parse(line); } catch { this.journal.write("malformed", { line }); return; }
|
|
182
199
|
const request = message.id !== undefined ? this.pending.get(message.id) : null;
|
|
183
|
-
this.
|
|
200
|
+
const expired = message.id !== undefined ? this.expiredRequests.get(message.id) : null;
|
|
201
|
+
const isResponse = message.id !== undefined && !message.method;
|
|
202
|
+
if (!isResponse || request) this.journal.writeInbound(message, request?.method);
|
|
184
203
|
if (typeof message.params?.sessionId === "string" && isProductiveSessionNotification(message)) this.sessionActivity.set(message.params.sessionId, Date.now());
|
|
185
204
|
const update = message.params?.update;
|
|
186
205
|
// ZCode emits this as `session/update`; Grok Build wraps the same ACP
|
|
@@ -195,6 +214,15 @@ export class AcpBridge {
|
|
|
195
214
|
const sessionId = message.params.sessionId;
|
|
196
215
|
this.assistantText.set(sessionId, `${this.assistantText.get(sessionId) ?? ""}${message.params.update.content.text ?? ""}`);
|
|
197
216
|
}
|
|
217
|
+
if (message.method === "zcode/session/allocated") {
|
|
218
|
+
const identity = message.params;
|
|
219
|
+
const observer = this.allocationObservers.get(identity?.adapter_session_id);
|
|
220
|
+
if (observer && typeof identity?.provider_session_id === "string" && identity.provider_session_id.length > 0) {
|
|
221
|
+
this.allocationObservers.delete(identity.adapter_session_id);
|
|
222
|
+
this.notifications = this.notifications.then(() => observer(identity));
|
|
223
|
+
void this.notifications.catch(error => { this.notificationError ??= error; });
|
|
224
|
+
}
|
|
225
|
+
}
|
|
198
226
|
this.observeToolCall(message);
|
|
199
227
|
if (message.method === "session/update" && this.options.onLifecycleNotification) {
|
|
200
228
|
const observer = this.options.onLifecycleNotification;
|
|
@@ -220,17 +248,26 @@ export class AcpBridge {
|
|
|
220
248
|
if (message.id !== undefined && message.method) { void this.answer(message).catch((error) => this.rejectAll(error)); return; }
|
|
221
249
|
if (message.id !== undefined) {
|
|
222
250
|
const pending = request ?? this.pending.get(message.id);
|
|
223
|
-
if (!pending)
|
|
251
|
+
if (!pending) {
|
|
252
|
+
this.lateResponses.set(message.id, message);
|
|
253
|
+
while (this.lateResponses.size > 128) this.lateResponses.delete(this.lateResponses.keys().next().value);
|
|
254
|
+
this.journal.write("late_response", responseSummary(message, expired?.method ?? null));
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
224
257
|
clearTimeout(pending.timer); if (pending.idleTimer) clearInterval(pending.idleTimer); this.pending.delete(message.id);
|
|
258
|
+
if (pending.timedOut) this.journal.write("late_response", { ...responseSummary(message, pending.method), grace_ms: LATE_RESPONSE_GRACE_MS });
|
|
225
259
|
if (message.error) {
|
|
226
|
-
const
|
|
260
|
+
const data = message.error.data && typeof message.error.data === "object" && !Array.isArray(message.error.data) ? message.error.data : null;
|
|
261
|
+
const details = data?.details ?? data?.message ?? data ?? null;
|
|
227
262
|
const provider = pending.providerError;
|
|
228
263
|
const providerDetails = provider?.message ?? null;
|
|
229
|
-
const messageText = [message.error.message, details, providerDetails].filter(Boolean).join(": ") || JSON.stringify(message.error);
|
|
264
|
+
const messageText = [message.error.message, details, providerDetails].filter(Boolean).map(value => typeof value === "string" ? value : JSON.stringify(value)).join(": ") || JSON.stringify(message.error);
|
|
230
265
|
const error = new Error(messageText);
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
error.
|
|
266
|
+
const nativeCode = typeof data?.code === "string" ? data.code : null;
|
|
267
|
+
const unknown = ["native_timeout", "native_backend_dead", "native_backend_pipe_broken", "native_outcome_unknown"].includes(nativeCode);
|
|
268
|
+
error.code = unknown ? nativeCode : /rate limit/i.test(messageText) ? "provider_rate_limited" : /payment required|usage balance exhausted/i.test(messageText) ? "provider_quota_exhausted" : "acp_request_failed";
|
|
269
|
+
error.retryable = !unknown && /rate limit/i.test(messageText);
|
|
270
|
+
error.details = { acp_code: message.error.code ?? null, ...(data ? { native: data } : {}), ...(details ? { provider_details: details } : {}), ...(providerDetails ? { provider_update: provider } : {}) };
|
|
234
271
|
pending.reject(error);
|
|
235
272
|
}
|
|
236
273
|
else { pending.resolve(message.result ?? {}); }
|
|
@@ -249,35 +286,18 @@ export class AcpBridge {
|
|
|
249
286
|
}
|
|
250
287
|
|
|
251
288
|
observeToolCall(message) {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
if (!update?.toolCallId) return;
|
|
255
|
-
const id = JSON.stringify([message.params?.sessionId ?? null, update.toolCallId]);
|
|
256
|
-
if (update.sessionUpdate === "tool_call") this.toolCalls.set(id, update?._meta?.claudeCode?.toolName ?? update.title?.split(":", 1)[0] ?? "unknown");
|
|
257
|
-
if (update.sessionUpdate === "tool_call_update" && ["failed", "error"].includes(update.status)) this.failedToolCalls.add(id);
|
|
289
|
+
const rootSessionId = this.modelSessions.get(message.params?.sessionId)?.sessionId;
|
|
290
|
+
this.toolObservations.add(acpToolObservation(message, { harness: this.options.observationHarness ?? "zcode-acp", rootSessionId }));
|
|
258
291
|
}
|
|
259
292
|
|
|
260
293
|
toolSummary(sessionId) {
|
|
261
|
-
|
|
262
|
-
let total = 0; let failures = 0;
|
|
263
|
-
for (const [id, name] of this.toolCalls) {
|
|
264
|
-
if (sessionId !== undefined && JSON.parse(id)[0] !== sessionId) continue;
|
|
265
|
-
total += 1; by_tool[name] = (by_tool[name] ?? 0) + 1;
|
|
266
|
-
if (this.failedToolCalls.has(id)) failures += 1;
|
|
267
|
-
}
|
|
268
|
-
return { total, failures, by_tool };
|
|
294
|
+
return this.toolObservations.summary(this.modelSessions.get(sessionId)?.sessionId ?? sessionId);
|
|
269
295
|
}
|
|
270
|
-
toolCursor() { return
|
|
296
|
+
toolCursor() { return this.toolObservations.cursor(); }
|
|
271
297
|
assistantTextCursor(sessionId) { return (this.assistantText.get(sessionId) ?? "").length; }
|
|
272
298
|
assistantTextSince(sessionId, cursor) { return (this.assistantText.get(sessionId) ?? "").slice(cursor); }
|
|
273
|
-
toolSummarySince(cursor) {
|
|
274
|
-
|
|
275
|
-
for (const [id, name] of this.toolCalls) {
|
|
276
|
-
if (cursor.has(id)) continue;
|
|
277
|
-
total += 1; by_tool[name] = (by_tool[name] ?? 0) + 1;
|
|
278
|
-
if (this.failedToolCalls.has(id)) failures += 1;
|
|
279
|
-
}
|
|
280
|
-
return { total, failures, by_tool };
|
|
299
|
+
toolSummarySince(cursor, sessionId) {
|
|
300
|
+
return this.toolObservations.summary(this.modelSessions.get(sessionId)?.sessionId ?? sessionId, cursor);
|
|
281
301
|
}
|
|
282
302
|
|
|
283
303
|
async answer(message) {
|
|
@@ -299,8 +319,9 @@ export class AcpBridge {
|
|
|
299
319
|
this.send({ jsonrpc: "2.0", id: message.id, result });
|
|
300
320
|
}
|
|
301
321
|
|
|
302
|
-
request(method, params = {}, timeoutMs = 30_000, { idleSessionId = null, idleTimeoutMs = null } = {}) {
|
|
322
|
+
request(method, params = {}, timeoutMs = 30_000, { idleSessionId = null, idleTimeoutMs = null, inactivityMode = "reject" } = {}) {
|
|
303
323
|
if ((this.notificationError?.code === "profile_integrity_violation" || fatalLifecycleError(this.notificationError)) && ["session/new", "session/prompt", "session/fork"].includes(method)) return Promise.reject(this.notificationError);
|
|
324
|
+
if (this.inactivityError && ["session/new", "session/prompt", "session/fork"].includes(method)) return Promise.reject(this.inactivityError);
|
|
304
325
|
const id = this.nextId++;
|
|
305
326
|
return new Promise((resolve, reject) => {
|
|
306
327
|
const finish = (error) => {
|
|
@@ -310,12 +331,27 @@ export class AcpBridge {
|
|
|
310
331
|
// Productive Turns use the sliding session-activity timeout below. A
|
|
311
332
|
// second wall-clock timeout would kill an active, long-running Turn.
|
|
312
333
|
const timer = Number.isFinite(timeoutMs) && timeoutMs > 0
|
|
313
|
-
? observedTimeout(() =>
|
|
334
|
+
? observedTimeout(() => {
|
|
335
|
+
const pending = this.pending.get(id);
|
|
336
|
+
if (!pending || pending.timedOut) return;
|
|
337
|
+
pending.timedOut = true;
|
|
338
|
+
const timeoutError = Object.assign(new Error(`${method} timed out`), {
|
|
339
|
+
code: "rpc_timeout",
|
|
340
|
+
details: { method, request_id: id, session_id: params.sessionId ?? null, timeout_ms: timeoutMs, grace_ms: LATE_RESPONSE_GRACE_MS },
|
|
341
|
+
});
|
|
342
|
+
this.journal.write("request_deadline", timeoutError.details);
|
|
343
|
+
pending.timer = observedTimeout(() => {
|
|
344
|
+
this.expiredRequests.set(id, { method, sessionId: params.sessionId ?? null });
|
|
345
|
+
while (this.expiredRequests.size > 128) this.expiredRequests.delete(this.expiredRequests.keys().next().value);
|
|
346
|
+
finish(timeoutError);
|
|
347
|
+
}, LATE_RESPONSE_GRACE_MS);
|
|
348
|
+
}, timeoutMs)
|
|
314
349
|
: null;
|
|
315
350
|
const validIdleTimeout = Number.isFinite(idleTimeoutMs) && idleTimeoutMs > 0 ? idleTimeoutMs : null;
|
|
316
351
|
if (idleSessionId) this.sessionActivity.set(idleSessionId, Date.now());
|
|
317
352
|
let lastTimerAt = Date.now();
|
|
318
353
|
let resumedAt = null;
|
|
354
|
+
let inactivityRecorded = false;
|
|
319
355
|
const checkIdle = async () => {
|
|
320
356
|
if (!this.pending.has(id)) return;
|
|
321
357
|
const observedAt = Date.now();
|
|
@@ -331,45 +367,73 @@ export class AcpBridge {
|
|
|
331
367
|
return;
|
|
332
368
|
}
|
|
333
369
|
const lastActivity = this.sessionActivity.get(idleSessionId) ?? Date.now();
|
|
334
|
-
if (observedAt - Math.max(lastActivity, resumedAt ?? lastActivity) < validIdleTimeout) return;
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
370
|
+
if (observedAt - Math.max(lastActivity, resumedAt ?? lastActivity) < validIdleTimeout || inactivityRecorded) return;
|
|
371
|
+
// ACP silence is diagnostic only. A native child can be computing while
|
|
372
|
+
// its Session is non-resident and therefore unsubscribable.
|
|
373
|
+
inactivityRecorded = true;
|
|
374
|
+
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() });
|
|
375
|
+
if (inactivityMode !== "diagnostic") {
|
|
376
|
+
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 } }));
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
await this.options.onInactivity?.({ method, requestId: id, sessionId: idleSessionId, lastActivityAt: lastActivity, idleTimeoutMs: validIdleTimeout });
|
|
339
380
|
};
|
|
340
|
-
const idleTimer = validIdleTimeout ? setInterval(() => {
|
|
381
|
+
const idleTimer = validIdleTimeout ? setInterval(() => {
|
|
382
|
+
void checkIdle().catch((error) => {
|
|
383
|
+
// A timer callback is outside the request promise. Preserve the
|
|
384
|
+
// failure for the owning daemon/flush path instead of creating an
|
|
385
|
+
// unhandled rejection that loses the primary request evidence.
|
|
386
|
+
this.inactivityError ??= error;
|
|
387
|
+
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(() => {});
|
|
388
|
+
});
|
|
389
|
+
}, Math.max(10, Math.min(1_000, Math.floor(validIdleTimeout / 4)))) : null;
|
|
341
390
|
this.pending.set(id, { resolve, reject, timer, idleTimer, method, sessionId: params.sessionId ?? null });
|
|
342
391
|
this.send({ jsonrpc: "2.0", id, method, params });
|
|
343
392
|
});
|
|
344
393
|
}
|
|
345
394
|
|
|
346
395
|
notify(method, params = {}) { this.send({ jsonrpc: "2.0", method, params }); }
|
|
347
|
-
send(message) {
|
|
396
|
+
send(message) {
|
|
397
|
+
this.journal.write("outbound", message);
|
|
398
|
+
const stdin = this.child?.stdin;
|
|
399
|
+
if (!stdin || stdin.destroyed) {
|
|
400
|
+
const error = Object.assign(new Error("zcode-acp stdin is closed"), { code: "bridge_pipe_broken" });
|
|
401
|
+
this.rejectAll(error);
|
|
402
|
+
throw error;
|
|
403
|
+
}
|
|
404
|
+
try { stdin.write(`${JSON.stringify(message)}\n`); }
|
|
405
|
+
catch (error) { error.details = { system_code: error.code ?? null }; error.code = "bridge_pipe_broken"; this.rejectAll(error); throw error; }
|
|
406
|
+
}
|
|
348
407
|
rejectAll(error) { for (const pending of this.pending.values()) { clearTimeout(pending.timer); if (pending.idleTimer) clearInterval(pending.idleTimer); pending.reject(error); } this.pending.clear(); }
|
|
349
408
|
async flush() {
|
|
350
409
|
try { await Promise.all([...this.lifecycleNotifications]); await this.notifications; await this.journal.flush(); }
|
|
351
410
|
catch (error) { throw this.notificationError ?? error; }
|
|
352
411
|
if (this.notificationError) throw this.notificationError;
|
|
412
|
+
if (this.inactivityError) throw this.inactivityError;
|
|
353
413
|
}
|
|
354
414
|
async close() {
|
|
355
|
-
|
|
356
|
-
|
|
415
|
+
// Productive notification failures belong to flush()/the original Turn.
|
|
416
|
+
// Draining them must not relabel a successful physical shutdown as failed.
|
|
417
|
+
await Promise.allSettled([...this.lifecycleNotifications, this.notifications]);
|
|
357
418
|
if (this.child) {
|
|
358
419
|
this.child.stdin.end();
|
|
359
420
|
await Promise.race([new Promise((resolve) => this.child.once("exit", resolve)), new Promise((resolve) => setTimeout(resolve, 2000))]);
|
|
360
421
|
await stopProcessGroup(this.child, 2_000);
|
|
361
422
|
}
|
|
362
423
|
await this.journal.flush();
|
|
363
|
-
if (flushError) throw flushError;
|
|
364
424
|
}
|
|
365
425
|
}
|
|
366
426
|
|
|
367
427
|
function isProductiveSessionNotification(message) {
|
|
368
428
|
if (!["session/update", "_x.ai/session_notification"].includes(message.method)) return false;
|
|
369
429
|
const update = message.params?.update;
|
|
370
|
-
// Transport
|
|
371
|
-
//
|
|
372
|
-
|
|
430
|
+
// Transport state and the bridge's cosmetic thinking hint are not native
|
|
431
|
+
// progress. The latter is identifiable without leaking a ZCode-specific
|
|
432
|
+
// synthetic flag through the ACP wire contract.
|
|
433
|
+
if (update?.sessionUpdate === "heartbeat" || update?.type === "heartbeat") return false;
|
|
434
|
+
if (update?.sessionUpdate === "agent_thought_chunk" && typeof update?.messageId === "string" && update.messageId.startsWith("thinking_")) return false;
|
|
435
|
+
if ((update?.sessionUpdate === "status" || update?.type === "status") && update?.status === "running") return false;
|
|
436
|
+
return true;
|
|
373
437
|
}
|
|
374
438
|
|
|
375
439
|
export function observedProfile(read) {
|
|
@@ -390,14 +454,16 @@ async function resume(bridge, sessionId, cwd) {
|
|
|
390
454
|
}
|
|
391
455
|
|
|
392
456
|
async function inspect(bridge, sessionId) {
|
|
393
|
-
const [read, subagents
|
|
457
|
+
const [read, subagents] = await Promise.all([
|
|
394
458
|
bridge.request("zcode/session/read", { sessionId }),
|
|
395
459
|
bridge.request("zcode/session/subagents", { sessionId }),
|
|
396
|
-
bridge.request("zcode/session/usage", { sessionId }),
|
|
397
460
|
]);
|
|
461
|
+
let usage = null, usage_observation_error = null;
|
|
462
|
+
try { usage = await bridge.request("zcode/session/usage", { sessionId }); }
|
|
463
|
+
catch (error) { usage_observation_error = { code: error.code ?? "usage_observation_failed", message: error.message ?? String(error) }; }
|
|
398
464
|
const observed = observedProfile(read);
|
|
399
465
|
await bridge.recordModel?.(sessionId, observed, "zcode/session/read");
|
|
400
|
-
return { read, subagents, usage, observed_profile: observed };
|
|
466
|
+
return { read, subagents, usage, ...(usage_observation_error ? { usage_observation_error } : {}), observed_profile: observed };
|
|
401
467
|
}
|
|
402
468
|
|
|
403
469
|
// The journal is the immutable transcript. CLI receipts only need the
|
|
@@ -590,7 +656,7 @@ function receipt(options, value) { return options.daemonId ? { daemon_id: option
|
|
|
590
656
|
|
|
591
657
|
async function withBridge(options, operation) {
|
|
592
658
|
const bridge = new AcpBridge(options);
|
|
593
|
-
try { const initialized = await bridge.start();
|
|
659
|
+
try { const initialized = await bridge.start(); const result = await operation(bridge, initialized); await bridge.flush(); return result; }
|
|
594
660
|
finally { await bridge.close(); }
|
|
595
661
|
}
|
|
596
662
|
|
|
@@ -619,8 +685,8 @@ export async function zcodeInvocationObserver(options, rootProviderSessionId, se
|
|
|
619
685
|
if (message.method !== "session/update" || update?.sessionUpdate !== "tool_call") return null;
|
|
620
686
|
if (options.adapterSessionId && message.params?.sessionId !== options.adapterSessionId) return null;
|
|
621
687
|
const native = update._meta?.zcodeRuntime ?? {};
|
|
622
|
-
|
|
623
|
-
|
|
688
|
+
const session = zcodePhysicalSession(update, rootProviderSessionId);
|
|
689
|
+
if (!session) throw Object.assign(new Error("Native subagent event has no physical child identity"), { code: "zcode_child_identity_missing" });
|
|
624
690
|
const key = update.toolCallId;
|
|
625
691
|
if (!key || typeof session !== "string") return null;
|
|
626
692
|
if (owners.has(key) && owners.get(key) !== session) throw Object.assign(new Error("Native tool id has conflicting Session ownership"), { code: "zcode_tool_identity_conflict" });
|
|
@@ -678,6 +744,11 @@ async function forwardUsage(options, providerSessionId, usage, toolCalls) {
|
|
|
678
744
|
await runWithStdin(options.ddFlowBin, ["zcode", "usage", "ingest", "--project-root", projectRoot, "--json"], `${JSON.stringify(payload)}\n`, options.ddFlowHome ? { DD_FLOW_HOME: absolute(options.ddFlowHome, "--dd-flow-home") } : {});
|
|
679
745
|
}
|
|
680
746
|
|
|
747
|
+
async function forwardUsageBestEffort(options, providerSessionId, usage, toolCalls) {
|
|
748
|
+
try { await forwardUsage(options, providerSessionId, usage, toolCalls); return null; }
|
|
749
|
+
catch (error) { return { code: error.code ?? "usage_ingest_failed", message: error.message ?? String(error) }; }
|
|
750
|
+
}
|
|
751
|
+
|
|
681
752
|
export function zcodeLifecycleQualification(sha256, bridgeCommit, harnessContract) {
|
|
682
753
|
// Behavioural evidence: dd-eval production-invocations probe, 2026-09-13.
|
|
683
754
|
// Pin both providers of native event identity, not a string in the bundle.
|
|
@@ -712,19 +783,36 @@ export async function createSessionWithBridge(bridge, options, initialized) {
|
|
|
712
783
|
let forward;
|
|
713
784
|
bridge.configure({ ...options, onNotification: async (event) => { await forward?.(event); await options.onNotification?.(event); } });
|
|
714
785
|
const created = await bridge.request("session/new", { cwd, mcpServers: [] });
|
|
786
|
+
// The ACP alias is the first durable identity available. Persist it before
|
|
787
|
+
// resolving the native Session so a later resolve timeout cannot erase the
|
|
788
|
+
// fact that allocation was already admitted.
|
|
789
|
+
await options.onSessionAliasAllocated?.({ adapter_session_id: created.sessionId, cwd });
|
|
790
|
+
bridge.allocationObservers?.set(created.sessionId, async identity => {
|
|
791
|
+
if (identity.cwd !== cwd) fail("ZCode allocation workspace mismatch", "session_identity_mismatch");
|
|
792
|
+
await options.onSessionAllocated?.(identity);
|
|
793
|
+
});
|
|
715
794
|
const resolved = await bridge.request("zcode/session/resolve", { sessionId: created.sessionId });
|
|
716
795
|
const providerSessionId = resolved.providerSessionId;
|
|
796
|
+
// Expose both identities before profile/usage bookkeeping. The owning
|
|
797
|
+
// daemon can persist this allocation even when a later step fails.
|
|
798
|
+
await options.onSessionAllocated?.({
|
|
799
|
+
adapter_session_id: created.sessionId,
|
|
800
|
+
provider_session_id: providerSessionId,
|
|
801
|
+
cwd,
|
|
802
|
+
});
|
|
717
803
|
bridge.registerModelSession?.(created.sessionId, providerSessionId, requested, null, false);
|
|
718
804
|
const profile_receipt = await applyProfile(bridge, created.sessionId, requested);
|
|
719
805
|
forward = flowForwarder(options, providerSessionId, bridge);
|
|
720
|
-
|
|
721
|
-
await
|
|
806
|
+
let usage = null, usage_observation_error = null;
|
|
807
|
+
try { usage = await bridge.request("zcode/session/usage", { sessionId: created.sessionId }); }
|
|
808
|
+
catch (error) { usage_observation_error = { code: error.code ?? "usage_observation_failed", message: error.message ?? String(error) }; }
|
|
809
|
+
const usage_ingest_error = await forwardUsageBestEffort(options, providerSessionId, usage, bridge.toolSummary(created.sessionId));
|
|
722
810
|
await bridge.flush();
|
|
723
811
|
// ZCode materializes `subagents` only after its first prompt. Session
|
|
724
812
|
// creation is intentionally nonproductive, so querying that topology here
|
|
725
813
|
// would both fail on real ZCode and turn the common create contract into an
|
|
726
814
|
// implicit first turn. The first prompt/inspect records full evidence.
|
|
727
|
-
const evidence = { read: null, subagents: { running: [], completed: [] }, usage, observed_profile: profile_receipt.observed, tool_calls: bridge.toolSummary(created.sessionId) };
|
|
815
|
+
const evidence = { read: null, subagents: { running: [], completed: [] }, usage, ...(usage_observation_error ? { usage_observation_error } : {}), ...(usage_ingest_error ? { usage_ingest_error } : {}), observed_profile: profile_receipt.observed, tool_calls: bridge.toolSummary(created.sessionId) };
|
|
728
816
|
return receipt(options, { harness: "zcode-acp", runtime_family: "zcode", adapter_session_id: created.sessionId, provider_session_id: providerSessionId, cwd, initialized, profile: profile_receipt, evidence: publicEvidence(evidence) });
|
|
729
817
|
}
|
|
730
818
|
|
|
@@ -743,25 +831,36 @@ export async function promptSessionWithBridge(bridge, options) {
|
|
|
743
831
|
bridge.configure({ onLifecycleNotification: await zcodeInvocationObserver({ ...options, adapterSessionId: identity.adapterSessionId }, identity.providerSessionId) });
|
|
744
832
|
const profile_receipt = await applyProfile(bridge, identity.adapterSessionId, requested, options.assertDispatch);
|
|
745
833
|
options.assertDispatch?.();
|
|
746
|
-
|
|
747
|
-
|
|
834
|
+
let initial_usage_error = null;
|
|
835
|
+
try { initial_usage_error = await forwardUsageBestEffort(options, identity.providerSessionId, await bridge.request("zcode/session/usage", { sessionId: identity.adapterSessionId }), bridge.toolSummary(identity.adapterSessionId)); }
|
|
836
|
+
catch (error) { initial_usage_error = { code: error.code ?? "usage_observation_failed", message: error.message ?? String(error) }; }
|
|
837
|
+
// This is the request budget. ACP silence is observed separately and must
|
|
838
|
+
// never turn a long native child request into a fabricated provider failure.
|
|
839
|
+
const timeoutMs = options.timeoutMs === undefined ? 1_800_000 : options.timeoutMs;
|
|
748
840
|
const textCursor = bridge.assistantTextCursor(identity.adapterSessionId);
|
|
749
841
|
options.assertDispatch?.();
|
|
750
842
|
const turn = await bridge.request("session/prompt", { sessionId: identity.adapterSessionId, prompt: [{ type: "text", text: prompt }] }, timeoutMs, {
|
|
751
843
|
idleSessionId: identity.adapterSessionId,
|
|
752
844
|
idleTimeoutMs: options.livenessTimeoutMs ?? null,
|
|
845
|
+
inactivityMode: "diagnostic",
|
|
753
846
|
});
|
|
754
|
-
await bridge.flush();
|
|
755
|
-
const evidence = await inspect(bridge, identity.adapterSessionId);
|
|
756
|
-
evidence.tool_calls = bridge.toolSummary(identity.adapterSessionId);
|
|
757
|
-
await forwardUsage(options, identity.providerSessionId, evidence.usage, evidence.tool_calls);
|
|
758
847
|
const stopReason = typeof turn?.stopReason === "string" ? turn.stopReason : null;
|
|
759
848
|
if (stopReason && stopReason !== "end_turn") {
|
|
849
|
+
// A close can settle the pending prompt before its terminal event. Do not
|
|
850
|
+
// collect live evidence first: ordinary inspection is allowed to recover
|
|
851
|
+
// an evicted Session, which would undo the just-confirmed close.
|
|
852
|
+
await bridge.flush();
|
|
760
853
|
const error = new Error(`ZCode ended the provider request with ${stopReason}, not end_turn`);
|
|
761
854
|
error.code = stopReason === "cancelled" ? "zcode_turn_cancelled" : "zcode_turn_not_completed";
|
|
762
|
-
error.details = { stop_reason: stopReason, provider_session_id: identity.providerSessionId, adapter_session_id: identity.adapterSessionId
|
|
855
|
+
error.details = { stop_reason: stopReason, provider_session_id: identity.providerSessionId, adapter_session_id: identity.adapterSessionId };
|
|
763
856
|
throw error;
|
|
764
857
|
}
|
|
858
|
+
await bridge.flush();
|
|
859
|
+
const evidence = await inspect(bridge, identity.adapterSessionId);
|
|
860
|
+
evidence.tool_calls = bridge.toolSummary(identity.adapterSessionId);
|
|
861
|
+
const usage_ingest_error = await forwardUsageBestEffort(options, identity.providerSessionId, evidence.usage, evidence.tool_calls);
|
|
862
|
+
if (initial_usage_error) evidence.initial_usage_error = initial_usage_error;
|
|
863
|
+
if (usage_ingest_error) evidence.usage_ingest_error = usage_ingest_error;
|
|
765
864
|
if (!options.allowBackground && (evidence.subagents.running ?? []).length) {
|
|
766
865
|
await cancelTree(bridge, identity.adapterSessionId, evidence.subagents, identity.providerSessionId);
|
|
767
866
|
fail("background ZCode subagents cannot outlive the one-shot dd-zcode connection; the live child tree was cancelled");
|
|
@@ -117,11 +117,11 @@ export async function reconcileDriverReplies(root) {
|
|
|
117
117
|
try { entries = await readdir(directory); } catch (error) { if (error.code === "ENOENT") return; throw error; }
|
|
118
118
|
for (const entry of entries.filter(name => name.endsWith(".json"))) {
|
|
119
119
|
const file = path.join(directory, entry), record = JSON.parse(await readFile(file, "utf8"));
|
|
120
|
-
if (["completed", "failed"].includes(record.state)) continue;
|
|
120
|
+
if (["completed", "failed", "interrupted"].includes(record.state)) continue;
|
|
121
121
|
let receipt;
|
|
122
122
|
try { receipt = await inspectDaemonOperation(root, record.operation_id); }
|
|
123
123
|
catch (error) { if (error.code !== "operation_not_found") throw error; }
|
|
124
|
-
if (!["completed", "failed"].includes(receipt?.state)) throw Object.assign(new Error("a previous driver request has an unknown outcome; no new productive request was sent"), {
|
|
124
|
+
if (!["completed", "failed", "interrupted"].includes(receipt?.state)) throw Object.assign(new Error("a previous driver request has an unknown outcome; no new productive request was sent"), {
|
|
125
125
|
code: "operation_observation_lost", details: { operation_id: record.operation_id, state_dir: root }
|
|
126
126
|
});
|
|
127
127
|
await writeJsonAtomic(file, { ...record, state: receipt.state, result: receipt.result, error: receipt.error, reconciled_at: new Date().toISOString() });
|
|
@@ -81,7 +81,7 @@ export async function budgetObservationScope(stateDir, sessionId) {
|
|
|
81
81
|
try {
|
|
82
82
|
const requested = JSON.parse(await readFile(path.join(directory, "requested.json"), "utf8"));
|
|
83
83
|
const result = JSON.parse(await readFile(path.join(directory, "result.json"), "utf8"));
|
|
84
|
-
if (requested.daemon_id === daemonId && requested.session_id === sessionId && ["completed", "failed"].includes(result.state) && ["session.prompt", "session.start", "session.resume"].includes(requested.operation)) operationIds.push(requested.operation_id);
|
|
84
|
+
if (requested.daemon_id === daemonId && requested.session_id === sessionId && ["completed", "failed", "interrupted"].includes(result.state) && ["session.prompt", "session.start", "session.resume"].includes(requested.operation)) operationIds.push(requested.operation_id);
|
|
85
85
|
} catch (error) { if (error.code !== "ENOENT") throw error; }
|
|
86
86
|
}
|
|
87
87
|
return { stateDir, state, daemonId, sessionId, operationIds };
|
|
@@ -100,12 +100,34 @@ export async function releaseBudgetObservation(scope, observation) {
|
|
|
100
100
|
|
|
101
101
|
export async function completeBudgetOperation(stateDir, request, result, observe) {
|
|
102
102
|
const state = await budgetState(stateDir);
|
|
103
|
-
if (!state?.config?.runtime_owner?.budget) return;
|
|
104
|
-
if (["session.create", "session.fork"].includes(request.operation))
|
|
103
|
+
if (!state?.config?.runtime_owner?.budget) return { state: "not_required" };
|
|
104
|
+
if (["session.create", "session.fork"].includes(request.operation)) {
|
|
105
|
+
await releaseBudget(state, { "operation-id": request.id });
|
|
106
|
+
return { state: "settled", operation_ids: [request.id], source: "creation_receipt" };
|
|
107
|
+
}
|
|
108
|
+
const sessionId = request.params?.sessionId;
|
|
109
|
+
// A productive daemon receipt is bound to this exact dispatch. Reuse its
|
|
110
|
+
// complete tree proof instead of immediately asking the provider again.
|
|
111
|
+
if (["session.prompt", "session.start", "session.resume"].includes(request.operation)
|
|
112
|
+
&& result?.settled === true
|
|
113
|
+
&& typeof sessionId === "string"
|
|
114
|
+
&& (result.provider_session_id ?? result.session_id ?? result.adapter_session_id) === sessionId) {
|
|
115
|
+
const scope = { stateDir, state, daemonId: state.daemon_id ?? state.config?.daemonId, sessionId, operationIds: [request.id] };
|
|
116
|
+
await releaseBudgetObservation(scope, result);
|
|
117
|
+
return { state: "settled", operation_ids: [request.id], source: "operation_receipt" };
|
|
118
|
+
}
|
|
119
|
+
// A known live tree is not a reason to poll it again. Preserve capacity and
|
|
120
|
+
// let the next explicit reconciliation obtain fresh proof.
|
|
121
|
+
if (result?.settled === false) return { state: "pending", reason: "tree_unsettled" };
|
|
105
122
|
const scope = await budgetObservationScope(stateDir, request.params?.sessionId);
|
|
106
|
-
if (!scope?.operationIds.length) return;
|
|
107
|
-
|
|
123
|
+
if (!scope?.operationIds.length) return { state: "not_required" };
|
|
124
|
+
// Reconciliation requires a fresh, explicit proof. Treating an arbitrary
|
|
125
|
+
// productive reply as proof would silently release a live turn.
|
|
126
|
+
if (!observe) return { state: "pending", reason: "settlement_evidence_required" };
|
|
127
|
+
const observation = await observe();
|
|
128
|
+
if (observation?.settled !== true) return { state: "pending", reason: "tree_unsettled" };
|
|
108
129
|
await releaseBudgetObservation(scope, observation);
|
|
130
|
+
return { state: "settled", operation_ids: scope.operationIds, source: "reconciliation_observation" };
|
|
109
131
|
}
|
|
110
132
|
|
|
111
133
|
async function budgetState(stateDir) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// An observer losing its response does not establish that a provider Turn failed.
|
|
2
2
|
export function isObservationLoss(error) {
|
|
3
|
-
return ["rpc_timeout", "daemon_timeout", "turn_timeout", "operation_observation_lost", "daemon_connection_closed"].includes(error?.code);
|
|
3
|
+
return ["rpc_timeout", "native_timeout", "native_outcome_unknown", "native_backend_dead", "native_backend_pipe_broken", "bridge_exited", "bridge_pipe_broken", "daemon_timeout", "turn_timeout", "operation_observation_lost", "daemon_connection_closed"].includes(error?.code);
|
|
4
4
|
}
|
|
5
5
|
|
|
6
6
|
export function errorRecord(error, depth = 0) {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface ToolEvent {
|
|
2
|
+
session?: { harness_id: string; session_id: string };
|
|
3
|
+
call_id?: string;
|
|
4
|
+
status?: string;
|
|
5
|
+
name?: string;
|
|
6
|
+
reason?: string;
|
|
7
|
+
[key: string]: unknown;
|
|
8
|
+
}
|
|
9
|
+
export class ToolObservations {
|
|
10
|
+
calls: Map<string, ToolEvent>;
|
|
11
|
+
gaps: Set<string>;
|
|
12
|
+
add(event: ToolEvent | null): void;
|
|
13
|
+
summary(sessionId?: string, cursor?: Set<string>): { total: number; failures: number; pending: number; by_tool: Record<string, number>; completeness: string; sessions: Array<{ session: { harness_id: string; session_id: string }; total: number; failures: number; pending: number; by_tool: Record<string, number> }>; reasons: string[] };
|
|
14
|
+
cursor(): Set<string>;
|
|
15
|
+
}
|
|
16
|
+
export function codexToolObservation(record: Record<string, unknown>, sessionId: string): ToolEvent | null;
|
|
17
|
+
export function replayToolJournal(file: string, options: { harness: string; rootSessionId: string; adapterSessionId?: string }, reducer?: ToolObservations): ToolObservations;
|