farming-code 2.2.8 → 2.2.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/README.md +42 -28
  2. package/README.zh_cn.md +42 -28
  3. package/THIRD_PARTY_NOTICES.md +11 -2
  4. package/backend/acp-checkpoint-store.js +198 -0
  5. package/backend/acp-runtime.js +306 -83
  6. package/backend/acp-session-state.js +202 -6
  7. package/backend/acp-transcript.js +112 -0
  8. package/backend/agent-activity.js +6 -157
  9. package/backend/agent-manager.js +1592 -741
  10. package/backend/agent-provider-session.js +17 -242
  11. package/backend/agent-runtime-binding.js +219 -0
  12. package/backend/agent-session-history.js +66 -1
  13. package/backend/auth.js +79 -6
  14. package/backend/codex-models.js +81 -84
  15. package/backend/codex-session-archive.js +54 -0
  16. package/backend/codex-terminal-profile.js +500 -0
  17. package/backend/codex-transcript-sanitizer.js +12 -0
  18. package/backend/codex-transcript.js +230 -8
  19. package/backend/config-manager.js +35 -0
  20. package/backend/control-api.js +192 -17
  21. package/backend/executable-discovery.js +2 -0
  22. package/backend/farming-net-pass.js +285 -0
  23. package/backend/farming-net-registry.js +112 -0
  24. package/backend/farming-net-server.js +298 -0
  25. package/backend/farming-session-store.js +5 -13
  26. package/backend/git-worktree-info.js +181 -0
  27. package/backend/local-session-engine.js +411 -186
  28. package/backend/main-page-session.js +5 -2
  29. package/backend/native-pty-controller-generation.js +106 -0
  30. package/backend/native-pty-host-client.js +275 -7
  31. package/backend/native-pty-host-identity.js +86 -0
  32. package/backend/native-pty-host.js +813 -114
  33. package/backend/native-session-engine.js +100 -28
  34. package/backend/packaged-node-pty.js +22 -2
  35. package/backend/provider-adapters.js +253 -0
  36. package/backend/provider-session-service.js +241 -0
  37. package/backend/runtime-observation.js +81 -0
  38. package/backend/server.js +354 -84
  39. package/backend/session-engine-bridge.js +21 -2
  40. package/backend/session-engine-router.js +1 -1
  41. package/backend/session-engine.js +1 -1
  42. package/backend/session-stream-protocol.js +185 -0
  43. package/backend/storage-layout.js +55 -0
  44. package/backend/terminal-attach-checkpoint.js +74 -0
  45. package/backend/terminal-exit-quiescence.js +39 -0
  46. package/backend/terminal-reducer-flow-control.js +97 -0
  47. package/backend/terminal-screen-state.js +11 -2
  48. package/backend/terminal-screen-worker-pool.js +59 -6
  49. package/backend/terminal-screen-worker-thread.js +97 -57
  50. package/backend/terminal-screen-worker.js +133 -51
  51. package/backend/terminal-state-serialization.js +127 -0
  52. package/backend/terminal-status.js +23 -4
  53. package/backend/usage-monitor.js +176 -17
  54. package/backend/workspace-directory.js +232 -0
  55. package/backend/workspace-file-router.js +182 -76
  56. package/backend/workspace-file-service.js +319 -4
  57. package/backend/workspace-root-registry.js +164 -0
  58. package/dist/assets/App-DWsa7DXG.js +206 -0
  59. package/dist/assets/{FileEditorMarkdownPreview-elKWc8Im.js → FileEditorMarkdownPreview-UlVd0O4D.js} +92 -92
  60. package/dist/assets/FileEditorPane-Dxx4rKOg.js +2 -0
  61. package/dist/assets/IconGlyphs-AU22gPDY.js +1 -0
  62. package/dist/assets/ProjectFilesSection-DE7Nb9Jk.js +12 -0
  63. package/dist/assets/ReviewPage-CeDKDyZh.js +3 -0
  64. package/dist/assets/code-dark-DPiuyeSp.css +1 -0
  65. package/dist/assets/file-icons-Bw2qd5iT.js +1 -0
  66. package/dist/assets/{index-BrbljRqn.js → index-CUFPJoa5.js} +3 -3
  67. package/dist/assets/main-Bqtb5kuz.css +1 -0
  68. package/dist/assets/{monaco.contribution-CyLosfZI.js → monaco.contribution-BXz-lfFB.js} +2 -2
  69. package/dist/assets/{tsMode-DGZTlTj8.js → tsMode-B0oUTcXQ.js} +1 -1
  70. package/dist/assets/workspace-editor-model-BQol4qbA.js +1 -0
  71. package/dist/assets/workspace-editor-monaco-BICJESG0.js +4 -0
  72. package/dist/assets/workspace-editor-monaco-DRJ7IaXk.js +1 -0
  73. package/dist/assets/workspace-view-state-DvYG_9PH.js +7 -0
  74. package/dist/assets/workspace-working-copy-D8-s_Sgh.js +1 -0
  75. package/dist/farming-2/site.webmanifest +2 -0
  76. package/dist/index.html +1 -1
  77. package/frontend/farming-net/app.css +625 -0
  78. package/frontend/farming-net/app.js +268 -0
  79. package/frontend/farming-net/index.html +86 -0
  80. package/frontend/reading-anchor.js +198 -0
  81. package/frontend/session-bridge.js +12 -3
  82. package/frontend/session-modal-bridge.js +5 -12
  83. package/frontend/skins/crt/app.js +1978 -793
  84. package/frontend/skins/crt/index.html +313 -23
  85. package/frontend/skins/crt/styles/billing.css +294 -223
  86. package/frontend/skins/crt/styles/monochrome-green.css +7 -2
  87. package/frontend/terminal-replay.js +372 -0
  88. package/package.json +10 -3
  89. package/shared/browser-protocol.d.ts +5 -0
  90. package/shared/browser-protocol.js +130 -0
  91. package/dist/assets/App-8dYAM6ql.js +0 -124
  92. package/dist/assets/FileEditorPane-RWiFD2cq.js +0 -5
  93. package/dist/assets/IconGlyphs-DfL0EBnj.js +0 -1
  94. package/dist/assets/ProjectFilesSection-Q4PDsWmM.js +0 -12
  95. package/dist/assets/ReviewPage-BaXu1ZdX.js +0 -3
  96. package/dist/assets/code-dark-CDkOQAtK.css +0 -1
  97. package/dist/assets/file-icons-EFUGSSwf.js +0 -1
  98. package/dist/assets/main-D073SnW4.css +0 -1
  99. package/dist/assets/qoder-C9LmmOSf.svg +0 -1
  100. package/dist/assets/qoder-Cf9gl0Y5.svg +0 -1
  101. package/dist/assets/qoder-gHCinseV.svg +0 -1
  102. package/dist/assets/workspace-view-state-CTyDzk2D.js +0 -1
  103. package/dist/assets/zsh-CLpveKlF.svg +0 -1
  104. package/dist/assets/zsh-FxSpMPbz.svg +0 -1
  105. /package/dist/assets/{api-D1lyBYIQ.js → api-D8nyOEbz.js} +0 -0
  106. /package/dist/assets/{core-ZlAPicox.js → core-D0LFJkDt.js} +0 -0
  107. /package/dist/assets/{useWorkspaceMenuKeyboard-CneKAZUJ.js → useWorkspaceMenuKeyboard-Brws6Ar9.js} +0 -0
@@ -6,19 +6,46 @@ const path = require('path');
6
6
  const { promisify } = require('util');
7
7
  const SystemMonitor = require('./system-monitor');
8
8
  const SessionEngineBridge = require('./session-engine-bridge');
9
- const { isSupportedHistoryAgent, resolveLaunchCommand } = require('./cli-agents');
10
- const { buildAgentSessionResumeCommand, findAgentSession } = require('./agent-session-history');
11
- const { listCodexSessions } = require('./codex-session-history');
9
+ const { isSupportedHistoryAgent, parseCommand, resolveLaunchCommand } = require('./cli-agents');
10
+ const {
11
+ buildAgentSessionResumeCommand,
12
+ findAgentSession,
13
+ } = require('./agent-session-history');
14
+ const { archiveCodexSession, unarchiveCodexSession } = require('./codex-session-archive');
12
15
  const { buildAgentProviderSessionPlan, sessionFromExactResumeSource } = require('./agent-provider-session');
13
16
  const { resolveAgentExecutable, resolveCompatibleCodexExecutable } = require('./executable-discovery');
14
17
  const { ensureMainAgentSkillFiles, renderMainAgentBootstrap } = require('./main-agent-skills');
15
18
  const { mainPageAgentSessionKey, resumedAgentSource } = require('./main-page-session');
16
19
  const { isSafeProviderSessionId, isTemporaryProviderSessionId } = require('./provider-session-id');
20
+ const { ProviderSessionService } = require('./provider-session-service');
21
+ const {
22
+ legacyRuntimeMetadata,
23
+ publicRuntimeBinding,
24
+ replaceRuntimeBinding,
25
+ RuntimeAgentMap,
26
+ runtimeBindingFor,
27
+ runtimeBindingOf,
28
+ runtimeKind,
29
+ } = require('./agent-runtime-binding');
30
+ const { deriveRuntimeObservation } = require('./runtime-observation');
31
+ const {
32
+ applyProviderHomeEnvironment,
33
+ getProviderAdapter,
34
+ isFreshAcpSessionSource,
35
+ providerCapabilities,
36
+ providerForProgram,
37
+ providerSupportsRuntime,
38
+ } = require('./provider-adapters');
17
39
  const { deriveTerminalStatus } = require('./terminal-status');
18
40
  const { CodexAppServerRuntime, normalizeCodexRuntimeMode } = require('./codex-app-server-runtime');
19
41
  const { JsonCliRuntime } = require('./json-cli-runtime');
20
42
  const { AcpRuntime } = require('./acp-runtime');
21
- const { acpToolChanges, acpToolDetail, acpToolReviewChanges } = require('./acp-transcript');
43
+ const { acpToolChanges, acpToolDetail, acpToolReviewChanges, acpTranscriptToolEntry } = require('./acp-transcript');
44
+ const {
45
+ applyCodexTerminalProfile,
46
+ codexTerminalProfileFromOutput,
47
+ codexTerminalProfileFromPreview,
48
+ } = require('./codex-terminal-profile');
22
49
  const { ensureCodexAppServerHome } = require('./codex-app-server-home');
23
50
  const {
24
51
  ensureAgentOrders,
@@ -32,22 +59,23 @@ const {
32
59
  normalizeInteractiveTerminalEnv,
33
60
  resolveUserShellEnvSync,
34
61
  } = require('./agent-env');
62
+ const { inspectGitWorktree } = require('./git-worktree-info');
63
+ const { deserializeTerminalState } = require('./terminal-state-serialization');
64
+ const { compareNativePtyRuntimeEpochs } = require('./native-pty-controller-generation');
65
+ const { canonicalWorkspacePath } = require('./workspace-root-registry');
35
66
 
36
67
  const SESSION_OUTPUT_LIMIT = 10000;
37
68
  const AGENT_USAGE_RATE_WINDOW_MS = 5 * 60 * 1000;
69
+ const AGENT_USAGE_RATE_REFRESH_MS = 5 * 1000;
38
70
  const ACTIVITY_UPDATE_INTERVAL_MS = 1000;
39
71
  const ACTIVITY_HOT_SEC = 30 * 60;
40
72
  const ACTIVITY_WARM_SEC = 3 * 60 * 60;
41
73
  const ACTIVITY_COOL_SEC = 12 * 60 * 60;
42
74
  const ZOMBIE_IDLE_MS = 72 * 60 * 60 * 1000;
43
75
  const ZOMBIE_SWEEP_INTERVAL_MS = 60 * 1000;
44
- const INPUT_SESSION_RETRY_DELAYS_MS = [25, 50, 100, 180, 300, 500];
45
76
  const MISSING_ENGINE_SESSION_STARTUP_GRACE_MS = 5000;
46
77
  const MIN_TERMINAL_RESIZE_COLS = 40;
47
78
  const MIN_TERMINAL_RESIZE_ROWS = 10;
48
- const CODEX_PROVIDER_SESSION_RESOLVE_COOLDOWN_MS = 1000;
49
- const CODEX_PROVIDER_SESSION_MATCH_GRACE_MS = 30 * 1000;
50
- const PROVIDER_SESSION_TITLE_RESOLVE_COOLDOWN_MS = 30 * 1000;
51
79
  const AGENT_DISCOVERY_CACHE_MAX_AGE_MS = 3_000;
52
80
  const SHELL_PROMPT_ENV_KEYS = [
53
81
  'PS1',
@@ -66,12 +94,107 @@ function trimSessionOutput(output) {
66
94
  return text.length > SESSION_OUTPUT_LIMIT ? text.slice(-SESSION_OUTPUT_LIMIT) : text;
67
95
  }
68
96
 
97
+ function terminalStateUpdateDisposition(agent, runtimeEpoch, outputSeq, stateRevision) {
98
+ const currentEpoch = typeof agent.runtimeEpoch === 'string' ? agent.runtimeEpoch : '';
99
+ const nextEpoch = typeof runtimeEpoch === 'string' ? runtimeEpoch : '';
100
+ if (currentEpoch && nextEpoch && currentEpoch !== nextEpoch) {
101
+ const relation = compareNativePtyRuntimeEpochs(nextEpoch, currentEpoch);
102
+ return relation === 1 ? 'new-epoch' : 'stale';
103
+ }
104
+ if (currentEpoch && !nextEpoch) return 'unversioned';
105
+ if (!currentEpoch || !nextEpoch) return 'current';
106
+
107
+ const currentRevision = Number(agent.stateRevision);
108
+ const nextRevision = Number(stateRevision);
109
+ if (Number.isFinite(currentRevision) && Number.isFinite(nextRevision)) {
110
+ if (nextRevision < currentRevision) return 'stale';
111
+ if (nextRevision === currentRevision) {
112
+ const currentOutputSeq = Number(agent.lastOutputSeq);
113
+ const nextOutputSeq = Number(outputSeq);
114
+ return Number.isFinite(nextOutputSeq)
115
+ && Number.isFinite(currentOutputSeq)
116
+ && nextOutputSeq === currentOutputSeq
117
+ ? 'duplicate'
118
+ : 'stale';
119
+ }
120
+ }
121
+ const currentOutputSeq = Number(agent.lastOutputSeq);
122
+ const nextOutputSeq = Number(outputSeq);
123
+ if (Number.isFinite(currentOutputSeq) && Number.isFinite(nextOutputSeq) && nextOutputSeq < currentOutputSeq) {
124
+ return 'stale';
125
+ }
126
+ return 'current';
127
+ }
128
+
129
+ function terminalRuntimeEventMatches(agent, runtimeEpoch) {
130
+ const currentEpoch = typeof agent?.runtimeEpoch === 'string' ? agent.runtimeEpoch : '';
131
+ if (!currentEpoch) return true;
132
+ return typeof runtimeEpoch === 'string' && runtimeEpoch === currentEpoch;
133
+ }
134
+
135
+ function setPendingTerminalStartSyncCut(agent, runtimeEpoch, outputSeq, stateRevision) {
136
+ if (
137
+ typeof runtimeEpoch !== 'string' || !runtimeEpoch ||
138
+ !Number.isFinite(outputSeq) ||
139
+ !Number.isFinite(stateRevision)
140
+ ) {
141
+ delete agent.pendingTerminalStartSyncCut;
142
+ return;
143
+ }
144
+ agent.pendingTerminalStartSyncCut = {
145
+ runtimeEpoch,
146
+ outputSeq,
147
+ stateRevision,
148
+ };
149
+ }
150
+
151
+ function consumesPendingTerminalStartSyncCut(agent, runtimeEpoch, outputSeq, stateRevision) {
152
+ const pending = agent?.pendingTerminalStartSyncCut;
153
+ return Boolean(
154
+ pending &&
155
+ pending.runtimeEpoch === runtimeEpoch &&
156
+ pending.outputSeq === outputSeq &&
157
+ pending.stateRevision === stateRevision &&
158
+ agent.runtimeEpoch === runtimeEpoch &&
159
+ agent.lastOutputSeq === outputSeq &&
160
+ agent.stateRevision === stateRevision
161
+ );
162
+ }
163
+
164
+ function clearPendingTerminalStartSyncCut(agent) {
165
+ if (agent) delete agent.pendingTerminalStartSyncCut;
166
+ }
167
+
168
+ function applyTerminalStateCursor(agent, runtimeEpoch, outputSeq, stateRevision, disposition) {
169
+ if (disposition === 'stale' || disposition === 'duplicate' || disposition === 'unversioned') return false;
170
+ if (disposition === 'new-epoch') {
171
+ agent.lastOutputSeq = 0;
172
+ agent.stateRevision = 0;
173
+ }
174
+ if (typeof runtimeEpoch === 'string' && runtimeEpoch) agent.runtimeEpoch = runtimeEpoch;
175
+ if (Number.isFinite(outputSeq)) agent.lastOutputSeq = outputSeq;
176
+ if (Number.isFinite(stateRevision)) agent.stateRevision = stateRevision;
177
+ return true;
178
+ }
179
+
69
180
  function hasResumeArg(args) {
70
181
  return Array.isArray(args)
71
182
  ? args.some(arg => arg === '--resume' || (typeof arg === 'string' && arg.startsWith('--resume=')))
72
183
  : false;
73
184
  }
74
185
 
186
+ function codexCommandContinuesSession(command) {
187
+ const parts = parseCommand(command);
188
+ return path.basename(parts[0] || '') === 'codex'
189
+ && parts.slice(1).some(arg => arg === 'resume' || arg === 'fork');
190
+ }
191
+
192
+ function preserveCodexSessionProfileOptions() {
193
+ return {
194
+ preserveProviderSessionProfile: true,
195
+ };
196
+ }
197
+
75
198
  function isSameOrDescendantPath(root, target) {
76
199
  const relative = path.relative(root, target);
77
200
  return relative === '' || Boolean(relative) && !relative.startsWith('..') && !path.isAbsolute(relative);
@@ -90,29 +213,27 @@ function agentProgramName(command) {
90
213
  }
91
214
 
92
215
  function agentHomeProviderForProgram(command) {
93
- const program = agentProgramName(command).toLowerCase();
94
- if (program === 'qodercli') return 'qoder';
95
- return ['codex', 'claude', 'opencode', 'qoder'].includes(program) ? program : '';
216
+ return providerForProgram(agentProgramName(command));
96
217
  }
97
218
 
98
219
  function hasUsableCodexAppServerHome(metadata) {
99
- const homePath = String(metadata?.codexAppServerHomePath || '').trim();
220
+ const homePath = String(runtimeBindingOf(metadata, 'app-server')?.homePath || '').trim();
100
221
  return Boolean(homePath) && fs.existsSync(homePath);
101
222
  }
102
223
 
103
224
  function shouldRecoverAsCodexAppServer(metadata) {
104
225
  const provider = metadata?.providerSessionProvider || metadata?.provider || '';
105
226
  return provider === 'codex'
106
- && metadata?.codexRuntimeMode === 'app-server'
227
+ && runtimeKind(metadata) === 'app-server'
107
228
  && hasUsableCodexAppServerHome(metadata);
108
229
  }
109
230
 
110
231
  function isJsonCliAgent(agent) {
111
- return agent && agent.agentRuntimeMode === 'json';
232
+ return runtimeKind(agent) === 'json';
112
233
  }
113
234
 
114
235
  function isAcpAgent(agent) {
115
- return agent && agent.agentRuntimeMode === 'acp';
236
+ return runtimeKind(agent) === 'acp';
116
237
  }
117
238
 
118
239
  function isShellProgram(command) {
@@ -123,6 +244,68 @@ function isEphemeralShellAgent(agent) {
123
244
  return agent && isShellProgram(agent.forkCommand || agent.command || '');
124
245
  }
125
246
 
247
+ function hasSubmittedTerminalInput(input) {
248
+ const parts = Array.isArray(input) ? input : [input];
249
+ return parts.some(part => {
250
+ // Composer paste parts carry draft text separately from the trailing Enter.
251
+ // Newlines inside that draft must not independently materialize a session.
252
+ if (part && typeof part === 'object' && part.type === 'paste') return false;
253
+ const text = String(part || '');
254
+ // xterm wraps native paste payloads so embedded newlines remain draft text.
255
+ // Remove complete bracketed-paste spans before looking for a submission.
256
+ const withoutBracketedPaste = text.replace(/\x1b\[200~[\s\S]*?\x1b\[201~/g, '');
257
+ return /[\r\n]/.test(withoutBracketedPaste);
258
+ });
259
+ }
260
+
261
+ function activeCodexTerminalProfile(agent, previewText) {
262
+ if (!agent) return null;
263
+ const provider = agent.providerSessionProvider
264
+ || agentHomeProviderForProgram(agent.forkCommand || agent.command || '');
265
+ if (
266
+ provider !== 'codex'
267
+ || isCodexAppServerAgent(agent)
268
+ || isJsonCliAgent(agent)
269
+ || isAcpAgent(agent)
270
+ || runtimeKind(agent) !== 'terminal'
271
+ ) {
272
+ return null;
273
+ }
274
+
275
+ const outputProfile = codexTerminalProfileFromOutput(agent.output || '');
276
+ const parsed = outputProfile || codexTerminalProfileFromPreview(previewText);
277
+ if (!parsed) return agent.codexTerminalProfile || null;
278
+ const previousServiceTier = agent.codexTerminalProfile?.serviceTier;
279
+ const profile = {
280
+ model: parsed.model,
281
+ reasoningEffort: parsed.effort,
282
+ serviceTier: typeof parsed.fast === 'boolean'
283
+ ? (parsed.fast ? 'priority' : 'default')
284
+ : (previousServiceTier || 'default'),
285
+ source: outputProfile ? 'terminal-output' : 'terminal-footer',
286
+ };
287
+ agent.codexTerminalProfile = profile;
288
+ return profile;
289
+ }
290
+
291
+ function terminalMetadataPatch(agent) {
292
+ const terminalStatus = deriveAgentTerminalStatus(agent);
293
+ return {
294
+ terminalBusy: typeof agent.terminalBusy === 'boolean' ? agent.terminalBusy : null,
295
+ shellCwd: agent.shellCwd || '',
296
+ shellLastExitCode: agent.shellLastExitCode ?? null,
297
+ shellLastEvent: agent.shellLastEvent || '',
298
+ shellCommand: agent.shellCommand || '',
299
+ shellLastCommand: agent.shellLastCommand || '',
300
+ shellCommandStartedAt: agent.shellCommandStartedAt ?? null,
301
+ shellLastCommandStartedAt: agent.shellLastCommandStartedAt ?? null,
302
+ shellLastCommandFinishedAt: agent.shellLastCommandFinishedAt ?? null,
303
+ shellLastCommandDurationMs: agent.shellLastCommandDurationMs ?? null,
304
+ terminalStatus,
305
+ runtimeObservation: deriveRuntimeObservation({ ...agent, terminalStatus }),
306
+ };
307
+ }
308
+
126
309
  function terminalRuntimeStatus(agentStatus) {
127
310
  return agentStatus === 'stopped' || agentStatus === 'dead' ? 'exited' : agentStatus;
128
311
  }
@@ -198,6 +381,9 @@ function hasAgentOutputAfterAttentionBaseline(agent) {
198
381
 
199
382
  function shouldRecoverEngineSession(metadata) {
200
383
  if (!metadata) return false;
384
+ // Main Agent shells are a product session, not an ephemeral scratch shell.
385
+ // Keep them attached to the persistent native PTY host across server restarts.
386
+ if (metadata.wantsMain === true) return true;
201
387
  if (metadata.category === 'shell') return false;
202
388
  return !isShellProgram(metadata.forkCommand || metadata.command || '');
203
389
  }
@@ -208,9 +394,7 @@ function recoveredEngineSessionId(entry, metadata = {}) {
208
394
 
209
395
  function agentDisplayName(command) {
210
396
  const program = agentProgramName(command).toLowerCase();
211
- if (program === 'codex') return 'codex';
212
- if (program === 'claude') return 'claude code';
213
- return program;
397
+ return getProviderAdapter(providerForProgram(program))?.displayName || program;
214
398
  }
215
399
 
216
400
  function titleComparisonKey(title) {
@@ -249,15 +433,8 @@ function isGenericSessionTitle(agent, title) {
249
433
  }
250
434
 
251
435
  function interruptInputForAgent(agent) {
252
- const program = agentProgramName(agent && agent.command);
253
- if (program === 'codex' || program === 'claude' || program === 'qodercli') {
254
- return '\x1b';
255
- }
256
- return '\x03';
257
- }
258
-
259
- function sleep(ms) {
260
- return new Promise(resolve => setTimeout(resolve, ms));
436
+ const provider = agent?.providerSessionProvider || agentHomeProviderForProgram(agent?.command);
437
+ return getProviderAdapter(provider)?.interruptInput || '\x03';
261
438
  }
262
439
 
263
440
  function normalizePathValue(value) {
@@ -267,9 +444,44 @@ function normalizePathValue(value) {
267
444
  return trimmed.replace(/[\\/]+$/, '');
268
445
  }
269
446
 
270
- function timestampMs(value) {
271
- const parsed = Date.parse(value || '');
272
- return Number.isFinite(parsed) ? parsed : 0;
447
+ function effectiveAgentWorkspaceRoot(agent) {
448
+ if (agent && agent.gitWorktree && agent.gitWorktree.workspace) {
449
+ return agent.gitWorktree.workspace;
450
+ }
451
+ return agent && (agent.projectWorkspace || agent.cwd) || '';
452
+ }
453
+
454
+ function publicAgentGitWorktree(agent) {
455
+ const worktree = agent && agent.gitWorktree;
456
+ if (!worktree || !worktree.workspace) return null;
457
+ return {
458
+ workspace: worktree.workspace,
459
+ commonDir: worktree.commonDir || '',
460
+ mainWorkspace: worktree.mainWorkspace || '',
461
+ linked: worktree.linked === true,
462
+ branch: worktree.branch || '',
463
+ head: worktree.head || '',
464
+ detached: worktree.detached === true,
465
+ locked: worktree.locked === true,
466
+ lockReason: worktree.lockReason || '',
467
+ prunable: worktree.prunable === true,
468
+ pruneReason: worktree.pruneReason || '',
469
+ worktrees: Array.isArray(worktree.worktrees)
470
+ ? worktree.worktrees.map(item => ({
471
+ workspace: item.workspace || '',
472
+ head: item.head || '',
473
+ branch: item.branch || '',
474
+ bare: item.bare === true,
475
+ detached: item.detached === true,
476
+ locked: item.locked === true,
477
+ lockReason: item.lockReason || '',
478
+ prunable: item.prunable === true,
479
+ pruneReason: item.pruneReason || '',
480
+ current: item.current === true,
481
+ main: item.main === true,
482
+ }))
483
+ : [],
484
+ };
273
485
  }
274
486
 
275
487
  function normalizePositiveInteger(value, fallback, min, max) {
@@ -289,7 +501,7 @@ function isRunningAgentRuntimeStatus(status) {
289
501
  }
290
502
 
291
503
  function isCodexAppServerAgent(agent) {
292
- return agent && agent.codexRuntimeMode === 'app-server';
504
+ return runtimeKind(agent) === 'app-server';
293
505
  }
294
506
 
295
507
  function isLiveEngineSessionState(sessionState) {
@@ -340,24 +552,27 @@ class AgentManager extends EventEmitter {
340
552
  0,
341
553
  60 * 60 * 1000
342
554
  );
343
- this.agents = new Map();
555
+ this.agents = new RuntimeAgentMap();
344
556
  this.mainAgentId = null;
345
557
  this.lastActivity = new Map();
346
558
  this.lastActivityUpdate = new Map();
347
559
  this.outputEvents = new Map(); // Map<agentId, Array<{timestamp, bytes}>> for rate tracking
560
+ this.agentUsageRateCache = new Map();
348
561
  this.lastResizeByAgent = new Map();
562
+ this.pendingResizeByAgent = new Map();
563
+ this.resizeDrains = new Map();
349
564
  this.inputQueues = new Map();
350
- this.codexSessionResolveInFlight = new Map();
351
- this.codexSessionResolveLastAttemptAt = new Map();
352
- this.providerSessionTitleResolveInFlight = new Map();
353
- this.providerSessionTitleResolveLastAttemptAt = new Map();
565
+ this.codexTerminalProfileQueues = new Map();
566
+ this.agentWorktreeResolveGeneration = new Map();
354
567
  this.permissionRestartInFlight = new Map();
355
568
  this.runtimeRestartInFlight = new Map();
569
+ this.codexSessionUnarchiveInFlight = new Map();
356
570
  this.permissionRestartSuppressedAgentIds = new Set();
357
571
  this.codexAppServerRuntime = options.codexAppServerRuntime || new CodexAppServerRuntime();
358
572
  this.jsonCliRuntime = options.jsonCliRuntime || new JsonCliRuntime();
359
- this.acpRuntime = options.acpRuntime || new AcpRuntime(
360
- process.env.FARMING_E2E_FAKE_ACP_AGENT === '1'
573
+ this.acpRuntime = options.acpRuntime || new AcpRuntime({
574
+ ...(this.configManager?.farmingDir ? { configDir: this.configManager.farmingDir } : {}),
575
+ ...(process.env.FARMING_E2E_FAKE_ACP_AGENT === '1'
361
576
  ? {
362
577
  resolveLaunch: () => ({
363
578
  command: process.execPath,
@@ -365,13 +580,31 @@ class AgentManager extends EventEmitter {
365
580
  version: 'e2e',
366
581
  }),
367
582
  }
368
- : undefined
369
- );
583
+ : {}),
584
+ });
585
+ this.archiveCodexSession = options.archiveCodexSession || archiveCodexSession;
586
+ this.unarchiveCodexSession = options.unarchiveCodexSession || unarchiveCodexSession;
370
587
  this.heartbeatInterval = null;
371
588
  this.disposed = false;
372
589
  this.systemMonitor = new SystemMonitor();
373
590
  this.startTime = Date.now();
374
591
  this.engineBridge = new SessionEngineBridge(configManager);
592
+ this.providerSessionService = new ProviderSessionService({
593
+ agents: this.agents,
594
+ getProviderHomes: () => this.configManager?.getSettings?.()?.agentHomes,
595
+ commit: (agent, change = {}) => {
596
+ if (change.kind === 'session-updated') this.ensurePersistentAgentSession(agent);
597
+ this.updateEngineProviderSessionMetadata(agent);
598
+ this.rememberMainPageProviderSession(agent);
599
+ if (change.event) {
600
+ this.emit('provider-session-updated', change.event);
601
+ this.emit('update');
602
+ }
603
+ if (change.refreshWorkspace) {
604
+ void this.refreshAgentWorktree(agent.id, change.refreshWorkspace);
605
+ }
606
+ },
607
+ });
375
608
  this.recoveryPromise = Promise.resolve();
376
609
  this.taskHistory = (this.configManager && this.configManager.getTaskHistory)
377
610
  ? [...this.configManager.getTaskHistory()]
@@ -393,17 +626,12 @@ class AgentManager extends EventEmitter {
393
626
  if (!this.jsonCliRuntime || typeof this.jsonCliRuntime.on !== 'function') return;
394
627
  this.jsonCliRuntime.on('agent-runtime', ({ agentId, state, error, sessionId }) => {
395
628
  const agent = this.agents.get(agentId);
396
- if (!agent) return;
397
- agent.jsonCliState = state || '';
398
- agent.jsonCliError = error || '';
629
+ const runtime = runtimeBindingOf(agent, 'json');
630
+ if (!runtime) return;
631
+ runtime.state = state || '';
632
+ runtime.error = error || '';
399
633
  if (sessionId) {
400
- agent.providerSessionId = sessionId;
401
- agent.providerSessionTemporary = false;
402
- agent.providerSessionKey = this.providerSessionKey(
403
- agent.providerSessionProvider,
404
- sessionId,
405
- agent.providerHomeId || 'default'
406
- );
634
+ this.providerSessionService.bindConfirmed(agentId, agent.providerSessionProvider, sessionId);
407
635
  this.ensurePersistentAgentSession(agent);
408
636
  }
409
637
  this.lastActivity.set(agentId, Date.now());
@@ -411,9 +639,10 @@ class AgentManager extends EventEmitter {
411
639
  });
412
640
  this.jsonCliRuntime.on('transcript', ({ agentId }) => {
413
641
  const agent = this.agents.get(agentId);
414
- if (!agent) return;
415
- agent.jsonCliEvents = this.jsonCliRuntime.getEvents(agentId);
416
- agent.jsonCliTranscriptUpdatedAt = new Date().toISOString();
642
+ const runtime = runtimeBindingOf(agent, 'json');
643
+ if (!runtime) return;
644
+ runtime.events = this.jsonCliRuntime.getEvents(agentId);
645
+ runtime.transcriptUpdatedAt = new Date().toISOString();
417
646
  this.emit('update');
418
647
  });
419
648
  }
@@ -422,24 +651,19 @@ class AgentManager extends EventEmitter {
422
651
  if (!this.acpRuntime || typeof this.acpRuntime.on !== 'function') return;
423
652
  this.acpRuntime.on('agent-runtime', ({ agentId, state, error, sessionId, stopReason, pendingPermission, pendingPermissions, pendingElicitation, pendingElicitations, activeElicitations, updatedAt }) => {
424
653
  const agent = this.agents.get(agentId);
425
- if (!agent) return;
426
- agent.acpState = state || '';
427
- agent.acpError = error || '';
428
- agent.acpStopReason = stopReason || '';
429
- agent.acpPendingPermission = pendingPermission || null;
430
- agent.acpPendingPermissions = Array.isArray(pendingPermissions) ? pendingPermissions : [];
431
- agent.acpPendingElicitation = pendingElicitation || null;
432
- agent.acpPendingElicitations = Array.isArray(pendingElicitations) ? pendingElicitations : [];
433
- agent.acpActiveElicitations = Array.isArray(activeElicitations) ? activeElicitations : [];
434
- agent.acpSessionUpdatedAt = updatedAt || '';
654
+ const runtime = runtimeBindingOf(agent, 'acp');
655
+ if (!runtime) return;
656
+ runtime.state = state || '';
657
+ runtime.error = error || '';
658
+ runtime.stopReason = stopReason || '';
659
+ runtime.pendingPermission = pendingPermission || null;
660
+ runtime.pendingPermissions = Array.isArray(pendingPermissions) ? pendingPermissions : [];
661
+ runtime.pendingElicitation = pendingElicitation || null;
662
+ runtime.pendingElicitations = Array.isArray(pendingElicitations) ? pendingElicitations : [];
663
+ runtime.activeElicitations = Array.isArray(activeElicitations) ? activeElicitations : [];
664
+ runtime.sessionUpdatedAt = updatedAt || '';
435
665
  if (sessionId) {
436
- agent.providerSessionId = sessionId;
437
- agent.providerSessionTemporary = false;
438
- agent.providerSessionKey = this.providerSessionKey(
439
- agent.providerSessionProvider,
440
- sessionId,
441
- agent.providerHomeId || 'default'
442
- );
666
+ this.providerSessionService.bindConfirmed(agentId, agent.providerSessionProvider, sessionId);
443
667
  this.ensurePersistentAgentSession(agent);
444
668
  }
445
669
  if (state === 'working' || state === 'waiting-for-permission' || state === 'waiting-for-input') this.lastActivity.set(agentId, Date.now());
@@ -447,9 +671,10 @@ class AgentManager extends EventEmitter {
447
671
  });
448
672
  this.acpRuntime.on('session', ({ agentId, revision }) => {
449
673
  const agent = this.agents.get(agentId);
450
- if (!agent) return;
451
- agent.acpSessionUpdatedAt = new Date().toISOString();
452
- agent.acpSessionRevision = Number.isFinite(Number(revision)) ? Number(revision) : (Number(agent.acpSessionRevision) || 0) + 1;
674
+ const runtime = runtimeBindingOf(agent, 'acp');
675
+ if (!runtime) return;
676
+ runtime.sessionUpdatedAt = new Date().toISOString();
677
+ runtime.sessionRevision = Number.isFinite(Number(revision)) ? Number(revision) : (Number(runtime.sessionRevision) || 0) + 1;
453
678
  this.emit('update');
454
679
  });
455
680
  }
@@ -458,34 +683,33 @@ class AgentManager extends EventEmitter {
458
683
  if (!this.codexAppServerRuntime || typeof this.codexAppServerRuntime.on !== 'function') return;
459
684
  this.codexAppServerRuntime.on('agent-runtime', ({ agentId, ...patch }) => {
460
685
  const agent = this.agents.get(agentId);
461
- if (!agent) return;
686
+ const runtime = runtimeBindingOf(agent, 'app-server');
687
+ if (!runtime) return;
462
688
 
463
689
  if (typeof patch.threadId === 'string' && patch.threadId) {
464
- agent.codexAppServerThreadId = patch.threadId;
465
- agent.providerSessionId = patch.threadId;
466
- agent.providerSessionTemporary = false;
467
- agent.providerSessionKey = this.providerSessionKey(
690
+ runtime.threadId = patch.threadId;
691
+ this.providerSessionService.bindConfirmed(
692
+ agentId,
468
693
  agent.providerSessionProvider || 'codex',
469
- patch.threadId,
470
- agent.providerHomeId || 'default'
694
+ patch.threadId
471
695
  );
472
696
  }
473
- if (typeof patch.turnId === 'string') agent.codexAppServerTurnId = patch.turnId;
474
- if (typeof patch.state === 'string' && patch.state) agent.codexAppServerState = patch.state;
475
- if (typeof patch.error === 'string') agent.codexAppServerError = patch.error;
476
- if (typeof patch.pendingRequestId === 'string') agent.codexAppServerPendingRequestId = patch.pendingRequestId;
477
- if (typeof patch.pendingRequestMethod === 'string') agent.codexAppServerPendingRequestMethod = patch.pendingRequestMethod;
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;
478
702
  if (Object.prototype.hasOwnProperty.call(patch, 'pendingRequest')) {
479
- agent.codexAppServerPendingRequest = patch.pendingRequest || null;
703
+ runtime.pendingRequest = patch.pendingRequest || null;
480
704
  }
481
705
  if (Object.prototype.hasOwnProperty.call(patch, 'notice')) {
482
- agent.codexAppServerNotice = patch.notice || null;
706
+ runtime.notice = patch.notice || null;
483
707
  }
484
708
  if (Object.prototype.hasOwnProperty.call(patch, 'goal')) {
485
- agent.codexAppServerGoal = patch.goal || null;
709
+ runtime.goal = patch.goal || null;
486
710
  }
487
711
  if (typeof patch.transcriptUpdatedAt === 'string') {
488
- agent.codexAppServerTranscriptUpdatedAt = patch.transcriptUpdatedAt;
712
+ runtime.transcriptUpdatedAt = patch.transcriptUpdatedAt;
489
713
  }
490
714
 
491
715
  if (patch.state === 'working' || patch.state === 'waiting-for-input') {
@@ -496,39 +720,57 @@ class AgentManager extends EventEmitter {
496
720
  }
497
721
 
498
722
  bindEngineEvents() {
499
- this.engineBridge.on('session-started', ({ sessionId, status, startedAt }) => {
723
+ this.engineBridge.on('session-started', ({
724
+ sessionId,
725
+ status,
726
+ startedAt,
727
+ runtimeEpoch,
728
+ outputSeq,
729
+ stateRevision,
730
+ }) => {
500
731
  const agent = this.agents.get(sessionId);
501
732
  if (!agent) return;
502
733
 
734
+ const disposition = terminalStateUpdateDisposition(agent, runtimeEpoch, outputSeq, stateRevision);
735
+ if (!applyTerminalStateCursor(agent, runtimeEpoch, outputSeq, stateRevision, disposition)) return;
736
+ setPendingTerminalStartSyncCut(agent, runtimeEpoch, outputSeq, stateRevision);
737
+
503
738
  agent.engineStarted = true;
504
739
  agent.engineStatus = status || 'running';
505
740
  agent.startedAt = startedAt || Date.now();
506
741
  this.observeAgentAttentionState(sessionId);
507
- this.observeAgentStateChange(sessionId, { force: true });
742
+ this.providerSessionService.observe(sessionId, { force: true });
508
743
  this.emit('update');
509
744
  });
510
745
 
511
- this.engineBridge.on('session-output', ({ sessionId, data, engineName, outputSeq }) => {
746
+ this.engineBridge.on('session-output', ({
747
+ sessionId,
748
+ data,
749
+ engineName,
750
+ runtimeEpoch,
751
+ outputSeq,
752
+ stateRevision,
753
+ }) => {
512
754
  const agent = this.agents.get(sessionId);
513
755
  if (!agent) return;
514
756
 
757
+ const disposition = terminalStateUpdateDisposition(agent, runtimeEpoch, outputSeq, stateRevision);
758
+ if (!applyTerminalStateCursor(agent, runtimeEpoch, outputSeq, stateRevision, disposition)) return;
759
+ clearPendingTerminalStartSyncCut(agent);
760
+
515
761
  this.reviveAgentRuntime(agent);
516
762
  agent.output = trimSessionOutput(agent.output + data);
517
- agent.lastEngineOutputAt = Date.now();
518
- if (Number.isFinite(outputSeq)) {
519
- agent.lastOutputSeq = outputSeq;
520
- }
521
-
522
- this.lastActivity.set(sessionId, Date.now());
763
+ const outputAt = Date.now();
764
+ agent.lastEngineOutputAt = outputAt;
765
+ this.lastActivity.set(sessionId, outputAt);
523
766
 
524
767
  // Track output events for rate calculation
525
768
  const events = this.outputEvents.get(sessionId) || [];
526
- events.push({ timestamp: Date.now(), bytes: Buffer.byteLength(String(data), 'utf8') });
527
- const cutoff = Date.now() - AGENT_USAGE_RATE_WINDOW_MS;
528
- this.outputEvents.set(sessionId, events.filter(e => e.timestamp > cutoff));
769
+ events.push({ timestamp: outputAt, bytes: Buffer.byteLength(String(data), 'utf8') });
770
+ this.outputEvents.set(sessionId, events);
771
+ this.getAgentUsageRate(sessionId, { now: outputAt });
529
772
 
530
773
  this.observeAgentAttentionState(sessionId);
531
- this.observeAgentStateChange(sessionId);
532
774
  const sessionSource = this.getEngineSessionSource(engineName);
533
775
  const stream = {
534
776
  agentId: sessionId,
@@ -538,44 +780,123 @@ class AgentManager extends EventEmitter {
538
780
  if (Number.isFinite(outputSeq)) {
539
781
  stream.outputSeq = outputSeq;
540
782
  }
783
+ if (Number.isFinite(stateRevision)) {
784
+ stream.stateRevision = stateRevision;
785
+ }
786
+ if (typeof runtimeEpoch === 'string' && runtimeEpoch) {
787
+ stream.runtimeEpoch = runtimeEpoch;
788
+ }
541
789
  this.emit('session-stream', stream);
542
790
  });
543
791
 
544
- this.engineBridge.on('session-sync', ({ sessionId, output, engineName, replaceLive = true, outputSeq }) => {
792
+ this.engineBridge.on('session-transition', ({
793
+ sessionId,
794
+ engineName,
795
+ kind,
796
+ data = '',
797
+ runtimeEpoch,
798
+ outputSeq,
799
+ stateRevision,
800
+ cols,
801
+ rows,
802
+ }) => {
803
+ const agent = this.agents.get(sessionId);
804
+ if (!agent) return;
805
+ const disposition = terminalStateUpdateDisposition(agent, runtimeEpoch, outputSeq, stateRevision);
806
+ if (!applyTerminalStateCursor(agent, runtimeEpoch, outputSeq, stateRevision, disposition)) return;
807
+ clearPendingTerminalStartSyncCut(agent);
808
+ this.reviveAgentRuntime(agent);
809
+ if (kind === 'clear') {
810
+ agent.output = '';
811
+ agent.previewText = '';
812
+ agent.previewSnapshot = null;
813
+ this.outputEvents.delete(sessionId);
814
+ this.agentUsageRateCache.delete(sessionId);
815
+ }
816
+ if (Number.isFinite(cols) && cols > 0) agent.previewCols = cols;
817
+ if (Number.isFinite(rows) && rows > 0) agent.previewRows = rows;
818
+ this.lastActivity.set(sessionId, Date.now());
819
+ this.emit('session-stream', {
820
+ agentId: sessionId,
821
+ sessionSource: this.getEngineSessionSource(engineName),
822
+ kind,
823
+ data,
824
+ runtimeEpoch,
825
+ outputSeq,
826
+ stateRevision,
827
+ cols,
828
+ rows,
829
+ });
830
+ this.observeAgentAttentionState(sessionId);
831
+ this.emit('update');
832
+ });
833
+
834
+ this.engineBridge.on('session-sync', ({
835
+ sessionId,
836
+ output,
837
+ engineName,
838
+ replaceLive = true,
839
+ runtimeEpoch,
840
+ outputSeq,
841
+ stateRevision,
842
+ textOutput,
843
+ cols,
844
+ rows,
845
+ }) => {
545
846
  const agent = this.agents.get(sessionId);
546
847
  if (!agent) return;
547
848
 
849
+ const hydratesStartedCut = consumesPendingTerminalStartSyncCut(
850
+ agent,
851
+ runtimeEpoch,
852
+ outputSeq,
853
+ stateRevision,
854
+ );
855
+ const disposition = terminalStateUpdateDisposition(agent, runtimeEpoch, outputSeq, stateRevision);
856
+ if (
857
+ !hydratesStartedCut &&
858
+ !applyTerminalStateCursor(agent, runtimeEpoch, outputSeq, stateRevision, disposition)
859
+ ) return;
860
+ clearPendingTerminalStartSyncCut(agent);
861
+
548
862
  this.reviveAgentRuntime(agent);
549
- agent.output = trimSessionOutput(output);
863
+ agent.output = trimSessionOutput(typeof textOutput === 'string' ? textOutput : output);
550
864
  agent.previewText = agent.output.slice(-2000);
551
- if (Number.isFinite(outputSeq)) {
552
- agent.lastOutputSeq = outputSeq;
553
- }
554
865
  this.lastActivity.set(sessionId, Date.now());
555
866
 
556
867
  if (replaceLive) {
557
868
  const sessionSource = this.getEngineSessionSource(engineName);
558
869
  const stream = {
559
870
  agentId: sessionId,
560
- data: agent.output,
871
+ data: output,
561
872
  sessionSource,
562
873
  replace: true,
563
874
  };
564
875
  if (Number.isFinite(outputSeq)) {
565
876
  stream.outputSeq = outputSeq;
566
877
  }
878
+ if (Number.isFinite(stateRevision)) {
879
+ stream.stateRevision = stateRevision;
880
+ }
881
+ if (typeof runtimeEpoch === 'string' && runtimeEpoch) {
882
+ stream.runtimeEpoch = runtimeEpoch;
883
+ }
884
+ if (Number.isFinite(cols) && cols > 0) {
885
+ stream.cols = cols;
886
+ }
887
+ if (Number.isFinite(rows) && rows > 0) {
888
+ stream.rows = rows;
889
+ }
567
890
  this.emit('session-stream', stream);
568
891
  }
569
892
  this.observeAgentAttentionState(sessionId);
570
- this.observeAgentStateChange(sessionId);
571
893
  this.emit('update');
572
894
  });
573
895
 
574
- this.engineBridge.on('session-preview', ({ sessionId, previewText, cols, rows, previewSnapshot, title }) => {
896
+ this.engineBridge.on('session-preview', ({ sessionId, previewText, cols, rows, previewSnapshot, title, runtimeEpoch }) => {
575
897
  const agent = this.agents.get(sessionId);
576
- if (!agent) return;
898
+ if (!agent || !terminalRuntimeEventMatches(agent, runtimeEpoch)) return;
577
899
 
578
- const revived = this.reviveAgentRuntime(agent);
579
900
  const titleChanged = typeof title === 'string'
580
901
  ? this.updateAgentSessionTitle(agent, title)
581
902
  : false;
@@ -587,44 +908,43 @@ class AgentManager extends EventEmitter {
587
908
  if (Number.isFinite(rows) && rows > 0) {
588
909
  agent.previewRows = rows;
589
910
  }
911
+ const terminalStatus = deriveAgentTerminalStatus(agent, {
912
+ previewText: agent.previewText,
913
+ title: agent.sessionTitle || '',
914
+ terminalBusy: typeof agent.terminalBusy === 'boolean' ? agent.terminalBusy : null,
915
+ });
590
916
  this.emit('session-preview-update', {
591
917
  agentId: sessionId,
592
918
  previewText: agent.previewText,
593
919
  cols: agent.previewCols || 80,
594
920
  rows: agent.previewRows || 30,
595
921
  previewSnapshot: agent.previewSnapshot,
596
- terminalStatus: deriveAgentTerminalStatus(agent, {
597
- previewText: agent.previewText,
598
- title: agent.sessionTitle || '',
599
- terminalBusy: typeof agent.terminalBusy === 'boolean' ? agent.terminalBusy : null,
600
- }),
922
+ codexTerminalProfile: activeCodexTerminalProfile(agent, agent.previewText),
923
+ terminalStatus,
924
+ runtimeObservation: deriveRuntimeObservation({ ...agent, terminalStatus }),
601
925
  });
602
926
  this.observeAgentAttentionState(sessionId);
603
- this.observeAgentStateChange(sessionId);
604
- if (titleChanged || revived) {
927
+ if (titleChanged) {
605
928
  this.emit('update');
606
929
  }
607
930
  });
608
931
 
609
- this.engineBridge.on('session-title', ({ sessionId, title }) => {
932
+ this.engineBridge.on('session-title', ({ sessionId, title, runtimeEpoch }) => {
610
933
  const agent = this.agents.get(sessionId);
611
- if (!agent) return;
934
+ if (!agent || !terminalRuntimeEventMatches(agent, runtimeEpoch)) return;
612
935
 
613
936
  if (this.updateAgentSessionTitle(agent, title)) {
614
937
  this.observeAgentAttentionState(sessionId);
615
- this.observeAgentStateChange(sessionId);
616
938
  this.emit('update');
617
939
  }
618
940
  });
619
941
 
620
- this.engineBridge.on('session-activity', ({ sessionId, lastActivityAt }) => {
942
+ this.engineBridge.on('session-activity', ({ sessionId, lastActivityAt, runtimeEpoch }) => {
621
943
  const agent = this.agents.get(sessionId);
622
- const revived = agent ? this.reviveAgentRuntime(agent) : false;
944
+ if (!agent || !terminalRuntimeEventMatches(agent, runtimeEpoch)) return;
623
945
  this.lastActivity.set(sessionId, lastActivityAt || Date.now());
624
946
  this.observeAgentAttentionState(sessionId);
625
- this.observeAgentStateChange(sessionId);
626
947
  this.emitActivityUpdate(sessionId, lastActivityAt || Date.now());
627
- if (revived) this.emit('update');
628
948
  });
629
949
 
630
950
  this.engineBridge.on('session-busy-state', (payload = {}) => {
@@ -642,9 +962,11 @@ class AgentManager extends EventEmitter {
642
962
  shellLastCommandDurationMs,
643
963
  statusMarkerSeen,
644
964
  busyMarkerSeen,
965
+ runtimeEpoch,
645
966
  } = payload;
646
967
  const agent = this.agents.get(sessionId);
647
- if (!agent) return;
968
+ if (!agent || !terminalRuntimeEventMatches(agent, runtimeEpoch)) return;
969
+ const previousShellCwd = agent.shellCwd || '';
648
970
 
649
971
  const previousState = JSON.stringify({
650
972
  terminalBusy: agent.terminalBusy,
@@ -714,14 +1036,23 @@ class AgentManager extends EventEmitter {
714
1036
  shellBusyMarkerSeen: agent.shellBusyMarkerSeen === true,
715
1037
  });
716
1038
  if (previousState === nextState) return;
1039
+ if (agent.shellCwd && agent.shellCwd !== previousShellCwd) {
1040
+ void this.refreshAgentWorktree(sessionId, agent.shellCwd);
1041
+ }
717
1042
  this.observeAgentAttentionState(sessionId);
718
- this.observeAgentStateChange(sessionId);
719
- this.emit('update');
1043
+ this.emit('agent-update', { agentId: sessionId, patch: terminalMetadataPatch(agent) });
720
1044
  });
721
1045
 
722
- this.engineBridge.on('session-exited', ({ sessionId, code, exitedAt }) => {
1046
+ this.engineBridge.on('session-exited', ({
1047
+ sessionId,
1048
+ code,
1049
+ exitedAt,
1050
+ runtimeEpoch,
1051
+ stateProofAvailable,
1052
+ }) => {
723
1053
  const agent = this.agents.get(sessionId);
724
- if (!agent) return;
1054
+ if (!agent || !terminalRuntimeEventMatches(agent, runtimeEpoch)) return;
1055
+ clearPendingTerminalStartSyncCut(agent);
725
1056
  if (this.permissionRestartSuppressedAgentIds.has(sessionId)) return;
726
1057
 
727
1058
  // A fresh App Server thread deliberately has no CLI/PTy observer until
@@ -730,13 +1061,29 @@ class AgentManager extends EventEmitter {
730
1061
  // open; it is not an exit of the App Server Agent.
731
1062
  if (isCodexAppServerAgent(agent)) return;
732
1063
 
1064
+ if (stateProofAvailable === false) {
1065
+ this.providerSessionService.stop(sessionId);
1066
+ agent.status = 'dead';
1067
+ agent.engineStatus = 'dead';
1068
+ agent.terminalBusy = false;
1069
+ agent.exitedAt = exitedAt || Date.now();
1070
+ const proofError = 'Terminal exited without an authoritative final checkpoint';
1071
+ if (!String(agent.output || '').includes(proofError)) {
1072
+ agent.output = trimSessionOutput(`${agent.output || ''}\n${proofError}`);
1073
+ }
1074
+ this.observeAgentAttentionState(sessionId);
1075
+ this.providerSessionService.observe(sessionId, { force: true });
1076
+ this.emit('update');
1077
+ return;
1078
+ }
1079
+
733
1080
  if (!agent.validated) {
734
- this.stopCodexProviderSessionResolver(sessionId);
735
- this.stopProviderSessionTitleResolver(sessionId);
1081
+ this.providerSessionService.stop(sessionId);
736
1082
  this.agents.delete(sessionId);
737
1083
  this.lastActivity.delete(sessionId);
738
1084
  this.lastActivityUpdate.delete(sessionId);
739
1085
  this.outputEvents.delete(sessionId);
1086
+ this.agentUsageRateCache.delete(sessionId);
740
1087
  this.lastResizeByAgent.delete(sessionId);
741
1088
 
742
1089
  if (this.mainAgentId === sessionId) {
@@ -747,13 +1094,12 @@ class AgentManager extends EventEmitter {
747
1094
  return;
748
1095
  }
749
1096
 
750
- this.stopCodexProviderSessionResolver(sessionId);
751
- this.stopProviderSessionTitleResolver(sessionId);
1097
+ this.providerSessionService.stop(sessionId);
752
1098
  agent.status = sessionId === this.mainAgentId ? 'dead' : 'stopped';
753
1099
  agent.exitedAt = exitedAt || Date.now();
754
1100
  agent.output = trimSessionOutput(`${agent.output}\nProcess exited with code ${code}`);
755
1101
  this.observeAgentAttentionState(sessionId);
756
- this.observeAgentStateChange(sessionId, { force: true });
1102
+ this.providerSessionService.observe(sessionId, { force: true });
757
1103
  if (sessionId !== this.mainAgentId) {
758
1104
  this.recordTaskHistory(agent, {
759
1105
  reason: 'process-exit',
@@ -763,9 +1109,9 @@ class AgentManager extends EventEmitter {
763
1109
  this.emit('update');
764
1110
  });
765
1111
 
766
- this.engineBridge.on('session-error', ({ sessionId, error, fatal = true }) => {
1112
+ this.engineBridge.on('session-error', ({ sessionId, error, fatal = true, runtimeEpoch }) => {
767
1113
  const agent = this.agents.get(sessionId);
768
- if (!agent) return;
1114
+ if (!agent || !terminalRuntimeEventMatches(agent, runtimeEpoch)) return;
769
1115
  if (this.permissionRestartSuppressedAgentIds.has(sessionId)) return;
770
1116
 
771
1117
  if (fatal === false) {
@@ -803,12 +1149,37 @@ class AgentManager extends EventEmitter {
803
1149
  // has already switched to ACP. Recovering that stale PTY first would
804
1150
  // overwrite the persisted record back to `terminal` before
805
1151
  // recoverAcpSessions() gets a chance to read it.
806
- if (persisted?.agentRuntimeMode === 'acp') {
1152
+ if (runtimeKind(persisted) === 'acp') {
807
1153
  await this.killRecoveredEngineSession(entry, engineMetadata, agentId);
808
1154
  continue;
809
1155
  }
1156
+ const persistedProvider = String(persisted?.providerSessionProvider || persisted?.provider || '').trim();
810
1157
  const metadata = persisted ? {
811
1158
  ...engineMetadata,
1159
+ // The native host owns the live PTY/reducer state, but the Farming
1160
+ // session record owns stable product identity. Legacy hosts can omit
1161
+ // these fields during recovery; projecting that incomplete metadata
1162
+ // even briefly makes Chat/Terminal switching disappear until a later
1163
+ // provider resolver update happens to repair it.
1164
+ source: persisted.source || engineMetadata.source,
1165
+ projectWorkspace: persisted.projectWorkspace || engineMetadata.projectWorkspace,
1166
+ provider: persistedProvider || engineMetadata.provider,
1167
+ providerSessionProvider: persistedProvider || engineMetadata.providerSessionProvider,
1168
+ providerHomeId: persisted.providerHomeId || engineMetadata.providerHomeId,
1169
+ providerHomePath: persisted.providerHomePath || engineMetadata.providerHomePath,
1170
+ providerSessionId: persisted.providerSessionId || engineMetadata.providerSessionId,
1171
+ providerSessionKey: persisted.providerSessionKey || engineMetadata.providerSessionKey,
1172
+ providerSessionTemporary: Object.prototype.hasOwnProperty.call(persisted, 'providerSessionTemporary')
1173
+ ? persisted.providerSessionTemporary === true
1174
+ : engineMetadata.providerSessionTemporary,
1175
+ providerSessionSource: persisted.providerSessionSource || engineMetadata.providerSessionSource,
1176
+ providerSessionResolvedAt: persisted.providerSessionResolvedAt || engineMetadata.providerSessionResolvedAt,
1177
+ providerSessionTitle: persisted.providerSessionTitle || engineMetadata.providerSessionTitle,
1178
+ providerSessionWorkspace: persisted.providerSessionWorkspace || engineMetadata.providerSessionWorkspace,
1179
+ terminalInputReceived: Object.prototype.hasOwnProperty.call(persisted, 'terminalInputReceived')
1180
+ ? persisted.terminalInputReceived === true
1181
+ : engineMetadata.terminalInputReceived,
1182
+ ...legacyRuntimeMetadata(persisted),
812
1183
  pinned: persisted.pinned === true,
813
1184
  projectOrder: finiteOrder(persisted.projectOrder) ?? finiteOrder(engineMetadata.projectOrder),
814
1185
  pinnedOrder: finiteOrder(persisted.pinnedOrder) ?? finiteOrder(engineMetadata.pinnedOrder),
@@ -824,11 +1195,12 @@ class AgentManager extends EventEmitter {
824
1195
  agentRecord.lastObservedTurnActive = this.isAgentAttentionTurnActive(agentRecord);
825
1196
  this.ensurePersistentAgentSession(agentRecord);
826
1197
  this.agents.set(agentId, agentRecord);
1198
+ void this.refreshAgentWorktree(agentId);
827
1199
  this.lastActivity.set(agentId, state.lastActivityAt || metadata.lastActivityAt || Date.now());
828
1200
  if (agentRecord.wantsMain && !this.mainAgentId) {
829
1201
  this.mainAgentId = agentId;
830
1202
  }
831
- this.activateProviderSessionTracking(agentId);
1203
+ this.providerSessionService.activate(agentId);
832
1204
  changed = true;
833
1205
  }
834
1206
 
@@ -836,29 +1208,272 @@ class AgentManager extends EventEmitter {
836
1208
  this.emit('update');
837
1209
  }
838
1210
 
1211
+ const runtimeRotations = this.engineBridge && typeof this.engineBridge.consumeRuntimeRotations === 'function'
1212
+ ? this.engineBridge.consumeRuntimeRotations()
1213
+ : [];
1214
+ if (runtimeRotations.length > 0) {
1215
+ await this.restoreTerminalSessionsAfterRuntimeRotation(persistedRecords, runtimeRotations);
1216
+ }
1217
+
839
1218
  await this.recoverCodexAppServerSessions();
840
1219
  await this.recoverAcpSessions();
841
1220
  }
842
1221
 
1222
+ async restoreTerminalSessionsAfterRuntimeRotation(records, rotations) {
1223
+ const mainPageOrder = new Map(this.getMainPageSessionKeys().map((key, index) => [key, index]));
1224
+ const liveProviderSessions = new Set(
1225
+ [...this.agents.values()]
1226
+ .filter(agent => agent?.providerSessionProvider && agent?.providerSessionId)
1227
+ .map(agent => mainPageAgentSessionKey(
1228
+ agent.providerSessionProvider,
1229
+ agent.providerSessionId,
1230
+ agent.providerHomeId || 'default'
1231
+ ))
1232
+ .filter(Boolean)
1233
+ );
1234
+ const recordList = Array.isArray(records) ? records : [];
1235
+ const recordByRuntimeAgentId = new Map(recordList
1236
+ .filter(record => record && typeof record.runtimeAgentId === 'string' && record.runtimeAgentId)
1237
+ .map(record => [record.runtimeAgentId, record]));
1238
+ const serializedStates = [];
1239
+ for (const rotation of Array.isArray(rotations) ? rotations : []) {
1240
+ if (!rotation || typeof rotation.serializedTerminalState !== 'string' || !rotation.serializedTerminalState) continue;
1241
+ try {
1242
+ serializedStates.push(...deserializeTerminalState(rotation.serializedTerminalState));
1243
+ } catch (error) {
1244
+ console.warn(
1245
+ 'Ignoring invalid serialized terminal state after native PTY runtime rotation:',
1246
+ error && (error.message || error)
1247
+ );
1248
+ }
1249
+ }
1250
+ const serializedByRuntimeAgentId = new Map(serializedStates.map(state => [state.id, state]));
1251
+ const candidateKeys = new Set();
1252
+ const fallbackCandidates = recordList
1253
+ .filter(record => {
1254
+ if (!record || record.archived === true) return false;
1255
+ if (runtimeKind(record) !== 'terminal') return false;
1256
+ const provider = String(record.providerSessionProvider || record.provider || '').trim();
1257
+ if (!getProviderAdapter(provider)) return false;
1258
+ if (!isSafeProviderSessionId(record.providerSessionId)) return false;
1259
+ const sessionKey = record.providerSessionKey || mainPageAgentSessionKey(
1260
+ provider,
1261
+ record.providerSessionId,
1262
+ record.providerHomeId || 'default'
1263
+ );
1264
+ if (!sessionKey || liveProviderSessions.has(sessionKey)) return false;
1265
+ if (record.wantsMain === true) return !this.mainAgentId;
1266
+ return mainPageOrder.has(sessionKey);
1267
+ })
1268
+ .sort((left, right) => {
1269
+ if (left.wantsMain === true && right.wantsMain !== true) return -1;
1270
+ if (right.wantsMain === true && left.wantsMain !== true) return 1;
1271
+ const leftProvider = String(left.providerSessionProvider || left.provider || '').trim();
1272
+ const rightProvider = String(right.providerSessionProvider || right.provider || '').trim();
1273
+ const leftKey = left.providerSessionKey || mainPageAgentSessionKey(
1274
+ leftProvider,
1275
+ left.providerSessionId,
1276
+ left.providerHomeId || 'default'
1277
+ );
1278
+ const rightKey = right.providerSessionKey || mainPageAgentSessionKey(
1279
+ rightProvider,
1280
+ right.providerSessionId,
1281
+ right.providerHomeId || 'default'
1282
+ );
1283
+ const orderDelta = (mainPageOrder.get(leftKey) ?? Number.MAX_SAFE_INTEGER) -
1284
+ (mainPageOrder.get(rightKey) ?? Number.MAX_SAFE_INTEGER);
1285
+ if (orderDelta !== 0) return orderDelta;
1286
+ return (Number(right.updatedAt) || 0) - (Number(left.updatedAt) || 0);
1287
+ })
1288
+ .filter(record => {
1289
+ const provider = String(record.providerSessionProvider || record.provider || '').trim();
1290
+ const sessionKey = record.providerSessionKey || mainPageAgentSessionKey(
1291
+ provider,
1292
+ record.providerSessionId,
1293
+ record.providerHomeId || 'default'
1294
+ );
1295
+ if (candidateKeys.has(sessionKey)) return false;
1296
+ candidateKeys.add(sessionKey);
1297
+ return true;
1298
+ });
1299
+ const candidates = serializedByRuntimeAgentId.size > 0
1300
+ ? [...serializedByRuntimeAgentId.values()]
1301
+ .map(serializedState => ({
1302
+ ...(serializedState.metadata || {}),
1303
+ ...(recordByRuntimeAgentId.get(serializedState.id) || {}),
1304
+ runtimeAgentId: serializedState.id,
1305
+ serializedState,
1306
+ }))
1307
+ .filter(record => {
1308
+ if (!record || record.archived === true) return false;
1309
+ return runtimeKind(record) === 'terminal';
1310
+ })
1311
+ .sort((left, right) => {
1312
+ if (left.wantsMain === true && right.wantsMain !== true) return -1;
1313
+ if (right.wantsMain === true && left.wantsMain !== true) return 1;
1314
+ return (Number(right.updatedAt) || 0) - (Number(left.updatedAt) || 0);
1315
+ })
1316
+ : fallbackCandidates;
1317
+
1318
+ if (candidates.length > 0) {
1319
+ const rotationSummary = (Array.isArray(rotations) ? rotations : []).map(rotation => {
1320
+ const { serializedTerminalState, ...rest } = rotation || {};
1321
+ return {
1322
+ ...rest,
1323
+ serializedTerminalStateBytes: typeof serializedTerminalState === 'string'
1324
+ ? Buffer.byteLength(serializedTerminalState, 'utf8')
1325
+ : 0,
1326
+ };
1327
+ });
1328
+ console.warn(
1329
+ `Restoring ${candidates.length} Terminal session(s) after native PTY runtime rotation`,
1330
+ rotationSummary
1331
+ );
1332
+ }
1333
+
1334
+ let changed = false;
1335
+ for (const record of candidates) {
1336
+ if (record.wantsMain === true && this.mainAgentId) continue;
1337
+ const provider = String(record.providerSessionProvider || record.provider || '').trim();
1338
+ const sessionId = record.providerSessionId;
1339
+ const sessionKey = record.providerSessionKey || mainPageAgentSessionKey(
1340
+ provider,
1341
+ sessionId,
1342
+ record.providerHomeId || 'default'
1343
+ );
1344
+ if (sessionKey && liveProviderSessions.has(sessionKey)) continue;
1345
+ const canResumeProvider = Boolean(getProviderAdapter(provider))
1346
+ && isSafeProviderSessionId(sessionId);
1347
+ const command = canResumeProvider
1348
+ ? buildAgentSessionResumeCommand(provider, sessionId, {
1349
+ cwd: record.cwd || record.projectWorkspace || '',
1350
+ providerHomePath: record.providerHomePath || '',
1351
+ })
1352
+ : (
1353
+ record.forkCommand ||
1354
+ record.command ||
1355
+ record.serializedState?.processLaunchConfig?.command ||
1356
+ ''
1357
+ );
1358
+ if (!command) continue;
1359
+
1360
+ const options = {
1361
+ wantsMain: record.wantsMain === true,
1362
+ skipRecoveryWait: true,
1363
+ task: record.task || record.providerSessionTitle || '',
1364
+ workflowTemplate: record.workflowTemplate || '',
1365
+ projectWorkspace: record.projectWorkspace || record.cwd || '',
1366
+ source: canResumeProvider
1367
+ ? resumedAgentSource(provider, sessionId, record.providerHomeId || 'default')
1368
+ : (record.source || 'terminal-revive'),
1369
+ providerHomeId: record.providerHomeId || '',
1370
+ providerHomePath: record.providerHomePath || '',
1371
+ providerSessionTitle: record.providerSessionTitle || '',
1372
+ restartedFromAgentId: record.restartedFromAgentId || '',
1373
+ restartedFromAgentIds: Array.isArray(record.restartedFromAgentIds)
1374
+ ? record.restartedFromAgentIds
1375
+ : [],
1376
+ projectOrder: finiteOrder(record.projectOrder),
1377
+ pinnedOrder: finiteOrder(record.pinnedOrder),
1378
+ customTitle: record.customTitle || '',
1379
+ persistentSessionId: record.id || '',
1380
+ runtimeAgentId: record.runtimeAgentId || '',
1381
+ reviveTerminalState: record.serializedState || null,
1382
+ ...(provider === 'codex'
1383
+ ? {
1384
+ codexApprovalMode: record.launchPermissionMode || undefined,
1385
+ ...preserveCodexSessionProfileOptions(),
1386
+ }
1387
+ : {}),
1388
+ ...(provider === 'claude'
1389
+ ? { claudePermissionMode: record.launchPermissionMode || undefined }
1390
+ : {}),
1391
+ };
1392
+
1393
+ let restartedAgentId = null;
1394
+ try {
1395
+ restartedAgentId = await this.startAgent(
1396
+ command,
1397
+ record.cwd || record.projectWorkspace || null,
1398
+ null,
1399
+ options
1400
+ );
1401
+ } catch (error) {
1402
+ console.warn(
1403
+ `Failed to restore Terminal session ${record.runtimeAgentId || sessionId} after native PTY runtime rotation:`,
1404
+ error && (error.message || error)
1405
+ );
1406
+ continue;
1407
+ }
1408
+ const replacement = restartedAgentId ? this.agents.get(restartedAgentId) : null;
1409
+ if (!replacement) {
1410
+ console.warn(
1411
+ `Failed to restore Terminal session ${record.runtimeAgentId || sessionId} after native PTY runtime rotation`
1412
+ );
1413
+ continue;
1414
+ }
1415
+
1416
+ replacement.pinned = record.pinned === true;
1417
+ replacement.projectOrder = finiteOrder(record.projectOrder);
1418
+ replacement.pinnedOrder = finiteOrder(record.pinnedOrder);
1419
+ replacement.customTitle = record.customTitle || replacement.customTitle || '';
1420
+ replacement.terminalInputReceived = record.terminalInputReceived === true;
1421
+ replacement.attentionSeq = finiteNonNegativeInteger(record.attentionSeq);
1422
+ replacement.readAttentionSeq = finiteNonNegativeInteger(record.readAttentionSeq);
1423
+ replacement.attentionUpdatedAt = finiteNumberOrNull(record.attentionUpdatedAt);
1424
+ replacement.readAttentionAt = finiteNumberOrNull(record.readAttentionAt);
1425
+ replacement.attentionReason = record.attentionReason || '';
1426
+ replacement.attentionOutputSeq = finiteNumberOrNull(record.attentionOutputSeq);
1427
+ replacement.unread = agentAttentionUnread(replacement);
1428
+ this.ensurePersistentAgentSession(replacement);
1429
+ if (sessionKey) liveProviderSessions.add(sessionKey);
1430
+ changed = true;
1431
+ }
1432
+ if (changed) this.emit('update');
1433
+ }
1434
+
843
1435
  async recoverAcpSessions() {
844
1436
  if (!this.acpRuntime || !this.configManager || typeof this.configManager.listAgentSessionRecords !== 'function') return;
845
- for (const record of this.configManager.listAgentSessionRecords()) {
846
- if (!record || record.archived === true || record.agentRuntimeMode !== 'acp') continue;
1437
+ const mainPageOrder = new Map(this.getMainPageSessionKeys().map((key, index) => [key, index]));
1438
+ const records = this.configManager.listAgentSessionRecords()
1439
+ .filter(record => record && record.archived !== true && runtimeKind(record) === 'acp')
1440
+ .sort((left, right) => {
1441
+ const leftOrder = mainPageOrder.get(left.providerSessionKey);
1442
+ const rightOrder = mainPageOrder.get(right.providerSessionKey);
1443
+ return (leftOrder ?? Number.MAX_SAFE_INTEGER) - (rightOrder ?? Number.MAX_SAFE_INTEGER);
1444
+ });
1445
+
1446
+ // Materialize every recoverable row before loading any transcript. Large
1447
+ // Codex histories can take tens of seconds each; creating rows one by one
1448
+ // after every await temporarily leaves later main-page sessions invisible
1449
+ // in both Projects and History.
1450
+ for (const record of records) {
847
1451
  const agentId = String(record.runtimeAgentId || '').trim();
848
1452
  const provider = String(record.providerSessionProvider || record.provider || '').trim();
849
1453
  const sessionId = String(record.providerSessionId || '').trim();
850
- if (!agentId || !sessionId || !['codex', 'claude', 'opencode', 'qoder'].includes(provider)) continue;
851
- let agent = this.agents.get(agentId);
1454
+ if (!agentId || !sessionId || !providerSupportsRuntime(provider, 'acp')) continue;
1455
+ const agent = this.agents.get(agentId);
852
1456
  if (!agent) {
853
- agent = this.recoveredAgentRecord(agentId, record.engine || 'native', record, { status: 'running' });
854
- ensureAgentOrders(agent, Array.from(this.agents.values()));
855
- agent.persistentSessionId = record.id || '';
856
- agent.engineStarted = false;
857
- this.agents.set(agentId, agent);
1457
+ const recoveredAgent = this.recoveredAgentRecord(agentId, record.engine || 'native', record, { status: 'running' });
1458
+ ensureAgentOrders(recoveredAgent, Array.from(this.agents.values()));
1459
+ recoveredAgent.persistentSessionId = record.id || '';
1460
+ recoveredAgent.engineStarted = false;
1461
+ runtimeBindingOf(recoveredAgent, 'acp').state = 'connecting';
1462
+ this.agents.set(agentId, recoveredAgent);
1463
+ void this.refreshAgentWorktree(agentId);
858
1464
  this.lastActivity.set(agentId, Date.now());
859
1465
  }
1466
+ }
1467
+ this.emit('update');
1468
+
1469
+ for (const record of records) {
1470
+ const agentId = String(record.runtimeAgentId || '').trim();
1471
+ const provider = String(record.providerSessionProvider || record.provider || '').trim();
1472
+ const sessionId = String(record.providerSessionId || '').trim();
1473
+ const agent = this.agents.get(agentId);
1474
+ if (!agent || !sessionId || !providerSupportsRuntime(provider, 'acp')) continue;
860
1475
  try {
861
- const executableName = provider === 'qoder' ? 'qodercli' : provider;
1476
+ const executableName = getProviderAdapter(provider).executable;
862
1477
  const executable = resolveAgentExecutable(executableName) || executableName;
863
1478
  const approvalMode = agent.launchPermissionMode || (
864
1479
  provider === 'codex' && this.configManager.getCodexApprovalMode
@@ -872,31 +1487,29 @@ class AgentManager extends EventEmitter {
872
1487
  env: this.buildAgentEnv(agentId, agent),
873
1488
  cwd: agent.cwd,
874
1489
  sessionId,
875
- historyMode: 'load',
1490
+ historyMode: 'checkpoint',
1491
+ providerHomeId: agent.providerHomeId || record.providerHomeId || 'default',
876
1492
  approvalMode,
877
- model: this.configManager.getCodexModel
878
- ? this.configManager.getCodexModel()
879
- : '',
880
- reasoningEffort: this.configManager.getCodexReasoningEffort
881
- ? this.configManager.getCodexReasoningEffort()
882
- : '',
883
- serviceTier: this.configManager.getCodexServiceTier
884
- ? this.configManager.getCodexServiceTier()
885
- : '',
1493
+ // Let Codex resolve its selected Home config and existing session
1494
+ // state instead of applying today's Farming launch defaults.
1495
+ model: 'config',
1496
+ reasoningEffort: 'config',
1497
+ serviceTier: 'config',
886
1498
  });
887
1499
  agent.providerSessionId = prepared.sessionId;
888
1500
  agent.providerSessionTemporary = false;
889
1501
  agent.providerSessionSource = `acp-${prepared.historyMode}`;
890
- agent.agentRuntimeMode = 'acp';
891
- agent.acpState = 'idle';
892
- agent.acpError = '';
1502
+ const runtime = replaceRuntimeBinding(agent, 'acp', runtimeBindingOf(agent, 'acp'));
1503
+ runtime.state = 'idle';
1504
+ runtime.error = '';
893
1505
  agent.status = 'running';
894
1506
  agent.engineStatus = 'running';
895
1507
  agent.engineStarted = false;
896
1508
  this.ensurePersistentAgentSession(agent);
897
1509
  } catch (error) {
898
- agent.acpState = 'error';
899
- agent.acpError = `ACP recovery failed: ${error && (error.message || error)}`;
1510
+ const runtime = runtimeBindingOf(agent, 'acp');
1511
+ runtime.state = 'error';
1512
+ runtime.error = `ACP recovery failed: ${error && (error.message || error)}`;
900
1513
  agent.status = 'stopped';
901
1514
  agent.engineStatus = 'stopped';
902
1515
  agent.engineStarted = false;
@@ -911,7 +1524,7 @@ class AgentManager extends EventEmitter {
911
1524
  if (!this.codexAppServerRuntime || !this.configManager || typeof this.configManager.listAgentSessionRecords !== 'function') return;
912
1525
  const records = this.configManager.listAgentSessionRecords();
913
1526
  for (const record of records) {
914
- if (!record || record.archived === true || record.codexRuntimeMode !== 'app-server') continue;
1527
+ if (!record || record.archived === true || runtimeKind(record) !== 'app-server') continue;
915
1528
  const agentId = String(record.runtimeAgentId || '').trim();
916
1529
  const threadId = String(record.codexAppServerThreadId || record.providerSessionId || '').trim();
917
1530
  const home = String(record.codexAppServerHomePath || '').trim();
@@ -924,16 +1537,7 @@ class AgentManager extends EventEmitter {
924
1537
  }
925
1538
  if (agent) {
926
1539
  ensureAgentOrders(agent, Array.from(this.agents.values()));
927
- agent.codexRuntimeMode = 'cli';
928
- agent.codexAppServerHomePath = '';
929
- agent.codexAppServerState = '';
930
- agent.codexAppServerEndpoint = '';
931
- agent.codexAppServerTurnId = '';
932
- agent.codexAppServerError = '';
933
- agent.codexAppServerPendingRequestId = '';
934
- agent.codexAppServerPendingRequestMethod = '';
935
- agent.codexAppServerPendingRequest = null;
936
- agent.codexCliObserverDeferred = false;
1540
+ replaceRuntimeBinding(agent, 'terminal');
937
1541
  this.ensurePersistentAgentSession(agent);
938
1542
  this.updateEngineProviderSessionMetadata(agent);
939
1543
  }
@@ -947,6 +1551,7 @@ class AgentManager extends EventEmitter {
947
1551
  agent.persistentSessionId = record.id || '';
948
1552
  agent.engineStarted = false;
949
1553
  this.agents.set(agentId, agent);
1554
+ void this.refreshAgentWorktree(agentId);
950
1555
  this.lastActivity.set(agentId, Date.now());
951
1556
  }
952
1557
  try {
@@ -961,24 +1566,26 @@ class AgentManager extends EventEmitter {
961
1566
  executable,
962
1567
  env: this.buildAgentEnv(agentId, agent),
963
1568
  cwd: agent.cwd,
964
- workspaceRoot: agent.projectWorkspace || agent.cwd,
1569
+ workspaceRoot: effectiveAgentWorkspaceRoot(agent),
965
1570
  approvalMode: agent.launchPermissionMode || 'approve',
966
1571
  });
967
- agent.codexAppServerThreadId = binding.threadId;
968
- agent.codexAppServerState = 'idle';
969
- agent.codexAppServerError = '';
1572
+ const runtime = runtimeBindingOf(agent, 'app-server');
1573
+ runtime.threadId = binding.threadId;
1574
+ runtime.state = 'idle';
1575
+ runtime.error = '';
970
1576
  // App Server owns this runtime. It deliberately has no CLI/PTy
971
1577
  // observer, so recovery cannot create a second reader or writer.
972
1578
  agent.status = 'running';
973
1579
  agent.engineStatus = 'running';
974
1580
  agent.engineStarted = false;
975
- agent.codexCliObserverDeferred = false;
1581
+ runtime.observerDeferred = false;
976
1582
  agent.exitedAt = null;
977
- this.activateProviderSessionTracking(agentId);
1583
+ this.providerSessionService.activate(agentId);
978
1584
  this.ensurePersistentAgentSession(agent);
979
1585
  } catch (error) {
980
- agent.codexAppServerError = `Codex App Server recovery failed: ${error && (error.message || error)}`;
981
- agent.codexAppServerState = 'error';
1586
+ const runtime = runtimeBindingOf(agent, 'app-server');
1587
+ runtime.error = `Codex App Server recovery failed: ${error && (error.message || error)}`;
1588
+ runtime.state = 'error';
982
1589
  agent.status = 'stopped';
983
1590
  agent.engineStatus = 'stopped';
984
1591
  agent.engineStarted = false;
@@ -1007,6 +1614,15 @@ class AgentManager extends EventEmitter {
1007
1614
  const wantsMain = metadata.wantsMain === true;
1008
1615
  const providerSessionProvider = metadata.providerSessionProvider || metadata.provider || '';
1009
1616
  const recoverCodexAppServer = shouldRecoverAsCodexAppServer(metadata);
1617
+ const persistedRuntimeKind = runtimeKind(metadata);
1618
+ const recoveredRuntimeKind = recoverCodexAppServer
1619
+ ? 'app-server'
1620
+ : (persistedRuntimeKind === 'app-server' ? 'terminal' : persistedRuntimeKind);
1621
+ const runtimeBinding = runtimeBindingFor(recoveredRuntimeKind, {
1622
+ ...metadata,
1623
+ codexAppServerThreadId: metadata.codexAppServerThreadId || metadata.providerSessionId || '',
1624
+ codexCliObserverDeferred: false,
1625
+ });
1010
1626
  return {
1011
1627
  id: agentId,
1012
1628
  command: metadata.forkCommand || metadata.command || '',
@@ -1042,34 +1658,14 @@ class AgentManager extends EventEmitter {
1042
1658
  providerSessionSource: metadata.providerSessionSource || '',
1043
1659
  providerSessionResolvedAt: metadata.providerSessionResolvedAt || null,
1044
1660
  providerSessionTitle: metadata.providerSessionTitle || '',
1661
+ providerSessionWorkspace: metadata.providerSessionWorkspace || '',
1045
1662
  terminalInputReceived: metadata.terminalInputReceived === true,
1046
1663
  // Older persisted sessions predate App Server mode. Also, a Codex
1047
1664
  // App Server record without its isolated runtime home is not actually
1048
1665
  // attachable; recover it as terminal-owned CLI instead of leaving the
1049
1666
  // UI in a split state where the pane is terminal but Composer sends to
1050
1667
  // App Server.
1051
- codexRuntimeMode: recoverCodexAppServer ? 'app-server' : 'cli',
1052
- agentRuntimeMode: metadata.agentRuntimeMode === 'acp' ? 'acp' : (metadata.agentRuntimeMode === 'json' ? 'json' : 'terminal'),
1053
- acpState: metadata.agentRuntimeMode === 'acp' ? (metadata.acpState || '') : '',
1054
- acpError: metadata.agentRuntimeMode === 'acp' ? (metadata.acpError || '') : '',
1055
- acpStopReason: metadata.agentRuntimeMode === 'acp' ? (metadata.acpStopReason || '') : '',
1056
- acpPendingPermission: null,
1057
- acpPendingPermissions: [],
1058
- acpPendingElicitation: null,
1059
- acpPendingElicitations: [],
1060
- acpActiveElicitations: [],
1061
- acpSessionUpdatedAt: metadata.agentRuntimeMode === 'acp' ? (metadata.acpSessionUpdatedAt || '') : '',
1062
- acpSessionRevision: metadata.agentRuntimeMode === 'acp' ? (Number(metadata.acpSessionRevision) || 0) : 0,
1063
- codexAppServerHomePath: recoverCodexAppServer ? metadata.codexAppServerHomePath : '',
1064
- codexAppServerState: recoverCodexAppServer ? (metadata.codexAppServerState || '') : '',
1065
- codexAppServerEndpoint: recoverCodexAppServer ? (metadata.codexAppServerEndpoint || '') : '',
1066
- codexAppServerThreadId: metadata.codexAppServerThreadId || metadata.providerSessionId || '',
1067
- codexAppServerTurnId: recoverCodexAppServer ? (metadata.codexAppServerTurnId || '') : '',
1068
- codexAppServerError: recoverCodexAppServer ? (metadata.codexAppServerError || '') : '',
1069
- codexAppServerPendingRequestId: recoverCodexAppServer ? (metadata.codexAppServerPendingRequestId || '') : '',
1070
- codexAppServerPendingRequestMethod: recoverCodexAppServer ? (metadata.codexAppServerPendingRequestMethod || '') : '',
1071
- codexAppServerPendingRequest: recoverCodexAppServer ? (metadata.codexAppServerPendingRequest || null) : null,
1072
- codexCliObserverDeferred: false,
1668
+ runtimeBinding,
1073
1669
  forkedFromProviderSessionId: metadata.forkedFromProviderSessionId || '',
1074
1670
  restartedFromAgentId: metadata.restartedFromAgentId || '',
1075
1671
  restartedFromAgentIds: Array.isArray(metadata.restartedFromAgentIds)
@@ -1095,7 +1691,10 @@ class AgentManager extends EventEmitter {
1095
1691
  attentionUpdatedAt: finiteNumberOrNull(metadata.attentionUpdatedAt),
1096
1692
  readAttentionAt: finiteNumberOrNull(metadata.readAttentionAt),
1097
1693
  attentionReason: metadata.attentionReason || '',
1694
+ attentionOutputEpoch: metadata.attentionOutputEpoch || '',
1098
1695
  attentionOutputSeq: finiteNumberOrNull(metadata.attentionOutputSeq),
1696
+ readOutputEpoch: metadata.readOutputEpoch || '',
1697
+ readOutputSeq: finiteNumberOrNull(metadata.readOutputSeq),
1099
1698
  unread: finiteNonNegativeInteger(metadata.attentionSeq) > finiteNonNegativeInteger(metadata.readAttentionSeq),
1100
1699
  archived: false,
1101
1700
  archivedAt: null,
@@ -1104,6 +1703,8 @@ class AgentManager extends EventEmitter {
1104
1703
  engineStarted: true,
1105
1704
  engineStatus: state.status || 'running',
1106
1705
  startedAt: state.startedAt || metadata.startedAt || Date.now(),
1706
+ runtimeEpoch: typeof state.runtimeEpoch === 'string' ? state.runtimeEpoch : '',
1707
+ stateRevision: finiteNumberOrNull(state.stateRevision),
1107
1708
  lastEngineOutputAt: Date.now(),
1108
1709
  lastOutputSeq: finiteNumberOrNull(state.outputSeq),
1109
1710
  attentionRequiresNewOutput: true,
@@ -1140,10 +1741,6 @@ class AgentManager extends EventEmitter {
1140
1741
  return Number.isFinite(startedAt) && Date.now() - startedAt < MISSING_ENGINE_SESSION_STARTUP_GRACE_MS;
1141
1742
  }
1142
1743
 
1143
- providerSessionKey(provider, sessionId, providerHomeId = '') {
1144
- return provider && sessionId ? mainPageAgentSessionKey(provider, sessionId, providerHomeId) : '';
1145
- }
1146
-
1147
1744
  getMainPageSessionKeys() {
1148
1745
  if (this.configManager && typeof this.configManager.getMainPageSessionKeys === 'function') {
1149
1746
  return this.configManager.getMainPageSessionKeys();
@@ -1177,19 +1774,6 @@ class AgentManager extends EventEmitter {
1177
1774
  return persistentSessionId;
1178
1775
  }
1179
1776
 
1180
- currentProviderSessionIds(provider, excludedAgentId = '', providerHomeId = 'default') {
1181
- const ids = new Set();
1182
- const normalizedHomeId = String(providerHomeId || 'default').trim() || 'default';
1183
- for (const agent of this.agents.values()) {
1184
- if (!agent || agent.id === excludedAgentId) continue;
1185
- if (agent.providerSessionProvider !== provider) continue;
1186
- if ((String(agent.providerHomeId || 'default').trim() || 'default') !== normalizedHomeId) continue;
1187
- if (!agent.providerSessionId || agent.providerSessionTemporary === true) continue;
1188
- ids.add(agent.providerSessionId);
1189
- }
1190
- return ids;
1191
- }
1192
-
1193
1777
  rememberMainPageProviderSession(agent) {
1194
1778
  if (!agent || agent.wantsMain) return;
1195
1779
  if (!agent.providerSessionProvider || !agent.providerSessionId || agent.providerSessionTemporary === true) return;
@@ -1197,7 +1781,7 @@ class AgentManager extends EventEmitter {
1197
1781
  return;
1198
1782
  }
1199
1783
 
1200
- const sessionKey = this.providerSessionKey(agent.providerSessionProvider, agent.providerSessionId, agent.providerHomeId || '');
1784
+ const sessionKey = mainPageAgentSessionKey(agent.providerSessionProvider, agent.providerSessionId, agent.providerHomeId || '');
1201
1785
  if (!sessionKey) return;
1202
1786
  const currentKeys = this.getMainPageSessionKeys();
1203
1787
  if (currentKeys[0] === sessionKey) {
@@ -1230,23 +1814,7 @@ class AgentManager extends EventEmitter {
1230
1814
  providerSessionResolvedAt: agent.providerSessionResolvedAt || null,
1231
1815
  providerSessionTitle: agent.providerSessionTitle || '',
1232
1816
  terminalInputReceived: agent.terminalInputReceived === true,
1233
- codexRuntimeMode: agent.codexRuntimeMode || '',
1234
- agentRuntimeMode: agent.agentRuntimeMode || 'terminal',
1235
- acpState: agent.acpState || '',
1236
- acpError: agent.acpError || '',
1237
- acpStopReason: agent.acpStopReason || '',
1238
- acpSessionUpdatedAt: agent.acpSessionUpdatedAt || '',
1239
- acpSessionRevision: Number(agent.acpSessionRevision) || 0,
1240
- codexAppServerHomePath: agent.codexAppServerHomePath || '',
1241
- codexAppServerState: agent.codexAppServerState || '',
1242
- codexAppServerEndpoint: agent.codexAppServerEndpoint || '',
1243
- codexAppServerThreadId: agent.codexAppServerThreadId || '',
1244
- codexAppServerTurnId: agent.codexAppServerTurnId || '',
1245
- codexAppServerError: agent.codexAppServerError || '',
1246
- codexAppServerPendingRequestId: agent.codexAppServerPendingRequestId || '',
1247
- codexAppServerPendingRequestMethod: agent.codexAppServerPendingRequestMethod || '',
1248
- codexAppServerPendingRequest: agent.codexAppServerPendingRequest || null,
1249
- codexCliObserverDeferred: agent.codexCliObserverDeferred === true,
1817
+ ...legacyRuntimeMetadata(agent),
1250
1818
  forkedFromProviderSessionId: agent.forkedFromProviderSessionId || '',
1251
1819
  launchPermissionMode: agent.launchPermissionMode || '',
1252
1820
  attentionSeq: finiteNonNegativeInteger(agent.attentionSeq),
@@ -1254,7 +1822,10 @@ class AgentManager extends EventEmitter {
1254
1822
  attentionUpdatedAt: finiteNumberOrNull(agent.attentionUpdatedAt),
1255
1823
  readAttentionAt: finiteNumberOrNull(agent.readAttentionAt),
1256
1824
  attentionReason: agent.attentionReason || '',
1825
+ attentionOutputEpoch: agent.attentionOutputEpoch || '',
1257
1826
  attentionOutputSeq: finiteNumberOrNull(agent.attentionOutputSeq),
1827
+ readOutputEpoch: agent.readOutputEpoch || '',
1828
+ readOutputSeq: finiteNumberOrNull(agent.readOutputSeq),
1258
1829
  projectOrder: finiteOrder(agent.projectOrder),
1259
1830
  pinnedOrder: finiteOrder(agent.pinnedOrder),
1260
1831
  })).catch((error) => {
@@ -1262,33 +1833,6 @@ class AgentManager extends EventEmitter {
1262
1833
  });
1263
1834
  }
1264
1835
 
1265
- activateProviderSessionTracking(agentId) {
1266
- const agent = this.agents.get(agentId);
1267
- if (!agent || !agent.providerSessionProvider || !agent.providerSessionId) return;
1268
-
1269
- if (agent.providerSessionProvider === 'codex' && agent.providerSessionTemporary === true) {
1270
- this.observeAgentStateChange(agentId, { force: true });
1271
- return;
1272
- }
1273
-
1274
- this.stopCodexProviderSessionResolver(agentId);
1275
- this.updateEngineProviderSessionMetadata(agent);
1276
- this.rememberMainPageProviderSession(agent);
1277
- this.attemptProviderSessionTitleResolution(agentId, { force: true }).catch((error) => {
1278
- console.warn('Failed to resolve provider session title:', error && (error.message || error));
1279
- });
1280
- }
1281
-
1282
- stopCodexProviderSessionResolver(agentId) {
1283
- this.codexSessionResolveInFlight.delete(agentId);
1284
- this.codexSessionResolveLastAttemptAt.delete(agentId);
1285
- }
1286
-
1287
- stopProviderSessionTitleResolver(agentId) {
1288
- this.providerSessionTitleResolveInFlight.delete(agentId);
1289
- this.providerSessionTitleResolveLastAttemptAt.delete(agentId);
1290
- }
1291
-
1292
1836
  isAgentAttentionTurnActive(agent) {
1293
1837
  if (!agent) return false;
1294
1838
  if (agent.status === 'pending') return true;
@@ -1333,8 +1877,16 @@ class AgentManager extends EventEmitter {
1333
1877
  agent.attentionSeq = nextSeq;
1334
1878
  agent.attentionUpdatedAt = now;
1335
1879
  agent.attentionReason = reason;
1880
+ agent.attentionOutputEpoch = typeof agent.runtimeEpoch === 'string' ? agent.runtimeEpoch : '';
1336
1881
  agent.attentionOutputSeq = Number.isFinite(agent.lastOutputSeq) ? agent.lastOutputSeq : null;
1337
- if (agent.attentionAutoReadNext === true) {
1882
+ const attentionOutputAlreadyRead = Boolean(
1883
+ agent.attentionOutputEpoch
1884
+ && agent.attentionOutputEpoch === agent.readOutputEpoch
1885
+ && agent.attentionOutputSeq !== null
1886
+ && Number.isFinite(agent.readOutputSeq)
1887
+ && agent.attentionOutputSeq <= agent.readOutputSeq
1888
+ );
1889
+ if (agent.attentionAutoReadNext === true || attentionOutputAlreadyRead) {
1338
1890
  agent.attentionAutoReadNext = false;
1339
1891
  agent.readAttentionSeq = nextSeq;
1340
1892
  agent.readAttentionAt = now;
@@ -1351,7 +1903,7 @@ class AgentManager extends EventEmitter {
1351
1903
  };
1352
1904
  }
1353
1905
 
1354
- markAgentReadCursor(agentId, readAttentionSeq) {
1906
+ markAgentReadCursor(agentId, readAttentionSeq, options = {}) {
1355
1907
  const agent = this.agents.get(agentId);
1356
1908
  if (!agent) {
1357
1909
  return { error: 'Agent not found' };
@@ -1370,7 +1922,7 @@ class AgentManager extends EventEmitter {
1370
1922
  if (changed) {
1371
1923
  this.ensurePersistentAgentSession(agent);
1372
1924
  this.updateEngineProviderSessionMetadata(agent);
1373
- this.emit('update');
1925
+ if (options.emitUpdate !== false) this.emit('update');
1374
1926
  }
1375
1927
  return {
1376
1928
  agentId,
@@ -1381,6 +1933,48 @@ class AgentManager extends EventEmitter {
1381
1933
  };
1382
1934
  }
1383
1935
 
1936
+ markAgentReadOutputCut(agentId, runtimeEpoch, outputSeq) {
1937
+ const agent = this.agents.get(agentId);
1938
+ if (!agent) {
1939
+ return { error: 'Agent not found' };
1940
+ }
1941
+
1942
+ const currentRuntimeEpoch = typeof agent.runtimeEpoch === 'string' ? agent.runtimeEpoch : '';
1943
+ const requestedRuntimeEpoch = typeof runtimeEpoch === 'string' ? runtimeEpoch : '';
1944
+ const currentOutputSeq = finiteNumberOrNull(agent.lastOutputSeq);
1945
+ if (
1946
+ !currentRuntimeEpoch
1947
+ || requestedRuntimeEpoch !== currentRuntimeEpoch
1948
+ || currentOutputSeq === null
1949
+ || !Number.isFinite(outputSeq)
1950
+ ) {
1951
+ return {
1952
+ agentId,
1953
+ readOutputEpoch: typeof agent.readOutputEpoch === 'string' ? agent.readOutputEpoch : '',
1954
+ readOutputSeq: finiteNumberOrNull(agent.readOutputSeq),
1955
+ changed: false,
1956
+ };
1957
+ }
1958
+
1959
+ const requestedOutputSeq = Math.max(0, Math.floor(outputSeq));
1960
+ const nextOutputSeq = Math.min(currentOutputSeq, requestedOutputSeq);
1961
+ const previousOutputSeq = agent.readOutputEpoch === currentRuntimeEpoch
1962
+ ? finiteNumberOrNull(agent.readOutputSeq)
1963
+ : null;
1964
+ const readOutputSeq = previousOutputSeq === null
1965
+ ? nextOutputSeq
1966
+ : Math.max(previousOutputSeq, nextOutputSeq);
1967
+ const changed = agent.readOutputEpoch !== currentRuntimeEpoch || previousOutputSeq !== readOutputSeq;
1968
+ agent.readOutputEpoch = currentRuntimeEpoch;
1969
+ agent.readOutputSeq = readOutputSeq;
1970
+ return {
1971
+ agentId,
1972
+ readOutputEpoch: agent.readOutputEpoch,
1973
+ readOutputSeq: agent.readOutputSeq,
1974
+ changed,
1975
+ };
1976
+ }
1977
+
1384
1978
  markAgentUnreadCursor(agentId) {
1385
1979
  const agent = this.agents.get(agentId);
1386
1980
  if (!agent) {
@@ -1405,182 +1999,47 @@ class AgentManager extends EventEmitter {
1405
1999
  this.updateEngineProviderSessionMetadata(agent);
1406
2000
  this.emit('update');
1407
2001
  }
1408
- return {
1409
- agentId,
1410
- attentionSeq: agent.attentionSeq,
1411
- readAttentionSeq: agent.readAttentionSeq,
1412
- unread: agent.unread,
1413
- changed,
1414
- };
1415
- }
1416
-
1417
- observeAgentStateChange(agentId, options = {}) {
1418
- this.attemptCodexProviderSessionResolution(agentId, options).catch((error) => {
1419
- console.warn('Failed to resolve Codex provider session:', error && (error.message || error));
1420
- });
1421
- this.attemptProviderSessionTitleResolution(agentId, options).catch((error) => {
1422
- console.warn('Failed to resolve provider session title:', error && (error.message || error));
1423
- });
1424
- }
1425
-
1426
- attemptCodexProviderSessionResolution(agentId, options = {}) {
1427
- const agent = this.agents.get(agentId);
1428
- if (!agent || agent.providerSessionProvider !== 'codex' || agent.providerSessionTemporary !== true) {
1429
- this.stopCodexProviderSessionResolver(agentId);
1430
- return Promise.resolve(false);
1431
- }
1432
-
1433
- const inFlight = this.codexSessionResolveInFlight.get(agentId);
1434
- if (inFlight) return inFlight;
1435
-
1436
- const now = Date.now();
1437
- const lastAttemptAt = this.codexSessionResolveLastAttemptAt.get(agentId) || 0;
1438
- if (options.force !== true && now - lastAttemptAt < CODEX_PROVIDER_SESSION_RESOLVE_COOLDOWN_MS) {
1439
- return Promise.resolve(false);
1440
- }
1441
- this.codexSessionResolveLastAttemptAt.set(agentId, now);
1442
-
1443
- const attempt = this.findCodexSessionForTemporaryAgent(agent)
1444
- .then((session) => {
1445
- if (!session || !session.id) return false;
1446
- return this.resolveProviderSession(agentId, {
1447
- provider: 'codex',
1448
- sessionId: session.id,
1449
- source: 'codex-rollout',
1450
- title: session.title || '',
1451
- });
1452
- })
1453
- .catch(() => false)
1454
- .finally(() => {
1455
- if (this.codexSessionResolveInFlight.get(agentId) === attempt) {
1456
- this.codexSessionResolveInFlight.delete(agentId);
1457
- }
1458
- });
1459
- this.codexSessionResolveInFlight.set(agentId, attempt);
1460
- return attempt;
1461
- }
1462
-
1463
- attemptProviderSessionTitleResolution(agentId, options = {}) {
1464
- const agent = this.agents.get(agentId);
1465
- if (
1466
- !agent
1467
- || !agent.providerSessionProvider
1468
- || !agent.providerSessionId
1469
- || agent.providerSessionTemporary === true
1470
- || isTemporaryProviderSessionId(agent.providerSessionId)
1471
- || String(agent.providerSessionTitle || '').trim()
1472
- ) {
1473
- this.stopProviderSessionTitleResolver(agentId);
1474
- return Promise.resolve(false);
1475
- }
1476
-
1477
- const inFlight = this.providerSessionTitleResolveInFlight.get(agentId);
1478
- if (inFlight) return inFlight;
1479
-
1480
- const now = Date.now();
1481
- const lastAttemptAt = this.providerSessionTitleResolveLastAttemptAt.get(agentId) || 0;
1482
- if (options.force !== true && now - lastAttemptAt < PROVIDER_SESSION_TITLE_RESOLVE_COOLDOWN_MS) {
1483
- return Promise.resolve(false);
1484
- }
1485
- this.providerSessionTitleResolveLastAttemptAt.set(agentId, now);
1486
-
1487
- const provider = agent.providerSessionProvider;
1488
- const sessionId = agent.providerSessionId;
1489
- const attempt = findAgentSession(provider, sessionId, { limit: 200, providerLimit: 200, providerHomeId: agent.providerHomeId || 'default', providerHomes: this.configManager && this.configManager.getSettings ? this.configManager.getSettings().agentHomes : undefined })
1490
- .then((session) => {
1491
- const title = String(session && session.title || '').trim().slice(0, 160);
1492
- if (!title) return false;
1493
-
1494
- const current = this.agents.get(agentId);
1495
- if (
1496
- !current
1497
- || current.providerSessionProvider !== provider
1498
- || current.providerSessionId !== sessionId
1499
- || current.providerSessionTemporary === true
1500
- || String(current.providerSessionTitle || '').trim()
1501
- ) {
1502
- return false;
1503
- }
1504
-
1505
- current.providerSessionTitle = title;
1506
- this.ensurePersistentAgentSession(current);
1507
- this.updateEngineProviderSessionMetadata(current);
1508
- this.rememberMainPageProviderSession(current);
1509
- this.emit('provider-session-updated', {
1510
- agentId,
1511
- provider,
1512
- sessionId,
1513
- title,
1514
- temporary: false,
1515
- });
1516
- this.emit('update');
1517
- return true;
1518
- })
1519
- .catch(() => false)
1520
- .finally(() => {
1521
- if (this.providerSessionTitleResolveInFlight.get(agentId) === attempt) {
1522
- this.providerSessionTitleResolveInFlight.delete(agentId);
1523
- }
1524
- });
1525
- this.providerSessionTitleResolveInFlight.set(agentId, attempt);
1526
- return attempt;
1527
- }
1528
-
1529
- async findCodexSessionForTemporaryAgent(agent) {
1530
- const sessions = await listCodexSessions({ codexHome: agent.providerHomePath || undefined, limit: 100, scanLimit: 1000 });
1531
- const workspace = normalizePathValue(agent.projectWorkspace || agent.cwd);
1532
- const startedAt = Number(agent.startedAt) || 0;
1533
- const claimedSessionIds = this.currentProviderSessionIds('codex', agent.id, agent.providerHomeId || 'default');
1534
- const candidates = sessions
1535
- .filter(session => {
1536
- if (!session || !session.id || claimedSessionIds.has(session.id)) return false;
1537
- const sessionWorkspace = normalizePathValue(session.workspace || session.cwd);
1538
- if (workspace && !sessionWorkspace) return false;
1539
- if (workspace && workspace !== sessionWorkspace) return false;
1540
- const sessionTime = timestampMs(session.createdAt || session.updatedAt);
1541
- if (!sessionTime || !startedAt) return true;
1542
- return sessionTime >= startedAt - CODEX_PROVIDER_SESSION_MATCH_GRACE_MS;
1543
- })
1544
- .sort((a, b) => {
1545
- const aTime = timestampMs(a.createdAt || a.updatedAt);
1546
- const bTime = timestampMs(b.createdAt || b.updatedAt);
1547
- const aDistance = startedAt && aTime ? Math.abs(aTime - startedAt) : Number.MAX_SAFE_INTEGER;
1548
- const bDistance = startedAt && bTime ? Math.abs(bTime - startedAt) : Number.MAX_SAFE_INTEGER;
1549
- if (aDistance !== bDistance) return aDistance - bDistance;
1550
- return bTime - aTime;
1551
- });
1552
-
1553
- return candidates[0] || null;
2002
+ return {
2003
+ agentId,
2004
+ attentionSeq: agent.attentionSeq,
2005
+ readAttentionSeq: agent.readAttentionSeq,
2006
+ unread: agent.unread,
2007
+ changed,
2008
+ };
1554
2009
  }
1555
2010
 
1556
- resolveProviderSession(agentId, { provider, sessionId, source, title }) {
2011
+ async refreshAgentWorktree(agentId, workspaceCandidate = '') {
1557
2012
  const agent = this.agents.get(agentId);
1558
- if (!agent || !provider || !sessionId || isTemporaryProviderSessionId(sessionId)) return false;
1559
-
1560
- const previousSessionId = agent.providerSessionId || '';
1561
- const providerSessionTitle = String(title || '').trim().slice(0, 160);
1562
- agent.providerSessionProvider = provider;
1563
- agent.providerSessionId = sessionId;
1564
- agent.providerSessionKey = this.providerSessionKey(provider, sessionId, agent.providerHomeId || '');
1565
- agent.providerSessionTemporary = false;
1566
- agent.providerSessionSource = source || agent.providerSessionSource || '';
1567
- agent.providerSessionResolvedAt = Date.now();
1568
- if (providerSessionTitle) {
1569
- agent.providerSessionTitle = providerSessionTitle;
1570
- }
1571
-
1572
- this.stopCodexProviderSessionResolver(agentId);
1573
- this.stopProviderSessionTitleResolver(agentId);
1574
- this.ensurePersistentAgentSession(agent);
1575
- this.updateEngineProviderSessionMetadata(agent);
1576
- this.rememberMainPageProviderSession(agent);
1577
- this.emit('provider-session-updated', {
1578
- agentId,
1579
- provider,
1580
- sessionId,
1581
- previousSessionId,
1582
- temporary: false,
1583
- });
2013
+ if (!agent || agent.isMain || agent.wantsMain) return false;
2014
+ const candidate = normalizePathValue(
2015
+ workspaceCandidate
2016
+ || agent.providerSessionWorkspace
2017
+ || agent.shellCwd
2018
+ || agent.projectWorkspace
2019
+ || agent.cwd
2020
+ );
2021
+ if (!candidate) return false;
2022
+
2023
+ const generation = (this.agentWorktreeResolveGeneration.get(agentId) || 0) + 1;
2024
+ this.agentWorktreeResolveGeneration.set(agentId, generation);
2025
+ const baseWorkspace = normalizePathValue(agent.projectWorkspace || agent.cwd);
2026
+ const [info, baseInfo] = await Promise.all([
2027
+ inspectGitWorktree(candidate),
2028
+ inspectGitWorktree(baseWorkspace),
2029
+ ]);
2030
+ if (this.agentWorktreeResolveGeneration.get(agentId) !== generation) return false;
2031
+
2032
+ const current = this.agents.get(agentId);
2033
+ if (!current) return false;
2034
+ const nextWorktree = info
2035
+ && baseInfo
2036
+ && info.commonDir === baseInfo.commonDir
2037
+ ? info
2038
+ : null;
2039
+ const previousProjection = JSON.stringify(publicAgentGitWorktree(current));
2040
+ current.gitWorktree = nextWorktree;
2041
+ const nextProjection = JSON.stringify(publicAgentGitWorktree(current));
2042
+ if (previousProjection === nextProjection) return false;
1584
2043
  this.emit('update');
1585
2044
  return true;
1586
2045
  }
@@ -1593,7 +2052,18 @@ class AgentManager extends EventEmitter {
1593
2052
  }
1594
2053
 
1595
2054
  this.lastActivityUpdate.set(sessionId, now);
1596
- this.emit('update');
2055
+ const agent = this.agents.get(sessionId);
2056
+ if (!agent) return;
2057
+ const isMain = this.isMainAgentRecord(sessionId, agent);
2058
+ const lastActivity = this.lastActivity.get(sessionId) || now;
2059
+ this.emit('agent-activity', {
2060
+ agentId: sessionId,
2061
+ lastActivity,
2062
+ activityLevel: isMain ? 'warm' : this.calculateActivityLevel(lastActivity, now),
2063
+ attentionScore: isMain ? 0 : this.calculateAttentionScore(sessionId, now),
2064
+ isZombie: isMain ? false : this.isZombie(sessionId, now),
2065
+ usageRate: this.getAgentUsageRate(sessionId, { now }),
2066
+ });
1597
2067
  }
1598
2068
 
1599
2069
  updateAgentSessionTitle(agent, title) {
@@ -1688,7 +2158,7 @@ class AgentManager extends EventEmitter {
1688
2158
  env.FARMING_IS_MAIN_AGENT = agent.wantsMain ? '1' : '0';
1689
2159
  env.FARMING_SKILLS_COMMAND = 'farming skills';
1690
2160
  env.FARMING_MAIN_WORKSPACE = agent.mainWorkspace || '';
1691
- env.FARMING_PROJECT_WORKSPACE = agent.projectWorkspace || '';
2161
+ env.FARMING_PROJECT_WORKSPACE = effectiveAgentWorkspaceRoot(agent);
1692
2162
 
1693
2163
  if (agent.parentAgentId) {
1694
2164
  env.FARMING_PARENT_AGENT_ID = agent.parentAgentId;
@@ -1710,10 +2180,12 @@ class AgentManager extends EventEmitter {
1710
2180
  }
1711
2181
  if (agent.providerHomePath) {
1712
2182
  const provider = agent.providerSessionProvider || agentHomeProviderForProgram(agent.forkCommand || agent.command);
1713
- if (provider === 'codex') env.CODEX_HOME = agent.codexAppServerHomePath || agent.providerHomePath;
1714
- if (provider === 'claude') env.CLAUDE_CONFIG_DIR = agent.providerHomePath;
1715
- if (provider === 'opencode') env.OPENCODE_CONFIG_DIR = agent.providerHomePath;
1716
- if (provider === 'qoder') env.QODER_CONFIG_DIR = agent.providerHomePath;
2183
+ const appServerHome = runtimeBindingOf(agent, 'app-server')?.homePath || '';
2184
+ applyProviderHomeEnvironment(
2185
+ env,
2186
+ provider,
2187
+ provider === 'codex' ? (appServerHome || agent.providerHomePath) : agent.providerHomePath
2188
+ );
1717
2189
  }
1718
2190
 
1719
2191
  return env;
@@ -1825,21 +2297,22 @@ class AgentManager extends EventEmitter {
1825
2297
  clearInterval(this.heartbeatInterval);
1826
2298
  this.heartbeatInterval = null;
1827
2299
  }
1828
- this.codexSessionResolveInFlight.clear();
1829
- this.codexSessionResolveLastAttemptAt.clear();
1830
- this.providerSessionTitleResolveInFlight.clear();
1831
- this.providerSessionTitleResolveLastAttemptAt.clear();
2300
+ this.providerSessionService.dispose();
2301
+ this.agentWorktreeResolveGeneration.clear();
1832
2302
  this.permissionRestartInFlight.clear();
1833
2303
  this.runtimeRestartInFlight.clear();
1834
2304
  this.permissionRestartSuppressedAgentIds.clear();
2305
+ this.pendingResizeByAgent.clear();
2306
+ this.resizeDrains.clear();
1835
2307
  this.inputQueues.clear();
2308
+ this.codexTerminalProfileQueues.clear();
1836
2309
  if (this.codexAppServerRuntime && typeof this.codexAppServerRuntime.dispose === 'function') {
1837
2310
  this.codexAppServerRuntime.dispose();
1838
2311
  }
1839
2312
  if (this.jsonCliRuntime) {
1840
2313
  for (const agentId of this.agents.keys()) this.jsonCliRuntime.unregisterAgent(agentId);
1841
2314
  }
1842
- if (this.acpRuntime && typeof this.acpRuntime.dispose === 'function') this.acpRuntime.dispose();
2315
+ if (this.acpRuntime && typeof this.acpRuntime.dispose === 'function') await this.acpRuntime.dispose();
1843
2316
  if (this.engineBridge && typeof this.engineBridge.dispose === 'function') {
1844
2317
  await this.engineBridge.dispose({
1845
2318
  preserveHost: options.preserveTerminalHost === true,
@@ -1867,6 +2340,7 @@ class AgentManager extends EventEmitter {
1867
2340
  forkCommand: agent.forkCommand,
1868
2341
  cwd: agent.cwd,
1869
2342
  projectWorkspace: agent.projectWorkspace || '',
2343
+ gitWorktree: publicAgentGitWorktree(agent),
1870
2344
  mainWorkspace: agent.mainWorkspace || '',
1871
2345
  wantsMain: agent.wantsMain === true,
1872
2346
  category: agent.category,
@@ -1884,31 +2358,9 @@ class AgentManager extends EventEmitter {
1884
2358
  providerSessionSource: agent.providerSessionSource,
1885
2359
  providerSessionResolvedAt: agent.providerSessionResolvedAt,
1886
2360
  providerSessionTitle: agent.providerSessionTitle,
2361
+ providerSessionWorkspace: agent.providerSessionWorkspace || '',
1887
2362
  terminalInputReceived: agent.terminalInputReceived === true,
1888
- codexRuntimeMode: agent.codexRuntimeMode,
1889
- agentRuntimeMode: agent.agentRuntimeMode || 'terminal',
1890
- jsonCliState: agent.jsonCliState || '',
1891
- jsonCliError: agent.jsonCliError || '',
1892
- acpState: agent.acpState || '',
1893
- acpError: agent.acpError || '',
1894
- acpStopReason: agent.acpStopReason || '',
1895
- acpPendingPermission: agent.acpPendingPermission || null,
1896
- acpPendingPermissions: Array.isArray(agent.acpPendingPermissions) ? agent.acpPendingPermissions : [],
1897
- acpPendingElicitation: agent.acpPendingElicitation || null,
1898
- acpPendingElicitations: Array.isArray(agent.acpPendingElicitations) ? agent.acpPendingElicitations : [],
1899
- acpActiveElicitations: Array.isArray(agent.acpActiveElicitations) ? agent.acpActiveElicitations : [],
1900
- acpSessionUpdatedAt: agent.acpSessionUpdatedAt || '',
1901
- acpSessionRevision: Number(agent.acpSessionRevision) || 0,
1902
- codexAppServerHomePath: agent.codexAppServerHomePath || '',
1903
- codexAppServerState: agent.codexAppServerState,
1904
- codexAppServerEndpoint: agent.codexAppServerEndpoint,
1905
- codexAppServerThreadId: agent.codexAppServerThreadId,
1906
- codexAppServerTurnId: agent.codexAppServerTurnId,
1907
- codexAppServerError: agent.codexAppServerError,
1908
- codexAppServerPendingRequestId: agent.codexAppServerPendingRequestId,
1909
- codexAppServerPendingRequestMethod: agent.codexAppServerPendingRequestMethod,
1910
- codexAppServerPendingRequest: agent.codexAppServerPendingRequest || null,
1911
- codexCliObserverDeferred: agent.codexCliObserverDeferred === true,
2363
+ ...legacyRuntimeMetadata(agent),
1912
2364
  forkedFromProviderSessionId: agent.forkedFromProviderSessionId,
1913
2365
  restartedFromAgentId: agent.restartedFromAgentId,
1914
2366
  restartedFromAgentIds: agent.restartedFromAgentIds,
@@ -1923,7 +2375,10 @@ class AgentManager extends EventEmitter {
1923
2375
  attentionUpdatedAt: agent.attentionUpdatedAt,
1924
2376
  readAttentionAt: agent.readAttentionAt,
1925
2377
  attentionReason: agent.attentionReason,
2378
+ attentionOutputEpoch: agent.attentionOutputEpoch,
1926
2379
  attentionOutputSeq: agent.attentionOutputSeq,
2380
+ readOutputEpoch: agent.readOutputEpoch,
2381
+ readOutputSeq: agent.readOutputSeq,
1927
2382
  };
1928
2383
  }
1929
2384
 
@@ -1936,11 +2391,12 @@ class AgentManager extends EventEmitter {
1936
2391
  env: this.buildAgentEnv(agent.id, agent),
1937
2392
  category: launch.category,
1938
2393
  metadata: this.engineSessionMetadata(agent),
2394
+ reviveState: launch.reviveState || null,
1939
2395
  });
1940
2396
  }
1941
2397
 
1942
2398
  async startAgent(command, customWorkspace, callback, options = {}) {
1943
- if (options.wantsMain !== false) {
2399
+ if (options.wantsMain !== false && options.skipRecoveryWait !== true) {
1944
2400
  await this.whenRecovered();
1945
2401
  }
1946
2402
 
@@ -1960,6 +2416,29 @@ class AgentManager extends EventEmitter {
1960
2416
  && this.configManager
1961
2417
  && this.configManager.getDangerouslySkipAgentPermissionsByDefault()
1962
2418
  );
2419
+ const preserveProviderSessionProfile = options.preserveProviderSessionProfile === true
2420
+ || codexCommandContinuesSession(command);
2421
+ const codexModel = preserveProviderSessionProfile
2422
+ ? 'config'
2423
+ : (typeof options.codexModel === 'string'
2424
+ ? options.codexModel
2425
+ : (this.configManager && this.configManager.getCodexModel
2426
+ ? this.configManager.getCodexModel()
2427
+ : 'gpt-5.5'));
2428
+ const codexReasoningEffort = preserveProviderSessionProfile
2429
+ ? 'config'
2430
+ : (typeof options.codexReasoningEffort === 'string'
2431
+ ? options.codexReasoningEffort
2432
+ : (this.configManager && this.configManager.getCodexReasoningEffort
2433
+ ? this.configManager.getCodexReasoningEffort()
2434
+ : 'xhigh'));
2435
+ const codexServiceTier = preserveProviderSessionProfile
2436
+ ? 'config'
2437
+ : (typeof options.codexServiceTier === 'string'
2438
+ ? options.codexServiceTier
2439
+ : (this.configManager && this.configManager.getCodexServiceTier
2440
+ ? this.configManager.getCodexServiceTier()
2441
+ : 'default'));
1963
2442
  const launch = resolveLaunchCommand(command, {
1964
2443
  dangerouslySkipPermissions,
1965
2444
  agentLaunchProfiles: this.configManager && this.configManager.getAgentLaunchProfiles
@@ -1974,15 +2453,9 @@ class AgentManager extends EventEmitter {
1974
2453
  codexModelPreset: this.configManager && this.configManager.getCodexModelPreset
1975
2454
  ? this.configManager.getCodexModelPreset()
1976
2455
  : 'gpt-5.5:xhigh',
1977
- codexModel: this.configManager && this.configManager.getCodexModel
1978
- ? this.configManager.getCodexModel()
1979
- : 'gpt-5.5',
1980
- codexReasoningEffort: this.configManager && this.configManager.getCodexReasoningEffort
1981
- ? this.configManager.getCodexReasoningEffort()
1982
- : 'xhigh',
1983
- codexServiceTier: this.configManager && this.configManager.getCodexServiceTier
1984
- ? this.configManager.getCodexServiceTier()
1985
- : 'default',
2456
+ codexModel,
2457
+ codexReasoningEffort,
2458
+ codexServiceTier,
1986
2459
  mainAgentSystemPrompt: wantsMain ? renderMainAgentBootstrap() : '',
1987
2460
  });
1988
2461
  const program = launch.program;
@@ -2105,7 +2578,19 @@ class AgentManager extends EventEmitter {
2105
2578
  return null;
2106
2579
  }
2107
2580
 
2108
- const agentId = `agent-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
2581
+ const requestedRuntimeAgentId = typeof options.runtimeAgentId === 'string'
2582
+ ? options.runtimeAgentId.trim()
2583
+ : '';
2584
+ if (requestedRuntimeAgentId && !/^agent-[A-Za-z0-9_-]+$/.test(requestedRuntimeAgentId)) {
2585
+ if (callback) callback(null, 'Invalid runtime Agent id');
2586
+ return null;
2587
+ }
2588
+ if (requestedRuntimeAgentId && this.agents.has(requestedRuntimeAgentId)) {
2589
+ if (callback) callback(null, `Runtime Agent id is already active: ${requestedRuntimeAgentId}`);
2590
+ return null;
2591
+ }
2592
+ const agentId = requestedRuntimeAgentId ||
2593
+ `agent-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
2109
2594
  const homeProvider = providerSessionPlan.provider || agentHomeProviderForProgram(program);
2110
2595
  const providerHomeId = typeof options.providerHomeId === 'string' && options.providerHomeId.trim()
2111
2596
  ? options.providerHomeId.trim()
@@ -2165,16 +2650,18 @@ class AgentManager extends EventEmitter {
2165
2650
  && process.env.FARMING_E2E_FAKE_EXECUTABLES !== '1'
2166
2651
  && normalizeCodexRuntimeMode(requestedCodexRuntimeMode) === 'app-server';
2167
2652
  const useJsonCli = requestedAgentRuntimeMode === 'json'
2168
- && ['codex', 'opencode'].includes(structuredRuntimeProvider)
2653
+ && providerSupportsRuntime(structuredRuntimeProvider, 'json')
2169
2654
  && !useCodexAppServer
2170
2655
  && process.env.FARMING_E2E_FAKE_EXECUTABLES !== '1';
2171
2656
  const useAcp = requestedAgentRuntimeMode === 'acp'
2172
- && ['codex', 'claude', 'opencode', 'qoder'].includes(structuredRuntimeProvider)
2657
+ && providerSupportsRuntime(structuredRuntimeProvider, 'acp')
2173
2658
  && !useCodexAppServer
2174
2659
  && (
2175
2660
  process.env.FARMING_E2E_FAKE_EXECUTABLES !== '1'
2176
2661
  || process.env.FARMING_E2E_FAKE_ACP_AGENT === '1'
2177
2662
  );
2663
+ const acpGeneratedFreshSession = useAcp
2664
+ && isFreshAcpSessionSource(structuredRuntimeProvider, providerSessionPlan.source);
2178
2665
  let codexAppServerHomePath = '';
2179
2666
  if (useCodexAppServer) {
2180
2667
  try {
@@ -2214,40 +2701,23 @@ class AgentManager extends EventEmitter {
2214
2701
  providerHomeId: resolvedProviderHomeId,
2215
2702
  providerHomePath,
2216
2703
  providerSessionId: providerSessionPlan.id || '',
2217
- providerSessionKey: this.providerSessionKey(providerSessionPlan.provider, providerSessionPlan.id, providerHome ? providerHome.id : providerHomeId),
2704
+ providerSessionKey: mainPageAgentSessionKey(providerSessionPlan.provider, providerSessionPlan.id, providerHome ? providerHome.id : providerHomeId),
2218
2705
  providerSessionTemporary: providerSessionPlan.temporary === true,
2219
2706
  providerSessionSource: providerSessionPlan.source || '',
2220
2707
  providerSessionResolvedAt: providerSessionPlan.temporary === true ? null : Date.now(),
2221
2708
  providerSessionTitle: typeof options.providerSessionTitle === 'string' ? options.providerSessionTitle.trim().slice(0, 160) : '',
2709
+ providerSessionWorkspace: '',
2222
2710
  terminalInputReceived: false,
2223
- codexRuntimeMode: structuredRuntimeProvider === 'codex'
2224
- ? (useCodexAppServer ? 'app-server' : 'cli')
2225
- : '',
2226
- agentRuntimeMode: useAcp ? 'acp' : (useJsonCli ? 'json' : 'terminal'),
2227
- jsonCliState: useJsonCli ? 'idle' : '',
2228
- jsonCliError: '',
2229
- jsonCliTranscriptUpdatedAt: '',
2230
- jsonCliEvents: Array.isArray(options.jsonCliEvents) ? options.jsonCliEvents : [],
2231
- acpState: useAcp ? 'connecting' : '',
2232
- acpError: '',
2233
- acpStopReason: '',
2234
- acpPendingPermission: null,
2235
- acpPendingPermissions: [],
2236
- acpPendingElicitation: null,
2237
- acpPendingElicitations: [],
2238
- acpActiveElicitations: [],
2239
- acpSessionUpdatedAt: '',
2240
- acpSessionRevision: 0,
2241
- codexAppServerHomePath,
2242
- codexAppServerState: useCodexAppServer ? 'connecting' : '',
2243
- codexAppServerEndpoint: '',
2244
- codexAppServerThreadId: '',
2245
- codexAppServerTurnId: '',
2246
- codexAppServerError: '',
2247
- codexAppServerPendingRequestId: '',
2248
- codexAppServerPendingRequestMethod: '',
2249
- codexAppServerPendingRequest: null,
2250
- codexCliObserverDeferred: false,
2711
+ runtimeBinding: useCodexAppServer
2712
+ ? runtimeBindingFor('app-server', { state: 'connecting', homePath: codexAppServerHomePath })
2713
+ : (useAcp
2714
+ ? runtimeBindingFor('acp', { state: 'connecting' })
2715
+ : (useJsonCli
2716
+ ? runtimeBindingFor('json', {
2717
+ state: 'idle',
2718
+ events: Array.isArray(options.jsonCliEvents) ? options.jsonCliEvents : [],
2719
+ })
2720
+ : runtimeBindingFor('terminal'))),
2251
2721
  forkedFromProviderSessionId: providerSessionPlan.forkedFromProviderSessionId || '',
2252
2722
  restartedFromAgentId: typeof options.restartedFromAgentId === 'string' ? options.restartedFromAgentId : '',
2253
2723
  restartedFromAgentIds: Array.isArray(options.restartedFromAgentIds)
@@ -2256,7 +2726,7 @@ class AgentManager extends EventEmitter {
2256
2726
  runtimeSwitchVerifiedSessionId: typeof options.runtimeSwitchVerifiedSessionId === 'string'
2257
2727
  ? options.runtimeSwitchVerifiedSessionId
2258
2728
  : '',
2259
- persistentSessionId: '',
2729
+ persistentSessionId: typeof options.persistentSessionId === 'string' ? options.persistentSessionId : '',
2260
2730
  customTitle: typeof options.customTitle === 'string' ? options.customTitle.trim().slice(0, 80) : '',
2261
2731
  terminalBusy: null,
2262
2732
  shellCwd: '',
@@ -2276,7 +2746,10 @@ class AgentManager extends EventEmitter {
2276
2746
  attentionUpdatedAt: null,
2277
2747
  readAttentionAt: null,
2278
2748
  attentionReason: '',
2749
+ attentionOutputEpoch: '',
2279
2750
  attentionOutputSeq: null,
2751
+ readOutputEpoch: '',
2752
+ readOutputSeq: null,
2280
2753
  unread: false,
2281
2754
  archived: false,
2282
2755
  archivedAt: null,
@@ -2293,6 +2766,7 @@ class AgentManager extends EventEmitter {
2293
2766
 
2294
2767
  ensureAgentOrders(agentRecord, Array.from(this.agents.values()));
2295
2768
  this.agents.set(agentId, agentRecord);
2769
+ void this.refreshAgentWorktree(agentId);
2296
2770
 
2297
2771
  this.lastActivity.set(agentId, Date.now());
2298
2772
 
@@ -2301,9 +2775,10 @@ class AgentManager extends EventEmitter {
2301
2775
  try {
2302
2776
  if (useCodexAppServer) {
2303
2777
  try {
2778
+ const appServer = runtimeBindingOf(agentRecord, 'app-server');
2304
2779
  const prepared = await this.codexAppServerRuntime.prepareAgent({
2305
2780
  agentId,
2306
- codexHome: agentRecord.codexAppServerHomePath,
2781
+ codexHome: appServer.homePath,
2307
2782
  executable: spawnProgram,
2308
2783
  env: this.buildAgentEnv(agentId, agentRecord),
2309
2784
  cwd: workspace,
@@ -2315,19 +2790,13 @@ class AgentManager extends EventEmitter {
2315
2790
  ? this.configManager.getCodexApprovalMode()
2316
2791
  : 'approve'
2317
2792
  ),
2318
- model: this.configManager && this.configManager.getCodexModel
2319
- ? this.configManager.getCodexModel()
2320
- : 'gpt-5.5',
2321
- reasoningEffort: this.configManager && this.configManager.getCodexReasoningEffort
2322
- ? this.configManager.getCodexReasoningEffort()
2323
- : 'xhigh',
2324
- serviceTier: this.configManager && this.configManager.getCodexServiceTier
2325
- ? this.configManager.getCodexServiceTier()
2326
- : 'default',
2793
+ model: codexModel,
2794
+ reasoningEffort: codexReasoningEffort,
2795
+ serviceTier: codexServiceTier,
2327
2796
  developerInstructions: wantsMain ? renderMainAgentBootstrap() : '',
2328
2797
  });
2329
2798
  agentRecord.providerSessionId = prepared.threadId;
2330
- agentRecord.providerSessionKey = this.providerSessionKey(
2799
+ agentRecord.providerSessionKey = mainPageAgentSessionKey(
2331
2800
  'codex',
2332
2801
  prepared.threadId,
2333
2802
  agentRecord.providerHomeId || 'default'
@@ -2335,13 +2804,13 @@ class AgentManager extends EventEmitter {
2335
2804
  agentRecord.providerSessionTemporary = false;
2336
2805
  agentRecord.providerSessionSource = prepared.resumed ? 'app-server-resume' : 'app-server-start';
2337
2806
  agentRecord.providerSessionResolvedAt = Date.now();
2338
- agentRecord.codexAppServerState = 'idle';
2339
- agentRecord.codexAppServerEndpoint = prepared.endpoint;
2340
- agentRecord.codexAppServerThreadId = prepared.threadId;
2341
- agentRecord.codexAppServerError = '';
2807
+ appServer.state = 'idle';
2808
+ appServer.endpoint = prepared.endpoint;
2809
+ appServer.threadId = prepared.threadId;
2810
+ appServer.error = '';
2342
2811
  // App Server mode has no embedded CLI observer. Its chat and
2343
2812
  // lifecycle both come from the structured App Server connection.
2344
- agentRecord.codexCliObserverDeferred = false;
2813
+ appServer.observerDeferred = false;
2345
2814
  } catch (error) {
2346
2815
  const reason = error && error.message ? error.message : String(error);
2347
2816
  throw new Error(`Codex App Server failed to start: ${reason}`, { cause: error });
@@ -2349,6 +2818,7 @@ class AgentManager extends EventEmitter {
2349
2818
  }
2350
2819
 
2351
2820
  if (useJsonCli) {
2821
+ const jsonRuntime = runtimeBindingOf(agentRecord, 'json');
2352
2822
  this.jsonCliRuntime.registerAgent({
2353
2823
  agentId,
2354
2824
  provider: structuredRuntimeProvider,
@@ -2358,34 +2828,32 @@ class AgentManager extends EventEmitter {
2358
2828
  sessionId: agentRecord.providerSessionTemporary ? '' : agentRecord.providerSessionId,
2359
2829
  approvalMode: agentRecord.launchPermissionMode || 'approve',
2360
2830
  autoApprove: options.dangerouslySkipPermissions === true,
2361
- initialEvents: agentRecord.jsonCliEvents,
2831
+ initialEvents: jsonRuntime.events,
2362
2832
  });
2363
2833
  }
2364
2834
 
2365
2835
  if (useAcp) {
2836
+ const acpRuntime = runtimeBindingOf(agentRecord, 'acp');
2366
2837
  const prepared = await this.acpRuntime.prepareAgent({
2367
2838
  agentId,
2368
2839
  provider: structuredRuntimeProvider,
2369
2840
  executable: spawnProgram,
2370
2841
  env: this.buildAgentEnv(agentId, agentRecord),
2371
2842
  cwd: workspace,
2372
- sessionId: options.acpStartFresh === true || agentRecord.providerSessionTemporary
2843
+ sessionId: options.acpStartFresh === true || agentRecord.providerSessionTemporary || acpGeneratedFreshSession
2373
2844
  ? ''
2374
2845
  : agentRecord.providerSessionId,
2375
- historyMode: options.acpHistoryMode === 'resume' ? 'resume' : 'load',
2846
+ historyMode: options.acpHistoryMode === 'resume'
2847
+ ? 'resume'
2848
+ : (options.acpHistoryMode === 'load' ? 'load' : 'checkpoint'),
2849
+ providerHomeId: agentRecord.providerHomeId || 'default',
2376
2850
  approvalMode: agentRecord.launchPermissionMode || 'approve',
2377
- model: this.configManager && this.configManager.getCodexModel
2378
- ? this.configManager.getCodexModel()
2379
- : '',
2380
- reasoningEffort: this.configManager && this.configManager.getCodexReasoningEffort
2381
- ? this.configManager.getCodexReasoningEffort()
2382
- : '',
2383
- serviceTier: this.configManager && this.configManager.getCodexServiceTier
2384
- ? this.configManager.getCodexServiceTier()
2385
- : '',
2851
+ model: codexModel,
2852
+ reasoningEffort: codexReasoningEffort,
2853
+ serviceTier: codexServiceTier,
2386
2854
  });
2387
2855
  agentRecord.providerSessionId = prepared.sessionId;
2388
- agentRecord.providerSessionKey = this.providerSessionKey(
2856
+ agentRecord.providerSessionKey = mainPageAgentSessionKey(
2389
2857
  structuredRuntimeProvider,
2390
2858
  prepared.sessionId,
2391
2859
  agentRecord.providerHomeId || 'default'
@@ -2393,8 +2861,8 @@ class AgentManager extends EventEmitter {
2393
2861
  agentRecord.providerSessionTemporary = false;
2394
2862
  agentRecord.providerSessionSource = `acp-${prepared.historyMode}`;
2395
2863
  agentRecord.providerSessionResolvedAt = Date.now();
2396
- agentRecord.acpState = 'idle';
2397
- agentRecord.acpError = '';
2864
+ acpRuntime.state = 'idle';
2865
+ acpRuntime.error = '';
2398
2866
  }
2399
2867
 
2400
2868
  agentRecord.persistentSessionId = this.ensurePersistentAgentSession(agentRecord);
@@ -2404,6 +2872,7 @@ class AgentManager extends EventEmitter {
2404
2872
  args,
2405
2873
  cwd: workspace,
2406
2874
  category: resolution.spec ? resolution.spec.category : 'shell',
2875
+ reviveState: options.reviveTerminalState || null,
2407
2876
  };
2408
2877
  await this.createAgentEngineSession(agentRecord, resolution.engine, engineLaunch);
2409
2878
  }
@@ -2419,18 +2888,33 @@ class AgentManager extends EventEmitter {
2419
2888
  }
2420
2889
  }
2421
2890
 
2422
- this.activateProviderSessionTracking(agentId);
2891
+ this.providerSessionService.activate(agentId);
2423
2892
  if (callback) callback(agentId);
2424
2893
  this.emit('update');
2425
2894
  return agentId;
2426
2895
  } catch (error) {
2427
2896
  console.error('Failed to start agent:', error);
2897
+ if (options.restoreRuntimeAgentIdOnFailure && agentRecord.persistentSessionId) {
2898
+ const failedAgentId = agentRecord.id;
2899
+ try {
2900
+ agentRecord.id = options.restoreRuntimeAgentIdOnFailure;
2901
+ this.ensurePersistentAgentSession(agentRecord);
2902
+ } catch (rollbackError) {
2903
+ console.error(
2904
+ 'Failed to roll back persisted Agent runtime id after restart failure:',
2905
+ rollbackError && (rollbackError.message || rollbackError)
2906
+ );
2907
+ } finally {
2908
+ agentRecord.id = failedAgentId;
2909
+ }
2910
+ }
2428
2911
  this.agents.delete(agentId);
2429
2912
  this.lastActivity.delete(agentId);
2430
2913
  this.lastActivityUpdate.delete(agentId);
2431
2914
  this.outputEvents.delete(agentId);
2915
+ this.agentUsageRateCache.delete(agentId);
2432
2916
  this.lastResizeByAgent.delete(agentId);
2433
- this.stopCodexProviderSessionResolver(agentId);
2917
+ this.providerSessionService.stop(agentId);
2434
2918
  if (this.codexAppServerRuntime && typeof this.codexAppServerRuntime.unregisterAgent === 'function') {
2435
2919
  this.codexAppServerRuntime.unregisterAgent(agentId);
2436
2920
  }
@@ -2446,15 +2930,15 @@ class AgentManager extends EventEmitter {
2446
2930
  }
2447
2931
  }
2448
2932
 
2449
- async sendInput(agentId, input) {
2933
+ async enqueueInputOperation(agentId, operation) {
2450
2934
  const previous = this.inputQueues.get(agentId) || Promise.resolve();
2451
2935
  const next = previous
2452
2936
  .catch(() => {})
2453
- .then(() => this.sendInputNow(agentId, input));
2937
+ .then(operation);
2454
2938
 
2455
2939
  this.inputQueues.set(agentId, next);
2456
2940
  try {
2457
- await next;
2941
+ return await next;
2458
2942
  } finally {
2459
2943
  if (this.inputQueues.get(agentId) === next) {
2460
2944
  this.inputQueues.delete(agentId);
@@ -2462,20 +2946,52 @@ class AgentManager extends EventEmitter {
2462
2946
  }
2463
2947
  }
2464
2948
 
2949
+ async enqueueInputOperationUntilReleased(agentId, operation) {
2950
+ const previous = this.inputQueues.get(agentId) || Promise.resolve();
2951
+ let released = false;
2952
+ let resolveReleased;
2953
+ const releasedPromise = new Promise(resolve => {
2954
+ resolveReleased = resolve;
2955
+ });
2956
+ const release = () => {
2957
+ if (released) return;
2958
+ released = true;
2959
+ resolveReleased();
2960
+ };
2961
+
2962
+ const ready = previous.catch(() => {});
2963
+ const completion = ready.then(() => operation(release));
2964
+ completion.catch(() => release());
2965
+ const boundary = ready.then(() => releasedPromise);
2966
+ this.inputQueues.set(agentId, boundary);
2967
+ boundary.then(() => {
2968
+ if (this.inputQueues.get(agentId) === boundary) {
2969
+ this.inputQueues.delete(agentId);
2970
+ }
2971
+ });
2972
+ return completion;
2973
+ }
2974
+
2975
+ async sendInput(agentId, input, options = {}) {
2976
+ return this.enqueueInputOperation(agentId, () => this.sendInputNow(agentId, input, options));
2977
+ }
2978
+
2465
2979
  codexAppServerOptionsForAgent(agent, message = '') {
2466
2980
  const codexResolution = resolveCompatibleCodexExecutable(agent.requiredCliVersion || '');
2467
2981
  if (!codexResolution.compatible) {
2468
2982
  throw new Error(codexResolution.error || 'Codex CLI is not compatible with App Server mode');
2469
2983
  }
2470
2984
 
2985
+ const runtime = runtimeBindingOf(agent, 'app-server');
2986
+ if (!runtime) throw new Error('Agent is not using the Codex App Server runtime');
2471
2987
  return {
2472
2988
  agentId: agent.id,
2473
- codexHome: agent.codexAppServerHomePath || '',
2989
+ codexHome: runtime.homePath,
2474
2990
  executable: codexResolution.path || resolveAgentExecutable(agent.command) || agent.command,
2475
2991
  env: this.buildAgentEnv(agent.id, agent),
2476
2992
  cwd: agent.cwd,
2477
- workspaceRoot: agent.projectWorkspace || agent.cwd,
2478
- threadId: agent.codexAppServerThreadId || agent.providerSessionId || '',
2993
+ workspaceRoot: effectiveAgentWorkspaceRoot(agent),
2994
+ threadId: runtime.threadId || agent.providerSessionId || '',
2479
2995
  approvalMode: agent.launchPermissionMode || (
2480
2996
  this.configManager && this.configManager.getCodexApprovalMode
2481
2997
  ? this.configManager.getCodexApprovalMode()
@@ -2495,21 +3011,101 @@ class AgentManager extends EventEmitter {
2495
3011
  }
2496
3012
 
2497
3013
  async sendComposerMessage(agentId, message) {
2498
- const previous = this.inputQueues.get(agentId) || Promise.resolve();
2499
- const next = previous
2500
- .catch(() => {})
2501
- .then(() => this.sendComposerMessageNow(agentId, message));
3014
+ return this.enqueueInputOperation(agentId, () => this.sendComposerMessageNow(agentId, message));
3015
+ }
2502
3016
 
2503
- this.inputQueues.set(agentId, next);
3017
+ async setCodexTerminalProfile(agentId, profile, options = {}) {
3018
+ const previous = this.codexTerminalProfileQueues.get(agentId) || Promise.resolve();
3019
+ const start = () => this.enqueueInputOperationUntilReleased(
3020
+ agentId,
3021
+ releaseInput => this.setCodexTerminalProfileNow(agentId, profile, {
3022
+ ...options,
3023
+ onInputSafe: releaseInput,
3024
+ }),
3025
+ );
3026
+ const next = this.codexTerminalProfileQueues.has(agentId)
3027
+ ? previous.catch(() => {}).then(start)
3028
+ : start();
3029
+ this.codexTerminalProfileQueues.set(agentId, next);
2504
3030
  try {
2505
3031
  return await next;
2506
3032
  } finally {
2507
- if (this.inputQueues.get(agentId) === next) {
2508
- this.inputQueues.delete(agentId);
3033
+ if (this.codexTerminalProfileQueues.get(agentId) === next) {
3034
+ this.codexTerminalProfileQueues.delete(agentId);
2509
3035
  }
2510
3036
  }
2511
3037
  }
2512
3038
 
3039
+ async setCodexTerminalProfileNow(agentId, profile, options = {}) {
3040
+ const agent = this.agents.get(agentId);
3041
+ if (!agent) throw new Error('Agent not found');
3042
+ if (
3043
+ agentProgramName(agent.command).toLowerCase() !== 'codex'
3044
+ || isCodexAppServerAgent(agent)
3045
+ || isJsonCliAgent(agent)
3046
+ || isAcpAgent(agent)
3047
+ || runtimeKind(agent) !== 'terminal'
3048
+ ) {
3049
+ throw new Error('This Agent is not using Codex Terminal');
3050
+ }
3051
+ if (!isRunningAgentRuntimeStatus(agent.status)) {
3052
+ throw new Error('Codex Terminal is not running');
3053
+ }
3054
+ if (this.isAgentAttentionTurnActive(agent)) {
3055
+ throw new Error('Wait for the active Codex Terminal turn to finish before changing its model');
3056
+ }
3057
+
3058
+ const applied = await applyCodexTerminalProfile({
3059
+ profile,
3060
+ timeoutMs: options.timeoutMs,
3061
+ signal: options.signal,
3062
+ onInputSafe: options.onInputSafe,
3063
+ readPreview: async () => {
3064
+ const view = await this.getAgentSessionView(agentId);
3065
+ if (!view) throw new Error('Agent not found');
3066
+ return view.previewText;
3067
+ },
3068
+ readOutput: async () => String(await this.getAgentSessionText(agentId) || ''),
3069
+ // `/model` and `/fast on|off` are Farming-owned control traffic. They must not
3070
+ // make a fresh Terminal look user-authored, because that would remove
3071
+ // the safe fresh-session path into ACP Chat before the provider has
3072
+ // materialized a resumable history record.
3073
+ sendInput: async input => this.sendInputNow(agentId, input, { markUserInput: false }),
3074
+ });
3075
+ agent.codexTerminalProfile = {
3076
+ model: applied.model,
3077
+ reasoningEffort: applied.effort,
3078
+ serviceTier: applied.serviceTier,
3079
+ source: 'terminal-command',
3080
+ };
3081
+
3082
+ // The HTTP response confirms the terminal has already reached this profile.
3083
+ // Publish that confirmation immediately instead of waiting for a later PTY
3084
+ // preview tick; otherwise the browser's bounded optimistic state can expire
3085
+ // and briefly fall back to the pre-command footer.
3086
+ const view = options.signal?.aborted
3087
+ ? null
3088
+ : await this.getAgentSessionView(agentId);
3089
+ if (view) {
3090
+ agent.previewText = view.previewText || agent.previewText || '';
3091
+ agent.previewSnapshot = view.previewSnapshot || agent.previewSnapshot || null;
3092
+ agent.previewCols = view.previewCols || agent.previewCols || 80;
3093
+ agent.previewRows = view.previewRows || agent.previewRows || 30;
3094
+ this.emit('session-preview-update', {
3095
+ agentId,
3096
+ previewText: agent.previewText,
3097
+ cols: agent.previewCols,
3098
+ rows: agent.previewRows,
3099
+ previewSnapshot: agent.previewSnapshot,
3100
+ codexTerminalProfile: agent.codexTerminalProfile,
3101
+ terminalStatus: view.terminalStatus,
3102
+ runtimeObservation: deriveRuntimeObservation({ ...agent, terminalStatus: view.terminalStatus }),
3103
+ });
3104
+ }
3105
+ this.emit('update');
3106
+ return applied;
3107
+ }
3108
+
2513
3109
  async sendComposerMessageNow(agentId, message) {
2514
3110
  const agent = this.agents.get(agentId);
2515
3111
  if (!agent) throw new Error('Agent not found');
@@ -2523,19 +3119,20 @@ class AgentManager extends EventEmitter {
2523
3119
  throw new Error('Composer message is empty');
2524
3120
  }
2525
3121
 
3122
+ const appServer = runtimeBindingOf(agent, 'app-server');
2526
3123
  if (
2527
3124
  agent.providerSessionProvider === 'codex'
2528
- && agent.codexRuntimeMode === 'app-server'
3125
+ && appServer
2529
3126
  && this.codexAppServerRuntime
2530
3127
  && typeof this.codexAppServerRuntime.submitComposerMessage === 'function'
2531
3128
  ) {
2532
3129
  const result = await this.codexAppServerRuntime.submitComposerMessage(
2533
3130
  this.codexAppServerOptionsForAgent(agent, text)
2534
3131
  );
2535
- agent.codexAppServerThreadId = result.threadId;
2536
- agent.codexAppServerTurnId = result.turnId;
2537
- agent.codexAppServerState = 'working';
2538
- agent.codexAppServerError = '';
3132
+ appServer.threadId = result.threadId;
3133
+ appServer.turnId = result.turnId;
3134
+ appServer.state = 'working';
3135
+ appServer.error = '';
2539
3136
  this.lastActivity.set(agentId, Date.now());
2540
3137
  this.emit('update');
2541
3138
  return result;
@@ -2553,8 +3150,9 @@ class AgentManager extends EventEmitter {
2553
3150
 
2554
3151
  if (isAcpAgent(agent)) {
2555
3152
  const result = await this.acpRuntime.prompt(agentId, prompt);
2556
- agent.acpState = 'idle';
2557
- agent.acpStopReason = result.stopReason || '';
3153
+ const runtime = runtimeBindingOf(agent, 'acp');
3154
+ runtime.state = 'idle';
3155
+ runtime.stopReason = result.stopReason || '';
2558
3156
  this.ensurePersistentAgentSession(agent);
2559
3157
  return { kind: 'acp', ...result };
2560
3158
  }
@@ -2566,11 +3164,12 @@ class AgentManager extends EventEmitter {
2566
3164
  respondToCodexAppServerRequest(agentId, requestId, result, options = {}) {
2567
3165
  const agent = this.agents.get(agentId);
2568
3166
  if (!agent) throw new Error('Agent not found');
2569
- if (agent.providerSessionProvider !== 'codex' || agent.codexRuntimeMode !== 'app-server') {
3167
+ const runtime = runtimeBindingOf(agent, 'app-server');
3168
+ if (agent.providerSessionProvider !== 'codex' || !runtime) {
2570
3169
  throw new Error('This Agent is not using the Codex App Server runtime');
2571
3170
  }
2572
3171
  const resolvedRequestId = String(requestId || '');
2573
- if (!resolvedRequestId || resolvedRequestId !== agent.codexAppServerPendingRequestId) {
3172
+ if (!resolvedRequestId || resolvedRequestId !== runtime.pendingRequestId) {
2574
3173
  throw new Error('Codex App Server request is no longer pending');
2575
3174
  }
2576
3175
  if (!this.codexAppServerRuntime) throw new Error('Codex App Server runtime is unavailable');
@@ -2582,10 +3181,10 @@ class AgentManager extends EventEmitter {
2582
3181
  { code: -32000, message: String(options.reason || 'Rejected by Farming') }
2583
3182
  )
2584
3183
  : this.codexAppServerRuntime.resolveAgentServerRequest(agentId, resolvedRequestId, result);
2585
- agent.codexAppServerPendingRequestId = '';
2586
- agent.codexAppServerPendingRequestMethod = '';
2587
- agent.codexAppServerPendingRequest = null;
2588
- agent.codexAppServerState = agent.codexAppServerTurnId ? 'working' : 'idle';
3184
+ runtime.pendingRequestId = '';
3185
+ runtime.pendingRequestMethod = '';
3186
+ runtime.pendingRequest = null;
3187
+ runtime.state = runtime.turnId ? 'working' : 'idle';
2589
3188
  this.emit('update');
2590
3189
  return response;
2591
3190
  }
@@ -2593,11 +3192,12 @@ class AgentManager extends EventEmitter {
2593
3192
  assertCodexAppServerGoalAgent(agentId) {
2594
3193
  const agent = this.agents.get(agentId);
2595
3194
  if (!agent) throw new Error('Agent not found');
2596
- if (agent.providerSessionProvider !== 'codex' || agent.codexRuntimeMode !== 'app-server') {
3195
+ const runtime = runtimeBindingOf(agent, 'app-server');
3196
+ if (agent.providerSessionProvider !== 'codex' || !runtime) {
2597
3197
  throw new Error('This Agent is not using the Codex App Server runtime');
2598
3198
  }
2599
3199
  if (!this.codexAppServerRuntime) throw new Error('Codex App Server runtime is unavailable');
2600
- if (!(agent.codexAppServerThreadId || agent.providerSessionId)) {
3200
+ if (!(runtime.threadId || agent.providerSessionId)) {
2601
3201
  throw new Error('Codex App Server thread id is not available yet');
2602
3202
  }
2603
3203
  return agent;
@@ -2606,7 +3206,7 @@ class AgentManager extends EventEmitter {
2606
3206
  async getCodexAppServerGoal(agentId) {
2607
3207
  const agent = this.assertCodexAppServerGoalAgent(agentId);
2608
3208
  const goal = await this.codexAppServerRuntime.getAgentGoal(this.codexAppServerOptionsForAgent(agent));
2609
- agent.codexAppServerGoal = goal || null;
3209
+ runtimeBindingOf(agent, 'app-server').goal = goal || null;
2610
3210
  this.emit('update');
2611
3211
  return goal || null;
2612
3212
  }
@@ -2637,7 +3237,11 @@ class AgentManager extends EventEmitter {
2637
3237
  const agent = this.agents.get(agentId);
2638
3238
  if (!agent) throw new Error('Agent not found');
2639
3239
  if (!isAcpAgent(agent)) throw new Error('Agent is not using the ACP runtime');
2640
- return this.acpRuntime.getTranscriptSession(agentId, options);
3240
+ const transcript = this.acpRuntime.getTranscriptSession(agentId, options);
3241
+ return {
3242
+ ...transcript,
3243
+ entries: transcript.entries.map(entry => entry?.type === 'tool' ? acpTranscriptToolEntry(entry) : entry),
3244
+ };
2641
3245
  }
2642
3246
 
2643
3247
  getAcpToolDetail(agentId, toolCallId) {
@@ -2778,7 +3382,7 @@ class AgentManager extends EventEmitter {
2778
3382
  ...(status !== undefined ? { status } : {}),
2779
3383
  ...(Object.prototype.hasOwnProperty.call(patch, 'tokenBudget') ? { tokenBudget } : {}),
2780
3384
  });
2781
- agent.codexAppServerGoal = goal || null;
3385
+ runtimeBindingOf(agent, 'app-server').goal = goal || null;
2782
3386
  this.emit('update');
2783
3387
  return goal || null;
2784
3388
  }
@@ -2786,43 +3390,43 @@ class AgentManager extends EventEmitter {
2786
3390
  async clearCodexAppServerGoal(agentId) {
2787
3391
  const agent = this.assertCodexAppServerGoalAgent(agentId);
2788
3392
  await this.codexAppServerRuntime.clearAgentGoal(this.codexAppServerOptionsForAgent(agent));
2789
- agent.codexAppServerGoal = null;
3393
+ runtimeBindingOf(agent, 'app-server').goal = null;
2790
3394
  this.emit('update');
2791
3395
  return null;
2792
3396
  }
2793
3397
 
2794
- async sendInputNow(agentId, input) {
3398
+ async sendInputNow(agentId, input, { markUserInput = true, expectedRuntimeEpoch = '' } = {}) {
2795
3399
  const agent = this.agents.get(agentId);
2796
3400
  if (!agent) return;
2797
3401
  // The observer PTY is optional telemetry for App Server Agents. Its
2798
3402
  // disappearance must never redefine the App Server Agent as dead.
2799
3403
  if (isCodexAppServerAgent(agent) || isJsonCliAgent(agent) || isAcpAgent(agent)) return;
3404
+ if (expectedRuntimeEpoch && agent.runtimeEpoch !== expectedRuntimeEpoch) {
3405
+ return { status: 'input-rejected', reason: 'runtime-epoch-mismatch' };
3406
+ }
2800
3407
 
2801
3408
  const engine = this.engineBridge.getEngine(agent.engineName);
2802
3409
  if (!engine) return;
2803
3410
 
2804
- if (agent.terminalInputReceived !== true) {
2805
- agent.terminalInputReceived = true;
2806
- this.ensurePersistentAgentSession(agent);
2807
- this.updateEngineProviderSessionMetadata(agent);
2808
- }
2809
-
2810
- for (let attempt = 0; ; attempt += 1) {
2811
- try {
2812
- await engine.sendInput(agentId, input);
2813
- this.observeAgentStateChange(agentId, { force: true });
2814
- return;
2815
- } catch (error) {
2816
- const delay = INPUT_SESSION_RETRY_DELAYS_MS[attempt];
2817
- if (!isSessionNotAvailableError(error) || delay === undefined) {
2818
- console.error('Failed to send input:', error);
2819
- if (isSessionNotAvailableError(error)) {
2820
- this.markAgentSessionDead(agentId, error);
2821
- }
2822
- return;
2823
- }
2824
- await sleep(delay);
3411
+ try {
3412
+ const submittedUserInput = markUserInput && hasSubmittedTerminalInput(input);
3413
+ const result = await engine.sendInput(agentId, input, { expectedRuntimeEpoch });
3414
+ if (submittedUserInput && agent.terminalInputReceived !== true) {
3415
+ agent.terminalInputReceived = true;
3416
+ this.ensurePersistentAgentSession(agent);
3417
+ this.updateEngineProviderSessionMetadata(agent);
3418
+ this.emit('agent-update', { agentId, patch: { terminalInputReceived: true } });
3419
+ }
3420
+ if (submittedUserInput) {
3421
+ this.providerSessionService.observe(agentId, { force: true });
2825
3422
  }
3423
+ return result;
3424
+ } catch (error) {
3425
+ console.error('Failed to send input:', error);
3426
+ if (isSessionNotAvailableError(error)) {
3427
+ this.markAgentSessionDead(agentId, error);
3428
+ }
3429
+ return;
2826
3430
  }
2827
3431
  }
2828
3432
 
@@ -2837,33 +3441,34 @@ class AgentManager extends EventEmitter {
2837
3441
  agent.terminalBusy = false;
2838
3442
  agent.exitedAt = Date.now();
2839
3443
  agent.output = trimSessionOutput(`${agent.output || ''}\n${message}`);
2840
- this.observeAgentStateChange(agentId, { force: true });
3444
+ this.providerSessionService.observe(agentId, { force: true });
2841
3445
  this.emit('update');
2842
3446
  }
2843
3447
 
2844
- async interruptAgent(agentId) {
3448
+ async interruptAgent(agentId, options = {}) {
2845
3449
  const agent = this.agents.get(agentId);
2846
3450
  if (!agent) return;
3451
+ const appServer = runtimeBindingOf(agent, 'app-server');
2847
3452
 
2848
3453
  try {
2849
3454
  if (
2850
3455
  agent.providerSessionProvider === 'codex'
2851
- && agent.codexRuntimeMode === 'app-server'
3456
+ && appServer
2852
3457
  && this.codexAppServerRuntime
2853
3458
  && typeof this.codexAppServerRuntime.interruptAgent === 'function'
2854
3459
  ) {
2855
3460
  try {
2856
3461
  const interrupted = await this.codexAppServerRuntime.interruptAgent(agentId);
2857
3462
  if (interrupted) {
2858
- agent.codexAppServerState = 'interrupting';
3463
+ appServer.state = 'interrupting';
2859
3464
  this.emit('update');
2860
3465
  return;
2861
3466
  }
2862
- agent.codexAppServerError = 'Codex App Server interrupt is not available';
3467
+ appServer.error = 'Codex App Server interrupt is not available';
2863
3468
  this.emit('update');
2864
3469
  return;
2865
3470
  } catch (error) {
2866
- agent.codexAppServerError = `Codex App Server interrupt failed: ${error && (error.message || error)}`;
3471
+ appServer.error = `Codex App Server interrupt failed: ${error && (error.message || error)}`;
2867
3472
  this.emit('update');
2868
3473
  return;
2869
3474
  }
@@ -2881,9 +3486,9 @@ class AgentManager extends EventEmitter {
2881
3486
 
2882
3487
  const input = interruptInputForAgent(agent);
2883
3488
  if (engine.interruptSession) {
2884
- await engine.interruptSession(agentId, input);
3489
+ return await engine.interruptSession(agentId, input, options);
2885
3490
  } else {
2886
- await engine.sendInput(agentId, input);
3491
+ return await engine.sendInput(agentId, input, options);
2887
3492
  }
2888
3493
  } catch (error) {
2889
3494
  console.error('Failed to interrupt agent:', error);
@@ -2893,10 +3498,48 @@ class AgentManager extends EventEmitter {
2893
3498
  }
2894
3499
  }
2895
3500
 
3501
+ agentSupportsTerminalInput(agentId) {
3502
+ const agent = this.agents.get(agentId);
3503
+ if (!agent) return false;
3504
+ return !isCodexAppServerAgent(agent) && !isJsonCliAgent(agent) && !isAcpAgent(agent);
3505
+ }
3506
+
3507
+ async getAgentSessionAttachCheckpoint(agentId) {
3508
+ const agent = this.agents.get(agentId);
3509
+ if (!agent || isCodexAppServerAgent(agent) || isAcpAgent(agent) || isJsonCliAgent(agent)) {
3510
+ return null;
3511
+ }
3512
+ try {
3513
+ return await this.engineBridge.getSessionAttachCheckpoint(agent.engineName, agentId);
3514
+ } catch (error) {
3515
+ console.error('Failed to read agent terminal attach checkpoint:', error);
3516
+ return null;
3517
+ }
3518
+ }
3519
+
3520
+ requestAgentSessionResize(agentId, cols, rows) {
3521
+ this.pendingResizeByAgent.set(agentId, { cols, rows });
3522
+ if (this.resizeDrains.has(agentId)) return true;
3523
+
3524
+ const drain = (async () => {
3525
+ while (this.pendingResizeByAgent.has(agentId)) {
3526
+ const next = this.pendingResizeByAgent.get(agentId);
3527
+ this.pendingResizeByAgent.delete(agentId);
3528
+ await this.resizeAgentSession(agentId, next.cols, next.rows);
3529
+ }
3530
+ })().finally(() => {
3531
+ this.resizeDrains.delete(agentId);
3532
+ });
3533
+ this.resizeDrains.set(agentId, drain);
3534
+ return true;
3535
+ }
3536
+
2896
3537
  async resizeAgentSession(agentId, cols, rows) {
2897
3538
  const agent = this.agents.get(agentId);
2898
- if (!agent) return;
2899
- if (isCodexAppServerAgent(agent) || isAcpAgent(agent) || isJsonCliAgent(agent)) return;
3539
+ if (!agent) return { status: 'resize-rejected', reason: 'session-unavailable', resized: false };
3540
+ if (isCodexAppServerAgent(agent) || isAcpAgent(agent) || isJsonCliAgent(agent)) {
3541
+ return { status: 'resize-rejected', reason: 'unsupported-session', resized: false };
3542
+ }
2900
3543
 
2901
3544
  const nextCols = Math.floor(Number(cols));
2902
3545
  const nextRows = Math.floor(Number(rows));
@@ -2906,49 +3549,50 @@ class AgentManager extends EventEmitter {
2906
3549
  nextCols < MIN_TERMINAL_RESIZE_COLS ||
2907
3550
  nextRows < MIN_TERMINAL_RESIZE_ROWS
2908
3551
  ) {
2909
- return;
3552
+ return { status: 'resize-rejected', reason: 'invalid-dimensions', resized: false };
2910
3553
  }
2911
3554
 
2912
3555
  try {
2913
- const previousSize = this.lastResizeByAgent.get(agentId);
2914
- if (previousSize && previousSize.cols === nextCols && previousSize.rows === nextRows) {
2915
- return;
2916
- }
2917
-
2918
3556
  const engine = this.engineBridge.getEngine(agent.engineName);
2919
- if (!engine || !engine.resizeSession) return;
3557
+ if (!engine || !engine.resizeSession) {
3558
+ return { status: 'resize-rejected', reason: 'unsupported-engine', resized: false };
3559
+ }
2920
3560
 
2921
3561
  const result = await engine.resizeSession(agentId, nextCols, nextRows);
2922
- if (result && result.resized === false) {
3562
+ if (result && result.resized === false && result.reason === 'session-unavailable') {
2923
3563
  this.markAgentSessionDead(agentId, 'Session not available');
2924
- return;
2925
3564
  }
2926
- this.lastResizeByAgent.set(agentId, { cols: nextCols, rows: nextRows });
3565
+ if (result && result.status === 'resize-committed') {
3566
+ this.lastResizeByAgent.set(agentId, { cols: nextCols, rows: nextRows });
3567
+ }
3568
+ return result;
2927
3569
  } catch (error) {
2928
3570
  console.error('Failed to resize agent session:', error);
3571
+ return { status: 'resize-rejected', reason: 'resize-failed', resized: false };
2929
3572
  }
2930
3573
  }
2931
3574
 
2932
- async clearAgentSessionBuffer(agentId) {
3575
+ async clearAgentSessionBuffer(agentId, options = {}) {
2933
3576
  const agent = this.agents.get(agentId);
2934
3577
  if (!agent) return { cleared: false };
2935
3578
  if (isCodexAppServerAgent(agent) || isAcpAgent(agent) || isJsonCliAgent(agent)) return { cleared: false };
3579
+ if (options.expectedRuntimeEpoch && agent.runtimeEpoch !== options.expectedRuntimeEpoch) {
3580
+ return { cleared: false, reason: 'runtime-epoch-mismatch' };
3581
+ }
2936
3582
 
2937
3583
  try {
2938
3584
  const engine = this.engineBridge.getEngine(agent.engineName);
2939
3585
  if (!engine || !engine.clearBuffer) return { cleared: false };
2940
- const result = await engine.clearBuffer(agentId);
3586
+ const result = await engine.clearBuffer(agentId, options);
2941
3587
  if (result && result.cleared === false) {
2942
- this.markAgentSessionDead(agentId, 'Session not available');
3588
+ if (result.reason === 'session-unavailable') {
3589
+ this.markAgentSessionDead(agentId, 'Session not available');
3590
+ }
2943
3591
  return result;
2944
3592
  }
2945
- agent.output = '';
2946
- agent.previewText = '';
2947
- agent.previewSnapshot = null;
2948
- this.outputEvents.delete(agentId);
2949
- this.lastActivity.set(agentId, Date.now());
2950
- this.observeAgentStateChange(agentId, { force: true });
2951
- this.emit('update');
3593
+ // The ordered clear transition is the single metadata writer. Output
3594
+ // committed immediately after clear must not be erased by this RPC
3595
+ // response path racing the transition stream.
2952
3596
  return result || { cleared: true };
2953
3597
  } catch (error) {
2954
3598
  console.error('Failed to clear agent session buffer:', error);
@@ -2990,9 +3634,12 @@ class AgentManager extends EventEmitter {
2990
3634
  }
2991
3635
 
2992
3636
  const updates = {};
3637
+ let structuralUpdateChanged = false;
3638
+ let readUpdateChanged = false;
2993
3639
  if (typeof flags.pinned === 'boolean') {
2994
3640
  const wasPinned = agent.pinned === true;
2995
3641
  agent.pinned = flags.pinned;
3642
+ structuralUpdateChanged = structuralUpdateChanged || wasPinned !== agent.pinned;
2996
3643
  updates.pinned = agent.pinned;
2997
3644
  if (!wasPinned && agent.pinned) {
2998
3645
  agent.pinnedOrder = nextPinnedOrder(Array.from(this.agents.values()));
@@ -3000,22 +3647,36 @@ class AgentManager extends EventEmitter {
3000
3647
  updates.pinnedOrder = finiteOrder(agent.pinnedOrder);
3001
3648
  }
3002
3649
 
3650
+ if (
3651
+ typeof flags.readOutputEpoch === 'string'
3652
+ && typeof flags.readOutputSeq === 'number'
3653
+ && Number.isFinite(flags.readOutputSeq)
3654
+ ) {
3655
+ const result = this.markAgentReadOutputCut(agentId, flags.readOutputEpoch, flags.readOutputSeq);
3656
+ if (result.error) return result;
3657
+ updates.readOutputEpoch = result.readOutputEpoch;
3658
+ updates.readOutputSeq = result.readOutputSeq;
3659
+ readUpdateChanged = readUpdateChanged || result.changed;
3660
+ }
3661
+
3003
3662
  if (typeof flags.unread === 'boolean') {
3004
3663
  const result = flags.unread
3005
3664
  ? this.markAgentUnreadCursor(agentId)
3006
- : this.markAgentReadCursor(agentId);
3665
+ : this.markAgentReadCursor(agentId, undefined, { emitUpdate: false });
3007
3666
  if (result.error) return result;
3008
3667
  updates.unread = result.unread;
3009
3668
  updates.attentionSeq = result.attentionSeq;
3010
3669
  updates.readAttentionSeq = result.readAttentionSeq;
3670
+ readUpdateChanged = readUpdateChanged || result.changed === true;
3011
3671
  }
3012
3672
 
3013
3673
  if (typeof flags.readAttentionSeq === 'number' && Number.isFinite(flags.readAttentionSeq)) {
3014
- const result = this.markAgentReadCursor(agentId, flags.readAttentionSeq);
3674
+ const result = this.markAgentReadCursor(agentId, flags.readAttentionSeq, { emitUpdate: false });
3015
3675
  if (result.error) return result;
3016
3676
  updates.unread = result.unread;
3017
3677
  updates.attentionSeq = result.attentionSeq;
3018
3678
  updates.readAttentionSeq = result.readAttentionSeq;
3679
+ readUpdateChanged = readUpdateChanged || result.changed === true;
3019
3680
  }
3020
3681
 
3021
3682
  if (flags.archived === true) {
@@ -3026,18 +3687,38 @@ class AgentManager extends EventEmitter {
3026
3687
  }
3027
3688
 
3028
3689
  if (flags.archived === false) {
3690
+ structuralUpdateChanged = structuralUpdateChanged || agent.archived === true || agent.archivedAt !== null;
3029
3691
  agent.archived = false;
3030
3692
  agent.archivedAt = null;
3031
3693
  updates.archived = agent.archived;
3032
3694
  updates.archivedAt = agent.archivedAt;
3033
3695
  }
3034
3696
 
3035
- if (typeof flags.pinned === 'boolean') {
3697
+ if (
3698
+ typeof flags.pinned === 'boolean'
3699
+ || (typeof flags.readOutputEpoch === 'string' && typeof flags.readOutputSeq === 'number')
3700
+ ) {
3036
3701
  this.ensurePersistentAgentSession(agent);
3037
3702
  this.updateEngineProviderSessionMetadata(agent);
3038
3703
  }
3039
- this.emit('update');
3040
- return { agentId, ...updates };
3704
+ if (structuralUpdateChanged) {
3705
+ this.emit('update');
3706
+ } else if (readUpdateChanged) {
3707
+ this.emit('agent-read', {
3708
+ agentId,
3709
+ unread: agent.unread === true,
3710
+ attentionSeq: finiteNonNegativeInteger(agent.attentionSeq),
3711
+ readAttentionSeq: finiteNonNegativeInteger(agent.readAttentionSeq),
3712
+ readOutputEpoch: typeof agent.readOutputEpoch === 'string' ? agent.readOutputEpoch : '',
3713
+ readOutputSeq: finiteNumberOrNull(agent.readOutputSeq),
3714
+ });
3715
+ }
3716
+ return {
3717
+ agentId,
3718
+ ...updates,
3719
+ changed: structuralUpdateChanged || readUpdateChanged,
3720
+ requiresState: structuralUpdateChanged,
3721
+ };
3041
3722
  }
3042
3723
 
3043
3724
  reorderProjectAgent(agentId, { beforeAgentId = '', afterAgentId = '' } = {}) {
@@ -3098,7 +3779,7 @@ class AgentManager extends EventEmitter {
3098
3779
 
3099
3780
  if (
3100
3781
  agent.providerSessionProvider === 'codex'
3101
- && agent.codexRuntimeMode === 'app-server'
3782
+ && runtimeKind(agent) === 'app-server'
3102
3783
  ) {
3103
3784
  return this.updateCodexAppServerPermissionMode(agentId, mode);
3104
3785
  }
@@ -3109,7 +3790,8 @@ class AgentManager extends EventEmitter {
3109
3790
  async updateCodexAppServerPermissionMode(agentId, mode) {
3110
3791
  const agent = this.agents.get(agentId);
3111
3792
  if (!agent) return { error: 'Agent not found' };
3112
- if (agent.providerSessionProvider !== 'codex' || agent.codexRuntimeMode !== 'app-server') {
3793
+ const runtime = runtimeBindingOf(agent, 'app-server');
3794
+ if (agent.providerSessionProvider !== 'codex' || !runtime) {
3113
3795
  return { error: 'Agent is not using the Codex App Server runtime' };
3114
3796
  }
3115
3797
 
@@ -3129,7 +3811,7 @@ class AgentManager extends EventEmitter {
3129
3811
  }
3130
3812
 
3131
3813
  agent.launchPermissionMode = nextMode;
3132
- agent.codexAppServerError = '';
3814
+ runtime.error = '';
3133
3815
  this.updateEngineProviderSessionMetadata(agent);
3134
3816
  this.ensurePersistentAgentSession(agent, { launchPermissionMode: nextMode });
3135
3817
  this.emit('update');
@@ -3185,29 +3867,30 @@ class AgentManager extends EventEmitter {
3185
3867
  const nextMode = ['terminal', 'json', 'acp'].includes(mode) ? mode : '';
3186
3868
  if (!nextMode) return { error: 'Unsupported Agent runtime mode' };
3187
3869
  const provider = agent.providerSessionProvider || '';
3870
+ const currentKind = runtimeKind(agent);
3871
+ const currentMode = ['acp', 'json'].includes(currentKind) ? currentKind : 'terminal';
3188
3872
  const leavesCodexAppServer = provider === 'codex'
3189
- && agent.codexRuntimeMode === 'app-server'
3873
+ && currentKind === 'app-server'
3190
3874
  && nextMode === 'terminal';
3191
- if (agent.agentRuntimeMode === nextMode && !leavesCodexAppServer) {
3875
+ if (currentMode === nextMode && !leavesCodexAppServer) {
3192
3876
  return { agentId, agentRuntimeMode: nextMode };
3193
3877
  }
3194
- const turnActive = agent.agentRuntimeMode === 'acp'
3195
- ? ['working', 'waiting-for-permission', 'interrupting'].includes(agent.acpState || '')
3878
+ const turnActive = currentKind === 'acp'
3879
+ ? ['working', 'waiting-for-permission', 'interrupting'].includes(runtimeBindingOf(agent, 'acp').state)
3196
3880
  : this.isAgentAttentionTurnActive(agent);
3197
3881
  if (turnActive) {
3198
3882
  return { error: 'Interrupt the active Agent turn before switching Chat and Terminal.' };
3199
3883
  }
3200
- const supportedProviders = nextMode === 'json' ? ['codex', 'opencode'] : ['codex', 'claude', 'opencode', 'qoder'];
3201
- if (!supportedProviders.includes(provider)) {
3884
+ if (!providerSupportsRuntime(provider, nextMode)) {
3202
3885
  return { error: `Agent does not support the ${nextMode.toUpperCase()} runtime` };
3203
3886
  }
3204
3887
  const sessionId = String(agent.providerSessionId || '').trim();
3205
3888
  const canStartFreshAcpSession = nextMode === 'acp'
3206
- && agent.agentRuntimeMode === 'terminal'
3889
+ && currentMode === 'terminal'
3207
3890
  && agent.terminalInputReceived !== true
3208
3891
  && (
3209
3892
  agent.providerSessionTemporary === true
3210
- || ['codex-temporary', 'claude-session-id', 'qoder-session-id'].includes(agent.providerSessionSource || '')
3893
+ || isFreshAcpSessionSource(provider, agent.providerSessionSource || '')
3211
3894
  );
3212
3895
  if (!isSafeProviderSessionId(sessionId) && !canStartFreshAcpSession) {
3213
3896
  return { error: 'Runtime switching requires a resumable provider session. Send the first message and try again.' };
@@ -3219,7 +3902,7 @@ class AgentManager extends EventEmitter {
3219
3902
  // fail. Historical/terminal sessions still use the provider lookup as a
3220
3903
  // stale-session guard.
3221
3904
  let liveAcpSession = false;
3222
- if (agent.agentRuntimeMode === 'acp' && this.acpRuntime && typeof this.acpRuntime.getSession === 'function') {
3905
+ if (currentKind === 'acp' && this.acpRuntime && typeof this.acpRuntime.getSession === 'function') {
3223
3906
  try {
3224
3907
  const snapshot = this.acpRuntime.getSession(agentId, { maxEntries: 0 });
3225
3908
  liveAcpSession = String(snapshot?.sessionId || '') === sessionId;
@@ -3239,7 +3922,8 @@ class AgentManager extends EventEmitter {
3239
3922
  const command = startsFreshAcpSession
3240
3923
  ? (agent.forkCommand || agent.command)
3241
3924
  : buildAgentSessionResumeCommand(provider, sessionId, {
3242
- cwd: agent.cwd || agent.projectWorkspace || '',
3925
+ cwd: effectiveAgentWorkspaceRoot(agent),
3926
+ providerHomePath: agent.providerHomePath || '',
3243
3927
  });
3244
3928
  if (!command) return { error: 'Failed to build provider resume command' };
3245
3929
  const preserved = {
@@ -3250,13 +3934,13 @@ class AgentManager extends EventEmitter {
3250
3934
  unread: agent.unread === true,
3251
3935
  jsonCliEvents: isJsonCliAgent(agent)
3252
3936
  ? this.jsonCliRuntime.getEvents(agentId)
3253
- : (Array.isArray(agent.jsonCliEvents) ? agent.jsonCliEvents : []),
3937
+ : (Array.isArray(agent.runtimeResumeState?.jsonEvents) ? agent.runtimeResumeState.jsonEvents : []),
3254
3938
  };
3255
3939
  const restartOptions = {
3256
3940
  wantsMain: agent.wantsMain === true,
3257
3941
  task: agent.task || agent.providerSessionTitle || '',
3258
3942
  workflowTemplate: agent.workflowTemplate || '',
3259
- projectWorkspace: agent.projectWorkspace || agent.cwd || '',
3943
+ projectWorkspace: effectiveAgentWorkspaceRoot(agent),
3260
3944
  source: startsFreshAcpSession
3261
3945
  ? 'ui-runtime-switch-fresh'
3262
3946
  : resumedAgentSource(provider, sessionId, agent.providerHomeId || ''),
@@ -3277,12 +3961,15 @@ class AgentManager extends EventEmitter {
3277
3961
  codexApprovalMode: agent.launchPermissionMode || undefined,
3278
3962
  jsonCliEvents: preserved.jsonCliEvents,
3279
3963
  runtimeSwitchVerifiedSessionId: startsFreshAcpSession ? '' : sessionId,
3964
+ ...(provider === 'codex' && !startsFreshAcpSession
3965
+ ? preserveCodexSessionProfileOptions()
3966
+ : {}),
3280
3967
  };
3281
- const originalMode = agent.agentRuntimeMode || 'terminal';
3968
+ const originalMode = currentMode;
3282
3969
  const originalOptions = {
3283
3970
  ...restartOptions,
3284
3971
  agentRuntimeMode: originalMode,
3285
- codexRuntimeMode: agent.codexRuntimeMode || 'cli',
3972
+ codexRuntimeMode: currentKind === 'app-server' ? 'app-server' : 'cli',
3286
3973
  acpStartFresh: false,
3287
3974
  };
3288
3975
  const startReplacement = options => new Promise(resolve => {
@@ -3295,7 +3982,7 @@ class AgentManager extends EventEmitter {
3295
3982
  try {
3296
3983
  const started = this.startAgent(
3297
3984
  command,
3298
- agent.cwd || agent.projectWorkspace || null,
3985
+ effectiveAgentWorkspaceRoot(agent) || null,
3299
3986
  (restartedAgentId, error) => finish(restartedAgentId, error),
3300
3987
  options
3301
3988
  );
@@ -3402,11 +4089,11 @@ class AgentManager extends EventEmitter {
3402
4089
  if (!hasResumableSession && !startsFreshCodexSession) {
3403
4090
  return { error: 'Permission changes require a resumable provider session. Try again after the session id is available.' };
3404
4091
  }
3405
-
3406
4092
  const command = startsFreshCodexSession
3407
4093
  ? 'codex'
3408
4094
  : buildAgentSessionResumeCommand(provider, sessionId, {
3409
- cwd: agent.cwd || agent.projectWorkspace || '',
4095
+ cwd: effectiveAgentWorkspaceRoot(agent),
4096
+ providerHomePath: agent.providerHomePath || '',
3410
4097
  });
3411
4098
  if (!command) {
3412
4099
  return { error: 'Failed to build provider resume command' };
@@ -3417,7 +4104,7 @@ class AgentManager extends EventEmitter {
3417
4104
  task: agent.task || agent.providerSessionTitle || '',
3418
4105
  workflowTemplate: agent.workflowTemplate || '',
3419
4106
  requiredCliVersion: provider === 'codex' ? (agent.requiredCliVersion || '') : '',
3420
- projectWorkspace: agent.projectWorkspace || agent.cwd || '',
4107
+ projectWorkspace: effectiveAgentWorkspaceRoot(agent),
3421
4108
  source: startsFreshCodexSession
3422
4109
  ? 'ui'
3423
4110
  : (resumedSessionFromSource(agent.source)
@@ -3435,6 +4122,9 @@ class AgentManager extends EventEmitter {
3435
4122
  projectOrder: finiteOrder(agent.projectOrder),
3436
4123
  pinnedOrder: finiteOrder(agent.pinnedOrder),
3437
4124
  ...(provider === 'codex' ? { codexApprovalMode: nextMode } : { claudePermissionMode: nextMode }),
4125
+ ...(provider === 'codex' && hasResumableSession
4126
+ ? preserveCodexSessionProfileOptions()
4127
+ : {}),
3438
4128
  };
3439
4129
  const preserved = {
3440
4130
  pinned: agent.pinned === true,
@@ -3458,7 +4148,7 @@ class AgentManager extends EventEmitter {
3458
4148
  }
3459
4149
 
3460
4150
  return new Promise((resolve) => {
3461
- const startResult = this.startAgent(command, agent.cwd || agent.projectWorkspace || null, (restartedAgentId, error) => {
4151
+ const startResult = this.startAgent(command, effectiveAgentWorkspaceRoot(agent) || null, (restartedAgentId, error) => {
3462
4152
  if (error) {
3463
4153
  this.emit('update');
3464
4154
  resolve({ error });
@@ -3513,23 +4203,26 @@ class AgentManager extends EventEmitter {
3513
4203
  : this.markAgentReadCursor(agentId);
3514
4204
  }
3515
4205
 
3516
- async createForkWorktree(workspace) {
4206
+ async resolveGitWorktreeSourceRoot(workspace) {
3517
4207
  const sourceWorkspace = this.expandWorkspacePath(workspace);
3518
4208
  if (!sourceWorkspace) {
3519
4209
  throw new Error('Source workspace is empty');
3520
4210
  }
3521
4211
 
3522
- let root;
3523
4212
  try {
3524
4213
  const { stdout } = await execFileAsync('git', ['-C', sourceWorkspace, 'rev-parse', '--show-toplevel'], {
3525
4214
  timeout: 15000,
3526
4215
  maxBuffer: 1024 * 1024,
3527
4216
  });
3528
- root = stdout.trim();
4217
+ return stdout.trim();
3529
4218
  } catch (error) {
3530
4219
  const message = error && error.stderr ? String(error.stderr).trim() : '';
3531
4220
  throw new Error(message || 'Source workspace is not inside a git repository', { cause: error });
3532
4221
  }
4222
+ }
4223
+
4224
+ async createForkWorktree(workspace) {
4225
+ const root = await this.resolveGitWorktreeSourceRoot(workspace);
3533
4226
 
3534
4227
  const parentDir = path.dirname(root);
3535
4228
  const baseName = path.basename(root);
@@ -3554,6 +4247,70 @@ class AgentManager extends EventEmitter {
3554
4247
  return target;
3555
4248
  }
3556
4249
 
4250
+ async createPermanentWorktree(workspace) {
4251
+ const root = await this.resolveGitWorktreeSourceRoot(workspace);
4252
+ const parentDir = path.dirname(root);
4253
+ const baseName = path.basename(root);
4254
+ const slug = timestampSlug();
4255
+ let suffix = 1;
4256
+
4257
+ while (suffix < 1000) {
4258
+ const suffixText = suffix === 1 ? '' : `-${suffix}`;
4259
+ const target = path.join(parentDir, `${baseName}-farming-worktree-${slug}${suffixText}`);
4260
+ const branch = `farming/worktree-${slug}${suffixText}`;
4261
+ let branchExists = false;
4262
+ try {
4263
+ await execFileAsync('git', ['-C', root, 'show-ref', '--verify', '--quiet', `refs/heads/${branch}`], {
4264
+ timeout: 15000,
4265
+ maxBuffer: 1024 * 1024,
4266
+ });
4267
+ branchExists = true;
4268
+ } catch (error) {
4269
+ if (error?.code !== 1) {
4270
+ const message = error && error.stderr ? String(error.stderr).trim() : '';
4271
+ throw new Error(message || 'Failed to inspect git branches', { cause: error });
4272
+ }
4273
+ }
4274
+
4275
+ if (!fs.existsSync(target) && !branchExists) {
4276
+ try {
4277
+ await execFileAsync('git', ['-C', root, 'worktree', 'add', '-b', branch, target, 'HEAD'], {
4278
+ timeout: 60000,
4279
+ maxBuffer: 1024 * 1024 * 4,
4280
+ });
4281
+ } catch (error) {
4282
+ const message = error && error.stderr ? String(error.stderr).trim() : '';
4283
+ throw new Error(message || 'Failed to create permanent git worktree', { cause: error });
4284
+ }
4285
+ return { workspace: target, branch, sourceWorkspace: root };
4286
+ }
4287
+ suffix += 1;
4288
+ }
4289
+
4290
+ throw new Error('Unable to allocate a permanent worktree name');
4291
+ }
4292
+
4293
+ async rollbackPermanentWorktree(created) {
4294
+ if (!created?.workspace || !created?.sourceWorkspace) return;
4295
+ try {
4296
+ await execFileAsync('git', ['-C', created.sourceWorkspace, 'worktree', 'remove', '--force', created.workspace], {
4297
+ timeout: 60000,
4298
+ maxBuffer: 1024 * 1024 * 4,
4299
+ });
4300
+ } catch {
4301
+ // Rollback is best-effort so the original create/persist failure remains visible.
4302
+ }
4303
+ if (!created.branch) return;
4304
+ try {
4305
+ await execFileAsync('git', ['-C', created.sourceWorkspace, 'branch', '-D', created.branch], {
4306
+ timeout: 30000,
4307
+ maxBuffer: 1024 * 1024 * 4,
4308
+ });
4309
+ } catch {
4310
+ // The worktree removal may already have pruned the newly created branch.
4311
+ }
4312
+ }
4313
+
3557
4314
  async inspectForkWorktreeProject(workspace) {
3558
4315
  const expanded = this.expandWorkspacePath(workspace);
3559
4316
  const resolvedWorkspace = expanded ? path.resolve(expanded) : '';
@@ -3608,7 +4365,7 @@ class AgentManager extends EventEmitter {
3608
4365
  const resolvedWorkspace = path.resolve(workspace);
3609
4366
  return Array.from(this.agents.values()).filter(agent => {
3610
4367
  if (!agent || agent.isMain) return false;
3611
- const agentWorkspace = this.expandWorkspacePath(agent.projectWorkspace || agent.cwd || '');
4368
+ const agentWorkspace = this.expandWorkspacePath(effectiveAgentWorkspaceRoot(agent));
3612
4369
  if (!agentWorkspace) return false;
3613
4370
  return path.resolve(agentWorkspace) === resolvedWorkspace;
3614
4371
  });
@@ -3621,7 +4378,7 @@ class AgentManager extends EventEmitter {
3621
4378
 
3622
4379
  const keysToRemove = new Set();
3623
4380
  agents.forEach(agent => {
3624
- const providerSessionKey = agent.providerSessionKey || this.providerSessionKey(
4381
+ const providerSessionKey = agent.providerSessionKey || mainPageAgentSessionKey(
3625
4382
  agent.providerSessionProvider,
3626
4383
  agent.providerSessionId,
3627
4384
  agent.providerHomeId || ''
@@ -3693,8 +4450,21 @@ class AgentManager extends EventEmitter {
3693
4450
  if (!agent) {
3694
4451
  return { error: 'Agent not found' };
3695
4452
  }
4453
+ if (!['same-worktree', 'new-worktree'].includes(mode)) {
4454
+ return { error: 'Unsupported fork mode' };
4455
+ }
4456
+
4457
+ const sourceWorkspace = effectiveAgentWorkspaceRoot(agent);
4458
+ const resumedSession = agent.providerSessionProvider
4459
+ && agent.providerSessionId
4460
+ && agent.providerSessionTemporary !== true
4461
+ ? { provider: agent.providerSessionProvider, providerHomeId: agent.providerHomeId || 'default', sessionId: agent.providerSessionId }
4462
+ : resumedSessionFromSource(agent.source);
4463
+ if (resumedSession?.provider === 'codex') {
4464
+ const availability = await this.ensureCodexSessionAvailableForFork(agent, resumedSession, sourceWorkspace);
4465
+ if (availability?.error) return availability;
4466
+ }
3696
4467
 
3697
- const sourceWorkspace = agent.projectWorkspace || agent.cwd;
3698
4468
  let targetWorkspace = sourceWorkspace;
3699
4469
  if (mode === 'new-worktree') {
3700
4470
  try {
@@ -3702,19 +4472,13 @@ class AgentManager extends EventEmitter {
3702
4472
  } catch (error) {
3703
4473
  return { error: error.message || 'Failed to create git worktree' };
3704
4474
  }
3705
- } else if (mode !== 'same-worktree') {
3706
- return { error: 'Unsupported fork mode' };
3707
4475
  }
3708
4476
 
3709
- const resumedSession = agent.providerSessionProvider
3710
- && agent.providerSessionId
3711
- && agent.providerSessionTemporary !== true
3712
- ? { provider: agent.providerSessionProvider, providerHomeId: agent.providerHomeId || 'default', sessionId: agent.providerSessionId }
3713
- : resumedSessionFromSource(agent.source);
3714
4477
  const forkCommand = resumedSession
3715
4478
  ? buildAgentSessionResumeCommand(resumedSession.provider, resumedSession.sessionId, {
3716
4479
  fork: true,
3717
4480
  cwd: targetWorkspace,
4481
+ providerHomePath: agent.providerHomePath || '',
3718
4482
  })
3719
4483
  : (agent.forkCommand || agent.command);
3720
4484
 
@@ -3741,10 +4505,62 @@ class AgentManager extends EventEmitter {
3741
4505
  source: mode === 'new-worktree' ? 'ui-fork-new-worktree' : 'ui-fork-same-worktree',
3742
4506
  providerHomeId: agent.providerHomeId || (resumedSession && resumedSession.providerHomeId) || '',
3743
4507
  providerHomePath: agent.providerHomePath || '',
4508
+ ...(resumedSession?.provider === 'codex'
4509
+ ? preserveCodexSessionProfileOptions()
4510
+ : {}),
3744
4511
  });
3745
4512
  });
3746
4513
  }
3747
4514
 
4515
+ async ensureCodexSessionAvailableForFork(agent, resumedSession, sourceWorkspace) {
4516
+ const providerHomeId = agent.providerHomeId || resumedSession.providerHomeId || 'default';
4517
+ const providerHomePath = agent.providerHomePath || '';
4518
+ const sessionId = resumedSession.sessionId;
4519
+ const unarchiveKey = `${providerHomePath || providerHomeId}:${sessionId}`;
4520
+ const inFlight = this.codexSessionUnarchiveInFlight.get(unarchiveKey);
4521
+ if (inFlight) return inFlight;
4522
+
4523
+ const unarchivePromise = (async () => {
4524
+ let session;
4525
+ try {
4526
+ session = await findAgentSession('codex', sessionId, {
4527
+ limit: 1000,
4528
+ providerLimit: 1000,
4529
+ scanLimit: 5000,
4530
+ providerHomeId,
4531
+ providerHomes: providerHomePath
4532
+ ? { codex: [{ id: providerHomeId, path: providerHomePath }] }
4533
+ : undefined,
4534
+ });
4535
+ } catch (error) {
4536
+ return {
4537
+ error: `Failed to inspect Codex session before forking: ${error && (error.message || error)}`,
4538
+ };
4539
+ }
4540
+ if (!session || session.archived !== true) return null;
4541
+
4542
+ const result = await this.unarchiveCodexSession(sessionId, {
4543
+ ...session,
4544
+ // Fork is an action on the live Farming Agent. Its current workspace is
4545
+ // authoritative even when the older provider history cwd no longer exists.
4546
+ cwd: sourceWorkspace || session.cwd || session.workspace,
4547
+ providerHomePath: session.providerHomePath || providerHomePath,
4548
+ });
4549
+ if (!result?.error) return null;
4550
+ return {
4551
+ error: `Codex session ${sessionId} is archived and could not be unarchived before forking: ${result.error}`,
4552
+ };
4553
+ })();
4554
+ this.codexSessionUnarchiveInFlight.set(unarchiveKey, unarchivePromise);
4555
+ try {
4556
+ return await unarchivePromise;
4557
+ } finally {
4558
+ if (this.codexSessionUnarchiveInFlight.get(unarchiveKey) === unarchivePromise) {
4559
+ this.codexSessionUnarchiveInFlight.delete(unarchiveKey);
4560
+ }
4561
+ }
4562
+ }
4563
+
3748
4564
  recordTaskHistory(agent, options = {}) {
3749
4565
  if (!agent || agent.id === this.mainAgentId) return;
3750
4566
  if (!isSupportedHistoryAgent(agent.forkCommand || agent.command || '')) return;
@@ -3758,7 +4574,7 @@ class AgentManager extends EventEmitter {
3758
4574
  agentId: agent.id,
3759
4575
  command: agent.command || '',
3760
4576
  cwd: agent.cwd || '',
3761
- projectWorkspace: agent.projectWorkspace || agent.cwd || '',
4577
+ projectWorkspace: effectiveAgentWorkspaceRoot(agent),
3762
4578
  title: agent.customTitle || agent.sessionTitle || agent.task || '',
3763
4579
  customTitle: agent.customTitle || '',
3764
4580
  task: agent.task || '',
@@ -3790,8 +4606,38 @@ class AgentManager extends EventEmitter {
3790
4606
  reason: 'manual-archive',
3791
4607
  recordHistory: !isEphemeralShellAgent(agent),
3792
4608
  });
4609
+ this.scheduleCodexSessionArchive(agent);
3793
4610
  return { agentId, archived: true, removed: true, removedMainPageSessionKeys };
3794
4611
  }
4612
+
4613
+ scheduleCodexSessionArchive(agent) {
4614
+ if (
4615
+ !agent
4616
+ || agent.providerSessionProvider !== 'codex'
4617
+ || !agent.providerSessionId
4618
+ || agent.providerSessionTemporary === true
4619
+ ) {
4620
+ return;
4621
+ }
4622
+
4623
+ const sessionId = agent.providerSessionId;
4624
+ const session = {
4625
+ cliVersion: agent.cliVersion || '',
4626
+ cwd: agent.cwd || '',
4627
+ workspace: agent.projectWorkspace || '',
4628
+ providerHomePath: agent.providerHomePath || '',
4629
+ };
4630
+ void Promise.resolve()
4631
+ .then(() => this.archiveCodexSession(sessionId, session))
4632
+ .then(result => {
4633
+ if (result?.error) {
4634
+ console.error(`Failed to archive Codex session ${sessionId}: ${result.error}`);
4635
+ }
4636
+ })
4637
+ .catch(error => {
4638
+ console.error(`Failed to archive Codex session ${sessionId}:`, error);
4639
+ });
4640
+ }
3795
4641
 
3796
4642
  async killAgent(agentId, options = {}) {
3797
4643
  const agent = this.agents.get(agentId);
@@ -3817,9 +4663,9 @@ class AgentManager extends EventEmitter {
3817
4663
  this.lastActivity.delete(agentId);
3818
4664
  this.lastActivityUpdate.delete(agentId);
3819
4665
  this.outputEvents.delete(agentId);
4666
+ this.agentUsageRateCache.delete(agentId);
3820
4667
  this.lastResizeByAgent.delete(agentId);
3821
- this.stopCodexProviderSessionResolver(agentId);
3822
- this.stopProviderSessionTitleResolver(agentId);
4668
+ this.providerSessionService.stop(agentId);
3823
4669
  if (this.codexAppServerRuntime && typeof this.codexAppServerRuntime.unregisterAgent === 'function') {
3824
4670
  this.codexAppServerRuntime.unregisterAgent(agentId);
3825
4671
  }
@@ -3873,19 +4719,20 @@ class AgentManager extends EventEmitter {
3873
4719
  return null;
3874
4720
  }
3875
4721
 
3876
- return agent.projectWorkspace || agent.cwd;
4722
+ return effectiveAgentWorkspaceRoot(agent);
3877
4723
  }
3878
4724
 
3879
4725
  getAgentProviderSession(agentId) {
3880
4726
  const agent = this.agents.get(agentId);
3881
4727
  if (!agent) return null;
4728
+ const appServer = runtimeBindingOf(agent, 'app-server');
3882
4729
  return {
3883
4730
  provider: agent.providerSessionProvider || '',
3884
4731
  sessionId: agent.providerSessionId || '',
3885
4732
  providerHomeId: agent.providerHomeId || '',
3886
4733
  providerHomePath: agent.providerHomePath || '',
3887
- codexAppServerHomePath: agent.codexAppServerHomePath || '',
3888
- codexRuntimeMode: agent.codexRuntimeMode || '',
4734
+ codexAppServerHomePath: appServer?.homePath || '',
4735
+ runtimeBinding: publicRuntimeBinding(agent),
3889
4736
  temporary: agent.providerSessionTemporary === true,
3890
4737
  title: agent.providerSessionTitle || '',
3891
4738
  };
@@ -3965,6 +4812,7 @@ class AgentManager extends EventEmitter {
3965
4812
  engineName: agent.engineName || '',
3966
4813
  cwd: agent.cwd,
3967
4814
  projectWorkspace: agent.projectWorkspace || '',
4815
+ gitWorktree: publicAgentGitWorktree(agent),
3968
4816
  status: sessionState && sessionState.status === 'exited'
3969
4817
  ? agent.status
3970
4818
  : (isLiveEngineSessionState(sessionState) ? 'running' : agent.status),
@@ -3989,27 +4837,9 @@ class AgentManager extends EventEmitter {
3989
4837
  providerSessionSource: agent.providerSessionSource || '',
3990
4838
  providerSessionResolvedAt: agent.providerSessionResolvedAt || null,
3991
4839
  providerSessionTitle: agent.providerSessionTitle || '',
4840
+ providerSessionWorkspace: agent.providerSessionWorkspace || '',
3992
4841
  terminalInputReceived: agent.terminalInputReceived === true,
3993
- codexRuntimeMode: agent.codexRuntimeMode || '',
3994
- agentRuntimeMode: agent.agentRuntimeMode || 'terminal',
3995
- acpState: agent.acpState || '',
3996
- acpError: agent.acpError || '',
3997
- acpStopReason: agent.acpStopReason || '',
3998
- acpPendingPermission: agent.acpPendingPermission || null,
3999
- acpPendingPermissions: Array.isArray(agent.acpPendingPermissions) ? agent.acpPendingPermissions : [],
4000
- acpPendingElicitation: agent.acpPendingElicitation || null,
4001
- acpPendingElicitations: Array.isArray(agent.acpPendingElicitations) ? agent.acpPendingElicitations : [],
4002
- acpActiveElicitations: Array.isArray(agent.acpActiveElicitations) ? agent.acpActiveElicitations : [],
4003
- acpSessionUpdatedAt: agent.acpSessionUpdatedAt || '',
4004
- acpSessionRevision: Number(agent.acpSessionRevision) || 0,
4005
- codexAppServerState: agent.codexAppServerState || '',
4006
- codexAppServerEndpoint: agent.codexAppServerEndpoint || '',
4007
- codexAppServerThreadId: agent.codexAppServerThreadId || '',
4008
- codexAppServerTurnId: agent.codexAppServerTurnId || '',
4009
- codexAppServerError: agent.codexAppServerError || '',
4010
- codexAppServerPendingRequestId: agent.codexAppServerPendingRequestId || '',
4011
- codexAppServerPendingRequestMethod: agent.codexAppServerPendingRequestMethod || '',
4012
- codexAppServerPendingRequest: agent.codexAppServerPendingRequest || null,
4842
+ runtimeBinding: publicRuntimeBinding(agent),
4013
4843
  forkedFromProviderSessionId: agent.forkedFromProviderSessionId || '',
4014
4844
  customTitle: agent.customTitle || '',
4015
4845
  pinned: agent.pinned === true,
@@ -4020,12 +4850,21 @@ class AgentManager extends EventEmitter {
4020
4850
  attentionUpdatedAt: finiteNumberOrNull(agent.attentionUpdatedAt),
4021
4851
  readAttentionAt: finiteNumberOrNull(agent.readAttentionAt),
4022
4852
  attentionReason: agent.attentionReason || '',
4853
+ attentionOutputEpoch: agent.attentionOutputEpoch || '',
4023
4854
  attentionOutputSeq: finiteNumberOrNull(agent.attentionOutputSeq),
4855
+ readOutputEpoch: agent.readOutputEpoch || '',
4856
+ readOutputSeq: finiteNumberOrNull(agent.readOutputSeq),
4024
4857
  unread: agentAttentionUnread(agent),
4025
4858
  archived: agent.archived === true,
4026
4859
  archivedAt: agent.archivedAt || null,
4027
4860
  sessionSource: this.getEngineSessionSource(agent.engineName),
4861
+ runtimeEpoch: sessionState && typeof sessionState.runtimeEpoch === 'string'
4862
+ ? sessionState.runtimeEpoch
4863
+ : (agent.runtimeEpoch || ''),
4028
4864
  outputSeq: sessionState && Number.isFinite(sessionState.outputSeq) ? sessionState.outputSeq : null,
4865
+ stateRevision: sessionState && Number.isFinite(sessionState.stateRevision)
4866
+ ? sessionState.stateRevision
4867
+ : null,
4029
4868
  isMain,
4030
4869
  activityLevel: isMain ? 'warm' : this.calculateActivityLevel(lastActivity, now),
4031
4870
  lastActivity,
@@ -4037,6 +4876,7 @@ class AgentManager extends EventEmitter {
4037
4876
  output: (sessionState && typeof sessionState.output === 'string') ? sessionState.output : fallbackOutput,
4038
4877
  renderOutput: (sessionState && typeof sessionState.renderOutput === 'string') ? sessionState.renderOutput : fallbackOutput,
4039
4878
  previewText,
4879
+ codexTerminalProfile: activeCodexTerminalProfile(agent, previewText),
4040
4880
  previewSnapshot: (sessionState && sessionState.previewSnapshot) || agent.previewSnapshot || null,
4041
4881
  previewCols: (sessionState && Number.isFinite(sessionState.previewCols) && sessionState.previewCols > 0)
4042
4882
  ? sessionState.previewCols
@@ -4044,10 +4884,28 @@ class AgentManager extends EventEmitter {
4044
4884
  previewRows: (sessionState && Number.isFinite(sessionState.previewRows) && sessionState.previewRows > 0)
4045
4885
  ? sessionState.previewRows
4046
4886
  : (agent.previewRows || 30),
4047
- usageRate: this.calculateAgentUsageRate(agent.id),
4887
+ usageRate: this.getAgentUsageRate(agent.id),
4048
4888
  };
4049
4889
  }
4050
4890
 
4891
+ getAgentUsageRate(agentId, options = {}) {
4892
+ const now = options.now || Date.now();
4893
+ const windowMs = options.windowMs || AGENT_USAGE_RATE_WINDOW_MS;
4894
+ const cached = this.agentUsageRateCache.get(agentId);
4895
+ if (
4896
+ cached
4897
+ && cached.windowMs === windowMs
4898
+ && now >= cached.sampledAt
4899
+ && now - cached.sampledAt < AGENT_USAGE_RATE_REFRESH_MS
4900
+ ) {
4901
+ return cached.value;
4902
+ }
4903
+
4904
+ const value = this.calculateAgentUsageRate(agentId, { now, windowMs });
4905
+ this.agentUsageRateCache.set(agentId, { windowMs, sampledAt: now, value });
4906
+ return value;
4907
+ }
4908
+
4051
4909
  calculateAgentUsageRate(agentId, options = {}) {
4052
4910
  const now = options.now || Date.now();
4053
4911
  const windowMs = options.windowMs || AGENT_USAGE_RATE_WINDOW_MS;
@@ -4084,7 +4942,7 @@ class AgentManager extends EventEmitter {
4084
4942
  cwd: agent.cwd,
4085
4943
  isMain: this.isMainAgentRecord(agent.id, agent),
4086
4944
  status: agent.status,
4087
- usageRate: this.calculateAgentUsageRate(agent.id, { now, windowMs }),
4945
+ usageRate: this.getAgentUsageRate(agent.id, { now, windowMs }),
4088
4946
  }));
4089
4947
  const totalOutputBytes = agents.reduce((sum, agent) => sum + agent.usageRate.outputBytes, 0);
4090
4948
  const estimatedOutputTokens = agents.reduce((sum, agent) => sum + agent.usageRate.estimatedOutputTokens, 0);
@@ -4125,13 +4983,18 @@ class AgentManager extends EventEmitter {
4125
4983
  command: agent.command,
4126
4984
  engineName: agent.engineName || '',
4127
4985
  cwd: agent.cwd,
4128
- projectWorkspace: agent.projectWorkspace || '',
4986
+ projectWorkspace: canonicalWorkspacePath(agent.projectWorkspace || ''),
4987
+ gitWorktree: publicAgentGitWorktree(agent),
4129
4988
  output: agent.output.slice(-2000),
4130
4989
  previewText: agent.previewText || '',
4990
+ codexTerminalProfile: activeCodexTerminalProfile(agent, agent.previewText || ''),
4131
4991
  previewCols: agent.previewCols || 80,
4132
4992
  previewRows: agent.previewRows || 30,
4133
4993
  sessionTitle: agent.sessionTitle || '',
4134
4994
  sessionSource: this.getEngineSessionSource(agent.engineName),
4995
+ runtimeEpoch: agent.runtimeEpoch || '',
4996
+ outputSeq: finiteNumberOrNull(agent.lastOutputSeq),
4997
+ stateRevision: finiteNumberOrNull(agent.stateRevision),
4135
4998
  status: agent.status,
4136
4999
  terminalBusy,
4137
5000
  terminalStatus,
@@ -4155,33 +5018,11 @@ class AgentManager extends EventEmitter {
4155
5018
  providerSessionSource: agent.providerSessionSource || '',
4156
5019
  providerSessionResolvedAt: agent.providerSessionResolvedAt || null,
4157
5020
  providerSessionTitle: agent.providerSessionTitle || '',
5021
+ providerSessionWorkspace: agent.providerSessionWorkspace || '',
5022
+ providerCapabilities: providerCapabilities(agent.providerSessionProvider),
4158
5023
  terminalInputReceived: agent.terminalInputReceived === true,
4159
- codexRuntimeMode: agent.codexRuntimeMode || '',
4160
- agentRuntimeMode: agent.agentRuntimeMode || 'terminal',
4161
- jsonCliState: agent.jsonCliState || '',
4162
- jsonCliError: agent.jsonCliError || '',
4163
- jsonCliTranscriptUpdatedAt: agent.jsonCliTranscriptUpdatedAt || '',
4164
- acpState: agent.acpState || '',
4165
- acpError: agent.acpError || '',
4166
- acpStopReason: agent.acpStopReason || '',
4167
- acpPendingPermission: agent.acpPendingPermission || null,
4168
- acpPendingPermissions: Array.isArray(agent.acpPendingPermissions) ? agent.acpPendingPermissions : [],
4169
- acpPendingElicitation: agent.acpPendingElicitation || null,
4170
- acpPendingElicitations: Array.isArray(agent.acpPendingElicitations) ? agent.acpPendingElicitations : [],
4171
- acpActiveElicitations: Array.isArray(agent.acpActiveElicitations) ? agent.acpActiveElicitations : [],
4172
- acpSessionUpdatedAt: agent.acpSessionUpdatedAt || '',
4173
- acpSessionRevision: Number(agent.acpSessionRevision) || 0,
4174
- codexAppServerState: agent.codexAppServerState || '',
4175
- codexAppServerEndpoint: agent.codexAppServerEndpoint || '',
4176
- codexAppServerThreadId: agent.codexAppServerThreadId || '',
4177
- codexAppServerTurnId: agent.codexAppServerTurnId || '',
4178
- codexAppServerError: agent.codexAppServerError || '',
4179
- codexAppServerPendingRequestId: agent.codexAppServerPendingRequestId || '',
4180
- codexAppServerPendingRequestMethod: agent.codexAppServerPendingRequestMethod || '',
4181
- codexAppServerPendingRequest: agent.codexAppServerPendingRequest || null,
4182
- codexAppServerNotice: agent.codexAppServerNotice || null,
4183
- codexAppServerGoal: agent.codexAppServerGoal || null,
4184
- codexCliObserverDeferred: agent.codexCliObserverDeferred === true,
5024
+ runtimeBinding: publicRuntimeBinding(agent),
5025
+ runtimeObservation: deriveRuntimeObservation(agent),
4185
5026
  forkedFromProviderSessionId: agent.forkedFromProviderSessionId || '',
4186
5027
  restartedFromAgentId: agent.restartedFromAgentId || '',
4187
5028
  restartedFromAgentIds: Array.isArray(agent.restartedFromAgentIds) ? agent.restartedFromAgentIds : [],
@@ -4195,11 +5036,14 @@ class AgentManager extends EventEmitter {
4195
5036
  attentionUpdatedAt: finiteNumberOrNull(agent.attentionUpdatedAt),
4196
5037
  readAttentionAt: finiteNumberOrNull(agent.readAttentionAt),
4197
5038
  attentionReason: agent.attentionReason || '',
5039
+ attentionOutputEpoch: agent.attentionOutputEpoch || '',
4198
5040
  attentionOutputSeq: finiteNumberOrNull(agent.attentionOutputSeq),
5041
+ readOutputEpoch: agent.readOutputEpoch || '',
5042
+ readOutputSeq: finiteNumberOrNull(agent.readOutputSeq),
4199
5043
  unread: agentAttentionUnread(agent),
4200
5044
  archived: agent.archived === true,
4201
5045
  archivedAt: agent.archivedAt || null,
4202
- canForkNewWorktree: this.canCreateForkWorktree(agent.projectWorkspace || agent.cwd || ''),
5046
+ canForkNewWorktree: this.canCreateForkWorktree(effectiveAgentWorkspaceRoot(agent)),
4203
5047
  startedAt: agent.startedAt || null,
4204
5048
  exitedAt: agent.exitedAt || null,
4205
5049
  // Main agent is exempt from activity/attention/zombie scoring
@@ -4207,7 +5051,7 @@ class AgentManager extends EventEmitter {
4207
5051
  lastActivity,
4208
5052
  attentionScore: isMain ? 0 : this.calculateAttentionScore(id, now),
4209
5053
  isZombie: isMain ? false : this.isZombie(id, now),
4210
- usageRate: this.calculateAgentUsageRate(id, { now })
5054
+ usageRate: this.getAgentUsageRate(id, { now })
4211
5055
  });
4212
5056
  }
4213
5057
 
@@ -4280,6 +5124,10 @@ class AgentManager extends EventEmitter {
4280
5124
  this.on('update', callback);
4281
5125
  }
4282
5126
 
5127
+ onAgentActivity(callback) {
5128
+ this.on('agent-activity', callback);
5129
+ }
5130
+
4283
5131
  onSessionStream(callback) {
4284
5132
  this.on('session-stream', callback);
4285
5133
  }
@@ -4295,17 +5143,20 @@ class AgentManager extends EventEmitter {
4295
5143
  continue;
4296
5144
  }
4297
5145
 
5146
+ const terminalStatus = deriveAgentTerminalStatus(agent, {
5147
+ previewText: agent.previewText || '',
5148
+ title: agent.sessionTitle || '',
5149
+ terminalBusy: typeof agent.terminalBusy === 'boolean' ? agent.terminalBusy : null,
5150
+ });
4298
5151
  previews.push({
4299
5152
  agentId: agent.id,
4300
5153
  previewText: agent.previewText || '',
4301
5154
  cols: agent.previewCols || 80,
4302
5155
  rows: agent.previewRows || 30,
4303
5156
  previewSnapshot: agent.previewSnapshot || null,
4304
- terminalStatus: deriveAgentTerminalStatus(agent, {
4305
- previewText: agent.previewText || '',
4306
- title: agent.sessionTitle || '',
4307
- terminalBusy: typeof agent.terminalBusy === 'boolean' ? agent.terminalBusy : null,
4308
- }),
5157
+ codexTerminalProfile: activeCodexTerminalProfile(agent, agent.previewText || ''),
5158
+ terminalStatus,
5159
+ runtimeObservation: deriveRuntimeObservation({ ...agent, terminalStatus }),
4309
5160
  });
4310
5161
  }
4311
5162