farming-code 2.2.7 → 2.2.11

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 (102) hide show
  1. package/README.md +110 -199
  2. package/README.zh_cn.md +106 -237
  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 +1496 -737
  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 +45 -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 +30 -0
  20. package/backend/control-api.js +192 -17
  21. package/backend/farming-net-pass.js +285 -0
  22. package/backend/farming-net-registry.js +112 -0
  23. package/backend/farming-net-server.js +298 -0
  24. package/backend/farming-session-store.js +5 -13
  25. package/backend/git-worktree-info.js +181 -0
  26. package/backend/local-session-engine.js +411 -186
  27. package/backend/main-page-session.js +5 -2
  28. package/backend/native-pty-controller-generation.js +106 -0
  29. package/backend/native-pty-host-client.js +275 -7
  30. package/backend/native-pty-host-identity.js +86 -0
  31. package/backend/native-pty-host.js +813 -114
  32. package/backend/native-session-engine.js +100 -28
  33. package/backend/packaged-node-pty.js +22 -2
  34. package/backend/provider-adapters.js +253 -0
  35. package/backend/provider-session-service.js +241 -0
  36. package/backend/runtime-observation.js +81 -0
  37. package/backend/server.js +294 -86
  38. package/backend/session-engine-bridge.js +21 -2
  39. package/backend/session-engine-router.js +1 -1
  40. package/backend/session-engine.js +1 -1
  41. package/backend/session-stream-protocol.js +185 -0
  42. package/backend/storage-layout.js +55 -0
  43. package/backend/terminal-attach-checkpoint.js +74 -0
  44. package/backend/terminal-exit-quiescence.js +39 -0
  45. package/backend/terminal-reducer-flow-control.js +97 -0
  46. package/backend/terminal-screen-state.js +11 -2
  47. package/backend/terminal-screen-worker-pool.js +59 -6
  48. package/backend/terminal-screen-worker-thread.js +97 -57
  49. package/backend/terminal-screen-worker.js +133 -51
  50. package/backend/terminal-state-serialization.js +127 -0
  51. package/backend/terminal-status.js +23 -4
  52. package/backend/usage-monitor.js +81 -4
  53. package/backend/workspace-directory.js +152 -0
  54. package/backend/workspace-file-router.js +182 -76
  55. package/backend/workspace-file-service.js +319 -4
  56. package/backend/workspace-root-registry.js +164 -0
  57. package/dist/assets/App-CHYpgECZ.js +208 -0
  58. package/dist/assets/{FileEditorMarkdownPreview-elKWc8Im.js → FileEditorMarkdownPreview--Cri80nQ.js} +92 -92
  59. package/dist/assets/FileEditorPane-DGJlEKdf.js +2 -0
  60. package/dist/assets/IconGlyphs-Cc14sepw.js +1 -0
  61. package/dist/assets/ProjectFilesSection-Cz1wG4NT.js +12 -0
  62. package/dist/assets/{ReviewPage-BaXu1ZdX.js → ReviewPage-LonRc6Fz.js} +1 -1
  63. package/dist/assets/code-dark-B1pAtO_P.css +1 -0
  64. package/dist/assets/file-icons-Bw2qd5iT.js +1 -0
  65. package/dist/assets/{index-B8SHldPE.js → index-miO2r9IZ.js} +3 -3
  66. package/dist/assets/main-DNe0jPw-.css +1 -0
  67. package/dist/assets/workspace-editor-model-BQol4qbA.js +1 -0
  68. package/dist/assets/workspace-editor-monaco-5gFbeDPF.js +1 -0
  69. package/dist/assets/workspace-editor-monaco-xPv8JzHH.js +4 -0
  70. package/dist/assets/workspace-view-state-DvYG_9PH.js +7 -0
  71. package/dist/assets/workspace-working-copy-D8-s_Sgh.js +1 -0
  72. package/dist/index.html +1 -1
  73. package/frontend/farming-net/app.css +625 -0
  74. package/frontend/farming-net/app.js +268 -0
  75. package/frontend/farming-net/index.html +86 -0
  76. package/frontend/reading-anchor.js +198 -0
  77. package/frontend/session-bridge.js +12 -3
  78. package/frontend/session-modal-bridge.js +5 -12
  79. package/frontend/skins/crt/app.js +1976 -793
  80. package/frontend/skins/crt/index.html +313 -23
  81. package/frontend/skins/crt/styles/billing.css +294 -223
  82. package/frontend/skins/crt/styles/monochrome-green.css +7 -2
  83. package/frontend/terminal-replay.js +372 -0
  84. package/package.json +10 -3
  85. package/shared/browser-protocol.d.ts +5 -0
  86. package/shared/browser-protocol.js +130 -0
  87. package/dist/assets/App-iA2BU9P6.js +0 -124
  88. package/dist/assets/FileEditorPane-RWiFD2cq.js +0 -5
  89. package/dist/assets/IconGlyphs-DfL0EBnj.js +0 -1
  90. package/dist/assets/ProjectFilesSection-Q4PDsWmM.js +0 -12
  91. package/dist/assets/code-dark-CDkOQAtK.css +0 -1
  92. package/dist/assets/file-icons-EFUGSSwf.js +0 -1
  93. package/dist/assets/main-DpYuBonn.css +0 -1
  94. package/dist/assets/qoder-C9LmmOSf.svg +0 -1
  95. package/dist/assets/qoder-Cf9gl0Y5.svg +0 -1
  96. package/dist/assets/qoder-gHCinseV.svg +0 -1
  97. package/dist/assets/workspace-view-state-CTyDzk2D.js +0 -1
  98. package/dist/assets/zsh-CLpveKlF.svg +0 -1
  99. package/dist/assets/zsh-FxSpMPbz.svg +0 -1
  100. /package/dist/assets/{api-D1lyBYIQ.js → api-D8nyOEbz.js} +0 -0
  101. /package/dist/assets/{core-ZlAPicox.js → core-D0LFJkDt.js} +0 -0
  102. /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 { 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,22 @@ 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');
35
65
 
36
66
  const SESSION_OUTPUT_LIMIT = 10000;
37
67
  const AGENT_USAGE_RATE_WINDOW_MS = 5 * 60 * 1000;
68
+ const AGENT_USAGE_RATE_REFRESH_MS = 5 * 1000;
38
69
  const ACTIVITY_UPDATE_INTERVAL_MS = 1000;
39
70
  const ACTIVITY_HOT_SEC = 30 * 60;
40
71
  const ACTIVITY_WARM_SEC = 3 * 60 * 60;
41
72
  const ACTIVITY_COOL_SEC = 12 * 60 * 60;
42
73
  const ZOMBIE_IDLE_MS = 72 * 60 * 60 * 1000;
43
74
  const ZOMBIE_SWEEP_INTERVAL_MS = 60 * 1000;
44
- const INPUT_SESSION_RETRY_DELAYS_MS = [25, 50, 100, 180, 300, 500];
45
75
  const MISSING_ENGINE_SESSION_STARTUP_GRACE_MS = 5000;
46
76
  const MIN_TERMINAL_RESIZE_COLS = 40;
47
77
  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
78
  const AGENT_DISCOVERY_CACHE_MAX_AGE_MS = 3_000;
52
79
  const SHELL_PROMPT_ENV_KEYS = [
53
80
  'PS1',
@@ -66,12 +93,107 @@ function trimSessionOutput(output) {
66
93
  return text.length > SESSION_OUTPUT_LIMIT ? text.slice(-SESSION_OUTPUT_LIMIT) : text;
67
94
  }
68
95
 
96
+ function terminalStateUpdateDisposition(agent, runtimeEpoch, outputSeq, stateRevision) {
97
+ const currentEpoch = typeof agent.runtimeEpoch === 'string' ? agent.runtimeEpoch : '';
98
+ const nextEpoch = typeof runtimeEpoch === 'string' ? runtimeEpoch : '';
99
+ if (currentEpoch && nextEpoch && currentEpoch !== nextEpoch) {
100
+ const relation = compareNativePtyRuntimeEpochs(nextEpoch, currentEpoch);
101
+ return relation === 1 ? 'new-epoch' : 'stale';
102
+ }
103
+ if (currentEpoch && !nextEpoch) return 'unversioned';
104
+ if (!currentEpoch || !nextEpoch) return 'current';
105
+
106
+ const currentRevision = Number(agent.stateRevision);
107
+ const nextRevision = Number(stateRevision);
108
+ if (Number.isFinite(currentRevision) && Number.isFinite(nextRevision)) {
109
+ if (nextRevision < currentRevision) return 'stale';
110
+ if (nextRevision === currentRevision) {
111
+ const currentOutputSeq = Number(agent.lastOutputSeq);
112
+ const nextOutputSeq = Number(outputSeq);
113
+ return Number.isFinite(nextOutputSeq)
114
+ && Number.isFinite(currentOutputSeq)
115
+ && nextOutputSeq === currentOutputSeq
116
+ ? 'duplicate'
117
+ : 'stale';
118
+ }
119
+ }
120
+ const currentOutputSeq = Number(agent.lastOutputSeq);
121
+ const nextOutputSeq = Number(outputSeq);
122
+ if (Number.isFinite(currentOutputSeq) && Number.isFinite(nextOutputSeq) && nextOutputSeq < currentOutputSeq) {
123
+ return 'stale';
124
+ }
125
+ return 'current';
126
+ }
127
+
128
+ function terminalRuntimeEventMatches(agent, runtimeEpoch) {
129
+ const currentEpoch = typeof agent?.runtimeEpoch === 'string' ? agent.runtimeEpoch : '';
130
+ if (!currentEpoch) return true;
131
+ return typeof runtimeEpoch === 'string' && runtimeEpoch === currentEpoch;
132
+ }
133
+
134
+ function setPendingTerminalStartSyncCut(agent, runtimeEpoch, outputSeq, stateRevision) {
135
+ if (
136
+ typeof runtimeEpoch !== 'string' || !runtimeEpoch ||
137
+ !Number.isFinite(outputSeq) ||
138
+ !Number.isFinite(stateRevision)
139
+ ) {
140
+ delete agent.pendingTerminalStartSyncCut;
141
+ return;
142
+ }
143
+ agent.pendingTerminalStartSyncCut = {
144
+ runtimeEpoch,
145
+ outputSeq,
146
+ stateRevision,
147
+ };
148
+ }
149
+
150
+ function consumesPendingTerminalStartSyncCut(agent, runtimeEpoch, outputSeq, stateRevision) {
151
+ const pending = agent?.pendingTerminalStartSyncCut;
152
+ return Boolean(
153
+ pending &&
154
+ pending.runtimeEpoch === runtimeEpoch &&
155
+ pending.outputSeq === outputSeq &&
156
+ pending.stateRevision === stateRevision &&
157
+ agent.runtimeEpoch === runtimeEpoch &&
158
+ agent.lastOutputSeq === outputSeq &&
159
+ agent.stateRevision === stateRevision
160
+ );
161
+ }
162
+
163
+ function clearPendingTerminalStartSyncCut(agent) {
164
+ if (agent) delete agent.pendingTerminalStartSyncCut;
165
+ }
166
+
167
+ function applyTerminalStateCursor(agent, runtimeEpoch, outputSeq, stateRevision, disposition) {
168
+ if (disposition === 'stale' || disposition === 'duplicate' || disposition === 'unversioned') return false;
169
+ if (disposition === 'new-epoch') {
170
+ agent.lastOutputSeq = 0;
171
+ agent.stateRevision = 0;
172
+ }
173
+ if (typeof runtimeEpoch === 'string' && runtimeEpoch) agent.runtimeEpoch = runtimeEpoch;
174
+ if (Number.isFinite(outputSeq)) agent.lastOutputSeq = outputSeq;
175
+ if (Number.isFinite(stateRevision)) agent.stateRevision = stateRevision;
176
+ return true;
177
+ }
178
+
69
179
  function hasResumeArg(args) {
70
180
  return Array.isArray(args)
71
181
  ? args.some(arg => arg === '--resume' || (typeof arg === 'string' && arg.startsWith('--resume=')))
72
182
  : false;
73
183
  }
74
184
 
185
+ function codexCommandContinuesSession(command) {
186
+ const parts = parseCommand(command);
187
+ return path.basename(parts[0] || '') === 'codex'
188
+ && parts.slice(1).some(arg => arg === 'resume' || arg === 'fork');
189
+ }
190
+
191
+ function preserveCodexSessionProfileOptions() {
192
+ return {
193
+ preserveProviderSessionProfile: true,
194
+ };
195
+ }
196
+
75
197
  function isSameOrDescendantPath(root, target) {
76
198
  const relative = path.relative(root, target);
77
199
  return relative === '' || Boolean(relative) && !relative.startsWith('..') && !path.isAbsolute(relative);
@@ -90,29 +212,27 @@ function agentProgramName(command) {
90
212
  }
91
213
 
92
214
  function agentHomeProviderForProgram(command) {
93
- const program = agentProgramName(command).toLowerCase();
94
- if (program === 'qodercli') return 'qoder';
95
- return ['codex', 'claude', 'opencode', 'qoder'].includes(program) ? program : '';
215
+ return providerForProgram(agentProgramName(command));
96
216
  }
97
217
 
98
218
  function hasUsableCodexAppServerHome(metadata) {
99
- const homePath = String(metadata?.codexAppServerHomePath || '').trim();
219
+ const homePath = String(runtimeBindingOf(metadata, 'app-server')?.homePath || '').trim();
100
220
  return Boolean(homePath) && fs.existsSync(homePath);
101
221
  }
102
222
 
103
223
  function shouldRecoverAsCodexAppServer(metadata) {
104
224
  const provider = metadata?.providerSessionProvider || metadata?.provider || '';
105
225
  return provider === 'codex'
106
- && metadata?.codexRuntimeMode === 'app-server'
226
+ && runtimeKind(metadata) === 'app-server'
107
227
  && hasUsableCodexAppServerHome(metadata);
108
228
  }
109
229
 
110
230
  function isJsonCliAgent(agent) {
111
- return agent && agent.agentRuntimeMode === 'json';
231
+ return runtimeKind(agent) === 'json';
112
232
  }
113
233
 
114
234
  function isAcpAgent(agent) {
115
- return agent && agent.agentRuntimeMode === 'acp';
235
+ return runtimeKind(agent) === 'acp';
116
236
  }
117
237
 
118
238
  function isShellProgram(command) {
@@ -123,6 +243,68 @@ function isEphemeralShellAgent(agent) {
123
243
  return agent && isShellProgram(agent.forkCommand || agent.command || '');
124
244
  }
125
245
 
246
+ function hasSubmittedTerminalInput(input) {
247
+ const parts = Array.isArray(input) ? input : [input];
248
+ return parts.some(part => {
249
+ // Composer paste parts carry draft text separately from the trailing Enter.
250
+ // Newlines inside that draft must not independently materialize a session.
251
+ if (part && typeof part === 'object' && part.type === 'paste') return false;
252
+ const text = String(part || '');
253
+ // xterm wraps native paste payloads so embedded newlines remain draft text.
254
+ // Remove complete bracketed-paste spans before looking for a submission.
255
+ const withoutBracketedPaste = text.replace(/\x1b\[200~[\s\S]*?\x1b\[201~/g, '');
256
+ return /[\r\n]/.test(withoutBracketedPaste);
257
+ });
258
+ }
259
+
260
+ function activeCodexTerminalProfile(agent, previewText) {
261
+ if (!agent) return null;
262
+ const provider = agent.providerSessionProvider
263
+ || agentHomeProviderForProgram(agent.forkCommand || agent.command || '');
264
+ if (
265
+ provider !== 'codex'
266
+ || isCodexAppServerAgent(agent)
267
+ || isJsonCliAgent(agent)
268
+ || isAcpAgent(agent)
269
+ || runtimeKind(agent) !== 'terminal'
270
+ ) {
271
+ return null;
272
+ }
273
+
274
+ const outputProfile = codexTerminalProfileFromOutput(agent.output || '');
275
+ const parsed = outputProfile || codexTerminalProfileFromPreview(previewText);
276
+ if (!parsed) return agent.codexTerminalProfile || null;
277
+ const previousServiceTier = agent.codexTerminalProfile?.serviceTier;
278
+ const profile = {
279
+ model: parsed.model,
280
+ reasoningEffort: parsed.effort,
281
+ serviceTier: typeof parsed.fast === 'boolean'
282
+ ? (parsed.fast ? 'priority' : 'default')
283
+ : (previousServiceTier || 'default'),
284
+ source: outputProfile ? 'terminal-output' : 'terminal-footer',
285
+ };
286
+ agent.codexTerminalProfile = profile;
287
+ return profile;
288
+ }
289
+
290
+ function terminalMetadataPatch(agent) {
291
+ const terminalStatus = deriveAgentTerminalStatus(agent);
292
+ return {
293
+ terminalBusy: typeof agent.terminalBusy === 'boolean' ? agent.terminalBusy : null,
294
+ shellCwd: agent.shellCwd || '',
295
+ shellLastExitCode: agent.shellLastExitCode ?? null,
296
+ shellLastEvent: agent.shellLastEvent || '',
297
+ shellCommand: agent.shellCommand || '',
298
+ shellLastCommand: agent.shellLastCommand || '',
299
+ shellCommandStartedAt: agent.shellCommandStartedAt ?? null,
300
+ shellLastCommandStartedAt: agent.shellLastCommandStartedAt ?? null,
301
+ shellLastCommandFinishedAt: agent.shellLastCommandFinishedAt ?? null,
302
+ shellLastCommandDurationMs: agent.shellLastCommandDurationMs ?? null,
303
+ terminalStatus,
304
+ runtimeObservation: deriveRuntimeObservation({ ...agent, terminalStatus }),
305
+ };
306
+ }
307
+
126
308
  function terminalRuntimeStatus(agentStatus) {
127
309
  return agentStatus === 'stopped' || agentStatus === 'dead' ? 'exited' : agentStatus;
128
310
  }
@@ -198,6 +380,9 @@ function hasAgentOutputAfterAttentionBaseline(agent) {
198
380
 
199
381
  function shouldRecoverEngineSession(metadata) {
200
382
  if (!metadata) return false;
383
+ // Main Agent shells are a product session, not an ephemeral scratch shell.
384
+ // Keep them attached to the persistent native PTY host across server restarts.
385
+ if (metadata.wantsMain === true) return true;
201
386
  if (metadata.category === 'shell') return false;
202
387
  return !isShellProgram(metadata.forkCommand || metadata.command || '');
203
388
  }
@@ -208,9 +393,7 @@ function recoveredEngineSessionId(entry, metadata = {}) {
208
393
 
209
394
  function agentDisplayName(command) {
210
395
  const program = agentProgramName(command).toLowerCase();
211
- if (program === 'codex') return 'codex';
212
- if (program === 'claude') return 'claude code';
213
- return program;
396
+ return getProviderAdapter(providerForProgram(program))?.displayName || program;
214
397
  }
215
398
 
216
399
  function titleComparisonKey(title) {
@@ -249,15 +432,8 @@ function isGenericSessionTitle(agent, title) {
249
432
  }
250
433
 
251
434
  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));
435
+ const provider = agent?.providerSessionProvider || agentHomeProviderForProgram(agent?.command);
436
+ return getProviderAdapter(provider)?.interruptInput || '\x03';
261
437
  }
262
438
 
263
439
  function normalizePathValue(value) {
@@ -267,9 +443,44 @@ function normalizePathValue(value) {
267
443
  return trimmed.replace(/[\\/]+$/, '');
268
444
  }
269
445
 
270
- function timestampMs(value) {
271
- const parsed = Date.parse(value || '');
272
- return Number.isFinite(parsed) ? parsed : 0;
446
+ function effectiveAgentWorkspaceRoot(agent) {
447
+ if (agent && agent.gitWorktree && agent.gitWorktree.workspace) {
448
+ return agent.gitWorktree.workspace;
449
+ }
450
+ return agent && (agent.projectWorkspace || agent.cwd) || '';
451
+ }
452
+
453
+ function publicAgentGitWorktree(agent) {
454
+ const worktree = agent && agent.gitWorktree;
455
+ if (!worktree || !worktree.workspace) return null;
456
+ return {
457
+ workspace: worktree.workspace,
458
+ commonDir: worktree.commonDir || '',
459
+ mainWorkspace: worktree.mainWorkspace || '',
460
+ linked: worktree.linked === true,
461
+ branch: worktree.branch || '',
462
+ head: worktree.head || '',
463
+ detached: worktree.detached === true,
464
+ locked: worktree.locked === true,
465
+ lockReason: worktree.lockReason || '',
466
+ prunable: worktree.prunable === true,
467
+ pruneReason: worktree.pruneReason || '',
468
+ worktrees: Array.isArray(worktree.worktrees)
469
+ ? worktree.worktrees.map(item => ({
470
+ workspace: item.workspace || '',
471
+ head: item.head || '',
472
+ branch: item.branch || '',
473
+ bare: item.bare === true,
474
+ detached: item.detached === true,
475
+ locked: item.locked === true,
476
+ lockReason: item.lockReason || '',
477
+ prunable: item.prunable === true,
478
+ pruneReason: item.pruneReason || '',
479
+ current: item.current === true,
480
+ main: item.main === true,
481
+ }))
482
+ : [],
483
+ };
273
484
  }
274
485
 
275
486
  function normalizePositiveInteger(value, fallback, min, max) {
@@ -289,7 +500,7 @@ function isRunningAgentRuntimeStatus(status) {
289
500
  }
290
501
 
291
502
  function isCodexAppServerAgent(agent) {
292
- return agent && agent.codexRuntimeMode === 'app-server';
503
+ return runtimeKind(agent) === 'app-server';
293
504
  }
294
505
 
295
506
  function isLiveEngineSessionState(sessionState) {
@@ -340,24 +551,27 @@ class AgentManager extends EventEmitter {
340
551
  0,
341
552
  60 * 60 * 1000
342
553
  );
343
- this.agents = new Map();
554
+ this.agents = new RuntimeAgentMap();
344
555
  this.mainAgentId = null;
345
556
  this.lastActivity = new Map();
346
557
  this.lastActivityUpdate = new Map();
347
558
  this.outputEvents = new Map(); // Map<agentId, Array<{timestamp, bytes}>> for rate tracking
559
+ this.agentUsageRateCache = new Map();
348
560
  this.lastResizeByAgent = new Map();
561
+ this.pendingResizeByAgent = new Map();
562
+ this.resizeDrains = new Map();
349
563
  this.inputQueues = new Map();
350
- this.codexSessionResolveInFlight = new Map();
351
- this.codexSessionResolveLastAttemptAt = new Map();
352
- this.providerSessionTitleResolveInFlight = new Map();
353
- this.providerSessionTitleResolveLastAttemptAt = new Map();
564
+ this.codexTerminalProfileQueues = new Map();
565
+ this.agentWorktreeResolveGeneration = new Map();
354
566
  this.permissionRestartInFlight = new Map();
355
567
  this.runtimeRestartInFlight = new Map();
568
+ this.codexSessionUnarchiveInFlight = new Map();
356
569
  this.permissionRestartSuppressedAgentIds = new Set();
357
570
  this.codexAppServerRuntime = options.codexAppServerRuntime || new CodexAppServerRuntime();
358
571
  this.jsonCliRuntime = options.jsonCliRuntime || new JsonCliRuntime();
359
- this.acpRuntime = options.acpRuntime || new AcpRuntime(
360
- process.env.FARMING_E2E_FAKE_ACP_AGENT === '1'
572
+ this.acpRuntime = options.acpRuntime || new AcpRuntime({
573
+ ...(this.configManager?.farmingDir ? { configDir: this.configManager.farmingDir } : {}),
574
+ ...(process.env.FARMING_E2E_FAKE_ACP_AGENT === '1'
361
575
  ? {
362
576
  resolveLaunch: () => ({
363
577
  command: process.execPath,
@@ -365,13 +579,30 @@ class AgentManager extends EventEmitter {
365
579
  version: 'e2e',
366
580
  }),
367
581
  }
368
- : undefined
369
- );
582
+ : {}),
583
+ });
584
+ this.unarchiveCodexSession = options.unarchiveCodexSession || unarchiveCodexSession;
370
585
  this.heartbeatInterval = null;
371
586
  this.disposed = false;
372
587
  this.systemMonitor = new SystemMonitor();
373
588
  this.startTime = Date.now();
374
589
  this.engineBridge = new SessionEngineBridge(configManager);
590
+ this.providerSessionService = new ProviderSessionService({
591
+ agents: this.agents,
592
+ getProviderHomes: () => this.configManager?.getSettings?.()?.agentHomes,
593
+ commit: (agent, change = {}) => {
594
+ if (change.kind === 'session-updated') this.ensurePersistentAgentSession(agent);
595
+ this.updateEngineProviderSessionMetadata(agent);
596
+ this.rememberMainPageProviderSession(agent);
597
+ if (change.event) {
598
+ this.emit('provider-session-updated', change.event);
599
+ this.emit('update');
600
+ }
601
+ if (change.refreshWorkspace) {
602
+ void this.refreshAgentWorktree(agent.id, change.refreshWorkspace);
603
+ }
604
+ },
605
+ });
375
606
  this.recoveryPromise = Promise.resolve();
376
607
  this.taskHistory = (this.configManager && this.configManager.getTaskHistory)
377
608
  ? [...this.configManager.getTaskHistory()]
@@ -393,17 +624,12 @@ class AgentManager extends EventEmitter {
393
624
  if (!this.jsonCliRuntime || typeof this.jsonCliRuntime.on !== 'function') return;
394
625
  this.jsonCliRuntime.on('agent-runtime', ({ agentId, state, error, sessionId }) => {
395
626
  const agent = this.agents.get(agentId);
396
- if (!agent) return;
397
- agent.jsonCliState = state || '';
398
- agent.jsonCliError = error || '';
627
+ const runtime = runtimeBindingOf(agent, 'json');
628
+ if (!runtime) return;
629
+ runtime.state = state || '';
630
+ runtime.error = error || '';
399
631
  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
- );
632
+ this.providerSessionService.bindConfirmed(agentId, agent.providerSessionProvider, sessionId);
407
633
  this.ensurePersistentAgentSession(agent);
408
634
  }
409
635
  this.lastActivity.set(agentId, Date.now());
@@ -411,9 +637,10 @@ class AgentManager extends EventEmitter {
411
637
  });
412
638
  this.jsonCliRuntime.on('transcript', ({ agentId }) => {
413
639
  const agent = this.agents.get(agentId);
414
- if (!agent) return;
415
- agent.jsonCliEvents = this.jsonCliRuntime.getEvents(agentId);
416
- agent.jsonCliTranscriptUpdatedAt = new Date().toISOString();
640
+ const runtime = runtimeBindingOf(agent, 'json');
641
+ if (!runtime) return;
642
+ runtime.events = this.jsonCliRuntime.getEvents(agentId);
643
+ runtime.transcriptUpdatedAt = new Date().toISOString();
417
644
  this.emit('update');
418
645
  });
419
646
  }
@@ -422,24 +649,19 @@ class AgentManager extends EventEmitter {
422
649
  if (!this.acpRuntime || typeof this.acpRuntime.on !== 'function') return;
423
650
  this.acpRuntime.on('agent-runtime', ({ agentId, state, error, sessionId, stopReason, pendingPermission, pendingPermissions, pendingElicitation, pendingElicitations, activeElicitations, updatedAt }) => {
424
651
  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 || '';
652
+ const runtime = runtimeBindingOf(agent, 'acp');
653
+ if (!runtime) return;
654
+ runtime.state = state || '';
655
+ runtime.error = error || '';
656
+ runtime.stopReason = stopReason || '';
657
+ runtime.pendingPermission = pendingPermission || null;
658
+ runtime.pendingPermissions = Array.isArray(pendingPermissions) ? pendingPermissions : [];
659
+ runtime.pendingElicitation = pendingElicitation || null;
660
+ runtime.pendingElicitations = Array.isArray(pendingElicitations) ? pendingElicitations : [];
661
+ runtime.activeElicitations = Array.isArray(activeElicitations) ? activeElicitations : [];
662
+ runtime.sessionUpdatedAt = updatedAt || '';
435
663
  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
- );
664
+ this.providerSessionService.bindConfirmed(agentId, agent.providerSessionProvider, sessionId);
443
665
  this.ensurePersistentAgentSession(agent);
444
666
  }
445
667
  if (state === 'working' || state === 'waiting-for-permission' || state === 'waiting-for-input') this.lastActivity.set(agentId, Date.now());
@@ -447,9 +669,10 @@ class AgentManager extends EventEmitter {
447
669
  });
448
670
  this.acpRuntime.on('session', ({ agentId, revision }) => {
449
671
  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;
672
+ const runtime = runtimeBindingOf(agent, 'acp');
673
+ if (!runtime) return;
674
+ runtime.sessionUpdatedAt = new Date().toISOString();
675
+ runtime.sessionRevision = Number.isFinite(Number(revision)) ? Number(revision) : (Number(runtime.sessionRevision) || 0) + 1;
453
676
  this.emit('update');
454
677
  });
455
678
  }
@@ -458,34 +681,33 @@ class AgentManager extends EventEmitter {
458
681
  if (!this.codexAppServerRuntime || typeof this.codexAppServerRuntime.on !== 'function') return;
459
682
  this.codexAppServerRuntime.on('agent-runtime', ({ agentId, ...patch }) => {
460
683
  const agent = this.agents.get(agentId);
461
- if (!agent) return;
684
+ const runtime = runtimeBindingOf(agent, 'app-server');
685
+ if (!runtime) return;
462
686
 
463
687
  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(
688
+ runtime.threadId = patch.threadId;
689
+ this.providerSessionService.bindConfirmed(
690
+ agentId,
468
691
  agent.providerSessionProvider || 'codex',
469
- patch.threadId,
470
- agent.providerHomeId || 'default'
692
+ patch.threadId
471
693
  );
472
694
  }
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;
695
+ if (typeof patch.turnId === 'string') runtime.turnId = patch.turnId;
696
+ if (typeof patch.state === 'string' && patch.state) runtime.state = patch.state;
697
+ if (typeof patch.error === 'string') runtime.error = patch.error;
698
+ if (typeof patch.pendingRequestId === 'string') runtime.pendingRequestId = patch.pendingRequestId;
699
+ if (typeof patch.pendingRequestMethod === 'string') runtime.pendingRequestMethod = patch.pendingRequestMethod;
478
700
  if (Object.prototype.hasOwnProperty.call(patch, 'pendingRequest')) {
479
- agent.codexAppServerPendingRequest = patch.pendingRequest || null;
701
+ runtime.pendingRequest = patch.pendingRequest || null;
480
702
  }
481
703
  if (Object.prototype.hasOwnProperty.call(patch, 'notice')) {
482
- agent.codexAppServerNotice = patch.notice || null;
704
+ runtime.notice = patch.notice || null;
483
705
  }
484
706
  if (Object.prototype.hasOwnProperty.call(patch, 'goal')) {
485
- agent.codexAppServerGoal = patch.goal || null;
707
+ runtime.goal = patch.goal || null;
486
708
  }
487
709
  if (typeof patch.transcriptUpdatedAt === 'string') {
488
- agent.codexAppServerTranscriptUpdatedAt = patch.transcriptUpdatedAt;
710
+ runtime.transcriptUpdatedAt = patch.transcriptUpdatedAt;
489
711
  }
490
712
 
491
713
  if (patch.state === 'working' || patch.state === 'waiting-for-input') {
@@ -496,39 +718,57 @@ class AgentManager extends EventEmitter {
496
718
  }
497
719
 
498
720
  bindEngineEvents() {
499
- this.engineBridge.on('session-started', ({ sessionId, status, startedAt }) => {
721
+ this.engineBridge.on('session-started', ({
722
+ sessionId,
723
+ status,
724
+ startedAt,
725
+ runtimeEpoch,
726
+ outputSeq,
727
+ stateRevision,
728
+ }) => {
500
729
  const agent = this.agents.get(sessionId);
501
730
  if (!agent) return;
502
731
 
732
+ const disposition = terminalStateUpdateDisposition(agent, runtimeEpoch, outputSeq, stateRevision);
733
+ if (!applyTerminalStateCursor(agent, runtimeEpoch, outputSeq, stateRevision, disposition)) return;
734
+ setPendingTerminalStartSyncCut(agent, runtimeEpoch, outputSeq, stateRevision);
735
+
503
736
  agent.engineStarted = true;
504
737
  agent.engineStatus = status || 'running';
505
738
  agent.startedAt = startedAt || Date.now();
506
739
  this.observeAgentAttentionState(sessionId);
507
- this.observeAgentStateChange(sessionId, { force: true });
740
+ this.providerSessionService.observe(sessionId, { force: true });
508
741
  this.emit('update');
509
742
  });
510
743
 
511
- this.engineBridge.on('session-output', ({ sessionId, data, engineName, outputSeq }) => {
744
+ this.engineBridge.on('session-output', ({
745
+ sessionId,
746
+ data,
747
+ engineName,
748
+ runtimeEpoch,
749
+ outputSeq,
750
+ stateRevision,
751
+ }) => {
512
752
  const agent = this.agents.get(sessionId);
513
753
  if (!agent) return;
514
754
 
755
+ const disposition = terminalStateUpdateDisposition(agent, runtimeEpoch, outputSeq, stateRevision);
756
+ if (!applyTerminalStateCursor(agent, runtimeEpoch, outputSeq, stateRevision, disposition)) return;
757
+ clearPendingTerminalStartSyncCut(agent);
758
+
515
759
  this.reviveAgentRuntime(agent);
516
760
  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());
761
+ const outputAt = Date.now();
762
+ agent.lastEngineOutputAt = outputAt;
763
+ this.lastActivity.set(sessionId, outputAt);
523
764
 
524
765
  // Track output events for rate calculation
525
766
  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));
767
+ events.push({ timestamp: outputAt, bytes: Buffer.byteLength(String(data), 'utf8') });
768
+ this.outputEvents.set(sessionId, events);
769
+ this.getAgentUsageRate(sessionId, { now: outputAt });
529
770
 
530
771
  this.observeAgentAttentionState(sessionId);
531
- this.observeAgentStateChange(sessionId);
532
772
  const sessionSource = this.getEngineSessionSource(engineName);
533
773
  const stream = {
534
774
  agentId: sessionId,
@@ -538,44 +778,123 @@ class AgentManager extends EventEmitter {
538
778
  if (Number.isFinite(outputSeq)) {
539
779
  stream.outputSeq = outputSeq;
540
780
  }
781
+ if (Number.isFinite(stateRevision)) {
782
+ stream.stateRevision = stateRevision;
783
+ }
784
+ if (typeof runtimeEpoch === 'string' && runtimeEpoch) {
785
+ stream.runtimeEpoch = runtimeEpoch;
786
+ }
541
787
  this.emit('session-stream', stream);
542
788
  });
543
789
 
544
- this.engineBridge.on('session-sync', ({ sessionId, output, engineName, replaceLive = true, outputSeq }) => {
790
+ this.engineBridge.on('session-transition', ({
791
+ sessionId,
792
+ engineName,
793
+ kind,
794
+ data = '',
795
+ runtimeEpoch,
796
+ outputSeq,
797
+ stateRevision,
798
+ cols,
799
+ rows,
800
+ }) => {
801
+ const agent = this.agents.get(sessionId);
802
+ if (!agent) return;
803
+ const disposition = terminalStateUpdateDisposition(agent, runtimeEpoch, outputSeq, stateRevision);
804
+ if (!applyTerminalStateCursor(agent, runtimeEpoch, outputSeq, stateRevision, disposition)) return;
805
+ clearPendingTerminalStartSyncCut(agent);
806
+ this.reviveAgentRuntime(agent);
807
+ if (kind === 'clear') {
808
+ agent.output = '';
809
+ agent.previewText = '';
810
+ agent.previewSnapshot = null;
811
+ this.outputEvents.delete(sessionId);
812
+ this.agentUsageRateCache.delete(sessionId);
813
+ }
814
+ if (Number.isFinite(cols) && cols > 0) agent.previewCols = cols;
815
+ if (Number.isFinite(rows) && rows > 0) agent.previewRows = rows;
816
+ this.lastActivity.set(sessionId, Date.now());
817
+ this.emit('session-stream', {
818
+ agentId: sessionId,
819
+ sessionSource: this.getEngineSessionSource(engineName),
820
+ kind,
821
+ data,
822
+ runtimeEpoch,
823
+ outputSeq,
824
+ stateRevision,
825
+ cols,
826
+ rows,
827
+ });
828
+ this.observeAgentAttentionState(sessionId);
829
+ this.emit('update');
830
+ });
831
+
832
+ this.engineBridge.on('session-sync', ({
833
+ sessionId,
834
+ output,
835
+ engineName,
836
+ replaceLive = true,
837
+ runtimeEpoch,
838
+ outputSeq,
839
+ stateRevision,
840
+ textOutput,
841
+ cols,
842
+ rows,
843
+ }) => {
545
844
  const agent = this.agents.get(sessionId);
546
845
  if (!agent) return;
547
846
 
847
+ const hydratesStartedCut = consumesPendingTerminalStartSyncCut(
848
+ agent,
849
+ runtimeEpoch,
850
+ outputSeq,
851
+ stateRevision,
852
+ );
853
+ const disposition = terminalStateUpdateDisposition(agent, runtimeEpoch, outputSeq, stateRevision);
854
+ if (
855
+ !hydratesStartedCut &&
856
+ !applyTerminalStateCursor(agent, runtimeEpoch, outputSeq, stateRevision, disposition)
857
+ ) return;
858
+ clearPendingTerminalStartSyncCut(agent);
859
+
548
860
  this.reviveAgentRuntime(agent);
549
- agent.output = trimSessionOutput(output);
861
+ agent.output = trimSessionOutput(typeof textOutput === 'string' ? textOutput : output);
550
862
  agent.previewText = agent.output.slice(-2000);
551
- if (Number.isFinite(outputSeq)) {
552
- agent.lastOutputSeq = outputSeq;
553
- }
554
863
  this.lastActivity.set(sessionId, Date.now());
555
864
 
556
865
  if (replaceLive) {
557
866
  const sessionSource = this.getEngineSessionSource(engineName);
558
867
  const stream = {
559
868
  agentId: sessionId,
560
- data: agent.output,
869
+ data: output,
561
870
  sessionSource,
562
871
  replace: true,
563
872
  };
564
873
  if (Number.isFinite(outputSeq)) {
565
874
  stream.outputSeq = outputSeq;
566
875
  }
876
+ if (Number.isFinite(stateRevision)) {
877
+ stream.stateRevision = stateRevision;
878
+ }
879
+ if (typeof runtimeEpoch === 'string' && runtimeEpoch) {
880
+ stream.runtimeEpoch = runtimeEpoch;
881
+ }
882
+ if (Number.isFinite(cols) && cols > 0) {
883
+ stream.cols = cols;
884
+ }
885
+ if (Number.isFinite(rows) && rows > 0) {
886
+ stream.rows = rows;
887
+ }
567
888
  this.emit('session-stream', stream);
568
889
  }
569
890
  this.observeAgentAttentionState(sessionId);
570
- this.observeAgentStateChange(sessionId);
571
891
  this.emit('update');
572
892
  });
573
893
 
574
- this.engineBridge.on('session-preview', ({ sessionId, previewText, cols, rows, previewSnapshot, title }) => {
894
+ this.engineBridge.on('session-preview', ({ sessionId, previewText, cols, rows, previewSnapshot, title, runtimeEpoch }) => {
575
895
  const agent = this.agents.get(sessionId);
576
- if (!agent) return;
896
+ if (!agent || !terminalRuntimeEventMatches(agent, runtimeEpoch)) return;
577
897
 
578
- const revived = this.reviveAgentRuntime(agent);
579
898
  const titleChanged = typeof title === 'string'
580
899
  ? this.updateAgentSessionTitle(agent, title)
581
900
  : false;
@@ -587,44 +906,43 @@ class AgentManager extends EventEmitter {
587
906
  if (Number.isFinite(rows) && rows > 0) {
588
907
  agent.previewRows = rows;
589
908
  }
909
+ const terminalStatus = deriveAgentTerminalStatus(agent, {
910
+ previewText: agent.previewText,
911
+ title: agent.sessionTitle || '',
912
+ terminalBusy: typeof agent.terminalBusy === 'boolean' ? agent.terminalBusy : null,
913
+ });
590
914
  this.emit('session-preview-update', {
591
915
  agentId: sessionId,
592
916
  previewText: agent.previewText,
593
917
  cols: agent.previewCols || 80,
594
918
  rows: agent.previewRows || 30,
595
919
  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
- }),
920
+ codexTerminalProfile: activeCodexTerminalProfile(agent, agent.previewText),
921
+ terminalStatus,
922
+ runtimeObservation: deriveRuntimeObservation({ ...agent, terminalStatus }),
601
923
  });
602
924
  this.observeAgentAttentionState(sessionId);
603
- this.observeAgentStateChange(sessionId);
604
- if (titleChanged || revived) {
925
+ if (titleChanged) {
605
926
  this.emit('update');
606
927
  }
607
928
  });
608
929
 
609
- this.engineBridge.on('session-title', ({ sessionId, title }) => {
930
+ this.engineBridge.on('session-title', ({ sessionId, title, runtimeEpoch }) => {
610
931
  const agent = this.agents.get(sessionId);
611
- if (!agent) return;
932
+ if (!agent || !terminalRuntimeEventMatches(agent, runtimeEpoch)) return;
612
933
 
613
934
  if (this.updateAgentSessionTitle(agent, title)) {
614
935
  this.observeAgentAttentionState(sessionId);
615
- this.observeAgentStateChange(sessionId);
616
936
  this.emit('update');
617
937
  }
618
938
  });
619
939
 
620
- this.engineBridge.on('session-activity', ({ sessionId, lastActivityAt }) => {
940
+ this.engineBridge.on('session-activity', ({ sessionId, lastActivityAt, runtimeEpoch }) => {
621
941
  const agent = this.agents.get(sessionId);
622
- const revived = agent ? this.reviveAgentRuntime(agent) : false;
942
+ if (!agent || !terminalRuntimeEventMatches(agent, runtimeEpoch)) return;
623
943
  this.lastActivity.set(sessionId, lastActivityAt || Date.now());
624
944
  this.observeAgentAttentionState(sessionId);
625
- this.observeAgentStateChange(sessionId);
626
945
  this.emitActivityUpdate(sessionId, lastActivityAt || Date.now());
627
- if (revived) this.emit('update');
628
946
  });
629
947
 
630
948
  this.engineBridge.on('session-busy-state', (payload = {}) => {
@@ -642,9 +960,11 @@ class AgentManager extends EventEmitter {
642
960
  shellLastCommandDurationMs,
643
961
  statusMarkerSeen,
644
962
  busyMarkerSeen,
963
+ runtimeEpoch,
645
964
  } = payload;
646
965
  const agent = this.agents.get(sessionId);
647
- if (!agent) return;
966
+ if (!agent || !terminalRuntimeEventMatches(agent, runtimeEpoch)) return;
967
+ const previousShellCwd = agent.shellCwd || '';
648
968
 
649
969
  const previousState = JSON.stringify({
650
970
  terminalBusy: agent.terminalBusy,
@@ -714,14 +1034,23 @@ class AgentManager extends EventEmitter {
714
1034
  shellBusyMarkerSeen: agent.shellBusyMarkerSeen === true,
715
1035
  });
716
1036
  if (previousState === nextState) return;
1037
+ if (agent.shellCwd && agent.shellCwd !== previousShellCwd) {
1038
+ void this.refreshAgentWorktree(sessionId, agent.shellCwd);
1039
+ }
717
1040
  this.observeAgentAttentionState(sessionId);
718
- this.observeAgentStateChange(sessionId);
719
- this.emit('update');
1041
+ this.emit('agent-update', { agentId: sessionId, patch: terminalMetadataPatch(agent) });
720
1042
  });
721
1043
 
722
- this.engineBridge.on('session-exited', ({ sessionId, code, exitedAt }) => {
1044
+ this.engineBridge.on('session-exited', ({
1045
+ sessionId,
1046
+ code,
1047
+ exitedAt,
1048
+ runtimeEpoch,
1049
+ stateProofAvailable,
1050
+ }) => {
723
1051
  const agent = this.agents.get(sessionId);
724
- if (!agent) return;
1052
+ if (!agent || !terminalRuntimeEventMatches(agent, runtimeEpoch)) return;
1053
+ clearPendingTerminalStartSyncCut(agent);
725
1054
  if (this.permissionRestartSuppressedAgentIds.has(sessionId)) return;
726
1055
 
727
1056
  // A fresh App Server thread deliberately has no CLI/PTy observer until
@@ -730,13 +1059,29 @@ class AgentManager extends EventEmitter {
730
1059
  // open; it is not an exit of the App Server Agent.
731
1060
  if (isCodexAppServerAgent(agent)) return;
732
1061
 
1062
+ if (stateProofAvailable === false) {
1063
+ this.providerSessionService.stop(sessionId);
1064
+ agent.status = 'dead';
1065
+ agent.engineStatus = 'dead';
1066
+ agent.terminalBusy = false;
1067
+ agent.exitedAt = exitedAt || Date.now();
1068
+ const proofError = 'Terminal exited without an authoritative final checkpoint';
1069
+ if (!String(agent.output || '').includes(proofError)) {
1070
+ agent.output = trimSessionOutput(`${agent.output || ''}\n${proofError}`);
1071
+ }
1072
+ this.observeAgentAttentionState(sessionId);
1073
+ this.providerSessionService.observe(sessionId, { force: true });
1074
+ this.emit('update');
1075
+ return;
1076
+ }
1077
+
733
1078
  if (!agent.validated) {
734
- this.stopCodexProviderSessionResolver(sessionId);
735
- this.stopProviderSessionTitleResolver(sessionId);
1079
+ this.providerSessionService.stop(sessionId);
736
1080
  this.agents.delete(sessionId);
737
1081
  this.lastActivity.delete(sessionId);
738
1082
  this.lastActivityUpdate.delete(sessionId);
739
1083
  this.outputEvents.delete(sessionId);
1084
+ this.agentUsageRateCache.delete(sessionId);
740
1085
  this.lastResizeByAgent.delete(sessionId);
741
1086
 
742
1087
  if (this.mainAgentId === sessionId) {
@@ -747,13 +1092,12 @@ class AgentManager extends EventEmitter {
747
1092
  return;
748
1093
  }
749
1094
 
750
- this.stopCodexProviderSessionResolver(sessionId);
751
- this.stopProviderSessionTitleResolver(sessionId);
1095
+ this.providerSessionService.stop(sessionId);
752
1096
  agent.status = sessionId === this.mainAgentId ? 'dead' : 'stopped';
753
1097
  agent.exitedAt = exitedAt || Date.now();
754
1098
  agent.output = trimSessionOutput(`${agent.output}\nProcess exited with code ${code}`);
755
1099
  this.observeAgentAttentionState(sessionId);
756
- this.observeAgentStateChange(sessionId, { force: true });
1100
+ this.providerSessionService.observe(sessionId, { force: true });
757
1101
  if (sessionId !== this.mainAgentId) {
758
1102
  this.recordTaskHistory(agent, {
759
1103
  reason: 'process-exit',
@@ -763,9 +1107,9 @@ class AgentManager extends EventEmitter {
763
1107
  this.emit('update');
764
1108
  });
765
1109
 
766
- this.engineBridge.on('session-error', ({ sessionId, error, fatal = true }) => {
1110
+ this.engineBridge.on('session-error', ({ sessionId, error, fatal = true, runtimeEpoch }) => {
767
1111
  const agent = this.agents.get(sessionId);
768
- if (!agent) return;
1112
+ if (!agent || !terminalRuntimeEventMatches(agent, runtimeEpoch)) return;
769
1113
  if (this.permissionRestartSuppressedAgentIds.has(sessionId)) return;
770
1114
 
771
1115
  if (fatal === false) {
@@ -803,12 +1147,37 @@ class AgentManager extends EventEmitter {
803
1147
  // has already switched to ACP. Recovering that stale PTY first would
804
1148
  // overwrite the persisted record back to `terminal` before
805
1149
  // recoverAcpSessions() gets a chance to read it.
806
- if (persisted?.agentRuntimeMode === 'acp') {
1150
+ if (runtimeKind(persisted) === 'acp') {
807
1151
  await this.killRecoveredEngineSession(entry, engineMetadata, agentId);
808
1152
  continue;
809
1153
  }
1154
+ const persistedProvider = String(persisted?.providerSessionProvider || persisted?.provider || '').trim();
810
1155
  const metadata = persisted ? {
811
1156
  ...engineMetadata,
1157
+ // The native host owns the live PTY/reducer state, but the Farming
1158
+ // session record owns stable product identity. Legacy hosts can omit
1159
+ // these fields during recovery; projecting that incomplete metadata
1160
+ // even briefly makes Chat/Terminal switching disappear until a later
1161
+ // provider resolver update happens to repair it.
1162
+ source: persisted.source || engineMetadata.source,
1163
+ projectWorkspace: persisted.projectWorkspace || engineMetadata.projectWorkspace,
1164
+ provider: persistedProvider || engineMetadata.provider,
1165
+ providerSessionProvider: persistedProvider || engineMetadata.providerSessionProvider,
1166
+ providerHomeId: persisted.providerHomeId || engineMetadata.providerHomeId,
1167
+ providerHomePath: persisted.providerHomePath || engineMetadata.providerHomePath,
1168
+ providerSessionId: persisted.providerSessionId || engineMetadata.providerSessionId,
1169
+ providerSessionKey: persisted.providerSessionKey || engineMetadata.providerSessionKey,
1170
+ providerSessionTemporary: Object.prototype.hasOwnProperty.call(persisted, 'providerSessionTemporary')
1171
+ ? persisted.providerSessionTemporary === true
1172
+ : engineMetadata.providerSessionTemporary,
1173
+ providerSessionSource: persisted.providerSessionSource || engineMetadata.providerSessionSource,
1174
+ providerSessionResolvedAt: persisted.providerSessionResolvedAt || engineMetadata.providerSessionResolvedAt,
1175
+ providerSessionTitle: persisted.providerSessionTitle || engineMetadata.providerSessionTitle,
1176
+ providerSessionWorkspace: persisted.providerSessionWorkspace || engineMetadata.providerSessionWorkspace,
1177
+ terminalInputReceived: Object.prototype.hasOwnProperty.call(persisted, 'terminalInputReceived')
1178
+ ? persisted.terminalInputReceived === true
1179
+ : engineMetadata.terminalInputReceived,
1180
+ ...legacyRuntimeMetadata(persisted),
812
1181
  pinned: persisted.pinned === true,
813
1182
  projectOrder: finiteOrder(persisted.projectOrder) ?? finiteOrder(engineMetadata.projectOrder),
814
1183
  pinnedOrder: finiteOrder(persisted.pinnedOrder) ?? finiteOrder(engineMetadata.pinnedOrder),
@@ -824,11 +1193,12 @@ class AgentManager extends EventEmitter {
824
1193
  agentRecord.lastObservedTurnActive = this.isAgentAttentionTurnActive(agentRecord);
825
1194
  this.ensurePersistentAgentSession(agentRecord);
826
1195
  this.agents.set(agentId, agentRecord);
1196
+ void this.refreshAgentWorktree(agentId);
827
1197
  this.lastActivity.set(agentId, state.lastActivityAt || metadata.lastActivityAt || Date.now());
828
1198
  if (agentRecord.wantsMain && !this.mainAgentId) {
829
1199
  this.mainAgentId = agentId;
830
1200
  }
831
- this.activateProviderSessionTracking(agentId);
1201
+ this.providerSessionService.activate(agentId);
832
1202
  changed = true;
833
1203
  }
834
1204
 
@@ -836,29 +1206,272 @@ class AgentManager extends EventEmitter {
836
1206
  this.emit('update');
837
1207
  }
838
1208
 
1209
+ const runtimeRotations = this.engineBridge && typeof this.engineBridge.consumeRuntimeRotations === 'function'
1210
+ ? this.engineBridge.consumeRuntimeRotations()
1211
+ : [];
1212
+ if (runtimeRotations.length > 0) {
1213
+ await this.restoreTerminalSessionsAfterRuntimeRotation(persistedRecords, runtimeRotations);
1214
+ }
1215
+
839
1216
  await this.recoverCodexAppServerSessions();
840
1217
  await this.recoverAcpSessions();
841
1218
  }
842
1219
 
1220
+ async restoreTerminalSessionsAfterRuntimeRotation(records, rotations) {
1221
+ const mainPageOrder = new Map(this.getMainPageSessionKeys().map((key, index) => [key, index]));
1222
+ const liveProviderSessions = new Set(
1223
+ [...this.agents.values()]
1224
+ .filter(agent => agent?.providerSessionProvider && agent?.providerSessionId)
1225
+ .map(agent => mainPageAgentSessionKey(
1226
+ agent.providerSessionProvider,
1227
+ agent.providerSessionId,
1228
+ agent.providerHomeId || 'default'
1229
+ ))
1230
+ .filter(Boolean)
1231
+ );
1232
+ const recordList = Array.isArray(records) ? records : [];
1233
+ const recordByRuntimeAgentId = new Map(recordList
1234
+ .filter(record => record && typeof record.runtimeAgentId === 'string' && record.runtimeAgentId)
1235
+ .map(record => [record.runtimeAgentId, record]));
1236
+ const serializedStates = [];
1237
+ for (const rotation of Array.isArray(rotations) ? rotations : []) {
1238
+ if (!rotation || typeof rotation.serializedTerminalState !== 'string' || !rotation.serializedTerminalState) continue;
1239
+ try {
1240
+ serializedStates.push(...deserializeTerminalState(rotation.serializedTerminalState));
1241
+ } catch (error) {
1242
+ console.warn(
1243
+ 'Ignoring invalid serialized terminal state after native PTY runtime rotation:',
1244
+ error && (error.message || error)
1245
+ );
1246
+ }
1247
+ }
1248
+ const serializedByRuntimeAgentId = new Map(serializedStates.map(state => [state.id, state]));
1249
+ const candidateKeys = new Set();
1250
+ const fallbackCandidates = recordList
1251
+ .filter(record => {
1252
+ if (!record || record.archived === true) return false;
1253
+ if (runtimeKind(record) !== 'terminal') return false;
1254
+ const provider = String(record.providerSessionProvider || record.provider || '').trim();
1255
+ if (!getProviderAdapter(provider)) return false;
1256
+ if (!isSafeProviderSessionId(record.providerSessionId)) return false;
1257
+ const sessionKey = record.providerSessionKey || mainPageAgentSessionKey(
1258
+ provider,
1259
+ record.providerSessionId,
1260
+ record.providerHomeId || 'default'
1261
+ );
1262
+ if (!sessionKey || liveProviderSessions.has(sessionKey)) return false;
1263
+ if (record.wantsMain === true) return !this.mainAgentId;
1264
+ return mainPageOrder.has(sessionKey);
1265
+ })
1266
+ .sort((left, right) => {
1267
+ if (left.wantsMain === true && right.wantsMain !== true) return -1;
1268
+ if (right.wantsMain === true && left.wantsMain !== true) return 1;
1269
+ const leftProvider = String(left.providerSessionProvider || left.provider || '').trim();
1270
+ const rightProvider = String(right.providerSessionProvider || right.provider || '').trim();
1271
+ const leftKey = left.providerSessionKey || mainPageAgentSessionKey(
1272
+ leftProvider,
1273
+ left.providerSessionId,
1274
+ left.providerHomeId || 'default'
1275
+ );
1276
+ const rightKey = right.providerSessionKey || mainPageAgentSessionKey(
1277
+ rightProvider,
1278
+ right.providerSessionId,
1279
+ right.providerHomeId || 'default'
1280
+ );
1281
+ const orderDelta = (mainPageOrder.get(leftKey) ?? Number.MAX_SAFE_INTEGER) -
1282
+ (mainPageOrder.get(rightKey) ?? Number.MAX_SAFE_INTEGER);
1283
+ if (orderDelta !== 0) return orderDelta;
1284
+ return (Number(right.updatedAt) || 0) - (Number(left.updatedAt) || 0);
1285
+ })
1286
+ .filter(record => {
1287
+ const provider = String(record.providerSessionProvider || record.provider || '').trim();
1288
+ const sessionKey = record.providerSessionKey || mainPageAgentSessionKey(
1289
+ provider,
1290
+ record.providerSessionId,
1291
+ record.providerHomeId || 'default'
1292
+ );
1293
+ if (candidateKeys.has(sessionKey)) return false;
1294
+ candidateKeys.add(sessionKey);
1295
+ return true;
1296
+ });
1297
+ const candidates = serializedByRuntimeAgentId.size > 0
1298
+ ? [...serializedByRuntimeAgentId.values()]
1299
+ .map(serializedState => ({
1300
+ ...(serializedState.metadata || {}),
1301
+ ...(recordByRuntimeAgentId.get(serializedState.id) || {}),
1302
+ runtimeAgentId: serializedState.id,
1303
+ serializedState,
1304
+ }))
1305
+ .filter(record => {
1306
+ if (!record || record.archived === true) return false;
1307
+ return runtimeKind(record) === 'terminal';
1308
+ })
1309
+ .sort((left, right) => {
1310
+ if (left.wantsMain === true && right.wantsMain !== true) return -1;
1311
+ if (right.wantsMain === true && left.wantsMain !== true) return 1;
1312
+ return (Number(right.updatedAt) || 0) - (Number(left.updatedAt) || 0);
1313
+ })
1314
+ : fallbackCandidates;
1315
+
1316
+ if (candidates.length > 0) {
1317
+ const rotationSummary = (Array.isArray(rotations) ? rotations : []).map(rotation => {
1318
+ const { serializedTerminalState, ...rest } = rotation || {};
1319
+ return {
1320
+ ...rest,
1321
+ serializedTerminalStateBytes: typeof serializedTerminalState === 'string'
1322
+ ? Buffer.byteLength(serializedTerminalState, 'utf8')
1323
+ : 0,
1324
+ };
1325
+ });
1326
+ console.warn(
1327
+ `Restoring ${candidates.length} Terminal session(s) after native PTY runtime rotation`,
1328
+ rotationSummary
1329
+ );
1330
+ }
1331
+
1332
+ let changed = false;
1333
+ for (const record of candidates) {
1334
+ if (record.wantsMain === true && this.mainAgentId) continue;
1335
+ const provider = String(record.providerSessionProvider || record.provider || '').trim();
1336
+ const sessionId = record.providerSessionId;
1337
+ const sessionKey = record.providerSessionKey || mainPageAgentSessionKey(
1338
+ provider,
1339
+ sessionId,
1340
+ record.providerHomeId || 'default'
1341
+ );
1342
+ if (sessionKey && liveProviderSessions.has(sessionKey)) continue;
1343
+ const canResumeProvider = Boolean(getProviderAdapter(provider))
1344
+ && isSafeProviderSessionId(sessionId);
1345
+ const command = canResumeProvider
1346
+ ? buildAgentSessionResumeCommand(provider, sessionId, {
1347
+ cwd: record.cwd || record.projectWorkspace || '',
1348
+ providerHomePath: record.providerHomePath || '',
1349
+ })
1350
+ : (
1351
+ record.forkCommand ||
1352
+ record.command ||
1353
+ record.serializedState?.processLaunchConfig?.command ||
1354
+ ''
1355
+ );
1356
+ if (!command) continue;
1357
+
1358
+ const options = {
1359
+ wantsMain: record.wantsMain === true,
1360
+ skipRecoveryWait: true,
1361
+ task: record.task || record.providerSessionTitle || '',
1362
+ workflowTemplate: record.workflowTemplate || '',
1363
+ projectWorkspace: record.projectWorkspace || record.cwd || '',
1364
+ source: canResumeProvider
1365
+ ? resumedAgentSource(provider, sessionId, record.providerHomeId || 'default')
1366
+ : (record.source || 'terminal-revive'),
1367
+ providerHomeId: record.providerHomeId || '',
1368
+ providerHomePath: record.providerHomePath || '',
1369
+ providerSessionTitle: record.providerSessionTitle || '',
1370
+ restartedFromAgentId: record.restartedFromAgentId || '',
1371
+ restartedFromAgentIds: Array.isArray(record.restartedFromAgentIds)
1372
+ ? record.restartedFromAgentIds
1373
+ : [],
1374
+ projectOrder: finiteOrder(record.projectOrder),
1375
+ pinnedOrder: finiteOrder(record.pinnedOrder),
1376
+ customTitle: record.customTitle || '',
1377
+ persistentSessionId: record.id || '',
1378
+ runtimeAgentId: record.runtimeAgentId || '',
1379
+ reviveTerminalState: record.serializedState || null,
1380
+ ...(provider === 'codex'
1381
+ ? {
1382
+ codexApprovalMode: record.launchPermissionMode || undefined,
1383
+ ...preserveCodexSessionProfileOptions(),
1384
+ }
1385
+ : {}),
1386
+ ...(provider === 'claude'
1387
+ ? { claudePermissionMode: record.launchPermissionMode || undefined }
1388
+ : {}),
1389
+ };
1390
+
1391
+ let restartedAgentId = null;
1392
+ try {
1393
+ restartedAgentId = await this.startAgent(
1394
+ command,
1395
+ record.cwd || record.projectWorkspace || null,
1396
+ null,
1397
+ options
1398
+ );
1399
+ } catch (error) {
1400
+ console.warn(
1401
+ `Failed to restore Terminal session ${record.runtimeAgentId || sessionId} after native PTY runtime rotation:`,
1402
+ error && (error.message || error)
1403
+ );
1404
+ continue;
1405
+ }
1406
+ const replacement = restartedAgentId ? this.agents.get(restartedAgentId) : null;
1407
+ if (!replacement) {
1408
+ console.warn(
1409
+ `Failed to restore Terminal session ${record.runtimeAgentId || sessionId} after native PTY runtime rotation`
1410
+ );
1411
+ continue;
1412
+ }
1413
+
1414
+ replacement.pinned = record.pinned === true;
1415
+ replacement.projectOrder = finiteOrder(record.projectOrder);
1416
+ replacement.pinnedOrder = finiteOrder(record.pinnedOrder);
1417
+ replacement.customTitle = record.customTitle || replacement.customTitle || '';
1418
+ replacement.terminalInputReceived = record.terminalInputReceived === true;
1419
+ replacement.attentionSeq = finiteNonNegativeInteger(record.attentionSeq);
1420
+ replacement.readAttentionSeq = finiteNonNegativeInteger(record.readAttentionSeq);
1421
+ replacement.attentionUpdatedAt = finiteNumberOrNull(record.attentionUpdatedAt);
1422
+ replacement.readAttentionAt = finiteNumberOrNull(record.readAttentionAt);
1423
+ replacement.attentionReason = record.attentionReason || '';
1424
+ replacement.attentionOutputSeq = finiteNumberOrNull(record.attentionOutputSeq);
1425
+ replacement.unread = agentAttentionUnread(replacement);
1426
+ this.ensurePersistentAgentSession(replacement);
1427
+ if (sessionKey) liveProviderSessions.add(sessionKey);
1428
+ changed = true;
1429
+ }
1430
+ if (changed) this.emit('update');
1431
+ }
1432
+
843
1433
  async recoverAcpSessions() {
844
1434
  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;
1435
+ const mainPageOrder = new Map(this.getMainPageSessionKeys().map((key, index) => [key, index]));
1436
+ const records = this.configManager.listAgentSessionRecords()
1437
+ .filter(record => record && record.archived !== true && runtimeKind(record) === 'acp')
1438
+ .sort((left, right) => {
1439
+ const leftOrder = mainPageOrder.get(left.providerSessionKey);
1440
+ const rightOrder = mainPageOrder.get(right.providerSessionKey);
1441
+ return (leftOrder ?? Number.MAX_SAFE_INTEGER) - (rightOrder ?? Number.MAX_SAFE_INTEGER);
1442
+ });
1443
+
1444
+ // Materialize every recoverable row before loading any transcript. Large
1445
+ // Codex histories can take tens of seconds each; creating rows one by one
1446
+ // after every await temporarily leaves later main-page sessions invisible
1447
+ // in both Projects and History.
1448
+ for (const record of records) {
847
1449
  const agentId = String(record.runtimeAgentId || '').trim();
848
1450
  const provider = String(record.providerSessionProvider || record.provider || '').trim();
849
1451
  const sessionId = String(record.providerSessionId || '').trim();
850
- if (!agentId || !sessionId || !['codex', 'claude', 'opencode', 'qoder'].includes(provider)) continue;
851
- let agent = this.agents.get(agentId);
1452
+ if (!agentId || !sessionId || !providerSupportsRuntime(provider, 'acp')) continue;
1453
+ const agent = this.agents.get(agentId);
852
1454
  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);
1455
+ const recoveredAgent = this.recoveredAgentRecord(agentId, record.engine || 'native', record, { status: 'running' });
1456
+ ensureAgentOrders(recoveredAgent, Array.from(this.agents.values()));
1457
+ recoveredAgent.persistentSessionId = record.id || '';
1458
+ recoveredAgent.engineStarted = false;
1459
+ runtimeBindingOf(recoveredAgent, 'acp').state = 'connecting';
1460
+ this.agents.set(agentId, recoveredAgent);
1461
+ void this.refreshAgentWorktree(agentId);
858
1462
  this.lastActivity.set(agentId, Date.now());
859
1463
  }
1464
+ }
1465
+ this.emit('update');
1466
+
1467
+ for (const record of records) {
1468
+ const agentId = String(record.runtimeAgentId || '').trim();
1469
+ const provider = String(record.providerSessionProvider || record.provider || '').trim();
1470
+ const sessionId = String(record.providerSessionId || '').trim();
1471
+ const agent = this.agents.get(agentId);
1472
+ if (!agent || !sessionId || !providerSupportsRuntime(provider, 'acp')) continue;
860
1473
  try {
861
- const executableName = provider === 'qoder' ? 'qodercli' : provider;
1474
+ const executableName = getProviderAdapter(provider).executable;
862
1475
  const executable = resolveAgentExecutable(executableName) || executableName;
863
1476
  const approvalMode = agent.launchPermissionMode || (
864
1477
  provider === 'codex' && this.configManager.getCodexApprovalMode
@@ -872,31 +1485,29 @@ class AgentManager extends EventEmitter {
872
1485
  env: this.buildAgentEnv(agentId, agent),
873
1486
  cwd: agent.cwd,
874
1487
  sessionId,
875
- historyMode: 'load',
1488
+ historyMode: 'checkpoint',
1489
+ providerHomeId: agent.providerHomeId || record.providerHomeId || 'default',
876
1490
  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
- : '',
1491
+ // Let Codex resolve its selected Home config and existing session
1492
+ // state instead of applying today's Farming launch defaults.
1493
+ model: 'config',
1494
+ reasoningEffort: 'config',
1495
+ serviceTier: 'config',
886
1496
  });
887
1497
  agent.providerSessionId = prepared.sessionId;
888
1498
  agent.providerSessionTemporary = false;
889
1499
  agent.providerSessionSource = `acp-${prepared.historyMode}`;
890
- agent.agentRuntimeMode = 'acp';
891
- agent.acpState = 'idle';
892
- agent.acpError = '';
1500
+ const runtime = replaceRuntimeBinding(agent, 'acp', runtimeBindingOf(agent, 'acp'));
1501
+ runtime.state = 'idle';
1502
+ runtime.error = '';
893
1503
  agent.status = 'running';
894
1504
  agent.engineStatus = 'running';
895
1505
  agent.engineStarted = false;
896
1506
  this.ensurePersistentAgentSession(agent);
897
1507
  } catch (error) {
898
- agent.acpState = 'error';
899
- agent.acpError = `ACP recovery failed: ${error && (error.message || error)}`;
1508
+ const runtime = runtimeBindingOf(agent, 'acp');
1509
+ runtime.state = 'error';
1510
+ runtime.error = `ACP recovery failed: ${error && (error.message || error)}`;
900
1511
  agent.status = 'stopped';
901
1512
  agent.engineStatus = 'stopped';
902
1513
  agent.engineStarted = false;
@@ -911,7 +1522,7 @@ class AgentManager extends EventEmitter {
911
1522
  if (!this.codexAppServerRuntime || !this.configManager || typeof this.configManager.listAgentSessionRecords !== 'function') return;
912
1523
  const records = this.configManager.listAgentSessionRecords();
913
1524
  for (const record of records) {
914
- if (!record || record.archived === true || record.codexRuntimeMode !== 'app-server') continue;
1525
+ if (!record || record.archived === true || runtimeKind(record) !== 'app-server') continue;
915
1526
  const agentId = String(record.runtimeAgentId || '').trim();
916
1527
  const threadId = String(record.codexAppServerThreadId || record.providerSessionId || '').trim();
917
1528
  const home = String(record.codexAppServerHomePath || '').trim();
@@ -924,16 +1535,7 @@ class AgentManager extends EventEmitter {
924
1535
  }
925
1536
  if (agent) {
926
1537
  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;
1538
+ replaceRuntimeBinding(agent, 'terminal');
937
1539
  this.ensurePersistentAgentSession(agent);
938
1540
  this.updateEngineProviderSessionMetadata(agent);
939
1541
  }
@@ -947,6 +1549,7 @@ class AgentManager extends EventEmitter {
947
1549
  agent.persistentSessionId = record.id || '';
948
1550
  agent.engineStarted = false;
949
1551
  this.agents.set(agentId, agent);
1552
+ void this.refreshAgentWorktree(agentId);
950
1553
  this.lastActivity.set(agentId, Date.now());
951
1554
  }
952
1555
  try {
@@ -961,24 +1564,26 @@ class AgentManager extends EventEmitter {
961
1564
  executable,
962
1565
  env: this.buildAgentEnv(agentId, agent),
963
1566
  cwd: agent.cwd,
964
- workspaceRoot: agent.projectWorkspace || agent.cwd,
1567
+ workspaceRoot: effectiveAgentWorkspaceRoot(agent),
965
1568
  approvalMode: agent.launchPermissionMode || 'approve',
966
1569
  });
967
- agent.codexAppServerThreadId = binding.threadId;
968
- agent.codexAppServerState = 'idle';
969
- agent.codexAppServerError = '';
1570
+ const runtime = runtimeBindingOf(agent, 'app-server');
1571
+ runtime.threadId = binding.threadId;
1572
+ runtime.state = 'idle';
1573
+ runtime.error = '';
970
1574
  // App Server owns this runtime. It deliberately has no CLI/PTy
971
1575
  // observer, so recovery cannot create a second reader or writer.
972
1576
  agent.status = 'running';
973
1577
  agent.engineStatus = 'running';
974
1578
  agent.engineStarted = false;
975
- agent.codexCliObserverDeferred = false;
1579
+ runtime.observerDeferred = false;
976
1580
  agent.exitedAt = null;
977
- this.activateProviderSessionTracking(agentId);
1581
+ this.providerSessionService.activate(agentId);
978
1582
  this.ensurePersistentAgentSession(agent);
979
1583
  } catch (error) {
980
- agent.codexAppServerError = `Codex App Server recovery failed: ${error && (error.message || error)}`;
981
- agent.codexAppServerState = 'error';
1584
+ const runtime = runtimeBindingOf(agent, 'app-server');
1585
+ runtime.error = `Codex App Server recovery failed: ${error && (error.message || error)}`;
1586
+ runtime.state = 'error';
982
1587
  agent.status = 'stopped';
983
1588
  agent.engineStatus = 'stopped';
984
1589
  agent.engineStarted = false;
@@ -1007,6 +1612,15 @@ class AgentManager extends EventEmitter {
1007
1612
  const wantsMain = metadata.wantsMain === true;
1008
1613
  const providerSessionProvider = metadata.providerSessionProvider || metadata.provider || '';
1009
1614
  const recoverCodexAppServer = shouldRecoverAsCodexAppServer(metadata);
1615
+ const persistedRuntimeKind = runtimeKind(metadata);
1616
+ const recoveredRuntimeKind = recoverCodexAppServer
1617
+ ? 'app-server'
1618
+ : (persistedRuntimeKind === 'app-server' ? 'terminal' : persistedRuntimeKind);
1619
+ const runtimeBinding = runtimeBindingFor(recoveredRuntimeKind, {
1620
+ ...metadata,
1621
+ codexAppServerThreadId: metadata.codexAppServerThreadId || metadata.providerSessionId || '',
1622
+ codexCliObserverDeferred: false,
1623
+ });
1010
1624
  return {
1011
1625
  id: agentId,
1012
1626
  command: metadata.forkCommand || metadata.command || '',
@@ -1042,34 +1656,14 @@ class AgentManager extends EventEmitter {
1042
1656
  providerSessionSource: metadata.providerSessionSource || '',
1043
1657
  providerSessionResolvedAt: metadata.providerSessionResolvedAt || null,
1044
1658
  providerSessionTitle: metadata.providerSessionTitle || '',
1659
+ providerSessionWorkspace: metadata.providerSessionWorkspace || '',
1045
1660
  terminalInputReceived: metadata.terminalInputReceived === true,
1046
1661
  // Older persisted sessions predate App Server mode. Also, a Codex
1047
1662
  // App Server record without its isolated runtime home is not actually
1048
1663
  // attachable; recover it as terminal-owned CLI instead of leaving the
1049
1664
  // UI in a split state where the pane is terminal but Composer sends to
1050
1665
  // 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,
1666
+ runtimeBinding,
1073
1667
  forkedFromProviderSessionId: metadata.forkedFromProviderSessionId || '',
1074
1668
  restartedFromAgentId: metadata.restartedFromAgentId || '',
1075
1669
  restartedFromAgentIds: Array.isArray(metadata.restartedFromAgentIds)
@@ -1095,7 +1689,10 @@ class AgentManager extends EventEmitter {
1095
1689
  attentionUpdatedAt: finiteNumberOrNull(metadata.attentionUpdatedAt),
1096
1690
  readAttentionAt: finiteNumberOrNull(metadata.readAttentionAt),
1097
1691
  attentionReason: metadata.attentionReason || '',
1692
+ attentionOutputEpoch: metadata.attentionOutputEpoch || '',
1098
1693
  attentionOutputSeq: finiteNumberOrNull(metadata.attentionOutputSeq),
1694
+ readOutputEpoch: metadata.readOutputEpoch || '',
1695
+ readOutputSeq: finiteNumberOrNull(metadata.readOutputSeq),
1099
1696
  unread: finiteNonNegativeInteger(metadata.attentionSeq) > finiteNonNegativeInteger(metadata.readAttentionSeq),
1100
1697
  archived: false,
1101
1698
  archivedAt: null,
@@ -1104,6 +1701,8 @@ class AgentManager extends EventEmitter {
1104
1701
  engineStarted: true,
1105
1702
  engineStatus: state.status || 'running',
1106
1703
  startedAt: state.startedAt || metadata.startedAt || Date.now(),
1704
+ runtimeEpoch: typeof state.runtimeEpoch === 'string' ? state.runtimeEpoch : '',
1705
+ stateRevision: finiteNumberOrNull(state.stateRevision),
1107
1706
  lastEngineOutputAt: Date.now(),
1108
1707
  lastOutputSeq: finiteNumberOrNull(state.outputSeq),
1109
1708
  attentionRequiresNewOutput: true,
@@ -1140,10 +1739,6 @@ class AgentManager extends EventEmitter {
1140
1739
  return Number.isFinite(startedAt) && Date.now() - startedAt < MISSING_ENGINE_SESSION_STARTUP_GRACE_MS;
1141
1740
  }
1142
1741
 
1143
- providerSessionKey(provider, sessionId, providerHomeId = '') {
1144
- return provider && sessionId ? mainPageAgentSessionKey(provider, sessionId, providerHomeId) : '';
1145
- }
1146
-
1147
1742
  getMainPageSessionKeys() {
1148
1743
  if (this.configManager && typeof this.configManager.getMainPageSessionKeys === 'function') {
1149
1744
  return this.configManager.getMainPageSessionKeys();
@@ -1177,19 +1772,6 @@ class AgentManager extends EventEmitter {
1177
1772
  return persistentSessionId;
1178
1773
  }
1179
1774
 
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
1775
  rememberMainPageProviderSession(agent) {
1194
1776
  if (!agent || agent.wantsMain) return;
1195
1777
  if (!agent.providerSessionProvider || !agent.providerSessionId || agent.providerSessionTemporary === true) return;
@@ -1197,7 +1779,7 @@ class AgentManager extends EventEmitter {
1197
1779
  return;
1198
1780
  }
1199
1781
 
1200
- const sessionKey = this.providerSessionKey(agent.providerSessionProvider, agent.providerSessionId, agent.providerHomeId || '');
1782
+ const sessionKey = mainPageAgentSessionKey(agent.providerSessionProvider, agent.providerSessionId, agent.providerHomeId || '');
1201
1783
  if (!sessionKey) return;
1202
1784
  const currentKeys = this.getMainPageSessionKeys();
1203
1785
  if (currentKeys[0] === sessionKey) {
@@ -1230,23 +1812,7 @@ class AgentManager extends EventEmitter {
1230
1812
  providerSessionResolvedAt: agent.providerSessionResolvedAt || null,
1231
1813
  providerSessionTitle: agent.providerSessionTitle || '',
1232
1814
  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,
1815
+ ...legacyRuntimeMetadata(agent),
1250
1816
  forkedFromProviderSessionId: agent.forkedFromProviderSessionId || '',
1251
1817
  launchPermissionMode: agent.launchPermissionMode || '',
1252
1818
  attentionSeq: finiteNonNegativeInteger(agent.attentionSeq),
@@ -1254,7 +1820,10 @@ class AgentManager extends EventEmitter {
1254
1820
  attentionUpdatedAt: finiteNumberOrNull(agent.attentionUpdatedAt),
1255
1821
  readAttentionAt: finiteNumberOrNull(agent.readAttentionAt),
1256
1822
  attentionReason: agent.attentionReason || '',
1823
+ attentionOutputEpoch: agent.attentionOutputEpoch || '',
1257
1824
  attentionOutputSeq: finiteNumberOrNull(agent.attentionOutputSeq),
1825
+ readOutputEpoch: agent.readOutputEpoch || '',
1826
+ readOutputSeq: finiteNumberOrNull(agent.readOutputSeq),
1258
1827
  projectOrder: finiteOrder(agent.projectOrder),
1259
1828
  pinnedOrder: finiteOrder(agent.pinnedOrder),
1260
1829
  })).catch((error) => {
@@ -1262,33 +1831,6 @@ class AgentManager extends EventEmitter {
1262
1831
  });
1263
1832
  }
1264
1833
 
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
1834
  isAgentAttentionTurnActive(agent) {
1293
1835
  if (!agent) return false;
1294
1836
  if (agent.status === 'pending') return true;
@@ -1333,8 +1875,16 @@ class AgentManager extends EventEmitter {
1333
1875
  agent.attentionSeq = nextSeq;
1334
1876
  agent.attentionUpdatedAt = now;
1335
1877
  agent.attentionReason = reason;
1878
+ agent.attentionOutputEpoch = typeof agent.runtimeEpoch === 'string' ? agent.runtimeEpoch : '';
1336
1879
  agent.attentionOutputSeq = Number.isFinite(agent.lastOutputSeq) ? agent.lastOutputSeq : null;
1337
- if (agent.attentionAutoReadNext === true) {
1880
+ const attentionOutputAlreadyRead = Boolean(
1881
+ agent.attentionOutputEpoch
1882
+ && agent.attentionOutputEpoch === agent.readOutputEpoch
1883
+ && agent.attentionOutputSeq !== null
1884
+ && Number.isFinite(agent.readOutputSeq)
1885
+ && agent.attentionOutputSeq <= agent.readOutputSeq
1886
+ );
1887
+ if (agent.attentionAutoReadNext === true || attentionOutputAlreadyRead) {
1338
1888
  agent.attentionAutoReadNext = false;
1339
1889
  agent.readAttentionSeq = nextSeq;
1340
1890
  agent.readAttentionAt = now;
@@ -1351,7 +1901,7 @@ class AgentManager extends EventEmitter {
1351
1901
  };
1352
1902
  }
1353
1903
 
1354
- markAgentReadCursor(agentId, readAttentionSeq) {
1904
+ markAgentReadCursor(agentId, readAttentionSeq, options = {}) {
1355
1905
  const agent = this.agents.get(agentId);
1356
1906
  if (!agent) {
1357
1907
  return { error: 'Agent not found' };
@@ -1370,7 +1920,7 @@ class AgentManager extends EventEmitter {
1370
1920
  if (changed) {
1371
1921
  this.ensurePersistentAgentSession(agent);
1372
1922
  this.updateEngineProviderSessionMetadata(agent);
1373
- this.emit('update');
1923
+ if (options.emitUpdate !== false) this.emit('update');
1374
1924
  }
1375
1925
  return {
1376
1926
  agentId,
@@ -1381,6 +1931,48 @@ class AgentManager extends EventEmitter {
1381
1931
  };
1382
1932
  }
1383
1933
 
1934
+ markAgentReadOutputCut(agentId, runtimeEpoch, outputSeq) {
1935
+ const agent = this.agents.get(agentId);
1936
+ if (!agent) {
1937
+ return { error: 'Agent not found' };
1938
+ }
1939
+
1940
+ const currentRuntimeEpoch = typeof agent.runtimeEpoch === 'string' ? agent.runtimeEpoch : '';
1941
+ const requestedRuntimeEpoch = typeof runtimeEpoch === 'string' ? runtimeEpoch : '';
1942
+ const currentOutputSeq = finiteNumberOrNull(agent.lastOutputSeq);
1943
+ if (
1944
+ !currentRuntimeEpoch
1945
+ || requestedRuntimeEpoch !== currentRuntimeEpoch
1946
+ || currentOutputSeq === null
1947
+ || !Number.isFinite(outputSeq)
1948
+ ) {
1949
+ return {
1950
+ agentId,
1951
+ readOutputEpoch: typeof agent.readOutputEpoch === 'string' ? agent.readOutputEpoch : '',
1952
+ readOutputSeq: finiteNumberOrNull(agent.readOutputSeq),
1953
+ changed: false,
1954
+ };
1955
+ }
1956
+
1957
+ const requestedOutputSeq = Math.max(0, Math.floor(outputSeq));
1958
+ const nextOutputSeq = Math.min(currentOutputSeq, requestedOutputSeq);
1959
+ const previousOutputSeq = agent.readOutputEpoch === currentRuntimeEpoch
1960
+ ? finiteNumberOrNull(agent.readOutputSeq)
1961
+ : null;
1962
+ const readOutputSeq = previousOutputSeq === null
1963
+ ? nextOutputSeq
1964
+ : Math.max(previousOutputSeq, nextOutputSeq);
1965
+ const changed = agent.readOutputEpoch !== currentRuntimeEpoch || previousOutputSeq !== readOutputSeq;
1966
+ agent.readOutputEpoch = currentRuntimeEpoch;
1967
+ agent.readOutputSeq = readOutputSeq;
1968
+ return {
1969
+ agentId,
1970
+ readOutputEpoch: agent.readOutputEpoch,
1971
+ readOutputSeq: agent.readOutputSeq,
1972
+ changed,
1973
+ };
1974
+ }
1975
+
1384
1976
  markAgentUnreadCursor(agentId) {
1385
1977
  const agent = this.agents.get(agentId);
1386
1978
  if (!agent) {
@@ -1414,173 +2006,38 @@ class AgentManager extends EventEmitter {
1414
2006
  };
1415
2007
  }
1416
2008
 
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 = {}) {
2009
+ async refreshAgentWorktree(agentId, workspaceCandidate = '') {
1427
2010
  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;
1554
- }
1555
-
1556
- resolveProviderSession(agentId, { provider, sessionId, source, title }) {
1557
- 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
- });
2011
+ if (!agent || agent.isMain || agent.wantsMain) return false;
2012
+ const candidate = normalizePathValue(
2013
+ workspaceCandidate
2014
+ || agent.providerSessionWorkspace
2015
+ || agent.shellCwd
2016
+ || agent.projectWorkspace
2017
+ || agent.cwd
2018
+ );
2019
+ if (!candidate) return false;
2020
+
2021
+ const generation = (this.agentWorktreeResolveGeneration.get(agentId) || 0) + 1;
2022
+ this.agentWorktreeResolveGeneration.set(agentId, generation);
2023
+ const baseWorkspace = normalizePathValue(agent.projectWorkspace || agent.cwd);
2024
+ const [info, baseInfo] = await Promise.all([
2025
+ inspectGitWorktree(candidate),
2026
+ inspectGitWorktree(baseWorkspace),
2027
+ ]);
2028
+ if (this.agentWorktreeResolveGeneration.get(agentId) !== generation) return false;
2029
+
2030
+ const current = this.agents.get(agentId);
2031
+ if (!current) return false;
2032
+ const nextWorktree = info
2033
+ && baseInfo
2034
+ && info.commonDir === baseInfo.commonDir
2035
+ ? info
2036
+ : null;
2037
+ const previousProjection = JSON.stringify(publicAgentGitWorktree(current));
2038
+ current.gitWorktree = nextWorktree;
2039
+ const nextProjection = JSON.stringify(publicAgentGitWorktree(current));
2040
+ if (previousProjection === nextProjection) return false;
1584
2041
  this.emit('update');
1585
2042
  return true;
1586
2043
  }
@@ -1593,7 +2050,18 @@ class AgentManager extends EventEmitter {
1593
2050
  }
1594
2051
 
1595
2052
  this.lastActivityUpdate.set(sessionId, now);
1596
- this.emit('update');
2053
+ const agent = this.agents.get(sessionId);
2054
+ if (!agent) return;
2055
+ const isMain = this.isMainAgentRecord(sessionId, agent);
2056
+ const lastActivity = this.lastActivity.get(sessionId) || now;
2057
+ this.emit('agent-activity', {
2058
+ agentId: sessionId,
2059
+ lastActivity,
2060
+ activityLevel: isMain ? 'warm' : this.calculateActivityLevel(lastActivity, now),
2061
+ attentionScore: isMain ? 0 : this.calculateAttentionScore(sessionId, now),
2062
+ isZombie: isMain ? false : this.isZombie(sessionId, now),
2063
+ usageRate: this.getAgentUsageRate(sessionId, { now }),
2064
+ });
1597
2065
  }
1598
2066
 
1599
2067
  updateAgentSessionTitle(agent, title) {
@@ -1688,7 +2156,7 @@ class AgentManager extends EventEmitter {
1688
2156
  env.FARMING_IS_MAIN_AGENT = agent.wantsMain ? '1' : '0';
1689
2157
  env.FARMING_SKILLS_COMMAND = 'farming skills';
1690
2158
  env.FARMING_MAIN_WORKSPACE = agent.mainWorkspace || '';
1691
- env.FARMING_PROJECT_WORKSPACE = agent.projectWorkspace || '';
2159
+ env.FARMING_PROJECT_WORKSPACE = effectiveAgentWorkspaceRoot(agent);
1692
2160
 
1693
2161
  if (agent.parentAgentId) {
1694
2162
  env.FARMING_PARENT_AGENT_ID = agent.parentAgentId;
@@ -1710,10 +2178,12 @@ class AgentManager extends EventEmitter {
1710
2178
  }
1711
2179
  if (agent.providerHomePath) {
1712
2180
  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;
2181
+ const appServerHome = runtimeBindingOf(agent, 'app-server')?.homePath || '';
2182
+ applyProviderHomeEnvironment(
2183
+ env,
2184
+ provider,
2185
+ provider === 'codex' ? (appServerHome || agent.providerHomePath) : agent.providerHomePath
2186
+ );
1717
2187
  }
1718
2188
 
1719
2189
  return env;
@@ -1825,21 +2295,22 @@ class AgentManager extends EventEmitter {
1825
2295
  clearInterval(this.heartbeatInterval);
1826
2296
  this.heartbeatInterval = null;
1827
2297
  }
1828
- this.codexSessionResolveInFlight.clear();
1829
- this.codexSessionResolveLastAttemptAt.clear();
1830
- this.providerSessionTitleResolveInFlight.clear();
1831
- this.providerSessionTitleResolveLastAttemptAt.clear();
2298
+ this.providerSessionService.dispose();
2299
+ this.agentWorktreeResolveGeneration.clear();
1832
2300
  this.permissionRestartInFlight.clear();
1833
2301
  this.runtimeRestartInFlight.clear();
1834
2302
  this.permissionRestartSuppressedAgentIds.clear();
2303
+ this.pendingResizeByAgent.clear();
2304
+ this.resizeDrains.clear();
1835
2305
  this.inputQueues.clear();
2306
+ this.codexTerminalProfileQueues.clear();
1836
2307
  if (this.codexAppServerRuntime && typeof this.codexAppServerRuntime.dispose === 'function') {
1837
2308
  this.codexAppServerRuntime.dispose();
1838
2309
  }
1839
2310
  if (this.jsonCliRuntime) {
1840
2311
  for (const agentId of this.agents.keys()) this.jsonCliRuntime.unregisterAgent(agentId);
1841
2312
  }
1842
- if (this.acpRuntime && typeof this.acpRuntime.dispose === 'function') this.acpRuntime.dispose();
2313
+ if (this.acpRuntime && typeof this.acpRuntime.dispose === 'function') await this.acpRuntime.dispose();
1843
2314
  if (this.engineBridge && typeof this.engineBridge.dispose === 'function') {
1844
2315
  await this.engineBridge.dispose({
1845
2316
  preserveHost: options.preserveTerminalHost === true,
@@ -1867,6 +2338,7 @@ class AgentManager extends EventEmitter {
1867
2338
  forkCommand: agent.forkCommand,
1868
2339
  cwd: agent.cwd,
1869
2340
  projectWorkspace: agent.projectWorkspace || '',
2341
+ gitWorktree: publicAgentGitWorktree(agent),
1870
2342
  mainWorkspace: agent.mainWorkspace || '',
1871
2343
  wantsMain: agent.wantsMain === true,
1872
2344
  category: agent.category,
@@ -1884,31 +2356,9 @@ class AgentManager extends EventEmitter {
1884
2356
  providerSessionSource: agent.providerSessionSource,
1885
2357
  providerSessionResolvedAt: agent.providerSessionResolvedAt,
1886
2358
  providerSessionTitle: agent.providerSessionTitle,
2359
+ providerSessionWorkspace: agent.providerSessionWorkspace || '',
1887
2360
  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,
2361
+ ...legacyRuntimeMetadata(agent),
1912
2362
  forkedFromProviderSessionId: agent.forkedFromProviderSessionId,
1913
2363
  restartedFromAgentId: agent.restartedFromAgentId,
1914
2364
  restartedFromAgentIds: agent.restartedFromAgentIds,
@@ -1923,7 +2373,10 @@ class AgentManager extends EventEmitter {
1923
2373
  attentionUpdatedAt: agent.attentionUpdatedAt,
1924
2374
  readAttentionAt: agent.readAttentionAt,
1925
2375
  attentionReason: agent.attentionReason,
2376
+ attentionOutputEpoch: agent.attentionOutputEpoch,
1926
2377
  attentionOutputSeq: agent.attentionOutputSeq,
2378
+ readOutputEpoch: agent.readOutputEpoch,
2379
+ readOutputSeq: agent.readOutputSeq,
1927
2380
  };
1928
2381
  }
1929
2382
 
@@ -1936,11 +2389,12 @@ class AgentManager extends EventEmitter {
1936
2389
  env: this.buildAgentEnv(agent.id, agent),
1937
2390
  category: launch.category,
1938
2391
  metadata: this.engineSessionMetadata(agent),
2392
+ reviveState: launch.reviveState || null,
1939
2393
  });
1940
2394
  }
1941
2395
 
1942
2396
  async startAgent(command, customWorkspace, callback, options = {}) {
1943
- if (options.wantsMain !== false) {
2397
+ if (options.wantsMain !== false && options.skipRecoveryWait !== true) {
1944
2398
  await this.whenRecovered();
1945
2399
  }
1946
2400
 
@@ -1960,6 +2414,29 @@ class AgentManager extends EventEmitter {
1960
2414
  && this.configManager
1961
2415
  && this.configManager.getDangerouslySkipAgentPermissionsByDefault()
1962
2416
  );
2417
+ const preserveProviderSessionProfile = options.preserveProviderSessionProfile === true
2418
+ || codexCommandContinuesSession(command);
2419
+ const codexModel = preserveProviderSessionProfile
2420
+ ? 'config'
2421
+ : (typeof options.codexModel === 'string'
2422
+ ? options.codexModel
2423
+ : (this.configManager && this.configManager.getCodexModel
2424
+ ? this.configManager.getCodexModel()
2425
+ : 'gpt-5.5'));
2426
+ const codexReasoningEffort = preserveProviderSessionProfile
2427
+ ? 'config'
2428
+ : (typeof options.codexReasoningEffort === 'string'
2429
+ ? options.codexReasoningEffort
2430
+ : (this.configManager && this.configManager.getCodexReasoningEffort
2431
+ ? this.configManager.getCodexReasoningEffort()
2432
+ : 'xhigh'));
2433
+ const codexServiceTier = preserveProviderSessionProfile
2434
+ ? 'config'
2435
+ : (typeof options.codexServiceTier === 'string'
2436
+ ? options.codexServiceTier
2437
+ : (this.configManager && this.configManager.getCodexServiceTier
2438
+ ? this.configManager.getCodexServiceTier()
2439
+ : 'default'));
1963
2440
  const launch = resolveLaunchCommand(command, {
1964
2441
  dangerouslySkipPermissions,
1965
2442
  agentLaunchProfiles: this.configManager && this.configManager.getAgentLaunchProfiles
@@ -1974,15 +2451,9 @@ class AgentManager extends EventEmitter {
1974
2451
  codexModelPreset: this.configManager && this.configManager.getCodexModelPreset
1975
2452
  ? this.configManager.getCodexModelPreset()
1976
2453
  : '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',
2454
+ codexModel,
2455
+ codexReasoningEffort,
2456
+ codexServiceTier,
1986
2457
  mainAgentSystemPrompt: wantsMain ? renderMainAgentBootstrap() : '',
1987
2458
  });
1988
2459
  const program = launch.program;
@@ -2105,7 +2576,19 @@ class AgentManager extends EventEmitter {
2105
2576
  return null;
2106
2577
  }
2107
2578
 
2108
- const agentId = `agent-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
2579
+ const requestedRuntimeAgentId = typeof options.runtimeAgentId === 'string'
2580
+ ? options.runtimeAgentId.trim()
2581
+ : '';
2582
+ if (requestedRuntimeAgentId && !/^agent-[A-Za-z0-9_-]+$/.test(requestedRuntimeAgentId)) {
2583
+ if (callback) callback(null, 'Invalid runtime Agent id');
2584
+ return null;
2585
+ }
2586
+ if (requestedRuntimeAgentId && this.agents.has(requestedRuntimeAgentId)) {
2587
+ if (callback) callback(null, `Runtime Agent id is already active: ${requestedRuntimeAgentId}`);
2588
+ return null;
2589
+ }
2590
+ const agentId = requestedRuntimeAgentId ||
2591
+ `agent-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
2109
2592
  const homeProvider = providerSessionPlan.provider || agentHomeProviderForProgram(program);
2110
2593
  const providerHomeId = typeof options.providerHomeId === 'string' && options.providerHomeId.trim()
2111
2594
  ? options.providerHomeId.trim()
@@ -2140,6 +2623,16 @@ class AgentManager extends EventEmitter {
2140
2623
  resolvedProviderHomeId = defaultCodexHome.id || 'default';
2141
2624
  }
2142
2625
  }
2626
+ const requestedAgentRuntimeMode = ['json', 'acp'].includes(options.agentRuntimeMode)
2627
+ ? options.agentRuntimeMode
2628
+ : 'terminal';
2629
+ // A fresh structured runtime does not need a provider CLI resume id yet:
2630
+ // ACP/JSON creates the provider session and writes the resulting id back
2631
+ // after connecting. OpenCode is the important case here because its
2632
+ // terminal CLI does not accept a pre-generated id for a fresh session.
2633
+ const structuredRuntimeProvider = ['json', 'acp'].includes(requestedAgentRuntimeMode)
2634
+ ? homeProvider
2635
+ : providerSessionPlan.provider;
2143
2636
  const requestedCodexRuntimeMode = options.codexRuntimeMode === 'app-server' || options.codexRuntimeMode === 'cli'
2144
2637
  ? options.codexRuntimeMode
2145
2638
  : (this.configManager && typeof this.configManager.getCodexRuntimeMode === 'function'
@@ -2154,20 +2647,19 @@ class AgentManager extends EventEmitter {
2154
2647
  // on the path they are designed to exercise.
2155
2648
  && process.env.FARMING_E2E_FAKE_EXECUTABLES !== '1'
2156
2649
  && normalizeCodexRuntimeMode(requestedCodexRuntimeMode) === 'app-server';
2157
- const requestedAgentRuntimeMode = ['json', 'acp'].includes(options.agentRuntimeMode)
2158
- ? options.agentRuntimeMode
2159
- : 'terminal';
2160
2650
  const useJsonCli = requestedAgentRuntimeMode === 'json'
2161
- && ['codex', 'opencode'].includes(providerSessionPlan.provider)
2651
+ && providerSupportsRuntime(structuredRuntimeProvider, 'json')
2162
2652
  && !useCodexAppServer
2163
2653
  && process.env.FARMING_E2E_FAKE_EXECUTABLES !== '1';
2164
2654
  const useAcp = requestedAgentRuntimeMode === 'acp'
2165
- && ['codex', 'claude', 'opencode', 'qoder'].includes(providerSessionPlan.provider)
2655
+ && providerSupportsRuntime(structuredRuntimeProvider, 'acp')
2166
2656
  && !useCodexAppServer
2167
2657
  && (
2168
2658
  process.env.FARMING_E2E_FAKE_EXECUTABLES !== '1'
2169
2659
  || process.env.FARMING_E2E_FAKE_ACP_AGENT === '1'
2170
2660
  );
2661
+ const acpGeneratedFreshSession = useAcp
2662
+ && isFreshAcpSessionSource(structuredRuntimeProvider, providerSessionPlan.source);
2171
2663
  let codexAppServerHomePath = '';
2172
2664
  if (useCodexAppServer) {
2173
2665
  try {
@@ -2203,44 +2695,27 @@ class AgentManager extends EventEmitter {
2203
2695
  task: typeof options.task === 'string' ? options.task : '',
2204
2696
  workflowTemplate: typeof options.workflowTemplate === 'string' ? options.workflowTemplate : '',
2205
2697
  source: typeof options.source === 'string' ? options.source : 'ui',
2206
- providerSessionProvider: providerSessionPlan.provider || '',
2698
+ providerSessionProvider: useAcp || useJsonCli ? structuredRuntimeProvider : (providerSessionPlan.provider || ''),
2207
2699
  providerHomeId: resolvedProviderHomeId,
2208
2700
  providerHomePath,
2209
2701
  providerSessionId: providerSessionPlan.id || '',
2210
- providerSessionKey: this.providerSessionKey(providerSessionPlan.provider, providerSessionPlan.id, providerHome ? providerHome.id : providerHomeId),
2702
+ providerSessionKey: mainPageAgentSessionKey(providerSessionPlan.provider, providerSessionPlan.id, providerHome ? providerHome.id : providerHomeId),
2211
2703
  providerSessionTemporary: providerSessionPlan.temporary === true,
2212
2704
  providerSessionSource: providerSessionPlan.source || '',
2213
2705
  providerSessionResolvedAt: providerSessionPlan.temporary === true ? null : Date.now(),
2214
2706
  providerSessionTitle: typeof options.providerSessionTitle === 'string' ? options.providerSessionTitle.trim().slice(0, 160) : '',
2707
+ providerSessionWorkspace: '',
2215
2708
  terminalInputReceived: false,
2216
- codexRuntimeMode: providerSessionPlan.provider === 'codex'
2217
- ? (useCodexAppServer ? 'app-server' : 'cli')
2218
- : '',
2219
- agentRuntimeMode: useAcp ? 'acp' : (useJsonCli ? 'json' : 'terminal'),
2220
- jsonCliState: useJsonCli ? 'idle' : '',
2221
- jsonCliError: '',
2222
- jsonCliTranscriptUpdatedAt: '',
2223
- jsonCliEvents: Array.isArray(options.jsonCliEvents) ? options.jsonCliEvents : [],
2224
- acpState: useAcp ? 'connecting' : '',
2225
- acpError: '',
2226
- acpStopReason: '',
2227
- acpPendingPermission: null,
2228
- acpPendingPermissions: [],
2229
- acpPendingElicitation: null,
2230
- acpPendingElicitations: [],
2231
- acpActiveElicitations: [],
2232
- acpSessionUpdatedAt: '',
2233
- acpSessionRevision: 0,
2234
- codexAppServerHomePath,
2235
- codexAppServerState: useCodexAppServer ? 'connecting' : '',
2236
- codexAppServerEndpoint: '',
2237
- codexAppServerThreadId: '',
2238
- codexAppServerTurnId: '',
2239
- codexAppServerError: '',
2240
- codexAppServerPendingRequestId: '',
2241
- codexAppServerPendingRequestMethod: '',
2242
- codexAppServerPendingRequest: null,
2243
- codexCliObserverDeferred: false,
2709
+ runtimeBinding: useCodexAppServer
2710
+ ? runtimeBindingFor('app-server', { state: 'connecting', homePath: codexAppServerHomePath })
2711
+ : (useAcp
2712
+ ? runtimeBindingFor('acp', { state: 'connecting' })
2713
+ : (useJsonCli
2714
+ ? runtimeBindingFor('json', {
2715
+ state: 'idle',
2716
+ events: Array.isArray(options.jsonCliEvents) ? options.jsonCliEvents : [],
2717
+ })
2718
+ : runtimeBindingFor('terminal'))),
2244
2719
  forkedFromProviderSessionId: providerSessionPlan.forkedFromProviderSessionId || '',
2245
2720
  restartedFromAgentId: typeof options.restartedFromAgentId === 'string' ? options.restartedFromAgentId : '',
2246
2721
  restartedFromAgentIds: Array.isArray(options.restartedFromAgentIds)
@@ -2249,7 +2724,7 @@ class AgentManager extends EventEmitter {
2249
2724
  runtimeSwitchVerifiedSessionId: typeof options.runtimeSwitchVerifiedSessionId === 'string'
2250
2725
  ? options.runtimeSwitchVerifiedSessionId
2251
2726
  : '',
2252
- persistentSessionId: '',
2727
+ persistentSessionId: typeof options.persistentSessionId === 'string' ? options.persistentSessionId : '',
2253
2728
  customTitle: typeof options.customTitle === 'string' ? options.customTitle.trim().slice(0, 80) : '',
2254
2729
  terminalBusy: null,
2255
2730
  shellCwd: '',
@@ -2269,7 +2744,10 @@ class AgentManager extends EventEmitter {
2269
2744
  attentionUpdatedAt: null,
2270
2745
  readAttentionAt: null,
2271
2746
  attentionReason: '',
2747
+ attentionOutputEpoch: '',
2272
2748
  attentionOutputSeq: null,
2749
+ readOutputEpoch: '',
2750
+ readOutputSeq: null,
2273
2751
  unread: false,
2274
2752
  archived: false,
2275
2753
  archivedAt: null,
@@ -2286,6 +2764,7 @@ class AgentManager extends EventEmitter {
2286
2764
 
2287
2765
  ensureAgentOrders(agentRecord, Array.from(this.agents.values()));
2288
2766
  this.agents.set(agentId, agentRecord);
2767
+ void this.refreshAgentWorktree(agentId);
2289
2768
 
2290
2769
  this.lastActivity.set(agentId, Date.now());
2291
2770
 
@@ -2294,9 +2773,10 @@ class AgentManager extends EventEmitter {
2294
2773
  try {
2295
2774
  if (useCodexAppServer) {
2296
2775
  try {
2776
+ const appServer = runtimeBindingOf(agentRecord, 'app-server');
2297
2777
  const prepared = await this.codexAppServerRuntime.prepareAgent({
2298
2778
  agentId,
2299
- codexHome: agentRecord.codexAppServerHomePath,
2779
+ codexHome: appServer.homePath,
2300
2780
  executable: spawnProgram,
2301
2781
  env: this.buildAgentEnv(agentId, agentRecord),
2302
2782
  cwd: workspace,
@@ -2308,19 +2788,13 @@ class AgentManager extends EventEmitter {
2308
2788
  ? this.configManager.getCodexApprovalMode()
2309
2789
  : 'approve'
2310
2790
  ),
2311
- model: this.configManager && this.configManager.getCodexModel
2312
- ? this.configManager.getCodexModel()
2313
- : 'gpt-5.5',
2314
- reasoningEffort: this.configManager && this.configManager.getCodexReasoningEffort
2315
- ? this.configManager.getCodexReasoningEffort()
2316
- : 'xhigh',
2317
- serviceTier: this.configManager && this.configManager.getCodexServiceTier
2318
- ? this.configManager.getCodexServiceTier()
2319
- : 'default',
2791
+ model: codexModel,
2792
+ reasoningEffort: codexReasoningEffort,
2793
+ serviceTier: codexServiceTier,
2320
2794
  developerInstructions: wantsMain ? renderMainAgentBootstrap() : '',
2321
2795
  });
2322
2796
  agentRecord.providerSessionId = prepared.threadId;
2323
- agentRecord.providerSessionKey = this.providerSessionKey(
2797
+ agentRecord.providerSessionKey = mainPageAgentSessionKey(
2324
2798
  'codex',
2325
2799
  prepared.threadId,
2326
2800
  agentRecord.providerHomeId || 'default'
@@ -2328,13 +2802,13 @@ class AgentManager extends EventEmitter {
2328
2802
  agentRecord.providerSessionTemporary = false;
2329
2803
  agentRecord.providerSessionSource = prepared.resumed ? 'app-server-resume' : 'app-server-start';
2330
2804
  agentRecord.providerSessionResolvedAt = Date.now();
2331
- agentRecord.codexAppServerState = 'idle';
2332
- agentRecord.codexAppServerEndpoint = prepared.endpoint;
2333
- agentRecord.codexAppServerThreadId = prepared.threadId;
2334
- agentRecord.codexAppServerError = '';
2805
+ appServer.state = 'idle';
2806
+ appServer.endpoint = prepared.endpoint;
2807
+ appServer.threadId = prepared.threadId;
2808
+ appServer.error = '';
2335
2809
  // App Server mode has no embedded CLI observer. Its chat and
2336
2810
  // lifecycle both come from the structured App Server connection.
2337
- agentRecord.codexCliObserverDeferred = false;
2811
+ appServer.observerDeferred = false;
2338
2812
  } catch (error) {
2339
2813
  const reason = error && error.message ? error.message : String(error);
2340
2814
  throw new Error(`Codex App Server failed to start: ${reason}`, { cause: error });
@@ -2342,52 +2816,51 @@ class AgentManager extends EventEmitter {
2342
2816
  }
2343
2817
 
2344
2818
  if (useJsonCli) {
2819
+ const jsonRuntime = runtimeBindingOf(agentRecord, 'json');
2345
2820
  this.jsonCliRuntime.registerAgent({
2346
2821
  agentId,
2347
- provider: providerSessionPlan.provider,
2822
+ provider: structuredRuntimeProvider,
2348
2823
  executable: spawnProgram,
2349
2824
  env: this.buildAgentEnv(agentId, agentRecord),
2350
2825
  cwd: workspace,
2351
2826
  sessionId: agentRecord.providerSessionTemporary ? '' : agentRecord.providerSessionId,
2352
2827
  approvalMode: agentRecord.launchPermissionMode || 'approve',
2353
2828
  autoApprove: options.dangerouslySkipPermissions === true,
2354
- initialEvents: agentRecord.jsonCliEvents,
2829
+ initialEvents: jsonRuntime.events,
2355
2830
  });
2356
2831
  }
2357
2832
 
2358
2833
  if (useAcp) {
2834
+ const acpRuntime = runtimeBindingOf(agentRecord, 'acp');
2359
2835
  const prepared = await this.acpRuntime.prepareAgent({
2360
2836
  agentId,
2361
- provider: providerSessionPlan.provider,
2837
+ provider: structuredRuntimeProvider,
2362
2838
  executable: spawnProgram,
2363
2839
  env: this.buildAgentEnv(agentId, agentRecord),
2364
2840
  cwd: workspace,
2365
- sessionId: options.acpStartFresh === true || agentRecord.providerSessionTemporary
2841
+ sessionId: options.acpStartFresh === true || agentRecord.providerSessionTemporary || acpGeneratedFreshSession
2366
2842
  ? ''
2367
2843
  : agentRecord.providerSessionId,
2368
- historyMode: options.acpHistoryMode === 'resume' ? 'resume' : 'load',
2844
+ historyMode: options.acpHistoryMode === 'resume'
2845
+ ? 'resume'
2846
+ : (options.acpHistoryMode === 'load' ? 'load' : 'checkpoint'),
2847
+ providerHomeId: agentRecord.providerHomeId || 'default',
2369
2848
  approvalMode: agentRecord.launchPermissionMode || 'approve',
2370
- model: this.configManager && this.configManager.getCodexModel
2371
- ? this.configManager.getCodexModel()
2372
- : '',
2373
- reasoningEffort: this.configManager && this.configManager.getCodexReasoningEffort
2374
- ? this.configManager.getCodexReasoningEffort()
2375
- : '',
2376
- serviceTier: this.configManager && this.configManager.getCodexServiceTier
2377
- ? this.configManager.getCodexServiceTier()
2378
- : '',
2849
+ model: codexModel,
2850
+ reasoningEffort: codexReasoningEffort,
2851
+ serviceTier: codexServiceTier,
2379
2852
  });
2380
2853
  agentRecord.providerSessionId = prepared.sessionId;
2381
- agentRecord.providerSessionKey = this.providerSessionKey(
2382
- providerSessionPlan.provider,
2854
+ agentRecord.providerSessionKey = mainPageAgentSessionKey(
2855
+ structuredRuntimeProvider,
2383
2856
  prepared.sessionId,
2384
2857
  agentRecord.providerHomeId || 'default'
2385
2858
  );
2386
2859
  agentRecord.providerSessionTemporary = false;
2387
2860
  agentRecord.providerSessionSource = `acp-${prepared.historyMode}`;
2388
2861
  agentRecord.providerSessionResolvedAt = Date.now();
2389
- agentRecord.acpState = 'idle';
2390
- agentRecord.acpError = '';
2862
+ acpRuntime.state = 'idle';
2863
+ acpRuntime.error = '';
2391
2864
  }
2392
2865
 
2393
2866
  agentRecord.persistentSessionId = this.ensurePersistentAgentSession(agentRecord);
@@ -2397,6 +2870,7 @@ class AgentManager extends EventEmitter {
2397
2870
  args,
2398
2871
  cwd: workspace,
2399
2872
  category: resolution.spec ? resolution.spec.category : 'shell',
2873
+ reviveState: options.reviveTerminalState || null,
2400
2874
  };
2401
2875
  await this.createAgentEngineSession(agentRecord, resolution.engine, engineLaunch);
2402
2876
  }
@@ -2412,18 +2886,33 @@ class AgentManager extends EventEmitter {
2412
2886
  }
2413
2887
  }
2414
2888
 
2415
- this.activateProviderSessionTracking(agentId);
2889
+ this.providerSessionService.activate(agentId);
2416
2890
  if (callback) callback(agentId);
2417
2891
  this.emit('update');
2418
2892
  return agentId;
2419
2893
  } catch (error) {
2420
2894
  console.error('Failed to start agent:', error);
2895
+ if (options.restoreRuntimeAgentIdOnFailure && agentRecord.persistentSessionId) {
2896
+ const failedAgentId = agentRecord.id;
2897
+ try {
2898
+ agentRecord.id = options.restoreRuntimeAgentIdOnFailure;
2899
+ this.ensurePersistentAgentSession(agentRecord);
2900
+ } catch (rollbackError) {
2901
+ console.error(
2902
+ 'Failed to roll back persisted Agent runtime id after restart failure:',
2903
+ rollbackError && (rollbackError.message || rollbackError)
2904
+ );
2905
+ } finally {
2906
+ agentRecord.id = failedAgentId;
2907
+ }
2908
+ }
2421
2909
  this.agents.delete(agentId);
2422
2910
  this.lastActivity.delete(agentId);
2423
2911
  this.lastActivityUpdate.delete(agentId);
2424
2912
  this.outputEvents.delete(agentId);
2913
+ this.agentUsageRateCache.delete(agentId);
2425
2914
  this.lastResizeByAgent.delete(agentId);
2426
- this.stopCodexProviderSessionResolver(agentId);
2915
+ this.providerSessionService.stop(agentId);
2427
2916
  if (this.codexAppServerRuntime && typeof this.codexAppServerRuntime.unregisterAgent === 'function') {
2428
2917
  this.codexAppServerRuntime.unregisterAgent(agentId);
2429
2918
  }
@@ -2439,15 +2928,15 @@ class AgentManager extends EventEmitter {
2439
2928
  }
2440
2929
  }
2441
2930
 
2442
- async sendInput(agentId, input) {
2931
+ async enqueueInputOperation(agentId, operation) {
2443
2932
  const previous = this.inputQueues.get(agentId) || Promise.resolve();
2444
2933
  const next = previous
2445
2934
  .catch(() => {})
2446
- .then(() => this.sendInputNow(agentId, input));
2935
+ .then(operation);
2447
2936
 
2448
2937
  this.inputQueues.set(agentId, next);
2449
2938
  try {
2450
- await next;
2939
+ return await next;
2451
2940
  } finally {
2452
2941
  if (this.inputQueues.get(agentId) === next) {
2453
2942
  this.inputQueues.delete(agentId);
@@ -2455,20 +2944,52 @@ class AgentManager extends EventEmitter {
2455
2944
  }
2456
2945
  }
2457
2946
 
2947
+ async enqueueInputOperationUntilReleased(agentId, operation) {
2948
+ const previous = this.inputQueues.get(agentId) || Promise.resolve();
2949
+ let released = false;
2950
+ let resolveReleased;
2951
+ const releasedPromise = new Promise(resolve => {
2952
+ resolveReleased = resolve;
2953
+ });
2954
+ const release = () => {
2955
+ if (released) return;
2956
+ released = true;
2957
+ resolveReleased();
2958
+ };
2959
+
2960
+ const ready = previous.catch(() => {});
2961
+ const completion = ready.then(() => operation(release));
2962
+ completion.catch(() => release());
2963
+ const boundary = ready.then(() => releasedPromise);
2964
+ this.inputQueues.set(agentId, boundary);
2965
+ boundary.then(() => {
2966
+ if (this.inputQueues.get(agentId) === boundary) {
2967
+ this.inputQueues.delete(agentId);
2968
+ }
2969
+ });
2970
+ return completion;
2971
+ }
2972
+
2973
+ async sendInput(agentId, input, options = {}) {
2974
+ return this.enqueueInputOperation(agentId, () => this.sendInputNow(agentId, input, options));
2975
+ }
2976
+
2458
2977
  codexAppServerOptionsForAgent(agent, message = '') {
2459
2978
  const codexResolution = resolveCompatibleCodexExecutable(agent.requiredCliVersion || '');
2460
2979
  if (!codexResolution.compatible) {
2461
2980
  throw new Error(codexResolution.error || 'Codex CLI is not compatible with App Server mode');
2462
2981
  }
2463
2982
 
2983
+ const runtime = runtimeBindingOf(agent, 'app-server');
2984
+ if (!runtime) throw new Error('Agent is not using the Codex App Server runtime');
2464
2985
  return {
2465
2986
  agentId: agent.id,
2466
- codexHome: agent.codexAppServerHomePath || '',
2987
+ codexHome: runtime.homePath,
2467
2988
  executable: codexResolution.path || resolveAgentExecutable(agent.command) || agent.command,
2468
2989
  env: this.buildAgentEnv(agent.id, agent),
2469
2990
  cwd: agent.cwd,
2470
- workspaceRoot: agent.projectWorkspace || agent.cwd,
2471
- threadId: agent.codexAppServerThreadId || agent.providerSessionId || '',
2991
+ workspaceRoot: effectiveAgentWorkspaceRoot(agent),
2992
+ threadId: runtime.threadId || agent.providerSessionId || '',
2472
2993
  approvalMode: agent.launchPermissionMode || (
2473
2994
  this.configManager && this.configManager.getCodexApprovalMode
2474
2995
  ? this.configManager.getCodexApprovalMode()
@@ -2488,21 +3009,101 @@ class AgentManager extends EventEmitter {
2488
3009
  }
2489
3010
 
2490
3011
  async sendComposerMessage(agentId, message) {
2491
- const previous = this.inputQueues.get(agentId) || Promise.resolve();
2492
- const next = previous
2493
- .catch(() => {})
2494
- .then(() => this.sendComposerMessageNow(agentId, message));
3012
+ return this.enqueueInputOperation(agentId, () => this.sendComposerMessageNow(agentId, message));
3013
+ }
2495
3014
 
2496
- this.inputQueues.set(agentId, next);
3015
+ async setCodexTerminalProfile(agentId, profile, options = {}) {
3016
+ const previous = this.codexTerminalProfileQueues.get(agentId) || Promise.resolve();
3017
+ const start = () => this.enqueueInputOperationUntilReleased(
3018
+ agentId,
3019
+ releaseInput => this.setCodexTerminalProfileNow(agentId, profile, {
3020
+ ...options,
3021
+ onInputSafe: releaseInput,
3022
+ }),
3023
+ );
3024
+ const next = this.codexTerminalProfileQueues.has(agentId)
3025
+ ? previous.catch(() => {}).then(start)
3026
+ : start();
3027
+ this.codexTerminalProfileQueues.set(agentId, next);
2497
3028
  try {
2498
3029
  return await next;
2499
3030
  } finally {
2500
- if (this.inputQueues.get(agentId) === next) {
2501
- this.inputQueues.delete(agentId);
3031
+ if (this.codexTerminalProfileQueues.get(agentId) === next) {
3032
+ this.codexTerminalProfileQueues.delete(agentId);
2502
3033
  }
2503
3034
  }
2504
3035
  }
2505
3036
 
3037
+ async setCodexTerminalProfileNow(agentId, profile, options = {}) {
3038
+ const agent = this.agents.get(agentId);
3039
+ if (!agent) throw new Error('Agent not found');
3040
+ if (
3041
+ agentProgramName(agent.command).toLowerCase() !== 'codex'
3042
+ || isCodexAppServerAgent(agent)
3043
+ || isJsonCliAgent(agent)
3044
+ || isAcpAgent(agent)
3045
+ || runtimeKind(agent) !== 'terminal'
3046
+ ) {
3047
+ throw new Error('This Agent is not using Codex Terminal');
3048
+ }
3049
+ if (!isRunningAgentRuntimeStatus(agent.status)) {
3050
+ throw new Error('Codex Terminal is not running');
3051
+ }
3052
+ if (this.isAgentAttentionTurnActive(agent)) {
3053
+ throw new Error('Wait for the active Codex Terminal turn to finish before changing its model');
3054
+ }
3055
+
3056
+ const applied = await applyCodexTerminalProfile({
3057
+ profile,
3058
+ timeoutMs: options.timeoutMs,
3059
+ signal: options.signal,
3060
+ onInputSafe: options.onInputSafe,
3061
+ readPreview: async () => {
3062
+ const view = await this.getAgentSessionView(agentId);
3063
+ if (!view) throw new Error('Agent not found');
3064
+ return view.previewText;
3065
+ },
3066
+ readOutput: async () => String(await this.getAgentSessionText(agentId) || ''),
3067
+ // `/model` and `/fast on|off` are Farming-owned control traffic. They must not
3068
+ // make a fresh Terminal look user-authored, because that would remove
3069
+ // the safe fresh-session path into ACP Chat before the provider has
3070
+ // materialized a resumable history record.
3071
+ sendInput: async input => this.sendInputNow(agentId, input, { markUserInput: false }),
3072
+ });
3073
+ agent.codexTerminalProfile = {
3074
+ model: applied.model,
3075
+ reasoningEffort: applied.effort,
3076
+ serviceTier: applied.serviceTier,
3077
+ source: 'terminal-command',
3078
+ };
3079
+
3080
+ // The HTTP response confirms the terminal has already reached this profile.
3081
+ // Publish that confirmation immediately instead of waiting for a later PTY
3082
+ // preview tick; otherwise the browser's bounded optimistic state can expire
3083
+ // and briefly fall back to the pre-command footer.
3084
+ const view = options.signal?.aborted
3085
+ ? null
3086
+ : await this.getAgentSessionView(agentId);
3087
+ if (view) {
3088
+ agent.previewText = view.previewText || agent.previewText || '';
3089
+ agent.previewSnapshot = view.previewSnapshot || agent.previewSnapshot || null;
3090
+ agent.previewCols = view.previewCols || agent.previewCols || 80;
3091
+ agent.previewRows = view.previewRows || agent.previewRows || 30;
3092
+ this.emit('session-preview-update', {
3093
+ agentId,
3094
+ previewText: agent.previewText,
3095
+ cols: agent.previewCols,
3096
+ rows: agent.previewRows,
3097
+ previewSnapshot: agent.previewSnapshot,
3098
+ codexTerminalProfile: agent.codexTerminalProfile,
3099
+ terminalStatus: view.terminalStatus,
3100
+ runtimeObservation: deriveRuntimeObservation({ ...agent, terminalStatus: view.terminalStatus }),
3101
+ });
3102
+ }
3103
+ this.emit('update');
3104
+ return applied;
3105
+ }
3106
+
2506
3107
  async sendComposerMessageNow(agentId, message) {
2507
3108
  const agent = this.agents.get(agentId);
2508
3109
  if (!agent) throw new Error('Agent not found');
@@ -2516,19 +3117,20 @@ class AgentManager extends EventEmitter {
2516
3117
  throw new Error('Composer message is empty');
2517
3118
  }
2518
3119
 
3120
+ const appServer = runtimeBindingOf(agent, 'app-server');
2519
3121
  if (
2520
3122
  agent.providerSessionProvider === 'codex'
2521
- && agent.codexRuntimeMode === 'app-server'
3123
+ && appServer
2522
3124
  && this.codexAppServerRuntime
2523
3125
  && typeof this.codexAppServerRuntime.submitComposerMessage === 'function'
2524
3126
  ) {
2525
3127
  const result = await this.codexAppServerRuntime.submitComposerMessage(
2526
3128
  this.codexAppServerOptionsForAgent(agent, text)
2527
3129
  );
2528
- agent.codexAppServerThreadId = result.threadId;
2529
- agent.codexAppServerTurnId = result.turnId;
2530
- agent.codexAppServerState = 'working';
2531
- agent.codexAppServerError = '';
3130
+ appServer.threadId = result.threadId;
3131
+ appServer.turnId = result.turnId;
3132
+ appServer.state = 'working';
3133
+ appServer.error = '';
2532
3134
  this.lastActivity.set(agentId, Date.now());
2533
3135
  this.emit('update');
2534
3136
  return result;
@@ -2546,8 +3148,9 @@ class AgentManager extends EventEmitter {
2546
3148
 
2547
3149
  if (isAcpAgent(agent)) {
2548
3150
  const result = await this.acpRuntime.prompt(agentId, prompt);
2549
- agent.acpState = 'idle';
2550
- agent.acpStopReason = result.stopReason || '';
3151
+ const runtime = runtimeBindingOf(agent, 'acp');
3152
+ runtime.state = 'idle';
3153
+ runtime.stopReason = result.stopReason || '';
2551
3154
  this.ensurePersistentAgentSession(agent);
2552
3155
  return { kind: 'acp', ...result };
2553
3156
  }
@@ -2559,11 +3162,12 @@ class AgentManager extends EventEmitter {
2559
3162
  respondToCodexAppServerRequest(agentId, requestId, result, options = {}) {
2560
3163
  const agent = this.agents.get(agentId);
2561
3164
  if (!agent) throw new Error('Agent not found');
2562
- if (agent.providerSessionProvider !== 'codex' || agent.codexRuntimeMode !== 'app-server') {
3165
+ const runtime = runtimeBindingOf(agent, 'app-server');
3166
+ if (agent.providerSessionProvider !== 'codex' || !runtime) {
2563
3167
  throw new Error('This Agent is not using the Codex App Server runtime');
2564
3168
  }
2565
3169
  const resolvedRequestId = String(requestId || '');
2566
- if (!resolvedRequestId || resolvedRequestId !== agent.codexAppServerPendingRequestId) {
3170
+ if (!resolvedRequestId || resolvedRequestId !== runtime.pendingRequestId) {
2567
3171
  throw new Error('Codex App Server request is no longer pending');
2568
3172
  }
2569
3173
  if (!this.codexAppServerRuntime) throw new Error('Codex App Server runtime is unavailable');
@@ -2575,10 +3179,10 @@ class AgentManager extends EventEmitter {
2575
3179
  { code: -32000, message: String(options.reason || 'Rejected by Farming') }
2576
3180
  )
2577
3181
  : this.codexAppServerRuntime.resolveAgentServerRequest(agentId, resolvedRequestId, result);
2578
- agent.codexAppServerPendingRequestId = '';
2579
- agent.codexAppServerPendingRequestMethod = '';
2580
- agent.codexAppServerPendingRequest = null;
2581
- agent.codexAppServerState = agent.codexAppServerTurnId ? 'working' : 'idle';
3182
+ runtime.pendingRequestId = '';
3183
+ runtime.pendingRequestMethod = '';
3184
+ runtime.pendingRequest = null;
3185
+ runtime.state = runtime.turnId ? 'working' : 'idle';
2582
3186
  this.emit('update');
2583
3187
  return response;
2584
3188
  }
@@ -2586,11 +3190,12 @@ class AgentManager extends EventEmitter {
2586
3190
  assertCodexAppServerGoalAgent(agentId) {
2587
3191
  const agent = this.agents.get(agentId);
2588
3192
  if (!agent) throw new Error('Agent not found');
2589
- if (agent.providerSessionProvider !== 'codex' || agent.codexRuntimeMode !== 'app-server') {
3193
+ const runtime = runtimeBindingOf(agent, 'app-server');
3194
+ if (agent.providerSessionProvider !== 'codex' || !runtime) {
2590
3195
  throw new Error('This Agent is not using the Codex App Server runtime');
2591
3196
  }
2592
3197
  if (!this.codexAppServerRuntime) throw new Error('Codex App Server runtime is unavailable');
2593
- if (!(agent.codexAppServerThreadId || agent.providerSessionId)) {
3198
+ if (!(runtime.threadId || agent.providerSessionId)) {
2594
3199
  throw new Error('Codex App Server thread id is not available yet');
2595
3200
  }
2596
3201
  return agent;
@@ -2599,7 +3204,7 @@ class AgentManager extends EventEmitter {
2599
3204
  async getCodexAppServerGoal(agentId) {
2600
3205
  const agent = this.assertCodexAppServerGoalAgent(agentId);
2601
3206
  const goal = await this.codexAppServerRuntime.getAgentGoal(this.codexAppServerOptionsForAgent(agent));
2602
- agent.codexAppServerGoal = goal || null;
3207
+ runtimeBindingOf(agent, 'app-server').goal = goal || null;
2603
3208
  this.emit('update');
2604
3209
  return goal || null;
2605
3210
  }
@@ -2630,7 +3235,11 @@ class AgentManager extends EventEmitter {
2630
3235
  const agent = this.agents.get(agentId);
2631
3236
  if (!agent) throw new Error('Agent not found');
2632
3237
  if (!isAcpAgent(agent)) throw new Error('Agent is not using the ACP runtime');
2633
- return this.acpRuntime.getTranscriptSession(agentId, options);
3238
+ const transcript = this.acpRuntime.getTranscriptSession(agentId, options);
3239
+ return {
3240
+ ...transcript,
3241
+ entries: transcript.entries.map(entry => entry?.type === 'tool' ? acpTranscriptToolEntry(entry) : entry),
3242
+ };
2634
3243
  }
2635
3244
 
2636
3245
  getAcpToolDetail(agentId, toolCallId) {
@@ -2771,7 +3380,7 @@ class AgentManager extends EventEmitter {
2771
3380
  ...(status !== undefined ? { status } : {}),
2772
3381
  ...(Object.prototype.hasOwnProperty.call(patch, 'tokenBudget') ? { tokenBudget } : {}),
2773
3382
  });
2774
- agent.codexAppServerGoal = goal || null;
3383
+ runtimeBindingOf(agent, 'app-server').goal = goal || null;
2775
3384
  this.emit('update');
2776
3385
  return goal || null;
2777
3386
  }
@@ -2779,43 +3388,43 @@ class AgentManager extends EventEmitter {
2779
3388
  async clearCodexAppServerGoal(agentId) {
2780
3389
  const agent = this.assertCodexAppServerGoalAgent(agentId);
2781
3390
  await this.codexAppServerRuntime.clearAgentGoal(this.codexAppServerOptionsForAgent(agent));
2782
- agent.codexAppServerGoal = null;
3391
+ runtimeBindingOf(agent, 'app-server').goal = null;
2783
3392
  this.emit('update');
2784
3393
  return null;
2785
3394
  }
2786
3395
 
2787
- async sendInputNow(agentId, input) {
3396
+ async sendInputNow(agentId, input, { markUserInput = true, expectedRuntimeEpoch = '' } = {}) {
2788
3397
  const agent = this.agents.get(agentId);
2789
3398
  if (!agent) return;
2790
3399
  // The observer PTY is optional telemetry for App Server Agents. Its
2791
3400
  // disappearance must never redefine the App Server Agent as dead.
2792
3401
  if (isCodexAppServerAgent(agent) || isJsonCliAgent(agent) || isAcpAgent(agent)) return;
3402
+ if (expectedRuntimeEpoch && agent.runtimeEpoch !== expectedRuntimeEpoch) {
3403
+ return { status: 'input-rejected', reason: 'runtime-epoch-mismatch' };
3404
+ }
2793
3405
 
2794
3406
  const engine = this.engineBridge.getEngine(agent.engineName);
2795
3407
  if (!engine) return;
2796
3408
 
2797
- if (agent.terminalInputReceived !== true) {
2798
- agent.terminalInputReceived = true;
2799
- this.ensurePersistentAgentSession(agent);
2800
- this.updateEngineProviderSessionMetadata(agent);
2801
- }
2802
-
2803
- for (let attempt = 0; ; attempt += 1) {
2804
- try {
2805
- await engine.sendInput(agentId, input);
2806
- this.observeAgentStateChange(agentId, { force: true });
2807
- return;
2808
- } catch (error) {
2809
- const delay = INPUT_SESSION_RETRY_DELAYS_MS[attempt];
2810
- if (!isSessionNotAvailableError(error) || delay === undefined) {
2811
- console.error('Failed to send input:', error);
2812
- if (isSessionNotAvailableError(error)) {
2813
- this.markAgentSessionDead(agentId, error);
2814
- }
2815
- return;
2816
- }
2817
- await sleep(delay);
3409
+ try {
3410
+ const submittedUserInput = markUserInput && hasSubmittedTerminalInput(input);
3411
+ const result = await engine.sendInput(agentId, input, { expectedRuntimeEpoch });
3412
+ if (submittedUserInput && agent.terminalInputReceived !== true) {
3413
+ agent.terminalInputReceived = true;
3414
+ this.ensurePersistentAgentSession(agent);
3415
+ this.updateEngineProviderSessionMetadata(agent);
3416
+ this.emit('agent-update', { agentId, patch: { terminalInputReceived: true } });
2818
3417
  }
3418
+ if (submittedUserInput) {
3419
+ this.providerSessionService.observe(agentId, { force: true });
3420
+ }
3421
+ return result;
3422
+ } catch (error) {
3423
+ console.error('Failed to send input:', error);
3424
+ if (isSessionNotAvailableError(error)) {
3425
+ this.markAgentSessionDead(agentId, error);
3426
+ }
3427
+ return;
2819
3428
  }
2820
3429
  }
2821
3430
 
@@ -2830,33 +3439,34 @@ class AgentManager extends EventEmitter {
2830
3439
  agent.terminalBusy = false;
2831
3440
  agent.exitedAt = Date.now();
2832
3441
  agent.output = trimSessionOutput(`${agent.output || ''}\n${message}`);
2833
- this.observeAgentStateChange(agentId, { force: true });
3442
+ this.providerSessionService.observe(agentId, { force: true });
2834
3443
  this.emit('update');
2835
3444
  }
2836
3445
 
2837
- async interruptAgent(agentId) {
3446
+ async interruptAgent(agentId, options = {}) {
2838
3447
  const agent = this.agents.get(agentId);
2839
3448
  if (!agent) return;
3449
+ const appServer = runtimeBindingOf(agent, 'app-server');
2840
3450
 
2841
3451
  try {
2842
3452
  if (
2843
3453
  agent.providerSessionProvider === 'codex'
2844
- && agent.codexRuntimeMode === 'app-server'
3454
+ && appServer
2845
3455
  && this.codexAppServerRuntime
2846
3456
  && typeof this.codexAppServerRuntime.interruptAgent === 'function'
2847
3457
  ) {
2848
3458
  try {
2849
3459
  const interrupted = await this.codexAppServerRuntime.interruptAgent(agentId);
2850
3460
  if (interrupted) {
2851
- agent.codexAppServerState = 'interrupting';
3461
+ appServer.state = 'interrupting';
2852
3462
  this.emit('update');
2853
3463
  return;
2854
3464
  }
2855
- agent.codexAppServerError = 'Codex App Server interrupt is not available';
3465
+ appServer.error = 'Codex App Server interrupt is not available';
2856
3466
  this.emit('update');
2857
3467
  return;
2858
3468
  } catch (error) {
2859
- agent.codexAppServerError = `Codex App Server interrupt failed: ${error && (error.message || error)}`;
3469
+ appServer.error = `Codex App Server interrupt failed: ${error && (error.message || error)}`;
2860
3470
  this.emit('update');
2861
3471
  return;
2862
3472
  }
@@ -2874,9 +3484,9 @@ class AgentManager extends EventEmitter {
2874
3484
 
2875
3485
  const input = interruptInputForAgent(agent);
2876
3486
  if (engine.interruptSession) {
2877
- await engine.interruptSession(agentId, input);
3487
+ return await engine.interruptSession(agentId, input, options);
2878
3488
  } else {
2879
- await engine.sendInput(agentId, input);
3489
+ return await engine.sendInput(agentId, input, options);
2880
3490
  }
2881
3491
  } catch (error) {
2882
3492
  console.error('Failed to interrupt agent:', error);
@@ -2886,10 +3496,48 @@ class AgentManager extends EventEmitter {
2886
3496
  }
2887
3497
  }
2888
3498
 
3499
+ agentSupportsTerminalInput(agentId) {
3500
+ const agent = this.agents.get(agentId);
3501
+ if (!agent) return false;
3502
+ return !isCodexAppServerAgent(agent) && !isJsonCliAgent(agent) && !isAcpAgent(agent);
3503
+ }
3504
+
3505
+ async getAgentSessionAttachCheckpoint(agentId) {
3506
+ const agent = this.agents.get(agentId);
3507
+ if (!agent || isCodexAppServerAgent(agent) || isAcpAgent(agent) || isJsonCliAgent(agent)) {
3508
+ return null;
3509
+ }
3510
+ try {
3511
+ return await this.engineBridge.getSessionAttachCheckpoint(agent.engineName, agentId);
3512
+ } catch (error) {
3513
+ console.error('Failed to read agent terminal attach checkpoint:', error);
3514
+ return null;
3515
+ }
3516
+ }
3517
+
3518
+ requestAgentSessionResize(agentId, cols, rows) {
3519
+ this.pendingResizeByAgent.set(agentId, { cols, rows });
3520
+ if (this.resizeDrains.has(agentId)) return true;
3521
+
3522
+ const drain = (async () => {
3523
+ while (this.pendingResizeByAgent.has(agentId)) {
3524
+ const next = this.pendingResizeByAgent.get(agentId);
3525
+ this.pendingResizeByAgent.delete(agentId);
3526
+ await this.resizeAgentSession(agentId, next.cols, next.rows);
3527
+ }
3528
+ })().finally(() => {
3529
+ this.resizeDrains.delete(agentId);
3530
+ });
3531
+ this.resizeDrains.set(agentId, drain);
3532
+ return true;
3533
+ }
3534
+
2889
3535
  async resizeAgentSession(agentId, cols, rows) {
2890
3536
  const agent = this.agents.get(agentId);
2891
- if (!agent) return;
2892
- if (isCodexAppServerAgent(agent) || isAcpAgent(agent) || isJsonCliAgent(agent)) return;
3537
+ if (!agent) return { status: 'resize-rejected', reason: 'session-unavailable', resized: false };
3538
+ if (isCodexAppServerAgent(agent) || isAcpAgent(agent) || isJsonCliAgent(agent)) {
3539
+ return { status: 'resize-rejected', reason: 'unsupported-session', resized: false };
3540
+ }
2893
3541
 
2894
3542
  const nextCols = Math.floor(Number(cols));
2895
3543
  const nextRows = Math.floor(Number(rows));
@@ -2899,49 +3547,50 @@ class AgentManager extends EventEmitter {
2899
3547
  nextCols < MIN_TERMINAL_RESIZE_COLS ||
2900
3548
  nextRows < MIN_TERMINAL_RESIZE_ROWS
2901
3549
  ) {
2902
- return;
3550
+ return { status: 'resize-rejected', reason: 'invalid-dimensions', resized: false };
2903
3551
  }
2904
3552
 
2905
3553
  try {
2906
- const previousSize = this.lastResizeByAgent.get(agentId);
2907
- if (previousSize && previousSize.cols === nextCols && previousSize.rows === nextRows) {
2908
- return;
2909
- }
2910
-
2911
3554
  const engine = this.engineBridge.getEngine(agent.engineName);
2912
- if (!engine || !engine.resizeSession) return;
3555
+ if (!engine || !engine.resizeSession) {
3556
+ return { status: 'resize-rejected', reason: 'unsupported-engine', resized: false };
3557
+ }
2913
3558
 
2914
3559
  const result = await engine.resizeSession(agentId, nextCols, nextRows);
2915
- if (result && result.resized === false) {
3560
+ if (result && result.resized === false && result.reason === 'session-unavailable') {
2916
3561
  this.markAgentSessionDead(agentId, 'Session not available');
2917
- return;
2918
3562
  }
2919
- this.lastResizeByAgent.set(agentId, { cols: nextCols, rows: nextRows });
3563
+ if (result && result.status === 'resize-committed') {
3564
+ this.lastResizeByAgent.set(agentId, { cols: nextCols, rows: nextRows });
3565
+ }
3566
+ return result;
2920
3567
  } catch (error) {
2921
3568
  console.error('Failed to resize agent session:', error);
3569
+ return { status: 'resize-rejected', reason: 'resize-failed', resized: false };
2922
3570
  }
2923
3571
  }
2924
3572
 
2925
- async clearAgentSessionBuffer(agentId) {
3573
+ async clearAgentSessionBuffer(agentId, options = {}) {
2926
3574
  const agent = this.agents.get(agentId);
2927
3575
  if (!agent) return { cleared: false };
2928
3576
  if (isCodexAppServerAgent(agent) || isAcpAgent(agent) || isJsonCliAgent(agent)) return { cleared: false };
3577
+ if (options.expectedRuntimeEpoch && agent.runtimeEpoch !== options.expectedRuntimeEpoch) {
3578
+ return { cleared: false, reason: 'runtime-epoch-mismatch' };
3579
+ }
2929
3580
 
2930
3581
  try {
2931
3582
  const engine = this.engineBridge.getEngine(agent.engineName);
2932
3583
  if (!engine || !engine.clearBuffer) return { cleared: false };
2933
- const result = await engine.clearBuffer(agentId);
3584
+ const result = await engine.clearBuffer(agentId, options);
2934
3585
  if (result && result.cleared === false) {
2935
- this.markAgentSessionDead(agentId, 'Session not available');
3586
+ if (result.reason === 'session-unavailable') {
3587
+ this.markAgentSessionDead(agentId, 'Session not available');
3588
+ }
2936
3589
  return result;
2937
3590
  }
2938
- agent.output = '';
2939
- agent.previewText = '';
2940
- agent.previewSnapshot = null;
2941
- this.outputEvents.delete(agentId);
2942
- this.lastActivity.set(agentId, Date.now());
2943
- this.observeAgentStateChange(agentId, { force: true });
2944
- this.emit('update');
3591
+ // The ordered clear transition is the single metadata writer. Output
3592
+ // committed immediately after clear must not be erased by this RPC
3593
+ // response path racing the transition stream.
2945
3594
  return result || { cleared: true };
2946
3595
  } catch (error) {
2947
3596
  console.error('Failed to clear agent session buffer:', error);
@@ -2983,9 +3632,12 @@ class AgentManager extends EventEmitter {
2983
3632
  }
2984
3633
 
2985
3634
  const updates = {};
3635
+ let structuralUpdateChanged = false;
3636
+ let readUpdateChanged = false;
2986
3637
  if (typeof flags.pinned === 'boolean') {
2987
3638
  const wasPinned = agent.pinned === true;
2988
3639
  agent.pinned = flags.pinned;
3640
+ structuralUpdateChanged = structuralUpdateChanged || wasPinned !== agent.pinned;
2989
3641
  updates.pinned = agent.pinned;
2990
3642
  if (!wasPinned && agent.pinned) {
2991
3643
  agent.pinnedOrder = nextPinnedOrder(Array.from(this.agents.values()));
@@ -2993,22 +3645,36 @@ class AgentManager extends EventEmitter {
2993
3645
  updates.pinnedOrder = finiteOrder(agent.pinnedOrder);
2994
3646
  }
2995
3647
 
3648
+ if (
3649
+ typeof flags.readOutputEpoch === 'string'
3650
+ && typeof flags.readOutputSeq === 'number'
3651
+ && Number.isFinite(flags.readOutputSeq)
3652
+ ) {
3653
+ const result = this.markAgentReadOutputCut(agentId, flags.readOutputEpoch, flags.readOutputSeq);
3654
+ if (result.error) return result;
3655
+ updates.readOutputEpoch = result.readOutputEpoch;
3656
+ updates.readOutputSeq = result.readOutputSeq;
3657
+ readUpdateChanged = readUpdateChanged || result.changed;
3658
+ }
3659
+
2996
3660
  if (typeof flags.unread === 'boolean') {
2997
3661
  const result = flags.unread
2998
3662
  ? this.markAgentUnreadCursor(agentId)
2999
- : this.markAgentReadCursor(agentId);
3663
+ : this.markAgentReadCursor(agentId, undefined, { emitUpdate: false });
3000
3664
  if (result.error) return result;
3001
3665
  updates.unread = result.unread;
3002
3666
  updates.attentionSeq = result.attentionSeq;
3003
3667
  updates.readAttentionSeq = result.readAttentionSeq;
3668
+ readUpdateChanged = readUpdateChanged || result.changed === true;
3004
3669
  }
3005
3670
 
3006
3671
  if (typeof flags.readAttentionSeq === 'number' && Number.isFinite(flags.readAttentionSeq)) {
3007
- const result = this.markAgentReadCursor(agentId, flags.readAttentionSeq);
3672
+ const result = this.markAgentReadCursor(agentId, flags.readAttentionSeq, { emitUpdate: false });
3008
3673
  if (result.error) return result;
3009
3674
  updates.unread = result.unread;
3010
3675
  updates.attentionSeq = result.attentionSeq;
3011
3676
  updates.readAttentionSeq = result.readAttentionSeq;
3677
+ readUpdateChanged = readUpdateChanged || result.changed === true;
3012
3678
  }
3013
3679
 
3014
3680
  if (flags.archived === true) {
@@ -3019,18 +3685,38 @@ class AgentManager extends EventEmitter {
3019
3685
  }
3020
3686
 
3021
3687
  if (flags.archived === false) {
3688
+ structuralUpdateChanged = structuralUpdateChanged || agent.archived === true || agent.archivedAt !== null;
3022
3689
  agent.archived = false;
3023
3690
  agent.archivedAt = null;
3024
3691
  updates.archived = agent.archived;
3025
3692
  updates.archivedAt = agent.archivedAt;
3026
3693
  }
3027
3694
 
3028
- if (typeof flags.pinned === 'boolean') {
3695
+ if (
3696
+ typeof flags.pinned === 'boolean'
3697
+ || (typeof flags.readOutputEpoch === 'string' && typeof flags.readOutputSeq === 'number')
3698
+ ) {
3029
3699
  this.ensurePersistentAgentSession(agent);
3030
3700
  this.updateEngineProviderSessionMetadata(agent);
3031
3701
  }
3032
- this.emit('update');
3033
- return { agentId, ...updates };
3702
+ if (structuralUpdateChanged) {
3703
+ this.emit('update');
3704
+ } else if (readUpdateChanged) {
3705
+ this.emit('agent-read', {
3706
+ agentId,
3707
+ unread: agent.unread === true,
3708
+ attentionSeq: finiteNonNegativeInteger(agent.attentionSeq),
3709
+ readAttentionSeq: finiteNonNegativeInteger(agent.readAttentionSeq),
3710
+ readOutputEpoch: typeof agent.readOutputEpoch === 'string' ? agent.readOutputEpoch : '',
3711
+ readOutputSeq: finiteNumberOrNull(agent.readOutputSeq),
3712
+ });
3713
+ }
3714
+ return {
3715
+ agentId,
3716
+ ...updates,
3717
+ changed: structuralUpdateChanged || readUpdateChanged,
3718
+ requiresState: structuralUpdateChanged,
3719
+ };
3034
3720
  }
3035
3721
 
3036
3722
  reorderProjectAgent(agentId, { beforeAgentId = '', afterAgentId = '' } = {}) {
@@ -3091,7 +3777,7 @@ class AgentManager extends EventEmitter {
3091
3777
 
3092
3778
  if (
3093
3779
  agent.providerSessionProvider === 'codex'
3094
- && agent.codexRuntimeMode === 'app-server'
3780
+ && runtimeKind(agent) === 'app-server'
3095
3781
  ) {
3096
3782
  return this.updateCodexAppServerPermissionMode(agentId, mode);
3097
3783
  }
@@ -3102,7 +3788,8 @@ class AgentManager extends EventEmitter {
3102
3788
  async updateCodexAppServerPermissionMode(agentId, mode) {
3103
3789
  const agent = this.agents.get(agentId);
3104
3790
  if (!agent) return { error: 'Agent not found' };
3105
- if (agent.providerSessionProvider !== 'codex' || agent.codexRuntimeMode !== 'app-server') {
3791
+ const runtime = runtimeBindingOf(agent, 'app-server');
3792
+ if (agent.providerSessionProvider !== 'codex' || !runtime) {
3106
3793
  return { error: 'Agent is not using the Codex App Server runtime' };
3107
3794
  }
3108
3795
 
@@ -3122,7 +3809,7 @@ class AgentManager extends EventEmitter {
3122
3809
  }
3123
3810
 
3124
3811
  agent.launchPermissionMode = nextMode;
3125
- agent.codexAppServerError = '';
3812
+ runtime.error = '';
3126
3813
  this.updateEngineProviderSessionMetadata(agent);
3127
3814
  this.ensurePersistentAgentSession(agent, { launchPermissionMode: nextMode });
3128
3815
  this.emit('update');
@@ -3178,29 +3865,30 @@ class AgentManager extends EventEmitter {
3178
3865
  const nextMode = ['terminal', 'json', 'acp'].includes(mode) ? mode : '';
3179
3866
  if (!nextMode) return { error: 'Unsupported Agent runtime mode' };
3180
3867
  const provider = agent.providerSessionProvider || '';
3868
+ const currentKind = runtimeKind(agent);
3869
+ const currentMode = ['acp', 'json'].includes(currentKind) ? currentKind : 'terminal';
3181
3870
  const leavesCodexAppServer = provider === 'codex'
3182
- && agent.codexRuntimeMode === 'app-server'
3871
+ && currentKind === 'app-server'
3183
3872
  && nextMode === 'terminal';
3184
- if (agent.agentRuntimeMode === nextMode && !leavesCodexAppServer) {
3873
+ if (currentMode === nextMode && !leavesCodexAppServer) {
3185
3874
  return { agentId, agentRuntimeMode: nextMode };
3186
3875
  }
3187
- const turnActive = agent.agentRuntimeMode === 'acp'
3188
- ? ['working', 'waiting-for-permission', 'interrupting'].includes(agent.acpState || '')
3876
+ const turnActive = currentKind === 'acp'
3877
+ ? ['working', 'waiting-for-permission', 'interrupting'].includes(runtimeBindingOf(agent, 'acp').state)
3189
3878
  : this.isAgentAttentionTurnActive(agent);
3190
3879
  if (turnActive) {
3191
3880
  return { error: 'Interrupt the active Agent turn before switching Chat and Terminal.' };
3192
3881
  }
3193
- const supportedProviders = nextMode === 'json' ? ['codex', 'opencode'] : ['codex', 'claude', 'opencode', 'qoder'];
3194
- if (!supportedProviders.includes(provider)) {
3882
+ if (!providerSupportsRuntime(provider, nextMode)) {
3195
3883
  return { error: `Agent does not support the ${nextMode.toUpperCase()} runtime` };
3196
3884
  }
3197
3885
  const sessionId = String(agent.providerSessionId || '').trim();
3198
3886
  const canStartFreshAcpSession = nextMode === 'acp'
3199
- && agent.agentRuntimeMode === 'terminal'
3887
+ && currentMode === 'terminal'
3200
3888
  && agent.terminalInputReceived !== true
3201
3889
  && (
3202
3890
  agent.providerSessionTemporary === true
3203
- || ['codex-temporary', 'claude-session-id', 'qoder-session-id'].includes(agent.providerSessionSource || '')
3891
+ || isFreshAcpSessionSource(provider, agent.providerSessionSource || '')
3204
3892
  );
3205
3893
  if (!isSafeProviderSessionId(sessionId) && !canStartFreshAcpSession) {
3206
3894
  return { error: 'Runtime switching requires a resumable provider session. Send the first message and try again.' };
@@ -3212,7 +3900,7 @@ class AgentManager extends EventEmitter {
3212
3900
  // fail. Historical/terminal sessions still use the provider lookup as a
3213
3901
  // stale-session guard.
3214
3902
  let liveAcpSession = false;
3215
- if (agent.agentRuntimeMode === 'acp' && this.acpRuntime && typeof this.acpRuntime.getSession === 'function') {
3903
+ if (currentKind === 'acp' && this.acpRuntime && typeof this.acpRuntime.getSession === 'function') {
3216
3904
  try {
3217
3905
  const snapshot = this.acpRuntime.getSession(agentId, { maxEntries: 0 });
3218
3906
  liveAcpSession = String(snapshot?.sessionId || '') === sessionId;
@@ -3232,7 +3920,8 @@ class AgentManager extends EventEmitter {
3232
3920
  const command = startsFreshAcpSession
3233
3921
  ? (agent.forkCommand || agent.command)
3234
3922
  : buildAgentSessionResumeCommand(provider, sessionId, {
3235
- cwd: agent.cwd || agent.projectWorkspace || '',
3923
+ cwd: effectiveAgentWorkspaceRoot(agent),
3924
+ providerHomePath: agent.providerHomePath || '',
3236
3925
  });
3237
3926
  if (!command) return { error: 'Failed to build provider resume command' };
3238
3927
  const preserved = {
@@ -3243,13 +3932,13 @@ class AgentManager extends EventEmitter {
3243
3932
  unread: agent.unread === true,
3244
3933
  jsonCliEvents: isJsonCliAgent(agent)
3245
3934
  ? this.jsonCliRuntime.getEvents(agentId)
3246
- : (Array.isArray(agent.jsonCliEvents) ? agent.jsonCliEvents : []),
3935
+ : (Array.isArray(agent.runtimeResumeState?.jsonEvents) ? agent.runtimeResumeState.jsonEvents : []),
3247
3936
  };
3248
3937
  const restartOptions = {
3249
3938
  wantsMain: agent.wantsMain === true,
3250
3939
  task: agent.task || agent.providerSessionTitle || '',
3251
3940
  workflowTemplate: agent.workflowTemplate || '',
3252
- projectWorkspace: agent.projectWorkspace || agent.cwd || '',
3941
+ projectWorkspace: effectiveAgentWorkspaceRoot(agent),
3253
3942
  source: startsFreshAcpSession
3254
3943
  ? 'ui-runtime-switch-fresh'
3255
3944
  : resumedAgentSource(provider, sessionId, agent.providerHomeId || ''),
@@ -3270,12 +3959,15 @@ class AgentManager extends EventEmitter {
3270
3959
  codexApprovalMode: agent.launchPermissionMode || undefined,
3271
3960
  jsonCliEvents: preserved.jsonCliEvents,
3272
3961
  runtimeSwitchVerifiedSessionId: startsFreshAcpSession ? '' : sessionId,
3962
+ ...(provider === 'codex' && !startsFreshAcpSession
3963
+ ? preserveCodexSessionProfileOptions()
3964
+ : {}),
3273
3965
  };
3274
- const originalMode = agent.agentRuntimeMode || 'terminal';
3966
+ const originalMode = currentMode;
3275
3967
  const originalOptions = {
3276
3968
  ...restartOptions,
3277
3969
  agentRuntimeMode: originalMode,
3278
- codexRuntimeMode: agent.codexRuntimeMode || 'cli',
3970
+ codexRuntimeMode: currentKind === 'app-server' ? 'app-server' : 'cli',
3279
3971
  acpStartFresh: false,
3280
3972
  };
3281
3973
  const startReplacement = options => new Promise(resolve => {
@@ -3288,7 +3980,7 @@ class AgentManager extends EventEmitter {
3288
3980
  try {
3289
3981
  const started = this.startAgent(
3290
3982
  command,
3291
- agent.cwd || agent.projectWorkspace || null,
3983
+ effectiveAgentWorkspaceRoot(agent) || null,
3292
3984
  (restartedAgentId, error) => finish(restartedAgentId, error),
3293
3985
  options
3294
3986
  );
@@ -3395,11 +4087,11 @@ class AgentManager extends EventEmitter {
3395
4087
  if (!hasResumableSession && !startsFreshCodexSession) {
3396
4088
  return { error: 'Permission changes require a resumable provider session. Try again after the session id is available.' };
3397
4089
  }
3398
-
3399
4090
  const command = startsFreshCodexSession
3400
4091
  ? 'codex'
3401
4092
  : buildAgentSessionResumeCommand(provider, sessionId, {
3402
- cwd: agent.cwd || agent.projectWorkspace || '',
4093
+ cwd: effectiveAgentWorkspaceRoot(agent),
4094
+ providerHomePath: agent.providerHomePath || '',
3403
4095
  });
3404
4096
  if (!command) {
3405
4097
  return { error: 'Failed to build provider resume command' };
@@ -3410,7 +4102,7 @@ class AgentManager extends EventEmitter {
3410
4102
  task: agent.task || agent.providerSessionTitle || '',
3411
4103
  workflowTemplate: agent.workflowTemplate || '',
3412
4104
  requiredCliVersion: provider === 'codex' ? (agent.requiredCliVersion || '') : '',
3413
- projectWorkspace: agent.projectWorkspace || agent.cwd || '',
4105
+ projectWorkspace: effectiveAgentWorkspaceRoot(agent),
3414
4106
  source: startsFreshCodexSession
3415
4107
  ? 'ui'
3416
4108
  : (resumedSessionFromSource(agent.source)
@@ -3428,6 +4120,9 @@ class AgentManager extends EventEmitter {
3428
4120
  projectOrder: finiteOrder(agent.projectOrder),
3429
4121
  pinnedOrder: finiteOrder(agent.pinnedOrder),
3430
4122
  ...(provider === 'codex' ? { codexApprovalMode: nextMode } : { claudePermissionMode: nextMode }),
4123
+ ...(provider === 'codex' && hasResumableSession
4124
+ ? preserveCodexSessionProfileOptions()
4125
+ : {}),
3431
4126
  };
3432
4127
  const preserved = {
3433
4128
  pinned: agent.pinned === true,
@@ -3451,7 +4146,7 @@ class AgentManager extends EventEmitter {
3451
4146
  }
3452
4147
 
3453
4148
  return new Promise((resolve) => {
3454
- const startResult = this.startAgent(command, agent.cwd || agent.projectWorkspace || null, (restartedAgentId, error) => {
4149
+ const startResult = this.startAgent(command, effectiveAgentWorkspaceRoot(agent) || null, (restartedAgentId, error) => {
3455
4150
  if (error) {
3456
4151
  this.emit('update');
3457
4152
  resolve({ error });
@@ -3601,7 +4296,7 @@ class AgentManager extends EventEmitter {
3601
4296
  const resolvedWorkspace = path.resolve(workspace);
3602
4297
  return Array.from(this.agents.values()).filter(agent => {
3603
4298
  if (!agent || agent.isMain) return false;
3604
- const agentWorkspace = this.expandWorkspacePath(agent.projectWorkspace || agent.cwd || '');
4299
+ const agentWorkspace = this.expandWorkspacePath(effectiveAgentWorkspaceRoot(agent));
3605
4300
  if (!agentWorkspace) return false;
3606
4301
  return path.resolve(agentWorkspace) === resolvedWorkspace;
3607
4302
  });
@@ -3614,7 +4309,7 @@ class AgentManager extends EventEmitter {
3614
4309
 
3615
4310
  const keysToRemove = new Set();
3616
4311
  agents.forEach(agent => {
3617
- const providerSessionKey = agent.providerSessionKey || this.providerSessionKey(
4312
+ const providerSessionKey = agent.providerSessionKey || mainPageAgentSessionKey(
3618
4313
  agent.providerSessionProvider,
3619
4314
  agent.providerSessionId,
3620
4315
  agent.providerHomeId || ''
@@ -3686,8 +4381,21 @@ class AgentManager extends EventEmitter {
3686
4381
  if (!agent) {
3687
4382
  return { error: 'Agent not found' };
3688
4383
  }
4384
+ if (!['same-worktree', 'new-worktree'].includes(mode)) {
4385
+ return { error: 'Unsupported fork mode' };
4386
+ }
4387
+
4388
+ const sourceWorkspace = effectiveAgentWorkspaceRoot(agent);
4389
+ const resumedSession = agent.providerSessionProvider
4390
+ && agent.providerSessionId
4391
+ && agent.providerSessionTemporary !== true
4392
+ ? { provider: agent.providerSessionProvider, providerHomeId: agent.providerHomeId || 'default', sessionId: agent.providerSessionId }
4393
+ : resumedSessionFromSource(agent.source);
4394
+ if (resumedSession?.provider === 'codex') {
4395
+ const availability = await this.ensureCodexSessionAvailableForFork(agent, resumedSession, sourceWorkspace);
4396
+ if (availability?.error) return availability;
4397
+ }
3689
4398
 
3690
- const sourceWorkspace = agent.projectWorkspace || agent.cwd;
3691
4399
  let targetWorkspace = sourceWorkspace;
3692
4400
  if (mode === 'new-worktree') {
3693
4401
  try {
@@ -3695,19 +4403,13 @@ class AgentManager extends EventEmitter {
3695
4403
  } catch (error) {
3696
4404
  return { error: error.message || 'Failed to create git worktree' };
3697
4405
  }
3698
- } else if (mode !== 'same-worktree') {
3699
- return { error: 'Unsupported fork mode' };
3700
4406
  }
3701
4407
 
3702
- const resumedSession = agent.providerSessionProvider
3703
- && agent.providerSessionId
3704
- && agent.providerSessionTemporary !== true
3705
- ? { provider: agent.providerSessionProvider, providerHomeId: agent.providerHomeId || 'default', sessionId: agent.providerSessionId }
3706
- : resumedSessionFromSource(agent.source);
3707
4408
  const forkCommand = resumedSession
3708
4409
  ? buildAgentSessionResumeCommand(resumedSession.provider, resumedSession.sessionId, {
3709
4410
  fork: true,
3710
4411
  cwd: targetWorkspace,
4412
+ providerHomePath: agent.providerHomePath || '',
3711
4413
  })
3712
4414
  : (agent.forkCommand || agent.command);
3713
4415
 
@@ -3734,10 +4436,62 @@ class AgentManager extends EventEmitter {
3734
4436
  source: mode === 'new-worktree' ? 'ui-fork-new-worktree' : 'ui-fork-same-worktree',
3735
4437
  providerHomeId: agent.providerHomeId || (resumedSession && resumedSession.providerHomeId) || '',
3736
4438
  providerHomePath: agent.providerHomePath || '',
4439
+ ...(resumedSession?.provider === 'codex'
4440
+ ? preserveCodexSessionProfileOptions()
4441
+ : {}),
3737
4442
  });
3738
4443
  });
3739
4444
  }
3740
4445
 
4446
+ async ensureCodexSessionAvailableForFork(agent, resumedSession, sourceWorkspace) {
4447
+ const providerHomeId = agent.providerHomeId || resumedSession.providerHomeId || 'default';
4448
+ const providerHomePath = agent.providerHomePath || '';
4449
+ const sessionId = resumedSession.sessionId;
4450
+ const unarchiveKey = `${providerHomePath || providerHomeId}:${sessionId}`;
4451
+ const inFlight = this.codexSessionUnarchiveInFlight.get(unarchiveKey);
4452
+ if (inFlight) return inFlight;
4453
+
4454
+ const unarchivePromise = (async () => {
4455
+ let session;
4456
+ try {
4457
+ session = await findAgentSession('codex', sessionId, {
4458
+ limit: 1000,
4459
+ providerLimit: 1000,
4460
+ scanLimit: 5000,
4461
+ providerHomeId,
4462
+ providerHomes: providerHomePath
4463
+ ? { codex: [{ id: providerHomeId, path: providerHomePath }] }
4464
+ : undefined,
4465
+ });
4466
+ } catch (error) {
4467
+ return {
4468
+ error: `Failed to inspect Codex session before forking: ${error && (error.message || error)}`,
4469
+ };
4470
+ }
4471
+ if (!session || session.archived !== true) return null;
4472
+
4473
+ const result = await this.unarchiveCodexSession(sessionId, {
4474
+ ...session,
4475
+ // Fork is an action on the live Farming Agent. Its current workspace is
4476
+ // authoritative even when the older provider history cwd no longer exists.
4477
+ cwd: sourceWorkspace || session.cwd || session.workspace,
4478
+ providerHomePath: session.providerHomePath || providerHomePath,
4479
+ });
4480
+ if (!result?.error) return null;
4481
+ return {
4482
+ error: `Codex session ${sessionId} is archived and could not be unarchived before forking: ${result.error}`,
4483
+ };
4484
+ })();
4485
+ this.codexSessionUnarchiveInFlight.set(unarchiveKey, unarchivePromise);
4486
+ try {
4487
+ return await unarchivePromise;
4488
+ } finally {
4489
+ if (this.codexSessionUnarchiveInFlight.get(unarchiveKey) === unarchivePromise) {
4490
+ this.codexSessionUnarchiveInFlight.delete(unarchiveKey);
4491
+ }
4492
+ }
4493
+ }
4494
+
3741
4495
  recordTaskHistory(agent, options = {}) {
3742
4496
  if (!agent || agent.id === this.mainAgentId) return;
3743
4497
  if (!isSupportedHistoryAgent(agent.forkCommand || agent.command || '')) return;
@@ -3751,7 +4505,7 @@ class AgentManager extends EventEmitter {
3751
4505
  agentId: agent.id,
3752
4506
  command: agent.command || '',
3753
4507
  cwd: agent.cwd || '',
3754
- projectWorkspace: agent.projectWorkspace || agent.cwd || '',
4508
+ projectWorkspace: effectiveAgentWorkspaceRoot(agent),
3755
4509
  title: agent.customTitle || agent.sessionTitle || agent.task || '',
3756
4510
  customTitle: agent.customTitle || '',
3757
4511
  task: agent.task || '',
@@ -3810,9 +4564,9 @@ class AgentManager extends EventEmitter {
3810
4564
  this.lastActivity.delete(agentId);
3811
4565
  this.lastActivityUpdate.delete(agentId);
3812
4566
  this.outputEvents.delete(agentId);
4567
+ this.agentUsageRateCache.delete(agentId);
3813
4568
  this.lastResizeByAgent.delete(agentId);
3814
- this.stopCodexProviderSessionResolver(agentId);
3815
- this.stopProviderSessionTitleResolver(agentId);
4569
+ this.providerSessionService.stop(agentId);
3816
4570
  if (this.codexAppServerRuntime && typeof this.codexAppServerRuntime.unregisterAgent === 'function') {
3817
4571
  this.codexAppServerRuntime.unregisterAgent(agentId);
3818
4572
  }
@@ -3866,19 +4620,20 @@ class AgentManager extends EventEmitter {
3866
4620
  return null;
3867
4621
  }
3868
4622
 
3869
- return agent.projectWorkspace || agent.cwd;
4623
+ return effectiveAgentWorkspaceRoot(agent);
3870
4624
  }
3871
4625
 
3872
4626
  getAgentProviderSession(agentId) {
3873
4627
  const agent = this.agents.get(agentId);
3874
4628
  if (!agent) return null;
4629
+ const appServer = runtimeBindingOf(agent, 'app-server');
3875
4630
  return {
3876
4631
  provider: agent.providerSessionProvider || '',
3877
4632
  sessionId: agent.providerSessionId || '',
3878
4633
  providerHomeId: agent.providerHomeId || '',
3879
4634
  providerHomePath: agent.providerHomePath || '',
3880
- codexAppServerHomePath: agent.codexAppServerHomePath || '',
3881
- codexRuntimeMode: agent.codexRuntimeMode || '',
4635
+ codexAppServerHomePath: appServer?.homePath || '',
4636
+ runtimeBinding: publicRuntimeBinding(agent),
3882
4637
  temporary: agent.providerSessionTemporary === true,
3883
4638
  title: agent.providerSessionTitle || '',
3884
4639
  };
@@ -3958,6 +4713,7 @@ class AgentManager extends EventEmitter {
3958
4713
  engineName: agent.engineName || '',
3959
4714
  cwd: agent.cwd,
3960
4715
  projectWorkspace: agent.projectWorkspace || '',
4716
+ gitWorktree: publicAgentGitWorktree(agent),
3961
4717
  status: sessionState && sessionState.status === 'exited'
3962
4718
  ? agent.status
3963
4719
  : (isLiveEngineSessionState(sessionState) ? 'running' : agent.status),
@@ -3982,27 +4738,9 @@ class AgentManager extends EventEmitter {
3982
4738
  providerSessionSource: agent.providerSessionSource || '',
3983
4739
  providerSessionResolvedAt: agent.providerSessionResolvedAt || null,
3984
4740
  providerSessionTitle: agent.providerSessionTitle || '',
4741
+ providerSessionWorkspace: agent.providerSessionWorkspace || '',
3985
4742
  terminalInputReceived: agent.terminalInputReceived === true,
3986
- codexRuntimeMode: agent.codexRuntimeMode || '',
3987
- agentRuntimeMode: agent.agentRuntimeMode || 'terminal',
3988
- acpState: agent.acpState || '',
3989
- acpError: agent.acpError || '',
3990
- acpStopReason: agent.acpStopReason || '',
3991
- acpPendingPermission: agent.acpPendingPermission || null,
3992
- acpPendingPermissions: Array.isArray(agent.acpPendingPermissions) ? agent.acpPendingPermissions : [],
3993
- acpPendingElicitation: agent.acpPendingElicitation || null,
3994
- acpPendingElicitations: Array.isArray(agent.acpPendingElicitations) ? agent.acpPendingElicitations : [],
3995
- acpActiveElicitations: Array.isArray(agent.acpActiveElicitations) ? agent.acpActiveElicitations : [],
3996
- acpSessionUpdatedAt: agent.acpSessionUpdatedAt || '',
3997
- acpSessionRevision: Number(agent.acpSessionRevision) || 0,
3998
- codexAppServerState: agent.codexAppServerState || '',
3999
- codexAppServerEndpoint: agent.codexAppServerEndpoint || '',
4000
- codexAppServerThreadId: agent.codexAppServerThreadId || '',
4001
- codexAppServerTurnId: agent.codexAppServerTurnId || '',
4002
- codexAppServerError: agent.codexAppServerError || '',
4003
- codexAppServerPendingRequestId: agent.codexAppServerPendingRequestId || '',
4004
- codexAppServerPendingRequestMethod: agent.codexAppServerPendingRequestMethod || '',
4005
- codexAppServerPendingRequest: agent.codexAppServerPendingRequest || null,
4743
+ runtimeBinding: publicRuntimeBinding(agent),
4006
4744
  forkedFromProviderSessionId: agent.forkedFromProviderSessionId || '',
4007
4745
  customTitle: agent.customTitle || '',
4008
4746
  pinned: agent.pinned === true,
@@ -4013,12 +4751,21 @@ class AgentManager extends EventEmitter {
4013
4751
  attentionUpdatedAt: finiteNumberOrNull(agent.attentionUpdatedAt),
4014
4752
  readAttentionAt: finiteNumberOrNull(agent.readAttentionAt),
4015
4753
  attentionReason: agent.attentionReason || '',
4754
+ attentionOutputEpoch: agent.attentionOutputEpoch || '',
4016
4755
  attentionOutputSeq: finiteNumberOrNull(agent.attentionOutputSeq),
4756
+ readOutputEpoch: agent.readOutputEpoch || '',
4757
+ readOutputSeq: finiteNumberOrNull(agent.readOutputSeq),
4017
4758
  unread: agentAttentionUnread(agent),
4018
4759
  archived: agent.archived === true,
4019
4760
  archivedAt: agent.archivedAt || null,
4020
4761
  sessionSource: this.getEngineSessionSource(agent.engineName),
4762
+ runtimeEpoch: sessionState && typeof sessionState.runtimeEpoch === 'string'
4763
+ ? sessionState.runtimeEpoch
4764
+ : (agent.runtimeEpoch || ''),
4021
4765
  outputSeq: sessionState && Number.isFinite(sessionState.outputSeq) ? sessionState.outputSeq : null,
4766
+ stateRevision: sessionState && Number.isFinite(sessionState.stateRevision)
4767
+ ? sessionState.stateRevision
4768
+ : null,
4022
4769
  isMain,
4023
4770
  activityLevel: isMain ? 'warm' : this.calculateActivityLevel(lastActivity, now),
4024
4771
  lastActivity,
@@ -4030,6 +4777,7 @@ class AgentManager extends EventEmitter {
4030
4777
  output: (sessionState && typeof sessionState.output === 'string') ? sessionState.output : fallbackOutput,
4031
4778
  renderOutput: (sessionState && typeof sessionState.renderOutput === 'string') ? sessionState.renderOutput : fallbackOutput,
4032
4779
  previewText,
4780
+ codexTerminalProfile: activeCodexTerminalProfile(agent, previewText),
4033
4781
  previewSnapshot: (sessionState && sessionState.previewSnapshot) || agent.previewSnapshot || null,
4034
4782
  previewCols: (sessionState && Number.isFinite(sessionState.previewCols) && sessionState.previewCols > 0)
4035
4783
  ? sessionState.previewCols
@@ -4037,10 +4785,28 @@ class AgentManager extends EventEmitter {
4037
4785
  previewRows: (sessionState && Number.isFinite(sessionState.previewRows) && sessionState.previewRows > 0)
4038
4786
  ? sessionState.previewRows
4039
4787
  : (agent.previewRows || 30),
4040
- usageRate: this.calculateAgentUsageRate(agent.id),
4788
+ usageRate: this.getAgentUsageRate(agent.id),
4041
4789
  };
4042
4790
  }
4043
4791
 
4792
+ getAgentUsageRate(agentId, options = {}) {
4793
+ const now = options.now || Date.now();
4794
+ const windowMs = options.windowMs || AGENT_USAGE_RATE_WINDOW_MS;
4795
+ const cached = this.agentUsageRateCache.get(agentId);
4796
+ if (
4797
+ cached
4798
+ && cached.windowMs === windowMs
4799
+ && now >= cached.sampledAt
4800
+ && now - cached.sampledAt < AGENT_USAGE_RATE_REFRESH_MS
4801
+ ) {
4802
+ return cached.value;
4803
+ }
4804
+
4805
+ const value = this.calculateAgentUsageRate(agentId, { now, windowMs });
4806
+ this.agentUsageRateCache.set(agentId, { windowMs, sampledAt: now, value });
4807
+ return value;
4808
+ }
4809
+
4044
4810
  calculateAgentUsageRate(agentId, options = {}) {
4045
4811
  const now = options.now || Date.now();
4046
4812
  const windowMs = options.windowMs || AGENT_USAGE_RATE_WINDOW_MS;
@@ -4077,7 +4843,7 @@ class AgentManager extends EventEmitter {
4077
4843
  cwd: agent.cwd,
4078
4844
  isMain: this.isMainAgentRecord(agent.id, agent),
4079
4845
  status: agent.status,
4080
- usageRate: this.calculateAgentUsageRate(agent.id, { now, windowMs }),
4846
+ usageRate: this.getAgentUsageRate(agent.id, { now, windowMs }),
4081
4847
  }));
4082
4848
  const totalOutputBytes = agents.reduce((sum, agent) => sum + agent.usageRate.outputBytes, 0);
4083
4849
  const estimatedOutputTokens = agents.reduce((sum, agent) => sum + agent.usageRate.estimatedOutputTokens, 0);
@@ -4119,12 +4885,17 @@ class AgentManager extends EventEmitter {
4119
4885
  engineName: agent.engineName || '',
4120
4886
  cwd: agent.cwd,
4121
4887
  projectWorkspace: agent.projectWorkspace || '',
4888
+ gitWorktree: publicAgentGitWorktree(agent),
4122
4889
  output: agent.output.slice(-2000),
4123
4890
  previewText: agent.previewText || '',
4891
+ codexTerminalProfile: activeCodexTerminalProfile(agent, agent.previewText || ''),
4124
4892
  previewCols: agent.previewCols || 80,
4125
4893
  previewRows: agent.previewRows || 30,
4126
4894
  sessionTitle: agent.sessionTitle || '',
4127
4895
  sessionSource: this.getEngineSessionSource(agent.engineName),
4896
+ runtimeEpoch: agent.runtimeEpoch || '',
4897
+ outputSeq: finiteNumberOrNull(agent.lastOutputSeq),
4898
+ stateRevision: finiteNumberOrNull(agent.stateRevision),
4128
4899
  status: agent.status,
4129
4900
  terminalBusy,
4130
4901
  terminalStatus,
@@ -4148,33 +4919,11 @@ class AgentManager extends EventEmitter {
4148
4919
  providerSessionSource: agent.providerSessionSource || '',
4149
4920
  providerSessionResolvedAt: agent.providerSessionResolvedAt || null,
4150
4921
  providerSessionTitle: agent.providerSessionTitle || '',
4922
+ providerSessionWorkspace: agent.providerSessionWorkspace || '',
4923
+ providerCapabilities: providerCapabilities(agent.providerSessionProvider),
4151
4924
  terminalInputReceived: agent.terminalInputReceived === true,
4152
- codexRuntimeMode: agent.codexRuntimeMode || '',
4153
- agentRuntimeMode: agent.agentRuntimeMode || 'terminal',
4154
- jsonCliState: agent.jsonCliState || '',
4155
- jsonCliError: agent.jsonCliError || '',
4156
- jsonCliTranscriptUpdatedAt: agent.jsonCliTranscriptUpdatedAt || '',
4157
- acpState: agent.acpState || '',
4158
- acpError: agent.acpError || '',
4159
- acpStopReason: agent.acpStopReason || '',
4160
- acpPendingPermission: agent.acpPendingPermission || null,
4161
- acpPendingPermissions: Array.isArray(agent.acpPendingPermissions) ? agent.acpPendingPermissions : [],
4162
- acpPendingElicitation: agent.acpPendingElicitation || null,
4163
- acpPendingElicitations: Array.isArray(agent.acpPendingElicitations) ? agent.acpPendingElicitations : [],
4164
- acpActiveElicitations: Array.isArray(agent.acpActiveElicitations) ? agent.acpActiveElicitations : [],
4165
- acpSessionUpdatedAt: agent.acpSessionUpdatedAt || '',
4166
- acpSessionRevision: Number(agent.acpSessionRevision) || 0,
4167
- codexAppServerState: agent.codexAppServerState || '',
4168
- codexAppServerEndpoint: agent.codexAppServerEndpoint || '',
4169
- codexAppServerThreadId: agent.codexAppServerThreadId || '',
4170
- codexAppServerTurnId: agent.codexAppServerTurnId || '',
4171
- codexAppServerError: agent.codexAppServerError || '',
4172
- codexAppServerPendingRequestId: agent.codexAppServerPendingRequestId || '',
4173
- codexAppServerPendingRequestMethod: agent.codexAppServerPendingRequestMethod || '',
4174
- codexAppServerPendingRequest: agent.codexAppServerPendingRequest || null,
4175
- codexAppServerNotice: agent.codexAppServerNotice || null,
4176
- codexAppServerGoal: agent.codexAppServerGoal || null,
4177
- codexCliObserverDeferred: agent.codexCliObserverDeferred === true,
4925
+ runtimeBinding: publicRuntimeBinding(agent),
4926
+ runtimeObservation: deriveRuntimeObservation(agent),
4178
4927
  forkedFromProviderSessionId: agent.forkedFromProviderSessionId || '',
4179
4928
  restartedFromAgentId: agent.restartedFromAgentId || '',
4180
4929
  restartedFromAgentIds: Array.isArray(agent.restartedFromAgentIds) ? agent.restartedFromAgentIds : [],
@@ -4188,11 +4937,14 @@ class AgentManager extends EventEmitter {
4188
4937
  attentionUpdatedAt: finiteNumberOrNull(agent.attentionUpdatedAt),
4189
4938
  readAttentionAt: finiteNumberOrNull(agent.readAttentionAt),
4190
4939
  attentionReason: agent.attentionReason || '',
4940
+ attentionOutputEpoch: agent.attentionOutputEpoch || '',
4191
4941
  attentionOutputSeq: finiteNumberOrNull(agent.attentionOutputSeq),
4942
+ readOutputEpoch: agent.readOutputEpoch || '',
4943
+ readOutputSeq: finiteNumberOrNull(agent.readOutputSeq),
4192
4944
  unread: agentAttentionUnread(agent),
4193
4945
  archived: agent.archived === true,
4194
4946
  archivedAt: agent.archivedAt || null,
4195
- canForkNewWorktree: this.canCreateForkWorktree(agent.projectWorkspace || agent.cwd || ''),
4947
+ canForkNewWorktree: this.canCreateForkWorktree(effectiveAgentWorkspaceRoot(agent)),
4196
4948
  startedAt: agent.startedAt || null,
4197
4949
  exitedAt: agent.exitedAt || null,
4198
4950
  // Main agent is exempt from activity/attention/zombie scoring
@@ -4200,7 +4952,7 @@ class AgentManager extends EventEmitter {
4200
4952
  lastActivity,
4201
4953
  attentionScore: isMain ? 0 : this.calculateAttentionScore(id, now),
4202
4954
  isZombie: isMain ? false : this.isZombie(id, now),
4203
- usageRate: this.calculateAgentUsageRate(id, { now })
4955
+ usageRate: this.getAgentUsageRate(id, { now })
4204
4956
  });
4205
4957
  }
4206
4958
 
@@ -4273,6 +5025,10 @@ class AgentManager extends EventEmitter {
4273
5025
  this.on('update', callback);
4274
5026
  }
4275
5027
 
5028
+ onAgentActivity(callback) {
5029
+ this.on('agent-activity', callback);
5030
+ }
5031
+
4276
5032
  onSessionStream(callback) {
4277
5033
  this.on('session-stream', callback);
4278
5034
  }
@@ -4288,17 +5044,20 @@ class AgentManager extends EventEmitter {
4288
5044
  continue;
4289
5045
  }
4290
5046
 
5047
+ const terminalStatus = deriveAgentTerminalStatus(agent, {
5048
+ previewText: agent.previewText || '',
5049
+ title: agent.sessionTitle || '',
5050
+ terminalBusy: typeof agent.terminalBusy === 'boolean' ? agent.terminalBusy : null,
5051
+ });
4291
5052
  previews.push({
4292
5053
  agentId: agent.id,
4293
5054
  previewText: agent.previewText || '',
4294
5055
  cols: agent.previewCols || 80,
4295
5056
  rows: agent.previewRows || 30,
4296
5057
  previewSnapshot: agent.previewSnapshot || null,
4297
- terminalStatus: deriveAgentTerminalStatus(agent, {
4298
- previewText: agent.previewText || '',
4299
- title: agent.sessionTitle || '',
4300
- terminalBusy: typeof agent.terminalBusy === 'boolean' ? agent.terminalBusy : null,
4301
- }),
5058
+ codexTerminalProfile: activeCodexTerminalProfile(agent, agent.previewText || ''),
5059
+ terminalStatus,
5060
+ runtimeObservation: deriveRuntimeObservation({ ...agent, terminalStatus }),
4302
5061
  });
4303
5062
  }
4304
5063