farming-code 2.2.14 → 2.2.17

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 (136) hide show
  1. package/README.md +3 -3
  2. package/README.zh_cn.md +3 -3
  3. package/THIRD_PARTY_NOTICES.md +17 -1
  4. package/backend/acp/packaged-codex-acp.js +13 -0
  5. package/backend/acp-runtime.js +413 -24
  6. package/backend/agent-manager.js +572 -557
  7. package/backend/agent-provider-session.js +15 -1
  8. package/backend/agent-runtime-binding.js +9 -55
  9. package/backend/chat-runtime.js +28 -0
  10. package/backend/codex-rollout-follower.js +83 -0
  11. package/backend/codex-session-archive.js +12 -1
  12. package/backend/codex-session-history.js +94 -0
  13. package/backend/codex-transcript.js +2 -1
  14. package/backend/config-manager.js +25 -17
  15. package/backend/control-api.js +3 -1
  16. package/backend/farming-app-cli.js +21 -0
  17. package/backend/farming-session-store.js +2 -1
  18. package/backend/native-pty-host-client.js +59 -2
  19. package/backend/npm-update-helper.js +65 -7
  20. package/backend/provider-adapters.js +123 -38
  21. package/backend/provider-session-service.js +138 -34
  22. package/backend/server.js +94 -105
  23. package/backend/storage-layout.js +5 -16
  24. package/backend/update-service.js +94 -5
  25. package/backend/usage-history-client.js +305 -0
  26. package/backend/usage-history-scanner.generated.js +2250 -0
  27. package/backend/usage-history-scanner.ts +2718 -0
  28. package/backend/usage-history-smoke.js +64 -0
  29. package/backend/usage-history-worker.js +34 -0
  30. package/backend/usage-monitor.js +188 -588
  31. package/backend/vendor/usage-parsers/CC_STATISTICS_LICENSE +21 -0
  32. package/backend/vendor/usage-parsers/CODEXBAR_LICENSE +21 -0
  33. package/backend/vendor/usage-parsers/VENDOR.json +45 -0
  34. package/dist/acp/LICENSE.codex-acp +190 -0
  35. package/dist/acp/codex-acp-1.1.4.js +30718 -0
  36. package/dist/assets/App-6yWdwzj8.js +210 -0
  37. package/dist/assets/{FileEditorMarkdownPreview-UlVd0O4D.js → FileEditorMarkdownPreview-R0jwwLME.js} +4 -4
  38. package/dist/assets/FileEditorPane-DNy3LAqS.js +2 -0
  39. package/dist/assets/{IconGlyphs-AU22gPDY.js → IconGlyphs-BsUfqpKN.js} +1 -1
  40. package/dist/assets/ProjectFilesSection-BhDEUVHA.js +12 -0
  41. package/dist/assets/{ReviewPage-CeDKDyZh.js → ReviewPage-ChtByBe3.js} +1 -1
  42. package/dist/assets/{abnfDiagram-VRR7QNED-BgawzDQT.js → abnfDiagram-VRR7QNED-nnglrXFi.js} +1 -1
  43. package/dist/assets/{architectureDiagram-ZJ3FMSHR-DUShwPg0.js → architectureDiagram-ZJ3FMSHR-BEXMaFfY.js} +1 -1
  44. package/dist/assets/{blockDiagram-677ZJIJ3-CeGZviXR.js → blockDiagram-677ZJIJ3-1mCezSBk.js} +1 -1
  45. package/dist/assets/{c4Diagram-LMCZKHZV-CLhYht6S.js → c4Diagram-LMCZKHZV-DhTiI6Uv.js} +1 -1
  46. package/dist/assets/channel-BVWNatUB.js +1 -0
  47. package/dist/assets/{chunk-32BRIVSS-LFYpT-Cu.js → chunk-32BRIVSS-CCdsKd73.js} +1 -1
  48. package/dist/assets/{chunk-52WLFC77-BH6jDw9J.js → chunk-52WLFC77-Gncjk4LF.js} +1 -1
  49. package/dist/assets/{chunk-C7G6YPKG-B_mIUlb7.js → chunk-C7G6YPKG-CiDVUP82.js} +1 -1
  50. package/dist/assets/{chunk-EX3LRPZG-C63hVuAJ.js → chunk-EX3LRPZG-z6gXP2_N.js} +1 -1
  51. package/dist/assets/{chunk-FWX5IMBZ-CboX-WF_.js → chunk-FWX5IMBZ-DMZBccQa.js} +2 -2
  52. package/dist/assets/{chunk-HOUHSVGY-BOgSd6Un.js → chunk-HOUHSVGY-D8tIeoPk.js} +1 -1
  53. package/dist/assets/{chunk-ICXQ74PX-C0K-pgZt.js → chunk-ICXQ74PX-b6oQ2Q17.js} +1 -1
  54. package/dist/assets/{chunk-MOJQB5TN-CVMfwN_7.js → chunk-MOJQB5TN-DAVH_p2a.js} +1 -1
  55. package/dist/assets/{chunk-OGEWGWER-DLT3pxyw.js → chunk-OGEWGWER-DxoiMCI-.js} +1 -1
  56. package/dist/assets/{chunk-PUDLZKDR-Cd079yIQ.js → chunk-PUDLZKDR-NsOWh7HM.js} +1 -1
  57. package/dist/assets/{chunk-Q4XR5HBZ-D0jHa6SO.js → chunk-Q4XR5HBZ-Di4H7L21.js} +1 -1
  58. package/dist/assets/{chunk-V7JOEXUC-CVDtR75H.js → chunk-V7JOEXUC-DttpZg9Y.js} +1 -1
  59. package/dist/assets/{chunk-VAUOI2AC-B-W2_9Ot.js → chunk-VAUOI2AC-UHNr_yhX.js} +1 -1
  60. package/dist/assets/{chunk-VR4S4FIN-FD3kWh32.js → chunk-VR4S4FIN-D-Fmtd4Z.js} +1 -1
  61. package/dist/assets/{chunk-WYO6CB5R-C2bGaS5r.js → chunk-WYO6CB5R-CUxMtfK2.js} +4 -4
  62. package/dist/assets/{chunk-ZGVPDNZ5-ymgts0zo.js → chunk-ZGVPDNZ5-CoV2QEl0.js} +1 -1
  63. package/dist/assets/classDiagram-OUVF2IWQ-Db0ib5iE.js +1 -0
  64. package/dist/assets/classDiagram-v2-EOCWNBFH-Bkf69XF9.js +1 -0
  65. package/dist/assets/code-dark-5esj0bRu.css +1 -0
  66. package/dist/assets/{cynefinDiagram-TSTJHNR4-CwpobXIl.js → cynefinDiagram-TSTJHNR4-0xPI92Lg.js} +1 -1
  67. package/dist/assets/{dagre-VKFMJZFB-Dkhlfjna.js → dagre-VKFMJZFB-DfuMG4gU.js} +1 -1
  68. package/dist/assets/{diagram-FQU43EPY-hA8dicp6.js → diagram-FQU43EPY-CT00bLZ6.js} +1 -1
  69. package/dist/assets/{diagram-G47NLZAW-BTY6JTzO.js → diagram-G47NLZAW-Xbh7zXUx.js} +1 -1
  70. package/dist/assets/{diagram-NH7WQ7WH-xkaHHX9M.js → diagram-NH7WQ7WH-SRDVPfn7.js} +1 -1
  71. package/dist/assets/{diagram-OA4YK3LP-gk_fg9UU.js → diagram-OA4YK3LP-CQRli4Np.js} +1 -1
  72. package/dist/assets/{diagram-WEI45ONY-BvC8xd6T.js → diagram-WEI45ONY-DMf17WQi.js} +1 -1
  73. package/dist/assets/{ebnfDiagram-CCIWWBDH-CR7RaNat.js → ebnfDiagram-CCIWWBDH-DhpFkgJ5.js} +1 -1
  74. package/dist/assets/{erDiagram-Q63AITRT-_M-JFPbj.js → erDiagram-Q63AITRT-1IwxWmJo.js} +1 -1
  75. package/dist/assets/file-icons-DpbrD5ZP.js +1 -0
  76. package/dist/assets/flowDiagram-23GEKE2U-DYFo2xmB.js +1 -0
  77. package/dist/assets/{ganttDiagram-NO4QXBWP-Bl8P1yf_.js → ganttDiagram-NO4QXBWP-BNCdfyr1.js} +1 -1
  78. package/dist/assets/{gitGraphDiagram-IHSO6WYX-DoHa5ysz.js → gitGraphDiagram-IHSO6WYX-pbyWZuiX.js} +1 -1
  79. package/dist/assets/{index-BA-iH2Kg.js → index-C8ZFKye1.js} +2 -2
  80. package/dist/assets/{infoDiagram-FWYZ7A6U-DSFcSuUs.js → infoDiagram-FWYZ7A6U-BcTOgDbe.js} +1 -1
  81. package/dist/assets/{ishikawaDiagram-FXEZZL3T-DdkqullI.js → ishikawaDiagram-FXEZZL3T-DCdvGKrn.js} +1 -1
  82. package/dist/assets/{journeyDiagram-5HDEW3XC-B4hRnQJr.js → journeyDiagram-5HDEW3XC-BZ-1q_IT.js} +1 -1
  83. package/dist/assets/{kanban-definition-HUTT4EX6-tm2FZB2E.js → kanban-definition-HUTT4EX6-BIgejCjj.js} +1 -1
  84. package/dist/assets/{line-CmsjK4kn.js → line-CfptFMmX.js} +1 -1
  85. package/dist/assets/main-DSO3i_Gs.css +1 -0
  86. package/dist/assets/{mermaid.core-C7UT_EEL.js → mermaid.core-gSpX30pV.js} +3 -3
  87. package/dist/assets/{mindmap-definition-LN4V7U3C-DplfjGms.js → mindmap-definition-LN4V7U3C-BKBaI4LW.js} +1 -1
  88. package/dist/assets/{pegDiagram-2B236MQR-lu3SdkQs.js → pegDiagram-2B236MQR-sBvDY3VX.js} +1 -1
  89. package/dist/assets/{pieDiagram-ENE6RG2P-3fHbmDf9.js → pieDiagram-ENE6RG2P-DWlWcu08.js} +1 -1
  90. package/dist/assets/{quadrantDiagram-ABIIQ3AL-a5qoWz-X.js → quadrantDiagram-ABIIQ3AL-DZutUd3J.js} +1 -1
  91. package/dist/assets/{railroadDiagram-RFXS5EU6-D2TOuXW9.js → railroadDiagram-RFXS5EU6-DQb3lXGM.js} +1 -1
  92. package/dist/assets/{requirementDiagram-TGXJPOKE-LQBWpUvd.js → requirementDiagram-TGXJPOKE-DqFGXu2e.js} +1 -1
  93. package/dist/assets/{sankeyDiagram-HTMAVEWB-B1ohYoyK.js → sankeyDiagram-HTMAVEWB-d7ECahnO.js} +1 -1
  94. package/dist/assets/{sequenceDiagram-DBY2YBRQ-C3vM-hMq.js → sequenceDiagram-DBY2YBRQ-DdsbKqct.js} +1 -1
  95. package/dist/assets/{stateDiagram-2N3HPSRC-BvFhOryI.js → stateDiagram-2N3HPSRC-P6iSrgwR.js} +1 -1
  96. package/dist/assets/stateDiagram-v2-6OUMAXLB-DIYSf_qh.js +1 -0
  97. package/dist/assets/{swimlanes-5IMT3BWC-DKF_b-2s.js → swimlanes-5IMT3BWC-BGnZJx1M.js} +1 -1
  98. package/dist/assets/swimlanesDiagram-G3AALYLV-BIkrlKrZ.js +8 -0
  99. package/dist/assets/{timeline-definition-FHXFAJF6-vAs11ux_.js → timeline-definition-FHXFAJF6-rA-r3aUR.js} +1 -1
  100. package/dist/assets/{vennDiagram-L72KCM5P-BybTX_9B.js → vennDiagram-L72KCM5P-DiVKUjA9.js} +1 -1
  101. package/dist/assets/{wardleyDiagram-EHGQE667-CicUmMBR.js → wardleyDiagram-EHGQE667-Cfxh7UOn.js} +1 -1
  102. package/dist/assets/workspace-editor-model-D0Im34S5.js +1 -0
  103. package/dist/assets/{workspace-editor-monaco-BICJESG0.js → workspace-editor-monaco-BIpqcWf1.js} +1 -1
  104. package/dist/assets/workspace-editor-monaco-DLlws5Ig.js +1 -0
  105. package/dist/assets/{workspace-view-state-DvYG_9PH.js → workspace-view-state-BpPINu84.js} +1 -1
  106. package/dist/assets/{xychartDiagram-FW5EYKEG-BhWDyCX-.js → xychartDiagram-FW5EYKEG--hmryQeL.js} +1 -1
  107. package/dist/crt-mermaid-renderer.js +313 -313
  108. package/dist/index.html +1 -1
  109. package/frontend/session-bridge.js +24 -2
  110. package/frontend/skins/crt/app.js +203 -85
  111. package/frontend/skins/crt/index.html +2 -11
  112. package/frontend/skins/crt/styles/effects.css +2 -34
  113. package/package.json +30 -12
  114. package/shared/backend-connection-status.d.ts +25 -0
  115. package/shared/backend-connection-status.js +40 -0
  116. package/shared/browser-protocol.js +3 -3
  117. package/backend/app-server-api.js +0 -917
  118. package/backend/codex-app-server-home.js +0 -39
  119. package/backend/codex-app-server-runtime.js +0 -901
  120. package/dist/assets/App-Bumi772v.js +0 -206
  121. package/dist/assets/FileEditorPane-Dxx4rKOg.js +0 -2
  122. package/dist/assets/ProjectFilesSection-DE7Nb9Jk.js +0 -12
  123. package/dist/assets/channel-Uw90AO0b.js +0 -1
  124. package/dist/assets/classDiagram-OUVF2IWQ-DKW_dtku.js +0 -1
  125. package/dist/assets/classDiagram-v2-EOCWNBFH-C8Fi7-yv.js +0 -1
  126. package/dist/assets/code-dark-DPiuyeSp.css +0 -1
  127. package/dist/assets/code-mobile-3C2Rr-CN.css +0 -1
  128. package/dist/assets/file-icons-Bw2qd5iT.js +0 -1
  129. package/dist/assets/flowDiagram-23GEKE2U-DAiS5Y7G.js +0 -1
  130. package/dist/assets/main-Bqtb5kuz.css +0 -1
  131. package/dist/assets/stateDiagram-v2-6OUMAXLB-WMrz4PNo.js +0 -1
  132. package/dist/assets/swimlanesDiagram-G3AALYLV-BGswC9oC.js +0 -8
  133. package/dist/assets/workspace-editor-model-BQol4qbA.js +0 -1
  134. package/dist/assets/workspace-editor-monaco-DRJ7IaXk.js +0 -1
  135. package/scripts/sync-ghostty-vendor.js +0 -66
  136. /package/dist/assets/{core-D0LFJkDt.js → core-B9MwRjDE.js} +0 -0
@@ -37,16 +37,15 @@ const {
37
37
  providerSupportsRuntime,
38
38
  } = require('./provider-adapters');
39
39
  const { deriveTerminalStatus } = require('./terminal-status');
40
- const { CodexAppServerRuntime, normalizeCodexRuntimeMode } = require('./codex-app-server-runtime');
41
40
  const { JsonCliRuntime } = require('./json-cli-runtime');
42
- const { AcpRuntime } = require('./acp-runtime');
41
+ const { AcpRuntime, isCodexSteerUnavailableError } = require('./acp-runtime');
42
+ const { chatRuntimeForProvider, isChatMode } = require('./chat-runtime');
43
43
  const { acpToolChanges, acpToolDetail, acpToolReviewChanges, acpTranscriptToolEntry } = require('./acp-transcript');
44
44
  const {
45
45
  applyCodexTerminalProfile,
46
46
  codexTerminalProfileFromOutput,
47
47
  codexTerminalProfileFromPreview,
48
48
  } = require('./codex-terminal-profile');
49
- const { ensureCodexAppServerHome } = require('./codex-app-server-home');
50
49
  const {
51
50
  ensureAgentOrders,
52
51
  finiteOrder,
@@ -77,6 +76,9 @@ const MISSING_ENGINE_SESSION_STARTUP_GRACE_MS = 5000;
77
76
  const MIN_TERMINAL_RESIZE_COLS = 40;
78
77
  const MIN_TERMINAL_RESIZE_ROWS = 10;
79
78
  const AGENT_DISCOVERY_CACHE_MAX_AGE_MS = 3_000;
79
+ const UNCERTAIN_TERMINAL_STOP_TIMEOUT_MS = 5_000;
80
+ const TERMINAL_STOP_STATE_READ_TIMEOUT_MS = 1_000;
81
+ const TERMINAL_STOP_POLL_MS = 50;
80
82
  const SHELL_PROMPT_ENV_KEYS = [
81
83
  'PS1',
82
84
  'PS2',
@@ -89,6 +91,31 @@ const SHELL_PROMPT_ENV_KEYS = [
89
91
  ];
90
92
  const execFileAsync = promisify(execFile);
91
93
 
94
+ function withBoundedWait(promise, timeoutMs, label) {
95
+ let timer;
96
+ const timeout = new Promise((_, reject) => {
97
+ timer = setTimeout(() => reject(new Error(`${label} timed out after ${timeoutMs}ms`)), timeoutMs);
98
+ timer.unref?.();
99
+ });
100
+ return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
101
+ }
102
+
103
+ async function deletePrecreatedProviderSession(options = {}) {
104
+ const provider = String(options.provider || '').trim().toLowerCase();
105
+ const sessionId = String(options.sessionId || '').trim();
106
+ if (!isSafeProviderSessionId(sessionId)) throw new Error('Invalid pre-created provider session id');
107
+ let args = [];
108
+ if (provider === 'codex') args = ['delete', '--force', sessionId];
109
+ else if (provider === 'opencode') args = ['session', 'delete', sessionId];
110
+ else throw new Error(`${provider || 'Provider'} does not support pre-created session rollback`);
111
+ await execFileAsync(options.executable, args, {
112
+ cwd: options.cwd,
113
+ env: options.env,
114
+ timeout: 15_000,
115
+ maxBuffer: 1024 * 1024,
116
+ });
117
+ }
118
+
92
119
  function trimSessionOutput(output) {
93
120
  const text = typeof output === 'string' ? output : '';
94
121
  return text.length > SESSION_OUTPUT_LIMIT ? text.slice(-SESSION_OUTPUT_LIMIT) : text;
@@ -177,12 +204,6 @@ function applyTerminalStateCursor(agent, runtimeEpoch, outputSeq, stateRevision,
177
204
  return true;
178
205
  }
179
206
 
180
- function hasResumeArg(args) {
181
- return Array.isArray(args)
182
- ? args.some(arg => arg === '--resume' || (typeof arg === 'string' && arg.startsWith('--resume=')))
183
- : false;
184
- }
185
-
186
207
  function codexCommandContinuesSession(command) {
187
208
  const parts = parseCommand(command);
188
209
  return path.basename(parts[0] || '') === 'codex'
@@ -216,18 +237,6 @@ function agentHomeProviderForProgram(command) {
216
237
  return providerForProgram(agentProgramName(command));
217
238
  }
218
239
 
219
- function hasUsableCodexAppServerHome(metadata) {
220
- const homePath = String(runtimeBindingOf(metadata, 'app-server')?.homePath || '').trim();
221
- return Boolean(homePath) && fs.existsSync(homePath);
222
- }
223
-
224
- function shouldRecoverAsCodexAppServer(metadata) {
225
- const provider = metadata?.providerSessionProvider || metadata?.provider || '';
226
- return provider === 'codex'
227
- && runtimeKind(metadata) === 'app-server'
228
- && hasUsableCodexAppServerHome(metadata);
229
- }
230
-
231
240
  function isJsonCliAgent(agent) {
232
241
  return runtimeKind(agent) === 'json';
233
242
  }
@@ -236,6 +245,19 @@ function isAcpAgent(agent) {
236
245
  return runtimeKind(agent) === 'acp';
237
246
  }
238
247
 
248
+ function normalizedComposerPrompt(message) {
249
+ const prompt = Array.isArray(message) ? message : [{ type: 'text', text: String(message || '') }];
250
+ const text = prompt
251
+ .filter(content => content?.type === 'text')
252
+ .map(content => String(content.text || ''))
253
+ .join('')
254
+ .trim();
255
+ if (prompt.length === 0 || (!text && !prompt.some(content => content?.type !== 'text'))) {
256
+ throw new Error('Composer message is empty');
257
+ }
258
+ return prompt;
259
+ }
260
+
239
261
  function isShellProgram(command) {
240
262
  return ['bash', 'zsh', 'sh', 'fish'].includes(agentProgramName(command).toLowerCase());
241
263
  }
@@ -264,7 +286,6 @@ function activeCodexTerminalProfile(agent, previewText) {
264
286
  || agentHomeProviderForProgram(agent.forkCommand || agent.command || '');
265
287
  if (
266
288
  provider !== 'codex'
267
- || isCodexAppServerAgent(agent)
268
289
  || isJsonCliAgent(agent)
269
290
  || isAcpAgent(agent)
270
291
  || runtimeKind(agent) !== 'terminal'
@@ -379,13 +400,23 @@ function hasAgentOutputAfterAttentionBaseline(agent) {
379
400
  return false;
380
401
  }
381
402
 
382
- function shouldRecoverEngineSession(metadata) {
383
- if (!metadata) return false;
384
- // Main Agent shells are a product session, not an ephemeral scratch shell.
385
- // Keep them attached to the persistent native PTY host across server restarts.
386
- if (metadata.wantsMain === true) return true;
387
- if (metadata.category === 'shell') return false;
388
- return !isShellProgram(metadata.forkCommand || metadata.command || '');
403
+ function shouldRestoreAgentFromMetadata(record, mainPageSessionKeys) {
404
+ if (!record || record.archived === true) return false;
405
+ if (record.wantsMain === true) return true;
406
+ if (
407
+ record.providerSessionTemporary === true
408
+ || isTemporaryProviderSessionId(record.providerSessionId)
409
+ ) {
410
+ return record.visibleOnMainPage === true;
411
+ }
412
+ const provider = String(record.providerSessionProvider || record.provider || '').trim();
413
+ const sessionKey = record.providerSessionKey || mainPageAgentSessionKey(
414
+ provider,
415
+ record.providerSessionId,
416
+ record.providerHomeId || 'default'
417
+ );
418
+ if (sessionKey) return mainPageSessionKeys.has(sessionKey);
419
+ return record.visibleOnMainPage === true;
389
420
  }
390
421
 
391
422
  function recoveredEngineSessionId(entry, metadata = {}) {
@@ -500,10 +531,6 @@ function isRunningAgentRuntimeStatus(status) {
500
531
  return String(status || '').toLowerCase() === 'running';
501
532
  }
502
533
 
503
- function isCodexAppServerAgent(agent) {
504
- return runtimeKind(agent) === 'app-server';
505
- }
506
-
507
534
  function isLiveEngineSessionState(sessionState) {
508
535
  return Boolean(sessionState && sessionState.status && sessionState.status !== 'exited');
509
536
  }
@@ -554,6 +581,7 @@ class AgentManager extends EventEmitter {
554
581
  );
555
582
  this.agents = new RuntimeAgentMap();
556
583
  this.mainAgentId = null;
584
+ this.mainAgentStartReservation = null;
557
585
  this.lastActivity = new Map();
558
586
  this.lastActivityUpdate = new Map();
559
587
  this.outputEvents = new Map(); // Map<agentId, Array<{timestamp, bytes}>> for rate tracking
@@ -567,8 +595,11 @@ class AgentManager extends EventEmitter {
567
595
  this.permissionRestartInFlight = new Map();
568
596
  this.runtimeRestartInFlight = new Map();
569
597
  this.codexSessionUnarchiveInFlight = new Map();
598
+ // Standard ACP session inputs may contain MCP credentials. Keep the live
599
+ // copy outside browser-facing Agent records; crash recovery persists it
600
+ // only through the private Farming session store.
601
+ this.acpSessionOptionsByKey = new Map();
570
602
  this.permissionRestartSuppressedAgentIds = new Set();
571
- this.codexAppServerRuntime = options.codexAppServerRuntime || new CodexAppServerRuntime();
572
603
  this.jsonCliRuntime = options.jsonCliRuntime || new JsonCliRuntime();
573
604
  this.acpRuntime = options.acpRuntime || new AcpRuntime({
574
605
  ...(this.configManager?.farmingDir ? { configDir: this.configManager.farmingDir } : {}),
@@ -582,6 +613,12 @@ class AgentManager extends EventEmitter {
582
613
  }
583
614
  : {}),
584
615
  });
616
+ this.createProviderSessionIdentity = typeof options.createProviderSessionIdentity === 'function'
617
+ ? options.createProviderSessionIdentity
618
+ : createOptions => this.acpRuntime.createSessionIdentity(createOptions);
619
+ this.deleteProviderSessionIdentity = typeof options.deleteProviderSessionIdentity === 'function'
620
+ ? options.deleteProviderSessionIdentity
621
+ : deletePrecreatedProviderSession;
585
622
  this.archiveCodexSession = options.archiveCodexSession || archiveCodexSession;
586
623
  this.unarchiveCodexSession = options.unarchiveCodexSession || unarchiveCodexSession;
587
624
  this.heartbeatInterval = null;
@@ -612,7 +649,6 @@ class AgentManager extends EventEmitter {
612
649
  this.lastZombieSweepAt = 0;
613
650
  this.startHeartbeat();
614
651
  this.bindEngineEvents();
615
- this.bindCodexAppServerRuntimeEvents();
616
652
  this.bindJsonCliRuntimeEvents();
617
653
  this.bindAcpRuntimeEvents();
618
654
  if (this.configManager && this.configManager.farmingDir) {
@@ -649,13 +685,14 @@ class AgentManager extends EventEmitter {
649
685
 
650
686
  bindAcpRuntimeEvents() {
651
687
  if (!this.acpRuntime || typeof this.acpRuntime.on !== 'function') return;
652
- this.acpRuntime.on('agent-runtime', ({ agentId, state, error, sessionId, stopReason, pendingPermission, pendingPermissions, pendingElicitation, pendingElicitations, activeElicitations, updatedAt }) => {
688
+ this.acpRuntime.on('agent-runtime', ({ agentId, state, error, sessionId, stopReason, supportsSteer, pendingPermission, pendingPermissions, pendingElicitation, pendingElicitations, activeElicitations, updatedAt }) => {
653
689
  const agent = this.agents.get(agentId);
654
690
  const runtime = runtimeBindingOf(agent, 'acp');
655
691
  if (!runtime) return;
656
692
  runtime.state = state || '';
657
693
  runtime.error = error || '';
658
694
  runtime.stopReason = stopReason || '';
695
+ runtime.supportsSteer = supportsSteer === true;
659
696
  runtime.pendingPermission = pendingPermission || null;
660
697
  runtime.pendingPermissions = Array.isArray(pendingPermissions) ? pendingPermissions : [];
661
698
  runtime.pendingElicitation = pendingElicitation || null;
@@ -679,46 +716,6 @@ class AgentManager extends EventEmitter {
679
716
  });
680
717
  }
681
718
 
682
- bindCodexAppServerRuntimeEvents() {
683
- if (!this.codexAppServerRuntime || typeof this.codexAppServerRuntime.on !== 'function') return;
684
- this.codexAppServerRuntime.on('agent-runtime', ({ agentId, ...patch }) => {
685
- const agent = this.agents.get(agentId);
686
- const runtime = runtimeBindingOf(agent, 'app-server');
687
- if (!runtime) return;
688
-
689
- if (typeof patch.threadId === 'string' && patch.threadId) {
690
- runtime.threadId = patch.threadId;
691
- this.providerSessionService.bindConfirmed(
692
- agentId,
693
- agent.providerSessionProvider || 'codex',
694
- patch.threadId
695
- );
696
- }
697
- if (typeof patch.turnId === 'string') runtime.turnId = patch.turnId;
698
- if (typeof patch.state === 'string' && patch.state) runtime.state = patch.state;
699
- if (typeof patch.error === 'string') runtime.error = patch.error;
700
- if (typeof patch.pendingRequestId === 'string') runtime.pendingRequestId = patch.pendingRequestId;
701
- if (typeof patch.pendingRequestMethod === 'string') runtime.pendingRequestMethod = patch.pendingRequestMethod;
702
- if (Object.prototype.hasOwnProperty.call(patch, 'pendingRequest')) {
703
- runtime.pendingRequest = patch.pendingRequest || null;
704
- }
705
- if (Object.prototype.hasOwnProperty.call(patch, 'notice')) {
706
- runtime.notice = patch.notice || null;
707
- }
708
- if (Object.prototype.hasOwnProperty.call(patch, 'goal')) {
709
- runtime.goal = patch.goal || null;
710
- }
711
- if (typeof patch.transcriptUpdatedAt === 'string') {
712
- runtime.transcriptUpdatedAt = patch.transcriptUpdatedAt;
713
- }
714
-
715
- if (patch.state === 'working' || patch.state === 'waiting-for-input') {
716
- this.lastActivity.set(agentId, Date.now());
717
- }
718
- this.emit('update');
719
- });
720
- }
721
-
722
719
  bindEngineEvents() {
723
720
  this.engineBridge.on('session-started', ({
724
721
  sessionId,
@@ -760,6 +757,9 @@ class AgentManager extends EventEmitter {
760
757
 
761
758
  this.reviveAgentRuntime(agent);
762
759
  agent.output = trimSessionOutput(agent.output + data);
760
+ if (agent.providerSessionProvider === 'codex' && agent.providerSessionTemporary === true) {
761
+ void this.providerSessionService.resolveTemporaryCodex(sessionId);
762
+ }
763
763
  const outputAt = Date.now();
764
764
  agent.lastEngineOutputAt = outputAt;
765
765
  this.lastActivity.set(sessionId, outputAt);
@@ -1055,12 +1055,6 @@ class AgentManager extends EventEmitter {
1055
1055
  clearPendingTerminalStartSyncCut(agent);
1056
1056
  if (this.permissionRestartSuppressedAgentIds.has(sessionId)) return;
1057
1057
 
1058
- // A fresh App Server thread deliberately has no CLI/PTy observer until
1059
- // its first turn has completed and become resumable. Native host
1060
- // cleanup can still report an old/missing session id while that gap is
1061
- // open; it is not an exit of the App Server Agent.
1062
- if (isCodexAppServerAgent(agent)) return;
1063
-
1064
1058
  if (stateProofAvailable === false) {
1065
1059
  this.providerSessionService.stop(sessionId);
1066
1060
  agent.status = 'dead';
@@ -1134,6 +1128,7 @@ class AgentManager extends EventEmitter {
1134
1128
  const persistedRecords = this.configManager && typeof this.configManager.listAgentSessionRecords === 'function'
1135
1129
  ? this.configManager.listAgentSessionRecords()
1136
1130
  : [];
1131
+ const mainPageSessionKeys = new Set(this.getMainPageSessionKeys());
1137
1132
  const persistedByRuntimeAgentId = new Map(persistedRecords
1138
1133
  .filter(record => record && record.runtimeAgentId)
1139
1134
  .map(record => [record.runtimeAgentId, record]));
@@ -1144,6 +1139,11 @@ class AgentManager extends EventEmitter {
1144
1139
  const state = entry.state || {};
1145
1140
  const agentId = recoveredEngineSessionId(entry, engineMetadata);
1146
1141
  const persisted = persistedByRuntimeAgentId.get(agentId);
1142
+ const desiredMetadata = persisted || engineMetadata;
1143
+ if (!shouldRestoreAgentFromMetadata(desiredMetadata, mainPageSessionKeys)) {
1144
+ await this.killRecoveredEngineSession(entry, engineMetadata, agentId);
1145
+ continue;
1146
+ }
1147
1147
  // The persisted runtime mode is authoritative. A PTY can outlive the
1148
1148
  // server long enough to appear in native-host recovery after the Agent
1149
1149
  // has already switched to ACP. Recovering that stale PTY first would
@@ -1162,6 +1162,7 @@ class AgentManager extends EventEmitter {
1162
1162
  // even briefly makes Chat/Terminal switching disappear until a later
1163
1163
  // provider resolver update happens to repair it.
1164
1164
  source: persisted.source || engineMetadata.source,
1165
+ persistentSessionId: persisted.id || persisted.persistentSessionId || engineMetadata.persistentSessionId,
1165
1166
  projectWorkspace: persisted.projectWorkspace || engineMetadata.projectWorkspace,
1166
1167
  provider: persistedProvider || engineMetadata.provider,
1167
1168
  providerSessionProvider: persistedProvider || engineMetadata.providerSessionProvider,
@@ -1179,21 +1180,23 @@ class AgentManager extends EventEmitter {
1179
1180
  terminalInputReceived: Object.prototype.hasOwnProperty.call(persisted, 'terminalInputReceived')
1180
1181
  ? persisted.terminalInputReceived === true
1181
1182
  : engineMetadata.terminalInputReceived,
1183
+ customTitle: Object.prototype.hasOwnProperty.call(persisted, 'customTitle')
1184
+ ? persisted.customTitle
1185
+ : engineMetadata.customTitle,
1182
1186
  ...legacyRuntimeMetadata(persisted),
1183
1187
  pinned: persisted.pinned === true,
1184
1188
  projectOrder: finiteOrder(persisted.projectOrder) ?? finiteOrder(engineMetadata.projectOrder),
1185
1189
  pinnedOrder: finiteOrder(persisted.pinnedOrder) ?? finiteOrder(engineMetadata.pinnedOrder),
1186
1190
  } : engineMetadata;
1187
1191
  if (!agentId || this.agents.has(agentId)) continue;
1188
- if (!shouldRecoverEngineSession(metadata)) {
1189
- await this.killRecoveredEngineSession(entry, metadata, agentId);
1190
- continue;
1191
- }
1192
1192
 
1193
1193
  const agentRecord = this.recoveredAgentRecord(agentId, entry.engineName || metadata.engineName || 'native', metadata, state);
1194
1194
  ensureAgentOrders(agentRecord, Array.from(this.agents.values()));
1195
1195
  agentRecord.lastObservedTurnActive = this.isAgentAttentionTurnActive(agentRecord);
1196
- this.ensurePersistentAgentSession(agentRecord);
1196
+ this.ensurePersistentAgentSession(agentRecord, {
1197
+ visibleOnMainPage: true,
1198
+ archived: false,
1199
+ });
1197
1200
  this.agents.set(agentId, agentRecord);
1198
1201
  void this.refreshAgentWorktree(agentId);
1199
1202
  this.lastActivity.set(agentId, state.lastActivityAt || metadata.lastActivityAt || Date.now());
@@ -1215,12 +1218,12 @@ class AgentManager extends EventEmitter {
1215
1218
  await this.restoreTerminalSessionsAfterRuntimeRotation(persistedRecords, runtimeRotations);
1216
1219
  }
1217
1220
 
1218
- await this.recoverCodexAppServerSessions();
1219
1221
  await this.recoverAcpSessions();
1220
1222
  }
1221
1223
 
1222
1224
  async restoreTerminalSessionsAfterRuntimeRotation(records, rotations) {
1223
1225
  const mainPageOrder = new Map(this.getMainPageSessionKeys().map((key, index) => [key, index]));
1226
+ const mainPageSessionKeys = new Set(mainPageOrder.keys());
1224
1227
  const liveProviderSessions = new Set(
1225
1228
  [...this.agents.values()]
1226
1229
  .filter(agent => agent?.providerSessionProvider && agent?.providerSessionId)
@@ -1314,8 +1317,9 @@ class AgentManager extends EventEmitter {
1314
1317
  return (Number(right.updatedAt) || 0) - (Number(left.updatedAt) || 0);
1315
1318
  })
1316
1319
  : fallbackCandidates;
1320
+ const desiredCandidates = candidates.filter(record => shouldRestoreAgentFromMetadata(record, mainPageSessionKeys));
1317
1321
 
1318
- if (candidates.length > 0) {
1322
+ if (desiredCandidates.length > 0) {
1319
1323
  const rotationSummary = (Array.isArray(rotations) ? rotations : []).map(rotation => {
1320
1324
  const { serializedTerminalState, ...rest } = rotation || {};
1321
1325
  return {
@@ -1326,13 +1330,13 @@ class AgentManager extends EventEmitter {
1326
1330
  };
1327
1331
  });
1328
1332
  console.warn(
1329
- `Restoring ${candidates.length} Terminal session(s) after native PTY runtime rotation`,
1333
+ `Restoring ${desiredCandidates.length} Terminal session(s) after native PTY runtime rotation`,
1330
1334
  rotationSummary
1331
1335
  );
1332
1336
  }
1333
1337
 
1334
1338
  let changed = false;
1335
- for (const record of candidates) {
1339
+ for (const record of desiredCandidates) {
1336
1340
  if (record.wantsMain === true && this.mainAgentId) continue;
1337
1341
  const provider = String(record.providerSessionProvider || record.provider || '').trim();
1338
1342
  const sessionId = record.providerSessionId;
@@ -1342,6 +1346,19 @@ class AgentManager extends EventEmitter {
1342
1346
  record.providerHomeId || 'default'
1343
1347
  );
1344
1348
  if (sessionKey && liveProviderSessions.has(sessionKey)) continue;
1349
+ if (
1350
+ provider === 'codex'
1351
+ && record.terminalInputReceived === true
1352
+ && (
1353
+ record.providerSessionTemporary === true
1354
+ || isTemporaryProviderSessionId(sessionId)
1355
+ )
1356
+ ) {
1357
+ console.warn(
1358
+ `Refusing to replace Codex Terminal ${record.runtimeAgentId || sessionId} after native PTY runtime rotation without an exact resume id`
1359
+ );
1360
+ continue;
1361
+ }
1345
1362
  const canResumeProvider = Boolean(getProviderAdapter(provider))
1346
1363
  && isSafeProviderSessionId(sessionId);
1347
1364
  const command = canResumeProvider
@@ -1435,8 +1452,12 @@ class AgentManager extends EventEmitter {
1435
1452
  async recoverAcpSessions() {
1436
1453
  if (!this.acpRuntime || !this.configManager || typeof this.configManager.listAgentSessionRecords !== 'function') return;
1437
1454
  const mainPageOrder = new Map(this.getMainPageSessionKeys().map((key, index) => [key, index]));
1455
+ const mainPageSessionKeys = new Set(mainPageOrder.keys());
1438
1456
  const records = this.configManager.listAgentSessionRecords()
1439
- .filter(record => record && record.archived !== true && runtimeKind(record) === 'acp')
1457
+ .filter(record => (
1458
+ shouldRestoreAgentFromMetadata(record, mainPageSessionKeys)
1459
+ && runtimeKind(record) === 'acp'
1460
+ ))
1440
1461
  .sort((left, right) => {
1441
1462
  const leftOrder = mainPageOrder.get(left.providerSessionKey);
1442
1463
  const rightOrder = mainPageOrder.get(right.providerSessionKey);
@@ -1450,7 +1471,7 @@ class AgentManager extends EventEmitter {
1450
1471
  for (const record of records) {
1451
1472
  const agentId = String(record.runtimeAgentId || '').trim();
1452
1473
  const provider = String(record.providerSessionProvider || record.provider || '').trim();
1453
- const sessionId = String(record.providerSessionId || '').trim();
1474
+ const sessionId = String(record.providerSessionId || record.codexAppServerThreadId || '').trim();
1454
1475
  if (!agentId || !sessionId || !providerSupportsRuntime(provider, 'acp')) continue;
1455
1476
  const agent = this.agents.get(agentId);
1456
1477
  if (!agent) {
@@ -1469,7 +1490,7 @@ class AgentManager extends EventEmitter {
1469
1490
  for (const record of records) {
1470
1491
  const agentId = String(record.runtimeAgentId || '').trim();
1471
1492
  const provider = String(record.providerSessionProvider || record.provider || '').trim();
1472
- const sessionId = String(record.providerSessionId || '').trim();
1493
+ const sessionId = String(record.providerSessionId || record.codexAppServerThreadId || '').trim();
1473
1494
  const agent = this.agents.get(agentId);
1474
1495
  if (!agent || !sessionId || !providerSupportsRuntime(provider, 'acp')) continue;
1475
1496
  try {
@@ -1495,8 +1516,23 @@ class AgentManager extends EventEmitter {
1495
1516
  model: 'config',
1496
1517
  reasoningEffort: 'config',
1497
1518
  serviceTier: 'config',
1519
+ additionalDirectories: Array.isArray(record.acpAdditionalDirectories) ? record.acpAdditionalDirectories : [],
1520
+ mcpServers: Array.isArray(record.acpMcpServers) ? record.acpMcpServers : [],
1498
1521
  });
1499
1522
  agent.providerSessionId = prepared.sessionId;
1523
+ agent.providerSessionKey = mainPageAgentSessionKey(
1524
+ provider,
1525
+ prepared.sessionId,
1526
+ agent.providerHomeId || record.providerHomeId || 'default'
1527
+ );
1528
+ this.acpSessionOptionsByKey.set(agent.providerSessionKey, {
1529
+ additionalDirectories: Array.isArray(record.acpAdditionalDirectories)
1530
+ ? [...record.acpAdditionalDirectories]
1531
+ : [],
1532
+ mcpServers: Array.isArray(record.acpMcpServers)
1533
+ ? JSON.parse(JSON.stringify(record.acpMcpServers))
1534
+ : [],
1535
+ });
1500
1536
  agent.providerSessionTemporary = false;
1501
1537
  agent.providerSessionSource = `acp-${prepared.historyMode}`;
1502
1538
  const runtime = replaceRuntimeBinding(agent, 'acp', runtimeBindingOf(agent, 'acp'));
@@ -1520,82 +1556,6 @@ class AgentManager extends EventEmitter {
1520
1556
  this.emit('update');
1521
1557
  }
1522
1558
 
1523
- async recoverCodexAppServerSessions() {
1524
- if (!this.codexAppServerRuntime || !this.configManager || typeof this.configManager.listAgentSessionRecords !== 'function') return;
1525
- const records = this.configManager.listAgentSessionRecords();
1526
- for (const record of records) {
1527
- if (!record || record.archived === true || runtimeKind(record) !== 'app-server') continue;
1528
- const agentId = String(record.runtimeAgentId || '').trim();
1529
- const threadId = String(record.codexAppServerThreadId || record.providerSessionId || '').trim();
1530
- const home = String(record.codexAppServerHomePath || '').trim();
1531
- if (!agentId || !threadId || !home || !fs.existsSync(home)) {
1532
- if (record.provider === 'codex' || record.providerSessionProvider === 'codex') {
1533
- let agent = this.agents.get(agentId);
1534
- if (!agent && agentId) {
1535
- agent = this.recoveredAgentRecord(agentId, record.engine || 'native', record, { status: 'running' });
1536
- agent.persistentSessionId = record.id || '';
1537
- }
1538
- if (agent) {
1539
- ensureAgentOrders(agent, Array.from(this.agents.values()));
1540
- replaceRuntimeBinding(agent, 'terminal');
1541
- this.ensurePersistentAgentSession(agent);
1542
- this.updateEngineProviderSessionMetadata(agent);
1543
- }
1544
- }
1545
- continue;
1546
- }
1547
- let agent = this.agents.get(agentId);
1548
- if (!agent) {
1549
- agent = this.recoveredAgentRecord(agentId, record.engine || 'native', record, { status: 'running' });
1550
- ensureAgentOrders(agent, Array.from(this.agents.values()));
1551
- agent.persistentSessionId = record.id || '';
1552
- agent.engineStarted = false;
1553
- this.agents.set(agentId, agent);
1554
- void this.refreshAgentWorktree(agentId);
1555
- this.lastActivity.set(agentId, Date.now());
1556
- }
1557
- try {
1558
- // Remove a CLI observer recovered from pre-split metadata before the
1559
- // structured runtime reconnects to this App Server Agent.
1560
- await this.killRecoveredEngineSession({ engineName: agent.engineName }, record, agentId);
1561
- const executable = resolveCompatibleCodexExecutable('').path || resolveAgentExecutable('codex') || 'codex';
1562
- const binding = await this.codexAppServerRuntime.reattachAgent({
1563
- agentId,
1564
- threadId,
1565
- codexHome: home,
1566
- executable,
1567
- env: this.buildAgentEnv(agentId, agent),
1568
- cwd: agent.cwd,
1569
- workspaceRoot: effectiveAgentWorkspaceRoot(agent),
1570
- approvalMode: agent.launchPermissionMode || 'approve',
1571
- });
1572
- const runtime = runtimeBindingOf(agent, 'app-server');
1573
- runtime.threadId = binding.threadId;
1574
- runtime.state = 'idle';
1575
- runtime.error = '';
1576
- // App Server owns this runtime. It deliberately has no CLI/PTy
1577
- // observer, so recovery cannot create a second reader or writer.
1578
- agent.status = 'running';
1579
- agent.engineStatus = 'running';
1580
- agent.engineStarted = false;
1581
- runtime.observerDeferred = false;
1582
- agent.exitedAt = null;
1583
- this.providerSessionService.activate(agentId);
1584
- this.ensurePersistentAgentSession(agent);
1585
- } catch (error) {
1586
- const runtime = runtimeBindingOf(agent, 'app-server');
1587
- runtime.error = `Codex App Server recovery failed: ${error && (error.message || error)}`;
1588
- runtime.state = 'error';
1589
- agent.status = 'stopped';
1590
- agent.engineStatus = 'stopped';
1591
- agent.engineStarted = false;
1592
- agent.exitedAt = Date.now();
1593
- this.ensurePersistentAgentSession(agent);
1594
- }
1595
- }
1596
- this.emit('update');
1597
- }
1598
-
1599
1559
  async killRecoveredEngineSession(entry, metadata, agentId) {
1600
1560
  if (!this.engineBridge || typeof this.engineBridge.killSession !== 'function') return;
1601
1561
  const engineName = entry.engineName || metadata.engineName || 'native';
@@ -1613,16 +1573,8 @@ class AgentManager extends EventEmitter {
1613
1573
  recoveredAgentRecord(agentId, engineName, metadata, state) {
1614
1574
  const wantsMain = metadata.wantsMain === true;
1615
1575
  const providerSessionProvider = metadata.providerSessionProvider || metadata.provider || '';
1616
- const recoverCodexAppServer = shouldRecoverAsCodexAppServer(metadata);
1617
- const persistedRuntimeKind = runtimeKind(metadata);
1618
- const recoveredRuntimeKind = recoverCodexAppServer
1619
- ? 'app-server'
1620
- : (persistedRuntimeKind === 'app-server' ? 'terminal' : persistedRuntimeKind);
1621
- const runtimeBinding = runtimeBindingFor(recoveredRuntimeKind, {
1622
- ...metadata,
1623
- codexAppServerThreadId: metadata.codexAppServerThreadId || metadata.providerSessionId || '',
1624
- codexCliObserverDeferred: false,
1625
- });
1576
+ const providerSessionId = metadata.providerSessionId || metadata.codexAppServerThreadId || '';
1577
+ const runtimeBinding = runtimeBindingFor(runtimeKind(metadata), metadata);
1626
1578
  return {
1627
1579
  id: agentId,
1628
1580
  command: metadata.forkCommand || metadata.command || '',
@@ -1648,23 +1600,21 @@ class AgentManager extends EventEmitter {
1648
1600
  providerSessionProvider,
1649
1601
  providerHomeId: metadata.providerHomeId || '',
1650
1602
  providerHomePath: metadata.providerHomePath || '',
1651
- providerSessionId: metadata.providerSessionId || '',
1603
+ providerSessionId,
1652
1604
  providerSessionKey: metadata.providerSessionKey || (
1653
- providerSessionProvider && metadata.providerSessionId
1654
- ? mainPageAgentSessionKey(providerSessionProvider, metadata.providerSessionId, metadata.providerHomeId || '')
1605
+ providerSessionProvider && providerSessionId
1606
+ ? mainPageAgentSessionKey(providerSessionProvider, providerSessionId, metadata.providerHomeId || '')
1655
1607
  : ''
1656
1608
  ),
1657
- providerSessionTemporary: metadata.providerSessionTemporary === true || isTemporaryProviderSessionId(metadata.providerSessionId),
1609
+ providerSessionTemporary: metadata.providerSessionTemporary === true || isTemporaryProviderSessionId(providerSessionId),
1658
1610
  providerSessionSource: metadata.providerSessionSource || '',
1659
1611
  providerSessionResolvedAt: metadata.providerSessionResolvedAt || null,
1660
1612
  providerSessionTitle: metadata.providerSessionTitle || '',
1661
1613
  providerSessionWorkspace: metadata.providerSessionWorkspace || '',
1662
1614
  terminalInputReceived: metadata.terminalInputReceived === true,
1663
- // Older persisted sessions predate App Server mode. Also, a Codex
1664
- // App Server record without its isolated runtime home is not actually
1665
- // attachable; recover it as terminal-owned CLI instead of leaving the
1666
- // UI in a split state where the pane is terminal but Composer sends to
1667
- // App Server.
1615
+ // Legacy App Server records normalize to ACP at the runtime-binding
1616
+ // boundary. Codex thread ids are valid ACP session ids, so the existing
1617
+ // conversation remains recoverable without restarting App Server.
1668
1618
  runtimeBinding,
1669
1619
  forkedFromProviderSessionId: metadata.forkedFromProviderSessionId || '',
1670
1620
  restartedFromAgentId: metadata.restartedFromAgentId || '',
@@ -1735,7 +1685,6 @@ class AgentManager extends EventEmitter {
1735
1685
 
1736
1686
  shouldDeferMissingEngineSession(agent) {
1737
1687
  if (!agent || !isRunningAgentRuntimeStatus(agent.status)) return false;
1738
- if (isCodexAppServerAgent(agent)) return true;
1739
1688
  if (agent.engineStarted === false) return true;
1740
1689
  const startedAt = Number(agent.startedAt);
1741
1690
  return Number.isFinite(startedAt) && Date.now() - startedAt < MISSING_ENGINE_SESSION_STARTUP_GRACE_MS;
@@ -1767,7 +1716,16 @@ class AgentManager extends EventEmitter {
1767
1716
  if (!agent || !this.configManager || typeof this.configManager.ensureAgentSessionRecord !== 'function') {
1768
1717
  return '';
1769
1718
  }
1770
- const persistentSessionId = this.configManager.ensureAgentSessionRecord(agent, patch);
1719
+ const sessionOptions = agent.providerSessionKey
1720
+ ? this.acpSessionOptionsByKey.get(agent.providerSessionKey)
1721
+ : null;
1722
+ const persistentSessionId = this.configManager.ensureAgentSessionRecord(agent, {
1723
+ ...(sessionOptions ? {
1724
+ acpAdditionalDirectories: [...sessionOptions.additionalDirectories],
1725
+ acpMcpServers: JSON.parse(JSON.stringify(sessionOptions.mcpServers)),
1726
+ } : {}),
1727
+ ...patch,
1728
+ });
1771
1729
  if (persistentSessionId && !agent.persistentSessionId) {
1772
1730
  agent.persistentSessionId = persistentSessionId;
1773
1731
  }
@@ -1828,6 +1786,7 @@ class AgentManager extends EventEmitter {
1828
1786
  readOutputSeq: finiteNumberOrNull(agent.readOutputSeq),
1829
1787
  projectOrder: finiteOrder(agent.projectOrder),
1830
1788
  pinnedOrder: finiteOrder(agent.pinnedOrder),
1789
+ customTitle: agent.customTitle || '',
1831
1790
  })).catch((error) => {
1832
1791
  console.warn('Failed to update provider session metadata:', error && (error.message || error));
1833
1792
  });
@@ -2180,11 +2139,10 @@ class AgentManager extends EventEmitter {
2180
2139
  }
2181
2140
  if (agent.providerHomePath) {
2182
2141
  const provider = agent.providerSessionProvider || agentHomeProviderForProgram(agent.forkCommand || agent.command);
2183
- const appServerHome = runtimeBindingOf(agent, 'app-server')?.homePath || '';
2184
2142
  applyProviderHomeEnvironment(
2185
2143
  env,
2186
2144
  provider,
2187
- provider === 'codex' ? (appServerHome || agent.providerHomePath) : agent.providerHomePath
2145
+ agent.providerHomePath
2188
2146
  );
2189
2147
  }
2190
2148
 
@@ -2306,9 +2264,7 @@ class AgentManager extends EventEmitter {
2306
2264
  this.resizeDrains.clear();
2307
2265
  this.inputQueues.clear();
2308
2266
  this.codexTerminalProfileQueues.clear();
2309
- if (this.codexAppServerRuntime && typeof this.codexAppServerRuntime.dispose === 'function') {
2310
- this.codexAppServerRuntime.dispose();
2311
- }
2267
+ this.acpSessionOptionsByKey.clear();
2312
2268
  if (this.jsonCliRuntime) {
2313
2269
  for (const agentId of this.agents.keys()) this.jsonCliRuntime.unregisterAgent(agentId);
2314
2270
  }
@@ -2395,11 +2351,103 @@ class AgentManager extends EventEmitter {
2395
2351
  });
2396
2352
  }
2397
2353
 
2354
+ async stopUncertainTerminalSession(engine, agentId) {
2355
+ if (typeof engine?.killSession !== 'function' || typeof engine?.getSessionState !== 'function') {
2356
+ throw new Error('Session engine cannot prove an uncertain Terminal start has stopped');
2357
+ }
2358
+ await withBoundedWait(
2359
+ Promise.resolve(engine.killSession(agentId)),
2360
+ UNCERTAIN_TERMINAL_STOP_TIMEOUT_MS,
2361
+ 'Terminal kill request',
2362
+ );
2363
+ const deadline = Date.now() + UNCERTAIN_TERMINAL_STOP_TIMEOUT_MS;
2364
+ while (Date.now() <= deadline) {
2365
+ const state = await withBoundedWait(
2366
+ Promise.resolve(engine.getSessionState(agentId)),
2367
+ TERMINAL_STOP_STATE_READ_TIMEOUT_MS,
2368
+ 'Terminal stop-state read',
2369
+ );
2370
+ if (!state || ['dead', 'exited', 'stopped'].includes(String(state.status || ''))) return;
2371
+ await new Promise(resolve => setTimeout(resolve, TERMINAL_STOP_POLL_MS));
2372
+ }
2373
+ throw new Error(`Terminal ${agentId} did not reach an exited state after kill`);
2374
+ }
2375
+
2398
2376
  async startAgent(command, customWorkspace, callback, options = {}) {
2399
2377
  if (options.wantsMain !== false && options.skipRecoveryWait !== true) {
2400
2378
  await this.whenRecovered();
2401
2379
  }
2402
2380
 
2381
+ const wantsMain = options.wantsMain === true || (options.wantsMain !== false && !this.mainAgentId);
2382
+ if (!wantsMain) {
2383
+ return this.startAgentUnreserved(command, customWorkspace, callback, {
2384
+ ...options,
2385
+ wantsMain: false,
2386
+ skipRecoveryWait: true,
2387
+ });
2388
+ }
2389
+
2390
+ if (this.mainAgentStartReservation) {
2391
+ const outcome = await this.mainAgentStartReservation.promise;
2392
+ if (callback) callback(outcome.agentId, outcome.error);
2393
+ return outcome.agentId;
2394
+ }
2395
+
2396
+ const existingMainStart = this.findActiveMainAgentStart();
2397
+ if (existingMainStart) {
2398
+ if (this.mainAgentId !== existingMainStart.id) {
2399
+ this.mainAgentId = existingMainStart.id;
2400
+ this.emit('update');
2401
+ }
2402
+ console.log('Main Agent already starting or running:', existingMainStart.id);
2403
+ if (callback) callback(existingMainStart.id);
2404
+ return existingMainStart.id;
2405
+ }
2406
+
2407
+ let resolveReservation;
2408
+ const reservation = {
2409
+ promise: new Promise(resolve => {
2410
+ resolveReservation = resolve;
2411
+ }),
2412
+ };
2413
+ this.mainAgentStartReservation = reservation;
2414
+ let outcome = null;
2415
+ let callbackCalled = false;
2416
+ const reservedCallback = (agentId, error) => {
2417
+ callbackCalled = true;
2418
+ outcome = { agentId: agentId || null, error: error || null };
2419
+ if (callback) callback(agentId, error);
2420
+ };
2421
+ try {
2422
+ const agentId = await this.startAgentUnreserved(command, customWorkspace, reservedCallback, {
2423
+ ...options,
2424
+ wantsMain: true,
2425
+ skipRecoveryWait: true,
2426
+ });
2427
+ if (!callbackCalled) {
2428
+ outcome = { agentId: agentId || null, error: null };
2429
+ if (callback) callback(agentId);
2430
+ }
2431
+ return agentId;
2432
+ } catch (error) {
2433
+ if (!callbackCalled) {
2434
+ outcome = { agentId: null, error: error && (error.message || String(error)) };
2435
+ if (callback) callback(null, outcome.error);
2436
+ }
2437
+ throw error;
2438
+ } finally {
2439
+ resolveReservation(outcome || { agentId: null, error: 'Main Agent failed to start' });
2440
+ if (this.mainAgentStartReservation === reservation) {
2441
+ this.mainAgentStartReservation = null;
2442
+ }
2443
+ }
2444
+ }
2445
+
2446
+ async startAgentUnreserved(command, customWorkspace, callback, options = {}) {
2447
+ if (options.wantsMain !== false && options.skipRecoveryWait !== true) {
2448
+ await this.whenRecovered();
2449
+ }
2450
+
2403
2451
  const wantsMain = options.wantsMain === true || (options.wantsMain !== false && !this.mainAgentId);
2404
2452
  if (wantsMain) {
2405
2453
  const existingMainStart = this.findActiveMainAgentStart();
@@ -2472,13 +2520,28 @@ class AgentManager extends EventEmitter {
2472
2520
  });
2473
2521
 
2474
2522
  const hasResumeSource = Boolean(resumedSessionFromSource(resolvedSource));
2475
- if (providerSessionPlan.source === 'resume-source' && hasResumeSource && !hasResumeArg(launch.args)) {
2476
- providerSessionPlan = buildAgentProviderSessionPlan({
2477
- command,
2478
- program,
2479
- args: launch.args,
2480
- source: 'ui',
2481
- });
2523
+ const commandProviderSessionPlan = hasResumeSource
2524
+ ? buildAgentProviderSessionPlan({
2525
+ command,
2526
+ program,
2527
+ args: launch.args,
2528
+ source: 'ui',
2529
+ })
2530
+ : null;
2531
+ const commandContinuesProviderSession = commandProviderSessionPlan
2532
+ && commandProviderSessionPlan.temporary !== true
2533
+ && commandProviderSessionPlan.provider === providerSessionPlan.provider
2534
+ && commandProviderSessionPlan.id === providerSessionPlan.id;
2535
+ if (
2536
+ providerSessionPlan.source === 'resume-source'
2537
+ && hasResumeSource
2538
+ && !commandContinuesProviderSession
2539
+ ) {
2540
+ providerSessionPlan = commandProviderSessionPlan;
2541
+ }
2542
+ if (providerSessionPlan.error) {
2543
+ if (callback) callback(null, providerSessionPlan.error);
2544
+ return null;
2482
2545
  }
2483
2546
 
2484
2547
  let args = providerSessionPlan.args;
@@ -2561,13 +2624,6 @@ class AgentManager extends EventEmitter {
2561
2624
  }
2562
2625
  }
2563
2626
 
2564
- const logArgs = args.map((arg, index) => (
2565
- index > 0 && args[index - 1] === '--append-system-prompt'
2566
- ? '<farming-main-agent-bootstrap>'
2567
- : arg
2568
- ));
2569
- console.log('Starting agent:', program, logArgs, 'workspace:', workspace, spawnProgram !== program ? `resolved: ${spawnProgram}` : '');
2570
-
2571
2627
  if (!fs.existsSync(workspace)) {
2572
2628
  console.log('Workspace does not exist:', workspace);
2573
2629
  if (callback) callback(null, `Workspace does not exist: ${workspace}`);
@@ -2629,56 +2685,34 @@ class AgentManager extends EventEmitter {
2629
2685
  resolvedProviderHomeId = defaultCodexHome.id || 'default';
2630
2686
  }
2631
2687
  }
2632
- const requestedAgentRuntimeMode = ['json', 'acp'].includes(options.agentRuntimeMode)
2688
+ const requestedAgentRuntimeMode = ['json', 'acp', 'chat'].includes(options.agentRuntimeMode)
2633
2689
  ? options.agentRuntimeMode
2634
2690
  : 'terminal';
2635
2691
  // A fresh structured runtime does not need a provider CLI resume id yet:
2636
2692
  // ACP/JSON creates the provider session and writes the resulting id back
2637
- // after connecting. OpenCode is the important case here because its
2638
- // terminal CLI does not accept a pre-generated id for a fresh session.
2639
- const structuredRuntimeProvider = ['json', 'acp'].includes(requestedAgentRuntimeMode)
2693
+ // after connecting. Fresh Terminal sessions are handled separately below.
2694
+ const structuredRuntimeProvider = ['json', 'acp', 'chat'].includes(requestedAgentRuntimeMode)
2640
2695
  ? homeProvider
2641
2696
  : providerSessionPlan.provider;
2642
- const requestedCodexRuntimeMode = options.codexRuntimeMode === 'app-server' || options.codexRuntimeMode === 'cli'
2643
- ? options.codexRuntimeMode
2644
- : (this.configManager && typeof this.configManager.getCodexRuntimeMode === 'function'
2645
- ? this.configManager.getCodexRuntimeMode()
2646
- : 'cli');
2647
- const useCodexAppServer = providerSessionPlan.provider === 'codex'
2648
- && this.codexAppServerRuntime
2649
- && options.agentRuntimeMode !== 'acp'
2650
- // The deterministic browser/server fixtures are terminal-only shims,
2651
- // not an implementation of the Codex App Server protocol. App Server
2652
- // behavior has its own mock runtime test; keep these legacy PTY tests
2653
- // on the path they are designed to exercise.
2654
- && process.env.FARMING_E2E_FAKE_EXECUTABLES !== '1'
2655
- && normalizeCodexRuntimeMode(requestedCodexRuntimeMode) === 'app-server';
2697
+ // `chat` is the only browser-facing structured-runtime request. Treat the
2698
+ // retired Codex ACP launch request as that intent too, so an old client
2699
+ // cannot create a second Codex Chat implementation.
2700
+ const requestedChatRuntime = isChatMode(requestedAgentRuntimeMode)
2701
+ || (requestedAgentRuntimeMode === 'acp' && homeProvider === 'codex');
2702
+ const resolvedChatRuntime = requestedChatRuntime
2703
+ ? chatRuntimeForProvider(homeProvider)
2704
+ : (requestedAgentRuntimeMode === 'acp' ? 'acp' : '');
2656
2705
  const useJsonCli = requestedAgentRuntimeMode === 'json'
2657
2706
  && providerSupportsRuntime(structuredRuntimeProvider, 'json')
2658
- && !useCodexAppServer
2659
2707
  && process.env.FARMING_E2E_FAKE_EXECUTABLES !== '1';
2660
- const useAcp = requestedAgentRuntimeMode === 'acp'
2708
+ const useAcp = resolvedChatRuntime === 'acp'
2661
2709
  && providerSupportsRuntime(structuredRuntimeProvider, 'acp')
2662
- && !useCodexAppServer
2663
2710
  && (
2664
2711
  process.env.FARMING_E2E_FAKE_EXECUTABLES !== '1'
2665
2712
  || process.env.FARMING_E2E_FAKE_ACP_AGENT === '1'
2666
2713
  );
2667
2714
  const acpGeneratedFreshSession = useAcp
2668
2715
  && isFreshAcpSessionSource(structuredRuntimeProvider, providerSessionPlan.source);
2669
- let codexAppServerHomePath = '';
2670
- if (useCodexAppServer) {
2671
- try {
2672
- codexAppServerHomePath = ensureCodexAppServerHome({
2673
- configDir: this.configManager && this.configManager.farmingDir,
2674
- agentId,
2675
- sourceHome: providerHomePath || path.join(os.homedir(), '.codex'),
2676
- });
2677
- } catch (error) {
2678
- if (callback) callback(null, error.message);
2679
- return null;
2680
- }
2681
- }
2682
2716
  const agentRecord = {
2683
2717
  id: agentId,
2684
2718
  command: launch.program,
@@ -2712,16 +2746,14 @@ class AgentManager extends EventEmitter {
2712
2746
  providerSessionTitle: typeof options.providerSessionTitle === 'string' ? options.providerSessionTitle.trim().slice(0, 160) : '',
2713
2747
  providerSessionWorkspace: '',
2714
2748
  terminalInputReceived: false,
2715
- runtimeBinding: useCodexAppServer
2716
- ? runtimeBindingFor('app-server', { state: 'connecting', homePath: codexAppServerHomePath })
2717
- : (useAcp
2718
- ? runtimeBindingFor('acp', { state: 'connecting' })
2719
- : (useJsonCli
2720
- ? runtimeBindingFor('json', {
2721
- state: 'idle',
2722
- events: Array.isArray(options.jsonCliEvents) ? options.jsonCliEvents : [],
2723
- })
2724
- : runtimeBindingFor('terminal'))),
2749
+ runtimeBinding: useAcp
2750
+ ? runtimeBindingFor('acp', { state: 'connecting' })
2751
+ : (useJsonCli
2752
+ ? runtimeBindingFor('json', {
2753
+ state: 'idle',
2754
+ events: Array.isArray(options.jsonCliEvents) ? options.jsonCliEvents : [],
2755
+ })
2756
+ : runtimeBindingFor('terminal')),
2725
2757
  forkedFromProviderSessionId: providerSessionPlan.forkedFromProviderSessionId || '',
2726
2758
  restartedFromAgentId: typeof options.restartedFromAgentId === 'string' ? options.restartedFromAgentId : '',
2727
2759
  restartedFromAgentIds: Array.isArray(options.restartedFromAgentIds)
@@ -2768,6 +2800,135 @@ class AgentManager extends EventEmitter {
2768
2800
  attentionSuppressUntil: 0
2769
2801
  };
2770
2802
 
2803
+ let precreatedProviderSession = null;
2804
+ if (
2805
+ providerSessionPlan.precreate === true
2806
+ && !useAcp
2807
+ && !useJsonCli
2808
+ ) {
2809
+ const adapter = getProviderAdapter(providerSessionPlan.provider);
2810
+ if (typeof adapter?.terminalResumeArgs !== 'function') {
2811
+ const message = `${providerSessionPlan.provider} cannot resume a pre-created Terminal session`;
2812
+ if (callback) callback(null, message);
2813
+ return null;
2814
+ }
2815
+ try {
2816
+ const requestedIdentityWorkspace = this.expandWorkspacePath(providerSessionPlan.identityWorkspace || '');
2817
+ const identityWorkspace = requestedIdentityWorkspace
2818
+ ? (path.isAbsolute(requestedIdentityWorkspace)
2819
+ ? path.resolve(requestedIdentityWorkspace)
2820
+ : path.resolve(workspace, requestedIdentityWorkspace))
2821
+ : workspace;
2822
+ let identityWorkspaceExists = false;
2823
+ try {
2824
+ identityWorkspaceExists = fs.statSync(identityWorkspace).isDirectory();
2825
+ } catch {
2826
+ identityWorkspaceExists = false;
2827
+ }
2828
+ if (!identityWorkspaceExists) {
2829
+ throw new Error(`Workspace does not exist: ${identityWorkspace}`);
2830
+ }
2831
+ const identityEnv = this.buildAgentEnv(agentId, agentRecord);
2832
+ const requestedAdditionalDirectories = Array.isArray(options.additionalDirectories)
2833
+ ? options.additionalDirectories
2834
+ : [];
2835
+ const requestedMcpServers = Array.isArray(options.mcpServers) ? options.mcpServers : [];
2836
+ const created = await this.createProviderSessionIdentity({
2837
+ provider: providerSessionPlan.provider,
2838
+ executable: spawnProgram,
2839
+ env: identityEnv,
2840
+ cwd: identityWorkspace,
2841
+ providerHomeId: agentRecord.providerHomeId || 'default',
2842
+ approvalMode: agentRecord.launchPermissionMode || 'approve',
2843
+ model: codexModel,
2844
+ reasoningEffort: codexReasoningEffort,
2845
+ serviceTier: codexServiceTier,
2846
+ additionalDirectories: requestedAdditionalDirectories,
2847
+ mcpServers: requestedMcpServers,
2848
+ });
2849
+ const providerSessionId = String(created?.sessionId || '').trim();
2850
+ if (!isSafeProviderSessionId(providerSessionId)) {
2851
+ throw new Error(`${providerSessionPlan.provider} ACP session/new returned an invalid session id`);
2852
+ }
2853
+ const normalizedSessionOptions = created?.sessionRequestOptions || {
2854
+ additionalDirectories: requestedAdditionalDirectories.map(directory => path.resolve(identityWorkspace, directory)),
2855
+ mcpServers: JSON.parse(JSON.stringify(requestedMcpServers)),
2856
+ };
2857
+ args = adapter.terminalResumeArgs(args, providerSessionId, providerSessionPlan);
2858
+ providerSessionPlan = {
2859
+ ...providerSessionPlan,
2860
+ id: providerSessionId,
2861
+ precreate: false,
2862
+ temporary: false,
2863
+ source: 'acp-precreated',
2864
+ args,
2865
+ };
2866
+ agentRecord.providerSessionId = providerSessionId;
2867
+ agentRecord.providerSessionKey = mainPageAgentSessionKey(
2868
+ providerSessionPlan.provider,
2869
+ providerSessionId,
2870
+ agentRecord.providerHomeId || 'default'
2871
+ );
2872
+ agentRecord.providerSessionTemporary = false;
2873
+ agentRecord.providerSessionSource = providerSessionPlan.source;
2874
+ agentRecord.providerSessionResolvedAt = Date.now();
2875
+ this.acpSessionOptionsByKey.set(agentRecord.providerSessionKey, {
2876
+ additionalDirectories: Array.isArray(normalizedSessionOptions.additionalDirectories)
2877
+ ? [...normalizedSessionOptions.additionalDirectories]
2878
+ : [],
2879
+ mcpServers: Array.isArray(normalizedSessionOptions.mcpServers)
2880
+ ? JSON.parse(JSON.stringify(normalizedSessionOptions.mcpServers))
2881
+ : [],
2882
+ });
2883
+ precreatedProviderSession = {
2884
+ provider: providerSessionPlan.provider,
2885
+ executable: spawnProgram,
2886
+ env: identityEnv,
2887
+ cwd: identityWorkspace,
2888
+ sessionId: providerSessionId,
2889
+ sessionKey: agentRecord.providerSessionKey,
2890
+ };
2891
+ } catch (error) {
2892
+ let identityRollbackError = null;
2893
+ let identityRetainedReason = '';
2894
+ const orphanedIdentity = error?.providerSessionIdentity;
2895
+ if (
2896
+ orphanedIdentity
2897
+ && orphanedIdentity.producerStopped === true
2898
+ && isSafeProviderSessionId(orphanedIdentity.sessionId)
2899
+ ) {
2900
+ try {
2901
+ await this.deleteProviderSessionIdentity(orphanedIdentity);
2902
+ } catch (cleanupError) {
2903
+ identityRollbackError = cleanupError;
2904
+ }
2905
+ } else if (orphanedIdentity && !isSafeProviderSessionId(orphanedIdentity.sessionId)) {
2906
+ identityRetainedReason = 'provider returned an unsafe session id; it was retained without invoking CLI rollback';
2907
+ } else if (orphanedIdentity) {
2908
+ identityRetainedReason = 'provider session retained because ACP producer shutdown could not be proven';
2909
+ }
2910
+ const baseMessage = error && error.message
2911
+ ? error.message
2912
+ : `Failed to create ${providerSessionPlan.provider} session identity`;
2913
+ let message = baseMessage;
2914
+ if (identityRollbackError) {
2915
+ message = `${baseMessage}; provider session rollback failed: ${identityRollbackError.message || identityRollbackError}`;
2916
+ } else if (identityRetainedReason) {
2917
+ message = `${baseMessage}; ${identityRetainedReason}`;
2918
+ }
2919
+ console.error('Failed to create provider session identity:', error);
2920
+ if (callback) callback(null, message);
2921
+ return null;
2922
+ }
2923
+ }
2924
+
2925
+ const logArgs = args.map((arg, index) => (
2926
+ index > 0 && args[index - 1] === '--append-system-prompt'
2927
+ ? '<farming-main-agent-bootstrap>'
2928
+ : arg
2929
+ ));
2930
+ console.log('Starting agent:', program, logArgs, 'workspace:', workspace, spawnProgram !== program ? `resolved: ${spawnProgram}` : '');
2931
+
2771
2932
  ensureAgentOrders(agentRecord, Array.from(this.agents.values()));
2772
2933
  this.agents.set(agentId, agentRecord);
2773
2934
  void this.refreshAgentWorktree(agentId);
@@ -2777,50 +2938,6 @@ class AgentManager extends EventEmitter {
2777
2938
  this.emit('update');
2778
2939
 
2779
2940
  try {
2780
- if (useCodexAppServer) {
2781
- try {
2782
- const appServer = runtimeBindingOf(agentRecord, 'app-server');
2783
- const prepared = await this.codexAppServerRuntime.prepareAgent({
2784
- agentId,
2785
- codexHome: appServer.homePath,
2786
- executable: spawnProgram,
2787
- env: this.buildAgentEnv(agentId, agentRecord),
2788
- cwd: workspace,
2789
- workspaceRoot: projectWorkspace || workspace,
2790
- resumeThreadId: agentRecord.providerSessionTemporary ? '' : agentRecord.providerSessionId,
2791
- temporaryThreadId: agentRecord.providerSessionTemporary ? agentRecord.providerSessionId : '',
2792
- approvalMode: agentRecord.launchPermissionMode || (
2793
- this.configManager && this.configManager.getCodexApprovalMode
2794
- ? this.configManager.getCodexApprovalMode()
2795
- : 'approve'
2796
- ),
2797
- model: codexModel,
2798
- reasoningEffort: codexReasoningEffort,
2799
- serviceTier: codexServiceTier,
2800
- developerInstructions: wantsMain ? renderMainAgentBootstrap() : '',
2801
- });
2802
- agentRecord.providerSessionId = prepared.threadId;
2803
- agentRecord.providerSessionKey = mainPageAgentSessionKey(
2804
- 'codex',
2805
- prepared.threadId,
2806
- agentRecord.providerHomeId || 'default'
2807
- );
2808
- agentRecord.providerSessionTemporary = false;
2809
- agentRecord.providerSessionSource = prepared.resumed ? 'app-server-resume' : 'app-server-start';
2810
- agentRecord.providerSessionResolvedAt = Date.now();
2811
- appServer.state = 'idle';
2812
- appServer.endpoint = prepared.endpoint;
2813
- appServer.threadId = prepared.threadId;
2814
- appServer.error = '';
2815
- // App Server mode has no embedded CLI observer. Its chat and
2816
- // lifecycle both come from the structured App Server connection.
2817
- appServer.observerDeferred = false;
2818
- } catch (error) {
2819
- const reason = error && error.message ? error.message : String(error);
2820
- throw new Error(`Codex App Server failed to start: ${reason}`, { cause: error });
2821
- }
2822
- }
2823
-
2824
2941
  if (useJsonCli) {
2825
2942
  const jsonRuntime = runtimeBindingOf(agentRecord, 'json');
2826
2943
  this.jsonCliRuntime.registerAgent({
@@ -2838,6 +2955,22 @@ class AgentManager extends EventEmitter {
2838
2955
 
2839
2956
  if (useAcp) {
2840
2957
  const acpRuntime = runtimeBindingOf(agentRecord, 'acp');
2958
+ const sessionOptionsKey = agentRecord.providerSessionId && !agentRecord.providerSessionTemporary
2959
+ ? mainPageAgentSessionKey(
2960
+ structuredRuntimeProvider,
2961
+ agentRecord.providerSessionId,
2962
+ agentRecord.providerHomeId || 'default'
2963
+ )
2964
+ : '';
2965
+ const rememberedSessionOptions = sessionOptionsKey
2966
+ ? this.acpSessionOptionsByKey.get(sessionOptionsKey) || {}
2967
+ : {};
2968
+ const additionalDirectories = Array.isArray(options.additionalDirectories)
2969
+ ? options.additionalDirectories
2970
+ : rememberedSessionOptions.additionalDirectories || [];
2971
+ const mcpServers = Array.isArray(options.mcpServers)
2972
+ ? options.mcpServers
2973
+ : rememberedSessionOptions.mcpServers || [];
2841
2974
  const prepared = await this.acpRuntime.prepareAgent({
2842
2975
  agentId,
2843
2976
  provider: structuredRuntimeProvider,
@@ -2855,6 +2988,8 @@ class AgentManager extends EventEmitter {
2855
2988
  model: codexModel,
2856
2989
  reasoningEffort: codexReasoningEffort,
2857
2990
  serviceTier: codexServiceTier,
2991
+ additionalDirectories,
2992
+ mcpServers,
2858
2993
  });
2859
2994
  agentRecord.providerSessionId = prepared.sessionId;
2860
2995
  agentRecord.providerSessionKey = mainPageAgentSessionKey(
@@ -2865,12 +3000,22 @@ class AgentManager extends EventEmitter {
2865
3000
  agentRecord.providerSessionTemporary = false;
2866
3001
  agentRecord.providerSessionSource = `acp-${prepared.historyMode}`;
2867
3002
  agentRecord.providerSessionResolvedAt = Date.now();
3003
+ let normalizedSessionOptions = { additionalDirectories, mcpServers };
3004
+ try {
3005
+ normalizedSessionOptions = this.acpRuntime.getSessionRequestOptions(agentId);
3006
+ } catch {
3007
+ // prepareAgent already validated the request; retain the caller copy
3008
+ // only for custom runtimes that do not expose the normalized scope.
3009
+ }
3010
+ this.acpSessionOptionsByKey.set(agentRecord.providerSessionKey, {
3011
+ additionalDirectories: [...normalizedSessionOptions.additionalDirectories],
3012
+ mcpServers: JSON.parse(JSON.stringify(normalizedSessionOptions.mcpServers)),
3013
+ });
2868
3014
  acpRuntime.state = 'idle';
2869
3015
  acpRuntime.error = '';
2870
3016
  }
2871
3017
 
2872
- agentRecord.persistentSessionId = this.ensurePersistentAgentSession(agentRecord);
2873
- if (!useCodexAppServer && !useJsonCli && !useAcp) {
3018
+ if (!useJsonCli && !useAcp) {
2874
3019
  const engineLaunch = {
2875
3020
  command: spawnProgram,
2876
3021
  args,
@@ -2880,6 +3025,10 @@ class AgentManager extends EventEmitter {
2880
3025
  };
2881
3026
  await this.createAgentEngineSession(agentRecord, resolution.engine, engineLaunch);
2882
3027
  }
3028
+ agentRecord.persistentSessionId = this.ensurePersistentAgentSession(agentRecord, {
3029
+ visibleOnMainPage: true,
3030
+ archived: false,
3031
+ });
2883
3032
 
2884
3033
  const agent = this.agents.get(agentId);
2885
3034
  if (agent && agent.status === 'pending') {
@@ -2898,6 +3047,33 @@ class AgentManager extends EventEmitter {
2898
3047
  return agentId;
2899
3048
  } catch (error) {
2900
3049
  console.error('Failed to start agent:', error);
3050
+ let runtimeCleanupError = null;
3051
+ if (!useJsonCli && !useAcp) {
3052
+ try {
3053
+ await this.stopUncertainTerminalSession(resolution.engine, agentId);
3054
+ } catch (engineCleanupError) {
3055
+ runtimeCleanupError = engineCleanupError;
3056
+ console.error(
3057
+ 'Failed to stop partially started Agent runtime:',
3058
+ engineCleanupError && (engineCleanupError.message || engineCleanupError)
3059
+ );
3060
+ }
3061
+ }
3062
+ let rollbackError = null;
3063
+ if (precreatedProviderSession && !runtimeCleanupError) {
3064
+ try {
3065
+ await this.deleteProviderSessionIdentity(precreatedProviderSession);
3066
+ } catch (cleanupError) {
3067
+ rollbackError = cleanupError;
3068
+ console.error(
3069
+ 'Failed to roll back pre-created provider session:',
3070
+ cleanupError && (cleanupError.message || cleanupError)
3071
+ );
3072
+ }
3073
+ this.acpSessionOptionsByKey.delete(precreatedProviderSession.sessionKey);
3074
+ } else if (precreatedProviderSession) {
3075
+ this.acpSessionOptionsByKey.delete(precreatedProviderSession.sessionKey);
3076
+ }
2901
3077
  if (options.restoreRuntimeAgentIdOnFailure && agentRecord.persistentSessionId) {
2902
3078
  const failedAgentId = agentRecord.id;
2903
3079
  try {
@@ -2919,9 +3095,6 @@ class AgentManager extends EventEmitter {
2919
3095
  this.agentUsageRateCache.delete(agentId);
2920
3096
  this.lastResizeByAgent.delete(agentId);
2921
3097
  this.providerSessionService.stop(agentId);
2922
- if (this.codexAppServerRuntime && typeof this.codexAppServerRuntime.unregisterAgent === 'function') {
2923
- this.codexAppServerRuntime.unregisterAgent(agentId);
2924
- }
2925
3098
  if (this.acpRuntime) this.acpRuntime.unregisterAgent(agentId);
2926
3099
 
2927
3100
  if (this.mainAgentId === agentId) {
@@ -2929,7 +3102,14 @@ class AgentManager extends EventEmitter {
2929
3102
  }
2930
3103
 
2931
3104
  this.emit('update');
2932
- if (callback) callback(null, error.message);
3105
+ const startupError = error && (error.message || String(error));
3106
+ const cleanupSuffix = rollbackError
3107
+ ? `; provider session rollback failed: ${rollbackError.message || rollbackError}`
3108
+ : '';
3109
+ const runtimeCleanupSuffix = runtimeCleanupError
3110
+ ? `; Terminal rollback failed and the provider session was retained: ${runtimeCleanupError.message || runtimeCleanupError}`
3111
+ : '';
3112
+ if (callback) callback(null, `${startupError}${runtimeCleanupSuffix}${cleanupSuffix}`);
2933
3113
  return null;
2934
3114
  }
2935
3115
  }
@@ -2980,41 +3160,14 @@ class AgentManager extends EventEmitter {
2980
3160
  return this.enqueueInputOperation(agentId, () => this.sendInputNow(agentId, input, options));
2981
3161
  }
2982
3162
 
2983
- codexAppServerOptionsForAgent(agent, message = '') {
2984
- const codexResolution = resolveCompatibleCodexExecutable(agent.requiredCliVersion || '');
2985
- if (!codexResolution.compatible) {
2986
- throw new Error(codexResolution.error || 'Codex CLI is not compatible with App Server mode');
2987
- }
2988
-
2989
- const runtime = runtimeBindingOf(agent, 'app-server');
2990
- if (!runtime) throw new Error('Agent is not using the Codex App Server runtime');
2991
- return {
2992
- agentId: agent.id,
2993
- codexHome: runtime.homePath,
2994
- executable: codexResolution.path || resolveAgentExecutable(agent.command) || agent.command,
2995
- env: this.buildAgentEnv(agent.id, agent),
2996
- cwd: agent.cwd,
2997
- workspaceRoot: effectiveAgentWorkspaceRoot(agent),
2998
- threadId: runtime.threadId || agent.providerSessionId || '',
2999
- approvalMode: agent.launchPermissionMode || (
3000
- this.configManager && this.configManager.getCodexApprovalMode
3001
- ? this.configManager.getCodexApprovalMode()
3002
- : 'approve'
3003
- ),
3004
- model: this.configManager && this.configManager.getCodexModel
3005
- ? this.configManager.getCodexModel()
3006
- : 'gpt-5.5',
3007
- reasoningEffort: this.configManager && this.configManager.getCodexReasoningEffort
3008
- ? this.configManager.getCodexReasoningEffort()
3009
- : 'xhigh',
3010
- serviceTier: this.configManager && this.configManager.getCodexServiceTier
3011
- ? this.configManager.getCodexServiceTier()
3012
- : 'default',
3013
- message,
3014
- };
3015
- }
3016
-
3017
3163
  async sendComposerMessage(agentId, message) {
3164
+ if (this.acpRuntime.canSteer?.(agentId)) {
3165
+ try {
3166
+ return await this.sendAcpSteerNow(agentId, message);
3167
+ } catch (error) {
3168
+ if (!isCodexSteerUnavailableError(error)) throw error;
3169
+ }
3170
+ }
3018
3171
  return this.enqueueInputOperation(agentId, () => this.sendComposerMessageNow(agentId, message));
3019
3172
  }
3020
3173
 
@@ -3045,7 +3198,6 @@ class AgentManager extends EventEmitter {
3045
3198
  if (!agent) throw new Error('Agent not found');
3046
3199
  if (
3047
3200
  agentProgramName(agent.command).toLowerCase() !== 'codex'
3048
- || isCodexAppServerAgent(agent)
3049
3201
  || isJsonCliAgent(agent)
3050
3202
  || isAcpAgent(agent)
3051
3203
  || runtimeKind(agent) !== 'terminal'
@@ -3113,34 +3265,12 @@ class AgentManager extends EventEmitter {
3113
3265
  async sendComposerMessageNow(agentId, message) {
3114
3266
  const agent = this.agents.get(agentId);
3115
3267
  if (!agent) throw new Error('Agent not found');
3116
- const prompt = Array.isArray(message) ? message : [{ type: 'text', text: String(message || '') }];
3268
+ const prompt = normalizedComposerPrompt(message);
3117
3269
  const text = prompt
3118
3270
  .filter(content => content?.type === 'text')
3119
3271
  .map(content => String(content.text || ''))
3120
3272
  .join('')
3121
3273
  .trim();
3122
- if (prompt.length === 0 || (!text && !prompt.some(content => content?.type !== 'text'))) {
3123
- throw new Error('Composer message is empty');
3124
- }
3125
-
3126
- const appServer = runtimeBindingOf(agent, 'app-server');
3127
- if (
3128
- agent.providerSessionProvider === 'codex'
3129
- && appServer
3130
- && this.codexAppServerRuntime
3131
- && typeof this.codexAppServerRuntime.submitComposerMessage === 'function'
3132
- ) {
3133
- const result = await this.codexAppServerRuntime.submitComposerMessage(
3134
- this.codexAppServerOptionsForAgent(agent, text)
3135
- );
3136
- appServer.threadId = result.threadId;
3137
- appServer.turnId = result.turnId;
3138
- appServer.state = 'working';
3139
- appServer.error = '';
3140
- this.lastActivity.set(agentId, Date.now());
3141
- this.emit('update');
3142
- return result;
3143
- }
3144
3274
 
3145
3275
  if (isJsonCliAgent(agent)) {
3146
3276
  const result = await this.jsonCliRuntime.submitComposerMessage(agentId, text, {
@@ -3165,62 +3295,14 @@ class AgentManager extends EventEmitter {
3165
3295
  return { kind: 'terminal' };
3166
3296
  }
3167
3297
 
3168
- respondToCodexAppServerRequest(agentId, requestId, result, options = {}) {
3298
+ async sendAcpSteerNow(agentId, message) {
3169
3299
  const agent = this.agents.get(agentId);
3170
3300
  if (!agent) throw new Error('Agent not found');
3171
- const runtime = runtimeBindingOf(agent, 'app-server');
3172
- if (agent.providerSessionProvider !== 'codex' || !runtime) {
3173
- throw new Error('This Agent is not using the Codex App Server runtime');
3174
- }
3175
- const resolvedRequestId = String(requestId || '');
3176
- if (!resolvedRequestId || resolvedRequestId !== runtime.pendingRequestId) {
3177
- throw new Error('Codex App Server request is no longer pending');
3178
- }
3179
- if (!this.codexAppServerRuntime) throw new Error('Codex App Server runtime is unavailable');
3180
-
3181
- const response = options.reject === true
3182
- ? this.codexAppServerRuntime.rejectAgentServerRequest(
3183
- agentId,
3184
- resolvedRequestId,
3185
- { code: -32000, message: String(options.reason || 'Rejected by Farming') }
3186
- )
3187
- : this.codexAppServerRuntime.resolveAgentServerRequest(agentId, resolvedRequestId, result);
3188
- runtime.pendingRequestId = '';
3189
- runtime.pendingRequestMethod = '';
3190
- runtime.pendingRequest = null;
3191
- runtime.state = runtime.turnId ? 'working' : 'idle';
3192
- this.emit('update');
3193
- return response;
3194
- }
3195
-
3196
- assertCodexAppServerGoalAgent(agentId) {
3197
- const agent = this.agents.get(agentId);
3198
- if (!agent) throw new Error('Agent not found');
3199
- const runtime = runtimeBindingOf(agent, 'app-server');
3200
- if (agent.providerSessionProvider !== 'codex' || !runtime) {
3201
- throw new Error('This Agent is not using the Codex App Server runtime');
3202
- }
3203
- if (!this.codexAppServerRuntime) throw new Error('Codex App Server runtime is unavailable');
3204
- if (!(runtime.threadId || agent.providerSessionId)) {
3205
- throw new Error('Codex App Server thread id is not available yet');
3206
- }
3207
- return agent;
3208
- }
3209
-
3210
- async getCodexAppServerGoal(agentId) {
3211
- const agent = this.assertCodexAppServerGoalAgent(agentId);
3212
- const goal = await this.codexAppServerRuntime.getAgentGoal(this.codexAppServerOptionsForAgent(agent));
3213
- runtimeBindingOf(agent, 'app-server').goal = goal || null;
3214
- this.emit('update');
3215
- return goal || null;
3216
- }
3217
-
3218
- getCodexAppServerTranscript(agentId, options = {}) {
3219
- const agent = this.assertCodexAppServerGoalAgent(agentId);
3220
- if (!this.codexAppServerRuntime || typeof this.codexAppServerRuntime.getAgentTranscript !== 'function') {
3221
- throw new Error('Codex App Server transcript is unavailable');
3222
- }
3223
- return this.codexAppServerRuntime.getAgentTranscript(agent.id, options);
3301
+ if (!isAcpAgent(agent)) throw new Error('Agent is not using ACP Chat');
3302
+ const prompt = normalizedComposerPrompt(message);
3303
+ const result = await this.acpRuntime.steer(agentId, prompt);
3304
+ this.ensurePersistentAgentSession(agent);
3305
+ return { kind: 'acp', steered: true, ...result };
3224
3306
  }
3225
3307
 
3226
3308
  getJsonCliTranscript(agentId, options = {}) {
@@ -3337,6 +3419,11 @@ class AgentManager extends EventEmitter {
3337
3419
  return this.acpRuntime.authenticate(agentId, methodId);
3338
3420
  }
3339
3421
 
3422
+ logoutAcpAgent(agentId) {
3423
+ this.getAcpSession(agentId);
3424
+ return this.acpRuntime.logout(agentId);
3425
+ }
3426
+
3340
3427
  forkAcpSession(agentId, options = {}) {
3341
3428
  this.getAcpSession(agentId);
3342
3429
  return this.acpRuntime.forkSession(agentId, options);
@@ -3367,44 +3454,10 @@ class AgentManager extends EventEmitter {
3367
3454
  return this.acpRuntime.setSessionConfigOptions(agentId, changes);
3368
3455
  }
3369
3456
 
3370
- async setCodexAppServerGoal(agentId, patch = {}) {
3371
- const agent = this.assertCodexAppServerGoalAgent(agentId);
3372
- const objective = typeof patch.objective === 'string' ? patch.objective.trim().slice(0, 4000) : undefined;
3373
- const status = typeof patch.status === 'string' ? patch.status : undefined;
3374
- let tokenBudget;
3375
- if (Object.prototype.hasOwnProperty.call(patch, 'tokenBudget')) {
3376
- tokenBudget = Number.isFinite(Number(patch.tokenBudget)) && Number(patch.tokenBudget) > 0
3377
- ? Math.floor(Number(patch.tokenBudget))
3378
- : null;
3379
- }
3380
- if (objective !== undefined && !objective) {
3381
- throw new Error('Codex goal objective is required');
3382
- }
3383
- const goal = await this.codexAppServerRuntime.setAgentGoal({
3384
- ...this.codexAppServerOptionsForAgent(agent),
3385
- ...(objective !== undefined ? { objective } : {}),
3386
- ...(status !== undefined ? { status } : {}),
3387
- ...(Object.prototype.hasOwnProperty.call(patch, 'tokenBudget') ? { tokenBudget } : {}),
3388
- });
3389
- runtimeBindingOf(agent, 'app-server').goal = goal || null;
3390
- this.emit('update');
3391
- return goal || null;
3392
- }
3393
-
3394
- async clearCodexAppServerGoal(agentId) {
3395
- const agent = this.assertCodexAppServerGoalAgent(agentId);
3396
- await this.codexAppServerRuntime.clearAgentGoal(this.codexAppServerOptionsForAgent(agent));
3397
- runtimeBindingOf(agent, 'app-server').goal = null;
3398
- this.emit('update');
3399
- return null;
3400
- }
3401
-
3402
3457
  async sendInputNow(agentId, input, { markUserInput = true, expectedRuntimeEpoch = '' } = {}) {
3403
3458
  const agent = this.agents.get(agentId);
3404
3459
  if (!agent) return;
3405
- // The observer PTY is optional telemetry for App Server Agents. Its
3406
- // disappearance must never redefine the App Server Agent as dead.
3407
- if (isCodexAppServerAgent(agent) || isJsonCliAgent(agent) || isAcpAgent(agent)) return;
3460
+ if (isJsonCliAgent(agent) || isAcpAgent(agent)) return;
3408
3461
  if (expectedRuntimeEpoch && agent.runtimeEpoch !== expectedRuntimeEpoch) {
3409
3462
  return { status: 'input-rejected', reason: 'runtime-epoch-mismatch' };
3410
3463
  }
@@ -3412,15 +3465,16 @@ class AgentManager extends EventEmitter {
3412
3465
  const engine = this.engineBridge.getEngine(agent.engineName);
3413
3466
  if (!engine) return;
3414
3467
 
3468
+ const submittedUserInput = markUserInput && hasSubmittedTerminalInput(input);
3469
+ if (submittedUserInput && agent.terminalInputReceived !== true) {
3470
+ agent.terminalInputReceived = true;
3471
+ this.ensurePersistentAgentSession(agent);
3472
+ this.updateEngineProviderSessionMetadata(agent);
3473
+ this.emit('agent-update', { agentId, patch: { terminalInputReceived: true } });
3474
+ }
3475
+
3415
3476
  try {
3416
- const submittedUserInput = markUserInput && hasSubmittedTerminalInput(input);
3417
3477
  const result = await engine.sendInput(agentId, input, { expectedRuntimeEpoch });
3418
- if (submittedUserInput && agent.terminalInputReceived !== true) {
3419
- agent.terminalInputReceived = true;
3420
- this.ensurePersistentAgentSession(agent);
3421
- this.updateEngineProviderSessionMetadata(agent);
3422
- this.emit('agent-update', { agentId, patch: { terminalInputReceived: true } });
3423
- }
3424
3478
  if (submittedUserInput) {
3425
3479
  this.providerSessionService.observe(agentId, { force: true });
3426
3480
  }
@@ -3437,8 +3491,6 @@ class AgentManager extends EventEmitter {
3437
3491
  markAgentSessionDead(agentId, error) {
3438
3492
  const agent = this.agents.get(agentId);
3439
3493
  if (!agent || agent.status === 'dead') return;
3440
- if (isCodexAppServerAgent(agent)) return;
3441
-
3442
3494
  const message = error && error.message ? error.message : String(error || 'Session not available');
3443
3495
  agent.status = 'dead';
3444
3496
  agent.engineStatus = 'dead';
@@ -3452,31 +3504,7 @@ class AgentManager extends EventEmitter {
3452
3504
  async interruptAgent(agentId, options = {}) {
3453
3505
  const agent = this.agents.get(agentId);
3454
3506
  if (!agent) return;
3455
- const appServer = runtimeBindingOf(agent, 'app-server');
3456
-
3457
3507
  try {
3458
- if (
3459
- agent.providerSessionProvider === 'codex'
3460
- && appServer
3461
- && this.codexAppServerRuntime
3462
- && typeof this.codexAppServerRuntime.interruptAgent === 'function'
3463
- ) {
3464
- try {
3465
- const interrupted = await this.codexAppServerRuntime.interruptAgent(agentId);
3466
- if (interrupted) {
3467
- appServer.state = 'interrupting';
3468
- this.emit('update');
3469
- return;
3470
- }
3471
- appServer.error = 'Codex App Server interrupt is not available';
3472
- this.emit('update');
3473
- return;
3474
- } catch (error) {
3475
- appServer.error = `Codex App Server interrupt failed: ${error && (error.message || error)}`;
3476
- this.emit('update');
3477
- return;
3478
- }
3479
- }
3480
3508
  if (isJsonCliAgent(agent)) {
3481
3509
  this.jsonCliRuntime.interruptAgent(agentId);
3482
3510
  return;
@@ -3505,12 +3533,12 @@ class AgentManager extends EventEmitter {
3505
3533
  agentSupportsTerminalInput(agentId) {
3506
3534
  const agent = this.agents.get(agentId);
3507
3535
  if (!agent) return false;
3508
- return !isCodexAppServerAgent(agent) && !isJsonCliAgent(agent) && !isAcpAgent(agent);
3536
+ return !isJsonCliAgent(agent) && !isAcpAgent(agent);
3509
3537
  }
3510
3538
 
3511
3539
  async getAgentSessionAttachCheckpoint(agentId) {
3512
3540
  const agent = this.agents.get(agentId);
3513
- if (!agent || isCodexAppServerAgent(agent) || isAcpAgent(agent) || isJsonCliAgent(agent)) {
3541
+ if (!agent || isAcpAgent(agent) || isJsonCliAgent(agent)) {
3514
3542
  return null;
3515
3543
  }
3516
3544
  try {
@@ -3541,7 +3569,7 @@ class AgentManager extends EventEmitter {
3541
3569
  async resizeAgentSession(agentId, cols, rows) {
3542
3570
  const agent = this.agents.get(agentId);
3543
3571
  if (!agent) return { status: 'resize-rejected', reason: 'session-unavailable', resized: false };
3544
- if (isCodexAppServerAgent(agent) || isAcpAgent(agent) || isJsonCliAgent(agent)) {
3572
+ if (isAcpAgent(agent) || isJsonCliAgent(agent)) {
3545
3573
  return { status: 'resize-rejected', reason: 'unsupported-session', resized: false };
3546
3574
  }
3547
3575
 
@@ -3579,7 +3607,7 @@ class AgentManager extends EventEmitter {
3579
3607
  async clearAgentSessionBuffer(agentId, options = {}) {
3580
3608
  const agent = this.agents.get(agentId);
3581
3609
  if (!agent) return { cleared: false };
3582
- if (isCodexAppServerAgent(agent) || isAcpAgent(agent) || isJsonCliAgent(agent)) return { cleared: false };
3610
+ if (isAcpAgent(agent) || isJsonCliAgent(agent)) return { cleared: false };
3583
3611
  if (options.expectedRuntimeEpoch && agent.runtimeEpoch !== options.expectedRuntimeEpoch) {
3584
3612
  return { cleared: false, reason: 'runtime-epoch-mismatch' };
3585
3613
  }
@@ -3615,6 +3643,8 @@ class AgentManager extends EventEmitter {
3615
3643
 
3616
3644
  const customTitle = String(title || '').trim().slice(0, 80);
3617
3645
  agent.customTitle = customTitle;
3646
+ this.ensurePersistentAgentSession(agent);
3647
+ this.updateEngineProviderSessionMetadata(agent);
3618
3648
  this.emit('update');
3619
3649
  return { agentId, customTitle };
3620
3650
  }
@@ -3780,52 +3810,9 @@ class AgentManager extends EventEmitter {
3780
3810
  async syncCodexTerminalPermissionMode(agentId, mode) {
3781
3811
  const agent = this.agents.get(agentId);
3782
3812
  if (!agent) return { error: 'Agent not found' };
3783
-
3784
- if (
3785
- agent.providerSessionProvider === 'codex'
3786
- && runtimeKind(agent) === 'app-server'
3787
- ) {
3788
- return this.updateCodexAppServerPermissionMode(agentId, mode);
3789
- }
3790
-
3791
3813
  return this.restartAgentWithPermissionMode(agentId, mode);
3792
3814
  }
3793
3815
 
3794
- async updateCodexAppServerPermissionMode(agentId, mode) {
3795
- const agent = this.agents.get(agentId);
3796
- if (!agent) return { error: 'Agent not found' };
3797
- const runtime = runtimeBindingOf(agent, 'app-server');
3798
- if (agent.providerSessionProvider !== 'codex' || !runtime) {
3799
- return { error: 'Agent is not using the Codex App Server runtime' };
3800
- }
3801
-
3802
- const nextMode = ['ask', 'approve', 'full', 'custom'].includes(mode) ? mode : '';
3803
- if (!nextMode) return { error: 'Unsupported Codex permission mode' };
3804
- if (!this.codexAppServerRuntime || typeof this.codexAppServerRuntime.updateAgentPermissionMode !== 'function') {
3805
- return { error: 'Codex App Server permission updates are unavailable' };
3806
- }
3807
-
3808
- try {
3809
- await this.codexAppServerRuntime.updateAgentPermissionMode({
3810
- ...this.codexAppServerOptionsForAgent(agent),
3811
- approvalMode: nextMode,
3812
- });
3813
- } catch (error) {
3814
- return { error: error && error.message ? error.message : 'Failed to update Codex App Server permissions' };
3815
- }
3816
-
3817
- agent.launchPermissionMode = nextMode;
3818
- runtime.error = '';
3819
- this.updateEngineProviderSessionMetadata(agent);
3820
- this.ensurePersistentAgentSession(agent, { launchPermissionMode: nextMode });
3821
- this.emit('update');
3822
- return {
3823
- agentId,
3824
- updated: true,
3825
- launchPermissionMode: nextMode,
3826
- };
3827
- }
3828
-
3829
3816
  async restartAgentWithPermissionMode(agentId, mode) {
3830
3817
  const inFlight = this.permissionRestartInFlight.get(agentId);
3831
3818
  if (inFlight) {
@@ -3868,15 +3855,16 @@ class AgentManager extends EventEmitter {
3868
3855
  async performAgentRuntimeModeRestart(agentId, mode) {
3869
3856
  const agent = this.agents.get(agentId);
3870
3857
  if (!agent) return { error: 'Agent not found' };
3871
- const nextMode = ['terminal', 'json', 'acp'].includes(mode) ? mode : '';
3872
- if (!nextMode) return { error: 'Unsupported Agent runtime mode' };
3873
3858
  const provider = agent.providerSessionProvider || '';
3859
+ const requestedMode = ['terminal', 'json', 'acp', 'chat'].includes(mode) ? mode : '';
3860
+ const nextMode = requestedMode === 'acp' && provider === 'codex' ? 'chat' : requestedMode;
3861
+ if (!nextMode) return { error: 'Unsupported Agent runtime mode' };
3874
3862
  const currentKind = runtimeKind(agent);
3875
- const currentMode = ['acp', 'json'].includes(currentKind) ? currentKind : 'terminal';
3876
- const leavesCodexAppServer = provider === 'codex'
3877
- && currentKind === 'app-server'
3878
- && nextMode === 'terminal';
3879
- if (currentMode === nextMode && !leavesCodexAppServer) {
3863
+ const currentMode = currentKind === 'acp'
3864
+ ? 'chat'
3865
+ : (currentKind === 'json' ? 'json' : 'terminal');
3866
+ const nextRuntimeKind = nextMode === 'chat' ? chatRuntimeForProvider(provider) : nextMode;
3867
+ if (currentMode === nextMode) {
3880
3868
  return { agentId, agentRuntimeMode: nextMode };
3881
3869
  }
3882
3870
  const turnActive = currentKind === 'acp'
@@ -3885,18 +3873,21 @@ class AgentManager extends EventEmitter {
3885
3873
  if (turnActive) {
3886
3874
  return { error: 'Interrupt the active Agent turn before switching Chat and Terminal.' };
3887
3875
  }
3888
- if (!providerSupportsRuntime(provider, nextMode)) {
3876
+ const supportsNextMode = nextMode === 'chat'
3877
+ ? providerCapabilities(provider).supportsChat === true
3878
+ : providerSupportsRuntime(provider, nextMode);
3879
+ if (!supportsNextMode) {
3889
3880
  return { error: `Agent does not support the ${nextMode.toUpperCase()} runtime` };
3890
3881
  }
3891
3882
  const sessionId = String(agent.providerSessionId || '').trim();
3892
- const canStartFreshAcpSession = nextMode === 'acp'
3883
+ const canStartFreshChatSession = nextMode === 'chat'
3893
3884
  && currentMode === 'terminal'
3894
3885
  && agent.terminalInputReceived !== true
3895
3886
  && (
3896
3887
  agent.providerSessionTemporary === true
3897
3888
  || isFreshAcpSessionSource(provider, agent.providerSessionSource || '')
3898
3889
  );
3899
- if (!isSafeProviderSessionId(sessionId) && !canStartFreshAcpSession) {
3890
+ if (!isSafeProviderSessionId(sessionId) && !canStartFreshChatSession) {
3900
3891
  return { error: 'Runtime switching requires a resumable provider session. Send the first message and try again.' };
3901
3892
  }
3902
3893
  // A live ACP binding is the authoritative owner of a newly-created
@@ -3915,15 +3906,15 @@ class AgentManager extends EventEmitter {
3915
3906
  }
3916
3907
  }
3917
3908
  const previouslyVerifiedSession = String(agent.runtimeSwitchVerifiedSessionId || '') === sessionId;
3918
- let startsFreshAcpSession = canStartFreshAcpSession && !isSafeProviderSessionId(sessionId);
3919
- if (!startsFreshAcpSession && !liveAcpSession && !previouslyVerifiedSession) {
3909
+ let startsFreshChatSession = canStartFreshChatSession && !isSafeProviderSessionId(sessionId);
3910
+ if (!startsFreshChatSession && !liveAcpSession && !previouslyVerifiedSession) {
3920
3911
  const providerSession = await this.findRuntimeSwitchSession(agent);
3921
3912
  if (!providerSession) {
3922
- if (canStartFreshAcpSession) startsFreshAcpSession = true;
3913
+ if (canStartFreshChatSession) startsFreshChatSession = true;
3923
3914
  else return { error: 'The saved Agent session is no longer available in the selected Agent Home.' };
3924
3915
  }
3925
3916
  }
3926
- const command = startsFreshAcpSession
3917
+ const command = startsFreshChatSession
3927
3918
  ? (agent.forkCommand || agent.command)
3928
3919
  : buildAgentSessionResumeCommand(provider, sessionId, {
3929
3920
  cwd: effectiveAgentWorkspaceRoot(agent),
@@ -3940,12 +3931,20 @@ class AgentManager extends EventEmitter {
3940
3931
  ? this.jsonCliRuntime.getEvents(agentId)
3941
3932
  : (Array.isArray(agent.runtimeResumeState?.jsonEvents) ? agent.runtimeResumeState.jsonEvents : []),
3942
3933
  };
3934
+ let acpSessionOptions = {};
3935
+ if (currentKind === 'acp' && this.acpRuntime?.getSessionRequestOptions) {
3936
+ try {
3937
+ acpSessionOptions = this.acpRuntime.getSessionRequestOptions(agentId);
3938
+ } catch {
3939
+ acpSessionOptions = {};
3940
+ }
3941
+ }
3943
3942
  const restartOptions = {
3944
3943
  wantsMain: agent.wantsMain === true,
3945
3944
  task: agent.task || agent.providerSessionTitle || '',
3946
3945
  workflowTemplate: agent.workflowTemplate || '',
3947
3946
  projectWorkspace: effectiveAgentWorkspaceRoot(agent),
3948
- source: startsFreshAcpSession
3947
+ source: startsFreshChatSession
3949
3948
  ? 'ui-runtime-switch-fresh'
3950
3949
  : resumedAgentSource(provider, sessionId, agent.providerHomeId || ''),
3951
3950
  providerHomeId: agent.providerHomeId || '',
@@ -3960,12 +3959,12 @@ class AgentManager extends EventEmitter {
3960
3959
  projectOrder: preserved.projectOrder,
3961
3960
  pinnedOrder: preserved.pinnedOrder,
3962
3961
  agentRuntimeMode: nextMode,
3963
- acpStartFresh: startsFreshAcpSession,
3964
- codexRuntimeMode: 'cli',
3962
+ acpStartFresh: startsFreshChatSession && nextRuntimeKind === 'acp',
3965
3963
  codexApprovalMode: agent.launchPermissionMode || undefined,
3966
3964
  jsonCliEvents: preserved.jsonCliEvents,
3967
- runtimeSwitchVerifiedSessionId: startsFreshAcpSession ? '' : sessionId,
3968
- ...(provider === 'codex' && !startsFreshAcpSession
3965
+ runtimeSwitchVerifiedSessionId: startsFreshChatSession ? '' : sessionId,
3966
+ ...acpSessionOptions,
3967
+ ...(provider === 'codex' && !startsFreshChatSession
3969
3968
  ? preserveCodexSessionProfileOptions()
3970
3969
  : {}),
3971
3970
  };
@@ -3973,7 +3972,6 @@ class AgentManager extends EventEmitter {
3973
3972
  const originalOptions = {
3974
3973
  ...restartOptions,
3975
3974
  agentRuntimeMode: originalMode,
3976
- codexRuntimeMode: currentKind === 'app-server' ? 'app-server' : 'cli',
3977
3975
  acpStartFresh: false,
3978
3976
  };
3979
3977
  const startReplacement = options => new Promise(resolve => {
@@ -4090,6 +4088,9 @@ class AgentManager extends EventEmitter {
4090
4088
  const startsFreshCodexSession = provider === 'codex'
4091
4089
  && !hasResumableSession
4092
4090
  && (agent.providerSessionTemporary === true || !String(agent.providerSessionId || '').trim());
4091
+ if (startsFreshCodexSession && agent.terminalInputReceived === true) {
4092
+ return { error: 'Permission changes require a resumable provider session. Try again after the session id is available.' };
4093
+ }
4093
4094
  if (!hasResumableSession && !startsFreshCodexSession) {
4094
4095
  return { error: 'Permission changes require a resumable provider session. Try again after the session id is available.' };
4095
4096
  }
@@ -4103,6 +4104,14 @@ class AgentManager extends EventEmitter {
4103
4104
  return { error: 'Failed to build provider resume command' };
4104
4105
  }
4105
4106
 
4107
+ let acpSessionOptions = {};
4108
+ if (isAcpAgent(agent) && this.acpRuntime?.getSessionRequestOptions) {
4109
+ try {
4110
+ acpSessionOptions = this.acpRuntime.getSessionRequestOptions(agentId);
4111
+ } catch {
4112
+ acpSessionOptions = {};
4113
+ }
4114
+ }
4106
4115
  const restartOptions = {
4107
4116
  wantsMain: agent.wantsMain === true,
4108
4117
  task: agent.task || agent.providerSessionTitle || '',
@@ -4125,6 +4134,7 @@ class AgentManager extends EventEmitter {
4125
4134
  ])),
4126
4135
  projectOrder: finiteOrder(agent.projectOrder),
4127
4136
  pinnedOrder: finiteOrder(agent.pinnedOrder),
4137
+ ...acpSessionOptions,
4128
4138
  ...(provider === 'codex' ? { codexApprovalMode: nextMode } : { claudePermissionMode: nextMode }),
4129
4139
  ...(provider === 'codex' && hasResumableSession
4130
4140
  ? preserveCodexSessionProfileOptions()
@@ -4457,6 +4467,12 @@ class AgentManager extends EventEmitter {
4457
4467
  if (!['same-worktree', 'new-worktree'].includes(mode)) {
4458
4468
  return { error: 'Unsupported fork mode' };
4459
4469
  }
4470
+ if (agent.providerSessionProvider === 'codex' && agent.providerSessionTemporary === true) {
4471
+ await this.providerSessionService.resolveTemporaryCodex(agentId, { force: true });
4472
+ if (agent.providerSessionTemporary === true) {
4473
+ return { error: 'Fork requires a resumable Codex session. Try again after the session id is available.' };
4474
+ }
4475
+ }
4460
4476
 
4461
4477
  const sourceWorkspace = effectiveAgentWorkspaceRoot(agent);
4462
4478
  const resumedSession = agent.providerSessionProvider
@@ -4606,6 +4622,7 @@ class AgentManager extends EventEmitter {
4606
4622
  }
4607
4623
 
4608
4624
  const removedMainPageSessionKeys = this.removeMainPageProviderSessionsForAgents([agent]);
4625
+ this.ensurePersistentAgentSession(agent, { visibleOnMainPage: false });
4609
4626
  await this.killAgent(agentId, {
4610
4627
  reason: 'manual-archive',
4611
4628
  recordHistory: !isEphemeralShellAgent(agent),
@@ -4670,9 +4687,6 @@ class AgentManager extends EventEmitter {
4670
4687
  this.agentUsageRateCache.delete(agentId);
4671
4688
  this.lastResizeByAgent.delete(agentId);
4672
4689
  this.providerSessionService.stop(agentId);
4673
- if (this.codexAppServerRuntime && typeof this.codexAppServerRuntime.unregisterAgent === 'function') {
4674
- this.codexAppServerRuntime.unregisterAgent(agentId);
4675
- }
4676
4690
  if (this.jsonCliRuntime) this.jsonCliRuntime.unregisterAgent(agentId);
4677
4691
  if (this.acpRuntime) this.acpRuntime.unregisterAgent(agentId);
4678
4692
 
@@ -4729,13 +4743,11 @@ class AgentManager extends EventEmitter {
4729
4743
  getAgentProviderSession(agentId) {
4730
4744
  const agent = this.agents.get(agentId);
4731
4745
  if (!agent) return null;
4732
- const appServer = runtimeBindingOf(agent, 'app-server');
4733
4746
  return {
4734
4747
  provider: agent.providerSessionProvider || '',
4735
4748
  sessionId: agent.providerSessionId || '',
4736
4749
  providerHomeId: agent.providerHomeId || '',
4737
4750
  providerHomePath: agent.providerHomePath || '',
4738
- codexAppServerHomePath: appServer?.homePath || '',
4739
4751
  runtimeBinding: publicRuntimeBinding(agent),
4740
4752
  temporary: agent.providerSessionTemporary === true,
4741
4753
  title: agent.providerSessionTitle || '',
@@ -5023,7 +5035,10 @@ class AgentManager extends EventEmitter {
5023
5035
  providerSessionResolvedAt: agent.providerSessionResolvedAt || null,
5024
5036
  providerSessionTitle: agent.providerSessionTitle || '',
5025
5037
  providerSessionWorkspace: agent.providerSessionWorkspace || '',
5026
- providerCapabilities: providerCapabilities(agent.providerSessionProvider),
5038
+ providerCapabilities: {
5039
+ ...providerCapabilities(agent.providerSessionProvider),
5040
+ supportsSteer: runtimeBindingOf(agent, 'acp')?.supportsSteer === true,
5041
+ },
5027
5042
  terminalInputReceived: agent.terminalInputReceived === true,
5028
5043
  runtimeBinding: publicRuntimeBinding(agent),
5029
5044
  runtimeObservation: deriveRuntimeObservation(agent),