@deksden-com/dd-flow-cli 0.9.0-beta.124 → 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 +16 -0
- package/README.md +10 -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/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/daemon-observation.d.mts +2 -0
- package/dist/harness-runtime/lib/daemon-observation.mjs +163 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +60 -13
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +151 -72
- package/dist/harness-runtime/lib/dd-agy.mjs +11 -23
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +9 -14
- package/dist/harness-runtime/lib/dd-codex.mjs +252 -80
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +14 -19
- package/dist/harness-runtime/lib/dd-droid.mjs +117 -31
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +56 -30
- package/dist/harness-runtime/lib/dd-grok.mjs +15 -13
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +18 -27
- package/dist/harness-runtime/lib/dd-opencode.mjs +44 -14
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +56 -53
- package/dist/harness-runtime/lib/dd-zcode.mjs +200 -111
- 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 +10 -1
- package/dist/harness-runtime/lib/managed-daemon.mjs +228 -40
- 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/run-control-receipt.schema.json +19 -0
- package/dist/services/code-checks.js +79 -9
- package/dist/services/codex-capacity.js +117 -41
- package/dist/services/codex-hook-delivery.js +2 -2
- package/dist/services/compatibility-preflight.js +4 -1
- package/dist/services/controller-fanout.js +9 -76
- package/dist/services/engines.js +70 -20
- package/dist/services/eval-snapshots.js +179 -40
- package/dist/services/external-work-launch.js +13 -8
- package/dist/services/harness-adapter.js +175 -11
- 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 +103 -47
- package/dist/services/merge-server.js +58 -21
- package/dist/services/recovery-observation-budget.js +67 -7
- package/dist/services/recovery-snapshot-database.js +7 -4
- 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 +169 -46
- package/dist/services/run-controller-capture.js +14 -2
- package/dist/services/run-controller-process.js +11 -4
- package/dist/services/run-controller-state.js +68 -7
- package/dist/services/run-controller.js +166 -94
- package/dist/services/run-fork.js +8 -3
- package/dist/services/run-recovery-guard.js +75 -0
- package/dist/services/run-recovery-runtime.js +1 -56
- package/dist/services/run-recovery.js +17 -13
- package/dist/services/runs.js +39 -6
- 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/vnext-code-review.js +10 -9
- 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 +10 -9
- package/dist/services/vnext-plan-review.js +21 -17
- 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 +34 -4
- 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,11 +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
6
|
import { closeBridgeProcess } from "./managed-daemon.mjs";
|
|
6
|
-
import { errorRecord } from "./operation-errors.mjs";
|
|
7
|
-
import {
|
|
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";
|
|
8
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";
|
|
9
14
|
|
|
10
15
|
const LATE_RESPONSE_GRACE_MS = 5_000;
|
|
11
16
|
const CODEX_SESSION_CONFIG = Object.freeze({
|
|
@@ -35,43 +40,51 @@ export function parseCodexCliVersion(value) {
|
|
|
35
40
|
return match[1];
|
|
36
41
|
}
|
|
37
42
|
|
|
38
|
-
|
|
39
|
-
return new Promise((resolve, reject) => {
|
|
40
|
-
const child = spawn(bin, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
41
|
-
let stdout = ""; let stderr = "";
|
|
42
|
-
child.stdout.setEncoding("utf8").on("data", (chunk) => { stdout += chunk; });
|
|
43
|
-
child.stderr.setEncoding("utf8").on("data", (chunk) => { stderr += chunk; });
|
|
44
|
-
child.on("error", reject);
|
|
45
|
-
child.on("exit", (code) => code === 0 ? resolve(stdout) : reject(Object.assign(new Error(`Codex CLI version failed: ${stderr.trim() || code}`), { code: "codex_version_unavailable" })));
|
|
46
|
-
});
|
|
47
|
-
}
|
|
43
|
+
|
|
48
44
|
|
|
49
45
|
class Journal {
|
|
50
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; }
|
|
51
|
-
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; }
|
|
52
48
|
flush() { return this.pending; }
|
|
53
49
|
}
|
|
54
50
|
|
|
55
51
|
export class CodexBridge {
|
|
56
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(); }
|
|
57
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
|
+
|
|
58
66
|
async start() {
|
|
67
|
+
await this.restoreItemPhases();
|
|
59
68
|
const bin = this.options.bin ?? process.env.DD_CODEX_BIN ?? "codex";
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
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)));
|
|
63
74
|
this.child.on("error", (error) => this.rejectAll(error));
|
|
64
|
-
this.child.on("
|
|
65
|
-
|
|
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));
|
|
66
77
|
if (this.options.onSpawn) await this.options.onSpawn(this.child);
|
|
67
78
|
await this.request("initialize", { clientInfo: { name: "dd-codex", version: "0.1.0" }, capabilities: { optOutNotificationMethods: [], experimentalApi: true } });
|
|
68
79
|
}
|
|
69
80
|
|
|
70
81
|
receive(line) {
|
|
71
82
|
let message; try { message = JSON.parse(line); } catch { this.journal.write("malformed", { line }); return; }
|
|
72
|
-
this.
|
|
73
|
-
this.
|
|
83
|
+
this.observeItemPhase(message);
|
|
84
|
+
try { this.observeTopology(message); }
|
|
85
|
+
catch (error) { this.topologyError = error; this.rejectAll(error); return; }
|
|
74
86
|
this.observeHookFailure(message);
|
|
87
|
+
this.observeNativeHook(message);
|
|
75
88
|
if (["model/rerouted", "thread/settings/updated"].includes(message.method)) {
|
|
76
89
|
const params = message.params ?? {}, id = params.threadId;
|
|
77
90
|
let root = id; const seen = new Set();
|
|
@@ -83,16 +96,57 @@ export class CodexBridge {
|
|
|
83
96
|
parentSessionId: this.descendants.get(id)?.parent_provider_session_id ?? null, requested: this.requestedProfiles.get(root),
|
|
84
97
|
observed: { model: rerouted ? params.toModel : settings.model, provider: settings.modelProvider, reasoning: settings.effort },
|
|
85
98
|
source: message.method, evidence: rerouted ? "response" : "configured", reason: params.reason ?? null, previousModel: params.fromModel ?? null
|
|
86
|
-
})).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);
|
|
87
110
|
}
|
|
88
111
|
}
|
|
89
|
-
|
|
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)) {
|
|
90
128
|
this.activity ??= new Map();
|
|
129
|
+
this.activityObservations ??= new Map();
|
|
91
130
|
const visited = new Set();
|
|
92
131
|
let id = message.params?.threadId;
|
|
93
132
|
while (id && !visited.has(id)) {
|
|
94
|
-
visited.add(id);
|
|
95
|
-
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;
|
|
96
150
|
}
|
|
97
151
|
}
|
|
98
152
|
// app-server currently returns complete turns even for `includeTurns:
|
|
@@ -117,6 +171,11 @@ export class CodexBridge {
|
|
|
117
171
|
return;
|
|
118
172
|
}
|
|
119
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
|
+
}
|
|
120
179
|
if (pending.timedOut) this.journal.write("late_response", { ...responseSummary(message), method: pending.method, grace_ms: LATE_RESPONSE_GRACE_MS });
|
|
121
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 ?? {});
|
|
122
181
|
return;
|
|
@@ -126,7 +185,10 @@ export class CodexBridge {
|
|
|
126
185
|
this.nativeErrors.set(key, { error: message.params.error, willRetry: message.params.willRetry });
|
|
127
186
|
while (this.nativeErrors.size > 128) this.nativeErrors.delete(this.nativeErrors.keys().next().value);
|
|
128
187
|
}
|
|
129
|
-
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
|
+
}
|
|
130
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 });
|
|
131
193
|
const threadId = message.params?.threadId;
|
|
132
194
|
if (threadId && message.method === "item/started") this.finalMessages.delete(threadId);
|
|
@@ -141,6 +203,14 @@ export class CodexBridge {
|
|
|
141
203
|
}
|
|
142
204
|
|
|
143
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
|
+
}
|
|
144
214
|
if (message.method === "turn/completed") {
|
|
145
215
|
const { threadId, turn } = message.params ?? {};
|
|
146
216
|
for (const [id, hook] of this.activeHooks ?? []) {
|
|
@@ -152,8 +222,12 @@ export class CodexBridge {
|
|
|
152
222
|
const { threadId, turnId, run } = message.params ?? {};
|
|
153
223
|
if (!this.isManagedHook(run) || typeof run.id !== "string" || !threadId) return;
|
|
154
224
|
this.activeHooks ??= new Map();
|
|
155
|
-
if (message.method === "hook/started")
|
|
156
|
-
|
|
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);
|
|
157
231
|
}
|
|
158
232
|
|
|
159
233
|
verifyNativeHook(input) {
|
|
@@ -167,7 +241,8 @@ export class CodexBridge {
|
|
|
167
241
|
seen.add(current); current = this.descendants.get(current)?.parent_provider_session_id;
|
|
168
242
|
}
|
|
169
243
|
if (!active || active.threadId !== sessionId || active.turnId !== turnId || !toolCallId
|
|
170
|
-
|| current !== rootSessionId || !this.requestedProfiles.has(rootSessionId)
|
|
244
|
+
|| current !== rootSessionId || !this.requestedProfiles.has(rootSessionId)
|
|
245
|
+
|| active.rootGeneration !== (this.hookPromptGenerations?.get(rootSessionId) ?? 0)) {
|
|
171
246
|
fail("No matching active managed native hook", "native_hook_unproven", { provider_session_id: sessionId, tool_call_id: toolCallId });
|
|
172
247
|
}
|
|
173
248
|
return { rootSessionId, sessionId, toolCallId, parentSessionId: sessionId === rootSessionId ? null : this.descendants.get(sessionId)?.parent_provider_session_id };
|
|
@@ -180,6 +255,11 @@ export class CodexBridge {
|
|
|
180
255
|
run.eventName !== "preToolUse" || run.handlerType !== "command" ||
|
|
181
256
|
run.statusMessage !== "dd-flow runtime observation" || !home ||
|
|
182
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;
|
|
183
263
|
const ancestors = new Set();
|
|
184
264
|
let root;
|
|
185
265
|
let current = threadId;
|
|
@@ -193,78 +273,120 @@ export class CodexBridge {
|
|
|
193
273
|
if (this.hookFailures.has(root)) return;
|
|
194
274
|
const entries = Array.isArray(run.entries) ? run.entries : [];
|
|
195
275
|
const reason = entries.filter(entry => entry.kind === "error").map(entry => entry.text).join("; ");
|
|
196
|
-
const error =
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
hook_run_id: run.id,
|
|
200
|
-
|
|
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
|
+
}
|
|
201
286
|
this.hookFailures.set(root, error);
|
|
202
|
-
this.journal.write("native_hook_failure", { code: error.code, message: error.message, ...error.details });
|
|
203
287
|
for (const [id, pending] of this.pending) {
|
|
204
288
|
if (ancestors.has(pending.threadId) && pending.method !== "turn/interrupt") {
|
|
205
289
|
clearTimeout(pending.timer); this.pending.delete(id); pending.reject(error);
|
|
206
290
|
}
|
|
207
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); }
|
|
208
294
|
}
|
|
209
295
|
|
|
210
296
|
assertHookHealthy(sessionId) {
|
|
297
|
+
if (this.topologyError) throw this.topologyError;
|
|
211
298
|
const visited = new Set();
|
|
212
299
|
while (sessionId && !visited.has(sessionId)) {
|
|
213
300
|
visited.add(sessionId);
|
|
301
|
+
if (this.turnObservers?.get(sessionId)?.progressError) throw this.turnObservers.get(sessionId).progressError;
|
|
214
302
|
const error = this.hookFailures?.get(sessionId);
|
|
215
303
|
if (error) throw error;
|
|
216
304
|
sessionId = this.descendants.get(sessionId)?.parent_provider_session_id;
|
|
217
305
|
}
|
|
218
306
|
}
|
|
219
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
|
+
|
|
220
334
|
// Collaboration child Threads are provider facts. Keep only objects that
|
|
221
335
|
// explicitly carry a parent relationship, including native spawn receipts.
|
|
222
336
|
// A wait/send receiver is not necessarily a child of its sender.
|
|
223
|
-
observeTopology(value, depth = 0) {
|
|
337
|
+
observeTopology(value, depth = 0, parentTurn = null) {
|
|
224
338
|
if (!value || typeof value !== "object" || depth > 8) return;
|
|
225
|
-
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; }
|
|
226
340
|
if (depth === 0 && ["turn/started", "turn/completed"].includes(value.method)) {
|
|
227
341
|
const { threadId, turn } = value.params ?? {};
|
|
228
|
-
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);
|
|
229
343
|
if (previous && typeof turn?.id === "string" && turn.id) {
|
|
230
344
|
if (value.method === "turn/started" && previous.active_turn_id !== turn.id && this.turns.get(turn.id)?.status !== "completed") {
|
|
231
345
|
this.descendants.set(threadId, { ...previous, status: "running", active_turn_id: turn.id, terminal_turn_id: null, provider_error: null });
|
|
232
346
|
} else if (value.method === "turn/completed" && (!previous.active_turn_id || previous.active_turn_id === turn.id)) {
|
|
233
347
|
this.descendants.set(threadId, { ...previous, status: turn.status === "completed" ? "completed" : turn.status ?? "unknown",
|
|
234
|
-
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) });
|
|
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) });
|
|
235
349
|
}
|
|
236
350
|
}
|
|
237
351
|
}
|
|
238
|
-
const activity = value.params?.item;
|
|
352
|
+
const activity = depth === 0 && ["item/started", "item/completed"].includes(value.method) ? value.params?.item : null;
|
|
239
353
|
const activityParent = value.params?.threadId;
|
|
240
354
|
if (activity?.type === "subAgentActivity" && typeof activity.agentThreadId === "string" && activity.agentThreadId &&
|
|
241
|
-
typeof activityParent === "string" && activityParent
|
|
355
|
+
typeof activityParent === "string" && activityParent) {
|
|
242
356
|
const previous = this.descendants.get(activity.agentThreadId);
|
|
243
357
|
const observed = activity.kind === "completed" ? "completed" : activity.kind === "interrupted" ? "interrupted" : "running";
|
|
244
358
|
// Parent activity has no child Turn identity on some app-server builds.
|
|
245
359
|
// It can discover a child, but cannot reopen or close a later known Turn.
|
|
246
360
|
const status = previous?.active_turn_id || previous?.status === "completed" ? previous.status : observed;
|
|
247
|
-
this.
|
|
248
|
-
|
|
249
|
-
|
|
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
|
+
}
|
|
250
365
|
}
|
|
251
366
|
if (value.type === "collabAgentToolCall" && value.tool === "spawnAgent" &&
|
|
252
367
|
typeof value.senderThreadId === "string" && value.senderThreadId && Array.isArray(value.receiverThreadIds)) {
|
|
253
368
|
for (const child of value.receiverThreadIds) {
|
|
254
|
-
if (typeof child !== "string" || !child
|
|
369
|
+
if (typeof child !== "string" || !child) continue;
|
|
255
370
|
const previous = this.descendants.get(child);
|
|
256
|
-
this.
|
|
371
|
+
this.bindChild(child, value.senderThreadId, {
|
|
372
|
+
parent_turn_id: parentTurn === this.currentTurns?.get(value.senderThreadId) ? parentTurn : null,
|
|
257
373
|
status: previous?.status === "completed" || previous?.active_turn_id ? previous.status : value.agentsStates?.[child]?.status ?? previous?.status ?? "unknown", source: "codex.app_server" });
|
|
258
374
|
}
|
|
259
375
|
}
|
|
260
|
-
const
|
|
261
|
-
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;
|
|
262
379
|
if (typeof id === "string" && id && typeof parent === "string" && parent) {
|
|
263
380
|
const previous = this.descendants.get(id);
|
|
264
|
-
this.
|
|
265
|
-
|
|
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);
|
|
266
389
|
}
|
|
267
|
-
for (const child of Object.values(value)) this.observeTopology(child, depth + 1);
|
|
268
390
|
}
|
|
269
391
|
|
|
270
392
|
directChildren(sessionId) {
|
|
@@ -285,6 +407,15 @@ export class CodexBridge {
|
|
|
285
407
|
}
|
|
286
408
|
|
|
287
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
|
+
}
|
|
288
419
|
const id = this.nextId++;
|
|
289
420
|
return new Promise((resolve, reject) => {
|
|
290
421
|
const timer = observedTimeout(() => {
|
|
@@ -295,9 +426,15 @@ export class CodexBridge {
|
|
|
295
426
|
pending.timer = observedTimeout(() => {
|
|
296
427
|
if (this.pending.get(id) !== pending) return;
|
|
297
428
|
this.pending.delete(id); reject(timeoutError);
|
|
298
|
-
}, LATE_RESPONSE_GRACE_MS);
|
|
299
|
-
}, timeoutMs, { onGap: gap => this.journal.write("observation_gap", { ...gap, method, request_id: id }) });
|
|
300
|
-
|
|
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 });
|
|
301
438
|
});
|
|
302
439
|
}
|
|
303
440
|
send(value) {
|
|
@@ -309,19 +446,19 @@ export class CodexBridge {
|
|
|
309
446
|
throw error;
|
|
310
447
|
}
|
|
311
448
|
try { stdin.write(`${JSON.stringify(value)}\n`); }
|
|
312
|
-
catch (
|
|
449
|
+
catch (cause) { const error = nativeObservationError(cause); this.rejectAll(error); throw error; }
|
|
313
450
|
}
|
|
314
|
-
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(); }
|
|
315
452
|
async close() {
|
|
316
|
-
return await closeBridgeProcess(this, async () => { await this.modelObservations; await this.journal.flush(); });
|
|
453
|
+
return await closeBridgeProcess(this, async () => { await Promise.all([this.nativeDrain, this.diagnostics]); await this.modelObservations; await this.journal.flush(); });
|
|
317
454
|
}
|
|
318
455
|
}
|
|
319
456
|
|
|
320
457
|
async function withBridge(options, action) {
|
|
321
|
-
const bridge = new CodexBridge(options); let primary;
|
|
322
|
-
try { await bridge.start();
|
|
458
|
+
const bridge = new CodexBridge(options); let primary, result;
|
|
459
|
+
try { await bridge.start(); result = await action(bridge); return result; }
|
|
323
460
|
catch (error) { primary = error; throw error; }
|
|
324
|
-
finally { try { await bridge.close(); } catch (error) { if (!primary) throw error; primary.cleanup_error = errorRecord(error); primary.details = { ...primary.details, cleanup_error: primary.cleanup_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 }; } }
|
|
325
462
|
}
|
|
326
463
|
function profile(options) { return { harness: "codex-desktop", model: options.model ?? null, reasoning: options.reasoning ?? null }; }
|
|
327
464
|
function threadId(result) { const id = result?.thread?.id ?? result?.threadId; return required(id, "Codex thread id"); }
|
|
@@ -335,7 +472,7 @@ async function observedChildren(bridge, sessionId) {
|
|
|
335
472
|
const turn = observed?.turn;
|
|
336
473
|
if (turn?.id !== child.terminal_turn_id || turn.status !== child.status) return child;
|
|
337
474
|
return { ...child, provider_error: nativeTurnError(bridge, child.provider_session_id, turn) ?? child.provider_error,
|
|
338
|
-
native_turn_items: turnItemEvidence(turn) };
|
|
475
|
+
native_turn_items: turnItemEvidence(turn, bridge, child.provider_session_id) };
|
|
339
476
|
}));
|
|
340
477
|
}
|
|
341
478
|
function codexSessionConfig() { return { ...CODEX_SESSION_CONFIG }; }
|
|
@@ -413,6 +550,7 @@ export async function startSessionWithBridge(bridge, options) {
|
|
|
413
550
|
bridge.finalMessages?.delete(sessionId);
|
|
414
551
|
options.assertDispatch?.();
|
|
415
552
|
bridge.assertHookHealthy?.(sessionId);
|
|
553
|
+
bridge.dispatchOperationId = options.operationId;
|
|
416
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);
|
|
417
555
|
const turn = started?.turn ?? started; const turnId = turn?.id ?? null;
|
|
418
556
|
if (typeof turnId !== "string" || !turnId) throw new Error("Codex did not return a Turn id");
|
|
@@ -421,6 +559,7 @@ export async function startSessionWithBridge(bridge, options) {
|
|
|
421
559
|
|
|
422
560
|
export async function promptSessionWithBridge(bridge, options) {
|
|
423
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);
|
|
424
563
|
await ensureThreadLoaded(bridge, sessionId, options, cwd);
|
|
425
564
|
// A final-message fallback belongs only to the Turn that produced it. A
|
|
426
565
|
// new Turn may start before app-server emits its first item notification;
|
|
@@ -429,26 +568,44 @@ export async function promptSessionWithBridge(bridge, options) {
|
|
|
429
568
|
bridge.finalMessages?.delete(sessionId);
|
|
430
569
|
options.assertDispatch?.();
|
|
431
570
|
bridge.assertHookHealthy?.(sessionId);
|
|
432
|
-
|
|
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);
|
|
433
574
|
const turn = started?.turn ?? started; const turnId = turn?.id ?? null;
|
|
434
575
|
let completed;
|
|
435
|
-
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); }
|
|
436
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
|
+
}
|
|
437
583
|
if (error?.code === "turn_interrupted") {
|
|
438
584
|
try {
|
|
439
585
|
const observed = await readTurn(bridge, sessionId, turnId, true);
|
|
440
586
|
if (observed?.turn?.id === turnId && observed.turn.status === error.details?.terminal_status) {
|
|
441
|
-
error.details.native_turn_items = turnItemEvidence(observed.turn);
|
|
587
|
+
error.details.native_turn_items = turnItemEvidence(observed.turn, bridge, sessionId);
|
|
442
588
|
error.details.provider_error ??= observed.turn.error ?? null;
|
|
443
589
|
}
|
|
444
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
|
+
}
|
|
445
596
|
}
|
|
446
597
|
throw error;
|
|
447
598
|
}
|
|
448
|
-
const
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
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
|
+
}
|
|
452
609
|
}
|
|
453
610
|
|
|
454
611
|
async function ensureThreadLoaded(bridge, sessionId, options, cwd) {
|
|
@@ -464,17 +621,17 @@ async function ensureThreadLoaded(bridge, sessionId, options, cwd) {
|
|
|
464
621
|
}, 30_000);
|
|
465
622
|
}
|
|
466
623
|
|
|
467
|
-
async function waitForTurn(bridge, sessionId, turnId, timeoutMs, options, cwd) {
|
|
468
|
-
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 }) })) {
|
|
469
625
|
// app-server can briefly expose the new Turn as `interrupted` between the
|
|
470
626
|
// turn/start response and its turn/started notification. Give that snapshot
|
|
471
627
|
// one event-loop grace window before treating failure as authoritative.
|
|
472
628
|
const failureNotBefore = Date.now() + 1_000;
|
|
473
629
|
let nextStoredReadAt = Date.now();
|
|
474
|
-
while (
|
|
630
|
+
while (true) {
|
|
475
631
|
bridge.assertHookHealthy?.(sessionId);
|
|
476
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);
|
|
477
|
-
|
|
633
|
+
const expired = clock.sample(bridge.activityObservations?.get(sessionId) ?? bridge.activity?.get(sessionId));
|
|
634
|
+
if (expired || Date.now() >= nextStoredReadAt) {
|
|
478
635
|
const stored = await readTurn(bridge, sessionId, turnId, false);
|
|
479
636
|
if (stored?.turn && isAuthoritativeTerminal(bridge, turnId, stored.turn.status, stored.threadStatus, failureNotBefore)) return terminalTurn({ turn: stored.turn }, sessionId, turnId, bridge);
|
|
480
637
|
// A loaded app-server can retain a stale `active` Thread state after a
|
|
@@ -516,9 +673,10 @@ async function waitForTurn(bridge, sessionId, turnId, timeoutMs, options, cwd) {
|
|
|
516
673
|
}
|
|
517
674
|
nextStoredReadAt = Date.now() < failureNotBefore ? failureNotBefore : Date.now() + 15_000;
|
|
518
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" });
|
|
519
678
|
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
520
679
|
}
|
|
521
|
-
fail("Codex turn timed out", "turn_timeout");
|
|
522
680
|
}
|
|
523
681
|
|
|
524
682
|
function terminalTurn(value, sessionId, turnId, bridge) {
|
|
@@ -529,7 +687,7 @@ function terminalTurn(value, sessionId, turnId, bridge) {
|
|
|
529
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 });
|
|
530
688
|
const providerError = nativeTurnError(bridge, sessionId, turn);
|
|
531
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(),
|
|
532
|
-
provider_error: providerError, native_turn_items: turnItemEvidence(turn) });
|
|
690
|
+
provider_error: providerError, native_turn_items: turnItemEvidence(turn, bridge, sessionId) });
|
|
533
691
|
return value;
|
|
534
692
|
}
|
|
535
693
|
|
|
@@ -540,12 +698,25 @@ function nativeTurnError(bridge, sessionId, turn) {
|
|
|
540
698
|
return error && native?.willRetry !== undefined && typeof error === "object" ? { ...error, willRetry: native.willRetry } : error;
|
|
541
699
|
}
|
|
542
700
|
|
|
543
|
-
function turnItemEvidence(turn) {
|
|
701
|
+
export function turnItemEvidence(turn, bridge, sessionId) {
|
|
544
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
|
+
};
|
|
545
712
|
return Array.isArray(items) && turn?.itemsView !== "notLoaded" ? { observed: true, count: items.length, types: items.map(item => item?.type ?? "unknown"),
|
|
546
|
-
items: items.map(item =>
|
|
547
|
-
|
|
548
|
-
|
|
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 };
|
|
549
720
|
}
|
|
550
721
|
|
|
551
722
|
export { terminalCodexOverload } from "./codex-capacity-policy.mjs";
|
|
@@ -575,11 +746,12 @@ async function readPersistedTurn(bridge, sessionId, turnId) {
|
|
|
575
746
|
}
|
|
576
747
|
|
|
577
748
|
async function readCompletedThread(bridge, sessionId) {
|
|
749
|
+
let lastError;
|
|
578
750
|
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
579
751
|
try { const result = await bridge.request("thread/read", { threadId: sessionId, includeTurns: true }, 10_000); return result?.thread ?? result; }
|
|
580
|
-
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)); }
|
|
581
753
|
}
|
|
582
|
-
|
|
754
|
+
throw lastError;
|
|
583
755
|
}
|
|
584
756
|
|
|
585
757
|
function latestAssistantText(value) {
|
|
@@ -604,7 +776,7 @@ export async function inspectSession(options) {
|
|
|
604
776
|
export async function inspectSessionWithBridge(bridge, options) {
|
|
605
777
|
const sessionId = required(options.sessionId, "--session-id"); const value = await bridge.request("thread/read", { threadId: sessionId, includeTurns: false }); const thread = value?.thread ?? value;
|
|
606
778
|
const terminal = options.turnId ? (await readTurn(bridge, sessionId, options.turnId, true))?.turn : null;
|
|
607
|
-
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) } : {};
|
|
608
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") };
|
|
609
781
|
}
|
|
610
782
|
|