farming-code 2.2.8 → 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 +36 -23
  2. package/README.zh_cn.md +36 -23
  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 +1482 -730
  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-BrbljRqn.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-8dYAM6ql.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-D073SnW4.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()
@@ -2165,16 +2648,18 @@ class AgentManager extends EventEmitter {
2165
2648
  && process.env.FARMING_E2E_FAKE_EXECUTABLES !== '1'
2166
2649
  && normalizeCodexRuntimeMode(requestedCodexRuntimeMode) === 'app-server';
2167
2650
  const useJsonCli = requestedAgentRuntimeMode === 'json'
2168
- && ['codex', 'opencode'].includes(structuredRuntimeProvider)
2651
+ && providerSupportsRuntime(structuredRuntimeProvider, 'json')
2169
2652
  && !useCodexAppServer
2170
2653
  && process.env.FARMING_E2E_FAKE_EXECUTABLES !== '1';
2171
2654
  const useAcp = requestedAgentRuntimeMode === 'acp'
2172
- && ['codex', 'claude', 'opencode', 'qoder'].includes(structuredRuntimeProvider)
2655
+ && providerSupportsRuntime(structuredRuntimeProvider, 'acp')
2173
2656
  && !useCodexAppServer
2174
2657
  && (
2175
2658
  process.env.FARMING_E2E_FAKE_EXECUTABLES !== '1'
2176
2659
  || process.env.FARMING_E2E_FAKE_ACP_AGENT === '1'
2177
2660
  );
2661
+ const acpGeneratedFreshSession = useAcp
2662
+ && isFreshAcpSessionSource(structuredRuntimeProvider, providerSessionPlan.source);
2178
2663
  let codexAppServerHomePath = '';
2179
2664
  if (useCodexAppServer) {
2180
2665
  try {
@@ -2214,40 +2699,23 @@ class AgentManager extends EventEmitter {
2214
2699
  providerHomeId: resolvedProviderHomeId,
2215
2700
  providerHomePath,
2216
2701
  providerSessionId: providerSessionPlan.id || '',
2217
- providerSessionKey: this.providerSessionKey(providerSessionPlan.provider, providerSessionPlan.id, providerHome ? providerHome.id : providerHomeId),
2702
+ providerSessionKey: mainPageAgentSessionKey(providerSessionPlan.provider, providerSessionPlan.id, providerHome ? providerHome.id : providerHomeId),
2218
2703
  providerSessionTemporary: providerSessionPlan.temporary === true,
2219
2704
  providerSessionSource: providerSessionPlan.source || '',
2220
2705
  providerSessionResolvedAt: providerSessionPlan.temporary === true ? null : Date.now(),
2221
2706
  providerSessionTitle: typeof options.providerSessionTitle === 'string' ? options.providerSessionTitle.trim().slice(0, 160) : '',
2707
+ providerSessionWorkspace: '',
2222
2708
  terminalInputReceived: false,
2223
- codexRuntimeMode: structuredRuntimeProvider === 'codex'
2224
- ? (useCodexAppServer ? 'app-server' : 'cli')
2225
- : '',
2226
- agentRuntimeMode: useAcp ? 'acp' : (useJsonCli ? 'json' : 'terminal'),
2227
- jsonCliState: useJsonCli ? 'idle' : '',
2228
- jsonCliError: '',
2229
- jsonCliTranscriptUpdatedAt: '',
2230
- jsonCliEvents: Array.isArray(options.jsonCliEvents) ? options.jsonCliEvents : [],
2231
- acpState: useAcp ? 'connecting' : '',
2232
- acpError: '',
2233
- acpStopReason: '',
2234
- acpPendingPermission: null,
2235
- acpPendingPermissions: [],
2236
- acpPendingElicitation: null,
2237
- acpPendingElicitations: [],
2238
- acpActiveElicitations: [],
2239
- acpSessionUpdatedAt: '',
2240
- acpSessionRevision: 0,
2241
- codexAppServerHomePath,
2242
- codexAppServerState: useCodexAppServer ? 'connecting' : '',
2243
- codexAppServerEndpoint: '',
2244
- codexAppServerThreadId: '',
2245
- codexAppServerTurnId: '',
2246
- codexAppServerError: '',
2247
- codexAppServerPendingRequestId: '',
2248
- codexAppServerPendingRequestMethod: '',
2249
- codexAppServerPendingRequest: null,
2250
- codexCliObserverDeferred: false,
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'))),
2251
2719
  forkedFromProviderSessionId: providerSessionPlan.forkedFromProviderSessionId || '',
2252
2720
  restartedFromAgentId: typeof options.restartedFromAgentId === 'string' ? options.restartedFromAgentId : '',
2253
2721
  restartedFromAgentIds: Array.isArray(options.restartedFromAgentIds)
@@ -2256,7 +2724,7 @@ class AgentManager extends EventEmitter {
2256
2724
  runtimeSwitchVerifiedSessionId: typeof options.runtimeSwitchVerifiedSessionId === 'string'
2257
2725
  ? options.runtimeSwitchVerifiedSessionId
2258
2726
  : '',
2259
- persistentSessionId: '',
2727
+ persistentSessionId: typeof options.persistentSessionId === 'string' ? options.persistentSessionId : '',
2260
2728
  customTitle: typeof options.customTitle === 'string' ? options.customTitle.trim().slice(0, 80) : '',
2261
2729
  terminalBusy: null,
2262
2730
  shellCwd: '',
@@ -2276,7 +2744,10 @@ class AgentManager extends EventEmitter {
2276
2744
  attentionUpdatedAt: null,
2277
2745
  readAttentionAt: null,
2278
2746
  attentionReason: '',
2747
+ attentionOutputEpoch: '',
2279
2748
  attentionOutputSeq: null,
2749
+ readOutputEpoch: '',
2750
+ readOutputSeq: null,
2280
2751
  unread: false,
2281
2752
  archived: false,
2282
2753
  archivedAt: null,
@@ -2293,6 +2764,7 @@ class AgentManager extends EventEmitter {
2293
2764
 
2294
2765
  ensureAgentOrders(agentRecord, Array.from(this.agents.values()));
2295
2766
  this.agents.set(agentId, agentRecord);
2767
+ void this.refreshAgentWorktree(agentId);
2296
2768
 
2297
2769
  this.lastActivity.set(agentId, Date.now());
2298
2770
 
@@ -2301,9 +2773,10 @@ class AgentManager extends EventEmitter {
2301
2773
  try {
2302
2774
  if (useCodexAppServer) {
2303
2775
  try {
2776
+ const appServer = runtimeBindingOf(agentRecord, 'app-server');
2304
2777
  const prepared = await this.codexAppServerRuntime.prepareAgent({
2305
2778
  agentId,
2306
- codexHome: agentRecord.codexAppServerHomePath,
2779
+ codexHome: appServer.homePath,
2307
2780
  executable: spawnProgram,
2308
2781
  env: this.buildAgentEnv(agentId, agentRecord),
2309
2782
  cwd: workspace,
@@ -2315,19 +2788,13 @@ class AgentManager extends EventEmitter {
2315
2788
  ? this.configManager.getCodexApprovalMode()
2316
2789
  : 'approve'
2317
2790
  ),
2318
- model: this.configManager && this.configManager.getCodexModel
2319
- ? this.configManager.getCodexModel()
2320
- : 'gpt-5.5',
2321
- reasoningEffort: this.configManager && this.configManager.getCodexReasoningEffort
2322
- ? this.configManager.getCodexReasoningEffort()
2323
- : 'xhigh',
2324
- serviceTier: this.configManager && this.configManager.getCodexServiceTier
2325
- ? this.configManager.getCodexServiceTier()
2326
- : 'default',
2791
+ model: codexModel,
2792
+ reasoningEffort: codexReasoningEffort,
2793
+ serviceTier: codexServiceTier,
2327
2794
  developerInstructions: wantsMain ? renderMainAgentBootstrap() : '',
2328
2795
  });
2329
2796
  agentRecord.providerSessionId = prepared.threadId;
2330
- agentRecord.providerSessionKey = this.providerSessionKey(
2797
+ agentRecord.providerSessionKey = mainPageAgentSessionKey(
2331
2798
  'codex',
2332
2799
  prepared.threadId,
2333
2800
  agentRecord.providerHomeId || 'default'
@@ -2335,13 +2802,13 @@ class AgentManager extends EventEmitter {
2335
2802
  agentRecord.providerSessionTemporary = false;
2336
2803
  agentRecord.providerSessionSource = prepared.resumed ? 'app-server-resume' : 'app-server-start';
2337
2804
  agentRecord.providerSessionResolvedAt = Date.now();
2338
- agentRecord.codexAppServerState = 'idle';
2339
- agentRecord.codexAppServerEndpoint = prepared.endpoint;
2340
- agentRecord.codexAppServerThreadId = prepared.threadId;
2341
- agentRecord.codexAppServerError = '';
2805
+ appServer.state = 'idle';
2806
+ appServer.endpoint = prepared.endpoint;
2807
+ appServer.threadId = prepared.threadId;
2808
+ appServer.error = '';
2342
2809
  // App Server mode has no embedded CLI observer. Its chat and
2343
2810
  // lifecycle both come from the structured App Server connection.
2344
- agentRecord.codexCliObserverDeferred = false;
2811
+ appServer.observerDeferred = false;
2345
2812
  } catch (error) {
2346
2813
  const reason = error && error.message ? error.message : String(error);
2347
2814
  throw new Error(`Codex App Server failed to start: ${reason}`, { cause: error });
@@ -2349,6 +2816,7 @@ class AgentManager extends EventEmitter {
2349
2816
  }
2350
2817
 
2351
2818
  if (useJsonCli) {
2819
+ const jsonRuntime = runtimeBindingOf(agentRecord, 'json');
2352
2820
  this.jsonCliRuntime.registerAgent({
2353
2821
  agentId,
2354
2822
  provider: structuredRuntimeProvider,
@@ -2358,34 +2826,32 @@ class AgentManager extends EventEmitter {
2358
2826
  sessionId: agentRecord.providerSessionTemporary ? '' : agentRecord.providerSessionId,
2359
2827
  approvalMode: agentRecord.launchPermissionMode || 'approve',
2360
2828
  autoApprove: options.dangerouslySkipPermissions === true,
2361
- initialEvents: agentRecord.jsonCliEvents,
2829
+ initialEvents: jsonRuntime.events,
2362
2830
  });
2363
2831
  }
2364
2832
 
2365
2833
  if (useAcp) {
2834
+ const acpRuntime = runtimeBindingOf(agentRecord, 'acp');
2366
2835
  const prepared = await this.acpRuntime.prepareAgent({
2367
2836
  agentId,
2368
2837
  provider: structuredRuntimeProvider,
2369
2838
  executable: spawnProgram,
2370
2839
  env: this.buildAgentEnv(agentId, agentRecord),
2371
2840
  cwd: workspace,
2372
- sessionId: options.acpStartFresh === true || agentRecord.providerSessionTemporary
2841
+ sessionId: options.acpStartFresh === true || agentRecord.providerSessionTemporary || acpGeneratedFreshSession
2373
2842
  ? ''
2374
2843
  : agentRecord.providerSessionId,
2375
- historyMode: options.acpHistoryMode === 'resume' ? 'resume' : 'load',
2844
+ historyMode: options.acpHistoryMode === 'resume'
2845
+ ? 'resume'
2846
+ : (options.acpHistoryMode === 'load' ? 'load' : 'checkpoint'),
2847
+ providerHomeId: agentRecord.providerHomeId || 'default',
2376
2848
  approvalMode: agentRecord.launchPermissionMode || 'approve',
2377
- model: this.configManager && this.configManager.getCodexModel
2378
- ? this.configManager.getCodexModel()
2379
- : '',
2380
- reasoningEffort: this.configManager && this.configManager.getCodexReasoningEffort
2381
- ? this.configManager.getCodexReasoningEffort()
2382
- : '',
2383
- serviceTier: this.configManager && this.configManager.getCodexServiceTier
2384
- ? this.configManager.getCodexServiceTier()
2385
- : '',
2849
+ model: codexModel,
2850
+ reasoningEffort: codexReasoningEffort,
2851
+ serviceTier: codexServiceTier,
2386
2852
  });
2387
2853
  agentRecord.providerSessionId = prepared.sessionId;
2388
- agentRecord.providerSessionKey = this.providerSessionKey(
2854
+ agentRecord.providerSessionKey = mainPageAgentSessionKey(
2389
2855
  structuredRuntimeProvider,
2390
2856
  prepared.sessionId,
2391
2857
  agentRecord.providerHomeId || 'default'
@@ -2393,8 +2859,8 @@ class AgentManager extends EventEmitter {
2393
2859
  agentRecord.providerSessionTemporary = false;
2394
2860
  agentRecord.providerSessionSource = `acp-${prepared.historyMode}`;
2395
2861
  agentRecord.providerSessionResolvedAt = Date.now();
2396
- agentRecord.acpState = 'idle';
2397
- agentRecord.acpError = '';
2862
+ acpRuntime.state = 'idle';
2863
+ acpRuntime.error = '';
2398
2864
  }
2399
2865
 
2400
2866
  agentRecord.persistentSessionId = this.ensurePersistentAgentSession(agentRecord);
@@ -2404,6 +2870,7 @@ class AgentManager extends EventEmitter {
2404
2870
  args,
2405
2871
  cwd: workspace,
2406
2872
  category: resolution.spec ? resolution.spec.category : 'shell',
2873
+ reviveState: options.reviveTerminalState || null,
2407
2874
  };
2408
2875
  await this.createAgentEngineSession(agentRecord, resolution.engine, engineLaunch);
2409
2876
  }
@@ -2419,18 +2886,33 @@ class AgentManager extends EventEmitter {
2419
2886
  }
2420
2887
  }
2421
2888
 
2422
- this.activateProviderSessionTracking(agentId);
2889
+ this.providerSessionService.activate(agentId);
2423
2890
  if (callback) callback(agentId);
2424
2891
  this.emit('update');
2425
2892
  return agentId;
2426
2893
  } catch (error) {
2427
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
+ }
2428
2909
  this.agents.delete(agentId);
2429
2910
  this.lastActivity.delete(agentId);
2430
2911
  this.lastActivityUpdate.delete(agentId);
2431
2912
  this.outputEvents.delete(agentId);
2913
+ this.agentUsageRateCache.delete(agentId);
2432
2914
  this.lastResizeByAgent.delete(agentId);
2433
- this.stopCodexProviderSessionResolver(agentId);
2915
+ this.providerSessionService.stop(agentId);
2434
2916
  if (this.codexAppServerRuntime && typeof this.codexAppServerRuntime.unregisterAgent === 'function') {
2435
2917
  this.codexAppServerRuntime.unregisterAgent(agentId);
2436
2918
  }
@@ -2446,15 +2928,15 @@ class AgentManager extends EventEmitter {
2446
2928
  }
2447
2929
  }
2448
2930
 
2449
- async sendInput(agentId, input) {
2931
+ async enqueueInputOperation(agentId, operation) {
2450
2932
  const previous = this.inputQueues.get(agentId) || Promise.resolve();
2451
2933
  const next = previous
2452
2934
  .catch(() => {})
2453
- .then(() => this.sendInputNow(agentId, input));
2935
+ .then(operation);
2454
2936
 
2455
2937
  this.inputQueues.set(agentId, next);
2456
2938
  try {
2457
- await next;
2939
+ return await next;
2458
2940
  } finally {
2459
2941
  if (this.inputQueues.get(agentId) === next) {
2460
2942
  this.inputQueues.delete(agentId);
@@ -2462,20 +2944,52 @@ class AgentManager extends EventEmitter {
2462
2944
  }
2463
2945
  }
2464
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
+
2465
2977
  codexAppServerOptionsForAgent(agent, message = '') {
2466
2978
  const codexResolution = resolveCompatibleCodexExecutable(agent.requiredCliVersion || '');
2467
2979
  if (!codexResolution.compatible) {
2468
2980
  throw new Error(codexResolution.error || 'Codex CLI is not compatible with App Server mode');
2469
2981
  }
2470
2982
 
2983
+ const runtime = runtimeBindingOf(agent, 'app-server');
2984
+ if (!runtime) throw new Error('Agent is not using the Codex App Server runtime');
2471
2985
  return {
2472
2986
  agentId: agent.id,
2473
- codexHome: agent.codexAppServerHomePath || '',
2987
+ codexHome: runtime.homePath,
2474
2988
  executable: codexResolution.path || resolveAgentExecutable(agent.command) || agent.command,
2475
2989
  env: this.buildAgentEnv(agent.id, agent),
2476
2990
  cwd: agent.cwd,
2477
- workspaceRoot: agent.projectWorkspace || agent.cwd,
2478
- threadId: agent.codexAppServerThreadId || agent.providerSessionId || '',
2991
+ workspaceRoot: effectiveAgentWorkspaceRoot(agent),
2992
+ threadId: runtime.threadId || agent.providerSessionId || '',
2479
2993
  approvalMode: agent.launchPermissionMode || (
2480
2994
  this.configManager && this.configManager.getCodexApprovalMode
2481
2995
  ? this.configManager.getCodexApprovalMode()
@@ -2495,21 +3009,101 @@ class AgentManager extends EventEmitter {
2495
3009
  }
2496
3010
 
2497
3011
  async sendComposerMessage(agentId, message) {
2498
- const previous = this.inputQueues.get(agentId) || Promise.resolve();
2499
- const next = previous
2500
- .catch(() => {})
2501
- .then(() => this.sendComposerMessageNow(agentId, message));
3012
+ return this.enqueueInputOperation(agentId, () => this.sendComposerMessageNow(agentId, message));
3013
+ }
2502
3014
 
2503
- 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);
2504
3028
  try {
2505
3029
  return await next;
2506
3030
  } finally {
2507
- if (this.inputQueues.get(agentId) === next) {
2508
- this.inputQueues.delete(agentId);
3031
+ if (this.codexTerminalProfileQueues.get(agentId) === next) {
3032
+ this.codexTerminalProfileQueues.delete(agentId);
2509
3033
  }
2510
3034
  }
2511
3035
  }
2512
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
+
2513
3107
  async sendComposerMessageNow(agentId, message) {
2514
3108
  const agent = this.agents.get(agentId);
2515
3109
  if (!agent) throw new Error('Agent not found');
@@ -2523,19 +3117,20 @@ class AgentManager extends EventEmitter {
2523
3117
  throw new Error('Composer message is empty');
2524
3118
  }
2525
3119
 
3120
+ const appServer = runtimeBindingOf(agent, 'app-server');
2526
3121
  if (
2527
3122
  agent.providerSessionProvider === 'codex'
2528
- && agent.codexRuntimeMode === 'app-server'
3123
+ && appServer
2529
3124
  && this.codexAppServerRuntime
2530
3125
  && typeof this.codexAppServerRuntime.submitComposerMessage === 'function'
2531
3126
  ) {
2532
3127
  const result = await this.codexAppServerRuntime.submitComposerMessage(
2533
3128
  this.codexAppServerOptionsForAgent(agent, text)
2534
3129
  );
2535
- agent.codexAppServerThreadId = result.threadId;
2536
- agent.codexAppServerTurnId = result.turnId;
2537
- agent.codexAppServerState = 'working';
2538
- agent.codexAppServerError = '';
3130
+ appServer.threadId = result.threadId;
3131
+ appServer.turnId = result.turnId;
3132
+ appServer.state = 'working';
3133
+ appServer.error = '';
2539
3134
  this.lastActivity.set(agentId, Date.now());
2540
3135
  this.emit('update');
2541
3136
  return result;
@@ -2553,8 +3148,9 @@ class AgentManager extends EventEmitter {
2553
3148
 
2554
3149
  if (isAcpAgent(agent)) {
2555
3150
  const result = await this.acpRuntime.prompt(agentId, prompt);
2556
- agent.acpState = 'idle';
2557
- agent.acpStopReason = result.stopReason || '';
3151
+ const runtime = runtimeBindingOf(agent, 'acp');
3152
+ runtime.state = 'idle';
3153
+ runtime.stopReason = result.stopReason || '';
2558
3154
  this.ensurePersistentAgentSession(agent);
2559
3155
  return { kind: 'acp', ...result };
2560
3156
  }
@@ -2566,11 +3162,12 @@ class AgentManager extends EventEmitter {
2566
3162
  respondToCodexAppServerRequest(agentId, requestId, result, options = {}) {
2567
3163
  const agent = this.agents.get(agentId);
2568
3164
  if (!agent) throw new Error('Agent not found');
2569
- if (agent.providerSessionProvider !== 'codex' || agent.codexRuntimeMode !== 'app-server') {
3165
+ const runtime = runtimeBindingOf(agent, 'app-server');
3166
+ if (agent.providerSessionProvider !== 'codex' || !runtime) {
2570
3167
  throw new Error('This Agent is not using the Codex App Server runtime');
2571
3168
  }
2572
3169
  const resolvedRequestId = String(requestId || '');
2573
- if (!resolvedRequestId || resolvedRequestId !== agent.codexAppServerPendingRequestId) {
3170
+ if (!resolvedRequestId || resolvedRequestId !== runtime.pendingRequestId) {
2574
3171
  throw new Error('Codex App Server request is no longer pending');
2575
3172
  }
2576
3173
  if (!this.codexAppServerRuntime) throw new Error('Codex App Server runtime is unavailable');
@@ -2582,10 +3179,10 @@ class AgentManager extends EventEmitter {
2582
3179
  { code: -32000, message: String(options.reason || 'Rejected by Farming') }
2583
3180
  )
2584
3181
  : this.codexAppServerRuntime.resolveAgentServerRequest(agentId, resolvedRequestId, result);
2585
- agent.codexAppServerPendingRequestId = '';
2586
- agent.codexAppServerPendingRequestMethod = '';
2587
- agent.codexAppServerPendingRequest = null;
2588
- agent.codexAppServerState = agent.codexAppServerTurnId ? 'working' : 'idle';
3182
+ runtime.pendingRequestId = '';
3183
+ runtime.pendingRequestMethod = '';
3184
+ runtime.pendingRequest = null;
3185
+ runtime.state = runtime.turnId ? 'working' : 'idle';
2589
3186
  this.emit('update');
2590
3187
  return response;
2591
3188
  }
@@ -2593,11 +3190,12 @@ class AgentManager extends EventEmitter {
2593
3190
  assertCodexAppServerGoalAgent(agentId) {
2594
3191
  const agent = this.agents.get(agentId);
2595
3192
  if (!agent) throw new Error('Agent not found');
2596
- if (agent.providerSessionProvider !== 'codex' || agent.codexRuntimeMode !== 'app-server') {
3193
+ const runtime = runtimeBindingOf(agent, 'app-server');
3194
+ if (agent.providerSessionProvider !== 'codex' || !runtime) {
2597
3195
  throw new Error('This Agent is not using the Codex App Server runtime');
2598
3196
  }
2599
3197
  if (!this.codexAppServerRuntime) throw new Error('Codex App Server runtime is unavailable');
2600
- if (!(agent.codexAppServerThreadId || agent.providerSessionId)) {
3198
+ if (!(runtime.threadId || agent.providerSessionId)) {
2601
3199
  throw new Error('Codex App Server thread id is not available yet');
2602
3200
  }
2603
3201
  return agent;
@@ -2606,7 +3204,7 @@ class AgentManager extends EventEmitter {
2606
3204
  async getCodexAppServerGoal(agentId) {
2607
3205
  const agent = this.assertCodexAppServerGoalAgent(agentId);
2608
3206
  const goal = await this.codexAppServerRuntime.getAgentGoal(this.codexAppServerOptionsForAgent(agent));
2609
- agent.codexAppServerGoal = goal || null;
3207
+ runtimeBindingOf(agent, 'app-server').goal = goal || null;
2610
3208
  this.emit('update');
2611
3209
  return goal || null;
2612
3210
  }
@@ -2637,7 +3235,11 @@ class AgentManager extends EventEmitter {
2637
3235
  const agent = this.agents.get(agentId);
2638
3236
  if (!agent) throw new Error('Agent not found');
2639
3237
  if (!isAcpAgent(agent)) throw new Error('Agent is not using the ACP runtime');
2640
- 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
+ };
2641
3243
  }
2642
3244
 
2643
3245
  getAcpToolDetail(agentId, toolCallId) {
@@ -2778,7 +3380,7 @@ class AgentManager extends EventEmitter {
2778
3380
  ...(status !== undefined ? { status } : {}),
2779
3381
  ...(Object.prototype.hasOwnProperty.call(patch, 'tokenBudget') ? { tokenBudget } : {}),
2780
3382
  });
2781
- agent.codexAppServerGoal = goal || null;
3383
+ runtimeBindingOf(agent, 'app-server').goal = goal || null;
2782
3384
  this.emit('update');
2783
3385
  return goal || null;
2784
3386
  }
@@ -2786,43 +3388,43 @@ class AgentManager extends EventEmitter {
2786
3388
  async clearCodexAppServerGoal(agentId) {
2787
3389
  const agent = this.assertCodexAppServerGoalAgent(agentId);
2788
3390
  await this.codexAppServerRuntime.clearAgentGoal(this.codexAppServerOptionsForAgent(agent));
2789
- agent.codexAppServerGoal = null;
3391
+ runtimeBindingOf(agent, 'app-server').goal = null;
2790
3392
  this.emit('update');
2791
3393
  return null;
2792
3394
  }
2793
3395
 
2794
- async sendInputNow(agentId, input) {
3396
+ async sendInputNow(agentId, input, { markUserInput = true, expectedRuntimeEpoch = '' } = {}) {
2795
3397
  const agent = this.agents.get(agentId);
2796
3398
  if (!agent) return;
2797
3399
  // The observer PTY is optional telemetry for App Server Agents. Its
2798
3400
  // disappearance must never redefine the App Server Agent as dead.
2799
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
+ }
2800
3405
 
2801
3406
  const engine = this.engineBridge.getEngine(agent.engineName);
2802
3407
  if (!engine) return;
2803
3408
 
2804
- if (agent.terminalInputReceived !== true) {
2805
- agent.terminalInputReceived = true;
2806
- this.ensurePersistentAgentSession(agent);
2807
- this.updateEngineProviderSessionMetadata(agent);
2808
- }
2809
-
2810
- for (let attempt = 0; ; attempt += 1) {
2811
- try {
2812
- await engine.sendInput(agentId, input);
2813
- this.observeAgentStateChange(agentId, { force: true });
2814
- return;
2815
- } catch (error) {
2816
- const delay = INPUT_SESSION_RETRY_DELAYS_MS[attempt];
2817
- if (!isSessionNotAvailableError(error) || delay === undefined) {
2818
- console.error('Failed to send input:', error);
2819
- if (isSessionNotAvailableError(error)) {
2820
- this.markAgentSessionDead(agentId, error);
2821
- }
2822
- return;
2823
- }
2824
- await sleep(delay);
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 } });
3417
+ }
3418
+ if (submittedUserInput) {
3419
+ this.providerSessionService.observe(agentId, { force: true });
2825
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;
2826
3428
  }
2827
3429
  }
2828
3430
 
@@ -2837,33 +3439,34 @@ class AgentManager extends EventEmitter {
2837
3439
  agent.terminalBusy = false;
2838
3440
  agent.exitedAt = Date.now();
2839
3441
  agent.output = trimSessionOutput(`${agent.output || ''}\n${message}`);
2840
- this.observeAgentStateChange(agentId, { force: true });
3442
+ this.providerSessionService.observe(agentId, { force: true });
2841
3443
  this.emit('update');
2842
3444
  }
2843
3445
 
2844
- async interruptAgent(agentId) {
3446
+ async interruptAgent(agentId, options = {}) {
2845
3447
  const agent = this.agents.get(agentId);
2846
3448
  if (!agent) return;
3449
+ const appServer = runtimeBindingOf(agent, 'app-server');
2847
3450
 
2848
3451
  try {
2849
3452
  if (
2850
3453
  agent.providerSessionProvider === 'codex'
2851
- && agent.codexRuntimeMode === 'app-server'
3454
+ && appServer
2852
3455
  && this.codexAppServerRuntime
2853
3456
  && typeof this.codexAppServerRuntime.interruptAgent === 'function'
2854
3457
  ) {
2855
3458
  try {
2856
3459
  const interrupted = await this.codexAppServerRuntime.interruptAgent(agentId);
2857
3460
  if (interrupted) {
2858
- agent.codexAppServerState = 'interrupting';
3461
+ appServer.state = 'interrupting';
2859
3462
  this.emit('update');
2860
3463
  return;
2861
3464
  }
2862
- agent.codexAppServerError = 'Codex App Server interrupt is not available';
3465
+ appServer.error = 'Codex App Server interrupt is not available';
2863
3466
  this.emit('update');
2864
3467
  return;
2865
3468
  } catch (error) {
2866
- agent.codexAppServerError = `Codex App Server interrupt failed: ${error && (error.message || error)}`;
3469
+ appServer.error = `Codex App Server interrupt failed: ${error && (error.message || error)}`;
2867
3470
  this.emit('update');
2868
3471
  return;
2869
3472
  }
@@ -2881,9 +3484,9 @@ class AgentManager extends EventEmitter {
2881
3484
 
2882
3485
  const input = interruptInputForAgent(agent);
2883
3486
  if (engine.interruptSession) {
2884
- await engine.interruptSession(agentId, input);
3487
+ return await engine.interruptSession(agentId, input, options);
2885
3488
  } else {
2886
- await engine.sendInput(agentId, input);
3489
+ return await engine.sendInput(agentId, input, options);
2887
3490
  }
2888
3491
  } catch (error) {
2889
3492
  console.error('Failed to interrupt agent:', error);
@@ -2893,10 +3496,48 @@ class AgentManager extends EventEmitter {
2893
3496
  }
2894
3497
  }
2895
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
+
2896
3535
  async resizeAgentSession(agentId, cols, rows) {
2897
3536
  const agent = this.agents.get(agentId);
2898
- if (!agent) return;
2899
- 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
+ }
2900
3541
 
2901
3542
  const nextCols = Math.floor(Number(cols));
2902
3543
  const nextRows = Math.floor(Number(rows));
@@ -2906,49 +3547,50 @@ class AgentManager extends EventEmitter {
2906
3547
  nextCols < MIN_TERMINAL_RESIZE_COLS ||
2907
3548
  nextRows < MIN_TERMINAL_RESIZE_ROWS
2908
3549
  ) {
2909
- return;
3550
+ return { status: 'resize-rejected', reason: 'invalid-dimensions', resized: false };
2910
3551
  }
2911
3552
 
2912
3553
  try {
2913
- const previousSize = this.lastResizeByAgent.get(agentId);
2914
- if (previousSize && previousSize.cols === nextCols && previousSize.rows === nextRows) {
2915
- return;
2916
- }
2917
-
2918
3554
  const engine = this.engineBridge.getEngine(agent.engineName);
2919
- if (!engine || !engine.resizeSession) return;
3555
+ if (!engine || !engine.resizeSession) {
3556
+ return { status: 'resize-rejected', reason: 'unsupported-engine', resized: false };
3557
+ }
2920
3558
 
2921
3559
  const result = await engine.resizeSession(agentId, nextCols, nextRows);
2922
- if (result && result.resized === false) {
3560
+ if (result && result.resized === false && result.reason === 'session-unavailable') {
2923
3561
  this.markAgentSessionDead(agentId, 'Session not available');
2924
- return;
2925
3562
  }
2926
- 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;
2927
3567
  } catch (error) {
2928
3568
  console.error('Failed to resize agent session:', error);
3569
+ return { status: 'resize-rejected', reason: 'resize-failed', resized: false };
2929
3570
  }
2930
3571
  }
2931
3572
 
2932
- async clearAgentSessionBuffer(agentId) {
3573
+ async clearAgentSessionBuffer(agentId, options = {}) {
2933
3574
  const agent = this.agents.get(agentId);
2934
3575
  if (!agent) return { cleared: false };
2935
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
+ }
2936
3580
 
2937
3581
  try {
2938
3582
  const engine = this.engineBridge.getEngine(agent.engineName);
2939
3583
  if (!engine || !engine.clearBuffer) return { cleared: false };
2940
- const result = await engine.clearBuffer(agentId);
3584
+ const result = await engine.clearBuffer(agentId, options);
2941
3585
  if (result && result.cleared === false) {
2942
- this.markAgentSessionDead(agentId, 'Session not available');
3586
+ if (result.reason === 'session-unavailable') {
3587
+ this.markAgentSessionDead(agentId, 'Session not available');
3588
+ }
2943
3589
  return result;
2944
3590
  }
2945
- agent.output = '';
2946
- agent.previewText = '';
2947
- agent.previewSnapshot = null;
2948
- this.outputEvents.delete(agentId);
2949
- this.lastActivity.set(agentId, Date.now());
2950
- this.observeAgentStateChange(agentId, { force: true });
2951
- this.emit('update');
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.
2952
3594
  return result || { cleared: true };
2953
3595
  } catch (error) {
2954
3596
  console.error('Failed to clear agent session buffer:', error);
@@ -2990,9 +3632,12 @@ class AgentManager extends EventEmitter {
2990
3632
  }
2991
3633
 
2992
3634
  const updates = {};
3635
+ let structuralUpdateChanged = false;
3636
+ let readUpdateChanged = false;
2993
3637
  if (typeof flags.pinned === 'boolean') {
2994
3638
  const wasPinned = agent.pinned === true;
2995
3639
  agent.pinned = flags.pinned;
3640
+ structuralUpdateChanged = structuralUpdateChanged || wasPinned !== agent.pinned;
2996
3641
  updates.pinned = agent.pinned;
2997
3642
  if (!wasPinned && agent.pinned) {
2998
3643
  agent.pinnedOrder = nextPinnedOrder(Array.from(this.agents.values()));
@@ -3000,22 +3645,36 @@ class AgentManager extends EventEmitter {
3000
3645
  updates.pinnedOrder = finiteOrder(agent.pinnedOrder);
3001
3646
  }
3002
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
+
3003
3660
  if (typeof flags.unread === 'boolean') {
3004
3661
  const result = flags.unread
3005
3662
  ? this.markAgentUnreadCursor(agentId)
3006
- : this.markAgentReadCursor(agentId);
3663
+ : this.markAgentReadCursor(agentId, undefined, { emitUpdate: false });
3007
3664
  if (result.error) return result;
3008
3665
  updates.unread = result.unread;
3009
3666
  updates.attentionSeq = result.attentionSeq;
3010
3667
  updates.readAttentionSeq = result.readAttentionSeq;
3668
+ readUpdateChanged = readUpdateChanged || result.changed === true;
3011
3669
  }
3012
3670
 
3013
3671
  if (typeof flags.readAttentionSeq === 'number' && Number.isFinite(flags.readAttentionSeq)) {
3014
- const result = this.markAgentReadCursor(agentId, flags.readAttentionSeq);
3672
+ const result = this.markAgentReadCursor(agentId, flags.readAttentionSeq, { emitUpdate: false });
3015
3673
  if (result.error) return result;
3016
3674
  updates.unread = result.unread;
3017
3675
  updates.attentionSeq = result.attentionSeq;
3018
3676
  updates.readAttentionSeq = result.readAttentionSeq;
3677
+ readUpdateChanged = readUpdateChanged || result.changed === true;
3019
3678
  }
3020
3679
 
3021
3680
  if (flags.archived === true) {
@@ -3026,18 +3685,38 @@ class AgentManager extends EventEmitter {
3026
3685
  }
3027
3686
 
3028
3687
  if (flags.archived === false) {
3688
+ structuralUpdateChanged = structuralUpdateChanged || agent.archived === true || agent.archivedAt !== null;
3029
3689
  agent.archived = false;
3030
3690
  agent.archivedAt = null;
3031
3691
  updates.archived = agent.archived;
3032
3692
  updates.archivedAt = agent.archivedAt;
3033
3693
  }
3034
3694
 
3035
- if (typeof flags.pinned === 'boolean') {
3695
+ if (
3696
+ typeof flags.pinned === 'boolean'
3697
+ || (typeof flags.readOutputEpoch === 'string' && typeof flags.readOutputSeq === 'number')
3698
+ ) {
3036
3699
  this.ensurePersistentAgentSession(agent);
3037
3700
  this.updateEngineProviderSessionMetadata(agent);
3038
3701
  }
3039
- this.emit('update');
3040
- 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
+ };
3041
3720
  }
3042
3721
 
3043
3722
  reorderProjectAgent(agentId, { beforeAgentId = '', afterAgentId = '' } = {}) {
@@ -3098,7 +3777,7 @@ class AgentManager extends EventEmitter {
3098
3777
 
3099
3778
  if (
3100
3779
  agent.providerSessionProvider === 'codex'
3101
- && agent.codexRuntimeMode === 'app-server'
3780
+ && runtimeKind(agent) === 'app-server'
3102
3781
  ) {
3103
3782
  return this.updateCodexAppServerPermissionMode(agentId, mode);
3104
3783
  }
@@ -3109,7 +3788,8 @@ class AgentManager extends EventEmitter {
3109
3788
  async updateCodexAppServerPermissionMode(agentId, mode) {
3110
3789
  const agent = this.agents.get(agentId);
3111
3790
  if (!agent) return { error: 'Agent not found' };
3112
- if (agent.providerSessionProvider !== 'codex' || agent.codexRuntimeMode !== 'app-server') {
3791
+ const runtime = runtimeBindingOf(agent, 'app-server');
3792
+ if (agent.providerSessionProvider !== 'codex' || !runtime) {
3113
3793
  return { error: 'Agent is not using the Codex App Server runtime' };
3114
3794
  }
3115
3795
 
@@ -3129,7 +3809,7 @@ class AgentManager extends EventEmitter {
3129
3809
  }
3130
3810
 
3131
3811
  agent.launchPermissionMode = nextMode;
3132
- agent.codexAppServerError = '';
3812
+ runtime.error = '';
3133
3813
  this.updateEngineProviderSessionMetadata(agent);
3134
3814
  this.ensurePersistentAgentSession(agent, { launchPermissionMode: nextMode });
3135
3815
  this.emit('update');
@@ -3185,29 +3865,30 @@ class AgentManager extends EventEmitter {
3185
3865
  const nextMode = ['terminal', 'json', 'acp'].includes(mode) ? mode : '';
3186
3866
  if (!nextMode) return { error: 'Unsupported Agent runtime mode' };
3187
3867
  const provider = agent.providerSessionProvider || '';
3868
+ const currentKind = runtimeKind(agent);
3869
+ const currentMode = ['acp', 'json'].includes(currentKind) ? currentKind : 'terminal';
3188
3870
  const leavesCodexAppServer = provider === 'codex'
3189
- && agent.codexRuntimeMode === 'app-server'
3871
+ && currentKind === 'app-server'
3190
3872
  && nextMode === 'terminal';
3191
- if (agent.agentRuntimeMode === nextMode && !leavesCodexAppServer) {
3873
+ if (currentMode === nextMode && !leavesCodexAppServer) {
3192
3874
  return { agentId, agentRuntimeMode: nextMode };
3193
3875
  }
3194
- const turnActive = agent.agentRuntimeMode === 'acp'
3195
- ? ['working', 'waiting-for-permission', 'interrupting'].includes(agent.acpState || '')
3876
+ const turnActive = currentKind === 'acp'
3877
+ ? ['working', 'waiting-for-permission', 'interrupting'].includes(runtimeBindingOf(agent, 'acp').state)
3196
3878
  : this.isAgentAttentionTurnActive(agent);
3197
3879
  if (turnActive) {
3198
3880
  return { error: 'Interrupt the active Agent turn before switching Chat and Terminal.' };
3199
3881
  }
3200
- const supportedProviders = nextMode === 'json' ? ['codex', 'opencode'] : ['codex', 'claude', 'opencode', 'qoder'];
3201
- if (!supportedProviders.includes(provider)) {
3882
+ if (!providerSupportsRuntime(provider, nextMode)) {
3202
3883
  return { error: `Agent does not support the ${nextMode.toUpperCase()} runtime` };
3203
3884
  }
3204
3885
  const sessionId = String(agent.providerSessionId || '').trim();
3205
3886
  const canStartFreshAcpSession = nextMode === 'acp'
3206
- && agent.agentRuntimeMode === 'terminal'
3887
+ && currentMode === 'terminal'
3207
3888
  && agent.terminalInputReceived !== true
3208
3889
  && (
3209
3890
  agent.providerSessionTemporary === true
3210
- || ['codex-temporary', 'claude-session-id', 'qoder-session-id'].includes(agent.providerSessionSource || '')
3891
+ || isFreshAcpSessionSource(provider, agent.providerSessionSource || '')
3211
3892
  );
3212
3893
  if (!isSafeProviderSessionId(sessionId) && !canStartFreshAcpSession) {
3213
3894
  return { error: 'Runtime switching requires a resumable provider session. Send the first message and try again.' };
@@ -3219,7 +3900,7 @@ class AgentManager extends EventEmitter {
3219
3900
  // fail. Historical/terminal sessions still use the provider lookup as a
3220
3901
  // stale-session guard.
3221
3902
  let liveAcpSession = false;
3222
- if (agent.agentRuntimeMode === 'acp' && this.acpRuntime && typeof this.acpRuntime.getSession === 'function') {
3903
+ if (currentKind === 'acp' && this.acpRuntime && typeof this.acpRuntime.getSession === 'function') {
3223
3904
  try {
3224
3905
  const snapshot = this.acpRuntime.getSession(agentId, { maxEntries: 0 });
3225
3906
  liveAcpSession = String(snapshot?.sessionId || '') === sessionId;
@@ -3239,7 +3920,8 @@ class AgentManager extends EventEmitter {
3239
3920
  const command = startsFreshAcpSession
3240
3921
  ? (agent.forkCommand || agent.command)
3241
3922
  : buildAgentSessionResumeCommand(provider, sessionId, {
3242
- cwd: agent.cwd || agent.projectWorkspace || '',
3923
+ cwd: effectiveAgentWorkspaceRoot(agent),
3924
+ providerHomePath: agent.providerHomePath || '',
3243
3925
  });
3244
3926
  if (!command) return { error: 'Failed to build provider resume command' };
3245
3927
  const preserved = {
@@ -3250,13 +3932,13 @@ class AgentManager extends EventEmitter {
3250
3932
  unread: agent.unread === true,
3251
3933
  jsonCliEvents: isJsonCliAgent(agent)
3252
3934
  ? this.jsonCliRuntime.getEvents(agentId)
3253
- : (Array.isArray(agent.jsonCliEvents) ? agent.jsonCliEvents : []),
3935
+ : (Array.isArray(agent.runtimeResumeState?.jsonEvents) ? agent.runtimeResumeState.jsonEvents : []),
3254
3936
  };
3255
3937
  const restartOptions = {
3256
3938
  wantsMain: agent.wantsMain === true,
3257
3939
  task: agent.task || agent.providerSessionTitle || '',
3258
3940
  workflowTemplate: agent.workflowTemplate || '',
3259
- projectWorkspace: agent.projectWorkspace || agent.cwd || '',
3941
+ projectWorkspace: effectiveAgentWorkspaceRoot(agent),
3260
3942
  source: startsFreshAcpSession
3261
3943
  ? 'ui-runtime-switch-fresh'
3262
3944
  : resumedAgentSource(provider, sessionId, agent.providerHomeId || ''),
@@ -3277,12 +3959,15 @@ class AgentManager extends EventEmitter {
3277
3959
  codexApprovalMode: agent.launchPermissionMode || undefined,
3278
3960
  jsonCliEvents: preserved.jsonCliEvents,
3279
3961
  runtimeSwitchVerifiedSessionId: startsFreshAcpSession ? '' : sessionId,
3962
+ ...(provider === 'codex' && !startsFreshAcpSession
3963
+ ? preserveCodexSessionProfileOptions()
3964
+ : {}),
3280
3965
  };
3281
- const originalMode = agent.agentRuntimeMode || 'terminal';
3966
+ const originalMode = currentMode;
3282
3967
  const originalOptions = {
3283
3968
  ...restartOptions,
3284
3969
  agentRuntimeMode: originalMode,
3285
- codexRuntimeMode: agent.codexRuntimeMode || 'cli',
3970
+ codexRuntimeMode: currentKind === 'app-server' ? 'app-server' : 'cli',
3286
3971
  acpStartFresh: false,
3287
3972
  };
3288
3973
  const startReplacement = options => new Promise(resolve => {
@@ -3295,7 +3980,7 @@ class AgentManager extends EventEmitter {
3295
3980
  try {
3296
3981
  const started = this.startAgent(
3297
3982
  command,
3298
- agent.cwd || agent.projectWorkspace || null,
3983
+ effectiveAgentWorkspaceRoot(agent) || null,
3299
3984
  (restartedAgentId, error) => finish(restartedAgentId, error),
3300
3985
  options
3301
3986
  );
@@ -3402,11 +4087,11 @@ class AgentManager extends EventEmitter {
3402
4087
  if (!hasResumableSession && !startsFreshCodexSession) {
3403
4088
  return { error: 'Permission changes require a resumable provider session. Try again after the session id is available.' };
3404
4089
  }
3405
-
3406
4090
  const command = startsFreshCodexSession
3407
4091
  ? 'codex'
3408
4092
  : buildAgentSessionResumeCommand(provider, sessionId, {
3409
- cwd: agent.cwd || agent.projectWorkspace || '',
4093
+ cwd: effectiveAgentWorkspaceRoot(agent),
4094
+ providerHomePath: agent.providerHomePath || '',
3410
4095
  });
3411
4096
  if (!command) {
3412
4097
  return { error: 'Failed to build provider resume command' };
@@ -3417,7 +4102,7 @@ class AgentManager extends EventEmitter {
3417
4102
  task: agent.task || agent.providerSessionTitle || '',
3418
4103
  workflowTemplate: agent.workflowTemplate || '',
3419
4104
  requiredCliVersion: provider === 'codex' ? (agent.requiredCliVersion || '') : '',
3420
- projectWorkspace: agent.projectWorkspace || agent.cwd || '',
4105
+ projectWorkspace: effectiveAgentWorkspaceRoot(agent),
3421
4106
  source: startsFreshCodexSession
3422
4107
  ? 'ui'
3423
4108
  : (resumedSessionFromSource(agent.source)
@@ -3435,6 +4120,9 @@ class AgentManager extends EventEmitter {
3435
4120
  projectOrder: finiteOrder(agent.projectOrder),
3436
4121
  pinnedOrder: finiteOrder(agent.pinnedOrder),
3437
4122
  ...(provider === 'codex' ? { codexApprovalMode: nextMode } : { claudePermissionMode: nextMode }),
4123
+ ...(provider === 'codex' && hasResumableSession
4124
+ ? preserveCodexSessionProfileOptions()
4125
+ : {}),
3438
4126
  };
3439
4127
  const preserved = {
3440
4128
  pinned: agent.pinned === true,
@@ -3458,7 +4146,7 @@ class AgentManager extends EventEmitter {
3458
4146
  }
3459
4147
 
3460
4148
  return new Promise((resolve) => {
3461
- const startResult = this.startAgent(command, agent.cwd || agent.projectWorkspace || null, (restartedAgentId, error) => {
4149
+ const startResult = this.startAgent(command, effectiveAgentWorkspaceRoot(agent) || null, (restartedAgentId, error) => {
3462
4150
  if (error) {
3463
4151
  this.emit('update');
3464
4152
  resolve({ error });
@@ -3608,7 +4296,7 @@ class AgentManager extends EventEmitter {
3608
4296
  const resolvedWorkspace = path.resolve(workspace);
3609
4297
  return Array.from(this.agents.values()).filter(agent => {
3610
4298
  if (!agent || agent.isMain) return false;
3611
- const agentWorkspace = this.expandWorkspacePath(agent.projectWorkspace || agent.cwd || '');
4299
+ const agentWorkspace = this.expandWorkspacePath(effectiveAgentWorkspaceRoot(agent));
3612
4300
  if (!agentWorkspace) return false;
3613
4301
  return path.resolve(agentWorkspace) === resolvedWorkspace;
3614
4302
  });
@@ -3621,7 +4309,7 @@ class AgentManager extends EventEmitter {
3621
4309
 
3622
4310
  const keysToRemove = new Set();
3623
4311
  agents.forEach(agent => {
3624
- const providerSessionKey = agent.providerSessionKey || this.providerSessionKey(
4312
+ const providerSessionKey = agent.providerSessionKey || mainPageAgentSessionKey(
3625
4313
  agent.providerSessionProvider,
3626
4314
  agent.providerSessionId,
3627
4315
  agent.providerHomeId || ''
@@ -3693,8 +4381,21 @@ class AgentManager extends EventEmitter {
3693
4381
  if (!agent) {
3694
4382
  return { error: 'Agent not found' };
3695
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
+ }
3696
4398
 
3697
- const sourceWorkspace = agent.projectWorkspace || agent.cwd;
3698
4399
  let targetWorkspace = sourceWorkspace;
3699
4400
  if (mode === 'new-worktree') {
3700
4401
  try {
@@ -3702,19 +4403,13 @@ class AgentManager extends EventEmitter {
3702
4403
  } catch (error) {
3703
4404
  return { error: error.message || 'Failed to create git worktree' };
3704
4405
  }
3705
- } else if (mode !== 'same-worktree') {
3706
- return { error: 'Unsupported fork mode' };
3707
4406
  }
3708
4407
 
3709
- const resumedSession = agent.providerSessionProvider
3710
- && agent.providerSessionId
3711
- && agent.providerSessionTemporary !== true
3712
- ? { provider: agent.providerSessionProvider, providerHomeId: agent.providerHomeId || 'default', sessionId: agent.providerSessionId }
3713
- : resumedSessionFromSource(agent.source);
3714
4408
  const forkCommand = resumedSession
3715
4409
  ? buildAgentSessionResumeCommand(resumedSession.provider, resumedSession.sessionId, {
3716
4410
  fork: true,
3717
4411
  cwd: targetWorkspace,
4412
+ providerHomePath: agent.providerHomePath || '',
3718
4413
  })
3719
4414
  : (agent.forkCommand || agent.command);
3720
4415
 
@@ -3741,10 +4436,62 @@ class AgentManager extends EventEmitter {
3741
4436
  source: mode === 'new-worktree' ? 'ui-fork-new-worktree' : 'ui-fork-same-worktree',
3742
4437
  providerHomeId: agent.providerHomeId || (resumedSession && resumedSession.providerHomeId) || '',
3743
4438
  providerHomePath: agent.providerHomePath || '',
4439
+ ...(resumedSession?.provider === 'codex'
4440
+ ? preserveCodexSessionProfileOptions()
4441
+ : {}),
3744
4442
  });
3745
4443
  });
3746
4444
  }
3747
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
+
3748
4495
  recordTaskHistory(agent, options = {}) {
3749
4496
  if (!agent || agent.id === this.mainAgentId) return;
3750
4497
  if (!isSupportedHistoryAgent(agent.forkCommand || agent.command || '')) return;
@@ -3758,7 +4505,7 @@ class AgentManager extends EventEmitter {
3758
4505
  agentId: agent.id,
3759
4506
  command: agent.command || '',
3760
4507
  cwd: agent.cwd || '',
3761
- projectWorkspace: agent.projectWorkspace || agent.cwd || '',
4508
+ projectWorkspace: effectiveAgentWorkspaceRoot(agent),
3762
4509
  title: agent.customTitle || agent.sessionTitle || agent.task || '',
3763
4510
  customTitle: agent.customTitle || '',
3764
4511
  task: agent.task || '',
@@ -3817,9 +4564,9 @@ class AgentManager extends EventEmitter {
3817
4564
  this.lastActivity.delete(agentId);
3818
4565
  this.lastActivityUpdate.delete(agentId);
3819
4566
  this.outputEvents.delete(agentId);
4567
+ this.agentUsageRateCache.delete(agentId);
3820
4568
  this.lastResizeByAgent.delete(agentId);
3821
- this.stopCodexProviderSessionResolver(agentId);
3822
- this.stopProviderSessionTitleResolver(agentId);
4569
+ this.providerSessionService.stop(agentId);
3823
4570
  if (this.codexAppServerRuntime && typeof this.codexAppServerRuntime.unregisterAgent === 'function') {
3824
4571
  this.codexAppServerRuntime.unregisterAgent(agentId);
3825
4572
  }
@@ -3873,19 +4620,20 @@ class AgentManager extends EventEmitter {
3873
4620
  return null;
3874
4621
  }
3875
4622
 
3876
- return agent.projectWorkspace || agent.cwd;
4623
+ return effectiveAgentWorkspaceRoot(agent);
3877
4624
  }
3878
4625
 
3879
4626
  getAgentProviderSession(agentId) {
3880
4627
  const agent = this.agents.get(agentId);
3881
4628
  if (!agent) return null;
4629
+ const appServer = runtimeBindingOf(agent, 'app-server');
3882
4630
  return {
3883
4631
  provider: agent.providerSessionProvider || '',
3884
4632
  sessionId: agent.providerSessionId || '',
3885
4633
  providerHomeId: agent.providerHomeId || '',
3886
4634
  providerHomePath: agent.providerHomePath || '',
3887
- codexAppServerHomePath: agent.codexAppServerHomePath || '',
3888
- codexRuntimeMode: agent.codexRuntimeMode || '',
4635
+ codexAppServerHomePath: appServer?.homePath || '',
4636
+ runtimeBinding: publicRuntimeBinding(agent),
3889
4637
  temporary: agent.providerSessionTemporary === true,
3890
4638
  title: agent.providerSessionTitle || '',
3891
4639
  };
@@ -3965,6 +4713,7 @@ class AgentManager extends EventEmitter {
3965
4713
  engineName: agent.engineName || '',
3966
4714
  cwd: agent.cwd,
3967
4715
  projectWorkspace: agent.projectWorkspace || '',
4716
+ gitWorktree: publicAgentGitWorktree(agent),
3968
4717
  status: sessionState && sessionState.status === 'exited'
3969
4718
  ? agent.status
3970
4719
  : (isLiveEngineSessionState(sessionState) ? 'running' : agent.status),
@@ -3989,27 +4738,9 @@ class AgentManager extends EventEmitter {
3989
4738
  providerSessionSource: agent.providerSessionSource || '',
3990
4739
  providerSessionResolvedAt: agent.providerSessionResolvedAt || null,
3991
4740
  providerSessionTitle: agent.providerSessionTitle || '',
4741
+ providerSessionWorkspace: agent.providerSessionWorkspace || '',
3992
4742
  terminalInputReceived: agent.terminalInputReceived === true,
3993
- codexRuntimeMode: agent.codexRuntimeMode || '',
3994
- agentRuntimeMode: agent.agentRuntimeMode || 'terminal',
3995
- acpState: agent.acpState || '',
3996
- acpError: agent.acpError || '',
3997
- acpStopReason: agent.acpStopReason || '',
3998
- acpPendingPermission: agent.acpPendingPermission || null,
3999
- acpPendingPermissions: Array.isArray(agent.acpPendingPermissions) ? agent.acpPendingPermissions : [],
4000
- acpPendingElicitation: agent.acpPendingElicitation || null,
4001
- acpPendingElicitations: Array.isArray(agent.acpPendingElicitations) ? agent.acpPendingElicitations : [],
4002
- acpActiveElicitations: Array.isArray(agent.acpActiveElicitations) ? agent.acpActiveElicitations : [],
4003
- acpSessionUpdatedAt: agent.acpSessionUpdatedAt || '',
4004
- acpSessionRevision: Number(agent.acpSessionRevision) || 0,
4005
- codexAppServerState: agent.codexAppServerState || '',
4006
- codexAppServerEndpoint: agent.codexAppServerEndpoint || '',
4007
- codexAppServerThreadId: agent.codexAppServerThreadId || '',
4008
- codexAppServerTurnId: agent.codexAppServerTurnId || '',
4009
- codexAppServerError: agent.codexAppServerError || '',
4010
- codexAppServerPendingRequestId: agent.codexAppServerPendingRequestId || '',
4011
- codexAppServerPendingRequestMethod: agent.codexAppServerPendingRequestMethod || '',
4012
- codexAppServerPendingRequest: agent.codexAppServerPendingRequest || null,
4743
+ runtimeBinding: publicRuntimeBinding(agent),
4013
4744
  forkedFromProviderSessionId: agent.forkedFromProviderSessionId || '',
4014
4745
  customTitle: agent.customTitle || '',
4015
4746
  pinned: agent.pinned === true,
@@ -4020,12 +4751,21 @@ class AgentManager extends EventEmitter {
4020
4751
  attentionUpdatedAt: finiteNumberOrNull(agent.attentionUpdatedAt),
4021
4752
  readAttentionAt: finiteNumberOrNull(agent.readAttentionAt),
4022
4753
  attentionReason: agent.attentionReason || '',
4754
+ attentionOutputEpoch: agent.attentionOutputEpoch || '',
4023
4755
  attentionOutputSeq: finiteNumberOrNull(agent.attentionOutputSeq),
4756
+ readOutputEpoch: agent.readOutputEpoch || '',
4757
+ readOutputSeq: finiteNumberOrNull(agent.readOutputSeq),
4024
4758
  unread: agentAttentionUnread(agent),
4025
4759
  archived: agent.archived === true,
4026
4760
  archivedAt: agent.archivedAt || null,
4027
4761
  sessionSource: this.getEngineSessionSource(agent.engineName),
4762
+ runtimeEpoch: sessionState && typeof sessionState.runtimeEpoch === 'string'
4763
+ ? sessionState.runtimeEpoch
4764
+ : (agent.runtimeEpoch || ''),
4028
4765
  outputSeq: sessionState && Number.isFinite(sessionState.outputSeq) ? sessionState.outputSeq : null,
4766
+ stateRevision: sessionState && Number.isFinite(sessionState.stateRevision)
4767
+ ? sessionState.stateRevision
4768
+ : null,
4029
4769
  isMain,
4030
4770
  activityLevel: isMain ? 'warm' : this.calculateActivityLevel(lastActivity, now),
4031
4771
  lastActivity,
@@ -4037,6 +4777,7 @@ class AgentManager extends EventEmitter {
4037
4777
  output: (sessionState && typeof sessionState.output === 'string') ? sessionState.output : fallbackOutput,
4038
4778
  renderOutput: (sessionState && typeof sessionState.renderOutput === 'string') ? sessionState.renderOutput : fallbackOutput,
4039
4779
  previewText,
4780
+ codexTerminalProfile: activeCodexTerminalProfile(agent, previewText),
4040
4781
  previewSnapshot: (sessionState && sessionState.previewSnapshot) || agent.previewSnapshot || null,
4041
4782
  previewCols: (sessionState && Number.isFinite(sessionState.previewCols) && sessionState.previewCols > 0)
4042
4783
  ? sessionState.previewCols
@@ -4044,10 +4785,28 @@ class AgentManager extends EventEmitter {
4044
4785
  previewRows: (sessionState && Number.isFinite(sessionState.previewRows) && sessionState.previewRows > 0)
4045
4786
  ? sessionState.previewRows
4046
4787
  : (agent.previewRows || 30),
4047
- usageRate: this.calculateAgentUsageRate(agent.id),
4788
+ usageRate: this.getAgentUsageRate(agent.id),
4048
4789
  };
4049
4790
  }
4050
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
+
4051
4810
  calculateAgentUsageRate(agentId, options = {}) {
4052
4811
  const now = options.now || Date.now();
4053
4812
  const windowMs = options.windowMs || AGENT_USAGE_RATE_WINDOW_MS;
@@ -4084,7 +4843,7 @@ class AgentManager extends EventEmitter {
4084
4843
  cwd: agent.cwd,
4085
4844
  isMain: this.isMainAgentRecord(agent.id, agent),
4086
4845
  status: agent.status,
4087
- usageRate: this.calculateAgentUsageRate(agent.id, { now, windowMs }),
4846
+ usageRate: this.getAgentUsageRate(agent.id, { now, windowMs }),
4088
4847
  }));
4089
4848
  const totalOutputBytes = agents.reduce((sum, agent) => sum + agent.usageRate.outputBytes, 0);
4090
4849
  const estimatedOutputTokens = agents.reduce((sum, agent) => sum + agent.usageRate.estimatedOutputTokens, 0);
@@ -4126,12 +4885,17 @@ class AgentManager extends EventEmitter {
4126
4885
  engineName: agent.engineName || '',
4127
4886
  cwd: agent.cwd,
4128
4887
  projectWorkspace: agent.projectWorkspace || '',
4888
+ gitWorktree: publicAgentGitWorktree(agent),
4129
4889
  output: agent.output.slice(-2000),
4130
4890
  previewText: agent.previewText || '',
4891
+ codexTerminalProfile: activeCodexTerminalProfile(agent, agent.previewText || ''),
4131
4892
  previewCols: agent.previewCols || 80,
4132
4893
  previewRows: agent.previewRows || 30,
4133
4894
  sessionTitle: agent.sessionTitle || '',
4134
4895
  sessionSource: this.getEngineSessionSource(agent.engineName),
4896
+ runtimeEpoch: agent.runtimeEpoch || '',
4897
+ outputSeq: finiteNumberOrNull(agent.lastOutputSeq),
4898
+ stateRevision: finiteNumberOrNull(agent.stateRevision),
4135
4899
  status: agent.status,
4136
4900
  terminalBusy,
4137
4901
  terminalStatus,
@@ -4155,33 +4919,11 @@ class AgentManager extends EventEmitter {
4155
4919
  providerSessionSource: agent.providerSessionSource || '',
4156
4920
  providerSessionResolvedAt: agent.providerSessionResolvedAt || null,
4157
4921
  providerSessionTitle: agent.providerSessionTitle || '',
4922
+ providerSessionWorkspace: agent.providerSessionWorkspace || '',
4923
+ providerCapabilities: providerCapabilities(agent.providerSessionProvider),
4158
4924
  terminalInputReceived: agent.terminalInputReceived === true,
4159
- codexRuntimeMode: agent.codexRuntimeMode || '',
4160
- agentRuntimeMode: agent.agentRuntimeMode || 'terminal',
4161
- jsonCliState: agent.jsonCliState || '',
4162
- jsonCliError: agent.jsonCliError || '',
4163
- jsonCliTranscriptUpdatedAt: agent.jsonCliTranscriptUpdatedAt || '',
4164
- acpState: agent.acpState || '',
4165
- acpError: agent.acpError || '',
4166
- acpStopReason: agent.acpStopReason || '',
4167
- acpPendingPermission: agent.acpPendingPermission || null,
4168
- acpPendingPermissions: Array.isArray(agent.acpPendingPermissions) ? agent.acpPendingPermissions : [],
4169
- acpPendingElicitation: agent.acpPendingElicitation || null,
4170
- acpPendingElicitations: Array.isArray(agent.acpPendingElicitations) ? agent.acpPendingElicitations : [],
4171
- acpActiveElicitations: Array.isArray(agent.acpActiveElicitations) ? agent.acpActiveElicitations : [],
4172
- acpSessionUpdatedAt: agent.acpSessionUpdatedAt || '',
4173
- acpSessionRevision: Number(agent.acpSessionRevision) || 0,
4174
- codexAppServerState: agent.codexAppServerState || '',
4175
- codexAppServerEndpoint: agent.codexAppServerEndpoint || '',
4176
- codexAppServerThreadId: agent.codexAppServerThreadId || '',
4177
- codexAppServerTurnId: agent.codexAppServerTurnId || '',
4178
- codexAppServerError: agent.codexAppServerError || '',
4179
- codexAppServerPendingRequestId: agent.codexAppServerPendingRequestId || '',
4180
- codexAppServerPendingRequestMethod: agent.codexAppServerPendingRequestMethod || '',
4181
- codexAppServerPendingRequest: agent.codexAppServerPendingRequest || null,
4182
- codexAppServerNotice: agent.codexAppServerNotice || null,
4183
- codexAppServerGoal: agent.codexAppServerGoal || null,
4184
- codexCliObserverDeferred: agent.codexCliObserverDeferred === true,
4925
+ runtimeBinding: publicRuntimeBinding(agent),
4926
+ runtimeObservation: deriveRuntimeObservation(agent),
4185
4927
  forkedFromProviderSessionId: agent.forkedFromProviderSessionId || '',
4186
4928
  restartedFromAgentId: agent.restartedFromAgentId || '',
4187
4929
  restartedFromAgentIds: Array.isArray(agent.restartedFromAgentIds) ? agent.restartedFromAgentIds : [],
@@ -4195,11 +4937,14 @@ class AgentManager extends EventEmitter {
4195
4937
  attentionUpdatedAt: finiteNumberOrNull(agent.attentionUpdatedAt),
4196
4938
  readAttentionAt: finiteNumberOrNull(agent.readAttentionAt),
4197
4939
  attentionReason: agent.attentionReason || '',
4940
+ attentionOutputEpoch: agent.attentionOutputEpoch || '',
4198
4941
  attentionOutputSeq: finiteNumberOrNull(agent.attentionOutputSeq),
4942
+ readOutputEpoch: agent.readOutputEpoch || '',
4943
+ readOutputSeq: finiteNumberOrNull(agent.readOutputSeq),
4199
4944
  unread: agentAttentionUnread(agent),
4200
4945
  archived: agent.archived === true,
4201
4946
  archivedAt: agent.archivedAt || null,
4202
- canForkNewWorktree: this.canCreateForkWorktree(agent.projectWorkspace || agent.cwd || ''),
4947
+ canForkNewWorktree: this.canCreateForkWorktree(effectiveAgentWorkspaceRoot(agent)),
4203
4948
  startedAt: agent.startedAt || null,
4204
4949
  exitedAt: agent.exitedAt || null,
4205
4950
  // Main agent is exempt from activity/attention/zombie scoring
@@ -4207,7 +4952,7 @@ class AgentManager extends EventEmitter {
4207
4952
  lastActivity,
4208
4953
  attentionScore: isMain ? 0 : this.calculateAttentionScore(id, now),
4209
4954
  isZombie: isMain ? false : this.isZombie(id, now),
4210
- usageRate: this.calculateAgentUsageRate(id, { now })
4955
+ usageRate: this.getAgentUsageRate(id, { now })
4211
4956
  });
4212
4957
  }
4213
4958
 
@@ -4280,6 +5025,10 @@ class AgentManager extends EventEmitter {
4280
5025
  this.on('update', callback);
4281
5026
  }
4282
5027
 
5028
+ onAgentActivity(callback) {
5029
+ this.on('agent-activity', callback);
5030
+ }
5031
+
4283
5032
  onSessionStream(callback) {
4284
5033
  this.on('session-stream', callback);
4285
5034
  }
@@ -4295,17 +5044,20 @@ class AgentManager extends EventEmitter {
4295
5044
  continue;
4296
5045
  }
4297
5046
 
5047
+ const terminalStatus = deriveAgentTerminalStatus(agent, {
5048
+ previewText: agent.previewText || '',
5049
+ title: agent.sessionTitle || '',
5050
+ terminalBusy: typeof agent.terminalBusy === 'boolean' ? agent.terminalBusy : null,
5051
+ });
4298
5052
  previews.push({
4299
5053
  agentId: agent.id,
4300
5054
  previewText: agent.previewText || '',
4301
5055
  cols: agent.previewCols || 80,
4302
5056
  rows: agent.previewRows || 30,
4303
5057
  previewSnapshot: agent.previewSnapshot || null,
4304
- terminalStatus: deriveAgentTerminalStatus(agent, {
4305
- previewText: agent.previewText || '',
4306
- title: agent.sessionTitle || '',
4307
- terminalBusy: typeof agent.terminalBusy === 'boolean' ? agent.terminalBusy : null,
4308
- }),
5058
+ codexTerminalProfile: activeCodexTerminalProfile(agent, agent.previewText || ''),
5059
+ terminalStatus,
5060
+ runtimeObservation: deriveRuntimeObservation({ ...agent, terminalStatus }),
4309
5061
  });
4310
5062
  }
4311
5063