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.
- package/README.md +3 -3
- package/README.zh_cn.md +3 -3
- package/THIRD_PARTY_NOTICES.md +17 -1
- package/backend/acp/packaged-codex-acp.js +13 -0
- package/backend/acp-runtime.js +413 -24
- package/backend/agent-manager.js +572 -557
- package/backend/agent-provider-session.js +15 -1
- package/backend/agent-runtime-binding.js +9 -55
- package/backend/chat-runtime.js +28 -0
- package/backend/codex-rollout-follower.js +83 -0
- package/backend/codex-session-archive.js +12 -1
- package/backend/codex-session-history.js +94 -0
- package/backend/codex-transcript.js +2 -1
- package/backend/config-manager.js +25 -17
- package/backend/control-api.js +3 -1
- package/backend/farming-app-cli.js +21 -0
- package/backend/farming-session-store.js +2 -1
- package/backend/native-pty-host-client.js +59 -2
- package/backend/npm-update-helper.js +65 -7
- package/backend/provider-adapters.js +123 -38
- package/backend/provider-session-service.js +138 -34
- package/backend/server.js +94 -105
- package/backend/storage-layout.js +5 -16
- package/backend/update-service.js +94 -5
- package/backend/usage-history-client.js +305 -0
- package/backend/usage-history-scanner.generated.js +2250 -0
- package/backend/usage-history-scanner.ts +2718 -0
- package/backend/usage-history-smoke.js +64 -0
- package/backend/usage-history-worker.js +34 -0
- package/backend/usage-monitor.js +188 -588
- package/backend/vendor/usage-parsers/CC_STATISTICS_LICENSE +21 -0
- package/backend/vendor/usage-parsers/CODEXBAR_LICENSE +21 -0
- package/backend/vendor/usage-parsers/VENDOR.json +45 -0
- package/dist/acp/LICENSE.codex-acp +190 -0
- package/dist/acp/codex-acp-1.1.4.js +30718 -0
- package/dist/assets/App-6yWdwzj8.js +210 -0
- package/dist/assets/{FileEditorMarkdownPreview-UlVd0O4D.js → FileEditorMarkdownPreview-R0jwwLME.js} +4 -4
- package/dist/assets/FileEditorPane-DNy3LAqS.js +2 -0
- package/dist/assets/{IconGlyphs-AU22gPDY.js → IconGlyphs-BsUfqpKN.js} +1 -1
- package/dist/assets/ProjectFilesSection-BhDEUVHA.js +12 -0
- package/dist/assets/{ReviewPage-CeDKDyZh.js → ReviewPage-ChtByBe3.js} +1 -1
- package/dist/assets/{abnfDiagram-VRR7QNED-BgawzDQT.js → abnfDiagram-VRR7QNED-nnglrXFi.js} +1 -1
- package/dist/assets/{architectureDiagram-ZJ3FMSHR-DUShwPg0.js → architectureDiagram-ZJ3FMSHR-BEXMaFfY.js} +1 -1
- package/dist/assets/{blockDiagram-677ZJIJ3-CeGZviXR.js → blockDiagram-677ZJIJ3-1mCezSBk.js} +1 -1
- package/dist/assets/{c4Diagram-LMCZKHZV-CLhYht6S.js → c4Diagram-LMCZKHZV-DhTiI6Uv.js} +1 -1
- package/dist/assets/channel-BVWNatUB.js +1 -0
- package/dist/assets/{chunk-32BRIVSS-LFYpT-Cu.js → chunk-32BRIVSS-CCdsKd73.js} +1 -1
- package/dist/assets/{chunk-52WLFC77-BH6jDw9J.js → chunk-52WLFC77-Gncjk4LF.js} +1 -1
- package/dist/assets/{chunk-C7G6YPKG-B_mIUlb7.js → chunk-C7G6YPKG-CiDVUP82.js} +1 -1
- package/dist/assets/{chunk-EX3LRPZG-C63hVuAJ.js → chunk-EX3LRPZG-z6gXP2_N.js} +1 -1
- package/dist/assets/{chunk-FWX5IMBZ-CboX-WF_.js → chunk-FWX5IMBZ-DMZBccQa.js} +2 -2
- package/dist/assets/{chunk-HOUHSVGY-BOgSd6Un.js → chunk-HOUHSVGY-D8tIeoPk.js} +1 -1
- package/dist/assets/{chunk-ICXQ74PX-C0K-pgZt.js → chunk-ICXQ74PX-b6oQ2Q17.js} +1 -1
- package/dist/assets/{chunk-MOJQB5TN-CVMfwN_7.js → chunk-MOJQB5TN-DAVH_p2a.js} +1 -1
- package/dist/assets/{chunk-OGEWGWER-DLT3pxyw.js → chunk-OGEWGWER-DxoiMCI-.js} +1 -1
- package/dist/assets/{chunk-PUDLZKDR-Cd079yIQ.js → chunk-PUDLZKDR-NsOWh7HM.js} +1 -1
- package/dist/assets/{chunk-Q4XR5HBZ-D0jHa6SO.js → chunk-Q4XR5HBZ-Di4H7L21.js} +1 -1
- package/dist/assets/{chunk-V7JOEXUC-CVDtR75H.js → chunk-V7JOEXUC-DttpZg9Y.js} +1 -1
- package/dist/assets/{chunk-VAUOI2AC-B-W2_9Ot.js → chunk-VAUOI2AC-UHNr_yhX.js} +1 -1
- package/dist/assets/{chunk-VR4S4FIN-FD3kWh32.js → chunk-VR4S4FIN-D-Fmtd4Z.js} +1 -1
- package/dist/assets/{chunk-WYO6CB5R-C2bGaS5r.js → chunk-WYO6CB5R-CUxMtfK2.js} +4 -4
- package/dist/assets/{chunk-ZGVPDNZ5-ymgts0zo.js → chunk-ZGVPDNZ5-CoV2QEl0.js} +1 -1
- package/dist/assets/classDiagram-OUVF2IWQ-Db0ib5iE.js +1 -0
- package/dist/assets/classDiagram-v2-EOCWNBFH-Bkf69XF9.js +1 -0
- package/dist/assets/code-dark-5esj0bRu.css +1 -0
- package/dist/assets/{cynefinDiagram-TSTJHNR4-CwpobXIl.js → cynefinDiagram-TSTJHNR4-0xPI92Lg.js} +1 -1
- package/dist/assets/{dagre-VKFMJZFB-Dkhlfjna.js → dagre-VKFMJZFB-DfuMG4gU.js} +1 -1
- package/dist/assets/{diagram-FQU43EPY-hA8dicp6.js → diagram-FQU43EPY-CT00bLZ6.js} +1 -1
- package/dist/assets/{diagram-G47NLZAW-BTY6JTzO.js → diagram-G47NLZAW-Xbh7zXUx.js} +1 -1
- package/dist/assets/{diagram-NH7WQ7WH-xkaHHX9M.js → diagram-NH7WQ7WH-SRDVPfn7.js} +1 -1
- package/dist/assets/{diagram-OA4YK3LP-gk_fg9UU.js → diagram-OA4YK3LP-CQRli4Np.js} +1 -1
- package/dist/assets/{diagram-WEI45ONY-BvC8xd6T.js → diagram-WEI45ONY-DMf17WQi.js} +1 -1
- package/dist/assets/{ebnfDiagram-CCIWWBDH-CR7RaNat.js → ebnfDiagram-CCIWWBDH-DhpFkgJ5.js} +1 -1
- package/dist/assets/{erDiagram-Q63AITRT-_M-JFPbj.js → erDiagram-Q63AITRT-1IwxWmJo.js} +1 -1
- package/dist/assets/file-icons-DpbrD5ZP.js +1 -0
- package/dist/assets/flowDiagram-23GEKE2U-DYFo2xmB.js +1 -0
- package/dist/assets/{ganttDiagram-NO4QXBWP-Bl8P1yf_.js → ganttDiagram-NO4QXBWP-BNCdfyr1.js} +1 -1
- package/dist/assets/{gitGraphDiagram-IHSO6WYX-DoHa5ysz.js → gitGraphDiagram-IHSO6WYX-pbyWZuiX.js} +1 -1
- package/dist/assets/{index-BA-iH2Kg.js → index-C8ZFKye1.js} +2 -2
- package/dist/assets/{infoDiagram-FWYZ7A6U-DSFcSuUs.js → infoDiagram-FWYZ7A6U-BcTOgDbe.js} +1 -1
- package/dist/assets/{ishikawaDiagram-FXEZZL3T-DdkqullI.js → ishikawaDiagram-FXEZZL3T-DCdvGKrn.js} +1 -1
- package/dist/assets/{journeyDiagram-5HDEW3XC-B4hRnQJr.js → journeyDiagram-5HDEW3XC-BZ-1q_IT.js} +1 -1
- package/dist/assets/{kanban-definition-HUTT4EX6-tm2FZB2E.js → kanban-definition-HUTT4EX6-BIgejCjj.js} +1 -1
- package/dist/assets/{line-CmsjK4kn.js → line-CfptFMmX.js} +1 -1
- package/dist/assets/main-DSO3i_Gs.css +1 -0
- package/dist/assets/{mermaid.core-C7UT_EEL.js → mermaid.core-gSpX30pV.js} +3 -3
- package/dist/assets/{mindmap-definition-LN4V7U3C-DplfjGms.js → mindmap-definition-LN4V7U3C-BKBaI4LW.js} +1 -1
- package/dist/assets/{pegDiagram-2B236MQR-lu3SdkQs.js → pegDiagram-2B236MQR-sBvDY3VX.js} +1 -1
- package/dist/assets/{pieDiagram-ENE6RG2P-3fHbmDf9.js → pieDiagram-ENE6RG2P-DWlWcu08.js} +1 -1
- package/dist/assets/{quadrantDiagram-ABIIQ3AL-a5qoWz-X.js → quadrantDiagram-ABIIQ3AL-DZutUd3J.js} +1 -1
- package/dist/assets/{railroadDiagram-RFXS5EU6-D2TOuXW9.js → railroadDiagram-RFXS5EU6-DQb3lXGM.js} +1 -1
- package/dist/assets/{requirementDiagram-TGXJPOKE-LQBWpUvd.js → requirementDiagram-TGXJPOKE-DqFGXu2e.js} +1 -1
- package/dist/assets/{sankeyDiagram-HTMAVEWB-B1ohYoyK.js → sankeyDiagram-HTMAVEWB-d7ECahnO.js} +1 -1
- package/dist/assets/{sequenceDiagram-DBY2YBRQ-C3vM-hMq.js → sequenceDiagram-DBY2YBRQ-DdsbKqct.js} +1 -1
- package/dist/assets/{stateDiagram-2N3HPSRC-BvFhOryI.js → stateDiagram-2N3HPSRC-P6iSrgwR.js} +1 -1
- package/dist/assets/stateDiagram-v2-6OUMAXLB-DIYSf_qh.js +1 -0
- package/dist/assets/{swimlanes-5IMT3BWC-DKF_b-2s.js → swimlanes-5IMT3BWC-BGnZJx1M.js} +1 -1
- package/dist/assets/swimlanesDiagram-G3AALYLV-BIkrlKrZ.js +8 -0
- package/dist/assets/{timeline-definition-FHXFAJF6-vAs11ux_.js → timeline-definition-FHXFAJF6-rA-r3aUR.js} +1 -1
- package/dist/assets/{vennDiagram-L72KCM5P-BybTX_9B.js → vennDiagram-L72KCM5P-DiVKUjA9.js} +1 -1
- package/dist/assets/{wardleyDiagram-EHGQE667-CicUmMBR.js → wardleyDiagram-EHGQE667-Cfxh7UOn.js} +1 -1
- package/dist/assets/workspace-editor-model-D0Im34S5.js +1 -0
- package/dist/assets/{workspace-editor-monaco-BICJESG0.js → workspace-editor-monaco-BIpqcWf1.js} +1 -1
- package/dist/assets/workspace-editor-monaco-DLlws5Ig.js +1 -0
- package/dist/assets/{workspace-view-state-DvYG_9PH.js → workspace-view-state-BpPINu84.js} +1 -1
- package/dist/assets/{xychartDiagram-FW5EYKEG-BhWDyCX-.js → xychartDiagram-FW5EYKEG--hmryQeL.js} +1 -1
- package/dist/crt-mermaid-renderer.js +313 -313
- package/dist/index.html +1 -1
- package/frontend/session-bridge.js +24 -2
- package/frontend/skins/crt/app.js +203 -85
- package/frontend/skins/crt/index.html +2 -11
- package/frontend/skins/crt/styles/effects.css +2 -34
- package/package.json +30 -12
- package/shared/backend-connection-status.d.ts +25 -0
- package/shared/backend-connection-status.js +40 -0
- package/shared/browser-protocol.js +3 -3
- package/backend/app-server-api.js +0 -917
- package/backend/codex-app-server-home.js +0 -39
- package/backend/codex-app-server-runtime.js +0 -901
- package/dist/assets/App-Bumi772v.js +0 -206
- package/dist/assets/FileEditorPane-Dxx4rKOg.js +0 -2
- package/dist/assets/ProjectFilesSection-DE7Nb9Jk.js +0 -12
- package/dist/assets/channel-Uw90AO0b.js +0 -1
- package/dist/assets/classDiagram-OUVF2IWQ-DKW_dtku.js +0 -1
- package/dist/assets/classDiagram-v2-EOCWNBFH-C8Fi7-yv.js +0 -1
- package/dist/assets/code-dark-DPiuyeSp.css +0 -1
- package/dist/assets/code-mobile-3C2Rr-CN.css +0 -1
- package/dist/assets/file-icons-Bw2qd5iT.js +0 -1
- package/dist/assets/flowDiagram-23GEKE2U-DAiS5Y7G.js +0 -1
- package/dist/assets/main-Bqtb5kuz.css +0 -1
- package/dist/assets/stateDiagram-v2-6OUMAXLB-WMrz4PNo.js +0 -1
- package/dist/assets/swimlanesDiagram-G3AALYLV-BGswC9oC.js +0 -8
- package/dist/assets/workspace-editor-model-BQol4qbA.js +0 -1
- package/dist/assets/workspace-editor-monaco-DRJ7IaXk.js +0 -1
- package/scripts/sync-ghostty-vendor.js +0 -66
- /package/dist/assets/{core-D0LFJkDt.js → core-B9MwRjDE.js} +0 -0
package/backend/agent-manager.js
CHANGED
|
@@ -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
|
|
383
|
-
if (!
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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 (
|
|
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 ${
|
|
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
|
|
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 =>
|
|
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
|
|
1617
|
-
const
|
|
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
|
|
1603
|
+
providerSessionId,
|
|
1652
1604
|
providerSessionKey: metadata.providerSessionKey || (
|
|
1653
|
-
providerSessionProvider &&
|
|
1654
|
-
? mainPageAgentSessionKey(providerSessionProvider,
|
|
1605
|
+
providerSessionProvider && providerSessionId
|
|
1606
|
+
? mainPageAgentSessionKey(providerSessionProvider, providerSessionId, metadata.providerHomeId || '')
|
|
1655
1607
|
: ''
|
|
1656
1608
|
),
|
|
1657
|
-
providerSessionTemporary: metadata.providerSessionTemporary === true || isTemporaryProviderSessionId(
|
|
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
|
-
//
|
|
1664
|
-
//
|
|
1665
|
-
//
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
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.
|
|
2638
|
-
|
|
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
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
const
|
|
2648
|
-
|
|
2649
|
-
|
|
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 =
|
|
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:
|
|
2716
|
-
? runtimeBindingFor('
|
|
2717
|
-
: (
|
|
2718
|
-
? runtimeBindingFor('
|
|
2719
|
-
|
|
2720
|
-
?
|
|
2721
|
-
|
|
2722
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
3298
|
+
async sendAcpSteerNow(agentId, message) {
|
|
3169
3299
|
const agent = this.agents.get(agentId);
|
|
3170
3300
|
if (!agent) throw new Error('Agent not found');
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
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
|
-
|
|
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 !
|
|
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 ||
|
|
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 (
|
|
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 (
|
|
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 =
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
if (currentMode === nextMode
|
|
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
|
-
|
|
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
|
|
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) && !
|
|
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
|
|
3919
|
-
if (!
|
|
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 (
|
|
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 =
|
|
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:
|
|
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:
|
|
3964
|
-
codexRuntimeMode: 'cli',
|
|
3962
|
+
acpStartFresh: startsFreshChatSession && nextRuntimeKind === 'acp',
|
|
3965
3963
|
codexApprovalMode: agent.launchPermissionMode || undefined,
|
|
3966
3964
|
jsonCliEvents: preserved.jsonCliEvents,
|
|
3967
|
-
runtimeSwitchVerifiedSessionId:
|
|
3968
|
-
...
|
|
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:
|
|
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),
|