@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.
- package/dist/{app-server-policy-BPTiVNsW.js → app-server-policy-C968Kgin.js} +5 -5
- package/dist/{attempt-notifications-BYnndyFl.js → attempt-notifications-BGsEIIDI.js} +29 -40
- package/dist/{command-handlers-DEPTJYk0.js → command-handlers-Dv-pzAmg.js} +278 -237
- package/dist/command-rpc-eLM_mtC7.js +784 -0
- package/dist/compact-4mVBriKT.js +603 -0
- package/dist/{computer-use-ClrJwoFp.js → computer-use-Bmaz333N.js} +3 -3
- package/dist/{config-CszD0vP3.js → config-CYEDnLJ2.js} +44 -18
- package/dist/conversation-binding-data-BHRpYbX3.js +121 -0
- package/dist/doctor-contract-api.js +590 -22
- package/dist/harness.js +29 -10
- package/dist/index.js +1134 -35
- package/dist/{media-understanding-provider-ZUQHN7gT.js → media-understanding-provider-BHOu9-Gg.js} +13 -8
- package/dist/media-understanding-provider.js +1 -1
- package/dist/{models-CAmfBWRl.js → models-DDmO1zwd.js} +2 -2
- package/dist/notification-correlation-Bo7KB3ks.js +35 -0
- package/dist/{plugin-app-cache-key-C9ILt7bl.js → plugin-app-cache-key-BrhVdeEf.js} +13 -5
- package/dist/{protocol-dh-ETiNd.js → protocol-2POPqAY4.js} +2 -1
- package/dist/{protocol-validators-B19q5BIX.js → protocol-validators-dZQ-UTOa.js} +205 -56
- package/dist/provider-capabilities-DzI8fgEu.js +6172 -0
- package/dist/provider-catalog.js +4 -2
- package/dist/{provider-B-OHpbD3.js → provider-zjPfx5Fs.js} +94 -23
- package/dist/provider.js +2 -2
- package/dist/{request-Dcd452Nk.js → request-BcJyl8KL.js} +2 -2
- package/dist/{run-attempt-CXxtFC9d.js → run-attempt-DNSNk_G8.js} +1250 -2992
- package/dist/{sandbox-guard-3tnjhjFb.js → sandbox-guard-DA2TQfZW.js} +5 -3
- package/dist/session-binding-C1ZXdP-x.js +627 -0
- package/dist/session-binding-meta-B7aEMU7g.js +6 -0
- package/dist/shared-client-4ICy3U6d.js +1853 -0
- package/dist/{side-question-C-DhgJOd.js → side-question-CMOpEhvY.js} +134 -142
- package/dist/{thread-lifecycle-DtT3-ehU.js → thread-lifecycle-qWE88Dn2.js} +803 -481
- package/dist/{web-search-provider.runtime-CkwxrXQj.js → web-search-provider.runtime-BSlriav6.js} +3 -3
- package/npm-shrinkwrap.json +34 -34
- package/openclaw.plugin.json +23 -3
- package/package.json +9 -6
- package/dist/client-CLrtWgrD.js +0 -729
- package/dist/client-factory-Bm6HsGob.js +0 -18
- package/dist/compact-aQJ0ZKhe.js +0 -278
- package/dist/conversation-binding-t0XMLz-j.js +0 -1557
- package/dist/provider-capabilities-D7hnTO76.js +0 -3201
- package/dist/rate-limit-cache-C7qmZ0Jh.js +0 -26
- package/dist/session-binding-HOuPt-E0.js +0 -338
- 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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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 };
|