@openclaw/codex 2026.9.1-beta.1 → 2026.9.1
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/dist/api.js +5 -6
- package/dist/{attempt-client-cleanup-bfsmcQ6m.js → attempt-client-cleanup-DYHcRQAa.js} +31 -45
- package/dist/attempt-context-DmQUNWwT.js +634 -0
- package/dist/{auth-binding-B4FfbcCg.js → auth-binding-Ds_QVm-X.js} +18 -3
- package/dist/{binding-connection-BMvPX73o.js → binding-connection-E6uDsPlx.js} +2 -2
- package/dist/{bounded-turn-B7iBUcuP.js → bounded-turn-wzJrwKKe.js} +15 -20
- package/dist/{capabilities-BRMh0aIq.js → capabilities-dIjNVCYH.js} +1 -1
- package/dist/cli-metadata.js +1 -1
- package/dist/{command-formatters-DEsIufkt.js → command-formatters-CLHhEB1Q.js} +5 -4
- package/dist/{command-handlers-D_-1_qRp.js → command-handlers-CTGb2csu.js} +124 -101
- package/dist/{compact-BglNFZrQ.js → compact-_t0HQrUS.js} +38 -29
- package/dist/{config-giW8X0QA.js → config-BhmfC1s6.js} +294 -282
- package/dist/{context-compaction-activity-BOeJf7yo.js → context-compaction-activity-CMla5FPJ.js} +4 -1
- package/dist/{conversation-binding-data-CW2GuupV.js → conversation-binding-data-BiodpIzq.js} +2 -1
- package/dist/doctor-contract-api.js +143 -12
- package/dist/{dynamic-tools-Dnlfa--w.js → dynamic-tools-BU9tmlYm.js} +599 -655
- package/dist/{effective-mcp-catalog-CTeGrnfO.js → effective-mcp-catalog-DzYBW4h1.js} +2 -2
- package/dist/{event-projector-assistant-message-C5-Ds8mA.js → event-projector-assistant-message-DHRFXKbS.js} +4 -4
- package/dist/{event-projector-usage-Bk1I90Bg.js → event-projector-usage-C5mcrGjp.js} +15 -148
- package/dist/{harness-Csz0F-DK.js → harness-DXVZ5II5.js} +42 -52
- package/dist/harness.js +1 -1
- package/dist/index.js +458 -404
- package/dist/{isolated-completion-Cv6FqygM.js → isolated-completion-kg9kv4iH.js} +4 -4
- package/dist/launch-args-RXQwn8zV.js +90 -0
- package/dist/{managed-binary-DFYK4DWq.js → managed-binary-4-Vjz5GH.js} +14 -9
- package/dist/media-understanding-provider.js +1 -1
- package/dist/model-catalog-CUM68Hkl.js +101 -0
- package/dist/{models-TS3HBU-q.js → models-sYnEc0gd.js} +25 -28
- package/dist/{native-subagent-monitor-4Mfq2HDs.js → native-subagent-monitor-CnH147jH.js} +55 -115
- package/dist/{node-exec-server.runtime-BpWqJO1V.js → node-exec-server.runtime-CstNHCmC.js} +9 -6
- package/dist/{plugin-approval-roundtrip-bUm777tR.js → plugin-approval-roundtrip-CNe-Xrjx.js} +30 -3
- package/dist/{plugin-marketplace-discovery-B9DvaAG-.js → plugin-marketplace-discovery-CfFlKqDl.js} +15 -14
- package/dist/{plugin-metadata-cache-CTCT3w5L.js → plugin-metadata-cache-BFrPhb2c.js} +6 -6
- package/dist/provider-capabilities-DW5p4k9w.js +101 -0
- package/dist/{rate-limits-C5_D8Gmb.js → rate-limits-CawQJfUS.js} +3 -2
- package/dist/{sandbox-guard-kuWTn9C5.js → request-DYAI5FQE.js} +195 -2
- package/dist/{run-attempt-DYc0QCBF.js → run-attempt-D1hJO4fX.js} +828 -2100
- package/dist/{runtime-artifact-BvyT1uyd.js → runtime-artifact-C3jVimFn.js} +21 -14
- package/dist/{session-binding-Bcynfd-_.js → session-binding-BHuTCRZQ.js} +198 -127
- package/dist/{session-catalog-DaLKVtpH.js → session-catalog-yyvTWFOw.js} +564 -408
- package/dist/{session-cli-DMc4oS2w.js → session-cli-CmFEXMAM.js} +1 -1
- package/dist/session-history-C5J1AKzG.js +87 -0
- package/dist/session-initialization-BeWzjGiR.js +74 -0
- package/dist/{session-permission-policy-BrNH1bjy.js → session-permission-policy-DeWVTCMj.js} +2 -2
- package/dist/session-retirement-BujDjXVF.js +97 -0
- package/dist/{session-upstream-marker-BIkusb95.js → session-upstream-marker-C351-5Qw.js} +19 -4
- package/dist/settled-turn-context-D3SNWtUn.js +268 -0
- package/dist/settled-turn-finalizer-BVJx4yoc.js +104 -0
- package/dist/{shared-client-DKz23hHY.js → shared-client-DxcfF9KX.js} +3480 -679
- package/dist/{side-question-DjA_jsNe.js → side-question-BTZAII4l.js} +108 -70
- package/dist/{thread-lifecycle-L7oEkTlq.js → thread-lifecycle-DAcEJ_Om.js} +2188 -1915
- package/dist/{thread-ownership-DZeuKH1j.js → thread-ownership-zGokd8i1.js} +25 -14
- package/dist/thread-policy-g4KbJPzp.js +54 -0
- package/dist/{plugin-app-cache-key-C1CAdfcP.js → thread-resume-C7pyVKeW.js} +59 -7
- package/dist/{transcript-mirror-DsCMWBca.js → transcript-mirror-DTUxFWa_.js} +79 -17
- package/dist/transport-stdio-CyPldczm.js +726 -0
- package/dist/{upstream-prompt-provenance-CFmsKwr9.js → upstream-prompt-provenance-LphB8slG.js} +8 -4
- package/dist/upstream-session-fork-CYlBIGVt.js +966 -0
- package/dist/{usage-ROIaW3wo.js → usage-Bj-rrzkR.js} +3 -3
- package/dist/{web-search-provider.runtime-ClaNPV7N.js → web-search-provider.runtime-D2FWSK4w.js} +3 -3
- package/openclaw.plugin.json +1 -30
- package/package.json +6 -6
- package/dist/command-rpc-CCeCkf_v.js +0 -90
- package/dist/incognito-session-ET_ufDH-.js +0 -2
- package/dist/model-catalog-i9rNBthX.js +0 -48
- package/dist/protocol-validators-VeJxxJ0U.js +0 -2421
- package/dist/request-DXXGHko0.js +0 -157
- package/dist/session-catalog-home-id-BNwAJ9D7.js +0 -18
- package/dist/session-retirement-CcNC3YSY.js +0 -36
- package/dist/settled-turn-finalizer-Dc3ApjU4.js +0 -328
- package/dist/thread-resume-BE9qNEW0.js +0 -30
- package/dist/transport-stdio-DhoGy33Z.js +0 -99
- package/dist/upstream-session-fork-DlaHxSLC.js +0 -326
|
@@ -0,0 +1,966 @@
|
|
|
1
|
+
import { $t as isJsonObject, Jt as readCodexSessionMeta, Q as assertCodexThreadForkResponse, Qt as flattenCodexDynamicToolFunctions, X as assertCodexThreadAcceptsDirectInput, f as readCodexAppServerClientDesktopGenerationFingerprint, wt as hasCodexAppServerLiveThread, xt as claimCodexAppServerLiveThread } from "./shared-client-DxcfF9KX.js";
|
|
2
|
+
import { E as readCodexPluginConfig, O as resolveOpenClawExecPolicyForCodexAppServer, p as assertCodexModelBackedReviewerEffectiveConfig, s as resolveCodexSupervisionAppServerRuntimeOptions } from "./config-BhmfC1s6.js";
|
|
3
|
+
import { d as sessionBindingIdentity } from "./session-binding-BHuTCRZQ.js";
|
|
4
|
+
import { i as buildCodexPluginAppCacheKey } from "./thread-resume-C7pyVKeW.js";
|
|
5
|
+
import { $ as resolveCodexNativeSkillIsolation, C as buildDeveloperInstructions, Ct as buildCodexPluginThreadConfig, Dt as mergeCodexThreadConfigs, F as assertCodexNativeHookRelayAllowed, I as buildCodexNativeHookRelayConfig, Ot as shouldBuildCodexPluginThreadConfig, Q as applyCodexNativeSkillIsolation, R as buildCodexNativeHookRelayId, S as resolveCodexWebSearchPlan, V as resolveCodexNativeHookRelayEvents, W as attestCodexPluginThreadApps, Z as parseCodexNativeToolCatalog, it as fingerprintUserMcpServersConfigPatch, rt as fingerprintJsonObject, tt as codexDynamicToolsFingerprint, y as buildCodexThreadConfiguration } from "./thread-lifecycle-DAcEJ_Om.js";
|
|
6
|
+
import { o as withCodexAppServerThreadMutation } from "./thread-ownership-zGokd8i1.js";
|
|
7
|
+
import { c as fingerprintCodexMirrorSourceMessage, m as projectCodexUserItemText, u as readCodexMirrorSourceFingerprint } from "./transcript-mirror-DTUxFWa_.js";
|
|
8
|
+
import { l as resolveCodexNativeExecutionPolicy, t as CodexAppServerScopedRequestRejectedError } from "./request-DYAI5FQE.js";
|
|
9
|
+
import { i as refreshCodexThreadPolicy, n as CodexThreadPolicyHandoffError } from "./thread-policy-g4KbJPzp.js";
|
|
10
|
+
import { i as readUpstreamUserText, r as readMirrorIdentity } from "./upstream-prompt-provenance-LphB8slG.js";
|
|
11
|
+
import { n as prepareCodexSessionInitialization } from "./session-initialization-BeWzjGiR.js";
|
|
12
|
+
import { i as createImportedCodexSession, n as codexUpstreamBaseline, t as codexLastTerminalTurnId } from "./session-upstream-marker-C351-5Qw.js";
|
|
13
|
+
import { l as isCodexDynamicToolExcluded, n as resolveCodexProviderWebSearchSupportForClient } from "./provider-capabilities-DW5p4k9w.js";
|
|
14
|
+
import { o as prepareCodexWorkspaceDeveloperInstructions } from "./attempt-context-DmQUNWwT.js";
|
|
15
|
+
import { isRecord, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
16
|
+
import { lstat } from "node:fs/promises";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
import { loadCodexBundleMcpThreadConfig } from "openclaw/plugin-sdk/agent-harness-runtime";
|
|
19
|
+
import { constants, createZstdDecompress } from "node:zlib";
|
|
20
|
+
import { root } from "openclaw/plugin-sdk/file-access-runtime";
|
|
21
|
+
import { randomUUID } from "node:crypto";
|
|
22
|
+
import { isDeepStrictEqual } from "node:util";
|
|
23
|
+
import { resolveAgentWorkspaceDir } from "openclaw/plugin-sdk/agent-runtime";
|
|
24
|
+
import { loadExecApprovals } from "openclaw/plugin-sdk/exec-approvals-runtime";
|
|
25
|
+
import { resolveStorePath } from "openclaw/plugin-sdk/session-store-runtime";
|
|
26
|
+
import { buildCodexUserMcpServersThreadConfigPatchForRuntime, resolveCodexMcpToolOverridesForAgent } from "openclaw/plugin-sdk/codex-mcp-projection";
|
|
27
|
+
import { buildNativeHookRelayCommandPlan } from "openclaw/plugin-sdk/native-hook-relay-runtime";
|
|
28
|
+
import { appendSessionTranscriptMessagesByIdentity, readVisibleSessionTranscriptMessageEntries } from "openclaw/plugin-sdk/session-transcript-runtime";
|
|
29
|
+
//#region extensions/codex/src/session-rollout-selection.ts
|
|
30
|
+
const CHUNK_BYTES = 65536;
|
|
31
|
+
const RECORD_BYTES = 1048576;
|
|
32
|
+
const SCAN_BYTES = 8388608;
|
|
33
|
+
const DECOMPRESSED_BYTES = 33554432;
|
|
34
|
+
const DEADLINE_MS = 5e3;
|
|
35
|
+
const utf8Decoder = new TextDecoder("utf-8", { fatal: true });
|
|
36
|
+
function scalar(value) {
|
|
37
|
+
if (typeof value !== "string" || !value.trim() || Buffer.byteLength(value) > 256) throw new Error("Codex rollout has an invalid model or provider selection");
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
function record(line) {
|
|
41
|
+
if (!line.length || line.length > RECORD_BYTES) throw new Error("Codex rollout record exceeds the observation limit");
|
|
42
|
+
const parsed = JSON.parse(utf8Decoder.decode(line));
|
|
43
|
+
if (!isRecord(parsed) || typeof parsed.type !== "string" || !isRecord(parsed.payload)) throw new Error("Codex rollout contains an incomplete record");
|
|
44
|
+
return {
|
|
45
|
+
type: parsed.type,
|
|
46
|
+
payload: parsed.payload
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/** Observe settings only; rollback changes history, never the native scalar selection. */
|
|
50
|
+
function observation(parsed, threadId) {
|
|
51
|
+
const payload = parsed.payload;
|
|
52
|
+
if (parsed.type === "session_meta") {
|
|
53
|
+
if (typeof payload.id !== "string") throw new Error("Codex rollout metadata is missing its thread identity");
|
|
54
|
+
return payload.id === threadId ? { provider: scalar(payload.model_provider) } : {};
|
|
55
|
+
}
|
|
56
|
+
if (parsed.type === "turn_context") return { model: scalar(payload.model) };
|
|
57
|
+
if (parsed.type === "event_msg" && payload.type === "thread_settings_applied") {
|
|
58
|
+
if (!isRecord(payload.thread_settings)) throw new Error("Codex rollout settings are incomplete");
|
|
59
|
+
return {
|
|
60
|
+
model: scalar(payload.thread_settings.model),
|
|
61
|
+
provider: scalar(payload.thread_settings.model_provider_id)
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
return {};
|
|
65
|
+
}
|
|
66
|
+
async function readExtent(handle, start, length) {
|
|
67
|
+
const buffer = Buffer.alloc(length);
|
|
68
|
+
for (let offset = 0; offset < length;) {
|
|
69
|
+
const { bytesRead } = await handle.read(buffer, offset, length - offset, start + offset);
|
|
70
|
+
if (!bytesRead) throw new Error("Codex rollout changed during model observation");
|
|
71
|
+
offset += bytesRead;
|
|
72
|
+
}
|
|
73
|
+
return buffer;
|
|
74
|
+
}
|
|
75
|
+
/** Latest durable selection at one verified local rollout snapshot, not live-memory state. */
|
|
76
|
+
async function readCodexRolloutSelection(params) {
|
|
77
|
+
const deadline = Date.now() + DEADLINE_MS;
|
|
78
|
+
const check = () => {
|
|
79
|
+
params.assertCurrent();
|
|
80
|
+
if (Date.now() >= deadline) throw new Error("Codex rollout model observation timed out");
|
|
81
|
+
};
|
|
82
|
+
check();
|
|
83
|
+
const rootStat = await lstat(params.sessionsRoot);
|
|
84
|
+
if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) throw new Error("Codex rollout root is not a verified local directory");
|
|
85
|
+
const safeRoot = await root(params.sessionsRoot, {
|
|
86
|
+
hardlinks: "reject",
|
|
87
|
+
symlinks: "reject",
|
|
88
|
+
maxBytes: Number.MAX_SAFE_INTEGER
|
|
89
|
+
});
|
|
90
|
+
const plain = params.rolloutPath.endsWith(".zst") ? params.rolloutPath.slice(0, -4) : params.rolloutPath;
|
|
91
|
+
let selected = plain;
|
|
92
|
+
let opened;
|
|
93
|
+
try {
|
|
94
|
+
opened = await safeRoot.open(path.relative(params.sessionsRoot, selected));
|
|
95
|
+
} catch (error) {
|
|
96
|
+
if (!isRecord(error) || error.code !== "not-found" && error.code !== "ENOENT") throw error;
|
|
97
|
+
selected = `${plain}.zst`;
|
|
98
|
+
opened = await safeRoot.open(path.relative(params.sessionsRoot, selected));
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
check();
|
|
102
|
+
const snapshot = opened.stat;
|
|
103
|
+
let metadata;
|
|
104
|
+
let model;
|
|
105
|
+
let provider;
|
|
106
|
+
const header = (parsed) => {
|
|
107
|
+
if (parsed.type !== "session_meta" || parsed.payload.id !== params.threadId) throw new Error("Codex rollout does not belong to the bound thread");
|
|
108
|
+
scalar(parsed.payload.model_provider);
|
|
109
|
+
metadata = parsed.payload;
|
|
110
|
+
};
|
|
111
|
+
if (selected.endsWith(".zst")) {
|
|
112
|
+
if (snapshot.size === 0 || snapshot.size > SCAN_BYTES) throw new Error("Compressed Codex rollout exceeds the observation limit");
|
|
113
|
+
const input = opened.handle.createReadStream({
|
|
114
|
+
autoClose: false,
|
|
115
|
+
highWaterMark: CHUNK_BYTES,
|
|
116
|
+
end: snapshot.size - 1
|
|
117
|
+
});
|
|
118
|
+
const decoder = createZstdDecompress({
|
|
119
|
+
chunkSize: CHUNK_BYTES,
|
|
120
|
+
params: { [constants.ZSTD_d_windowLogMax]: 25 }
|
|
121
|
+
});
|
|
122
|
+
const timer = setTimeout(() => decoder.destroy(/* @__PURE__ */ new Error("Codex rollout observation timed out")), Math.max(1, deadline - Date.now()));
|
|
123
|
+
input.on("error", (error) => decoder.destroy(error));
|
|
124
|
+
const forward = input.pipe(decoder);
|
|
125
|
+
let tail = Buffer.alloc(0);
|
|
126
|
+
let total = 0;
|
|
127
|
+
try {
|
|
128
|
+
for await (const chunk of forward) {
|
|
129
|
+
check();
|
|
130
|
+
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
131
|
+
total += bytes.length;
|
|
132
|
+
if (total > DECOMPRESSED_BYTES) throw new Error("Decompressed Codex rollout exceeds the observation limit");
|
|
133
|
+
tail = Buffer.concat([tail, bytes]);
|
|
134
|
+
let newline;
|
|
135
|
+
while ((newline = tail.indexOf(10)) >= 0) {
|
|
136
|
+
const parsed = record(tail.subarray(0, newline));
|
|
137
|
+
if (!metadata) header(parsed);
|
|
138
|
+
const value = observation(parsed, params.threadId);
|
|
139
|
+
model = value.model ?? model;
|
|
140
|
+
provider = value.provider ?? provider;
|
|
141
|
+
tail = tail.subarray(newline + 1);
|
|
142
|
+
}
|
|
143
|
+
if (tail.length > RECORD_BYTES) throw new Error("Codex rollout record exceeds the observation limit");
|
|
144
|
+
}
|
|
145
|
+
if (tail.length) throw new Error("Codex rollout has an incomplete trailing record");
|
|
146
|
+
} finally {
|
|
147
|
+
clearTimeout(timer);
|
|
148
|
+
if (!input.readableEnded) input.destroy();
|
|
149
|
+
decoder.destroy();
|
|
150
|
+
}
|
|
151
|
+
} else {
|
|
152
|
+
let first = Buffer.alloc(0);
|
|
153
|
+
for (let offset = 0; offset < snapshot.size && first.length <= RECORD_BYTES;) {
|
|
154
|
+
check();
|
|
155
|
+
const bytes = await readExtent(opened.handle, offset, Math.min(CHUNK_BYTES, snapshot.size - offset));
|
|
156
|
+
offset += bytes.length;
|
|
157
|
+
first = Buffer.concat([first, bytes]);
|
|
158
|
+
const newline = first.indexOf(10);
|
|
159
|
+
if (newline >= 0) {
|
|
160
|
+
header(record(first.subarray(0, newline)));
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (!metadata) throw new Error("Codex rollout metadata is incomplete or oversized");
|
|
165
|
+
let end = snapshot.size;
|
|
166
|
+
let scanned = 0;
|
|
167
|
+
let tail = Buffer.alloc(0);
|
|
168
|
+
while (end > 0 && (!model || !provider)) {
|
|
169
|
+
check();
|
|
170
|
+
const length = Math.min(CHUNK_BYTES, end, SCAN_BYTES - scanned);
|
|
171
|
+
if (!length) throw new Error("Codex rollout model observation exhausted its scan budget");
|
|
172
|
+
end -= length;
|
|
173
|
+
const bytes = await readExtent(opened.handle, end, length);
|
|
174
|
+
if (!scanned) {
|
|
175
|
+
if (bytes.at(-1) !== 10) throw new Error("Codex rollout has an incomplete trailing record");
|
|
176
|
+
tail = bytes.subarray(0, -1);
|
|
177
|
+
} else tail = Buffer.concat([bytes, tail]);
|
|
178
|
+
scanned += length;
|
|
179
|
+
for (;;) {
|
|
180
|
+
const newline = tail.lastIndexOf(10);
|
|
181
|
+
if (newline < 0 && (end !== 0 || tail.length === 0)) break;
|
|
182
|
+
const value = observation(record(tail.subarray(newline + 1)), params.threadId);
|
|
183
|
+
model ??= value.model;
|
|
184
|
+
provider ??= value.provider;
|
|
185
|
+
tail = newline < 0 ? Buffer.alloc(0) : tail.subarray(0, newline);
|
|
186
|
+
if (model && provider) break;
|
|
187
|
+
}
|
|
188
|
+
if (tail.length > RECORD_BYTES) throw new Error("Codex rollout record exceeds the observation limit");
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const assertUnchanged = async () => {
|
|
192
|
+
params.assertCurrent();
|
|
193
|
+
if (selected !== plain) {
|
|
194
|
+
try {
|
|
195
|
+
await lstat(plain);
|
|
196
|
+
} catch (error) {
|
|
197
|
+
if (!isRecord(error) || error.code !== "ENOENT") throw error;
|
|
198
|
+
return await verifySelected();
|
|
199
|
+
}
|
|
200
|
+
throw new Error("Codex rollout selection changed during model observation");
|
|
201
|
+
}
|
|
202
|
+
await verifySelected();
|
|
203
|
+
};
|
|
204
|
+
const verifySelected = async () => {
|
|
205
|
+
params.assertCurrent();
|
|
206
|
+
const currentRoot = await lstat(params.sessionsRoot);
|
|
207
|
+
const fresh = await safeRoot.open(path.relative(params.sessionsRoot, selected));
|
|
208
|
+
try {
|
|
209
|
+
const stat = fresh.stat;
|
|
210
|
+
if (stat.dev !== snapshot.dev || stat.ino !== snapshot.ino || stat.size !== snapshot.size || stat.mtimeMs !== snapshot.mtimeMs || stat.ctimeMs !== snapshot.ctimeMs || stat.nlink !== 1 || currentRoot.isSymbolicLink() || currentRoot.ino !== rootStat.ino || currentRoot.dev !== rootStat.dev) throw new Error("Codex rollout changed during model observation");
|
|
211
|
+
} finally {
|
|
212
|
+
await fresh.handle.close();
|
|
213
|
+
}
|
|
214
|
+
params.assertCurrent();
|
|
215
|
+
};
|
|
216
|
+
const descriptor = await opened.handle.stat();
|
|
217
|
+
if (descriptor.dev !== snapshot.dev || descriptor.ino !== snapshot.ino || descriptor.size !== snapshot.size || descriptor.mtimeMs !== snapshot.mtimeMs || descriptor.ctimeMs !== snapshot.ctimeMs || descriptor.nlink !== 1) throw new Error("Codex rollout changed during model observation");
|
|
218
|
+
await assertUnchanged();
|
|
219
|
+
check();
|
|
220
|
+
if (!model || !provider || !metadata) throw new Error("Codex rollout has no complete durable model selection");
|
|
221
|
+
return {
|
|
222
|
+
model,
|
|
223
|
+
modelProvider: provider,
|
|
224
|
+
metadata,
|
|
225
|
+
assertUnchanged
|
|
226
|
+
};
|
|
227
|
+
} finally {
|
|
228
|
+
await opened.handle.close();
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
//#endregion
|
|
232
|
+
//#region extensions/codex/src/app-server/canonical-fork-preparation.ts
|
|
233
|
+
/** Creation preserves native declarations; admitted turns own all live executors. */
|
|
234
|
+
async function prepareCanonicalCodexFork(params) {
|
|
235
|
+
const { created, initialization, config, context } = params;
|
|
236
|
+
const assertCurrent = initialization.assertCurrent;
|
|
237
|
+
assertCurrent();
|
|
238
|
+
if (!initialization.prepareNativeToolPolicy) throw new Error("Canonical Codex forks require host native tool policy preparation. Update the Gateway before retrying.");
|
|
239
|
+
const workspaceDir = resolveAgentWorkspaceDir(config, created.agentId);
|
|
240
|
+
const cwd = created.entry.spawnedCwd ?? workspaceDir;
|
|
241
|
+
const execution = resolveCodexNativeExecutionPolicy({
|
|
242
|
+
config,
|
|
243
|
+
agentId: created.agentId,
|
|
244
|
+
sessionKey: created.key,
|
|
245
|
+
sessionEntry: created.entry
|
|
246
|
+
});
|
|
247
|
+
if (params.sandbox === "required" || !execution.nativeToolSurfaceAllowed || execution.effectiveExecHost !== "gateway" || context.appServer.remoteWorkspaceRoot) throw new Error("This child requires an execution environment that cannot be prepared during a native fork. Fork an original imported message instead.");
|
|
248
|
+
const pluginConfig = readCodexPluginConfig(context.pluginConfig);
|
|
249
|
+
const appServer = {
|
|
250
|
+
...resolveCodexSupervisionAppServerRuntimeOptions({
|
|
251
|
+
pluginConfig,
|
|
252
|
+
config,
|
|
253
|
+
agentDir: context.agentDir,
|
|
254
|
+
model: params.model,
|
|
255
|
+
modelProvider: params.modelProvider,
|
|
256
|
+
execPolicy: resolveOpenClawExecPolicyForCodexAppServer({
|
|
257
|
+
config,
|
|
258
|
+
agentId: created.agentId,
|
|
259
|
+
approvals: loadExecApprovals()
|
|
260
|
+
})
|
|
261
|
+
}),
|
|
262
|
+
start: context.appServer.start
|
|
263
|
+
};
|
|
264
|
+
const { webSearchAllowed: hostWebSearchAllowed } = await initialization.prepareNativeToolPolicy({
|
|
265
|
+
provider: params.modelProvider,
|
|
266
|
+
runtimeProvider: "codex",
|
|
267
|
+
id: params.model
|
|
268
|
+
});
|
|
269
|
+
assertCurrent();
|
|
270
|
+
const webSearchAllowed = hostWebSearchAllowed && !isCodexDynamicToolExcluded(pluginConfig, ["web_search"]);
|
|
271
|
+
const dynamicTools = params.dynamicTools;
|
|
272
|
+
const nativeProviderWebSearchSupport = await resolveCodexProviderWebSearchSupportForClient({
|
|
273
|
+
client: context.client,
|
|
274
|
+
timeoutMs: appServer.requestTimeoutMs,
|
|
275
|
+
modelProviderOverride: params.modelProvider,
|
|
276
|
+
signal: AbortSignal.timeout(appServer.requestTimeoutMs)
|
|
277
|
+
});
|
|
278
|
+
assertCurrent();
|
|
279
|
+
const webSearch = resolveCodexWebSearchPlan({
|
|
280
|
+
config,
|
|
281
|
+
nativeToolSurfaceEnabled: true,
|
|
282
|
+
nativeProviderWebSearchSupport,
|
|
283
|
+
webSearchAllowed
|
|
284
|
+
});
|
|
285
|
+
const toolOverrides = resolveCodexMcpToolOverridesForAgent(config, {
|
|
286
|
+
agentId: created.agentId,
|
|
287
|
+
toolOverrides: created.entry.toolOverrides
|
|
288
|
+
});
|
|
289
|
+
const bundleMcp = await loadCodexBundleMcpThreadConfig({
|
|
290
|
+
workspaceDir,
|
|
291
|
+
agentId: created.agentId,
|
|
292
|
+
cfg: config,
|
|
293
|
+
toolOverrides,
|
|
294
|
+
preparationOnly: true
|
|
295
|
+
});
|
|
296
|
+
assertCurrent();
|
|
297
|
+
if (bundleMcp.diagnostics.length) throw new Error("The child's MCP configuration could not be prepared completely.");
|
|
298
|
+
const userMcp = await buildCodexUserMcpServersThreadConfigPatchForRuntime(config, {
|
|
299
|
+
agentId: created.agentId,
|
|
300
|
+
agentDir: context.agentDir,
|
|
301
|
+
toolOverrides,
|
|
302
|
+
preparationOnly: true
|
|
303
|
+
});
|
|
304
|
+
assertCurrent();
|
|
305
|
+
const apps = shouldBuildCodexPluginThreadConfig(pluginConfig) ? await buildCodexPluginThreadConfig({
|
|
306
|
+
pluginConfig,
|
|
307
|
+
configCwd: cwd,
|
|
308
|
+
appCacheKey: buildCodexPluginAppCacheKey({
|
|
309
|
+
appServer,
|
|
310
|
+
agentDir: context.agentDir,
|
|
311
|
+
runtimeIdentity: context.client.getRuntimeIdentity(),
|
|
312
|
+
desktopGenerationFingerprint: readCodexAppServerClientDesktopGenerationFingerprint(context.client)
|
|
313
|
+
}),
|
|
314
|
+
request: async (method, requestParams) => {
|
|
315
|
+
assertCurrent();
|
|
316
|
+
if (![
|
|
317
|
+
"plugin/list",
|
|
318
|
+
"plugin/read",
|
|
319
|
+
"plugin/installed",
|
|
320
|
+
"app/list",
|
|
321
|
+
"app/read",
|
|
322
|
+
"app/installed",
|
|
323
|
+
"config/read",
|
|
324
|
+
"configRequirements/read"
|
|
325
|
+
].includes(method)) throw new Error("Codex plugin setup is required before native fork preparation.");
|
|
326
|
+
const response = await context.client.request(method, requestParams);
|
|
327
|
+
assertCurrent();
|
|
328
|
+
return response;
|
|
329
|
+
}
|
|
330
|
+
}) : void 0;
|
|
331
|
+
assertCurrent();
|
|
332
|
+
if (apps?.diagnostics.length) throw new Error("Codex app policy is not ready for a native fork. Complete plugin setup and retry.");
|
|
333
|
+
const nativeSkillIsolation = await resolveCodexNativeSkillIsolation({
|
|
334
|
+
client: context.client,
|
|
335
|
+
cwd,
|
|
336
|
+
codexHome: appServer.start.env?.CODEX_HOME,
|
|
337
|
+
home: appServer.start.env?.HOME,
|
|
338
|
+
userProfile: appServer.start.env?.USERPROFILE
|
|
339
|
+
});
|
|
340
|
+
assertCurrent();
|
|
341
|
+
const generation = randomUUID();
|
|
342
|
+
const relay = buildNativeHookRelayCommandPlan({
|
|
343
|
+
provider: "codex",
|
|
344
|
+
agentId: created.agentId,
|
|
345
|
+
sessionKey: created.key,
|
|
346
|
+
config,
|
|
347
|
+
relayId: buildCodexNativeHookRelayId({
|
|
348
|
+
agentId: created.agentId,
|
|
349
|
+
sessionKey: created.key,
|
|
350
|
+
sessionId: created.sessionId
|
|
351
|
+
}),
|
|
352
|
+
generation,
|
|
353
|
+
preToolUseLoopDetection: appServer.loopDetectionPreToolUseRelay
|
|
354
|
+
});
|
|
355
|
+
const events = resolveCodexNativeHookRelayEvents({ appServer });
|
|
356
|
+
if (events.includes("pre_tool_use") && relay.shouldRelayEvent("pre_tool_use")) {
|
|
357
|
+
await assertCodexNativeHookRelayAllowed(context.client);
|
|
358
|
+
assertCurrent();
|
|
359
|
+
}
|
|
360
|
+
await assertCodexModelBackedReviewerEffectiveConfig({
|
|
361
|
+
client: context.client,
|
|
362
|
+
approvalsReviewer: appServer.approvalsReviewer,
|
|
363
|
+
cwd
|
|
364
|
+
});
|
|
365
|
+
assertCurrent();
|
|
366
|
+
const workspaceInstructions = await prepareCodexWorkspaceDeveloperInstructions({
|
|
367
|
+
config,
|
|
368
|
+
agentId: created.agentId,
|
|
369
|
+
sessionKey: created.key,
|
|
370
|
+
sessionId: created.sessionId,
|
|
371
|
+
workspaceDir,
|
|
372
|
+
cwd
|
|
373
|
+
});
|
|
374
|
+
assertCurrent();
|
|
375
|
+
const promptContext = {
|
|
376
|
+
config,
|
|
377
|
+
agentId: created.agentId,
|
|
378
|
+
sessionKey: created.key,
|
|
379
|
+
modelId: params.model
|
|
380
|
+
};
|
|
381
|
+
const developerInstructions = [buildDeveloperInstructions(promptContext, { dynamicTools }), workspaceInstructions].filter(Boolean).join("\n\n");
|
|
382
|
+
const bundleConfig = bundleMcp.configPatch;
|
|
383
|
+
if (bundleConfig && !isJsonObject(bundleConfig)) throw new Error("Invalid child MCP thread configuration");
|
|
384
|
+
const threadConfig = applyCodexNativeSkillIsolation(mergeCodexThreadConfigs(bundleConfig, userMcp, apps?.configPatch, appServer.networkProxy?.configPatch, buildCodexNativeHookRelayConfig({
|
|
385
|
+
relay,
|
|
386
|
+
events,
|
|
387
|
+
clearOmittedEvents: true
|
|
388
|
+
})), nativeSkillIsolation);
|
|
389
|
+
return {
|
|
390
|
+
request: buildCodexThreadConfiguration(promptContext, {
|
|
391
|
+
cwd,
|
|
392
|
+
appServer,
|
|
393
|
+
dynamicTools,
|
|
394
|
+
developerInstructions,
|
|
395
|
+
config: threadConfig,
|
|
396
|
+
nativeCodeModeEnabled: true,
|
|
397
|
+
nativeCodeModeOnlyEnabled: appServer.codeModeOnly,
|
|
398
|
+
nativeProviderWebSearchSupport,
|
|
399
|
+
webSearchAllowed,
|
|
400
|
+
hostSystemAgentActive: false
|
|
401
|
+
}),
|
|
402
|
+
provisionalAppIds: apps?.provisionalAppIds ?? [],
|
|
403
|
+
bindingPolicy: {
|
|
404
|
+
nativeHookRelayGeneration: generation,
|
|
405
|
+
agentWorkspaceDeveloperInstructions: workspaceInstructions,
|
|
406
|
+
networkProxyProfileName: appServer.networkProxy?.profileName,
|
|
407
|
+
networkProxyConfigFingerprint: appServer.networkProxy?.configFingerprint,
|
|
408
|
+
nativeSkillIsolationFingerprint: nativeSkillIsolation ? fingerprintJsonObject({
|
|
409
|
+
version: 1,
|
|
410
|
+
disabledUserSkillPaths: nativeSkillIsolation.disabledUserSkillPaths
|
|
411
|
+
}) : void 0,
|
|
412
|
+
userMcpServersFingerprint: fingerprintUserMcpServersConfigPatch(userMcp),
|
|
413
|
+
mcpServersFingerprint: bundleMcp.fingerprint,
|
|
414
|
+
webSearchThreadConfigFingerprint: fingerprintJsonObject(webSearch.threadConfig),
|
|
415
|
+
pluginAppsFingerprint: apps?.fingerprint,
|
|
416
|
+
pluginAppsInputFingerprint: apps?.inputFingerprint,
|
|
417
|
+
pluginAppPolicyContext: apps?.policyContext
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
//#endregion
|
|
422
|
+
//#region extensions/codex/src/app-server/upstream-fork-boundary.ts
|
|
423
|
+
const TURN_PAGE_LIMIT = 100;
|
|
424
|
+
function failure(code, message) {
|
|
425
|
+
return {
|
|
426
|
+
ok: false,
|
|
427
|
+
code,
|
|
428
|
+
message
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
function textOnlyMessage(content) {
|
|
432
|
+
if (typeof content === "string") return content;
|
|
433
|
+
if (!Array.isArray(content)) return;
|
|
434
|
+
const texts = [];
|
|
435
|
+
for (const block of content) {
|
|
436
|
+
if (!block || typeof block !== "object" || Array.isArray(block)) return;
|
|
437
|
+
const typed = block;
|
|
438
|
+
if (typed.type !== "text" || typeof typed.text !== "string") return;
|
|
439
|
+
texts.push(typed.text);
|
|
440
|
+
}
|
|
441
|
+
return texts.join("\n");
|
|
442
|
+
}
|
|
443
|
+
function resolveCodexUpstreamForkBoundaryFromTurns(params) {
|
|
444
|
+
let localIndex = 0;
|
|
445
|
+
let matchedPrefix = false;
|
|
446
|
+
for (const [turnIndex, turn] of params.turns.entries()) {
|
|
447
|
+
let userMessagesInTurn = 0;
|
|
448
|
+
for (const item of turn.items) {
|
|
449
|
+
if (item.type !== "userMessage") continue;
|
|
450
|
+
const isSteer = userMessagesInTurn > 0;
|
|
451
|
+
userMessagesInTurn += 1;
|
|
452
|
+
const nativeText = textOnlyMessage(item.content);
|
|
453
|
+
if (nativeText === void 0) return failure("drift-mismatch", "A message before the fork point contains images or attachments that cannot be verified across OpenClaw and Codex. Fork from a text-only span instead.");
|
|
454
|
+
const local = params.localPrefix[localIndex];
|
|
455
|
+
const upstreamText = local && readUpstreamUserText(local.message);
|
|
456
|
+
const text = upstreamText ? nativeText : projectCodexUserItemText(item);
|
|
457
|
+
if (!text) continue;
|
|
458
|
+
const identity = local && readMirrorIdentity(local.message);
|
|
459
|
+
const matchesIdentity = identity === `${turn.id}:${item.id}` || !isSteer && identity === `${turn.id}:prompt`;
|
|
460
|
+
if (!matchedPrefix && !matchesIdentity) continue;
|
|
461
|
+
matchedPrefix = true;
|
|
462
|
+
const localText = textOnlyMessage(local && "content" in local.message ? local.message.content : void 0);
|
|
463
|
+
const upstreamPromptVerified = !upstreamText || local?.message.role === "user" && readCodexMirrorSourceFingerprint(local.message) === fingerprintCodexMirrorSourceMessage(local.message);
|
|
464
|
+
if (!matchesIdentity || !upstreamPromptVerified || localText === void 0 || text !== (upstreamText ?? localText)) return failure("drift-mismatch", "The local conversation no longer matches the Codex thread. Refresh the session and try again.");
|
|
465
|
+
if (localIndex < params.localPrefix.length - 1) {
|
|
466
|
+
localIndex += 1;
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
if (isSteer) return failure("steer-message", "This message steered an existing Codex turn and cannot be forked independently. Fork from the turn's first message instead.");
|
|
470
|
+
if (turn.status === "inProgress") return failure("in-progress-turn", "This Codex turn is still in progress. Wait for it to finish, then try forking again.");
|
|
471
|
+
const retained = turnIndex > 0 ? params.turns[turnIndex - 1] : void 0;
|
|
472
|
+
return {
|
|
473
|
+
ok: true,
|
|
474
|
+
boundary: {
|
|
475
|
+
beforeTurnId: turn.id,
|
|
476
|
+
lastRetainedTurnId: retained?.id ?? null
|
|
477
|
+
}
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
return failure("drift-mismatch", "The local history has no verified boundary in this Codex thread. Use native Codex to fork this conversation.");
|
|
482
|
+
}
|
|
483
|
+
async function listCodexUpstreamTurns(control, threadId) {
|
|
484
|
+
const turns = [];
|
|
485
|
+
const seenCursors = /* @__PURE__ */ new Set();
|
|
486
|
+
let cursor;
|
|
487
|
+
for (;;) {
|
|
488
|
+
const page = await control.listTurnPage({
|
|
489
|
+
threadId,
|
|
490
|
+
limit: TURN_PAGE_LIMIT,
|
|
491
|
+
sortDirection: "asc",
|
|
492
|
+
itemsView: "full",
|
|
493
|
+
...cursor ? { cursor } : {}
|
|
494
|
+
});
|
|
495
|
+
turns.push(...page.data);
|
|
496
|
+
const nextCursor = page.nextCursor?.trim() || void 0;
|
|
497
|
+
if (!nextCursor) return turns;
|
|
498
|
+
if (seenCursors.has(nextCursor)) throw new Error("Codex returned a repeated thread/turns/list cursor");
|
|
499
|
+
seenCursors.add(nextCursor);
|
|
500
|
+
cursor = nextCursor;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
async function resolveCodexUpstreamForkBoundary(params) {
|
|
504
|
+
try {
|
|
505
|
+
const entries = await readVisibleSessionTranscriptMessageEntries({
|
|
506
|
+
agentId: params.agentId,
|
|
507
|
+
sessionId: params.sessionId,
|
|
508
|
+
sessionKey: params.sessionKey,
|
|
509
|
+
storePath: params.storePath
|
|
510
|
+
});
|
|
511
|
+
const visibleUserEntries = entries.filter((entry) => entry.role === "user");
|
|
512
|
+
const targetIndex = visibleUserEntries.findIndex((entry) => entry.entryId === params.entryId);
|
|
513
|
+
if (targetIndex < 0) return failure("drift-mismatch", "The local message could not be mapped to the Codex thread. Refresh the session and try again.");
|
|
514
|
+
const target = visibleUserEntries[targetIndex];
|
|
515
|
+
const isOriginal = (entry) => {
|
|
516
|
+
const identity = readMirrorIdentity(entry.message);
|
|
517
|
+
return Boolean(identity && "idempotencyKey" in entry.message && entry.message.idempotencyKey === `codex-app-server:${params.threadId}:history:${identity}` && readCodexMirrorSourceFingerprint(entry.message));
|
|
518
|
+
};
|
|
519
|
+
const canonical = Boolean(params.canonicalThreadId && !isOriginal(target));
|
|
520
|
+
const threadId = canonical ? params.canonicalThreadId : params.threadId;
|
|
521
|
+
const thread = await params.control.readThread(threadId, false);
|
|
522
|
+
if (thread.id !== threadId) return failure("upstream-unavailable", "This Codex thread is unavailable or its identity changed.");
|
|
523
|
+
assertCodexThreadAcceptsDirectInput(thread);
|
|
524
|
+
if (thread.status?.type === "active") return failure("in-progress-turn", "This Codex thread is active. Wait for it to finish before forking.");
|
|
525
|
+
const localPrefix = visibleUserEntries.slice(0, targetIndex + 1).filter((entry) => {
|
|
526
|
+
if (!canonical) return true;
|
|
527
|
+
if (isOriginal(entry)) return false;
|
|
528
|
+
const meta = "__openclaw" in entry.message ? entry.message["__openclaw"] : void 0;
|
|
529
|
+
const blocked = isRecord(meta) ? meta.beforeAgentRunBlocked : void 0;
|
|
530
|
+
return !(entry !== target && isRecord(blocked) && typeof blocked.blockedBy === "string" && typeof blocked.blockedAt === "number");
|
|
531
|
+
});
|
|
532
|
+
const turns = await listCodexUpstreamTurns(params.control, threadId);
|
|
533
|
+
const resolved = resolveCodexUpstreamForkBoundaryFromTurns({
|
|
534
|
+
turns,
|
|
535
|
+
localPrefix
|
|
536
|
+
});
|
|
537
|
+
const selected = canonical ? entries.slice(0, entries.indexOf(target) + 1) : [];
|
|
538
|
+
const displayPrefix = selected.slice(0, -1);
|
|
539
|
+
if (canonical && (displayPrefix.length > 200 || Buffer.byteLength(JSON.stringify(displayPrefix)) > 524288)) return failure("upstream-unavailable", "The local display prefix exceeds the safe fork-copy limit. Use native Codex to fork this conversation.");
|
|
540
|
+
const frozen = structuredClone(selected);
|
|
541
|
+
const prefix = frozen.slice(0, -1);
|
|
542
|
+
return resolved.ok ? {
|
|
543
|
+
...resolved,
|
|
544
|
+
editorText: textOnlyMessage("content" in target.message ? target.message.content : void 0),
|
|
545
|
+
...canonical ? { canonical: {
|
|
546
|
+
thread,
|
|
547
|
+
turns,
|
|
548
|
+
prefix,
|
|
549
|
+
assertUnchanged: async () => {
|
|
550
|
+
const current = await readVisibleSessionTranscriptMessageEntries(params);
|
|
551
|
+
const index = current.findIndex((entry) => entry.entryId === params.entryId);
|
|
552
|
+
if (index < 0 || !isDeepStrictEqual(current.slice(0, index + 1), frozen)) throw new Error("The local Codex fork prefix changed during initialization");
|
|
553
|
+
const currentThread = await params.control.readThread(threadId, false);
|
|
554
|
+
assertCodexThreadAcceptsDirectInput(currentThread);
|
|
555
|
+
if (currentThread.id !== thread.id || currentThread.path !== thread.path || currentThread.cwd !== thread.cwd || currentThread.historyMode !== thread.historyMode || currentThread.status?.type === "active") throw new Error("The canonical Codex source changed during initialization");
|
|
556
|
+
const currentTurns = await listCodexUpstreamTurns(params.control, threadId);
|
|
557
|
+
const cut = turns.findIndex((turn) => turn.id === resolved.boundary.beforeTurnId);
|
|
558
|
+
if (!isDeepStrictEqual(currentTurns.slice(0, cut + 1), turns.slice(0, cut + 1))) throw new Error("The canonical Codex fork boundary changed during initialization");
|
|
559
|
+
}
|
|
560
|
+
} } : {}
|
|
561
|
+
} : resolved;
|
|
562
|
+
} catch {
|
|
563
|
+
return failure("upstream-unavailable", "The Codex thread could not be read. Check that Codex is available, then try again.");
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
function precheckCodexUpstreamForkBoundary(params) {
|
|
567
|
+
const target = params.turns.find((turn) => turn.id === params.boundary.beforeTurnId);
|
|
568
|
+
if (!target) return failure("upstream-unavailable", "The Codex thread changed before it could be forked. Refresh the session and try again.");
|
|
569
|
+
if (target.status === "inProgress") return failure("in-progress-turn", "This Codex turn is still in progress. Wait for it to finish, then try forking again.");
|
|
570
|
+
return {
|
|
571
|
+
ok: true,
|
|
572
|
+
boundary: params.boundary
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
//#endregion
|
|
576
|
+
//#region extensions/codex/src/app-server/canonical-session-fork.ts
|
|
577
|
+
/** Native history stays native; only the verified local display prefix is copied. */
|
|
578
|
+
async function forkCanonicalCodexSession(params) {
|
|
579
|
+
const { fork, resolved, control, bindingStore, sourceBinding, config } = params;
|
|
580
|
+
const context = control.forkContext;
|
|
581
|
+
if (!context?.localSessionsRoot || !resolved.canonical.thread.path) throw new Error("Canonical Codex forks require the verified local rollout on its selected connection. Fork an original imported message instead.");
|
|
582
|
+
const sourceIdentity = sessionBindingIdentity({
|
|
583
|
+
...fork.source,
|
|
584
|
+
config
|
|
585
|
+
});
|
|
586
|
+
return bindingStore.withLease(sourceIdentity, async () => {
|
|
587
|
+
if (!isDeepStrictEqual(await bindingStore.read(sourceIdentity), sourceBinding)) throw new Error("Codex canonical source binding changed before initialization");
|
|
588
|
+
let freshThreadId;
|
|
589
|
+
let ownership;
|
|
590
|
+
let subscriptionReleased = false;
|
|
591
|
+
let policyWriteUncertain = false;
|
|
592
|
+
return {
|
|
593
|
+
status: "created",
|
|
594
|
+
key: (await params.runtime.agent.session.createSessionEntry({
|
|
595
|
+
cfg: config,
|
|
596
|
+
key: fork.targetKey,
|
|
597
|
+
agentId: fork.source.agentId,
|
|
598
|
+
spawnedCwd: resolved.canonical.thread.cwd ?? sourceBinding.cwd,
|
|
599
|
+
initialEntry: {
|
|
600
|
+
agentHarnessId: params.harnessRuntimeId,
|
|
601
|
+
modelSelectionLocked: true
|
|
602
|
+
},
|
|
603
|
+
afterCreate: async (created) => {
|
|
604
|
+
if (!created.initialization) throw new Error("Canonical Codex forks require host creation authority");
|
|
605
|
+
const host = created.initialization;
|
|
606
|
+
const initialization = prepareCodexSessionInitialization({
|
|
607
|
+
initialization: host,
|
|
608
|
+
bindingStore,
|
|
609
|
+
identity: sessionBindingIdentity({
|
|
610
|
+
agentId: created.agentId,
|
|
611
|
+
sessionId: created.sessionId,
|
|
612
|
+
sessionKey: created.key,
|
|
613
|
+
config
|
|
614
|
+
}),
|
|
615
|
+
assertCleanupAllowed: () => {
|
|
616
|
+
if (policyWriteUncertain) throw new Error("Fresh Codex policy delivery is uncertain; cleanup could not be verified. Inspect the retained thread before retrying.");
|
|
617
|
+
},
|
|
618
|
+
prepareCleanup: () => async (assertCurrent) => {
|
|
619
|
+
if (!freshThreadId) return;
|
|
620
|
+
const threadId = freshThreadId;
|
|
621
|
+
await withCodexAppServerThreadMutation(threadId, async () => {
|
|
622
|
+
assertCurrent();
|
|
623
|
+
if (!subscriptionReleased) {
|
|
624
|
+
if (!ownership) throw new Error("The fresh Codex fork has no verified subscription owner; inspect it before retrying.");
|
|
625
|
+
ownership.assertCurrent();
|
|
626
|
+
}
|
|
627
|
+
if (await bindingStore.hasOtherThreadOwner(threadId)) throw new Error("Codex fork cleanup refused: a successor owns the native thread");
|
|
628
|
+
assertCurrent();
|
|
629
|
+
try {
|
|
630
|
+
await control.archiveThread(threadId, assertCurrent);
|
|
631
|
+
} catch (cause) {
|
|
632
|
+
control.retireConnection?.();
|
|
633
|
+
throw new Error("Fresh Codex fork cleanup could not be verified; inspect the retained thread before retrying.", { cause });
|
|
634
|
+
}
|
|
635
|
+
subscriptionReleased = true;
|
|
636
|
+
assertCurrent();
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
});
|
|
640
|
+
const assertCurrent = () => {
|
|
641
|
+
initialization.assertCurrent();
|
|
642
|
+
if (ownership && !subscriptionReleased) ownership.assertCurrent();
|
|
643
|
+
};
|
|
644
|
+
const selection = await readCodexRolloutSelection({
|
|
645
|
+
sessionsRoot: context.localSessionsRoot,
|
|
646
|
+
rolloutPath: resolved.canonical.thread.path,
|
|
647
|
+
threadId: sourceBinding.threadId,
|
|
648
|
+
assertCurrent
|
|
649
|
+
});
|
|
650
|
+
assertCurrent();
|
|
651
|
+
if (!sourceBinding.dynamicToolsFingerprint) throw new Error("The canonical source has no verified native catalog binding");
|
|
652
|
+
const sourceCatalog = parseCodexNativeToolCatalog(selection.metadata, sourceBinding.threadId, sourceBinding.dynamicToolsFingerprint);
|
|
653
|
+
const prepared = await prepareCanonicalCodexFork({
|
|
654
|
+
created,
|
|
655
|
+
initialization: host,
|
|
656
|
+
config,
|
|
657
|
+
context,
|
|
658
|
+
model: selection.model,
|
|
659
|
+
modelProvider: selection.modelProvider,
|
|
660
|
+
sandbox: fork.sandbox,
|
|
661
|
+
dynamicTools: sourceCatalog
|
|
662
|
+
});
|
|
663
|
+
assertCurrent();
|
|
664
|
+
await selection.assertUnchanged();
|
|
665
|
+
assertCurrent();
|
|
666
|
+
await resolved.canonical.assertUnchanged();
|
|
667
|
+
assertCurrent();
|
|
668
|
+
if (!isDeepStrictEqual(await bindingStore.read(sourceIdentity), sourceBinding)) throw new Error("Codex canonical source binding changed during preparation");
|
|
669
|
+
assertCurrent();
|
|
670
|
+
let raw;
|
|
671
|
+
try {
|
|
672
|
+
raw = await control.forkThread({
|
|
673
|
+
...prepared.request,
|
|
674
|
+
threadId: sourceBinding.threadId,
|
|
675
|
+
beforeTurnId: resolved.boundary.beforeTurnId,
|
|
676
|
+
model: selection.model,
|
|
677
|
+
modelProvider: selection.modelProvider,
|
|
678
|
+
threadSource: "appServer",
|
|
679
|
+
excludeTurns: true
|
|
680
|
+
}, assertCurrent);
|
|
681
|
+
} catch (error) {
|
|
682
|
+
if (!(error instanceof CodexAppServerScopedRequestRejectedError)) control.retireConnection?.();
|
|
683
|
+
throw error;
|
|
684
|
+
}
|
|
685
|
+
let response;
|
|
686
|
+
try {
|
|
687
|
+
response = assertCodexThreadForkResponse(raw);
|
|
688
|
+
} catch (error) {
|
|
689
|
+
control.retireConnection?.();
|
|
690
|
+
throw error;
|
|
691
|
+
}
|
|
692
|
+
if (!response.thread.id.trim() || response.thread.id === sourceBinding.threadId || response.thread.id === fork.upstream.threadId || response.thread.forkedFromId !== sourceBinding.threadId || await bindingStore.hasOtherThreadOwner(response.thread.id)) {
|
|
693
|
+
control.retireConnection?.();
|
|
694
|
+
throw new Error("Codex fork returned an unsafe native thread identity");
|
|
695
|
+
}
|
|
696
|
+
freshThreadId = response.thread.id;
|
|
697
|
+
ownership = await claimCodexAppServerLiveThread(context.client, freshThreadId);
|
|
698
|
+
if (!ownership) {
|
|
699
|
+
control.retireConnection?.();
|
|
700
|
+
throw new Error("Codex fork subscription ownership could not be acquired");
|
|
701
|
+
}
|
|
702
|
+
assertCurrent();
|
|
703
|
+
if (response.model !== selection.model || response.modelProvider !== selection.modelProvider || response.thread.modelProvider !== selection.modelProvider) throw new Error("Codex fork did not preserve the exact canonical source and selected native model");
|
|
704
|
+
const turns = await listCodexUpstreamTurns(control, freshThreadId);
|
|
705
|
+
assertCurrent();
|
|
706
|
+
const cut = resolved.canonical.turns.findIndex((turn) => turn.id === resolved.boundary.beforeTurnId);
|
|
707
|
+
if (cut < 0 || !isDeepStrictEqual(turns, resolved.canonical.turns.slice(0, cut))) throw new Error("Codex did not apply the exact beforeTurnId cut. Reconnect to a compatible server and retry.");
|
|
708
|
+
const metadata = response.thread.path ? await readCodexSessionMeta(context.localSessionsRoot, response.thread.path, freshThreadId) : void 0;
|
|
709
|
+
assertCurrent();
|
|
710
|
+
if (!metadata || metadata.model_provider !== selection.modelProvider) throw new Error("The fresh Codex fork metadata could not be verified");
|
|
711
|
+
const childCatalog = parseCodexNativeToolCatalog(metadata, freshThreadId);
|
|
712
|
+
if (!isDeepStrictEqual(sourceCatalog, childCatalog)) throw new Error("Codex fork did not preserve the actual native tool catalog");
|
|
713
|
+
await selection.assertUnchanged();
|
|
714
|
+
assertCurrent();
|
|
715
|
+
await attestCodexPluginThreadApps({
|
|
716
|
+
client: context.client,
|
|
717
|
+
threadId: freshThreadId,
|
|
718
|
+
appIds: prepared.provisionalAppIds
|
|
719
|
+
});
|
|
720
|
+
assertCurrent();
|
|
721
|
+
await resolved.canonical.assertUnchanged();
|
|
722
|
+
assertCurrent();
|
|
723
|
+
try {
|
|
724
|
+
await refreshCodexThreadPolicy({
|
|
725
|
+
client: context.client,
|
|
726
|
+
threadId: freshThreadId,
|
|
727
|
+
developerInstructions: prepared.request.developerInstructions,
|
|
728
|
+
timeoutMs: context.appServer.requestTimeoutMs,
|
|
729
|
+
assertCurrent
|
|
730
|
+
});
|
|
731
|
+
} catch (error) {
|
|
732
|
+
if (error instanceof CodexThreadPolicyHandoffError && error.outcome === "unknown") {
|
|
733
|
+
policyWriteUncertain = true;
|
|
734
|
+
control.retireConnection?.();
|
|
735
|
+
}
|
|
736
|
+
throw error;
|
|
737
|
+
}
|
|
738
|
+
const appended = await appendSessionTranscriptMessagesByIdentity({
|
|
739
|
+
config,
|
|
740
|
+
storePath: resolveStorePath(config.session?.store, { agentId: created.agentId }),
|
|
741
|
+
agentId: created.agentId,
|
|
742
|
+
sessionKey: created.key,
|
|
743
|
+
sessionId: created.sessionId,
|
|
744
|
+
messages: resolved.canonical.prefix.map((entry) => ({
|
|
745
|
+
message: structuredClone(entry.message),
|
|
746
|
+
idempotencyLookup: "scan"
|
|
747
|
+
}))
|
|
748
|
+
});
|
|
749
|
+
assertCurrent();
|
|
750
|
+
if (appended.length !== resolved.canonical.prefix.length || appended.some((item, index) => !item.appended || !isDeepStrictEqual(item.message, resolved.canonical.prefix[index]?.message) || index > 0 && item.effectiveParentId !== appended[index - 1]?.messageId)) throw new Error("The canonical Codex display prefix could not be copied completely");
|
|
751
|
+
initialization.link({
|
|
752
|
+
sessionKey: created.key,
|
|
753
|
+
agentId: created.agentId,
|
|
754
|
+
catalogId: fork.upstream.catalogId,
|
|
755
|
+
hostId: fork.upstream.hostId,
|
|
756
|
+
threadId: fork.upstream.threadId,
|
|
757
|
+
upstreamKind: fork.upstream.kind,
|
|
758
|
+
upstreamRef: fork.upstream.ref,
|
|
759
|
+
marker: codexUpstreamBaseline({
|
|
760
|
+
...response.thread,
|
|
761
|
+
turns
|
|
762
|
+
}, normalizeOptionalString)
|
|
763
|
+
});
|
|
764
|
+
await initialization.bind({
|
|
765
|
+
threadId: freshThreadId,
|
|
766
|
+
connectionScope: "supervision",
|
|
767
|
+
supervisionSourceThreadId: fork.upstream.threadId,
|
|
768
|
+
preserveNativeModel: true,
|
|
769
|
+
conversationSourceTransferComplete: true,
|
|
770
|
+
cwd: prepared.request.cwd ?? "",
|
|
771
|
+
rolloutPath: response.thread.path ?? void 0,
|
|
772
|
+
model: selection.model,
|
|
773
|
+
modelProvider: selection.modelProvider,
|
|
774
|
+
appServerRuntimeFingerprint: control.connectionFingerprint,
|
|
775
|
+
dynamicToolsFingerprint: codexDynamicToolsFingerprint(childCatalog),
|
|
776
|
+
dynamicToolsContainDeferred: flattenCodexDynamicToolFunctions(childCatalog).some((tool) => tool.deferLoading === true),
|
|
777
|
+
...prepared.bindingPolicy,
|
|
778
|
+
historyCoveredThrough: (/* @__PURE__ */ new Date()).toISOString()
|
|
779
|
+
});
|
|
780
|
+
assertCurrent();
|
|
781
|
+
await ownership.release(freshThreadId, assertCurrent);
|
|
782
|
+
subscriptionReleased = true;
|
|
783
|
+
initialization.assertCurrent();
|
|
784
|
+
await resolved.canonical.assertUnchanged();
|
|
785
|
+
initialization.assertCurrent();
|
|
786
|
+
if (!isDeepStrictEqual(await bindingStore.read(sourceIdentity), sourceBinding)) throw new Error("Codex source binding changed before fork readiness");
|
|
787
|
+
initialization.assertCurrent();
|
|
788
|
+
return { pluginExtensions: created.entry.pluginExtensions };
|
|
789
|
+
}
|
|
790
|
+
}).finally(() => {
|
|
791
|
+
if (freshThreadId && !subscriptionReleased && hasCodexAppServerLiveThread(context.client, freshThreadId)) control.retireConnection?.();
|
|
792
|
+
})).key,
|
|
793
|
+
...resolved.editorText !== void 0 ? { editorText: resolved.editorText } : {}
|
|
794
|
+
};
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
//#endregion
|
|
798
|
+
//#region extensions/codex/src/app-server/upstream-session-fork.ts
|
|
799
|
+
function readConnectionFingerprint(ref) {
|
|
800
|
+
if (!isRecord(ref)) return;
|
|
801
|
+
return typeof ref.connectionFingerprint === "string" && ref.connectionFingerprint.trim() ? ref.connectionFingerprint : void 0;
|
|
802
|
+
}
|
|
803
|
+
async function forkCodexUpstreamSession(params, options) {
|
|
804
|
+
try {
|
|
805
|
+
const sourceFingerprint = params.upstream.kind === "codex-app-server" ? readConnectionFingerprint(params.upstream.ref) : void 0;
|
|
806
|
+
const requestControl = sourceFingerprint ? options.controlFactory.forUpstream(params.source.agentId, sourceFingerprint) : void 0;
|
|
807
|
+
if (!sourceFingerprint || !requestControl) return {
|
|
808
|
+
status: "failed",
|
|
809
|
+
code: "upstream-unavailable",
|
|
810
|
+
message: "This Codex thread is not available on the current connection. Reconnect to its host and try again."
|
|
811
|
+
};
|
|
812
|
+
return await requestControl.withPinnedConnection(async (control) => {
|
|
813
|
+
const sourceBinding = await options.bindingStore.read(sessionBindingIdentity({
|
|
814
|
+
...params.source,
|
|
815
|
+
config: options.resolveConfig?.()
|
|
816
|
+
}));
|
|
817
|
+
const supervised = sourceBinding?.connectionScope === "supervision";
|
|
818
|
+
const sourceThreadId = params.upstream.threadId;
|
|
819
|
+
let initializerOwnsFork = false;
|
|
820
|
+
const archiveFreshFork = async (forkedThreadId, assertCurrent) => withCodexAppServerThreadMutation(forkedThreadId, async () => {
|
|
821
|
+
assertCurrent?.();
|
|
822
|
+
if (await options.bindingStore.hasOtherThreadOwner(forkedThreadId)) throw new Error("Codex fork cleanup refused: the native thread has another owner");
|
|
823
|
+
assertCurrent?.();
|
|
824
|
+
try {
|
|
825
|
+
await control.archiveThread(forkedThreadId, assertCurrent);
|
|
826
|
+
assertCurrent?.();
|
|
827
|
+
} catch (cause) {
|
|
828
|
+
control.retireConnection?.();
|
|
829
|
+
throw new Error("Codex fork cleanup could not be verified; inspect the retained native thread before retrying.", { cause });
|
|
830
|
+
}
|
|
831
|
+
});
|
|
832
|
+
if (sourceFingerprint !== control.connectionFingerprint || supervised && (sourceBinding.supervisionSourceThreadId !== params.upstream.threadId || (sourceBinding.pendingSupervisionBranch?.connectionFingerprint ?? sourceBinding.appServerRuntimeFingerprint) !== sourceFingerprint)) return {
|
|
833
|
+
status: "failed",
|
|
834
|
+
code: "upstream-unavailable",
|
|
835
|
+
message: "This Codex thread is not available on the current connection. Reconnect to its host and try again."
|
|
836
|
+
};
|
|
837
|
+
const resolved = await resolveCodexUpstreamForkBoundary({
|
|
838
|
+
...params.source,
|
|
839
|
+
threadId: sourceThreadId,
|
|
840
|
+
canonicalThreadId: supervised && !sourceBinding.pendingSupervisionBranch ? sourceBinding.threadId : void 0,
|
|
841
|
+
control
|
|
842
|
+
});
|
|
843
|
+
if (!resolved.ok) return {
|
|
844
|
+
status: "failed",
|
|
845
|
+
code: resolved.code,
|
|
846
|
+
message: resolved.message
|
|
847
|
+
};
|
|
848
|
+
if (resolved.canonical && sourceBinding) return await forkCanonicalCodexSession({
|
|
849
|
+
fork: params,
|
|
850
|
+
resolved: {
|
|
851
|
+
...resolved,
|
|
852
|
+
canonical: resolved.canonical
|
|
853
|
+
},
|
|
854
|
+
sourceBinding,
|
|
855
|
+
control,
|
|
856
|
+
bindingStore: options.bindingStore,
|
|
857
|
+
runtime: options.runtime,
|
|
858
|
+
harnessRuntimeId: options.harnessRuntimeId,
|
|
859
|
+
config: options.resolveConfig?.() ?? {}
|
|
860
|
+
});
|
|
861
|
+
const liveTurns = await listCodexUpstreamTurns(control, sourceThreadId);
|
|
862
|
+
const precheck = precheckCodexUpstreamForkBoundary({
|
|
863
|
+
boundary: resolved.boundary,
|
|
864
|
+
turns: liveTurns
|
|
865
|
+
});
|
|
866
|
+
if (!precheck.ok) return {
|
|
867
|
+
status: "failed",
|
|
868
|
+
code: precheck.code,
|
|
869
|
+
message: precheck.message
|
|
870
|
+
};
|
|
871
|
+
const rawResponse = await control.forkThread({
|
|
872
|
+
threadId: sourceThreadId,
|
|
873
|
+
beforeTurnId: resolved.boundary.beforeTurnId,
|
|
874
|
+
...params.sandbox === "required" ? { sandbox: "workspace-write" } : {},
|
|
875
|
+
excludeTurns: true
|
|
876
|
+
});
|
|
877
|
+
const response = assertCodexThreadForkResponse(rawResponse);
|
|
878
|
+
const threadId = response.thread.id.trim();
|
|
879
|
+
if (!threadId) throw new Error("Codex thread/fork response did not include a thread id");
|
|
880
|
+
if (threadId === sourceThreadId || threadId === sourceBinding?.threadId) throw new Error("Codex thread/fork response reused the source thread id");
|
|
881
|
+
const forkedThreadId = threadId;
|
|
882
|
+
try {
|
|
883
|
+
const connectionFingerprint = normalizeOptionalString(control.connectionFingerprint);
|
|
884
|
+
if (!connectionFingerprint) throw new Error("Codex fork connection did not include a fingerprint");
|
|
885
|
+
const forkedTurns = await listCodexUpstreamTurns(control, threadId);
|
|
886
|
+
const expectedLastTurnId = resolved.boundary.lastRetainedTurnId;
|
|
887
|
+
if ((forkedTurns.at(-1)?.id ?? null) !== expectedLastTurnId) throw new Error("This Codex version does not support message-level forks. Update Codex, reconnect, and try again.");
|
|
888
|
+
const forkedThread = {
|
|
889
|
+
...response.thread,
|
|
890
|
+
turns: forkedTurns
|
|
891
|
+
};
|
|
892
|
+
const throughTurnId = codexLastTerminalTurnId(forkedThread, normalizeOptionalString) ?? null;
|
|
893
|
+
const marker = codexUpstreamBaseline(forkedThread, normalizeOptionalString);
|
|
894
|
+
const config = options.resolveConfig?.() ?? {};
|
|
895
|
+
return {
|
|
896
|
+
status: "created",
|
|
897
|
+
key: (await createImportedCodexSession({
|
|
898
|
+
runtime: options.runtime,
|
|
899
|
+
bindingStore: options.bindingStore,
|
|
900
|
+
prepareCleanup: () => {
|
|
901
|
+
initializerOwnsFork = true;
|
|
902
|
+
return (assertCurrent) => archiveFreshFork(forkedThreadId, assertCurrent);
|
|
903
|
+
},
|
|
904
|
+
config,
|
|
905
|
+
key: params.targetKey,
|
|
906
|
+
agentId: params.source.agentId,
|
|
907
|
+
thread: forkedThread,
|
|
908
|
+
throughTurnId,
|
|
909
|
+
initialEntry: {
|
|
910
|
+
agentHarnessId: options.harnessRuntimeId,
|
|
911
|
+
modelSelectionLocked: true
|
|
912
|
+
},
|
|
913
|
+
afterImport: async (entry, initialization) => {
|
|
914
|
+
initialization.link({
|
|
915
|
+
sessionKey: entry.key,
|
|
916
|
+
agentId: entry.agentId,
|
|
917
|
+
catalogId: params.upstream.catalogId,
|
|
918
|
+
hostId: params.upstream.hostId,
|
|
919
|
+
threadId,
|
|
920
|
+
upstreamKind: params.upstream.kind,
|
|
921
|
+
upstreamRef: {
|
|
922
|
+
connectionFingerprint,
|
|
923
|
+
threadId
|
|
924
|
+
},
|
|
925
|
+
marker
|
|
926
|
+
});
|
|
927
|
+
await initialization.bind({
|
|
928
|
+
threadId,
|
|
929
|
+
connectionScope: "supervision",
|
|
930
|
+
supervisionSourceThreadId: threadId,
|
|
931
|
+
preserveNativeModel: true,
|
|
932
|
+
conversationSourceTransferComplete: true,
|
|
933
|
+
pendingSupervisionBranch: {
|
|
934
|
+
sourceThreadId: threadId,
|
|
935
|
+
connectionFingerprint,
|
|
936
|
+
...throughTurnId ? { lastTurnId: throughTurnId } : {}
|
|
937
|
+
},
|
|
938
|
+
cwd: forkedThread.cwd ?? "",
|
|
939
|
+
model: response.model,
|
|
940
|
+
modelProvider: response.modelProvider ?? void 0,
|
|
941
|
+
historyCoveredThrough: (/* @__PURE__ */ new Date()).toISOString()
|
|
942
|
+
});
|
|
943
|
+
return { pluginExtensions: entry.entry.pluginExtensions };
|
|
944
|
+
}
|
|
945
|
+
})).key,
|
|
946
|
+
...resolved.editorText !== void 0 ? { editorText: resolved.editorText } : {}
|
|
947
|
+
};
|
|
948
|
+
} catch (error) {
|
|
949
|
+
if (!initializerOwnsFork) await options.bindingStore.withThreadArchiveFence(() => archiveFreshFork(forkedThreadId));
|
|
950
|
+
return {
|
|
951
|
+
status: "failed",
|
|
952
|
+
code: "upstream-unavailable",
|
|
953
|
+
message: error instanceof Error ? error.message : "The Codex fork could not be imported. Refresh sessions and try again."
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
});
|
|
957
|
+
} catch (error) {
|
|
958
|
+
return {
|
|
959
|
+
status: "failed",
|
|
960
|
+
code: "upstream-unavailable",
|
|
961
|
+
message: error instanceof Error ? error.message : "The Codex thread could not be forked. Check that Codex is available, then try again."
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
//#endregion
|
|
966
|
+
export { forkCodexUpstreamSession };
|