@openclaw/codex 2026.7.2-beta.4 → 2026.7.2-beta.5

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 (41) hide show
  1. package/dist/{app-server-policy-BO3jYBkv.js → app-server-policy-BmDgseo_.js} +1 -1
  2. package/dist/attempt-client-cleanup-Ia52yRWc.js +127 -0
  3. package/dist/{attempt-notifications-DdmnBMUp.js → attempt-notifications-CMR2_fmR.js} +1 -1
  4. package/dist/{bounded-turn-CaQL96kg.js → bounded-turn-FEbUXoYT.js} +36 -13
  5. package/dist/{capabilities-CLICvTxf.js → capabilities-BHRSI6Cn.js} +1 -1
  6. package/dist/cli-metadata.js +1 -1
  7. package/dist/{command-formatters-DgADqnHa.js → command-formatters-ico4ZFnk.js} +2 -2
  8. package/dist/{command-handlers-l_lVPNlB.js → command-handlers-CRmnwI2N.js} +1232 -1221
  9. package/dist/{compact-CC5KhBzX.js → compact-dEN49UH6.js} +7 -7
  10. package/dist/{computer-use-Ip1-QGa-.js → computer-use-BjGylO-E.js} +16 -6
  11. package/dist/{conversation-control-sNPsbTCP.js → conversation-control-DE7d9DOj.js} +6 -6
  12. package/dist/doctor-contract-api.js +24 -86
  13. package/dist/{dynamic-tools-CQI3UwqY.js → dynamic-tools-LIt9ojep.js} +50 -46
  14. package/dist/{event-projector-usage-D6tE7_qD.js → event-projector-usage-tvgb1NCX.js} +2 -2
  15. package/dist/harness.js +14 -13
  16. package/dist/index.js +208 -267
  17. package/dist/media-understanding-provider.js +6 -1
  18. package/dist/{models-BNeMlHgg.js → models-BA01NEez.js} +2 -2
  19. package/dist/{notification-correlation-BGLixv5_.js → notification-correlation-Bj_Ak_jI.js} +3 -3
  20. package/dist/{plugin-app-cache-key-CvzvmBRz.js → plugin-app-cache-key-DSXX-rXO.js} +5 -5
  21. package/dist/{transcript-mirror-2p7kl6e4.js → protocol-validators-Dm4eXZmt.js} +2 -550
  22. package/dist/{rate-limits-D0IJT-a6.js → rate-limits-S_PXs0ul.js} +1 -1
  23. package/dist/realtime-voice-api-BPcA1uN9.js +494 -0
  24. package/dist/realtime-voice-api.js +2 -0
  25. package/dist/{request-BTccUTD3.js → request-CXpPennK.js} +1 -1
  26. package/dist/{run-attempt--858NXDl.js → run-attempt-C5GTPJOE.js} +417 -237
  27. package/dist/{session-binding-ZSMX1omP.js → session-binding-DlVQ-Pxs.js} +952 -890
  28. package/dist/{session-catalog-DsKaezgR.js → session-catalog-CP2oCcEb.js} +32 -23
  29. package/dist/{session-cli-CYJTfRLG.js → session-cli-CqZ0Fp2a.js} +2 -2
  30. package/dist/{session-upstream-marker-D7XlI9us.js → session-upstream-marker-QE-m5QFG.js} +1 -1
  31. package/dist/{settled-turn-finalizer-DHltNgtF.js → settled-turn-finalizer-CA_2QXIE.js} +2 -2
  32. package/dist/{shared-client-wmgNFNYp.js → shared-client-BVKE40c-.js} +1863 -1707
  33. package/dist/{side-question-BfFZCsJ-.js → side-question-CO12sHxo.js} +11 -9
  34. package/dist/{thread-lifecycle-cqrOlJkq.js → thread-lifecycle-DRg_FdzC.js} +12 -132
  35. package/dist/transcript-mirror-uKA-A4tB.js +550 -0
  36. package/dist/{upstream-session-fork-vPjwkT1i.js → upstream-session-fork-_Vrz4WJr.js} +3 -3
  37. package/dist/{usage-DM_I3mPT.js → usage-UpVbdX5G.js} +3 -3
  38. package/dist/{web-search-provider.runtime-Bx6HAc_k.js → web-search-provider.runtime-CaQMYDbp.js} +3 -3
  39. package/package.json +4 -5
  40. package/npm-shrinkwrap.json +0 -202
  41. package/dist/{auth-binding-C1czuHPA.js → auth-binding-B4FfbcCg.js} +1 -1
@@ -1,23 +1,23 @@
1
- import { Q as isJsonObject, S as resolveCodexAppServerAuthProfileIdForAgent } from "./shared-client-wmgNFNYp.js";
2
- import { c as reclaimCurrentCodexSessionGeneration, i as createCodexSessionGenerationSupersededError, l as sessionBindingIdentity, n as assertCodexBindingMayBeReplaced, r as bindingStoreKey, s as normalizeCodexAppServerBindingModelProvider, v as isCodexFastServiceTier } from "./session-binding-ZSMX1omP.js";
3
- import { g as assertCodexThreadResumeResponse } from "./transcript-mirror-2p7kl6e4.js";
4
- import { t as listAllCodexAppServerModels } from "./models-BNeMlHgg.js";
5
- import { t as CODEX_CONTROL_METHODS } from "./capabilities-CLICvTxf.js";
6
- import { s as summarizeCodexAccountUsage } from "./rate-limits-D0IJT-a6.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-DgADqnHa.js";
1
+ import { N as resolveCodexAppServerAuthProfileIdForAgent, V as isJsonObject } from "./shared-client-BVKE40c-.js";
2
+ import { O as isCodexFastServiceTier, c as reclaimCurrentCodexSessionGeneration, i as createCodexSessionGenerationSupersededError, l as sessionBindingIdentity, n as assertCodexBindingMayBeReplaced, r as bindingStoreKey, s as normalizeCodexAppServerBindingModelProvider } from "./session-binding-DlVQ-Pxs.js";
3
+ import { r as assertCodexThreadResumeResponse } from "./protocol-validators-Dm4eXZmt.js";
4
+ import { t as listAllCodexAppServerModels } from "./models-BA01NEez.js";
5
+ import { t as CODEX_CONTROL_METHODS } from "./capabilities-BHRSI6Cn.js";
6
+ import { s as summarizeCodexAccountUsage } from "./rate-limits-S_PXs0ul.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-ico4ZFnk.js";
8
8
  import { n as resolveCodexNativeExecutionBlock, r as resolveCodexNativeSandboxBlock } from "./sandbox-guard-D1Wky__S.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-sNPsbTCP.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-DE7d9DOj.js";
10
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-BGLixv5_.js";
12
- import { c as requestOptions, l as safeCodexControlRequest, o as codexControlRequest, p as formatCodexCliSessions, s as readCodexStatusProbes } from "./session-catalog-DsKaezgR.js";
13
- import { i as readCodexComputerUseStatus, n as installCodexComputerUse } from "./computer-use-Ip1-QGa-.js";
11
+ import { a as resolveCodexBindingAppServerConnection } from "./notification-correlation-Bj_Ak_jI.js";
12
+ import { c as requestOptions, l as safeCodexControlRequest, o as codexControlRequest, p as formatCodexCliSessions, s as readCodexStatusProbes } from "./session-catalog-CP2oCcEb.js";
13
+ import { i as readCodexComputerUseStatus, n as installCodexComputerUse } from "./computer-use-BjGylO-E.js";
14
14
  import { parseStrictPositiveInteger } from "openclaw/plugin-sdk/number-runtime";
15
- import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
16
15
  import crypto from "node:crypto";
17
16
  import { ensureAuthProfileStore, findNormalizedProviderValue, resolveAgentDir, resolveAuthProfileEligibility, resolveAuthProfileOrder, resolveProfileUnusableUntilForDisplay, resolveSessionAgentIds } from "openclaw/plugin-sdk/agent-runtime";
18
17
  import { parseAgentSessionKey } from "openclaw/plugin-sdk/routing";
19
18
  import { normalizeOptionalString, normalizeUniqueStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
20
19
  import { getSessionEntry, resolveStorePath } from "openclaw/plugin-sdk/session-store-runtime";
20
+ import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
21
21
  import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
22
22
  import { MODEL_SELECTION_LOCKED_MESSAGE, isModelSelectionLocked } from "openclaw/plugin-sdk/model-session-runtime";
23
23
  //#region extensions/codex/src/command-account.ts
@@ -350,210 +350,8 @@ function buildCodexCommandPickerPresentation(title, prompt, buttons) {
350
350
  };
351
351
  }
352
352
  //#endregion
353
- //#region extensions/codex/src/command-plugins-management.ts
354
- const POLICY_REFRESH_HINT = "New Codex conversations pick this up automatically. Use /new or /reset to refresh the current one.";
355
- async function handleCodexPluginsSubcommand(ctx, rest, io) {
356
- const [verb = "list", ...args] = rest;
357
- const normalized = verb.toLowerCase();
358
- if (normalized === "menu") {
359
- if (args.length > 0) return { text: "Usage: /codex plugins menu" };
360
- return buildPluginsMenuReply();
361
- }
362
- if (normalized === "help") {
363
- if (args.length > 0) return { text: "Usage: /codex plugins help" };
364
- return { text: buildPluginsHelp() };
365
- }
366
- if (normalized === "list") {
367
- if (args.length > 0) return { text: "Usage: /codex plugins list" };
368
- const current = await io.readConfig();
369
- return { text: formatPluginList(current.plugins ?? {}, { globalEnabled: current.enabled === true }) };
370
- }
371
- const target = args[0];
372
- if (normalized === "enable" || normalized === "disable") {
373
- if (args.length === 0) return buildPluginNamePickerReply(normalized, await io.readConfig());
374
- if (!target || args.length > 1) return { text: `Usage: /codex plugins ${normalized} <name>` };
375
- if (!canMutateCodexHost(ctx)) return { text: `Only an owner or operator.admin gateway client can run /codex plugins ${normalized}.` };
376
- const wantEnabled = normalized === "enable";
377
- if (!((await io.readConfig()).plugins ?? {})[target]) return { text: `Codex sub-plugin '${formatCodexDisplayText(target)}' is not configured. Run '/codex plugins list' to see configured plugins.` };
378
- await io.mutate((block) => {
379
- if (wantEnabled) block.enabled = true;
380
- block.plugins ??= {};
381
- block.plugins[target] = {
382
- ...block.plugins[target],
383
- enabled: wantEnabled
384
- };
385
- });
386
- return { text: `${formatCodexDisplayText(target)}: ${wantEnabled ? "enabled" : "disabled"} in openclaw.json. ${POLICY_REFRESH_HINT}` };
387
- }
388
- return { text: `Unknown /codex plugins subcommand: ${formatCodexDisplayText(verb)}\n\n${buildPluginsHelp()}` };
389
- }
390
- function buildPluginsMenuReply() {
391
- return {
392
- text: [
393
- "Codex sub-plugins. Pick a sub-action or type:",
394
- "",
395
- " 1. /codex plugins list",
396
- " 2. /codex plugins enable",
397
- " 3. /codex plugins disable",
398
- " 4. /codex plugins help",
399
- "",
400
- "Type '/codex' to go back to the main menu."
401
- ].join("\n"),
402
- presentation: buildCodexCommandPickerPresentation("Codex sub-plugins", "Pick a Codex sub-plugin action:", [
403
- {
404
- label: "list",
405
- command: "/codex plugins list"
406
- },
407
- {
408
- label: "enable",
409
- command: "/codex plugins enable"
410
- },
411
- {
412
- label: "disable",
413
- command: "/codex plugins disable"
414
- },
415
- {
416
- label: "help",
417
- command: "/codex plugins help"
418
- },
419
- {
420
- label: "back",
421
- command: "/codex"
422
- }
423
- ])
424
- };
425
- }
426
- function buildPluginNamePickerReply(verb, current) {
427
- const globalEnabled = current.enabled === true;
428
- const eligible = Object.entries(current.plugins ?? {}).toSorted(([left], [right]) => left.localeCompare(right)).filter(([, entry]) => {
429
- const effectivelyEnabled = globalEnabled && entry.enabled !== false;
430
- return verb === "disable" ? effectivelyEnabled : !effectivelyEnabled;
431
- });
432
- if (eligible.length === 0) return {
433
- text: [
434
- `No configured ${verb === "enable" ? "disabled" : "enabled"} Codex sub-plugins found.`,
435
- "",
436
- "Type '/codex plugins list' to inspect configured sub-plugins.",
437
- "Type '/codex plugins menu' to go back to the plugins menu."
438
- ].join("\n"),
439
- presentation: buildCodexCommandPickerPresentation("Codex sub-plugins", "Pick another Codex sub-plugin action:", [{
440
- label: "list",
441
- command: "/codex plugins list"
442
- }, {
443
- label: "back",
444
- command: "/codex plugins menu"
445
- }])
446
- };
447
- const buttons = [...eligible.map(([key]) => ({
448
- label: formatCodexDisplayText(key),
449
- command: `/codex plugins ${verb} ${key}`
450
- })), {
451
- label: "back",
452
- command: "/codex plugins menu"
453
- }];
454
- return {
455
- text: [
456
- `Codex sub-plugins to ${verb}. Pick one or type:`,
457
- "",
458
- ...eligible.map(([key], index) => ` ${index + 1}. /codex plugins ${verb} ${key}`),
459
- "",
460
- ...verb === "enable" && !globalEnabled ? ["Global codexPlugins.enabled is off; enabling one configured sub-plugin turns it on.", ""] : [],
461
- "Type '/codex plugins menu' to go back to the plugins menu."
462
- ].join("\n"),
463
- presentation: buildCodexCommandPickerPresentation("Codex sub-plugins", `Pick a Codex sub-plugin to ${verb}:`, buttons)
464
- };
465
- }
466
- function buildPluginsHelp() {
467
- return [
468
- "Codex sub-plugin management (writes only to ~/.openclaw/openclaw.json, never to ~/.codex/config.toml):",
469
- "- /codex plugins (alias for list)",
470
- "- /codex plugins list show all configured Codex sub-plugins",
471
- "- /codex plugins enable <name> enable a configured sub-plugin",
472
- "- /codex plugins disable <name> disable a configured sub-plugin"
473
- ].join("\n");
474
- }
475
- function formatPluginList(plugins, options = {}) {
476
- const globalEnabled = options.globalEnabled === true;
477
- const keys = Object.keys(plugins).toSorted();
478
- if (keys.length === 0) return "No Codex sub-plugins configured under plugins.entries.codex.config.codexPlugins.plugins";
479
- const rows = keys.map((key) => {
480
- const entry = plugins[key] ?? {};
481
- const state = globalEnabled && entry.enabled !== false ? "ON " : "OFF";
482
- return {
483
- displayKey: formatCodexDisplayText(key),
484
- state,
485
- pluginName: formatCodexDisplayText(entry.pluginName ?? key),
486
- marketplace: formatCodexDisplayText(entry.marketplaceName ?? "?")
487
- };
488
- });
489
- const keyW = Math.max(...rows.map((r) => r.displayKey.length));
490
- const pluginW = Math.max(...rows.map((r) => r.pluginName.length));
491
- return [
492
- "Codex sub-plugins in Openclaw config (~/.openclaw/openclaw.json):",
493
- "",
494
- ...rows.map((r) => ` ${r.state} ${r.displayKey.padEnd(keyW)} ${r.pluginName.padEnd(pluginW)} [${r.marketplace}]`),
495
- "",
496
- ...globalEnabled ? [] : ["Global codexPlugins.enabled is off; configured sub-plugins are inactive.", ""],
497
- "New Codex conversations pick up policy changes automatically; /new or /reset to refresh the current one."
498
- ].join("\n");
499
- }
500
- //#endregion
501
- //#region extensions/codex/src/command-handlers.ts
502
- const defaultCodexCommandDeps = {
503
- codexControlRequest,
504
- listCodexAppServerModels: listAllCodexAppServerModels,
505
- readCodexStatusProbes,
506
- requestOptions,
507
- safeCodexControlRequest,
508
- readCodexComputerUseStatus,
509
- installCodexComputerUse,
510
- resolveCodexDefaultWorkspaceDir,
511
- readCodexConversationActiveTurn,
512
- setCodexConversationFastMode,
513
- setCodexConversationModel,
514
- setCodexConversationPermissions,
515
- steerCodexConversationTurn,
516
- stopCodexConversationTurn,
517
- listCodexCliSessionsOnNode: async () => {
518
- throw new Error("Codex CLI node sessions require Gateway node runtime.");
519
- },
520
- resolveCodexCliSessionForBindingOnNode: async () => {
521
- throw new Error("Codex CLI node sessions require Gateway node runtime.");
522
- }
523
- };
524
- const CODEX_DIAGNOSTICS_SOURCE = "openclaw-diagnostics";
525
- const CODEX_DIAGNOSTICS_REASON_MAX_CHARS = 2048;
526
- const CODEX_DIAGNOSTICS_COOLDOWN_MS = 6e4;
527
- const CODEX_DIAGNOSTICS_ERROR_MAX_CHARS = 500;
528
- const CODEX_DIAGNOSTICS_COOLDOWN_MAX_THREADS = 100;
529
- const CODEX_DIAGNOSTICS_COOLDOWN_MAX_SCOPES = 100;
530
- const CODEX_DIAGNOSTICS_CONFIRMATION_TTL_MS = 5 * 6e4;
531
- const CODEX_DIAGNOSTICS_CONFIRMATION_MAX_REQUESTS_PER_SCOPE = 100;
532
- const CODEX_DIAGNOSTICS_CONFIRMATION_MAX_SCOPES = 100;
533
- const CODEX_DIAGNOSTICS_SCOPE_FIELD_MAX_CHARS = 128;
534
- const CODEX_RESUME_SAFE_THREAD_ID_PATTERN = /^[A-Za-z0-9._:-]+$/;
535
- const CODEX_NATIVE_EXECUTION_SUBCOMMANDS = /* @__PURE__ */ new Set([
536
- "bind",
537
- "resume",
538
- "steer",
539
- "model",
540
- "fast",
541
- "permissions",
542
- "compact",
543
- "review",
544
- "goal"
545
- ]);
546
- const CODEX_NATIVE_CONTROL_SUBCOMMANDS = /* @__PURE__ */ new Set([
547
- ...CODEX_NATIVE_EXECUTION_SUBCOMMANDS,
548
- "detach",
549
- "unbind",
550
- "stop"
551
- ]);
552
- const { lastUploadByThread: lastCodexDiagnosticsUploadByThread, lastUploadByScope: lastCodexDiagnosticsUploadByScope, pendingConfirmations: pendingCodexDiagnosticsConfirmations, pendingTokensByScope: pendingCodexDiagnosticsConfirmationTokensByScope } = codexDiagnosticsFeedbackState;
553
- /**
554
- * No-arg `/codex` picker. Codex owns the command tree; channels render the
555
- * portable command actions as inline controls when their transport can.
556
- */
353
+ //#region extensions/codex/src/command-handler-args.ts
354
+ /** No-arg `/codex` picker. */
557
355
  function buildCodexSubcommandPickerReply() {
558
356
  const verbs = [
559
357
  {
@@ -596,7 +394,6 @@ function buildCodexSubcommandPickerReply() {
596
394
  presentation: buildCodexCommandPickerPresentation("Codex commands", "Pick a Codex subcommand:", verbs)
597
395
  };
598
396
  }
599
- /** Sub-picker for `/codex fast menu` (on / off / status). */
600
397
  function buildCodexFastMenuReply() {
601
398
  const modes = [
602
399
  "on",
@@ -621,7 +418,6 @@ function buildCodexFastMenuReply() {
621
418
  presentation: buildCodexCommandPickerPresentation("Codex fast mode", "Pick a Codex fast mode:", buttons)
622
419
  };
623
420
  }
624
- /** Sub-picker for `/codex permissions menu` (default / yolo / status). */
625
421
  function buildCodexPermissionsMenuReply() {
626
422
  const modes = [
627
423
  "default",
@@ -646,7 +442,6 @@ function buildCodexPermissionsMenuReply() {
646
442
  presentation: buildCodexCommandPickerPresentation("Codex permissions", "Pick a Codex permissions mode:", buttons)
647
443
  };
648
444
  }
649
- /** Sub-picker for `/codex computer-use menu` (status / install). */
650
445
  function buildCodexComputerUseMenuReply() {
651
446
  const actions = ["status", "install"];
652
447
  const buttons = [...actions.map((action) => ({
@@ -669,503 +464,595 @@ function buildCodexComputerUseMenuReply() {
669
464
  presentation: buildCodexCommandPickerPresentation("Codex computer-use", "Pick a Codex computer-use action:", buttons)
670
465
  };
671
466
  }
672
- /** Returns true when the rest-args are exactly `["menu"]` (case-insensitive). */
673
467
  function isMenuVerb(rest) {
674
468
  return rest.length === 1 && (rest[0] ?? "").trim().toLowerCase() === "menu";
675
469
  }
676
- async function handleCodexSubcommand(ctx, options) {
677
- const deps = {
678
- ...defaultCodexCommandDeps,
679
- ...options.deps
680
- };
681
- const args = splitArgs(ctx.args);
682
- if (args.length === 0) return buildCodexSubcommandPickerReply();
683
- const [subcommand = "status", ...rest] = args;
684
- const normalized = subcommand.toLowerCase();
685
- if (normalized === "help") return { text: buildHelp() };
686
- if (CODEX_NATIVE_CONTROL_SUBCOMMANDS.has(normalized) && !returnsBeforeNativeCodexExecution(normalized, rest) && !isReadOnlyCodexGoalCommand(normalized, rest) && !canMutateCodexHost(ctx)) return { text: CODEX_NATIVE_EXECUTION_AUTH_ERROR };
687
- const sandboxBlock = resolveCodexNativeCommandSandboxBlock(ctx, normalized, rest);
688
- if (sandboxBlock) return { text: sandboxBlock };
689
- if (normalized === "plugins") {
690
- 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." };
691
- return await handleCodexPluginsSubcommand(ctx, rest, deps.codexPluginsManagementIo);
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;
692
510
  }
693
- if (normalized === "status") {
694
- if (rest.length > 0) return { text: "Usage: /codex status" };
695
- const { agentDir } = resolveCodexConversationControlScope(ctx);
696
- return { text: formatCodexStatus(await deps.readCodexStatusProbes(options.pluginConfig, ctx.config, agentDir)) };
697
- }
698
- if (normalized === "models") {
699
- if (rest.length > 0) return { text: "Usage: /codex models" };
700
- const { agentDir } = resolveCodexConversationControlScope(ctx);
701
- return { text: formatModels(await deps.listCodexAppServerModels(deps.requestOptions(options.pluginConfig, 100, ctx.config, agentDir))) };
702
- }
703
- if (normalized === "threads") return { text: await buildThreads(deps, ctx, options.pluginConfig, rest.join(" ")) };
704
- if (normalized === "goal") return { text: await handleNativeGoal(deps, ctx, options.pluginConfig, rest) };
705
- if (normalized === "sessions") return { text: await buildCodexCliSessions(deps, rest) };
706
- if (normalized === "resume") return { text: await resumeThread(deps, ctx, options.pluginConfig, rest) };
707
- if (normalized === "bind") return await bindConversation(deps, ctx, options.pluginConfig, rest);
708
- if (normalized === "detach" || normalized === "unbind") {
709
- if (rest.length > 0) return { text: "Usage: /codex detach" };
710
- return { text: await detachConversation(deps, ctx) };
711
- }
712
- if (normalized === "binding") {
713
- if (rest.length > 0) return { text: "Usage: /codex binding" };
714
- return { text: await describeConversationBinding(deps, ctx) };
715
- }
716
- if (normalized === "stop") {
717
- if (rest.length > 0) return { text: "Usage: /codex stop" };
718
- return { text: await stopConversationTurn(deps, ctx, options.pluginConfig) };
719
- }
720
- if (normalized === "steer") return { text: await steerConversationTurn(deps, ctx, options.pluginConfig, rest.join(" ")) };
721
- if (normalized === "model") return { text: await setConversationModel(deps, ctx, options.pluginConfig, rest) };
722
- if (normalized === "fast") {
723
- if (isMenuVerb(rest)) return buildCodexFastMenuReply();
724
- return { text: await setConversationFastMode(deps, ctx, rest) };
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;
725
558
  }
726
- if (normalized === "permissions") {
727
- if (isMenuVerb(rest)) return buildCodexPermissionsMenuReply();
728
- return { text: await setConversationPermissions(deps, ctx, rest) };
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);
729
599
  }
730
- if (normalized === "compact") return { text: await startThreadAction(deps, ctx, options.pluginConfig, "compact", rest) };
731
- if (normalized === "review") return { text: await startThreadAction(deps, ctx, options.pluginConfig, "review", rest) };
732
- if (normalized === "diagnostics") return await handleCodexDiagnosticsFeedback(deps, ctx, options.pluginConfig, rest.join(" "), "/codex diagnostics");
733
- if (normalized === "computer-use" || normalized === "computeruse") {
734
- if (isMenuVerb(rest)) return buildCodexComputerUseMenuReply();
735
- return { text: await handleComputerUseCommand(deps, ctx, options.pluginConfig, rest) };
600
+ parsed.host = normalizeOptionalString(parsed.host);
601
+ parsed.filter = filter.join(" ").trim();
602
+ return parsed;
603
+ }
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;
736
636
  }
737
- if (normalized === "mcp") {
738
- if (rest.length > 0) return { text: "Usage: /codex mcp" };
739
- const scope = await resolveCommandAppServerScope(deps, ctx, options.pluginConfig);
740
- return { text: formatList(await deps.codexControlRequest(options.pluginConfig, CODEX_CONTROL_METHODS.listMcpServers, { limit: 100 }, {
741
- config: ctx.config,
742
- ...scope
743
- }), "MCP servers") };
637
+ parsed.threadId = normalizeOptionalString(parsed.threadId);
638
+ parsed.host = normalizeOptionalString(parsed.host);
639
+ return parsed;
640
+ }
641
+ function parseComputerUseArgs(args) {
642
+ const parsed = {
643
+ action: "status",
644
+ overrides: {},
645
+ hasOverrides: false,
646
+ persistentIdentity: {}
647
+ };
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;
744
706
  }
745
- if (normalized === "skills") {
746
- if (rest.length > 0) return { text: "Usage: /codex skills" };
747
- const scope = await resolveCommandAppServerScope(deps, ctx, options.pluginConfig);
748
- return { text: formatSkills(await deps.codexControlRequest(options.pluginConfig, CODEX_CONTROL_METHODS.listSkills, {}, {
749
- config: ctx.config,
750
- ...scope
751
- })) };
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;
737
+ }
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;
755
+ }
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
+ };
772
+ }
773
+ function formatDiagnosticsUsage(commandPrefix) {
774
+ return [
775
+ `Usage: ${commandPrefix} [note]`,
776
+ `Usage: ${commandPrefix} confirm <token>`,
777
+ `Usage: ${commandPrefix} cancel <token>`
778
+ ].join("\n");
779
+ }
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);
752
785
  }
753
- if (normalized === "account") {
754
- if (rest.length > 0) return { text: "Usage: /codex account" };
755
- const scope = await resolveCommandAppServerScope(deps, ctx, options.pluginConfig);
756
- const requestScope = {
757
- config: ctx.config,
758
- ...scope
759
- };
760
- 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)]);
761
- return { text: formatAccount(account, limits, await readCodexAccountAuthOverview({
762
- ctx,
763
- agentDir: scope.agentDir,
764
- pluginConfig: options.pluginConfig,
765
- safeCodexControlRequest: deps.safeCodexControlRequest,
766
- account,
767
- limits
768
- })) };
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);
769
791
  }
770
- return { text: `Unknown Codex command: ${formatCodexDisplayText(subcommand)}\n\n${buildHelp()}` };
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
809
+ });
810
+ return token;
771
811
  }
772
- function isCurrentSessionModelSelectionLocked(ctx) {
773
- const sessionKey = ctx.sessionKey?.trim();
774
- if (!sessionKey) return false;
775
- return isModelSelectionLocked(getSessionEntry({
776
- storePath: resolveStorePath(ctx.config.session?.store, { agentId: ctx.agentId }),
777
- sessionKey,
778
- hydrateSkillPromptRefs: false,
779
- readConsistency: "latest"
780
- }));
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
+ };
781
820
  }
782
- function resolveCodexNativeCommandSandboxBlock(ctx, subcommand, args) {
783
- if (isReadOnlyCodexGoalCommand(subcommand, args)) return;
784
- if (!CODEX_NATIVE_EXECUTION_SUBCOMMANDS.has(subcommand)) return;
785
- if (returnsBeforeNativeCodexExecution(subcommand, args)) return;
786
- if (isCodexCliNodeResumeBind(subcommand, args)) return resolveCodexNativeSandboxBlock({
787
- config: ctx.config,
788
- sessionKey: ctx.sessionKey,
789
- sessionId: ctx.sessionId,
790
- surface: `/${["codex", subcommand].join(" ")}`
791
- });
792
- return resolveCodexNativeExecutionBlock({
793
- config: ctx.config,
794
- agentId: ctx.agentId,
795
- sessionKey: ctx.sessionKey,
796
- sessionId: ctx.sessionId,
797
- surface: `/${["codex", subcommand].join(" ")}`
798
- });
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
+ };
799
844
  }
800
- function isReadOnlyCodexGoalCommand(subcommand, args) {
801
- if (subcommand !== "goal" || args.length > 1) return false;
802
- const action = (args[0] ?? "status").toLowerCase();
803
- return action === "status" || action === "get";
845
+ function readPendingCodexDiagnosticsConfirmation(token, now) {
846
+ prunePendingCodexDiagnosticsConfirmations(now);
847
+ return pendingCodexDiagnosticsConfirmations.get(token);
804
848
  }
805
- function returnsBeforeNativeCodexExecution(subcommand, args) {
806
- switch (subcommand) {
807
- case "bind": return parseBindArgs([...args]).help === true;
808
- case "resume": return returnsBeforeNativeCodexResume(args);
809
- case "steer": return args.join(" ").trim() === "";
810
- case "model": return args.length === 0 || args.length > 1;
811
- case "fast": return args.length === 0 || args.length > 1 || parseCodexFastModeArg(args[0]) === void 0;
812
- case "permissions": return args.length === 0 || args.length > 1 || parseCodexPermissionsModeArg(args[0]) === void 0;
813
- case "compact":
814
- case "review":
815
- case "detach":
816
- case "unbind":
817
- case "stop": return args.length > 0;
818
- default: return false;
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);
858
+ }
859
+ function prunePendingCodexDiagnosticsConfirmations(now) {
860
+ for (const [token, pending] of pendingCodexDiagnosticsConfirmations) if (now - pending.createdAt >= CODEX_DIAGNOSTICS_CONFIRMATION_TTL_MS) deletePendingCodexDiagnosticsConfirmation(token);
861
+ }
862
+ function deletePendingCodexDiagnosticsConfirmationScope(scopeKey) {
863
+ const scopeTokens = pendingCodexDiagnosticsConfirmationTokensByScope.get(scopeKey) ?? [];
864
+ for (const token of scopeTokens) pendingCodexDiagnosticsConfirmations.delete(token);
865
+ pendingCodexDiagnosticsConfirmationTokensByScope.delete(scopeKey);
866
+ }
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]);
878
+ }
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)}`));
819
892
  }
893
+ return lines.join("\n");
820
894
  }
821
- function isCodexCliNodeResumeBind(subcommand, args) {
822
- if (subcommand !== "resume") return false;
823
- const parsed = parseResumeArgs([...args]);
824
- return Boolean(parsed.host && parsed.threadId && parsed.bindHere === true && !parsed.help);
895
+ function formatCodexDiagnosticsTargetLines(targets) {
896
+ return targets.flatMap((target, index) => {
897
+ const lines = formatCodexDiagnosticsTargetBlock(target, index);
898
+ return index < targets.length - 1 ? [...lines, ""] : lines;
899
+ });
825
900
  }
826
- function returnsBeforeNativeCodexResume(args) {
827
- const parsed = parseResumeArgs([...args]);
828
- const normalizedThreadId = parsed.threadId?.trim();
829
- if (parsed.help) return true;
830
- if (parsed.host) return !normalizedThreadId || parsed.bindHere !== true;
831
- return !normalizedThreadId || args.length !== 1;
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;
832
909
  }
833
- async function handleComputerUseCommand(deps, ctx, pluginConfig, args) {
834
- const parsed = parseComputerUseArgs(args);
835
- 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");
836
- if (Object.keys(parsed.persistentIdentity).length > 0) return formatComputerUsePersistentIdentityMigration(parsed);
837
- if (parsed.action === "install" && !canMutateCodexHost(ctx)) return "Only an owner or operator.admin gateway client can configure Codex Computer Use.";
838
- const { agentDir } = resolveCodexConversationControlScope(ctx);
839
- const params = {
840
- pluginConfig,
841
- config: ctx.config,
842
- agentDir,
843
- forceEnable: parsed.action === "install" || parsed.hasOverrides,
844
- ...Object.keys(parsed.overrides).length > 0 ? { overrides: parsed.overrides } : {}
845
- };
846
- if (parsed.action === "install") return formatComputerUseStatus(await deps.installCodexComputerUse(params));
847
- return formatComputerUseStatus(await deps.readCodexComputerUseStatus(params));
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(", ")}`;
848
917
  }
849
- async function bindConversation(deps, ctx, pluginConfig, args) {
850
- const parsed = parseBindArgs(args);
851
- if (parsed.help) return { text: "Usage: /codex bind [thread-id] [--cwd <path>] [--model <model>] [--provider <provider>]" };
852
- if (isCurrentSessionModelSelectionLocked(ctx)) return { text: MODEL_SELECTION_LOCKED_MESSAGE };
853
- const scope = resolveCodexConversationControlScope(ctx);
854
- const workspaceDir = parsed.cwd ?? deps.resolveCodexDefaultWorkspaceDir(pluginConfig);
855
- const currentConversation = await ctx.getCurrentConversationBinding();
856
- const currentConversationData = readCodexConversationBindingData(currentConversation);
857
- const bindingId = currentConversationData?.kind === "codex-app-server-session" ? currentConversationData.bindingId : currentConversation ? `conversation-${currentConversation.bindingId}` : void 0;
858
- const sessionOwner = ctx.sessionId ? sessionBindingIdentity({
859
- sessionId: ctx.sessionId,
860
- sessionKey: ctx.sessionKey,
861
- agentId: scope.agentId,
862
- config: ctx.config
863
- }) : void 0;
864
- const currentOwner = currentConversationData?.kind === "codex-app-server-session" ? conversationBindingIdentity(currentConversationData.bindingId) : sessionOwner;
865
- const existingBinding = currentOwner ? await deps.bindingStore.read(currentOwner) : void 0;
866
- assertCodexBindingMayBeReplaced(existingBinding, "binding this conversation to another thread");
867
- const sessionSource = sessionOwner && existingBinding ? {
868
- agentId: sessionOwner.agentId,
869
- sessionId: sessionOwner.sessionId,
870
- threadId: existingBinding.threadId,
871
- ...sessionOwner.sessionKey ? { sessionKey: sessionOwner.sessionKey } : {}
872
- } : void 0;
873
- const authProfileId = existingBinding?.authProfileId;
874
- const data = createCodexConversationBindingData({
875
- bindingId,
876
- workspaceDir,
877
- agentId: scope.agentId,
878
- agentDir: scope.agentDir,
879
- source: currentConversationData?.kind === "codex-app-server-session" ? currentConversationData.source : sessionSource,
880
- start: {
881
- id: crypto.randomUUID(),
882
- threadId: parsed.threadId,
883
- model: parsed.model,
884
- modelProvider: parsed.provider,
885
- authProfileId
918
+ function escapeCodexChatText(value) {
919
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("@", "@").replaceAll("`", "`").replaceAll("[", "[").replaceAll("]", "]").replaceAll("(", "(").replaceAll(")", ")").replaceAll("*", "∗").replaceAll("_", "_").replaceAll("~", "~").replaceAll("|", "|");
920
+ }
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>";
929
+ }
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.`;
886
936
  }
887
- });
888
- const threadLabel = parsed.threadId ?? "a new thread";
889
- const request = await ctx.requestConversationBinding({
890
- summary: `Codex app-server thread ${formatCodexDisplayText(threadLabel)} in ${formatCodexDisplayText(workspaceDir)}`,
891
- detachHint: "/codex detach",
892
- data
893
- });
894
- if (request.status === "pending") return request.reply;
895
- if (request.status === "error") return { text: formatCodexDisplayText(request.message) };
896
- return { text: `Bound this conversation to ${formatCodexDisplayText(threadLabel)} in ${formatCodexDisplayText(workspaceDir)}. The next message will initialize it.` };
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");
897
958
  }
898
- async function detachConversation(deps, ctx) {
899
- if (isCurrentSessionModelSelectionLocked(ctx)) return MODEL_SELECTION_LOCKED_MESSAGE;
900
- const data = readCodexConversationBindingData(await ctx.getCurrentConversationBinding());
901
- if (data?.kind === "codex-app-server-session") assertCodexBindingMayBeReplaced(await deps.bindingStore.read(conversationBindingIdentity(data.bindingId)), "detaching its conversation binding");
902
- const detached = await ctx.detachConversationBinding();
903
- if (data?.kind === "codex-app-server-session") await deps.bindingStore.mutate(conversationBindingIdentity(data.bindingId), { kind: "clear" });
904
- return detached.removed ? "Detached this conversation from Codex." : "No Codex conversation binding was attached.";
959
+ function buildDiagnosticsTags(ctx) {
960
+ const tags = { source: CODEX_DIAGNOSTICS_SOURCE };
961
+ addTag(tags, "channel", ctx.channel);
962
+ return tags;
905
963
  }
906
- async function describeConversationBinding(deps, ctx) {
907
- const current = await ctx.getCurrentConversationBinding();
908
- const data = readCodexConversationBindingData(current);
909
- if (!current || !data) return "No Codex conversation binding is attached.";
910
- if (data.kind === "codex-cli-node-session") return [
911
- "Codex conversation binding:",
912
- "- Mode: Codex CLI node session",
913
- `- Node: ${formatCodexDisplayText(data.nodeId)}`,
914
- `- Session: ${formatCodexDisplayText(data.sessionId)}`,
915
- `- Workspace: ${formatCodexDisplayText(data.cwd ?? "unknown")}`,
916
- "- Active run: not tracked"
917
- ].join("\n");
918
- const identity = conversationBindingIdentity(data.bindingId);
919
- const threadBinding = await deps.bindingStore.read(identity);
920
- const active = deps.readCodexConversationActiveTurn(identity);
921
- return [
922
- "Codex conversation binding:",
923
- `- Thread: ${formatCodexDisplayText(threadBinding?.threadId ?? "unknown")}`,
924
- `- Workspace: ${formatCodexDisplayText(data.workspaceDir)}`,
925
- `- Model: ${formatCodexDisplayText(threadBinding?.model ?? "default")}`,
926
- `- Fast: ${isCodexFastServiceTier(threadBinding?.serviceTier) ? "on" : "off"}`,
927
- `- Permissions: ${threadBinding ? formatPermissionsMode(threadBinding) : "default"}`,
928
- `- Active run: ${formatCodexDisplayText(active ? active.turnId : "none")}`,
929
- `- Binding: ${formatCodexDisplayText(data.bindingId)}`
930
- ].join("\n");
964
+ function addTag(tags, key, value) {
965
+ if (typeof value === "string" && value.trim()) tags[key] = value.trim();
931
966
  }
932
- async function buildThreads(deps, ctx, pluginConfig, filter) {
933
- const scope = await resolveCommandAppServerScope(deps, ctx, pluginConfig);
934
- return formatThreads(await deps.codexControlRequest(pluginConfig, CODEX_CONTROL_METHODS.listThreads, {
935
- limit: 10,
936
- ...filter.trim() ? { searchTerm: filter.trim() } : {}
937
- }, {
938
- config: ctx.config,
939
- ...scope
940
- }));
967
+ function formatCodexThreadIdForDisplay(threadId) {
968
+ return escapeCodexChatText(formatCodexTextForDisplay(threadId));
941
969
  }
942
- async function handleNativeGoal(deps, ctx, pluginConfig, args) {
943
- const action = (args[0] ?? "status").toLowerCase();
944
- const objective = args.slice(1).join(" ").trim();
945
- const target = await resolveControlTarget(ctx);
946
- if (!target) return "Cannot manage the Codex goal because this command has no stable binding identity.";
947
- const binding = await deps.bindingStore.read(target.identity);
948
- if (!binding?.threadId) return "No Codex thread is attached to this OpenClaw session yet.";
949
- const connection = resolveCodexBindingAppServerConnection({
950
- binding,
951
- authProfileId: binding.authProfileId,
952
- pluginConfig
953
- });
954
- const goalRequestOptions = {
955
- agentDir: target.agentDir,
956
- authProfileId: connection.clientAuthProfileId,
957
- config: ctx.config,
958
- ...connection.usesSupervisionConnection ? { startOptions: connection.appServer.start } : {}
959
- };
960
- if (action === "status" || action === "get") {
961
- if (args.length > 1) return "Usage: /codex goal [status]";
962
- return formatNativeGoal(await deps.codexControlRequest(pluginConfig, CODEX_CONTROL_METHODS.getThreadGoal, { threadId: binding.threadId }, goalRequestOptions));
963
- }
964
- if (action === "clear") {
965
- if (args.length > 1) return "Usage: /codex goal clear";
966
- const response = await deps.codexControlRequest(pluginConfig, CODEX_CONTROL_METHODS.clearThreadGoal, { threadId: binding.threadId }, goalRequestOptions);
967
- return isJsonObject(response) && response.cleared === true ? "Cleared the Codex goal." : "No Codex goal was active.";
968
- }
969
- const requestedStatus = action === "pause" ? "paused" : action === "resume" ? "active" : action === "block" ? "blocked" : action === "complete" ? "complete" : void 0;
970
- const isObjectiveUpdate = action === "set";
971
- if (!requestedStatus && !isObjectiveUpdate || isObjectiveUpdate && !objective) return "Usage: /codex goal [status|set <objective>|pause|resume|block|complete|clear]";
972
- if (requestedStatus && args.length > 1) return `Usage: /codex goal ${action}`;
973
- return formatNativeGoal(await deps.codexControlRequest(pluginConfig, CODEX_CONTROL_METHODS.setThreadGoal, {
974
- threadId: binding.threadId,
975
- ...objective ? { objective } : {},
976
- ...requestedStatus ? { status: requestedStatus } : {}
977
- }, goalRequestOptions));
970
+ function formatCodexValueForDisplay(value) {
971
+ return escapeCodexChatText(formatCodexTextForDisplay(value));
978
972
  }
979
- function formatNativeGoal(response) {
980
- const goal = isJsonObject(response) && isJsonObject(response.goal) ? response.goal : void 0;
981
- if (!goal) return "No Codex goal is active.";
982
- const objective = readString$1(goal, "objective") ?? "unknown";
983
- const status = readString$1(goal, "status") ?? "unknown";
984
- const tokensUsed = typeof goal.tokensUsed === "number" ? goal.tokensUsed : 0;
985
- const tokenBudget = typeof goal.tokenBudget === "number" ? goal.tokenBudget : void 0;
986
- return [
987
- `Codex goal: ${formatCodexDisplayText(objective)}`,
988
- `- Status: ${formatCodexDisplayText(status)}`,
989
- `- Tokens: ${tokensUsed}${tokenBudget === void 0 ? "" : ` / ${tokenBudget}`}`
990
- ].join("\n");
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);
991
977
  }
992
- async function buildCodexCliSessions(deps, args) {
993
- const parsed = parseCodexCliSessionsArgs(args);
994
- if (parsed.help || !parsed.host) return "Usage: /codex sessions --host <node> [filter] [--limit <n>]";
995
- return formatCodexCliSessions(await deps.listCodexCliSessionsOnNode({
996
- requestedNode: parsed.host,
997
- filter: parsed.filter,
998
- limit: parsed.limit
999
- }));
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;
1000
984
  }
1001
- async function resumeThread(deps, ctx, pluginConfig, args) {
1002
- const parsed = parseResumeArgs(args);
1003
- const normalizedThreadId = parsed.threadId?.trim();
1004
- 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>";
1005
- if (parsed.host) return await bindCodexCliNodeSession(deps, ctx, parsed);
1006
- if (!normalizedThreadId || args.length !== 1) return "Usage: /codex resume <thread-id>";
1007
- if (isCurrentSessionModelSelectionLocked(ctx)) return MODEL_SELECTION_LOCKED_MESSAGE;
1008
- if (!ctx.sessionId) return "Cannot attach a Codex thread because this command did not include an OpenClaw session id.";
1009
- const scope = resolveCodexConversationControlScope(ctx);
1010
- const identity = sessionBindingIdentity({
1011
- sessionId: ctx.sessionId,
1012
- sessionKey: ctx.sessionKey,
1013
- agentId: scope.agentId,
1014
- config: ctx.config
1015
- });
1016
- return await deps.bindingStore.withLease(identity, async () => {
1017
- if (!await reclaimCurrentCodexSessionGeneration({
1018
- bindingStore: deps.bindingStore,
1019
- identity,
1020
- config: ctx.config
1021
- })) throw createCodexSessionGenerationSupersededError(identity.sessionId);
1022
- const currentBinding = await deps.bindingStore.read(identity);
1023
- assertCodexBindingMayBeReplaced(currentBinding, "attaching a different resumed thread");
1024
- const authProfileId = resolveCodexAppServerAuthProfileIdForAgent({
1025
- authProfileId: currentBinding?.authProfileId,
1026
- agentDir: scope.agentDir,
1027
- config: ctx.config
1028
- });
1029
- const response = assertCodexThreadResumeResponse(await deps.codexControlRequest(pluginConfig, CODEX_CONTROL_METHODS.resumeThread, {
1030
- threadId: normalizedThreadId,
1031
- excludeTurns: true
1032
- }, {
1033
- config: ctx.config,
1034
- agentDir: scope.agentDir,
1035
- authProfileId,
1036
- sessionKey: ctx.sessionKey,
1037
- sessionId: ctx.sessionId
1038
- }));
1039
- const effectiveThreadId = response.thread.id;
1040
- if (effectiveThreadId !== normalizedThreadId) throw new Error(`Codex thread/resume returned ${effectiveThreadId} for ${normalizedThreadId}`);
1041
- const resumedCwd = response.thread.cwd;
1042
- if (typeof resumedCwd !== "string") throw new Error(`Codex thread/resume returned no cwd for ${normalizedThreadId}`);
1043
- const modelProvider = normalizeCodexAppServerBindingModelProvider({
1044
- authProfileId,
1045
- modelProvider: response.modelProvider ?? void 0,
1046
- agentDir: scope.agentDir,
1047
- config: ctx.config
1048
- });
1049
- assertCodexBindingMayBeReplaced(await deps.bindingStore.read(identity), "committing a different resumed thread");
1050
- if (!await deps.bindingStore.mutate(identity, {
1051
- kind: "set",
1052
- binding: {
1053
- threadId: effectiveThreadId,
1054
- cwd: resumedCwd,
1055
- authProfileId,
1056
- model: response.model,
1057
- modelProvider,
1058
- historyCoveredThrough: (/* @__PURE__ */ new Date()).toISOString()
1059
- }
1060
- })) throw new Error("Codex thread binding changed while attaching the resumed thread.");
1061
- return `Attached this OpenClaw session to Codex thread ${formatCodexDisplayText(effectiveThreadId)}.`;
1062
- });
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;
1063
991
  }
1064
- async function bindCodexCliNodeSession(deps, ctx, parsed) {
1065
- if (!parsed.threadId || !parsed.host || parsed.bindHere !== true) return "Usage: /codex resume <session-id> --host <node> --bind here";
1066
- if (isCurrentSessionModelSelectionLocked(ctx)) return MODEL_SELECTION_LOCKED_MESSAGE;
1067
- if (ctx.sessionId) {
1068
- const scope = resolveCodexConversationControlScope(ctx);
1069
- assertCodexBindingMayBeReplaced(await deps.bindingStore.read(sessionBindingIdentity({
1070
- sessionId: ctx.sessionId,
1071
- sessionKey: ctx.sessionKey,
1072
- agentId: scope.agentId,
1073
- config: ctx.config
1074
- })), "binding a Codex CLI node session");
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);
1075
997
  }
1076
- const resolved = await deps.resolveCodexCliSessionForBindingOnNode({
1077
- requestedNode: parsed.host,
1078
- sessionId: parsed.threadId
1079
- });
1080
- if (!resolved.session) return `No Codex CLI session ${formatCodexDisplayText(parsed.threadId)} was found on ${formatCodexDisplayText(parsed.host)}.`;
1081
- const nodeId = resolved.node.nodeId;
1082
- if (!nodeId) return "Cannot bind Codex CLI session because the selected node did not include a node id.";
1083
- const scope = resolveCodexConversationControlScope(ctx);
1084
- const data = createCodexCliNodeConversationBindingData({
1085
- nodeId,
1086
- sessionId: parsed.threadId,
1087
- agentId: scope.agentId,
1088
- cwd: resolved.session?.cwd
1089
- });
1090
- const summary = `Codex CLI session ${formatCodexDisplayText(parsed.threadId)} on ${formatCodexDisplayText(nodeId)}`;
1091
- const request = await ctx.requestConversationBinding({
1092
- summary,
1093
- detachHint: "/codex detach",
1094
- data
1095
- });
1096
- if (request.status === "bound") return `Bound this conversation to Codex CLI session ${formatCodexDisplayText(parsed.threadId)} on ${formatCodexDisplayText(nodeId)}.`;
1097
- if (request.status === "pending") return request.reply.text ?? "Codex CLI session binding is pending approval.";
1098
- return formatCodexDisplayText(request.message);
998
+ map.set(key, now);
1099
999
  }
1100
- async function stopConversationTurn(deps, ctx, pluginConfig) {
1101
- const target = await resolveControlTarget(ctx);
1102
- if (!target) return "Cannot stop Codex because this command did not include a stable binding identity.";
1103
- return (await deps.stopCodexConversationTurn({
1104
- identity: target.identity,
1105
- bindingStore: deps.bindingStore,
1106
- pluginConfig,
1107
- agentDir: target.agentDir,
1108
- config: ctx.config
1109
- })).message;
1000
+ function pruneCodexDiagnosticsCooldowns(now) {
1001
+ pruneCodexDiagnosticsCooldownMap(lastCodexDiagnosticsUploadByThread, now);
1002
+ pruneCodexDiagnosticsCooldownMap(lastCodexDiagnosticsUploadByScope, now);
1110
1003
  }
1111
- async function steerConversationTurn(deps, ctx, pluginConfig, message) {
1112
- const target = await resolveControlTarget(ctx);
1113
- if (!target) return "Cannot steer Codex because this command did not include a stable binding identity.";
1114
- return (await deps.steerCodexConversationTurn({
1115
- identity: target.identity,
1116
- bindingStore: deps.bindingStore,
1117
- message,
1118
- pluginConfig,
1119
- agentDir: target.agentDir,
1120
- config: ctx.config
1121
- })).message;
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";
1122
1009
  }
1123
- async function setConversationModel(deps, ctx, pluginConfig, args) {
1124
- if (args.length > 1) return "Usage: /codex model <model>";
1125
- const [model = ""] = args;
1126
- const normalized = model.trim();
1127
- if (normalized && isCurrentSessionModelSelectionLocked(ctx)) return MODEL_SELECTION_LOCKED_MESSAGE;
1128
- const target = await resolveControlTarget(ctx);
1129
- if (!target) return "Cannot set Codex model because this command did not include a stable binding identity.";
1130
- if (!normalized) {
1131
- const binding = await deps.bindingStore.read(target.identity);
1132
- return binding?.model ? `Codex model: ${formatCodexDisplayText(binding.model)}` : "Usage: /codex model <model>";
1133
- }
1134
- return await deps.setCodexConversationModel({
1135
- identity: target.identity,
1136
- bindingStore: deps.bindingStore,
1137
- pluginConfig,
1138
- model: normalized,
1139
- agentDir: target.agentDir,
1140
- config: ctx.config
1141
- });
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}\``;
1142
1014
  }
1143
- async function setConversationFastMode(deps, ctx, args) {
1144
- if (args.length > 1) return "Usage: /codex fast [on|off|status]";
1145
- const target = await resolveControlTarget(ctx);
1146
- if (!target) return "Cannot set Codex fast mode because this command did not include a stable binding identity.";
1147
- const value = args[0];
1148
- const parsed = parseCodexFastModeArg(value);
1149
- if (value && parsed == null && value.trim().toLowerCase() !== "status") return "Usage: /codex fast [on|off|status]";
1150
- return await deps.setCodexConversationFastMode({
1151
- identity: target.identity,
1152
- bindingStore: deps.bindingStore,
1153
- enabled: parsed
1154
- });
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;
1155
1017
  }
1156
- async function setConversationPermissions(deps, ctx, args) {
1157
- if (args.length > 1) return "Usage: /codex permissions [default|yolo|status]";
1158
- const target = await resolveControlTarget(ctx);
1159
- if (!target) return "Cannot set Codex permissions because this command did not include a stable binding identity.";
1160
- const value = args[0];
1161
- const parsed = parseCodexPermissionsModeArg(value);
1162
- if (value && !parsed && value.trim().toLowerCase() !== "status") return "Usage: /codex permissions [default|yolo|status]";
1163
- return await deps.setCodexConversationPermissions({
1164
- identity: target.identity,
1165
- bindingStore: deps.bindingStore,
1166
- mode: parsed
1167
- });
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
+ };
1168
1053
  }
1054
+ //#endregion
1055
+ //#region extensions/codex/src/command-handler-scope.ts
1169
1056
  async function resolveControlTarget(ctx) {
1170
1057
  const data = readCodexConversationBindingData(await ctx.getCurrentConversationBinding());
1171
1058
  const scope = resolveCodexConversationControlScope(ctx);
@@ -1226,6 +1113,8 @@ function resolveCodexConversationControlScope(ctx) {
1226
1113
  agentDir: resolveAgentDir(ctx.config, sessionAgentId)
1227
1114
  };
1228
1115
  }
1116
+ //#endregion
1117
+ //#region extensions/codex/src/command-diagnostics.ts
1229
1118
  async function handleCodexDiagnosticsFeedback(deps, ctx, pluginConfig, args, commandPrefix) {
1230
1119
  if (ctx.senderIsOwner !== true) return { text: "Only an owner can send Codex diagnostics." };
1231
1120
  const parsed = parseDiagnosticsArgs(args);
@@ -1449,316 +1338,463 @@ async function resolveCodexDiagnosticsTargets(deps, ctx) {
1449
1338
  }
1450
1339
  return targets;
1451
1340
  }
1452
- async function resolvePendingCodexDiagnosticsTargets(deps, targets, config) {
1453
- const resolved = [];
1454
- for (const target of targets) {
1455
- const binding = await deps.bindingStore.read(target.identity);
1456
- if (!binding?.threadId) continue;
1457
- resolved.push(resolveCodexDiagnosticsTarget(target, binding, config));
1458
- }
1459
- return resolved;
1460
- }
1461
- function resolveCodexDiagnosticsTarget(target, binding, config) {
1462
- const candidate = {
1463
- identity: target.identity,
1464
- agentDir: target.agentDir,
1465
- sessionKey: target.sessionKey,
1466
- sessionId: target.sessionId,
1467
- channel: target.channel,
1468
- channelId: target.channelId,
1469
- accountId: target.accountId,
1470
- messageThreadId: target.messageThreadId,
1471
- threadParentId: target.threadParentId
1472
- };
1473
- if (binding.connectionScope === "supervision") return {
1474
- ...candidate,
1475
- threadId: binding.threadId,
1476
- connectionScope: binding.connectionScope,
1477
- appServerRuntimeFingerprint: binding.appServerRuntimeFingerprint,
1478
- pendingSupervisionBranch: binding.pendingSupervisionBranch
1479
- };
1480
- const authProfileId = resolveCodexAppServerAuthProfileIdForAgent({
1481
- authProfileId: binding.authProfileId,
1482
- agentDir: target.agentDir,
1483
- config
1484
- });
1485
- return {
1486
- ...candidate,
1487
- threadId: binding.threadId,
1488
- authProfileId
1489
- };
1490
- }
1491
- function codexDiagnosticsTargetsMatch(expected, actual) {
1492
- const fingerprint = (target) => JSON.stringify([
1493
- bindingStoreKey(target.identity),
1494
- target.threadId,
1495
- target.connectionScope ?? null,
1496
- target.pendingSupervisionBranch?.connectionFingerprint ?? target.appServerRuntimeFingerprint ?? null,
1497
- target.authProfileId ?? null
1498
- ]);
1499
- const expectedTargets = expected.map(fingerprint).toSorted();
1500
- const actualTargets = actual.map(fingerprint).toSorted();
1501
- return expectedTargets.length === actualTargets.length && expectedTargets.every((target, index) => target === actualTargets[index]);
1502
- }
1503
- function formatCodexDiagnosticsUploadResult(sent, failed) {
1504
- const lines = [];
1505
- if (sent.length > 0) {
1506
- lines.push("Codex diagnostics sent to OpenAI servers:");
1507
- lines.push(...formatCodexDiagnosticsTargetLines(sent));
1508
- lines.push("Included Codex logs and spawned Codex subthreads when available.");
1509
- }
1510
- if (failed.length > 0) {
1511
- if (lines.length > 0) lines.push("");
1512
- lines.push("Could not send Codex diagnostics:");
1513
- lines.push(...failed.map(({ target, error }) => `${formatCodexDiagnosticsTargetLine(target)}: ${formatCodexErrorForDisplay(error)}`));
1514
- lines.push("Inspect locally:");
1515
- lines.push(...failed.map(({ target }) => `- ${formatCodexResumeCommandForDisplay(target.threadId)}`));
1516
- }
1517
- return lines.join("\n");
1518
- }
1519
- function formatCodexDiagnosticsTargetLines(targets) {
1520
- return targets.flatMap((target, index) => {
1521
- const lines = formatCodexDiagnosticsTargetBlock(target, index);
1522
- return index < targets.length - 1 ? [...lines, ""] : lines;
1523
- });
1524
- }
1525
- function formatCodexDiagnosticsTargetBlock(target, index) {
1526
- const lines = [`Session ${index + 1}`];
1527
- if (target.channel) lines.push(`Channel: ${formatCodexValueForDisplay(target.channel)}`);
1528
- if (target.sessionKey) lines.push(`OpenClaw session key: ${formatCodexCopyableValueForDisplay(target.sessionKey)}`);
1529
- if (target.sessionId) lines.push(`OpenClaw session id: ${formatCodexCopyableValueForDisplay(target.sessionId)}`);
1530
- lines.push(`Codex thread id: ${formatCodexCopyableValueForDisplay(target.threadId)}`);
1531
- lines.push(`Inspect locally: ${formatCodexResumeCommandForDisplay(target.threadId)}`);
1532
- return lines;
1533
- }
1534
- function formatCodexDiagnosticsTargetLine(target) {
1535
- const parts = [];
1536
- if (target.channel) parts.push(`channel ${formatCodexValueForDisplay(target.channel)}`);
1537
- const sessionLabel = target.sessionId || target.sessionKey;
1538
- if (sessionLabel) parts.push(`OpenClaw session ${formatCodexValueForDisplay(sessionLabel)}`);
1539
- parts.push(`Codex thread ${formatCodexThreadIdForDisplay(target.threadId)}`);
1540
- return `- ${parts.join(", ")}`;
1541
- }
1542
- function normalizeDiagnosticsReason(note) {
1543
- const normalized = normalizeOptionalString(note);
1544
- return normalized ? truncateUtf16Safe(normalized, CODEX_DIAGNOSTICS_REASON_MAX_CHARS) : void 0;
1545
- }
1546
- function parseDiagnosticsArgs(args) {
1547
- const [action, token, ...extra] = splitArgs(args);
1548
- const normalizedAction = action?.toLowerCase();
1549
- if ((normalizedAction === "confirm" || normalizedAction === "--confirm") && token && extra.length === 0) return {
1550
- action: "confirm",
1551
- token
1552
- };
1553
- if ((normalizedAction === "cancel" || normalizedAction === "--cancel") && token && extra.length === 0) return {
1554
- action: "cancel",
1555
- token
1556
- };
1557
- if (normalizedAction === "confirm" || normalizedAction === "--confirm" || normalizedAction === "cancel" || normalizedAction === "--cancel") return { action: "usage" };
1558
- return {
1559
- action: "request",
1560
- note: args
1561
- };
1562
- }
1563
- function formatDiagnosticsUsage(commandPrefix) {
1564
- return [
1565
- `Usage: ${commandPrefix} [note]`,
1566
- `Usage: ${commandPrefix} confirm <token>`,
1567
- `Usage: ${commandPrefix} cancel <token>`
1568
- ].join("\n");
1569
- }
1570
- function createCodexDiagnosticsConfirmation(params) {
1571
- prunePendingCodexDiagnosticsConfirmations(params.now);
1572
- if (!pendingCodexDiagnosticsConfirmationTokensByScope.has(params.scopeKey) && pendingCodexDiagnosticsConfirmationTokensByScope.size >= CODEX_DIAGNOSTICS_CONFIRMATION_MAX_SCOPES) {
1573
- const oldestScopeKey = pendingCodexDiagnosticsConfirmationTokensByScope.keys().next().value;
1574
- if (typeof oldestScopeKey === "string") deletePendingCodexDiagnosticsConfirmationScope(oldestScopeKey);
1575
- }
1576
- const scopeTokens = pendingCodexDiagnosticsConfirmationTokensByScope.get(params.scopeKey) ?? [];
1577
- while (scopeTokens.length >= CODEX_DIAGNOSTICS_CONFIRMATION_MAX_REQUESTS_PER_SCOPE) {
1578
- const oldestToken = scopeTokens.shift();
1579
- if (!oldestToken) break;
1580
- pendingCodexDiagnosticsConfirmations.delete(oldestToken);
1581
- }
1582
- const token = crypto.randomBytes(6).toString("hex");
1583
- scopeTokens.push(token);
1584
- pendingCodexDiagnosticsConfirmationTokensByScope.set(params.scopeKey, scopeTokens);
1585
- pendingCodexDiagnosticsConfirmations.set(token, {
1586
- token,
1587
- targets: params.targets,
1588
- note: params.note,
1589
- senderId: params.senderId,
1590
- channel: params.channel,
1591
- accountId: params.accountId,
1592
- channelId: params.channelId,
1593
- messageThreadId: params.messageThreadId,
1594
- threadParentId: params.threadParentId,
1595
- sessionKey: params.sessionKey,
1596
- scopeKey: params.scopeKey,
1597
- ...params.privateRouted === void 0 ? {} : { privateRouted: params.privateRouted },
1598
- createdAt: params.now
1599
- });
1600
- return token;
1601
- }
1602
- function readCodexDiagnosticsConfirmationScope(ctx) {
1603
- return {
1604
- accountId: normalizeCodexDiagnosticsScopeField(ctx.accountId),
1605
- channelId: normalizeCodexDiagnosticsScopeField(ctx.channelId),
1606
- messageThreadId: typeof ctx.messageThreadId === "string" || typeof ctx.messageThreadId === "number" ? normalizeCodexDiagnosticsScopeField(String(ctx.messageThreadId)) : void 0,
1607
- threadParentId: normalizeCodexDiagnosticsScopeField(ctx.threadParentId),
1608
- sessionKey: normalizeCodexDiagnosticsScopeField(ctx.sessionKey)
1609
- };
1610
- }
1611
- function readCodexDiagnosticsScopeMismatch(pending, ctx) {
1612
- const current = readCodexDiagnosticsConfirmationScope(ctx);
1613
- if (pending.accountId !== current.accountId) return {
1614
- confirmMessage: "This Codex diagnostics confirmation belongs to a different account.",
1615
- cancelMessage: "This Codex diagnostics confirmation belongs to a different account."
1616
- };
1617
- if (pending.privateRouted) return;
1618
- if (pending.channelId !== current.channelId) return {
1619
- confirmMessage: "This Codex diagnostics confirmation belongs to a different channel instance.",
1620
- cancelMessage: "This Codex diagnostics confirmation belongs to a different channel instance."
1621
- };
1622
- if (pending.messageThreadId !== current.messageThreadId) return {
1623
- confirmMessage: "This Codex diagnostics confirmation belongs to a different thread.",
1624
- cancelMessage: "This Codex diagnostics confirmation belongs to a different thread."
1341
+ async function resolvePendingCodexDiagnosticsTargets(deps, targets, config) {
1342
+ const resolved = [];
1343
+ for (const target of targets) {
1344
+ const binding = await deps.bindingStore.read(target.identity);
1345
+ if (!binding?.threadId) continue;
1346
+ resolved.push(resolveCodexDiagnosticsTarget(target, binding, config));
1347
+ }
1348
+ return resolved;
1349
+ }
1350
+ 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
1625
1361
  };
1626
- if (pending.threadParentId !== current.threadParentId) return {
1627
- confirmMessage: "This Codex diagnostics confirmation belongs to a different parent thread.",
1628
- cancelMessage: "This Codex diagnostics confirmation belongs to a different parent thread."
1362
+ if (binding.connectionScope === "supervision") return {
1363
+ ...candidate,
1364
+ threadId: binding.threadId,
1365
+ connectionScope: binding.connectionScope,
1366
+ appServerRuntimeFingerprint: binding.appServerRuntimeFingerprint,
1367
+ pendingSupervisionBranch: binding.pendingSupervisionBranch
1629
1368
  };
1630
- if (pending.sessionKey !== current.sessionKey) return {
1631
- confirmMessage: "This Codex diagnostics confirmation belongs to a different session.",
1632
- cancelMessage: "This Codex diagnostics confirmation belongs to a different session."
1369
+ const authProfileId = resolveCodexAppServerAuthProfileIdForAgent({
1370
+ authProfileId: binding.authProfileId,
1371
+ agentDir: target.agentDir,
1372
+ config
1373
+ });
1374
+ return {
1375
+ ...candidate,
1376
+ threadId: binding.threadId,
1377
+ authProfileId
1633
1378
  };
1634
1379
  }
1635
- function readPendingCodexDiagnosticsConfirmation(token, now) {
1636
- prunePendingCodexDiagnosticsConfirmations(now);
1637
- return pendingCodexDiagnosticsConfirmations.get(token);
1638
- }
1639
- function prunePendingCodexDiagnosticsConfirmations(now) {
1640
- for (const [token, pending] of pendingCodexDiagnosticsConfirmations) if (now - pending.createdAt >= CODEX_DIAGNOSTICS_CONFIRMATION_TTL_MS) deletePendingCodexDiagnosticsConfirmation(token);
1641
- }
1642
- function deletePendingCodexDiagnosticsConfirmation(token) {
1643
- const pending = pendingCodexDiagnosticsConfirmations.get(token);
1644
- pendingCodexDiagnosticsConfirmations.delete(token);
1645
- if (!pending) return;
1646
- const scopeTokens = pendingCodexDiagnosticsConfirmationTokensByScope.get(pending.scopeKey);
1647
- if (!scopeTokens) return;
1648
- const tokenIndex = scopeTokens.indexOf(token);
1649
- if (tokenIndex >= 0) scopeTokens.splice(tokenIndex, 1);
1650
- if (scopeTokens.length === 0) pendingCodexDiagnosticsConfirmationTokensByScope.delete(pending.scopeKey);
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
+ }));
1651
1392
  }
1652
- function deletePendingCodexDiagnosticsConfirmationScope(scopeKey) {
1653
- const scopeTokens = pendingCodexDiagnosticsConfirmationTokensByScope.get(scopeKey) ?? [];
1654
- for (const token of scopeTokens) pendingCodexDiagnosticsConfirmations.delete(token);
1655
- pendingCodexDiagnosticsConfirmationTokensByScope.delete(scopeKey);
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
1437
+ });
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.` };
1656
1441
  }
1657
- function buildDiagnosticsTags(ctx) {
1658
- const tags = { source: CODEX_DIAGNOSTICS_SOURCE };
1659
- addTag(tags, "channel", ctx.channel);
1660
- return tags;
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.";
1661
1449
  }
1662
- function addTag(tags, key, value) {
1663
- if (typeof value === "string" && value.trim()) tags[key] = value.trim();
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");
1664
1475
  }
1665
- function formatCodexThreadIdForDisplay(threadId) {
1666
- return escapeCodexChatText(formatCodexTextForDisplay(threadId));
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
+ }));
1667
1485
  }
1668
- function formatCodexValueForDisplay(value) {
1669
- return escapeCodexChatText(formatCodexTextForDisplay(value));
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
+ }));
1670
1494
  }
1671
- function formatCodexCopyableValueForDisplay(value) {
1672
- const safe = formatCodexTextForDisplay(value);
1673
- if (CODEX_RESUME_SAFE_THREAD_ID_PATTERN.test(safe)) return `\`${safe}\``;
1674
- return escapeCodexChatText(safe);
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
+ });
1675
1557
  }
1676
- function formatCodexTextForDisplay(value) {
1677
- let safe = "";
1678
- for (const character of value) {
1679
- const codePoint = character.codePointAt(0);
1680
- safe += codePoint != null && isUnsafeDisplayCodePoint(codePoint) ? "?" : character;
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");
1681
1569
  }
1682
- safe = safe.trim();
1683
- return safe || "<unknown>";
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
1589
+ });
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);
1684
1593
  }
1685
- function escapeCodexChatText(value) {
1686
- return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("@", "@").replaceAll("`", "`").replaceAll("[", "[").replaceAll("]", "]").replaceAll("(", "(").replaceAll(")", ")").replaceAll("*", "∗").replaceAll("_", "_").replaceAll("~", "~").replaceAll("|", "|");
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
+ });
1687
1630
  }
1688
- function readCodexDiagnosticsCooldownMs(threadId, now) {
1689
- const lastSentAt = lastCodexDiagnosticsUploadByThread.get(threadId);
1690
- if (!lastSentAt) return 0;
1691
- const remainingMs = Math.max(0, CODEX_DIAGNOSTICS_COOLDOWN_MS - (now - lastSentAt));
1692
- if (remainingMs === 0) lastCodexDiagnosticsUploadByThread.delete(threadId);
1693
- return remainingMs;
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";
1694
1635
  }
1695
- function readCodexDiagnosticsTargetsCooldownMessage(targets, ctx, now, options = {}) {
1696
- for (const target of targets) {
1697
- const cooldownMs = readCodexDiagnosticsCooldownMs(target.threadId, now);
1698
- if (cooldownMs > 0) {
1699
- 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.`;
1700
- return `Codex diagnostics were already sent for thread ${formatCodexThreadIdForDisplay(target.threadId)} recently. Try again in ${Math.ceil(cooldownMs / 1e3)}s.`;
1701
- }
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;
1702
1650
  }
1703
- const scopeCooldownMs = readCodexDiagnosticsScopeCooldownMs(options.cooldownScope ?? readCodexDiagnosticsCooldownScope(ctx), now);
1704
- if (scopeCooldownMs > 0) return `Codex diagnostics were already sent for this account or channel recently. Try again in ${Math.ceil(scopeCooldownMs / 1e3)}s.`;
1705
1651
  }
1706
- function readCodexDiagnosticsScopeCooldownMs(scope, now) {
1707
- const lastSentAt = lastCodexDiagnosticsUploadByScope.get(scope);
1708
- if (!lastSentAt) return 0;
1709
- const remainingMs = Math.max(0, CODEX_DIAGNOSTICS_COOLDOWN_MS - (now - lastSentAt));
1710
- if (remainingMs === 0) lastCodexDiagnosticsUploadByScope.delete(scope);
1711
- return remainingMs;
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);
1712
1656
  }
1713
- function recordCodexDiagnosticsUpload(threadId, ctx, now, cooldownScope) {
1714
- pruneCodexDiagnosticsCooldowns(now);
1715
- recordBoundedCodexDiagnosticsCooldown(lastCodexDiagnosticsUploadByScope, cooldownScope ?? readCodexDiagnosticsCooldownScope(ctx), CODEX_DIAGNOSTICS_COOLDOWN_MAX_SCOPES, now);
1716
- recordBoundedCodexDiagnosticsCooldown(lastCodexDiagnosticsUploadByThread, threadId, CODEX_DIAGNOSTICS_COOLDOWN_MAX_THREADS, now);
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;
1717
1663
  }
1718
- function recordBoundedCodexDiagnosticsCooldown(map, key, maxSize, now) {
1719
- if (!map.has(key)) while (map.size >= maxSize) {
1720
- const oldestKey = map.keys().next().value;
1721
- if (typeof oldestKey !== "string") break;
1722
- map.delete(oldestKey);
1723
- }
1724
- map.set(key, now);
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));
1725
1679
  }
1726
- function readCodexDiagnosticsCooldownScope(ctx) {
1727
- const scope = readCodexDiagnosticsConfirmationScope(ctx);
1728
- const payload = JSON.stringify({
1729
- accountId: scope.accountId ?? null,
1730
- channelId: scope.channelId ?? null,
1731
- sessionKey: scope.sessionKey ?? null,
1732
- messageThreadId: scope.messageThreadId ?? null,
1733
- threadParentId: scope.threadParentId ?? null,
1734
- senderId: normalizeCodexDiagnosticsScopeField(ctx.senderId) ?? null,
1735
- channel: normalizeCodexDiagnosticsScopeField(ctx.channel) ?? ""
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
1736
1691
  });
1737
- return crypto.createHash("sha256").update(payload).digest("hex");
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));
1701
+ }
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.";
1706
+ }
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));
1738
1716
  }
1739
- function pruneCodexDiagnosticsCooldowns(now) {
1740
- pruneCodexDiagnosticsCooldownMap(lastCodexDiagnosticsUploadByThread, now);
1741
- pruneCodexDiagnosticsCooldownMap(lastCodexDiagnosticsUploadByScope, now);
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");
1742
1729
  }
1743
- function pruneCodexDiagnosticsCooldownMap(map, now) {
1744
- for (const [key, lastSentAt] of map) if (now - lastSentAt >= CODEX_DIAGNOSTICS_COOLDOWN_MS) map.delete(key);
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;
1745
1740
  }
1746
- function formatCodexErrorForDisplay(error) {
1747
- return escapeCodexChatText(truncateUtf16Safe(formatCodexTextForDisplay(error), CODEX_DIAGNOSTICS_ERROR_MAX_CHARS)) || "unknown error";
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;
1748
1752
  }
1749
- function formatCodexResumeCommandForDisplay(threadId) {
1750
- const safeThreadId = formatCodexTextForDisplay(threadId);
1751
- if (!CODEX_RESUME_SAFE_THREAD_ID_PATTERN.test(safeThreadId)) return "run codex resume and paste the thread id shown above";
1752
- return `\`codex resume ${safeThreadId}\``;
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>";
1763
+ }
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
+ });
1753
1772
  }
1754
- function isUnsafeDisplayCodePoint(codePoint) {
1755
- 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;
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
1784
+ });
1756
1785
  }
1757
- function normalizeCodexDiagnosticsScopeField(value) {
1758
- const normalized = normalizeOptionalString(value);
1759
- if (!normalized) return;
1760
- if (normalized.length <= CODEX_DIAGNOSTICS_SCOPE_FIELD_MAX_CHARS) return normalized;
1761
- return `sha256:${crypto.createHash("sha256").update(normalized).digest("hex")}`;
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
+ });
1762
1798
  }
1763
1799
  async function startThreadAction(deps, ctx, pluginConfig, kind, args) {
1764
1800
  const label = kind === "compact" ? "compaction" : "review";
@@ -1783,273 +1819,248 @@ async function startThreadAction(deps, ctx, pluginConfig, kind, args) {
1783
1819
  });
1784
1820
  return `Started Codex ${label} for thread ${formatCodexDisplayText(binding.threadId)}.`;
1785
1821
  }
1786
- function splitArgs(value) {
1787
- const input = value ?? "";
1788
- const args = [];
1789
- let current = "";
1790
- let quote;
1791
- let escaping = false;
1792
- let tokenStarted = false;
1793
- for (const char of input) {
1794
- if (escaping) {
1795
- current += char;
1796
- escaping = false;
1797
- tokenStarted = true;
1798
- continue;
1799
- }
1800
- if (char === "\\" && quote !== "'") {
1801
- escaping = true;
1802
- tokenStarted = true;
1803
- continue;
1804
- }
1805
- if (quote) {
1806
- if (char === quote) quote = void 0;
1807
- else current += char;
1808
- tokenStarted = true;
1809
- continue;
1810
- }
1811
- if (char === "\"" || char === "'") {
1812
- quote = char;
1813
- tokenStarted = true;
1814
- continue;
1815
- }
1816
- if (/\s/.test(char)) {
1817
- if (tokenStarted) {
1818
- args.push(current);
1819
- current = "";
1820
- tokenStarted = false;
1821
- }
1822
- continue;
1823
- }
1824
- current += char;
1825
- tokenStarted = true;
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();
1826
1831
  }
1827
- if (escaping) current += "\\";
1828
- if (tokenStarted) args.push(current);
1829
- return args;
1830
- }
1831
- function parseBindArgs(args) {
1832
- const parsed = {};
1833
- for (let index = 0; index < args.length; index += 1) {
1834
- const arg = expectDefined(args[index], "current Codex bind argument");
1835
- if (arg === "--help" || arg === "-h") {
1836
- parsed.help = true;
1837
- continue;
1838
- }
1839
- if (arg === "--cwd") {
1840
- const value = readRequiredOptionValue(args, index);
1841
- if (!value || parsed.cwd !== void 0) {
1842
- parsed.help = true;
1843
- continue;
1844
- }
1845
- parsed.cwd = value;
1846
- index += 1;
1847
- continue;
1848
- }
1849
- if (arg === "--model") {
1850
- const value = readRequiredOptionValue(args, index);
1851
- if (!value || parsed.model !== void 0) {
1852
- parsed.help = true;
1853
- continue;
1854
- }
1855
- parsed.model = value;
1856
- index += 1;
1857
- continue;
1858
- }
1859
- if (arg === "--provider" || arg === "--model-provider") {
1860
- const value = readRequiredOptionValue(args, index);
1861
- if (!value || parsed.provider !== void 0) {
1862
- parsed.help = true;
1863
- continue;
1864
- }
1865
- parsed.provider = value;
1866
- index += 1;
1867
- continue;
1868
- }
1869
- if (!arg.startsWith("-") && !parsed.threadId) {
1870
- parsed.threadId = arg;
1871
- continue;
1872
- }
1873
- parsed.help = true;
1832
+ if (normalized === "help") {
1833
+ if (args.length > 0) return { text: "Usage: /codex plugins help" };
1834
+ return { text: buildPluginsHelp() };
1874
1835
  }
1875
- parsed.threadId = normalizeOptionalString(parsed.threadId);
1876
- parsed.cwd = normalizeOptionalString(parsed.cwd);
1877
- parsed.model = normalizeOptionalString(parsed.model);
1878
- parsed.provider = normalizeOptionalString(parsed.provider);
1879
- return parsed;
1880
- }
1881
- function parseCodexCliSessionsArgs(args) {
1882
- const parsed = { filter: "" };
1883
- const filter = [];
1884
- for (let index = 0; index < args.length; index += 1) {
1885
- const arg = expectDefined(args[index], "current Codex sessions argument");
1886
- if (arg === "--help" || arg === "-h") {
1887
- parsed.help = true;
1888
- continue;
1889
- }
1890
- if (arg === "--host" || arg === "--node") {
1891
- const value = readRequiredOptionValue(args, index);
1892
- if (!value || parsed.host !== void 0) {
1893
- parsed.help = true;
1894
- continue;
1895
- }
1896
- parsed.host = value;
1897
- index += 1;
1898
- continue;
1899
- }
1900
- if (arg === "--limit") {
1901
- const parsedLimit = parseStrictPositiveInteger(readRequiredOptionValue(args, index));
1902
- if (parsedLimit === void 0) {
1903
- parsed.help = true;
1904
- continue;
1905
- }
1906
- parsed.limit = parsedLimit;
1907
- index += 1;
1908
- continue;
1909
- }
1910
- if (arg.startsWith("-")) {
1911
- parsed.help = true;
1912
- continue;
1913
- }
1914
- filter.push(arg);
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 }) };
1915
1840
  }
1916
- parsed.host = normalizeOptionalString(parsed.host);
1917
- parsed.filter = filter.join(" ").trim();
1918
- return parsed;
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()}` };
1919
1859
  }
1920
- function parseResumeArgs(args) {
1921
- const parsed = {};
1922
- for (let index = 0; index < args.length; index += 1) {
1923
- const arg = expectDefined(args[index], "current Codex resume argument");
1924
- if (arg === "--help" || arg === "-h") {
1925
- parsed.help = true;
1926
- continue;
1927
- }
1928
- if (arg === "--host" || arg === "--node") {
1929
- const value = readRequiredOptionValue(args, index);
1930
- if (!value || parsed.host !== void 0) {
1931
- parsed.help = true;
1932
- continue;
1933
- }
1934
- parsed.host = value;
1935
- index += 1;
1936
- continue;
1937
- }
1938
- if (arg === "--bind") {
1939
- if (readRequiredOptionValue(args, index) !== "here" || parsed.bindHere !== void 0) {
1940
- parsed.help = true;
1941
- continue;
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"
1942
1892
  }
1943
- parsed.bindHere = true;
1944
- index += 1;
1945
- continue;
1946
- }
1947
- if (!arg.startsWith("-") && !parsed.threadId) {
1948
- parsed.threadId = arg;
1949
- continue;
1950
- }
1951
- parsed.help = true;
1952
- }
1953
- parsed.threadId = normalizeOptionalString(parsed.threadId);
1954
- parsed.host = normalizeOptionalString(parsed.host);
1955
- return parsed;
1893
+ ])
1894
+ };
1956
1895
  }
1957
- function parseComputerUseArgs(args) {
1958
- const parsed = {
1959
- action: "status",
1960
- overrides: {},
1961
- hasOverrides: false,
1962
- persistentIdentity: {}
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)
1963
1934
  };
1964
- let sawAction = false;
1965
- for (let index = 0; index < args.length; index += 1) {
1966
- const arg = args[index];
1967
- if (arg === "--help" || arg === "-h") {
1968
- parsed.help = true;
1969
- continue;
1970
- }
1971
- if (arg === "status" || arg === "install") {
1972
- if (sawAction) {
1973
- parsed.help = true;
1974
- continue;
1975
- }
1976
- sawAction = true;
1977
- parsed.action = arg;
1978
- continue;
1979
- }
1980
- if (arg === "--source" || arg === "--marketplace-source") {
1981
- const value = readRequiredOptionValue(args, index);
1982
- if (!value || parsed.overrides.marketplaceSource !== void 0) {
1983
- parsed.help = true;
1984
- continue;
1985
- }
1986
- parsed.overrides.marketplaceSource = value;
1987
- index += 1;
1988
- continue;
1989
- }
1990
- if (arg === "--marketplace-path" || arg === "--path") {
1991
- const value = readRequiredOptionValue(args, index);
1992
- if (!value || parsed.overrides.marketplacePath !== void 0) {
1993
- parsed.help = true;
1994
- continue;
1995
- }
1996
- parsed.overrides.marketplacePath = value;
1997
- index += 1;
1998
- continue;
1999
- }
2000
- if (arg === "--marketplace") {
2001
- const value = readRequiredOptionValue(args, index);
2002
- if (!value || parsed.overrides.marketplaceName !== void 0) {
2003
- parsed.help = true;
2004
- continue;
2005
- }
2006
- parsed.overrides.marketplaceName = value;
2007
- index += 1;
2008
- continue;
2009
- }
2010
- if (arg === "--plugin" || arg === "--server" || arg === "--mcp-server") {
2011
- const value = readRequiredOptionValue(args, index);
2012
- const configKey = arg === "--plugin" ? "pluginName" : "mcpServerName";
2013
- if (!value || parsed.persistentIdentity[configKey] !== void 0) {
2014
- parsed.help = true;
2015
- continue;
2016
- }
2017
- parsed.persistentIdentity[configKey] = value.trim();
2018
- index += 1;
2019
- continue;
2020
- }
2021
- parsed.help = true;
2022
- }
2023
- parsed.overrides = normalizeComputerUseStringOverrides(parsed.overrides);
2024
- parsed.hasOverrides = Object.values(parsed.overrides).some(Boolean);
2025
- return parsed;
2026
1935
  }
2027
- function formatComputerUsePersistentIdentityMigration(parsed) {
2028
- const configPrefix = "plugins.entries.codex.config.computerUse";
2029
- 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));
2030
- const retryArgs = [
2031
- `/codex computer-use ${parsed.action}`,
2032
- parsed.overrides.marketplaceSource ? `--source ${JSON.stringify(parsed.overrides.marketplaceSource)}` : void 0,
2033
- parsed.overrides.marketplacePath ? `--marketplace-path ${JSON.stringify(parsed.overrides.marketplacePath)}` : void 0,
2034
- parsed.overrides.marketplaceName ? `--marketplace ${JSON.stringify(parsed.overrides.marketplaceName)}` : void 0
2035
- ].filter((arg) => Boolean(arg));
2036
- return ["One-off Computer Use plugin/server overrides are no longer supported.", `Set ${settings.join(" and ")} persistently, then rerun ${retryArgs.join(" ")}.`].join(" ");
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");
2037
1944
  }
2038
- function readRequiredOptionValue(args, index) {
2039
- const value = args[index + 1];
2040
- const normalized = value?.trim();
2041
- if (!normalized || normalized.startsWith("-")) return;
2042
- return value;
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");
2043
1969
  }
2044
- function normalizeComputerUseStringOverrides(overrides) {
2045
- const normalized = {};
2046
- const marketplaceSource = normalizeOptionalString(overrides.marketplaceSource);
2047
- if (marketplaceSource) normalized.marketplaceSource = marketplaceSource;
2048
- const marketplacePath = normalizeOptionalString(overrides.marketplacePath);
2049
- if (marketplacePath) normalized.marketplacePath = marketplacePath;
2050
- const marketplaceName = normalizeOptionalString(overrides.marketplaceName);
2051
- if (marketplaceName) normalized.marketplaceName = marketplaceName;
2052
- return normalized;
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) };
2022
+ }
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) };
2029
+ }
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") };
2037
+ }
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
+ })) };
2045
+ }
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
+ })) };
2062
+ }
2063
+ return { text: `Unknown Codex command: ${formatCodexDisplayText(subcommand)}\n\n${buildHelp()}` };
2053
2064
  }
2054
2065
  //#endregion
2055
2066
  export { handleCodexSubcommand };