@openclaw/codex 2026.7.2-beta.7 → 2026.7.34

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 (64) hide show
  1. package/README.md +3 -27
  2. package/dist/{command-formatters-C336eRVS.js → .setup/app-server-policy-C_qTMEMy.mjs} +51 -9
  3. package/dist/{attempt-notifications-AP-a-Q5k.js → .setup/attempt-notifications-F8krwWL4.mjs} +6 -3
  4. package/dist/{command-handlers-DAHUSry9.js → .setup/command-handlers-DSRFfor4.mjs} +1156 -1319
  5. package/dist/.setup/command-rpc-BvYsfQ7G.mjs +786 -0
  6. package/dist/{compact-D_uvHurS.js → .setup/compact-BNgh7I4t.mjs} +10 -40
  7. package/dist/.setup/computer-use-C3QkmSO3.mjs +377 -0
  8. package/dist/.setup/config-CYEDnLJ2.mjs +1138 -0
  9. package/dist/{conversation-binding-data-BHRpYbX3.js → .setup/conversation-binding-data-CrRxtDBu.mjs} +1 -1
  10. package/dist/{bounded-turn-D2ZAgZVz.js → .setup/media-understanding-provider-EbJCyDqe.mjs} +203 -109
  11. package/dist/{models-BBMKU4jA.js → .setup/models-C_aZk03A.mjs} +11 -6
  12. package/dist/.setup/notification-correlation-iYs4WieS.mjs +35 -0
  13. package/dist/.setup/plugin-app-cache-key-DR8jV9oS.mjs +99 -0
  14. package/dist/.setup/protocol-2POPqAY4.mjs +13 -0
  15. package/dist/{transcript-mirror-DzsdNCf5.js → .setup/protocol-validators-dZQ-UTOa.mjs} +112 -922
  16. package/dist/{rate-limits-CU-gVCCS.js → .setup/provider-DGxDs6g8.mjs} +269 -13
  17. package/dist/{dynamic-tools-D2gYkSBO.js → .setup/provider-capabilities-DS2N_zc1.mjs} +5632 -3953
  18. package/dist/.setup/request-DFR0JyoV.mjs +36 -0
  19. package/dist/.setup/run-attempt-C7xEKFVB.mjs +8180 -0
  20. package/dist/{sandbox-guard-NuhpXmPB.js → .setup/sandbox-guard-DA2TQfZW.mjs} +1 -14
  21. package/dist/.setup/session-binding-DuihlMJc.mjs +628 -0
  22. package/dist/.setup/shared-client-B2KmLPRw.mjs +1854 -0
  23. package/dist/{side-question-_ulBk1AL.js → .setup/side-question-Cv6JXWCS.mjs} +84 -159
  24. package/dist/.setup/thread-lifecycle-u6CZdOOH.mjs +2593 -0
  25. package/dist/{web-search-provider.runtime-CCFhg2z5.js → .setup/web-search-provider.runtime-Ce2zOHxW.mjs} +3 -3
  26. package/dist/doctor-contract-api.js +122 -88
  27. package/dist/harness.js +20 -89
  28. package/dist/index.js +436 -1615
  29. package/dist/media-understanding-provider.js +1 -173
  30. package/dist/prompt-overlay.js +15 -0
  31. package/dist/provider-catalog.js +79 -0
  32. package/dist/provider-discovery.js +34 -0
  33. package/dist/provider.js +2 -0
  34. package/dist/web-search-contract-api.js +1 -1
  35. package/openclaw.plugin.json +178 -221
  36. package/package.json +8 -9
  37. package/dist/app-server-policy-BSaO2JIT.js +0 -49
  38. package/dist/auth-binding-B4FfbcCg.js +0 -62
  39. package/dist/capabilities-BFDTVYr4.js +0 -36
  40. package/dist/cli-metadata.js +0 -20
  41. package/dist/computer-use-CYEc8U3w.js +0 -693
  42. package/dist/conversation-control-xsIeXbo4.js +0 -287
  43. package/dist/effective-mcp-catalog-Bbd98Inw.js +0 -114
  44. package/dist/event-projector-assistant-message-DTgnPPVF.js +0 -101
  45. package/dist/event-projector-usage-Xp4AlR2k.js +0 -86
  46. package/dist/incognito-session-ET_ufDH-.js +0 -2
  47. package/dist/notification-correlation-CU6_AnOb.js +0 -67
  48. package/dist/plugin-app-cache-key-DztPafOA.js +0 -408
  49. package/dist/request-qKh331yu.js +0 -157
  50. package/dist/run-attempt-CY9WlDLc.js +0 -12413
  51. package/dist/runtime-artifact-CmtG_t1L.js +0 -554
  52. package/dist/session-binding-BXpo9ymA.js +0 -2214
  53. package/dist/session-catalog-DyOgR4WH.js +0 -2668
  54. package/dist/session-cli-2PZaFLRh.js +0 -175
  55. package/dist/session-upstream-marker-B6U7tLw5.js +0 -82
  56. package/dist/settled-turn-finalizer-CXY2Z9dT.js +0 -316
  57. package/dist/shared-client-DTbvH-XV.js +0 -3069
  58. package/dist/thread-lifecycle-DF53ogiA.js +0 -3987
  59. package/dist/transport-stdio-DXgGZ7Ib.js +0 -88
  60. package/dist/upstream-session-fork-Dy4Xvuoj.js +0 -320
  61. package/dist/usage-CQ6GhD5-.js +0 -25
  62. /package/dist/{rolldown-runtime-8H4AJuhK.js → .setup/rolldown-runtime-8H4AJuhK.mjs} +0 -0
  63. /package/dist/{session-binding-meta-B7aEMU7g.js → .setup/session-binding-meta-B7aEMU7g.mjs} +0 -0
  64. /package/dist/{web-search-provider.shared-BrZmlqyR.js → .setup/web-search-provider.shared-BrZmlqyR.mjs} +0 -0
@@ -1,31 +1,27 @@
1
- import { I as resolveCodexAppServerAuthProfileIdForAgent, W as isJsonObject } from "./shared-client-DTbvH-XV.js";
2
- import { c as reclaimCurrentCodexSessionGeneration, d as sessionBindingIdentity, i as createCodexSessionGenerationSupersededError, j as isCodexFastServiceTier, n as assertCodexBindingMayBeReplaced, r as bindingStoreKey, s as normalizeCodexAppServerBindingModelProvider } from "./session-binding-BXpo9ymA.js";
3
- import { g as assertCodexThreadResumeResponse } from "./transcript-mirror-DzsdNCf5.js";
4
- import { t as listAllCodexAppServerModels } from "./models-BBMKU4jA.js";
5
- import { t as CODEX_CONTROL_METHODS } from "./capabilities-BFDTVYr4.js";
6
- import { s as summarizeCodexAccountUsage } from "./rate-limits-CU-gVCCS.js";
7
- import { a as formatComputerUseStatus, c as formatSkills, i as formatCodexStatus, l as formatThreads, n as formatAccount, o as formatList, r as formatCodexDisplayText, s as formatModels, t as buildHelp, u as readString$1 } from "./command-formatters-C336eRVS.js";
8
- import { n as resolveCodexNativeExecutionBlock, r as resolveCodexNativeSandboxBlock } from "./sandbox-guard-NuhpXmPB.js";
9
- import { a as setCodexConversationFastMode, c as steerCodexConversationTurn, d as CODEX_NATIVE_EXECUTION_AUTH_ERROR, f as canMutateCodexHost, i as readCodexConversationActiveTurn, l as stopCodexConversationTurn, n as parseCodexFastModeArg, o as setCodexConversationModel, r as parseCodexPermissionsModeArg, s as setCodexConversationPermissions, t as formatPermissionsMode } from "./conversation-control-xsIeXbo4.js";
10
- import { i as readCodexConversationBindingData, n as createCodexCliNodeConversationBindingData, o as resolveCodexDefaultWorkspaceDir, r as createCodexConversationBindingData } from "./conversation-binding-data-BHRpYbX3.js";
11
- import { a as resolveCodexBindingAppServerConnection } from "./notification-correlation-CU6_AnOb.js";
12
- import { c as requestOptions, l as safeCodexControlRequest, o as codexControlRequest, p as formatCodexCliSessions, s as readCodexStatusProbes } from "./session-catalog-DyOgR4WH.js";
13
- import { n as installCodexComputerUse, r as readCodexComputerUseStatus } from "./computer-use-CYEc8U3w.js";
1
+ import { r as isJsonObject } from "./protocol-2POPqAY4.mjs";
2
+ import { o as isCodexFastServiceTier } from "./config-CYEDnLJ2.mjs";
3
+ import { n as assertCodexThreadResumeResponse } from "./protocol-validators-dZQ-UTOa.mjs";
4
+ import { t as listAllCodexAppServerModels } from "./models-C_aZk03A.mjs";
5
+ import { p as summarizeCodexAccountUsage } from "./provider-DGxDs6g8.mjs";
6
+ import { y as resolveCodexAppServerAuthProfileIdForAgent } from "./shared-client-B2KmLPRw.mjs";
7
+ import { a as normalizeCodexAppServerBindingModelProvider, n as bindingStoreKey, o as reclaimCurrentCodexSessionGeneration, s as sessionBindingIdentity } from "./session-binding-DuihlMJc.mjs";
8
+ import { r as CODEX_CONTROL_METHODS } from "./plugin-app-cache-key-DR8jV9oS.mjs";
9
+ import { a as formatCodexDisplayText, c as formatList, d as formatThreads, f as readString$1, i as formatAccount, l as formatModels, o as formatCodexStatus, r as buildHelp, s as formatComputerUseStatus, u as formatSkills } from "./app-server-policy-C_qTMEMy.mjs";
10
+ import { n as resolveCodexNativeExecutionBlock, r as resolveCodexNativeSandboxBlock } from "./sandbox-guard-DA2TQfZW.mjs";
11
+ import { S as canMutateCodexHost, _ as setCodexConversationPermissions, d as formatPermissionsMode, f as parseCodexFastModeArg, g as setCodexConversationModel, h as setCodexConversationFastMode, i as safeCodexControlRequest, m as readCodexConversationActiveTurn, n as readCodexStatusProbes, p as parseCodexPermissionsModeArg, r as requestOptions, s as formatCodexCliSessions, t as codexControlRequest, v as steerCodexConversationTurn, x as CODEX_NATIVE_EXECUTION_AUTH_ERROR, y as stopCodexConversationTurn } from "./command-rpc-BvYsfQ7G.mjs";
12
+ import { i as readCodexConversationBindingData, n as createCodexCliNodeConversationBindingData, o as resolveCodexDefaultWorkspaceDir, r as createCodexConversationBindingData } from "./conversation-binding-data-CrRxtDBu.mjs";
13
+ import { n as installCodexComputerUse, r as readCodexComputerUseStatus } from "./computer-use-C3QkmSO3.mjs";
14
14
  import { parseStrictPositiveInteger } from "openclaw/plugin-sdk/number-runtime";
15
15
  import crypto from "node:crypto";
16
- import { ensureAuthProfileStore, findNormalizedProviderValue, resolveAgentDir, resolveAuthProfileEligibility, resolveAuthProfileOrder, resolveProfileUnusableUntilForDisplay, resolveSessionAgentIds } from "openclaw/plugin-sdk/agent-runtime";
16
+ import { ensureAuthProfileStore, findNormalizedProviderValue, resolveAgentDir, resolveAuthProfileEligibility, resolveAuthProfileOrder, resolveDefaultAgentDir, resolveProfileUnusableUntilForDisplay, resolveSessionAgentIds } from "openclaw/plugin-sdk/agent-runtime";
17
17
  import { parseAgentSessionKey } from "openclaw/plugin-sdk/routing";
18
18
  import { normalizeOptionalString, normalizeUniqueStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
19
- import { getSessionEntry, resolveStorePath } from "openclaw/plugin-sdk/session-store-runtime";
20
- import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
21
- import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
22
- import { MODEL_SELECTION_LOCKED_MESSAGE, isModelSelectionLocked } from "openclaw/plugin-sdk/model-session-runtime";
23
19
  //#region extensions/codex/src/command-account.ts
24
20
  const OPENAI_PROVIDER_ID = "openai";
25
21
  const OPENAI_CODEX_PROVIDER_ID = OPENAI_PROVIDER_ID;
26
22
  async function readCodexAccountAuthOverview(params) {
27
23
  const config = params.ctx.config;
28
- const agentDir = params.agentDir;
24
+ const agentDir = resolveDefaultAgentDir(config);
29
25
  const store = ensureAuthProfileStore(agentDir, {
30
26
  allowKeychainPrompt: false,
31
27
  config
@@ -50,7 +46,6 @@ async function readCodexAccountAuthOverview(params) {
50
46
  const activeUsage = activeIsSubscription && params.limits.ok ? summarizeCodexAccountUsage(params.limits.value, now) : void 0;
51
47
  const subscriptionUsage = subscriptionProfileId && (!activeIsSubscription || subscriptionProfileId !== activeProfileId) ? await readSubscriptionUsage({
52
48
  ...params,
53
- agentDir,
54
49
  config,
55
50
  subscriptionProfileId,
56
51
  now
@@ -155,7 +150,7 @@ function resolveLiveAccountProfileId(params) {
155
150
  if (!email) return firstSubscription;
156
151
  return params.order.find((profileId) => {
157
152
  const credential = params.store.profiles[profileId];
158
- if (!credential || !isChatGptSubscriptionProfile(credential)) return false;
153
+ if (!isChatGptSubscriptionProfile(credential)) return false;
159
154
  return (credential.email?.trim().toLowerCase() ?? extractEmailFromProfileId(profileId))?.toLowerCase() === email;
160
155
  }) ?? firstSubscription;
161
156
  }
@@ -167,7 +162,6 @@ function shouldInferApiKeyActiveFromRateLimitProbe(limits) {
167
162
  async function readSubscriptionUsage(params) {
168
163
  const limits = await params.safeCodexControlRequest(params.pluginConfig, CODEX_CONTROL_METHODS.rateLimits, void 0, {
169
164
  config: params.config,
170
- agentDir: params.agentDir,
171
165
  authProfileId: params.subscriptionProfileId,
172
166
  isolated: true
173
167
  });
@@ -315,21 +309,6 @@ function formatRelativeReset(untilMs, nowMs) {
315
309
  return `in ${days} ${days === 1 ? "day" : "days"}`;
316
310
  }
317
311
  //#endregion
318
- //#region extensions/codex/src/command-diagnostics-state.ts
319
- /** Runtime state for diagnostics upload throttling and confirmation handshakes. */
320
- const codexDiagnosticsFeedbackState = {
321
- lastUploadByThread: /* @__PURE__ */ new Map(),
322
- lastUploadByScope: /* @__PURE__ */ new Map(),
323
- pendingConfirmations: /* @__PURE__ */ new Map(),
324
- pendingTokensByScope: /* @__PURE__ */ new Map(),
325
- clear() {
326
- this.lastUploadByThread.clear();
327
- this.lastUploadByScope.clear();
328
- this.pendingConfirmations.clear();
329
- this.pendingTokensByScope.clear();
330
- }
331
- };
332
- //#endregion
333
312
  //#region extensions/codex/src/command-presentation.ts
334
313
  function buildCodexCommandPickerPresentation(title, prompt, buttons) {
335
314
  return {
@@ -350,8 +329,212 @@ function buildCodexCommandPickerPresentation(title, prompt, buttons) {
350
329
  };
351
330
  }
352
331
  //#endregion
353
- //#region extensions/codex/src/command-handler-args.ts
354
- /** No-arg `/codex` picker. */
332
+ //#region extensions/codex/src/command-plugins-management.ts
333
+ const POLICY_REFRESH_HINT = "New Codex conversations pick this up automatically. Use /new or /reset to refresh the current one.";
334
+ async function handleCodexPluginsSubcommand(ctx, rest, io) {
335
+ const [verb = "list", ...args] = rest;
336
+ const normalized = verb.toLowerCase();
337
+ if (normalized === "menu") {
338
+ if (args.length > 0) return { text: "Usage: /codex plugins menu" };
339
+ return buildPluginsMenuReply();
340
+ }
341
+ if (normalized === "help") {
342
+ if (args.length > 0) return { text: "Usage: /codex plugins help" };
343
+ return { text: buildPluginsHelp() };
344
+ }
345
+ if (normalized === "list") {
346
+ if (args.length > 0) return { text: "Usage: /codex plugins list" };
347
+ const current = await io.readConfig();
348
+ return { text: formatPluginList(current.plugins ?? {}, { globalEnabled: current.enabled === true }) };
349
+ }
350
+ const target = args[0];
351
+ if (normalized === "enable" || normalized === "disable") {
352
+ if (args.length === 0) return buildPluginNamePickerReply(normalized, await io.readConfig());
353
+ if (!target || args.length > 1) return { text: `Usage: /codex plugins ${normalized} <name>` };
354
+ if (!canMutateCodexHost(ctx)) return { text: `Only an owner or operator.admin gateway client can run /codex plugins ${normalized}.` };
355
+ const wantEnabled = normalized === "enable";
356
+ if (!((await io.readConfig()).plugins ?? {})[target]) return { text: `Codex sub-plugin '${formatCodexDisplayText(target)}' is not configured. Run '/codex plugins list' to see configured plugins.` };
357
+ await io.mutate((block) => {
358
+ if (wantEnabled) block.enabled = true;
359
+ block.plugins ??= {};
360
+ block.plugins[target] = {
361
+ ...block.plugins[target],
362
+ enabled: wantEnabled
363
+ };
364
+ });
365
+ return { text: `${formatCodexDisplayText(target)}: ${wantEnabled ? "enabled" : "disabled"} in openclaw.json. ${POLICY_REFRESH_HINT}` };
366
+ }
367
+ return { text: `Unknown /codex plugins subcommand: ${formatCodexDisplayText(verb)}\n\n${buildPluginsHelp()}` };
368
+ }
369
+ function buildPluginsMenuReply() {
370
+ return {
371
+ text: [
372
+ "Codex sub-plugins. Pick a sub-action or type:",
373
+ "",
374
+ " 1. /codex plugins list",
375
+ " 2. /codex plugins enable",
376
+ " 3. /codex plugins disable",
377
+ " 4. /codex plugins help",
378
+ "",
379
+ "Type '/codex' to go back to the main menu."
380
+ ].join("\n"),
381
+ presentation: buildCodexCommandPickerPresentation("Codex sub-plugins", "Pick a Codex sub-plugin action:", [
382
+ {
383
+ label: "list",
384
+ command: "/codex plugins list"
385
+ },
386
+ {
387
+ label: "enable",
388
+ command: "/codex plugins enable"
389
+ },
390
+ {
391
+ label: "disable",
392
+ command: "/codex plugins disable"
393
+ },
394
+ {
395
+ label: "help",
396
+ command: "/codex plugins help"
397
+ },
398
+ {
399
+ label: "back",
400
+ command: "/codex"
401
+ }
402
+ ])
403
+ };
404
+ }
405
+ function buildPluginNamePickerReply(verb, current) {
406
+ const globalEnabled = current.enabled === true;
407
+ const eligible = Object.entries(current.plugins ?? {}).toSorted(([left], [right]) => left.localeCompare(right)).filter(([, entry]) => {
408
+ const effectivelyEnabled = globalEnabled && entry.enabled !== false;
409
+ return verb === "disable" ? effectivelyEnabled : !effectivelyEnabled;
410
+ });
411
+ if (eligible.length === 0) return {
412
+ text: [
413
+ `No configured ${verb === "enable" ? "disabled" : "enabled"} Codex sub-plugins found.`,
414
+ "",
415
+ "Type '/codex plugins list' to inspect configured sub-plugins.",
416
+ "Type '/codex plugins menu' to go back to the plugins menu."
417
+ ].join("\n"),
418
+ presentation: buildCodexCommandPickerPresentation("Codex sub-plugins", "Pick another Codex sub-plugin action:", [{
419
+ label: "list",
420
+ command: "/codex plugins list"
421
+ }, {
422
+ label: "back",
423
+ command: "/codex plugins menu"
424
+ }])
425
+ };
426
+ const buttons = [...eligible.map(([key]) => ({
427
+ label: formatCodexDisplayText(key),
428
+ command: `/codex plugins ${verb} ${key}`
429
+ })), {
430
+ label: "back",
431
+ command: "/codex plugins menu"
432
+ }];
433
+ return {
434
+ text: [
435
+ `Codex sub-plugins to ${verb}. Pick one or type:`,
436
+ "",
437
+ ...eligible.map(([key], index) => ` ${index + 1}. /codex plugins ${verb} ${key}`),
438
+ "",
439
+ ...verb === "enable" && !globalEnabled ? ["Global codexPlugins.enabled is off; enabling one configured sub-plugin turns it on.", ""] : [],
440
+ "Type '/codex plugins menu' to go back to the plugins menu."
441
+ ].join("\n"),
442
+ presentation: buildCodexCommandPickerPresentation("Codex sub-plugins", `Pick a Codex sub-plugin to ${verb}:`, buttons)
443
+ };
444
+ }
445
+ function buildPluginsHelp() {
446
+ return [
447
+ "Codex sub-plugin management (writes only to ~/.openclaw/openclaw.json, never to ~/.codex/config.toml):",
448
+ "- /codex plugins (alias for list)",
449
+ "- /codex plugins list show all configured Codex sub-plugins",
450
+ "- /codex plugins enable <name> enable a configured sub-plugin",
451
+ "- /codex plugins disable <name> disable a configured sub-plugin"
452
+ ].join("\n");
453
+ }
454
+ function formatPluginList(plugins, options = {}) {
455
+ const globalEnabled = options.globalEnabled === true;
456
+ const keys = Object.keys(plugins).toSorted();
457
+ if (keys.length === 0) return "No Codex sub-plugins configured under plugins.entries.codex.config.codexPlugins.plugins";
458
+ const rows = keys.map((key) => {
459
+ const entry = plugins[key] ?? {};
460
+ const state = globalEnabled && entry.enabled !== false ? "ON " : "OFF";
461
+ return {
462
+ displayKey: formatCodexDisplayText(key),
463
+ state,
464
+ pluginName: formatCodexDisplayText(entry.pluginName ?? key),
465
+ marketplace: formatCodexDisplayText(entry.marketplaceName ?? "?")
466
+ };
467
+ });
468
+ const keyW = Math.max(...rows.map((r) => r.displayKey.length));
469
+ const pluginW = Math.max(...rows.map((r) => r.pluginName.length));
470
+ return [
471
+ "Codex sub-plugins in Openclaw config (~/.openclaw/openclaw.json):",
472
+ "",
473
+ ...rows.map((r) => ` ${r.state} ${r.displayKey.padEnd(keyW)} ${r.pluginName.padEnd(pluginW)} [${r.marketplace}]`),
474
+ "",
475
+ ...globalEnabled ? [] : ["Global codexPlugins.enabled is off; configured sub-plugins are inactive.", ""],
476
+ "New Codex conversations pick up policy changes automatically; /new or /reset to refresh the current one."
477
+ ].join("\n");
478
+ }
479
+ //#endregion
480
+ //#region extensions/codex/src/command-handlers.ts
481
+ const defaultCodexCommandDeps = {
482
+ codexControlRequest,
483
+ listCodexAppServerModels: listAllCodexAppServerModels,
484
+ readCodexStatusProbes,
485
+ requestOptions,
486
+ safeCodexControlRequest,
487
+ readCodexComputerUseStatus,
488
+ installCodexComputerUse,
489
+ resolveCodexDefaultWorkspaceDir,
490
+ readCodexConversationActiveTurn,
491
+ setCodexConversationFastMode,
492
+ setCodexConversationModel,
493
+ setCodexConversationPermissions,
494
+ steerCodexConversationTurn,
495
+ stopCodexConversationTurn,
496
+ listCodexCliSessionsOnNode: async () => {
497
+ throw new Error("Codex CLI node sessions require Gateway node runtime.");
498
+ },
499
+ resolveCodexCliSessionForBindingOnNode: async () => {
500
+ throw new Error("Codex CLI node sessions require Gateway node runtime.");
501
+ }
502
+ };
503
+ const CODEX_DIAGNOSTICS_SOURCE = "openclaw-diagnostics";
504
+ const CODEX_DIAGNOSTICS_REASON_MAX_CHARS = 2048;
505
+ const CODEX_DIAGNOSTICS_COOLDOWN_MS = 6e4;
506
+ const CODEX_DIAGNOSTICS_ERROR_MAX_CHARS = 500;
507
+ const CODEX_DIAGNOSTICS_COOLDOWN_MAX_THREADS = 100;
508
+ const CODEX_DIAGNOSTICS_COOLDOWN_MAX_SCOPES = 100;
509
+ const CODEX_DIAGNOSTICS_CONFIRMATION_TTL_MS = 3e5;
510
+ const CODEX_DIAGNOSTICS_CONFIRMATION_MAX_REQUESTS_PER_SCOPE = 100;
511
+ const CODEX_DIAGNOSTICS_CONFIRMATION_MAX_SCOPES = 100;
512
+ const CODEX_DIAGNOSTICS_SCOPE_FIELD_MAX_CHARS = 128;
513
+ const CODEX_RESUME_SAFE_THREAD_ID_PATTERN = /^[A-Za-z0-9._:-]+$/;
514
+ const CODEX_NATIVE_EXECUTION_SUBCOMMANDS = /* @__PURE__ */ new Set([
515
+ "bind",
516
+ "resume",
517
+ "steer",
518
+ "model",
519
+ "fast",
520
+ "permissions",
521
+ "compact",
522
+ "review"
523
+ ]);
524
+ const CODEX_NATIVE_CONTROL_SUBCOMMANDS = /* @__PURE__ */ new Set([
525
+ ...CODEX_NATIVE_EXECUTION_SUBCOMMANDS,
526
+ "detach",
527
+ "unbind",
528
+ "stop"
529
+ ]);
530
+ const lastCodexDiagnosticsUploadByThread = /* @__PURE__ */ new Map();
531
+ const lastCodexDiagnosticsUploadByScope = /* @__PURE__ */ new Map();
532
+ const pendingCodexDiagnosticsConfirmations = /* @__PURE__ */ new Map();
533
+ const pendingCodexDiagnosticsConfirmationTokensByScope = /* @__PURE__ */ new Map();
534
+ /**
535
+ * No-arg `/codex` picker. Codex owns the command tree; channels render the
536
+ * portable command actions as inline controls when their transport can.
537
+ */
355
538
  function buildCodexSubcommandPickerReply() {
356
539
  const verbs = [
357
540
  {
@@ -394,6 +577,7 @@ function buildCodexSubcommandPickerReply() {
394
577
  presentation: buildCodexCommandPickerPresentation("Codex commands", "Pick a Codex subcommand:", verbs)
395
578
  };
396
579
  }
580
+ /** Sub-picker for `/codex fast menu` (on / off / status). */
397
581
  function buildCodexFastMenuReply() {
398
582
  const modes = [
399
583
  "on",
@@ -418,6 +602,7 @@ function buildCodexFastMenuReply() {
418
602
  presentation: buildCodexCommandPickerPresentation("Codex fast mode", "Pick a Codex fast mode:", buttons)
419
603
  };
420
604
  }
605
+ /** Sub-picker for `/codex permissions menu` (default / yolo / status). */
421
606
  function buildCodexPermissionsMenuReply() {
422
607
  const modes = [
423
608
  "default",
@@ -442,6 +627,7 @@ function buildCodexPermissionsMenuReply() {
442
627
  presentation: buildCodexCommandPickerPresentation("Codex permissions", "Pick a Codex permissions mode:", buttons)
443
628
  };
444
629
  }
630
+ /** Sub-picker for `/codex computer-use menu` (status / install). */
445
631
  function buildCodexComputerUseMenuReply() {
446
632
  const actions = ["status", "install"];
447
633
  const buttons = [...actions.map((action) => ({
@@ -464,643 +650,457 @@ function buildCodexComputerUseMenuReply() {
464
650
  presentation: buildCodexCommandPickerPresentation("Codex computer-use", "Pick a Codex computer-use action:", buttons)
465
651
  };
466
652
  }
653
+ /** Returns true when the rest-args are exactly `["menu"]` (case-insensitive). */
467
654
  function isMenuVerb(rest) {
468
655
  return rest.length === 1 && (rest[0] ?? "").trim().toLowerCase() === "menu";
469
656
  }
470
- function splitArgs(value) {
471
- const input = value ?? "";
472
- const args = [];
473
- let current = "";
474
- let quote;
475
- let escaping = false;
476
- let tokenStarted = false;
477
- for (const char of input) {
478
- if (escaping) {
479
- current += char;
480
- escaping = false;
481
- tokenStarted = true;
482
- continue;
483
- }
484
- if (char === "\\" && quote !== "'") {
485
- escaping = true;
486
- tokenStarted = true;
487
- continue;
488
- }
489
- if (quote) {
490
- if (char === quote) quote = void 0;
491
- else current += char;
492
- tokenStarted = true;
493
- continue;
494
- }
495
- if (char === "\"" || char === "'") {
496
- quote = char;
497
- tokenStarted = true;
498
- continue;
499
- }
500
- if (/\s/.test(char)) {
501
- if (tokenStarted) {
502
- args.push(current);
503
- current = "";
504
- tokenStarted = false;
505
- }
506
- continue;
507
- }
508
- current += char;
509
- tokenStarted = true;
657
+ async function handleCodexSubcommand(ctx, options) {
658
+ const deps = {
659
+ ...defaultCodexCommandDeps,
660
+ ...options.deps
661
+ };
662
+ const args = splitArgs(ctx.args);
663
+ if (args.length === 0) return buildCodexSubcommandPickerReply();
664
+ const [subcommand = "status", ...rest] = args;
665
+ const normalized = subcommand.toLowerCase();
666
+ if (normalized === "help") return { text: buildHelp() };
667
+ if (CODEX_NATIVE_CONTROL_SUBCOMMANDS.has(normalized) && !returnsBeforeNativeCodexExecution(normalized, rest) && !canMutateCodexHost(ctx)) return { text: CODEX_NATIVE_EXECUTION_AUTH_ERROR };
668
+ const sandboxBlock = resolveCodexNativeCommandSandboxBlock(ctx, normalized, rest);
669
+ if (sandboxBlock) return { text: sandboxBlock };
670
+ if (normalized === "plugins") {
671
+ if (!deps.codexPluginsManagementIo) return { text: "Codex sub-plugin management is not wired up (codexPluginsManagementIo dep is undefined). Edit ~/.openclaw/openclaw.json or use `openclaw config patch` until the runtime exposes the IO." };
672
+ return await handleCodexPluginsSubcommand(ctx, rest, deps.codexPluginsManagementIo);
510
673
  }
511
- if (escaping) current += "\\";
512
- if (tokenStarted) args.push(current);
513
- return args;
514
- }
515
- function parseBindArgs(args) {
516
- const parsed = {};
517
- for (let index = 0; index < args.length; index += 1) {
518
- const arg = expectDefined(args[index], "current Codex bind argument");
519
- if (arg === "--help" || arg === "-h") {
520
- parsed.help = true;
521
- continue;
522
- }
523
- if (arg === "--cwd") {
524
- const value = readRequiredOptionValue(args, index);
525
- if (!value || parsed.cwd !== void 0) {
526
- parsed.help = true;
527
- continue;
528
- }
529
- parsed.cwd = value;
530
- index += 1;
531
- continue;
532
- }
533
- if (arg === "--model") {
534
- const value = readRequiredOptionValue(args, index);
535
- if (!value || parsed.model !== void 0) {
536
- parsed.help = true;
537
- continue;
538
- }
539
- parsed.model = value;
540
- index += 1;
541
- continue;
542
- }
543
- if (arg === "--provider" || arg === "--model-provider") {
544
- const value = readRequiredOptionValue(args, index);
545
- if (!value || parsed.provider !== void 0) {
546
- parsed.help = true;
547
- continue;
548
- }
549
- parsed.provider = value;
550
- index += 1;
551
- continue;
552
- }
553
- if (!arg.startsWith("-") && !parsed.threadId) {
554
- parsed.threadId = arg;
555
- continue;
556
- }
557
- parsed.help = true;
674
+ if (normalized === "status") {
675
+ if (rest.length > 0) return { text: "Usage: /codex status" };
676
+ return { text: formatCodexStatus(await deps.readCodexStatusProbes(options.pluginConfig, ctx.config)) };
558
677
  }
559
- parsed.threadId = normalizeOptionalString(parsed.threadId);
560
- parsed.cwd = normalizeOptionalString(parsed.cwd);
561
- parsed.model = normalizeOptionalString(parsed.model);
562
- parsed.provider = normalizeOptionalString(parsed.provider);
563
- return parsed;
564
- }
565
- function parseCodexCliSessionsArgs(args) {
566
- const parsed = { filter: "" };
567
- const filter = [];
568
- for (let index = 0; index < args.length; index += 1) {
569
- const arg = expectDefined(args[index], "current Codex sessions argument");
570
- if (arg === "--help" || arg === "-h") {
571
- parsed.help = true;
572
- continue;
573
- }
574
- if (arg === "--host" || arg === "--node") {
575
- const value = readRequiredOptionValue(args, index);
576
- if (!value || parsed.host !== void 0) {
577
- parsed.help = true;
578
- continue;
579
- }
580
- parsed.host = value;
581
- index += 1;
582
- continue;
583
- }
584
- if (arg === "--limit") {
585
- const parsedLimit = parseStrictPositiveInteger(readRequiredOptionValue(args, index));
586
- if (parsedLimit === void 0) {
587
- parsed.help = true;
588
- continue;
589
- }
590
- parsed.limit = parsedLimit;
591
- index += 1;
592
- continue;
593
- }
594
- if (arg.startsWith("-")) {
595
- parsed.help = true;
596
- continue;
597
- }
598
- filter.push(arg);
678
+ if (normalized === "models") {
679
+ if (rest.length > 0) return { text: "Usage: /codex models" };
680
+ return { text: formatModels(await deps.listCodexAppServerModels(deps.requestOptions(options.pluginConfig, 100, ctx.config))) };
599
681
  }
600
- parsed.host = normalizeOptionalString(parsed.host);
601
- parsed.filter = filter.join(" ").trim();
602
- return parsed;
682
+ if (normalized === "threads") return { text: await buildThreads(deps, ctx, options.pluginConfig, rest.join(" ")) };
683
+ if (normalized === "sessions") return { text: await buildCodexCliSessions(deps, rest) };
684
+ if (normalized === "resume") return { text: await resumeThread(deps, ctx, options.pluginConfig, rest) };
685
+ if (normalized === "bind") return await bindConversation(deps, ctx, options.pluginConfig, rest);
686
+ if (normalized === "detach" || normalized === "unbind") {
687
+ if (rest.length > 0) return { text: "Usage: /codex detach" };
688
+ return { text: await detachConversation(deps, ctx) };
689
+ }
690
+ if (normalized === "binding") {
691
+ if (rest.length > 0) return { text: "Usage: /codex binding" };
692
+ return { text: await describeConversationBinding(deps, ctx) };
693
+ }
694
+ if (normalized === "stop") {
695
+ if (rest.length > 0) return { text: "Usage: /codex stop" };
696
+ return { text: await stopConversationTurn(deps, ctx, options.pluginConfig) };
697
+ }
698
+ if (normalized === "steer") return { text: await steerConversationTurn(deps, ctx, options.pluginConfig, rest.join(" ")) };
699
+ if (normalized === "model") return { text: await setConversationModel(deps, ctx, options.pluginConfig, rest) };
700
+ if (normalized === "fast") {
701
+ if (isMenuVerb(rest)) return buildCodexFastMenuReply();
702
+ return { text: await setConversationFastMode(deps, ctx, rest) };
703
+ }
704
+ if (normalized === "permissions") {
705
+ if (isMenuVerb(rest)) return buildCodexPermissionsMenuReply();
706
+ return { text: await setConversationPermissions(deps, ctx, rest) };
707
+ }
708
+ if (normalized === "compact") return { text: await startThreadAction(deps, ctx, options.pluginConfig, "compact", rest) };
709
+ if (normalized === "review") return { text: await startThreadAction(deps, ctx, options.pluginConfig, "review", rest) };
710
+ if (normalized === "diagnostics") return await handleCodexDiagnosticsFeedback(deps, ctx, options.pluginConfig, rest.join(" "), "/codex diagnostics");
711
+ if (normalized === "computer-use" || normalized === "computeruse") {
712
+ if (isMenuVerb(rest)) return buildCodexComputerUseMenuReply();
713
+ return { text: await handleComputerUseCommand(deps, ctx, options.pluginConfig, rest) };
714
+ }
715
+ if (normalized === "mcp") {
716
+ if (rest.length > 0) return { text: "Usage: /codex mcp" };
717
+ const scope = await resolveCommandAppServerScope(deps, ctx);
718
+ return { text: formatList(await deps.codexControlRequest(options.pluginConfig, CODEX_CONTROL_METHODS.listMcpServers, { limit: 100 }, {
719
+ config: ctx.config,
720
+ ...scope
721
+ }), "MCP servers") };
722
+ }
723
+ if (normalized === "skills") {
724
+ if (rest.length > 0) return { text: "Usage: /codex skills" };
725
+ const scope = await resolveCommandAppServerScope(deps, ctx);
726
+ return { text: formatSkills(await deps.codexControlRequest(options.pluginConfig, CODEX_CONTROL_METHODS.listSkills, {}, {
727
+ config: ctx.config,
728
+ ...scope
729
+ })) };
730
+ }
731
+ if (normalized === "account") {
732
+ if (rest.length > 0) return { text: "Usage: /codex account" };
733
+ const scope = await resolveCommandAppServerScope(deps, ctx);
734
+ const requestScope = {
735
+ config: ctx.config,
736
+ ...scope
737
+ };
738
+ const [account, limits] = await Promise.all([deps.safeCodexControlRequest(options.pluginConfig, CODEX_CONTROL_METHODS.account, { refreshToken: false }, requestScope), deps.safeCodexControlRequest(options.pluginConfig, CODEX_CONTROL_METHODS.rateLimits, void 0, requestScope)]);
739
+ return { text: formatAccount(account, limits, await readCodexAccountAuthOverview({
740
+ ctx,
741
+ pluginConfig: options.pluginConfig,
742
+ safeCodexControlRequest: deps.safeCodexControlRequest,
743
+ account,
744
+ limits
745
+ })) };
746
+ }
747
+ return { text: `Unknown Codex command: ${formatCodexDisplayText(subcommand)}\n\n${buildHelp()}` };
603
748
  }
604
- function parseResumeArgs(args) {
605
- const parsed = {};
606
- for (let index = 0; index < args.length; index += 1) {
607
- const arg = expectDefined(args[index], "current Codex resume argument");
608
- if (arg === "--help" || arg === "-h") {
609
- parsed.help = true;
610
- continue;
611
- }
612
- if (arg === "--host" || arg === "--node") {
613
- const value = readRequiredOptionValue(args, index);
614
- if (!value || parsed.host !== void 0) {
615
- parsed.help = true;
616
- continue;
617
- }
618
- parsed.host = value;
619
- index += 1;
620
- continue;
621
- }
622
- if (arg === "--bind") {
623
- if (readRequiredOptionValue(args, index) !== "here" || parsed.bindHere !== void 0) {
624
- parsed.help = true;
625
- continue;
626
- }
627
- parsed.bindHere = true;
628
- index += 1;
629
- continue;
630
- }
631
- if (!arg.startsWith("-") && !parsed.threadId) {
632
- parsed.threadId = arg;
633
- continue;
634
- }
635
- parsed.help = true;
749
+ function resolveCodexNativeCommandSandboxBlock(ctx, subcommand, args) {
750
+ if (!CODEX_NATIVE_EXECUTION_SUBCOMMANDS.has(subcommand)) return;
751
+ if (returnsBeforeNativeCodexExecution(subcommand, args)) return;
752
+ if (isCodexCliNodeResumeBind(subcommand, args)) return resolveCodexNativeSandboxBlock({
753
+ config: ctx.config,
754
+ sessionKey: ctx.sessionKey,
755
+ sessionId: ctx.sessionId,
756
+ surface: `/${["codex", subcommand].join(" ")}`
757
+ });
758
+ return resolveCodexNativeExecutionBlock({
759
+ config: ctx.config,
760
+ agentId: ctx.agentId,
761
+ sessionKey: ctx.sessionKey,
762
+ sessionId: ctx.sessionId,
763
+ surface: `/${["codex", subcommand].join(" ")}`
764
+ });
765
+ }
766
+ function returnsBeforeNativeCodexExecution(subcommand, args) {
767
+ switch (subcommand) {
768
+ case "bind": return parseBindArgs([...args]).help === true;
769
+ case "resume": return returnsBeforeNativeCodexResume(args);
770
+ case "steer": return args.join(" ").trim() === "";
771
+ case "model": return args.length === 0 || args.length > 1;
772
+ case "fast": return args.length === 0 || args.length > 1 || parseCodexFastModeArg(args[0]) === void 0;
773
+ case "permissions": return args.length === 0 || args.length > 1 || parseCodexPermissionsModeArg(args[0]) === void 0;
774
+ case "compact":
775
+ case "review":
776
+ case "detach":
777
+ case "unbind":
778
+ case "stop": return args.length > 0;
779
+ default: return false;
636
780
  }
637
- parsed.threadId = normalizeOptionalString(parsed.threadId);
638
- parsed.host = normalizeOptionalString(parsed.host);
639
- return parsed;
640
781
  }
641
- function parseComputerUseArgs(args) {
642
- const parsed = {
643
- action: "status",
644
- overrides: {},
645
- hasOverrides: false,
646
- persistentIdentity: {}
782
+ function isCodexCliNodeResumeBind(subcommand, args) {
783
+ if (subcommand !== "resume") return false;
784
+ const parsed = parseResumeArgs([...args]);
785
+ return Boolean(parsed.host && parsed.threadId && parsed.bindHere === true && !parsed.help);
786
+ }
787
+ function returnsBeforeNativeCodexResume(args) {
788
+ const parsed = parseResumeArgs([...args]);
789
+ const normalizedThreadId = parsed.threadId?.trim();
790
+ if (parsed.help) return true;
791
+ if (parsed.host) return !normalizedThreadId || parsed.bindHere !== true;
792
+ return !normalizedThreadId || args.length !== 1;
793
+ }
794
+ async function handleComputerUseCommand(deps, ctx, pluginConfig, args) {
795
+ const parsed = parseComputerUseArgs(args);
796
+ if (parsed.help) return ["Usage: /codex computer-use [status|install] [--source <marketplace-source>] [--marketplace-path <path>] [--marketplace <name>]", "Checks or installs the configured Codex Computer Use plugin through app-server."].join("\n");
797
+ if (parsed.action === "install" && !canMutateCodexHost(ctx)) return "Only an owner or operator.admin gateway client can configure Codex Computer Use.";
798
+ const params = {
799
+ pluginConfig,
800
+ forceEnable: parsed.action === "install" || parsed.hasOverrides,
801
+ ...Object.keys(parsed.overrides).length > 0 ? { overrides: parsed.overrides } : {}
647
802
  };
648
- let sawAction = false;
649
- for (let index = 0; index < args.length; index += 1) {
650
- const arg = args[index];
651
- if (arg === "--help" || arg === "-h") {
652
- parsed.help = true;
653
- continue;
654
- }
655
- if (arg === "status" || arg === "install") {
656
- if (sawAction) {
657
- parsed.help = true;
658
- continue;
659
- }
660
- sawAction = true;
661
- parsed.action = arg;
662
- continue;
663
- }
664
- if (arg === "--source" || arg === "--marketplace-source") {
665
- const value = readRequiredOptionValue(args, index);
666
- if (!value || parsed.overrides.marketplaceSource !== void 0) {
667
- parsed.help = true;
668
- continue;
669
- }
670
- parsed.overrides.marketplaceSource = value;
671
- index += 1;
672
- continue;
673
- }
674
- if (arg === "--marketplace-path" || arg === "--path") {
675
- const value = readRequiredOptionValue(args, index);
676
- if (!value || parsed.overrides.marketplacePath !== void 0) {
677
- parsed.help = true;
678
- continue;
679
- }
680
- parsed.overrides.marketplacePath = value;
681
- index += 1;
682
- continue;
683
- }
684
- if (arg === "--marketplace") {
685
- const value = readRequiredOptionValue(args, index);
686
- if (!value || parsed.overrides.marketplaceName !== void 0) {
687
- parsed.help = true;
688
- continue;
689
- }
690
- parsed.overrides.marketplaceName = value;
691
- index += 1;
692
- continue;
693
- }
694
- if (arg === "--plugin" || arg === "--server" || arg === "--mcp-server") {
695
- const value = readRequiredOptionValue(args, index);
696
- const configKey = arg === "--plugin" ? "pluginName" : "mcpServerName";
697
- if (!value || parsed.persistentIdentity[configKey] !== void 0) {
698
- parsed.help = true;
699
- continue;
700
- }
701
- parsed.persistentIdentity[configKey] = value.trim();
702
- index += 1;
703
- continue;
704
- }
705
- parsed.help = true;
706
- }
707
- parsed.overrides = normalizeComputerUseStringOverrides(parsed.overrides);
708
- parsed.hasOverrides = Object.values(parsed.overrides).some(Boolean);
709
- return parsed;
710
- }
711
- function formatComputerUsePersistentIdentityMigration(parsed) {
712
- const configPrefix = "plugins.entries.codex.config.computerUse";
713
- const settings = [parsed.persistentIdentity.pluginName ? `${configPrefix}.pluginName = ${JSON.stringify(parsed.persistentIdentity.pluginName)}` : void 0, parsed.persistentIdentity.mcpServerName ? `${configPrefix}.mcpServerName = ${JSON.stringify(parsed.persistentIdentity.mcpServerName)}` : void 0].filter((setting) => Boolean(setting));
714
- const retryArgs = [
715
- `/codex computer-use ${parsed.action}`,
716
- parsed.overrides.marketplaceSource ? `--source ${JSON.stringify(parsed.overrides.marketplaceSource)}` : void 0,
717
- parsed.overrides.marketplacePath ? `--marketplace-path ${JSON.stringify(parsed.overrides.marketplacePath)}` : void 0,
718
- parsed.overrides.marketplaceName ? `--marketplace ${JSON.stringify(parsed.overrides.marketplaceName)}` : void 0
719
- ].filter((arg) => Boolean(arg));
720
- return ["One-off Computer Use plugin/server overrides are no longer supported.", `Set ${settings.join(" and ")} persistently, then rerun ${retryArgs.join(" ")}.`].join(" ");
721
- }
722
- function readRequiredOptionValue(args, index) {
723
- const value = args[index + 1];
724
- const normalized = value?.trim();
725
- if (!normalized || normalized.startsWith("-")) return;
726
- return value;
727
- }
728
- function normalizeComputerUseStringOverrides(overrides) {
729
- const normalized = {};
730
- const marketplaceSource = normalizeOptionalString(overrides.marketplaceSource);
731
- if (marketplaceSource) normalized.marketplaceSource = marketplaceSource;
732
- const marketplacePath = normalizeOptionalString(overrides.marketplacePath);
733
- if (marketplacePath) normalized.marketplacePath = marketplacePath;
734
- const marketplaceName = normalizeOptionalString(overrides.marketplaceName);
735
- if (marketplaceName) normalized.marketplaceName = marketplaceName;
736
- return normalized;
803
+ if (parsed.action === "install") return formatComputerUseStatus(await deps.installCodexComputerUse(params));
804
+ return formatComputerUseStatus(await deps.readCodexComputerUseStatus(params));
737
805
  }
738
- //#endregion
739
- //#region extensions/codex/src/command-diagnostics-support.ts
740
- const CODEX_DIAGNOSTICS_SOURCE = "openclaw-diagnostics";
741
- const CODEX_DIAGNOSTICS_REASON_MAX_CHARS = 2048;
742
- const CODEX_DIAGNOSTICS_COOLDOWN_MS = 6e4;
743
- const CODEX_DIAGNOSTICS_ERROR_MAX_CHARS = 500;
744
- const CODEX_DIAGNOSTICS_COOLDOWN_MAX_THREADS = 100;
745
- const CODEX_DIAGNOSTICS_COOLDOWN_MAX_SCOPES = 100;
746
- const CODEX_DIAGNOSTICS_CONFIRMATION_TTL_MS = 5 * 6e4;
747
- const CODEX_DIAGNOSTICS_CONFIRMATION_MAX_REQUESTS_PER_SCOPE = 100;
748
- const CODEX_DIAGNOSTICS_CONFIRMATION_MAX_SCOPES = 100;
749
- const CODEX_DIAGNOSTICS_SCOPE_FIELD_MAX_CHARS = 128;
750
- const CODEX_RESUME_SAFE_THREAD_ID_PATTERN = /^[A-Za-z0-9._:-]+$/;
751
- const { lastUploadByThread: lastCodexDiagnosticsUploadByThread, lastUploadByScope: lastCodexDiagnosticsUploadByScope, pendingConfirmations: pendingCodexDiagnosticsConfirmations, pendingTokensByScope: pendingCodexDiagnosticsConfirmationTokensByScope } = codexDiagnosticsFeedbackState;
752
- function normalizeDiagnosticsReason(note) {
753
- const normalized = normalizeOptionalString(note);
754
- return normalized ? truncateUtf16Safe(normalized, CODEX_DIAGNOSTICS_REASON_MAX_CHARS) : void 0;
806
+ async function bindConversation(deps, ctx, pluginConfig, args) {
807
+ const parsed = parseBindArgs(args);
808
+ if (parsed.help) return { text: "Usage: /codex bind [thread-id] [--cwd <path>] [--model <model>] [--provider <provider>]" };
809
+ const scope = resolveCodexConversationControlScope(ctx);
810
+ const workspaceDir = parsed.cwd ?? deps.resolveCodexDefaultWorkspaceDir(pluginConfig);
811
+ const currentConversation = await ctx.getCurrentConversationBinding();
812
+ const currentConversationData = readCodexConversationBindingData(currentConversation);
813
+ const bindingId = currentConversationData?.kind === "codex-app-server-session" ? currentConversationData.bindingId : currentConversation ? `conversation-${currentConversation.bindingId}` : void 0;
814
+ const sessionOwner = ctx.sessionId ? sessionBindingIdentity({
815
+ sessionId: ctx.sessionId,
816
+ sessionKey: ctx.sessionKey,
817
+ agentId: scope.agentId,
818
+ config: ctx.config
819
+ }) : void 0;
820
+ const currentOwner = currentConversationData?.kind === "codex-app-server-session" ? conversationBindingIdentity(currentConversationData.bindingId) : sessionOwner;
821
+ const existingBinding = currentOwner ? await deps.bindingStore.read(currentOwner) : void 0;
822
+ const sessionSource = sessionOwner && existingBinding ? {
823
+ agentId: sessionOwner.agentId,
824
+ sessionId: sessionOwner.sessionId,
825
+ threadId: existingBinding.threadId,
826
+ ...sessionOwner.sessionKey ? { sessionKey: sessionOwner.sessionKey } : {}
827
+ } : void 0;
828
+ const authProfileId = existingBinding?.authProfileId;
829
+ const data = createCodexConversationBindingData({
830
+ bindingId,
831
+ workspaceDir,
832
+ agentId: scope.agentId,
833
+ agentDir: scope.agentDir,
834
+ source: currentConversationData?.kind === "codex-app-server-session" ? currentConversationData.source : sessionSource,
835
+ start: {
836
+ id: crypto.randomUUID(),
837
+ threadId: parsed.threadId,
838
+ model: parsed.model,
839
+ modelProvider: parsed.provider,
840
+ authProfileId
841
+ }
842
+ });
843
+ const threadLabel = parsed.threadId ?? "a new thread";
844
+ const request = await ctx.requestConversationBinding({
845
+ summary: `Codex app-server thread ${formatCodexDisplayText(threadLabel)} in ${formatCodexDisplayText(workspaceDir)}`,
846
+ detachHint: "/codex detach",
847
+ data
848
+ });
849
+ if (request.status === "pending") return request.reply;
850
+ if (request.status === "error") return { text: formatCodexDisplayText(request.message) };
851
+ return { text: `Bound this conversation to ${formatCodexDisplayText(threadLabel)} in ${formatCodexDisplayText(workspaceDir)}. The next message will initialize it.` };
755
852
  }
756
- function parseDiagnosticsArgs(args) {
757
- const [action, token, ...extra] = splitArgs(args);
758
- const normalizedAction = action?.toLowerCase();
759
- if ((normalizedAction === "confirm" || normalizedAction === "--confirm") && token && extra.length === 0) return {
760
- action: "confirm",
761
- token
762
- };
763
- if ((normalizedAction === "cancel" || normalizedAction === "--cancel") && token && extra.length === 0) return {
764
- action: "cancel",
765
- token
766
- };
767
- if (normalizedAction === "confirm" || normalizedAction === "--confirm" || normalizedAction === "cancel" || normalizedAction === "--cancel") return { action: "usage" };
768
- return {
769
- action: "request",
770
- note: args
771
- };
853
+ async function detachConversation(deps, ctx) {
854
+ const current = await ctx.getCurrentConversationBinding();
855
+ const data = readCodexConversationBindingData(current);
856
+ const detached = await ctx.detachConversationBinding();
857
+ if (data?.kind === "codex-app-server-session") await deps.bindingStore.mutate(conversationBindingIdentity(data.bindingId), { kind: "clear" });
858
+ return detached.removed ? "Detached this conversation from Codex." : "No Codex conversation binding was attached.";
772
859
  }
773
- function formatDiagnosticsUsage(commandPrefix) {
860
+ async function describeConversationBinding(deps, ctx) {
861
+ const current = await ctx.getCurrentConversationBinding();
862
+ const data = readCodexConversationBindingData(current);
863
+ if (!current || !data) return "No Codex conversation binding is attached.";
864
+ if (data.kind === "codex-cli-node-session") return [
865
+ "Codex conversation binding:",
866
+ "- Mode: Codex CLI node session",
867
+ `- Node: ${formatCodexDisplayText(data.nodeId)}`,
868
+ `- Session: ${formatCodexDisplayText(data.sessionId)}`,
869
+ `- Workspace: ${formatCodexDisplayText(data.cwd ?? "unknown")}`,
870
+ "- Active run: not tracked"
871
+ ].join("\n");
872
+ const identity = conversationBindingIdentity(data.bindingId);
873
+ const threadBinding = await deps.bindingStore.read(identity);
874
+ const active = deps.readCodexConversationActiveTurn(identity);
774
875
  return [
775
- `Usage: ${commandPrefix} [note]`,
776
- `Usage: ${commandPrefix} confirm <token>`,
777
- `Usage: ${commandPrefix} cancel <token>`
876
+ "Codex conversation binding:",
877
+ `- Thread: ${formatCodexDisplayText(threadBinding?.threadId ?? "unknown")}`,
878
+ `- Workspace: ${formatCodexDisplayText(data.workspaceDir)}`,
879
+ `- Model: ${formatCodexDisplayText(threadBinding?.model ?? "default")}`,
880
+ `- Fast: ${isCodexFastServiceTier(threadBinding?.serviceTier) ? "on" : "off"}`,
881
+ `- Permissions: ${threadBinding ? formatPermissionsMode(threadBinding) : "default"}`,
882
+ `- Active run: ${formatCodexDisplayText(active ? active.turnId : "none")}`,
883
+ `- Binding: ${formatCodexDisplayText(data.bindingId)}`
778
884
  ].join("\n");
779
885
  }
780
- function createCodexDiagnosticsConfirmation(params) {
781
- prunePendingCodexDiagnosticsConfirmations(params.now);
782
- if (!pendingCodexDiagnosticsConfirmationTokensByScope.has(params.scopeKey) && pendingCodexDiagnosticsConfirmationTokensByScope.size >= CODEX_DIAGNOSTICS_CONFIRMATION_MAX_SCOPES) {
783
- const oldestScopeKey = pendingCodexDiagnosticsConfirmationTokensByScope.keys().next().value;
784
- if (typeof oldestScopeKey === "string") deletePendingCodexDiagnosticsConfirmationScope(oldestScopeKey);
785
- }
786
- const scopeTokens = pendingCodexDiagnosticsConfirmationTokensByScope.get(params.scopeKey) ?? [];
787
- while (scopeTokens.length >= CODEX_DIAGNOSTICS_CONFIRMATION_MAX_REQUESTS_PER_SCOPE) {
788
- const oldestToken = scopeTokens.shift();
789
- if (!oldestToken) break;
790
- pendingCodexDiagnosticsConfirmations.delete(oldestToken);
791
- }
792
- const token = crypto.randomBytes(6).toString("hex");
793
- scopeTokens.push(token);
794
- pendingCodexDiagnosticsConfirmationTokensByScope.set(params.scopeKey, scopeTokens);
795
- pendingCodexDiagnosticsConfirmations.set(token, {
796
- token,
797
- targets: params.targets,
798
- note: params.note,
799
- senderId: params.senderId,
800
- channel: params.channel,
801
- accountId: params.accountId,
802
- channelId: params.channelId,
803
- messageThreadId: params.messageThreadId,
804
- threadParentId: params.threadParentId,
805
- sessionKey: params.sessionKey,
806
- scopeKey: params.scopeKey,
807
- ...params.privateRouted === void 0 ? {} : { privateRouted: params.privateRouted },
808
- createdAt: params.now
886
+ async function buildThreads(deps, ctx, pluginConfig, filter) {
887
+ const scope = await resolveCommandAppServerScope(deps, ctx);
888
+ const response = await deps.codexControlRequest(pluginConfig, CODEX_CONTROL_METHODS.listThreads, {
889
+ limit: 10,
890
+ ...filter.trim() ? { searchTerm: filter.trim() } : {}
891
+ }, {
892
+ config: ctx.config,
893
+ ...scope
809
894
  });
810
- return token;
811
- }
812
- function readCodexDiagnosticsConfirmationScope(ctx) {
813
- return {
814
- accountId: normalizeCodexDiagnosticsScopeField(ctx.accountId),
815
- channelId: normalizeCodexDiagnosticsScopeField(ctx.channelId),
816
- messageThreadId: typeof ctx.messageThreadId === "string" || typeof ctx.messageThreadId === "number" ? normalizeCodexDiagnosticsScopeField(String(ctx.messageThreadId)) : void 0,
817
- threadParentId: normalizeCodexDiagnosticsScopeField(ctx.threadParentId),
818
- sessionKey: normalizeCodexDiagnosticsScopeField(ctx.sessionKey)
819
- };
820
- }
821
- function readCodexDiagnosticsScopeMismatch(pending, ctx) {
822
- const current = readCodexDiagnosticsConfirmationScope(ctx);
823
- if (pending.accountId !== current.accountId) return {
824
- confirmMessage: "This Codex diagnostics confirmation belongs to a different account.",
825
- cancelMessage: "This Codex diagnostics confirmation belongs to a different account."
826
- };
827
- if (pending.privateRouted) return;
828
- if (pending.channelId !== current.channelId) return {
829
- confirmMessage: "This Codex diagnostics confirmation belongs to a different channel instance.",
830
- cancelMessage: "This Codex diagnostics confirmation belongs to a different channel instance."
831
- };
832
- if (pending.messageThreadId !== current.messageThreadId) return {
833
- confirmMessage: "This Codex diagnostics confirmation belongs to a different thread.",
834
- cancelMessage: "This Codex diagnostics confirmation belongs to a different thread."
835
- };
836
- if (pending.threadParentId !== current.threadParentId) return {
837
- confirmMessage: "This Codex diagnostics confirmation belongs to a different parent thread.",
838
- cancelMessage: "This Codex diagnostics confirmation belongs to a different parent thread."
839
- };
840
- if (pending.sessionKey !== current.sessionKey) return {
841
- confirmMessage: "This Codex diagnostics confirmation belongs to a different session.",
842
- cancelMessage: "This Codex diagnostics confirmation belongs to a different session."
843
- };
895
+ return formatThreads(response);
844
896
  }
845
- function readPendingCodexDiagnosticsConfirmation(token, now) {
846
- prunePendingCodexDiagnosticsConfirmations(now);
847
- return pendingCodexDiagnosticsConfirmations.get(token);
897
+ async function buildCodexCliSessions(deps, args) {
898
+ const parsed = parseCodexCliSessionsArgs(args);
899
+ if (parsed.help || !parsed.host) return "Usage: /codex sessions --host <node> [filter] [--limit <n>]";
900
+ return formatCodexCliSessions(await deps.listCodexCliSessionsOnNode({
901
+ requestedNode: parsed.host,
902
+ filter: parsed.filter,
903
+ limit: parsed.limit
904
+ }));
848
905
  }
849
- function deletePendingCodexDiagnosticsConfirmation(token) {
850
- const pending = pendingCodexDiagnosticsConfirmations.get(token);
851
- pendingCodexDiagnosticsConfirmations.delete(token);
852
- if (!pending) return;
853
- const scopeTokens = pendingCodexDiagnosticsConfirmationTokensByScope.get(pending.scopeKey);
854
- if (!scopeTokens) return;
855
- const tokenIndex = scopeTokens.indexOf(token);
856
- if (tokenIndex >= 0) scopeTokens.splice(tokenIndex, 1);
857
- if (scopeTokens.length === 0) pendingCodexDiagnosticsConfirmationTokensByScope.delete(pending.scopeKey);
906
+ async function resumeThread(deps, ctx, pluginConfig, args) {
907
+ const parsed = parseResumeArgs(args);
908
+ const normalizedThreadId = parsed.threadId?.trim();
909
+ if (parsed.help) return args.includes("--help") || args.includes("-h") || parsed.host ? "Usage: /codex resume <thread-id>\nUsage: /codex resume <session-id> --host <node> --bind here" : "Usage: /codex resume <thread-id>";
910
+ if (parsed.host) return await bindCodexCliNodeSession(deps, ctx, parsed);
911
+ if (!normalizedThreadId || args.length !== 1) return "Usage: /codex resume <thread-id>";
912
+ if (!ctx.sessionId) return "Cannot attach a Codex thread because this command did not include an OpenClaw session id.";
913
+ const scope = resolveCodexConversationControlScope(ctx);
914
+ const identity = sessionBindingIdentity({
915
+ sessionId: ctx.sessionId,
916
+ sessionKey: ctx.sessionKey,
917
+ agentId: scope.agentId,
918
+ config: ctx.config
919
+ });
920
+ return await deps.bindingStore.withLease(identity, async () => {
921
+ if (!await reclaimCurrentCodexSessionGeneration({
922
+ bindingStore: deps.bindingStore,
923
+ identity,
924
+ config: ctx.config
925
+ })) throw new Error(`Codex session generation is no longer current: ${identity.sessionId}`);
926
+ const currentBinding = await deps.bindingStore.read(identity);
927
+ const authProfileId = resolveCodexAppServerAuthProfileIdForAgent({
928
+ authProfileId: currentBinding?.authProfileId,
929
+ agentDir: scope.agentDir,
930
+ config: ctx.config
931
+ });
932
+ const response = assertCodexThreadResumeResponse(await deps.codexControlRequest(pluginConfig, CODEX_CONTROL_METHODS.resumeThread, {
933
+ threadId: normalizedThreadId,
934
+ excludeTurns: true
935
+ }, {
936
+ config: ctx.config,
937
+ agentDir: scope.agentDir,
938
+ authProfileId,
939
+ sessionKey: ctx.sessionKey,
940
+ sessionId: ctx.sessionId
941
+ }));
942
+ const effectiveThreadId = response.thread.id;
943
+ if (effectiveThreadId !== normalizedThreadId) throw new Error(`Codex thread/resume returned ${effectiveThreadId} for ${normalizedThreadId}`);
944
+ const resumedCwd = response.thread.cwd;
945
+ if (typeof resumedCwd !== "string") throw new Error(`Codex thread/resume returned no cwd for ${normalizedThreadId}`);
946
+ const modelProvider = normalizeCodexAppServerBindingModelProvider({
947
+ authProfileId,
948
+ modelProvider: response.modelProvider ?? void 0,
949
+ agentDir: scope.agentDir,
950
+ config: ctx.config
951
+ });
952
+ if (!await deps.bindingStore.mutate(identity, {
953
+ kind: "set",
954
+ binding: {
955
+ threadId: effectiveThreadId,
956
+ cwd: resumedCwd,
957
+ authProfileId,
958
+ model: response.model,
959
+ modelProvider,
960
+ historyCoveredThrough: (/* @__PURE__ */ new Date()).toISOString()
961
+ }
962
+ })) throw new Error("Codex thread binding changed while attaching the resumed thread.");
963
+ return `Attached this OpenClaw session to Codex thread ${formatCodexDisplayText(effectiveThreadId)}.`;
964
+ });
858
965
  }
859
- function prunePendingCodexDiagnosticsConfirmations(now) {
860
- for (const [token, pending] of pendingCodexDiagnosticsConfirmations) if (now - pending.createdAt >= CODEX_DIAGNOSTICS_CONFIRMATION_TTL_MS) deletePendingCodexDiagnosticsConfirmation(token);
966
+ async function bindCodexCliNodeSession(deps, ctx, parsed) {
967
+ if (!parsed.threadId || !parsed.host || parsed.bindHere !== true) return "Usage: /codex resume <session-id> --host <node> --bind here";
968
+ const resolved = await deps.resolveCodexCliSessionForBindingOnNode({
969
+ requestedNode: parsed.host,
970
+ sessionId: parsed.threadId
971
+ });
972
+ if (!resolved.session) return `No Codex CLI session ${formatCodexDisplayText(parsed.threadId)} was found on ${formatCodexDisplayText(parsed.host)}.`;
973
+ const nodeId = resolved.node.nodeId;
974
+ if (!nodeId) return "Cannot bind Codex CLI session because the selected node did not include a node id.";
975
+ const scope = resolveCodexConversationControlScope(ctx);
976
+ const data = createCodexCliNodeConversationBindingData({
977
+ nodeId,
978
+ sessionId: parsed.threadId,
979
+ agentId: scope.agentId,
980
+ cwd: resolved.session?.cwd
981
+ });
982
+ const summary = `Codex CLI session ${formatCodexDisplayText(parsed.threadId)} on ${formatCodexDisplayText(nodeId)}`;
983
+ const request = await ctx.requestConversationBinding({
984
+ summary,
985
+ detachHint: "/codex detach",
986
+ data
987
+ });
988
+ if (request.status === "bound") return `Bound this conversation to Codex CLI session ${formatCodexDisplayText(parsed.threadId)} on ${formatCodexDisplayText(nodeId)}.`;
989
+ if (request.status === "pending") return request.reply.text ?? "Codex CLI session binding is pending approval.";
990
+ return formatCodexDisplayText(request.message);
861
991
  }
862
- function deletePendingCodexDiagnosticsConfirmationScope(scopeKey) {
863
- const scopeTokens = pendingCodexDiagnosticsConfirmationTokensByScope.get(scopeKey) ?? [];
864
- for (const token of scopeTokens) pendingCodexDiagnosticsConfirmations.delete(token);
865
- pendingCodexDiagnosticsConfirmationTokensByScope.delete(scopeKey);
992
+ async function stopConversationTurn(deps, ctx, pluginConfig) {
993
+ const target = await resolveControlTarget(ctx);
994
+ if (!target) return "Cannot stop Codex because this command did not include a stable binding identity.";
995
+ return (await deps.stopCodexConversationTurn({
996
+ identity: target.identity,
997
+ bindingStore: deps.bindingStore,
998
+ pluginConfig,
999
+ agentDir: target.agentDir,
1000
+ config: ctx.config
1001
+ })).message;
866
1002
  }
867
- function codexDiagnosticsTargetsMatch(expected, actual) {
868
- const fingerprint = (target) => JSON.stringify([
869
- bindingStoreKey(target.identity),
870
- target.threadId,
871
- target.connectionScope ?? null,
872
- target.pendingSupervisionBranch?.connectionFingerprint ?? target.appServerRuntimeFingerprint ?? null,
873
- target.authProfileId ?? null
874
- ]);
875
- const expectedTargets = expected.map(fingerprint).toSorted();
876
- const actualTargets = actual.map(fingerprint).toSorted();
877
- return expectedTargets.length === actualTargets.length && expectedTargets.every((target, index) => target === actualTargets[index]);
1003
+ async function steerConversationTurn(deps, ctx, pluginConfig, message) {
1004
+ const target = await resolveControlTarget(ctx);
1005
+ if (!target) return "Cannot steer Codex because this command did not include a stable binding identity.";
1006
+ return (await deps.steerCodexConversationTurn({
1007
+ identity: target.identity,
1008
+ bindingStore: deps.bindingStore,
1009
+ message,
1010
+ pluginConfig,
1011
+ agentDir: target.agentDir,
1012
+ config: ctx.config
1013
+ })).message;
878
1014
  }
879
- function formatCodexDiagnosticsUploadResult(sent, failed) {
880
- const lines = [];
881
- if (sent.length > 0) {
882
- lines.push("Codex diagnostics sent to OpenAI servers:");
883
- lines.push(...formatCodexDiagnosticsTargetLines(sent));
884
- lines.push("Included Codex logs and spawned Codex subthreads when available.");
885
- }
886
- if (failed.length > 0) {
887
- if (lines.length > 0) lines.push("");
888
- lines.push("Could not send Codex diagnostics:");
889
- lines.push(...failed.map(({ target, error }) => `${formatCodexDiagnosticsTargetLine(target)}: ${formatCodexErrorForDisplay(error)}`));
890
- lines.push("Inspect locally:");
891
- lines.push(...failed.map(({ target }) => `- ${formatCodexResumeCommandForDisplay(target.threadId)}`));
1015
+ async function setConversationModel(deps, ctx, pluginConfig, args) {
1016
+ if (args.length > 1) return "Usage: /codex model <model>";
1017
+ const target = await resolveControlTarget(ctx);
1018
+ if (!target) return "Cannot set Codex model because this command did not include a stable binding identity.";
1019
+ const [model = ""] = args;
1020
+ const normalized = model.trim();
1021
+ if (!normalized) {
1022
+ const binding = await deps.bindingStore.read(target.identity);
1023
+ return binding?.model ? `Codex model: ${formatCodexDisplayText(binding.model)}` : "Usage: /codex model <model>";
892
1024
  }
893
- return lines.join("\n");
894
- }
895
- function formatCodexDiagnosticsTargetLines(targets) {
896
- return targets.flatMap((target, index) => {
897
- const lines = formatCodexDiagnosticsTargetBlock(target, index);
898
- return index < targets.length - 1 ? [...lines, ""] : lines;
1025
+ return await deps.setCodexConversationModel({
1026
+ identity: target.identity,
1027
+ bindingStore: deps.bindingStore,
1028
+ pluginConfig,
1029
+ model: normalized,
1030
+ agentDir: target.agentDir,
1031
+ config: ctx.config
899
1032
  });
900
1033
  }
901
- function formatCodexDiagnosticsTargetBlock(target, index) {
902
- const lines = [`Session ${index + 1}`];
903
- if (target.channel) lines.push(`Channel: ${formatCodexValueForDisplay(target.channel)}`);
904
- if (target.sessionKey) lines.push(`OpenClaw session key: ${formatCodexCopyableValueForDisplay(target.sessionKey)}`);
905
- if (target.sessionId) lines.push(`OpenClaw session id: ${formatCodexCopyableValueForDisplay(target.sessionId)}`);
906
- lines.push(`Codex thread id: ${formatCodexCopyableValueForDisplay(target.threadId)}`);
907
- lines.push(`Inspect locally: ${formatCodexResumeCommandForDisplay(target.threadId)}`);
908
- return lines;
1034
+ async function setConversationFastMode(deps, ctx, args) {
1035
+ if (args.length > 1) return "Usage: /codex fast [on|off|status]";
1036
+ const target = await resolveControlTarget(ctx);
1037
+ if (!target) return "Cannot set Codex fast mode because this command did not include a stable binding identity.";
1038
+ const value = args[0];
1039
+ const parsed = parseCodexFastModeArg(value);
1040
+ if (value && parsed == null && value.trim().toLowerCase() !== "status") return "Usage: /codex fast [on|off|status]";
1041
+ return await deps.setCodexConversationFastMode({
1042
+ identity: target.identity,
1043
+ bindingStore: deps.bindingStore,
1044
+ enabled: parsed
1045
+ });
909
1046
  }
910
- function formatCodexDiagnosticsTargetLine(target) {
911
- const parts = [];
912
- if (target.channel) parts.push(`channel ${formatCodexValueForDisplay(target.channel)}`);
913
- const sessionLabel = target.sessionId || target.sessionKey;
914
- if (sessionLabel) parts.push(`OpenClaw session ${formatCodexValueForDisplay(sessionLabel)}`);
915
- parts.push(`Codex thread ${formatCodexThreadIdForDisplay(target.threadId)}`);
916
- return `- ${parts.join(", ")}`;
1047
+ async function setConversationPermissions(deps, ctx, args) {
1048
+ if (args.length > 1) return "Usage: /codex permissions [default|yolo|status]";
1049
+ const target = await resolveControlTarget(ctx);
1050
+ if (!target) return "Cannot set Codex permissions because this command did not include a stable binding identity.";
1051
+ const value = args[0];
1052
+ const parsed = parseCodexPermissionsModeArg(value);
1053
+ if (value && !parsed && value.trim().toLowerCase() !== "status") return "Usage: /codex permissions [default|yolo|status]";
1054
+ return await deps.setCodexConversationPermissions({
1055
+ identity: target.identity,
1056
+ bindingStore: deps.bindingStore,
1057
+ mode: parsed
1058
+ });
917
1059
  }
918
- function escapeCodexChatText(value) {
919
- return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("@", "@").replaceAll("`", "`").replaceAll("[", "[").replaceAll("]", "]").replaceAll("(", "(").replaceAll(")", ")").replaceAll("*", "∗").replaceAll("_", "_").replaceAll("~", "~").replaceAll("|", "|");
1060
+ async function resolveControlTarget(ctx) {
1061
+ const binding = await ctx.getCurrentConversationBinding();
1062
+ const data = readCodexConversationBindingData(binding);
1063
+ const scope = resolveCodexConversationControlScope(ctx);
1064
+ if (data?.kind === "codex-app-server-session") return {
1065
+ identity: conversationBindingIdentity(data.bindingId),
1066
+ agentId: data.agentId ?? scope.agentId,
1067
+ agentDir: data.agentDir ?? scope.agentDir,
1068
+ requestedAuthProfileId: data.start?.authProfileId
1069
+ };
1070
+ return ctx.sessionId ? {
1071
+ identity: sessionBindingIdentity({
1072
+ sessionId: ctx.sessionId,
1073
+ sessionKey: ctx.sessionKey,
1074
+ agentId: scope.agentId,
1075
+ config: ctx.config
1076
+ }),
1077
+ agentId: scope.agentId,
1078
+ agentDir: scope.agentDir
1079
+ } : void 0;
920
1080
  }
921
- function formatCodexTextForDisplay(value) {
922
- let safe = "";
923
- for (const character of value) {
924
- const codePoint = character.codePointAt(0);
925
- safe += codePoint != null && isUnsafeDisplayCodePoint(codePoint) ? "?" : character;
926
- }
927
- safe = safe.trim();
928
- return safe || "<unknown>";
1081
+ async function resolveCommandAppServerScope(deps, ctx) {
1082
+ const target = await resolveControlTarget(ctx);
1083
+ const fallback = resolveCodexConversationControlScope(ctx);
1084
+ const agentDir = target?.agentDir ?? fallback.agentDir;
1085
+ const binding = target ? await deps.bindingStore.read(target.identity) : void 0;
1086
+ const authProfileId = resolveCodexAppServerAuthProfileIdForAgent({
1087
+ authProfileId: binding?.authProfileId ?? target?.requestedAuthProfileId,
1088
+ agentDir,
1089
+ config: ctx.config
1090
+ });
1091
+ return {
1092
+ agentId: target?.agentId ?? fallback.agentId,
1093
+ agentDir,
1094
+ ...authProfileId ? { authProfileId } : {},
1095
+ ...ctx.sessionKey ? { sessionKey: ctx.sessionKey } : {},
1096
+ ...ctx.sessionId ? { sessionId: ctx.sessionId } : {}
1097
+ };
929
1098
  }
930
- function readCodexDiagnosticsTargetsCooldownMessage(targets, ctx, now, options = {}) {
931
- for (const target of targets) {
932
- const cooldownMs = readCodexDiagnosticsCooldownMs(target.threadId, now);
933
- if (cooldownMs > 0) {
934
- if (options.includeThreadId === false) return `Codex diagnostics were already sent for one of these Codex threads recently. Try again in ${Math.ceil(cooldownMs / 1e3)}s.`;
935
- return `Codex diagnostics were already sent for thread ${formatCodexThreadIdForDisplay(target.threadId)} recently. Try again in ${Math.ceil(cooldownMs / 1e3)}s.`;
936
- }
937
- }
938
- const scopeCooldownMs = readCodexDiagnosticsScopeCooldownMs(options.cooldownScope ?? readCodexDiagnosticsCooldownScope(ctx), now);
939
- if (scopeCooldownMs > 0) return `Codex diagnostics were already sent for this account or channel recently. Try again in ${Math.ceil(scopeCooldownMs / 1e3)}s.`;
940
- }
941
- function recordCodexDiagnosticsUpload(threadId, ctx, now, cooldownScope) {
942
- pruneCodexDiagnosticsCooldowns(now);
943
- recordBoundedCodexDiagnosticsCooldown(lastCodexDiagnosticsUploadByScope, cooldownScope ?? readCodexDiagnosticsCooldownScope(ctx), CODEX_DIAGNOSTICS_COOLDOWN_MAX_SCOPES, now);
944
- recordBoundedCodexDiagnosticsCooldown(lastCodexDiagnosticsUploadByThread, threadId, CODEX_DIAGNOSTICS_COOLDOWN_MAX_THREADS, now);
945
- }
946
- function readCodexDiagnosticsCooldownScope(ctx) {
947
- const scope = readCodexDiagnosticsConfirmationScope(ctx);
948
- const payload = JSON.stringify({
949
- accountId: scope.accountId ?? null,
950
- channelId: scope.channelId ?? null,
951
- sessionKey: scope.sessionKey ?? null,
952
- messageThreadId: scope.messageThreadId ?? null,
953
- threadParentId: scope.threadParentId ?? null,
954
- senderId: normalizeCodexDiagnosticsScopeField(ctx.senderId) ?? null,
955
- channel: normalizeCodexDiagnosticsScopeField(ctx.channel) ?? ""
956
- });
957
- return crypto.createHash("sha256").update(payload).digest("hex");
958
- }
959
- function buildDiagnosticsTags(ctx) {
960
- const tags = { source: CODEX_DIAGNOSTICS_SOURCE };
961
- addTag(tags, "channel", ctx.channel);
962
- return tags;
963
- }
964
- function addTag(tags, key, value) {
965
- if (typeof value === "string" && value.trim()) tags[key] = value.trim();
966
- }
967
- function formatCodexThreadIdForDisplay(threadId) {
968
- return escapeCodexChatText(formatCodexTextForDisplay(threadId));
969
- }
970
- function formatCodexValueForDisplay(value) {
971
- return escapeCodexChatText(formatCodexTextForDisplay(value));
972
- }
973
- function formatCodexCopyableValueForDisplay(value) {
974
- const safe = formatCodexTextForDisplay(value);
975
- if (CODEX_RESUME_SAFE_THREAD_ID_PATTERN.test(safe)) return `\`${safe}\``;
976
- return escapeCodexChatText(safe);
977
- }
978
- function readCodexDiagnosticsCooldownMs(threadId, now) {
979
- const lastSentAt = lastCodexDiagnosticsUploadByThread.get(threadId);
980
- if (!lastSentAt) return 0;
981
- const remainingMs = Math.max(0, CODEX_DIAGNOSTICS_COOLDOWN_MS - (now - lastSentAt));
982
- if (remainingMs === 0) lastCodexDiagnosticsUploadByThread.delete(threadId);
983
- return remainingMs;
984
- }
985
- function readCodexDiagnosticsScopeCooldownMs(scope, now) {
986
- const lastSentAt = lastCodexDiagnosticsUploadByScope.get(scope);
987
- if (!lastSentAt) return 0;
988
- const remainingMs = Math.max(0, CODEX_DIAGNOSTICS_COOLDOWN_MS - (now - lastSentAt));
989
- if (remainingMs === 0) lastCodexDiagnosticsUploadByScope.delete(scope);
990
- return remainingMs;
991
- }
992
- function recordBoundedCodexDiagnosticsCooldown(map, key, maxSize, now) {
993
- if (!map.has(key)) while (map.size >= maxSize) {
994
- const oldestKey = map.keys().next().value;
995
- if (typeof oldestKey !== "string") break;
996
- map.delete(oldestKey);
997
- }
998
- map.set(key, now);
999
- }
1000
- function pruneCodexDiagnosticsCooldowns(now) {
1001
- pruneCodexDiagnosticsCooldownMap(lastCodexDiagnosticsUploadByThread, now);
1002
- pruneCodexDiagnosticsCooldownMap(lastCodexDiagnosticsUploadByScope, now);
1003
- }
1004
- function pruneCodexDiagnosticsCooldownMap(map, now) {
1005
- for (const [key, lastSentAt] of map) if (now - lastSentAt >= CODEX_DIAGNOSTICS_COOLDOWN_MS) map.delete(key);
1006
- }
1007
- function formatCodexErrorForDisplay(error) {
1008
- return escapeCodexChatText(truncateUtf16Safe(formatCodexTextForDisplay(error), CODEX_DIAGNOSTICS_ERROR_MAX_CHARS)) || "unknown error";
1009
- }
1010
- function formatCodexResumeCommandForDisplay(threadId) {
1011
- const safeThreadId = formatCodexTextForDisplay(threadId);
1012
- if (!CODEX_RESUME_SAFE_THREAD_ID_PATTERN.test(safeThreadId)) return "run codex resume and paste the thread id shown above";
1013
- return `\`codex resume ${safeThreadId}\``;
1014
- }
1015
- function isUnsafeDisplayCodePoint(codePoint) {
1016
- return codePoint <= 31 || codePoint >= 127 && codePoint <= 159 || codePoint === 173 || codePoint === 1564 || codePoint === 6158 || codePoint >= 8203 && codePoint <= 8207 || codePoint >= 8234 && codePoint <= 8238 || codePoint >= 8288 && codePoint <= 8303 || codePoint === 65279 || codePoint >= 65529 && codePoint <= 65531 || codePoint >= 917504 && codePoint <= 917631;
1017
- }
1018
- function normalizeCodexDiagnosticsScopeField(value) {
1019
- const normalized = normalizeOptionalString(value);
1020
- if (!normalized) return;
1021
- if (normalized.length <= CODEX_DIAGNOSTICS_SCOPE_FIELD_MAX_CHARS) return normalized;
1022
- return `sha256:${crypto.createHash("sha256").update(normalized).digest("hex")}`;
1023
- }
1024
- //#endregion
1025
- //#region extensions/codex/src/command-handler-deps.ts
1026
- const defaultCodexCommandDeps = {
1027
- codexControlRequest,
1028
- listCodexAppServerModels: listAllCodexAppServerModels,
1029
- readCodexStatusProbes,
1030
- requestOptions,
1031
- safeCodexControlRequest,
1032
- readCodexComputerUseStatus,
1033
- installCodexComputerUse,
1034
- resolveCodexDefaultWorkspaceDir,
1035
- readCodexConversationActiveTurn,
1036
- setCodexConversationFastMode,
1037
- setCodexConversationModel,
1038
- setCodexConversationPermissions,
1039
- steerCodexConversationTurn,
1040
- stopCodexConversationTurn,
1041
- listCodexCliSessionsOnNode: async () => {
1042
- throw new Error("Codex CLI node sessions require Gateway node runtime.");
1043
- },
1044
- resolveCodexCliSessionForBindingOnNode: async () => {
1045
- throw new Error("Codex CLI node sessions require Gateway node runtime.");
1046
- }
1047
- };
1048
- function resolveCodexCommandDeps(overrides) {
1049
- return {
1050
- ...defaultCodexCommandDeps,
1051
- ...overrides
1052
- };
1053
- }
1054
- //#endregion
1055
- //#region extensions/codex/src/command-handler-scope.ts
1056
- async function resolveControlTarget(ctx) {
1057
- const data = readCodexConversationBindingData(await ctx.getCurrentConversationBinding());
1058
- const scope = resolveCodexConversationControlScope(ctx);
1059
- if (data?.kind === "codex-app-server-session") return {
1060
- identity: conversationBindingIdentity(data.bindingId),
1061
- agentId: data.agentId ?? scope.agentId,
1062
- agentDir: data.agentDir ?? scope.agentDir,
1063
- requestedAuthProfileId: data.start?.authProfileId
1064
- };
1065
- return ctx.sessionId ? {
1066
- identity: sessionBindingIdentity({
1067
- sessionId: ctx.sessionId,
1068
- sessionKey: ctx.sessionKey,
1069
- agentId: scope.agentId,
1070
- config: ctx.config
1071
- }),
1072
- agentId: scope.agentId,
1073
- agentDir: scope.agentDir
1074
- } : void 0;
1075
- }
1076
- async function resolveCommandAppServerScope(deps, ctx, pluginConfig) {
1077
- const target = await resolveControlTarget(ctx);
1078
- const fallback = resolveCodexConversationControlScope(ctx);
1079
- const agentDir = target?.agentDir ?? fallback.agentDir;
1080
- const binding = target ? await deps.bindingStore.read(target.identity) : void 0;
1081
- const connection = resolveCodexBindingAppServerConnection({
1082
- binding,
1083
- authProfileId: binding?.connectionScope === "supervision" ? void 0 : resolveCodexAppServerAuthProfileIdForAgent({
1084
- authProfileId: binding?.authProfileId ?? target?.requestedAuthProfileId,
1085
- agentDir,
1086
- config: ctx.config
1087
- }),
1088
- pluginConfig
1089
- });
1090
- return {
1091
- agentId: target?.agentId ?? fallback.agentId,
1092
- agentDir,
1093
- ...connection.clientAuthProfileId !== void 0 ? { authProfileId: connection.clientAuthProfileId } : {},
1094
- ...connection.usesSupervisionConnection ? { startOptions: connection.appServer.start } : {},
1095
- ...ctx.sessionKey ? { sessionKey: ctx.sessionKey } : {},
1096
- ...ctx.sessionId ? { sessionId: ctx.sessionId } : {}
1097
- };
1098
- }
1099
- function conversationBindingIdentity(bindingId) {
1100
- return {
1101
- kind: "conversation",
1102
- bindingId
1103
- };
1099
+ function conversationBindingIdentity(bindingId) {
1100
+ return {
1101
+ kind: "conversation",
1102
+ bindingId
1103
+ };
1104
1104
  }
1105
1105
  function resolveCodexConversationControlScope(ctx) {
1106
1106
  const { sessionAgentId } = resolveSessionAgentIds({
@@ -1113,8 +1113,6 @@ function resolveCodexConversationControlScope(ctx) {
1113
1113
  agentDir: resolveAgentDir(ctx.config, sessionAgentId)
1114
1114
  };
1115
1115
  }
1116
- //#endregion
1117
- //#region extensions/codex/src/command-diagnostics.ts
1118
1116
  async function handleCodexDiagnosticsFeedback(deps, ctx, pluginConfig, args, commandPrefix) {
1119
1117
  if (ctx.senderIsOwner !== true) return { text: "Only an owner can send Codex diagnostics." };
1120
1118
  const parsed = parseDiagnosticsArgs(args);
@@ -1241,20 +1239,6 @@ async function sendCodexDiagnosticsFeedbackForTargets(deps, ctx, pluginConfig, n
1241
1239
  const sent = [];
1242
1240
  const failed = [];
1243
1241
  for (const target of targets) {
1244
- let connection;
1245
- try {
1246
- connection = resolveCodexBindingAppServerConnection({
1247
- binding: target,
1248
- authProfileId: target.authProfileId,
1249
- pluginConfig
1250
- });
1251
- } catch (error) {
1252
- failed.push({
1253
- target,
1254
- error: error instanceof Error ? error.message : String(error)
1255
- });
1256
- continue;
1257
- }
1258
1242
  const response = await deps.safeCodexControlRequest(pluginConfig, CODEX_CONTROL_METHODS.feedback, {
1259
1243
  classification: "bug",
1260
1244
  threadId: target.threadId,
@@ -1264,8 +1248,7 @@ async function sendCodexDiagnosticsFeedbackForTargets(deps, ctx, pluginConfig, n
1264
1248
  }, {
1265
1249
  config: ctx.config,
1266
1250
  agentDir: target.agentDir,
1267
- ...connection.clientAuthProfileId !== void 0 ? { authProfileId: connection.clientAuthProfileId } : {},
1268
- ...connection.usesSupervisionConnection ? { startOptions: connection.appServer.start } : {},
1251
+ ...target.authProfileId ? { authProfileId: target.authProfileId } : {},
1269
1252
  ...target.sessionId ? { sessionId: target.sessionId } : {},
1270
1253
  ...target.sessionKey ? { sessionKey: target.sessionKey } : {}
1271
1254
  });
@@ -1348,453 +1331,286 @@ async function resolvePendingCodexDiagnosticsTargets(deps, targets, config) {
1348
1331
  return resolved;
1349
1332
  }
1350
1333
  function resolveCodexDiagnosticsTarget(target, binding, config) {
1351
- const candidate = {
1352
- identity: target.identity,
1353
- agentDir: target.agentDir,
1354
- sessionKey: target.sessionKey,
1355
- sessionId: target.sessionId,
1356
- channel: target.channel,
1357
- channelId: target.channelId,
1358
- accountId: target.accountId,
1359
- messageThreadId: target.messageThreadId,
1360
- threadParentId: target.threadParentId
1361
- };
1362
- if (binding.connectionScope === "supervision") return {
1363
- ...candidate,
1364
- threadId: binding.threadId,
1365
- connectionScope: binding.connectionScope,
1366
- appServerRuntimeFingerprint: binding.appServerRuntimeFingerprint,
1367
- pendingSupervisionBranch: binding.pendingSupervisionBranch
1368
- };
1369
1334
  const authProfileId = resolveCodexAppServerAuthProfileIdForAgent({
1370
1335
  authProfileId: binding.authProfileId,
1371
1336
  agentDir: target.agentDir,
1372
1337
  config
1373
1338
  });
1374
1339
  return {
1375
- ...candidate,
1340
+ ...target,
1376
1341
  threadId: binding.threadId,
1377
1342
  authProfileId
1378
1343
  };
1379
1344
  }
1380
- //#endregion
1381
- //#region extensions/codex/src/command-handler-bindings.ts
1382
- function isCurrentSessionModelSelectionLocked(ctx) {
1383
- const sessionKey = ctx.sessionKey?.trim();
1384
- if (!sessionKey) return false;
1385
- const { agentId } = resolveCodexConversationControlScope(ctx);
1386
- return isModelSelectionLocked(getSessionEntry({
1387
- storePath: resolveStorePath(ctx.config.session?.store, { agentId }),
1388
- sessionKey,
1389
- hydrateSkillPromptRefs: false,
1390
- readConsistency: "latest"
1391
- }));
1345
+ function codexDiagnosticsTargetsMatch(expected, actual) {
1346
+ const fingerprint = (target) => JSON.stringify([
1347
+ bindingStoreKey(target.identity),
1348
+ target.threadId,
1349
+ target.authProfileId ?? null
1350
+ ]);
1351
+ const expectedTargets = expected.map(fingerprint).toSorted();
1352
+ const actualTargets = actual.map(fingerprint).toSorted();
1353
+ return expectedTargets.length === actualTargets.length && expectedTargets.every((target, index) => target === actualTargets[index]);
1392
1354
  }
1393
- async function bindConversation(deps, ctx, pluginConfig, args) {
1394
- const parsed = parseBindArgs(args);
1395
- if (parsed.help) return { text: "Usage: /codex bind [thread-id] [--cwd <path>] [--model <model>] [--provider <provider>]" };
1396
- if (isCurrentSessionModelSelectionLocked(ctx)) return { text: MODEL_SELECTION_LOCKED_MESSAGE };
1397
- const scope = resolveCodexConversationControlScope(ctx);
1398
- const workspaceDir = parsed.cwd ?? deps.resolveCodexDefaultWorkspaceDir(pluginConfig);
1399
- const currentConversation = await ctx.getCurrentConversationBinding();
1400
- const currentConversationData = readCodexConversationBindingData(currentConversation);
1401
- const bindingId = currentConversationData?.kind === "codex-app-server-session" ? currentConversationData.bindingId : currentConversation ? `conversation-${currentConversation.bindingId}` : void 0;
1402
- const sessionOwner = ctx.sessionId ? sessionBindingIdentity({
1403
- sessionId: ctx.sessionId,
1404
- sessionKey: ctx.sessionKey,
1405
- agentId: scope.agentId,
1406
- config: ctx.config
1407
- }) : void 0;
1408
- const currentOwner = currentConversationData?.kind === "codex-app-server-session" ? conversationBindingIdentity(currentConversationData.bindingId) : sessionOwner;
1409
- const existingBinding = currentOwner ? await deps.bindingStore.read(currentOwner) : void 0;
1410
- assertCodexBindingMayBeReplaced(existingBinding, "binding this conversation to another thread");
1411
- const sessionSource = sessionOwner && existingBinding ? {
1412
- agentId: sessionOwner.agentId,
1413
- sessionId: sessionOwner.sessionId,
1414
- threadId: existingBinding.threadId,
1415
- ...sessionOwner.sessionKey ? { sessionKey: sessionOwner.sessionKey } : {}
1416
- } : void 0;
1417
- const authProfileId = existingBinding?.authProfileId;
1418
- const data = createCodexConversationBindingData({
1419
- bindingId,
1420
- workspaceDir,
1421
- agentId: scope.agentId,
1422
- agentDir: scope.agentDir,
1423
- source: currentConversationData?.kind === "codex-app-server-session" ? currentConversationData.source : sessionSource,
1424
- start: {
1425
- id: crypto.randomUUID(),
1426
- threadId: parsed.threadId,
1427
- model: parsed.model,
1428
- modelProvider: parsed.provider,
1429
- authProfileId
1430
- }
1431
- });
1432
- const threadLabel = parsed.threadId ?? "a new thread";
1433
- const request = await ctx.requestConversationBinding({
1434
- summary: `Codex app-server thread ${formatCodexDisplayText(threadLabel)} in ${formatCodexDisplayText(workspaceDir)}`,
1435
- detachHint: "/codex detach",
1436
- data
1355
+ function formatCodexDiagnosticsUploadResult(sent, failed) {
1356
+ const lines = [];
1357
+ if (sent.length > 0) {
1358
+ lines.push("Codex diagnostics sent to OpenAI servers:");
1359
+ lines.push(...formatCodexDiagnosticsTargetLines(sent));
1360
+ lines.push("Included Codex logs and spawned Codex subthreads when available.");
1361
+ }
1362
+ if (failed.length > 0) {
1363
+ if (lines.length > 0) lines.push("");
1364
+ lines.push("Could not send Codex diagnostics:");
1365
+ lines.push(...failed.map(({ target, error }) => `${formatCodexDiagnosticsTargetLine(target)}: ${formatCodexErrorForDisplay(error)}`));
1366
+ lines.push("Inspect locally:");
1367
+ lines.push(...failed.map(({ target }) => `- ${formatCodexResumeCommandForDisplay(target.threadId)}`));
1368
+ }
1369
+ return lines.join("\n");
1370
+ }
1371
+ function formatCodexDiagnosticsTargetLines(targets) {
1372
+ return targets.flatMap((target, index) => {
1373
+ const lines = formatCodexDiagnosticsTargetBlock(target, index);
1374
+ return index < targets.length - 1 ? [...lines, ""] : lines;
1437
1375
  });
1438
- if (request.status === "pending") return request.reply;
1439
- if (request.status === "error") return { text: formatCodexDisplayText(request.message) };
1440
- return { text: `Bound this conversation to ${formatCodexDisplayText(threadLabel)} in ${formatCodexDisplayText(workspaceDir)}. The next message will initialize it.` };
1441
1376
  }
1442
- async function detachConversation(deps, ctx) {
1443
- if (isCurrentSessionModelSelectionLocked(ctx)) return MODEL_SELECTION_LOCKED_MESSAGE;
1444
- const data = readCodexConversationBindingData(await ctx.getCurrentConversationBinding());
1445
- if (data?.kind === "codex-app-server-session") assertCodexBindingMayBeReplaced(await deps.bindingStore.read(conversationBindingIdentity(data.bindingId)), "detaching its conversation binding");
1446
- const detached = await ctx.detachConversationBinding();
1447
- if (data?.kind === "codex-app-server-session") await deps.bindingStore.mutate(conversationBindingIdentity(data.bindingId), { kind: "clear" });
1448
- return detached.removed ? "Detached this conversation from Codex." : "No Codex conversation binding was attached.";
1377
+ function formatCodexDiagnosticsTargetBlock(target, index) {
1378
+ const lines = [`Session ${index + 1}`];
1379
+ if (target.channel) lines.push(`Channel: ${formatCodexValueForDisplay(target.channel)}`);
1380
+ if (target.sessionKey) lines.push(`OpenClaw session key: ${formatCodexCopyableValueForDisplay(target.sessionKey)}`);
1381
+ if (target.sessionId) lines.push(`OpenClaw session id: ${formatCodexCopyableValueForDisplay(target.sessionId)}`);
1382
+ lines.push(`Codex thread id: ${formatCodexCopyableValueForDisplay(target.threadId)}`);
1383
+ lines.push(`Inspect locally: ${formatCodexResumeCommandForDisplay(target.threadId)}`);
1384
+ return lines;
1449
1385
  }
1450
- async function describeConversationBinding(deps, ctx) {
1451
- const current = await ctx.getCurrentConversationBinding();
1452
- const data = readCodexConversationBindingData(current);
1453
- if (!current || !data) return "No Codex conversation binding is attached.";
1454
- if (data.kind === "codex-cli-node-session") return [
1455
- "Codex conversation binding:",
1456
- "- Mode: Codex CLI node session",
1457
- `- Node: ${formatCodexDisplayText(data.nodeId)}`,
1458
- `- Session: ${formatCodexDisplayText(data.sessionId)}`,
1459
- `- Workspace: ${formatCodexDisplayText(data.cwd ?? "unknown")}`,
1460
- "- Active run: not tracked"
1461
- ].join("\n");
1462
- const identity = conversationBindingIdentity(data.bindingId);
1463
- const threadBinding = await deps.bindingStore.read(identity);
1464
- const active = deps.readCodexConversationActiveTurn(identity);
1465
- return [
1466
- "Codex conversation binding:",
1467
- `- Thread: ${formatCodexDisplayText(threadBinding?.threadId ?? "unknown")}`,
1468
- `- Workspace: ${formatCodexDisplayText(data.workspaceDir)}`,
1469
- `- Model: ${formatCodexDisplayText(threadBinding?.model ?? "default")}`,
1470
- `- Fast: ${isCodexFastServiceTier(threadBinding?.serviceTier) ? "on" : "off"}`,
1471
- `- Permissions: ${threadBinding ? formatPermissionsMode(threadBinding) : "default"}`,
1472
- `- Active run: ${formatCodexDisplayText(active ? active.turnId : "none")}`,
1473
- `- Binding: ${formatCodexDisplayText(data.bindingId)}`
1474
- ].join("\n");
1386
+ function formatCodexDiagnosticsTargetLine(target) {
1387
+ const parts = [];
1388
+ if (target.channel) parts.push(`channel ${formatCodexValueForDisplay(target.channel)}`);
1389
+ const sessionLabel = target.sessionId || target.sessionKey;
1390
+ if (sessionLabel) parts.push(`OpenClaw session ${formatCodexValueForDisplay(sessionLabel)}`);
1391
+ parts.push(`Codex thread ${formatCodexThreadIdForDisplay(target.threadId)}`);
1392
+ return `- ${parts.join(", ")}`;
1475
1393
  }
1476
- async function buildThreads(deps, ctx, pluginConfig, filter) {
1477
- const scope = await resolveCommandAppServerScope(deps, ctx, pluginConfig);
1478
- return formatThreads(await deps.codexControlRequest(pluginConfig, CODEX_CONTROL_METHODS.listThreads, {
1479
- limit: 10,
1480
- ...filter.trim() ? { searchTerm: filter.trim() } : {}
1481
- }, {
1482
- config: ctx.config,
1483
- ...scope
1484
- }));
1394
+ function normalizeDiagnosticsReason(note) {
1395
+ const normalized = normalizeOptionalString(note);
1396
+ return normalized ? normalized.slice(0, CODEX_DIAGNOSTICS_REASON_MAX_CHARS) : void 0;
1485
1397
  }
1486
- async function buildCodexCliSessions(deps, args) {
1487
- const parsed = parseCodexCliSessionsArgs(args);
1488
- if (parsed.help || !parsed.host) return "Usage: /codex sessions --host <node> [filter] [--limit <n>]";
1489
- return formatCodexCliSessions(await deps.listCodexCliSessionsOnNode({
1490
- requestedNode: parsed.host,
1491
- filter: parsed.filter,
1492
- limit: parsed.limit
1493
- }));
1398
+ function parseDiagnosticsArgs(args) {
1399
+ const [action, token, ...extra] = splitArgs(args);
1400
+ const normalizedAction = action?.toLowerCase();
1401
+ if ((normalizedAction === "confirm" || normalizedAction === "--confirm") && token && extra.length === 0) return {
1402
+ action: "confirm",
1403
+ token
1404
+ };
1405
+ if ((normalizedAction === "cancel" || normalizedAction === "--cancel") && token && extra.length === 0) return {
1406
+ action: "cancel",
1407
+ token
1408
+ };
1409
+ if (normalizedAction === "confirm" || normalizedAction === "--confirm" || normalizedAction === "cancel" || normalizedAction === "--cancel") return { action: "usage" };
1410
+ return {
1411
+ action: "request",
1412
+ note: args
1413
+ };
1494
1414
  }
1495
- async function resumeThread(deps, ctx, pluginConfig, args) {
1496
- const parsed = parseResumeArgs(args);
1497
- const normalizedThreadId = parsed.threadId?.trim();
1498
- if (parsed.help) return args.includes("--help") || args.includes("-h") || parsed.host ? "Usage: /codex resume <thread-id>\nUsage: /codex resume <session-id> --host <node> --bind here" : "Usage: /codex resume <thread-id>";
1499
- if (parsed.host) return await bindCodexCliNodeSession(deps, ctx, parsed);
1500
- if (!normalizedThreadId || args.length !== 1) return "Usage: /codex resume <thread-id>";
1501
- if (isCurrentSessionModelSelectionLocked(ctx)) return MODEL_SELECTION_LOCKED_MESSAGE;
1502
- if (!ctx.sessionId) return "Cannot attach a Codex thread because this command did not include an OpenClaw session id.";
1503
- const scope = resolveCodexConversationControlScope(ctx);
1504
- const identity = sessionBindingIdentity({
1505
- sessionId: ctx.sessionId,
1506
- sessionKey: ctx.sessionKey,
1507
- agentId: scope.agentId,
1508
- config: ctx.config
1509
- });
1510
- return await deps.bindingStore.withLease(identity, async () => {
1511
- if (!await reclaimCurrentCodexSessionGeneration({
1512
- bindingStore: deps.bindingStore,
1513
- identity,
1514
- config: ctx.config
1515
- })) throw createCodexSessionGenerationSupersededError(identity.sessionId);
1516
- const currentBinding = await deps.bindingStore.read(identity);
1517
- assertCodexBindingMayBeReplaced(currentBinding, "attaching a different resumed thread");
1518
- const authProfileId = resolveCodexAppServerAuthProfileIdForAgent({
1519
- authProfileId: currentBinding?.authProfileId,
1520
- agentDir: scope.agentDir,
1521
- config: ctx.config
1522
- });
1523
- const response = assertCodexThreadResumeResponse(await deps.codexControlRequest(pluginConfig, CODEX_CONTROL_METHODS.resumeThread, {
1524
- threadId: normalizedThreadId,
1525
- excludeTurns: true
1526
- }, {
1527
- config: ctx.config,
1528
- agentDir: scope.agentDir,
1529
- authProfileId,
1530
- sessionKey: ctx.sessionKey,
1531
- sessionId: ctx.sessionId
1532
- }));
1533
- const effectiveThreadId = response.thread.id;
1534
- if (effectiveThreadId !== normalizedThreadId) throw new Error(`Codex thread/resume returned ${effectiveThreadId} for ${normalizedThreadId}`);
1535
- const resumedCwd = response.thread.cwd;
1536
- if (typeof resumedCwd !== "string") throw new Error(`Codex thread/resume returned no cwd for ${normalizedThreadId}`);
1537
- const modelProvider = normalizeCodexAppServerBindingModelProvider({
1538
- authProfileId,
1539
- modelProvider: response.modelProvider ?? void 0,
1540
- agentDir: scope.agentDir,
1541
- config: ctx.config
1542
- });
1543
- assertCodexBindingMayBeReplaced(await deps.bindingStore.read(identity), "committing a different resumed thread");
1544
- if (!await deps.bindingStore.mutate(identity, {
1545
- kind: "set",
1546
- binding: {
1547
- threadId: effectiveThreadId,
1548
- cwd: resumedCwd,
1549
- authProfileId,
1550
- model: response.model,
1551
- modelProvider,
1552
- historyCoveredThrough: (/* @__PURE__ */ new Date()).toISOString()
1553
- }
1554
- })) throw new Error("Codex thread binding changed while attaching the resumed thread.");
1555
- return `Attached this OpenClaw session to Codex thread ${formatCodexDisplayText(effectiveThreadId)}.`;
1556
- });
1415
+ function formatDiagnosticsUsage(commandPrefix) {
1416
+ return [
1417
+ `Usage: ${commandPrefix} [note]`,
1418
+ `Usage: ${commandPrefix} confirm <token>`,
1419
+ `Usage: ${commandPrefix} cancel <token>`
1420
+ ].join("\n");
1557
1421
  }
1558
- async function bindCodexCliNodeSession(deps, ctx, parsed) {
1559
- if (!parsed.threadId || !parsed.host || parsed.bindHere !== true) return "Usage: /codex resume <session-id> --host <node> --bind here";
1560
- if (isCurrentSessionModelSelectionLocked(ctx)) return MODEL_SELECTION_LOCKED_MESSAGE;
1561
- if (ctx.sessionId) {
1562
- const scope = resolveCodexConversationControlScope(ctx);
1563
- assertCodexBindingMayBeReplaced(await deps.bindingStore.read(sessionBindingIdentity({
1564
- sessionId: ctx.sessionId,
1565
- sessionKey: ctx.sessionKey,
1566
- agentId: scope.agentId,
1567
- config: ctx.config
1568
- })), "binding a Codex CLI node session");
1422
+ function createCodexDiagnosticsConfirmation(params) {
1423
+ prunePendingCodexDiagnosticsConfirmations(params.now);
1424
+ if (!pendingCodexDiagnosticsConfirmationTokensByScope.has(params.scopeKey) && pendingCodexDiagnosticsConfirmationTokensByScope.size >= CODEX_DIAGNOSTICS_CONFIRMATION_MAX_SCOPES) {
1425
+ const oldestScopeKey = pendingCodexDiagnosticsConfirmationTokensByScope.keys().next().value;
1426
+ if (typeof oldestScopeKey === "string") deletePendingCodexDiagnosticsConfirmationScope(oldestScopeKey);
1569
1427
  }
1570
- const resolved = await deps.resolveCodexCliSessionForBindingOnNode({
1571
- requestedNode: parsed.host,
1572
- sessionId: parsed.threadId
1573
- });
1574
- if (!resolved.session) return `No Codex CLI session ${formatCodexDisplayText(parsed.threadId)} was found on ${formatCodexDisplayText(parsed.host)}.`;
1575
- const nodeId = resolved.node.nodeId;
1576
- if (!nodeId) return "Cannot bind Codex CLI session because the selected node did not include a node id.";
1577
- const scope = resolveCodexConversationControlScope(ctx);
1578
- const data = createCodexCliNodeConversationBindingData({
1579
- nodeId,
1580
- sessionId: parsed.threadId,
1581
- agentId: scope.agentId,
1582
- cwd: resolved.session?.cwd
1583
- });
1584
- const summary = `Codex CLI session ${formatCodexDisplayText(parsed.threadId)} on ${formatCodexDisplayText(nodeId)}`;
1585
- const request = await ctx.requestConversationBinding({
1586
- summary,
1587
- detachHint: "/codex detach",
1588
- data
1428
+ const scopeTokens = pendingCodexDiagnosticsConfirmationTokensByScope.get(params.scopeKey) ?? [];
1429
+ while (scopeTokens.length >= CODEX_DIAGNOSTICS_CONFIRMATION_MAX_REQUESTS_PER_SCOPE) {
1430
+ const oldestToken = scopeTokens.shift();
1431
+ if (!oldestToken) break;
1432
+ pendingCodexDiagnosticsConfirmations.delete(oldestToken);
1433
+ }
1434
+ const token = crypto.randomBytes(6).toString("hex");
1435
+ scopeTokens.push(token);
1436
+ pendingCodexDiagnosticsConfirmationTokensByScope.set(params.scopeKey, scopeTokens);
1437
+ pendingCodexDiagnosticsConfirmations.set(token, {
1438
+ token,
1439
+ targets: params.targets,
1440
+ note: params.note,
1441
+ senderId: params.senderId,
1442
+ channel: params.channel,
1443
+ accountId: params.accountId,
1444
+ channelId: params.channelId,
1445
+ messageThreadId: params.messageThreadId,
1446
+ threadParentId: params.threadParentId,
1447
+ sessionKey: params.sessionKey,
1448
+ scopeKey: params.scopeKey,
1449
+ ...params.privateRouted === void 0 ? {} : { privateRouted: params.privateRouted },
1450
+ createdAt: params.now
1589
1451
  });
1590
- if (request.status === "bound") return `Bound this conversation to Codex CLI session ${formatCodexDisplayText(parsed.threadId)} on ${formatCodexDisplayText(nodeId)}.`;
1591
- if (request.status === "pending") return request.reply.text ?? "Codex CLI session binding is pending approval.";
1592
- return formatCodexDisplayText(request.message);
1452
+ return token;
1593
1453
  }
1594
- //#endregion
1595
- //#region extensions/codex/src/command-handler-actions.ts
1596
- const CODEX_NATIVE_EXECUTION_SUBCOMMANDS = /* @__PURE__ */ new Set([
1597
- "bind",
1598
- "resume",
1599
- "steer",
1600
- "model",
1601
- "fast",
1602
- "permissions",
1603
- "compact",
1604
- "review",
1605
- "goal"
1606
- ]);
1607
- const CODEX_NATIVE_CONTROL_SUBCOMMANDS = /* @__PURE__ */ new Set([
1608
- ...CODEX_NATIVE_EXECUTION_SUBCOMMANDS,
1609
- "detach",
1610
- "unbind",
1611
- "stop"
1612
- ]);
1613
- function resolveCodexNativeCommandSandboxBlock(ctx, subcommand, args) {
1614
- if (isReadOnlyCodexGoalCommand(subcommand, args)) return;
1615
- if (!CODEX_NATIVE_EXECUTION_SUBCOMMANDS.has(subcommand)) return;
1616
- if (returnsBeforeNativeCodexExecution(subcommand, args)) return;
1617
- if (isCodexCliNodeResumeBind(subcommand, args)) return resolveCodexNativeSandboxBlock({
1618
- config: ctx.config,
1619
- sessionKey: ctx.sessionKey,
1620
- sessionId: ctx.sessionId,
1621
- surface: `/${["codex", subcommand].join(" ")}`
1622
- });
1623
- return resolveCodexNativeExecutionBlock({
1624
- config: ctx.config,
1625
- agentId: ctx.agentId,
1626
- sessionKey: ctx.sessionKey,
1627
- sessionId: ctx.sessionId,
1628
- surface: `/${["codex", subcommand].join(" ")}`
1629
- });
1454
+ function readCodexDiagnosticsConfirmationScope(ctx) {
1455
+ return {
1456
+ accountId: normalizeCodexDiagnosticsScopeField(ctx.accountId),
1457
+ channelId: normalizeCodexDiagnosticsScopeField(ctx.channelId),
1458
+ messageThreadId: typeof ctx.messageThreadId === "string" || typeof ctx.messageThreadId === "number" ? normalizeCodexDiagnosticsScopeField(String(ctx.messageThreadId)) : void 0,
1459
+ threadParentId: normalizeCodexDiagnosticsScopeField(ctx.threadParentId),
1460
+ sessionKey: normalizeCodexDiagnosticsScopeField(ctx.sessionKey)
1461
+ };
1630
1462
  }
1631
- function isReadOnlyCodexGoalCommand(subcommand, args) {
1632
- if (subcommand !== "goal" || args.length > 1) return false;
1633
- const action = (args[0] ?? "status").toLowerCase();
1634
- return action === "status" || action === "get";
1463
+ function readCodexDiagnosticsScopeMismatch(pending, ctx) {
1464
+ const current = readCodexDiagnosticsConfirmationScope(ctx);
1465
+ if (pending.accountId !== current.accountId) return {
1466
+ confirmMessage: "This Codex diagnostics confirmation belongs to a different account.",
1467
+ cancelMessage: "This Codex diagnostics confirmation belongs to a different account."
1468
+ };
1469
+ if (pending.privateRouted) return;
1470
+ if (pending.channelId !== current.channelId) return {
1471
+ confirmMessage: "This Codex diagnostics confirmation belongs to a different channel instance.",
1472
+ cancelMessage: "This Codex diagnostics confirmation belongs to a different channel instance."
1473
+ };
1474
+ if (pending.messageThreadId !== current.messageThreadId) return {
1475
+ confirmMessage: "This Codex diagnostics confirmation belongs to a different thread.",
1476
+ cancelMessage: "This Codex diagnostics confirmation belongs to a different thread."
1477
+ };
1478
+ if (pending.threadParentId !== current.threadParentId) return {
1479
+ confirmMessage: "This Codex diagnostics confirmation belongs to a different parent thread.",
1480
+ cancelMessage: "This Codex diagnostics confirmation belongs to a different parent thread."
1481
+ };
1482
+ if (pending.sessionKey !== current.sessionKey) return {
1483
+ confirmMessage: "This Codex diagnostics confirmation belongs to a different session.",
1484
+ cancelMessage: "This Codex diagnostics confirmation belongs to a different session."
1485
+ };
1635
1486
  }
1636
- function returnsBeforeNativeCodexExecution(subcommand, args) {
1637
- switch (subcommand) {
1638
- case "bind": return parseBindArgs([...args]).help === true;
1639
- case "resume": return returnsBeforeNativeCodexResume(args);
1640
- case "steer": return args.join(" ").trim() === "";
1641
- case "model": return args.length === 0 || args.length > 1;
1642
- case "fast": return args.length === 0 || args.length > 1 || parseCodexFastModeArg(args[0]) === void 0;
1643
- case "permissions": return args.length === 0 || args.length > 1 || parseCodexPermissionsModeArg(args[0]) === void 0;
1644
- case "compact":
1645
- case "review":
1646
- case "detach":
1647
- case "unbind":
1648
- case "stop": return args.length > 0;
1649
- default: return false;
1650
- }
1487
+ function readPendingCodexDiagnosticsConfirmation(token, now) {
1488
+ prunePendingCodexDiagnosticsConfirmations(now);
1489
+ return pendingCodexDiagnosticsConfirmations.get(token);
1651
1490
  }
1652
- function isCodexCliNodeResumeBind(subcommand, args) {
1653
- if (subcommand !== "resume") return false;
1654
- const parsed = parseResumeArgs([...args]);
1655
- return Boolean(parsed.host && parsed.threadId && parsed.bindHere === true && !parsed.help);
1491
+ function prunePendingCodexDiagnosticsConfirmations(now) {
1492
+ for (const [token, pending] of pendingCodexDiagnosticsConfirmations) if (now - pending.createdAt >= CODEX_DIAGNOSTICS_CONFIRMATION_TTL_MS) deletePendingCodexDiagnosticsConfirmation(token);
1656
1493
  }
1657
- function returnsBeforeNativeCodexResume(args) {
1658
- const parsed = parseResumeArgs([...args]);
1659
- const normalizedThreadId = parsed.threadId?.trim();
1660
- if (parsed.help) return true;
1661
- if (parsed.host) return !normalizedThreadId || parsed.bindHere !== true;
1662
- return !normalizedThreadId || args.length !== 1;
1494
+ function deletePendingCodexDiagnosticsConfirmation(token) {
1495
+ const pending = pendingCodexDiagnosticsConfirmations.get(token);
1496
+ pendingCodexDiagnosticsConfirmations.delete(token);
1497
+ if (!pending) return;
1498
+ const scopeTokens = pendingCodexDiagnosticsConfirmationTokensByScope.get(pending.scopeKey);
1499
+ if (!scopeTokens) return;
1500
+ const tokenIndex = scopeTokens.indexOf(token);
1501
+ if (tokenIndex >= 0) scopeTokens.splice(tokenIndex, 1);
1502
+ if (scopeTokens.length === 0) pendingCodexDiagnosticsConfirmationTokensByScope.delete(pending.scopeKey);
1663
1503
  }
1664
- async function handleComputerUseCommand(deps, ctx, pluginConfig, args) {
1665
- const parsed = parseComputerUseArgs(args);
1666
- if (parsed.help) return ["Usage: /codex computer-use [status|install] [--source <marketplace-source>] [--marketplace-path <path>] [--marketplace <name>]", "Checks or installs the configured Codex Computer Use plugin through app-server."].join("\n");
1667
- if (Object.keys(parsed.persistentIdentity).length > 0) return formatComputerUsePersistentIdentityMigration(parsed);
1668
- if (parsed.action === "install" && !canMutateCodexHost(ctx)) return "Only an owner or operator.admin gateway client can configure Codex Computer Use.";
1669
- const { agentDir } = resolveCodexConversationControlScope(ctx);
1670
- const params = {
1671
- pluginConfig,
1672
- config: ctx.config,
1673
- agentDir,
1674
- forceEnable: parsed.action === "install" || parsed.hasOverrides,
1675
- ...Object.keys(parsed.overrides).length > 0 ? { overrides: parsed.overrides } : {}
1676
- };
1677
- if (parsed.action === "install") return formatComputerUseStatus(await deps.installCodexComputerUse(params));
1678
- return formatComputerUseStatus(await deps.readCodexComputerUseStatus(params));
1504
+ function deletePendingCodexDiagnosticsConfirmationScope(scopeKey) {
1505
+ const scopeTokens = pendingCodexDiagnosticsConfirmationTokensByScope.get(scopeKey) ?? [];
1506
+ for (const token of scopeTokens) pendingCodexDiagnosticsConfirmations.delete(token);
1507
+ pendingCodexDiagnosticsConfirmationTokensByScope.delete(scopeKey);
1679
1508
  }
1680
- async function handleNativeGoal(deps, ctx, pluginConfig, args) {
1681
- const action = (args[0] ?? "status").toLowerCase();
1682
- const objective = args.slice(1).join(" ").trim();
1683
- const target = await resolveControlTarget(ctx);
1684
- if (!target) return "Cannot manage the Codex goal because this command has no stable binding identity.";
1685
- const binding = await deps.bindingStore.read(target.identity);
1686
- if (!binding?.threadId) return "No Codex thread is attached to this OpenClaw session yet.";
1687
- const connection = resolveCodexBindingAppServerConnection({
1688
- binding,
1689
- authProfileId: binding.authProfileId,
1690
- pluginConfig
1691
- });
1692
- const goalRequestOptions = {
1693
- agentDir: target.agentDir,
1694
- authProfileId: connection.clientAuthProfileId,
1695
- config: ctx.config,
1696
- ...connection.usesSupervisionConnection ? { startOptions: connection.appServer.start } : {}
1697
- };
1698
- if (action === "status" || action === "get") {
1699
- if (args.length > 1) return "Usage: /codex goal [status]";
1700
- return formatNativeGoal(await deps.codexControlRequest(pluginConfig, CODEX_CONTROL_METHODS.getThreadGoal, { threadId: binding.threadId }, goalRequestOptions));
1509
+ function buildDiagnosticsTags(ctx) {
1510
+ const tags = { source: CODEX_DIAGNOSTICS_SOURCE };
1511
+ addTag(tags, "channel", ctx.channel);
1512
+ return tags;
1513
+ }
1514
+ function addTag(tags, key, value) {
1515
+ if (typeof value === "string" && value.trim()) tags[key] = value.trim();
1516
+ }
1517
+ function formatCodexThreadIdForDisplay(threadId) {
1518
+ return escapeCodexChatText(formatCodexTextForDisplay(threadId));
1519
+ }
1520
+ function formatCodexValueForDisplay(value) {
1521
+ return escapeCodexChatText(formatCodexTextForDisplay(value));
1522
+ }
1523
+ function formatCodexCopyableValueForDisplay(value) {
1524
+ const safe = formatCodexTextForDisplay(value);
1525
+ if (CODEX_RESUME_SAFE_THREAD_ID_PATTERN.test(safe)) return `\`${safe}\``;
1526
+ return escapeCodexChatText(safe);
1527
+ }
1528
+ function formatCodexTextForDisplay(value) {
1529
+ let safe = "";
1530
+ for (const character of value) {
1531
+ const codePoint = character.codePointAt(0);
1532
+ safe += codePoint != null && isUnsafeDisplayCodePoint(codePoint) ? "?" : character;
1701
1533
  }
1702
- if (action === "clear") {
1703
- if (args.length > 1) return "Usage: /codex goal clear";
1704
- const response = await deps.codexControlRequest(pluginConfig, CODEX_CONTROL_METHODS.clearThreadGoal, { threadId: binding.threadId }, goalRequestOptions);
1705
- return isJsonObject(response) && response.cleared === true ? "Cleared the Codex goal." : "No Codex goal was active.";
1534
+ safe = safe.trim();
1535
+ return safe || "<unknown>";
1536
+ }
1537
+ function escapeCodexChatText(value) {
1538
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("@", "@").replaceAll("`", "`").replaceAll("[", "[").replaceAll("]", "]").replaceAll("(", "(").replaceAll(")", ")").replaceAll("*", "∗").replaceAll("_", "_").replaceAll("~", "~").replaceAll("|", "|");
1539
+ }
1540
+ function readCodexDiagnosticsCooldownMs(threadId, now) {
1541
+ const lastSentAt = lastCodexDiagnosticsUploadByThread.get(threadId);
1542
+ if (!lastSentAt) return 0;
1543
+ const remainingMs = Math.max(0, CODEX_DIAGNOSTICS_COOLDOWN_MS - (now - lastSentAt));
1544
+ if (remainingMs === 0) lastCodexDiagnosticsUploadByThread.delete(threadId);
1545
+ return remainingMs;
1546
+ }
1547
+ function readCodexDiagnosticsTargetsCooldownMessage(targets, ctx, now, options = {}) {
1548
+ for (const target of targets) {
1549
+ const cooldownMs = readCodexDiagnosticsCooldownMs(target.threadId, now);
1550
+ if (cooldownMs > 0) {
1551
+ if (options.includeThreadId === false) return `Codex diagnostics were already sent for one of these Codex threads recently. Try again in ${Math.ceil(cooldownMs / 1e3)}s.`;
1552
+ return `Codex diagnostics were already sent for thread ${formatCodexThreadIdForDisplay(target.threadId)} recently. Try again in ${Math.ceil(cooldownMs / 1e3)}s.`;
1553
+ }
1706
1554
  }
1707
- const requestedStatus = action === "pause" ? "paused" : action === "resume" ? "active" : action === "block" ? "blocked" : action === "complete" ? "complete" : void 0;
1708
- const isObjectiveUpdate = action === "set";
1709
- if (!requestedStatus && !isObjectiveUpdate || isObjectiveUpdate && !objective) return "Usage: /codex goal [status|set <objective>|pause|resume|block|complete|clear]";
1710
- if (requestedStatus && args.length > 1) return `Usage: /codex goal ${action}`;
1711
- return formatNativeGoal(await deps.codexControlRequest(pluginConfig, CODEX_CONTROL_METHODS.setThreadGoal, {
1712
- threadId: binding.threadId,
1713
- ...objective ? { objective } : {},
1714
- ...requestedStatus ? { status: requestedStatus } : {}
1715
- }, goalRequestOptions));
1716
- }
1717
- function formatNativeGoal(response) {
1718
- const goal = isJsonObject(response) && isJsonObject(response.goal) ? response.goal : void 0;
1719
- if (!goal) return "No Codex goal is active.";
1720
- const objective = readString$1(goal, "objective") ?? "unknown";
1721
- const status = readString$1(goal, "status") ?? "unknown";
1722
- const tokensUsed = typeof goal.tokensUsed === "number" ? goal.tokensUsed : 0;
1723
- const tokenBudget = typeof goal.tokenBudget === "number" ? goal.tokenBudget : void 0;
1724
- return [
1725
- `Codex goal: ${formatCodexDisplayText(objective)}`,
1726
- `- Status: ${formatCodexDisplayText(status)}`,
1727
- `- Tokens: ${tokensUsed}${tokenBudget === void 0 ? "" : ` / ${tokenBudget}`}`
1728
- ].join("\n");
1555
+ const scopeCooldownMs = readCodexDiagnosticsScopeCooldownMs(options.cooldownScope ?? readCodexDiagnosticsCooldownScope(ctx), now);
1556
+ if (scopeCooldownMs > 0) return `Codex diagnostics were already sent for this account or channel recently. Try again in ${Math.ceil(scopeCooldownMs / 1e3)}s.`;
1729
1557
  }
1730
- async function stopConversationTurn(deps, ctx, pluginConfig) {
1731
- const target = await resolveControlTarget(ctx);
1732
- if (!target) return "Cannot stop Codex because this command did not include a stable binding identity.";
1733
- return (await deps.stopCodexConversationTurn({
1734
- identity: target.identity,
1735
- bindingStore: deps.bindingStore,
1736
- pluginConfig,
1737
- agentDir: target.agentDir,
1738
- config: ctx.config
1739
- })).message;
1558
+ function readCodexDiagnosticsScopeCooldownMs(scope, now) {
1559
+ const lastSentAt = lastCodexDiagnosticsUploadByScope.get(scope);
1560
+ if (!lastSentAt) return 0;
1561
+ const remainingMs = Math.max(0, CODEX_DIAGNOSTICS_COOLDOWN_MS - (now - lastSentAt));
1562
+ if (remainingMs === 0) lastCodexDiagnosticsUploadByScope.delete(scope);
1563
+ return remainingMs;
1740
1564
  }
1741
- async function steerConversationTurn(deps, ctx, pluginConfig, message) {
1742
- const target = await resolveControlTarget(ctx);
1743
- if (!target) return "Cannot steer Codex because this command did not include a stable binding identity.";
1744
- return (await deps.steerCodexConversationTurn({
1745
- identity: target.identity,
1746
- bindingStore: deps.bindingStore,
1747
- message,
1748
- pluginConfig,
1749
- agentDir: target.agentDir,
1750
- config: ctx.config
1751
- })).message;
1565
+ function recordCodexDiagnosticsUpload(threadId, ctx, now, cooldownScope) {
1566
+ pruneCodexDiagnosticsCooldowns(now);
1567
+ recordBoundedCodexDiagnosticsCooldown(lastCodexDiagnosticsUploadByScope, cooldownScope ?? readCodexDiagnosticsCooldownScope(ctx), CODEX_DIAGNOSTICS_COOLDOWN_MAX_SCOPES, now);
1568
+ recordBoundedCodexDiagnosticsCooldown(lastCodexDiagnosticsUploadByThread, threadId, CODEX_DIAGNOSTICS_COOLDOWN_MAX_THREADS, now);
1752
1569
  }
1753
- async function setConversationModel(deps, ctx, pluginConfig, args) {
1754
- if (args.length > 1) return "Usage: /codex model <model>";
1755
- const [model = ""] = args;
1756
- const normalized = model.trim();
1757
- if (normalized && isCurrentSessionModelSelectionLocked(ctx)) return MODEL_SELECTION_LOCKED_MESSAGE;
1758
- const target = await resolveControlTarget(ctx);
1759
- if (!target) return "Cannot set Codex model because this command did not include a stable binding identity.";
1760
- if (!normalized) {
1761
- const binding = await deps.bindingStore.read(target.identity);
1762
- return binding?.model ? `Codex model: ${formatCodexDisplayText(binding.model)}` : "Usage: /codex model <model>";
1570
+ function recordBoundedCodexDiagnosticsCooldown(map, key, maxSize, now) {
1571
+ if (!map.has(key)) while (map.size >= maxSize) {
1572
+ const oldestKey = map.keys().next().value;
1573
+ if (typeof oldestKey !== "string") break;
1574
+ map.delete(oldestKey);
1763
1575
  }
1764
- return await deps.setCodexConversationModel({
1765
- identity: target.identity,
1766
- bindingStore: deps.bindingStore,
1767
- pluginConfig,
1768
- model: normalized,
1769
- agentDir: target.agentDir,
1770
- config: ctx.config
1771
- });
1576
+ map.set(key, now);
1772
1577
  }
1773
- async function setConversationFastMode(deps, ctx, args) {
1774
- if (args.length > 1) return "Usage: /codex fast [on|off|status]";
1775
- const target = await resolveControlTarget(ctx);
1776
- if (!target) return "Cannot set Codex fast mode because this command did not include a stable binding identity.";
1777
- const value = args[0];
1778
- const parsed = parseCodexFastModeArg(value);
1779
- if (value && parsed == null && value.trim().toLowerCase() !== "status") return "Usage: /codex fast [on|off|status]";
1780
- return await deps.setCodexConversationFastMode({
1781
- identity: target.identity,
1782
- bindingStore: deps.bindingStore,
1783
- enabled: parsed
1578
+ function readCodexDiagnosticsCooldownScope(ctx) {
1579
+ const scope = readCodexDiagnosticsConfirmationScope(ctx);
1580
+ const payload = JSON.stringify({
1581
+ accountId: scope.accountId ?? null,
1582
+ channelId: scope.channelId ?? null,
1583
+ sessionKey: scope.sessionKey ?? null,
1584
+ messageThreadId: scope.messageThreadId ?? null,
1585
+ threadParentId: scope.threadParentId ?? null,
1586
+ senderId: normalizeCodexDiagnosticsScopeField(ctx.senderId) ?? null,
1587
+ channel: normalizeCodexDiagnosticsScopeField(ctx.channel) ?? ""
1784
1588
  });
1589
+ return crypto.createHash("sha256").update(payload).digest("hex");
1785
1590
  }
1786
- async function setConversationPermissions(deps, ctx, args) {
1787
- if (args.length > 1) return "Usage: /codex permissions [default|yolo|status]";
1788
- const target = await resolveControlTarget(ctx);
1789
- if (!target) return "Cannot set Codex permissions because this command did not include a stable binding identity.";
1790
- const value = args[0];
1791
- const parsed = parseCodexPermissionsModeArg(value);
1792
- if (value && !parsed && value.trim().toLowerCase() !== "status") return "Usage: /codex permissions [default|yolo|status]";
1793
- return await deps.setCodexConversationPermissions({
1794
- identity: target.identity,
1795
- bindingStore: deps.bindingStore,
1796
- mode: parsed
1797
- });
1591
+ function pruneCodexDiagnosticsCooldowns(now) {
1592
+ pruneCodexDiagnosticsCooldownMap(lastCodexDiagnosticsUploadByThread, now);
1593
+ pruneCodexDiagnosticsCooldownMap(lastCodexDiagnosticsUploadByScope, now);
1594
+ }
1595
+ function pruneCodexDiagnosticsCooldownMap(map, now) {
1596
+ for (const [key, lastSentAt] of map) if (now - lastSentAt >= CODEX_DIAGNOSTICS_COOLDOWN_MS) map.delete(key);
1597
+ }
1598
+ function formatCodexErrorForDisplay(error) {
1599
+ return escapeCodexChatText(formatCodexTextForDisplay(error).slice(0, CODEX_DIAGNOSTICS_ERROR_MAX_CHARS)) || "unknown error";
1600
+ }
1601
+ function formatCodexResumeCommandForDisplay(threadId) {
1602
+ const safeThreadId = formatCodexTextForDisplay(threadId);
1603
+ if (!CODEX_RESUME_SAFE_THREAD_ID_PATTERN.test(safeThreadId)) return "run codex resume and paste the thread id shown above";
1604
+ return `\`codex resume ${safeThreadId}\``;
1605
+ }
1606
+ function isUnsafeDisplayCodePoint(codePoint) {
1607
+ return codePoint <= 31 || codePoint >= 127 && codePoint <= 159 || codePoint === 173 || codePoint === 1564 || codePoint === 6158 || codePoint >= 8203 && codePoint <= 8207 || codePoint >= 8234 && codePoint <= 8238 || codePoint >= 8288 && codePoint <= 8303 || codePoint === 65279 || codePoint >= 65529 && codePoint <= 65531 || codePoint >= 917504 && codePoint <= 917631;
1608
+ }
1609
+ function normalizeCodexDiagnosticsScopeField(value) {
1610
+ const normalized = normalizeOptionalString(value);
1611
+ if (!normalized) return;
1612
+ if (normalized.length <= CODEX_DIAGNOSTICS_SCOPE_FIELD_MAX_CHARS) return normalized;
1613
+ return `sha256:${crypto.createHash("sha256").update(normalized).digest("hex")}`;
1798
1614
  }
1799
1615
  async function startThreadAction(deps, ctx, pluginConfig, kind, args) {
1800
1616
  const label = kind === "compact" ? "compaction" : "review";
@@ -1803,264 +1619,285 @@ async function startThreadAction(deps, ctx, pluginConfig, kind, args) {
1803
1619
  if (!target) return `Cannot start Codex ${label} because this command did not include a stable binding identity.`;
1804
1620
  const binding = await deps.bindingStore.read(target.identity);
1805
1621
  if (!binding?.threadId) return `No Codex thread is attached to this OpenClaw session yet.`;
1806
- const connection = resolveCodexBindingAppServerConnection({
1807
- binding,
1808
- authProfileId: binding.authProfileId,
1809
- pluginConfig
1810
- });
1811
1622
  await deps.codexControlRequest(pluginConfig, kind === "compact" ? CODEX_CONTROL_METHODS.compact : CODEX_CONTROL_METHODS.review, kind === "review" ? {
1812
1623
  threadId: binding.threadId,
1813
1624
  target: { type: "uncommittedChanges" }
1814
1625
  } : { threadId: binding.threadId }, {
1815
1626
  agentDir: target.agentDir,
1816
- authProfileId: connection.clientAuthProfileId,
1817
- config: ctx.config,
1818
- ...connection.usesSupervisionConnection ? { startOptions: connection.appServer.start } : {}
1627
+ authProfileId: binding.authProfileId,
1628
+ config: ctx.config
1819
1629
  });
1820
1630
  return `Started Codex ${label} for thread ${formatCodexDisplayText(binding.threadId)}.`;
1821
1631
  }
1822
- //#endregion
1823
- //#region extensions/codex/src/command-plugins-management.ts
1824
- const POLICY_REFRESH_HINT = "New Codex conversations pick this up automatically. Use /new or /reset to refresh the current one.";
1825
- async function handleCodexPluginsSubcommand(ctx, rest, io) {
1826
- const [verb = "list", ...args] = rest;
1827
- const normalized = verb.toLowerCase();
1828
- if (normalized === "menu") {
1829
- if (args.length > 0) return { text: "Usage: /codex plugins menu" };
1830
- return buildPluginsMenuReply();
1831
- }
1832
- if (normalized === "help") {
1833
- if (args.length > 0) return { text: "Usage: /codex plugins help" };
1834
- return { text: buildPluginsHelp() };
1835
- }
1836
- if (normalized === "list") {
1837
- if (args.length > 0) return { text: "Usage: /codex plugins list" };
1838
- const current = await io.readConfig();
1839
- return { text: formatPluginList(current.plugins ?? {}, { globalEnabled: current.enabled === true }) };
1840
- }
1841
- const target = args[0];
1842
- if (normalized === "enable" || normalized === "disable") {
1843
- if (args.length === 0) return buildPluginNamePickerReply(normalized, await io.readConfig());
1844
- if (!target || args.length > 1) return { text: `Usage: /codex plugins ${normalized} <name>` };
1845
- if (!canMutateCodexHost(ctx)) return { text: `Only an owner or operator.admin gateway client can run /codex plugins ${normalized}.` };
1846
- const wantEnabled = normalized === "enable";
1847
- if (!((await io.readConfig()).plugins ?? {})[target]) return { text: `Codex sub-plugin '${formatCodexDisplayText(target)}' is not configured. Run '/codex plugins list' to see configured plugins.` };
1848
- await io.mutate((block) => {
1849
- if (wantEnabled) block.enabled = true;
1850
- block.plugins ??= {};
1851
- block.plugins[target] = {
1852
- ...block.plugins[target],
1853
- enabled: wantEnabled
1854
- };
1855
- });
1856
- return { text: `${formatCodexDisplayText(target)}: ${wantEnabled ? "enabled" : "disabled"} in openclaw.json. ${POLICY_REFRESH_HINT}` };
1857
- }
1858
- return { text: `Unknown /codex plugins subcommand: ${formatCodexDisplayText(verb)}\n\n${buildPluginsHelp()}` };
1859
- }
1860
- function buildPluginsMenuReply() {
1861
- return {
1862
- text: [
1863
- "Codex sub-plugins. Pick a sub-action or type:",
1864
- "",
1865
- " 1. /codex plugins list",
1866
- " 2. /codex plugins enable",
1867
- " 3. /codex plugins disable",
1868
- " 4. /codex plugins help",
1869
- "",
1870
- "Type '/codex' to go back to the main menu."
1871
- ].join("\n"),
1872
- presentation: buildCodexCommandPickerPresentation("Codex sub-plugins", "Pick a Codex sub-plugin action:", [
1873
- {
1874
- label: "list",
1875
- command: "/codex plugins list"
1876
- },
1877
- {
1878
- label: "enable",
1879
- command: "/codex plugins enable"
1880
- },
1881
- {
1882
- label: "disable",
1883
- command: "/codex plugins disable"
1884
- },
1885
- {
1886
- label: "help",
1887
- command: "/codex plugins help"
1888
- },
1889
- {
1890
- label: "back",
1891
- command: "/codex"
1632
+ function splitArgs(value) {
1633
+ const input = value ?? "";
1634
+ const args = [];
1635
+ let current = "";
1636
+ let quote;
1637
+ let escaping = false;
1638
+ let tokenStarted = false;
1639
+ for (const char of input) {
1640
+ if (escaping) {
1641
+ current += char;
1642
+ escaping = false;
1643
+ tokenStarted = true;
1644
+ continue;
1645
+ }
1646
+ if (char === "\\" && quote !== "'") {
1647
+ escaping = true;
1648
+ tokenStarted = true;
1649
+ continue;
1650
+ }
1651
+ if (quote) {
1652
+ if (char === quote) quote = void 0;
1653
+ else current += char;
1654
+ tokenStarted = true;
1655
+ continue;
1656
+ }
1657
+ if (char === "\"" || char === "'") {
1658
+ quote = char;
1659
+ tokenStarted = true;
1660
+ continue;
1661
+ }
1662
+ if (/\s/.test(char)) {
1663
+ if (tokenStarted) {
1664
+ args.push(current);
1665
+ current = "";
1666
+ tokenStarted = false;
1892
1667
  }
1893
- ])
1894
- };
1895
- }
1896
- function buildPluginNamePickerReply(verb, current) {
1897
- const globalEnabled = current.enabled === true;
1898
- const eligible = Object.entries(current.plugins ?? {}).toSorted(([left], [right]) => left.localeCompare(right)).filter(([, entry]) => {
1899
- const effectivelyEnabled = globalEnabled && entry.enabled !== false;
1900
- return verb === "disable" ? effectivelyEnabled : !effectivelyEnabled;
1901
- });
1902
- if (eligible.length === 0) return {
1903
- text: [
1904
- `No configured ${verb === "enable" ? "disabled" : "enabled"} Codex sub-plugins found.`,
1905
- "",
1906
- "Type '/codex plugins list' to inspect configured sub-plugins.",
1907
- "Type '/codex plugins menu' to go back to the plugins menu."
1908
- ].join("\n"),
1909
- presentation: buildCodexCommandPickerPresentation("Codex sub-plugins", "Pick another Codex sub-plugin action:", [{
1910
- label: "list",
1911
- command: "/codex plugins list"
1912
- }, {
1913
- label: "back",
1914
- command: "/codex plugins menu"
1915
- }])
1916
- };
1917
- const buttons = [...eligible.map(([key]) => ({
1918
- label: formatCodexDisplayText(key),
1919
- command: `/codex plugins ${verb} ${key}`
1920
- })), {
1921
- label: "back",
1922
- command: "/codex plugins menu"
1923
- }];
1924
- return {
1925
- text: [
1926
- `Codex sub-plugins to ${verb}. Pick one or type:`,
1927
- "",
1928
- ...eligible.map(([key], index) => ` ${index + 1}. /codex plugins ${verb} ${key}`),
1929
- "",
1930
- ...verb === "enable" && !globalEnabled ? ["Global codexPlugins.enabled is off; enabling one configured sub-plugin turns it on.", ""] : [],
1931
- "Type '/codex plugins menu' to go back to the plugins menu."
1932
- ].join("\n"),
1933
- presentation: buildCodexCommandPickerPresentation("Codex sub-plugins", `Pick a Codex sub-plugin to ${verb}:`, buttons)
1934
- };
1935
- }
1936
- function buildPluginsHelp() {
1937
- return [
1938
- "Codex sub-plugin management (writes only to ~/.openclaw/openclaw.json, never to ~/.codex/config.toml):",
1939
- "- /codex plugins (alias for list)",
1940
- "- /codex plugins list show all configured Codex sub-plugins",
1941
- "- /codex plugins enable <name> enable a configured sub-plugin",
1942
- "- /codex plugins disable <name> disable a configured sub-plugin"
1943
- ].join("\n");
1944
- }
1945
- function formatPluginList(plugins, options = {}) {
1946
- const globalEnabled = options.globalEnabled === true;
1947
- const keys = Object.keys(plugins).toSorted();
1948
- if (keys.length === 0) return "No Codex sub-plugins configured under plugins.entries.codex.config.codexPlugins.plugins";
1949
- const rows = keys.map((key) => {
1950
- const entry = plugins[key] ?? {};
1951
- const state = globalEnabled && entry.enabled !== false ? "ON " : "OFF";
1952
- return {
1953
- displayKey: formatCodexDisplayText(key),
1954
- state,
1955
- pluginName: formatCodexDisplayText(entry.pluginName ?? key),
1956
- marketplace: formatCodexDisplayText(entry.marketplaceName ?? "?")
1957
- };
1958
- });
1959
- const keyW = Math.max(...rows.map((r) => r.displayKey.length));
1960
- const pluginW = Math.max(...rows.map((r) => r.pluginName.length));
1961
- return [
1962
- "Codex sub-plugins in Openclaw config (~/.openclaw/openclaw.json):",
1963
- "",
1964
- ...rows.map((r) => ` ${r.state} ${r.displayKey.padEnd(keyW)} ${r.pluginName.padEnd(pluginW)} [${r.marketplace}]`),
1965
- "",
1966
- ...globalEnabled ? [] : ["Global codexPlugins.enabled is off; configured sub-plugins are inactive.", ""],
1967
- "New Codex conversations pick up policy changes automatically; /new or /reset to refresh the current one."
1968
- ].join("\n");
1969
- }
1970
- //#endregion
1971
- //#region extensions/codex/src/command-handlers.ts
1972
- async function handleCodexSubcommand(ctx, options) {
1973
- const deps = resolveCodexCommandDeps(options.deps);
1974
- const args = splitArgs(ctx.args);
1975
- if (args.length === 0) return buildCodexSubcommandPickerReply();
1976
- const [subcommand = "status", ...rest] = args;
1977
- const normalized = subcommand.toLowerCase();
1978
- if (normalized === "help") return { text: buildHelp() };
1979
- if (CODEX_NATIVE_CONTROL_SUBCOMMANDS.has(normalized) && !returnsBeforeNativeCodexExecution(normalized, rest) && !isReadOnlyCodexGoalCommand(normalized, rest) && !canMutateCodexHost(ctx)) return { text: CODEX_NATIVE_EXECUTION_AUTH_ERROR };
1980
- const sandboxBlock = resolveCodexNativeCommandSandboxBlock(ctx, normalized, rest);
1981
- if (sandboxBlock) return { text: sandboxBlock };
1982
- if (normalized === "plugins") {
1983
- if (!deps.codexPluginsManagementIo) return { text: "Codex sub-plugin management is not wired up (codexPluginsManagementIo dep is undefined). Edit ~/.openclaw/openclaw.json or use `openclaw config patch` until the runtime exposes the IO." };
1984
- return await handleCodexPluginsSubcommand(ctx, rest, deps.codexPluginsManagementIo);
1985
- }
1986
- if (normalized === "status") {
1987
- if (rest.length > 0) return { text: "Usage: /codex status" };
1988
- const { agentDir } = resolveCodexConversationControlScope(ctx);
1989
- return { text: formatCodexStatus(await deps.readCodexStatusProbes(options.pluginConfig, ctx.config, agentDir)) };
1990
- }
1991
- if (normalized === "models") {
1992
- if (rest.length > 0) return { text: "Usage: /codex models" };
1993
- const { agentDir } = resolveCodexConversationControlScope(ctx);
1994
- return { text: formatModels(await deps.listCodexAppServerModels(deps.requestOptions(options.pluginConfig, 100, ctx.config, agentDir))) };
1995
- }
1996
- if (normalized === "threads") return { text: await buildThreads(deps, ctx, options.pluginConfig, rest.join(" ")) };
1997
- if (normalized === "goal") return { text: await handleNativeGoal(deps, ctx, options.pluginConfig, rest) };
1998
- if (normalized === "sessions") return { text: await buildCodexCliSessions(deps, rest) };
1999
- if (normalized === "resume") return { text: await resumeThread(deps, ctx, options.pluginConfig, rest) };
2000
- if (normalized === "bind") return await bindConversation(deps, ctx, options.pluginConfig, rest);
2001
- if (normalized === "detach" || normalized === "unbind") {
2002
- if (rest.length > 0) return { text: "Usage: /codex detach" };
2003
- return { text: await detachConversation(deps, ctx) };
2004
- }
2005
- if (normalized === "binding") {
2006
- if (rest.length > 0) return { text: "Usage: /codex binding" };
2007
- return { text: await describeConversationBinding(deps, ctx) };
2008
- }
2009
- if (normalized === "stop") {
2010
- if (rest.length > 0) return { text: "Usage: /codex stop" };
2011
- return { text: await stopConversationTurn(deps, ctx, options.pluginConfig) };
2012
- }
2013
- if (normalized === "steer") return { text: await steerConversationTurn(deps, ctx, options.pluginConfig, rest.join(" ")) };
2014
- if (normalized === "model") return { text: await setConversationModel(deps, ctx, options.pluginConfig, rest) };
2015
- if (normalized === "fast") {
2016
- if (isMenuVerb(rest)) return buildCodexFastMenuReply();
2017
- return { text: await setConversationFastMode(deps, ctx, rest) };
2018
- }
2019
- if (normalized === "permissions") {
2020
- if (isMenuVerb(rest)) return buildCodexPermissionsMenuReply();
2021
- return { text: await setConversationPermissions(deps, ctx, rest) };
1668
+ continue;
1669
+ }
1670
+ current += char;
1671
+ tokenStarted = true;
2022
1672
  }
2023
- if (normalized === "compact") return { text: await startThreadAction(deps, ctx, options.pluginConfig, "compact", rest) };
2024
- if (normalized === "review") return { text: await startThreadAction(deps, ctx, options.pluginConfig, "review", rest) };
2025
- if (normalized === "diagnostics") return await handleCodexDiagnosticsFeedback(deps, ctx, options.pluginConfig, rest.join(" "), "/codex diagnostics");
2026
- if (normalized === "computer-use" || normalized === "computeruse") {
2027
- if (isMenuVerb(rest)) return buildCodexComputerUseMenuReply();
2028
- return { text: await handleComputerUseCommand(deps, ctx, options.pluginConfig, rest) };
1673
+ if (escaping) current += "\\";
1674
+ if (tokenStarted) args.push(current);
1675
+ return args;
1676
+ }
1677
+ function parseBindArgs(args) {
1678
+ const parsed = {};
1679
+ for (let index = 0; index < args.length; index += 1) {
1680
+ const arg = args[index];
1681
+ if (arg === "--help" || arg === "-h") {
1682
+ parsed.help = true;
1683
+ continue;
1684
+ }
1685
+ if (arg === "--cwd") {
1686
+ const value = readRequiredOptionValue(args, index);
1687
+ if (!value || parsed.cwd !== void 0) {
1688
+ parsed.help = true;
1689
+ continue;
1690
+ }
1691
+ parsed.cwd = value;
1692
+ index += 1;
1693
+ continue;
1694
+ }
1695
+ if (arg === "--model") {
1696
+ const value = readRequiredOptionValue(args, index);
1697
+ if (!value || parsed.model !== void 0) {
1698
+ parsed.help = true;
1699
+ continue;
1700
+ }
1701
+ parsed.model = value;
1702
+ index += 1;
1703
+ continue;
1704
+ }
1705
+ if (arg === "--provider" || arg === "--model-provider") {
1706
+ const value = readRequiredOptionValue(args, index);
1707
+ if (!value || parsed.provider !== void 0) {
1708
+ parsed.help = true;
1709
+ continue;
1710
+ }
1711
+ parsed.provider = value;
1712
+ index += 1;
1713
+ continue;
1714
+ }
1715
+ if (!arg.startsWith("-") && !parsed.threadId) {
1716
+ parsed.threadId = arg;
1717
+ continue;
1718
+ }
1719
+ parsed.help = true;
2029
1720
  }
2030
- if (normalized === "mcp") {
2031
- if (rest.length > 0) return { text: "Usage: /codex mcp" };
2032
- const scope = await resolveCommandAppServerScope(deps, ctx, options.pluginConfig);
2033
- return { text: formatList(await deps.codexControlRequest(options.pluginConfig, CODEX_CONTROL_METHODS.listMcpServers, { limit: 100 }, {
2034
- config: ctx.config,
2035
- ...scope
2036
- }), "MCP servers") };
1721
+ parsed.threadId = normalizeOptionalString(parsed.threadId);
1722
+ parsed.cwd = normalizeOptionalString(parsed.cwd);
1723
+ parsed.model = normalizeOptionalString(parsed.model);
1724
+ parsed.provider = normalizeOptionalString(parsed.provider);
1725
+ return parsed;
1726
+ }
1727
+ function parseCodexCliSessionsArgs(args) {
1728
+ const parsed = { filter: "" };
1729
+ const filter = [];
1730
+ for (let index = 0; index < args.length; index += 1) {
1731
+ const arg = args[index];
1732
+ if (arg === "--help" || arg === "-h") {
1733
+ parsed.help = true;
1734
+ continue;
1735
+ }
1736
+ if (arg === "--host" || arg === "--node") {
1737
+ const value = readRequiredOptionValue(args, index);
1738
+ if (!value || parsed.host !== void 0) {
1739
+ parsed.help = true;
1740
+ continue;
1741
+ }
1742
+ parsed.host = value;
1743
+ index += 1;
1744
+ continue;
1745
+ }
1746
+ if (arg === "--limit") {
1747
+ const value = readRequiredOptionValue(args, index);
1748
+ const parsedLimit = parseStrictPositiveInteger(value);
1749
+ if (parsedLimit === void 0) {
1750
+ parsed.help = true;
1751
+ continue;
1752
+ }
1753
+ parsed.limit = parsedLimit;
1754
+ index += 1;
1755
+ continue;
1756
+ }
1757
+ if (arg.startsWith("-")) {
1758
+ parsed.help = true;
1759
+ continue;
1760
+ }
1761
+ filter.push(arg);
2037
1762
  }
2038
- if (normalized === "skills") {
2039
- if (rest.length > 0) return { text: "Usage: /codex skills" };
2040
- const scope = await resolveCommandAppServerScope(deps, ctx, options.pluginConfig);
2041
- return { text: formatSkills(await deps.codexControlRequest(options.pluginConfig, CODEX_CONTROL_METHODS.listSkills, {}, {
2042
- config: ctx.config,
2043
- ...scope
2044
- })) };
1763
+ parsed.host = normalizeOptionalString(parsed.host);
1764
+ parsed.filter = filter.join(" ").trim();
1765
+ return parsed;
1766
+ }
1767
+ function parseResumeArgs(args) {
1768
+ const parsed = {};
1769
+ for (let index = 0; index < args.length; index += 1) {
1770
+ const arg = args[index];
1771
+ if (arg === "--help" || arg === "-h") {
1772
+ parsed.help = true;
1773
+ continue;
1774
+ }
1775
+ if (arg === "--host" || arg === "--node") {
1776
+ const value = readRequiredOptionValue(args, index);
1777
+ if (!value || parsed.host !== void 0) {
1778
+ parsed.help = true;
1779
+ continue;
1780
+ }
1781
+ parsed.host = value;
1782
+ index += 1;
1783
+ continue;
1784
+ }
1785
+ if (arg === "--bind") {
1786
+ if (readRequiredOptionValue(args, index) !== "here" || parsed.bindHere !== void 0) {
1787
+ parsed.help = true;
1788
+ continue;
1789
+ }
1790
+ parsed.bindHere = true;
1791
+ index += 1;
1792
+ continue;
1793
+ }
1794
+ if (!arg.startsWith("-") && !parsed.threadId) {
1795
+ parsed.threadId = arg;
1796
+ continue;
1797
+ }
1798
+ parsed.help = true;
2045
1799
  }
2046
- if (normalized === "account") {
2047
- if (rest.length > 0) return { text: "Usage: /codex account" };
2048
- const scope = await resolveCommandAppServerScope(deps, ctx, options.pluginConfig);
2049
- const requestScope = {
2050
- config: ctx.config,
2051
- ...scope
2052
- };
2053
- const [account, limits] = await Promise.all([deps.safeCodexControlRequest(options.pluginConfig, CODEX_CONTROL_METHODS.account, { refreshToken: false }, requestScope), deps.safeCodexControlRequest(options.pluginConfig, CODEX_CONTROL_METHODS.rateLimits, void 0, requestScope)]);
2054
- return { text: formatAccount(account, limits, await readCodexAccountAuthOverview({
2055
- ctx,
2056
- agentDir: scope.agentDir,
2057
- pluginConfig: options.pluginConfig,
2058
- safeCodexControlRequest: deps.safeCodexControlRequest,
2059
- account,
2060
- limits
2061
- })) };
1800
+ parsed.threadId = normalizeOptionalString(parsed.threadId);
1801
+ parsed.host = normalizeOptionalString(parsed.host);
1802
+ return parsed;
1803
+ }
1804
+ function parseComputerUseArgs(args) {
1805
+ const parsed = {
1806
+ action: "status",
1807
+ overrides: {},
1808
+ hasOverrides: false
1809
+ };
1810
+ let sawAction = false;
1811
+ for (let index = 0; index < args.length; index += 1) {
1812
+ const arg = args[index];
1813
+ if (arg === "--help" || arg === "-h") {
1814
+ parsed.help = true;
1815
+ continue;
1816
+ }
1817
+ if (arg === "status" || arg === "install") {
1818
+ if (sawAction) {
1819
+ parsed.help = true;
1820
+ continue;
1821
+ }
1822
+ sawAction = true;
1823
+ parsed.action = arg;
1824
+ continue;
1825
+ }
1826
+ if (arg === "--source" || arg === "--marketplace-source") {
1827
+ const value = readRequiredOptionValue(args, index);
1828
+ if (!value || parsed.overrides.marketplaceSource !== void 0) {
1829
+ parsed.help = true;
1830
+ continue;
1831
+ }
1832
+ parsed.overrides.marketplaceSource = value;
1833
+ index += 1;
1834
+ continue;
1835
+ }
1836
+ if (arg === "--marketplace-path" || arg === "--path") {
1837
+ const value = readRequiredOptionValue(args, index);
1838
+ if (!value || parsed.overrides.marketplacePath !== void 0) {
1839
+ parsed.help = true;
1840
+ continue;
1841
+ }
1842
+ parsed.overrides.marketplacePath = value;
1843
+ index += 1;
1844
+ continue;
1845
+ }
1846
+ if (arg === "--marketplace") {
1847
+ const value = readRequiredOptionValue(args, index);
1848
+ if (!value || parsed.overrides.marketplaceName !== void 0) {
1849
+ parsed.help = true;
1850
+ continue;
1851
+ }
1852
+ parsed.overrides.marketplaceName = value;
1853
+ index += 1;
1854
+ continue;
1855
+ }
1856
+ if (arg === "--plugin") {
1857
+ const value = readRequiredOptionValue(args, index);
1858
+ if (!value || parsed.overrides.pluginName !== void 0) {
1859
+ parsed.help = true;
1860
+ continue;
1861
+ }
1862
+ parsed.overrides.pluginName = value;
1863
+ index += 1;
1864
+ continue;
1865
+ }
1866
+ if (arg === "--server" || arg === "--mcp-server") {
1867
+ const value = readRequiredOptionValue(args, index);
1868
+ if (!value || parsed.overrides.mcpServerName !== void 0) {
1869
+ parsed.help = true;
1870
+ continue;
1871
+ }
1872
+ parsed.overrides.mcpServerName = value;
1873
+ index += 1;
1874
+ continue;
1875
+ }
1876
+ parsed.help = true;
2062
1877
  }
2063
- return { text: `Unknown Codex command: ${formatCodexDisplayText(subcommand)}\n\n${buildHelp()}` };
1878
+ parsed.overrides = normalizeComputerUseStringOverrides(parsed.overrides);
1879
+ parsed.hasOverrides = Object.values(parsed.overrides).some(Boolean);
1880
+ return parsed;
1881
+ }
1882
+ function readRequiredOptionValue(args, index) {
1883
+ const value = args[index + 1];
1884
+ const normalized = value?.trim();
1885
+ if (!normalized || normalized.startsWith("-")) return;
1886
+ return value;
1887
+ }
1888
+ function normalizeComputerUseStringOverrides(overrides) {
1889
+ const normalized = {};
1890
+ const marketplaceSource = normalizeOptionalString(overrides.marketplaceSource);
1891
+ if (marketplaceSource) normalized.marketplaceSource = marketplaceSource;
1892
+ const marketplacePath = normalizeOptionalString(overrides.marketplacePath);
1893
+ if (marketplacePath) normalized.marketplacePath = marketplacePath;
1894
+ const marketplaceName = normalizeOptionalString(overrides.marketplaceName);
1895
+ if (marketplaceName) normalized.marketplaceName = marketplaceName;
1896
+ const pluginName = normalizeOptionalString(overrides.pluginName);
1897
+ if (pluginName) normalized.pluginName = pluginName;
1898
+ const mcpServerName = normalizeOptionalString(overrides.mcpServerName);
1899
+ if (mcpServerName) normalized.mcpServerName = mcpServerName;
1900
+ return normalized;
2064
1901
  }
2065
1902
  //#endregion
2066
1903
  export { handleCodexSubcommand };