@openclaw/codex 2026.8.1-beta.2 → 2026.8.1-beta.3

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 (59) hide show
  1. package/dist/api.js +147 -0
  2. package/dist/app-server-policy-bg5QPSVP.js +25 -0
  3. package/dist/{attempt-client-cleanup-DMLXUfa8.js → attempt-client-cleanup-URjUlCHg.js} +1 -1
  4. package/dist/{binding-connection-I5Bo-AsX.js → binding-connection-DAvZwp96.js} +15 -3
  5. package/dist/{bounded-turn-qgZmzy_4.js → bounded-turn-4BjrZEwW.js} +22 -16
  6. package/dist/{capabilities-CybGI1oz.js → capabilities-CH3K57v4.js} +1 -1
  7. package/dist/cli-metadata.js +1 -1
  8. package/dist/{command-formatters-B1AEz8ZA.js → command-formatters-D0nZ3qtd.js} +2 -2
  9. package/dist/{command-handlers-Db8pu_ds.js → command-handlers-DC86lgT4.js} +39 -22
  10. package/dist/command-rpc-xUIIonsO.js +90 -0
  11. package/dist/{compact-LspQaGfl.js → compact-COqgBRny.js} +35 -29
  12. package/dist/{session-binding-DPxIvMgE.js → config-CpNeo5AE.js} +28 -888
  13. package/dist/context-compaction-activity-BOeJf7yo.js +58 -0
  14. package/dist/doctor-contract-api.js +15 -14
  15. package/dist/{dynamic-tools-BVdyWkUD.js → dynamic-tools-fJZhl4CE.js} +1284 -651
  16. package/dist/{effective-mcp-catalog-DIIhAtOZ.js → effective-mcp-catalog-4YKn51aG.js} +2 -2
  17. package/dist/elicitation-input-DMkeki8d.js +71 -0
  18. package/dist/{event-projector-usage-e5huIQWu.js → event-projector-usage-CLwNEJqW.js} +16 -16
  19. package/dist/harness-DoKQcF8n.js +258 -0
  20. package/dist/harness.js +2 -226
  21. package/dist/index.js +171 -48
  22. package/dist/{isolated-completion-C58NbL40.js → isolated-completion-DPMtlH9r.js} +3 -3
  23. package/dist/managed-binary-By4FZC6i.js +212 -0
  24. package/dist/media-understanding-provider.js +2 -2
  25. package/dist/model-catalog-CmGoqVKB.js +48 -0
  26. package/dist/{models-BFlpn1Ex.js → models-CZw1wClT.js} +2 -2
  27. package/dist/{native-subagent-monitor-DyTGmTUx.js → native-subagent-monitor-GGq3RnOo.js} +185 -71
  28. package/dist/node-exec-server.runtime-Bsu1LdFZ.js +192 -0
  29. package/dist/{plugin-app-cache-key-C3H2CVp6.js → plugin-app-cache-key-DMNQ8Zi8.js} +15 -3
  30. package/dist/plugin-approval-roundtrip-C75Yiiwc.js +95 -0
  31. package/dist/{plugin-marketplace-discovery-CzrGJdCR.js → plugin-marketplace-discovery-CSWDkMrP.js} +40 -26
  32. package/dist/{plugin-metadata-cache-CNJPj4_b.js → plugin-metadata-cache-BXH5NF-4.js} +8 -5
  33. package/dist/{protocol-validators-B4F7L8Rf.js → protocol-validators-VAVyEnDp.js} +55 -2
  34. package/dist/{rate-limits-DZXmOL8-.js → rate-limits-BI1dMHDG.js} +1 -1
  35. package/dist/{request-BRl5_UfX.js → request-DSK0W6YI.js} +2 -2
  36. package/dist/{run-attempt-P8BdxC1I.js → run-attempt-BV3HzuCf.js} +1098 -666
  37. package/dist/{runtime-artifact-BVSPdd6g.js → runtime-artifact-BvyT1uyd.js} +4 -7
  38. package/dist/{sandbox-guard-Cmwc9eIk.js → sandbox-guard-C1qEeZ5C.js} +25 -19
  39. package/dist/session-binding-BTrLf9Qm.js +874 -0
  40. package/dist/{session-catalog-xwNi_SDM.js → session-catalog-ezP_UcRM.js} +1541 -1296
  41. package/dist/session-catalog-home-id-BNwAJ9D7.js +18 -0
  42. package/dist/{session-cli-7hgXZDV7.js → session-cli-B1hKvfkI.js} +20 -7
  43. package/dist/{session-retirement-DoMMJFKm.js → session-retirement-CE4iMiLh.js} +3 -3
  44. package/dist/{session-upstream-marker-CrIFX06J.js → session-upstream-marker-D_2npHlA.js} +1 -1
  45. package/dist/{settled-turn-finalizer-BRWBO4MO.js → settled-turn-finalizer-Dx1onIqg.js} +23 -19
  46. package/dist/{shared-client-kIBbGjk4.js → shared-client-Dw-rzii0.js} +755 -273
  47. package/dist/{side-question-9HAL26l9.js → side-question-DbXkJZzg.js} +52 -30
  48. package/dist/{thread-lifecycle-Bu1BTDXr.js → thread-lifecycle-DKdrnTMX.js} +310 -92
  49. package/dist/{thread-ownership-6kOQHlyC.js → thread-ownership-i2NBwDUL.js} +2 -2
  50. package/dist/{thread-resume-CPrJNQVM.js → thread-resume-CdJk2Ehf.js} +3 -3
  51. package/dist/{transcript-mirror-B9gGgnJS.js → transcript-mirror-BU0_LKAL.js} +46 -28
  52. package/dist/{transport-stdio-DIImwDVS.js → transport-stdio-DhoGy33Z.js} +2 -2
  53. package/dist/{upstream-prompt-provenance-ClRBije0.js → upstream-prompt-provenance-CFmsKwr9.js} +12 -14
  54. package/dist/{upstream-session-fork-CE-_LdnP.js → upstream-session-fork-BDVJCLAD.js} +12 -6
  55. package/dist/{usage-CHlV0e-o.js → usage-9dKJCgcy.js} +3 -3
  56. package/dist/{web-search-provider.runtime-DRw2Vl42.js → web-search-provider.runtime-DCdx4DGu.js} +3 -3
  57. package/openclaw.plugin.json +31 -1
  58. package/package.json +6 -5
  59. package/dist/app-server-policy-zcBDc1JU.js +0 -49
@@ -1,32 +1,37 @@
1
- import { c as reclaimCurrentCodexSessionGeneration, d as sessionBindingIdentity, g as resolveCodexAppServerRuntimeOptions, y as resolveCodexSupervisionAppServerRuntimeOptions } from "./session-binding-DPxIvMgE.js";
2
- import { R as resolveCodexAppServerClientInstanceId, _ as withTimeout, a as getLeasedSharedCodexAppServerClient, at as CODEX_INTERACTIVE_THREAD_SOURCE_KINDS, ct as isJsonObject, dt as resolveCodexAppServerLocalHomeDir, it as CODEX_INTERACTIVE_CUSTOM_THREAD_SOURCES, l as releaseLeasedSharedCodexAppServerClient, z as CodexAppServerRpcError } from "./shared-client-kIBbGjk4.js";
3
- import { n as assertCodexThreadForkParams } from "./protocol-validators-B4F7L8Rf.js";
4
- import { n as listCodexAppServerModels } from "./models-BFlpn1Ex.js";
5
- import { t as buildCodexAppServerConnectionFingerprint } from "./plugin-app-cache-key-C3H2CVp6.js";
6
- import { r as importCodexThreadHistoryToTranscript } from "./transcript-mirror-B9gGgnJS.js";
7
- import { n as describeControlFailure, t as CODEX_CONTROL_METHODS } from "./capabilities-CybGI1oz.js";
8
- import { r as formatCodexDisplayText } from "./command-formatters-B1AEz8ZA.js";
1
+ import { h as readCodexPluginConfig, s as resolveCodexSupervisionAppServerRuntimeOptions, x as resolveCodexAppServerUserHomeDir } from "./config-CpNeo5AE.js";
2
+ import { B as CodexAppServerRpcError, R as resolveCodexAppServerClientInstanceId, _ as withTimeout, a as getLeasedSharedCodexAppServerClient, ft as resolveCodexAppServerHomeDir, l as releaseLeasedSharedCodexAppServerClient, ot as CODEX_INTERACTIVE_CUSTOM_THREAD_SOURCES, pt as resolveCodexAppServerLocalHomeDir, st as CODEX_INTERACTIVE_THREAD_SOURCE_KINDS, ut as isJsonObject } from "./shared-client-Dw-rzii0.js";
3
+ import { c as reclaimCurrentCodexSessionGeneration, d as sessionBindingIdentity } from "./session-binding-BTrLf9Qm.js";
4
+ import { n as assertCodexThreadForkParams } from "./protocol-validators-VAVyEnDp.js";
5
+ import { i as replaceCodexCatalogConnectionHomes, t as buildCodexAppServerConnectionFingerprint } from "./plugin-app-cache-key-DMNQ8Zi8.js";
6
+ import { n as codexCatalogHomeId, t as canonicalCodexCatalogHome } from "./session-catalog-home-id-BNwAJ9D7.js";
7
+ import { r as importCodexThreadHistoryToTranscript } from "./transcript-mirror-BU0_LKAL.js";
8
+ import { t as CODEX_CONTROL_METHODS } from "./capabilities-CH3K57v4.js";
9
+ import { r as formatCodexDisplayText } from "./command-formatters-D0nZ3qtd.js";
9
10
  import { n as createCodexCliNodeConversationBindingData } from "./conversation-binding-data-CW2GuupV.js";
10
- import { i as withCodexAppServerJsonClient, n as requestCodexAppServerClientJson, r as requestCodexAppServerJson } from "./request-BRl5_UfX.js";
11
- import { i as createImportedCodexSession, n as codexUpstreamBaseline, r as codexUpstreamContinueResult, t as codexLastTerminalTurnId } from "./session-upstream-marker-CrIFX06J.js";
11
+ import { n as requestCodexAppServerClientJson } from "./request-DSK0W6YI.js";
12
+ import { t as codexControlRequest } from "./command-rpc-xUIIonsO.js";
13
+ import { i as createImportedCodexSession, n as codexUpstreamBaseline, r as codexUpstreamContinueResult, t as codexLastTerminalTurnId } from "./session-upstream-marker-D_2npHlA.js";
12
14
  import { asFiniteNumber, isRecord, normalizeBoundedOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
13
15
  import fs from "node:fs/promises";
14
16
  import path from "node:path";
15
17
  import { timestampMsToIsoString } from "openclaw/plugin-sdk/number-runtime";
16
18
  import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/temp-path";
17
- import { createHash } from "node:crypto";
19
+ import fsSync from "node:fs";
18
20
  import os from "node:os";
19
- import { listAgentIds, resolveAllowedModelRef, resolveDefaultAgentDir, resolveDefaultAgentId, resolveDefaultModelForAgent } from "openclaw/plugin-sdk/agent-runtime";
20
- import { parseAgentSessionKey } from "openclaw/plugin-sdk/routing";
21
+ import { listAgentIds, resolveAgentDir, resolveAllowedModelRef, resolveDefaultAgentDir, resolveDefaultAgentId, resolveDefaultModelForAgent, resolveSessionAgentIds } from "openclaw/plugin-sdk/agent-runtime";
22
+ import { pruneMapToMaxSize } from "openclaw/plugin-sdk/collection-runtime";
23
+ import { runCommandBuffered } from "openclaw/plugin-sdk/process-runtime";
21
24
  import { materializeWindowsSpawnProgram, resolveWindowsSpawnProgram } from "openclaw/plugin-sdk/windows-spawn";
22
25
  import { resolveStorePath } from "openclaw/plugin-sdk/session-store-runtime";
23
- import { spawn } from "node:child_process";
24
26
  import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
25
27
  import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
26
28
  import { sanitizeTerminalText } from "openclaw/plugin-sdk/text-chunking";
29
+ import { parseAgentSessionKey } from "openclaw/plugin-sdk/routing";
30
+ import { root } from "openclaw/plugin-sdk/file-access-runtime";
27
31
  import process$1 from "node:process";
28
- import { listSessionCatalogEntries } from "openclaw/plugin-sdk/session-catalog";
32
+ import { createSessionCatalogAdoptionCoordinator, listSessionCatalogEntries, sessionCatalogAdoptedSessionKey, sessionCatalogAdoptedSourceKey } from "openclaw/plugin-sdk/session-catalog";
29
33
  import { decodeNodePtyResumeParams, resolveNodeHostExecutable, runNodePtyCommand } from "openclaw/plugin-sdk/node-host";
34
+ import { createZstdDecompress } from "node:zlib";
30
35
  //#region extensions/codex/src/jsonl-lines.ts
31
36
  const JSONL_STREAM_THRESHOLD_BYTES = 4 * 1024 * 1024;
32
37
  const JSONL_READ_CHUNK_BYTES = 1024 * 1024;
@@ -132,11 +137,6 @@ const MAX_SESSION_LIMIT = 50;
132
137
  const DEFAULT_RESUME_TIMEOUT_MS = 20 * 6e4;
133
138
  const SESSION_ID_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/;
134
139
  const activeResumeSessions = /* @__PURE__ */ new Set();
135
- const DEFAULT_RESUME_SPAWN_RUNTIME = {
136
- platform: process$1.platform,
137
- env: process$1.env,
138
- execPath: process$1.execPath
139
- };
140
140
  function createCodexCliSessionNodeHostCommands() {
141
141
  return [{
142
142
  command: CODEX_CLI_SESSIONS_LIST_COMMAND,
@@ -272,7 +272,7 @@ async function resumeLocalCodexCliSession(paramsJSON) {
272
272
  async function runCodexExecResume(params) {
273
273
  const outputPath = path.join(await fs.mkdtemp(path.join(resolvePreferredOpenClawTmpDir(), "openclaw-codex-cli-")), "last-message.txt");
274
274
  try {
275
- const invocation = resolveCodexCliResumeSpawnInvocation([
275
+ const args = [
276
276
  "exec",
277
277
  "resume",
278
278
  "--skip-git-repo-check",
@@ -280,45 +280,27 @@ async function runCodexExecResume(params) {
280
280
  outputPath,
281
281
  params.sessionId,
282
282
  "-"
283
- ], {
283
+ ];
284
+ const invocation = materializeWindowsSpawnProgram(resolveWindowsSpawnProgram({
285
+ command: "codex",
284
286
  platform: process$1.platform,
285
287
  env: process$1.env,
286
- execPath: process$1.execPath
287
- });
288
- const child = spawn(invocation.command, invocation.args, {
288
+ execPath: process$1.execPath,
289
+ packageName: "@openai/codex"
290
+ }), args);
291
+ const result = await runCommandBuffered([invocation.command, ...invocation.argv], {
289
292
  cwd: params.cwd || process$1.cwd(),
290
- stdio: [
291
- "pipe",
292
- "pipe",
293
- "pipe"
294
- ],
293
+ input: params.prompt,
295
294
  env: process$1.env,
296
- shell: invocation.shell,
297
- windowsHide: invocation.windowsHide
298
- });
299
- const stdout = [];
300
- const stderr = [];
301
- let timedOut = false;
302
- let forceKillTimeout;
303
- const timeout = setTimeout(() => {
304
- timedOut = true;
305
- child.kill("SIGTERM");
306
- forceKillTimeout = setTimeout(() => child.kill("SIGKILL"), 2e3);
307
- forceKillTimeout.unref?.();
308
- }, params.timeoutMs);
309
- child.stdout.on("data", (chunk) => stdout.push(chunk));
310
- child.stderr.on("data", (chunk) => stderr.push(chunk));
311
- child.stdin.end(params.prompt);
312
- const exitCode = await new Promise((resolve, reject) => {
313
- child.on("error", reject);
314
- child.on("exit", (code) => resolve(code));
315
- }).finally(() => {
316
- clearTimeout(timeout);
317
- if (forceKillTimeout) clearTimeout(forceKillTimeout);
295
+ killGraceMs: 2e3,
296
+ killProcessTree: false,
297
+ terminateOnOutputError: true,
298
+ timeoutMs: params.timeoutMs
318
299
  });
319
- if (timedOut) throw new Error(`codex exec resume timed out after ${String(params.timeoutMs)}ms`);
320
- if (exitCode !== 0) {
321
- const message = Buffer.concat(stderr).toString("utf8").trim() || Buffer.concat(stdout).toString("utf8").trim() || `codex exec resume exited with code ${String(exitCode)}`;
300
+ if (result.termination === "timeout") throw new Error(`codex exec resume timed out after ${String(params.timeoutMs)}ms`);
301
+ if (result.termination === "error" && result.error) throw result.error;
302
+ if (result.code !== 0) {
303
+ const message = result.stderr.toString("utf8").trim() || result.stdout.toString("utf8").trim() || `codex exec resume exited with code ${String(result.code)}`;
322
304
  throw new Error(message);
323
305
  }
324
306
  return await fs.readFile(outputPath, "utf8");
@@ -329,21 +311,6 @@ async function runCodexExecResume(params) {
329
311
  });
330
312
  }
331
313
  }
332
- function resolveCodexCliResumeSpawnInvocation(args, runtime = DEFAULT_RESUME_SPAWN_RUNTIME) {
333
- const resolved = materializeWindowsSpawnProgram(resolveWindowsSpawnProgram({
334
- command: "codex",
335
- platform: runtime.platform,
336
- env: runtime.env,
337
- execPath: runtime.execPath,
338
- packageName: "@openai/codex"
339
- }), args);
340
- return {
341
- command: resolved.command,
342
- args: resolved.argv,
343
- shell: resolved.shell,
344
- windowsHide: resolved.windowsHide
345
- };
346
- }
347
314
  async function readHistorySessions(codexHome) {
348
315
  const summaries = /* @__PURE__ */ new Map();
349
316
  if (!(await visitJsonlLines(path.join(codexHome, "history.jsonl"), (line) => {
@@ -579,91 +546,6 @@ function formatNodeLabel(node) {
579
546
  ].filter(Boolean).join(" / ") || "node";
580
547
  }
581
548
  //#endregion
582
- //#region extensions/codex/src/command-rpc.ts
583
- function requestOptions(pluginConfig, limit, config, agentDir) {
584
- const runtime = resolveCodexAppServerRuntimeOptions({ pluginConfig });
585
- return {
586
- limit,
587
- timeoutMs: runtime.requestTimeoutMs,
588
- startOptions: runtime.start,
589
- config,
590
- agentDir
591
- };
592
- }
593
- async function codexControlRequest(pluginConfig, method, requestParams, options = {}) {
594
- const runtime = options.startOptions ? resolveCodexSupervisionAppServerRuntimeOptions({ pluginConfig }) : resolveCodexAppServerRuntimeOptions({ pluginConfig });
595
- const controlRequestOptions = {
596
- timeoutMs: options.timeoutMs ?? runtime.requestTimeoutMs,
597
- startOptions: options.startOptions ?? runtime.start,
598
- config: options.config,
599
- sessionKey: options.sessionKey,
600
- sessionId: options.sessionId,
601
- authProfileId: options.authProfileId,
602
- agentDir: options.agentDir,
603
- isolated: options.isolated
604
- };
605
- if (options.onResponse) return await withCodexAppServerJsonClient(controlRequestOptions, async (request, client) => {
606
- const response = await request({
607
- method,
608
- requestParams
609
- });
610
- await options.onResponse(response, client);
611
- return response;
612
- });
613
- return await requestCodexAppServerJson({
614
- method,
615
- requestParams,
616
- ...controlRequestOptions
617
- });
618
- }
619
- async function safeCodexControlRequest(pluginConfig, method, requestParams, options = {}) {
620
- return await safeValue(async () => await codexControlRequest(pluginConfig, method, requestParams, options));
621
- }
622
- async function safeCodexModelList(pluginConfig, limit, config, agentDir) {
623
- return await safeValue(async () => await listCodexAppServerModels(requestOptions(pluginConfig, limit, config, agentDir)));
624
- }
625
- async function readCodexStatusProbes(pluginConfig, config, agentDir) {
626
- const [models, account, limits, mcps, skills] = await Promise.all([
627
- safeCodexModelList(pluginConfig, 20, config, agentDir),
628
- safeCodexControlRequest(pluginConfig, CODEX_CONTROL_METHODS.account, { refreshToken: false }, {
629
- config,
630
- agentDir
631
- }),
632
- safeCodexControlRequest(pluginConfig, CODEX_CONTROL_METHODS.rateLimits, void 0, {
633
- config,
634
- agentDir
635
- }),
636
- safeCodexControlRequest(pluginConfig, CODEX_CONTROL_METHODS.listMcpServers, { limit: 100 }, {
637
- config,
638
- agentDir
639
- }),
640
- safeCodexControlRequest(pluginConfig, CODEX_CONTROL_METHODS.listSkills, {}, {
641
- config,
642
- agentDir
643
- })
644
- ]);
645
- return {
646
- models,
647
- account,
648
- limits,
649
- mcps,
650
- skills
651
- };
652
- }
653
- async function safeValue(read) {
654
- try {
655
- return {
656
- ok: true,
657
- value: await read()
658
- };
659
- } catch (error) {
660
- return {
661
- ok: false,
662
- error: describeControlFailure(error)
663
- };
664
- }
665
- }
666
- //#endregion
667
549
  //#region extensions/codex/src/app-server/thread-archive-guard.ts
668
550
  const DESCENDANT_PAGE_LIMIT = 100;
669
551
  const MAX_DESCENDANT_PAGES = 100;
@@ -714,29 +596,6 @@ async function assertCodexArchiveDescendantsUnowned(params) {
714
596
  throw new Error("Codex descendant enumeration exceeded its safety limit");
715
597
  }
716
598
  //#endregion
717
- //#region extensions/codex/src/session-catalog-create.ts
718
- const CODEX_AGENT_RUNTIME_ID = "codex";
719
- const CODEX_CATALOG_DEFAULT_MODEL_REF = "openai/gpt-5.6-sol";
720
- function resolveCodexCatalogCreateSession(config, requestedAgentId) {
721
- if (!config) return;
722
- const agentId = requestedAgentId ?? resolveDefaultAgentId(config);
723
- const defaultModel = resolveDefaultModelForAgent({
724
- cfg: config,
725
- agentId
726
- });
727
- return "error" in resolveAllowedModelRef({
728
- cfg: config,
729
- catalog: [],
730
- raw: CODEX_CATALOG_DEFAULT_MODEL_REF,
731
- defaultProvider: defaultModel.provider,
732
- defaultModel: defaultModel.model,
733
- agentId
734
- }) ? void 0 : {
735
- model: CODEX_CATALOG_DEFAULT_MODEL_REF,
736
- agentRuntime: CODEX_AGENT_RUNTIME_ID
737
- };
738
- }
739
- //#endregion
740
599
  //#region extensions/codex/src/session-catalog-parsing.ts
741
600
  const DEFAULT_PAGE_LIMIT = 50;
742
601
  const CODEX_APP_SERVER_THREADS_CAPABILITY = "codex-app-server-threads";
@@ -1030,24 +889,9 @@ function adoptionSessionKeyRest(sessionKey) {
1030
889
  const trimmed = sessionKey.trim();
1031
890
  return parseAgentSessionKey(trimmed)?.rest ?? trimmed;
1032
891
  }
1033
- function listSupervisionAgentIds(config) {
1034
- const defaultAgentId = resolveDefaultAgentId(config);
1035
- return [defaultAgentId, ...listAgentIds(config).filter((agentId) => agentId !== defaultAgentId)];
1036
- }
1037
- function adoptedSourceKey(hostId, threadId) {
1038
- return `${hostId}\u0000${threadId}`;
1039
- }
1040
- function lastTerminalTurnId(thread) {
1041
- for (let index = (thread.turns?.length ?? 0) - 1; index >= 0; index -= 1) {
1042
- const turn = thread.turns?.[index];
1043
- const turnId = boundedCatalogString(turn?.id, 256);
1044
- if (!turnId) continue;
1045
- if (turn?.status === "completed" || turn?.status === "interrupted" || turn?.status === "failed") return turnId;
1046
- }
1047
- }
1048
892
  function nodeAdoptionSessionKey(hostId, threadId) {
1049
- const digest = createHash("sha256").update(JSON.stringify([hostId, threadId])).digest("hex");
1050
- return `${CODEX_NODE_SESSION_KEY_PREFIX}${digest}`;
893
+ const source = JSON.stringify([hostId, threadId]);
894
+ return sessionCatalogAdoptedSessionKey(CODEX_NODE_SESSION_KEY_PREFIX, source);
1051
895
  }
1052
896
  function readNodeSessionMarker(entry) {
1053
897
  const codex = isRecord(entry.pluginExtensions?.codex) ? entry.pluginExtensions.codex : void 0;
@@ -1063,6 +907,7 @@ function readNodeSessionMarker(entry) {
1063
907
  function listNodeAdoptedSessionEntries(params) {
1064
908
  const adopted = /* @__PURE__ */ new Map();
1065
909
  for (const { agentId, entry, sessionKey } of listSessionCatalogEntries({
910
+ ...params.agentId ? { agentId: params.agentId } : {},
1066
911
  config: params.config ?? {},
1067
912
  runtime: params.runtime,
1068
913
  sessionEntries: params.sessionEntries
@@ -1070,7 +915,7 @@ function listNodeAdoptedSessionEntries(params) {
1070
915
  const marker = readNodeSessionMarker(entry);
1071
916
  const sessionId = entry.sessionId?.trim();
1072
917
  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;
1073
- const sourceKey = adoptedSourceKey(marker.sourceHostId, marker.sourceThreadId);
918
+ const sourceKey = sessionCatalogAdoptedSourceKey(marker.sourceHostId, marker.sourceThreadId);
1074
919
  if (adopted.has(sourceKey)) throw new Error(`multiple OpenClaw sessions adopt Codex thread ${marker.sourceThreadId} on ${marker.sourceHostId}`);
1075
920
  adopted.set(sourceKey, {
1076
921
  key: sessionKey,
@@ -1082,7 +927,7 @@ function listNodeAdoptedSessionEntries(params) {
1082
927
  return adopted;
1083
928
  }
1084
929
  function findNodeAdoptedSessionEntry(params) {
1085
- return listNodeAdoptedSessionEntries(params).get(adoptedSourceKey(params.hostId, params.threadId));
930
+ return listNodeAdoptedSessionEntries(params).get(sessionCatalogAdoptedSourceKey(params.hostId, params.threadId));
1086
931
  }
1087
932
  function nodeSessionMarker(params) {
1088
933
  return {
@@ -1130,6 +975,7 @@ async function finalizeNodeAdoptedSession(params) {
1130
975
  }
1131
976
  async function createOrReuseNodeAdoptedSession(params) {
1132
977
  const existing = findNodeAdoptedSessionEntry({
978
+ agentId: params.agentId,
1133
979
  config: params.config,
1134
980
  runtime: params.api.runtime,
1135
981
  hostId: params.hostId,
@@ -1149,7 +995,7 @@ async function createOrReuseNodeAdoptedSession(params) {
1149
995
  const created = await params.api.runtime.agent.session.createSessionEntry({
1150
996
  cfg: params.config,
1151
997
  key: nodeAdoptionSessionKey(params.hostId, params.record.threadId),
1152
- agentId: resolveDefaultAgentId(params.config),
998
+ agentId: params.agentId,
1153
999
  recoverMatchingInitialEntry: true,
1154
1000
  ...params.record.name?.trim() ? { label: params.record.name.trim() } : {},
1155
1001
  ...params.record.cwd?.trim() ? { spawnedCwd: params.record.cwd.trim() } : {},
@@ -1182,6 +1028,7 @@ async function createOrReuseNodeAdoptedSession(params) {
1182
1028
  };
1183
1029
  } catch (error) {
1184
1030
  const raced = findNodeAdoptedSessionEntry({
1031
+ agentId: params.agentId,
1185
1032
  config: params.config,
1186
1033
  runtime: params.api.runtime,
1187
1034
  hostId: params.hostId,
@@ -1193,346 +1040,127 @@ async function createOrReuseNodeAdoptedSession(params) {
1193
1040
  }
1194
1041
  }
1195
1042
  //#endregion
1196
- //#region extensions/codex/src/session-catalog-node-continue.ts
1197
- const CODEX_NODE_CONTINUE_COMMANDS = [
1198
- CODEX_APP_SERVER_THREADS_LIST_COMMAND,
1199
- CODEX_APP_SERVER_THREAD_TURNS_LIST_COMMAND,
1200
- CODEX_CLI_SESSION_RESUME_COMMAND
1201
- ];
1202
- const NODE_CATALOG_LIST_RESPONSE_TIMEOUT_MS = 8e3;
1203
- function nodeLabel(node) {
1204
- return node.displayName?.trim() || node.remoteIp?.trim() || node.nodeId;
1043
+ //#region extensions/codex/src/session-catalog-terminal.ts
1044
+ const CODEX_TERMINAL_RESUME_COMMAND = "codex.terminal.resume.v1";
1045
+ function resolveCodexCatalogTerminalHome(sources) {
1046
+ const runtimeConfig = sources.getRuntimeConfig();
1047
+ if (!runtimeConfig) throw new Error("OpenClaw runtime config is unavailable");
1048
+ const agentDir = sources.source?.agentDir ?? (sources.agentId ? resolveAgentDir(runtimeConfig, sources.agentId) : resolveDefaultAgentDir(runtimeConfig));
1049
+ return resolveCodexAppServerLocalHomeDir(sources.source?.appServer.start ?? resolveCodexSupervisionAppServerRuntimeOptions({ pluginConfig: sources.getPluginConfig() }).start, agentDir);
1205
1050
  }
1206
- function compareNodeLabels(left, right) {
1207
- const leftLabel = nodeLabel(left);
1208
- const rightLabel = nodeLabel(right);
1209
- if (leftLabel < rightLabel) return -1;
1210
- if (leftLabel > rightLabel) return 1;
1211
- return 0;
1051
+ function resolveLocalCodexTerminalExecutable(env = process.env) {
1052
+ return resolveLocalCodexTerminalResolution(env)?.executable;
1212
1053
  }
1213
- function canContinueCodexOnNode(node) {
1214
- return node.connected === true && CODEX_NODE_CONTINUE_COMMANDS.every((command) => node.commands?.includes(command) === true && node.invocableCommands?.includes(command) === true);
1054
+ function resolveLocalCodexTerminalResolution(env = process.env) {
1055
+ return resolveNodeHostExecutable("codex", {
1056
+ env,
1057
+ pathEnv: env.PATH ?? env.Path ?? "",
1058
+ strategy: "fallback"
1059
+ });
1215
1060
  }
1216
- async function listPairedNode(params) {
1217
- const hostId = `node:${params.node.nodeId}`;
1218
- const common = {
1219
- hostId,
1220
- label: nodeLabel(params.node),
1221
- kind: "node",
1222
- nodeId: params.node.nodeId,
1223
- canContinueCodex: canContinueCodexOnNode(params.node)
1224
- };
1225
- if (params.node.connected !== true) {
1226
- const host = {
1227
- ...common,
1228
- connected: false,
1229
- sessions: [],
1230
- error: {
1231
- code: "NODE_OFFLINE",
1232
- message: "Paired node is offline"
1233
- }
1234
- };
1235
- params.onHost?.(host);
1236
- return host;
1237
- }
1238
- const eventualHost = Promise.resolve().then(async () => {
1239
- const page = filterCatalogPageByTitle(parseCatalogPage(unwrapNodeInvokePayload(await params.runtime.nodes.invoke({
1240
- nodeId: params.node.nodeId,
1241
- command: CODEX_APP_SERVER_THREADS_LIST_COMMAND,
1242
- params: {
1243
- cursor: params.query.cursors?.[hostId],
1244
- limit: params.query.limitPerHost,
1245
- searchTerm: params.query.search
1246
- },
1247
- timeoutMs: NODE_INVOKE_TIMEOUT_MS,
1248
- scopes: ["operator.write"]
1249
- }))), params.query.search);
1250
- return {
1251
- ...common,
1252
- connected: true,
1253
- ...page,
1254
- sessions: page.sessions.map((session) => {
1255
- const adopted = params.adoptedSessions.get(adoptedSourceKey(hostId, session.threadId));
1256
- return adopted ? Object.assign({}, session, { sessionKey: adopted.key }) : session;
1257
- })
1258
- };
1259
- }).catch((error) => ({
1260
- ...common,
1261
- connected: true,
1262
- sessions: [],
1263
- error: catalogError("NODE_INVOKE_FAILED", error)
1264
- }));
1265
- if (params.onHost) eventualHost.then(params.onHost).catch(() => void 0);
1266
- try {
1267
- return await withTimeout(eventualHost, NODE_CATALOG_LIST_RESPONSE_TIMEOUT_MS, "paired node Codex session catalog timed out");
1268
- } catch (error) {
1269
- return {
1270
- ...common,
1271
- connected: true,
1272
- sessions: [],
1273
- error: catalogError("NODE_INVOKE_FAILED", error)
1274
- };
1061
+ function codexNodeTerminalCapability(node) {
1062
+ const commands = node.invocableCommands ?? node.commands;
1063
+ return node.connected === true && commands?.includes("codex.terminal.resume.v1") === true ? { canOpenTerminalCodex: true } : {};
1064
+ }
1065
+ async function requireCatalogEligibleThread(control, threadId) {
1066
+ const cached = await findCatalogEligibleThread(control, threadId, false);
1067
+ if (cached) return cached;
1068
+ const refreshed = await findCatalogEligibleThread(control, threadId, true);
1069
+ if (refreshed) return refreshed;
1070
+ throw new CatalogParamsError("Codex session is not a non-archived interactive Codex session");
1071
+ }
1072
+ async function findCatalogEligibleThread(control, threadId, forceRefresh) {
1073
+ let cursor;
1074
+ const seenCursors = /* @__PURE__ */ new Set();
1075
+ for (let pageIndex = 0; pageIndex < 100; pageIndex += 1) {
1076
+ const page = await control.listPage({
1077
+ limit: 100,
1078
+ ...cursor ? { cursor } : {},
1079
+ ...forceRefresh ? { forceRefresh: true } : {}
1080
+ });
1081
+ const candidate = page.sessions.find((session) => session.threadId === threadId);
1082
+ if (candidate) {
1083
+ if (isInteractiveThreadSource(candidate.source)) return candidate;
1084
+ throw new CatalogParamsError("Codex session is not a non-archived interactive Codex session");
1085
+ }
1086
+ const nextCursor = page.nextCursor?.trim();
1087
+ if (!nextCursor) return;
1088
+ if (seenCursors.has(nextCursor)) throw new CatalogParamsError("Codex session eligibility could not be verified");
1089
+ seenCursors.add(nextCursor);
1090
+ cursor = nextCursor;
1275
1091
  }
1092
+ throw new CatalogParamsError("Codex session eligibility could not be verified");
1276
1093
  }
1277
- async function requireNodeForCodexContinue(params) {
1278
- const nodeId = params.hostId.slice(5).trim();
1279
- if (!nodeId || params.hostId !== `node:${nodeId}`) throw new CatalogParamsError("Codex session catalog hostId is invalid");
1280
- const node = (await params.runtime.nodes.list()).nodes.find((candidate) => candidate.nodeId === nodeId);
1281
- if (!node || !canContinueCodexOnNode(node)) throw new CatalogParamsError("paired node does not permit Codex session continuation");
1094
+ function createCodexTerminalNodeHostCommand(bindRequest, configSources) {
1282
1095
  return {
1283
- node,
1284
- nodeId
1096
+ command: CODEX_TERMINAL_RESUME_COMMAND,
1097
+ cap: CODEX_APP_SERVER_THREADS_CAPABILITY,
1098
+ dangerous: false,
1099
+ duplex: true,
1100
+ isAvailable: ({ env }) => Boolean(resolveNodeHostExecutable("codex", {
1101
+ env,
1102
+ pathEnv: env.PATH ?? env.Path ?? "",
1103
+ strategy: "direct"
1104
+ })),
1105
+ handle: async (paramsJSON, io) => {
1106
+ if (!io) throw new Error("Codex terminal command requires duplex transport");
1107
+ const request = bindRequest(paramsJSON);
1108
+ const resume = decodeNodePtyResumeParams(request.paramsJSON, (value) => {
1109
+ 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");
1110
+ return value;
1111
+ });
1112
+ const record = await requireCatalogEligibleThread(request.control, resume.threadId);
1113
+ const resolution = resolveNodeHostExecutable("codex", {
1114
+ env: process.env,
1115
+ pathEnv: process.env.PATH ?? process.env.Path ?? "",
1116
+ strategy: "direct"
1117
+ });
1118
+ if (!resolution) throw new Error("Codex CLI is unavailable");
1119
+ return JSON.stringify(await runNodePtyCommand({
1120
+ file: resolution.executable,
1121
+ args: ["resume", resume.threadId],
1122
+ cwd: record.cwd,
1123
+ env: { CODEX_HOME: resolveCodexCatalogTerminalHome({
1124
+ ...configSources,
1125
+ agentId: request.agentId
1126
+ }) },
1127
+ cols: resume.cols,
1128
+ rows: resume.rows
1129
+ }, io));
1130
+ }
1285
1131
  };
1286
1132
  }
1287
- async function resolveNodeCodexRecord(params) {
1133
+ async function resolveNodeCatalogEligibleThread(params) {
1288
1134
  let cursor;
1289
1135
  const seenCursors = /* @__PURE__ */ new Set();
1290
1136
  for (let pageIndex = 0; pageIndex < 100; pageIndex += 1) {
1291
- const page = parseCatalogPage(unwrapNodeInvokePayload(await params.runtime.nodes.invoke({
1137
+ const raw = await params.runtime.nodes.invoke({
1292
1138
  nodeId: params.nodeId,
1293
1139
  command: CODEX_APP_SERVER_THREADS_LIST_COMMAND,
1294
1140
  params: {
1141
+ agentId: params.agentId,
1295
1142
  limit: 100,
1296
1143
  ...cursor ? { cursor } : {}
1297
1144
  },
1298
1145
  timeoutMs: NODE_INVOKE_TIMEOUT_MS,
1299
1146
  scopes: ["operator.write"]
1300
- })));
1147
+ });
1148
+ const page = params.parseCatalogPage(unwrapNodeInvokePayload(raw));
1301
1149
  const record = page.sessions.find((candidate) => candidate.threadId === params.threadId);
1302
- if (record) return record;
1150
+ if (record) {
1151
+ if (isInteractiveThreadSource(record.source)) return record;
1152
+ break;
1153
+ }
1303
1154
  const nextCursor = page.nextCursor?.trim();
1304
- if (!nextCursor) break;
1305
- if (seenCursors.has(nextCursor)) throw new CatalogParamsError("Codex session eligibility could not be verified");
1155
+ if (!nextCursor || seenCursors.has(nextCursor)) break;
1306
1156
  seenCursors.add(nextCursor);
1307
1157
  cursor = nextCursor;
1308
1158
  }
1309
- throw new CatalogParamsError("Codex session is unavailable on the paired node");
1310
- }
1311
- function requireContinuableNodeRecord(record) {
1312
- if (record.archived) throw new CatalogParamsError("Codex session is archived on the paired node");
1313
- if (!isInteractiveThreadSource(record.source)) throw new CatalogParamsError("Codex session is not a non-archived interactive Codex session");
1314
- if (record.status === "idle" || record.status === "notLoaded") return;
1315
- if (record.status === "active") throw new CatalogParamsError("Codex session is active on the paired node; wait for it to finish before continuing");
1316
- throw new CatalogParamsError("Codex session cannot be continued in its current state");
1317
- }
1318
- async function readNodeCodexHistory(params) {
1319
- const page = parseTranscriptPage(unwrapNodeInvokePayload(await params.runtime.nodes.invoke({
1320
- nodeId: params.nodeId,
1321
- command: CODEX_APP_SERVER_THREAD_TURNS_LIST_COMMAND,
1322
- params: {
1323
- threadId: params.record.threadId,
1324
- limit: 50
1325
- },
1326
- timeoutMs: NODE_INVOKE_TIMEOUT_MS,
1327
- scopes: ["operator.write"]
1328
- })));
1329
- const thread = {
1330
- id: params.record.threadId,
1331
- createdAt: params.record.createdAt ?? 0,
1332
- modelProvider: params.record.modelProvider ?? "openai",
1333
- turns: page.data.toReversed()
1334
- };
1335
- return {
1336
- thread,
1337
- throughTurnId: lastTerminalTurnId(thread) ?? null
1338
- };
1339
- }
1340
- async function continueNodeCodexSessionInner(params) {
1341
- const { nodeId } = await requireNodeForCodexContinue({
1342
- runtime: params.api.runtime,
1343
- hostId: params.hostId
1344
- });
1345
- const record = await resolveNodeCodexRecord({
1346
- runtime: params.api.runtime,
1347
- nodeId,
1348
- threadId: params.threadId
1349
- });
1350
- requireContinuableNodeRecord(record);
1351
- const existing = findNodeAdoptedSessionEntry({
1352
- config: params.config,
1353
- runtime: params.api.runtime,
1354
- hostId: params.hostId,
1355
- threadId: params.threadId,
1356
- includeInitializing: true
1357
- });
1358
- let adopted;
1359
- let disposition;
1360
- if (existing) {
1361
- adopted = existing;
1362
- disposition = "existing";
1363
- } else {
1364
- const history = await readNodeCodexHistory({
1365
- runtime: params.api.runtime,
1366
- nodeId,
1367
- record
1368
- });
1369
- adopted = await createOrReuseNodeAdoptedSession({
1370
- api: params.api,
1371
- config: params.config,
1372
- hostId: params.hostId,
1373
- nodeId,
1374
- record,
1375
- history
1376
- });
1377
- disposition = "forked";
1378
- }
1379
- const marker = nodeSessionMarker({
1380
- hostId: params.hostId,
1381
- threadId: params.threadId,
1382
- nodeId
1383
- });
1384
- return {
1385
- sessionKey: adopted.key,
1386
- disposition,
1387
- conversationBinding: {
1388
- summary: "Continue this Codex session on its paired node.",
1389
- detachHint: "Start a new chat to leave the paired-node Codex session.",
1390
- data: createCodexCliNodeConversationBindingData({
1391
- nodeId,
1392
- sessionId: record.sessionId?.trim() || params.threadId,
1393
- agentId: adopted.agentId,
1394
- cwd: record.cwd
1395
- })
1396
- },
1397
- afterConversationBound: async () => await finalizeNodeAdoptedSession({
1398
- api: params.api,
1399
- adopted,
1400
- marker
1401
- })
1402
- };
1403
- }
1404
- async function continueNodeCodexSession(params) {
1405
- if (params.clientScopes?.includes("operator.admin") !== true) throw new CatalogParamsError("continuing a paired-node Codex session requires operator.admin");
1406
- const nodeId = params.hostId.slice(5).trim();
1407
- if (!nodeId || params.hostId !== `node:${nodeId}`) throw new CatalogParamsError("Codex session catalog hostId is invalid");
1408
- const sourceKey = adoptedSourceKey(`node:${nodeId}`, params.threadId);
1409
- const current = continueOperations.get(sourceKey);
1410
- if (current) return await current;
1411
- const operation = runSessionActionExclusive(sourceKey, async () => continueNodeCodexSessionInner(params));
1412
- continueOperations.set(sourceKey, operation);
1413
- try {
1414
- return await operation;
1415
- } finally {
1416
- if (continueOperations.get(sourceKey) === operation) continueOperations.delete(sourceKey);
1417
- }
1418
- }
1419
- //#endregion
1420
- //#region extensions/codex/src/session-catalog-terminal.ts
1421
- const CODEX_TERMINAL_RESUME_COMMAND = "codex.terminal.resume.v1";
1422
- function resolveCodexCatalogTerminalHome(sources) {
1423
- const runtimeConfig = sources.getRuntimeConfig();
1424
- if (!runtimeConfig) throw new Error("OpenClaw runtime config is unavailable");
1425
- const startOptions = resolveCodexSupervisionAppServerRuntimeOptions({ pluginConfig: sources.getPluginConfig() }).start;
1426
- return resolveCodexAppServerLocalHomeDir(startOptions, resolveDefaultAgentDir(runtimeConfig));
1427
- }
1428
- function resolveLocalCodexTerminalExecutable(env = process.env) {
1429
- return resolveLocalCodexTerminalResolution(env)?.executable;
1430
- }
1431
- function resolveLocalCodexTerminalResolution(env = process.env) {
1432
- return resolveNodeHostExecutable("codex", {
1433
- env,
1434
- pathEnv: env.PATH ?? env.Path ?? "",
1435
- strategy: "fallback"
1436
- });
1437
- }
1438
- function codexNodeTerminalCapability(node) {
1439
- const commands = node.invocableCommands ?? node.commands;
1440
- return node.connected === true && commands?.includes("codex.terminal.resume.v1") === true ? { canOpenTerminalCodex: true } : {};
1441
- }
1442
- async function requireCatalogEligibleThread(control, threadId) {
1443
- const cached = await findCatalogEligibleThread(control, threadId, false);
1444
- if (cached) return cached;
1445
- const refreshed = await findCatalogEligibleThread(control, threadId, true);
1446
- if (refreshed) return refreshed;
1447
- throw new CatalogParamsError("Codex session is not a non-archived interactive Codex session");
1448
- }
1449
- async function findCatalogEligibleThread(control, threadId, forceRefresh) {
1450
- let cursor;
1451
- const seenCursors = /* @__PURE__ */ new Set();
1452
- for (let pageIndex = 0; pageIndex < 100; pageIndex += 1) {
1453
- const page = await control.listPage({
1454
- limit: 100,
1455
- ...cursor ? { cursor } : {},
1456
- ...forceRefresh ? { forceRefresh: true } : {}
1457
- });
1458
- const candidate = page.sessions.find((session) => session.threadId === threadId);
1459
- if (candidate) {
1460
- if (isInteractiveThreadSource(candidate.source)) return candidate;
1461
- throw new CatalogParamsError("Codex session is not a non-archived interactive Codex session");
1462
- }
1463
- const nextCursor = page.nextCursor?.trim();
1464
- if (!nextCursor) return;
1465
- if (seenCursors.has(nextCursor)) throw new CatalogParamsError("Codex session eligibility could not be verified");
1466
- seenCursors.add(nextCursor);
1467
- cursor = nextCursor;
1468
- }
1469
- throw new CatalogParamsError("Codex session eligibility could not be verified");
1470
- }
1471
- function createCodexTerminalNodeHostCommand(control, configSources) {
1472
- return {
1473
- command: CODEX_TERMINAL_RESUME_COMMAND,
1474
- cap: CODEX_APP_SERVER_THREADS_CAPABILITY,
1475
- dangerous: false,
1476
- duplex: true,
1477
- isAvailable: ({ env }) => Boolean(resolveNodeHostExecutable("codex", {
1478
- env,
1479
- pathEnv: env.PATH ?? env.Path ?? "",
1480
- strategy: "direct"
1481
- })),
1482
- handle: async (paramsJSON, io) => {
1483
- if (!io) throw new Error("Codex terminal command requires duplex transport");
1484
- const resume = decodeNodePtyResumeParams(paramsJSON, (value) => {
1485
- 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");
1486
- return value;
1487
- });
1488
- const record = await requireCatalogEligibleThread(control, resume.threadId);
1489
- const resolution = resolveNodeHostExecutable("codex", {
1490
- env: process.env,
1491
- pathEnv: process.env.PATH ?? process.env.Path ?? "",
1492
- strategy: "direct"
1493
- });
1494
- if (!resolution) throw new Error("Codex CLI is unavailable");
1495
- return JSON.stringify(await runNodePtyCommand({
1496
- file: resolution.executable,
1497
- args: ["resume", resume.threadId],
1498
- cwd: record.cwd,
1499
- env: { CODEX_HOME: resolveCodexCatalogTerminalHome(configSources) },
1500
- cols: resume.cols,
1501
- rows: resume.rows
1502
- }, io));
1503
- }
1504
- };
1505
- }
1506
- async function resolveNodeCatalogEligibleThread(params) {
1507
- let cursor;
1508
- const seenCursors = /* @__PURE__ */ new Set();
1509
- for (let pageIndex = 0; pageIndex < 100; pageIndex += 1) {
1510
- const raw = await params.runtime.nodes.invoke({
1511
- nodeId: params.nodeId,
1512
- command: CODEX_APP_SERVER_THREADS_LIST_COMMAND,
1513
- params: {
1514
- limit: 100,
1515
- ...cursor ? { cursor } : {}
1516
- },
1517
- timeoutMs: NODE_INVOKE_TIMEOUT_MS,
1518
- scopes: ["operator.write"]
1519
- });
1520
- const page = params.parseCatalogPage(unwrapNodeInvokePayload(raw));
1521
- const record = page.sessions.find((candidate) => candidate.threadId === params.threadId);
1522
- if (record) {
1523
- if (isInteractiveThreadSource(record.source)) return record;
1524
- break;
1525
- }
1526
- const nextCursor = page.nextCursor?.trim();
1527
- if (!nextCursor || seenCursors.has(nextCursor)) break;
1528
- seenCursors.add(nextCursor);
1529
- cursor = nextCursor;
1530
- }
1531
- throw new CatalogParamsError("Codex session is not a non-archived interactive Codex session");
1159
+ throw new CatalogParamsError("Codex session is not a non-archived interactive Codex session");
1532
1160
  }
1533
1161
  async function openCodexCatalogTerminal(params) {
1534
1162
  const title = `codex resume ${params.threadId.slice(0, 8)}…`;
1535
- if (params.hostId === "gateway:local") {
1163
+ if (params.hostId === "gateway:local" || params.hostId.startsWith(`gateway:local:`)) {
1536
1164
  const record = await requireCatalogEligibleThread(params.control, params.threadId);
1537
1165
  const resolution = resolveLocalCodexTerminalResolution();
1538
1166
  if (!resolution) throw new CatalogParamsError("Codex CLI is unavailable");
@@ -1556,6 +1184,7 @@ async function openCodexCatalogTerminal(params) {
1556
1184
  return candidate.nodeId === nodeId && candidate.connected === true && commands?.includes("codex.appServer.threads.list.v1") === true && commands.includes("codex.terminal.resume.v1");
1557
1185
  })) throw new CatalogParamsError("paired-node Codex terminal is unavailable");
1558
1186
  const record = await resolveNodeCatalogEligibleThread({
1187
+ agentId: params.agentId,
1559
1188
  runtime: params.api.runtime,
1560
1189
  nodeId,
1561
1190
  threadId: params.threadId,
@@ -1565,7 +1194,10 @@ async function openCodexCatalogTerminal(params) {
1565
1194
  kind: "node",
1566
1195
  nodeId,
1567
1196
  command: CODEX_TERMINAL_RESUME_COMMAND,
1568
- paramsJSON: JSON.stringify({ threadId: params.threadId }),
1197
+ paramsJSON: JSON.stringify({
1198
+ agentId: params.agentId,
1199
+ threadId: params.threadId
1200
+ }),
1569
1201
  ...record.cwd ? { cwd: record.cwd } : {},
1570
1202
  title
1571
1203
  };
@@ -1584,480 +1216,684 @@ async function startCodexCatalogTerminal(params) {
1584
1216
  };
1585
1217
  }
1586
1218
  //#endregion
1587
- //#region extensions/codex/src/session-catalog-transcript-item.ts
1588
- const CODEX_MESSAGE_TYPES = /* @__PURE__ */ new Map([
1589
- ["userMessage", "userMessage"],
1590
- ["agentMessage", "agentMessage"],
1591
- ["reasoning", "reasoning"]
1592
- ]);
1593
- const CODEX_TOOL_TYPES = /* @__PURE__ */ new Set([
1594
- "commandExecution",
1595
- "fileChange",
1596
- "mcpToolCall",
1597
- "dynamicToolCall",
1598
- "collabAgentToolCall",
1599
- "webSearch",
1600
- "imageView",
1601
- "imageGeneration"
1602
- ]);
1603
- function toGenericTranscriptItem(item) {
1604
- let type = CODEX_MESSAGE_TYPES.get(item.type);
1605
- if (!type && CODEX_TOOL_TYPES.has(item.type)) type = item.result !== void 0 || Boolean(item.aggregatedOutput) ? "toolResult" : "toolCall";
1606
- type ??= "other";
1607
- const fallback = item.title ?? item.name ?? item.tool ?? item.command ?? item.query ?? void 0;
1608
- const resultText = item.aggregatedOutput || (item.result === void 0 ? void 0 : JSON.stringify(item.result, null, 2));
1609
- const changesText = Array.isArray(item.changes) ? item.changes.map((change) => `${change.kind}: ${change.path}`).join("\n") || void 0 : void 0;
1610
- const text = item.text || resultText || changesText || fallback;
1611
- return {
1612
- id: item.id,
1613
- type,
1614
- ...text ? { text } : {},
1615
- raw: item
1616
- };
1219
+ //#region extensions/codex/src/session-catalog-adoption.ts
1220
+ const CODEX_SUPERVISION_SESSION_KEY_PREFIX = "harness:codex:supervision:";
1221
+ const boundCatalogSessionId = (value) => boundedCatalogString(value, 256);
1222
+ function requireIdleThread(thread, action) {
1223
+ if (thread.status?.type === "idle" || action === "archive" && thread.status?.type === "notLoaded") return;
1224
+ 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"}`);
1225
+ throw new CatalogParamsError(action === "archive" ? "Codex session cannot be archived in its current state" : "Codex session cannot start a branch in its current state");
1617
1226
  }
1618
- //#endregion
1619
- //#region extensions/codex/src/session-upstream-activity.ts
1620
- const CODEX_UPSTREAM_TURN_LIMIT = 100;
1621
- const CODEX_APP_SERVER_INVALID_REQUEST_CODE = -32600;
1622
- const CODEX_THREAD_NOT_LOADED_MESSAGE_PREFIX = "thread not loaded:";
1623
- function isCodexThreadGoneError(error) {
1624
- return error instanceof CodexAppServerRpcError && error.code === CODEX_APP_SERVER_INVALID_REQUEST_CODE && error.message.startsWith(CODEX_THREAD_NOT_LOADED_MESSAGE_PREFIX);
1227
+ function adoptionSessionKey(threadId, sourceHomeId) {
1228
+ const source = sourceHomeId ? JSON.stringify([sourceHomeId, threadId]) : threadId;
1229
+ return sessionCatalogAdoptedSessionKey(CODEX_SUPERVISION_SESSION_KEY_PREFIX, source);
1625
1230
  }
1626
- function readMarker(probe) {
1627
- if (!isRecord(probe.marker)) return;
1628
- const turnId = probe.marker.turnId;
1629
- if (turnId !== null && typeof turnId !== "string") return;
1630
- const count = probe.marker.userMessageCount;
1631
- if (count !== void 0 && (!Number.isSafeInteger(count) || count < 0)) return;
1231
+ function isAdoptionSessionKeyForThread(sessionKey, threadId, sourceHomeId) {
1232
+ return adoptionSessionKeyRest(sessionKey) === adoptionSessionKey(threadId, sourceHomeId);
1233
+ }
1234
+ function readCodexSupervisionMarker(entry) {
1235
+ const codex = isRecord(entry.pluginExtensions?.codex) ? entry.pluginExtensions.codex : void 0;
1236
+ const marker = codex && isRecord(codex.supervision) ? codex.supervision : void 0;
1237
+ const sourceThreadId = marker?.sourceThreadId;
1238
+ const sourceHomeId = marker?.sourceHomeId;
1239
+ if (typeof sourceThreadId !== "string" || !sourceThreadId.trim() || sourceHomeId !== void 0 && (typeof sourceHomeId !== "string" || !sourceHomeId.trim())) return;
1632
1240
  return {
1633
- turnId,
1634
- ...count === void 0 ? {} : { userMessageCount: count }
1241
+ sourceThreadId: sourceThreadId.trim(),
1242
+ ...typeof sourceHomeId === "string" ? { sourceHomeId: sourceHomeId.trim() } : {}
1635
1243
  };
1636
1244
  }
1637
- function upstreamConnectionFingerprint(probe) {
1638
- return isRecord(probe.upstreamRef) && typeof probe.upstreamRef.connectionFingerprint === "string" ? probe.upstreamRef.connectionFingerprint : void 0;
1639
- }
1640
- function classifyCodexUpstreamTurns(params) {
1641
- const marker = readMarker(params.probe);
1642
- if (!marker) return;
1643
- const newest = params.turns[0];
1644
- if (!newest?.id) return;
1645
- const markerIndex = marker.turnId === null ? -1 : params.turns.findIndex((turn) => turn.id === marker.turnId);
1646
- const candidateTurns = markerIndex < 0 ? params.turns : params.turns.slice(0, markerIndex + 1);
1647
- const newestUserMessageCount = countUserMessages(newest);
1648
- if (!(marker.turnId !== newest.id || marker.userMessageCount === void 0 || newestUserMessageCount > marker.userMessageCount)) return;
1649
- const ownTexts = new Set(params.probe.ownRecentUserTexts);
1650
- let humanTurns = 0;
1651
- let occurredAt;
1652
- for (const turn of candidateTurns) {
1653
- const userMessages = turn.items.filter((item) => item.type === "userMessage");
1654
- const alreadySeen = turn.id === marker.turnId ? marker.userMessageCount ?? userMessages.length : 0;
1655
- for (const item of userMessages.slice(alreadySeen)) {
1656
- const texts = normalizeUserMessageTexts(item);
1657
- if (ownTexts.has(texts.join(" ")) || texts.length > 1 && texts.every((text) => ownTexts.has(text))) continue;
1658
- humanTurns += 1;
1659
- if (occurredAt === void 0) {
1660
- const timestampSeconds = turn.completedAt ?? turn.startedAt;
1661
- occurredAt = typeof timestampSeconds === "number" && Number.isFinite(timestampSeconds) ? timestampSeconds * 1e3 : params.now ?? Date.now();
1245
+ async function listAdoptedSessionEntries(params) {
1246
+ const adopted = /* @__PURE__ */ new Map();
1247
+ for (const { agentId, entry, sessionKey } of listSessionCatalogEntries({
1248
+ ...params.agentId ? { agentId: params.agentId } : {},
1249
+ config: params.config ?? {},
1250
+ runtime: params.runtime,
1251
+ sessionEntries: params.sessionEntries
1252
+ })) {
1253
+ const sessionKeyRest = adoptionSessionKeyRest(sessionKey);
1254
+ const marker = readCodexSupervisionMarker(entry);
1255
+ if (!sessionKeyRest.startsWith(CODEX_SUPERVISION_SESSION_KEY_PREFIX) || !marker || entry.initializationPending === true || entry.agentHarnessId !== "codex" || entry.modelSelectionLocked !== true) continue;
1256
+ const sessionId = entry.sessionId?.trim();
1257
+ if (!sessionId) continue;
1258
+ const binding = await params.bindingStore.read(sessionBindingIdentity({
1259
+ sessionId,
1260
+ sessionKey,
1261
+ config: params.config
1262
+ }));
1263
+ const sourceThreadId = binding?.supervisionSourceThreadId?.trim();
1264
+ const boundThreadId = binding?.threadId.trim();
1265
+ if (binding?.connectionScope !== "supervision" || !sourceThreadId || !boundThreadId || sessionKeyRest !== adoptionSessionKey(sourceThreadId, marker.sourceHomeId)) continue;
1266
+ const sourceKey = sessionCatalogAdoptedSourceKey(marker.sourceHomeId ?? "gateway:local", sourceThreadId);
1267
+ if (adopted.has(sourceKey)) throw new Error(`multiple OpenClaw sessions adopt Codex thread ${sourceThreadId} from the same home`);
1268
+ adopted.set(sourceKey, {
1269
+ key: sessionKey,
1270
+ sessionId,
1271
+ agentId,
1272
+ boundThreadId
1273
+ });
1274
+ }
1275
+ return adopted;
1276
+ }
1277
+ async function findAdoptedSessionEntry(params) {
1278
+ const adopted = await listAdoptedSessionEntries(params);
1279
+ return adopted.get(sessionCatalogAdoptedSourceKey(params.sourceHomeId ?? "gateway:local", params.threadId)) ?? (params.sourceHomeId && params.allowLegacy === true ? adopted.get(sessionCatalogAdoptedSourceKey("gateway:local", params.threadId)) : void 0);
1280
+ }
1281
+ async function clearCreatedAdoptionBinding(params) {
1282
+ let cleared = false;
1283
+ let clearError;
1284
+ try {
1285
+ cleared = await params.bindingStore.mutate(params.identity, {
1286
+ kind: "clear",
1287
+ threadId: params.sourceThreadId,
1288
+ expectedPendingSupervisionBranch: params.expectedPending
1289
+ });
1290
+ } catch (error) {
1291
+ clearError = error;
1292
+ }
1293
+ if (cleared) return;
1294
+ let current;
1295
+ try {
1296
+ current = await params.bindingStore.read(params.identity);
1297
+ } catch (readError) {
1298
+ throw new AggregateError([
1299
+ params.cause,
1300
+ ...clearError ? [clearError] : [],
1301
+ readError
1302
+ ], `OpenClaw session creation failed and the Codex binding could not be verified for ${params.sourceThreadId}`, { cause: readError });
1303
+ }
1304
+ if (!matchesPendingSupervisionOwner(current, params.expectedPending)) return;
1305
+ throw new AggregateError([params.cause, ...clearError ? [clearError] : []], `OpenClaw session creation failed and the Codex binding could not be cleared for ${params.sourceThreadId}`, { cause: params.cause });
1306
+ }
1307
+ function matchesPendingAdoptionBinding(binding, expected) {
1308
+ const historyCoveredThrough = binding?.historyCoveredThrough;
1309
+ 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));
1310
+ }
1311
+ function matchesPendingSupervisionOwner(binding, expected) {
1312
+ const pending = binding?.pendingSupervisionBranch;
1313
+ const cleanupThreadIds = pending?.cleanupThreadIds ?? [];
1314
+ const expectedCleanupThreadIds = expected.cleanupThreadIds ?? [];
1315
+ 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]);
1316
+ }
1317
+ async function ensurePendingAdoptionBinding(params) {
1318
+ const pending = {
1319
+ sourceThreadId: params.sourceThreadId,
1320
+ connectionFingerprint: params.connectionFingerprint,
1321
+ ...params.lastTurnId ? { lastTurnId: params.lastTurnId } : {}
1322
+ };
1323
+ if (!await reclaimCurrentCodexSessionGeneration({
1324
+ bindingStore: params.bindingStore,
1325
+ identity: params.identity,
1326
+ config: params.config
1327
+ })) throw new Error(`failed to claim the OpenClaw session generation for ${params.sourceThreadId}`);
1328
+ const existing = await params.bindingStore.read(params.identity);
1329
+ if (existing) {
1330
+ if (matchesPendingAdoptionBinding(existing, params)) return;
1331
+ throw new Error(`OpenClaw session is already bound to Codex thread ${existing.threadId}`);
1332
+ }
1333
+ const binding = {
1334
+ threadId: params.sourceThreadId,
1335
+ connectionScope: "supervision",
1336
+ supervisionSourceThreadId: params.sourceThreadId,
1337
+ cwd: params.cwd,
1338
+ historyCoveredThrough: (/* @__PURE__ */ new Date()).toISOString(),
1339
+ conversationSourceTransferComplete: true,
1340
+ preserveNativeModel: true,
1341
+ pendingSupervisionBranch: pending
1342
+ };
1343
+ let stored;
1344
+ try {
1345
+ stored = await params.bindingStore.mutate(params.identity, {
1346
+ kind: "set",
1347
+ if: { kind: "absent" },
1348
+ binding
1349
+ });
1350
+ } catch (error) {
1351
+ if (matchesPendingAdoptionBinding(await params.bindingStore.read(params.identity), params)) return;
1352
+ throw error;
1353
+ }
1354
+ if (stored) return;
1355
+ if (!matchesPendingAdoptionBinding(await params.bindingStore.read(params.identity), params)) throw new Error(`failed to bind OpenClaw session to Codex thread ${params.sourceThreadId}`);
1356
+ }
1357
+ async function createOrReuseAdoptedSession(params) {
1358
+ const runtime = params.api.runtime;
1359
+ const lookup = {
1360
+ ...params,
1361
+ runtime,
1362
+ threadId: params.sourceThread.id
1363
+ };
1364
+ const existing = await findAdoptedSessionEntry(lookup);
1365
+ if (existing) return existing;
1366
+ let createdBindingIdentity;
1367
+ let createdPendingBinding;
1368
+ try {
1369
+ const spawnedCwd = params.sourceThread.cwd?.trim() || void 0;
1370
+ const pendingLastTurnId = codexLastTerminalTurnId(params.sourceThread, boundCatalogSessionId);
1371
+ const marker = {
1372
+ sourceThreadId: params.sourceThread.id,
1373
+ ...params.sourceHomeId ? { sourceHomeId: params.sourceHomeId } : {}
1374
+ };
1375
+ const created = await createImportedCodexSession({
1376
+ runtime: params.api.runtime,
1377
+ config: params.config,
1378
+ key: adoptionSessionKey(params.sourceThread.id, params.sourceHomeId),
1379
+ agentId: params.agentId,
1380
+ thread: params.sourceThread,
1381
+ throughTurnId: pendingLastTurnId ?? null,
1382
+ recoverMatchingInitialEntry: true,
1383
+ initialEntry: {
1384
+ agentHarnessId: "codex",
1385
+ modelSelectionLocked: true,
1386
+ pluginExtensions: { codex: { supervision: {
1387
+ ...marker,
1388
+ initializing: true,
1389
+ modelLocked: true
1390
+ } } }
1391
+ },
1392
+ afterImport: async (entry) => {
1393
+ createdBindingIdentity = sessionBindingIdentity({
1394
+ sessionId: entry.sessionId,
1395
+ sessionKey: entry.key,
1396
+ config: params.config
1397
+ });
1398
+ createdPendingBinding = {
1399
+ sourceThreadId: params.sourceThread.id,
1400
+ connectionFingerprint: params.connectionFingerprint,
1401
+ ...pendingLastTurnId ? { lastTurnId: pendingLastTurnId } : {}
1402
+ };
1403
+ await ensurePendingAdoptionBinding({
1404
+ bindingStore: params.bindingStore,
1405
+ config: params.config,
1406
+ identity: createdBindingIdentity,
1407
+ sourceThreadId: params.sourceThread.id,
1408
+ connectionFingerprint: params.connectionFingerprint,
1409
+ cwd: spawnedCwd ?? "",
1410
+ ...pendingLastTurnId ? { lastTurnId: pendingLastTurnId } : {}
1411
+ });
1412
+ return { pluginExtensions: { codex: { supervision: {
1413
+ ...marker,
1414
+ modelLocked: true
1415
+ } } } };
1416
+ }
1417
+ });
1418
+ return {
1419
+ key: created.key,
1420
+ sessionId: created.sessionId,
1421
+ agentId: created.agentId,
1422
+ boundThreadId: params.sourceThread.id
1423
+ };
1424
+ } catch (error) {
1425
+ let raced = await findAdoptedSessionEntry(lookup);
1426
+ if (raced) return raced;
1427
+ if (createdBindingIdentity && createdPendingBinding) {
1428
+ await clearCreatedAdoptionBinding({
1429
+ bindingStore: params.bindingStore,
1430
+ identity: createdBindingIdentity,
1431
+ sourceThreadId: params.sourceThread.id,
1432
+ expectedPending: createdPendingBinding,
1433
+ cause: error
1434
+ });
1435
+ raced = await findAdoptedSessionEntry(lookup);
1436
+ if (raced) return raced;
1437
+ }
1438
+ throw error;
1439
+ }
1440
+ }
1441
+ async function continueLocalCodexSessionInner(params) {
1442
+ await requireCatalogEligibleThread(params.control, params.threadId);
1443
+ const existing = await findAdoptedSessionEntry({
1444
+ ...params,
1445
+ runtime: params.api.runtime
1446
+ });
1447
+ if (existing) {
1448
+ const boundThreadId = requireBoundThread(existing);
1449
+ const boundThread = await params.control.readThread(boundThreadId, true);
1450
+ if (boundThread.id !== boundThreadId) throw new Error("Codex app-server returned a different thread than requested");
1451
+ const changedError = () => new CatalogParamsError("Codex OpenClaw session changed before it could be opened. Retry.");
1452
+ if (!await params.api.runtime.agent.session.patchSessionEntry({
1453
+ sessionKey: existing.key,
1454
+ readConsistency: "latest",
1455
+ preserveActivity: true,
1456
+ update: (entry) => {
1457
+ if (entry.sessionId?.trim() !== existing.sessionId || entry.initializationPending === true || entry.agentHarnessId !== "codex" || entry.modelSelectionLocked !== true) throw changedError();
1458
+ return { archivedAt: void 0 };
1459
+ }
1460
+ })) throw changedError();
1461
+ const connectionFingerprint = params.control.connectionFingerprint;
1462
+ if (connectionFingerprint) params.onContinued?.({
1463
+ connectionFingerprint,
1464
+ ...codexUpstreamBaseline(boundThread, boundCatalogSessionId)
1465
+ });
1466
+ return {
1467
+ sessionKey: existing.key,
1468
+ disposition: "existing"
1469
+ };
1470
+ }
1471
+ const sourceThread = await params.control.readThread(params.threadId, true);
1472
+ if (sourceThread.id !== params.threadId) throw new Error("Codex app-server returned a different thread than requested");
1473
+ if (sourceThread.status?.type !== "notLoaded") requireIdleThread(sourceThread, "continue");
1474
+ const connectionFingerprint = params.control.connectionFingerprint;
1475
+ if (!connectionFingerprint) throw new Error("Codex Continue requires a pinned app-server connection");
1476
+ const adopted = await createOrReuseAdoptedSession({
1477
+ ...params,
1478
+ sourceThread,
1479
+ connectionFingerprint
1480
+ });
1481
+ const boundThreadId = requireBoundThread(adopted);
1482
+ const baselineThread = boundThreadId === sourceThread.id ? sourceThread : await params.control.readThread(boundThreadId, true);
1483
+ if (baselineThread.id !== boundThreadId) throw new Error("Codex app-server returned a different thread than requested");
1484
+ params.onContinued?.({
1485
+ connectionFingerprint,
1486
+ ...codexUpstreamBaseline(baselineThread, boundCatalogSessionId)
1487
+ });
1488
+ return {
1489
+ sessionKey: adopted.key,
1490
+ disposition: "forked"
1491
+ };
1492
+ }
1493
+ /** Creates one locked OpenClaw branch whose first harness run forks the Codex source. */
1494
+ async function continueLocalCodexSession(params) {
1495
+ const sourceKey = sessionCatalogAdoptedSourceKey(params.hostId ?? "gateway:local", params.threadId);
1496
+ const operationKey = sessionCatalogAdoptedSourceKey(params.agentId, sourceKey);
1497
+ const current = continueOperations.get(operationKey);
1498
+ if (current) return await current;
1499
+ const run = async (control) => await continueLocalCodexSessionInner({
1500
+ ...params,
1501
+ control
1502
+ });
1503
+ const operation = runSessionActionExclusive(sourceKey, async () => params.control.withPinnedConnection(run));
1504
+ continueOperations.set(operationKey, operation);
1505
+ try {
1506
+ return await operation;
1507
+ } finally {
1508
+ if (continueOperations.get(operationKey) === operation) continueOperations.delete(operationKey);
1509
+ }
1510
+ }
1511
+ //#endregion
1512
+ //#region extensions/codex/src/session-catalog-archive.ts
1513
+ async function assertNoPendingSupervisionBranch(params) {
1514
+ const adoptedEntries = [params.agentId, ...listAgentIds(params.config).filter((agentId) => agentId !== params.agentId)].flatMap((agentId) => params.runtime.agent.session.listSessionEntries({
1515
+ agentId,
1516
+ readOnly: true
1517
+ })).filter((candidate) => isAdoptionSessionKeyForThread(candidate.sessionKey, params.threadId, params.sourceHomeId) || params.sourceHomeId !== void 0 && params.allowLegacy === true && isAdoptionSessionKeyForThread(candidate.sessionKey, params.threadId));
1518
+ for (const adopted of adoptedEntries) {
1519
+ if (adopted.entry.initializationPending === true) throw new CatalogParamsError("Codex session cannot be archived while its OpenClaw branch is initializing");
1520
+ const sessionId = adopted.entry.sessionId?.trim();
1521
+ if (!sessionId) continue;
1522
+ const binding = await params.bindingStore.read(sessionBindingIdentity({
1523
+ sessionId,
1524
+ sessionKey: adopted.sessionKey,
1525
+ config: params.config
1526
+ }));
1527
+ 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");
1528
+ }
1529
+ }
1530
+ /** Archives one inactive Gateway-local Codex thread after a fresh status read. */
1531
+ async function archiveLocalCodexSession(params) {
1532
+ return await runSessionActionExclusive(sessionCatalogAdoptedSourceKey(params.hostId ?? "gateway:local", params.threadId), async () => {
1533
+ return await params.bindingStore.withThreadArchiveFence(async () => {
1534
+ const run = async (control) => {
1535
+ await requireCatalogEligibleThread(control, params.threadId);
1536
+ await assertNoPendingSupervisionBranch(params);
1537
+ const thread = await control.readThread(params.threadId, false);
1538
+ if (thread.id !== params.threadId) throw new Error("Codex app-server returned a different thread than requested");
1539
+ requireIdleThread(thread, "archive");
1540
+ if (await params.bindingStore.hasOtherThreadOwner(params.threadId)) throw new CatalogParamsError("Codex session cannot be archived while it is attached to an OpenClaw session");
1541
+ await assertCodexArchiveDescendantsUnowned({
1542
+ bindingStore: params.bindingStore,
1543
+ threadId: params.threadId,
1544
+ listPage: (request) => control.listDescendantPage(request),
1545
+ assertDescendantIdle: async (descendantThreadId) => {
1546
+ const descendant = await control.readThread(descendantThreadId, false);
1547
+ if (descendant.id !== descendantThreadId) throw new Error("Codex app-server returned a different descendant than requested");
1548
+ requireIdleThread(descendant, "archive");
1549
+ }
1550
+ });
1551
+ await control.archiveThread(params.threadId);
1552
+ return { archived: true };
1553
+ };
1554
+ return await params.control.withPinnedConnection(run);
1555
+ });
1556
+ });
1557
+ }
1558
+ //#endregion
1559
+ //#region extensions/codex/src/session-catalog-create.ts
1560
+ const CODEX_AGENT_RUNTIME_ID = "codex";
1561
+ const CODEX_CATALOG_DEFAULT_MODEL_REF = "openai/gpt-5.6-sol";
1562
+ function resolveCodexCatalogCreateSession(config, requestedAgentId) {
1563
+ if (!config) return;
1564
+ const agentId = requestedAgentId ?? resolveDefaultAgentId(config);
1565
+ const defaultModel = resolveDefaultModelForAgent({
1566
+ cfg: config,
1567
+ agentId
1568
+ });
1569
+ return "error" in resolveAllowedModelRef({
1570
+ cfg: config,
1571
+ catalog: [],
1572
+ raw: CODEX_CATALOG_DEFAULT_MODEL_REF,
1573
+ defaultProvider: defaultModel.provider,
1574
+ defaultModel: defaultModel.model,
1575
+ agentId
1576
+ }) ? void 0 : {
1577
+ model: CODEX_CATALOG_DEFAULT_MODEL_REF,
1578
+ agentRuntime: CODEX_AGENT_RUNTIME_ID
1579
+ };
1580
+ }
1581
+ //#endregion
1582
+ //#region extensions/codex/src/session-catalog-node-continue.ts
1583
+ const CODEX_NODE_CONTINUE_COMMANDS = [
1584
+ CODEX_APP_SERVER_THREADS_LIST_COMMAND,
1585
+ CODEX_APP_SERVER_THREAD_TURNS_LIST_COMMAND,
1586
+ CODEX_CLI_SESSION_RESUME_COMMAND
1587
+ ];
1588
+ const NODE_CATALOG_LIST_RESPONSE_TIMEOUT_MS = 8e3;
1589
+ const continueNodeAdoption = createSessionCatalogAdoptionCoordinator();
1590
+ function nodeLabel(node) {
1591
+ return node.displayName?.trim() || node.remoteIp?.trim() || node.nodeId;
1592
+ }
1593
+ function compareNodeLabels(left, right) {
1594
+ const leftLabel = nodeLabel(left);
1595
+ const rightLabel = nodeLabel(right);
1596
+ if (leftLabel < rightLabel) return -1;
1597
+ if (leftLabel > rightLabel) return 1;
1598
+ return 0;
1599
+ }
1600
+ function canContinueCodexOnNode(node) {
1601
+ return node.connected === true && CODEX_NODE_CONTINUE_COMMANDS.every((command) => node.commands?.includes(command) === true && node.invocableCommands?.includes(command) === true);
1602
+ }
1603
+ async function listPairedNode(params) {
1604
+ const hostId = `node:${params.node.nodeId}`;
1605
+ const common = {
1606
+ hostId,
1607
+ label: nodeLabel(params.node),
1608
+ kind: "node",
1609
+ nodeId: params.node.nodeId,
1610
+ canContinueCodex: canContinueCodexOnNode(params.node)
1611
+ };
1612
+ if (params.node.connected !== true) {
1613
+ const host = {
1614
+ ...common,
1615
+ connected: false,
1616
+ sessions: [],
1617
+ error: {
1618
+ code: "NODE_OFFLINE",
1619
+ message: "Paired node is offline"
1662
1620
  }
1663
- }
1621
+ };
1622
+ params.onHost?.(host);
1623
+ return host;
1664
1624
  }
1665
- const activityId = `${newest.id}:${newestUserMessageCount}`;
1625
+ const eventualHost = Promise.resolve().then(async () => {
1626
+ const page = filterCatalogPageByTitle(parseCatalogPage(unwrapNodeInvokePayload(await params.runtime.nodes.invoke({
1627
+ nodeId: params.node.nodeId,
1628
+ command: CODEX_APP_SERVER_THREADS_LIST_COMMAND,
1629
+ params: {
1630
+ agentId: params.agentId,
1631
+ cursor: params.query.cursors?.[hostId],
1632
+ limit: params.query.limitPerHost,
1633
+ searchTerm: params.query.search
1634
+ },
1635
+ timeoutMs: NODE_INVOKE_TIMEOUT_MS,
1636
+ scopes: ["operator.write"]
1637
+ }))), params.query.search);
1638
+ return {
1639
+ ...common,
1640
+ connected: true,
1641
+ ...page,
1642
+ sessions: page.sessions.map((session) => {
1643
+ const adopted = params.adoptedSessions.get(sessionCatalogAdoptedSourceKey(hostId, session.threadId));
1644
+ return adopted ? Object.assign({}, session, { sessionKey: adopted.key }) : session;
1645
+ })
1646
+ };
1647
+ }).catch((error) => ({
1648
+ ...common,
1649
+ connected: true,
1650
+ sessions: [],
1651
+ error: catalogError("NODE_INVOKE_FAILED", error)
1652
+ }));
1653
+ if (params.onHost) eventualHost.then(params.onHost).catch(() => void 0);
1654
+ try {
1655
+ return await withTimeout(eventualHost, NODE_CATALOG_LIST_RESPONSE_TIMEOUT_MS, "paired node Codex session catalog timed out");
1656
+ } catch (error) {
1657
+ return {
1658
+ ...common,
1659
+ connected: true,
1660
+ sessions: [],
1661
+ error: catalogError("NODE_INVOKE_FAILED", error)
1662
+ };
1663
+ }
1664
+ }
1665
+ async function requireNodeForCodexContinue(params) {
1666
+ const nodeId = params.hostId.slice(5).trim();
1667
+ if (!nodeId || params.hostId !== `node:${nodeId}`) throw new CatalogParamsError("Codex session catalog hostId is invalid");
1668
+ const node = (await params.runtime.nodes.list()).nodes.find((candidate) => candidate.nodeId === nodeId);
1669
+ if (!node || !canContinueCodexOnNode(node)) throw new CatalogParamsError("paired node does not permit Codex session continuation");
1666
1670
  return {
1667
- kind: "activity",
1668
- sessionKey: params.probe.sessionKey,
1669
- humanTurns,
1670
- nextMarker: {
1671
- turnId: newest.id,
1672
- userMessageCount: newestUserMessageCount
1673
- },
1674
- ...humanTurns > 0 ? {
1675
- occurredAt: occurredAt ?? params.now ?? Date.now(),
1676
- dedupeId: activityId
1677
- } : {}
1671
+ node,
1672
+ nodeId
1678
1673
  };
1679
1674
  }
1680
- function countUserMessages(turn) {
1681
- return turn.items.filter((item) => item.type === "userMessage").length;
1675
+ async function resolveNodeCodexRecord(params) {
1676
+ let cursor;
1677
+ const seenCursors = /* @__PURE__ */ new Set();
1678
+ for (let pageIndex = 0; pageIndex < 100; pageIndex += 1) {
1679
+ const page = parseCatalogPage(unwrapNodeInvokePayload(await params.runtime.nodes.invoke({
1680
+ nodeId: params.nodeId,
1681
+ command: CODEX_APP_SERVER_THREADS_LIST_COMMAND,
1682
+ params: {
1683
+ agentId: params.agentId,
1684
+ limit: 100,
1685
+ ...cursor ? { cursor } : {}
1686
+ },
1687
+ timeoutMs: NODE_INVOKE_TIMEOUT_MS,
1688
+ scopes: ["operator.write"]
1689
+ })));
1690
+ const record = page.sessions.find((candidate) => candidate.threadId === params.threadId);
1691
+ if (record) return record;
1692
+ const nextCursor = page.nextCursor?.trim();
1693
+ if (!nextCursor) break;
1694
+ if (seenCursors.has(nextCursor)) throw new CatalogParamsError("Codex session eligibility could not be verified");
1695
+ seenCursors.add(nextCursor);
1696
+ cursor = nextCursor;
1697
+ }
1698
+ throw new CatalogParamsError("Codex session is unavailable on the paired node");
1682
1699
  }
1683
- function normalizeUserMessageTexts(item) {
1684
- const typed = item;
1685
- const contentTexts = typed.content?.filter((input) => input.type === "text").map((input) => input.text.trim().replace(/\s+/g, " ")).filter(Boolean);
1686
- return contentTexts?.length ? contentTexts : [(typed.text ?? "").trim().replace(/\s+/g, " ")];
1700
+ function requireContinuableNodeRecord(record) {
1701
+ if (record.archived) throw new CatalogParamsError("Codex session is archived on the paired node");
1702
+ if (!isInteractiveThreadSource(record.source)) throw new CatalogParamsError("Codex session is not a non-archived interactive Codex session");
1703
+ if (record.status === "idle" || record.status === "notLoaded") return;
1704
+ if (record.status === "active") throw new CatalogParamsError("Codex session is active on the paired node; wait for it to finish before continuing");
1705
+ throw new CatalogParamsError("Codex session cannot be continued in its current state");
1687
1706
  }
1688
- async function checkCodexUpstreamActivity(probes, control, resolveThreadId = async (probe) => probe.threadId) {
1689
- return await control.withPinnedConnection(async (pinned) => {
1690
- const activities = [];
1691
- for (const probe of probes) {
1692
- const fingerprint = upstreamConnectionFingerprint(probe);
1693
- if (probe.upstreamKind !== "codex-app-server" || !fingerprint || fingerprint !== pinned.connectionFingerprint) continue;
1694
- try {
1695
- const threadId = await resolveThreadId(probe);
1696
- const page = await pinned.listTurnPage({
1697
- threadId,
1698
- limit: CODEX_UPSTREAM_TURN_LIMIT,
1699
- sortDirection: "desc",
1700
- itemsView: "full"
1701
- });
1702
- const marker = readMarker(probe);
1703
- if (page.data.length === 0 && marker) {
1704
- try {
1705
- await pinned.readThread(threadId, false);
1706
- } catch (error) {
1707
- if (isCodexThreadGoneError(error)) activities.push({
1708
- kind: "missing",
1709
- sessionKey: probe.sessionKey
1710
- });
1711
- }
1712
- continue;
1713
- }
1714
- const activity = classifyCodexUpstreamTurns({
1715
- probe,
1716
- turns: page.data
1717
- });
1718
- if (activity) activities.push(activity);
1719
- } catch {}
1720
- }
1721
- return activities;
1722
- });
1707
+ async function readNodeCodexHistory(params) {
1708
+ const page = parseTranscriptPage(unwrapNodeInvokePayload(await params.runtime.nodes.invoke({
1709
+ nodeId: params.nodeId,
1710
+ command: CODEX_APP_SERVER_THREAD_TURNS_LIST_COMMAND,
1711
+ params: {
1712
+ agentId: params.agentId,
1713
+ threadId: params.record.threadId,
1714
+ limit: 50
1715
+ },
1716
+ timeoutMs: NODE_INVOKE_TIMEOUT_MS,
1717
+ scopes: ["operator.write"]
1718
+ })));
1719
+ const thread = {
1720
+ id: params.record.threadId,
1721
+ createdAt: params.record.createdAt ?? 0,
1722
+ modelProvider: params.record.modelProvider ?? "openai",
1723
+ turns: page.data.toReversed()
1724
+ };
1725
+ return {
1726
+ thread,
1727
+ throughTurnId: codexLastTerminalTurnId(thread, (value) => boundedCatalogString(value, 256)) ?? null
1728
+ };
1723
1729
  }
1724
- function createChecker(params) {
1725
- return async (probes) => await checkCodexUpstreamActivity(probes, params.control, async (probe) => {
1726
- const config = params.getRuntimeConfig();
1727
- const sessionId = params.api.runtime.agent.session.getSessionEntry({
1728
- agentId: probe.agentId,
1729
- sessionKey: probe.sessionKey,
1730
- readConsistency: "latest"
1731
- })?.sessionId?.trim();
1732
- if (!sessionId) return probe.threadId;
1733
- const binding = await params.bindingStore.read(sessionBindingIdentity({
1734
- sessionId,
1735
- sessionKey: probe.sessionKey,
1736
- config
1737
- }));
1738
- return binding?.connectionScope === "supervision" && binding.supervisionSourceThreadId === probe.threadId ? binding.threadId : probe.threadId;
1730
+ async function continueNodeCodexSessionInner(params) {
1731
+ const { nodeId } = await requireNodeForCodexContinue({
1732
+ runtime: params.api.runtime,
1733
+ hostId: params.hostId
1739
1734
  });
1740
- }
1741
- //#endregion
1742
- //#region extensions/codex/src/session-catalog.ts
1743
- const boundCatalogSessionId = (value) => boundedCatalogString(value, 256);
1744
- const CODEX_SUPERVISION_SESSION_KEY_PREFIX = "harness:codex:supervision:";
1745
- const CODEX_SESSION_CATALOG_LIST_TTL_MS = 32e3;
1746
- const CODEX_SESSION_CATALOG_LIST_CACHE_MAX_ENTRIES = 32;
1747
- function codexCatalogPageCacheKey(params) {
1748
- return JSON.stringify([
1749
- params.cursor ?? null,
1750
- params.limit ?? null,
1751
- params.searchTerm?.trim().toLocaleLowerCase() || null,
1752
- params.cwd?.trim() || null
1753
- ]);
1754
- }
1755
- function createCodexSessionCatalogControlFromRequests(params) {
1756
- return {
1757
- ...params.clientId ? { clientId: params.clientId } : {},
1758
- ...params.connectionFingerprint ? { connectionFingerprint: params.connectionFingerprint } : {},
1759
- withPinnedConnection: params.withPinnedConnection,
1760
- async listPage(pageParams) {
1761
- const limit = normalizeLimit(pageParams.limit, "limit");
1762
- const search = pageParams.searchTerm?.trim().toLocaleLowerCase() || void 0;
1763
- const cwd = pageParams.cwd?.trim() || void 0;
1764
- const maxPages = search ? 20 : 1;
1765
- const sessions = [];
1766
- let cursor = readControlCursor(pageParams.cursor, "request");
1767
- let nextCursor;
1768
- let backwardsCursor;
1769
- const seenCursors = new Set(cursor ? [cursor] : []);
1770
- const requests = params.createRequestSnapshot();
1771
- const deadline = params.now() + requests.requestTimeoutMs;
1772
- for (let pageIndex = 0; pageIndex < maxPages; pageIndex += 1) {
1773
- const remaining = limit - sessions.length;
1774
- const remainingTimeoutMs = Math.ceil(deadline - params.now());
1775
- if (remainingTimeoutMs <= 0) throw new Error("Codex session catalog listing timed out");
1776
- const response = await requests.listThreads({
1777
- archived: false,
1778
- limit: remaining,
1779
- modelProviders: [],
1780
- sortKey: "updated_at",
1781
- sortDirection: "desc",
1782
- ...cwd ? { cwd } : {},
1783
- ...cursor ? { cursor } : {}
1784
- }, remainingTimeoutMs);
1785
- if (pageIndex === 0) backwardsCursor = readControlCursor(response.backwardsCursor, "backwards response");
1786
- sessions.push(...response.data.flatMap((thread) => {
1787
- const session = toCatalogSession(thread, false);
1788
- return session ? [session] : [];
1789
- }).filter((session) => !search || session.name?.toLocaleLowerCase().includes(search)));
1790
- nextCursor = readControlCursor(response.nextCursor, "next response");
1791
- if (!nextCursor || sessions.length >= limit) break;
1792
- if (seenCursors.has(nextCursor)) throw new Error("Codex session catalog returned a repeated search cursor");
1793
- seenCursors.add(nextCursor);
1794
- cursor = nextCursor;
1795
- }
1796
- return {
1797
- sessions,
1798
- ...nextCursor ? { nextCursor } : {},
1799
- ...backwardsCursor ? { backwardsCursor } : {}
1800
- };
1801
- },
1802
- async listDescendantPage(listParams) {
1803
- const requests = params.createRequestSnapshot();
1804
- return await requests.listThreads(listParams, requests.requestTimeoutMs);
1805
- },
1806
- async readThread(threadId, includeTurns = false) {
1807
- return await params.createRequestSnapshot().readThread(threadId, includeTurns);
1808
- },
1809
- async listTurnPage(listParams) {
1810
- return await params.createRequestSnapshot().listThreadTurns(listParams);
1811
- },
1812
- async forkThread(forkParams) {
1813
- return await params.createRequestSnapshot().forkThread(forkParams);
1735
+ const record = await resolveNodeCodexRecord({
1736
+ agentId: params.agentId,
1737
+ runtime: params.api.runtime,
1738
+ nodeId,
1739
+ threadId: params.threadId
1740
+ });
1741
+ requireContinuableNodeRecord(record);
1742
+ const existing = findNodeAdoptedSessionEntry({
1743
+ agentId: params.agentId,
1744
+ config: params.config,
1745
+ runtime: params.api.runtime,
1746
+ hostId: params.hostId,
1747
+ threadId: params.threadId,
1748
+ includeInitializing: true
1749
+ });
1750
+ let adopted;
1751
+ let disposition;
1752
+ if (existing) {
1753
+ adopted = existing;
1754
+ disposition = "existing";
1755
+ } else {
1756
+ const history = await readNodeCodexHistory({
1757
+ agentId: params.agentId,
1758
+ runtime: params.api.runtime,
1759
+ nodeId,
1760
+ record
1761
+ });
1762
+ adopted = await createOrReuseNodeAdoptedSession({
1763
+ agentId: params.agentId,
1764
+ api: params.api,
1765
+ config: params.config,
1766
+ hostId: params.hostId,
1767
+ nodeId,
1768
+ record,
1769
+ history
1770
+ });
1771
+ disposition = "forked";
1772
+ }
1773
+ const marker = nodeSessionMarker({
1774
+ hostId: params.hostId,
1775
+ threadId: params.threadId,
1776
+ nodeId
1777
+ });
1778
+ return {
1779
+ sessionKey: adopted.key,
1780
+ disposition,
1781
+ conversationBinding: {
1782
+ summary: "Continue this Codex session on its paired node.",
1783
+ detachHint: "Start a new chat to leave the paired-node Codex session.",
1784
+ data: createCodexCliNodeConversationBindingData({
1785
+ nodeId,
1786
+ sessionId: record.sessionId?.trim() || params.threadId,
1787
+ agentId: adopted.agentId,
1788
+ cwd: record.cwd
1789
+ })
1814
1790
  },
1815
- async archiveThread(threadId) {
1816
- await params.createRequestSnapshot().archiveThread(threadId);
1817
- }
1791
+ afterConversationBound: async () => await finalizeNodeAdoptedSession({
1792
+ api: params.api,
1793
+ adopted,
1794
+ marker
1795
+ })
1818
1796
  };
1819
1797
  }
1820
- /** Builds the passive catalog over the Codex plugin's canonical shared client. */
1821
- function createCodexSessionCatalogControl(params) {
1822
- const now = params.now ?? Date.now;
1823
- const getPluginConfig = () => params.getPluginConfig();
1824
- const requestOptionsByConfig = /* @__PURE__ */ new WeakMap();
1825
- const catalogPagesByConfig = /* @__PURE__ */ new WeakMap();
1826
- const resolveRequestOptions = (startOptions) => {
1827
- const runtimeConfig = params.getRuntimeConfig();
1828
- if (!runtimeConfig) return {
1829
- config: void 0,
1830
- startOptions: structuredClone(startOptions)
1831
- };
1832
- const cached = requestOptionsByConfig.get(runtimeConfig);
1833
- if (cached) return cached;
1834
- const resolved = {
1835
- config: structuredClone(runtimeConfig),
1836
- startOptions: structuredClone(startOptions)
1837
- };
1838
- requestOptionsByConfig.set(runtimeConfig, resolved);
1839
- return resolved;
1840
- };
1841
- const createRequestSnapshot = () => {
1842
- const pluginConfig = getPluginConfig();
1843
- const runtime = resolveCodexSupervisionAppServerRuntimeOptions({ pluginConfig });
1844
- const requestOptions = resolveRequestOptions(runtime.start);
1845
- return {
1846
- requestTimeoutMs: runtime.requestTimeoutMs,
1847
- listThreads: async (listParams, timeoutMs) => await codexControlRequest(pluginConfig, CODEX_CONTROL_METHODS.listThreads, listParams, {
1848
- ...requestOptions,
1849
- timeoutMs
1850
- }),
1851
- readThread: async (threadId, includeTurns) => (await codexControlRequest(pluginConfig, CODEX_CONTROL_METHODS.readThread, {
1852
- threadId,
1853
- includeTurns
1854
- }, requestOptions)).thread,
1855
- listThreadTurns: async (listParams) => await codexControlRequest(pluginConfig, CODEX_CONTROL_METHODS.listThreadTurns, listParams, requestOptions),
1856
- forkThread: async (forkParams) => await codexControlRequest(pluginConfig, CODEX_CONTROL_METHODS.forkThread, assertCodexThreadForkParams(forkParams), requestOptions),
1857
- archiveThread: async (threadId) => {
1858
- await codexControlRequest(pluginConfig, CODEX_CONTROL_METHODS.archiveThread, { threadId }, requestOptions);
1859
- }
1860
- };
1861
- };
1862
- const withPinnedConnection = async (run) => {
1863
- const runtime = resolveCodexSupervisionAppServerRuntimeOptions({ pluginConfig: getPluginConfig() });
1864
- const { config: runtimeConfig, startOptions } = resolveRequestOptions(runtime.start);
1865
- const client = await getLeasedSharedCodexAppServerClient({
1866
- config: runtimeConfig,
1867
- startOptions,
1868
- timeoutMs: runtime.requestTimeoutMs
1798
+ async function continueNodeCodexSession(params) {
1799
+ if (params.clientScopes?.includes("operator.admin") !== true) throw new CatalogParamsError("continuing a paired-node Codex session requires operator.admin");
1800
+ const nodeId = params.hostId.slice(5).trim();
1801
+ if (!nodeId || params.hostId !== `node:${nodeId}`) throw new CatalogParamsError("Codex session catalog hostId is invalid");
1802
+ const agentId = resolveSessionAgentIds({
1803
+ config: params.config,
1804
+ agentId: params.agentId
1805
+ }).defaultAgentId;
1806
+ const sourceKey = sessionCatalogAdoptedSourceKey(`node:${nodeId}`, params.threadId);
1807
+ const operationKey = sessionCatalogAdoptedSourceKey(agentId, sourceKey);
1808
+ return await continueNodeAdoption({
1809
+ sourceKey: operationKey,
1810
+ findExisting: () => void 0,
1811
+ create: () => runSessionActionExclusive(sourceKey, async () => continueNodeCodexSessionInner({
1812
+ ...params,
1813
+ agentId
1814
+ })),
1815
+ complete: async (continued) => continued
1816
+ });
1817
+ }
1818
+ //#endregion
1819
+ //#region extensions/codex/src/session-catalog-listing.ts
1820
+ async function listVisiblePage(params) {
1821
+ const excluded = params.excludedThreadIds;
1822
+ const sessions = [];
1823
+ let cursor = params.cursor;
1824
+ let nextCursor;
1825
+ let backwardsCursor;
1826
+ const seenCursors = /* @__PURE__ */ new Set();
1827
+ for (let pageIndex = 0; pageIndex < 20; pageIndex += 1) {
1828
+ let excludedFromPage = false;
1829
+ const rawPage = await params.control.listPage({
1830
+ limit: params.limit - sessions.length,
1831
+ ...cursor ? { cursor } : {},
1832
+ ...params.searchTerm ? { searchTerm: params.searchTerm } : {},
1833
+ ...params.cwd ? { cwd: params.cwd } : {}
1869
1834
  });
1870
- try {
1871
- const requests = {
1872
- requestTimeoutMs: runtime.requestTimeoutMs,
1873
- listThreads: async (listParams, timeoutMs) => await requestCodexAppServerClientJson({
1874
- client,
1875
- method: CODEX_CONTROL_METHODS.listThreads,
1876
- requestParams: listParams,
1877
- config: runtimeConfig,
1878
- timeoutMs
1879
- }),
1880
- readThread: async (threadId, includeTurns) => (await requestCodexAppServerClientJson({
1881
- client,
1882
- method: CODEX_CONTROL_METHODS.readThread,
1883
- requestParams: {
1884
- threadId,
1885
- includeTurns
1886
- },
1887
- config: runtimeConfig,
1888
- timeoutMs: runtime.requestTimeoutMs
1889
- })).thread,
1890
- listThreadTurns: async (listParams) => await requestCodexAppServerClientJson({
1891
- client,
1892
- method: CODEX_CONTROL_METHODS.listThreadTurns,
1893
- requestParams: listParams,
1894
- config: runtimeConfig,
1895
- timeoutMs: runtime.requestTimeoutMs
1896
- }),
1897
- forkThread: async (forkParams) => await requestCodexAppServerClientJson({
1898
- client,
1899
- method: CODEX_CONTROL_METHODS.forkThread,
1900
- requestParams: assertCodexThreadForkParams(forkParams),
1901
- config: runtimeConfig,
1902
- timeoutMs: runtime.requestTimeoutMs
1903
- }),
1904
- archiveThread: async (threadId) => {
1905
- await requestCodexAppServerClientJson({
1906
- client,
1907
- method: CODEX_CONTROL_METHODS.archiveThread,
1908
- requestParams: { threadId },
1909
- config: runtimeConfig,
1910
- timeoutMs: runtime.requestTimeoutMs
1911
- });
1912
- }
1913
- };
1914
- const pinnedControl = createCodexSessionCatalogControlFromRequests({
1915
- clientId: resolveCodexAppServerClientInstanceId(client),
1916
- connectionFingerprint: buildCodexAppServerConnectionFingerprint(runtime, resolveDefaultAgentDir(runtimeConfig ?? {})),
1917
- createRequestSnapshot: () => requests,
1918
- now,
1919
- withPinnedConnection: async (nestedRun) => await nestedRun(pinnedControl)
1920
- });
1921
- return await run(pinnedControl);
1922
- } finally {
1923
- releaseLeasedSharedCodexAppServerClient(client);
1835
+ const page = filterCatalogPageByTitle(parseCatalogPage(rawPage), params.searchTerm);
1836
+ if (pageIndex === 0) backwardsCursor = page.backwardsCursor;
1837
+ for (const managed of rawPage.managedThreads ?? []) {
1838
+ excludedFromPage = true;
1839
+ await params.onExcludedThread?.(managed);
1924
1840
  }
1925
- };
1926
- const control = createCodexSessionCatalogControlFromRequests({
1927
- createRequestSnapshot,
1928
- now,
1929
- withPinnedConnection
1930
- });
1931
- return {
1932
- ...control,
1933
- async listPage(pageParams) {
1934
- const runtimeConfig = params.getRuntimeConfig();
1935
- if (!runtimeConfig) return await control.listPage(pageParams);
1936
- let cache = catalogPagesByConfig.get(runtimeConfig);
1937
- if (!cache) {
1938
- cache = /* @__PURE__ */ new Map();
1939
- catalogPagesByConfig.set(runtimeConfig, cache);
1940
- }
1941
- const key = codexCatalogPageCacheKey(pageParams);
1942
- const cached = cache.get(key);
1943
- if (pageParams.forceRefresh !== true && cached) {
1944
- cache.delete(key);
1945
- cache.set(key, cached);
1946
- if (cached.stalePage) return await cached.stalePage;
1947
- if (cached.expiresAt > now()) return await cached.page;
1948
- const stalePage = cached.settledPage;
1949
- if (!stalePage) return await cached.page;
1950
- const page = control.listPage(pageParams);
1951
- const entry = {
1952
- expiresAt: Number.POSITIVE_INFINITY,
1953
- page,
1954
- settledPage: stalePage,
1955
- stalePage
1956
- };
1957
- cache.set(key, entry);
1958
- page.then(() => {
1959
- if (cache.get(key) === entry) {
1960
- delete entry.stalePage;
1961
- entry.settledPage = page;
1962
- entry.expiresAt = now() + CODEX_SESSION_CATALOG_LIST_TTL_MS;
1963
- }
1964
- }, async () => {
1965
- let stale;
1966
- try {
1967
- stale = await stalePage;
1968
- } catch {}
1969
- if (cache.get(key) !== entry) return;
1970
- if (stale) {
1971
- cache.delete(key);
1972
- const settledPage = Promise.resolve(stale);
1973
- cache.set(key, {
1974
- expiresAt: now(),
1975
- page: settledPage,
1976
- settledPage
1977
- });
1978
- } else cache.delete(key);
1979
- });
1980
- return await stalePage;
1981
- }
1982
- if (cached) cache.delete(key);
1983
- const serveStaleOnError = pageParams.forceRefresh !== true;
1984
- const page = control.listPage(pageParams);
1985
- const stalePage = cached?.settledPage;
1986
- const entry = {
1987
- expiresAt: Number.POSITIVE_INFINITY,
1988
- page,
1989
- ...stalePage ? {
1990
- stalePage,
1991
- settledPage: stalePage
1992
- } : {}
1993
- };
1994
- cache.set(key, entry);
1995
- while (cache.size > CODEX_SESSION_CATALOG_LIST_CACHE_MAX_ENTRIES) {
1996
- const oldest = cache.keys().next();
1997
- if (oldest.done) break;
1998
- cache.delete(oldest.value);
1999
- }
2000
- try {
2001
- const result = await page;
2002
- if (cache.get(key) === entry) {
2003
- delete entry.stalePage;
2004
- entry.settledPage = page;
2005
- entry.expiresAt = now() + CODEX_SESSION_CATALOG_LIST_TTL_MS;
2006
- }
2007
- return result;
2008
- } catch (error) {
2009
- if (stalePage) {
2010
- let stale;
2011
- try {
2012
- stale = await stalePage;
2013
- } catch {}
2014
- if (stale) {
2015
- if (cache.get(key) === entry) {
2016
- cache.delete(key);
2017
- const settledPage = Promise.resolve(stale);
2018
- cache.set(key, {
2019
- expiresAt: now(),
2020
- page: settledPage,
2021
- settledPage
2022
- });
2023
- }
2024
- if (serveStaleOnError) return stale;
2025
- }
2026
- }
2027
- if (cache.get(key) === entry) cache.delete(key);
2028
- throw error;
1841
+ for (const session of page.sessions) {
1842
+ if (!excluded?.has(session.threadId)) {
1843
+ sessions.push(session);
1844
+ continue;
2029
1845
  }
1846
+ excludedFromPage = true;
1847
+ await params.onExcludedThread?.({ threadId: session.threadId });
2030
1848
  }
1849
+ nextCursor = page.nextCursor;
1850
+ if (!nextCursor || sessions.length >= params.limit || !excludedFromPage) break;
1851
+ if (seenCursors.has(nextCursor)) throw new Error("Codex session catalog returned a repeated exclusion cursor");
1852
+ seenCursors.add(nextCursor);
1853
+ cursor = nextCursor;
1854
+ }
1855
+ return {
1856
+ sessions: sessions.slice(0, params.limit),
1857
+ ...nextCursor ? { nextCursor } : {},
1858
+ ...backwardsCursor ? { backwardsCursor } : {}
2031
1859
  };
2032
1860
  }
2033
1861
  async function listGatewayHost(params) {
1862
+ const hostId = params.source?.hostId ?? "gateway:local";
1863
+ const label = params.source?.label ?? "Local Codex";
1864
+ const sourceHomeId = params.source?.sourceHomeId ?? "gateway:local";
2034
1865
  try {
2035
- const page = parseCatalogPage(await params.control.listPage({
1866
+ const page = await listVisiblePage({
1867
+ control: params.control,
1868
+ cursor: params.query.cursors?.[hostId],
1869
+ excludedThreadIds: params.excludedThreadIds,
2036
1870
  limit: params.query.limitPerHost,
2037
- ...params.query.cursors?.["gateway:local"] ? { cursor: params.query.cursors[CODEX_LOCAL_SESSION_HOST_ID] } : {},
2038
- ...params.query.search ? { searchTerm: params.query.search } : {}
2039
- }));
1871
+ onExcludedThread: params.onExcludedThread,
1872
+ searchTerm: params.query.search
1873
+ });
2040
1874
  const adoptedSessions = await listAdoptedSessionEntries({
1875
+ agentId: params.agentId,
2041
1876
  bindingStore: params.bindingStore,
2042
1877
  config: params.config,
2043
1878
  runtime: params.runtime,
2044
1879
  sessionEntries: params.sessionEntries
2045
1880
  });
2046
1881
  return {
2047
- hostId: CODEX_LOCAL_SESSION_HOST_ID,
2048
- label: "Local Codex",
1882
+ hostId,
1883
+ label,
2049
1884
  kind: "gateway",
2050
1885
  connected: true,
2051
1886
  ...page,
2052
1887
  sessions: page.sessions.map((session) => {
2053
- const adopted = adoptedSessions.get(session.threadId);
2054
- return adopted ? Object.assign({}, session, { sessionKey: adopted.key }) : session;
1888
+ const adopted = adoptedSessions.get(sessionCatalogAdoptedSourceKey(sourceHomeId, session.threadId)) ?? (hostId === "gateway:local" ? adoptedSessions.get(sessionCatalogAdoptedSourceKey("gateway:local", session.threadId)) : void 0);
1889
+ const sourced = params.source ? Object.assign({}, session, { sourceHomeId: params.source.sourceHomeId }) : session;
1890
+ return adopted ? Object.assign({}, sourced, { sessionKey: adopted.key }) : sourced;
2055
1891
  })
2056
1892
  };
2057
1893
  } catch (error) {
2058
1894
  return {
2059
- hostId: CODEX_LOCAL_SESSION_HOST_ID,
2060
- label: "Local Codex",
1895
+ hostId,
1896
+ label,
2061
1897
  kind: "gateway",
2062
1898
  connected: false,
2063
1899
  sessions: [],
@@ -2067,16 +1903,37 @@ async function listGatewayHost(params) {
2067
1903
  }
2068
1904
  /** Lists Gateway-local and paired-node Codex sessions with per-host failures. */
2069
1905
  async function listCodexSessionCatalog(params) {
1906
+ const agentId = resolveSessionAgentIds({
1907
+ config: params.config ?? {},
1908
+ agentId: params.agentId
1909
+ }).sessionAgentId;
2070
1910
  const query = readGatewayParams(params.query);
2071
1911
  const requestedHostIds = query.hostIds ? new Set(query.hostIds) : void 0;
2072
- const localHosts = params.includeLocal !== false && (!requestedHostIds || requestedHostIds.has("gateway:local")) ? [listGatewayHost({
2073
- bindingStore: params.bindingStore,
2074
- config: params.config,
2075
- control: params.control,
2076
- query,
2077
- runtime: params.runtime,
2078
- sessionEntries: params.sessionEntries
2079
- })] : [];
1912
+ const localSources = params.localHomes?.filter((source) => !requestedHostIds || requestedHostIds.has(source.hostId)) ?? (params.includeLocal !== false && (!requestedHostIds || requestedHostIds.has("gateway:local")) ? [void 0] : []);
1913
+ const managedThreads = await params.bindingStore.managedThreads?.snapshot();
1914
+ const fallbackSource = params.control.homesForAgent(agentId)[0];
1915
+ const localHosts = localSources.map((source) => (() => {
1916
+ const ownershipSource = source ?? fallbackSource;
1917
+ const managedThreadIds = ownershipSource ? managedThreads?.get(ownershipSource.sourceHomeId) : void 0;
1918
+ return listGatewayHost({
1919
+ agentId,
1920
+ bindingStore: params.bindingStore,
1921
+ config: params.config,
1922
+ control: params.control.forRequest(agentId, ownershipSource),
1923
+ query,
1924
+ runtime: params.runtime,
1925
+ sessionEntries: params.sessionEntries,
1926
+ excludedThreadIds: managedThreadIds,
1927
+ ...ownershipSource && params.bindingStore.managedThreads ? { onExcludedThread: async ({ threadId, rolloutPath }) => {
1928
+ if (!managedThreadIds?.has(threadId)) await params.bindingStore.managedThreads?.mark({
1929
+ sourceHomeId: ownershipSource.sourceHomeId,
1930
+ threadId,
1931
+ ...rolloutPath ? { rolloutPath } : {}
1932
+ });
1933
+ } } : {},
1934
+ ...source ? { source } : {}
1935
+ });
1936
+ })());
2080
1937
  for (const host of localHosts) if (params.onHost) host.then(params.onHost).catch(() => void 0);
2081
1938
  if (!(!requestedHostIds || query.hostIds?.some((hostId) => hostId.startsWith("node:")))) return { hosts: await Promise.all(localHosts) };
2082
1939
  let nodes;
@@ -2095,12 +1952,14 @@ async function listCodexSessionCatalog(params) {
2095
1952
  return { hosts: [...await Promise.all(localHosts), registryHost] };
2096
1953
  }
2097
1954
  const adoptedNodeSessions = listNodeAdoptedSessionEntries({
1955
+ agentId,
2098
1956
  config: params.config,
2099
1957
  runtime: params.runtime,
2100
1958
  sessionEntries: params.sessionEntries
2101
1959
  });
2102
1960
  const nodeHosts = nodes.toSorted(compareNodeLabels).map(async (node) => {
2103
1961
  const host = await listPairedNode({
1962
+ agentId,
2104
1963
  runtime: params.runtime,
2105
1964
  node,
2106
1965
  query,
@@ -2112,16 +1971,55 @@ async function listCodexSessionCatalog(params) {
2112
1971
  return { hosts: await Promise.all([...localHosts, ...nodeHosts]) };
2113
1972
  }
2114
1973
  /** Builds the node-local read-only Codex app-server catalog command. */
2115
- function createCodexSessionCatalogNodeHostCommands(control, configSources) {
1974
+ function createCodexSessionCatalogNodeHostCommands(controlFactory, configSources, bindingStore) {
1975
+ const bindRequest = (paramsJSON) => {
1976
+ const parsed = parseJsonParams(paramsJSON);
1977
+ if (!isRecord(parsed)) throw new CatalogParamsError("Codex session catalog parameters must be an object");
1978
+ const requestedAgentId = readBoundedOptionalString(parsed, "agentId", 256);
1979
+ const config = configSources.getRuntimeConfig() ?? {};
1980
+ const agentId = resolveSessionAgentIds({
1981
+ config,
1982
+ agentId: requestedAgentId
1983
+ }).sessionAgentId;
1984
+ if (!listAgentIds(config).includes(agentId)) throw new CatalogParamsError(`unknown Codex session catalog agent: ${agentId}`);
1985
+ const request = { ...parsed };
1986
+ delete request.agentId;
1987
+ const source = controlFactory.homesForAgent(agentId)[0];
1988
+ return {
1989
+ agentId,
1990
+ control: controlFactory.forRequest(agentId, source),
1991
+ sourceHomeId: source?.sourceHomeId,
1992
+ params: request,
1993
+ paramsJSON: JSON.stringify(request)
1994
+ };
1995
+ };
2116
1996
  return [
2117
1997
  {
2118
1998
  command: CODEX_APP_SERVER_THREADS_LIST_COMMAND,
2119
1999
  cap: CODEX_APP_SERVER_THREADS_CAPABILITY,
2120
2000
  dangerous: false,
2121
2001
  handle: async (paramsJSON) => {
2122
- const pageParams = readPageParams(parseJsonParams(paramsJSON));
2002
+ const request = bindRequest(paramsJSON);
2003
+ const pageParams = readPageParams(request.params);
2123
2004
  try {
2124
- const page = filterCatalogPageByTitle(parseCatalogPage(await control.listPage(pageParams)), pageParams.searchTerm);
2005
+ const managedThreads = await bindingStore?.managedThreads?.snapshot();
2006
+ const sourceHomeId = request.sourceHomeId;
2007
+ const managedThreadIds = sourceHomeId ? managedThreads?.get(sourceHomeId) : void 0;
2008
+ const page = await listVisiblePage({
2009
+ control: request.control,
2010
+ cursor: pageParams.cursor,
2011
+ cwd: pageParams.cwd,
2012
+ excludedThreadIds: managedThreadIds,
2013
+ limit: pageParams.limit,
2014
+ ...sourceHomeId && bindingStore?.managedThreads ? { onExcludedThread: async ({ threadId, rolloutPath }) => {
2015
+ if (!managedThreadIds?.has(threadId)) await bindingStore.managedThreads?.mark({
2016
+ sourceHomeId,
2017
+ threadId,
2018
+ ...rolloutPath ? { rolloutPath } : {}
2019
+ });
2020
+ } } : {},
2021
+ searchTerm: pageParams.searchTerm
2022
+ });
2125
2023
  return JSON.stringify(page);
2126
2024
  } catch {
2127
2025
  throw new Error("Codex app-server catalog is unavailable");
@@ -2133,10 +2031,11 @@ function createCodexSessionCatalogNodeHostCommands(control, configSources) {
2133
2031
  cap: CODEX_APP_SERVER_THREADS_CAPABILITY,
2134
2032
  dangerous: false,
2135
2033
  handle: async (paramsJSON) => {
2136
- const action = readNodeTranscriptParams(parseJsonParams(paramsJSON));
2034
+ const request = bindRequest(paramsJSON);
2035
+ const action = readNodeTranscriptParams(request.params);
2137
2036
  try {
2138
- await requireCatalogEligibleThread(control, action.threadId);
2139
- const page = parseTranscriptPage(await control.listTurnPage({
2037
+ await requireCatalogEligibleThread(request.control, action.threadId);
2038
+ const page = parseTranscriptPage(await request.control.listTurnPage({
2140
2039
  threadId: action.threadId,
2141
2040
  limit: action.limit,
2142
2041
  sortDirection: "desc",
@@ -2150,7 +2049,7 @@ function createCodexSessionCatalogNodeHostCommands(control, configSources) {
2150
2049
  }
2151
2050
  }
2152
2051
  },
2153
- createCodexTerminalNodeHostCommand(control, configSources)
2052
+ createCodexTerminalNodeHostCommand(bindRequest, configSources)
2154
2053
  ];
2155
2054
  }
2156
2055
  function readNodeTranscriptParams(value) {
@@ -2179,18 +2078,19 @@ function flattenTranscriptPageDesc(page) {
2179
2078
  }
2180
2079
  /** Reads the persisted transcript for a Gateway-local or paired-node Codex session. */
2181
2080
  async function readCodexSessionTranscript(params) {
2182
- if (params.hostId === "gateway:local") {
2081
+ if (params.source || params.hostId === "gateway:local") {
2183
2082
  await requireCatalogEligibleThread(params.control, params.threadId);
2184
- const page = parseTranscriptPage(await params.control.listTurnPage({
2083
+ const listParams = {
2185
2084
  threadId: params.threadId,
2186
2085
  limit: params.limit,
2187
2086
  sortDirection: "desc",
2188
2087
  itemsView: "full",
2189
2088
  ...params.cursor ? { cursor: params.cursor } : {}
2190
- }));
2089
+ };
2090
+ const page = parseTranscriptPage(await params.control.listTurnPage(listParams));
2191
2091
  return {
2192
2092
  hostId: params.hostId,
2193
- label: "Local Codex",
2093
+ label: params.source?.label ?? "Local Codex",
2194
2094
  threadId: params.threadId,
2195
2095
  items: flattenTranscriptPageDesc(page),
2196
2096
  ...page.nextCursor ? { nextCursor: page.nextCursor } : {},
@@ -2204,6 +2104,7 @@ async function readCodexSessionTranscript(params) {
2204
2104
  nodeId,
2205
2105
  command: CODEX_APP_SERVER_THREAD_TURNS_LIST_COMMAND,
2206
2106
  params: {
2107
+ agentId: params.agentId,
2207
2108
  threadId: params.threadId,
2208
2109
  limit: params.limit,
2209
2110
  ...params.cursor ? { cursor: params.cursor } : {}
@@ -2220,332 +2121,629 @@ async function readCodexSessionTranscript(params) {
2220
2121
  ...page.backwardsCursor ? { backwardsCursor: page.backwardsCursor } : {}
2221
2122
  };
2222
2123
  }
2223
- function requireIdleThread(thread, action) {
2224
- if (thread.status?.type === "idle" || action === "archive" && thread.status?.type === "notLoaded") return;
2225
- 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"}`);
2226
- throw new CatalogParamsError(action === "archive" ? "Codex session cannot be archived in its current state" : "Codex session cannot start a branch in its current state");
2227
- }
2228
- function adoptionSessionKey(threadId) {
2229
- const digest = createHash("sha256").update(threadId).digest("hex");
2230
- return `${CODEX_SUPERVISION_SESSION_KEY_PREFIX}${digest}`;
2231
- }
2232
- function isAdoptionSessionKeyForThread(sessionKey, threadId) {
2233
- return adoptionSessionKeyRest(sessionKey) === adoptionSessionKey(threadId);
2234
- }
2235
- async function listAdoptedSessionEntries(params) {
2236
- const adopted = /* @__PURE__ */ new Map();
2237
- for (const { agentId, entry, sessionKey } of listSessionCatalogEntries({
2238
- config: params.config ?? {},
2239
- runtime: params.runtime,
2240
- sessionEntries: params.sessionEntries
2241
- })) {
2242
- const sessionKeyRest = adoptionSessionKeyRest(sessionKey);
2243
- if (!sessionKeyRest.startsWith(CODEX_SUPERVISION_SESSION_KEY_PREFIX) || entry.initializationPending === true || entry.agentHarnessId !== "codex" || entry.modelSelectionLocked !== true) continue;
2244
- const sessionId = entry.sessionId?.trim();
2245
- if (!sessionId) continue;
2246
- const binding = await params.bindingStore.read(sessionBindingIdentity({
2247
- sessionId,
2248
- sessionKey,
2249
- config: params.config
2250
- }));
2251
- const sourceThreadId = binding?.supervisionSourceThreadId?.trim();
2252
- const boundThreadId = binding?.threadId.trim();
2253
- if (binding?.connectionScope !== "supervision" || !sourceThreadId || !boundThreadId || sessionKeyRest !== adoptionSessionKey(sourceThreadId)) continue;
2254
- if (adopted.has(sourceThreadId)) throw new Error(`multiple OpenClaw sessions adopt Codex thread ${sourceThreadId}`);
2255
- adopted.set(sourceThreadId, {
2256
- key: sessionKey,
2257
- sessionId,
2258
- agentId,
2259
- boundThreadId
2260
- });
2261
- }
2262
- return adopted;
2263
- }
2264
- async function findAdoptedSessionEntry(params) {
2265
- return (await listAdoptedSessionEntries(params)).get(params.threadId);
2266
- }
2267
- async function clearCreatedAdoptionBinding(params) {
2268
- let cleared = false;
2269
- let clearError;
2270
- try {
2271
- cleared = await params.bindingStore.mutate(params.identity, {
2272
- kind: "clear",
2273
- threadId: params.sourceThreadId,
2274
- expectedPendingSupervisionBranch: params.expectedPending
2275
- });
2276
- } catch (error) {
2277
- clearError = error;
2278
- }
2279
- if (cleared) return;
2280
- let current;
2281
- try {
2282
- current = await params.bindingStore.read(params.identity);
2283
- } catch (readError) {
2284
- throw new AggregateError([
2285
- params.cause,
2286
- ...clearError ? [clearError] : [],
2287
- readError
2288
- ], `OpenClaw session creation failed and the Codex binding could not be verified for ${params.sourceThreadId}`, { cause: readError });
2289
- }
2290
- if (!matchesPendingSupervisionOwner(current, params.expectedPending)) return;
2291
- throw new AggregateError([params.cause, ...clearError ? [clearError] : []], `OpenClaw session creation failed and the Codex binding could not be cleared for ${params.sourceThreadId}`, { cause: params.cause });
2292
- }
2293
- function matchesPendingAdoptionBinding(binding, expected) {
2294
- const historyCoveredThrough = binding?.historyCoveredThrough;
2295
- 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));
2124
+ //#endregion
2125
+ //#region extensions/codex/src/session-catalog-transcript-item.ts
2126
+ const CODEX_MESSAGE_TYPES = /* @__PURE__ */ new Map([
2127
+ ["userMessage", "userMessage"],
2128
+ ["agentMessage", "agentMessage"],
2129
+ ["reasoning", "reasoning"]
2130
+ ]);
2131
+ const CODEX_TOOL_TYPES = /* @__PURE__ */ new Set([
2132
+ "commandExecution",
2133
+ "fileChange",
2134
+ "mcpToolCall",
2135
+ "dynamicToolCall",
2136
+ "collabAgentToolCall",
2137
+ "webSearch",
2138
+ "imageView",
2139
+ "imageGeneration"
2140
+ ]);
2141
+ function toGenericTranscriptItem(item) {
2142
+ let type = CODEX_MESSAGE_TYPES.get(item.type);
2143
+ if (!type && CODEX_TOOL_TYPES.has(item.type)) type = item.result !== void 0 || Boolean(item.aggregatedOutput) ? "toolResult" : "toolCall";
2144
+ type ??= "other";
2145
+ const fallback = item.title ?? item.name ?? item.tool ?? item.command ?? item.query ?? void 0;
2146
+ const resultText = item.aggregatedOutput || (item.result === void 0 ? void 0 : JSON.stringify(item.result, null, 2));
2147
+ const changesText = Array.isArray(item.changes) ? item.changes.map((change) => `${change.kind}: ${change.path}`).join("\n") || void 0 : void 0;
2148
+ const text = item.text || resultText || changesText || fallback;
2149
+ return {
2150
+ id: item.id,
2151
+ type,
2152
+ ...text ? { text } : {},
2153
+ raw: item
2154
+ };
2296
2155
  }
2297
- function matchesPendingSupervisionOwner(binding, expected) {
2298
- const pending = binding?.pendingSupervisionBranch;
2299
- const cleanupThreadIds = pending?.cleanupThreadIds ?? [];
2300
- const expectedCleanupThreadIds = expected.cleanupThreadIds ?? [];
2301
- 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]);
2156
+ //#endregion
2157
+ //#region extensions/codex/src/session-upstream-activity.ts
2158
+ const CODEX_UPSTREAM_TURN_LIMIT = 100;
2159
+ const CODEX_APP_SERVER_INVALID_REQUEST_CODE = -32600;
2160
+ const CODEX_THREAD_NOT_LOADED_MESSAGE_PREFIX = "thread not loaded:";
2161
+ function isCodexThreadGoneError(error) {
2162
+ return error instanceof CodexAppServerRpcError && error.code === CODEX_APP_SERVER_INVALID_REQUEST_CODE && error.message.startsWith(CODEX_THREAD_NOT_LOADED_MESSAGE_PREFIX);
2302
2163
  }
2303
- async function ensurePendingAdoptionBinding(params) {
2304
- const pending = {
2305
- sourceThreadId: params.sourceThreadId,
2306
- connectionFingerprint: params.connectionFingerprint,
2307
- ...params.lastTurnId ? { lastTurnId: params.lastTurnId } : {}
2164
+ function readMarker(probe) {
2165
+ if (!isRecord(probe.marker)) return;
2166
+ const turnId = probe.marker.turnId;
2167
+ if (turnId !== null && typeof turnId !== "string") return;
2168
+ const count = probe.marker.userMessageCount;
2169
+ if (count !== void 0 && (!Number.isSafeInteger(count) || count < 0)) return;
2170
+ return {
2171
+ turnId,
2172
+ ...count === void 0 ? {} : { userMessageCount: count }
2308
2173
  };
2309
- if (!await reclaimCurrentCodexSessionGeneration({
2310
- bindingStore: params.bindingStore,
2311
- identity: params.identity,
2312
- config: params.config
2313
- })) throw new Error(`failed to claim the OpenClaw session generation for ${params.sourceThreadId}`);
2314
- const existing = await params.bindingStore.read(params.identity);
2315
- if (existing) {
2316
- if (matchesPendingAdoptionBinding(existing, params)) return;
2317
- throw new Error(`OpenClaw session is already bound to Codex thread ${existing.threadId}`);
2174
+ }
2175
+ function upstreamConnectionFingerprint(probe) {
2176
+ return isRecord(probe.upstreamRef) && typeof probe.upstreamRef.connectionFingerprint === "string" ? probe.upstreamRef.connectionFingerprint : void 0;
2177
+ }
2178
+ function classifyCodexUpstreamTurns(params) {
2179
+ const marker = readMarker(params.probe);
2180
+ if (!marker) return;
2181
+ const newest = params.turns[0];
2182
+ if (!newest?.id) return;
2183
+ const markerIndex = marker.turnId === null ? -1 : params.turns.findIndex((turn) => turn.id === marker.turnId);
2184
+ const candidateTurns = markerIndex < 0 ? params.turns : params.turns.slice(0, markerIndex + 1);
2185
+ const newestUserMessageCount = countUserMessages(newest);
2186
+ if (!(marker.turnId !== newest.id || marker.userMessageCount === void 0 || newestUserMessageCount > marker.userMessageCount)) return;
2187
+ const ownTexts = new Set(params.probe.ownRecentUserTexts);
2188
+ let humanTurns = 0;
2189
+ let occurredAt;
2190
+ for (const turn of candidateTurns) {
2191
+ const userMessages = turn.items.filter((item) => item.type === "userMessage");
2192
+ const alreadySeen = turn.id === marker.turnId ? marker.userMessageCount ?? userMessages.length : 0;
2193
+ for (const item of userMessages.slice(alreadySeen)) {
2194
+ const texts = normalizeUserMessageTexts(item);
2195
+ if (ownTexts.has(texts.join(" ")) || texts.length > 1 && texts.every((text) => ownTexts.has(text))) continue;
2196
+ humanTurns += 1;
2197
+ if (occurredAt === void 0) {
2198
+ const timestampSeconds = turn.completedAt ?? turn.startedAt;
2199
+ occurredAt = typeof timestampSeconds === "number" && Number.isFinite(timestampSeconds) ? timestampSeconds * 1e3 : params.now ?? Date.now();
2200
+ }
2201
+ }
2318
2202
  }
2319
- const binding = {
2320
- threadId: params.sourceThreadId,
2321
- connectionScope: "supervision",
2322
- supervisionSourceThreadId: params.sourceThreadId,
2323
- cwd: params.cwd,
2324
- historyCoveredThrough: (/* @__PURE__ */ new Date()).toISOString(),
2325
- conversationSourceTransferComplete: true,
2326
- preserveNativeModel: true,
2327
- pendingSupervisionBranch: pending
2203
+ const activityId = `${newest.id}:${newestUserMessageCount}`;
2204
+ return {
2205
+ kind: "activity",
2206
+ sessionKey: params.probe.sessionKey,
2207
+ humanTurns,
2208
+ nextMarker: {
2209
+ turnId: newest.id,
2210
+ userMessageCount: newestUserMessageCount
2211
+ },
2212
+ ...humanTurns > 0 ? {
2213
+ occurredAt: occurredAt ?? params.now ?? Date.now(),
2214
+ dedupeId: activityId
2215
+ } : {}
2328
2216
  };
2329
- let stored;
2330
- try {
2331
- stored = await params.bindingStore.mutate(params.identity, {
2332
- kind: "set",
2333
- if: { kind: "absent" },
2334
- binding
2335
- });
2336
- } catch (error) {
2337
- if (matchesPendingAdoptionBinding(await params.bindingStore.read(params.identity), params)) return;
2338
- throw error;
2339
- }
2340
- if (stored) return;
2341
- if (!matchesPendingAdoptionBinding(await params.bindingStore.read(params.identity), params)) throw new Error(`failed to bind OpenClaw session to Codex thread ${params.sourceThreadId}`);
2342
2217
  }
2343
- async function createOrReuseAdoptedSession(params) {
2344
- const existing = await findAdoptedSessionEntry({
2345
- bindingStore: params.bindingStore,
2346
- config: params.config,
2347
- runtime: params.api.runtime,
2348
- threadId: params.sourceThread.id
2349
- });
2350
- if (existing) return existing;
2351
- let createdBindingIdentity;
2352
- let createdPendingBinding;
2353
- try {
2354
- const spawnedCwd = params.sourceThread.cwd?.trim() || void 0;
2355
- const pendingLastTurnId = codexLastTerminalTurnId(params.sourceThread, boundCatalogSessionId);
2356
- const marker = { sourceThreadId: params.sourceThread.id };
2357
- const created = await createImportedCodexSession({
2358
- runtime: params.api.runtime,
2359
- config: params.config,
2360
- key: adoptionSessionKey(params.sourceThread.id),
2361
- agentId: resolveDefaultAgentId(params.config),
2362
- thread: params.sourceThread,
2363
- throughTurnId: pendingLastTurnId ?? null,
2364
- recoverMatchingInitialEntry: true,
2365
- initialEntry: {
2366
- agentHarnessId: "codex",
2367
- modelSelectionLocked: true,
2368
- pluginExtensions: { codex: { supervision: {
2369
- ...marker,
2370
- initializing: true,
2371
- modelLocked: true
2372
- } } }
2373
- },
2374
- afterImport: async (entry) => {
2375
- createdBindingIdentity = sessionBindingIdentity({
2376
- sessionId: entry.sessionId,
2377
- sessionKey: entry.key,
2378
- config: params.config
2218
+ function countUserMessages(turn) {
2219
+ return turn.items.filter((item) => item.type === "userMessage").length;
2220
+ }
2221
+ function normalizeUserMessageTexts(item) {
2222
+ const typed = item;
2223
+ const contentTexts = typed.content?.filter((input) => input.type === "text").map((input) => input.text.trim().replace(/\s+/g, " ")).filter(Boolean);
2224
+ return contentTexts?.length ? contentTexts : [(typed.text ?? "").trim().replace(/\s+/g, " ")];
2225
+ }
2226
+ async function checkCodexUpstreamActivity(probes, control, resolveThreadId = async (probe) => probe.threadId) {
2227
+ return await control.withPinnedConnection(async (pinned) => {
2228
+ const activities = [];
2229
+ for (const probe of probes) {
2230
+ const fingerprint = upstreamConnectionFingerprint(probe);
2231
+ if (probe.upstreamKind !== "codex-app-server" || !fingerprint || fingerprint !== pinned.connectionFingerprint) continue;
2232
+ try {
2233
+ const threadId = await resolveThreadId(probe);
2234
+ const page = await pinned.listTurnPage({
2235
+ threadId,
2236
+ limit: CODEX_UPSTREAM_TURN_LIMIT,
2237
+ sortDirection: "desc",
2238
+ itemsView: "full"
2379
2239
  });
2380
- createdPendingBinding = {
2381
- sourceThreadId: params.sourceThread.id,
2382
- connectionFingerprint: params.connectionFingerprint,
2383
- ...pendingLastTurnId ? { lastTurnId: pendingLastTurnId } : {}
2384
- };
2385
- await ensurePendingAdoptionBinding({
2386
- bindingStore: params.bindingStore,
2387
- config: params.config,
2388
- identity: createdBindingIdentity,
2389
- sourceThreadId: params.sourceThread.id,
2390
- connectionFingerprint: params.connectionFingerprint,
2391
- cwd: spawnedCwd ?? "",
2392
- ...pendingLastTurnId ? { lastTurnId: pendingLastTurnId } : {}
2240
+ const marker = readMarker(probe);
2241
+ if (page.data.length === 0 && marker) {
2242
+ try {
2243
+ await pinned.readThread(threadId, false);
2244
+ } catch (error) {
2245
+ if (isCodexThreadGoneError(error)) activities.push({
2246
+ kind: "missing",
2247
+ sessionKey: probe.sessionKey
2248
+ });
2249
+ }
2250
+ continue;
2251
+ }
2252
+ const activity = classifyCodexUpstreamTurns({
2253
+ probe,
2254
+ turns: page.data
2393
2255
  });
2394
- return { pluginExtensions: { codex: { supervision: {
2395
- ...marker,
2396
- modelLocked: true
2397
- } } } };
2398
- }
2399
- });
2400
- return {
2401
- key: created.key,
2402
- sessionId: created.sessionId,
2403
- agentId: created.agentId,
2404
- boundThreadId: params.sourceThread.id
2405
- };
2406
- } catch (error) {
2407
- let raced = await findAdoptedSessionEntry({
2408
- bindingStore: params.bindingStore,
2409
- config: params.config,
2410
- runtime: params.api.runtime,
2411
- threadId: params.sourceThread.id
2412
- });
2413
- if (raced) return raced;
2414
- if (createdBindingIdentity && createdPendingBinding) {
2415
- await clearCreatedAdoptionBinding({
2416
- bindingStore: params.bindingStore,
2417
- identity: createdBindingIdentity,
2418
- sourceThreadId: params.sourceThread.id,
2419
- expectedPending: createdPendingBinding,
2420
- cause: error
2421
- });
2422
- raced = await findAdoptedSessionEntry({
2423
- bindingStore: params.bindingStore,
2424
- config: params.config,
2425
- runtime: params.api.runtime,
2426
- threadId: params.sourceThread.id
2427
- });
2428
- if (raced) return raced;
2256
+ if (activity) activities.push(activity);
2257
+ } catch {}
2429
2258
  }
2430
- throw error;
2431
- }
2432
- }
2433
- async function continueLocalCodexSessionInner(params) {
2434
- await requireCatalogEligibleThread(params.control, params.threadId);
2435
- const existing = await findAdoptedSessionEntry({
2436
- bindingStore: params.bindingStore,
2437
- config: params.config,
2438
- runtime: params.api.runtime,
2439
- threadId: params.threadId
2440
- });
2441
- if (existing) {
2442
- const boundThreadId = requireBoundThread(existing);
2443
- const boundThread = await params.control.readThread(boundThreadId, true);
2444
- if (boundThread.id !== boundThreadId) throw new Error("Codex app-server returned a different thread than requested");
2445
- const changedError = () => new CatalogParamsError("Codex OpenClaw session changed before it could be opened. Retry.");
2446
- if (!await params.api.runtime.agent.session.patchSessionEntry({
2447
- sessionKey: existing.key,
2448
- readConsistency: "latest",
2449
- preserveActivity: true,
2450
- update: (entry) => {
2451
- if (entry.sessionId?.trim() !== existing.sessionId || entry.initializationPending === true || entry.agentHarnessId !== "codex" || entry.modelSelectionLocked !== true) throw changedError();
2452
- return { archivedAt: void 0 };
2453
- }
2454
- })) throw changedError();
2455
- const connectionFingerprint = params.control.connectionFingerprint;
2456
- if (connectionFingerprint) params.onContinued?.({
2457
- connectionFingerprint,
2458
- ...codexUpstreamBaseline(boundThread, boundCatalogSessionId)
2459
- });
2460
- return {
2461
- sessionKey: existing.key,
2462
- disposition: "existing"
2463
- };
2464
- }
2465
- const sourceThread = await params.control.readThread(params.threadId, true);
2466
- if (sourceThread.id !== params.threadId) throw new Error("Codex app-server returned a different thread than requested");
2467
- if (sourceThread.status?.type !== "notLoaded") requireIdleThread(sourceThread, "continue");
2468
- const connectionFingerprint = params.control.connectionFingerprint;
2469
- if (!connectionFingerprint) throw new Error("Codex Continue requires a pinned app-server connection");
2470
- const adopted = await createOrReuseAdoptedSession({
2471
- api: params.api,
2472
- bindingStore: params.bindingStore,
2473
- config: params.config,
2474
- sourceThread,
2475
- connectionFingerprint
2476
- });
2477
- const boundThreadId = requireBoundThread(adopted);
2478
- const baselineThread = boundThreadId === sourceThread.id ? sourceThread : await params.control.readThread(boundThreadId, true);
2479
- if (baselineThread.id !== boundThreadId) throw new Error("Codex app-server returned a different thread than requested");
2480
- params.onContinued?.({
2481
- connectionFingerprint,
2482
- ...codexUpstreamBaseline(baselineThread, boundCatalogSessionId)
2259
+ return activities;
2483
2260
  });
2484
- return {
2485
- sessionKey: adopted.key,
2486
- disposition: "forked"
2261
+ }
2262
+ function createChecker(params) {
2263
+ const resolveThreadId = async (probe) => {
2264
+ const config = params.getRuntimeConfig();
2265
+ const sessionId = params.api.runtime.agent.session.getSessionEntry({
2266
+ agentId: probe.agentId,
2267
+ sessionKey: probe.sessionKey,
2268
+ readConsistency: "latest"
2269
+ })?.sessionId?.trim();
2270
+ if (!sessionId) return probe.threadId;
2271
+ const binding = await params.bindingStore.read(sessionBindingIdentity({
2272
+ sessionId,
2273
+ sessionKey: probe.sessionKey,
2274
+ config
2275
+ }));
2276
+ return binding?.connectionScope === "supervision" && binding.supervisionSourceThreadId === probe.threadId ? binding.threadId : probe.threadId;
2277
+ };
2278
+ return async (probes) => {
2279
+ const groups = /* @__PURE__ */ new Map();
2280
+ for (const probe of probes) {
2281
+ const fingerprint = upstreamConnectionFingerprint(probe);
2282
+ if (!fingerprint) continue;
2283
+ const control = params.control.forUpstream(probe.agentId, fingerprint);
2284
+ if (!control) continue;
2285
+ const key = `${probe.agentId}\0${fingerprint}`;
2286
+ const group = groups.get(key) ?? {
2287
+ control,
2288
+ probes: []
2289
+ };
2290
+ group.probes.push(probe);
2291
+ groups.set(key, group);
2292
+ }
2293
+ return (await Promise.all([...groups.values()].map((group) => checkCodexUpstreamActivity(group.probes, group.control, resolveThreadId)))).flat();
2487
2294
  };
2488
2295
  }
2489
- /** Creates one locked OpenClaw branch whose first harness run forks the Codex source. */
2490
- async function continueLocalCodexSession(params) {
2491
- const sourceKey = adoptedSourceKey(CODEX_LOCAL_SESSION_HOST_ID, params.threadId);
2492
- const current = continueOperations.get(sourceKey);
2493
- if (current) return await current;
2494
- const operation = runSessionActionExclusive(sourceKey, async () => params.control.withPinnedConnection(async (control) => continueLocalCodexSessionInner({
2495
- ...params,
2496
- control
2497
- })));
2498
- continueOperations.set(sourceKey, operation);
2296
+ //#endregion
2297
+ //#region extensions/codex/src/session-catalog-homes.ts
2298
+ function existingCatalogHomeCandidates(value, label) {
2299
+ const codexHome = canonicalCodexCatalogHome(value);
2499
2300
  try {
2500
- return await operation;
2501
- } finally {
2502
- if (continueOperations.get(sourceKey) === operation) continueOperations.delete(sourceKey);
2301
+ if (!fsSync.statSync(codexHome).isDirectory()) return [];
2302
+ } catch {
2303
+ return [];
2503
2304
  }
2305
+ return [{
2306
+ codexHome,
2307
+ label: `Local Codex · ${label ?? path.basename(codexHome)}`
2308
+ }];
2504
2309
  }
2505
- async function assertNoPendingSupervisionBranch(params) {
2506
- const adoptedEntries = listSupervisionAgentIds(params.config).flatMap((agentId) => params.runtime.agent.session.listSessionEntries({
2507
- agentId,
2508
- readOnly: true
2509
- })).filter((candidate) => isAdoptionSessionKeyForThread(candidate.sessionKey, params.threadId));
2510
- for (const adopted of adoptedEntries) {
2511
- if (adopted.entry.initializationPending === true) throw new CatalogParamsError("Codex session cannot be archived while its OpenClaw branch is initializing");
2512
- const sessionId = adopted.entry.sessionId?.trim();
2513
- if (!sessionId) continue;
2514
- const binding = await params.bindingStore.read(sessionBindingIdentity({
2515
- sessionId,
2516
- sessionKey: adopted.sessionKey,
2517
- config: params.config
2310
+ /** Resolves every local Codex store the operator already owns, without path disclosure. */
2311
+ function resolveCodexCatalogHomes(params) {
2312
+ const { config, env, ownerAgentId, pluginConfig } = params;
2313
+ const ownerAgentDir = resolveAgentDir(config, ownerAgentId, env);
2314
+ const configuredHomes = readCodexPluginConfig(pluginConfig).sessionCatalog?.homes ?? [];
2315
+ const base = resolveCodexSupervisionAppServerRuntimeOptions({
2316
+ pluginConfig,
2317
+ env,
2318
+ agentDir: ownerAgentDir,
2319
+ config
2320
+ });
2321
+ const primaryCodexHome = canonicalCodexCatalogHome(resolveCodexAppServerLocalHomeDir(base.start, ownerAgentDir, env));
2322
+ const processUserHome = canonicalCodexCatalogHome(resolveCodexAppServerUserHomeDir(env));
2323
+ const processHomeConfigured = Boolean(env.CODEX_HOME?.trim());
2324
+ const candidates = [{
2325
+ codexHome: primaryCodexHome,
2326
+ label: "Local Codex",
2327
+ usesProcessHomeFallback: base.start.transport === "stdio" && base.start.homeScope === "user" && !processHomeConfigured
2328
+ }];
2329
+ if (base.start.transport === "stdio") {
2330
+ candidates.push({
2331
+ codexHome: processUserHome,
2332
+ label: "Local Codex · user",
2333
+ usesProcessHomeFallback: !processHomeConfigured
2334
+ });
2335
+ const agentIds = listAgentIds(config).toSorted((left, right) => left === ownerAgentId ? -1 : right === ownerAgentId ? 1 : left.localeCompare(right));
2336
+ candidates.push(...agentIds.flatMap((agentId) => existingCatalogHomeCandidates(resolveCodexAppServerHomeDir(resolveAgentDir(config, agentId, env)), agentId)), ...configuredHomes.flatMap((entry) => {
2337
+ const { path: home, label } = typeof entry === "string" ? { path: entry } : entry;
2338
+ return existingCatalogHomeCandidates(home, label);
2518
2339
  }));
2519
- 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");
2520
2340
  }
2341
+ const seen = /* @__PURE__ */ new Set();
2342
+ const homes = [];
2343
+ for (const candidate of candidates) {
2344
+ if (seen.has(candidate.codexHome)) continue;
2345
+ seen.add(candidate.codexHome);
2346
+ const sourceHomeId = codexCatalogHomeId(candidate.codexHome);
2347
+ const primary = homes.length === 0;
2348
+ homes.push({
2349
+ sourceHomeId,
2350
+ hostId: primary ? CODEX_LOCAL_SESSION_HOST_ID : `${CODEX_LOCAL_SESSION_HOST_ID}:${sourceHomeId}`,
2351
+ label: candidate.label,
2352
+ agentDir: ownerAgentDir,
2353
+ appServer: primary ? base : {
2354
+ ...base,
2355
+ start: {
2356
+ ...base.start,
2357
+ homeScope: "user",
2358
+ env: {
2359
+ ...base.start.env,
2360
+ CODEX_HOME: candidate.codexHome
2361
+ }
2362
+ }
2363
+ },
2364
+ ...base.connectionClass === "remote" ? {} : { localSessionsRoot: path.join(candidate.codexHome, "sessions") },
2365
+ usesProcessHomeFallback: candidate.usesProcessHomeFallback ?? false
2366
+ });
2367
+ if (homes.length >= 100) break;
2368
+ }
2369
+ return homes;
2370
+ }
2371
+ /** Discovers Codex homes once per immutable Gateway config generation. */
2372
+ function createCodexCatalogHomeResolver(params) {
2373
+ const env = params.env ?? process.env;
2374
+ const homesByConfig = /* @__PURE__ */ new WeakMap();
2375
+ const buildSnapshot = (config) => {
2376
+ const pluginConfig = params.getPluginConfig();
2377
+ const homesByAgent = new Map(listAgentIds(config).map((agentId) => [agentId, resolveCodexCatalogHomes({
2378
+ config,
2379
+ pluginConfig,
2380
+ ownerAgentId: agentId,
2381
+ env
2382
+ })]));
2383
+ replaceCodexCatalogConnectionHomes([...homesByAgent.values()].flatMap((homes) => homes.filter((home) => home.appServer.start.transport === "stdio").map((home) => ({
2384
+ agentDir: home.agentDir,
2385
+ fingerprint: buildCodexAppServerConnectionFingerprint(home.appServer, home.agentDir),
2386
+ codexHome: resolveCodexAppServerLocalHomeDir(home.appServer.start, home.agentDir, env)
2387
+ }))));
2388
+ homesByConfig.set(config, homesByAgent);
2389
+ return homesByAgent;
2390
+ };
2391
+ let lastSnapshot = buildSnapshot(params.config);
2392
+ return { forAgent(agentId) {
2393
+ const config = params.getRuntimeConfig();
2394
+ if (!config) return lastSnapshot.get(agentId) ?? [];
2395
+ const cached = homesByConfig.get(config);
2396
+ if (cached) return cached.get(agentId) ?? [];
2397
+ lastSnapshot = buildSnapshot(config);
2398
+ return lastSnapshot.get(agentId) ?? [];
2399
+ } };
2521
2400
  }
2522
- /** Archives one inactive Gateway-local Codex thread after a fresh status read. */
2523
- async function archiveLocalCodexSession(params) {
2524
- return await runSessionActionExclusive(adoptedSourceKey(CODEX_LOCAL_SESSION_HOST_ID, params.threadId), async () => {
2525
- return await params.bindingStore.withThreadArchiveFence(async () => {
2526
- return await params.control.withPinnedConnection(async (control) => {
2527
- await requireCatalogEligibleThread(control, params.threadId);
2528
- await assertNoPendingSupervisionBranch(params);
2529
- const thread = await control.readThread(params.threadId, false);
2530
- if (thread.id !== params.threadId) throw new Error("Codex app-server returned a different thread than requested");
2531
- requireIdleThread(thread, "archive");
2532
- if (await params.bindingStore.hasOtherThreadOwner(params.threadId)) throw new CatalogParamsError("Codex session cannot be archived while it is attached to an OpenClaw session");
2533
- await assertCodexArchiveDescendantsUnowned({
2534
- bindingStore: params.bindingStore,
2535
- threadId: params.threadId,
2536
- listPage: (request) => control.listDescendantPage(request),
2537
- assertDescendantIdle: async (descendantThreadId) => {
2538
- const descendant = await control.readThread(descendantThreadId, false);
2539
- if (descendant.id !== descendantThreadId) throw new Error("Codex app-server returned a different descendant than requested");
2540
- requireIdleThread(descendant, "archive");
2401
+ //#endregion
2402
+ //#region extensions/codex/src/session-catalog-provenance.ts
2403
+ const MAX_SESSION_META_BYTES = 1024 * 1024;
2404
+ const SESSION_META_READ_CHUNK_BYTES = 64 * 1024;
2405
+ const MAX_PROVENANCE_CACHE_ENTRIES = 2e4;
2406
+ const provenanceByPath = /* @__PURE__ */ new Map();
2407
+ function cacheProvenance(key, value) {
2408
+ provenanceByPath.delete(key);
2409
+ provenanceByPath.set(key, value);
2410
+ while (provenanceByPath.size > MAX_PROVENANCE_CACHE_ENTRIES) {
2411
+ const oldest = provenanceByPath.keys().next().value;
2412
+ if (oldest === void 0) break;
2413
+ provenanceByPath.delete(oldest);
2414
+ }
2415
+ }
2416
+ /** Undefined means the metadata line is not durable enough to cache yet. */
2417
+ async function readOpenClawOriginator(sessionsRoot, rolloutPath, threadId) {
2418
+ let safeRoot;
2419
+ try {
2420
+ safeRoot = await root(sessionsRoot, {
2421
+ hardlinks: "reject",
2422
+ maxBytes: Number.MAX_SAFE_INTEGER,
2423
+ symlinks: "reject"
2424
+ });
2425
+ } catch {
2426
+ return;
2427
+ }
2428
+ const candidates = rolloutPath.endsWith(".zst") ? [rolloutPath, rolloutPath.slice(0, -4)] : [rolloutPath, `${rolloutPath}.zst`];
2429
+ for (const candidate of candidates) {
2430
+ let opened;
2431
+ try {
2432
+ opened = await safeRoot.open(path.relative(sessionsRoot, candidate));
2433
+ } catch {
2434
+ continue;
2435
+ }
2436
+ const input = opened.handle.createReadStream({
2437
+ autoClose: false,
2438
+ highWaterMark: SESSION_META_READ_CHUNK_BYTES
2439
+ });
2440
+ const reader = candidate.endsWith(".zst") ? input.pipe(createZstdDecompress()) : input;
2441
+ try {
2442
+ const chunks = [];
2443
+ let bytesReadTotal = 0;
2444
+ let line;
2445
+ for await (const value of reader) {
2446
+ const chunk = Buffer.isBuffer(value) ? value : Buffer.from(value);
2447
+ const remaining = MAX_SESSION_META_BYTES - bytesReadTotal;
2448
+ if (remaining <= 0) break;
2449
+ const bounded = chunk.subarray(0, remaining);
2450
+ bytesReadTotal += bounded.length;
2451
+ const newline = bounded.indexOf(10);
2452
+ chunks.push(newline >= 0 ? bounded.subarray(0, newline) : bounded);
2453
+ if (newline >= 0) {
2454
+ line = Buffer.concat(chunks).toString("utf8");
2455
+ break;
2456
+ }
2457
+ }
2458
+ if (!line) continue;
2459
+ let parsed;
2460
+ try {
2461
+ parsed = JSON.parse(line);
2462
+ } catch {
2463
+ continue;
2464
+ }
2465
+ if (!isRecord(parsed) || parsed.type !== "session_meta" || !isRecord(parsed.payload)) return false;
2466
+ const payload = parsed.payload;
2467
+ return (payload.id ?? payload.session_id) === threadId && payload.originator === "openclaw";
2468
+ } catch {
2469
+ continue;
2470
+ } finally {
2471
+ reader.destroy();
2472
+ input.destroy();
2473
+ await opened.handle.close().catch(() => void 0);
2474
+ }
2475
+ }
2476
+ }
2477
+ /**
2478
+ * Codex 0.147 reports OpenClaw app-server rollouts as `vscode`, so the rollout's
2479
+ * immutable session metadata is the authoritative historical provenance.
2480
+ */
2481
+ async function isOpenClawManagedCodexThread(thread, localSessionsRoot) {
2482
+ const rolloutPath = typeof thread.path === "string" ? thread.path.trim() : "";
2483
+ if (!localSessionsRoot || !rolloutPath) return false;
2484
+ const cacheKey = `${localSessionsRoot}\0${rolloutPath}`;
2485
+ const cached = provenanceByPath.get(cacheKey);
2486
+ if (cached !== void 0) return cached;
2487
+ const managed = await readOpenClawOriginator(localSessionsRoot, rolloutPath, thread.id);
2488
+ if (managed !== void 0) cacheProvenance(cacheKey, managed);
2489
+ return managed ?? false;
2490
+ }
2491
+ //#endregion
2492
+ //#region extensions/codex/src/session-catalog-control.ts
2493
+ const CODEX_SESSION_CATALOG_LIST_TTL_MS = 32e3;
2494
+ const CODEX_SESSION_CATALOG_LIST_CACHE_MAX_ENTRIES = 32;
2495
+ function codexCatalogPageCacheKey(params, agentId, source) {
2496
+ return JSON.stringify([
2497
+ agentId,
2498
+ source?.sourceHomeId ?? null,
2499
+ params.cursor ?? null,
2500
+ params.limit ?? null,
2501
+ params.searchTerm?.trim().toLocaleLowerCase() || null,
2502
+ params.cwd?.trim() || null
2503
+ ]);
2504
+ }
2505
+ function createCodexCatalogRequestSnapshot(requestTimeoutMs, request) {
2506
+ return {
2507
+ requestTimeoutMs,
2508
+ listThreads: (params, timeoutMs) => request(CODEX_CONTROL_METHODS.listThreads, params, timeoutMs),
2509
+ listThreadTurns: (params) => request(CODEX_CONTROL_METHODS.listThreadTurns, params),
2510
+ forkThread: (params) => request(CODEX_CONTROL_METHODS.forkThread, assertCodexThreadForkParams(params)),
2511
+ readThread: async (threadId, includeTurns) => (await request(CODEX_CONTROL_METHODS.readThread, {
2512
+ threadId,
2513
+ includeTurns
2514
+ })).thread,
2515
+ archiveThread: async (threadId) => {
2516
+ await request(CODEX_CONTROL_METHODS.archiveThread, { threadId });
2517
+ }
2518
+ };
2519
+ }
2520
+ function createCodexSessionCatalogControlFromRequests(params) {
2521
+ return {
2522
+ ...params.clientId ? { clientId: params.clientId } : {},
2523
+ ...params.connectionFingerprint ? { connectionFingerprint: params.connectionFingerprint } : {},
2524
+ withPinnedConnection: params.withPinnedConnection,
2525
+ async listPage(pageParams) {
2526
+ const limit = normalizeLimit(pageParams.limit, "limit");
2527
+ const search = pageParams.searchTerm?.trim().toLocaleLowerCase() || void 0;
2528
+ const cwd = pageParams.cwd?.trim() || void 0;
2529
+ const maxPages = search ? 20 : 1;
2530
+ const sessions = [];
2531
+ const managedThreads = [];
2532
+ let cursor = readControlCursor(pageParams.cursor, "request");
2533
+ let nextCursor;
2534
+ let backwardsCursor;
2535
+ const seenCursors = new Set(cursor ? [cursor] : []);
2536
+ const requests = params.createRequestSnapshot();
2537
+ const deadline = params.now() + requests.requestTimeoutMs;
2538
+ for (let pageIndex = 0; pageIndex < maxPages; pageIndex += 1) {
2539
+ const remainingTimeoutMs = Math.ceil(deadline - params.now());
2540
+ if (remainingTimeoutMs <= 0) throw new Error("Codex session catalog listing timed out");
2541
+ const response = await requests.listThreads({
2542
+ archived: false,
2543
+ limit: limit - sessions.length,
2544
+ modelProviders: [],
2545
+ sortKey: "updated_at",
2546
+ sortDirection: "desc",
2547
+ ...cwd ? { cwd } : {},
2548
+ ...cursor ? { cursor } : {}
2549
+ }, remainingTimeoutMs);
2550
+ if (pageIndex === 0) backwardsCursor = readControlCursor(response.backwardsCursor, "backwards response");
2551
+ for (const thread of response.data) {
2552
+ if (await isOpenClawManagedCodexThread(thread, params.localSessionsRoot)) {
2553
+ const rolloutPath = typeof thread.path === "string" ? thread.path.trim() : "";
2554
+ managedThreads.push({
2555
+ threadId: thread.id,
2556
+ ...rolloutPath ? { rolloutPath } : {}
2557
+ });
2558
+ continue;
2541
2559
  }
2542
- });
2543
- await control.archiveThread(params.threadId);
2544
- return { archived: true };
2560
+ const session = toCatalogSession(thread, false);
2561
+ if (session && (!search || session.name?.toLocaleLowerCase().includes(search))) sessions.push(session);
2562
+ }
2563
+ nextCursor = readControlCursor(response.nextCursor, "next response");
2564
+ if (!nextCursor || sessions.length >= limit) break;
2565
+ if (seenCursors.has(nextCursor)) throw new Error("Codex session catalog returned a repeated search cursor");
2566
+ seenCursors.add(nextCursor);
2567
+ cursor = nextCursor;
2568
+ }
2569
+ return {
2570
+ sessions,
2571
+ ...managedThreads.length > 0 ? { managedThreads } : {},
2572
+ ...nextCursor ? { nextCursor } : {},
2573
+ ...backwardsCursor ? { backwardsCursor } : {}
2574
+ };
2575
+ },
2576
+ async listDescendantPage(listParams) {
2577
+ const requests = params.createRequestSnapshot();
2578
+ return await requests.listThreads(listParams, requests.requestTimeoutMs);
2579
+ },
2580
+ async readThread(threadId, includeTurns = false) {
2581
+ return await params.createRequestSnapshot().readThread(threadId, includeTurns);
2582
+ },
2583
+ async listTurnPage(listParams) {
2584
+ return await params.createRequestSnapshot().listThreadTurns(listParams);
2585
+ },
2586
+ async forkThread(forkParams) {
2587
+ return await params.createRequestSnapshot().forkThread(forkParams);
2588
+ },
2589
+ async archiveThread(threadId) {
2590
+ await params.createRequestSnapshot().archiveThread(threadId);
2591
+ }
2592
+ };
2593
+ }
2594
+ /** Builds the passive catalog over the Codex plugin's canonical shared client. */
2595
+ function createCodexSessionCatalogControl(params) {
2596
+ const now = params.now ?? Date.now;
2597
+ const getPluginConfig = () => params.getPluginConfig();
2598
+ const homeResolver = createCodexCatalogHomeResolver({
2599
+ config: params.getRuntimeConfig() ?? params.config ?? {},
2600
+ getRuntimeConfig: params.getRuntimeConfig,
2601
+ getPluginConfig: params.getPluginConfig,
2602
+ ...params.env ? { env: params.env } : {}
2603
+ });
2604
+ const requestOptionsByConfig = /* @__PURE__ */ new WeakMap();
2605
+ const catalogPagesByConfig = /* @__PURE__ */ new WeakMap();
2606
+ const resolveRequestOptions = (startOptions, agentId, source) => {
2607
+ const runtimeConfig = params.getRuntimeConfig();
2608
+ const agentDir = source?.agentDir ?? resolveAgentDir(runtimeConfig ?? {}, agentId);
2609
+ const resolvedStartOptions = source?.appServer.start ?? startOptions;
2610
+ if (!runtimeConfig) return {
2611
+ agentDir,
2612
+ config: void 0,
2613
+ startOptions: structuredClone(resolvedStartOptions)
2614
+ };
2615
+ let byAgent = requestOptionsByConfig.get(runtimeConfig);
2616
+ const cacheKey = `${agentId ?? ""}\0${source?.sourceHomeId ?? ""}`;
2617
+ const cached = byAgent?.get(cacheKey);
2618
+ if (cached) return cached;
2619
+ const resolved = {
2620
+ agentDir,
2621
+ config: structuredClone(runtimeConfig),
2622
+ startOptions: structuredClone(resolvedStartOptions)
2623
+ };
2624
+ if (!byAgent) {
2625
+ byAgent = /* @__PURE__ */ new Map();
2626
+ requestOptionsByConfig.set(runtimeConfig, byAgent);
2627
+ }
2628
+ byAgent.set(cacheKey, resolved);
2629
+ return resolved;
2630
+ };
2631
+ const createRequestSnapshot = (agentId, source) => {
2632
+ const pluginConfig = getPluginConfig();
2633
+ const runtime = source?.appServer ?? resolveCodexSupervisionAppServerRuntimeOptions({ pluginConfig });
2634
+ const requestOptions = resolveRequestOptions(runtime.start, agentId, source);
2635
+ return createCodexCatalogRequestSnapshot(runtime.requestTimeoutMs, async (method, requestParams, timeoutMs) => await codexControlRequest(pluginConfig, method, requestParams, {
2636
+ ...requestOptions,
2637
+ ...timeoutMs === void 0 ? {} : { timeoutMs }
2638
+ }));
2639
+ };
2640
+ const forRequest = (agentId, source) => {
2641
+ const withPinnedConnection = async (run) => {
2642
+ const pluginConfig = getPluginConfig();
2643
+ const runtime = source?.appServer ?? resolveCodexSupervisionAppServerRuntimeOptions({ pluginConfig });
2644
+ const { agentDir, config: runtimeConfig, startOptions } = resolveRequestOptions(runtime.start, agentId, source);
2645
+ const client = await getLeasedSharedCodexAppServerClient({
2646
+ agentDir,
2647
+ config: runtimeConfig,
2648
+ startOptions,
2649
+ timeoutMs: runtime.requestTimeoutMs
2545
2650
  });
2651
+ try {
2652
+ const requests = createCodexCatalogRequestSnapshot(runtime.requestTimeoutMs, async (method, requestParams, timeoutMs) => await requestCodexAppServerClientJson({
2653
+ client,
2654
+ method,
2655
+ requestParams,
2656
+ config: runtimeConfig,
2657
+ timeoutMs: timeoutMs ?? runtime.requestTimeoutMs
2658
+ }));
2659
+ const pinnedControl = createCodexSessionCatalogControlFromRequests({
2660
+ clientId: resolveCodexAppServerClientInstanceId(client),
2661
+ connectionFingerprint: buildCodexAppServerConnectionFingerprint(runtime, agentDir),
2662
+ createRequestSnapshot: () => requests,
2663
+ ...source?.localSessionsRoot ? { localSessionsRoot: source.localSessionsRoot } : {},
2664
+ now,
2665
+ withPinnedConnection: async (nestedRun) => await nestedRun(pinnedControl)
2666
+ });
2667
+ return await run(pinnedControl);
2668
+ } finally {
2669
+ releaseLeasedSharedCodexAppServerClient(client);
2670
+ }
2671
+ };
2672
+ const control = createCodexSessionCatalogControlFromRequests({
2673
+ createRequestSnapshot: () => createRequestSnapshot(agentId, source),
2674
+ ...source?.localSessionsRoot ? { localSessionsRoot: source.localSessionsRoot } : {},
2675
+ now,
2676
+ withPinnedConnection
2546
2677
  });
2547
- });
2678
+ return {
2679
+ ...control,
2680
+ async listPage(pageParams) {
2681
+ const runtimeConfig = params.getRuntimeConfig();
2682
+ if (!runtimeConfig) return await control.listPage(pageParams);
2683
+ let cache = catalogPagesByConfig.get(runtimeConfig);
2684
+ if (!cache) {
2685
+ cache = /* @__PURE__ */ new Map();
2686
+ catalogPagesByConfig.set(runtimeConfig, cache);
2687
+ }
2688
+ const key = codexCatalogPageCacheKey(pageParams, agentId, source);
2689
+ const cached = cache.get(key);
2690
+ if (pageParams.forceRefresh !== true && cached) {
2691
+ cache.delete(key);
2692
+ cache.set(key, cached);
2693
+ if (cached.expiresAt > now()) return cached.value ?? await cached.page;
2694
+ }
2695
+ if (cached) cache.delete(key);
2696
+ const page = control.listPage(pageParams);
2697
+ const staleValue = cached?.value;
2698
+ const entry = {
2699
+ expiresAt: Number.POSITIVE_INFINITY,
2700
+ page,
2701
+ ...staleValue ? { value: staleValue } : {}
2702
+ };
2703
+ cache.set(key, entry);
2704
+ pruneMapToMaxSize(cache, CODEX_SESSION_CATALOG_LIST_CACHE_MAX_ENTRIES);
2705
+ const settle = (value) => {
2706
+ if (cache.get(key) === entry) {
2707
+ entry.value = value;
2708
+ entry.expiresAt = now() + CODEX_SESSION_CATALOG_LIST_TTL_MS;
2709
+ }
2710
+ return value;
2711
+ };
2712
+ const restore = () => {
2713
+ if (cache.get(key) !== entry) return;
2714
+ if (staleValue) cache.set(key, {
2715
+ expiresAt: now(),
2716
+ page: Promise.resolve(staleValue),
2717
+ value: staleValue
2718
+ });
2719
+ else cache.delete(key);
2720
+ };
2721
+ if (pageParams.forceRefresh !== true && staleValue) {
2722
+ page.then(settle, restore);
2723
+ return staleValue;
2724
+ }
2725
+ try {
2726
+ return settle(await page);
2727
+ } catch (error) {
2728
+ restore();
2729
+ throw error;
2730
+ }
2731
+ }
2732
+ };
2733
+ };
2734
+ const homesForAgent = (agentId) => homeResolver.forAgent(agentId);
2735
+ const forUpstream = (agentId, connectionFingerprint) => {
2736
+ const source = homesForAgent(agentId).find((home) => buildCodexAppServerConnectionFingerprint(home.appServer, home.agentDir) === connectionFingerprint);
2737
+ return source ? forRequest(agentId, source) : void 0;
2738
+ };
2739
+ return {
2740
+ forRequest,
2741
+ forUpstream,
2742
+ homesForAgent
2743
+ };
2548
2744
  }
2745
+ //#endregion
2746
+ //#region extensions/codex/src/session-catalog.ts
2549
2747
  /** Allows read-only catalog and transcript commands on supported paired-node platforms. */
2550
2748
  function createCodexSessionCatalogNodeInvokePolicies() {
2551
2749
  return [{
@@ -2563,7 +2761,7 @@ function createCodexSessionCatalogNodeInvokePolicies() {
2563
2761
  }];
2564
2762
  }
2565
2763
  function toGenericCatalogHost(host, localTerminalAvailable) {
2566
- const local = host.hostId === CODEX_LOCAL_SESSION_HOST_ID;
2764
+ const local = isLocalCodexCatalogHost(host.hostId);
2567
2765
  return {
2568
2766
  hostId: host.hostId,
2569
2767
  label: host.label,
@@ -2578,6 +2776,7 @@ function toGenericCatalogHost(host, localTerminalAvailable) {
2578
2776
  const name = session.name ?? session.fallbackName;
2579
2777
  return {
2580
2778
  threadId: session.threadId,
2779
+ ...session.sourceHomeId ? { sourceHomeId: session.sourceHomeId } : {},
2581
2780
  ...name ? { name } : {},
2582
2781
  ...session.cwd ? { cwd: session.cwd } : {},
2583
2782
  status: session.status,
@@ -2599,13 +2798,44 @@ function toGenericCatalogHost(host, localTerminalAvailable) {
2599
2798
  ...host.error ? { error: host.error } : {}
2600
2799
  };
2601
2800
  }
2801
+ function isLocalCodexCatalogHost(hostId) {
2802
+ return hostId === "gateway:local" || hostId.startsWith(`gateway:local:`);
2803
+ }
2804
+ function resolveLocalCatalogHomeForThread(params) {
2805
+ if (params.homes.length === 0) throw new CatalogParamsError("local Codex sessions are unavailable in isolated state");
2806
+ const exact = params.sourceHomeId ? params.homes.filter((home) => home.sourceHomeId === params.sourceHomeId) : params.homes.filter((home) => home.hostId === params.hostId);
2807
+ if (exact.length === 0 || params.sourceHomeId && exact[0]?.hostId !== params.hostId) throw new CatalogParamsError("Codex session source home is unavailable");
2808
+ return exact[0];
2809
+ }
2602
2810
  function registerCodexSessionCatalog(params) {
2603
- const usesProcessHomeFallback = () => {
2604
- const start = resolveCodexSupervisionAppServerRuntimeOptions({ pluginConfig: params.getPluginConfig() }).start;
2605
- return start.transport === "stdio" && start.homeScope === "user" && !process.env.CODEX_HOME?.trim();
2811
+ const catalogHomes = (agentId, allowProcessHomeFallback) => {
2812
+ const homes = params.control.homesForAgent(agentId);
2813
+ return allowProcessHomeFallback === false ? homes.filter((home) => !home.usesProcessHomeFallback) : homes;
2814
+ };
2815
+ const resolveRequestAgentId = (agentId) => resolveSessionAgentIds({
2816
+ config: params.getRuntimeConfig() ?? params.api.config,
2817
+ agentId
2818
+ }).sessionAgentId;
2819
+ const bindRequest = (request) => {
2820
+ const agentId = resolveRequestAgentId(request.agentId);
2821
+ const source = isLocalCodexCatalogHost(request.hostId) ? resolveLocalCatalogHomeForThread({
2822
+ homes: [...catalogHomes(agentId, request.allowProcessHomeFallback)],
2823
+ hostId: request.hostId,
2824
+ ...request.sourceHomeId ? { sourceHomeId: request.sourceHomeId } : {}
2825
+ }) : void 0;
2826
+ return {
2827
+ agentId,
2828
+ source,
2829
+ control: params.control.forRequest(agentId, source)
2830
+ };
2606
2831
  };
2607
- const assertLocalAccess = (hostId, allowProcessHomeFallback) => {
2608
- if (hostId === "gateway:local" && allowProcessHomeFallback === false && usesProcessHomeFallback()) throw new CatalogParamsError("local Codex sessions are unavailable in isolated state");
2832
+ const bindLocalRequest = (request) => {
2833
+ const bound = bindRequest(request);
2834
+ if (!bound.source) throw new CatalogParamsError("Codex session catalog hostId is invalid");
2835
+ return {
2836
+ ...bound,
2837
+ source: bound.source
2838
+ };
2609
2839
  };
2610
2840
  const checkUpstreamActivity = createChecker(params);
2611
2841
  params.api.registerSessionCatalog({
@@ -2615,9 +2845,12 @@ function registerCodexSessionCatalog(params) {
2615
2845
  resolveCreateSession: ({ agentId }) => resolveCodexCatalogCreateSession(params.getRuntimeConfig() ?? params.api.config, agentId),
2616
2846
  list: async (query) => {
2617
2847
  const localTerminalAvailable = resolveLocalCodexTerminalExecutable() !== void 0;
2618
- const { allowProcessHomeFallback, listNodes, onHost, sessionEntries, ...gatewayQuery } = query;
2848
+ const { agentId: requestedAgentId, allowProcessHomeFallback, listNodes, onHost, sessionEntries, ...gatewayQuery } = query;
2849
+ const agentId = resolveRequestAgentId(requestedAgentId);
2619
2850
  const mapHost = (host) => toGenericCatalogHost(host, localTerminalAvailable);
2851
+ const localHomes = [...catalogHomes(agentId, allowProcessHomeFallback)];
2620
2852
  return (await listCodexSessionCatalog({
2853
+ agentId,
2621
2854
  bindingStore: params.bindingStore,
2622
2855
  config: params.getRuntimeConfig(),
2623
2856
  runtime: params.api.runtime,
@@ -2625,20 +2858,21 @@ function registerCodexSessionCatalog(params) {
2625
2858
  query: gatewayQuery,
2626
2859
  listNodes,
2627
2860
  sessionEntries,
2628
- includeLocal: allowProcessHomeFallback !== false || !usesProcessHomeFallback(),
2861
+ localHomes,
2629
2862
  ...onHost ? { onHost: (host) => onHost(mapHost(host)) } : {}
2630
2863
  })).hosts.map(mapHost);
2631
2864
  },
2632
2865
  read: async (request) => {
2633
- const { allowProcessHomeFallback, ...catalogRequest } = request;
2634
- assertLocalAccess(catalogRequest.hostId, allowProcessHomeFallback);
2866
+ const { agentId, source, control } = bindRequest(request);
2635
2867
  const page = await readCodexSessionTranscript({
2868
+ agentId,
2636
2869
  runtime: params.api.runtime,
2637
- control: params.control,
2638
- hostId: catalogRequest.hostId,
2639
- threadId: catalogRequest.threadId,
2640
- cursor: catalogRequest.cursor,
2641
- limit: catalogRequest.limit ?? 20
2870
+ control,
2871
+ hostId: request.hostId,
2872
+ threadId: request.threadId,
2873
+ cursor: request.cursor,
2874
+ limit: request.limit ?? 20,
2875
+ ...source ? { source } : {}
2642
2876
  });
2643
2877
  return {
2644
2878
  ...page,
@@ -2646,58 +2880,69 @@ function registerCodexSessionCatalog(params) {
2646
2880
  };
2647
2881
  },
2648
2882
  continueSession: async (request) => {
2649
- assertLocalAccess(request.hostId, request.allowProcessHomeFallback);
2650
2883
  const config = params.getRuntimeConfig();
2651
2884
  if (!config) throw new Error("OpenClaw runtime config is unavailable");
2652
2885
  if (request.hostId.startsWith("node:")) return await continueNodeCodexSession({
2886
+ agentId: resolveRequestAgentId(request.agentId),
2653
2887
  api: params.api,
2654
2888
  config,
2655
2889
  hostId: request.hostId,
2656
2890
  threadId: request.threadId,
2657
2891
  clientScopes: request.clientScopes
2658
2892
  });
2659
- if (request.hostId !== "gateway:local") throw new CatalogParamsError("Codex session catalog hostId is invalid");
2893
+ if (!isLocalCodexCatalogHost(request.hostId)) throw new CatalogParamsError("Codex session catalog hostId is invalid");
2894
+ const { agentId, source, control } = bindLocalRequest(request);
2660
2895
  let upstreamBaseline;
2661
2896
  return codexUpstreamContinueResult((await continueLocalCodexSession({
2897
+ agentId,
2662
2898
  api: params.api,
2663
2899
  bindingStore: params.bindingStore,
2664
2900
  config,
2665
- control: params.control,
2901
+ control,
2666
2902
  threadId: request.threadId,
2903
+ hostId: source.hostId,
2904
+ sourceHomeId: source.sourceHomeId,
2905
+ ...source.hostId === "gateway:local" ? { allowLegacy: true } : {},
2667
2906
  onContinued: (baseline) => {
2668
2907
  upstreamBaseline = baseline;
2669
2908
  }
2670
2909
  })).sessionKey, request.threadId, upstreamBaseline);
2671
2910
  },
2672
- checkUpstreamActivity: (probes, policy) => checkUpstreamActivity(probes.filter((probe) => probe.hostId !== "gateway:local" || policy?.allowProcessHomeFallback !== false || !usesProcessHomeFallback())),
2911
+ checkUpstreamActivity: (probes, policy) => checkUpstreamActivity(probes.filter((probe) => !isLocalCodexCatalogHost(probe.hostId) || policy?.allowProcessHomeFallback !== false || catalogHomes(probe.agentId, false).some((home) => home.hostId === probe.hostId))),
2673
2912
  archive: async (request) => {
2674
- assertLocalAccess(request.hostId, request.allowProcessHomeFallback);
2675
2913
  if (request.confirmNoOtherRunner !== true) throw new CatalogParamsError("archive requires confirmation that no other runner is active");
2676
- if (request.hostId !== "gateway:local") throw new CatalogParamsError("paired-node Codex sessions are view-only");
2914
+ if (!isLocalCodexCatalogHost(request.hostId)) throw new CatalogParamsError("paired-node Codex sessions are view-only");
2677
2915
  const config = params.getRuntimeConfig();
2678
2916
  if (!config) throw new Error("OpenClaw runtime config is unavailable");
2917
+ const { agentId, source, control } = bindLocalRequest(request);
2679
2918
  await archiveLocalCodexSession({
2919
+ agentId,
2680
2920
  bindingStore: params.bindingStore,
2681
2921
  config,
2682
- control: params.control,
2922
+ control,
2683
2923
  runtime: params.api.runtime,
2684
- threadId: request.threadId
2924
+ threadId: request.threadId,
2925
+ hostId: source.hostId,
2926
+ sourceHomeId: source.sourceHomeId,
2927
+ ...source.hostId === "gateway:local" ? { allowLegacy: true } : {}
2685
2928
  });
2686
2929
  return { ok: true };
2687
2930
  },
2688
2931
  openTerminal: async (request) => {
2689
- assertLocalAccess(request.hostId, request.allowProcessHomeFallback);
2932
+ const { agentId, source, control } = bindRequest(request);
2690
2933
  return await openCodexCatalogTerminal({
2691
2934
  api: params.api,
2692
- control: params.control,
2935
+ control,
2693
2936
  getPluginConfig: params.getPluginConfig,
2694
2937
  getRuntimeConfig: params.getRuntimeConfig,
2695
2938
  parseCatalogPage,
2696
- ...request
2939
+ ...source ? { source } : {},
2940
+ ...request,
2941
+ agentId
2697
2942
  });
2698
2943
  },
2699
2944
  startTerminalSession: async (request) => {
2700
- assertLocalAccess(request.nodeId ? `node:${request.nodeId}` : CODEX_LOCAL_SESSION_HOST_ID, request.allowProcessHomeFallback);
2945
+ if (!request.nodeId && catalogHomes(request.agentId, request.allowProcessHomeFallback).length === 0) throw new CatalogParamsError("local Codex sessions are unavailable in isolated state");
2701
2946
  return await startCodexCatalogTerminal({
2702
2947
  getPluginConfig: params.getPluginConfig,
2703
2948
  getRuntimeConfig: params.getRuntimeConfig,
@@ -2715,4 +2960,4 @@ const codexSessionCatalogRuntime = {
2715
2960
  archiveLocal: archiveLocalCodexSession
2716
2961
  };
2717
2962
  //#endregion
2718
- export { CODEX_LOCAL_SESSION_HOST_ID as a, readCodexStatusProbes as c, createCodexCliSessionNodeHostCommands as d, createCodexCliSessionNodeInvokePolicies as f, resumeCodexCliSessionOnNode as g, resolveCodexCliSessionForBindingOnNode as h, createCodexSessionCatalogNodeInvokePolicies as i, requestOptions as l, listCodexCliSessionsOnNode as m, createCodexSessionCatalogControl as n, assertCodexArchiveDescendantsUnowned as o, formatCodexCliSessions as p, createCodexSessionCatalogNodeHostCommands as r, codexControlRequest as s, codexSessionCatalogRuntime as t, safeCodexControlRequest as u };
2963
+ export { CODEX_LOCAL_SESSION_HOST_ID as a, createCodexCliSessionNodeInvokePolicies as c, resolveCodexCliSessionForBindingOnNode as d, resumeCodexCliSessionOnNode as f, createCodexSessionCatalogNodeHostCommands as i, formatCodexCliSessions as l, createCodexSessionCatalogNodeInvokePolicies as n, assertCodexArchiveDescendantsUnowned as o, createCodexSessionCatalogControl as r, createCodexCliSessionNodeHostCommands as s, codexSessionCatalogRuntime as t, listCodexCliSessionsOnNode as u };