@deksden-com/dd-flow-cli 0.9.0-beta.141 → 0.9.0-beta.143
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 +17 -0
- package/dist/build-info.json +7 -7
- package/dist/cli/command-inputs.js +1 -1
- package/dist/cli/command-option-descriptions.js +1 -0
- package/dist/cli/help.js +1 -0
- package/dist/cli/run-cli.js +9 -3
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +8 -3
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +29 -8
- package/dist/harness-runtime/lib/dd-codex.mjs +184 -51
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +26 -7
- package/dist/schemas/code-review-decision.schema.json +9 -0
- package/dist/schemas/flow-run.schema.json +1 -0
- package/dist/schemas/plan-review-decision.schema.json +9 -0
- package/dist/services/accepted-intent.js +127 -0
- package/dist/services/check-runner-evidence.js +2 -0
- package/dist/services/code-checks.js +255 -65
- package/dist/services/repair-intents.js +1 -1
- package/dist/services/run-control.js +36 -1
- package/dist/services/runtime-budget.js +16 -0
- package/dist/services/stage-pause.js +30 -0
- package/dist/services/verification-matrix.js +20 -2
- package/dist/services/vnext-code-review.js +14 -2
- package/dist/services/vnext-plan-review.js +6 -1
- package/dist/services/vnext-protocolize.js +2 -0
- package/dist/services/vnext-specify.js +4 -0
- package/dist/services/work-registry.js +4 -2
- package/dist/storage/database.js +4 -0
- package/package.json +1 -1
- package/tools/probe-codex-history.mjs +37 -0
|
@@ -4,7 +4,7 @@ import { appendFile, mkdir, readFile } from "node:fs/promises";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import readline from "node:readline";
|
|
6
6
|
import { closeBridgeProcess } from "./managed-daemon.mjs";
|
|
7
|
-
import { errorRecord, isProvenShellCorrection } from "./operation-errors.mjs";
|
|
7
|
+
import { errorRecord, isObservationLoss, isProvenShellCorrection } from "./operation-errors.mjs";
|
|
8
8
|
import { reportedFailure, commandText } from "./process-json.mjs";
|
|
9
9
|
import { ObservationClock, observedTimeout, validateObservationDuration } from "./observation-clock.mjs";
|
|
10
10
|
import { observeModel } from "./model-observations.mjs";
|
|
@@ -12,9 +12,105 @@ import { classifyCodexItem } from "./tool-observations.mjs";
|
|
|
12
12
|
import { readNativeLines, streamNativeDiagnostics, nativeObservationError } from "./native-streams.mjs";
|
|
13
13
|
import { operationProgressReporter } from "./operation-progress.mjs";
|
|
14
14
|
import { publishSetup } from './setup-progress.mjs';
|
|
15
|
+
import { capacityBackoff } from './provider-overload-policy.mjs';
|
|
15
16
|
|
|
16
17
|
export const CODEX_REQUEST_TIMEOUT_MS = 120_000;
|
|
17
18
|
export const LATE_RESPONSE_GRACE_MS = 5_000;
|
|
19
|
+
export function codexReadPhase(bridge, options = {}) {
|
|
20
|
+
const timeoutMs = Math.min(CODEX_REQUEST_TIMEOUT_MS, options.livenessTimeoutMs ?? options.timeoutMs ?? CODEX_REQUEST_TIMEOUT_MS);
|
|
21
|
+
return { clock: new ObservationClock({ timeoutMs, ...options.observationClock,
|
|
22
|
+
onGap: gap => bridge.journal?.write("observation_gap", { ...gap, phase: "metadata_read" }) }),
|
|
23
|
+
random: options.readRandom ?? Math.random,
|
|
24
|
+
sleep: options.readSleep ?? (ms => new Promise(resolve => setTimeout(resolve, ms))) };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function dispatchBudget(phase, method, limitMs) {
|
|
28
|
+
const clock = phase.outerClock ?? phase.clock;
|
|
29
|
+
if (clock.sample(phase.progress?.())) throw Object.assign(new Error("Codex owner observation window expired before dispatch"), {
|
|
30
|
+
code: clock.observationLost ? "operation_observation_lost" : "rpc_timeout",
|
|
31
|
+
details: { method, phase: "before_dispatch", effect: "no_effect", observation: clock.state() }
|
|
32
|
+
});
|
|
33
|
+
const remaining = Math.max(1, Math.ceil(clock.timeoutMs - clock.elapsed));
|
|
34
|
+
const timeoutMs = Math.min(limitMs, remaining);
|
|
35
|
+
return [timeoutMs, { graceMs: Math.min(LATE_RESPONSE_GRACE_MS, remaining - timeoutMs) }];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function readCodexRpc(bridge, method, params, phase = codexReadPhase(bridge)) {
|
|
39
|
+
if (!["thread/read", "thread/turns/list"].includes(method)) throw new Error("Codex read policy only accepts read-only RPC");
|
|
40
|
+
const sample = () => phase.clock.sample(phase.progress?.());
|
|
41
|
+
let primary;
|
|
42
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
43
|
+
if (sample()) throw primary ?? Object.assign(new Error("Codex metadata observation window expired"), { code: phase.clock.observationLost ? "operation_observation_lost" : "rpc_timeout", details: { method, phase: "metadata_read", observation: phase.clock.state() } });
|
|
44
|
+
const remaining = Math.min(CODEX_REQUEST_TIMEOUT_MS, Math.max(1, Math.ceil(phase.clock.timeoutMs - phase.clock.elapsed)));
|
|
45
|
+
try {
|
|
46
|
+
const result = await bridge.request(method, params, remaining, { graceMs: 0, observe: () => { sample(); phase.outerClock?.sample(); } });
|
|
47
|
+
if (!result || typeof result !== "object" || Array.isArray(result)
|
|
48
|
+
|| method === "thread/read" && (!result.thread || typeof result.thread !== "object" || Array.isArray(result.thread) || typeof result.thread.id !== "string" || !result.thread.id)
|
|
49
|
+
|| method === "thread/turns/list" && !Array.isArray(result.data))
|
|
50
|
+
throw Object.assign(new Error("Codex read returned invalid data"), { code: "rpc_schema_invalid", details: { method } });
|
|
51
|
+
if (method === "thread/read" && result.thread.id !== params.threadId)
|
|
52
|
+
throw Object.assign(new Error("Codex read returned a foreign Thread"), { code: "thread_identity_mismatch" });
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
primary ??= error; bridge.assertHookHealthy?.(params.threadId);
|
|
57
|
+
// A broken process cannot be replaced under the guise of a read retry.
|
|
58
|
+
const current = bridge.currentTurns?.get(params.threadId);
|
|
59
|
+
const materializing = method === "thread/read" && current && !bridge.turns?.has(current)
|
|
60
|
+
&& bridge.nativeHookTurns?.get(params.threadId) !== current && error?.code === "rpc_error" && error.details?.code === -32603;
|
|
61
|
+
const emptyMetadata = materializing && error.details.message?.match(/^failed to read thread: thread-store internal error: failed to read session metadata (.+): rollout at (.+) is empty$/);
|
|
62
|
+
const transient = error?.code === "rpc_timeout" || error?.code === "EAGAIN" || error?.code === "EINTR"
|
|
63
|
+
|| Boolean(emptyMetadata && emptyMetadata[1] === emptyMetadata[2] && emptyMetadata[1].endsWith(`-${params.threadId}.jsonl`));
|
|
64
|
+
if (!transient) { if (error !== primary) error.previous_read_error = errorRecord(primary); throw error; }
|
|
65
|
+
if (attempt === 2 || sample()) { if (error !== primary) primary.secondary_error = errorRecord(error); throw primary; }
|
|
66
|
+
const delay = Math.min(capacityBackoff(error, attempt + 1, phase.clock.wall(), phase.random), Math.max(0, phase.clock.timeoutMs - phase.clock.elapsed));
|
|
67
|
+
bridge.journal?.write("thread_read_retry", { method, session_id: params.threadId, attempt: attempt + 1, delay_ms: delay, error: errorRecord(error) });
|
|
68
|
+
await phase.sleep(delay);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Availability belongs to this read of this Thread, not to the whole binary.
|
|
74
|
+
export async function readCodexHistory(bridge, method, params, phase = codexReadPhase(bridge)) {
|
|
75
|
+
if (method !== "thread/turns/list" && !(method === "thread/read" && params.includeTurns === true))
|
|
76
|
+
throw new Error("Codex history policy only accepts history reads");
|
|
77
|
+
let result, error;
|
|
78
|
+
try {
|
|
79
|
+
result = await readCodexRpc(bridge, method, params, phase);
|
|
80
|
+
if (method === "thread/read" && result.thread.turns !== undefined && !Array.isArray(result.thread.turns))
|
|
81
|
+
fail("Codex history read returned invalid turns", "rpc_schema_invalid", { method });
|
|
82
|
+
}
|
|
83
|
+
catch (cause) {
|
|
84
|
+
bridge.assertHookHealthy?.(params.threadId);
|
|
85
|
+
const native = cause?.details;
|
|
86
|
+
const operation = method === "thread/read" ? "includeTurns" : "thread/turns/list";
|
|
87
|
+
const unsupported = native?.code === -32601 && native.message === "list_turns is not supported yet";
|
|
88
|
+
const unmaterialized = native?.code === -32600 && native.message ===
|
|
89
|
+
`thread ${params.threadId} is not materialized yet; ${operation} is unavailable before first user message`;
|
|
90
|
+
if (cause?.code !== "rpc_error" || !unsupported && !unmaterialized) throw cause;
|
|
91
|
+
error = errorRecord(cause);
|
|
92
|
+
}
|
|
93
|
+
const available = !error;
|
|
94
|
+
phase.history ??= new Map();
|
|
95
|
+
const key = `${params.threadId}:${method}`;
|
|
96
|
+
if (phase.history.get(key)?.available !== available) bridge.journal?.write("history_availability", {
|
|
97
|
+
method, session_id: params.threadId, available, ...(error ? { error } : {})
|
|
98
|
+
});
|
|
99
|
+
phase.history.set(key, { available, error });
|
|
100
|
+
return { available, result, error };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function retainedCodexTurn(bridge, sessionId, turnId = bridge.currentTurns?.get(sessionId)) {
|
|
104
|
+
if (!turnId) return null;
|
|
105
|
+
const retained = bridge.turns?.get(turnId);
|
|
106
|
+
if (!retained) return null;
|
|
107
|
+
if (retained.threadId && retained.threadId !== sessionId || retained.value?.threadId && retained.value.threadId !== sessionId)
|
|
108
|
+
fail("Codex retained Thread identity differs", "turn_identity_mismatch");
|
|
109
|
+
if (!retained.threadId && !retained.value?.threadId) return null;
|
|
110
|
+
const turn = retained.value?.turn;
|
|
111
|
+
if (turn?.id !== turnId) fail("Codex retained Turn identity differs", "turn_identity_mismatch");
|
|
112
|
+
return turn;
|
|
113
|
+
}
|
|
18
114
|
const CODEX_SESSION_CONFIG = Object.freeze({
|
|
19
115
|
bypass_hook_trust: true,
|
|
20
116
|
"features.plugins": false,
|
|
@@ -158,7 +254,7 @@ export class CodexBridge {
|
|
|
158
254
|
// retain a compact read receipt instead of writing the same transcript on
|
|
159
255
|
// every status poll.
|
|
160
256
|
const request = message.id !== undefined ? this.pending.get(message.id) : null;
|
|
161
|
-
if (request?.method === "thread/read") {
|
|
257
|
+
if (request?.method === "thread/read" && !message.error) {
|
|
162
258
|
const thread = message.result?.thread ?? message.result ?? {};
|
|
163
259
|
this.journal.write("inbound", { jsonrpc: message.jsonrpc, id: message.id, result: { thread: {
|
|
164
260
|
id: thread.id ?? null, status: thread.status ?? null, model: thread.model ?? null, modelProvider: thread.modelProvider ?? null, reasoningEffort: thread.reasoningEffort ?? null, updatedAt: thread.updatedAt ?? null,
|
|
@@ -410,7 +506,7 @@ export class CodexBridge {
|
|
|
410
506
|
this.send({ jsonrpc: "2.0", id: message.id, error });
|
|
411
507
|
}
|
|
412
508
|
|
|
413
|
-
request(method, params = {}, timeoutMs = this.options.timeoutMs ?? CODEX_REQUEST_TIMEOUT_MS) {
|
|
509
|
+
request(method, params = {}, timeoutMs = this.options.timeoutMs ?? CODEX_REQUEST_TIMEOUT_MS, policy = {}) {
|
|
414
510
|
const turnFloor = method === "turn/start" ? new Set([this.currentTurns?.get(params.threadId), ...[...this.turns].filter(([, value]) => value.threadId === params.threadId).map(([id]) => id)].filter(Boolean)) : null;
|
|
415
511
|
if (method === "turn/start" && params.threadId) {
|
|
416
512
|
// Only one productive root Turn is admitted by the daemon fence. Drop
|
|
@@ -422,16 +518,18 @@ export class CodexBridge {
|
|
|
422
518
|
}
|
|
423
519
|
const id = this.nextId++;
|
|
424
520
|
return new Promise((resolve, reject) => {
|
|
425
|
-
const timer = observedTimeout(
|
|
521
|
+
const timer = observedTimeout(clock => {
|
|
426
522
|
const pending = this.pending.get(id); if (!pending || pending.timedOut) return;
|
|
427
523
|
pending.timedOut = true;
|
|
428
|
-
const
|
|
524
|
+
const graceMs = policy.graceMs ?? LATE_RESPONSE_GRACE_MS;
|
|
525
|
+
const timeoutError = Object.assign(new Error(`${method} timed out`), { code: clock.observationLost ? "operation_observation_lost" : "rpc_timeout", details: { method, request_id: id, timeout_ms: timeoutMs, grace_ms: graceMs, observation: clock.state() } });
|
|
429
526
|
this.journal.write("request_deadline", timeoutError.details);
|
|
527
|
+
if (!graceMs) { this.pending.delete(id); reject(timeoutError); return; }
|
|
430
528
|
pending.timer = observedTimeout(() => {
|
|
431
529
|
if (this.pending.get(id) !== pending) return;
|
|
432
530
|
this.pending.delete(id); reject(timeoutError);
|
|
433
|
-
},
|
|
434
|
-
}, timeoutMs, { progress: () => this.pending.get(id)?.progress, onGap: gap => this.journal.write("observation_gap", { ...gap, method, request_id: id }) });
|
|
531
|
+
}, graceMs, { progress: () => pending.progress });
|
|
532
|
+
}, timeoutMs, { progress: () => { policy.observe?.(); return this.pending.get(id)?.progress; }, onGap: gap => this.journal.write("observation_gap", { ...gap, method, request_id: id }) });
|
|
435
533
|
const publish = operationProgressReporter();
|
|
436
534
|
this.pending.set(id, { id, resolve, reject, timer, method, threadId: params.threadId, turnFloor });
|
|
437
535
|
if (method === "turn/start" && params.threadId) {
|
|
@@ -467,12 +565,12 @@ async function withBridge(options, action) {
|
|
|
467
565
|
function profile(options) { return { harness: "codex-desktop", model: options.model ?? null, reasoning: options.reasoning ?? null }; }
|
|
468
566
|
function threadId(result) { const id = result?.thread?.id ?? result?.threadId; return required(id, "Codex thread id"); }
|
|
469
567
|
function directChildren(bridge, sessionId) { return typeof bridge?.directChildren === "function" ? bridge.directChildren(sessionId) : []; }
|
|
470
|
-
async function observedChildren(bridge, sessionId) {
|
|
568
|
+
async function observedChildren(bridge, sessionId, phase) {
|
|
471
569
|
const children = directChildren(bridge, sessionId);
|
|
472
570
|
let reads = 0;
|
|
473
571
|
return await Promise.all(children.map(async child => {
|
|
474
572
|
if (child.status !== "failed" || !child.terminal_turn_id || reads++ >= 16) return child;
|
|
475
|
-
const observed = await readTurn(bridge, child.provider_session_id, child.terminal_turn_id, true);
|
|
573
|
+
const observed = await readTurn(bridge, child.provider_session_id, child.terminal_turn_id, true, phase);
|
|
476
574
|
const turn = observed?.turn;
|
|
477
575
|
if (turn?.id !== child.terminal_turn_id || turn.status !== child.status) return child;
|
|
478
576
|
return { ...child, provider_error: nativeTurnError(bridge, child.provider_session_id, turn) ?? child.provider_error,
|
|
@@ -481,8 +579,8 @@ async function observedChildren(bridge, sessionId) {
|
|
|
481
579
|
}
|
|
482
580
|
function codexSessionConfig() { return { ...CODEX_SESSION_CONFIG }; }
|
|
483
581
|
|
|
484
|
-
async function requestConfiguredThread(bridge, method, params, timeoutMs) {
|
|
485
|
-
try { return await bridge.request(method, { ...params, config: codexSessionConfig() }, timeoutMs); }
|
|
582
|
+
async function requestConfiguredThread(bridge, method, params, timeoutMs, policy) {
|
|
583
|
+
try { return await bridge.request(method, { ...params, config: codexSessionConfig() }, timeoutMs, policy); }
|
|
486
584
|
catch (error) {
|
|
487
585
|
const data = error?.details?.data;
|
|
488
586
|
const key = data?.configKey ?? data?.config_key ?? null;
|
|
@@ -506,7 +604,7 @@ function threadReceipt(thread) {
|
|
|
506
604
|
|
|
507
605
|
export async function doctor(options = {}) {
|
|
508
606
|
const codex_cli = parseCodexCliVersion(await commandText(options.bin ?? process.env.DD_CODEX_BIN ?? "codex", ["--version"], { phase: 'native-version' }));
|
|
509
|
-
return await withBridge(options, async () => ({ harness: "codex-desktop", app_server: "available", observed_runtime: { codex_cli, dd_harness_contract: "dd-codex-harness@1" }, requested_profile: profile(options), observed_at: now() }));
|
|
607
|
+
return await withBridge(options, async () => ({ harness: "codex-desktop", app_server: "available", observation_scope: "version_and_initialize", observed_runtime: { codex_cli, dd_harness_contract: "dd-codex-harness@1" }, requested_profile: profile(options), observed_at: now() }));
|
|
510
608
|
}
|
|
511
609
|
|
|
512
610
|
async function observeCodexProfile(bridge, options, id, observed, source) {
|
|
@@ -548,6 +646,9 @@ export async function startSession(options) {
|
|
|
548
646
|
}
|
|
549
647
|
|
|
550
648
|
export async function startSessionWithBridge(bridge, options) {
|
|
649
|
+
options = { ...options, readPhase: codexReadPhase(bridge, options) };
|
|
650
|
+
options.readPhase.outerClock = new ObservationClock({ timeoutMs: options.livenessTimeoutMs ?? options.timeoutMs ?? CODEX_REQUEST_TIMEOUT_MS,
|
|
651
|
+
...options.observationClock, onGap: gap => bridge.journal?.write("observation_gap", { ...gap, phase: "start", session_id: options.sessionId }) });
|
|
551
652
|
const sessionId = required(options.sessionId, "--session-id"); const cwd = absolute(options.cwd, "--cwd"); const prompt = required(options.prompt, "--prompt");
|
|
552
653
|
await ensureThreadLoaded(bridge, sessionId, options, cwd);
|
|
553
654
|
bridge.requestedProfiles?.set(sessionId, profile(options));
|
|
@@ -555,13 +656,16 @@ export async function startSessionWithBridge(bridge, options) {
|
|
|
555
656
|
options.assertDispatch?.();
|
|
556
657
|
bridge.assertHookHealthy?.(sessionId);
|
|
557
658
|
bridge.dispatchOperationId = options.operationId;
|
|
558
|
-
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 ?? 30_000);
|
|
659
|
+
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" } }, ...dispatchBudget(options.readPhase, "turn/start", options.timeoutMs ?? 30_000));
|
|
559
660
|
const turn = started?.turn ?? started; const turnId = turn?.id ?? null;
|
|
560
661
|
if (typeof turnId !== "string" || !turnId) throw new Error("Codex did not return a Turn id");
|
|
561
662
|
return { harness: "codex-desktop", provider_session_id: sessionId, adapter_session_id: sessionId, turn_id: turnId, status: "started", requested_profile: profile(options), observed_profile: { model: null, reasoning: null } };
|
|
562
663
|
}
|
|
563
664
|
|
|
564
665
|
export async function promptSessionWithBridge(bridge, options) {
|
|
666
|
+
options = { ...options, readPhase: codexReadPhase(bridge, options) };
|
|
667
|
+
const clock = new ObservationClock({ timeoutMs: options.livenessTimeoutMs ?? options.timeoutMs ?? 1_800_000, ...options.observationClock, onGap: gap => bridge.journal?.write("observation_gap", { ...gap, phase: "prompt", session_id: options.sessionId }) });
|
|
668
|
+
options.readPhase.outerClock = clock;
|
|
565
669
|
const sessionId = required(options.sessionId, "--session-id"); const cwd = absolute(options.cwd, "--cwd"); const prompt = required(options.prompt, "--prompt");
|
|
566
670
|
validateObservationDuration(options.livenessTimeoutMs ?? options.timeoutMs ?? 1_800_000);
|
|
567
671
|
await ensureThreadLoaded(bridge, sessionId, options, cwd);
|
|
@@ -573,8 +677,7 @@ export async function promptSessionWithBridge(bridge, options) {
|
|
|
573
677
|
options.assertDispatch?.();
|
|
574
678
|
bridge.assertHookHealthy?.(sessionId);
|
|
575
679
|
bridge.dispatchOperationId = options.operationId;
|
|
576
|
-
const
|
|
577
|
-
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" } }, 30_000);
|
|
680
|
+
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" } }, ...dispatchBudget(options.readPhase, "turn/start", 30_000));
|
|
578
681
|
const turn = started?.turn ?? started; const turnId = turn?.id ?? null;
|
|
579
682
|
let completed;
|
|
580
683
|
try { completed = isTerminalTurnStatus(turn?.status) ? terminalTurn({ turn }, sessionId, turnId, bridge)
|
|
@@ -599,14 +702,34 @@ export async function promptSessionWithBridge(bridge, options) {
|
|
|
599
702
|
details: { native_outcome: { provider_session_id: sessionId, turn_id: turnId, status: "failed", provider_error: error.details?.provider_error }, settled: false, observation_error: errorRecord(observationError) }, cause: error });
|
|
600
703
|
}
|
|
601
704
|
}
|
|
705
|
+
if (isObservationLoss(error)) throw Object.assign(new Error("Codex accepted Turn outcome could not be observed", { cause: error }), {
|
|
706
|
+
code: "operation_observation_lost", retryable: false, details: { provider_session_id: sessionId, turn_id: turnId,
|
|
707
|
+
operation_id: options.operationId ?? null, phase: "turn_observation", native_dispatch_started: true,
|
|
708
|
+
native_outcome: "unknown", observation_error: errorRecord(error), history_reads: Object.fromEntries(options.readPhase.history ?? []) }
|
|
709
|
+
});
|
|
602
710
|
throw error;
|
|
603
711
|
}
|
|
604
712
|
const nativeOutcome = { harness: "codex-desktop", provider_session_id: sessionId, adapter_session_id: sessionId, turn_id: turnId, status: "completed", turn: completed };
|
|
605
713
|
try {
|
|
606
|
-
const
|
|
714
|
+
const phase = codexReadPhase(bridge, options);
|
|
715
|
+
const history = await readCodexHistory(bridge, "thread/read", { threadId: sessionId, includeTurns: true }, phase);
|
|
716
|
+
const thread = history.available ? history.result.thread
|
|
717
|
+
: (await readCodexRpc(bridge, "thread/read", { threadId: sessionId, includeTurns: false }, phase)).thread;
|
|
607
718
|
bridge.assertHookHealthy?.(sessionId);
|
|
608
|
-
const
|
|
609
|
-
|
|
719
|
+
const final = bridge.finalMessages?.get(sessionId);
|
|
720
|
+
const historyTurn = thread?.turns?.find(item => item?.id === turnId);
|
|
721
|
+
const completeHistory = history.available && (turnItemEvidence(historyTurn, bridge, sessionId).observed
|
|
722
|
+
// Preserve the existing idle + readable-history final-message receipt.
|
|
723
|
+
|| completed?.turn?.synthetic === true && Array.isArray(thread.turns) && final?.turnId === turnId && thread.status?.type === "idle");
|
|
724
|
+
const assistantText = latestAssistantText(completed) ?? latestAssistantText(historyTurn)
|
|
725
|
+
?? (final?.turnId === turnId ? latestAssistantText({ items: [final.item] }) : null);
|
|
726
|
+
if (!completeHistory && (!assistantText || !turnItemEvidence(completed?.turn ?? completed, bridge, sessionId).observed))
|
|
727
|
+
fail("Codex completed but its current output/items remain unavailable", "operation_observation_lost", { history_error: history.error, provider_session_id: sessionId, turn_id: turnId });
|
|
728
|
+
if (!completeHistory && (options.model && !thread.model || options.reasoning && !thread.reasoningEffort))
|
|
729
|
+
fail("Codex completed but its applied profile remains unavailable", "operation_observation_lost", { history_error: history.error, provider_session_id: sessionId, turn_id: turnId });
|
|
730
|
+
const result = { ...nativeOutcome, ...(thread ? { thread: threadReceipt(thread) } : {}), descendants: await observedChildren(bridge, sessionId, phase), ...(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") };
|
|
731
|
+
await bridge.journal?.flush?.();
|
|
732
|
+
return result;
|
|
610
733
|
} catch (error) {
|
|
611
734
|
throw Object.assign(new Error("Codex completed natively but mandatory observation failed"), { code: "native_outcome_observation_failed", retryable: false,
|
|
612
735
|
details: { native_outcome: nativeOutcome, settled: false, observation_error: errorRecord(error) }, cause: error });
|
|
@@ -614,7 +737,7 @@ export async function promptSessionWithBridge(bridge, options) {
|
|
|
614
737
|
}
|
|
615
738
|
|
|
616
739
|
async function ensureThreadLoaded(bridge, sessionId, options, cwd) {
|
|
617
|
-
const value = await bridge
|
|
740
|
+
const value = await readCodexRpc(bridge, "thread/read", { threadId: sessionId, includeTurns: false }, options.readPhase);
|
|
618
741
|
const thread = value?.thread ?? value;
|
|
619
742
|
if (thread?.status?.type !== "notLoaded") return;
|
|
620
743
|
await requestConfiguredThread(bridge, "thread/resume", {
|
|
@@ -623,10 +746,14 @@ async function ensureThreadLoaded(bridge, sessionId, options, cwd) {
|
|
|
623
746
|
model: options.model ?? null,
|
|
624
747
|
approvalPolicy: "never",
|
|
625
748
|
sandbox: "danger-full-access"
|
|
626
|
-
}, 30_000);
|
|
749
|
+
}, ...dispatchBudget(options.readPhase, "thread/resume", 30_000));
|
|
627
750
|
}
|
|
628
751
|
|
|
629
752
|
async function waitForTurn(bridge, sessionId, turnId, timeoutMs, options, cwd, clock = new ObservationClock({ timeoutMs, onGap: gap => bridge.journal?.write("observation_gap", { ...gap, session_id: sessionId, turn_id: turnId }) }), startStatus) {
|
|
753
|
+
const readPhase = codexReadPhase(bridge, options);
|
|
754
|
+
readPhase.clock = clock;
|
|
755
|
+
readPhase.progress = () => bridge.activityObservations?.get(sessionId) ?? bridge.activity?.get(sessionId);
|
|
756
|
+
options = { ...options, readPhase };
|
|
630
757
|
// app-server can briefly expose the new Turn as `interrupted` between the
|
|
631
758
|
// turn/start response and its turn/started notification. Give that snapshot
|
|
632
759
|
// one event-loop grace window before treating failure as authoritative.
|
|
@@ -636,7 +763,7 @@ async function waitForTurn(bridge, sessionId, turnId, timeoutMs, options, cwd, c
|
|
|
636
763
|
if (status === "completed" || startStatus !== "inProgress" || bridge.turns.get(turnId)?.status === "completed") return true;
|
|
637
764
|
// A provisional failure can contradict the accepted start before its
|
|
638
765
|
// delayed notification arrives. Recheck this Turn, never start another.
|
|
639
|
-
const current = await readTurn(bridge, sessionId, turnId, true);
|
|
766
|
+
const current = await readTurn(bridge, sessionId, turnId, true, readPhase);
|
|
640
767
|
return bridge.turns.get(turnId)?.status === "completed" || current?.turn?.status === status
|
|
641
768
|
&& isAuthoritativeTerminal(bridge, turnId, status, current.threadStatus, failureNotBefore);
|
|
642
769
|
};
|
|
@@ -646,27 +773,29 @@ async function waitForTurn(bridge, sessionId, turnId, timeoutMs, options, cwd, c
|
|
|
646
773
|
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);
|
|
647
774
|
const expired = clock.sample(bridge.activityObservations?.get(sessionId) ?? bridge.activity?.get(sessionId));
|
|
648
775
|
if (expired || Date.now() >= nextStoredReadAt) {
|
|
649
|
-
const stored = await readTurn(bridge, sessionId, turnId, false);
|
|
776
|
+
const stored = await readTurn(bridge, sessionId, turnId, false, readPhase);
|
|
650
777
|
if (stored?.turn && await reconcileSnapshot(stored.turn.status, stored.threadStatus)) return terminalTurn({ turn: stored.turn }, sessionId, turnId, bridge);
|
|
651
778
|
// A loaded app-server can retain a stale `active` Thread state after a
|
|
652
779
|
// provider-side interruption. The persisted turn page is independent of
|
|
653
780
|
// that live subscription and is the compact authority for its terminal
|
|
654
|
-
// status
|
|
655
|
-
const persisted = await readPersistedTurn(bridge, sessionId, turnId);
|
|
656
|
-
if (persisted && await reconcileSnapshot(persisted.status, stored?.threadStatus)) return terminalTurn({ turn: persisted }, sessionId, turnId, bridge);
|
|
781
|
+
// status. Hydrate only when that read surface is unavailable.
|
|
782
|
+
const persisted = await readPersistedTurn(bridge, sessionId, turnId, readPhase);
|
|
783
|
+
if (persisted.turn && await reconcileSnapshot(persisted.turn.status, persisted.threadStatus ?? stored?.threadStatus)) return terminalTurn({ turn: persisted.turn }, sessionId, turnId, bridge);
|
|
784
|
+
const native = bridge.turns.get(turnId);
|
|
785
|
+
if (native?.status === "completed" && (!native.threadId || native.threadId === sessionId)) return terminalTurn(native.value, sessionId, turnId, bridge);
|
|
657
786
|
// Desktop may unload an idle Thread immediately after an interrupted
|
|
658
787
|
// Turn. Reload it once before polling again: otherwise the compact
|
|
659
788
|
// status remains `notLoaded` forever and hides the terminal Turn.
|
|
660
789
|
if (stored?.threadStatus === "notLoaded") {
|
|
661
790
|
await ensureThreadLoaded(bridge, sessionId, options, cwd);
|
|
662
|
-
const hydrated = await readTurn(bridge, sessionId, turnId, true);
|
|
791
|
+
const hydrated = await readTurn(bridge, sessionId, turnId, true, readPhase);
|
|
663
792
|
if (hydrated?.turn && await reconcileSnapshot(hydrated.turn.status, hydrated.threadStatus)) return terminalTurn({ turn: hydrated.turn }, sessionId, turnId, bridge);
|
|
664
793
|
}
|
|
665
794
|
// Some app-server versions correctly omit turns for a compact read.
|
|
666
795
|
// Once the thread is idle, hydrate exactly once to recover the terminal
|
|
667
796
|
// turn instead of waiting for the global timeout.
|
|
668
797
|
if (stored?.threadStatus === "idle") {
|
|
669
|
-
const hydrated = await readTurn(bridge, sessionId, turnId, true);
|
|
798
|
+
const hydrated = persisted.hydrated ? persisted : await readTurn(bridge, sessionId, turnId, true, readPhase);
|
|
670
799
|
// "idle" only describes the Thread. Some app-server builds retain
|
|
671
800
|
// an inProgress Turn after emitting the final agent message; treating
|
|
672
801
|
// that stale Turn as terminal incorrectly aborts an otherwise usable
|
|
@@ -681,7 +810,7 @@ async function waitForTurn(bridge, sessionId, turnId, timeoutMs, options, cwd, c
|
|
|
681
810
|
// reasoning and calling tools. They are terminal evidence only after
|
|
682
811
|
// app-server reports the Thread idle.
|
|
683
812
|
if (final?.turnId === turnId && stored?.threadStatus === "idle" && Date.now() - final.completedAt >= 1_000) {
|
|
684
|
-
const hydrated = await readTurn(bridge, sessionId, turnId, true);
|
|
813
|
+
const hydrated = await readTurn(bridge, sessionId, turnId, true, readPhase);
|
|
685
814
|
if (hydrated?.turn && await reconcileSnapshot(hydrated.turn.status, hydrated.threadStatus)) return terminalTurn({ turn: hydrated.turn }, sessionId, turnId, bridge);
|
|
686
815
|
if (hydrated?.threadStatus === "idle" && !isTerminalTurnStatus(hydrated.turn?.status))
|
|
687
816
|
return terminalTurn({ turn: { id: turnId, status: "completed", synthetic: true, final_message: final.item } }, sessionId, turnId, bridge);
|
|
@@ -689,7 +818,7 @@ async function waitForTurn(bridge, sessionId, turnId, timeoutMs, options, cwd, c
|
|
|
689
818
|
nextStoredReadAt = Date.now() < failureNotBefore ? failureNotBefore : Date.now() + 15_000;
|
|
690
819
|
}
|
|
691
820
|
const terminal = bridge.turns.get(turnId); if (terminal?.status === "completed" && (!terminal.threadId || terminal.threadId === sessionId)) return terminalTurn(terminal.value, sessionId, turnId, bridge);
|
|
692
|
-
if (expired && clock.sample(bridge.activityObservations?.get(sessionId) ?? bridge.activity?.get(sessionId))) fail("Codex native outcome remains unknown after its silence window", "operation_observation_lost", { provider_session_id: sessionId, turn_id: turnId, native_outcome: "unknown" });
|
|
821
|
+
if (expired && clock.sample(bridge.activityObservations?.get(sessionId) ?? bridge.activity?.get(sessionId))) fail("Codex native outcome remains unknown after its silence window", "operation_observation_lost", { provider_session_id: sessionId, turn_id: turnId, operation_id: options.operationId ?? null, native_dispatch_started: true, phase: "turn_observation", native_outcome: "unknown", history_reads: Object.fromEntries(readPhase.history ?? []) });
|
|
693
822
|
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
694
823
|
}
|
|
695
824
|
}
|
|
@@ -744,29 +873,23 @@ function isAuthoritativeTerminal(bridge, turnId, status, threadStatus, failureNo
|
|
|
744
873
|
return !(bridge.turns.get(turnId)?.status === "running" && threadStatus === "active");
|
|
745
874
|
}
|
|
746
875
|
|
|
747
|
-
async function readTurn(bridge, sessionId, turnId, includeTurns) {
|
|
876
|
+
async function readTurn(bridge, sessionId, turnId, includeTurns, phase) {
|
|
748
877
|
if (!turnId) return null;
|
|
749
878
|
try {
|
|
750
|
-
const
|
|
879
|
+
const history = includeTurns ? await readCodexHistory(bridge, "thread/read", { threadId: sessionId, includeTurns }, phase) : null;
|
|
880
|
+
if (history && !history.available) return { turn: null, threadStatus: null, historyAvailable: false };
|
|
881
|
+
const result = history?.result ?? await readCodexRpc(bridge, "thread/read", { threadId: sessionId, includeTurns }, phase);
|
|
751
882
|
const thread = result?.thread ?? result;
|
|
752
883
|
return { turn: Array.isArray(thread?.turns) ? thread.turns.find((turn) => turn?.id === turnId) ?? null : null, threadStatus: thread?.status?.type ?? null };
|
|
753
|
-
} catch { bridge.assertHookHealthy?.(sessionId);
|
|
884
|
+
} catch (error) { bridge.assertHookHealthy?.(sessionId); throw error; }
|
|
754
885
|
}
|
|
755
886
|
|
|
756
|
-
async function readPersistedTurn(bridge, sessionId, turnId) {
|
|
887
|
+
async function readPersistedTurn(bridge, sessionId, turnId, phase) {
|
|
757
888
|
try {
|
|
758
|
-
const
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
async function readCompletedThread(bridge, sessionId) {
|
|
764
|
-
let lastError;
|
|
765
|
-
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
766
|
-
try { const result = await bridge.request("thread/read", { threadId: sessionId, includeTurns: true }, 10_000); return result?.thread ?? result; }
|
|
767
|
-
catch (error) { lastError = error; bridge.assertHookHealthy?.(sessionId); bridge.journal.write("thread_read_retry", { session_id: sessionId, attempt, error: errorRecord(error) }); await new Promise((resolve) => setTimeout(resolve, 150)); }
|
|
768
|
-
}
|
|
769
|
-
throw lastError;
|
|
889
|
+
const history = await readCodexHistory(bridge, "thread/turns/list", { threadId: sessionId, limit: 20, sortDirection: "desc", itemsView: "summary" }, phase);
|
|
890
|
+
if (!history.available) return { ...await readTurn(bridge, sessionId, turnId, true, phase), hydrated: true };
|
|
891
|
+
return { available: history.available, turn: history.result?.data.find((turn) => turn?.id === turnId) ?? null };
|
|
892
|
+
} catch (error) { bridge.assertHookHealthy?.(sessionId); throw error; }
|
|
770
893
|
}
|
|
771
894
|
|
|
772
895
|
function latestAssistantText(value) {
|
|
@@ -789,10 +912,11 @@ export async function inspectSession(options) {
|
|
|
789
912
|
}
|
|
790
913
|
|
|
791
914
|
export async function inspectSessionWithBridge(bridge, options) {
|
|
792
|
-
const
|
|
793
|
-
const
|
|
915
|
+
const phase = options.readPhase ?? codexReadPhase(bridge, options);
|
|
916
|
+
const sessionId = required(options.sessionId, "--session-id"); const value = await readCodexRpc(bridge, "thread/read", { threadId: sessionId, includeTurns: false }, phase); const thread = value?.thread ?? value;
|
|
917
|
+
const terminal = options.turnId ? retainedCodexTurn(bridge, sessionId, options.turnId) ?? (await readTurn(bridge, sessionId, options.turnId, true, phase))?.turn : null;
|
|
794
918
|
const exact = terminal?.id === options.turnId && terminal?.status === "failed" ? { terminal_turn_id: terminal.id, terminal_status: terminal.status, provider_error: nativeTurnError(bridge, sessionId, terminal), native_turn_items: turnItemEvidence(terminal, bridge, sessionId) } : {};
|
|
795
|
-
return { harness: "codex-desktop", provider_session_id: sessionId, adapter_session_id: sessionId, status: thread.status ?? null, thread: threadReceipt(thread), ...exact, 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") };
|
|
919
|
+
return { harness: "codex-desktop", provider_session_id: sessionId, adapter_session_id: sessionId, status: thread.status ?? null, thread: threadReceipt(thread), ...exact, descendants: await observedChildren(bridge, sessionId, phase), 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") };
|
|
796
920
|
}
|
|
797
921
|
|
|
798
922
|
export async function cancelSession(options) {
|
|
@@ -800,12 +924,21 @@ export async function cancelSession(options) {
|
|
|
800
924
|
}
|
|
801
925
|
|
|
802
926
|
export async function cancelSessionWithBridge(bridge, options) {
|
|
927
|
+
const phase = options.readPhase ?? codexReadPhase(bridge, options);
|
|
803
928
|
const sessionId = required(options.sessionId, "--session-id");
|
|
804
929
|
let turnId = options.turnId ?? null;
|
|
805
930
|
if (!turnId) {
|
|
806
|
-
const
|
|
807
|
-
const
|
|
808
|
-
turnId =
|
|
931
|
+
const currentId = bridge.currentTurns?.get(sessionId);
|
|
932
|
+
const retained = retainedCodexTurn(bridge, sessionId, currentId);
|
|
933
|
+
turnId = currentId && !isTerminalTurnStatus(retained?.status) ? currentId : null;
|
|
934
|
+
if (!turnId) {
|
|
935
|
+
const history = await readCodexHistory(bridge, "thread/read", { threadId: sessionId, includeTurns: true }, phase);
|
|
936
|
+
if (!history.available || !Array.isArray(history.result.thread.turns)
|
|
937
|
+
|| history.result.thread.turns.some(turn => typeof turn?.id !== "string" || !turn.id.trim()
|
|
938
|
+
|| turn.status !== "inProgress" && !isTerminalTurnStatus(turn.status)))
|
|
939
|
+
fail("Codex active Turn cannot be established from incomplete history", "operation_observation_lost", { provider_session_id: sessionId, native_outcome: "unknown", history_error: history.error });
|
|
940
|
+
turnId = [...history.result.thread.turns].reverse().find((turn) => turn.status === "inProgress")?.id ?? null;
|
|
941
|
+
}
|
|
809
942
|
}
|
|
810
943
|
if (!turnId) fail("Codex Session has no active Turn to interrupt", "turn_not_active");
|
|
811
944
|
await bridge.request("turn/interrupt", { threadId: sessionId, turnId });
|
|
@@ -178,8 +178,26 @@ export async function startDaemon(options) {
|
|
|
178
178
|
export const stopDaemon = async options => { const timeoutMs = options.timeoutMs ?? 30_000; return await confirmDaemonStopped(() => callDaemon(options.stateDir, "daemon.stop", { cancelTree: options.cancelTree === true }, timeoutMs), locations(options.stateDir).socket, timeoutMs, { readState: () => readState(options.stateDir) }); };
|
|
179
179
|
|
|
180
180
|
export class Runtime {
|
|
181
|
-
constructor(paths, state, client, child, providerProcess) { this.paths = paths; this.state = state; this.client = client; this.child = child; this.providerProcess = providerProcess; this.sessions = new Map(
|
|
181
|
+
constructor(paths, state, client, child, providerProcess) { this.paths = paths; this.state = state; this.client = client; this.child = child; this.providerProcess = providerProcess; this.sessions = new Map();
|
|
182
|
+
for (const record of state.sessions ?? []) {
|
|
183
|
+
if (typeof record?.provider_session_id !== "string" || !record.provider_session_id || this.sessions.has(record.provider_session_id))
|
|
184
|
+
throw new DaemonError("session_identity_mismatch", "OpenCode retained Session identity is invalid or duplicated");
|
|
185
|
+
this.sessions.set(record.provider_session_id, record);
|
|
186
|
+
}
|
|
187
|
+
this.active = null; this.persisting = Promise.resolve(); }
|
|
182
188
|
persist(patch = {}) { Object.assign(this.state, patch, { pid: process.pid, sessions: [...this.sessions.values()] }); const snapshot = structuredClone(this.state); this.persisting = this.persisting.catch(() => {}).then(() => writeJson(this.paths.state, snapshot)); return this.persisting; }
|
|
189
|
+
retainSession(info, root) {
|
|
190
|
+
const id = info.id, parent = info.fork ? null : info.parentID ?? null, retained = this.sessions.get(id);
|
|
191
|
+
if (typeof id !== "string" || !id || typeof info.directory !== "string" || !path.isAbsolute(info.directory)
|
|
192
|
+
|| retained && ((retained.parent_provider_session_id ?? null) !== parent
|
|
193
|
+
|| retained.adapter_session_id && retained.adapter_session_id !== id
|
|
194
|
+
|| retained.root_provider_session_id && retained.root_provider_session_id !== root))
|
|
195
|
+
throw new DaemonError("session_identity_mismatch", "OpenCode retained Session identity changed");
|
|
196
|
+
// Cwd is observed routing authority, not ownership identity: a moved owned
|
|
197
|
+
// Session must remain observable/cancellable. requireSettled fences dispatch.
|
|
198
|
+
const record = { provider_session_id: id, adapter_session_id: id, parent_provider_session_id: parent, root_provider_session_id: root, cwd: info.directory };
|
|
199
|
+
this.sessions.set(id, record); return record;
|
|
200
|
+
}
|
|
183
201
|
async journal(operation, result) { const line = `${JSON.stringify({ schema_id: "dd-opencode/daemon-event@1", event_id: randomUUID(), observed_at: new Date().toISOString(), operation, active_tree: this.state.active_tree, session_id: result?.provider_session_id ?? null })}\n`; for (const journal of new Set([this.paths.journal, this.state.config.journal].filter(Boolean))) { await mkdir(path.dirname(journal), { recursive: true, mode: 0o700 }); await appendFile(journal, line, { mode: 0o600 }); } }
|
|
184
202
|
async childTree(id, seen = new Set()) {
|
|
185
203
|
if (seen.has(id)) throw new DaemonError("session_identity_mismatch", "OpenCode native topology contains a cycle");
|
|
@@ -195,13 +213,11 @@ export class Runtime {
|
|
|
195
213
|
const parent = info.fork ? null : info.parentID ?? null, existing = this.sessions.get(id);
|
|
196
214
|
if (info.id !== id || !existing && (!parent || !this.sessions.has(parent) || info.fork)
|
|
197
215
|
|| existing && (existing.parent_provider_session_id ?? null) !== parent) throw new DaemonError("foreign_session", "OpenCode Session is not in the retained owned tree");
|
|
198
|
-
const record =
|
|
199
|
-
root_provider_session_id: parent ? this.sessions.get(parent)?.root_provider_session_id ?? parent : existing?.root_provider_session_id ?? id, cwd: info.directory };
|
|
200
|
-
this.sessions.set(id, record);
|
|
216
|
+
const record = this.retainSession(info, parent ? this.sessions.get(parent)?.root_provider_session_id ?? parent : existing?.root_provider_session_id ?? id);
|
|
201
217
|
for (const child of children) {
|
|
202
218
|
const retained = this.sessions.get(child.id);
|
|
203
219
|
if (child.fork || retained && retained.parent_provider_session_id !== child.parentID) throw new DaemonError("session_identity_mismatch", "OpenCode child ancestry changed");
|
|
204
|
-
this.
|
|
220
|
+
this.retainSession(child, record.root_provider_session_id);
|
|
205
221
|
}
|
|
206
222
|
const pending = new Map();
|
|
207
223
|
for (const session of [id, ...children.map(child => child.id)]) {
|
|
@@ -287,6 +303,7 @@ export class Runtime {
|
|
|
287
303
|
if (seen.has(id)) return [];
|
|
288
304
|
const native = await this.client.inspect(id), retained = this.sessions.get(id);
|
|
289
305
|
if (native.id !== id || (retained.parent_provider_session_id ?? null) !== (native.fork ? null : native.parentID ?? null)) throw new DaemonError("session_identity_mismatch", "OpenCode cancellation Session ancestry changed");
|
|
306
|
+
this.retainSession(native, retained.root_provider_session_id ?? id);
|
|
290
307
|
seen.add(id);
|
|
291
308
|
const errors = []; let children = [];
|
|
292
309
|
try { children = await this.client.children(id); } catch (error) { errors.push({ session_id: id, phase: "children", ...errorPayload(error) }); }
|
|
@@ -294,7 +311,7 @@ export class Runtime {
|
|
|
294
311
|
if (child.parentID !== id || child.fork || this.sessions.has(child.id) && this.sessions.get(child.id).parent_provider_session_id !== id) {
|
|
295
312
|
errors.push({ session_id: child.id, phase: "identity", code: "session_identity_mismatch" }); continue;
|
|
296
313
|
}
|
|
297
|
-
this.
|
|
314
|
+
this.retainSession(child, this.sessions.get(id)?.root_provider_session_id ?? id);
|
|
298
315
|
try { errors.push(...await this.cancelTree(child.id, seen)); }
|
|
299
316
|
catch (error) { errors.push({ session_id: child.id, phase: "cancel_child", ...errorPayload(error) }); }
|
|
300
317
|
}
|
|
@@ -499,7 +516,9 @@ export async function awaitOpenCodeStartup(client, state, child, { timeoutMs = S
|
|
|
499
516
|
if (["loading", "activating", "registered"].includes(plugin?.state?.status)) await mark(`required_plugin_${plugin.state.status}`);
|
|
500
517
|
} catch (error) {
|
|
501
518
|
const transient = error.code === "opencode_unavailable" && error.retryable === true
|
|
502
|
-
|
|
519
|
+
// Safe pre-identity reads use the normalized observation category,
|
|
520
|
+
// not a version-specific SDK error wrapper. No productive RPC here.
|
|
521
|
+
&& (phase === "server" || error.details?.native_type === "ServiceUnavailableError");
|
|
503
522
|
if (!transient) throw error;
|
|
504
523
|
}
|
|
505
524
|
const pause = Math.min([250, 500, 1000, 2000][Math.min(attempt++, 3)], remaining());
|
|
@@ -5,6 +5,15 @@
|
|
|
5
5
|
"additionalProperties": false,
|
|
6
6
|
"required": ["schema_id", "summary", "findings"],
|
|
7
7
|
"properties": {
|
|
8
|
+
"accepted_intent_acknowledgement": {
|
|
9
|
+
"type": "object", "additionalProperties": false,
|
|
10
|
+
"required": ["reviewed_fingerprint", "current_fingerprint", "clarification_refs"],
|
|
11
|
+
"properties": {
|
|
12
|
+
"reviewed_fingerprint": {"type": "string", "pattern": "^[a-f0-9]{64}$"},
|
|
13
|
+
"current_fingerprint": {"type": "string", "pattern": "^[a-f0-9]{64}$"},
|
|
14
|
+
"clarification_refs": {"type": "array", "uniqueItems": true, "items": {"type": "string", "pattern": "^HITL-[A-Za-z0-9._-]+$"}}
|
|
15
|
+
}
|
|
16
|
+
},
|
|
8
17
|
"schema_id": {"const": "dd-flow/code-review-decision@3"},
|
|
9
18
|
"summary": {"type": "string", "minLength": 1},
|
|
10
19
|
"findings": {
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
}
|
|
40
40
|
},
|
|
41
41
|
"verification_matrix_contract": {"const": "dd-flow/verification-matrix@1"},
|
|
42
|
+
"accepted_intent_contract": {"const": "dd-flow/accepted-intent@1"},
|
|
42
43
|
"final_check_coverage_contract": {"const": "dd-flow/final-check-coverage@1"},
|
|
43
44
|
"status": {"enum": ["created", "running", "paused", "waiting_for_user", "done", "failed", "blocked", "cancelled", "discarded"]},
|
|
44
45
|
"verdict": {"type": ["string", "null"]},
|
|
@@ -5,6 +5,15 @@
|
|
|
5
5
|
"additionalProperties": false,
|
|
6
6
|
"required": ["schema_id", "outcome", "summary", "finding_decisions", "correction"],
|
|
7
7
|
"properties": {
|
|
8
|
+
"accepted_intent_acknowledgement": {
|
|
9
|
+
"type": "object", "additionalProperties": false,
|
|
10
|
+
"required": ["reviewed_fingerprint", "current_fingerprint", "clarification_refs"],
|
|
11
|
+
"properties": {
|
|
12
|
+
"reviewed_fingerprint": {"type": "string", "pattern": "^[a-f0-9]{64}$"},
|
|
13
|
+
"current_fingerprint": {"type": "string", "pattern": "^[a-f0-9]{64}$"},
|
|
14
|
+
"clarification_refs": {"type": "array", "uniqueItems": true, "items": {"type": "string", "pattern": "^HITL-[A-Za-z0-9._-]+$"}}
|
|
15
|
+
}
|
|
16
|
+
},
|
|
8
17
|
"schema_id": {"const": "dd-flow/plan-review-decision@4"},
|
|
9
18
|
"outcome": {"enum": ["accepted", "failed", "cancelled"]},
|
|
10
19
|
"summary": {"type": "string", "minLength": 1},
|