@openclaw/codex 2026.6.11 → 2026.7.1-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.
Files changed (42) hide show
  1. package/dist/{app-server-policy-BPTiVNsW.js → app-server-policy-C968Kgin.js} +5 -5
  2. package/dist/{attempt-notifications-BYnndyFl.js → attempt-notifications-BGsEIIDI.js} +29 -40
  3. package/dist/{command-handlers-DEPTJYk0.js → command-handlers-Dv-pzAmg.js} +278 -237
  4. package/dist/command-rpc-eLM_mtC7.js +784 -0
  5. package/dist/compact-4mVBriKT.js +603 -0
  6. package/dist/{computer-use-ClrJwoFp.js → computer-use-Bmaz333N.js} +3 -3
  7. package/dist/{config-CszD0vP3.js → config-CYEDnLJ2.js} +44 -18
  8. package/dist/conversation-binding-data-BHRpYbX3.js +121 -0
  9. package/dist/doctor-contract-api.js +590 -22
  10. package/dist/harness.js +29 -10
  11. package/dist/index.js +1134 -35
  12. package/dist/{media-understanding-provider-ZUQHN7gT.js → media-understanding-provider-BHOu9-Gg.js} +13 -8
  13. package/dist/media-understanding-provider.js +1 -1
  14. package/dist/{models-CAmfBWRl.js → models-DDmO1zwd.js} +2 -2
  15. package/dist/notification-correlation-Bo7KB3ks.js +35 -0
  16. package/dist/{plugin-app-cache-key-C9ILt7bl.js → plugin-app-cache-key-BrhVdeEf.js} +13 -5
  17. package/dist/{protocol-dh-ETiNd.js → protocol-2POPqAY4.js} +2 -1
  18. package/dist/{protocol-validators-B19q5BIX.js → protocol-validators-dZQ-UTOa.js} +205 -56
  19. package/dist/provider-capabilities-DzI8fgEu.js +6172 -0
  20. package/dist/provider-catalog.js +4 -2
  21. package/dist/{provider-B-OHpbD3.js → provider-zjPfx5Fs.js} +94 -23
  22. package/dist/provider.js +2 -2
  23. package/dist/{request-Dcd452Nk.js → request-BcJyl8KL.js} +2 -2
  24. package/dist/{run-attempt-CXxtFC9d.js → run-attempt-DNSNk_G8.js} +1250 -2992
  25. package/dist/{sandbox-guard-3tnjhjFb.js → sandbox-guard-DA2TQfZW.js} +5 -3
  26. package/dist/session-binding-C1ZXdP-x.js +627 -0
  27. package/dist/session-binding-meta-B7aEMU7g.js +6 -0
  28. package/dist/shared-client-4ICy3U6d.js +1853 -0
  29. package/dist/{side-question-C-DhgJOd.js → side-question-CMOpEhvY.js} +134 -142
  30. package/dist/{thread-lifecycle-DtT3-ehU.js → thread-lifecycle-qWE88Dn2.js} +803 -481
  31. package/dist/{web-search-provider.runtime-CkwxrXQj.js → web-search-provider.runtime-BSlriav6.js} +3 -3
  32. package/npm-shrinkwrap.json +34 -34
  33. package/openclaw.plugin.json +23 -3
  34. package/package.json +9 -6
  35. package/dist/client-CLrtWgrD.js +0 -729
  36. package/dist/client-factory-Bm6HsGob.js +0 -18
  37. package/dist/compact-aQJ0ZKhe.js +0 -278
  38. package/dist/conversation-binding-t0XMLz-j.js +0 -1557
  39. package/dist/provider-capabilities-D7hnTO76.js +0 -3201
  40. package/dist/rate-limit-cache-C7qmZ0Jh.js +0 -26
  41. package/dist/session-binding-HOuPt-E0.js +0 -338
  42. package/dist/shared-client-Dtx5i7Ez.js +0 -1013
@@ -1,3 +1,547 @@
1
+ import { n as CODEX_APP_SERVER_BINDING_NAMESPACE, t as CODEX_APP_SERVER_BINDING_MAX_ENTRIES } from "./session-binding-meta-B7aEMU7g.js";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { listAgentIds, resolveAgentDir, resolveSessionAgentIds } from "openclaw/plugin-sdk/agent-runtime";
5
+ import { isDeepStrictEqual } from "node:util";
6
+ import { listSessionEntries, resolveSessionFilePath, resolveStorePath, updateSessionStoreEntry } from "openclaw/plugin-sdk/session-store-runtime";
7
+ import { withFileLock } from "openclaw/plugin-sdk/file-lock";
8
+ //#region extensions/codex/src/migration/session-binding-sidecars.ts
9
+ const LEGACY_BINDING_SUFFIX = ".codex-app-server.json";
10
+ const CODEX_AGENT_HARNESS_ID = "codex";
11
+ const MAX_SESSION_DIRECTORY_DEPTH = 16;
12
+ const LEGACY_BINDING_LOCK_OPTIONS = {
13
+ retries: {
14
+ retries: 75,
15
+ factor: 1,
16
+ minTimeout: 1e3,
17
+ maxTimeout: 1e3
18
+ },
19
+ stale: 12e4
20
+ };
21
+ async function collectSessionSurfaces(params) {
22
+ const surfaces = /* @__PURE__ */ new Map();
23
+ const stateRoot = await canonicalizePath(params.stateDir);
24
+ const add = async (root, storePath, agentId, scan) => {
25
+ const canonicalRoot = await canonicalizePath(root);
26
+ const surface = surfaces.get(canonicalRoot) ?? {
27
+ root: canonicalRoot,
28
+ scan: false,
29
+ storePaths: /* @__PURE__ */ new Set(),
30
+ agentIds: /* @__PURE__ */ new Set()
31
+ };
32
+ surface.scan ||= scan;
33
+ surface.storePaths.add(path.resolve(storePath));
34
+ surface.agentIds.add(agentId);
35
+ surfaces.set(canonicalRoot, surface);
36
+ };
37
+ const agentIds = new Set(listAgentIds(params.config));
38
+ const agentsDir = path.join(params.stateDir, "agents");
39
+ for (const entry of await readDirectoryEntries(agentsDir)) {
40
+ if (!entry.isDirectory() || entry.isSymbolicLink()) continue;
41
+ const agentId = resolveSessionAgentIds({
42
+ agentId: entry.name,
43
+ config: params.config
44
+ }).sessionAgentId;
45
+ agentIds.add(agentId);
46
+ const root = path.join(agentsDir, entry.name, "sessions");
47
+ await add(root, path.join(root, "sessions.json"), agentId, true);
48
+ }
49
+ for (const agentId of agentIds) {
50
+ const storePath = resolveStorePath(params.config.session?.store, {
51
+ agentId,
52
+ env: params.env
53
+ });
54
+ const root = path.dirname(storePath);
55
+ await add(root, storePath, agentId, isPathWithin(stateRoot, await canonicalizePath(root)));
56
+ }
57
+ const legacyRoot = path.join(params.stateDir, "sessions");
58
+ const defaultAgentId = resolveSessionAgentIds({ config: params.config }).defaultAgentId;
59
+ await add(legacyRoot, path.join(legacyRoot, "sessions.json"), defaultAgentId, true);
60
+ return [...surfaces.values()].toSorted((a, b) => a.root.localeCompare(b.root));
61
+ }
62
+ async function collectLegacyBindingSources(params, options = {}) {
63
+ const surfaces = await collectSessionSurfaces(params);
64
+ const sources = /* @__PURE__ */ new Map();
65
+ const addSource = async (sidecarPath, surface) => {
66
+ const canonicalSidecar = await canonicalizePath(sidecarPath);
67
+ const source = sources.get(canonicalSidecar) ?? {
68
+ sidecarPath: canonicalSidecar,
69
+ transcriptPath: sidecarPath.slice(0, -22),
70
+ agentIds: /* @__PURE__ */ new Set()
71
+ };
72
+ for (const agentId of surface.agentIds) source.agentIds.add(agentId);
73
+ sources.set(canonicalSidecar, source);
74
+ return source;
75
+ };
76
+ for (const surface of surfaces) {
77
+ const sidecars = surface.scan ? walkSidecars(surface.root) : iterateIndexedSidecars(surface, params);
78
+ for await (const sidecarPath of sidecars) {
79
+ const source = await addSource(sidecarPath, surface);
80
+ if (options.firstOnly) return {
81
+ sources: [source],
82
+ surfaces
83
+ };
84
+ }
85
+ }
86
+ return {
87
+ sources: [...sources.values()].toSorted((a, b) => a.sidecarPath.localeCompare(b.sidecarPath)),
88
+ surfaces
89
+ };
90
+ }
91
+ async function readLegacySessionIndex(storePath) {
92
+ let contents;
93
+ try {
94
+ contents = await fs.readFile(storePath, "utf8");
95
+ } catch (error) {
96
+ const code = error.code;
97
+ return code === "ENOENT" ? { entries: [] } : { failure: `session index ${storePath} could not be read${code ? ` (${code})` : ""}` };
98
+ }
99
+ let raw;
100
+ try {
101
+ raw = JSON.parse(contents);
102
+ } catch {
103
+ return { failure: `session index ${storePath} could not be read (invalid JSON)` };
104
+ }
105
+ 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
+ const entries = [];
117
+ for (const [sessionKey, value] of Object.entries(raw)) {
118
+ if (!isRecord(value)) return { failure: `session index ${storePath} has invalid entries` };
119
+ if (value.sessionId === void 0) continue;
120
+ const rawSessionId = typeof value.sessionId === "string" ? value.sessionId.trim() : "";
121
+ const sessionId = normalizedByKey.get(sessionKey)?.sessionId?.trim() ?? "";
122
+ const sessionFile = value.sessionFile;
123
+ const lifecycleRevision = value.lifecycleRevision;
124
+ 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` };
126
+ entries.push({
127
+ sessionKey,
128
+ entry: {
129
+ sessionId,
130
+ ...typeof sessionFile === "string" ? { sessionFile } : {},
131
+ ...typeof lifecycleRevision === "string" ? { lifecycleRevision } : {},
132
+ ...typeof agentHarnessId === "string" ? { agentHarnessId } : {}
133
+ }
134
+ });
135
+ }
136
+ return { entries };
137
+ }
138
+ async function* iterateIndexedSidecars(surface, params) {
139
+ for (const storePath of surface.storePaths) {
140
+ const index = await readLegacySessionIndex(storePath);
141
+ 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
+ });
148
+ let transcriptPath;
149
+ try {
150
+ transcriptPath = resolveSessionFilePath(entry.sessionId, entry, {
151
+ sessionsDir: path.dirname(storePath),
152
+ agentId
153
+ });
154
+ } catch {
155
+ continue;
156
+ }
157
+ const sidecarPath = `${transcriptPath}${LEGACY_BINDING_SUFFIX}`;
158
+ if (await isRegularFile(sidecarPath)) yield sidecarPath;
159
+ }
160
+ }
161
+ }
162
+ async function* walkSidecars(root) {
163
+ const pending = [{
164
+ directory: root,
165
+ depth: 0
166
+ }];
167
+ while (pending.length > 0) {
168
+ const current = pending.pop();
169
+ for (const entry of (await readDirectoryEntries(current.directory)).toSorted((a, b) => a.name.localeCompare(b.name))) {
170
+ if (entry.isSymbolicLink()) continue;
171
+ const entryPath = path.join(current.directory, entry.name);
172
+ if (entry.isFile() && entry.name.endsWith(LEGACY_BINDING_SUFFIX)) yield entryPath;
173
+ else if (entry.isDirectory() && current.depth < MAX_SESSION_DIRECTORY_DEPTH) pending.push({
174
+ directory: entryPath,
175
+ depth: current.depth + 1
176
+ });
177
+ }
178
+ }
179
+ }
180
+ async function collectBindingOwners(sources, surfaces, params) {
181
+ const sourcePaths = new Set(await Promise.all(sources.map((source) => canonicalizePath(source.transcriptPath))));
182
+ const owners = /* @__PURE__ */ new Map();
183
+ const storePaths = new Set(surfaces.flatMap((surface) => [...surface.storePaths]));
184
+ const storeAgentIds = /* @__PURE__ */ new Map();
185
+ for (const surface of surfaces) for (const storePath of surface.storePaths) {
186
+ const agents = storeAgentIds.get(storePath) ?? /* @__PURE__ */ new Set();
187
+ for (const agentId of surface.agentIds) agents.add(agentId);
188
+ storeAgentIds.set(storePath, agents);
189
+ }
190
+ const failures = [];
191
+ for (const storePath of storePaths) {
192
+ const canonicalStorePath = await canonicalizePath(storePath);
193
+ const index = await readLegacySessionIndex(storePath);
194
+ if ("failure" in index) {
195
+ failures.push(index.failure);
196
+ continue;
197
+ }
198
+ const sessionsDir = path.dirname(storePath);
199
+ for (const { sessionKey, entry } of index.entries) {
200
+ const sessionId = entry.sessionId;
201
+ const agentId = resolveLegacyBindingOwnerAgentId({
202
+ sessionKey,
203
+ config: params.config,
204
+ storeAgentIds: storeAgentIds.get(storePath)
205
+ });
206
+ let effectiveTranscriptPath;
207
+ try {
208
+ effectiveTranscriptPath = resolveSessionFilePath(sessionId, entry, {
209
+ sessionsDir,
210
+ agentId
211
+ });
212
+ } catch {
213
+ failures.push(`session index ${storePath} has an invalid locator for ${sessionKey}`);
214
+ continue;
215
+ }
216
+ const transcriptPath = await canonicalizePath(effectiveTranscriptPath);
217
+ if (!sourcePaths.has(transcriptPath)) continue;
218
+ const owner = {
219
+ agentId,
220
+ sessionId,
221
+ sessionKey,
222
+ storePath,
223
+ transcriptPath: effectiveTranscriptPath,
224
+ ...entry.lifecycleRevision ? { lifecycleRevision: entry.lifecycleRevision } : {},
225
+ ...entry.agentHarnessId?.trim() ? { agentHarnessId: entry.agentHarnessId.trim() } : {}
226
+ };
227
+ const candidates = owners.get(transcriptPath) ?? /* @__PURE__ */ new Map();
228
+ const ownerKey = `${agentId}\0${sessionId}\0${sessionKey}\0${canonicalStorePath}`;
229
+ const configuredStorePath = resolveStorePath(params.config.session?.store, {
230
+ agentId,
231
+ env: params.env
232
+ });
233
+ if (!candidates.has(ownerKey) || storePath === configuredStorePath) candidates.set(ownerKey, owner);
234
+ owners.set(transcriptPath, candidates);
235
+ }
236
+ }
237
+ return {
238
+ owners: new Map([...owners].map(([key, values]) => [key, [...values.values()]])),
239
+ failures
240
+ };
241
+ }
242
+ function resolveLegacyBindingOwnerAgentId(params) {
243
+ if (params.sessionKey.trim().toLowerCase().startsWith("agent:")) return resolveSessionAgentIds({
244
+ sessionKey: params.sessionKey,
245
+ config: params.config
246
+ }).sessionAgentId;
247
+ const storeAgentId = params.storeAgentIds?.size === 1 ? [...params.storeAgentIds][0] : void 0;
248
+ return resolveSessionAgentIds({
249
+ sessionKey: params.sessionKey,
250
+ config: params.config,
251
+ ...storeAgentId ? { agentId: storeAgentId } : {}
252
+ }).sessionAgentId;
253
+ }
254
+ function copyBindingForSession(stored, sessionId) {
255
+ return stored.state === "active" ? {
256
+ version: 1,
257
+ state: "active",
258
+ binding: stored.binding,
259
+ sessionId
260
+ } : {
261
+ version: 1,
262
+ state: "cleared",
263
+ sessionId,
264
+ ...stored.retired ? { retired: true } : {}
265
+ };
266
+ }
267
+ async function migrateSource(source, candidates, params, store) {
268
+ let importedKeys = 0;
269
+ const retain = (reason) => ({
270
+ archived: false,
271
+ importedKeys,
272
+ warning: `Left Codex binding sidecar in place because ${reason}: ${source.sidecarPath}`
273
+ });
274
+ const retainNotice = (reason) => ({
275
+ archived: false,
276
+ importedKeys,
277
+ notice: `Left Codex binding sidecar in place because ${reason}: ${source.sidecarPath}`
278
+ });
279
+ const owner = candidates.length === 1 ? candidates[0] : void 0;
280
+ try {
281
+ return await withFileLock(source.sidecarPath, LEGACY_BINDING_LOCK_OPTIONS, async () => {
282
+ const [contents, stat] = await Promise.all([fs.readFile(source.sidecarPath, "utf8"), fs.stat(source.sidecarPath)]);
283
+ const raw = JSON.parse(contents);
284
+ 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)]);
285
+ const agentId = owner?.agentId ?? (source.agentIds.size === 1 ? [...source.agentIds][0] : void 0);
286
+ const baseStored = createStoredCodexAppServerBinding(raw, {
287
+ now: stat.mtime.toISOString(),
288
+ lookup: {
289
+ config: params.config,
290
+ ...agentId ? { agentDir: resolveAgentDir(params.config, agentId, params.env) } : {}
291
+ }
292
+ });
293
+ if (!baseStored) return retain("its binding is invalid");
294
+ if (candidates.length > 1) return retain(`${candidates.length} matching session owners make ownership ambiguous`);
295
+ if (owner?.agentHarnessId && owner.agentHarnessId !== CODEX_AGENT_HARNESS_ID) return retainNotice(`its session is owned by agent harness ${owner.agentHarnessId}`);
296
+ const sourceSessionFile = typeof raw.sessionFile === "string" && raw.sessionFile.trim() ? raw.sessionFile : source.transcriptPath;
297
+ const ownerSessionFile = typeof raw.sessionFile === "string" && raw.sessionFile.trim() ? raw.sessionFile : owner?.transcriptPath;
298
+ const conversationKeys = [sourceSessionFile, ...ownerSessionFile && ownerSessionFile !== sourceSessionFile ? [ownerSessionFile] : []].map((sessionFile) => bindingStoreKey({
299
+ kind: "conversation",
300
+ bindingId: legacyCodexConversationBindingId(sessionFile)
301
+ }));
302
+ const normalizeStoredRow = async (key, current) => {
303
+ const parsed = readStoredCodexAppServerBinding(current);
304
+ if (!parsed) return { warning: `canonical plugin state is invalid at ${key}` };
305
+ const normalized = normalizeStoredCodexAppServerBindingFingerprints(parsed);
306
+ if (!normalized) return { warning: `canonical plugin state is invalid at ${key}` };
307
+ if (isDeepStrictEqual(parsed, normalized)) return { value: parsed };
308
+ if (parsed.lease && parsed.lease.expiresAt > Date.now()) return { warning: `canonical plugin state is leased at ${key}` };
309
+ const update = store.update;
310
+ if (!update) return { warning: `canonical plugin state could not be normalized at ${key}` };
311
+ await update(key, (candidate) => {
312
+ const candidateParsed = readStoredCodexAppServerBinding(candidate);
313
+ if (!candidateParsed || !isDeepStrictEqual(candidateParsed, parsed)) return;
314
+ return normalized;
315
+ });
316
+ const persisted = readStoredCodexAppServerBinding(await store.lookup(key));
317
+ if (!persisted || !isDeepStrictEqual(persisted, normalized)) return { warning: `canonical plugin state changed at ${key}` };
318
+ importedKeys++;
319
+ return { value: normalized };
320
+ };
321
+ let currentConversation;
322
+ for (const key of conversationKeys) {
323
+ const current = await store.lookup(key);
324
+ if (current === void 0) continue;
325
+ const result = await normalizeStoredRow(key, current);
326
+ if (result.warning || !result.value) return retain(result.warning ?? `canonical plugin state is invalid at ${key}`);
327
+ currentConversation ??= result.value;
328
+ }
329
+ const stored = currentConversation ?? baseStored;
330
+ const sessionKey = owner ? bindingStoreKey({
331
+ kind: "session",
332
+ agentId: owner.agentId,
333
+ sessionId: owner.sessionId,
334
+ sessionKey: owner.sessionKey
335
+ }) : void 0;
336
+ const conversationEntries = conversationKeys.map((key) => ({
337
+ key,
338
+ value: stored
339
+ }));
340
+ const sessionEntry = owner && sessionKey ? {
341
+ key: sessionKey,
342
+ value: copyBindingForSession(stored, owner.sessionId)
343
+ } : void 0;
344
+ const entries = [...conversationEntries, ...sessionEntry ? [sessionEntry] : []];
345
+ const hasExpected = (value, target) => {
346
+ const parsed = readStoredCodexAppServerBinding(value);
347
+ if (!parsed) return false;
348
+ return target.state === "cleared" ? parsed.state === "cleared" && parsed.sessionId === target.sessionId && parsed.retired === target.retired : parsed.state === "active" && parsed.sessionId === target.sessionId && isDeepStrictEqual(parsed.binding, target.binding);
349
+ };
350
+ for (const entry of entries) {
351
+ const current = await store.lookup(entry.key);
352
+ if (current === void 0) continue;
353
+ const result = await normalizeStoredRow(entry.key, current);
354
+ if (result.warning || !result.value) return retain(result.warning ?? `canonical plugin state is invalid at ${entry.key}`);
355
+ if (!hasExpected(result.value, entry.value)) return retain(`canonical plugin state changed at ${entry.key}`);
356
+ }
357
+ for (const entry of entries) {
358
+ if (await store.registerIfAbsent(entry.key, entry.value)) importedKeys++;
359
+ if (!hasExpected(await store.lookup(entry.key), entry.value)) return retain(`canonical plugin state changed at ${entry.key}`);
360
+ }
361
+ if (owner) {
362
+ const ownershipWarning = await recordSessionOwner(owner);
363
+ if (ownershipWarning) {
364
+ if (sessionEntry?.value.state === "active") {
365
+ const update = store.update;
366
+ if (!update) return retain(`${ownershipWarning}; its stale session binding could not be retired`);
367
+ await update(sessionEntry.key, (current) => {
368
+ const parsed = readStoredCodexAppServerBinding(current);
369
+ if (parsed?.lease && parsed.lease.expiresAt > Date.now()) return;
370
+ if (!hasExpected(current, sessionEntry.value)) return;
371
+ return {
372
+ version: 1,
373
+ state: "cleared",
374
+ sessionId: owner.sessionId,
375
+ retired: true
376
+ };
377
+ });
378
+ if (hasExpected(await store.lookup(sessionEntry.key), sessionEntry.value)) return retain(`${ownershipWarning}; its stale session binding could not be retired`);
379
+ }
380
+ return retain(ownershipWarning);
381
+ }
382
+ for (const entry of entries) if (!hasExpected(await store.lookup(entry.key), entry.value)) return retain(`canonical plugin state changed at ${entry.key}`);
383
+ }
384
+ await archiveBindingSidecar(source.sidecarPath);
385
+ return {
386
+ archived: true,
387
+ importedKeys
388
+ };
389
+ });
390
+ } catch (error) {
391
+ if (error.code === "ENOENT" && !await pathExists(source.sidecarPath)) return {
392
+ archived: true,
393
+ importedKeys
394
+ };
395
+ return retain(`migration or archiving failed: ${String(error)}`);
396
+ }
397
+ }
398
+ async function recordSessionOwner(owner) {
399
+ let observedForeignHarness;
400
+ const updated = await updateSessionStoreEntry({
401
+ storePath: owner.storePath,
402
+ sessionKey: owner.sessionKey,
403
+ skipMaintenance: true,
404
+ requireWriteSuccess: true,
405
+ update: (entry) => {
406
+ const transcriptPath = resolveOwnerTranscriptPath(owner, entry);
407
+ if (entry.sessionId.trim() !== owner.sessionId || transcriptPath !== owner.transcriptPath || entry.lifecycleRevision !== owner.lifecycleRevision) return null;
408
+ const harnessId = typeof entry.agentHarnessId === "string" ? entry.agentHarnessId.trim() : void 0;
409
+ if (entry.agentHarnessId !== void 0 && harnessId === void 0) return null;
410
+ if (harnessId && harnessId !== CODEX_AGENT_HARNESS_ID) {
411
+ observedForeignHarness = harnessId;
412
+ return null;
413
+ }
414
+ return { agentHarnessId: CODEX_AGENT_HARNESS_ID };
415
+ }
416
+ });
417
+ if (!updated) return observedForeignHarness ? `its session is owned by agent harness ${observedForeignHarness}` : "its session owner changed before Codex ownership could be recorded";
418
+ const transcriptPath = resolveOwnerTranscriptPath(owner, updated);
419
+ if (updated.sessionId.trim() !== owner.sessionId || transcriptPath !== owner.transcriptPath || updated.lifecycleRevision !== owner.lifecycleRevision) return "its session owner changed before Codex ownership could be recorded";
420
+ const harnessId = updated.agentHarnessId?.trim();
421
+ 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";
422
+ }
423
+ function resolveOwnerTranscriptPath(owner, entry) {
424
+ try {
425
+ return resolveSessionFilePath(entry.sessionId, entry, {
426
+ sessionsDir: path.dirname(owner.storePath),
427
+ agentId: owner.agentId
428
+ });
429
+ } catch {
430
+ return;
431
+ }
432
+ }
433
+ async function readDirectoryEntries(directory) {
434
+ try {
435
+ return await fs.readdir(directory, { withFileTypes: true });
436
+ } catch (error) {
437
+ if ([
438
+ "EACCES",
439
+ "ENOENT",
440
+ "ENOTDIR",
441
+ "EPERM"
442
+ ].includes(error.code ?? "")) return [];
443
+ throw error;
444
+ }
445
+ }
446
+ async function isRegularFile(filePath) {
447
+ try {
448
+ return (await fs.stat(filePath)).isFile();
449
+ } catch {
450
+ return false;
451
+ }
452
+ }
453
+ function isRecord(value) {
454
+ return value !== null && typeof value === "object" && !Array.isArray(value);
455
+ }
456
+ function isPathWithin(root, candidate) {
457
+ const relative = path.relative(root, candidate);
458
+ return relative === "" || relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
459
+ }
460
+ async function canonicalizePath(filePath) {
461
+ try {
462
+ return await fs.realpath(filePath);
463
+ } catch {
464
+ return path.resolve(filePath);
465
+ }
466
+ }
467
+ async function pathExists(filePath) {
468
+ try {
469
+ await fs.access(filePath);
470
+ return true;
471
+ } catch {
472
+ return false;
473
+ }
474
+ }
475
+ async function firstFreeArchivePath(sourcePath) {
476
+ for (let index = 2;; index++) {
477
+ const candidate = `${sourcePath}.migrated.${index}`;
478
+ if (!await pathExists(candidate)) return candidate;
479
+ }
480
+ }
481
+ async function archiveBindingSidecar(sourcePath) {
482
+ const archivePath = `${sourcePath}.migrated`;
483
+ if (await pathExists(archivePath)) {
484
+ const [sourceBytes, archiveBytes] = await Promise.all([fs.readFile(sourcePath), fs.readFile(archivePath)]);
485
+ if (sourceBytes.equals(archiveBytes)) {
486
+ await fs.rm(sourcePath, { force: true });
487
+ return;
488
+ }
489
+ await fs.rename(sourcePath, await firstFreeArchivePath(sourcePath));
490
+ return;
491
+ }
492
+ await fs.rename(sourcePath, archivePath);
493
+ }
494
+ const stateMigrations = [{
495
+ id: "codex-app-server-sidecars-to-plugin-state",
496
+ label: "Codex app-server thread bindings",
497
+ async detectLegacyState(params) {
498
+ const { sources } = await collectLegacyBindingSources(params, { firstOnly: true });
499
+ return sources.length > 0 ? { preview: [`- Codex app-server bindings: legacy sidecar -> plugin state (${CODEX_APP_SERVER_BINDING_NAMESPACE})`] } : null;
500
+ },
501
+ async migrateLegacyState(params) {
502
+ const changes = [];
503
+ const warnings = [];
504
+ const notices = [];
505
+ const { sources, surfaces } = await collectLegacyBindingSources(params);
506
+ if (sources.length === 0) return {
507
+ changes,
508
+ warnings
509
+ };
510
+ const ownerCollection = await collectBindingOwners(sources, surfaces, params);
511
+ if (ownerCollection.failures.length > 0) {
512
+ warnings.push(`Left ${sources.length} Codex binding sidecar(s) in place because session ownership is indeterminate: ${ownerCollection.failures.join("; ")}`);
513
+ return {
514
+ changes,
515
+ warnings
516
+ };
517
+ }
518
+ const store = params.context.openPluginStateKeyedStore({
519
+ namespace: CODEX_APP_SERVER_BINDING_NAMESPACE,
520
+ maxEntries: CODEX_APP_SERVER_BINDING_MAX_ENTRIES,
521
+ overflowPolicy: "reject-new"
522
+ });
523
+ let migrated = 0;
524
+ let partialImports = 0;
525
+ for (const source of sources) {
526
+ const result = await migrateSource(source, ownerCollection.owners.get(await canonicalizePath(source.transcriptPath)) ?? [], params, store);
527
+ if (result.warning) warnings.push(result.warning);
528
+ if (result.notice) notices.push(result.notice);
529
+ if (result.archived) migrated++;
530
+ else partialImports += result.importedKeys;
531
+ }
532
+ if (migrated > 0) changes.push(`Migrated ${migrated} Codex app-server binding sidecar(s) to plugin state and archived the legacy sources`);
533
+ if (partialImports > 0) changes.push(`Migrated ${partialImports} safe Codex app-server binding row(s) to plugin state; retained legacy sidecars needing review`);
534
+ return notices.length > 0 ? {
535
+ changes,
536
+ warnings,
537
+ notices
538
+ } : {
539
+ changes,
540
+ warnings
541
+ };
542
+ }
543
+ }];
544
+ //#endregion
1
545
  //#region extensions/codex/doctor-contract-api.ts
2
546
  function asRecord(value) {
3
547
  return value && typeof value === "object" && !Array.isArray(value) ? value : null;
@@ -12,36 +556,55 @@ function hasLegacyPluginDestructivePolicy(value) {
12
556
  const plugins = asRecord(codexPlugins.plugins);
13
557
  return Object.values(plugins ?? {}).some((plugin) => asRecord(plugin)?.allow_destructive_actions === "on-request");
14
558
  }
559
+ function hasRetiredOnFailureApprovalPolicy(value) {
560
+ return asRecord(value)?.approvalPolicy === "on-failure";
561
+ }
15
562
  /** Legacy Codex config keys that doctor should report or repair. */
16
- const legacyConfigRules = [{
17
- path: [
18
- "plugins",
19
- "entries",
20
- "codex",
21
- "config"
22
- ],
23
- message: "plugins.entries.codex.config.codexDynamicToolsProfile is retired; Codex app-server always keeps Codex-native workspace tools native. Run \"openclaw doctor --fix\".",
24
- match: hasRetiredDynamicToolsProfile
25
- }, {
26
- path: [
27
- "plugins",
28
- "entries",
29
- "codex",
30
- "config",
31
- "codexPlugins"
32
- ],
33
- message: "plugins.entries.codex.config.codexPlugins.allow_destructive_actions=\"on-request\" was renamed to \"auto\". Run \"openclaw doctor --fix\".",
34
- match: hasLegacyPluginDestructivePolicy
35
- }];
563
+ const legacyConfigRules = [
564
+ {
565
+ path: [
566
+ "plugins",
567
+ "entries",
568
+ "codex",
569
+ "config"
570
+ ],
571
+ message: "plugins.entries.codex.config.codexDynamicToolsProfile is retired; Codex app-server always keeps Codex-native workspace tools native. Run \"openclaw doctor --fix\".",
572
+ match: hasRetiredDynamicToolsProfile
573
+ },
574
+ {
575
+ path: [
576
+ "plugins",
577
+ "entries",
578
+ "codex",
579
+ "config",
580
+ "codexPlugins"
581
+ ],
582
+ message: "plugins.entries.codex.config.codexPlugins.allow_destructive_actions=\"on-request\" was renamed to \"auto\". Run \"openclaw doctor --fix\".",
583
+ match: hasLegacyPluginDestructivePolicy
584
+ },
585
+ {
586
+ path: [
587
+ "plugins",
588
+ "entries",
589
+ "codex",
590
+ "config",
591
+ "appServer"
592
+ ],
593
+ message: "plugins.entries.codex.config.appServer.approvalPolicy=\"on-failure\" was retired by Codex 0.143; use \"on-request\". Run \"openclaw doctor --fix\".",
594
+ match: hasRetiredOnFailureApprovalPolicy
595
+ }
596
+ ];
36
597
  /**
37
598
  * Removes retired Codex plugin config keys while preserving unrelated config.
38
599
  */
39
600
  function normalizeCompatibilityConfig({ cfg }) {
40
601
  const rawPluginConfig = asRecord(asRecord(cfg.plugins?.entries?.codex)?.config);
41
602
  const rawCodexPlugins = asRecord(rawPluginConfig?.codexPlugins);
603
+ const rawAppServer = asRecord(rawPluginConfig?.appServer);
42
604
  const shouldRemoveDynamicToolsProfile = rawPluginConfig !== null && hasRetiredDynamicToolsProfile(rawPluginConfig);
43
605
  const shouldRewriteDestructivePolicy = hasLegacyPluginDestructivePolicy(rawCodexPlugins);
44
- if (!rawPluginConfig || !shouldRemoveDynamicToolsProfile && !shouldRewriteDestructivePolicy) return {
606
+ const shouldRewriteApprovalPolicy = hasRetiredOnFailureApprovalPolicy(rawAppServer);
607
+ if (!rawPluginConfig || !shouldRemoveDynamicToolsProfile && !shouldRewriteDestructivePolicy && !shouldRewriteApprovalPolicy) return {
45
608
  config: cfg,
46
609
  changes: []
47
610
  };
@@ -66,6 +629,11 @@ function normalizeCompatibilityConfig({ cfg }) {
66
629
  }
67
630
  changes.push("Renamed plugins.entries.codex.config.codexPlugins allow_destructive_actions=\"on-request\" values to \"auto\".");
68
631
  }
632
+ if (shouldRewriteApprovalPolicy) {
633
+ const nextAppServer = asRecord(nextPluginConfig.appServer);
634
+ if (nextAppServer?.approvalPolicy === "on-failure") nextAppServer.approvalPolicy = "on-request";
635
+ changes.push("Renamed plugins.entries.codex.config.appServer.approvalPolicy=\"on-failure\" to \"on-request\".");
636
+ }
69
637
  return {
70
638
  config: nextConfig,
71
639
  changes
@@ -89,4 +657,4 @@ const sessionRouteStateOwners = [{
89
657
  ]
90
658
  }];
91
659
  //#endregion
92
- export { legacyConfigRules, normalizeCompatibilityConfig, sessionRouteStateOwners };
660
+ export { legacyConfigRules, normalizeCompatibilityConfig, sessionRouteStateOwners, stateMigrations };