happy-imou-cloud 2.0.0 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/dist/{BaseReasoningProcessor-BRCQXCZY.cjs → BaseReasoningProcessor-B6tJ_eL5.cjs} +96 -9
  2. package/dist/{BaseReasoningProcessor-BKLRCKTU.mjs → BaseReasoningProcessor-D8VhEbs2.mjs} +95 -10
  3. package/dist/{api-D7OK-mML.cjs → api-D2Njw9Im.cjs} +124 -6
  4. package/dist/{api-BGXYX0yH.mjs → api-MYhAGPLn.mjs} +122 -7
  5. package/dist/{command-G85giEAF.cjs → command-CVldr51S.cjs} +3 -3
  6. package/dist/{command-CnLtKtP-.mjs → command-nmK6O-ab.mjs} +3 -3
  7. package/dist/{index-C7Y0R-MI.mjs → index-B97L7qLD.mjs} +689 -229
  8. package/dist/{index-B_wlQBy2.cjs → index-Bg-YziG2.cjs} +691 -229
  9. package/dist/index.cjs +4 -4
  10. package/dist/index.mjs +4 -4
  11. package/dist/lib.cjs +1 -1
  12. package/dist/lib.d.cts +7 -0
  13. package/dist/lib.d.mts +7 -0
  14. package/dist/lib.mjs +1 -1
  15. package/dist/{persistence-DHgf1CTG.cjs → persistence-D_2GkJAO.cjs} +28 -6
  16. package/dist/{persistence-BA_unuca.mjs → persistence-Dkm7rm8k.mjs} +29 -7
  17. package/dist/{registerKillSessionHandler-C2-yHm1V.mjs → registerKillSessionHandler-5GbrO0FM.mjs} +6 -4
  18. package/dist/{registerKillSessionHandler-CLREXN11.cjs → registerKillSessionHandler-BAXmJQRt.cjs} +6 -4
  19. package/dist/{runClaude-CwAitpX-.cjs → runClaude-B-GNEkKg.cjs} +237 -45
  20. package/dist/{runClaude-uNC5Eym4.mjs → runClaude-Cii3R2Fv.mjs} +238 -46
  21. package/dist/{runCodex-B-05E-YZ.mjs → runCodex-C--ZwAhl.mjs} +636 -819
  22. package/dist/{runCodex-Cm0VTqw_.cjs → runCodex-CPHyGwj9.cjs} +639 -819
  23. package/dist/{runGemini-_biXvQAH.mjs → runGemini-CQp7Nuzn.mjs} +20 -16
  24. package/dist/{runGemini-CLWjwDYS.cjs → runGemini-DaDz1bzQ.cjs} +20 -16
  25. package/package.json +14 -15
  26. package/scripts/env-wrapper.cjs +11 -11
  27. package/scripts/setup-dev.cjs +4 -4
@@ -1,11 +1,11 @@
1
1
  'use strict';
2
2
 
3
3
  var node_crypto = require('node:crypto');
4
- var api = require('./api-D7OK-mML.cjs');
5
- var persistence = require('./persistence-DHgf1CTG.cjs');
6
- var index = require('./index-B_wlQBy2.cjs');
7
- var BaseReasoningProcessor = require('./BaseReasoningProcessor-BRCQXCZY.cjs');
8
- var registerKillSessionHandler = require('./registerKillSessionHandler-CLREXN11.cjs');
4
+ var api = require('./api-D2Njw9Im.cjs');
5
+ var persistence = require('./persistence-D_2GkJAO.cjs');
6
+ var index = require('./index-Bg-YziG2.cjs');
7
+ var BaseReasoningProcessor = require('./BaseReasoningProcessor-B6tJ_eL5.cjs');
8
+ var registerKillSessionHandler = require('./registerKillSessionHandler-BAXmJQRt.cjs');
9
9
  var future = require('./future-Dq4Ha1Dn.cjs');
10
10
  var node_child_process = require('node:child_process');
11
11
  var fs = require('node:fs');
@@ -13,17 +13,14 @@ var os = require('node:os');
13
13
  var path = require('node:path');
14
14
  var React = require('react');
15
15
  var ink = require('ink');
16
- var index_js = require('@modelcontextprotocol/sdk/client/index.js');
17
- var stdio_js = require('@modelcontextprotocol/sdk/client/stdio.js');
18
- var z = require('zod');
19
- var types_js = require('@modelcontextprotocol/sdk/types.js');
20
- var child_process = require('child_process');
21
16
  require('axios');
22
17
  require('chalk');
23
18
  require('fs');
24
19
  require('node:events');
25
20
  require('socket.io-client');
21
+ require('zod');
26
22
  require('tweetnacl');
23
+ require('child_process');
27
24
  require('util');
28
25
  require('fs/promises');
29
26
  require('crypto');
@@ -133,6 +130,7 @@ class CodexSession {
133
130
  sessionId;
134
131
  mode;
135
132
  thinking = false;
133
+ localModePinned = false;
136
134
  keepAliveInterval;
137
135
  onModeChangeCallback;
138
136
  clientSwapCallbacks = [];
@@ -187,11 +185,23 @@ class CodexSession {
187
185
  this.thinking = thinking;
188
186
  this.client.keepAlive(thinking, this.mode);
189
187
  };
190
- onModeChange = (mode) => {
188
+ onModeChange = async (mode) => {
191
189
  this.mode = mode;
192
190
  this.client.keepAlive(this.thinking, mode);
193
- this.onModeChangeCallback(mode);
191
+ await this.onModeChangeCallback(mode);
194
192
  };
193
+ pinLocalMode = () => {
194
+ this.localModePinned = true;
195
+ api.logger.debug("[CodexSession] Local mode pinned");
196
+ };
197
+ clearLocalModePin = () => {
198
+ if (!this.localModePinned) {
199
+ return;
200
+ }
201
+ this.localModePinned = false;
202
+ api.logger.debug("[CodexSession] Local mode pin cleared");
203
+ };
204
+ isLocalModePinned = () => this.localModePinned;
195
205
  onSessionFound = (sessionId) => {
196
206
  if (this.sessionId === sessionId) {
197
207
  return;
@@ -234,21 +244,6 @@ function extractCodexSessionIdFromPath(filePath) {
234
244
  const match = filePath.match(/-([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})\.jsonl$/);
235
245
  return match?.[1] ?? null;
236
246
  }
237
- function findCodexResumeFile(sessionId) {
238
- if (!sessionId) return null;
239
- try {
240
- const candidates = collectFilesRecursive(getCodexSessionsRoot()).filter((full) => full.endsWith(`-${sessionId}.jsonl`)).filter((full) => {
241
- try {
242
- return fs.statSync(full).isFile();
243
- } catch {
244
- return false;
245
- }
246
- }).sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs);
247
- return candidates[0] || null;
248
- } catch {
249
- return null;
250
- }
251
- }
252
247
  function findLatestCodexSessionIdSince(startTimeMs) {
253
248
  try {
254
249
  const candidates = collectFilesRecursive(getCodexSessionsRoot()).filter((full) => full.endsWith(".jsonl")).filter((full) => {
@@ -270,6 +265,36 @@ function findLatestCodexSessionIdSince(startTimeMs) {
270
265
  return null;
271
266
  }
272
267
 
268
+ function firstExistingPath(candidates) {
269
+ for (const candidate of candidates) {
270
+ try {
271
+ if (fs.existsSync(candidate)) {
272
+ return candidate;
273
+ }
274
+ } catch {
275
+ }
276
+ }
277
+ return null;
278
+ }
279
+ function resolveCodexExecutable() {
280
+ if (process.platform === "win32") {
281
+ const appData = process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming");
282
+ const npmGlobalBin = path.join(appData, "npm");
283
+ const resolved = firstExistingPath([
284
+ path.join(npmGlobalBin, "codex.cmd"),
285
+ path.join(npmGlobalBin, "codex.ps1"),
286
+ path.join(npmGlobalBin, "codex")
287
+ ]);
288
+ if (resolved) {
289
+ return resolved;
290
+ }
291
+ }
292
+ return "codex";
293
+ }
294
+ function shouldUseShellForCodex(executable) {
295
+ return process.platform === "win32" && /\.(cmd|bat|ps1)$/i.test(executable);
296
+ }
297
+
273
298
  class CodexExitCodeError extends Error {
274
299
  exitCode;
275
300
  constructor(exitCode) {
@@ -297,7 +322,7 @@ async function codexLocal(opts) {
297
322
  const args = opts.sessionId ? ["resume", opts.sessionId, ...baseArgs] : baseArgs;
298
323
  const startTime = Date.now();
299
324
  let detectedSessionId = opts.sessionId;
300
- const codexExecutable = index.resolveCodexExecutable();
325
+ const codexExecutable = resolveCodexExecutable();
301
326
  api.logger.debug(`[CodexLocal] Spawning ${codexExecutable} with args: ${JSON.stringify(args)}`);
302
327
  process.stdin.pause();
303
328
  try {
@@ -307,7 +332,7 @@ async function codexLocal(opts) {
307
332
  env: process.env,
308
333
  stdio: "inherit",
309
334
  signal: opts.abort,
310
- shell: index.shouldUseShellForCodex(codexExecutable)
335
+ shell: shouldUseShellForCodex(codexExecutable)
311
336
  });
312
337
  const abortChild = () => {
313
338
  if (child.exitCode === null) {
@@ -387,10 +412,18 @@ async function codexLocalLauncher(session) {
387
412
  session.client.rpcHandlerManager.registerHandler("abort", doAbort);
388
413
  session.client.rpcHandlerManager.registerHandler("switch", doSwitch);
389
414
  session.queue.setOnMessage(() => {
415
+ if (session.isLocalModePinned()) {
416
+ api.logger.debug("[codex-local]: message arrived while local mode is pinned; staying local");
417
+ return;
418
+ }
390
419
  void doSwitch();
391
420
  });
392
421
  if (session.queue.size() > 0) {
393
- return { type: "switch" };
422
+ if (session.isLocalModePinned()) {
423
+ api.logger.debug("[codex-local]: pending queued messages detected, but local mode is pinned; staying local");
424
+ } else {
425
+ return { type: "switch" };
426
+ }
394
427
  }
395
428
  while (true) {
396
429
  if (exitReason) {
@@ -524,7 +557,7 @@ const CodexDisplay = ({ messageBuffer, logPath, onExit, onSwitchToLocal, title }
524
557
  }
525
558
  };
526
559
  const formatMessage = (msg) => {
527
- const lines = msg.content.split("\n");
560
+ const lines = index.formatDisplayMessage(msg.content).split("\n");
528
561
  const maxLineLength = terminalWidth - 10;
529
562
  return lines.map((line) => {
530
563
  if (line.length <= maxLineLength) return line;
@@ -566,462 +599,6 @@ const CodexDisplay = ({ messageBuffer, logPath, onExit, onSwitchToLocal, title }
566
599
  ));
567
600
  };
568
601
 
569
- const DEFAULT_TIMEOUT = 14 * 24 * 60 * 60 * 1e3;
570
- function getCodexMcpCommand() {
571
- const codexExecutable = index.resolveCodexExecutable();
572
- try {
573
- const version = child_process.execSync(`"${codexExecutable}" --version`, { encoding: "utf8" }).trim();
574
- const match = version.match(/codex-cli\s+(\d+\.\d+\.\d+(?:-alpha\.\d+)?)/);
575
- if (!match) {
576
- api.logger.debug("[CodexMCP] Could not parse codex version:", version);
577
- return null;
578
- }
579
- const versionStr = match[1];
580
- const [major, minor, patch] = versionStr.split(/[-.]/).map(Number);
581
- if (major > 0 || minor > 43) return "mcp-server";
582
- if (minor === 43 && patch === 0) {
583
- if (versionStr.includes("-alpha.")) {
584
- const alphaNum = parseInt(versionStr.split("-alpha.")[1]);
585
- return alphaNum >= 5 ? "mcp-server" : "mcp";
586
- }
587
- return "mcp-server";
588
- }
589
- return "mcp";
590
- } catch (error) {
591
- api.logger.debug("[CodexMCP] Codex CLI not found or not executable:", error);
592
- return null;
593
- }
594
- }
595
- function resolveCodexMcpTransport(executable, mcpCommand) {
596
- return {
597
- // Preserve the resolved executable path so daemon/service environments do not
598
- // depend on APPDATA npm shims being present on PATH.
599
- command: executable,
600
- args: [mcpCommand]
601
- };
602
- }
603
- function extractCodexIdentifiers(response) {
604
- const meta = response?.meta || {};
605
- const structured = response?.structuredContent || response?.structured_content || response?.structured_output || {};
606
- const sessionId = meta.sessionId || meta.session_id || response?.sessionId || response?.session_id || meta.threadId || meta.thread_id || response?.threadId || response?.thread_id || structured.threadId || structured.thread_id || null;
607
- const conversationId = meta.conversationId || meta.conversation_id || response?.conversationId || response?.conversation_id || meta.threadId || meta.thread_id || response?.threadId || response?.thread_id || structured.threadId || structured.thread_id || structured.conversationId || structured.conversation_id || null;
608
- return { sessionId, conversationId };
609
- }
610
- function normalizeCodexPermissionRequest(params) {
611
- return {
612
- requestId: params.codex_call_id || params.call_id || params.codex_mcp_tool_call_id || params.codex_event_id || `codex-permission-${Date.now()}`,
613
- toolName: "CodexBash",
614
- input: {
615
- command: params.codex_command || params.command,
616
- cwd: params.codex_cwd || params.cwd,
617
- proposed_execpolicy_amendment: params.proposed_execpolicy_amendment,
618
- parsed_cmd: params.parsed_cmd
619
- }
620
- };
621
- }
622
- function resolveCodexPermissionRequest(params, fallback) {
623
- const normalized = normalizeCodexPermissionRequest(params);
624
- const hasInlinePayload = Boolean(
625
- normalized.input.command || normalized.input.cwd || normalized.input.proposed_execpolicy_amendment || normalized.input.parsed_cmd
626
- );
627
- const hasInlineId = Boolean(
628
- params.codex_call_id || params.call_id || params.codex_mcp_tool_call_id || params.codex_event_id
629
- );
630
- if (!fallback || hasInlinePayload && hasInlineId) {
631
- return normalized;
632
- }
633
- return {
634
- requestId: hasInlineId ? normalized.requestId : fallback.call_id || normalized.requestId,
635
- toolName: normalized.toolName,
636
- input: {
637
- command: normalized.input.command || fallback.command,
638
- cwd: normalized.input.cwd || fallback.cwd,
639
- proposed_execpolicy_amendment: normalized.input.proposed_execpolicy_amendment || fallback.proposed_execpolicy_amendment,
640
- parsed_cmd: normalized.input.parsed_cmd || fallback.parsed_cmd
641
- }
642
- };
643
- }
644
- class CodexMcpClient {
645
- client;
646
- transport = null;
647
- connected = false;
648
- sessionId = null;
649
- conversationId = null;
650
- handler = null;
651
- permissionHandler = null;
652
- selectionHandler = null;
653
- pendingExecApprovalContexts = [];
654
- constructor() {
655
- this.client = this.createClient();
656
- }
657
- createClient() {
658
- const client = new index_js.Client(
659
- { name: "happy-codex-client", version: "1.0.0" },
660
- { capabilities: { elicitation: {} } }
661
- );
662
- client.setNotificationHandler(z.z.object({
663
- method: z.z.literal("codex/event"),
664
- params: z.z.object({
665
- msg: z.z.any()
666
- })
667
- }).passthrough(), (data) => {
668
- const msg = data.params.msg;
669
- if (msg?.type === "exec_approval_request") {
670
- this.pendingExecApprovalContexts.push({
671
- call_id: msg.call_id,
672
- command: msg.command,
673
- cwd: msg.cwd,
674
- proposed_execpolicy_amendment: msg.proposed_execpolicy_amendment,
675
- parsed_cmd: msg.parsed_cmd
676
- });
677
- }
678
- this.updateIdentifiersFromEvent(msg);
679
- this.handler?.(msg);
680
- });
681
- return client;
682
- }
683
- setHandler(handler) {
684
- this.handler = handler;
685
- }
686
- /**
687
- * Set the permission handler for tool approval
688
- */
689
- setPermissionHandler(handler) {
690
- this.permissionHandler = handler;
691
- }
692
- setSelectionHandler(handler) {
693
- this.selectionHandler = handler;
694
- }
695
- async connect() {
696
- if (this.connected) return;
697
- const mcpCommand = getCodexMcpCommand();
698
- if (mcpCommand === null) {
699
- throw new Error(
700
- "Codex CLI not found or not executable.\n\nTo install codex:\n npm install -g @openai/codex\n\nAlternatively, use Claude:\n happy claude"
701
- );
702
- }
703
- api.logger.debug(`[CodexMCP] Connecting to Codex MCP server using command: codex ${mcpCommand}`);
704
- const codexExecutable = index.resolveCodexExecutable();
705
- const transport = resolveCodexMcpTransport(codexExecutable, mcpCommand);
706
- this.transport = new stdio_js.StdioClientTransport({
707
- command: transport.command,
708
- args: transport.args,
709
- env: Object.keys(process.env).reduce((acc, key) => {
710
- const value = process.env[key];
711
- if (typeof value === "string") acc[key] = value;
712
- return acc;
713
- }, {})
714
- });
715
- this.registerPermissionHandlers();
716
- await this.client.connect(this.transport);
717
- this.connected = true;
718
- api.logger.debug("[CodexMCP] Connected to Codex");
719
- }
720
- isClosedTransportError(error) {
721
- const record = error;
722
- const code = typeof record?.code === "number" ? record.code : null;
723
- if (code === -32e3) {
724
- return true;
725
- }
726
- const message = typeof record?.message === "string" ? record.message.toLowerCase() : "";
727
- if (code === -32001 && message.includes("aborterror")) {
728
- return true;
729
- }
730
- return message.includes("connection closed") || message.includes("not connected");
731
- }
732
- async callToolWithReconnectRetry(call) {
733
- try {
734
- return await call();
735
- } catch (error) {
736
- if (!this.isClosedTransportError(error)) {
737
- throw error;
738
- }
739
- api.logger.debug("[CodexMCP] Transport closed during tool call; reconnecting and retrying once");
740
- try {
741
- await this.disconnect();
742
- } catch {
743
- }
744
- await this.connect();
745
- return await call();
746
- }
747
- }
748
- registerPermissionHandlers() {
749
- this.client.setRequestHandler(
750
- types_js.ElicitRequestSchema,
751
- async (request) => {
752
- const params = request.params;
753
- api.logger.debug("[CodexMCP] Received elicitation request:", params);
754
- try {
755
- if (this.isSelectionRequest(params)) {
756
- return await this.handleSelectionRequest(params);
757
- }
758
- if (!this.permissionHandler) {
759
- api.logger.debug("[CodexMCP] No permission handler set, denying by default");
760
- return {
761
- action: "decline",
762
- decision: "denied",
763
- content: {
764
- decision: "denied"
765
- }
766
- };
767
- }
768
- const normalized = resolveCodexPermissionRequest(
769
- params,
770
- this.takeExecApprovalContextForPermission(params)
771
- );
772
- const result = await this.permissionHandler.handleToolCall(
773
- normalized.requestId,
774
- normalized.toolName,
775
- normalized.input
776
- );
777
- api.logger.debug("[CodexMCP] Permission result:", result);
778
- return {
779
- action: result.decision === "abort" ? "cancel" : result.decision === "denied" ? "decline" : "accept",
780
- decision: result.decision,
781
- content: {
782
- decision: result.decision
783
- }
784
- };
785
- } catch (error) {
786
- if (error instanceof Error && error.message === "Session reset") {
787
- api.logger.debug("[CodexMCP] Permission request cancelled during session reset");
788
- return {
789
- action: "cancel"
790
- };
791
- }
792
- if (error instanceof Error && error.message === BaseReasoningProcessor.INTERACTION_SUPERSEDED_ERROR) {
793
- api.logger.debug("[CodexMCP] Selection request cancelled because a new user message arrived");
794
- return {
795
- action: "cancel"
796
- };
797
- }
798
- api.logger.debug("[CodexMCP] Error handling permission request:", error);
799
- return {
800
- action: "decline",
801
- decision: "denied",
802
- reason: error instanceof Error ? error.message : "Permission request failed"
803
- };
804
- }
805
- }
806
- );
807
- api.logger.debug("[CodexMCP] Permission handlers registered");
808
- }
809
- isSelectionRequest(params) {
810
- if (Array.isArray(params.codex_command) && params.codex_command.length > 0) {
811
- return false;
812
- }
813
- if (Array.isArray(params.options) && params.options.length > 0) {
814
- return true;
815
- }
816
- return Boolean(params.requestedSchema?.properties?.optionId);
817
- }
818
- async handleSelectionRequest(params) {
819
- const optionMap = (params.options || []).filter((option) => Boolean(option.optionId));
820
- const options = optionMap.map((option) => ({
821
- optionId: option.optionId,
822
- label: option.label || option.name || option.optionId,
823
- description: option.description || option.name || option.optionId
824
- }));
825
- const defaultOptionId = params.requestedSchema?.properties?.optionId?.default;
826
- if (options.length === 0) {
827
- api.logger.debug("[CodexMCP] Selection request has no options, cancelling");
828
- return {
829
- action: "cancel"
830
- };
831
- }
832
- if (!this.selectionHandler) {
833
- api.logger.debug("[CodexMCP] No selection handler set, cancelling selection request");
834
- return {
835
- action: "cancel"
836
- };
837
- }
838
- const requestId = params.codex_event_id || params.codex_elicitation || `selection-${Date.now()}`;
839
- const response = await this.selectionHandler.requestSelection({
840
- id: requestId,
841
- message: params.message,
842
- options,
843
- defaultOptionId
844
- });
845
- return {
846
- action: "accept",
847
- optionId: response.optionId,
848
- content: {
849
- optionId: response.optionId
850
- }
851
- };
852
- }
853
- takeExecApprovalContextForPermission(params) {
854
- const hasInlinePayload = Boolean(
855
- params.codex_command || params.command || params.codex_cwd || params.cwd || params.proposed_execpolicy_amendment || params.parsed_cmd
856
- );
857
- const explicitId = params.codex_call_id || params.call_id || params.codex_mcp_tool_call_id || params.codex_event_id;
858
- if (hasInlinePayload && explicitId) {
859
- return null;
860
- }
861
- if (!this.pendingExecApprovalContexts.length) {
862
- return null;
863
- }
864
- if (explicitId) {
865
- const index = this.pendingExecApprovalContexts.findIndex((context) => context.call_id === explicitId);
866
- if (index >= 0) {
867
- return this.pendingExecApprovalContexts.splice(index, 1)[0] || null;
868
- }
869
- }
870
- return this.pendingExecApprovalContexts.shift() || null;
871
- }
872
- async startSession(config, options) {
873
- if (!this.connected) await this.connect();
874
- api.logger.debug("[CodexMCP] Starting Codex session:", config);
875
- const response = await this.callToolWithReconnectRetry(
876
- () => this.client.callTool({
877
- name: "codex",
878
- arguments: config
879
- }, void 0, {
880
- signal: options?.signal,
881
- timeout: DEFAULT_TIMEOUT
882
- })
883
- );
884
- api.logger.debug("[CodexMCP] startSession response:", response);
885
- this.extractIdentifiers(response);
886
- return response;
887
- }
888
- async continueSession(prompt, options) {
889
- if (!this.connected) await this.connect();
890
- if (!this.sessionId) {
891
- throw new Error("No active session. Call startSession first.");
892
- }
893
- if (!this.conversationId) {
894
- this.conversationId = this.sessionId;
895
- api.logger.debug("[CodexMCP] conversationId missing, defaulting to sessionId:", this.conversationId);
896
- }
897
- const args = { sessionId: this.sessionId, conversationId: this.conversationId, prompt };
898
- api.logger.debug("[CodexMCP] Continuing Codex session:", args);
899
- const response = await this.callToolWithReconnectRetry(
900
- () => this.client.callTool({
901
- name: "codex-reply",
902
- arguments: args
903
- }, void 0, {
904
- signal: options?.signal,
905
- timeout: DEFAULT_TIMEOUT
906
- })
907
- );
908
- api.logger.debug("[CodexMCP] continueSession response:", response);
909
- this.extractIdentifiers(response);
910
- return response;
911
- }
912
- updateIdentifiersFromEvent(event) {
913
- if (!event || typeof event !== "object") {
914
- return;
915
- }
916
- const candidates = [event];
917
- if (event.data && typeof event.data === "object") {
918
- candidates.push(event.data);
919
- }
920
- for (const candidate of candidates) {
921
- const sessionId = candidate.session_id ?? candidate.sessionId ?? candidate.thread_id ?? candidate.threadId;
922
- if (sessionId) {
923
- this.sessionId = sessionId;
924
- api.logger.debug("[CodexMCP] Session ID extracted from event:", this.sessionId);
925
- }
926
- const conversationId = candidate.conversation_id ?? candidate.conversationId;
927
- if (conversationId) {
928
- this.conversationId = conversationId;
929
- api.logger.debug("[CodexMCP] Conversation ID extracted from event:", this.conversationId);
930
- }
931
- }
932
- }
933
- extractIdentifiers(response) {
934
- const identifiers = extractCodexIdentifiers(response);
935
- if (identifiers.sessionId) {
936
- this.sessionId = identifiers.sessionId;
937
- api.logger.debug("[CodexMCP] Session ID extracted:", this.sessionId);
938
- }
939
- if (identifiers.conversationId) {
940
- this.conversationId = identifiers.conversationId;
941
- api.logger.debug("[CodexMCP] Conversation ID extracted:", this.conversationId);
942
- }
943
- const content = response?.content;
944
- if (Array.isArray(content)) {
945
- for (const item of content) {
946
- if (!this.sessionId && item?.sessionId) {
947
- this.sessionId = item.sessionId;
948
- api.logger.debug("[CodexMCP] Session ID extracted from content:", this.sessionId);
949
- }
950
- if (!this.conversationId && item && typeof item === "object" && "conversationId" in item && item.conversationId) {
951
- this.conversationId = item.conversationId;
952
- api.logger.debug("[CodexMCP] Conversation ID extracted from content:", this.conversationId);
953
- }
954
- }
955
- }
956
- }
957
- getSessionId() {
958
- return this.sessionId;
959
- }
960
- hasActiveSession() {
961
- return this.sessionId !== null;
962
- }
963
- clearSession() {
964
- const previousSessionId = this.sessionId;
965
- this.sessionId = null;
966
- this.conversationId = null;
967
- api.logger.debug("[CodexMCP] Session cleared, previous sessionId:", previousSessionId);
968
- }
969
- /**
970
- * Store the current session ID without clearing it, useful for abort handling
971
- */
972
- storeSessionForResume() {
973
- api.logger.debug("[CodexMCP] Storing session for potential resume:", this.sessionId);
974
- return this.sessionId;
975
- }
976
- /**
977
- * Force close the Codex MCP transport and clear all session identifiers.
978
- * Use this for permanent shutdown (e.g. kill/exit). Prefer `disconnect()` for
979
- * transient connection resets where you may want to keep the session id.
980
- */
981
- async forceCloseSession() {
982
- api.logger.debug("[CodexMCP] Force closing session");
983
- try {
984
- await this.disconnect();
985
- } finally {
986
- this.clearSession();
987
- }
988
- api.logger.debug("[CodexMCP] Session force-closed");
989
- }
990
- async disconnect() {
991
- if (!this.connected) return;
992
- const pid = this.transport?.pid ?? null;
993
- api.logger.debug(`[CodexMCP] Disconnecting; child pid=${pid ?? "none"}`);
994
- try {
995
- api.logger.debug("[CodexMCP] client.close begin");
996
- await this.client.close();
997
- api.logger.debug("[CodexMCP] client.close done");
998
- } catch (e) {
999
- api.logger.debug("[CodexMCP] Error closing client, attempting transport close directly", e);
1000
- try {
1001
- api.logger.debug("[CodexMCP] transport.close begin");
1002
- await this.transport?.close?.();
1003
- api.logger.debug("[CodexMCP] transport.close done");
1004
- } catch {
1005
- }
1006
- }
1007
- if (pid) {
1008
- try {
1009
- process.kill(pid, 0);
1010
- api.logger.debug("[CodexMCP] Child still alive, sending SIGKILL");
1011
- try {
1012
- process.kill(pid, "SIGKILL");
1013
- } catch {
1014
- }
1015
- } catch {
1016
- }
1017
- }
1018
- this.transport = null;
1019
- this.connected = false;
1020
- this.client = this.createClient();
1021
- api.logger.debug(`[CodexMCP] Disconnected; session ${this.sessionId ?? "none"} preserved`);
1022
- }
1023
- }
1024
-
1025
602
  class CodexPermissionHandler extends BaseReasoningProcessor.BasePermissionHandler {
1026
603
  constructor(session) {
1027
604
  super(session);
@@ -1037,16 +614,7 @@ class CodexPermissionHandler extends BaseReasoningProcessor.BasePermissionHandle
1037
614
  * @returns Promise resolving to permission result
1038
615
  */
1039
616
  async handleToolCall(toolCallId, toolName, input) {
1040
- return new Promise((resolve, reject) => {
1041
- this.pendingRequests.set(toolCallId, {
1042
- resolve,
1043
- reject,
1044
- toolName,
1045
- input
1046
- });
1047
- this.addPendingRequestToState(toolCallId, toolName, input);
1048
- api.logger.debug(`${this.getLogPrefix()} Permission request sent for tool: ${toolName} (${toolCallId})`);
1049
- });
617
+ return this.registerPendingRequest(toolCallId, toolName, input);
1050
618
  }
1051
619
  }
1052
620
 
@@ -1063,11 +631,15 @@ class CodexSelectionHandler {
1063
631
  }
1064
632
  async requestSelection(request) {
1065
633
  return new Promise((resolve, reject) => {
1066
- this.pendingRequests.set(request.id, {
634
+ const pending = {
1067
635
  resolve,
1068
636
  reject,
1069
637
  request
1070
- });
638
+ };
639
+ pending.timeoutHandle = setTimeout(() => {
640
+ this.handleSelectionTimeout(request.id, pending);
641
+ }, BaseReasoningProcessor.getPendingInteractionTimeoutMs());
642
+ this.pendingRequests.set(request.id, pending);
1071
643
  this.session.updateAgentState((currentState) => ({
1072
644
  ...currentState,
1073
645
  requests: {
@@ -1110,6 +682,7 @@ class CodexSelectionHandler {
1110
682
  this.pendingRequests.clear();
1111
683
  const completedAt = Date.now();
1112
684
  for (const [, pending] of pendingSnapshot) {
685
+ this.clearPendingRequestTimeout(pending);
1113
686
  pending.reject(new Error(reason));
1114
687
  }
1115
688
  this.session.updateAgentState((currentState) => {
@@ -1141,6 +714,7 @@ class CodexSelectionHandler {
1141
714
  const pendingSnapshot = Array.from(this.pendingRequests.entries());
1142
715
  this.pendingRequests.clear();
1143
716
  for (const [, pending] of pendingSnapshot) {
717
+ this.clearPendingRequestTimeout(pending);
1144
718
  pending.reject(new Error(reason));
1145
719
  }
1146
720
  this.session.updateAgentState((currentState) => {
@@ -1174,6 +748,7 @@ class CodexSelectionHandler {
1174
748
  return;
1175
749
  }
1176
750
  this.pendingRequests.delete(response.id);
751
+ this.clearPendingRequestTimeout(pending);
1177
752
  pending.resolve(response);
1178
753
  this.session.updateAgentState((currentState) => {
1179
754
  const request = currentState.requests?.[response.id];
@@ -1198,6 +773,54 @@ class CodexSelectionHandler {
1198
773
  });
1199
774
  });
1200
775
  }
776
+ clearPendingRequestTimeout(pending) {
777
+ if (pending?.timeoutHandle) {
778
+ clearTimeout(pending.timeoutHandle);
779
+ pending.timeoutHandle = void 0;
780
+ }
781
+ }
782
+ handleSelectionTimeout(requestId, pending) {
783
+ const active = this.pendingRequests.get(requestId);
784
+ if (!active || active !== pending) {
785
+ return;
786
+ }
787
+ this.pendingRequests.delete(requestId);
788
+ this.clearPendingRequestTimeout(active);
789
+ active.reject(new Error(BaseReasoningProcessor.INTERACTION_TIMED_OUT_ERROR));
790
+ this.session.updateAgentState((currentState) => {
791
+ const request = currentState.requests?.[requestId] || {
792
+ tool: "AskUserQuestion",
793
+ arguments: {
794
+ requestKind: "selection",
795
+ questions: []
796
+ },
797
+ createdAt: Date.now(),
798
+ requestKind: "selection",
799
+ options: active.request.options,
800
+ defaultOptionId: active.request.defaultOptionId
801
+ };
802
+ const { [requestId]: _, ...remainingRequests } = currentState.requests || {};
803
+ return {
804
+ ...currentState,
805
+ requests: remainingRequests,
806
+ completedRequests: {
807
+ ...currentState.completedRequests,
808
+ [requestId]: {
809
+ ...request,
810
+ completedAt: Date.now(),
811
+ status: "canceled",
812
+ reason: BaseReasoningProcessor.INTERACTION_TIMED_OUT_ERROR,
813
+ requestKind: "selection"
814
+ }
815
+ }
816
+ };
817
+ });
818
+ this.session.sendSessionEvent({
819
+ type: "message",
820
+ message: "Pending interaction timed out waiting for a response. Send a new message to continue."
821
+ });
822
+ api.logger.debug(`[Codex] Selection request timed out (${requestId})`);
823
+ }
1201
824
  }
1202
825
 
1203
826
  class ReasoningProcessor extends BaseReasoningProcessor.BaseReasoningProcessor {
@@ -1219,67 +842,62 @@ class ReasoningProcessor extends BaseReasoningProcessor.BaseReasoningProcessor {
1219
842
  complete(fullText) {
1220
843
  this.completeReasoning(fullText);
1221
844
  }
845
+ completeCurrent() {
846
+ return this.completeReasoning();
847
+ }
1222
848
  }
1223
849
 
1224
- class DiffProcessor {
1225
- previousDiff = null;
1226
- onMessage = null;
1227
- constructor(onMessage) {
1228
- this.onMessage = onMessage || null;
1229
- }
1230
- /**
1231
- * Process a turn_diff message and check if the unified_diff has changed
1232
- */
1233
- processDiff(unifiedDiff) {
1234
- if (this.previousDiff !== unifiedDiff) {
1235
- api.logger.debug("[DiffProcessor] Unified diff changed, sending CodexDiff tool call");
1236
- const callId = node_crypto.randomUUID();
1237
- const toolCall = {
1238
- type: "tool-call",
1239
- name: "CodexDiff",
1240
- callId,
1241
- input: {
1242
- unified_diff: unifiedDiff
1243
- },
1244
- id: node_crypto.randomUUID()
1245
- };
1246
- this.onMessage?.(toolCall);
1247
- const toolResult = {
1248
- type: "tool-call-result",
1249
- callId,
1250
- output: {
1251
- status: "completed"
1252
- },
1253
- id: node_crypto.randomUUID()
1254
- };
1255
- this.onMessage?.(toolResult);
1256
- }
1257
- this.previousDiff = unifiedDiff;
1258
- api.logger.debug("[DiffProcessor] Updated stored diff");
1259
- }
1260
- /**
1261
- * Reset the processor state (called on task_complete or turn_aborted)
1262
- */
1263
- reset() {
1264
- api.logger.debug("[DiffProcessor] Resetting diff state");
1265
- this.previousDiff = null;
1266
- }
1267
- /**
1268
- * Set the message callback for sending messages directly
1269
- */
1270
- setMessageCallback(callback) {
1271
- this.onMessage = callback;
850
+ function createAbortError() {
851
+ const error = new Error("Operation aborted");
852
+ error.name = "AbortError";
853
+ return error;
854
+ }
855
+ async function waitForResponseCompleteWithAbort(backend, signal, timeoutMs = 12e4) {
856
+ if (!backend.waitForResponseComplete) {
857
+ return;
1272
858
  }
1273
- /**
1274
- * Get the current diff value
1275
- */
1276
- getCurrentDiff() {
1277
- return this.previousDiff;
859
+ if (signal.aborted) {
860
+ throw createAbortError();
1278
861
  }
862
+ await new Promise((resolve, reject) => {
863
+ const onAbort = () => reject(createAbortError());
864
+ signal.addEventListener("abort", onAbort, { once: true });
865
+ backend.waitForResponseComplete(timeoutMs).then(resolve).catch(reject).finally(() => {
866
+ signal.removeEventListener("abort", onAbort);
867
+ });
868
+ });
1279
869
  }
1280
-
1281
- function shouldEmitCodexReadyOnTurnEnd(opts) {
1282
- return !opts.shouldExit && opts.exitReason !== "switch";
870
+ function normalizeCodexBackendError(error) {
871
+ const record = typeof error === "object" && error !== null ? error : null;
872
+ const text = index.formatDisplayMessage(error).trim();
873
+ const stderrText = record ? index.formatDisplayMessage(record.stderr).trim() : "";
874
+ const detailText = record ? index.formatDisplayMessage(record.detail).trim() : "";
875
+ const searchableText = [text, stderrText, detailText].filter(Boolean).join("\n");
876
+ const prefix = typeof error === "object" && error !== null ? [
877
+ record?.code !== void 0 && record?.code !== null ? `[code=${String(record.code)}]` : "",
878
+ record?.status !== void 0 && record?.status !== null ? `[status=${String(record.status)}]` : ""
879
+ ].filter(Boolean).join(" ") : "";
880
+ if (searchableText.includes("Failed to locate @zed-industries/codex-acp-")) {
881
+ const hint = "Codex ACP could not start because the @zed-industries/codex-acp platform binary is missing. Reinstall codex-acp for this machine, or set HAPPY_CODEX_ACP_BIN to a working codex-acp executable.";
882
+ return prefix ? `${prefix} ${hint}` : hint;
883
+ }
884
+ if (searchableText.includes("method not found: initialize") || searchableText.includes("method not found: session/new")) {
885
+ const hint = "The configured Codex ACP command does not speak the ACP protocol. Make sure HAPPY_CODEX_ACP_BIN points to codex-acp, not the codex CLI.";
886
+ return prefix ? `${prefix} ${hint}` : hint;
887
+ }
888
+ if (error instanceof Error && text) {
889
+ return text;
890
+ }
891
+ if (typeof error === "object" && error !== null && !Array.isArray(error) && Object.keys(error).length === 0) {
892
+ return "Codex backend exited unexpectedly";
893
+ }
894
+ if (typeof error === "object" && error !== null) {
895
+ const message = text;
896
+ if (message) {
897
+ return prefix ? `${prefix} ${message}` : message;
898
+ }
899
+ }
900
+ return text || "Codex backend exited unexpectedly";
1283
901
  }
1284
902
  function shouldEmitCodexReadyAfterWait(opts) {
1285
903
  if (opts.readyAlreadySent) {
@@ -1292,6 +910,68 @@ function registerCodexRemoteRpcHandlers(clientSession, handlers) {
1292
910
  clientSession.rpcHandlerManager.registerHandler("switch", handlers.handleSwitchToLocal);
1293
911
  registerKillSessionHandler.registerKillSessionHandler(clientSession.rpcHandlerManager, handlers.handleKillSession);
1294
912
  }
913
+ function handleIncomingCodexMessageDuringRemoteTurn(opts) {
914
+ const reason = opts.reason ?? BaseReasoningProcessor.INTERACTION_SUPERSEDED_ERROR;
915
+ const supersededPermissions = opts.supersedePermissions(reason);
916
+ const supersededSelections = opts.supersedeSelections(reason);
917
+ const interruptedTurn = opts.turnInFlight;
918
+ const total = supersededPermissions + supersededSelections;
919
+ if (interruptedTurn) {
920
+ opts.abortActiveTurn();
921
+ }
922
+ if (total > 0 || interruptedTurn) {
923
+ const message = interruptedTurn ? "Current Codex task was canceled because a new user message arrived. Processing the latest message instead." : "Previous pending interaction was canceled because a new user message arrived. Processing the latest message instead.";
924
+ opts.onInterrupted(message);
925
+ }
926
+ return total > 0 || interruptedTurn;
927
+ }
928
+ function resolveCodexAcpExecutionMode(mode) {
929
+ const approvalPolicy = (() => {
930
+ switch (mode.permissionMode) {
931
+ case "default":
932
+ return "on-request";
933
+ case "read-only":
934
+ return "on-request";
935
+ case "safe-yolo":
936
+ return "on-request";
937
+ case "yolo":
938
+ return "never";
939
+ case "bypassPermissions":
940
+ return "never";
941
+ case "acceptEdits":
942
+ return "on-request";
943
+ case "plan":
944
+ return "on-request";
945
+ default:
946
+ return "on-request";
947
+ }
948
+ })();
949
+ const sandbox = (() => {
950
+ switch (mode.permissionMode) {
951
+ case "default":
952
+ return "workspace-write";
953
+ case "read-only":
954
+ return "read-only";
955
+ case "safe-yolo":
956
+ return "workspace-write";
957
+ case "yolo":
958
+ return "danger-full-access";
959
+ case "bypassPermissions":
960
+ return "danger-full-access";
961
+ case "acceptEdits":
962
+ return "workspace-write";
963
+ case "plan":
964
+ return "read-only";
965
+ default:
966
+ return "read-only";
967
+ }
968
+ })();
969
+ return {
970
+ approvalPolicy,
971
+ sandbox,
972
+ model: mode.model
973
+ };
974
+ }
1295
975
  async function codexRemoteLauncher(session) {
1296
976
  const messageBuffer = new registerKillSessionHandler.MessageBuffer();
1297
977
  const hasTTY = process.stdout.isTTY && process.stdin.isTTY;
@@ -1299,46 +979,26 @@ async function codexRemoteLauncher(session) {
1299
979
  let exitReason = null;
1300
980
  let shouldExit = false;
1301
981
  let abortController = new AbortController();
1302
- let storedSessionIdForResume = null;
1303
- const client = new CodexMcpClient();
1304
- let pendingModeResumeFile = null;
982
+ let turnInFlight = false;
983
+ let backend = null;
984
+ let acpSessionId = null;
985
+ let wasSessionCreated = false;
986
+ let currentModeHash = null;
987
+ let pending = null;
988
+ let readyAlreadySent = false;
989
+ let accumulatedResponse = "";
990
+ let isResponseInProgress = false;
991
+ let taskStartedSent = false;
1305
992
  const permissionHandler = new CodexPermissionHandler(session.client);
1306
993
  const selectionHandler = new CodexSelectionHandler(session.client);
1307
994
  const reasoningProcessor = new ReasoningProcessor((message) => {
1308
995
  session.runtimeSession.sendCodexMessage(message);
1309
996
  });
1310
- const diffProcessor = new DiffProcessor((message) => {
1311
- session.runtimeSession.sendCodexMessage(message);
1312
- });
1313
- const handleSwitchToLocal = async () => {
1314
- api.logger.debug("[Codex] Switching to local from RPC");
1315
- exitReason = "switch";
1316
- shouldExit = true;
1317
- await handleAbort();
1318
- };
1319
- client.setPermissionHandler(permissionHandler);
1320
- client.setSelectionHandler(selectionHandler);
1321
- session.setPendingInteractionSuperseder((reason = BaseReasoningProcessor.INTERACTION_SUPERSEDED_ERROR) => {
1322
- const supersededPermissions = permissionHandler.supersedePendingRequests(reason);
1323
- const supersededSelections = selectionHandler.supersedePendingRequests(reason);
1324
- const total = supersededPermissions + supersededSelections;
1325
- if (total > 0) {
1326
- const message = "Previous pending interaction was canceled because a new user message arrived. Processing the latest message instead.";
1327
- messageBuffer.addMessage(message, "status");
1328
- session.runtimeSession.sendSessionEvent({ type: "message", message });
1329
- }
1330
- return total > 0;
1331
- });
1332
- const handleClientSwap = (clientSession) => {
1333
- permissionHandler.updateSession(clientSession);
1334
- selectionHandler.updateSession(clientSession);
1335
- registerCodexRemoteRpcHandlers(clientSession, {
1336
- handleAbort,
1337
- handleSwitchToLocal,
1338
- handleKillSession
1339
- });
997
+ const rotateAbortController = () => {
998
+ const activeController = abortController;
999
+ abortController = new AbortController();
1000
+ return activeController;
1340
1001
  };
1341
- session.addClientSwapCallback(handleClientSwap);
1342
1002
  const sendReady = () => {
1343
1003
  session.runtimeSession.sendSessionEvent({ type: "ready" });
1344
1004
  try {
@@ -1351,18 +1011,282 @@ async function codexRemoteLauncher(session) {
1351
1011
  api.logger.debug("[Codex] Failed to send ready push", pushError);
1352
1012
  }
1353
1013
  };
1014
+ const resetTurnState = () => {
1015
+ reasoningProcessor.abort();
1016
+ accumulatedResponse = "";
1017
+ isResponseInProgress = false;
1018
+ taskStartedSent = false;
1019
+ session.onThinkingChange(false);
1020
+ };
1021
+ const abortActiveTurn = () => {
1022
+ const activeController = rotateAbortController();
1023
+ activeController.abort();
1024
+ reasoningProcessor.abort();
1025
+ session.onThinkingChange(false);
1026
+ if (backend && acpSessionId) {
1027
+ void backend.cancel(acpSessionId).catch((error) => {
1028
+ api.logger.debug("[Codex] Error cancelling ACP session:", error);
1029
+ });
1030
+ }
1031
+ };
1032
+ const disposeBackend = async () => {
1033
+ if (!backend) {
1034
+ return;
1035
+ }
1036
+ const currentBackend = backend;
1037
+ backend = null;
1038
+ acpSessionId = null;
1039
+ wasSessionCreated = false;
1040
+ try {
1041
+ await currentBackend.dispose();
1042
+ } catch (error) {
1043
+ api.logger.debug("[Codex] Error disposing ACP backend:", error);
1044
+ }
1045
+ };
1046
+ const emitFinalAssistantMessage = () => {
1047
+ if (!accumulatedResponse.trim()) {
1048
+ return;
1049
+ }
1050
+ session.runtimeSession.sendCodexMessage({
1051
+ type: "message",
1052
+ message: accumulatedResponse,
1053
+ id: node_crypto.randomUUID()
1054
+ });
1055
+ accumulatedResponse = "";
1056
+ isResponseInProgress = false;
1057
+ };
1058
+ const setupBackendMessageHandler = (currentBackend) => {
1059
+ currentBackend.onMessage((msg) => {
1060
+ switch (msg.type) {
1061
+ case "model-output": {
1062
+ const text = msg.textDelta ?? msg.fullText ?? "";
1063
+ if (!text) {
1064
+ return;
1065
+ }
1066
+ if (!isResponseInProgress) {
1067
+ messageBuffer.removeLastMessage("system");
1068
+ messageBuffer.addMessage(text, "assistant");
1069
+ isResponseInProgress = true;
1070
+ } else {
1071
+ messageBuffer.updateLastMessage(text, "assistant");
1072
+ }
1073
+ accumulatedResponse += text;
1074
+ return;
1075
+ }
1076
+ case "status": {
1077
+ if (msg.status === "running") {
1078
+ session.onThinkingChange(true);
1079
+ if (!taskStartedSent) {
1080
+ session.runtimeSession.sendCodexMessage({
1081
+ type: "task_started",
1082
+ id: node_crypto.randomUUID()
1083
+ });
1084
+ taskStartedSent = true;
1085
+ }
1086
+ if (!isResponseInProgress) {
1087
+ messageBuffer.addMessage("Thinking...", "system");
1088
+ }
1089
+ return;
1090
+ }
1091
+ if (msg.status === "idle" || msg.status === "stopped") {
1092
+ reasoningProcessor.completeCurrent();
1093
+ return;
1094
+ }
1095
+ if (msg.status === "error") {
1096
+ const errorMessage = normalizeCodexBackendError(msg.detail);
1097
+ messageBuffer.addMessage(`Error: ${errorMessage}`, "status");
1098
+ return;
1099
+ }
1100
+ return;
1101
+ }
1102
+ case "tool-call": {
1103
+ const toolArgs = msg.args ? index.truncateDisplayMessage(msg.args, 100) : "";
1104
+ messageBuffer.addMessage(
1105
+ `Executing: ${msg.toolName}${toolArgs ? ` ${toolArgs}` : ""}`,
1106
+ "tool"
1107
+ );
1108
+ session.runtimeSession.sendCodexMessage({
1109
+ type: "tool-call",
1110
+ name: msg.toolName,
1111
+ callId: msg.callId,
1112
+ input: msg.args,
1113
+ id: node_crypto.randomUUID()
1114
+ });
1115
+ return;
1116
+ }
1117
+ case "tool-result": {
1118
+ const isError = msg.result && typeof msg.result === "object" && "error" in msg.result;
1119
+ const resultText = index.truncateDisplayMessage(msg.result, 200) || (isError ? "Unknown error" : "");
1120
+ messageBuffer.addMessage(
1121
+ `${isError ? "Error:" : "Result:"} ${resultText}`.trim(),
1122
+ "result"
1123
+ );
1124
+ session.runtimeSession.sendCodexMessage({
1125
+ type: "tool-call-result",
1126
+ callId: msg.callId,
1127
+ output: msg.result,
1128
+ id: node_crypto.randomUUID()
1129
+ });
1130
+ return;
1131
+ }
1132
+ case "fs-edit": {
1133
+ messageBuffer.addMessage(`File edit: ${msg.description}`, "tool");
1134
+ session.runtimeSession.sendCodexMessage({
1135
+ type: "file-edit",
1136
+ description: msg.description,
1137
+ filePath: msg.path || "unknown",
1138
+ diff: msg.diff,
1139
+ id: node_crypto.randomUUID()
1140
+ });
1141
+ return;
1142
+ }
1143
+ case "terminal-output": {
1144
+ const terminalOutput = index.formatDisplayMessage(msg.data);
1145
+ messageBuffer.addMessage(terminalOutput, "result");
1146
+ session.runtimeSession.sendCodexMessage({
1147
+ type: "terminal-output",
1148
+ data: terminalOutput,
1149
+ callId: node_crypto.randomUUID()
1150
+ });
1151
+ return;
1152
+ }
1153
+ case "permission-request": {
1154
+ const payload = msg.payload || {};
1155
+ session.runtimeSession.sendCodexMessage({
1156
+ type: "permission-request",
1157
+ permissionId: msg.id,
1158
+ toolName: typeof payload.toolName === "string" ? payload.toolName : msg.reason,
1159
+ description: msg.reason,
1160
+ options: payload
1161
+ });
1162
+ return;
1163
+ }
1164
+ case "exec-approval-request": {
1165
+ const { call_id, type, ...inputs } = msg;
1166
+ messageBuffer.addMessage(`Exec approval requested: ${call_id}`, "tool");
1167
+ session.runtimeSession.sendCodexMessage({
1168
+ type: "tool-call",
1169
+ name: "CodexBash",
1170
+ callId: call_id,
1171
+ input: inputs,
1172
+ id: node_crypto.randomUUID()
1173
+ });
1174
+ return;
1175
+ }
1176
+ case "patch-apply-begin": {
1177
+ const changeCount = Object.keys(msg.changes || {}).length;
1178
+ const filesMsg = changeCount === 1 ? "1 file" : `${changeCount} files`;
1179
+ messageBuffer.addMessage(`Modifying ${filesMsg}...`, "tool");
1180
+ session.runtimeSession.sendCodexMessage({
1181
+ type: "tool-call",
1182
+ name: "CodexPatch",
1183
+ callId: msg.call_id,
1184
+ input: {
1185
+ auto_approved: msg.auto_approved,
1186
+ changes: msg.changes
1187
+ },
1188
+ id: node_crypto.randomUUID()
1189
+ });
1190
+ return;
1191
+ }
1192
+ case "patch-apply-end": {
1193
+ if (msg.success) {
1194
+ messageBuffer.addMessage(index.truncateDisplayMessage(msg.stdout || "Files modified successfully", 200), "result");
1195
+ } else {
1196
+ messageBuffer.addMessage(`Error: ${index.truncateDisplayMessage(msg.stderr || "Failed to modify files", 200)}`, "result");
1197
+ }
1198
+ session.runtimeSession.sendCodexMessage({
1199
+ type: "tool-call-result",
1200
+ callId: msg.call_id,
1201
+ output: {
1202
+ stdout: msg.stdout,
1203
+ stderr: msg.stderr,
1204
+ success: msg.success
1205
+ },
1206
+ id: node_crypto.randomUUID()
1207
+ });
1208
+ return;
1209
+ }
1210
+ case "token-count": {
1211
+ session.runtimeSession.sendCodexMessage({
1212
+ ...msg,
1213
+ id: node_crypto.randomUUID()
1214
+ });
1215
+ return;
1216
+ }
1217
+ case "event": {
1218
+ if (msg.name === "thinking") {
1219
+ const thinkingPayload = msg.payload;
1220
+ const thinkingText = typeof thinkingPayload?.text === "string" ? thinkingPayload.text : "";
1221
+ if (thinkingText) {
1222
+ reasoningProcessor.processDelta(thinkingText);
1223
+ if (!thinkingText.startsWith("**")) {
1224
+ messageBuffer.updateLastMessage(`[Thinking] ${thinkingText.substring(0, 100)}...`, "system");
1225
+ }
1226
+ }
1227
+ }
1228
+ return;
1229
+ }
1230
+ default:
1231
+ return;
1232
+ }
1233
+ });
1234
+ };
1235
+ const createBackend = (mode) => {
1236
+ const executionMode = resolveCodexAcpExecutionMode(mode);
1237
+ const validation = index.validateCodexAcpSpawn({
1238
+ command: process.env.HAPPY_CODEX_ACP_COMMAND,
1239
+ model: executionMode.model,
1240
+ sandbox: executionMode.sandbox,
1241
+ approvalPolicy: executionMode.approvalPolicy,
1242
+ baseArgs: session.codexArgs
1243
+ });
1244
+ if (!validation.ok) {
1245
+ throw new Error(validation.errorMessage);
1246
+ }
1247
+ const result = index.createCodexBackend({
1248
+ cwd: session.path,
1249
+ baseArgs: session.codexArgs,
1250
+ model: executionMode.model,
1251
+ sandbox: executionMode.sandbox,
1252
+ approvalPolicy: executionMode.approvalPolicy,
1253
+ mcpServers: {},
1254
+ permissionHandler,
1255
+ selectionHandler: {
1256
+ handleSelection: (request) => selectionHandler.requestSelection(request)
1257
+ }
1258
+ });
1259
+ backend = result.backend;
1260
+ setupBackendMessageHandler(result.backend);
1261
+ return result.backend;
1262
+ };
1263
+ const handleSwitchToLocal = async () => {
1264
+ api.logger.debug("[Codex] Switching to local from RPC");
1265
+ session.pinLocalMode();
1266
+ exitReason = "switch";
1267
+ shouldExit = true;
1268
+ await handleAbort();
1269
+ };
1270
+ session.setPendingInteractionSuperseder((reason = BaseReasoningProcessor.INTERACTION_SUPERSEDED_ERROR) => {
1271
+ return handleIncomingCodexMessageDuringRemoteTurn({
1272
+ reason,
1273
+ supersedePermissions: (value) => permissionHandler.supersedePendingRequests(value),
1274
+ supersedeSelections: (value) => selectionHandler.supersedePendingRequests(value),
1275
+ turnInFlight,
1276
+ abortActiveTurn,
1277
+ onInterrupted: (message) => {
1278
+ api.logger.debug("[Codex] Incoming user message interrupted ACP remote turn");
1279
+ messageBuffer.addMessage(message, "status");
1280
+ session.runtimeSession.sendSessionEvent({ type: "message", message });
1281
+ }
1282
+ });
1283
+ });
1354
1284
  async function handleAbort() {
1355
1285
  api.logger.debug("[Codex] Abort requested - stopping current task");
1356
1286
  try {
1357
- if (client.hasActiveSession()) {
1358
- storedSessionIdForResume = client.storeSessionForResume();
1359
- }
1360
- abortController.abort();
1361
- reasoningProcessor.abort();
1287
+ abortActiveTurn();
1362
1288
  } catch (error) {
1363
1289
  api.logger.debug("[Codex] Error during abort:", error);
1364
- } finally {
1365
- abortController = new AbortController();
1366
1290
  }
1367
1291
  }
1368
1292
  async function handleKillSession() {
@@ -1379,7 +1303,7 @@ async function codexRemoteLauncher(session) {
1379
1303
  session.runtimeSession.sendSessionDeath();
1380
1304
  await session.runtimeSession.flush();
1381
1305
  await session.runtimeSession.close();
1382
- await client.forceCloseSession();
1306
+ await disposeBackend();
1383
1307
  index.stopCaffeinate();
1384
1308
  process.exit(0);
1385
1309
  } catch (error) {
@@ -1387,6 +1311,16 @@ async function codexRemoteLauncher(session) {
1387
1311
  process.exit(1);
1388
1312
  }
1389
1313
  }
1314
+ const handleClientSwap = (clientSession) => {
1315
+ permissionHandler.updateSession(clientSession);
1316
+ selectionHandler.updateSession(clientSession);
1317
+ registerCodexRemoteRpcHandlers(clientSession, {
1318
+ handleAbort,
1319
+ handleSwitchToLocal,
1320
+ handleKillSession
1321
+ });
1322
+ };
1323
+ session.addClientSwapCallback(handleClientSwap);
1390
1324
  if (hasTTY) {
1391
1325
  console.clear();
1392
1326
  inkInstance = ink.render(React.createElement(CodexDisplay, {
@@ -1401,6 +1335,7 @@ async function codexRemoteLauncher(session) {
1401
1335
  },
1402
1336
  onSwitchToLocal: async () => {
1403
1337
  api.logger.debug("[Codex] Switching to local from local keyboard");
1338
+ session.pinLocalMode();
1404
1339
  exitReason = "switch";
1405
1340
  shouldExit = true;
1406
1341
  await handleAbort();
@@ -1422,116 +1357,7 @@ async function codexRemoteLauncher(session) {
1422
1357
  handleSwitchToLocal,
1423
1358
  handleKillSession
1424
1359
  });
1425
- client.setHandler((msg) => {
1426
- api.logger.debug(`[Codex] Message: ${JSON.stringify(msg)}`);
1427
- if (msg.type === "agent_message") {
1428
- messageBuffer.addMessage(msg.message, "assistant");
1429
- } else if (msg.type === "agent_reasoning") {
1430
- messageBuffer.addMessage(`[Thinking] ${msg.text.substring(0, 100)}...`, "system");
1431
- } else if (msg.type === "exec_command_begin") {
1432
- messageBuffer.addMessage(`Executing: ${msg.command}`, "tool");
1433
- } else if (msg.type === "exec_command_end") {
1434
- const output = msg.output || msg.error || "Command completed";
1435
- const truncatedOutput = output.substring(0, 200);
1436
- messageBuffer.addMessage(`Result: ${truncatedOutput}${output.length > 200 ? "..." : ""}`, "result");
1437
- } else if (msg.type === "task_started") {
1438
- messageBuffer.addMessage("Starting task...", "status");
1439
- session.onThinkingChange(true);
1440
- } else if (msg.type === "task_complete") {
1441
- messageBuffer.addMessage("Task completed", "status");
1442
- session.onThinkingChange(false);
1443
- diffProcessor.reset();
1444
- if (shouldEmitCodexReadyOnTurnEnd({ shouldExit, exitReason })) {
1445
- sendReady();
1446
- readyAlreadySent = true;
1447
- }
1448
- } else if (msg.type === "turn_aborted") {
1449
- messageBuffer.addMessage("Turn aborted", "status");
1450
- session.onThinkingChange(false);
1451
- diffProcessor.reset();
1452
- if (shouldEmitCodexReadyOnTurnEnd({ shouldExit, exitReason })) {
1453
- sendReady();
1454
- readyAlreadySent = true;
1455
- }
1456
- }
1457
- if (msg.type === "agent_reasoning_section_break") {
1458
- reasoningProcessor.handleSectionBreak();
1459
- }
1460
- if (msg.type === "agent_reasoning_delta") {
1461
- reasoningProcessor.processDelta(msg.delta);
1462
- }
1463
- if (msg.type === "agent_reasoning") {
1464
- reasoningProcessor.complete(msg.text);
1465
- }
1466
- if (msg.type === "agent_message") {
1467
- session.runtimeSession.sendCodexMessage({
1468
- type: "message",
1469
- message: msg.message,
1470
- id: node_crypto.randomUUID()
1471
- });
1472
- }
1473
- if (msg.type === "exec_command_begin" || msg.type === "exec_approval_request") {
1474
- const { call_id, type, ...inputs } = msg;
1475
- session.runtimeSession.sendCodexMessage({
1476
- type: "tool-call",
1477
- name: "CodexBash",
1478
- callId: call_id,
1479
- input: inputs,
1480
- id: node_crypto.randomUUID()
1481
- });
1482
- }
1483
- if (msg.type === "exec_command_end") {
1484
- const { call_id, type, ...output } = msg;
1485
- session.runtimeSession.sendCodexMessage({
1486
- type: "tool-call-result",
1487
- callId: call_id,
1488
- output,
1489
- id: node_crypto.randomUUID()
1490
- });
1491
- }
1492
- if (msg.type === "token_count") {
1493
- session.runtimeSession.sendCodexMessage({
1494
- ...msg,
1495
- id: node_crypto.randomUUID()
1496
- });
1497
- }
1498
- if (msg.type === "patch_apply_begin") {
1499
- const { call_id, auto_approved, changes } = msg;
1500
- const changeCount = Object.keys(changes).length;
1501
- const filesMsg = changeCount === 1 ? "1 file" : `${changeCount} files`;
1502
- messageBuffer.addMessage(`Modifying ${filesMsg}...`, "tool");
1503
- session.runtimeSession.sendCodexMessage({
1504
- type: "tool-call",
1505
- name: "CodexPatch",
1506
- callId: call_id,
1507
- input: { auto_approved, changes },
1508
- id: node_crypto.randomUUID()
1509
- });
1510
- }
1511
- if (msg.type === "patch_apply_end") {
1512
- const { call_id, stdout, stderr, success } = msg;
1513
- if (success) {
1514
- messageBuffer.addMessage((stdout || "Files modified successfully").substring(0, 200), "result");
1515
- } else {
1516
- messageBuffer.addMessage(`Error: ${(stderr || "Failed to modify files").substring(0, 200)}`, "result");
1517
- }
1518
- session.runtimeSession.sendCodexMessage({
1519
- type: "tool-call-result",
1520
- callId: call_id,
1521
- output: { stdout, stderr, success },
1522
- id: node_crypto.randomUUID()
1523
- });
1524
- }
1525
- if (msg.type === "turn_diff" && msg.unified_diff) {
1526
- diffProcessor.processDiff(msg.unified_diff);
1527
- }
1528
- });
1529
- let readyAlreadySent = false;
1530
1360
  try {
1531
- await client.connect();
1532
- let wasCreated = false;
1533
- let currentModeHash = null;
1534
- let pending = null;
1535
1361
  while (!shouldExit) {
1536
1362
  let message = pending;
1537
1363
  pending = null;
@@ -1549,122 +1375,71 @@ async function codexRemoteLauncher(session) {
1549
1375
  if (!message) {
1550
1376
  break;
1551
1377
  }
1552
- if (wasCreated && currentModeHash && message.hash !== currentModeHash) {
1378
+ if (wasSessionCreated && currentModeHash && message.hash !== currentModeHash) {
1553
1379
  messageBuffer.addMessage("\u2550".repeat(40), "status");
1554
1380
  messageBuffer.addMessage("Starting new Codex session (mode changed)...", "status");
1555
- const prevSessionId = client.getSessionId();
1556
- pendingModeResumeFile = findCodexResumeFile(prevSessionId);
1557
- client.clearSession();
1381
+ await disposeBackend();
1558
1382
  session.clearSessionId();
1559
- wasCreated = false;
1560
1383
  currentModeHash = null;
1561
1384
  pending = message;
1562
1385
  permissionHandler.reset();
1563
1386
  selectionHandler.reset();
1564
- reasoningProcessor.abort();
1565
- diffProcessor.reset();
1566
- session.onThinkingChange(false);
1387
+ resetTurnState();
1567
1388
  continue;
1568
1389
  }
1569
1390
  currentModeHash = message.hash;
1570
1391
  readyAlreadySent = false;
1571
1392
  messageBuffer.addMessage(message.message, "user");
1393
+ const turnSignal = abortController.signal;
1572
1394
  try {
1573
- const approvalPolicy = (() => {
1574
- switch (message.mode.permissionMode) {
1575
- case "default":
1576
- return "untrusted";
1577
- case "read-only":
1578
- return "never";
1579
- case "safe-yolo":
1580
- return "on-failure";
1581
- case "yolo":
1582
- return "on-failure";
1583
- case "bypassPermissions":
1584
- return "on-failure";
1585
- case "acceptEdits":
1586
- return "on-request";
1587
- case "plan":
1588
- return "untrusted";
1589
- default:
1590
- return "untrusted";
1591
- }
1592
- })();
1593
- const sandbox = (() => {
1594
- switch (message.mode.permissionMode) {
1595
- case "default":
1596
- return "workspace-write";
1597
- case "read-only":
1598
- return "read-only";
1599
- case "safe-yolo":
1600
- return "workspace-write";
1601
- case "yolo":
1602
- return "danger-full-access";
1603
- case "bypassPermissions":
1604
- return "danger-full-access";
1605
- case "acceptEdits":
1606
- return "workspace-write";
1607
- case "plan":
1608
- return "workspace-write";
1609
- default:
1610
- return "workspace-write";
1611
- }
1612
- })();
1613
- if (!wasCreated) {
1614
- const startConfig = {
1615
- prompt: message.message,
1616
- sandbox,
1617
- "approval-policy": approvalPolicy,
1618
- config: {}
1619
- };
1620
- if (message.mode.model) {
1621
- startConfig.model = message.mode.model;
1622
- }
1623
- let resumeFile = null;
1624
- if (pendingModeResumeFile) {
1625
- resumeFile = pendingModeResumeFile;
1626
- pendingModeResumeFile = null;
1627
- } else if (storedSessionIdForResume) {
1628
- resumeFile = findCodexResumeFile(storedSessionIdForResume);
1629
- storedSessionIdForResume = null;
1630
- } else if (session.sessionId) {
1631
- resumeFile = findCodexResumeFile(session.sessionId);
1632
- }
1633
- if (resumeFile) {
1634
- startConfig.config = {
1635
- ...startConfig.config || {},
1636
- experimental_resume: resumeFile
1637
- };
1638
- }
1639
- await client.startSession(startConfig, { signal: abortController.signal });
1640
- const currentSessionId = client.getSessionId();
1641
- if (currentSessionId) {
1642
- session.onSessionFound(currentSessionId);
1643
- }
1644
- wasCreated = true;
1645
- } else {
1646
- await client.continueSession(message.message, { signal: abortController.signal });
1395
+ turnInFlight = true;
1396
+ const activeBackend = backend ?? createBackend(message.mode);
1397
+ if (!activeBackend) {
1398
+ throw new Error("Failed to create Codex ACP backend");
1647
1399
  }
1400
+ if (!acpSessionId) {
1401
+ const { sessionId } = await activeBackend.startSession();
1402
+ acpSessionId = sessionId;
1403
+ wasSessionCreated = true;
1404
+ session.onSessionFound(sessionId);
1405
+ }
1406
+ session.onThinkingChange(true);
1407
+ await activeBackend.sendPrompt(acpSessionId, message.message);
1408
+ await waitForResponseCompleteWithAbort(activeBackend, turnSignal);
1409
+ reasoningProcessor.completeCurrent();
1648
1410
  } catch (error) {
1649
- api.logger.warn("Error in codex session:", error);
1411
+ api.logger.warn("Error in codex ACP session:", error);
1650
1412
  const isAbortError = error instanceof Error && error.name === "AbortError";
1651
- const isExpectedInterruption = isAbortError || abortController.signal.aborted || shouldExit || exitReason === "switch";
1652
- if (isExpectedInterruption) {
1413
+ const isExpectedInterruption = isAbortError || turnSignal.aborted || shouldExit || exitReason === "switch";
1414
+ if (exitReason === "switch") {
1415
+ messageBuffer.addMessage("Switching to local mode...", "status");
1416
+ session.runtimeSession.sendSessionEvent({ type: "message", message: "Switching to local mode..." });
1417
+ } else if (isExpectedInterruption) {
1418
+ session.runtimeSession.sendCodexMessage({
1419
+ type: "turn_aborted",
1420
+ id: node_crypto.randomUUID()
1421
+ });
1653
1422
  messageBuffer.addMessage("Aborted by user", "status");
1654
1423
  session.runtimeSession.sendSessionEvent({ type: "message", message: "Aborted by user" });
1655
1424
  } else {
1656
- messageBuffer.addMessage("Process exited unexpectedly", "status");
1657
- session.runtimeSession.sendSessionEvent({ type: "message", message: "Process exited unexpectedly" });
1658
- if (client.hasActiveSession()) {
1659
- storedSessionIdForResume = client.storeSessionForResume();
1660
- }
1425
+ const errorMessage = normalizeCodexBackendError(error);
1426
+ messageBuffer.addMessage(errorMessage, "status");
1427
+ session.runtimeSession.sendSessionEvent({ type: "message", message: errorMessage });
1428
+ await disposeBackend();
1429
+ session.clearSessionId();
1661
1430
  }
1662
1431
  } finally {
1432
+ turnInFlight = false;
1433
+ emitFinalAssistantMessage();
1434
+ if (!shouldExit) {
1435
+ session.runtimeSession.sendCodexMessage({
1436
+ type: "task_complete",
1437
+ id: node_crypto.randomUUID()
1438
+ });
1439
+ }
1663
1440
  permissionHandler.reset();
1664
1441
  selectionHandler.reset();
1665
- reasoningProcessor.abort();
1666
- diffProcessor.reset();
1667
- session.onThinkingChange(false);
1442
+ resetTurnState();
1668
1443
  if (shouldEmitCodexReadyAfterWait({
1669
1444
  pending: Boolean(pending),
1670
1445
  queueSize: session.queue.size(),
@@ -1677,15 +1452,7 @@ async function codexRemoteLauncher(session) {
1677
1452
  }
1678
1453
  }
1679
1454
  } finally {
1680
- client.setHandler(null);
1681
- client.setPermissionHandler(null);
1682
- client.setSelectionHandler(null);
1683
- if (exitReason === "switch") {
1684
- api.logger.debug("[Codex] Remote launcher exiting via switch; waiting for MCP transport disconnect");
1685
- await client.disconnect();
1686
- } else {
1687
- await client.forceCloseSession();
1688
- }
1455
+ await disposeBackend();
1689
1456
  permissionHandler.reset();
1690
1457
  selectionHandler.reset();
1691
1458
  session.setPendingInteractionSuperseder(null);
@@ -1711,20 +1478,21 @@ async function codexRemoteLauncher(session) {
1711
1478
  }
1712
1479
  messageBuffer.clear();
1713
1480
  }
1714
- api.logger.debug(`[Codex] Remote launcher returning: ${exitReason || "exit"}`);
1481
+ api.logger.debug(`[Codex] ACP remote launcher returning: ${exitReason || "exit"}`);
1715
1482
  return exitReason || "exit";
1716
1483
  }
1717
1484
 
1718
1485
  async function codexLoop(opts) {
1719
1486
  let mode = opts.startingMode ?? "local";
1720
- opts.session.onModeChange(mode);
1487
+ await opts.session.onModeChange(mode);
1721
1488
  while (true) {
1722
1489
  api.logger.debug(`[codex-loop] Iteration with mode: ${mode}`);
1723
1490
  if (mode === "local") {
1724
1491
  const result = await codexLocalLauncher(opts.session);
1725
1492
  if (result.type === "switch") {
1493
+ opts.session.clearLocalModePin();
1726
1494
  mode = "remote";
1727
- opts.session.onModeChange(mode);
1495
+ await opts.session.onModeChange(mode);
1728
1496
  continue;
1729
1497
  }
1730
1498
  return result.code;
@@ -1732,13 +1500,53 @@ async function codexLoop(opts) {
1732
1500
  const reason = await codexRemoteLauncher(opts.session);
1733
1501
  if (reason === "switch") {
1734
1502
  mode = "local";
1735
- opts.session.onModeChange(mode);
1503
+ await opts.session.onModeChange(mode);
1736
1504
  continue;
1737
1505
  }
1738
1506
  return 0;
1739
1507
  }
1740
1508
  }
1741
1509
 
1510
+ function supportsAgentStateUpdateEvents(sessionClient) {
1511
+ return typeof sessionClient.once === "function" && typeof sessionClient.off === "function";
1512
+ }
1513
+ async function syncControlledByUserState(sessionClient, controlledByUser) {
1514
+ if (!supportsAgentStateUpdateEvents(sessionClient)) {
1515
+ sessionClient.updateAgentState((currentState) => ({
1516
+ ...currentState,
1517
+ controlledByUser
1518
+ }));
1519
+ return;
1520
+ }
1521
+ await new Promise((resolve) => {
1522
+ let settled = false;
1523
+ const handleUpdated = () => {
1524
+ if (settled) {
1525
+ return;
1526
+ }
1527
+ settled = true;
1528
+ clearTimeout(timeout);
1529
+ sessionClient.off("agent-state-updated", handleUpdated);
1530
+ resolve();
1531
+ };
1532
+ const timeout = setTimeout(() => {
1533
+ if (settled) {
1534
+ return;
1535
+ }
1536
+ settled = true;
1537
+ sessionClient.off("agent-state-updated", handleUpdated);
1538
+ resolve();
1539
+ }, 1500);
1540
+ sessionClient.once("agent-state-updated", handleUpdated);
1541
+ sessionClient.updateAgentState((currentState) => ({
1542
+ ...currentState,
1543
+ controlledByUser
1544
+ }));
1545
+ });
1546
+ }
1547
+ function shouldSupersedeCodexPendingInteractions(opts) {
1548
+ return (opts.currentMode ?? opts.startingMode) === "remote";
1549
+ }
1742
1550
  async function runCodex(opts) {
1743
1551
  const sessionTag = node_crypto.randomUUID();
1744
1552
  api.connectionState.setBackend("Codex");
@@ -1762,7 +1570,15 @@ async function runCodex(opts) {
1762
1570
  machineId,
1763
1571
  startedBy: opts.startedBy
1764
1572
  });
1765
- const response = await api$1.getOrCreateSession({ tag: sessionTag, metadata, state });
1573
+ let response = null;
1574
+ try {
1575
+ response = await api$1.getOrCreateSession({ tag: sessionTag, metadata, state });
1576
+ } catch (error) {
1577
+ if (!api.isAuthenticationRequiredError(error)) {
1578
+ throw error;
1579
+ }
1580
+ api.logger.debug("[codex] Falling back to offline session stub after authentication failure");
1581
+ }
1766
1582
  let sessionClient;
1767
1583
  let codexSession = null;
1768
1584
  const { session: initialSession, reconnectionHandle } = BaseReasoningProcessor.setupOfflineReconnection({
@@ -1804,7 +1620,11 @@ async function runCodex(opts) {
1804
1620
  messageModel = message.meta.model || void 0;
1805
1621
  currentModel = messageModel;
1806
1622
  }
1807
- if (opts.startingMode === "remote") {
1623
+ if (shouldSupersedeCodexPendingInteractions({
1624
+ startingMode: opts.startingMode,
1625
+ currentMode: codexSession?.mode
1626
+ })) {
1627
+ api.logger.debug("[codex] Incoming user message superseding active remote interaction");
1808
1628
  codexSession?.supersedePendingInteractions(BaseReasoningProcessor.INTERACTION_SUPERSEDED_ERROR);
1809
1629
  }
1810
1630
  messageQueue.push(message.content.text, {
@@ -1821,12 +1641,9 @@ async function runCodex(opts) {
1821
1641
  mode: opts.startingMode ?? "local",
1822
1642
  messageQueue,
1823
1643
  codexArgs: opts.codexArgs,
1824
- onModeChange: (mode) => {
1644
+ onModeChange: async (mode) => {
1825
1645
  sessionClient.sendSessionEvent({ type: "switch", mode });
1826
- sessionClient.updateAgentState((currentState) => ({
1827
- ...currentState,
1828
- controlledByUser: mode === "local"
1829
- }));
1646
+ await syncControlledByUserState(sessionClient, mode === "local");
1830
1647
  }
1831
1648
  });
1832
1649
  try {
@@ -1846,3 +1663,6 @@ async function runCodex(opts) {
1846
1663
  }
1847
1664
 
1848
1665
  exports.runCodex = runCodex;
1666
+ exports.shouldSupersedeCodexPendingInteractions = shouldSupersedeCodexPendingInteractions;
1667
+ exports.supportsAgentStateUpdateEvents = supportsAgentStateUpdateEvents;
1668
+ exports.syncControlledByUserState = syncControlledByUserState;