@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 +7 -0
- package/dist/build-info.json +3 -3
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +3 -2
- package/dist/harness-runtime/lib/dd-codex.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-codex.mjs +100 -23
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +12 -2
- package/dist/harness-runtime/lib/dd-opencode.mjs +2 -2
- package/dist/harness-runtime/lib/dd-zcode.mjs +5 -3
- package/dist/services/code-checks.js +40 -0
- package/dist/services/codex-capacity.js +71 -0
- package/dist/services/controller-fanout.js +13 -1
- package/dist/services/external-work-launch.js +37 -3
- package/dist/services/managed-processes.js +42 -6
- package/dist/services/merge-server.js +34 -4
- package/dist/services/run-control.js +7 -1
- package/dist/services/run-controller-adapter.js +20 -2
- package/dist/services/run-controller-recovery.js +47 -3
- package/dist/services/run-controller.js +105 -10
- package/dist/services/run-recovery-runtime.js +43 -2
- package/dist/services/runtime-scope-stop.js +23 -2
- package/dist/services/vnext-code.js +7 -1
- package/dist/services/vnext-plan.js +29 -14
- package/package.json +1 -1
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
|
package/dist/build-info.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"cli_package": "@deksden-com/dd-flow-cli",
|
|
3
|
-
"cli_version": "0.9.0-beta.
|
|
4
|
-
"cli_commit": "
|
|
5
|
-
"built_at": "2026-09-
|
|
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
|
|
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,
|
|
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 === "
|
|
124
|
-
|
|
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
|
-
|
|
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" ?
|
|
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" ?
|
|
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
|
-
|
|
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:
|
|
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?.
|
|
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:
|
|
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) {
|
|
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 ===
|
|
72
|
-
throw new OpenCodeError(code, message,
|
|
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 : /
|
|
272
|
-
error.retryable =
|
|
273
|
-
error.details = { acp_code: message.error.code ?? null,
|
|
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:
|
|
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
|
|
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:
|
|
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
|
}
|