@deksden-com/dd-flow-cli 0.9.0-beta.119 → 0.9.0-beta.121

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 CHANGED
@@ -1,5 +1,12 @@
1
1
  # @deksden-com/dd-flow-cli
2
2
 
3
+ ## 0.9.0-beta.120
4
+
5
+ ### Patch Changes
6
+
7
+ - Continue only confirmed, effect-free Codex `serverOverloaded` Turns in the same Session with bounded backoff and retained owner, budget, HITL, Work, MERGE, and recovery admission. Preserve exact native child Turn evidence and fail closed when effects are unknown.
8
+ - Distinguish hard provider quota from short rate limiting and ambiguous 429 responses across AGY, ZCode, and OpenCode.
9
+
3
10
  ## 0.9.0-beta.119
4
11
 
5
12
  ### Patch Changes
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "cli_package": "@deksden-com/dd-flow-cli",
3
- "cli_version": "0.9.0-beta.119",
4
- "cli_commit": "9bf32901c2f926dd5819e3695dbab4fae1869724",
5
- "built_at": "2026-09-28T13:03:08.769Z",
3
+ "cli_version": "0.9.0-beta.121",
4
+ "cli_commit": "2e7bef9a7813bd003855d94b394e63d8aa5f6419",
5
+ "built_at": "2026-09-29T16:13:36.621Z",
6
6
  "built_with_canon": {
7
7
  "version": "4.1.1",
8
8
  "commit": "678daa038287c948ada5b2d785a6dcc925c7b891",
@@ -327,10 +327,11 @@ export class Runtime {
327
327
  this.active = null;
328
328
  const message = String(result.error ?? "Antigravity returned ERROR"); const normalized = message.toLowerCase();
329
329
  const code = result.status === "CANCELLED" ? "agy_turn_cancelled" : /quota|insufficient.?credit|billing|usage limit/.test(normalized) ? "agy_provider_quota_exhausted"
330
- : /rate.?limit|too many requests|\b429\b/.test(normalized) ? "agy_provider_rate_limited"
330
+ : /rate.?limit|too many requests/.test(normalized) ? "agy_provider_rate_limited"
331
+ : /\b429\b/.test(normalized) ? "agy_provider_limit_unknown"
331
332
  : /auth|unauthori[sz]ed|forbidden|account/.test(normalized) ? "agy_provider_account"
332
333
  : "agy_provider_failed";
333
- return current.reject(new DaemonError(code, message, ["agy_provider_quota_exhausted", "agy_provider_rate_limited"].includes(code), { provider_result: result, provider_error_scope: current.previousError && current.previousError === result.error ? "repeated_conversation_error" : "terminal_result", usage_ingest: this.usageIngest }));
334
+ return current.reject(new DaemonError(code, message, code === "agy_provider_rate_limited", { provider_result: result, provider_error_scope: current.previousError && current.previousError === result.error ? "repeated_conversation_error" : "terminal_result", usage_ingest: this.usageIngest }));
334
335
  }
335
336
  await this.completePendingTurn();
336
337
  }
@@ -0,0 +1 @@
1
+ export function terminalCodexOverload(error: unknown, expectedSessionId: string, expectedTurnId?: string): boolean;
@@ -52,7 +52,7 @@ class Journal {
52
52
  }
53
53
 
54
54
  export class CodexBridge {
55
- constructor(options = {}) { this.options = options; this.nextId = 1; this.pending = new Map(); this.lateResponses = new Map(); this.turns = new Map(); this.finalMessages = new Map(); this.descendants = new Map(); this.journal = new Journal(options.journal); this.stderr = ""; this.requestedProfiles = new Map(); this.modelObservations = Promise.resolve(); }
55
+ constructor(options = {}) { this.options = options; this.nextId = 1; this.pending = new Map(); this.lateResponses = new Map(); this.turns = new Map(); this.nativeErrors = new Map(); this.finalMessages = new Map(); this.descendants = new Map(); this.journal = new Journal(options.journal); this.stderr = ""; this.requestedProfiles = new Map(); this.modelObservations = Promise.resolve(); }
56
56
 
57
57
  async start() {
58
58
  const bin = this.options.bin ?? process.env.DD_CODEX_BIN ?? "codex";
@@ -120,11 +120,16 @@ export class CodexBridge {
120
120
  message.error ? pending.reject(Object.assign(new Error(message.error.message ?? JSON.stringify(message.error)), { code: "rpc_error", details: message.error })) : pending.resolve(message.result ?? {});
121
121
  return;
122
122
  }
123
- if (message.method === "turn/completed") this.turns.set(message.params?.turn?.id ?? message.params?.threadId, { status: "completed", value: message.params });
124
- if (message.method === "turn/started") this.turns.set(message.params?.turn?.id ?? message.params?.threadId, { status: "running", value: message.params });
123
+ if (message.method === "error" && message.params?.threadId && message.params?.turnId) {
124
+ const key = `${message.params.threadId}:${message.params.turnId}`;
125
+ this.nativeErrors.set(key, { error: message.params.error, willRetry: message.params.willRetry });
126
+ while (this.nativeErrors.size > 128) this.nativeErrors.delete(this.nativeErrors.keys().next().value);
127
+ }
128
+ if (message.method === "turn/completed") this.turns.set(message.params?.turn?.id ?? message.params?.threadId, { status: "completed", value: message.params, threadId: message.params?.threadId });
129
+ if (message.method === "turn/started" && this.turns.get(message.params?.turn?.id)?.status !== "completed") this.turns.set(message.params?.turn?.id ?? message.params?.threadId, { status: "running", value: message.params, threadId: message.params?.threadId });
125
130
  const threadId = message.params?.threadId;
126
131
  if (threadId && message.method === "item/started") this.finalMessages.delete(threadId);
127
- if (threadId && message.method === "item/completed" && message.params?.item?.type === "agentMessage") this.finalMessages.set(threadId, { item: message.params.item, completedAt: Date.now() });
132
+ if (threadId && message.method === "item/completed" && message.params?.item?.type === "agentMessage") this.finalMessages.set(threadId, { item: message.params.item, turnId: message.params?.turnId ?? null, completedAt: Date.now() });
128
133
  }
129
134
 
130
135
  isManagedHook(run) {
@@ -217,13 +222,27 @@ export class CodexBridge {
217
222
  observeTopology(value, depth = 0) {
218
223
  if (!value || typeof value !== "object" || depth > 8) return;
219
224
  if (Array.isArray(value)) { for (const item of value) this.observeTopology(item, depth + 1); return; }
225
+ if (depth === 0 && ["turn/started", "turn/completed"].includes(value.method)) {
226
+ const { threadId, turn } = value.params ?? {};
227
+ const previous = this.descendants.get(threadId);
228
+ if (previous && typeof turn?.id === "string" && turn.id) {
229
+ if (value.method === "turn/started" && previous.active_turn_id !== turn.id && this.turns.get(turn.id)?.status !== "completed") {
230
+ this.descendants.set(threadId, { ...previous, status: "running", active_turn_id: turn.id, terminal_turn_id: null, provider_error: null });
231
+ } else if (value.method === "turn/completed" && (!previous.active_turn_id || previous.active_turn_id === turn.id)) {
232
+ this.descendants.set(threadId, { ...previous, status: turn.status === "completed" ? "completed" : turn.status ?? "unknown",
233
+ active_turn_id: turn.id, terminal_turn_id: turn.id, terminal_observed_at: now(), provider_error: nativeTurnError(this, threadId, turn), native_turn_items: turnItemEvidence(turn) });
234
+ }
235
+ }
236
+ }
220
237
  const activity = value.params?.item;
221
238
  const activityParent = value.params?.threadId;
222
239
  if (activity?.type === "subAgentActivity" && typeof activity.agentThreadId === "string" && activity.agentThreadId &&
223
240
  typeof activityParent === "string" && activityParent && activity.agentThreadId !== activityParent) {
224
241
  const previous = this.descendants.get(activity.agentThreadId);
225
242
  const observed = activity.kind === "completed" ? "completed" : activity.kind === "interrupted" ? "interrupted" : "running";
226
- const status = previous?.status === "completed" ? "completed" : observed;
243
+ // Parent activity has no child Turn identity on some app-server builds.
244
+ // It can discover a child, but cannot reopen or close a later known Turn.
245
+ const status = previous?.active_turn_id || previous?.status === "completed" ? previous.status : observed;
227
246
  this.descendants.set(activity.agentThreadId, { ...previous, provider_session_id: activity.agentThreadId,
228
247
  parent_provider_session_id: activityParent, status, agent_path: activity.agentPath ?? previous?.agent_path ?? null,
229
248
  source: "codex.app_server.subAgentActivity" });
@@ -234,7 +253,7 @@ export class CodexBridge {
234
253
  if (typeof child !== "string" || !child || child === value.senderThreadId) continue;
235
254
  const previous = this.descendants.get(child);
236
255
  this.descendants.set(child, { ...previous, provider_session_id: child, parent_provider_session_id: value.senderThreadId,
237
- status: previous?.status === "completed" ? "completed" : value.agentsStates?.[child]?.status ?? "unknown", source: "codex.app_server" });
256
+ status: previous?.status === "completed" || previous?.active_turn_id ? previous.status : value.agentsStates?.[child]?.status ?? previous?.status ?? "unknown", source: "codex.app_server" });
238
257
  }
239
258
  }
240
259
  const id = value.threadId ?? value.id ?? null;
@@ -242,7 +261,7 @@ export class CodexBridge {
242
261
  if (typeof id === "string" && id && typeof parent === "string" && parent) {
243
262
  const previous = this.descendants.get(id);
244
263
  this.descendants.set(id, { ...previous, provider_session_id: id, parent_provider_session_id: parent,
245
- status: previous?.status === "completed" ? "completed" : value.status?.type ?? value.status ?? "unknown", source: "codex.app_server" });
264
+ status: previous?.status === "completed" || previous?.active_turn_id ? previous.status : value.status?.type ?? value.status ?? previous?.status ?? "unknown", source: "codex.app_server" });
246
265
  }
247
266
  for (const child of Object.values(value)) this.observeTopology(child, depth + 1);
248
267
  }
@@ -306,6 +325,18 @@ async function withBridge(options, action) { const bridge = new CodexBridge(opti
306
325
  function profile(options) { return { harness: "codex-desktop", model: options.model ?? null, reasoning: options.reasoning ?? null }; }
307
326
  function threadId(result) { const id = result?.thread?.id ?? result?.threadId; return required(id, "Codex thread id"); }
308
327
  function directChildren(bridge, sessionId) { return typeof bridge?.directChildren === "function" ? bridge.directChildren(sessionId) : []; }
328
+ async function observedChildren(bridge, sessionId) {
329
+ const children = directChildren(bridge, sessionId);
330
+ let reads = 0;
331
+ return await Promise.all(children.map(async child => {
332
+ if (child.status !== "failed" || !child.terminal_turn_id || reads++ >= 16) return child;
333
+ const observed = await readTurn(bridge, child.provider_session_id, child.terminal_turn_id, true);
334
+ const turn = observed?.turn;
335
+ if (turn?.id !== child.terminal_turn_id || turn.status !== child.status) return child;
336
+ return { ...child, provider_error: nativeTurnError(bridge, child.provider_session_id, turn) ?? child.provider_error,
337
+ native_turn_items: turnItemEvidence(turn) };
338
+ }));
339
+ }
309
340
  function codexSessionConfig() { return { ...CODEX_SESSION_CONFIG }; }
310
341
 
311
342
  async function requestConfiguredThread(bridge, method, params, timeoutMs) {
@@ -399,11 +430,24 @@ export async function promptSessionWithBridge(bridge, options) {
399
430
  bridge.assertHookHealthy?.(sessionId);
400
431
  const started = await bridge.request("turn/start", { threadId: sessionId, input: [{ type: "text", text: prompt }], cwd, model: options.model ?? null, effort: options.reasoning ?? null, approvalPolicy: "never", sandboxPolicy: { type: "dangerFullAccess" } }, options.timeoutMs ?? 1_800_000);
401
432
  const turn = started?.turn ?? started; const turnId = turn?.id ?? null;
402
- const completed = await waitForTurn(bridge, sessionId, turnId, options.timeoutMs ?? 1_800_000, options, cwd);
433
+ let completed;
434
+ try { completed = await waitForTurn(bridge, sessionId, turnId, options.timeoutMs ?? 1_800_000, options, cwd); }
435
+ catch (error) {
436
+ if (error?.code === "turn_interrupted") {
437
+ try {
438
+ const observed = await readTurn(bridge, sessionId, turnId, true);
439
+ if (observed?.turn?.id === turnId && observed.turn.status === error.details?.terminal_status) {
440
+ error.details.native_turn_items = turnItemEvidence(observed.turn);
441
+ error.details.provider_error ??= observed.turn.error ?? null;
442
+ }
443
+ } catch { /* The primary native terminal failure remains authoritative. */ }
444
+ }
445
+ throw error;
446
+ }
403
447
  const thread = await readCompletedThread(bridge, sessionId);
404
448
  bridge.assertHookHealthy?.(sessionId);
405
- const assistantText = latestAssistantText(thread) ?? latestAssistantText(completed);
406
- return { harness: "codex-desktop", provider_session_id: sessionId, adapter_session_id: sessionId, turn_id: turnId, status: "completed", turn: completed, ...(thread ? { thread: threadReceipt(thread) } : {}), descendants: directChildren(bridge, sessionId), ...(assistantText ? { assistant_text: assistantText } : {}), requested_profile: profile(options), observed_profile: await observeCodexProfile(bridge, options, sessionId, { model: thread?.model ?? null, reasoning: thread?.reasoningEffort ?? null, provider: thread?.modelProvider ?? null }, "thread/read.after_turn") };
449
+ const assistantText = latestAssistantText(thread?.turns?.find(item => item?.id === turnId)) ?? latestAssistantText(completed);
450
+ return { harness: "codex-desktop", provider_session_id: sessionId, adapter_session_id: sessionId, turn_id: turnId, status: "completed", turn: completed, ...(thread ? { thread: threadReceipt(thread) } : {}), descendants: await observedChildren(bridge, sessionId), ...(assistantText ? { assistant_text: assistantText } : {}), requested_profile: profile(options), observed_profile: await observeCodexProfile(bridge, options, sessionId, { model: thread?.model ?? null, reasoning: thread?.reasoningEffort ?? null, provider: thread?.modelProvider ?? null }, "thread/read.after_turn") };
407
451
  }
408
452
 
409
453
  async function ensureThreadLoaded(bridge, sessionId, options, cwd) {
@@ -428,23 +472,23 @@ async function waitForTurn(bridge, sessionId, turnId, timeoutMs, options, cwd) {
428
472
  let nextStoredReadAt = Date.now();
429
473
  while (!clock.sample(bridge.activity?.get(sessionId))) {
430
474
  bridge.assertHookHealthy?.(sessionId);
431
- const known = bridge.turns.get(turnId) ?? bridge.turns.get(sessionId); if (known?.status === "completed") return terminalTurn(known.value, turnId, bridge);
475
+ const known = bridge.turns.get(turnId) ?? bridge.turns.get(sessionId); if (known?.status === "completed" && (!known.threadId || known.threadId === sessionId)) return terminalTurn(known.value, sessionId, turnId, bridge);
432
476
  if (Date.now() >= nextStoredReadAt) {
433
477
  const stored = await readTurn(bridge, sessionId, turnId, false);
434
- if (stored?.turn && isAuthoritativeTerminal(bridge, turnId, stored.turn.status, stored.threadStatus, failureNotBefore)) return terminalTurn({ turn: stored.turn }, turnId, bridge);
478
+ if (stored?.turn && isAuthoritativeTerminal(bridge, turnId, stored.turn.status, stored.threadStatus, failureNotBefore)) return terminalTurn({ turn: stored.turn }, sessionId, turnId, bridge);
435
479
  // A loaded app-server can retain a stale `active` Thread state after a
436
480
  // provider-side interruption. The persisted turn page is independent of
437
481
  // that live subscription and is the compact authority for its terminal
438
482
  // status; unlike `includeTurns`, it never hydrates the whole history.
439
483
  const persisted = await readPersistedTurn(bridge, sessionId, turnId);
440
- if (persisted && isAuthoritativeTerminal(bridge, turnId, persisted.status, stored?.threadStatus, failureNotBefore)) return terminalTurn({ turn: persisted }, turnId, bridge);
484
+ if (persisted && isAuthoritativeTerminal(bridge, turnId, persisted.status, stored?.threadStatus, failureNotBefore)) return terminalTurn({ turn: persisted }, sessionId, turnId, bridge);
441
485
  // Desktop may unload an idle Thread immediately after an interrupted
442
486
  // Turn. Reload it once before polling again: otherwise the compact
443
487
  // status remains `notLoaded` forever and hides the terminal Turn.
444
488
  if (stored?.threadStatus === "notLoaded") {
445
489
  await ensureThreadLoaded(bridge, sessionId, options, cwd);
446
490
  const hydrated = await readTurn(bridge, sessionId, turnId, true);
447
- if (hydrated?.turn && isAuthoritativeTerminal(bridge, turnId, hydrated.turn.status, hydrated.threadStatus, failureNotBefore)) return terminalTurn({ turn: hydrated.turn }, turnId, bridge);
491
+ if (hydrated?.turn && isAuthoritativeTerminal(bridge, turnId, hydrated.turn.status, hydrated.threadStatus, failureNotBefore)) return terminalTurn({ turn: hydrated.turn }, sessionId, turnId, bridge);
448
492
  }
449
493
  // Some app-server versions correctly omit turns for a compact read.
450
494
  // Once the thread is idle, hydrate exactly once to recover the terminal
@@ -455,7 +499,7 @@ async function waitForTurn(bridge, sessionId, turnId, timeoutMs, options, cwd) {
455
499
  // an inProgress Turn after emitting the final agent message; treating
456
500
  // that stale Turn as terminal incorrectly aborts an otherwise usable
457
501
  // worker session.
458
- if (hydrated?.turn && isAuthoritativeTerminal(bridge, turnId, hydrated.turn.status, hydrated.threadStatus, failureNotBefore)) return terminalTurn({ turn: hydrated.turn }, turnId, bridge);
502
+ if (hydrated?.turn && isAuthoritativeTerminal(bridge, turnId, hydrated.turn.status, hydrated.threadStatus, failureNotBefore)) return terminalTurn({ turn: hydrated.turn }, sessionId, turnId, bridge);
459
503
  }
460
504
  // Some app-server builds emit a final agent message but omit both the
461
505
  // terminal event and the compact turn record. Wait briefly, hydrate once,
@@ -464,10 +508,10 @@ async function waitForTurn(bridge, sessionId, turnId, timeoutMs, options, cwd) {
464
508
  // Agent messages are streamed as completed items while a Turn is still
465
509
  // reasoning and calling tools. They are terminal evidence only after
466
510
  // app-server reports the Thread idle.
467
- if (final && stored?.threadStatus === "idle" && Date.now() - final.completedAt >= 1_000) {
511
+ if (final?.turnId === turnId && stored?.threadStatus === "idle" && Date.now() - final.completedAt >= 1_000) {
468
512
  const hydrated = await readTurn(bridge, sessionId, turnId, true);
469
- if (hydrated?.turn && isAuthoritativeTerminal(bridge, turnId, hydrated.turn.status, hydrated.threadStatus, failureNotBefore)) return terminalTurn({ turn: hydrated.turn }, turnId, bridge);
470
- return terminalTurn({ turn: { id: turnId, status: "completed", synthetic: true, final_message: final.item } }, turnId, bridge);
513
+ if (hydrated?.turn && isAuthoritativeTerminal(bridge, turnId, hydrated.turn.status, hydrated.threadStatus, failureNotBefore)) return terminalTurn({ turn: hydrated.turn }, sessionId, turnId, bridge);
514
+ return terminalTurn({ turn: { id: turnId, status: "completed", synthetic: true, final_message: final.item } }, sessionId, turnId, bridge);
471
515
  }
472
516
  nextStoredReadAt = Date.now() < failureNotBefore ? failureNotBefore : Date.now() + 15_000;
473
517
  }
@@ -476,15 +520,48 @@ async function waitForTurn(bridge, sessionId, turnId, timeoutMs, options, cwd) {
476
520
  fail("Codex turn timed out", "turn_timeout");
477
521
  }
478
522
 
479
- function terminalTurn(value, turnId, bridge) {
523
+ function terminalTurn(value, sessionId, turnId, bridge) {
480
524
  // A terminal event may arrive while an older snapshot RPC is in flight.
481
525
  const observed = bridge.turns.get(turnId);
482
- if (observed?.status === "completed") value = observed.value;
526
+ if (observed?.status === "completed" && (!observed.threadId || observed.threadId === sessionId)) value = observed.value;
483
527
  const turn = value?.turn ?? value;
484
- if (turn?.status && turn.status !== "completed") fail(`Codex turn ${turnId ?? "unknown"} ended as ${turn.status}`, "turn_interrupted", { turn_id: turn.id ?? turnId ?? null, terminal_status: turn.status, provider_error: turn.error ?? turn.codexErrorInfo ?? null });
528
+ if (turn?.id && turn.id !== turnId || value?.threadId && value.threadId !== sessionId) fail("Codex terminal Turn identity does not match the requested Session/Turn", "turn_identity_mismatch", { provider_session_id: sessionId, turn_id: turnId, native_thread_id: value?.threadId ?? null, native_turn_id: turn?.id ?? null });
529
+ const providerError = nativeTurnError(bridge, sessionId, turn);
530
+ if (turn?.status && turn.status !== "completed") fail(`Codex turn ${turnId ?? "unknown"} ended as ${turn.status}`, "turn_interrupted", { provider_session_id: sessionId, turn_id: turnId, native_turn_id: turn.id ?? null, terminal_status: turn.status, observed_at: now(),
531
+ provider_error: providerError, native_turn_items: turnItemEvidence(turn) });
485
532
  return value;
486
533
  }
487
534
 
535
+ function nativeTurnError(bridge, sessionId, turn) {
536
+ const native = bridge.nativeErrors?.get(`${sessionId}:${turn?.id}`);
537
+ const error = turn?.error && typeof turn.error === "object" && native?.error && typeof native.error === "object"
538
+ ? { ...native.error, ...turn.error } : turn?.error ?? native?.error ?? (turn?.codexErrorInfo ? { codexErrorInfo: turn.codexErrorInfo } : null);
539
+ return error && native?.willRetry !== undefined && typeof error === "object" ? { ...error, willRetry: native.willRetry } : error;
540
+ }
541
+
542
+ function turnItemEvidence(turn) {
543
+ const items = turn?.items;
544
+ return Array.isArray(items) && turn?.itemsView !== "notLoaded" ? { observed: true, count: items.length, types: items.map(item => item?.type ?? "unknown"),
545
+ possible_effects: items.some(item => !["agentMessage", "reasoning", "userMessage"].includes(item?.type)) } : { observed: false };
546
+ }
547
+
548
+ export function terminalCodexOverload(error, expectedSessionId, expectedTurnId) {
549
+ let current = error;
550
+ for (let depth = 0; depth < 4 && current && typeof current === "object"; depth += 1) {
551
+ if (current.code === "turn_interrupted") {
552
+ const details = current.details ?? {}, native = details.provider_error;
553
+ const code = native?.codexErrorInfo;
554
+ return details.terminal_status === "failed" && typeof details.provider_session_id === "string" && details.provider_session_id === expectedSessionId
555
+ && typeof details.turn_id === "string" && Boolean(details.turn_id) && details.native_turn_id === details.turn_id
556
+ && (expectedTurnId === undefined || details.turn_id === expectedTurnId)
557
+ && (code === "serverOverloaded" || code?.code === "serverOverloaded");
558
+ }
559
+ if (current.code !== "harness_adapter_failed") return false;
560
+ current = current.details?.cause ?? current.cause;
561
+ }
562
+ return false;
563
+ }
564
+
488
565
  function isTerminalTurnStatus(status) { return ["completed", "failed", "interrupted", "cancelled"].includes(status); }
489
566
  function isAuthoritativeTerminal(bridge, turnId, status, threadStatus, failureNotBefore) {
490
567
  if (!isTerminalTurnStatus(status)) return false;
@@ -518,7 +595,7 @@ async function readCompletedThread(bridge, sessionId) {
518
595
  }
519
596
 
520
597
  function latestAssistantText(value) {
521
- const messages = value?.messages ?? value?.items ?? value?.turn?.items ?? [];
598
+ const messages = value?.messages ?? value?.items ?? value?.turn?.items ?? (value?.turn?.final_message ? [value.turn.final_message] : []);
522
599
  if (!Array.isArray(messages)) return null;
523
600
  for (const message of [...messages].reverse()) {
524
601
  if (message?.role !== "assistant" && message?.type !== "agentMessage") continue;
@@ -538,7 +615,7 @@ export async function inspectSession(options) {
538
615
 
539
616
  export async function inspectSessionWithBridge(bridge, options) {
540
617
  const sessionId = required(options.sessionId, "--session-id"); const value = await bridge.request("thread/read", { threadId: sessionId, includeTurns: false }); const thread = value?.thread ?? value;
541
- return { harness: "codex-desktop", provider_session_id: sessionId, adapter_session_id: sessionId, status: thread.status ?? null, thread: threadReceipt(thread), descendants: directChildren(bridge, sessionId), requested_profile: profile(options), observed_profile: await observeCodexProfile(bridge, options, sessionId, { model: thread.model ?? null, reasoning: thread.reasoningEffort ?? null, provider: thread.modelProvider ?? null }, "thread/read") };
618
+ return { harness: "codex-desktop", provider_session_id: sessionId, adapter_session_id: sessionId, status: thread.status ?? null, thread: threadReceipt(thread), descendants: await observedChildren(bridge, sessionId), requested_profile: profile(options), observed_profile: await observeCodexProfile(bridge, options, sessionId, { model: thread.model ?? null, reasoning: thread.reasoningEffort ?? null, provider: thread.modelProvider ?? null }, "thread/read") };
542
619
  }
543
620
 
544
621
  export async function cancelSession(options) {
@@ -315,7 +315,7 @@ export class Runtime {
315
315
  // creation/end_turn receipt can then establish an idle root. Dispatch
316
316
  // clears that receipt before sending any subsequent productive request.
317
317
  const receipt = this.sessions.get(result?.provider_session_id)?.native_root_receipt;
318
- if (status === "unknown" && ["created", "end_turn", "cancelled"].includes(receipt?.kind)) return false;
318
+ if (status === "unknown" && ["created", "end_turn", "cancelled", "failed"].includes(receipt?.kind)) return false;
319
319
  return !["completed", "failed", "cancelled", "stopped", "idle"].includes(status);
320
320
  }
321
321
  async refreshTree(cancel = false) {
@@ -356,7 +356,17 @@ export class Runtime {
356
356
  const running = name === "session.archive" ? false : this.treeRunning(result);
357
357
  await this.persist({ active_tree: running, active_operation: null });
358
358
  return { ...result, descendants: this.topology(result.provider_session_id), settled: !running };
359
- } catch (error) { await this.persist({ active_tree: true, active_operation: null }); throw error; }
359
+ } catch (error) {
360
+ // A matched JSON-RPC error completes this native prompt request. It is
361
+ // root-turn proof, not proof that any descendants have stopped. A lost
362
+ // response, bridge error, or failure before dispatch earns no receipt.
363
+ const response = error?.details?.terminal_response;
364
+ if (name === "session.prompt" && response?.method === "session/prompt" && response.session_id === sessionId) {
365
+ const session = this.sessions.get(sessionId);
366
+ if (session) session.native_root_receipt = { kind: "failed", daemon_id: this.state.daemon_id, request_id: response.request_id };
367
+ }
368
+ await this.persist({ active_tree: true, active_operation: null }); throw error;
369
+ }
360
370
  finally { this.active = null; }
361
371
  }
362
372
  async dispatch(operation, params) {
@@ -68,8 +68,8 @@ export class OpenCodeClient {
68
68
  const native = reply.info.error;
69
69
  const message = native.data?.message ?? native.message ?? native.name ?? "OpenCode provider failed";
70
70
  const status = native.data?.statusCode;
71
- const code = status === 429 || /rate limit/i.test(message) ? "provider_rate_limited" : status === 402 || /payment required|quota|usage balance exhausted/i.test(message) ? "provider_quota_exhausted" : "opencode_provider_failed";
72
- throw new OpenCodeError(code, message, native.data?.isRetryable === true || code === "provider_rate_limited", { session_id: id, message_id: reply.info.id, provider_error: native });
71
+ const code = status === 402 || /payment required|quota|usage balance exhausted|insufficient.?credit|billing/i.test(message) ? "provider_quota_exhausted" : /rate.?limit|too many requests/i.test(message) ? "provider_rate_limited" : status === 429 ? "provider_limit_unknown" : "opencode_provider_failed";
72
+ throw new OpenCodeError(code, message, code === "provider_rate_limited", { session_id: id, message_id: reply.info.id, provider_error: native });
73
73
  }
74
74
  return reply;
75
75
  }
@@ -268,9 +268,11 @@ export class AcpBridge {
268
268
  const error = new Error(messageText);
269
269
  const nativeCode = typeof data?.code === "string" ? data.code : typeof data?.native_code === "string" ? data.native_code : null;
270
270
  const unknown = ["native_timeout", "native_backend_dead", "native_backend_pipe_broken", "native_outcome_unknown"].includes(nativeCode);
271
- 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";
272
- error.retryable = !unknown && /rate limit/i.test(messageText);
273
- error.details = { acp_code: message.error.code ?? null, ...(data ? { native: data } : {}), ...(details ? { provider_details: details } : {}), ...(providerDetails ? { provider_update: provider } : {}) };
271
+ 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";
272
+ error.retryable = error.code === "provider_rate_limited";
273
+ error.details = { acp_code: message.error.code ?? null,
274
+ terminal_response: { method: pending.method, session_id: pending.sessionId, request_id: message.id },
275
+ ...(data ? { native: data } : {}), ...(details ? { provider_details: details } : {}), ...(providerDetails ? { provider_update: provider } : {}) };
274
276
  pending.reject(error);
275
277
  }
276
278
  else { pending.resolve(message.result ?? {}); }
@@ -9,11 +9,51 @@ import { assertRunMutationAllowed } from "./run-recovery.js";
9
9
  import { AppError } from "../shared/errors.js";
10
10
  import { confirmManagedProcess, finishManagedProcess, heartbeatManagedProcess, managedProcessStatus, managedProcessRuntimeHome, managedOwnerIsAlive, processTreeIsAlive, registerManagedProcess, reservePorts } from "./managed-processes.js";
11
11
  import { admitRuntimeCheck, runtimeBudgetForRun } from "./runtime-budget.js";
12
+ import { getResourceDatabase } from "../storage/database.js";
12
13
  const profileRelativePath = path.join(".memory-bank", "spec", "engineering", "code-check-profile.json");
13
14
  // Receipt reservation precedes child spawn so an interrupted CLI leaves an
14
15
  // auditable attempt. A second caller must not mistake that small handoff window
15
16
  // for a dead check and overwrite its outcome.
16
17
  const receiptStartingGraceMs = 30_000;
18
+ /** The check writer proves an empty tree before marking its registration terminal. */
19
+ export function terminalCheckSettled(context, record) {
20
+ if (record.kind !== "check" || !record.check_id || record.owner_id !== `check:${record.check_id}` || !record.project_id || !record.run_id
21
+ || !record.pid || !record.pid_started_at || !["stopped", "failed"].includes(record.state) || !record.finished_at)
22
+ return null;
23
+ let metadata;
24
+ try {
25
+ metadata = JSON.parse(record.metadata_json);
26
+ }
27
+ catch {
28
+ return null;
29
+ }
30
+ if (metadata.role !== "check" || managedProcessRuntimeHome(record, context) !== context.ddFlowHome)
31
+ return null;
32
+ let resource;
33
+ try {
34
+ resource = getResourceDatabase(context.env.DD_FLOW_RESOURCE_HOME ?? context.ddFlowHome, "read_existing");
35
+ }
36
+ catch {
37
+ return null;
38
+ }
39
+ try {
40
+ const current = resource.get("SELECT * FROM managed_processes WHERE id = ?", [record.id]);
41
+ if (!current || current.lease_token !== record.lease_token || current.owner_id !== record.owner_id || current.pid !== record.pid
42
+ || current.pid_started_at !== record.pid_started_at || current.metadata_json !== record.metadata_json || current.state !== record.state
43
+ || current.project_id !== record.project_id || current.run_id !== record.run_id || current.work_id !== record.work_id
44
+ || current.check_id !== record.check_id || current.finished_at !== record.finished_at)
45
+ return null;
46
+ const receipt = context.db.get("SELECT status, finished_at, work_id FROM check_receipts WHERE id = ? AND project_id = ? AND run_id = ?", [record.check_id, record.project_id, record.run_id]);
47
+ return receipt && receipt.work_id === record.work_id && ["passed", "failed", "aborted"].includes(receipt.status) && receipt.finished_at
48
+ ? { status: receipt.status, finished_at: receipt.finished_at } : null;
49
+ }
50
+ catch {
51
+ return null;
52
+ }
53
+ finally {
54
+ resource.close?.();
55
+ }
56
+ }
17
57
  export function codeExecutionEnvironment(workspaceRoot) {
18
58
  const suffix = crypto.createHash("sha256").update(path.resolve(workspaceRoot)).digest("hex").slice(0, 12);
19
59
  // The managed CLI's Node must win over a provider app's PATH: a Rosetta
@@ -0,0 +1,71 @@
1
+ import { setTimeout as delay } from "node:timers/promises";
2
+ import { terminalCodexOverload } from "../harness-runtime/lib/dd-codex.mjs";
3
+ /** Only a terminal, effect-free native Turn may get another productive Turn. */
4
+ export async function continueCodexCapacity(input) {
5
+ let { error, operationId } = input;
6
+ for (let ordinal = 1; ordinal <= 2; ordinal += 1) {
7
+ if (!terminalCodexOverload(error, input.sessionId)) {
8
+ if (ordinal === 1)
9
+ return null;
10
+ throw error;
11
+ }
12
+ let prior;
13
+ try {
14
+ prior = await input.inspectOperation(operationId);
15
+ }
16
+ catch {
17
+ if (ordinal === 1)
18
+ return null;
19
+ throw error;
20
+ }
21
+ const failure = prior.error;
22
+ const turnId = failure?.details?.turn_id;
23
+ if (prior.operation_id !== operationId || prior.operation !== "session.prompt" || prior.session_id !== input.sessionId
24
+ || prior.state !== "failed" || prior.primary_error || !turnId || !terminalCodexOverload(failure, input.sessionId, turnId)
25
+ || failure.details?.native_turn_items?.observed !== true || failure.details.native_turn_items.possible_effects !== false) {
26
+ if (ordinal === 1)
27
+ return null;
28
+ throw error;
29
+ }
30
+ const settled = async () => {
31
+ const seen = await input.inspectSession();
32
+ const settlement = seen.settlement;
33
+ const ids = Array.isArray(seen.tree_session_ids) ? seen.tree_session_ids : [];
34
+ const observations = Array.isArray(seen.tree_observations) ? seen.tree_observations : [];
35
+ const treeProven = input.allowSettledDescendants
36
+ ? ids.length > 0 && ids[0] === input.sessionId && observations.length === ids.length
37
+ && ids.every(id => observations.some(item => item.provider_session_id === id && item.settled === true))
38
+ && Array.isArray(seen.unsettled_sessions) && seen.unsettled_sessions.length === 0
39
+ : ids.length === 1 && ids[0] === input.sessionId;
40
+ return seen.provider_session_id === input.sessionId && seen.settled === true && treeProven
41
+ && (!settlement || ["settled", "not_required"].includes(settlement.state ?? ""));
42
+ };
43
+ input.assertCurrent();
44
+ try {
45
+ if (!await settled()) {
46
+ if (ordinal === 1)
47
+ return null;
48
+ throw error;
49
+ }
50
+ }
51
+ catch {
52
+ if (ordinal === 1)
53
+ return null;
54
+ throw error;
55
+ }
56
+ const nextId = `${input.operationId}:capacity:${ordinal}:session.prompt`;
57
+ input.retainIntent({ predecessor: operationId, operation_id: nextId, turn_id: turnId, ordinal, not_before: new Date(Date.now() + (ordinal === 1 ? 5_000 : 15_000)).toISOString() });
58
+ await delay(ordinal === 1 ? 5_000 : 15_000);
59
+ input.assertCurrent();
60
+ if (!await settled().catch(() => false))
61
+ throw error;
62
+ try {
63
+ return await input.prompt(nextId, input.continuationText ?? "Continue the same assigned task in this Session. Check current durable RUN, Work and Stage state first. Never repeat an observed lifecycle command, accepted HITL answer, check or merge apply; use only an exact issued successor. Stop at the assigned boundary.", ordinal);
64
+ }
65
+ catch (nextError) {
66
+ error = nextError;
67
+ operationId = nextId;
68
+ }
69
+ }
70
+ throw error;
71
+ }
@@ -136,7 +136,10 @@ export function controllerNativeChildren(receipt, rootSessionId) {
136
136
  const status = ["completed", "complete", "done", "success", "succeeded"].includes(state) ? "completed" : state === "settled_by_root" ? "settled_by_root" : ["failed", "error"].includes(state) ? "failed" : ["cancelled", "canceled", "interrupted"].includes(state) ? "cancelled" : ["running", "active", "pending"].includes(state) ? "running" : fallback;
137
137
  const rootChild = parent === rootSessionId;
138
138
  const settled = rootChild && status === "unknown" && settledTree.has(id) ? "settled_by_root" : status;
139
- candidates.push({ session_id: id, parent_session_id: parent ?? "", status: settled, source, ...(rootChild ? {} : { provenance: "parent_mismatch" }) });
139
+ candidates.push({ session_id: id, parent_session_id: parent ?? "", status: settled, source, ...(rootChild ? {} : { provenance: "parent_mismatch" }),
140
+ ...(text(value, ["terminal_turn_id"]) ? { terminal_turn_id: text(value, ["terminal_turn_id"]) } : {}),
141
+ ...(value.provider_error !== undefined ? { provider_error: value.provider_error } : {}),
142
+ ...(value.native_turn_items && typeof value.native_turn_items === "object" ? { native_turn_items: value.native_turn_items } : {}) });
140
143
  };
141
144
  for (const value of list(receipt.descendants))
142
145
  add(value, "adapter.descendants");
@@ -166,6 +169,15 @@ export function controllerNativeChildren(receipt, rootSessionId) {
166
169
  }
167
170
  if (previous && rank[previous.status] === 3 && rank[child.status] === 3 && previous.status !== child.status)
168
171
  throw new AppError("native_child_outcome_conflict", "Native child has contradictory terminal outcomes", 1, { session_id: child.session_id, observations: [previous, child] });
172
+ if (previous && previous.status === child.status && previous.terminal_turn_id && child.terminal_turn_id && previous.terminal_turn_id !== child.terminal_turn_id)
173
+ throw new AppError("native_child_turn_conflict", "Native child has contradictory terminal Turn identities", 1, { session_id: child.session_id, observations: [previous, child] });
174
+ if (previous && previous.status === child.status) {
175
+ unique.set(child.session_id, { ...previous, ...child,
176
+ ...(child.terminal_turn_id ?? previous.terminal_turn_id ? { terminal_turn_id: child.terminal_turn_id ?? previous.terminal_turn_id } : {}),
177
+ ...(child.provider_error ?? previous.provider_error ? { provider_error: child.provider_error ?? previous.provider_error } : {}),
178
+ ...(child.native_turn_items ?? previous.native_turn_items ? { native_turn_items: child.native_turn_items ?? previous.native_turn_items } : {}) });
179
+ continue;
180
+ }
169
181
  if (!previous || rank[child.status] > rank[previous.status])
170
182
  unique.set(child.session_id, child);
171
183
  }
@@ -16,6 +16,8 @@ import { reviewExecutionCopyHash, reviewSourceFilter, reviewSourceHash, reviewSo
16
16
  import { workspaceGitFiles } from "./workspace-files.js";
17
17
  import { qualifyAgyHooks } from "../harness-runtime/lib/dd-agy-daemon.mjs";
18
18
  import { lifecycleRetryInstruction } from "../harness-runtime/lib/delegation-instructions.mjs";
19
+ import { continueCodexCapacity } from "./codex-capacity.js";
20
+ import { errorRecord } from "./run-controller-state.js";
19
21
  /**
20
22
  * Creates an external provider root for one already-created Work. The launch
21
23
  * is not a Work claim: only the worker's trusted `work start` hook binds it.
@@ -91,7 +93,7 @@ export async function launchExternalWork(context, input) {
91
93
  const common = ["--dd-flow-bin", flowExecutable, "--state-dir", root, "--journal", journal, "--cwd", isolatedWorkspace ?? run.workspace_root, "--provider", resolved.worker.provider, "--model", resolved.worker.model, "--reasoning", resolved.worker.reasoning, "--mode", resolved.worker.mode, "--permission", resolved.worker.permission, "--project-root", run.project_root, "--dd-flow-home", context.ddFlowHome, ...harnessRuntimeArguments(context.ddFlowHome, runtimeHarness(resolved.worker)), "--json"];
92
94
  const evidence = (value) => fs.appendFileSync(journal, `${JSON.stringify({ type: "adapter_call", at: context.now(), ...value })}\n`);
93
95
  try {
94
- const call = (args, timeoutMs) => runHarnessAdapter({ executable, args, env: runtimeContext.env, operationId: `${operationId}:${args[0]}.${args[1]}`, runtimeOwner, ...(timeoutMs ? { timeoutMs } : {}), ...(input.progress ? { progress: input.progress } : {}), onEvidence: evidence });
96
+ const call = (args, timeoutMs, id = `${operationId}:${args[0]}.${args[1]}`) => runHarnessAdapter({ executable, args, env: runtimeContext.env, operationId: id, runtimeOwner, ...(timeoutMs ? { timeoutMs } : {}), ...(input.progress ? { progress: input.progress } : {}), onEvidence: evidence });
95
97
  await call(["daemon", "start", ...common]);
96
98
  assertRunMutationAllowed(context, work.project_id, work.run_id, generation);
97
99
  const created = await call(["session", "create", ...common], runtimeHarness(resolved.worker) === "zcode-acp" ? 180_000 : undefined);
@@ -108,13 +110,45 @@ export async function launchExternalWork(context, input) {
108
110
  throw new AppError("read_only_input_changed", "Authoritative review workspace changed after the execution copy was made", 2, { work_id: work.work_id });
109
111
  const promptFile = path.join(root, "launch.md");
110
112
  fs.writeFileSync(promptFile, `Start the assigned Work now. Your first tool call must be this exact standalone command:\n\n${workStartCommand(runtimeContext, work)}\n\nUse the packet returned by that command. Do not claim another Work or run this command more than once.\n\n${lifecycleRetryInstruction}${isolatedWorkspace ? `\n\nThe review copy has no .git. Read the captured status/diff or gitless note in ${path.join(isolatedWorkspace, ".dd-flow", "review-evidence")}; do not run Git inside the copy.\n` : "\n"}`);
111
- const prompted = await call(["session", "prompt", "--session-id", sessionId, "--prompt-file", promptFile, ...common], 45 * 60_000);
113
+ const promptOperationId = `${operationId}:session.prompt`;
114
+ let prompted;
115
+ try {
116
+ prompted = await call(["session", "prompt", "--session-id", sessionId, "--prompt-file", promptFile, ...common], 45 * 60_000);
117
+ }
118
+ catch (error) {
119
+ if (runtimeHarness(resolved.worker) !== "codex-desktop")
120
+ throw error;
121
+ const resumed = await continueCodexCapacity({ error, sessionId, operationId: promptOperationId,
122
+ inspectOperation: id => call(["daemon", "operation", "--state-dir", root, "--operation-id", id, "--json"], 30_000, `${id}:capacity-observe`),
123
+ inspectSession: () => call(["session", "inspect", "--session-id", sessionId, ...common], 30_000, `${operationId}:capacity-inspect`),
124
+ assertCurrent: () => {
125
+ assertRunMutationAllowed(context, work.project_id, work.run_id, generation);
126
+ const current = context.db.get("SELECT w.status, l.status AS launch_status FROM works w JOIN work_launches l ON l.work_id = w.work_id WHERE w.work_id = ? AND l.operation_id = ?", [work.work_id, operationId]);
127
+ if (current?.status !== "created" || current.launch_status !== "launching")
128
+ throw new AppError("capacity_continuation_blocked", "External Work changed before its native continuation", 1, { work_id: work.work_id, state: current });
129
+ if (reviewCopy && reviewSourceHash(run.workspace_root, reviewInputs, reviewBase) !== reviewCopy.source_sha256)
130
+ throw new AppError("read_only_input_changed", "Authoritative review workspace changed during capacity wait", 2);
131
+ },
132
+ retainIntent: intent => {
133
+ const changed = context.db.run("UPDATE work_launches SET adapter_receipt_json = ?, updated_at = ? WHERE operation_id = ? AND status = 'launching'", [JSON.stringify({ create: created, capacity_intent: intent }), context.now(), operationId]);
134
+ if (changed.changes !== 1)
135
+ throw new AppError("capacity_continuation_blocked", "External Work launch owner changed", 1);
136
+ },
137
+ prompt: (id, text, ordinal) => {
138
+ const file = path.join(root, `capacity-${ordinal}.md`);
139
+ fs.writeFileSync(file, `${text}\n`, { flag: "wx", mode: 0o600 });
140
+ return call(["session", "prompt", "--session-id", sessionId, "--prompt-file", file, ...common], 45 * 60_000, id);
141
+ } });
142
+ if (!resumed)
143
+ throw error;
144
+ prompted = resumed;
145
+ }
112
146
  assertRunMutationAllowed(context, work.project_id, work.run_id, generation);
113
147
  context.db.run("UPDATE work_launches SET status = 'start_requested', provider_session_id = ?, adapter_receipt_json = ?, updated_at = ? WHERE operation_id = ?", [sessionId, JSON.stringify({ create: created, prompt: prompted }), context.now(), operationId]);
114
148
  return { ok: true, work_id: work.work_id, operation_id: operationId, status: "start_requested", session: { harness_id: runtimeHarness(resolved.worker), session_id: sessionId }, profile_hash: resolved.profile_hash, journal_path: journal };
115
149
  }
116
150
  catch (error) {
117
- context.db.run("UPDATE work_launches SET status = 'failed', adapter_receipt_json = ?, updated_at = ? WHERE operation_id = ?", [JSON.stringify({ error: String(error) }), context.now(), operationId]);
151
+ context.db.run("UPDATE work_launches SET status = 'failed', adapter_receipt_json = ?, updated_at = ? WHERE operation_id = ?", [JSON.stringify({ error: errorRecord(error) }), context.now(), operationId]);
118
152
  throw error;
119
153
  }
120
154
  }