@openclaw/codex 2026.7.1-beta.6 → 2026.7.2-beta.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 (46) hide show
  1. package/README.md +27 -3
  2. package/dist/app-server-policy-DVcxdse_.js +49 -0
  3. package/dist/{attempt-notifications-BGsEIIDI.js → attempt-notifications-DEv9h7iC.js} +1 -1
  4. package/dist/auth-binding-C1czuHPA.js +62 -0
  5. package/dist/capabilities-42Dfn2TV.js +33 -0
  6. package/dist/cli-metadata.js +20 -0
  7. package/dist/{app-server-policy-C968Kgin.js → command-formatters-D6ZlGNmb.js} +8 -51
  8. package/dist/{command-handlers-Dv-pzAmg.js → command-handlers-Cpl9fUWv.js} +163 -60
  9. package/dist/{compact-4mVBriKT.js → compact-FnJmiPNA.js} +40 -10
  10. package/dist/computer-use-DVzr8OP1.js +685 -0
  11. package/dist/{config-CYEDnLJ2.js → config-c48K5HP9.js} +200 -29
  12. package/dist/conversation-control-DtQ-E7mr.js +288 -0
  13. package/dist/doctor-contract-api.js +120 -81
  14. package/dist/{provider-capabilities-CDnHbmUZ.js → dynamic-tools-BMLoaTeG.js} +5246 -5140
  15. package/dist/harness.js +55 -15
  16. package/dist/index.js +1435 -265
  17. package/dist/{media-understanding-provider-BHOu9-Gg.js → media-understanding-provider-D580L0P8.js} +28 -10
  18. package/dist/media-understanding-provider.js +1 -1
  19. package/dist/{models-DDmO1zwd.js → models-DuKzZA6G.js} +1 -1
  20. package/dist/notification-correlation-DfaCm0mx.js +67 -0
  21. package/dist/plugin-app-cache-key-BKNjHMEs.js +315 -0
  22. package/dist/{protocol-2POPqAY4.js → protocol-BMifTfdW.js} +3 -1
  23. package/dist/provider-catalog.js +66 -27
  24. package/dist/provider.js +306 -1
  25. package/dist/{provider-zjPfx5Fs.js → rate-limits-Dhp04Rqo.js} +4 -261
  26. package/dist/request-DborTWgw.js +111 -0
  27. package/dist/{run-attempt-FUyOjGCV.js → run-attempt-VPVJoYDP.js} +7897 -7136
  28. package/dist/runtime-artifact-C2ITXdhM.js +488 -0
  29. package/dist/{sandbox-guard-DA2TQfZW.js → sandbox-guard-D1Wky__S.js} +10 -1
  30. package/dist/{session-binding-C1ZXdP-x.js → session-binding-BMfX1OX-.js} +247 -88
  31. package/dist/session-catalog-7H112Tr_.js +2396 -0
  32. package/dist/session-cli-B28RhCyp.js +175 -0
  33. package/dist/{shared-client-4ICy3U6d.js → shared-client-D4mFI9al.js} +1997 -1297
  34. package/dist/{side-question-N4OaDer2.js → side-question-CgJBz52s.js} +135 -67
  35. package/dist/{thread-lifecycle-qWE88Dn2.js → thread-lifecycle-BgLXzjvV.js} +747 -317
  36. package/dist/transcript-mirror-DhLwFIL4.js +485 -0
  37. package/dist/transport-stdio-DXgGZ7Ib.js +88 -0
  38. package/dist/{web-search-provider.runtime-BSlriav6.js → web-search-provider.runtime-Bs-eTn5U.js} +3 -3
  39. package/npm-shrinkwrap.json +56 -30
  40. package/openclaw.plugin.json +168 -10
  41. package/package.json +8 -6
  42. package/dist/command-rpc-eLM_mtC7.js +0 -784
  43. package/dist/computer-use-Bmaz333N.js +0 -377
  44. package/dist/notification-correlation-Bo7KB3ks.js +0 -35
  45. package/dist/plugin-app-cache-key-BrhVdeEf.js +0 -98
  46. package/dist/request-BcJyl8KL.js +0 -36
@@ -0,0 +1,2396 @@
1
+ import { i as isJsonObject, t as CODEX_INTERACTIVE_THREAD_SOURCE_KINDS } from "./protocol-BMifTfdW.js";
2
+ import { f as resolveCodexAppServerRuntimeOptions, v as resolveCodexSupervisionAppServerRuntimeOptions } from "./config-c48K5HP9.js";
3
+ import { n as listCodexAppServerModels } from "./models-DuKzZA6G.js";
4
+ import { t as buildCodexAppServerConnectionFingerprint } from "./plugin-app-cache-key-BKNjHMEs.js";
5
+ import { O as CodexAppServerRpcError, a as getLeasedSharedCodexAppServerClient, c as releaseLeasedSharedCodexAppServerClient } from "./shared-client-D4mFI9al.js";
6
+ import { c as sessionBindingIdentity, s as reclaimCurrentCodexSessionGeneration } from "./session-binding-BMfX1OX-.js";
7
+ import { r as importCodexThreadHistoryToTranscript } from "./transcript-mirror-DhLwFIL4.js";
8
+ import { n as describeControlFailure, t as CODEX_CONTROL_METHODS } from "./capabilities-42Dfn2TV.js";
9
+ import { r as formatCodexDisplayText } from "./command-formatters-D6ZlGNmb.js";
10
+ import { n as createCodexCliNodeConversationBindingData } from "./conversation-binding-data-BHRpYbX3.js";
11
+ import { n as requestCodexAppServerJson, t as requestCodexAppServerClientJson } from "./request-DborTWgw.js";
12
+ import fs from "node:fs/promises";
13
+ import path from "node:path";
14
+ import { timestampMsToIsoString } from "openclaw/plugin-sdk/number-runtime";
15
+ import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/temp-path";
16
+ import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
17
+ import { createHash } from "node:crypto";
18
+ import os from "node:os";
19
+ import { listAgentIds, resolveDefaultAgentDir, resolveDefaultAgentId } from "openclaw/plugin-sdk/agent-runtime";
20
+ import { parseAgentSessionKey } from "openclaw/plugin-sdk/routing";
21
+ import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
22
+ import { materializeWindowsSpawnProgram, resolveWindowsSpawnProgram } from "openclaw/plugin-sdk/windows-spawn";
23
+ import { spawn } from "node:child_process";
24
+ import { resolveStorePath } from "openclaw/plugin-sdk/session-store-runtime";
25
+ import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
26
+ import process$1 from "node:process";
27
+ import { decodeNodePtyResumeParams, resolveExecutableFromPathEnv, runNodePtyCommand } from "openclaw/plugin-sdk/node-host";
28
+ //#region extensions/codex/src/node-cli-sessions.ts
29
+ const CODEX_CLI_SESSIONS_LIST_COMMAND = "codex.cli.sessions.list";
30
+ const CODEX_CLI_SESSION_RESUME_COMMAND = "codex.cli.session.resume";
31
+ const DEFAULT_SESSION_LIMIT = 10;
32
+ const MAX_SESSION_LIMIT = 50;
33
+ const DEFAULT_RESUME_TIMEOUT_MS = 20 * 6e4;
34
+ const SESSION_ID_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/;
35
+ const activeResumeSessions = /* @__PURE__ */ new Set();
36
+ const DEFAULT_RESUME_SPAWN_RUNTIME = {
37
+ platform: process$1.platform,
38
+ env: process$1.env,
39
+ execPath: process$1.execPath
40
+ };
41
+ function createCodexCliSessionNodeHostCommands() {
42
+ return [{
43
+ command: CODEX_CLI_SESSIONS_LIST_COMMAND,
44
+ cap: "codex-cli-sessions",
45
+ handle: listLocalCodexCliSessions
46
+ }, {
47
+ command: CODEX_CLI_SESSION_RESUME_COMMAND,
48
+ cap: "codex-cli-sessions",
49
+ dangerous: true,
50
+ handle: resumeLocalCodexCliSession
51
+ }];
52
+ }
53
+ function createCodexCliSessionNodeInvokePolicies() {
54
+ return [{
55
+ commands: [CODEX_CLI_SESSIONS_LIST_COMMAND],
56
+ defaultPlatforms: [
57
+ "macos",
58
+ "linux",
59
+ "windows"
60
+ ],
61
+ handle: (ctx) => ctx.invokeNode()
62
+ }, {
63
+ commands: [CODEX_CLI_SESSION_RESUME_COMMAND],
64
+ dangerous: true,
65
+ handle: (ctx) => ctx.invokeNode()
66
+ }];
67
+ }
68
+ async function listCodexCliSessionsOnNode(params) {
69
+ const node = await resolveCodexCliNode({
70
+ runtime: params.runtime,
71
+ requestedNode: params.requestedNode,
72
+ command: CODEX_CLI_SESSIONS_LIST_COMMAND
73
+ });
74
+ return {
75
+ node,
76
+ result: parseCodexCliSessionsListResult(await params.runtime.nodes.invoke({
77
+ nodeId: readNodeId(node),
78
+ command: CODEX_CLI_SESSIONS_LIST_COMMAND,
79
+ params: {
80
+ limit: params.limit,
81
+ filter: params.filter
82
+ },
83
+ timeoutMs: 15e3,
84
+ scopes: ["operator.write"]
85
+ }))
86
+ };
87
+ }
88
+ async function resolveCodexCliSessionForBindingOnNode(params) {
89
+ const listing = await listCodexCliSessionsOnNode({
90
+ runtime: params.runtime,
91
+ requestedNode: params.requestedNode,
92
+ filter: params.sessionId,
93
+ limit: MAX_SESSION_LIMIT
94
+ });
95
+ if (!listing.node.commands?.includes("codex.cli.session.resume")) throw new Error(`Node ${formatNodeLabel(listing.node)} does not expose ${CODEX_CLI_SESSION_RESUME_COMMAND}.`);
96
+ return {
97
+ node: listing.node,
98
+ session: listing.result.sessions.find((session) => session.sessionId === params.sessionId)
99
+ };
100
+ }
101
+ async function resumeCodexCliSessionOnNode(params) {
102
+ const payload = unwrapNodeInvokePayload$1(await params.runtime.nodes.invoke({
103
+ nodeId: params.nodeId,
104
+ command: CODEX_CLI_SESSION_RESUME_COMMAND,
105
+ params: {
106
+ sessionId: params.sessionId,
107
+ prompt: params.prompt,
108
+ cwd: params.cwd,
109
+ timeoutMs: params.timeoutMs
110
+ },
111
+ timeoutMs: (params.timeoutMs ?? DEFAULT_RESUME_TIMEOUT_MS) + 5e3,
112
+ scopes: ["operator.write"]
113
+ }));
114
+ if (!isRecord(payload) || payload.ok !== true || typeof payload.text !== "string") throw new Error("Codex CLI resume returned an invalid payload.");
115
+ return {
116
+ ok: true,
117
+ sessionId: typeof payload.sessionId === "string" ? payload.sessionId : params.sessionId,
118
+ text: payload.text
119
+ };
120
+ }
121
+ function formatCodexCliSessions(params) {
122
+ if (params.result.sessions.length === 0) return `No Codex CLI sessions returned from ${formatCodexDisplayText(formatNodeLabel(params.node))}.`;
123
+ return [`Codex CLI sessions on ${formatCodexDisplayText(formatNodeLabel(params.node))}:`, ...params.result.sessions.map((session) => {
124
+ const details = [session.cwd, session.updatedAt].filter((value) => Boolean(value));
125
+ return `- ${formatCodexDisplayText(session.sessionId)}${session.lastMessage ? ` - ${formatCodexDisplayText(session.lastMessage)}` : ""}${details.length > 0 ? ` (${details.map(formatCodexDisplayText).join(", ")})` : ""}\n Bind: /codex resume ${formatCodexDisplayText(session.sessionId)} --host ${formatCodexDisplayText(readNodeId(params.node))} --bind here`;
126
+ })].join("\n");
127
+ }
128
+ async function listLocalCodexCliSessions(paramsJSON) {
129
+ const params = readRecordParam(paramsJSON);
130
+ const limit = normalizeLimit$1(params.limit);
131
+ const filter = typeof params.filter === "string" ? params.filter.trim().toLowerCase() : "";
132
+ const codexHome = resolveCodexHome();
133
+ const summaries = await readHistorySessions(codexHome);
134
+ await hydrateSessionFiles(codexHome, summaries);
135
+ await hydrateSessionsFromSessionFiles(codexHome, summaries);
136
+ const sessions = [...summaries.values()].filter((session) => {
137
+ if (!filter) return true;
138
+ return [
139
+ session.sessionId,
140
+ session.cwd,
141
+ session.lastMessage
142
+ ].some((value) => value?.toLowerCase().includes(filter));
143
+ }).toSorted((a, b) => compareOptionalStringsDesc(a.updatedAt, b.updatedAt)).slice(0, limit);
144
+ return JSON.stringify({
145
+ sessions,
146
+ codexHome
147
+ });
148
+ }
149
+ async function resumeLocalCodexCliSession(paramsJSON) {
150
+ const params = readRecordParam(paramsJSON);
151
+ const sessionId = typeof params.sessionId === "string" ? params.sessionId.trim() : "";
152
+ const prompt = typeof params.prompt === "string" ? params.prompt.trim() : "";
153
+ if (!sessionId || !SESSION_ID_PATTERN.test(sessionId)) throw new Error("Missing or invalid Codex CLI session id.");
154
+ if (!prompt) throw new Error("Missing Codex CLI prompt.");
155
+ if (activeResumeSessions.has(sessionId)) throw new Error(`Codex CLI session ${sessionId} already has an active resume turn.`);
156
+ activeResumeSessions.add(sessionId);
157
+ try {
158
+ const text = await runCodexExecResume({
159
+ sessionId,
160
+ prompt,
161
+ cwd: typeof params.cwd === "string" && params.cwd.trim() ? params.cwd.trim() : void 0,
162
+ timeoutMs: normalizeTimeoutMs(params.timeoutMs)
163
+ });
164
+ return JSON.stringify({
165
+ ok: true,
166
+ sessionId,
167
+ text: text.trim() || "Codex completed without a text reply."
168
+ });
169
+ } finally {
170
+ activeResumeSessions.delete(sessionId);
171
+ }
172
+ }
173
+ async function runCodexExecResume(params) {
174
+ const outputPath = path.join(await fs.mkdtemp(path.join(resolvePreferredOpenClawTmpDir(), "openclaw-codex-cli-")), "last-message.txt");
175
+ try {
176
+ const invocation = resolveCodexCliResumeSpawnInvocation([
177
+ "exec",
178
+ "resume",
179
+ "--skip-git-repo-check",
180
+ "--output-last-message",
181
+ outputPath,
182
+ params.sessionId,
183
+ "-"
184
+ ], {
185
+ platform: process$1.platform,
186
+ env: process$1.env,
187
+ execPath: process$1.execPath
188
+ });
189
+ const child = spawn(invocation.command, invocation.args, {
190
+ cwd: params.cwd || process$1.cwd(),
191
+ stdio: [
192
+ "pipe",
193
+ "pipe",
194
+ "pipe"
195
+ ],
196
+ env: process$1.env,
197
+ shell: invocation.shell,
198
+ windowsHide: invocation.windowsHide
199
+ });
200
+ const stdout = [];
201
+ const stderr = [];
202
+ let timedOut = false;
203
+ let forceKillTimeout;
204
+ const timeout = setTimeout(() => {
205
+ timedOut = true;
206
+ child.kill("SIGTERM");
207
+ forceKillTimeout = setTimeout(() => child.kill("SIGKILL"), 2e3);
208
+ forceKillTimeout.unref?.();
209
+ }, params.timeoutMs);
210
+ child.stdout.on("data", (chunk) => stdout.push(chunk));
211
+ child.stderr.on("data", (chunk) => stderr.push(chunk));
212
+ child.stdin.end(params.prompt);
213
+ const exitCode = await new Promise((resolve, reject) => {
214
+ child.on("error", reject);
215
+ child.on("exit", (code) => resolve(code));
216
+ }).finally(() => {
217
+ clearTimeout(timeout);
218
+ if (forceKillTimeout) clearTimeout(forceKillTimeout);
219
+ });
220
+ if (timedOut) throw new Error(`codex exec resume timed out after ${String(params.timeoutMs)}ms`);
221
+ if (exitCode !== 0) {
222
+ const message = Buffer.concat(stderr).toString("utf8").trim() || Buffer.concat(stdout).toString("utf8").trim() || `codex exec resume exited with code ${String(exitCode)}`;
223
+ throw new Error(message);
224
+ }
225
+ return await fs.readFile(outputPath, "utf8");
226
+ } finally {
227
+ await fs.rm(path.dirname(outputPath), {
228
+ recursive: true,
229
+ force: true
230
+ });
231
+ }
232
+ }
233
+ function resolveCodexCliResumeSpawnInvocation(args, runtime = DEFAULT_RESUME_SPAWN_RUNTIME) {
234
+ const resolved = materializeWindowsSpawnProgram(resolveWindowsSpawnProgram({
235
+ command: "codex",
236
+ platform: runtime.platform,
237
+ env: runtime.env,
238
+ execPath: runtime.execPath,
239
+ packageName: "@openai/codex"
240
+ }), args);
241
+ return {
242
+ command: resolved.command,
243
+ args: resolved.argv,
244
+ shell: resolved.shell,
245
+ windowsHide: resolved.windowsHide
246
+ };
247
+ }
248
+ async function readHistorySessions(codexHome) {
249
+ const summaries = /* @__PURE__ */ new Map();
250
+ const content = await readFileIfExists(path.join(codexHome, "history.jsonl"));
251
+ if (!content) return summaries;
252
+ for (const line of content.split(/\r?\n/u)) {
253
+ const trimmed = line.trim();
254
+ if (!trimmed) continue;
255
+ let parsed;
256
+ try {
257
+ parsed = JSON.parse(trimmed);
258
+ } catch {
259
+ continue;
260
+ }
261
+ if (!isRecord(parsed) || typeof parsed.session_id !== "string") continue;
262
+ const sessionId = parsed.session_id.trim();
263
+ if (!sessionId) continue;
264
+ const entry = summaries.get(sessionId) ?? {
265
+ sessionId,
266
+ messageCount: 0
267
+ };
268
+ entry.messageCount += 1;
269
+ if (typeof parsed.text === "string" && parsed.text.trim()) entry.lastMessage = truncateText(parsed.text.trim(), 140);
270
+ if (typeof parsed.ts === "number") entry.updatedAt = timestampMsToIsoString(parsed.ts * 1e3) ?? entry.updatedAt;
271
+ summaries.set(sessionId, entry);
272
+ }
273
+ return summaries;
274
+ }
275
+ async function hydrateSessionFiles(codexHome, summaries) {
276
+ if (summaries.size === 0) return;
277
+ const files = await findSessionFiles(path.join(codexHome, "sessions"), 4);
278
+ const pending = new Set(summaries.keys());
279
+ for (const file of files) {
280
+ const basename = path.basename(file);
281
+ const sessionId = [...pending].find((id) => basename.includes(id));
282
+ if (!sessionId) continue;
283
+ const entry = summaries.get(sessionId);
284
+ if (!entry) continue;
285
+ entry.sessionFile = file;
286
+ const cwd = readSessionMetaCwd(await readFirstLine(file) ?? "");
287
+ if (cwd) entry.cwd = cwd;
288
+ pending.delete(sessionId);
289
+ if (pending.size === 0) return;
290
+ }
291
+ }
292
+ async function hydrateSessionsFromSessionFiles(codexHome, summaries) {
293
+ const files = await findSessionFiles(path.join(codexHome, "sessions"), 4);
294
+ for (const file of files) {
295
+ const summary = await readSessionFileSummary(file);
296
+ if (!summary) continue;
297
+ const existing = summaries.get(summary.sessionId);
298
+ summaries.set(summary.sessionId, {
299
+ ...summary,
300
+ ...existing,
301
+ cwd: existing?.cwd ?? summary.cwd,
302
+ sessionFile: existing?.sessionFile ?? summary.sessionFile,
303
+ updatedAt: existing?.updatedAt ?? summary.updatedAt,
304
+ lastMessage: existing?.lastMessage ?? summary.lastMessage,
305
+ messageCount: existing?.messageCount ?? summary.messageCount
306
+ });
307
+ }
308
+ }
309
+ async function readSessionFileSummary(file) {
310
+ const content = await readFileIfExists(file);
311
+ if (!content) return null;
312
+ let sessionId = "";
313
+ let cwd;
314
+ let updatedAt;
315
+ let lastMessage;
316
+ let messageCount = 0;
317
+ for (const line of content.split(/\r?\n/u)) {
318
+ const trimmed = line.trim();
319
+ if (!trimmed) continue;
320
+ let parsed;
321
+ try {
322
+ parsed = JSON.parse(trimmed);
323
+ } catch {
324
+ continue;
325
+ }
326
+ if (!isRecord(parsed)) continue;
327
+ if (typeof parsed.timestamp === "string" && parsed.timestamp.trim()) updatedAt = parsed.timestamp.trim();
328
+ if (parsed.type === "session_meta" && isRecord(parsed.payload)) {
329
+ if (typeof parsed.payload.id === "string" && parsed.payload.id.trim()) sessionId = parsed.payload.id.trim();
330
+ if (typeof parsed.payload.cwd === "string" && parsed.payload.cwd.trim()) cwd = parsed.payload.cwd.trim();
331
+ continue;
332
+ }
333
+ const messageText = readResponseItemMessageText(parsed);
334
+ if (messageText) {
335
+ messageCount += 1;
336
+ lastMessage = truncateText(messageText, 140);
337
+ }
338
+ }
339
+ if (!sessionId) sessionId = readSessionIdFromFilename(file) ?? "";
340
+ if (!sessionId) return null;
341
+ return {
342
+ sessionId,
343
+ updatedAt: updatedAt ?? await readFileMtimeIso(file),
344
+ lastMessage,
345
+ cwd,
346
+ sessionFile: file,
347
+ messageCount
348
+ };
349
+ }
350
+ async function findSessionFiles(dir, maxDepth) {
351
+ if (maxDepth < 0) return [];
352
+ let entries;
353
+ try {
354
+ entries = await fs.readdir(dir, { withFileTypes: true });
355
+ } catch {
356
+ return [];
357
+ }
358
+ const files = [];
359
+ for (const entry of entries) {
360
+ const entryPath = path.join(dir, entry.name);
361
+ if (entry.isDirectory()) files.push(...await findSessionFiles(entryPath, maxDepth - 1));
362
+ else if (entry.isFile() && entry.name.endsWith(".jsonl")) files.push(entryPath);
363
+ }
364
+ return files;
365
+ }
366
+ function readSessionMetaCwd(line) {
367
+ try {
368
+ const parsed = JSON.parse(line);
369
+ if (!isRecord(parsed) || parsed.type !== "session_meta" || !isRecord(parsed.payload)) return;
370
+ return typeof parsed.payload.cwd === "string" && parsed.payload.cwd.trim() ? parsed.payload.cwd.trim() : void 0;
371
+ } catch {
372
+ return;
373
+ }
374
+ }
375
+ function readResponseItemMessageText(parsed) {
376
+ if (parsed.type !== "response_item" || !isRecord(parsed.payload)) return;
377
+ if (parsed.payload.type !== "message") return;
378
+ if ((typeof parsed.payload.role === "string" ? parsed.payload.role : "") !== "user") return;
379
+ const parts = (Array.isArray(parsed.payload.content) ? parsed.payload.content : []).flatMap((entry) => {
380
+ if (!isRecord(entry)) return [];
381
+ const text = typeof entry.text === "string" ? entry.text : typeof entry.input_text === "string" ? entry.input_text : void 0;
382
+ return text?.trim() ? [text.trim()] : [];
383
+ });
384
+ return parts.length > 0 ? parts.join(" ") : void 0;
385
+ }
386
+ function readSessionIdFromFilename(file) {
387
+ return path.basename(file).match(/[0-9a-f]{8}-[0-9a-f-]{27,}/iu)?.[0];
388
+ }
389
+ async function resolveCodexCliNode(params) {
390
+ const list = await params.runtime.nodes.list(params.requestedNode ? void 0 : { connected: true });
391
+ const requested = params.requestedNode?.trim();
392
+ const candidates = list.nodes.filter((node) => {
393
+ if (requested) return [
394
+ node.nodeId,
395
+ node.displayName,
396
+ node.remoteIp
397
+ ].some((value) => value === requested);
398
+ return node.connected === true && node.commands?.includes(params.command);
399
+ });
400
+ if (candidates.length === 0) throw new Error(requested ? `Codex CLI node ${requested} was not found.` : "No connected node exposes Codex CLI session commands.");
401
+ const usable = candidates.filter((node) => node.commands?.includes(params.command));
402
+ if (usable.length === 0) throw new Error(`Node ${requested ?? "candidate"} does not expose ${params.command}.`);
403
+ if (usable.length > 1) throw new Error("Multiple Codex CLI-capable nodes connected. Pass --host <node-id>.");
404
+ return expectDefined(usable[0], "single usable Codex CLI node");
405
+ }
406
+ function parseCodexCliSessionsListResult(raw) {
407
+ const payload = unwrapNodeInvokePayload$1(raw);
408
+ if (!isRecord(payload) || !Array.isArray(payload.sessions)) throw new Error("Codex CLI session list returned an invalid payload.");
409
+ return {
410
+ codexHome: typeof payload.codexHome === "string" ? payload.codexHome : "",
411
+ sessions: payload.sessions.flatMap((entry) => {
412
+ if (!isRecord(entry) || typeof entry.sessionId !== "string") return [];
413
+ return [{
414
+ sessionId: entry.sessionId,
415
+ updatedAt: typeof entry.updatedAt === "string" ? entry.updatedAt : void 0,
416
+ lastMessage: typeof entry.lastMessage === "string" ? entry.lastMessage : void 0,
417
+ cwd: typeof entry.cwd === "string" ? entry.cwd : void 0,
418
+ sessionFile: typeof entry.sessionFile === "string" ? entry.sessionFile : void 0,
419
+ messageCount: typeof entry.messageCount === "number" && Number.isFinite(entry.messageCount) ? entry.messageCount : 0
420
+ }];
421
+ })
422
+ };
423
+ }
424
+ function unwrapNodeInvokePayload$1(raw) {
425
+ const record = isRecord(raw) ? raw : {};
426
+ if (typeof record.payloadJSON === "string" && record.payloadJSON.trim()) try {
427
+ return JSON.parse(record.payloadJSON);
428
+ } catch (error) {
429
+ throw new Error("Codex CLI node command returned malformed payloadJSON.", { cause: error });
430
+ }
431
+ if ("payload" in record) return record.payload;
432
+ return raw;
433
+ }
434
+ function readRecordParam(paramsJSON) {
435
+ if (!paramsJSON?.trim()) return {};
436
+ try {
437
+ const parsed = JSON.parse(paramsJSON);
438
+ return isRecord(parsed) ? parsed : {};
439
+ } catch {
440
+ return {};
441
+ }
442
+ }
443
+ function resolveCodexHome() {
444
+ return process$1.env.CODEX_HOME?.trim() || path.join(os.homedir(), ".codex");
445
+ }
446
+ async function readFileIfExists(file) {
447
+ try {
448
+ return await fs.readFile(file, "utf8");
449
+ } catch {
450
+ return;
451
+ }
452
+ }
453
+ async function readFirstLine(file) {
454
+ return (await readFileIfExists(file))?.split(/\r?\n/u)[0];
455
+ }
456
+ async function readFileMtimeIso(file) {
457
+ try {
458
+ return (await fs.stat(file)).mtime.toISOString();
459
+ } catch {
460
+ return;
461
+ }
462
+ }
463
+ function normalizeLimit$1(value) {
464
+ return typeof value === "number" && Number.isFinite(value) ? Math.min(MAX_SESSION_LIMIT, Math.max(1, Math.floor(value))) : DEFAULT_SESSION_LIMIT;
465
+ }
466
+ function normalizeTimeoutMs(value) {
467
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.min(60 * 6e4, Math.floor(value)) : DEFAULT_RESUME_TIMEOUT_MS;
468
+ }
469
+ function truncateText(value, max) {
470
+ if (value.length <= max) return value;
471
+ return `${truncateUtf16Safe(value, Math.max(0, max - 3))}...`;
472
+ }
473
+ function compareOptionalStringsDesc(a, b) {
474
+ return (b ?? "").localeCompare(a ?? "");
475
+ }
476
+ function readNodeId(node) {
477
+ if (!node.nodeId) throw new Error("Codex CLI node did not include a node id.");
478
+ return node.nodeId;
479
+ }
480
+ function formatNodeLabel(node) {
481
+ return [
482
+ node.displayName,
483
+ node.nodeId,
484
+ node.remoteIp
485
+ ].filter(Boolean).join(" / ") || "node";
486
+ }
487
+ //#endregion
488
+ //#region extensions/codex/src/app-server/thread-archive-guard.ts
489
+ const DESCENDANT_PAGE_LIMIT = 100;
490
+ const MAX_DESCENDANT_PAGES = 100;
491
+ const MAX_THREAD_ID_LENGTH = 256;
492
+ const MAX_CURSOR_LENGTH$1 = 4096;
493
+ function readBoundedId(value) {
494
+ if (typeof value !== "string") return;
495
+ const normalized = value.trim();
496
+ return normalized && normalized.length <= MAX_THREAD_ID_LENGTH ? normalized : void 0;
497
+ }
498
+ function readNextCursor(value) {
499
+ if (value === void 0 || value === null) return;
500
+ if (typeof value !== "string" || !value.trim() || value.length > MAX_CURSOR_LENGTH$1) throw new Error("Codex app-server returned an invalid descendant-list cursor");
501
+ return value;
502
+ }
503
+ /**
504
+ * Native archive includes the spawned subtree. Enumerate that same subtree first so an
505
+ * OpenClaw-owned descendant cannot be stopped as an undocumented side effect.
506
+ */
507
+ async function assertCodexArchiveDescendantsUnowned(params) {
508
+ const ancestorThreadId = readBoundedId(params.threadId);
509
+ if (!ancestorThreadId) throw new Error("cannot verify Codex archive descendants for an invalid thread id");
510
+ const seenCursors = /* @__PURE__ */ new Set();
511
+ const seenThreadIds = /* @__PURE__ */ new Set([ancestorThreadId]);
512
+ let cursor;
513
+ for (let pageIndex = 0; pageIndex < MAX_DESCENDANT_PAGES; pageIndex += 1) {
514
+ const response = await params.listPage({
515
+ ancestorThreadId,
516
+ archived: false,
517
+ limit: DESCENDANT_PAGE_LIMIT,
518
+ sortKey: "created_at",
519
+ sortDirection: "desc",
520
+ useStateDbOnly: true,
521
+ ...cursor ? { cursor } : {}
522
+ });
523
+ if (!isJsonObject(response) || !Array.isArray(response.data)) throw new Error("Codex app-server returned an invalid descendant-list response");
524
+ if (response.data.length > DESCENDANT_PAGE_LIMIT) throw new Error("Codex app-server exceeded the descendant-list page limit");
525
+ for (const value of response.data) {
526
+ if (!isJsonObject(value)) throw new Error("Codex app-server returned an invalid descendant thread");
527
+ const descendantThreadId = readBoundedId(value.id);
528
+ if (!descendantThreadId) throw new Error("Codex app-server returned a descendant without a valid thread id");
529
+ if (seenThreadIds.has(descendantThreadId)) throw new Error("Codex app-server returned a cyclic descendant thread list");
530
+ seenThreadIds.add(descendantThreadId);
531
+ await params.assertDescendantIdle(descendantThreadId);
532
+ if (await params.bindingStore.hasOtherThreadOwner(descendantThreadId)) throw new Error("cannot archive a Codex thread while a spawned descendant is owned by an OpenClaw session");
533
+ }
534
+ const nextCursor = readNextCursor(response.nextCursor);
535
+ if (!nextCursor) return;
536
+ if (seenCursors.has(nextCursor)) throw new Error("Codex app-server returned a repeated descendant-list cursor");
537
+ seenCursors.add(nextCursor);
538
+ cursor = nextCursor;
539
+ }
540
+ throw new Error("Codex descendant enumeration exceeded its safety limit");
541
+ }
542
+ //#endregion
543
+ //#region extensions/codex/src/command-rpc.ts
544
+ function requestOptions(pluginConfig, limit, config, agentDir) {
545
+ const runtime = resolveCodexAppServerRuntimeOptions({ pluginConfig });
546
+ return {
547
+ limit,
548
+ timeoutMs: runtime.requestTimeoutMs,
549
+ startOptions: runtime.start,
550
+ config,
551
+ agentDir
552
+ };
553
+ }
554
+ async function codexControlRequest(pluginConfig, method, requestParams, options = {}) {
555
+ const runtime = resolveCodexAppServerRuntimeOptions({ pluginConfig });
556
+ return await requestCodexAppServerJson({
557
+ method,
558
+ requestParams,
559
+ timeoutMs: options.timeoutMs ?? runtime.requestTimeoutMs,
560
+ startOptions: options.startOptions ?? runtime.start,
561
+ config: options.config,
562
+ sessionKey: options.sessionKey,
563
+ sessionId: options.sessionId,
564
+ authProfileId: options.authProfileId,
565
+ agentDir: options.agentDir,
566
+ isolated: options.isolated
567
+ });
568
+ }
569
+ async function safeCodexControlRequest(pluginConfig, method, requestParams, options = {}) {
570
+ return await safeValue(async () => await codexControlRequest(pluginConfig, method, requestParams, options));
571
+ }
572
+ async function safeCodexModelList(pluginConfig, limit, config, agentDir) {
573
+ return await safeValue(async () => await listCodexAppServerModels(requestOptions(pluginConfig, limit, config, agentDir)));
574
+ }
575
+ async function readCodexStatusProbes(pluginConfig, config, agentDir) {
576
+ const [models, account, limits, mcps, skills] = await Promise.all([
577
+ safeCodexModelList(pluginConfig, 20, config, agentDir),
578
+ safeCodexControlRequest(pluginConfig, CODEX_CONTROL_METHODS.account, { refreshToken: false }, {
579
+ config,
580
+ agentDir
581
+ }),
582
+ safeCodexControlRequest(pluginConfig, CODEX_CONTROL_METHODS.rateLimits, void 0, {
583
+ config,
584
+ agentDir
585
+ }),
586
+ safeCodexControlRequest(pluginConfig, CODEX_CONTROL_METHODS.listMcpServers, { limit: 100 }, {
587
+ config,
588
+ agentDir
589
+ }),
590
+ safeCodexControlRequest(pluginConfig, CODEX_CONTROL_METHODS.listSkills, {}, {
591
+ config,
592
+ agentDir
593
+ })
594
+ ]);
595
+ return {
596
+ models,
597
+ account,
598
+ limits,
599
+ mcps,
600
+ skills
601
+ };
602
+ }
603
+ async function safeValue(read) {
604
+ try {
605
+ return {
606
+ ok: true,
607
+ value: await read()
608
+ };
609
+ } catch (error) {
610
+ return {
611
+ ok: false,
612
+ error: describeControlFailure(error)
613
+ };
614
+ }
615
+ }
616
+ //#endregion
617
+ //#region extensions/codex/src/session-catalog-parsing.ts
618
+ const DEFAULT_PAGE_LIMIT = 50;
619
+ const CODEX_APP_SERVER_THREADS_CAPABILITY = "codex-app-server-threads";
620
+ const CODEX_APP_SERVER_THREADS_LIST_COMMAND = "codex.appServer.threads.list.v1";
621
+ const CODEX_APP_SERVER_THREAD_TURNS_LIST_COMMAND = "codex.appServer.thread.turns.list.v1";
622
+ const CODEX_LOCAL_SESSION_HOST_ID = "gateway:local";
623
+ const NODE_INVOKE_TIMEOUT_MS = 65e3;
624
+ const MAX_SEARCH_LENGTH = 500;
625
+ const MAX_CURSOR_LENGTH = 4096;
626
+ const MAX_CURSOR_COUNT = 100;
627
+ const MAX_HOST_ID_LENGTH = 256;
628
+ const MAX_CWD_LENGTH = 4096;
629
+ const MAX_SESSION_NAME_LENGTH = 500;
630
+ const MAX_SESSION_KEY_LENGTH = 1024;
631
+ const MAX_METADATA_LENGTH = 500;
632
+ const MAX_ACTIVE_FLAGS = 16;
633
+ const MAX_TRANSCRIPT_PAGE_BYTES = 20 * 1024 * 1024;
634
+ var CatalogParamsError = class extends Error {};
635
+ function readControlCursor(value, label) {
636
+ if (value === void 0 || value === null) return;
637
+ if (typeof value !== "string" || !value.trim() || value.length > 4096) throw new CatalogParamsError(`invalid Codex session catalog ${label} cursor`);
638
+ return value;
639
+ }
640
+ function boundedCatalogString(value, maxLength, overflow = "omit") {
641
+ if (typeof value !== "string") return;
642
+ const normalized = value.trim();
643
+ if (!normalized) return;
644
+ if (normalized.length <= maxLength) return normalized;
645
+ return overflow === "truncate" ? truncateUtf16Safe(normalized, maxLength) : void 0;
646
+ }
647
+ function isInteractiveThreadSource(source) {
648
+ return CODEX_INTERACTIVE_THREAD_SOURCE_KINDS.some((kind) => kind === source);
649
+ }
650
+ function toCatalogSession(thread, archived) {
651
+ const source = thread.source;
652
+ if (!isInteractiveThreadSource(source)) return;
653
+ const record = thread;
654
+ const threadId = boundedCatalogString(thread.id, 256);
655
+ if (!threadId) return;
656
+ const activeFlags = thread.status?.type === "active" ? thread.status.activeFlags?.flatMap((flag) => {
657
+ const normalized = boundedCatalogString(flag, 128);
658
+ return normalized ? [normalized] : [];
659
+ }).slice(0, MAX_ACTIVE_FLAGS) : void 0;
660
+ const gitInfo = isRecord(record.gitInfo) ? record.gitInfo : void 0;
661
+ const sessionId = boundedCatalogString(thread.sessionId, 256);
662
+ const name = boundedCatalogString(thread.name, MAX_SESSION_NAME_LENGTH, "truncate");
663
+ const cwd = boundedCatalogString(thread.cwd, MAX_CWD_LENGTH);
664
+ const modelProvider = boundedCatalogString(record.modelProvider, MAX_METADATA_LENGTH, "truncate");
665
+ const cliVersion = boundedCatalogString(record.cliVersion, MAX_METADATA_LENGTH, "truncate");
666
+ const gitBranch = boundedCatalogString(gitInfo?.branch, MAX_METADATA_LENGTH, "truncate");
667
+ return {
668
+ threadId,
669
+ status: thread.status?.type ?? "notLoaded",
670
+ archived,
671
+ ...sessionId ? { sessionId } : {},
672
+ ...thread.name === null ? { name: null } : name ? { name } : {},
673
+ ...cwd ? { cwd } : {},
674
+ ...activeFlags?.length ? { activeFlags } : {},
675
+ ...typeof thread.createdAt === "number" && Number.isFinite(thread.createdAt) ? { createdAt: thread.createdAt } : {},
676
+ ...typeof thread.updatedAt === "number" && Number.isFinite(thread.updatedAt) ? { updatedAt: thread.updatedAt } : {},
677
+ ...typeof record.recencyAt === "number" && Number.isFinite(record.recencyAt) ? { recencyAt: record.recencyAt } : record.recencyAt === null ? { recencyAt: null } : {},
678
+ source,
679
+ ...modelProvider ? { modelProvider } : {},
680
+ ...cliVersion ? { cliVersion } : {},
681
+ ...gitBranch ? { gitBranch } : {}
682
+ };
683
+ }
684
+ function normalizeLimit(value, key) {
685
+ if (value === void 0) return DEFAULT_PAGE_LIMIT;
686
+ if (!Number.isInteger(value) || value < 1 || value > 100) throw new CatalogParamsError(`${key} must be an integer from 1 to 100`);
687
+ return value;
688
+ }
689
+ function readOptionalString(params, key, maxLength) {
690
+ const value = params[key];
691
+ if (value === void 0) return;
692
+ if (typeof value !== "string") throw new CatalogParamsError(`${key} must be a string`);
693
+ const trimmed = value.trim();
694
+ if (!trimmed) return;
695
+ if (trimmed.length > maxLength) throw new CatalogParamsError(`${key} must be at most ${maxLength} characters`);
696
+ return trimmed;
697
+ }
698
+ function requireOnlyKeys(params, allowed) {
699
+ const unknown = Object.keys(params).find((key) => !allowed.has(key));
700
+ if (unknown) throw new CatalogParamsError(`unknown Codex session catalog parameter: ${unknown}`);
701
+ }
702
+ function readPageParams(value) {
703
+ if (!isRecord(value)) throw new CatalogParamsError("Codex session catalog parameters must be an object");
704
+ const params = value;
705
+ requireOnlyKeys(params, /* @__PURE__ */ new Set([
706
+ "cursor",
707
+ "limit",
708
+ "searchTerm",
709
+ "cwd"
710
+ ]));
711
+ const cursor = readOptionalString(params, "cursor", MAX_CURSOR_LENGTH);
712
+ const searchTerm = readOptionalString(params, "searchTerm", MAX_SEARCH_LENGTH);
713
+ const cwd = readOptionalString(params, "cwd", MAX_CWD_LENGTH);
714
+ return {
715
+ limit: normalizeLimit(params.limit, "limit"),
716
+ ...cursor ? { cursor } : {},
717
+ ...searchTerm ? { searchTerm } : {},
718
+ ...cwd ? { cwd } : {}
719
+ };
720
+ }
721
+ function readGatewayParams(value) {
722
+ if (value !== void 0 && !isRecord(value)) throw new CatalogParamsError("Codex session catalog parameters must be an object");
723
+ const params = isRecord(value) ? value : {};
724
+ requireOnlyKeys(params, /* @__PURE__ */ new Set([
725
+ "search",
726
+ "limitPerHost",
727
+ "hostIds",
728
+ "cursors"
729
+ ]));
730
+ const search = readOptionalString(params, "search", MAX_SEARCH_LENGTH);
731
+ let hostIds;
732
+ if (params.hostIds !== void 0) {
733
+ if (!Array.isArray(params.hostIds) || params.hostIds.length > 100) throw new CatalogParamsError(`hostIds must contain at most 100 host ids`);
734
+ hostIds = [...new Set(params.hostIds.map((hostId) => readHostId(hostId)))];
735
+ }
736
+ let cursors;
737
+ if (params.cursors !== void 0) {
738
+ if (!isRecord(params.cursors)) throw new CatalogParamsError("cursors must be an object");
739
+ const entries = Object.entries(params.cursors);
740
+ if (entries.length > MAX_CURSOR_COUNT) throw new CatalogParamsError(`cursors may contain at most ${MAX_CURSOR_COUNT} hosts`);
741
+ cursors = {};
742
+ for (const [hostId, cursor] of entries) {
743
+ const normalizedHostId = hostId.trim();
744
+ if (normalizedHostId.length === 0 || normalizedHostId.length > MAX_HOST_ID_LENGTH || !normalizedHostId.startsWith("gateway:") && !normalizedHostId.startsWith("node:")) throw new CatalogParamsError(`invalid Codex session catalog host id: ${hostId}`);
745
+ if (typeof cursor !== "string" || !cursor.trim() || cursor.trim().length > 4096) throw new CatalogParamsError(`invalid cursor for Codex session catalog host: ${hostId}`);
746
+ cursors[normalizedHostId] = cursor.trim();
747
+ }
748
+ }
749
+ return {
750
+ limitPerHost: normalizeLimit(params.limitPerHost, "limitPerHost"),
751
+ ...search ? { search } : {},
752
+ ...hostIds && hostIds.length > 0 ? { hostIds } : {},
753
+ ...cursors && Object.keys(cursors).length > 0 ? { cursors } : {}
754
+ };
755
+ }
756
+ function readHostId(value) {
757
+ if (typeof value !== "string") throw new CatalogParamsError("Codex session catalog host ids must be strings");
758
+ const hostId = value.trim();
759
+ if (hostId.length === 0 || hostId.length > MAX_HOST_ID_LENGTH || !hostId.startsWith("gateway:") && !hostId.startsWith("node:")) throw new CatalogParamsError(`invalid Codex session catalog host id: ${value}`);
760
+ return hostId;
761
+ }
762
+ function parseJsonParams(paramsJSON) {
763
+ if (!paramsJSON?.trim()) return {};
764
+ try {
765
+ return JSON.parse(paramsJSON);
766
+ } catch (error) {
767
+ throw new Error("Codex session catalog parameters must be valid JSON", { cause: error });
768
+ }
769
+ }
770
+ function readFiniteNumber(value) {
771
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
772
+ }
773
+ function parseOptionalCatalogString(value, field, maxLength) {
774
+ if (value === void 0) return;
775
+ if (typeof value !== "string" || value.length > maxLength) throw new Error(`Codex session catalog returned an invalid ${field}`);
776
+ return value;
777
+ }
778
+ function parseCatalogSession(value, options = {}) {
779
+ if (!isRecord(value) || typeof value.threadId !== "string" || !value.threadId.trim() || value.threadId.length > 256 || value.archived !== false) throw new Error("Codex session catalog returned an invalid session");
780
+ const status = parseOptionalCatalogString(value.status, "status", 64);
781
+ if (!status?.trim()) throw new Error("Codex session catalog returned an invalid status");
782
+ if (value.activeFlags !== void 0 && !Array.isArray(value.activeFlags)) throw new Error("Codex session catalog returned invalid active flags");
783
+ if (Array.isArray(value.activeFlags) && value.activeFlags.length > MAX_ACTIVE_FLAGS) throw new Error("Codex session catalog returned too many active flags");
784
+ const activeFlags = Array.isArray(value.activeFlags) ? value.activeFlags.map((entry) => {
785
+ const flag = parseOptionalCatalogString(entry, "active flag", 128);
786
+ if (flag === void 0) throw new Error("Codex session catalog returned an invalid active flag");
787
+ return flag;
788
+ }) : void 0;
789
+ const sessionId = parseOptionalCatalogString(value.sessionId, "session id", 256);
790
+ const name = value.name === null ? null : parseOptionalCatalogString(value.name, "session name", MAX_SESSION_NAME_LENGTH);
791
+ const cwd = parseOptionalCatalogString(value.cwd, "cwd", MAX_CWD_LENGTH);
792
+ const source = parseOptionalCatalogString(value.source, "source", MAX_METADATA_LENGTH);
793
+ const modelProvider = parseOptionalCatalogString(value.modelProvider, "model provider", MAX_METADATA_LENGTH);
794
+ const cliVersion = parseOptionalCatalogString(value.cliVersion, "CLI version", MAX_METADATA_LENGTH);
795
+ const gitBranch = parseOptionalCatalogString(value.gitBranch, "Git branch", MAX_METADATA_LENGTH);
796
+ const openClawSessionKey = options.allowOpenClawSessionKey ? parseOptionalCatalogString(value.openClawSessionKey, "OpenClaw session key", MAX_SESSION_KEY_LENGTH) : void 0;
797
+ const createdAt = readFiniteNumber(value.createdAt);
798
+ const updatedAt = readFiniteNumber(value.updatedAt);
799
+ const recencyAt = value.recencyAt === null ? null : readFiniteNumber(value.recencyAt);
800
+ return {
801
+ threadId: value.threadId,
802
+ status,
803
+ archived: value.archived,
804
+ ...sessionId !== void 0 ? { sessionId } : {},
805
+ ...name !== void 0 ? { name } : {},
806
+ ...cwd !== void 0 ? { cwd } : {},
807
+ ...activeFlags && activeFlags.length > 0 ? { activeFlags } : {},
808
+ ...createdAt !== void 0 ? { createdAt } : {},
809
+ ...updatedAt !== void 0 ? { updatedAt } : {},
810
+ ...recencyAt !== void 0 ? { recencyAt } : {},
811
+ ...source !== void 0 ? { source } : {},
812
+ ...modelProvider !== void 0 ? { modelProvider } : {},
813
+ ...cliVersion !== void 0 ? { cliVersion } : {},
814
+ ...gitBranch !== void 0 ? { gitBranch } : {},
815
+ ...openClawSessionKey !== void 0 ? { openClawSessionKey } : {}
816
+ };
817
+ }
818
+ function parseCatalogPage(value, options = {}) {
819
+ if (!isRecord(value) || !Array.isArray(value.sessions) || value.sessions.length > 100) throw new Error("Codex session catalog returned an invalid page");
820
+ const nextCursor = parseOptionalCatalogString(value.nextCursor, "next cursor", MAX_CURSOR_LENGTH);
821
+ const backwardsCursor = parseOptionalCatalogString(value.backwardsCursor, "backwards cursor", MAX_CURSOR_LENGTH);
822
+ return {
823
+ sessions: value.sessions.map((session) => parseCatalogSession(session, options)),
824
+ ...nextCursor ? { nextCursor } : {},
825
+ ...backwardsCursor ? { backwardsCursor } : {}
826
+ };
827
+ }
828
+ function filterCatalogPageByTitle(page, searchTerm) {
829
+ if (!searchTerm) return page;
830
+ return {
831
+ ...page,
832
+ sessions: page.sessions.filter((session) => session.name?.toLocaleLowerCase().includes(searchTerm.toLocaleLowerCase()))
833
+ };
834
+ }
835
+ function unwrapNodeInvokePayload(value) {
836
+ if (!isRecord(value)) return value;
837
+ if (typeof value.payloadJSON === "string" && value.payloadJSON.trim()) try {
838
+ return JSON.parse(value.payloadJSON);
839
+ } catch (error) {
840
+ throw new Error("Codex node returned malformed session catalog JSON", { cause: error });
841
+ }
842
+ return "payload" in value ? value.payload : value;
843
+ }
844
+ function catalogErrorDetail(error) {
845
+ if (error instanceof Error) return error.message.trim();
846
+ if (typeof error === "string") return error.trim();
847
+ if (error && typeof error === "object" && "message" in error) {
848
+ const message = error.message;
849
+ return typeof message === "string" ? message.trim() : "";
850
+ }
851
+ return "";
852
+ }
853
+ function catalogError(code, error) {
854
+ const summary = {
855
+ APP_SERVER_UNAVAILABLE: "Codex app-server is unavailable on this host",
856
+ NODE_INVOKE_FAILED: "The paired node could not return its Codex session catalog",
857
+ NODE_LIST_FAILED: "Paired nodes could not be listed"
858
+ }[code] ?? "Codex session catalog request failed";
859
+ const detail = code === "NODE_LIST_FAILED" ? catalogErrorDetail(error) : "";
860
+ return {
861
+ code,
862
+ message: detail && detail !== summary ? `${summary}: ${detail}` : summary
863
+ };
864
+ }
865
+ function parseTranscriptPage(value) {
866
+ if (!isRecord(value) || !Array.isArray(value.data) || value.data.length > 50 || value.data.some((turn) => !isRecord(turn) || !Array.isArray(turn.items) || turn.items.some((item) => !isRecord(item)))) throw new Error("Codex app-server returned an invalid transcript page");
867
+ const nextCursor = readControlCursor(value.nextCursor, "transcript next response");
868
+ const backwardsCursor = readControlCursor(value.backwardsCursor, "transcript backwards response");
869
+ const page = {
870
+ data: value.data,
871
+ ...nextCursor ? { nextCursor } : {},
872
+ ...backwardsCursor ? { backwardsCursor } : {}
873
+ };
874
+ if (Buffer.byteLength(JSON.stringify(page), "utf8") > MAX_TRANSCRIPT_PAGE_BYTES) throw new Error("Codex app-server transcript page exceeds the safe response size");
875
+ return page;
876
+ }
877
+ function requireBoundThread(entry) {
878
+ if (!entry.boundThreadId) throw new CatalogParamsError("Codex adoption is missing its bound thread. Retry.");
879
+ return entry.boundThreadId;
880
+ }
881
+ //#endregion
882
+ //#region extensions/codex/src/session-catalog-node-adoption.ts
883
+ const CODEX_NODE_SESSION_KEY_PREFIX = "harness:codex:node-session:";
884
+ const continueOperations = /* @__PURE__ */ new Map();
885
+ const sessionActionTails = /* @__PURE__ */ new Map();
886
+ async function runSessionActionExclusive(threadId, run) {
887
+ const operation = (sessionActionTails.get(threadId) ?? Promise.resolve()).then(run);
888
+ const tail = operation.then(() => void 0, () => void 0);
889
+ sessionActionTails.set(threadId, tail);
890
+ try {
891
+ return await operation;
892
+ } finally {
893
+ if (sessionActionTails.get(threadId) === tail) sessionActionTails.delete(threadId);
894
+ }
895
+ }
896
+ function adoptionSessionKeyRest(sessionKey) {
897
+ const trimmed = sessionKey.trim();
898
+ return parseAgentSessionKey(trimmed)?.rest ?? trimmed;
899
+ }
900
+ function listSupervisionAgentIds(config) {
901
+ const defaultAgentId = resolveDefaultAgentId(config);
902
+ return [defaultAgentId, ...listAgentIds(config).filter((agentId) => agentId !== defaultAgentId)];
903
+ }
904
+ function adoptedSourceKey(hostId, threadId) {
905
+ return `${hostId}\u0000${threadId}`;
906
+ }
907
+ function lastTerminalTurnId(thread) {
908
+ for (let index = (thread.turns?.length ?? 0) - 1; index >= 0; index -= 1) {
909
+ const turn = thread.turns?.[index];
910
+ const turnId = boundedCatalogString(turn?.id, 256);
911
+ if (!turnId) continue;
912
+ if (turn?.status === "completed" || turn?.status === "interrupted" || turn?.status === "failed") return turnId;
913
+ }
914
+ }
915
+ function nodeAdoptionSessionKey(hostId, threadId) {
916
+ const digest = createHash("sha256").update(JSON.stringify([hostId, threadId])).digest("hex");
917
+ return `${CODEX_NODE_SESSION_KEY_PREFIX}${digest}`;
918
+ }
919
+ function readNodeSessionMarker(entry) {
920
+ const codex = isRecord(entry.pluginExtensions?.codex) ? entry.pluginExtensions.codex : void 0;
921
+ const marker = codex && isRecord(codex.sessionCatalog) ? codex.sessionCatalog : void 0;
922
+ if (!marker || typeof marker.sourceHostId !== "string" || !marker.sourceHostId.startsWith("node:") || typeof marker.sourceThreadId !== "string" || !marker.sourceThreadId.trim() || typeof marker.nodeId !== "string" || !marker.nodeId.trim()) return;
923
+ return {
924
+ sourceHostId: marker.sourceHostId,
925
+ sourceThreadId: marker.sourceThreadId,
926
+ nodeId: marker.nodeId,
927
+ ...marker.initializing === true ? { initializing: true } : {}
928
+ };
929
+ }
930
+ function listNodeAdoptedSessionEntries(params) {
931
+ const adopted = /* @__PURE__ */ new Map();
932
+ for (const agentId of listSupervisionAgentIds(params.config ?? {})) for (const { entry, sessionKey } of params.runtime.agent.session.listSessionEntries({ agentId })) {
933
+ const marker = readNodeSessionMarker(entry);
934
+ const sessionId = entry.sessionId?.trim();
935
+ if (!marker || marker.initializing === true && params.includeInitializing !== true || entry.initializationPending === true || entry.agentHarnessId !== "codex" || entry.modelSelectionLocked !== true || !sessionId || adoptionSessionKeyRest(sessionKey) !== nodeAdoptionSessionKey(marker.sourceHostId, marker.sourceThreadId) || marker.sourceHostId !== `node:${marker.nodeId}`) continue;
936
+ const sourceKey = adoptedSourceKey(marker.sourceHostId, marker.sourceThreadId);
937
+ if (adopted.has(sourceKey)) throw new Error(`multiple OpenClaw sessions adopt Codex thread ${marker.sourceThreadId} on ${marker.sourceHostId}`);
938
+ adopted.set(sourceKey, {
939
+ key: sessionKey,
940
+ sessionId,
941
+ agentId,
942
+ ...marker.initializing === true ? { initializing: true } : {}
943
+ });
944
+ }
945
+ return adopted;
946
+ }
947
+ function findNodeAdoptedSessionEntry(params) {
948
+ return listNodeAdoptedSessionEntries(params).get(adoptedSourceKey(params.hostId, params.threadId));
949
+ }
950
+ function nodeSessionMarker(params) {
951
+ return {
952
+ sourceHostId: params.hostId,
953
+ sourceThreadId: params.threadId,
954
+ nodeId: params.nodeId,
955
+ ...params.initializing === true ? { initializing: true } : {}
956
+ };
957
+ }
958
+ async function finalizeNodeAdoptedSession(params) {
959
+ const changedError = () => new CatalogParamsError("Codex OpenClaw session changed before it could be bound. Retry.");
960
+ let finalized;
961
+ try {
962
+ finalized = await params.api.runtime.agent.session.patchSessionEntry({
963
+ sessionKey: params.adopted.key,
964
+ readConsistency: "latest",
965
+ preserveActivity: true,
966
+ update: (entry) => {
967
+ const current = readNodeSessionMarker(entry);
968
+ if (entry.sessionId?.trim() !== params.adopted.sessionId || entry.initializationPending === true || entry.agentHarnessId !== "codex" || entry.modelSelectionLocked !== true || !current || current.sourceHostId !== params.marker.sourceHostId || current.sourceThreadId !== params.marker.sourceThreadId || current.nodeId !== params.marker.nodeId) throw changedError();
969
+ if (current.initializing !== true) return { archivedAt: void 0 };
970
+ const codex = isRecord(entry.pluginExtensions?.codex) ? entry.pluginExtensions.codex : {};
971
+ return {
972
+ archivedAt: void 0,
973
+ pluginExtensions: {
974
+ ...entry.pluginExtensions,
975
+ codex: {
976
+ ...codex,
977
+ sessionCatalog: params.marker
978
+ }
979
+ }
980
+ };
981
+ }
982
+ });
983
+ } catch (error) {
984
+ const currentEntry = params.api.runtime.agent.session.getSessionEntry({
985
+ sessionKey: params.adopted.key,
986
+ readConsistency: "latest"
987
+ });
988
+ const current = currentEntry ? readNodeSessionMarker(currentEntry) : void 0;
989
+ if (currentEntry?.sessionId?.trim() === params.adopted.sessionId && current?.initializing !== true && current?.sourceHostId === params.marker.sourceHostId && current.sourceThreadId === params.marker.sourceThreadId && current.nodeId === params.marker.nodeId) return;
990
+ throw error;
991
+ }
992
+ if (!finalized) throw changedError();
993
+ }
994
+ async function createOrReuseNodeAdoptedSession(params) {
995
+ const existing = findNodeAdoptedSessionEntry({
996
+ config: params.config,
997
+ runtime: params.api.runtime,
998
+ hostId: params.hostId,
999
+ threadId: params.record.threadId,
1000
+ includeInitializing: true
1001
+ });
1002
+ if (existing) return existing;
1003
+ const initializingMarker = {
1004
+ ...nodeSessionMarker({
1005
+ hostId: params.hostId,
1006
+ threadId: params.record.threadId,
1007
+ nodeId: params.nodeId
1008
+ }),
1009
+ initializing: true
1010
+ };
1011
+ try {
1012
+ const created = await params.api.runtime.agent.session.createSessionEntry({
1013
+ cfg: params.config,
1014
+ key: nodeAdoptionSessionKey(params.hostId, params.record.threadId),
1015
+ agentId: resolveDefaultAgentId(params.config),
1016
+ recoverMatchingInitialEntry: true,
1017
+ ...params.record.name?.trim() ? { label: params.record.name.trim() } : {},
1018
+ ...params.record.cwd?.trim() ? { spawnedCwd: params.record.cwd.trim() } : {},
1019
+ initialEntry: {
1020
+ agentHarnessId: "codex",
1021
+ modelSelectionLocked: true,
1022
+ pluginExtensions: { codex: { sessionCatalog: initializingMarker } }
1023
+ },
1024
+ afterCreate: async (entry) => {
1025
+ const storePath = resolveStorePath(params.config.session?.store, { agentId: entry.agentId });
1026
+ await importCodexThreadHistoryToTranscript({
1027
+ thread: params.history.thread,
1028
+ throughTurnId: params.history.throughTurnId,
1029
+ storePath,
1030
+ sessionId: entry.sessionId,
1031
+ sessionKey: entry.key,
1032
+ agentId: entry.agentId,
1033
+ ...params.record.cwd?.trim() ? { cwd: params.record.cwd.trim() } : {},
1034
+ modelProvider: params.record.modelProvider,
1035
+ config: params.config
1036
+ });
1037
+ return { pluginExtensions: { codex: { sessionCatalog: initializingMarker } } };
1038
+ }
1039
+ });
1040
+ return {
1041
+ key: created.key,
1042
+ sessionId: created.sessionId,
1043
+ agentId: created.agentId,
1044
+ initializing: true
1045
+ };
1046
+ } catch (error) {
1047
+ const raced = findNodeAdoptedSessionEntry({
1048
+ config: params.config,
1049
+ runtime: params.api.runtime,
1050
+ hostId: params.hostId,
1051
+ threadId: params.record.threadId,
1052
+ includeInitializing: true
1053
+ });
1054
+ if (raced) return raced;
1055
+ throw error;
1056
+ }
1057
+ }
1058
+ //#endregion
1059
+ //#region extensions/codex/src/session-catalog-node-continue.ts
1060
+ const CODEX_NODE_CONTINUE_COMMANDS = [
1061
+ CODEX_APP_SERVER_THREADS_LIST_COMMAND,
1062
+ CODEX_APP_SERVER_THREAD_TURNS_LIST_COMMAND,
1063
+ CODEX_CLI_SESSION_RESUME_COMMAND
1064
+ ];
1065
+ function nodeLabel(node) {
1066
+ return node.displayName?.trim() || node.remoteIp?.trim() || node.nodeId;
1067
+ }
1068
+ function compareNodeLabels(left, right) {
1069
+ const leftLabel = nodeLabel(left);
1070
+ const rightLabel = nodeLabel(right);
1071
+ if (leftLabel < rightLabel) return -1;
1072
+ if (leftLabel > rightLabel) return 1;
1073
+ return 0;
1074
+ }
1075
+ function canContinueCodexOnNode(node) {
1076
+ return node.connected === true && CODEX_NODE_CONTINUE_COMMANDS.every((command) => node.commands?.includes(command) === true && node.invocableCommands?.includes(command) === true);
1077
+ }
1078
+ async function listPairedNode(params) {
1079
+ const hostId = `node:${params.node.nodeId}`;
1080
+ const common = {
1081
+ hostId,
1082
+ label: nodeLabel(params.node),
1083
+ kind: "node",
1084
+ nodeId: params.node.nodeId,
1085
+ canContinueCodex: canContinueCodexOnNode(params.node)
1086
+ };
1087
+ if (params.node.connected !== true) return {
1088
+ ...common,
1089
+ connected: false,
1090
+ sessions: [],
1091
+ error: {
1092
+ code: "NODE_OFFLINE",
1093
+ message: "Paired node is offline"
1094
+ }
1095
+ };
1096
+ try {
1097
+ const page = filterCatalogPageByTitle(parseCatalogPage(unwrapNodeInvokePayload(await params.runtime.nodes.invoke({
1098
+ nodeId: params.node.nodeId,
1099
+ command: CODEX_APP_SERVER_THREADS_LIST_COMMAND,
1100
+ params: {
1101
+ cursor: params.query.cursors?.[hostId],
1102
+ limit: params.query.limitPerHost,
1103
+ searchTerm: params.query.search
1104
+ },
1105
+ timeoutMs: NODE_INVOKE_TIMEOUT_MS,
1106
+ scopes: ["operator.write"]
1107
+ }))), params.query.search);
1108
+ return {
1109
+ ...common,
1110
+ connected: true,
1111
+ ...page,
1112
+ sessions: page.sessions.map((session) => {
1113
+ const adopted = params.adoptedSessions.get(adoptedSourceKey(hostId, session.threadId));
1114
+ return adopted ? Object.assign({}, session, { openClawSessionKey: adopted.key }) : session;
1115
+ })
1116
+ };
1117
+ } catch (error) {
1118
+ return {
1119
+ ...common,
1120
+ connected: true,
1121
+ sessions: [],
1122
+ error: catalogError("NODE_INVOKE_FAILED", error)
1123
+ };
1124
+ }
1125
+ }
1126
+ async function requireNodeForCodexContinue(params) {
1127
+ const nodeId = params.hostId.slice(5).trim();
1128
+ if (!nodeId || params.hostId !== `node:${nodeId}`) throw new CatalogParamsError("Codex session catalog hostId is invalid");
1129
+ const node = (await params.runtime.nodes.list()).nodes.find((candidate) => candidate.nodeId === nodeId);
1130
+ if (!node || !canContinueCodexOnNode(node)) throw new CatalogParamsError("paired node does not permit Codex session continuation");
1131
+ return {
1132
+ node,
1133
+ nodeId
1134
+ };
1135
+ }
1136
+ async function resolveNodeCodexRecord(params) {
1137
+ let cursor;
1138
+ const seenCursors = /* @__PURE__ */ new Set();
1139
+ for (let pageIndex = 0; pageIndex < 100; pageIndex += 1) {
1140
+ const page = parseCatalogPage(unwrapNodeInvokePayload(await params.runtime.nodes.invoke({
1141
+ nodeId: params.nodeId,
1142
+ command: CODEX_APP_SERVER_THREADS_LIST_COMMAND,
1143
+ params: {
1144
+ limit: 100,
1145
+ ...cursor ? { cursor } : {}
1146
+ },
1147
+ timeoutMs: NODE_INVOKE_TIMEOUT_MS,
1148
+ scopes: ["operator.write"]
1149
+ })));
1150
+ const record = page.sessions.find((candidate) => candidate.threadId === params.threadId);
1151
+ if (record) return record;
1152
+ const nextCursor = page.nextCursor?.trim();
1153
+ if (!nextCursor) break;
1154
+ if (seenCursors.has(nextCursor)) throw new CatalogParamsError("Codex session eligibility could not be verified");
1155
+ seenCursors.add(nextCursor);
1156
+ cursor = nextCursor;
1157
+ }
1158
+ throw new CatalogParamsError("Codex session is unavailable on the paired node");
1159
+ }
1160
+ function requireContinuableNodeRecord(record) {
1161
+ if (record.archived) throw new CatalogParamsError("Codex session is archived on the paired node");
1162
+ if (!isInteractiveThreadSource(record.source)) throw new CatalogParamsError("Codex session is not a non-archived interactive CLI or VS Code session");
1163
+ if (record.status === "idle" || record.status === "notLoaded") return;
1164
+ if (record.status === "active") throw new CatalogParamsError("Codex session is active on the paired node; wait for it to finish before continuing");
1165
+ throw new CatalogParamsError("Codex session cannot be continued in its current state");
1166
+ }
1167
+ async function readNodeCodexHistory(params) {
1168
+ const page = parseTranscriptPage(unwrapNodeInvokePayload(await params.runtime.nodes.invoke({
1169
+ nodeId: params.nodeId,
1170
+ command: CODEX_APP_SERVER_THREAD_TURNS_LIST_COMMAND,
1171
+ params: {
1172
+ threadId: params.record.threadId,
1173
+ limit: 50
1174
+ },
1175
+ timeoutMs: NODE_INVOKE_TIMEOUT_MS,
1176
+ scopes: ["operator.write"]
1177
+ })));
1178
+ const thread = {
1179
+ id: params.record.threadId,
1180
+ createdAt: params.record.createdAt ?? 0,
1181
+ modelProvider: params.record.modelProvider ?? "openai",
1182
+ turns: page.data.toReversed()
1183
+ };
1184
+ return {
1185
+ thread,
1186
+ throughTurnId: lastTerminalTurnId(thread) ?? null
1187
+ };
1188
+ }
1189
+ async function continueNodeCodexSessionInner(params) {
1190
+ const { nodeId } = await requireNodeForCodexContinue({
1191
+ runtime: params.api.runtime,
1192
+ hostId: params.hostId
1193
+ });
1194
+ const record = await resolveNodeCodexRecord({
1195
+ runtime: params.api.runtime,
1196
+ nodeId,
1197
+ threadId: params.threadId
1198
+ });
1199
+ requireContinuableNodeRecord(record);
1200
+ const existing = findNodeAdoptedSessionEntry({
1201
+ config: params.config,
1202
+ runtime: params.api.runtime,
1203
+ hostId: params.hostId,
1204
+ threadId: params.threadId,
1205
+ includeInitializing: true
1206
+ });
1207
+ let adopted;
1208
+ let disposition;
1209
+ if (existing) {
1210
+ adopted = existing;
1211
+ disposition = "existing";
1212
+ } else {
1213
+ const history = await readNodeCodexHistory({
1214
+ runtime: params.api.runtime,
1215
+ nodeId,
1216
+ record
1217
+ });
1218
+ adopted = await createOrReuseNodeAdoptedSession({
1219
+ api: params.api,
1220
+ config: params.config,
1221
+ hostId: params.hostId,
1222
+ nodeId,
1223
+ record,
1224
+ history
1225
+ });
1226
+ disposition = "forked";
1227
+ }
1228
+ const marker = nodeSessionMarker({
1229
+ hostId: params.hostId,
1230
+ threadId: params.threadId,
1231
+ nodeId
1232
+ });
1233
+ return {
1234
+ sessionKey: adopted.key,
1235
+ disposition,
1236
+ conversationBinding: {
1237
+ summary: "Continue this Codex session on its paired node.",
1238
+ detachHint: "Start a new chat to leave the paired-node Codex session.",
1239
+ data: createCodexCliNodeConversationBindingData({
1240
+ nodeId,
1241
+ sessionId: record.sessionId?.trim() || params.threadId,
1242
+ agentId: adopted.agentId,
1243
+ cwd: record.cwd
1244
+ })
1245
+ },
1246
+ afterConversationBound: async () => await finalizeNodeAdoptedSession({
1247
+ api: params.api,
1248
+ adopted,
1249
+ marker
1250
+ })
1251
+ };
1252
+ }
1253
+ async function continueNodeCodexSession(params) {
1254
+ if (params.clientScopes?.includes("operator.admin") !== true) throw new CatalogParamsError("continuing a paired-node Codex session requires operator.admin");
1255
+ const nodeId = params.hostId.slice(5).trim();
1256
+ if (!nodeId || params.hostId !== `node:${nodeId}`) throw new CatalogParamsError("Codex session catalog hostId is invalid");
1257
+ const sourceKey = adoptedSourceKey(`node:${nodeId}`, params.threadId);
1258
+ const current = continueOperations.get(sourceKey);
1259
+ if (current) return await current;
1260
+ const operation = runSessionActionExclusive(sourceKey, async () => continueNodeCodexSessionInner(params));
1261
+ continueOperations.set(sourceKey, operation);
1262
+ try {
1263
+ return await operation;
1264
+ } finally {
1265
+ if (continueOperations.get(sourceKey) === operation) continueOperations.delete(sourceKey);
1266
+ }
1267
+ }
1268
+ //#endregion
1269
+ //#region extensions/codex/src/session-catalog-terminal.ts
1270
+ const CODEX_TERMINAL_RESUME_COMMAND = "codex.terminal.resume.v1";
1271
+ function resolveLocalCodexTerminalExecutable(pathEnv = process.env.PATH ?? "") {
1272
+ return resolveExecutableFromPathEnv("codex", pathEnv);
1273
+ }
1274
+ function codexNodeTerminalCapability(node) {
1275
+ const commands = node.invocableCommands ?? node.commands;
1276
+ return node.connected === true && commands?.includes("codex.terminal.resume.v1") === true ? { canOpenTerminalCodex: true } : {};
1277
+ }
1278
+ async function requireCatalogEligibleThread(control, threadId) {
1279
+ let cursor;
1280
+ const seenCursors = /* @__PURE__ */ new Set();
1281
+ for (let pageIndex = 0; pageIndex < 100; pageIndex += 1) {
1282
+ const page = await control.listPage({
1283
+ limit: 100,
1284
+ ...cursor ? { cursor } : {}
1285
+ });
1286
+ const candidate = page.sessions.find((session) => session.threadId === threadId);
1287
+ if (candidate) {
1288
+ if (candidate.source === "cli" || candidate.source === "vscode") return candidate;
1289
+ throw new CatalogParamsError("Codex session is not a non-archived interactive CLI or VS Code session");
1290
+ }
1291
+ const nextCursor = page.nextCursor?.trim();
1292
+ if (!nextCursor) throw new CatalogParamsError("Codex session is not a non-archived interactive CLI or VS Code session");
1293
+ if (seenCursors.has(nextCursor)) throw new CatalogParamsError("Codex session eligibility could not be verified");
1294
+ seenCursors.add(nextCursor);
1295
+ cursor = nextCursor;
1296
+ }
1297
+ throw new CatalogParamsError("Codex session eligibility could not be verified");
1298
+ }
1299
+ function createCodexTerminalNodeHostCommand(control) {
1300
+ return {
1301
+ command: CODEX_TERMINAL_RESUME_COMMAND,
1302
+ cap: CODEX_APP_SERVER_THREADS_CAPABILITY,
1303
+ dangerous: false,
1304
+ duplex: true,
1305
+ isAvailable: ({ env }) => Boolean(resolveExecutableFromPathEnv("codex", env.PATH ?? "")),
1306
+ handle: async (paramsJSON, io) => {
1307
+ if (!io) throw new Error("Codex terminal command requires duplex transport");
1308
+ const resume = decodeNodePtyResumeParams(paramsJSON, (value) => {
1309
+ if (typeof value !== "string" || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu.test(value)) throw new CatalogParamsError("threadId must be a UUID");
1310
+ return value;
1311
+ });
1312
+ const record = await requireCatalogEligibleThread(control, resume.threadId);
1313
+ const file = resolveExecutableFromPathEnv("codex", process.env.PATH ?? "");
1314
+ if (!file) throw new Error("Codex CLI is unavailable");
1315
+ return JSON.stringify(await runNodePtyCommand({
1316
+ file,
1317
+ args: ["resume", resume.threadId],
1318
+ cwd: record.cwd,
1319
+ cols: resume.cols,
1320
+ rows: resume.rows
1321
+ }, io));
1322
+ }
1323
+ };
1324
+ }
1325
+ async function resolveNodeCatalogEligibleThread(params) {
1326
+ let cursor;
1327
+ const seenCursors = /* @__PURE__ */ new Set();
1328
+ for (let pageIndex = 0; pageIndex < 100; pageIndex += 1) {
1329
+ const raw = await params.runtime.nodes.invoke({
1330
+ nodeId: params.nodeId,
1331
+ command: CODEX_APP_SERVER_THREADS_LIST_COMMAND,
1332
+ params: {
1333
+ limit: 100,
1334
+ ...cursor ? { cursor } : {}
1335
+ },
1336
+ timeoutMs: NODE_INVOKE_TIMEOUT_MS,
1337
+ scopes: ["operator.write"]
1338
+ });
1339
+ const page = params.parseCatalogPage(unwrapNodeInvokePayload(raw));
1340
+ const record = page.sessions.find((candidate) => candidate.threadId === params.threadId);
1341
+ if (record) {
1342
+ if (isInteractiveThreadSource(record.source)) return record;
1343
+ break;
1344
+ }
1345
+ const nextCursor = page.nextCursor?.trim();
1346
+ if (!nextCursor || seenCursors.has(nextCursor)) break;
1347
+ seenCursors.add(nextCursor);
1348
+ cursor = nextCursor;
1349
+ }
1350
+ throw new CatalogParamsError("Codex session is not a non-archived interactive CLI or VS Code session");
1351
+ }
1352
+ async function openCodexCatalogTerminal(params) {
1353
+ const title = `codex resume ${params.threadId.slice(0, 8)}…`;
1354
+ if (params.hostId === "gateway:local") {
1355
+ const record = await requireCatalogEligibleThread(params.control, params.threadId);
1356
+ const executable = resolveLocalCodexTerminalExecutable();
1357
+ if (!executable) throw new CatalogParamsError("Codex CLI is unavailable");
1358
+ return {
1359
+ kind: "local",
1360
+ argv: [
1361
+ executable,
1362
+ "resume",
1363
+ params.threadId
1364
+ ],
1365
+ ...record.cwd ? { cwd: record.cwd } : {},
1366
+ title
1367
+ };
1368
+ }
1369
+ if (!params.hostId.startsWith("node:")) throw new CatalogParamsError("hostId is invalid");
1370
+ const nodeId = params.hostId.slice(5);
1371
+ if (!(await params.api.runtime.nodes.list()).nodes.find((candidate) => {
1372
+ const commands = candidate.invocableCommands ?? candidate.commands;
1373
+ return candidate.nodeId === nodeId && candidate.connected === true && commands?.includes("codex.appServer.threads.list.v1") === true && commands.includes("codex.terminal.resume.v1");
1374
+ })) throw new CatalogParamsError("paired-node Codex terminal is unavailable");
1375
+ const record = await resolveNodeCatalogEligibleThread({
1376
+ runtime: params.api.runtime,
1377
+ nodeId,
1378
+ threadId: params.threadId,
1379
+ parseCatalogPage: params.parseCatalogPage
1380
+ });
1381
+ return {
1382
+ kind: "node",
1383
+ nodeId,
1384
+ command: CODEX_TERMINAL_RESUME_COMMAND,
1385
+ paramsJSON: JSON.stringify({ threadId: params.threadId }),
1386
+ ...record.cwd ? { cwd: record.cwd } : {},
1387
+ title
1388
+ };
1389
+ }
1390
+ //#endregion
1391
+ //#region extensions/codex/src/session-catalog-transcript-item.ts
1392
+ const CODEX_MESSAGE_TYPES = /* @__PURE__ */ new Map([
1393
+ ["userMessage", "userMessage"],
1394
+ ["agentMessage", "agentMessage"],
1395
+ ["reasoning", "reasoning"]
1396
+ ]);
1397
+ const CODEX_TOOL_TYPES = /* @__PURE__ */ new Set([
1398
+ "commandExecution",
1399
+ "fileChange",
1400
+ "mcpToolCall",
1401
+ "dynamicToolCall",
1402
+ "collabAgentToolCall",
1403
+ "webSearch",
1404
+ "imageView",
1405
+ "imageGeneration"
1406
+ ]);
1407
+ function toGenericTranscriptItem(item) {
1408
+ let type = CODEX_MESSAGE_TYPES.get(item.type);
1409
+ if (!type && CODEX_TOOL_TYPES.has(item.type)) type = item.result !== void 0 || Boolean(item.aggregatedOutput) ? "toolResult" : "toolCall";
1410
+ type ??= "other";
1411
+ const fallback = item.title ?? item.name ?? item.tool ?? item.command ?? item.query ?? void 0;
1412
+ const resultText = item.aggregatedOutput || (item.result === void 0 ? void 0 : JSON.stringify(item.result, null, 2));
1413
+ const changesText = Array.isArray(item.changes) ? item.changes.map((change) => `${change.kind}: ${change.path}`).join("\n") || void 0 : void 0;
1414
+ const text = item.text || resultText || changesText || fallback;
1415
+ return {
1416
+ id: item.id,
1417
+ type,
1418
+ ...text ? { text } : {},
1419
+ raw: item
1420
+ };
1421
+ }
1422
+ //#endregion
1423
+ //#region extensions/codex/src/session-upstream-activity.ts
1424
+ const CODEX_UPSTREAM_TURN_LIMIT = 100;
1425
+ const CODEX_APP_SERVER_INVALID_REQUEST_CODE = -32600;
1426
+ const CODEX_THREAD_NOT_LOADED_MESSAGE_PREFIX = "thread not loaded:";
1427
+ function isCodexThreadGoneError(error) {
1428
+ return error instanceof CodexAppServerRpcError && error.code === CODEX_APP_SERVER_INVALID_REQUEST_CODE && error.message.startsWith(CODEX_THREAD_NOT_LOADED_MESSAGE_PREFIX);
1429
+ }
1430
+ function readMarker(probe) {
1431
+ if (!isRecord(probe.marker)) return;
1432
+ const turnId = probe.marker.turnId;
1433
+ if (turnId !== null && typeof turnId !== "string") return;
1434
+ const count = probe.marker.userMessageCount;
1435
+ if (count !== void 0 && (!Number.isSafeInteger(count) || count < 0)) return;
1436
+ return {
1437
+ turnId,
1438
+ ...count === void 0 ? {} : { userMessageCount: count }
1439
+ };
1440
+ }
1441
+ function upstreamConnectionFingerprint(probe) {
1442
+ return isRecord(probe.upstreamRef) && typeof probe.upstreamRef.connectionFingerprint === "string" ? probe.upstreamRef.connectionFingerprint : void 0;
1443
+ }
1444
+ function classifyCodexUpstreamTurns(params) {
1445
+ const marker = readMarker(params.probe);
1446
+ if (!marker) return;
1447
+ const newest = params.turns[0];
1448
+ if (!newest?.id) return;
1449
+ const markerIndex = marker.turnId === null ? -1 : params.turns.findIndex((turn) => turn.id === marker.turnId);
1450
+ const candidateTurns = markerIndex < 0 ? params.turns : params.turns.slice(0, markerIndex + 1);
1451
+ const newestUserMessageCount = countUserMessages(newest);
1452
+ if (!(marker.turnId !== newest.id || marker.userMessageCount === void 0 || newestUserMessageCount > marker.userMessageCount)) return;
1453
+ const ownTexts = new Set(params.probe.ownRecentUserTexts);
1454
+ let humanTurns = 0;
1455
+ let occurredAt;
1456
+ for (const turn of candidateTurns) {
1457
+ const userMessages = turn.items.filter((item) => item.type === "userMessage");
1458
+ const alreadySeen = turn.id === marker.turnId ? marker.userMessageCount ?? userMessages.length : 0;
1459
+ for (const item of userMessages.slice(alreadySeen)) {
1460
+ const texts = normalizeUserMessageTexts(item);
1461
+ if (ownTexts.has(texts.join(" ")) || texts.length > 1 && texts.every((text) => ownTexts.has(text))) continue;
1462
+ humanTurns += 1;
1463
+ if (occurredAt === void 0) {
1464
+ const timestampSeconds = turn.completedAt ?? turn.startedAt;
1465
+ occurredAt = typeof timestampSeconds === "number" && Number.isFinite(timestampSeconds) ? timestampSeconds * 1e3 : params.now ?? Date.now();
1466
+ }
1467
+ }
1468
+ }
1469
+ const activityId = `${newest.id}:${newestUserMessageCount}`;
1470
+ return {
1471
+ kind: "activity",
1472
+ sessionKey: params.probe.sessionKey,
1473
+ humanTurns,
1474
+ nextMarker: {
1475
+ turnId: newest.id,
1476
+ userMessageCount: newestUserMessageCount
1477
+ },
1478
+ ...humanTurns > 0 ? {
1479
+ occurredAt: occurredAt ?? params.now ?? Date.now(),
1480
+ dedupeId: activityId
1481
+ } : {}
1482
+ };
1483
+ }
1484
+ function countUserMessages(turn) {
1485
+ return turn.items.filter((item) => item.type === "userMessage").length;
1486
+ }
1487
+ function normalizeUserMessageTexts(item) {
1488
+ const typed = item;
1489
+ const contentTexts = typed.content?.filter((input) => input.type === "text").map((input) => input.text.trim().replace(/\s+/g, " ")).filter(Boolean);
1490
+ return contentTexts?.length ? contentTexts : [(typed.text ?? "").trim().replace(/\s+/g, " ")];
1491
+ }
1492
+ async function checkCodexUpstreamActivity(probes, control, resolveThreadId = async (probe) => probe.threadId) {
1493
+ return await control.withPinnedConnection(async (pinned) => {
1494
+ const activities = [];
1495
+ for (const probe of probes) {
1496
+ const fingerprint = upstreamConnectionFingerprint(probe);
1497
+ if (probe.upstreamKind !== "codex-app-server" || !fingerprint || fingerprint !== pinned.connectionFingerprint) continue;
1498
+ try {
1499
+ const threadId = await resolveThreadId(probe);
1500
+ const page = await pinned.listTurnPage({
1501
+ threadId,
1502
+ limit: CODEX_UPSTREAM_TURN_LIMIT,
1503
+ sortDirection: "desc",
1504
+ itemsView: "full"
1505
+ });
1506
+ const marker = readMarker(probe);
1507
+ if (page.data.length === 0 && marker) {
1508
+ try {
1509
+ await pinned.readThread(threadId, false);
1510
+ } catch (error) {
1511
+ if (isCodexThreadGoneError(error)) activities.push({
1512
+ kind: "missing",
1513
+ sessionKey: probe.sessionKey
1514
+ });
1515
+ }
1516
+ continue;
1517
+ }
1518
+ const activity = classifyCodexUpstreamTurns({
1519
+ probe,
1520
+ turns: page.data
1521
+ });
1522
+ if (activity) activities.push(activity);
1523
+ } catch {}
1524
+ }
1525
+ return activities;
1526
+ });
1527
+ }
1528
+ function createChecker(params) {
1529
+ return async (probes) => await checkCodexUpstreamActivity(probes, params.control, async (probe) => {
1530
+ const config = params.getRuntimeConfig();
1531
+ const sessionId = params.api.runtime.agent.session.getSessionEntry({
1532
+ agentId: probe.agentId,
1533
+ sessionKey: probe.sessionKey,
1534
+ readConsistency: "latest"
1535
+ })?.sessionId?.trim();
1536
+ if (!sessionId) return probe.threadId;
1537
+ const binding = await params.bindingStore.read(sessionBindingIdentity({
1538
+ sessionId,
1539
+ sessionKey: probe.sessionKey,
1540
+ config
1541
+ }));
1542
+ return binding?.connectionScope === "supervision" && binding.supervisionSourceThreadId === probe.threadId ? binding.threadId : probe.threadId;
1543
+ });
1544
+ }
1545
+ //#endregion
1546
+ //#region extensions/codex/src/session-upstream-marker.ts
1547
+ function lastIdentifiableTurn(thread, normalizeTurnId) {
1548
+ for (let index = (thread.turns?.length ?? 0) - 1; index >= 0; index -= 1) {
1549
+ const turn = thread.turns?.[index];
1550
+ const turnId = normalizeTurnId(turn?.id);
1551
+ if (turn && turnId) return {
1552
+ ...turn,
1553
+ id: turnId
1554
+ };
1555
+ }
1556
+ }
1557
+ function codexUpstreamBaseline(thread, normalizeTurnId) {
1558
+ const turn = lastIdentifiableTurn(thread, normalizeTurnId);
1559
+ return {
1560
+ turnId: turn?.id ?? null,
1561
+ userMessageCount: turn?.items.filter((item) => item.type === "userMessage").length ?? 0
1562
+ };
1563
+ }
1564
+ function codexLastTerminalTurnId(thread, normalizeTurnId) {
1565
+ for (let index = (thread.turns?.length ?? 0) - 1; index >= 0; index -= 1) {
1566
+ const turn = thread.turns?.[index];
1567
+ const turnId = normalizeTurnId(turn?.id);
1568
+ if (!turn || !turnId) continue;
1569
+ if (turn.status === "completed" || turn.status === "interrupted" || turn.status === "failed") return turnId;
1570
+ }
1571
+ }
1572
+ /** Build the upstream link seed for a continued Codex session, if a baseline exists. */
1573
+ function codexUpstreamContinueResult(sessionKey, threadId, baseline) {
1574
+ if (!baseline) return { sessionKey };
1575
+ return {
1576
+ sessionKey,
1577
+ upstream: {
1578
+ kind: "codex-app-server",
1579
+ ref: {
1580
+ connectionFingerprint: baseline.connectionFingerprint,
1581
+ threadId
1582
+ },
1583
+ marker: {
1584
+ turnId: baseline.turnId,
1585
+ userMessageCount: baseline.userMessageCount
1586
+ }
1587
+ }
1588
+ };
1589
+ }
1590
+ //#endregion
1591
+ //#region extensions/codex/src/session-catalog.ts
1592
+ const boundCatalogSessionId = (value) => boundedCatalogString(value, 256);
1593
+ const CODEX_SUPERVISION_SESSION_KEY_PREFIX = "harness:codex:supervision:";
1594
+ function createCodexSessionCatalogControlFromRequests(params) {
1595
+ return {
1596
+ ...params.connectionFingerprint ? { connectionFingerprint: params.connectionFingerprint } : {},
1597
+ withPinnedConnection: params.withPinnedConnection,
1598
+ async listPage(pageParams) {
1599
+ const limit = normalizeLimit(pageParams.limit, "limit");
1600
+ const search = pageParams.searchTerm?.trim().toLocaleLowerCase() || void 0;
1601
+ const cwd = pageParams.cwd?.trim() || void 0;
1602
+ const maxPages = search ? 20 : 1;
1603
+ const sessions = [];
1604
+ let cursor = readControlCursor(pageParams.cursor, "request");
1605
+ let nextCursor;
1606
+ let backwardsCursor;
1607
+ const seenCursors = new Set(cursor ? [cursor] : []);
1608
+ const requests = params.createRequestSnapshot();
1609
+ const deadline = params.now() + requests.requestTimeoutMs;
1610
+ for (let pageIndex = 0; pageIndex < maxPages; pageIndex += 1) {
1611
+ const remaining = limit - sessions.length;
1612
+ const remainingTimeoutMs = Math.ceil(deadline - params.now());
1613
+ if (remainingTimeoutMs <= 0) throw new Error("Codex session catalog listing timed out");
1614
+ const response = await requests.listThreads({
1615
+ archived: false,
1616
+ limit: remaining,
1617
+ modelProviders: [],
1618
+ sortKey: "recency_at",
1619
+ sortDirection: "desc",
1620
+ sourceKinds: [...CODEX_INTERACTIVE_THREAD_SOURCE_KINDS],
1621
+ ...cwd ? { cwd } : {},
1622
+ ...cursor ? { cursor } : {}
1623
+ }, remainingTimeoutMs);
1624
+ if (pageIndex === 0) backwardsCursor = readControlCursor(response.backwardsCursor, "backwards response");
1625
+ sessions.push(...response.data.flatMap((thread) => {
1626
+ const session = toCatalogSession(thread, false);
1627
+ return session ? [session] : [];
1628
+ }).filter((session) => !search || session.name?.toLocaleLowerCase().includes(search)));
1629
+ nextCursor = readControlCursor(response.nextCursor, "next response");
1630
+ if (!nextCursor || sessions.length >= limit) break;
1631
+ if (seenCursors.has(nextCursor)) throw new Error("Codex session catalog returned a repeated search cursor");
1632
+ seenCursors.add(nextCursor);
1633
+ cursor = nextCursor;
1634
+ }
1635
+ return {
1636
+ sessions,
1637
+ ...nextCursor ? { nextCursor } : {},
1638
+ ...backwardsCursor ? { backwardsCursor } : {}
1639
+ };
1640
+ },
1641
+ async listDescendantPage(listParams) {
1642
+ const requests = params.createRequestSnapshot();
1643
+ return await requests.listThreads(listParams, requests.requestTimeoutMs);
1644
+ },
1645
+ async readThread(threadId, includeTurns = false) {
1646
+ return await params.createRequestSnapshot().readThread(threadId, includeTurns);
1647
+ },
1648
+ async listTurnPage(listParams) {
1649
+ return await params.createRequestSnapshot().listThreadTurns(listParams);
1650
+ },
1651
+ async archiveThread(threadId) {
1652
+ await params.createRequestSnapshot().archiveThread(threadId);
1653
+ }
1654
+ };
1655
+ }
1656
+ /** Builds the passive catalog over the Codex plugin's canonical shared client. */
1657
+ function createCodexSessionCatalogControl(params) {
1658
+ const now = params.now ?? Date.now;
1659
+ const getPluginConfig = () => params.getPluginConfig();
1660
+ const createRequestSnapshot = () => {
1661
+ const pluginConfig = getPluginConfig();
1662
+ const runtime = resolveCodexSupervisionAppServerRuntimeOptions({ pluginConfig });
1663
+ const requestOptions = {
1664
+ config: structuredClone(params.getRuntimeConfig()),
1665
+ startOptions: structuredClone(runtime.start)
1666
+ };
1667
+ return {
1668
+ requestTimeoutMs: runtime.requestTimeoutMs,
1669
+ listThreads: async (listParams, timeoutMs) => await codexControlRequest(pluginConfig, CODEX_CONTROL_METHODS.listThreads, listParams, {
1670
+ ...requestOptions,
1671
+ timeoutMs
1672
+ }),
1673
+ readThread: async (threadId, includeTurns) => (await codexControlRequest(pluginConfig, CODEX_CONTROL_METHODS.readThread, {
1674
+ threadId,
1675
+ includeTurns
1676
+ }, requestOptions)).thread,
1677
+ listThreadTurns: async (listParams) => await codexControlRequest(pluginConfig, CODEX_CONTROL_METHODS.listThreadTurns, listParams, requestOptions),
1678
+ archiveThread: async (threadId) => {
1679
+ await codexControlRequest(pluginConfig, CODEX_CONTROL_METHODS.archiveThread, { threadId }, requestOptions);
1680
+ }
1681
+ };
1682
+ };
1683
+ const withPinnedConnection = async (run) => {
1684
+ const runtime = resolveCodexSupervisionAppServerRuntimeOptions({ pluginConfig: getPluginConfig() });
1685
+ const runtimeConfig = structuredClone(params.getRuntimeConfig());
1686
+ const client = await getLeasedSharedCodexAppServerClient({
1687
+ config: runtimeConfig,
1688
+ startOptions: structuredClone(runtime.start),
1689
+ timeoutMs: runtime.requestTimeoutMs
1690
+ });
1691
+ try {
1692
+ const requests = {
1693
+ requestTimeoutMs: runtime.requestTimeoutMs,
1694
+ listThreads: async (listParams, timeoutMs) => await requestCodexAppServerClientJson({
1695
+ client,
1696
+ method: CODEX_CONTROL_METHODS.listThreads,
1697
+ requestParams: listParams,
1698
+ config: runtimeConfig,
1699
+ timeoutMs
1700
+ }),
1701
+ readThread: async (threadId, includeTurns) => (await requestCodexAppServerClientJson({
1702
+ client,
1703
+ method: CODEX_CONTROL_METHODS.readThread,
1704
+ requestParams: {
1705
+ threadId,
1706
+ includeTurns
1707
+ },
1708
+ config: runtimeConfig,
1709
+ timeoutMs: runtime.requestTimeoutMs
1710
+ })).thread,
1711
+ listThreadTurns: async (listParams) => await requestCodexAppServerClientJson({
1712
+ client,
1713
+ method: CODEX_CONTROL_METHODS.listThreadTurns,
1714
+ requestParams: listParams,
1715
+ config: runtimeConfig,
1716
+ timeoutMs: runtime.requestTimeoutMs
1717
+ }),
1718
+ archiveThread: async (threadId) => {
1719
+ await requestCodexAppServerClientJson({
1720
+ client,
1721
+ method: CODEX_CONTROL_METHODS.archiveThread,
1722
+ requestParams: { threadId },
1723
+ config: runtimeConfig,
1724
+ timeoutMs: runtime.requestTimeoutMs
1725
+ });
1726
+ }
1727
+ };
1728
+ const pinnedControl = createCodexSessionCatalogControlFromRequests({
1729
+ connectionFingerprint: buildCodexAppServerConnectionFingerprint(runtime, resolveDefaultAgentDir(runtimeConfig ?? {})),
1730
+ createRequestSnapshot: () => requests,
1731
+ now,
1732
+ withPinnedConnection: async (nestedRun) => await nestedRun(pinnedControl)
1733
+ });
1734
+ return await run(pinnedControl);
1735
+ } finally {
1736
+ releaseLeasedSharedCodexAppServerClient(client);
1737
+ }
1738
+ };
1739
+ return createCodexSessionCatalogControlFromRequests({
1740
+ createRequestSnapshot,
1741
+ now,
1742
+ withPinnedConnection
1743
+ });
1744
+ }
1745
+ async function listGatewayHost(params) {
1746
+ try {
1747
+ const page = parseCatalogPage(await params.control.listPage({
1748
+ limit: params.query.limitPerHost,
1749
+ ...params.query.cursors?.["gateway:local"] ? { cursor: params.query.cursors[CODEX_LOCAL_SESSION_HOST_ID] } : {},
1750
+ ...params.query.search ? { searchTerm: params.query.search } : {}
1751
+ }));
1752
+ const adoptedSessions = await listAdoptedSessionEntries({
1753
+ bindingStore: params.bindingStore,
1754
+ config: params.config,
1755
+ runtime: params.runtime
1756
+ });
1757
+ return {
1758
+ hostId: CODEX_LOCAL_SESSION_HOST_ID,
1759
+ label: "Local Codex",
1760
+ kind: "gateway",
1761
+ connected: true,
1762
+ ...page,
1763
+ sessions: page.sessions.map((session) => {
1764
+ const adopted = adoptedSessions.get(session.threadId);
1765
+ return adopted ? Object.assign({}, session, { openClawSessionKey: adopted.key }) : session;
1766
+ })
1767
+ };
1768
+ } catch (error) {
1769
+ return {
1770
+ hostId: CODEX_LOCAL_SESSION_HOST_ID,
1771
+ label: "Local Codex",
1772
+ kind: "gateway",
1773
+ connected: false,
1774
+ sessions: [],
1775
+ error: catalogError("APP_SERVER_UNAVAILABLE", error)
1776
+ };
1777
+ }
1778
+ }
1779
+ /** Lists Gateway-local and paired-node Codex sessions with per-host failures. */
1780
+ async function listCodexSessionCatalog(params) {
1781
+ const query = readGatewayParams(params.query);
1782
+ const requestedHostIds = query.hostIds ? new Set(query.hostIds) : void 0;
1783
+ const localHosts = !requestedHostIds || requestedHostIds.has("gateway:local") ? [listGatewayHost({
1784
+ bindingStore: params.bindingStore,
1785
+ config: params.config,
1786
+ control: params.control,
1787
+ query,
1788
+ runtime: params.runtime
1789
+ })] : [];
1790
+ if (!(!requestedHostIds || query.hostIds?.some((hostId) => hostId.startsWith("node:")))) return { hosts: await Promise.all(localHosts) };
1791
+ let nodes;
1792
+ try {
1793
+ nodes = (await params.runtime.nodes.list()).nodes.filter((node) => node.commands?.includes("codex.appServer.threads.list.v1") && (!requestedHostIds || requestedHostIds.has(`node:${node.nodeId}`))).slice(0, 100 - localHosts.length);
1794
+ } catch (error) {
1795
+ return { hosts: [...await Promise.all(localHosts), {
1796
+ hostId: "node:registry",
1797
+ label: "Paired nodes",
1798
+ kind: "node",
1799
+ connected: false,
1800
+ sessions: [],
1801
+ error: catalogError("NODE_LIST_FAILED", error)
1802
+ }] };
1803
+ }
1804
+ const adoptedNodeSessions = listNodeAdoptedSessionEntries({
1805
+ config: params.config,
1806
+ runtime: params.runtime
1807
+ });
1808
+ const nodeHosts = nodes.toSorted(compareNodeLabels).map(async (node) => {
1809
+ const host = await listPairedNode({
1810
+ runtime: params.runtime,
1811
+ node,
1812
+ query,
1813
+ adoptedSessions: adoptedNodeSessions
1814
+ });
1815
+ return Object.assign(host, codexNodeTerminalCapability(node));
1816
+ });
1817
+ return { hosts: await Promise.all([...localHosts, ...nodeHosts]) };
1818
+ }
1819
+ /** Builds the node-local read-only Codex app-server catalog command. */
1820
+ function createCodexSessionCatalogNodeHostCommands(control) {
1821
+ return [
1822
+ {
1823
+ command: CODEX_APP_SERVER_THREADS_LIST_COMMAND,
1824
+ cap: CODEX_APP_SERVER_THREADS_CAPABILITY,
1825
+ dangerous: false,
1826
+ handle: async (paramsJSON) => {
1827
+ const pageParams = readPageParams(parseJsonParams(paramsJSON));
1828
+ try {
1829
+ const page = filterCatalogPageByTitle(parseCatalogPage(await control.listPage(pageParams)), pageParams.searchTerm);
1830
+ return JSON.stringify(page);
1831
+ } catch {
1832
+ throw new Error("Codex app-server catalog is unavailable");
1833
+ }
1834
+ }
1835
+ },
1836
+ {
1837
+ command: CODEX_APP_SERVER_THREAD_TURNS_LIST_COMMAND,
1838
+ cap: CODEX_APP_SERVER_THREADS_CAPABILITY,
1839
+ dangerous: false,
1840
+ handle: async (paramsJSON) => {
1841
+ const action = readNodeTranscriptParams(parseJsonParams(paramsJSON));
1842
+ try {
1843
+ await requireCatalogEligibleThread(control, action.threadId);
1844
+ const page = parseTranscriptPage(await control.listTurnPage({
1845
+ threadId: action.threadId,
1846
+ limit: action.limit,
1847
+ sortDirection: "desc",
1848
+ itemsView: "full",
1849
+ ...action.cursor ? { cursor: action.cursor } : {}
1850
+ }));
1851
+ return JSON.stringify(page);
1852
+ } catch (error) {
1853
+ if (error instanceof CatalogParamsError) throw error;
1854
+ throw new Error("Codex app-server transcript is unavailable", { cause: error });
1855
+ }
1856
+ }
1857
+ },
1858
+ createCodexTerminalNodeHostCommand(control)
1859
+ ];
1860
+ }
1861
+ function readNodeTranscriptParams(value) {
1862
+ if (!isRecord(value)) throw new CatalogParamsError("Codex session read parameters must be an object");
1863
+ requireOnlyKeys(value, /* @__PURE__ */ new Set([
1864
+ "threadId",
1865
+ "cursor",
1866
+ "limit"
1867
+ ]));
1868
+ const threadId = readOptionalString(value, "threadId", 256);
1869
+ if (!threadId) throw new CatalogParamsError("threadId is required");
1870
+ const cursor = readOptionalString(value, "cursor", MAX_CURSOR_LENGTH);
1871
+ return {
1872
+ threadId,
1873
+ limit: readBoundedLimit(value.limit, "limit", 20, 50),
1874
+ ...cursor ? { cursor } : {}
1875
+ };
1876
+ }
1877
+ function readBoundedLimit(value, key, fallback, max) {
1878
+ if (value === void 0) return fallback;
1879
+ if (!Number.isInteger(value) || value < 1 || value > max) throw new CatalogParamsError(`${key} must be an integer from 1 to ${max}`);
1880
+ return value;
1881
+ }
1882
+ function flattenTranscriptPageDesc(page) {
1883
+ return page.data.flatMap((turn) => turn.items.toReversed());
1884
+ }
1885
+ /** Reads the persisted transcript for a Gateway-local or paired-node Codex session. */
1886
+ async function readCodexSessionTranscript(params) {
1887
+ if (params.hostId === "gateway:local") {
1888
+ await requireCatalogEligibleThread(params.control, params.threadId);
1889
+ const page = parseTranscriptPage(await params.control.listTurnPage({
1890
+ threadId: params.threadId,
1891
+ limit: params.limit,
1892
+ sortDirection: "desc",
1893
+ itemsView: "full",
1894
+ ...params.cursor ? { cursor: params.cursor } : {}
1895
+ }));
1896
+ return {
1897
+ hostId: params.hostId,
1898
+ label: "Local Codex",
1899
+ threadId: params.threadId,
1900
+ items: flattenTranscriptPageDesc(page),
1901
+ ...page.nextCursor ? { nextCursor: page.nextCursor } : {},
1902
+ ...page.backwardsCursor ? { backwardsCursor: page.backwardsCursor } : {}
1903
+ };
1904
+ }
1905
+ const nodeId = params.hostId.slice(5);
1906
+ const node = (await params.runtime.nodes.list()).nodes.find((candidate) => candidate.nodeId === nodeId && candidate.connected === true && candidate.commands?.includes("codex.appServer.thread.turns.list.v1"));
1907
+ if (!node) throw new CatalogParamsError("paired-node Codex session host is offline or unavailable");
1908
+ const page = parseTranscriptPage(unwrapNodeInvokePayload(await params.runtime.nodes.invoke({
1909
+ nodeId,
1910
+ command: CODEX_APP_SERVER_THREAD_TURNS_LIST_COMMAND,
1911
+ params: {
1912
+ threadId: params.threadId,
1913
+ limit: params.limit,
1914
+ ...params.cursor ? { cursor: params.cursor } : {}
1915
+ },
1916
+ timeoutMs: NODE_INVOKE_TIMEOUT_MS,
1917
+ scopes: ["operator.write"]
1918
+ })));
1919
+ return {
1920
+ hostId: params.hostId,
1921
+ label: nodeLabel(node),
1922
+ threadId: params.threadId,
1923
+ items: flattenTranscriptPageDesc(page),
1924
+ ...page.nextCursor ? { nextCursor: page.nextCursor } : {},
1925
+ ...page.backwardsCursor ? { backwardsCursor: page.backwardsCursor } : {}
1926
+ };
1927
+ }
1928
+ function requireIdleThread(thread, action) {
1929
+ if (thread.status?.type === "idle" || action === "archive" && thread.status?.type === "notLoaded") return;
1930
+ if (thread.status?.type === "active") throw new CatalogParamsError(`Codex session is active in this App Server; wait for it to finish before ${action === "continue" ? "starting a branch" : "archiving"}`);
1931
+ throw new CatalogParamsError(action === "archive" ? "Codex session cannot be archived in its current state" : "Codex session cannot start a branch in its current state");
1932
+ }
1933
+ function adoptionSessionKey(threadId) {
1934
+ const digest = createHash("sha256").update(threadId).digest("hex");
1935
+ return `${CODEX_SUPERVISION_SESSION_KEY_PREFIX}${digest}`;
1936
+ }
1937
+ function isAdoptionSessionKeyForThread(sessionKey, threadId) {
1938
+ return adoptionSessionKeyRest(sessionKey) === adoptionSessionKey(threadId);
1939
+ }
1940
+ async function listAdoptedSessionEntries(params) {
1941
+ const adopted = /* @__PURE__ */ new Map();
1942
+ for (const agentId of listSupervisionAgentIds(params.config ?? {})) for (const { entry, sessionKey } of params.runtime.agent.session.listSessionEntries({ agentId })) {
1943
+ const sessionKeyRest = adoptionSessionKeyRest(sessionKey);
1944
+ if (!sessionKeyRest.startsWith(CODEX_SUPERVISION_SESSION_KEY_PREFIX) || entry.initializationPending === true || entry.agentHarnessId !== "codex" || entry.modelSelectionLocked !== true) continue;
1945
+ const sessionId = entry.sessionId?.trim();
1946
+ if (!sessionId) continue;
1947
+ const binding = await params.bindingStore.read(sessionBindingIdentity({
1948
+ sessionId,
1949
+ sessionKey,
1950
+ config: params.config
1951
+ }));
1952
+ const sourceThreadId = binding?.supervisionSourceThreadId?.trim();
1953
+ const boundThreadId = binding?.threadId.trim();
1954
+ if (binding?.connectionScope !== "supervision" || !sourceThreadId || !boundThreadId || sessionKeyRest !== adoptionSessionKey(sourceThreadId)) continue;
1955
+ if (adopted.has(sourceThreadId)) throw new Error(`multiple OpenClaw sessions adopt Codex thread ${sourceThreadId}`);
1956
+ adopted.set(sourceThreadId, {
1957
+ key: sessionKey,
1958
+ sessionId,
1959
+ agentId,
1960
+ boundThreadId
1961
+ });
1962
+ }
1963
+ return adopted;
1964
+ }
1965
+ async function findAdoptedSessionEntry(params) {
1966
+ return (await listAdoptedSessionEntries(params)).get(params.threadId);
1967
+ }
1968
+ var CodexAdoptionBindingCleanupError = class extends AggregateError {};
1969
+ async function clearCreatedAdoptionBinding(params) {
1970
+ let cleared = false;
1971
+ let clearError;
1972
+ try {
1973
+ cleared = await params.bindingStore.mutate(params.identity, {
1974
+ kind: "clear",
1975
+ threadId: params.sourceThreadId,
1976
+ expectedPendingSupervisionBranch: params.expectedPending
1977
+ });
1978
+ } catch (error) {
1979
+ clearError = error;
1980
+ }
1981
+ if (cleared) return;
1982
+ let current;
1983
+ try {
1984
+ current = await params.bindingStore.read(params.identity);
1985
+ } catch (readError) {
1986
+ throw new CodexAdoptionBindingCleanupError([
1987
+ params.cause,
1988
+ ...clearError ? [clearError] : [],
1989
+ readError
1990
+ ], `OpenClaw session creation failed and the Codex binding could not be verified for ${params.sourceThreadId}`);
1991
+ }
1992
+ if (!matchesPendingSupervisionOwner(current, params.expectedPending)) return;
1993
+ throw new CodexAdoptionBindingCleanupError([params.cause, ...clearError ? [clearError] : []], `OpenClaw session creation failed and the Codex binding could not be cleared for ${params.sourceThreadId}`);
1994
+ }
1995
+ function matchesPendingAdoptionBinding(binding, expected) {
1996
+ const historyCoveredThrough = binding?.historyCoveredThrough;
1997
+ return binding?.threadId === expected.sourceThreadId && binding.connectionScope === "supervision" && binding.supervisionSourceThreadId === expected.sourceThreadId && binding.cwd === expected.cwd && binding.conversationSourceTransferComplete === true && binding.preserveNativeModel === true && binding.pendingSupervisionBranch?.sourceThreadId === expected.sourceThreadId && binding.pendingSupervisionBranch.connectionFingerprint === expected.connectionFingerprint && binding.pendingSupervisionBranch.lastTurnId === expected.lastTurnId && (binding.pendingSupervisionBranch.cleanupThreadIds?.length ?? 0) === 0 && typeof historyCoveredThrough === "string" && Number.isFinite(Date.parse(historyCoveredThrough));
1998
+ }
1999
+ function matchesPendingSupervisionOwner(binding, expected) {
2000
+ const pending = binding?.pendingSupervisionBranch;
2001
+ const cleanupThreadIds = pending?.cleanupThreadIds ?? [];
2002
+ const expectedCleanupThreadIds = expected.cleanupThreadIds ?? [];
2003
+ return binding?.threadId === expected.sourceThreadId && binding.connectionScope === "supervision" && binding.supervisionSourceThreadId === expected.sourceThreadId && pending?.sourceThreadId === expected.sourceThreadId && pending.connectionFingerprint === expected.connectionFingerprint && pending.lastTurnId === expected.lastTurnId && cleanupThreadIds.length === expectedCleanupThreadIds.length && cleanupThreadIds.every((threadId, index) => threadId === expectedCleanupThreadIds[index]);
2004
+ }
2005
+ async function ensurePendingAdoptionBinding(params) {
2006
+ const pending = {
2007
+ sourceThreadId: params.sourceThreadId,
2008
+ connectionFingerprint: params.connectionFingerprint,
2009
+ ...params.lastTurnId ? { lastTurnId: params.lastTurnId } : {}
2010
+ };
2011
+ if (!await reclaimCurrentCodexSessionGeneration({
2012
+ bindingStore: params.bindingStore,
2013
+ identity: params.identity,
2014
+ config: params.config
2015
+ })) throw new Error(`failed to claim the OpenClaw session generation for ${params.sourceThreadId}`);
2016
+ const existing = await params.bindingStore.read(params.identity);
2017
+ if (existing) {
2018
+ if (matchesPendingAdoptionBinding(existing, params)) return;
2019
+ throw new Error(`OpenClaw session is already bound to Codex thread ${existing.threadId}`);
2020
+ }
2021
+ const binding = {
2022
+ threadId: params.sourceThreadId,
2023
+ connectionScope: "supervision",
2024
+ supervisionSourceThreadId: params.sourceThreadId,
2025
+ cwd: params.cwd,
2026
+ historyCoveredThrough: (/* @__PURE__ */ new Date()).toISOString(),
2027
+ conversationSourceTransferComplete: true,
2028
+ preserveNativeModel: true,
2029
+ pendingSupervisionBranch: pending
2030
+ };
2031
+ let stored;
2032
+ try {
2033
+ stored = await params.bindingStore.mutate(params.identity, {
2034
+ kind: "set",
2035
+ if: { kind: "absent" },
2036
+ binding
2037
+ });
2038
+ } catch (error) {
2039
+ if (matchesPendingAdoptionBinding(await params.bindingStore.read(params.identity), params)) return;
2040
+ throw error;
2041
+ }
2042
+ if (stored) return;
2043
+ if (!matchesPendingAdoptionBinding(await params.bindingStore.read(params.identity), params)) throw new Error(`failed to bind OpenClaw session to Codex thread ${params.sourceThreadId}`);
2044
+ }
2045
+ async function createOrReuseAdoptedSession(params) {
2046
+ const existing = await findAdoptedSessionEntry({
2047
+ bindingStore: params.bindingStore,
2048
+ config: params.config,
2049
+ runtime: params.api.runtime,
2050
+ threadId: params.sourceThread.id
2051
+ });
2052
+ if (existing) return existing;
2053
+ let createdBindingIdentity;
2054
+ let createdPendingBinding;
2055
+ try {
2056
+ const label = params.sourceThread.name?.trim() || void 0;
2057
+ const spawnedCwd = params.sourceThread.cwd?.trim() || void 0;
2058
+ const pendingLastTurnId = codexLastTerminalTurnId(params.sourceThread, boundCatalogSessionId);
2059
+ const marker = { sourceThreadId: params.sourceThread.id };
2060
+ const created = await params.api.runtime.agent.session.createSessionEntry({
2061
+ cfg: params.config,
2062
+ key: adoptionSessionKey(params.sourceThread.id),
2063
+ agentId: resolveDefaultAgentId(params.config),
2064
+ recoverMatchingInitialEntry: true,
2065
+ ...label ? { label } : {},
2066
+ ...spawnedCwd ? { spawnedCwd } : {},
2067
+ initialEntry: {
2068
+ agentHarnessId: "codex",
2069
+ modelSelectionLocked: true,
2070
+ pluginExtensions: { codex: { supervision: {
2071
+ ...marker,
2072
+ initializing: true,
2073
+ modelLocked: true
2074
+ } } }
2075
+ },
2076
+ afterCreate: async (entry) => {
2077
+ createdBindingIdentity = sessionBindingIdentity({
2078
+ sessionId: entry.sessionId,
2079
+ sessionKey: entry.key,
2080
+ config: params.config
2081
+ });
2082
+ const storePath = resolveStorePath(params.config.session?.store, { agentId: entry.agentId });
2083
+ await importCodexThreadHistoryToTranscript({
2084
+ thread: params.sourceThread,
2085
+ throughTurnId: pendingLastTurnId ?? null,
2086
+ storePath,
2087
+ sessionId: entry.sessionId,
2088
+ sessionKey: entry.key,
2089
+ agentId: entry.agentId,
2090
+ ...spawnedCwd ? { cwd: spawnedCwd } : {},
2091
+ modelProvider: params.sourceThread.modelProvider,
2092
+ config: params.config
2093
+ });
2094
+ createdPendingBinding = {
2095
+ sourceThreadId: params.sourceThread.id,
2096
+ connectionFingerprint: params.connectionFingerprint,
2097
+ ...pendingLastTurnId ? { lastTurnId: pendingLastTurnId } : {}
2098
+ };
2099
+ await ensurePendingAdoptionBinding({
2100
+ bindingStore: params.bindingStore,
2101
+ config: params.config,
2102
+ identity: createdBindingIdentity,
2103
+ sourceThreadId: params.sourceThread.id,
2104
+ connectionFingerprint: params.connectionFingerprint,
2105
+ cwd: spawnedCwd ?? "",
2106
+ ...pendingLastTurnId ? { lastTurnId: pendingLastTurnId } : {}
2107
+ });
2108
+ return { pluginExtensions: { codex: { supervision: {
2109
+ ...marker,
2110
+ modelLocked: true
2111
+ } } } };
2112
+ }
2113
+ });
2114
+ return {
2115
+ key: created.key,
2116
+ sessionId: created.sessionId,
2117
+ agentId: created.agentId,
2118
+ boundThreadId: params.sourceThread.id
2119
+ };
2120
+ } catch (error) {
2121
+ let raced = await findAdoptedSessionEntry({
2122
+ bindingStore: params.bindingStore,
2123
+ config: params.config,
2124
+ runtime: params.api.runtime,
2125
+ threadId: params.sourceThread.id
2126
+ });
2127
+ if (raced) return raced;
2128
+ if (createdBindingIdentity && createdPendingBinding) {
2129
+ await clearCreatedAdoptionBinding({
2130
+ bindingStore: params.bindingStore,
2131
+ identity: createdBindingIdentity,
2132
+ sourceThreadId: params.sourceThread.id,
2133
+ expectedPending: createdPendingBinding,
2134
+ cause: error
2135
+ });
2136
+ raced = await findAdoptedSessionEntry({
2137
+ bindingStore: params.bindingStore,
2138
+ config: params.config,
2139
+ runtime: params.api.runtime,
2140
+ threadId: params.sourceThread.id
2141
+ });
2142
+ if (raced) return raced;
2143
+ }
2144
+ throw error;
2145
+ }
2146
+ }
2147
+ async function continueLocalCodexSessionInner(params) {
2148
+ await requireCatalogEligibleThread(params.control, params.threadId);
2149
+ const existing = await findAdoptedSessionEntry({
2150
+ bindingStore: params.bindingStore,
2151
+ config: params.config,
2152
+ runtime: params.api.runtime,
2153
+ threadId: params.threadId
2154
+ });
2155
+ if (existing) {
2156
+ const boundThreadId = requireBoundThread(existing);
2157
+ const boundThread = await params.control.readThread(boundThreadId, true);
2158
+ if (boundThread.id !== boundThreadId) throw new Error("Codex app-server returned a different thread than requested");
2159
+ const changedError = () => new CatalogParamsError("Codex OpenClaw session changed before it could be opened. Retry.");
2160
+ if (!await params.api.runtime.agent.session.patchSessionEntry({
2161
+ sessionKey: existing.key,
2162
+ readConsistency: "latest",
2163
+ preserveActivity: true,
2164
+ update: (entry) => {
2165
+ if (entry.sessionId?.trim() !== existing.sessionId || entry.initializationPending === true || entry.agentHarnessId !== "codex" || entry.modelSelectionLocked !== true) throw changedError();
2166
+ return { archivedAt: void 0 };
2167
+ }
2168
+ })) throw changedError();
2169
+ const connectionFingerprint = params.control.connectionFingerprint;
2170
+ if (connectionFingerprint) params.onContinued?.({
2171
+ connectionFingerprint,
2172
+ ...codexUpstreamBaseline(boundThread, boundCatalogSessionId)
2173
+ });
2174
+ return {
2175
+ sessionKey: existing.key,
2176
+ disposition: "existing"
2177
+ };
2178
+ }
2179
+ const sourceThread = await params.control.readThread(params.threadId, true);
2180
+ if (sourceThread.id !== params.threadId) throw new Error("Codex app-server returned a different thread than requested");
2181
+ if (sourceThread.status?.type !== "notLoaded") requireIdleThread(sourceThread, "continue");
2182
+ const connectionFingerprint = params.control.connectionFingerprint;
2183
+ if (!connectionFingerprint) throw new Error("Codex Continue requires a pinned app-server connection");
2184
+ const adopted = await createOrReuseAdoptedSession({
2185
+ api: params.api,
2186
+ bindingStore: params.bindingStore,
2187
+ config: params.config,
2188
+ sourceThread,
2189
+ connectionFingerprint
2190
+ });
2191
+ const boundThreadId = requireBoundThread(adopted);
2192
+ const baselineThread = boundThreadId === sourceThread.id ? sourceThread : await params.control.readThread(boundThreadId, true);
2193
+ if (baselineThread.id !== boundThreadId) throw new Error("Codex app-server returned a different thread than requested");
2194
+ params.onContinued?.({
2195
+ connectionFingerprint,
2196
+ ...codexUpstreamBaseline(baselineThread, boundCatalogSessionId)
2197
+ });
2198
+ return {
2199
+ sessionKey: adopted.key,
2200
+ disposition: "forked"
2201
+ };
2202
+ }
2203
+ /** Creates one locked OpenClaw branch whose first harness run forks the Codex source. */
2204
+ async function continueLocalCodexSession(params) {
2205
+ const sourceKey = adoptedSourceKey(CODEX_LOCAL_SESSION_HOST_ID, params.threadId);
2206
+ const current = continueOperations.get(sourceKey);
2207
+ if (current) return await current;
2208
+ const operation = runSessionActionExclusive(sourceKey, async () => params.control.withPinnedConnection(async (control) => continueLocalCodexSessionInner({
2209
+ ...params,
2210
+ control
2211
+ })));
2212
+ continueOperations.set(sourceKey, operation);
2213
+ try {
2214
+ return await operation;
2215
+ } finally {
2216
+ if (continueOperations.get(sourceKey) === operation) continueOperations.delete(sourceKey);
2217
+ }
2218
+ }
2219
+ async function assertNoPendingSupervisionBranch(params) {
2220
+ const adoptedEntries = listSupervisionAgentIds(params.config).flatMap((agentId) => params.runtime.agent.session.listSessionEntries({ agentId })).filter((candidate) => isAdoptionSessionKeyForThread(candidate.sessionKey, params.threadId));
2221
+ for (const adopted of adoptedEntries) {
2222
+ if (adopted.entry.initializationPending === true) throw new CatalogParamsError("Codex session cannot be archived while its OpenClaw branch is initializing");
2223
+ const sessionId = adopted.entry.sessionId?.trim();
2224
+ if (!sessionId) continue;
2225
+ const binding = await params.bindingStore.read(sessionBindingIdentity({
2226
+ sessionId,
2227
+ sessionKey: adopted.sessionKey,
2228
+ config: params.config
2229
+ }));
2230
+ if (binding?.connectionScope === "supervision" && binding.supervisionSourceThreadId === params.threadId && binding.pendingSupervisionBranch?.sourceThreadId === params.threadId) throw new CatalogParamsError("Codex session cannot be archived until its OpenClaw branch starts");
2231
+ }
2232
+ }
2233
+ /** Archives one inactive Gateway-local Codex thread after a fresh status read. */
2234
+ async function archiveLocalCodexSession(params) {
2235
+ return await runSessionActionExclusive(adoptedSourceKey(CODEX_LOCAL_SESSION_HOST_ID, params.threadId), async () => {
2236
+ return await params.bindingStore.withThreadArchiveFence(async () => {
2237
+ return await params.control.withPinnedConnection(async (control) => {
2238
+ await requireCatalogEligibleThread(control, params.threadId);
2239
+ await assertNoPendingSupervisionBranch(params);
2240
+ const thread = await control.readThread(params.threadId, false);
2241
+ if (thread.id !== params.threadId) throw new Error("Codex app-server returned a different thread than requested");
2242
+ requireIdleThread(thread, "archive");
2243
+ if (await params.bindingStore.hasOtherThreadOwner(params.threadId)) throw new CatalogParamsError("Codex session cannot be archived while it is attached to an OpenClaw session");
2244
+ await assertCodexArchiveDescendantsUnowned({
2245
+ bindingStore: params.bindingStore,
2246
+ threadId: params.threadId,
2247
+ listPage: (request) => control.listDescendantPage(request),
2248
+ assertDescendantIdle: async (descendantThreadId) => {
2249
+ const descendant = await control.readThread(descendantThreadId, false);
2250
+ if (descendant.id !== descendantThreadId) throw new Error("Codex app-server returned a different descendant than requested");
2251
+ requireIdleThread(descendant, "archive");
2252
+ }
2253
+ });
2254
+ await control.archiveThread(params.threadId);
2255
+ return { archived: true };
2256
+ });
2257
+ });
2258
+ });
2259
+ }
2260
+ /** Allows read-only catalog and transcript commands on supported paired-node platforms. */
2261
+ function createCodexSessionCatalogNodeInvokePolicies() {
2262
+ return [{
2263
+ commands: [
2264
+ CODEX_APP_SERVER_THREADS_LIST_COMMAND,
2265
+ CODEX_APP_SERVER_THREAD_TURNS_LIST_COMMAND,
2266
+ CODEX_TERMINAL_RESUME_COMMAND
2267
+ ],
2268
+ defaultPlatforms: [
2269
+ "macos",
2270
+ "linux",
2271
+ "windows"
2272
+ ],
2273
+ handle: (context) => context.command === "codex.terminal.resume.v1" ? { ok: true } : context.invokeNode()
2274
+ }];
2275
+ }
2276
+ function toGenericCatalogHost(host, localTerminalAvailable) {
2277
+ const local = host.hostId === CODEX_LOCAL_SESSION_HOST_ID;
2278
+ return {
2279
+ hostId: host.hostId,
2280
+ label: host.label,
2281
+ kind: host.kind,
2282
+ connected: host.connected,
2283
+ ...host.nodeId ? { nodeId: host.nodeId } : {},
2284
+ sessions: host.sessions.map((session) => {
2285
+ const continuableStatus = !session.archived && (session.status === "idle" || session.status === "notLoaded");
2286
+ const canContinue = (local || host.canContinueCodex === true) && continuableStatus && isInteractiveThreadSource(session.source);
2287
+ const canArchive = local && continuableStatus && isInteractiveThreadSource(session.source);
2288
+ const canOpenTerminal = isInteractiveThreadSource(session.source) && (local ? localTerminalAvailable : host.canOpenTerminalCodex === true);
2289
+ return {
2290
+ threadId: session.threadId,
2291
+ ...session.name != null ? { name: session.name } : {},
2292
+ ...session.cwd ? { cwd: session.cwd } : {},
2293
+ status: session.status,
2294
+ ...session.createdAt != null ? { createdAt: session.createdAt } : {},
2295
+ ...session.updatedAt != null ? { updatedAt: session.updatedAt } : {},
2296
+ ...session.recencyAt != null ? { recencyAt: session.recencyAt } : {},
2297
+ ...session.source ? { source: session.source } : {},
2298
+ ...session.modelProvider ? { modelProvider: session.modelProvider } : {},
2299
+ ...session.cliVersion ? { cliVersion: session.cliVersion } : {},
2300
+ ...session.gitBranch ? { gitBranch: session.gitBranch } : {},
2301
+ archived: session.archived,
2302
+ ...session.openClawSessionKey ? { openClawSessionKey: session.openClawSessionKey } : {},
2303
+ canContinue,
2304
+ canArchive,
2305
+ canOpenTerminal
2306
+ };
2307
+ }),
2308
+ ...host.nextCursor ? { nextCursor: host.nextCursor } : {},
2309
+ ...host.error ? { error: host.error } : {}
2310
+ };
2311
+ }
2312
+ function registerCodexSessionCatalog(params) {
2313
+ const provider = {
2314
+ id: "codex",
2315
+ label: "Codex",
2316
+ list: async (query) => {
2317
+ const localTerminalAvailable = resolveLocalCodexTerminalExecutable() !== void 0;
2318
+ return (await listCodexSessionCatalog({
2319
+ bindingStore: params.bindingStore,
2320
+ config: params.getRuntimeConfig(),
2321
+ runtime: params.api.runtime,
2322
+ control: params.control,
2323
+ query
2324
+ })).hosts.map((host) => toGenericCatalogHost(host, localTerminalAvailable));
2325
+ },
2326
+ read: async (request) => {
2327
+ const page = await readCodexSessionTranscript({
2328
+ runtime: params.api.runtime,
2329
+ control: params.control,
2330
+ hostId: request.hostId,
2331
+ threadId: request.threadId,
2332
+ cursor: request.cursor,
2333
+ limit: request.limit ?? 20
2334
+ });
2335
+ return {
2336
+ ...page,
2337
+ items: page.items.map(toGenericTranscriptItem)
2338
+ };
2339
+ },
2340
+ continueSession: async (request) => {
2341
+ const config = params.getRuntimeConfig();
2342
+ if (!config) throw new Error("OpenClaw runtime config is unavailable");
2343
+ if (request.hostId.startsWith("node:")) return await continueNodeCodexSession({
2344
+ api: params.api,
2345
+ config,
2346
+ hostId: request.hostId,
2347
+ threadId: request.threadId,
2348
+ clientScopes: request.clientScopes
2349
+ });
2350
+ if (request.hostId !== "gateway:local") throw new CatalogParamsError("Codex session catalog hostId is invalid");
2351
+ let upstreamBaseline;
2352
+ return codexUpstreamContinueResult((await continueLocalCodexSession({
2353
+ api: params.api,
2354
+ bindingStore: params.bindingStore,
2355
+ config,
2356
+ control: params.control,
2357
+ threadId: request.threadId,
2358
+ onContinued: (baseline) => {
2359
+ upstreamBaseline = baseline;
2360
+ }
2361
+ })).sessionKey, request.threadId, upstreamBaseline);
2362
+ },
2363
+ checkUpstreamActivity: createChecker(params),
2364
+ archive: async (request) => {
2365
+ if (request.confirmNoOtherRunner !== true) throw new CatalogParamsError("archive requires confirmation that no other runner is active");
2366
+ if (request.hostId !== "gateway:local") throw new CatalogParamsError("paired-node Codex sessions are view-only");
2367
+ const config = params.getRuntimeConfig();
2368
+ if (!config) throw new Error("OpenClaw runtime config is unavailable");
2369
+ await archiveLocalCodexSession({
2370
+ bindingStore: params.bindingStore,
2371
+ config,
2372
+ control: params.control,
2373
+ runtime: params.api.runtime,
2374
+ threadId: request.threadId
2375
+ });
2376
+ return { ok: true };
2377
+ },
2378
+ openTerminal: (request) => openCodexCatalogTerminal({
2379
+ api: params.api,
2380
+ control: params.control,
2381
+ parseCatalogPage,
2382
+ ...request
2383
+ })
2384
+ };
2385
+ params.api.registerSessionCatalog(provider);
2386
+ }
2387
+ const codexSessionCatalogRuntime = {
2388
+ register: registerCodexSessionCatalog,
2389
+ list: listCodexSessionCatalog,
2390
+ readTranscript: readCodexSessionTranscript,
2391
+ continueLocal: continueLocalCodexSession,
2392
+ continueNode: continueNodeCodexSession,
2393
+ archiveLocal: archiveLocalCodexSession
2394
+ };
2395
+ //#endregion
2396
+ export { CODEX_LOCAL_SESSION_HOST_ID as a, requestOptions as c, createCodexCliSessionNodeHostCommands as d, createCodexCliSessionNodeInvokePolicies as f, resumeCodexCliSessionOnNode as g, resolveCodexCliSessionForBindingOnNode as h, createCodexSessionCatalogNodeInvokePolicies as i, safeCodexControlRequest as l, listCodexCliSessionsOnNode as m, createCodexSessionCatalogControl as n, codexControlRequest as o, formatCodexCliSessions as p, createCodexSessionCatalogNodeHostCommands as r, readCodexStatusProbes as s, codexSessionCatalogRuntime as t, assertCodexArchiveDescendantsUnowned as u };