@deksden-com/dd-flow-cli 0.9.0-beta.123 → 0.9.0-beta.127
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/README.md +33 -1
- package/dist/build-info.json +7 -7
- package/dist/cli/argument-values.js +38 -0
- package/dist/cli/command-inputs.js +4 -3
- package/dist/cli/help.js +1 -0
- package/dist/cli/hook-ingress.js +1 -1
- package/dist/cli/run-cli.js +126 -193
- package/dist/cli/runtime-maintenance-diagnostics.js +46 -0
- package/dist/cli/runtime-maintenance-ingress.js +51 -0
- package/dist/cli/runtime-process.js +128 -0
- package/dist/cli.js +8 -5
- package/dist/domain/stage-catalog.js +1 -1
- package/dist/harness-runtime/bin/dd-agy.mjs +3 -2
- package/dist/harness-runtime/bin/dd-codex.mjs +3 -2
- package/dist/harness-runtime/bin/dd-droid.mjs +3 -2
- package/dist/harness-runtime/bin/dd-grok.mjs +16 -6
- package/dist/harness-runtime/bin/dd-opencode.mjs +3 -2
- package/dist/harness-runtime/bin/dd-zcode.mjs +20 -16
- package/dist/harness-runtime/lib/adapter-timeouts.d.mts +5 -0
- package/dist/harness-runtime/lib/adapter-timeouts.mjs +34 -0
- package/dist/harness-runtime/lib/codex-capacity-policy.d.mts +10 -0
- package/dist/harness-runtime/lib/codex-capacity-policy.mjs +89 -0
- package/dist/harness-runtime/lib/daemon-observation.d.mts +2 -0
- package/dist/harness-runtime/lib/daemon-observation.mjs +163 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +159 -15
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +215 -86
- package/dist/harness-runtime/lib/dd-agy.mjs +11 -13
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +47 -23
- package/dist/harness-runtime/lib/dd-codex.mjs +257 -99
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +32 -30
- package/dist/harness-runtime/lib/dd-droid.mjs +117 -31
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +96 -38
- package/dist/harness-runtime/lib/dd-grok.mjs +15 -13
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +54 -20
- package/dist/harness-runtime/lib/dd-opencode.mjs +44 -14
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +81 -76
- package/dist/harness-runtime/lib/dd-zcode.mjs +204 -116
- package/dist/harness-runtime/lib/driver-recovery.mjs +12 -2
- package/dist/harness-runtime/lib/lease-renewal.d.mts +8 -0
- package/dist/harness-runtime/lib/lease-renewal.mjs +23 -0
- package/dist/harness-runtime/lib/lifecycle-command.d.mts +3 -3
- package/dist/harness-runtime/lib/lifecycle-command.mjs +74 -20
- package/dist/harness-runtime/lib/managed-daemon.d.mts +11 -0
- package/dist/harness-runtime/lib/managed-daemon.mjs +321 -65
- package/dist/harness-runtime/lib/native-children.d.mts +13 -0
- package/dist/harness-runtime/lib/native-children.mjs +78 -0
- package/dist/harness-runtime/lib/native-hook-command.d.mts +4 -0
- package/dist/harness-runtime/lib/native-hook-command.mjs +115 -11
- package/dist/harness-runtime/lib/native-streams.mjs +45 -0
- package/dist/harness-runtime/lib/observation-clock.d.mts +18 -0
- package/dist/harness-runtime/lib/observation-clock.mjs +73 -17
- package/dist/harness-runtime/lib/operation-errors.d.mts +14 -0
- package/dist/harness-runtime/lib/operation-errors.mjs +105 -8
- package/dist/harness-runtime/lib/operation-progress.d.mts +7 -0
- package/dist/harness-runtime/lib/operation-progress.mjs +112 -0
- package/dist/harness-runtime/lib/process-json.mjs +136 -38
- package/dist/harness-runtime/lib/tool-observations.d.mts +1 -0
- package/dist/harness-runtime/lib/tool-observations.mjs +5 -2
- package/dist/harness-runtime/lib/zcode-usage.mjs +3 -2
- package/dist/runtime/context.js +8 -4
- package/dist/schemas/flow-run.schema.json +1 -0
- package/dist/schemas/plan-review-decision.schema.json +3 -3
- package/dist/schemas/run-control-receipt.schema.json +19 -0
- package/dist/services/code-checks.js +93 -27
- package/dist/services/codex-capacity.js +251 -74
- package/dist/services/codex-hook-delivery.js +2 -2
- package/dist/services/compatibility-preflight.js +4 -1
- package/dist/services/controller-fanout.js +9 -75
- package/dist/services/engines.js +70 -20
- package/dist/services/eval-snapshots.js +179 -40
- package/dist/services/external-work-launch.js +21 -9
- package/dist/services/harness-adapter.js +195 -25
- package/dist/services/hooks.js +65 -21
- package/dist/services/lifecycle-invocations.js +404 -32
- package/dist/services/managed-lease-monitor.js +128 -0
- package/dist/services/managed-processes.js +117 -53
- package/dist/services/merge-server.js +66 -23
- package/dist/services/recovery-observation-budget.js +67 -7
- package/dist/services/recovery-snapshot-database.js +7 -4
- package/dist/services/review-deferral.js +23 -0
- package/dist/services/run-control-receipt.js +1 -1
- package/dist/services/run-control-worker.js +177 -118
- package/dist/services/run-control.js +99 -8
- package/dist/services/run-controller-adapter.js +177 -51
- package/dist/services/run-controller-capture.js +14 -2
- package/dist/services/run-controller-process.js +11 -4
- package/dist/services/run-controller-recovery.js +17 -0
- package/dist/services/run-controller-state.js +68 -7
- package/dist/services/run-controller.js +207 -107
- package/dist/services/run-fork.js +8 -3
- package/dist/services/run-recovery-guard.js +75 -0
- package/dist/services/run-recovery-runtime.js +21 -30
- package/dist/services/run-recovery.js +17 -13
- package/dist/services/runs.js +40 -7
- package/dist/services/runtime-budget.js +32 -15
- package/dist/services/runtime-scope-control.js +8 -0
- package/dist/services/runtime-scope-resume.js +30 -8
- package/dist/services/runtime-scope-worker.js +118 -73
- package/dist/services/runtime-service.js +6 -7
- package/dist/services/scope-writer-cleanup.js +3 -2
- package/dist/services/stage-context.js +135 -0
- package/dist/services/stage-lifecycle.js +3 -15
- package/dist/services/stage-pause.js +3 -2
- package/dist/services/usage.js +14 -3
- package/dist/services/verification-matrix.js +30 -1
- package/dist/services/vnext-code-review.js +26 -13
- package/dist/services/vnext-code.js +10 -8
- package/dist/services/vnext-contracts.js +10 -1
- package/dist/services/vnext-execution-profile.js +75 -2
- package/dist/services/vnext-fanout.js +5 -0
- package/dist/services/vnext-merge.js +30 -11
- package/dist/services/vnext-plan-review.js +102 -35
- package/dist/services/vnext-plan.js +9 -7
- package/dist/services/vnext-protocolize.js +15 -9
- package/dist/services/vnext-specify.js +6 -6
- package/dist/services/work-registry.js +43 -6
- package/dist/services/workspace-bootstrap.js +14 -3
- package/dist/shared/errors.js +11 -6
- package/dist/storage/agent-output.js +26 -0
- package/dist/storage/database.js +80 -21
- package/package.json +7 -4
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import { createReadStream } from "node:fs";
|
|
2
3
|
import { appendFile, mkdir, readFile } from "node:fs/promises";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import readline from "node:readline";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
6
|
+
import { closeBridgeProcess } from "./managed-daemon.mjs";
|
|
7
|
+
import { errorRecord, isProvenShellCorrection } from "./operation-errors.mjs";
|
|
8
|
+
import { reportedFailure, commandText } from "./process-json.mjs";
|
|
9
|
+
import { ObservationClock, observedTimeout, validateObservationDuration } from "./observation-clock.mjs";
|
|
7
10
|
import { observeModel } from "./model-observations.mjs";
|
|
11
|
+
import { classifyCodexItem } from "./tool-observations.mjs";
|
|
12
|
+
import { readNativeLines, streamNativeDiagnostics, nativeObservationError } from "./native-streams.mjs";
|
|
13
|
+
import { operationProgressReporter } from "./operation-progress.mjs";
|
|
8
14
|
|
|
9
15
|
const LATE_RESPONSE_GRACE_MS = 5_000;
|
|
10
16
|
const CODEX_SESSION_CONFIG = Object.freeze({
|
|
@@ -34,43 +40,51 @@ export function parseCodexCliVersion(value) {
|
|
|
34
40
|
return match[1];
|
|
35
41
|
}
|
|
36
42
|
|
|
37
|
-
|
|
38
|
-
return new Promise((resolve, reject) => {
|
|
39
|
-
const child = spawn(bin, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
40
|
-
let stdout = ""; let stderr = "";
|
|
41
|
-
child.stdout.setEncoding("utf8").on("data", (chunk) => { stdout += chunk; });
|
|
42
|
-
child.stderr.setEncoding("utf8").on("data", (chunk) => { stderr += chunk; });
|
|
43
|
-
child.on("error", reject);
|
|
44
|
-
child.on("exit", (code) => code === 0 ? resolve(stdout) : reject(Object.assign(new Error(`Codex CLI version failed: ${stderr.trim() || code}`), { code: "codex_version_unavailable" })));
|
|
45
|
-
});
|
|
46
|
-
}
|
|
43
|
+
|
|
47
44
|
|
|
48
45
|
class Journal {
|
|
49
46
|
constructor(file) { this.file = file ? absolute(file, "--journal") : null; this.pending = this.file ? mkdir(path.dirname(this.file), { recursive: true }) : Promise.resolve(); this.order = 0; }
|
|
50
|
-
write(kind, payload) { if (!this.file) return; this.pending = this.pending.then(() => appendFile(this.file, `${JSON.stringify({ order: ++this.order, observed_at: now(), kind, payload })}\n`)); }
|
|
47
|
+
write(kind, payload) { if (!this.file) return; this.pending = this.pending.then(() => appendFile(this.file, `${JSON.stringify({ order: ++this.order, observed_at: now(), kind, payload })}\n`)); void this.pending.catch(() => {}); return this.pending; }
|
|
51
48
|
flush() { return this.pending; }
|
|
52
49
|
}
|
|
53
50
|
|
|
54
51
|
export class CodexBridge {
|
|
55
52
|
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
53
|
|
|
54
|
+
async restoreItemPhases() {
|
|
55
|
+
if (this.options.journal) {
|
|
56
|
+
const source = createReadStream(this.options.journal, { encoding: "utf8" });
|
|
57
|
+
try {
|
|
58
|
+
for await (const line of readline.createInterface({ input: source })) {
|
|
59
|
+
let record; try { record = JSON.parse(line); } catch { continue; }
|
|
60
|
+
if (record.direction === "inbound" || record.kind === "inbound") this.observeItemPhase(record.payload ?? record.message ?? record);
|
|
61
|
+
}
|
|
62
|
+
} catch (error) { if (error.code !== "ENOENT") throw error; }
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
57
66
|
async start() {
|
|
67
|
+
await this.restoreItemPhases();
|
|
58
68
|
const bin = this.options.bin ?? process.env.DD_CODEX_BIN ?? "codex";
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
this.child
|
|
69
|
+
// Thread overrides arrive too late to prevent app-server's background
|
|
70
|
+
// marketplace clones. Apply the same isolation policy before startup.
|
|
71
|
+
this.child = spawn(bin, ["-c", `features.plugins=${CODEX_SESSION_CONFIG["features.plugins"]}`, "app-server"], { cwd: this.options.cwd ? absolute(this.options.cwd, "--cwd") : process.cwd(), env: { ...process.env, ...(this.options.env ?? {}) }, detached: process.platform !== "win32", stdio: ["pipe", "pipe", "pipe"] });
|
|
72
|
+
this.diagnostics = streamNativeDiagnostics(this.child.stderr, async chunk => { this.stderr = (this.stderr + chunk).slice(-65_536); await this.journal.write("stderr", { text: chunk }); }, error => this.rejectAll(error));
|
|
73
|
+
this.child.stdin?.on("error", (error) => this.rejectAll(nativeObservationError(error)));
|
|
62
74
|
this.child.on("error", (error) => this.rejectAll(error));
|
|
63
|
-
this.child.on("
|
|
64
|
-
|
|
75
|
+
this.child.on("close", code => { void Promise.all([this.nativeDrain, this.diagnostics]).then(() => this.rejectAll(Object.assign(new Error(`Codex app-server exited ${code}${this.stderr.trim() ? `: ${this.stderr.trim()}` : ""}`), { code: "bridge_exited" }))); });
|
|
76
|
+
this.nativeDrain = readNativeLines(this.child.stdout, async line => { this.receive(line); await this.journal.flush(); }, error => this.rejectAll(error));
|
|
65
77
|
if (this.options.onSpawn) await this.options.onSpawn(this.child);
|
|
66
78
|
await this.request("initialize", { clientInfo: { name: "dd-codex", version: "0.1.0" }, capabilities: { optOutNotificationMethods: [], experimentalApi: true } });
|
|
67
79
|
}
|
|
68
80
|
|
|
69
81
|
receive(line) {
|
|
70
82
|
let message; try { message = JSON.parse(line); } catch { this.journal.write("malformed", { line }); return; }
|
|
71
|
-
this.
|
|
72
|
-
this.
|
|
83
|
+
this.observeItemPhase(message);
|
|
84
|
+
try { this.observeTopology(message); }
|
|
85
|
+
catch (error) { this.topologyError = error; this.rejectAll(error); return; }
|
|
73
86
|
this.observeHookFailure(message);
|
|
87
|
+
this.observeNativeHook(message);
|
|
74
88
|
if (["model/rerouted", "thread/settings/updated"].includes(message.method)) {
|
|
75
89
|
const params = message.params ?? {}, id = params.threadId;
|
|
76
90
|
let root = id; const seen = new Set();
|
|
@@ -82,16 +96,57 @@ export class CodexBridge {
|
|
|
82
96
|
parentSessionId: this.descendants.get(id)?.parent_provider_session_id ?? null, requested: this.requestedProfiles.get(root),
|
|
83
97
|
observed: { model: rerouted ? params.toModel : settings.model, provider: settings.modelProvider, reasoning: settings.effort },
|
|
84
98
|
source: message.method, evidence: rerouted ? "response" : "configured", reason: params.reason ?? null, previousModel: params.fromModel ?? null
|
|
85
|
-
})).catch(
|
|
99
|
+
})).catch(cause => { const error = nativeObservationError(cause); this.modelObservationError = error; this.rejectAll(error); });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (message.method === "turn/started") {
|
|
103
|
+
this.currentTurns ??= new Map(); const id = message.params?.turn?.id, thread = message.params?.threadId;
|
|
104
|
+
const pendingStart = [...this.pending.values()].find(value => value.method === "turn/start" && value.threadId === thread);
|
|
105
|
+
const dispatching = pendingStart && !pendingStart.turnFloor?.has(id);
|
|
106
|
+
const child = this.descendants.get(thread), parentCurrent = child?.parent_turn_id && child.parent_turn_id === this.currentTurns.get(child.parent_provider_session_id);
|
|
107
|
+
if (id && this.turns.get(id)?.status !== "completed" && (dispatching || parentCurrent && child.status === "running" || !this.currentTurns.has(thread) && !this.requestedProfiles.has(thread))) {
|
|
108
|
+
if (this.currentTurns.get(thread) !== id) this.usageMarkers?.delete(thread);
|
|
109
|
+
this.currentTurns.set(thread, id);
|
|
86
110
|
}
|
|
87
111
|
}
|
|
88
|
-
|
|
112
|
+
const params = message.params ?? {}, currentTurn = this.currentTurns?.get(params.threadId);
|
|
113
|
+
let productive = message.method === "turn/started" || /^item\/(started|completed|[^/]+\/(delta|outputDelta|summaryTextDelta|textDelta))$/.test(message.method ?? "");
|
|
114
|
+
if (productive && currentTurn && (params.turnId ?? params.turn?.id) === currentTurn && ["turn/started", "item/started", "item/completed"].includes(message.method)) {
|
|
115
|
+
this.progressPhases ??= new Map(); const key = JSON.stringify([params.threadId, currentTurn, message.method, params.item?.id ?? null]);
|
|
116
|
+
if (this.progressPhases.has(key)) productive = false;
|
|
117
|
+
else if (this.progressPhases.size < 1_024) this.progressPhases.set(key, true);
|
|
118
|
+
else productive = false;
|
|
119
|
+
}
|
|
120
|
+
if (productive && /(?:Delta|delta)$/.test(message.method ?? "") && !params.delta && !params.outputDelta && !params.text) productive = false;
|
|
121
|
+
let usageProgress = false;
|
|
122
|
+
if (message.method === "thread/tokenUsage/updated" && params.turnId === currentTurn) {
|
|
123
|
+
this.usageMarkers ??= new Map(); const count = params.tokenUsage?.last?.totalTokens ?? params.tokenUsage?.total?.totalTokens;
|
|
124
|
+
const prior = this.usageMarkers.get(params.threadId) ?? 0;
|
|
125
|
+
if (Number.isFinite(count) && count > prior) { usageProgress = true; this.usageMarkers.set(params.threadId, count); }
|
|
126
|
+
}
|
|
127
|
+
if (message.id === undefined && currentTurn && (params.turnId ?? params.turn?.id) === currentTurn && (productive || usageProgress)) {
|
|
89
128
|
this.activity ??= new Map();
|
|
129
|
+
this.activityObservations ??= new Map();
|
|
90
130
|
const visited = new Set();
|
|
91
131
|
let id = message.params?.threadId;
|
|
92
132
|
while (id && !visited.has(id)) {
|
|
93
|
-
visited.add(id);
|
|
94
|
-
id = this.
|
|
133
|
+
visited.add(id); const cursor = (this.activity.get(id) ?? 0) + 1; this.activity.set(id, cursor); this.activityObservations.set(id, { cursor, observed_at: Date.now() });
|
|
134
|
+
const observer = this.turnObservers?.get(id), turnId = this.currentTurns?.get(id);
|
|
135
|
+
if (observer && turnId && !observer.turnFloor.has(turnId)) {
|
|
136
|
+
const pending = this.pending.get(observer.id);
|
|
137
|
+
if (pending) pending.progress = this.activityObservations.get(id);
|
|
138
|
+
const session = id;
|
|
139
|
+
void observer.publish("native_progress", { native_request_id: String(observer.id), session_id: id, observed_at: new Date(this.activityObservations.get(id).observed_at).toISOString() }).catch(cause => {
|
|
140
|
+
// Pipe publications can finish after this Turn's terminal response.
|
|
141
|
+
// Never attribute an old operation's storage failure to its successor.
|
|
142
|
+
if (this.turnObservers?.get(session) !== observer) return;
|
|
143
|
+
const error = nativeObservationError(cause); observer.progressError ??= error;
|
|
144
|
+
const current = this.pending.get(observer.id);
|
|
145
|
+
if (current === pending && current) { clearTimeout(current.timer); this.pending.delete(observer.id); current.reject(error); }
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
const child = this.descendants.get(id), parent = child?.parent_provider_session_id;
|
|
149
|
+
id = parent && child.parent_turn_id && child.parent_turn_id === this.currentTurns?.get(parent) ? parent : null;
|
|
95
150
|
}
|
|
96
151
|
}
|
|
97
152
|
// app-server currently returns complete turns even for `includeTurns:
|
|
@@ -116,6 +171,11 @@ export class CodexBridge {
|
|
|
116
171
|
return;
|
|
117
172
|
}
|
|
118
173
|
clearTimeout(pending.timer); this.pending.delete(message.id);
|
|
174
|
+
if (pending.method === "turn/start" && pending.threadId && message.result?.turn?.id) {
|
|
175
|
+
this.currentTurns ??= new Map();
|
|
176
|
+
if (this.currentTurns.get(pending.threadId) !== message.result.turn.id) this.usageMarkers?.delete(pending.threadId);
|
|
177
|
+
this.currentTurns.set(pending.threadId, message.result.turn.id);
|
|
178
|
+
}
|
|
119
179
|
if (pending.timedOut) this.journal.write("late_response", { ...responseSummary(message), method: pending.method, grace_ms: LATE_RESPONSE_GRACE_MS });
|
|
120
180
|
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
181
|
return;
|
|
@@ -125,7 +185,10 @@ export class CodexBridge {
|
|
|
125
185
|
this.nativeErrors.set(key, { error: message.params.error, willRetry: message.params.willRetry });
|
|
126
186
|
while (this.nativeErrors.size > 128) this.nativeErrors.delete(this.nativeErrors.keys().next().value);
|
|
127
187
|
}
|
|
128
|
-
if (message.method === "turn/completed")
|
|
188
|
+
if (message.method === "turn/completed") {
|
|
189
|
+
this.turns.set(message.params?.turn?.id ?? message.params?.threadId, { status: "completed", value: message.params, threadId: message.params?.threadId });
|
|
190
|
+
if (this.currentTurns?.get(message.params?.threadId) === message.params?.turn?.id) this.turnObservers?.delete(message.params.threadId);
|
|
191
|
+
}
|
|
129
192
|
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 });
|
|
130
193
|
const threadId = message.params?.threadId;
|
|
131
194
|
if (threadId && message.method === "item/started") this.finalMessages.delete(threadId);
|
|
@@ -140,6 +203,14 @@ export class CodexBridge {
|
|
|
140
203
|
}
|
|
141
204
|
|
|
142
205
|
observeNativeHook(message) {
|
|
206
|
+
if (message.method === "turn/started") {
|
|
207
|
+
const { threadId, turn } = message.params ?? {};
|
|
208
|
+
if (threadId && turn?.id && this.turns.get(turn.id)?.status !== "completed") {
|
|
209
|
+
this.nativeHookTurns ??= new Map(); this.nativeHookTurns.set(threadId, turn.id);
|
|
210
|
+
for (const [id, hook] of this.activeHooks ?? []) if (hook.threadId === threadId && hook.turnId !== turn.id) this.activeHooks.delete(id);
|
|
211
|
+
}
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
143
214
|
if (message.method === "turn/completed") {
|
|
144
215
|
const { threadId, turn } = message.params ?? {};
|
|
145
216
|
for (const [id, hook] of this.activeHooks ?? []) {
|
|
@@ -151,8 +222,12 @@ export class CodexBridge {
|
|
|
151
222
|
const { threadId, turnId, run } = message.params ?? {};
|
|
152
223
|
if (!this.isManagedHook(run) || typeof run.id !== "string" || !threadId) return;
|
|
153
224
|
this.activeHooks ??= new Map();
|
|
154
|
-
if (message.method === "hook/started")
|
|
155
|
-
|
|
225
|
+
if (message.method === "hook/started") {
|
|
226
|
+
if (this.nativeHookTurns?.has(threadId) && this.nativeHookTurns.get(threadId) !== turnId) return;
|
|
227
|
+
let root = threadId; const seen = new Set();
|
|
228
|
+
while (this.descendants.has(root) && !seen.has(root)) { seen.add(root); root = this.descendants.get(root).parent_provider_session_id; }
|
|
229
|
+
this.activeHooks.set(run.id, { threadId, turnId, root, rootTurnId: this.nativeHookTurns?.get(root) ?? null, rootGeneration: this.hookPromptGenerations?.get(root) ?? 0 });
|
|
230
|
+
} else if (this.activeHooks.get(run.id)?.turnId === turnId) this.activeHooks.delete(run.id);
|
|
156
231
|
}
|
|
157
232
|
|
|
158
233
|
verifyNativeHook(input) {
|
|
@@ -166,7 +241,8 @@ export class CodexBridge {
|
|
|
166
241
|
seen.add(current); current = this.descendants.get(current)?.parent_provider_session_id;
|
|
167
242
|
}
|
|
168
243
|
if (!active || active.threadId !== sessionId || active.turnId !== turnId || !toolCallId
|
|
169
|
-
|| current !== rootSessionId || !this.requestedProfiles.has(rootSessionId)
|
|
244
|
+
|| current !== rootSessionId || !this.requestedProfiles.has(rootSessionId)
|
|
245
|
+
|| active.rootGeneration !== (this.hookPromptGenerations?.get(rootSessionId) ?? 0)) {
|
|
170
246
|
fail("No matching active managed native hook", "native_hook_unproven", { provider_session_id: sessionId, tool_call_id: toolCallId });
|
|
171
247
|
}
|
|
172
248
|
return { rootSessionId, sessionId, toolCallId, parentSessionId: sessionId === rootSessionId ? null : this.descendants.get(sessionId)?.parent_provider_session_id };
|
|
@@ -179,6 +255,11 @@ export class CodexBridge {
|
|
|
179
255
|
run.eventName !== "preToolUse" || run.handlerType !== "command" ||
|
|
180
256
|
run.statusMessage !== "dd-flow runtime observation" || !home ||
|
|
181
257
|
run.sourcePath !== path.join(home, "hooks.json")) return;
|
|
258
|
+
const active = this.activeHooks?.get(run.id);
|
|
259
|
+
if (!active || active.threadId !== threadId || active.turnId !== turnId) return;
|
|
260
|
+
if (this.nativeHookTurns?.has(threadId) && this.nativeHookTurns.get(threadId) !== turnId
|
|
261
|
+
|| active.rootTurnId !== null && this.nativeHookTurns?.get(active.root) !== active.rootTurnId
|
|
262
|
+
|| active.rootGeneration !== (this.hookPromptGenerations?.get(active.root) ?? 0)) return;
|
|
182
263
|
const ancestors = new Set();
|
|
183
264
|
let root;
|
|
184
265
|
let current = threadId;
|
|
@@ -192,78 +273,120 @@ export class CodexBridge {
|
|
|
192
273
|
if (this.hookFailures.has(root)) return;
|
|
193
274
|
const entries = Array.isArray(run.entries) ? run.entries : [];
|
|
194
275
|
const reason = entries.filter(entry => entry.kind === "error").map(entry => entry.text).join("; ");
|
|
195
|
-
const error =
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
hook_run_id: run.id,
|
|
199
|
-
|
|
276
|
+
const error = reportedFailure(entries.filter(entry => entry.kind === "error").map(entry => entry.text).join("\n"), "", reason || "Managed Codex hook failed", /timed out/i.test(reason) ? "native_hook_timeout" : "native_hook_failed");
|
|
277
|
+
const correction = isProvenShellCorrection(error);
|
|
278
|
+
error.details = { ...error.details, native_hook_binding: { provider_session_id: threadId, root_provider_session_id: root, turn_id: turnId,
|
|
279
|
+
hook_run_id: run.id, source: "codex.hook/completed" }, provider_session_id: threadId, root_provider_session_id: root, turn_id: turnId,
|
|
280
|
+
hook_run_id: run.id, effect: correction ? "no_effect" : "unknown", duration_ms: run.durationMs };
|
|
281
|
+
if (correction) {
|
|
282
|
+
try { void Promise.resolve(this.journal.write("native_hook_correction", errorRecord(error))).catch(storage => { error.cleanup_error ??= errorRecord(storage); }); }
|
|
283
|
+
catch (storage) { error.cleanup_error ??= errorRecord(storage); }
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
200
286
|
this.hookFailures.set(root, error);
|
|
201
|
-
this.journal.write("native_hook_failure", { code: error.code, message: error.message, ...error.details });
|
|
202
287
|
for (const [id, pending] of this.pending) {
|
|
203
288
|
if (ancestors.has(pending.threadId) && pending.method !== "turn/interrupt") {
|
|
204
289
|
clearTimeout(pending.timer); this.pending.delete(id); pending.reject(error);
|
|
205
290
|
}
|
|
206
291
|
}
|
|
292
|
+
try { void Promise.resolve(this.journal.write("native_hook_failure", errorRecord(error))).catch(storage => { error.cleanup_error ??= errorRecord(storage); }); }
|
|
293
|
+
catch (storage) { error.cleanup_error ??= errorRecord(storage); }
|
|
207
294
|
}
|
|
208
295
|
|
|
209
296
|
assertHookHealthy(sessionId) {
|
|
297
|
+
if (this.topologyError) throw this.topologyError;
|
|
210
298
|
const visited = new Set();
|
|
211
299
|
while (sessionId && !visited.has(sessionId)) {
|
|
212
300
|
visited.add(sessionId);
|
|
301
|
+
if (this.turnObservers?.get(sessionId)?.progressError) throw this.turnObservers.get(sessionId).progressError;
|
|
213
302
|
const error = this.hookFailures?.get(sessionId);
|
|
214
303
|
if (error) throw error;
|
|
215
304
|
sessionId = this.descendants.get(sessionId)?.parent_provider_session_id;
|
|
216
305
|
}
|
|
217
306
|
}
|
|
218
307
|
|
|
308
|
+
observeItemPhase(message) {
|
|
309
|
+
const { threadId, turnId, item } = message?.params ?? {};
|
|
310
|
+
if (message?.method !== "item/completed" || !threadId || !turnId || !item?.id || classifyCodexItem(item.type) !== "tool") return;
|
|
311
|
+
this.itemPhases ??= new Map();
|
|
312
|
+
const key = JSON.stringify([threadId, turnId, item.id]);
|
|
313
|
+
const previous = this.itemPhases.get(key);
|
|
314
|
+
if (previous && (previous.type !== item.type || previous.status !== (item.status ?? null))) {
|
|
315
|
+
this.topologyError = Object.assign(new Error("Conflicting native item completion evidence"), { code: "native_item_completion_conflict", details: { provider_session_id: threadId, turn_id: turnId, item_id: item.id } });
|
|
316
|
+
this.rejectAll(this.topologyError); return;
|
|
317
|
+
}
|
|
318
|
+
this.itemPhases.set(key, { type: item.type, status: item.status ?? null });
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
bindChild(child, parent, facts) {
|
|
322
|
+
const previous = this.descendants.get(child);
|
|
323
|
+
if (child === parent || this.requestedProfiles.has(child) || previous?.parent_provider_session_id && previous.parent_provider_session_id !== parent) {
|
|
324
|
+
fail("Conflicting native child parent identity", "native_topology_conflict", { child_session_id: child, parent_session_id: parent, retained_parent: previous?.parent_provider_session_id ?? null });
|
|
325
|
+
}
|
|
326
|
+
const seen = new Set([child]);
|
|
327
|
+
for (let ancestor = parent; ancestor; ancestor = this.descendants.get(ancestor)?.parent_provider_session_id) {
|
|
328
|
+
if (seen.has(ancestor)) fail("Native child ancestry contains a cycle", "native_topology_cycle", { child_session_id: child, parent_session_id: parent });
|
|
329
|
+
seen.add(ancestor);
|
|
330
|
+
}
|
|
331
|
+
this.descendants.set(child, { ...previous, ...facts, parent_turn_id: facts.parent_turn_id ?? previous?.parent_turn_id ?? null, provider_session_id: child, parent_provider_session_id: parent });
|
|
332
|
+
}
|
|
333
|
+
|
|
219
334
|
// Collaboration child Threads are provider facts. Keep only objects that
|
|
220
335
|
// explicitly carry a parent relationship, including native spawn receipts.
|
|
221
336
|
// A wait/send receiver is not necessarily a child of its sender.
|
|
222
|
-
observeTopology(value, depth = 0) {
|
|
337
|
+
observeTopology(value, depth = 0, parentTurn = null) {
|
|
223
338
|
if (!value || typeof value !== "object" || depth > 8) return;
|
|
224
|
-
if (Array.isArray(value)) { for (const item of value) this.observeTopology(item, depth + 1); return; }
|
|
339
|
+
if (Array.isArray(value)) { for (const item of value) this.observeTopology(item, depth + 1, parentTurn); return; }
|
|
225
340
|
if (depth === 0 && ["turn/started", "turn/completed"].includes(value.method)) {
|
|
226
341
|
const { threadId, turn } = value.params ?? {};
|
|
227
|
-
const previous = this.descendants.get(threadId);
|
|
342
|
+
const previous = this.descendants.get(threadId) ?? (threadId && !this.requestedProfiles.has(threadId) ? { provider_session_id: threadId, status: "unknown" } : null);
|
|
228
343
|
if (previous && typeof turn?.id === "string" && turn.id) {
|
|
229
344
|
if (value.method === "turn/started" && previous.active_turn_id !== turn.id && this.turns.get(turn.id)?.status !== "completed") {
|
|
230
345
|
this.descendants.set(threadId, { ...previous, status: "running", active_turn_id: turn.id, terminal_turn_id: null, provider_error: null });
|
|
231
346
|
} else if (value.method === "turn/completed" && (!previous.active_turn_id || previous.active_turn_id === turn.id)) {
|
|
232
347
|
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) });
|
|
348
|
+
active_turn_id: turn.id, terminal_turn_id: turn.id, terminal_observed_at: previous.terminal_turn_id === turn.id ? previous.terminal_observed_at ?? now() : now(), provider_error: nativeTurnError(this, threadId, turn), native_turn_items: turnItemEvidence(turn, this, threadId) });
|
|
234
349
|
}
|
|
235
350
|
}
|
|
236
351
|
}
|
|
237
|
-
const activity = value.params?.item;
|
|
352
|
+
const activity = depth === 0 && ["item/started", "item/completed"].includes(value.method) ? value.params?.item : null;
|
|
238
353
|
const activityParent = value.params?.threadId;
|
|
239
354
|
if (activity?.type === "subAgentActivity" && typeof activity.agentThreadId === "string" && activity.agentThreadId &&
|
|
240
|
-
typeof activityParent === "string" && activityParent
|
|
355
|
+
typeof activityParent === "string" && activityParent) {
|
|
241
356
|
const previous = this.descendants.get(activity.agentThreadId);
|
|
242
357
|
const observed = activity.kind === "completed" ? "completed" : activity.kind === "interrupted" ? "interrupted" : "running";
|
|
243
358
|
// Parent activity has no child Turn identity on some app-server builds.
|
|
244
359
|
// It can discover a child, but cannot reopen or close a later known Turn.
|
|
245
360
|
const status = previous?.active_turn_id || previous?.status === "completed" ? previous.status : observed;
|
|
246
|
-
this.
|
|
247
|
-
|
|
248
|
-
|
|
361
|
+
if (activity.kind === "started") this.bindChild(activity.agentThreadId, activityParent, { status, parent_turn_id: value.params?.turnId === this.currentTurns?.get(activityParent) ? value.params.turnId : null, agent_path: activity.agentPath ?? previous?.agent_path ?? null, source: "codex.app_server.subAgentActivity" });
|
|
362
|
+
else if (["completed", "interrupted"].includes(activity.kind) && activity.agentThreadId !== activityParent && !this.requestedProfiles.has(activity.agentThreadId)) {
|
|
363
|
+
this.descendants.set(activity.agentThreadId, { ...previous, provider_session_id: activity.agentThreadId, status });
|
|
364
|
+
}
|
|
249
365
|
}
|
|
250
366
|
if (value.type === "collabAgentToolCall" && value.tool === "spawnAgent" &&
|
|
251
367
|
typeof value.senderThreadId === "string" && value.senderThreadId && Array.isArray(value.receiverThreadIds)) {
|
|
252
368
|
for (const child of value.receiverThreadIds) {
|
|
253
|
-
if (typeof child !== "string" || !child
|
|
369
|
+
if (typeof child !== "string" || !child) continue;
|
|
254
370
|
const previous = this.descendants.get(child);
|
|
255
|
-
this.
|
|
371
|
+
this.bindChild(child, value.senderThreadId, {
|
|
372
|
+
parent_turn_id: parentTurn === this.currentTurns?.get(value.senderThreadId) ? parentTurn : null,
|
|
256
373
|
status: previous?.status === "completed" || previous?.active_turn_id ? previous.status : value.agentsStates?.[child]?.status ?? previous?.status ?? "unknown", source: "codex.app_server" });
|
|
257
374
|
}
|
|
258
375
|
}
|
|
259
|
-
const
|
|
260
|
-
const
|
|
376
|
+
const thread = depth === 0 && ["thread/started", "thread/status/changed"].includes(value.method) ? value.params?.thread : null;
|
|
377
|
+
const id = thread?.id ?? null;
|
|
378
|
+
const parent = thread?.parentThreadId ?? thread?.parent_thread_id ?? null;
|
|
261
379
|
if (typeof id === "string" && id && typeof parent === "string" && parent) {
|
|
262
380
|
const previous = this.descendants.get(id);
|
|
263
|
-
this.
|
|
264
|
-
|
|
381
|
+
this.bindChild(id, parent, {
|
|
382
|
+
parent_turn_id: !previous ? this.currentTurns?.get(parent) ?? null : previous.parent_turn_id,
|
|
383
|
+
status: previous?.status === "completed" || previous?.active_turn_id ? previous.status : thread.status?.type ?? thread.status ?? previous?.status ?? "unknown", source: "codex.app_server" });
|
|
384
|
+
}
|
|
385
|
+
if (depth === 0) {
|
|
386
|
+
if (["item/started", "item/completed"].includes(value.method)) this.observeTopology(value.params?.item, depth + 1, value.params?.turnId);
|
|
387
|
+
const nativeThread = this.pending.get(value.id)?.method === "thread/read" ? value.result?.thread : null;
|
|
388
|
+
if (nativeThread && Array.isArray(nativeThread.turns)) for (const turn of nativeThread.turns) this.observeTopology(turn.items, depth + 1, turn.id);
|
|
265
389
|
}
|
|
266
|
-
for (const child of Object.values(value)) this.observeTopology(child, depth + 1);
|
|
267
390
|
}
|
|
268
391
|
|
|
269
392
|
directChildren(sessionId) {
|
|
@@ -284,6 +407,15 @@ export class CodexBridge {
|
|
|
284
407
|
}
|
|
285
408
|
|
|
286
409
|
request(method, params = {}, timeoutMs = this.options.timeoutMs ?? 120_000) {
|
|
410
|
+
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;
|
|
411
|
+
if (method === "turn/start" && params.threadId) {
|
|
412
|
+
// Only one productive root Turn is admitted by the daemon fence. Drop
|
|
413
|
+
// lifecycle replay keys at the next dispatch, not at arbitrary reads.
|
|
414
|
+
this.progressPhases?.clear();
|
|
415
|
+
this.hookPromptGenerations ??= new Map();
|
|
416
|
+
this.hookPromptGenerations.set(params.threadId, (this.hookPromptGenerations.get(params.threadId) ?? 0) + 1);
|
|
417
|
+
this.currentTurns?.delete(params.threadId);
|
|
418
|
+
}
|
|
287
419
|
const id = this.nextId++;
|
|
288
420
|
return new Promise((resolve, reject) => {
|
|
289
421
|
const timer = observedTimeout(() => {
|
|
@@ -294,9 +426,15 @@ export class CodexBridge {
|
|
|
294
426
|
pending.timer = observedTimeout(() => {
|
|
295
427
|
if (this.pending.get(id) !== pending) return;
|
|
296
428
|
this.pending.delete(id); reject(timeoutError);
|
|
297
|
-
}, LATE_RESPONSE_GRACE_MS);
|
|
298
|
-
}, timeoutMs, { onGap: gap => this.journal.write("observation_gap", { ...gap, method, request_id: id }) });
|
|
299
|
-
|
|
429
|
+
}, LATE_RESPONSE_GRACE_MS, { progress: () => pending.progress });
|
|
430
|
+
}, timeoutMs, { progress: () => this.pending.get(id)?.progress, onGap: gap => this.journal.write("observation_gap", { ...gap, method, request_id: id }) });
|
|
431
|
+
const publish = operationProgressReporter();
|
|
432
|
+
this.pending.set(id, { id, resolve, reject, timer, method, threadId: params.threadId, turnFloor });
|
|
433
|
+
if (method === "turn/start" && params.threadId) {
|
|
434
|
+
this.turnObservers ??= new Map(); this.turnObservers.set(params.threadId, { id, turnFloor, publish });
|
|
435
|
+
}
|
|
436
|
+
this.send({ jsonrpc: "2.0", id, method, params });
|
|
437
|
+
if (method === "turn/start") this.journal.write("native_dispatch", { operation_id: this.dispatchOperationId ?? process.env.DD_EVAL_OPERATION_ID ?? null, provider_session_id: params.threadId, native_request_id: id });
|
|
300
438
|
});
|
|
301
439
|
}
|
|
302
440
|
send(value) {
|
|
@@ -308,20 +446,20 @@ export class CodexBridge {
|
|
|
308
446
|
throw error;
|
|
309
447
|
}
|
|
310
448
|
try { stdin.write(`${JSON.stringify(value)}\n`); }
|
|
311
|
-
catch (
|
|
449
|
+
catch (cause) { const error = nativeObservationError(cause); this.rejectAll(error); throw error; }
|
|
312
450
|
}
|
|
313
|
-
rejectAll(error) { for (const pending of this.pending.values()) { clearTimeout(pending.timer); pending.reject(error); } this.pending.clear(); }
|
|
451
|
+
rejectAll(error) { for (const pending of this.pending.values()) { clearTimeout(pending.timer); pending.reject(error); } this.pending.clear(); this.turnObservers?.clear(); }
|
|
314
452
|
async close() {
|
|
315
|
-
|
|
316
|
-
this.child.stdin.end();
|
|
317
|
-
await Promise.race([new Promise((resolve) => this.child.once("exit", resolve)), new Promise((resolve) => setTimeout(resolve, 2_000))]);
|
|
318
|
-
await stopProcessGroup(this.child, 2_000);
|
|
319
|
-
await this.modelObservations;
|
|
320
|
-
await this.journal.flush();
|
|
453
|
+
return await closeBridgeProcess(this, async () => { await Promise.all([this.nativeDrain, this.diagnostics]); await this.modelObservations; await this.journal.flush(); });
|
|
321
454
|
}
|
|
322
455
|
}
|
|
323
456
|
|
|
324
|
-
async function withBridge(options, action) {
|
|
457
|
+
async function withBridge(options, action) {
|
|
458
|
+
const bridge = new CodexBridge(options); let primary, result;
|
|
459
|
+
try { await bridge.start(); result = await action(bridge); return result; }
|
|
460
|
+
catch (error) { primary = error; throw error; }
|
|
461
|
+
finally { try { await bridge.close(); } catch (error) { if (!primary) throw nativeObservationError(error, result === undefined ? {} : { result }); primary.cleanup_error = errorRecord(error); primary.details = { ...primary.details, cleanup_error: primary.cleanup_error }; } }
|
|
462
|
+
}
|
|
325
463
|
function profile(options) { return { harness: "codex-desktop", model: options.model ?? null, reasoning: options.reasoning ?? null }; }
|
|
326
464
|
function threadId(result) { const id = result?.thread?.id ?? result?.threadId; return required(id, "Codex thread id"); }
|
|
327
465
|
function directChildren(bridge, sessionId) { return typeof bridge?.directChildren === "function" ? bridge.directChildren(sessionId) : []; }
|
|
@@ -334,7 +472,7 @@ async function observedChildren(bridge, sessionId) {
|
|
|
334
472
|
const turn = observed?.turn;
|
|
335
473
|
if (turn?.id !== child.terminal_turn_id || turn.status !== child.status) return child;
|
|
336
474
|
return { ...child, provider_error: nativeTurnError(bridge, child.provider_session_id, turn) ?? child.provider_error,
|
|
337
|
-
native_turn_items: turnItemEvidence(turn) };
|
|
475
|
+
native_turn_items: turnItemEvidence(turn, bridge, child.provider_session_id) };
|
|
338
476
|
}));
|
|
339
477
|
}
|
|
340
478
|
function codexSessionConfig() { return { ...CODEX_SESSION_CONFIG }; }
|
|
@@ -412,6 +550,7 @@ export async function startSessionWithBridge(bridge, options) {
|
|
|
412
550
|
bridge.finalMessages?.delete(sessionId);
|
|
413
551
|
options.assertDispatch?.();
|
|
414
552
|
bridge.assertHookHealthy?.(sessionId);
|
|
553
|
+
bridge.dispatchOperationId = options.operationId;
|
|
415
554
|
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);
|
|
416
555
|
const turn = started?.turn ?? started; const turnId = turn?.id ?? null;
|
|
417
556
|
if (typeof turnId !== "string" || !turnId) throw new Error("Codex did not return a Turn id");
|
|
@@ -420,6 +559,7 @@ export async function startSessionWithBridge(bridge, options) {
|
|
|
420
559
|
|
|
421
560
|
export async function promptSessionWithBridge(bridge, options) {
|
|
422
561
|
const sessionId = required(options.sessionId, "--session-id"); const cwd = absolute(options.cwd, "--cwd"); const prompt = required(options.prompt, "--prompt");
|
|
562
|
+
validateObservationDuration(options.livenessTimeoutMs ?? options.timeoutMs ?? 1_800_000);
|
|
423
563
|
await ensureThreadLoaded(bridge, sessionId, options, cwd);
|
|
424
564
|
// A final-message fallback belongs only to the Turn that produced it. A
|
|
425
565
|
// new Turn may start before app-server emits its first item notification;
|
|
@@ -428,26 +568,44 @@ export async function promptSessionWithBridge(bridge, options) {
|
|
|
428
568
|
bridge.finalMessages?.delete(sessionId);
|
|
429
569
|
options.assertDispatch?.();
|
|
430
570
|
bridge.assertHookHealthy?.(sessionId);
|
|
431
|
-
|
|
571
|
+
bridge.dispatchOperationId = options.operationId;
|
|
572
|
+
const clock = new ObservationClock({ timeoutMs: options.livenessTimeoutMs ?? options.timeoutMs ?? 1_800_000, onGap: gap => bridge.journal?.write("observation_gap", { ...gap, session_id: sessionId }) });
|
|
573
|
+
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);
|
|
432
574
|
const turn = started?.turn ?? started; const turnId = turn?.id ?? null;
|
|
433
575
|
let completed;
|
|
434
|
-
try { completed = await waitForTurn(bridge, sessionId, turnId, options.timeoutMs ?? 1_800_000, options, cwd); }
|
|
576
|
+
try { completed = await waitForTurn(bridge, sessionId, turnId, options.livenessTimeoutMs ?? options.timeoutMs ?? 1_800_000, options, cwd, clock); }
|
|
435
577
|
catch (error) {
|
|
578
|
+
const retained = bridge.turns?.get(turnId);
|
|
579
|
+
if (retained?.value?.threadId === sessionId && retained.value.turn?.id === turnId && retained.value.turn.status === "completed") {
|
|
580
|
+
throw Object.assign(new Error("Codex completed natively but mandatory observation failed"), { code: "native_outcome_observation_failed", retryable: false,
|
|
581
|
+
details: { native_outcome: { harness: "codex-desktop", provider_session_id: sessionId, turn_id: turnId, status: "completed", turn: retained.value.turn }, settled: false, observation_error: errorRecord(error) }, cause: error });
|
|
582
|
+
}
|
|
436
583
|
if (error?.code === "turn_interrupted") {
|
|
437
584
|
try {
|
|
438
585
|
const observed = await readTurn(bridge, sessionId, turnId, true);
|
|
439
586
|
if (observed?.turn?.id === turnId && observed.turn.status === error.details?.terminal_status) {
|
|
440
|
-
error.details.native_turn_items = turnItemEvidence(observed.turn);
|
|
587
|
+
error.details.native_turn_items = turnItemEvidence(observed.turn, bridge, sessionId);
|
|
441
588
|
error.details.provider_error ??= observed.turn.error ?? null;
|
|
442
589
|
}
|
|
443
590
|
} catch { /* The primary native terminal failure remains authoritative. */ }
|
|
591
|
+
try { await bridge.journal?.flush?.(); }
|
|
592
|
+
catch (observationError) {
|
|
593
|
+
throw Object.assign(new Error("Codex terminal failure lacks durable native item evidence"), { code: "native_outcome_observation_failed", retryable: false,
|
|
594
|
+
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 });
|
|
595
|
+
}
|
|
444
596
|
}
|
|
445
597
|
throw error;
|
|
446
598
|
}
|
|
447
|
-
const
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
599
|
+
const nativeOutcome = { harness: "codex-desktop", provider_session_id: sessionId, adapter_session_id: sessionId, turn_id: turnId, status: "completed", turn: completed };
|
|
600
|
+
try {
|
|
601
|
+
const thread = await readCompletedThread(bridge, sessionId);
|
|
602
|
+
bridge.assertHookHealthy?.(sessionId);
|
|
603
|
+
const assistantText = latestAssistantText(thread?.turns?.find(item => item?.id === turnId)) ?? latestAssistantText(completed);
|
|
604
|
+
return { ...nativeOutcome, ...(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") };
|
|
605
|
+
} catch (error) {
|
|
606
|
+
throw Object.assign(new Error("Codex completed natively but mandatory observation failed"), { code: "native_outcome_observation_failed", retryable: false,
|
|
607
|
+
details: { native_outcome: nativeOutcome, settled: false, observation_error: errorRecord(error) }, cause: error });
|
|
608
|
+
}
|
|
451
609
|
}
|
|
452
610
|
|
|
453
611
|
async function ensureThreadLoaded(bridge, sessionId, options, cwd) {
|
|
@@ -463,17 +621,17 @@ async function ensureThreadLoaded(bridge, sessionId, options, cwd) {
|
|
|
463
621
|
}, 30_000);
|
|
464
622
|
}
|
|
465
623
|
|
|
466
|
-
async function waitForTurn(bridge, sessionId, turnId, timeoutMs, options, cwd) {
|
|
467
|
-
const clock = new ObservationClock({ timeoutMs, onGap: gap => bridge.journal?.write("observation_gap", { ...gap, session_id: sessionId, turn_id: turnId }) });
|
|
624
|
+
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 }) })) {
|
|
468
625
|
// app-server can briefly expose the new Turn as `interrupted` between the
|
|
469
626
|
// turn/start response and its turn/started notification. Give that snapshot
|
|
470
627
|
// one event-loop grace window before treating failure as authoritative.
|
|
471
628
|
const failureNotBefore = Date.now() + 1_000;
|
|
472
629
|
let nextStoredReadAt = Date.now();
|
|
473
|
-
while (
|
|
630
|
+
while (true) {
|
|
474
631
|
bridge.assertHookHealthy?.(sessionId);
|
|
475
632
|
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);
|
|
476
|
-
|
|
633
|
+
const expired = clock.sample(bridge.activityObservations?.get(sessionId) ?? bridge.activity?.get(sessionId));
|
|
634
|
+
if (expired || Date.now() >= nextStoredReadAt) {
|
|
477
635
|
const stored = await readTurn(bridge, sessionId, turnId, false);
|
|
478
636
|
if (stored?.turn && isAuthoritativeTerminal(bridge, turnId, stored.turn.status, stored.threadStatus, failureNotBefore)) return terminalTurn({ turn: stored.turn }, sessionId, turnId, bridge);
|
|
479
637
|
// A loaded app-server can retain a stale `active` Thread state after a
|
|
@@ -515,9 +673,10 @@ async function waitForTurn(bridge, sessionId, turnId, timeoutMs, options, cwd) {
|
|
|
515
673
|
}
|
|
516
674
|
nextStoredReadAt = Date.now() < failureNotBefore ? failureNotBefore : Date.now() + 15_000;
|
|
517
675
|
}
|
|
676
|
+
const terminal = bridge.turns.get(turnId); if (terminal?.status === "completed" && (!terminal.threadId || terminal.threadId === sessionId)) return terminalTurn(terminal.value, sessionId, turnId, bridge);
|
|
677
|
+
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" });
|
|
518
678
|
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
519
679
|
}
|
|
520
|
-
fail("Codex turn timed out", "turn_timeout");
|
|
521
680
|
}
|
|
522
681
|
|
|
523
682
|
function terminalTurn(value, sessionId, turnId, bridge) {
|
|
@@ -528,7 +687,7 @@ function terminalTurn(value, sessionId, turnId, bridge) {
|
|
|
528
687
|
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
688
|
const providerError = nativeTurnError(bridge, sessionId, turn);
|
|
530
689
|
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) });
|
|
690
|
+
provider_error: providerError, native_turn_items: turnItemEvidence(turn, bridge, sessionId) });
|
|
532
691
|
return value;
|
|
533
692
|
}
|
|
534
693
|
|
|
@@ -539,30 +698,28 @@ function nativeTurnError(bridge, sessionId, turn) {
|
|
|
539
698
|
return error && native?.willRetry !== undefined && typeof error === "object" ? { ...error, willRetry: native.willRetry } : error;
|
|
540
699
|
}
|
|
541
700
|
|
|
542
|
-
function turnItemEvidence(turn) {
|
|
701
|
+
export function turnItemEvidence(turn, bridge, sessionId) {
|
|
543
702
|
const items = turn?.items;
|
|
703
|
+
const pending = item => {
|
|
704
|
+
const kind = classifyCodexItem(item?.type);
|
|
705
|
+
if (kind === "non_tool") return false;
|
|
706
|
+
if (kind === "unknown") return true;
|
|
707
|
+
const phase = bridge?.itemPhases?.get(JSON.stringify([sessionId, turn?.id, item?.id]));
|
|
708
|
+
if (phase && (phase.type !== item?.type || phase.status !== null && item?.status !== undefined && phase.status !== item.status)) return true;
|
|
709
|
+
if (phase && ![null, "completed", "failed", "declined", "interrupted"].includes(phase.status)) return true;
|
|
710
|
+
return !["completed", "failed", "declined", "interrupted"].includes(item?.status) && !(phase && phase.type === item?.type && (item?.status === undefined || item?.status === null));
|
|
711
|
+
};
|
|
544
712
|
return Array.isArray(items) && turn?.itemsView !== "notLoaded" ? { observed: true, count: items.length, types: items.map(item => item?.type ?? "unknown"),
|
|
545
|
-
items: items.map(item =>
|
|
546
|
-
|
|
547
|
-
|
|
713
|
+
items: items.map(item => {
|
|
714
|
+
const phase = bridge?.itemPhases?.get(JSON.stringify([sessionId, turn?.id, item?.id]));
|
|
715
|
+
return { id: item?.id ?? null, type: item?.type ?? "unknown", status: item?.status ?? null,
|
|
716
|
+
...(phase && !pending(item) ? { completion_phase: { session_id: sessionId, turn_id: turn.id, item_id: item.id, status: phase.status, source: "codex.item/completed" } } : {}) };
|
|
717
|
+
}),
|
|
718
|
+
pending: items.some(pending),
|
|
719
|
+
possible_effects: items.some(item => classifyCodexItem(item?.type) !== "non_tool") } : { observed: false };
|
|
548
720
|
}
|
|
549
721
|
|
|
550
|
-
export
|
|
551
|
-
let current = error;
|
|
552
|
-
for (let depth = 0; depth < 4 && current && typeof current === "object"; depth += 1) {
|
|
553
|
-
if (current.code === "turn_interrupted") {
|
|
554
|
-
const details = current.details ?? {}, native = details.provider_error;
|
|
555
|
-
const code = native?.codexErrorInfo;
|
|
556
|
-
return details.terminal_status === "failed" && typeof details.provider_session_id === "string" && details.provider_session_id === expectedSessionId
|
|
557
|
-
&& typeof details.turn_id === "string" && Boolean(details.turn_id) && details.native_turn_id === details.turn_id
|
|
558
|
-
&& (expectedTurnId === undefined || details.turn_id === expectedTurnId)
|
|
559
|
-
&& (code === "serverOverloaded" || code?.code === "serverOverloaded");
|
|
560
|
-
}
|
|
561
|
-
if (current.code !== "harness_adapter_failed") return false;
|
|
562
|
-
current = current.details?.cause ?? current.cause;
|
|
563
|
-
}
|
|
564
|
-
return false;
|
|
565
|
-
}
|
|
722
|
+
export { terminalCodexOverload } from "./codex-capacity-policy.mjs";
|
|
566
723
|
|
|
567
724
|
function isTerminalTurnStatus(status) { return ["completed", "failed", "interrupted", "cancelled"].includes(status); }
|
|
568
725
|
function isAuthoritativeTerminal(bridge, turnId, status, threadStatus, failureNotBefore) {
|
|
@@ -589,11 +746,12 @@ async function readPersistedTurn(bridge, sessionId, turnId) {
|
|
|
589
746
|
}
|
|
590
747
|
|
|
591
748
|
async function readCompletedThread(bridge, sessionId) {
|
|
749
|
+
let lastError;
|
|
592
750
|
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
593
751
|
try { const result = await bridge.request("thread/read", { threadId: sessionId, includeTurns: true }, 10_000); return result?.thread ?? result; }
|
|
594
|
-
catch (error) { bridge.assertHookHealthy?.(sessionId); bridge.journal.write("thread_read_retry", { session_id: sessionId, attempt, error: error
|
|
752
|
+
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)); }
|
|
595
753
|
}
|
|
596
|
-
|
|
754
|
+
throw lastError;
|
|
597
755
|
}
|
|
598
756
|
|
|
599
757
|
function latestAssistantText(value) {
|
|
@@ -618,7 +776,7 @@ export async function inspectSession(options) {
|
|
|
618
776
|
export async function inspectSessionWithBridge(bridge, options) {
|
|
619
777
|
const sessionId = required(options.sessionId, "--session-id"); const value = await bridge.request("thread/read", { threadId: sessionId, includeTurns: false }); const thread = value?.thread ?? value;
|
|
620
778
|
const terminal = options.turnId ? (await readTurn(bridge, sessionId, options.turnId, true))?.turn : null;
|
|
621
|
-
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) } : {};
|
|
779
|
+
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) } : {};
|
|
622
780
|
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") };
|
|
623
781
|
}
|
|
624
782
|
|