@openclaw/codex 2026.7.1 → 2026.7.2-beta.2

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.
Files changed (48) hide show
  1. package/README.md +27 -3
  2. package/dist/app-server-policy-Scc-Wevo.js +49 -0
  3. package/dist/{attempt-notifications-BGsEIIDI.js → attempt-notifications-BFmNhqFl.js} +3 -6
  4. package/dist/auth-binding-C1czuHPA.js +62 -0
  5. package/dist/cli-metadata.js +20 -0
  6. package/dist/{app-server-policy-C968Kgin.js → command-formatters-Dq9InZUK.js} +39 -51
  7. package/dist/{command-handlers-Dv-pzAmg.js → command-handlers-IgqmL_kv.js} +161 -61
  8. package/dist/{compact-4mVBriKT.js → compact-DCTmg3id.js} +39 -10
  9. package/dist/computer-use-CPcU8TjG.js +685 -0
  10. package/dist/conversation-control-CGkBlfc5.js +286 -0
  11. package/dist/doctor-contract-api.js +120 -81
  12. package/dist/{provider-capabilities-CDnHbmUZ.js → dynamic-tools-C_1tEs34.js} +6109 -5725
  13. package/dist/harness.js +59 -15
  14. package/dist/index.js +1460 -269
  15. package/dist/{media-understanding-provider-BHOu9-Gg.js → media-understanding-provider-D31dOJwb.js} +32 -15
  16. package/dist/media-understanding-provider.js +1 -1
  17. package/dist/{models-DDmO1zwd.js → models-uh26C8QU.js} +5 -9
  18. package/dist/notification-correlation-KmfV4EkP.js +67 -0
  19. package/dist/plugin-app-cache-key-Ceb-lm2c.js +315 -0
  20. package/dist/{provider-zjPfx5Fs.js → rate-limits-DyXaYAxU.js} +10 -266
  21. package/dist/request-B_oQsCXy.js +157 -0
  22. package/dist/{run-attempt-FUyOjGCV.js → run-attempt-MLzoMe_m.js} +7395 -6430
  23. package/dist/runtime-artifact-C2ITXdhM.js +488 -0
  24. package/dist/{sandbox-guard-DA2TQfZW.js → sandbox-guard-D1Wky__S.js} +10 -1
  25. package/dist/{config-CYEDnLJ2.js → session-binding-h1mmCGnl.js} +985 -31
  26. package/dist/session-catalog-CEvoXWHA.js +2412 -0
  27. package/dist/session-cli-XsEuWb_C.js +175 -0
  28. package/dist/{shared-client-4ICy3U6d.js → shared-client-JiAnW6pc.js} +2193 -1319
  29. package/dist/{side-question-N4OaDer2.js → side-question-DqDvIwSU.js} +140 -70
  30. package/dist/{thread-lifecycle-qWE88Dn2.js → thread-lifecycle-CN_pPtPh.js} +1826 -1188
  31. package/dist/{protocol-validators-dZQ-UTOa.js → transcript-mirror-D9rTxx2P.js} +485 -2
  32. package/dist/transport-stdio-DXgGZ7Ib.js +88 -0
  33. package/dist/usage-D5Rohxoc.js +25 -0
  34. package/dist/{web-search-provider.runtime-BSlriav6.js → web-search-provider.runtime-CKVBn3fP.js} +3 -3
  35. package/npm-shrinkwrap.json +56 -30
  36. package/openclaw.plugin.json +177 -14
  37. package/package.json +8 -6
  38. package/dist/command-rpc-eLM_mtC7.js +0 -784
  39. package/dist/computer-use-Bmaz333N.js +0 -377
  40. package/dist/notification-correlation-Bo7KB3ks.js +0 -35
  41. package/dist/plugin-app-cache-key-BrhVdeEf.js +0 -98
  42. package/dist/prompt-overlay.js +0 -15
  43. package/dist/protocol-2POPqAY4.js +0 -13
  44. package/dist/provider-catalog.js +0 -79
  45. package/dist/provider-discovery.js +0 -34
  46. package/dist/provider.js +0 -2
  47. package/dist/request-BcJyl8KL.js +0 -36
  48. package/dist/session-binding-C1ZXdP-x.js +0 -627
@@ -0,0 +1,286 @@
1
+ import { a as getLeasedSharedCodexAppServerClient, c as releaseLeasedSharedCodexAppServerClient, p as withLeasedCodexAppServerClientStartSelectionRetry, s as releaseCodexAppServerClientLease } from "./shared-client-JiAnW6pc.js";
2
+ import { _ as isCodexFastServiceTier, a as isCodexAppServerNativeAuthProfile, b as resolveCodexAppServerRuntimeOptions, o as normalizeCodexAppServerBindingModelProvider, r as bindingStoreKey, w as resolveCodexModelBackedReviewerPolicyContext } from "./session-binding-h1mmCGnl.js";
3
+ import { _ as resolveCodexBindingModelProviderFallback, h as resolveCodexAppServerRequestModelSelection } from "./thread-lifecycle-CN_pPtPh.js";
4
+ import { d as CODEX_CONTROL_METHODS, r as formatCodexDisplayText } from "./command-formatters-Dq9InZUK.js";
5
+ import { a as resolveCodexBindingAppServerConnection } from "./notification-correlation-KmfV4EkP.js";
6
+ import { ModelSelectionLockedError } from "openclaw/plugin-sdk/model-session-runtime";
7
+ //#region extensions/codex/src/command-authorization.ts
8
+ const CODEX_NATIVE_EXECUTION_AUTH_ERROR = "Only an owner or operator.admin can control Codex native execution.";
9
+ function canMutateCodexHost(ctx) {
10
+ return ctx.senderIsOwner === true || ctx.gatewayClientScopes?.includes("operator.admin") === true;
11
+ }
12
+ //#endregion
13
+ //#region extensions/codex/src/conversation-control.ts
14
+ const CODEX_CONVERSATION_CONTROL_STATE = Symbol.for("openclaw.codex.conversationControl");
15
+ function getActiveTurns() {
16
+ const globalState = globalThis;
17
+ globalState[CODEX_CONVERSATION_CONTROL_STATE] ??= /* @__PURE__ */ new Map();
18
+ return globalState[CODEX_CONVERSATION_CONTROL_STATE];
19
+ }
20
+ function trackCodexConversationActiveTurn(active) {
21
+ const activeTurns = getActiveTurns();
22
+ const key = bindingStoreKey(active.identity);
23
+ activeTurns.set(key, active);
24
+ return () => {
25
+ if (activeTurns.get(key)?.turnId === active.turnId) activeTurns.delete(key);
26
+ };
27
+ }
28
+ function readCodexConversationActiveTurn(identity) {
29
+ return getActiveTurns().get(bindingStoreKey(identity));
30
+ }
31
+ async function stopCodexConversationTurn(params) {
32
+ const active = readCodexConversationActiveTurn(params.identity);
33
+ if (!active) return {
34
+ stopped: false,
35
+ message: "No active Codex run to stop."
36
+ };
37
+ const lookup = buildBindingLookup(params);
38
+ const binding = await params.bindingStore.read(params.identity);
39
+ if (binding?.threadId !== active.threadId) return {
40
+ stopped: false,
41
+ message: "The active Codex run no longer matches this session binding."
42
+ };
43
+ const connection = resolveCodexBindingAppServerConnection({
44
+ binding,
45
+ authProfileId: binding?.authProfileId,
46
+ pluginConfig: params.pluginConfig
47
+ });
48
+ const runtime = connection.appServer;
49
+ const client = active.client ?? await getLeasedSharedCodexAppServerClient({
50
+ startOptions: runtime.start,
51
+ timeoutMs: runtime.requestTimeoutMs,
52
+ authProfileId: connection.clientAuthProfileId,
53
+ ...lookup
54
+ });
55
+ try {
56
+ await client.request("turn/interrupt", {
57
+ threadId: active.threadId,
58
+ turnId: active.turnId
59
+ }, { timeoutMs: runtime.requestTimeoutMs });
60
+ } finally {
61
+ if (!active.client) releaseLeasedSharedCodexAppServerClient(client);
62
+ }
63
+ return {
64
+ stopped: true,
65
+ message: "Codex stop requested."
66
+ };
67
+ }
68
+ async function steerCodexConversationTurn(params) {
69
+ const active = readCodexConversationActiveTurn(params.identity);
70
+ const text = params.message.trim();
71
+ if (!text) return {
72
+ steered: false,
73
+ message: "Usage: /codex steer <message>"
74
+ };
75
+ if (!active) return {
76
+ steered: false,
77
+ message: "No active Codex run to steer."
78
+ };
79
+ const lookup = buildBindingLookup(params);
80
+ const binding = await params.bindingStore.read(params.identity);
81
+ if (binding?.threadId !== active.threadId) return {
82
+ steered: false,
83
+ message: "The active Codex run no longer matches this session binding."
84
+ };
85
+ const connection = resolveCodexBindingAppServerConnection({
86
+ binding,
87
+ authProfileId: binding?.authProfileId,
88
+ pluginConfig: params.pluginConfig
89
+ });
90
+ const runtime = connection.appServer;
91
+ const client = active.client ?? await getLeasedSharedCodexAppServerClient({
92
+ startOptions: runtime.start,
93
+ timeoutMs: runtime.requestTimeoutMs,
94
+ authProfileId: connection.clientAuthProfileId,
95
+ ...lookup
96
+ });
97
+ try {
98
+ await client.request("turn/steer", {
99
+ threadId: active.threadId,
100
+ expectedTurnId: active.turnId,
101
+ input: [{
102
+ type: "text",
103
+ text,
104
+ text_elements: []
105
+ }]
106
+ }, { timeoutMs: runtime.requestTimeoutMs });
107
+ } finally {
108
+ if (!active.client) releaseLeasedSharedCodexAppServerClient(client);
109
+ }
110
+ return {
111
+ steered: true,
112
+ message: "Sent steer message to Codex."
113
+ };
114
+ }
115
+ async function setCodexConversationModel(params) {
116
+ const model = params.model.trim();
117
+ if (!model) return "Usage: /codex model <model>";
118
+ const lookup = buildBindingLookup(params);
119
+ const binding = await requireThreadBinding(params.bindingStore, params.identity);
120
+ if (binding.connectionScope === "supervision") throw new ModelSelectionLockedError();
121
+ const reviewerPolicyContext = resolveCodexModelBackedReviewerPolicyContext({
122
+ provider: "codex",
123
+ model,
124
+ bindingModelProvider: binding.modelProvider,
125
+ bindingModel: binding.model,
126
+ nativeAuthProfile: isCodexAppServerNativeAuthProfile({
127
+ authProfileId: binding.authProfileId,
128
+ ...lookup
129
+ })
130
+ });
131
+ const runtime = resolveCodexAppServerRuntimeOptions({
132
+ pluginConfig: params.pluginConfig,
133
+ modelProvider: reviewerPolicyContext.modelProvider,
134
+ model: reviewerPolicyContext.model,
135
+ config: params.config,
136
+ agentDir: params.agentDir
137
+ });
138
+ const modelSelection = resolveCodexAppServerRequestModelSelection({
139
+ model,
140
+ modelProvider: resolveConversationControlModelProvider({
141
+ authProfileId: binding.authProfileId,
142
+ bindingModel: binding.model,
143
+ bindingModelProvider: binding.modelProvider,
144
+ currentModel: model,
145
+ ...lookup
146
+ }),
147
+ authProfileId: binding.authProfileId,
148
+ ...lookup
149
+ });
150
+ const resumed = await resumeThreadWithOverrides({
151
+ runtime,
152
+ threadId: binding.threadId,
153
+ authProfileId: binding.authProfileId,
154
+ ...lookup,
155
+ model: modelSelection.model,
156
+ modelProvider: modelSelection.modelProvider
157
+ });
158
+ const response = resumed.response;
159
+ const nextModel = response.model ?? modelSelection.model;
160
+ const nextModelProvider = normalizeCodexAppServerBindingModelProvider({
161
+ authProfileId: binding.authProfileId,
162
+ modelProvider: response.modelProvider ?? modelSelection.modelProvider,
163
+ ...lookup
164
+ });
165
+ const modelChanged = nextModel !== binding.model || nextModelProvider !== binding.modelProvider;
166
+ await patchThreadBinding(params.bindingStore, params.identity, binding.threadId, {
167
+ clientId: resumed.clientId,
168
+ cwd: response.thread.cwd ?? binding.cwd,
169
+ model: nextModel,
170
+ modelProvider: nextModelProvider,
171
+ ...modelChanged && binding.contextEngine?.projection ? { contextEngine: {
172
+ ...binding.contextEngine,
173
+ projection: void 0
174
+ } } : {},
175
+ approvalPolicy: binding.approvalPolicy,
176
+ sandbox: binding.sandbox,
177
+ serviceTier: binding.serviceTier ?? runtime.serviceTier ?? void 0
178
+ });
179
+ return `Codex model set to ${formatCodexDisplayText(response.model ?? model)}.`;
180
+ }
181
+ async function setCodexConversationFastMode(params) {
182
+ const binding = await requireThreadBinding(params.bindingStore, params.identity);
183
+ if (params.enabled == null) return `Codex fast mode: ${isCodexFastServiceTier(binding.serviceTier) ? "on" : "off"}.`;
184
+ const serviceTier = params.enabled ? "priority" : "flex";
185
+ await patchThreadBinding(params.bindingStore, params.identity, binding.threadId, { serviceTier });
186
+ return `Codex fast mode ${params.enabled ? "enabled" : "disabled"}.`;
187
+ }
188
+ async function setCodexConversationPermissions(params) {
189
+ const binding = await requireThreadBinding(params.bindingStore, params.identity);
190
+ if (!params.mode) return `Codex permissions: ${formatPermissionsMode(binding)}.`;
191
+ const policy = permissionsForMode(params.mode);
192
+ await patchThreadBinding(params.bindingStore, params.identity, binding.threadId, {
193
+ approvalPolicy: policy.approvalPolicy,
194
+ sandbox: policy.sandbox
195
+ });
196
+ return `Codex permissions set to ${params.mode === "yolo" ? "full access" : "default"}.`;
197
+ }
198
+ function parseCodexFastModeArg(arg) {
199
+ const normalized = arg?.trim().toLowerCase();
200
+ if (!normalized || normalized === "status") return;
201
+ if (normalized === "on" || normalized === "true" || normalized === "fast") return true;
202
+ if (normalized === "off" || normalized === "false" || normalized === "flex") return false;
203
+ }
204
+ function parseCodexPermissionsModeArg(arg) {
205
+ const normalized = arg?.trim().toLowerCase();
206
+ if (!normalized || normalized === "status") return;
207
+ if (normalized === "yolo" || normalized === "full" || normalized === "full-access") return "yolo";
208
+ if (normalized === "default" || normalized === "guardian") return "default";
209
+ }
210
+ function formatPermissionsMode(binding) {
211
+ return binding.approvalPolicy === "never" && binding.sandbox === "danger-full-access" ? "full access" : "default";
212
+ }
213
+ async function requireThreadBinding(bindingStore, identity) {
214
+ const binding = await bindingStore.read(identity);
215
+ if (!binding?.threadId) throw new Error("No Codex thread is attached to this OpenClaw session yet.");
216
+ return binding;
217
+ }
218
+ async function patchThreadBinding(bindingStore, identity, threadId, patch) {
219
+ if (!await bindingStore.mutate(identity, {
220
+ kind: "patch",
221
+ threadId,
222
+ patch
223
+ })) throw new Error("Codex thread binding changed while applying the control update.");
224
+ }
225
+ async function resumeThreadWithOverrides(params) {
226
+ const runtime = params.runtime;
227
+ const clientOptions = {
228
+ startOptions: runtime.start,
229
+ timeoutMs: runtime.requestTimeoutMs,
230
+ authProfileId: params.authProfileId,
231
+ ...buildBindingLookup(params)
232
+ };
233
+ let client = await getLeasedSharedCodexAppServerClient(clientOptions);
234
+ const clientLease = { client };
235
+ try {
236
+ return {
237
+ response: await withLeasedCodexAppServerClientStartSelectionRetry({
238
+ lease: clientLease,
239
+ options: clientOptions,
240
+ run: async (requestClient, requestOptions) => await requestClient.request(CODEX_CONTROL_METHODS.resumeThread, {
241
+ threadId: params.threadId,
242
+ ...params.model ? { model: params.model } : {},
243
+ ...params.modelProvider ? { modelProvider: params.modelProvider } : {},
244
+ approvalPolicy: params.approvalPolicy ?? runtime.approvalPolicy,
245
+ sandbox: params.sandbox ?? runtime.sandbox,
246
+ approvalsReviewer: runtime.approvalsReviewer,
247
+ ...params.serviceTier ? { serviceTier: params.serviceTier } : {}
248
+ }, requestOptions),
249
+ onClientChange: (nextClient) => {
250
+ client = nextClient;
251
+ }
252
+ }),
253
+ clientId: client.getInstanceId()
254
+ };
255
+ } finally {
256
+ releaseCodexAppServerClientLease(clientLease);
257
+ }
258
+ }
259
+ function buildBindingLookup(params) {
260
+ const agentDir = params.agentDir?.trim();
261
+ return {
262
+ ...agentDir ? { agentDir } : {},
263
+ ...params.config ? { config: params.config } : {}
264
+ };
265
+ }
266
+ function resolveConversationControlModelProvider(params) {
267
+ const modelProvider = resolveCodexBindingModelProviderFallback({
268
+ currentModel: params.currentModel,
269
+ bindingModel: params.bindingModel,
270
+ bindingModelProvider: params.bindingModelProvider
271
+ })?.trim();
272
+ if (!modelProvider || modelProvider.toLowerCase() === "codex") return;
273
+ if (isCodexAppServerNativeAuthProfile(params) && modelProvider.toLowerCase() === "openai") return;
274
+ return modelProvider.toLowerCase() === "openai" ? "openai" : modelProvider;
275
+ }
276
+ function permissionsForMode(mode) {
277
+ return mode === "yolo" ? {
278
+ approvalPolicy: "never",
279
+ sandbox: "danger-full-access"
280
+ } : {
281
+ approvalPolicy: "on-request",
282
+ sandbox: "workspace-write"
283
+ };
284
+ }
285
+ //#endregion
286
+ export { setCodexConversationFastMode as a, steerCodexConversationTurn as c, CODEX_NATIVE_EXECUTION_AUTH_ERROR as d, canMutateCodexHost as f, readCodexConversationActiveTurn as i, stopCodexConversationTurn as l, parseCodexFastModeArg as n, setCodexConversationModel as o, parseCodexPermissionsModeArg as r, setCodexConversationPermissions as s, formatPermissionsMode as t, trackCodexConversationActiveTurn as u };
@@ -3,8 +3,37 @@ import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { listAgentIds, resolveAgentDir, resolveSessionAgentIds } from "openclaw/plugin-sdk/agent-runtime";
5
5
  import { isDeepStrictEqual } from "node:util";
6
- import { listSessionEntries, resolveSessionFilePath, resolveStorePath, updateSessionStoreEntry } from "openclaw/plugin-sdk/session-store-runtime";
6
+ import { patchSessionEntry, resolveStorePath } from "openclaw/plugin-sdk/session-store-runtime";
7
7
  import { withFileLock } from "openclaw/plugin-sdk/file-lock";
8
+ //#region extensions/codex/src/migration/session-binding-sidecar-archive.ts
9
+ async function pathExists$1(filePath) {
10
+ try {
11
+ await fs.access(filePath);
12
+ return true;
13
+ } catch {
14
+ return false;
15
+ }
16
+ }
17
+ async function firstFreeArchivePath(sourcePath) {
18
+ for (let index = 2;; index++) {
19
+ const candidate = `${sourcePath}.migrated.${index}`;
20
+ if (!await pathExists$1(candidate)) return candidate;
21
+ }
22
+ }
23
+ async function archiveBindingSidecar(sourcePath) {
24
+ const archivePath = `${sourcePath}.migrated`;
25
+ if (!await pathExists$1(archivePath)) {
26
+ await fs.rename(sourcePath, archivePath);
27
+ return;
28
+ }
29
+ const [sourceBytes, archiveBytes] = await Promise.all([fs.readFile(sourcePath), fs.readFile(archivePath)]);
30
+ if (sourceBytes.equals(archiveBytes)) {
31
+ await fs.rm(sourcePath, { force: true });
32
+ return;
33
+ }
34
+ await fs.rename(sourcePath, await firstFreeArchivePath(sourcePath));
35
+ }
36
+ //#endregion
8
37
  //#region extensions/codex/src/migration/session-binding-sidecars.ts
9
38
  const LEGACY_BINDING_SUFFIX = ".codex-app-server.json";
10
39
  const CODEX_AGENT_HARNESS_ID = "codex";
@@ -74,7 +103,7 @@ async function collectLegacyBindingSources(params, options = {}) {
74
103
  return source;
75
104
  };
76
105
  for (const surface of surfaces) {
77
- const sidecars = surface.scan ? walkSidecars(surface.root) : iterateIndexedSidecars(surface, params);
106
+ const sidecars = surface.scan ? walkSidecars(surface.root) : iterateIndexedSidecars(surface);
78
107
  for await (const sidecarPath of sidecars) {
79
108
  const source = await addSource(sidecarPath, surface);
80
109
  if (options.firstOnly) return {
@@ -103,54 +132,37 @@ async function readLegacySessionIndex(storePath) {
103
132
  return { failure: `session index ${storePath} could not be read (invalid JSON)` };
104
133
  }
105
134
  if (!isRecord(raw)) return { failure: `session index ${storePath} has invalid entries` };
106
- let normalizedEntries;
107
- try {
108
- normalizedEntries = listSessionEntries({
109
- storePath,
110
- hydrateSkillPromptRefs: false
111
- });
112
- } catch {
113
- return { failure: `session index ${storePath} could not be normalized` };
114
- }
115
- const normalizedByKey = new Map(normalizedEntries.map(({ sessionKey, entry }) => [sessionKey, entry]));
116
135
  const entries = [];
117
136
  for (const [sessionKey, value] of Object.entries(raw)) {
118
137
  if (!isRecord(value)) return { failure: `session index ${storePath} has invalid entries` };
119
138
  if (value.sessionId === void 0) continue;
120
- const rawSessionId = typeof value.sessionId === "string" ? value.sessionId.trim() : "";
121
- const sessionId = normalizedByKey.get(sessionKey)?.sessionId?.trim() ?? "";
139
+ const sessionId = typeof value.sessionId === "string" ? value.sessionId.trim() : "";
122
140
  const sessionFile = value.sessionFile;
123
141
  const lifecycleRevision = value.lifecycleRevision;
124
142
  const agentHarnessId = value.agentHarnessId;
125
- if (!sessionId || sessionId !== rawSessionId || sessionFile !== void 0 && typeof sessionFile !== "string" || lifecycleRevision !== void 0 && typeof lifecycleRevision !== "string" || agentHarnessId !== void 0 && typeof agentHarnessId !== "string") return { failure: `session index ${storePath} has invalid entries` };
143
+ if (!isSafeLegacySessionId(value.sessionId) || sessionFile !== void 0 && typeof sessionFile !== "string" || lifecycleRevision !== void 0 && typeof lifecycleRevision !== "string" || agentHarnessId !== void 0 && typeof agentHarnessId !== "string") return { failure: `session index ${storePath} has invalid entries` };
126
144
  entries.push({
127
145
  sessionKey,
128
146
  entry: {
129
147
  sessionId,
130
148
  ...typeof sessionFile === "string" ? { sessionFile } : {},
131
149
  ...typeof lifecycleRevision === "string" ? { lifecycleRevision } : {},
132
- ...typeof agentHarnessId === "string" ? { agentHarnessId } : {}
150
+ ...typeof agentHarnessId === "string" ? { agentHarnessId } : {},
151
+ ...typeof value.updatedAt === "number" && Number.isFinite(value.updatedAt) && value.updatedAt >= 0 ? { updatedAt: value.updatedAt } : {}
133
152
  }
134
153
  });
135
154
  }
136
155
  return { entries };
137
156
  }
138
- async function* iterateIndexedSidecars(surface, params) {
157
+ async function* iterateIndexedSidecars(surface) {
139
158
  for (const storePath of surface.storePaths) {
140
159
  const index = await readLegacySessionIndex(storePath);
141
160
  if ("failure" in index) continue;
142
- for (const { sessionKey, entry } of index.entries) {
143
- const agentId = resolveLegacyBindingOwnerAgentId({
144
- sessionKey,
145
- config: params.config,
146
- storeAgentIds: surface.agentIds
147
- });
161
+ for (const { entry } of index.entries) {
162
+ if ((entry.sessionFile?.trim() ?? "").startsWith("sqlite:")) continue;
148
163
  let transcriptPath;
149
164
  try {
150
- transcriptPath = resolveSessionFilePath(entry.sessionId, entry, {
151
- sessionsDir: path.dirname(storePath),
152
- agentId
153
- });
165
+ transcriptPath = await resolveLegacySessionFileLocator(path.dirname(storePath), entry, entry.sessionId);
154
166
  } catch {
155
167
  continue;
156
168
  }
@@ -203,35 +215,34 @@ async function collectBindingOwners(sources, surfaces, params) {
203
215
  config: params.config,
204
216
  storeAgentIds: storeAgentIds.get(storePath)
205
217
  });
206
- let effectiveTranscriptPath;
218
+ let legacyTranscriptPath;
219
+ let canonicalLegacyTranscriptPath;
207
220
  try {
208
- effectiveTranscriptPath = resolveSessionFilePath(sessionId, entry, {
209
- sessionsDir,
210
- agentId
211
- });
221
+ legacyTranscriptPath = await resolveLegacySessionFileLocator(sessionsDir, entry, sessionId);
222
+ canonicalLegacyTranscriptPath = await canonicalizePath(legacyTranscriptPath);
212
223
  } catch {
213
224
  failures.push(`session index ${storePath} has an invalid locator for ${sessionKey}`);
214
225
  continue;
215
226
  }
216
- const transcriptPath = await canonicalizePath(effectiveTranscriptPath);
217
- if (!sourcePaths.has(transcriptPath)) continue;
227
+ if (!sourcePaths.has(canonicalLegacyTranscriptPath)) continue;
218
228
  const owner = {
219
229
  agentId,
220
230
  sessionId,
221
231
  sessionKey,
222
232
  storePath,
223
- transcriptPath: effectiveTranscriptPath,
233
+ transcriptPath: legacyTranscriptPath,
224
234
  ...entry.lifecycleRevision ? { lifecycleRevision: entry.lifecycleRevision } : {},
225
- ...entry.agentHarnessId?.trim() ? { agentHarnessId: entry.agentHarnessId.trim() } : {}
235
+ ...entry.agentHarnessId?.trim() ? { agentHarnessId: entry.agentHarnessId.trim() } : {},
236
+ ...entry.updatedAt !== void 0 ? { updatedAt: entry.updatedAt } : {}
226
237
  };
227
- const candidates = owners.get(transcriptPath) ?? /* @__PURE__ */ new Map();
238
+ const candidates = owners.get(canonicalLegacyTranscriptPath) ?? /* @__PURE__ */ new Map();
228
239
  const ownerKey = `${agentId}\0${sessionId}\0${sessionKey}\0${canonicalStorePath}`;
229
240
  const configuredStorePath = resolveStorePath(params.config.session?.store, {
230
241
  agentId,
231
242
  env: params.env
232
243
  });
233
244
  if (!candidates.has(ownerKey) || storePath === configuredStorePath) candidates.set(ownerKey, owner);
234
- owners.set(transcriptPath, candidates);
245
+ owners.set(canonicalLegacyTranscriptPath, candidates);
235
246
  }
236
247
  }
237
248
  return {
@@ -239,6 +250,16 @@ async function collectBindingOwners(sources, surfaces, params) {
239
250
  failures
240
251
  };
241
252
  }
253
+ async function resolveLegacySessionFileLocator(sessionsDir, entry, sessionId) {
254
+ const base = path.resolve(sessionsDir);
255
+ const fallback = path.join(base, `${sessionId}.jsonl`);
256
+ const sessionFile = entry.sessionFile?.trim();
257
+ if (!sessionFile) return fallback;
258
+ const candidate = path.resolve(base, sessionFile);
259
+ const [canonicalBase, canonicalCandidate] = await Promise.all([canonicalizePathForContainment(base), canonicalizePathForContainment(candidate)]);
260
+ if (!isPathWithin(canonicalBase, canonicalCandidate)) throw new Error("legacy session file locator escapes its session directory");
261
+ return candidate;
262
+ }
242
263
  function resolveLegacyBindingOwnerAgentId(params) {
243
264
  if (params.sessionKey.trim().toLowerCase().startsWith("agent:")) return resolveSessionAgentIds({
244
265
  sessionKey: params.sessionKey,
@@ -271,12 +292,17 @@ async function migrateSource(source, candidates, params, store) {
271
292
  importedKeys,
272
293
  warning: `Left Codex binding sidecar in place because ${reason}: ${source.sidecarPath}`
273
294
  });
295
+ const retainNotice = (reason) => ({
296
+ archived: false,
297
+ importedKeys,
298
+ notice: `Left Codex binding sidecar in place because ${reason}: ${source.sidecarPath}`
299
+ });
274
300
  const owner = candidates.length === 1 ? candidates[0] : void 0;
275
301
  try {
276
302
  return await withFileLock(source.sidecarPath, LEGACY_BINDING_LOCK_OPTIONS, async () => {
277
303
  const [contents, stat] = await Promise.all([fs.readFile(source.sidecarPath, "utf8"), fs.stat(source.sidecarPath)]);
278
304
  const raw = JSON.parse(contents);
279
- const [{ bindingStoreKey, createStoredCodexAppServerBinding, normalizeStoredCodexAppServerBindingFingerprints, readStoredCodexAppServerBinding }, { legacyCodexConversationBindingId }] = await Promise.all([import("./session-binding-C1ZXdP-x.js").then((n) => n.c), import("./conversation-binding-data-BHRpYbX3.js").then((n) => n.t)]);
305
+ const [{ bindingStoreKey, createStoredCodexAppServerBinding, normalizeStoredCodexAppServerBindingFingerprints, readStoredCodexAppServerBinding }, { legacyCodexConversationBindingId }] = await Promise.all([import("./session-binding-h1mmCGnl.js").then((n) => n.l), import("./conversation-binding-data-BHRpYbX3.js").then((n) => n.t)]);
280
306
  const agentId = owner?.agentId ?? (source.agentIds.size === 1 ? [...source.agentIds][0] : void 0);
281
307
  const baseStored = createStoredCodexAppServerBinding(raw, {
282
308
  now: stat.mtime.toISOString(),
@@ -287,7 +313,7 @@ async function migrateSource(source, candidates, params, store) {
287
313
  });
288
314
  if (!baseStored) return retain("its binding is invalid");
289
315
  if (candidates.length > 1) return retain(`${candidates.length} matching session owners make ownership ambiguous`);
290
- if (owner?.agentHarnessId && owner.agentHarnessId !== CODEX_AGENT_HARNESS_ID) return retain(`its session is owned by agent harness ${owner.agentHarnessId}`);
316
+ if (owner?.agentHarnessId && owner.agentHarnessId !== CODEX_AGENT_HARNESS_ID) return retainNotice(`its session is owned by agent harness ${owner.agentHarnessId}`);
291
317
  const sourceSessionFile = typeof raw.sessionFile === "string" && raw.sessionFile.trim() ? raw.sessionFile : source.transcriptPath;
292
318
  const ownerSessionFile = typeof raw.sessionFile === "string" && raw.sessionFile.trim() ? raw.sessionFile : owner?.transcriptPath;
293
319
  const conversationKeys = [sourceSessionFile, ...ownerSessionFile && ownerSessionFile !== sourceSessionFile ? [ownerSessionFile] : []].map((sessionFile) => bindingStoreKey({
@@ -354,7 +380,7 @@ async function migrateSource(source, candidates, params, store) {
354
380
  if (!hasExpected(await store.lookup(entry.key), entry.value)) return retain(`canonical plugin state changed at ${entry.key}`);
355
381
  }
356
382
  if (owner) {
357
- const ownershipWarning = await recordSessionOwner(owner);
383
+ const ownershipWarning = await recordSessionOwner(owner, params.env);
358
384
  if (ownershipWarning) {
359
385
  if (sessionEntry?.value.state === "active") {
360
386
  const update = store.update;
@@ -372,7 +398,7 @@ async function migrateSource(source, candidates, params, store) {
372
398
  });
373
399
  if (hasExpected(await store.lookup(sessionEntry.key), sessionEntry.value)) return retain(`${ownershipWarning}; its stale session binding could not be retired`);
374
400
  }
375
- return retain(ownershipWarning);
401
+ return retainNotice(ownershipWarning);
376
402
  }
377
403
  for (const entry of entries) if (!hasExpected(await store.lookup(entry.key), entry.value)) return retain(`canonical plugin state changed at ${entry.key}`);
378
404
  }
@@ -390,18 +416,39 @@ async function migrateSource(source, candidates, params, store) {
390
416
  return retain(`migration or archiving failed: ${String(error)}`);
391
417
  }
392
418
  }
393
- async function recordSessionOwner(owner) {
419
+ async function recordSessionOwner(owner, env) {
420
+ const currentIndex = await readLegacySessionIndex(owner.storePath);
421
+ if ("failure" in currentIndex) return "its legacy session owner could not be revalidated";
422
+ const currentOwner = currentIndex.entries.find(({ sessionKey }) => sessionKey === owner.sessionKey);
423
+ if (!currentOwner || currentOwner.entry.sessionId !== owner.sessionId) return "its session owner changed before Codex ownership could be recorded";
424
+ let currentTranscriptPath;
425
+ try {
426
+ currentTranscriptPath = await resolveLegacySessionFileLocator(path.dirname(owner.storePath), currentOwner.entry, currentOwner.entry.sessionId);
427
+ } catch {
428
+ return "its session owner changed before Codex ownership could be recorded";
429
+ }
430
+ if (await canonicalizePath(currentTranscriptPath) !== await canonicalizePath(owner.transcriptPath) || currentOwner.entry.lifecycleRevision !== owner.lifecycleRevision) return "its session owner changed before Codex ownership could be recorded";
431
+ const legacyHarnessId = currentOwner.entry.agentHarnessId?.trim();
432
+ if (currentOwner.entry.agentHarnessId !== void 0 && !legacyHarnessId) return "its session owner changed before Codex ownership could be recorded";
433
+ if (legacyHarnessId && legacyHarnessId !== CODEX_AGENT_HARNESS_ID) return `its session is owned by agent harness ${legacyHarnessId}`;
394
434
  let observedForeignHarness;
395
- const updated = await updateSessionStoreEntry({
435
+ const updated = await patchSessionEntry({
436
+ agentId: owner.agentId,
437
+ env,
438
+ fallbackEntry: {
439
+ sessionId: owner.sessionId,
440
+ updatedAt: currentOwner.entry.updatedAt ?? owner.updatedAt ?? 0,
441
+ ...owner.lifecycleRevision ? { lifecycleRevision: owner.lifecycleRevision } : {}
442
+ },
443
+ preserveActivity: true,
444
+ requireWriteSuccess: true,
445
+ skipMaintenance: true,
396
446
  storePath: owner.storePath,
397
447
  sessionKey: owner.sessionKey,
398
- skipMaintenance: true,
399
- requireWriteSuccess: true,
400
448
  update: (entry) => {
401
- const transcriptPath = resolveOwnerTranscriptPath(owner, entry);
402
- if (entry.sessionId.trim() !== owner.sessionId || transcriptPath !== owner.transcriptPath || entry.lifecycleRevision !== owner.lifecycleRevision) return null;
449
+ if (entry.sessionId.trim() !== owner.sessionId || entry.lifecycleRevision !== owner.lifecycleRevision) return null;
403
450
  const harnessId = typeof entry.agentHarnessId === "string" ? entry.agentHarnessId.trim() : void 0;
404
- if (entry.agentHarnessId !== void 0 && harnessId === void 0) return null;
451
+ if (entry.agentHarnessId !== void 0 && !harnessId) return null;
405
452
  if (harnessId && harnessId !== CODEX_AGENT_HARNESS_ID) {
406
453
  observedForeignHarness = harnessId;
407
454
  return null;
@@ -410,21 +457,10 @@ async function recordSessionOwner(owner) {
410
457
  }
411
458
  });
412
459
  if (!updated) return observedForeignHarness ? `its session is owned by agent harness ${observedForeignHarness}` : "its session owner changed before Codex ownership could be recorded";
413
- const transcriptPath = resolveOwnerTranscriptPath(owner, updated);
414
- if (updated.sessionId.trim() !== owner.sessionId || transcriptPath !== owner.transcriptPath || updated.lifecycleRevision !== owner.lifecycleRevision) return "its session owner changed before Codex ownership could be recorded";
460
+ if (updated.sessionId.trim() !== owner.sessionId || updated.lifecycleRevision !== owner.lifecycleRevision) return "its session owner changed before Codex ownership could be recorded";
415
461
  const harnessId = updated.agentHarnessId?.trim();
416
462
  return harnessId === CODEX_AGENT_HARNESS_ID ? void 0 : harnessId ? `its session is owned by agent harness ${harnessId}` : "Codex harness ownership could not be recorded on its session";
417
463
  }
418
- function resolveOwnerTranscriptPath(owner, entry) {
419
- try {
420
- return resolveSessionFilePath(entry.sessionId, entry, {
421
- sessionsDir: path.dirname(owner.storePath),
422
- agentId: owner.agentId
423
- });
424
- } catch {
425
- return;
426
- }
427
- }
428
464
  async function readDirectoryEntries(directory) {
429
465
  try {
430
466
  return await fs.readdir(directory, { withFileTypes: true });
@@ -448,6 +484,11 @@ async function isRegularFile(filePath) {
448
484
  function isRecord(value) {
449
485
  return value !== null && typeof value === "object" && !Array.isArray(value);
450
486
  }
487
+ function isSafeLegacySessionId(value) {
488
+ if (typeof value !== "string") return false;
489
+ const trimmed = value.trim();
490
+ return trimmed.length > 0 && trimmed.length <= 255 && /^[A-Za-z0-9][A-Za-z0-9._:@-]*$/.test(trimmed);
491
+ }
451
492
  function isPathWithin(root, candidate) {
452
493
  const relative = path.relative(root, candidate);
453
494
  return relative === "" || relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
@@ -459,6 +500,20 @@ async function canonicalizePath(filePath) {
459
500
  return path.resolve(filePath);
460
501
  }
461
502
  }
503
+ async function canonicalizePathForContainment(filePath) {
504
+ const resolved = path.resolve(filePath);
505
+ const suffix = [];
506
+ let probe = resolved;
507
+ while (true) try {
508
+ const realProbe = await fs.realpath(probe);
509
+ return suffix.length === 0 ? realProbe : path.join(realProbe, ...suffix.toReversed());
510
+ } catch {
511
+ const parent = path.dirname(probe);
512
+ if (parent === probe) return resolved;
513
+ suffix.push(path.basename(probe));
514
+ probe = parent;
515
+ }
516
+ }
462
517
  async function pathExists(filePath) {
463
518
  try {
464
519
  await fs.access(filePath);
@@ -467,25 +522,6 @@ async function pathExists(filePath) {
467
522
  return false;
468
523
  }
469
524
  }
470
- async function firstFreeArchivePath(sourcePath) {
471
- for (let index = 2;; index++) {
472
- const candidate = `${sourcePath}.migrated.${index}`;
473
- if (!await pathExists(candidate)) return candidate;
474
- }
475
- }
476
- async function archiveBindingSidecar(sourcePath) {
477
- const archivePath = `${sourcePath}.migrated`;
478
- if (await pathExists(archivePath)) {
479
- const [sourceBytes, archiveBytes] = await Promise.all([fs.readFile(sourcePath), fs.readFile(archivePath)]);
480
- if (sourceBytes.equals(archiveBytes)) {
481
- await fs.rm(sourcePath, { force: true });
482
- return;
483
- }
484
- await fs.rename(sourcePath, await firstFreeArchivePath(sourcePath));
485
- return;
486
- }
487
- await fs.rename(sourcePath, archivePath);
488
- }
489
525
  const stateMigrations = [{
490
526
  id: "codex-app-server-sidecars-to-plugin-state",
491
527
  label: "Codex app-server thread bindings",
@@ -496,6 +532,7 @@ const stateMigrations = [{
496
532
  async migrateLegacyState(params) {
497
533
  const changes = [];
498
534
  const warnings = [];
535
+ const notices = [];
499
536
  const { sources, surfaces } = await collectLegacyBindingSources(params);
500
537
  if (sources.length === 0) return {
501
538
  changes,
@@ -519,6 +556,7 @@ const stateMigrations = [{
519
556
  for (const source of sources) {
520
557
  const result = await migrateSource(source, ownerCollection.owners.get(await canonicalizePath(source.transcriptPath)) ?? [], params, store);
521
558
  if (result.warning) warnings.push(result.warning);
559
+ if (result.notice) notices.push(result.notice);
522
560
  if (result.archived) migrated++;
523
561
  else partialImports += result.importedKeys;
524
562
  }
@@ -526,7 +564,8 @@ const stateMigrations = [{
526
564
  if (partialImports > 0) changes.push(`Migrated ${partialImports} safe Codex app-server binding row(s) to plugin state; retained legacy sidecars needing review`);
527
565
  return {
528
566
  changes,
529
- warnings
567
+ warnings,
568
+ ...notices.length > 0 ? { notices } : {}
530
569
  };
531
570
  }
532
571
  }];