@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.
- package/README.md +27 -3
- package/dist/app-server-policy-Scc-Wevo.js +49 -0
- package/dist/{attempt-notifications-BGsEIIDI.js → attempt-notifications-BFmNhqFl.js} +3 -6
- package/dist/auth-binding-C1czuHPA.js +62 -0
- package/dist/cli-metadata.js +20 -0
- package/dist/{app-server-policy-C968Kgin.js → command-formatters-Dq9InZUK.js} +39 -51
- package/dist/{command-handlers-Dv-pzAmg.js → command-handlers-IgqmL_kv.js} +161 -61
- package/dist/{compact-4mVBriKT.js → compact-DCTmg3id.js} +39 -10
- package/dist/computer-use-CPcU8TjG.js +685 -0
- package/dist/conversation-control-CGkBlfc5.js +286 -0
- package/dist/doctor-contract-api.js +120 -81
- package/dist/{provider-capabilities-CDnHbmUZ.js → dynamic-tools-C_1tEs34.js} +6109 -5725
- package/dist/harness.js +59 -15
- package/dist/index.js +1460 -269
- package/dist/{media-understanding-provider-BHOu9-Gg.js → media-understanding-provider-D31dOJwb.js} +32 -15
- package/dist/media-understanding-provider.js +1 -1
- package/dist/{models-DDmO1zwd.js → models-uh26C8QU.js} +5 -9
- package/dist/notification-correlation-KmfV4EkP.js +67 -0
- package/dist/plugin-app-cache-key-Ceb-lm2c.js +315 -0
- package/dist/{provider-zjPfx5Fs.js → rate-limits-DyXaYAxU.js} +10 -266
- package/dist/request-B_oQsCXy.js +157 -0
- package/dist/{run-attempt-FUyOjGCV.js → run-attempt-MLzoMe_m.js} +7395 -6430
- package/dist/runtime-artifact-C2ITXdhM.js +488 -0
- package/dist/{sandbox-guard-DA2TQfZW.js → sandbox-guard-D1Wky__S.js} +10 -1
- package/dist/{config-CYEDnLJ2.js → session-binding-h1mmCGnl.js} +985 -31
- package/dist/session-catalog-CEvoXWHA.js +2412 -0
- package/dist/session-cli-XsEuWb_C.js +175 -0
- package/dist/{shared-client-4ICy3U6d.js → shared-client-JiAnW6pc.js} +2193 -1319
- package/dist/{side-question-N4OaDer2.js → side-question-DqDvIwSU.js} +140 -70
- package/dist/{thread-lifecycle-qWE88Dn2.js → thread-lifecycle-CN_pPtPh.js} +1826 -1188
- package/dist/{protocol-validators-dZQ-UTOa.js → transcript-mirror-D9rTxx2P.js} +485 -2
- package/dist/transport-stdio-DXgGZ7Ib.js +88 -0
- package/dist/usage-D5Rohxoc.js +25 -0
- package/dist/{web-search-provider.runtime-BSlriav6.js → web-search-provider.runtime-CKVBn3fP.js} +3 -3
- package/npm-shrinkwrap.json +56 -30
- package/openclaw.plugin.json +177 -14
- package/package.json +8 -6
- package/dist/command-rpc-eLM_mtC7.js +0 -784
- package/dist/computer-use-Bmaz333N.js +0 -377
- package/dist/notification-correlation-Bo7KB3ks.js +0 -35
- package/dist/plugin-app-cache-key-BrhVdeEf.js +0 -98
- package/dist/prompt-overlay.js +0 -15
- package/dist/protocol-2POPqAY4.js +0 -13
- package/dist/provider-catalog.js +0 -79
- package/dist/provider-discovery.js +0 -34
- package/dist/provider.js +0 -2
- package/dist/request-BcJyl8KL.js +0 -36
- 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 {
|
|
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
|
|
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
|
|
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 ||
|
|
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
|
|
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 {
|
|
143
|
-
|
|
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 =
|
|
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
|
|
218
|
+
let legacyTranscriptPath;
|
|
219
|
+
let canonicalLegacyTranscriptPath;
|
|
207
220
|
try {
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
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:
|
|
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(
|
|
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(
|
|
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-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
}];
|