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.
- package/README.md +36 -23
- package/README.zh_cn.md +36 -23
- package/THIRD_PARTY_NOTICES.md +11 -2
- package/backend/acp-checkpoint-store.js +198 -0
- package/backend/acp-runtime.js +306 -83
- package/backend/acp-session-state.js +202 -6
- package/backend/acp-transcript.js +112 -0
- package/backend/agent-activity.js +6 -157
- package/backend/agent-manager.js +1482 -730
- package/backend/agent-provider-session.js +17 -242
- package/backend/agent-runtime-binding.js +219 -0
- package/backend/agent-session-history.js +66 -1
- package/backend/auth.js +79 -6
- package/backend/codex-models.js +81 -84
- package/backend/codex-session-archive.js +45 -0
- package/backend/codex-terminal-profile.js +500 -0
- package/backend/codex-transcript-sanitizer.js +12 -0
- package/backend/codex-transcript.js +230 -8
- package/backend/config-manager.js +30 -0
- package/backend/control-api.js +192 -17
- package/backend/farming-net-pass.js +285 -0
- package/backend/farming-net-registry.js +112 -0
- package/backend/farming-net-server.js +298 -0
- package/backend/farming-session-store.js +5 -13
- package/backend/git-worktree-info.js +181 -0
- package/backend/local-session-engine.js +411 -186
- package/backend/main-page-session.js +5 -2
- package/backend/native-pty-controller-generation.js +106 -0
- package/backend/native-pty-host-client.js +275 -7
- package/backend/native-pty-host-identity.js +86 -0
- package/backend/native-pty-host.js +813 -114
- package/backend/native-session-engine.js +100 -28
- package/backend/packaged-node-pty.js +22 -2
- package/backend/provider-adapters.js +253 -0
- package/backend/provider-session-service.js +241 -0
- package/backend/runtime-observation.js +81 -0
- package/backend/server.js +294 -86
- package/backend/session-engine-bridge.js +21 -2
- package/backend/session-engine-router.js +1 -1
- package/backend/session-engine.js +1 -1
- package/backend/session-stream-protocol.js +185 -0
- package/backend/storage-layout.js +55 -0
- package/backend/terminal-attach-checkpoint.js +74 -0
- package/backend/terminal-exit-quiescence.js +39 -0
- package/backend/terminal-reducer-flow-control.js +97 -0
- package/backend/terminal-screen-state.js +11 -2
- package/backend/terminal-screen-worker-pool.js +59 -6
- package/backend/terminal-screen-worker-thread.js +97 -57
- package/backend/terminal-screen-worker.js +133 -51
- package/backend/terminal-state-serialization.js +127 -0
- package/backend/terminal-status.js +23 -4
- package/backend/usage-monitor.js +81 -4
- package/backend/workspace-directory.js +152 -0
- package/backend/workspace-file-router.js +182 -76
- package/backend/workspace-file-service.js +319 -4
- package/backend/workspace-root-registry.js +164 -0
- package/dist/assets/App-CHYpgECZ.js +208 -0
- package/dist/assets/{FileEditorMarkdownPreview-elKWc8Im.js → FileEditorMarkdownPreview--Cri80nQ.js} +92 -92
- package/dist/assets/FileEditorPane-DGJlEKdf.js +2 -0
- package/dist/assets/IconGlyphs-Cc14sepw.js +1 -0
- package/dist/assets/ProjectFilesSection-Cz1wG4NT.js +12 -0
- package/dist/assets/{ReviewPage-BaXu1ZdX.js → ReviewPage-LonRc6Fz.js} +1 -1
- package/dist/assets/code-dark-B1pAtO_P.css +1 -0
- package/dist/assets/file-icons-Bw2qd5iT.js +1 -0
- package/dist/assets/{index-BrbljRqn.js → index-miO2r9IZ.js} +3 -3
- package/dist/assets/main-DNe0jPw-.css +1 -0
- package/dist/assets/workspace-editor-model-BQol4qbA.js +1 -0
- package/dist/assets/workspace-editor-monaco-5gFbeDPF.js +1 -0
- package/dist/assets/workspace-editor-monaco-xPv8JzHH.js +4 -0
- package/dist/assets/workspace-view-state-DvYG_9PH.js +7 -0
- package/dist/assets/workspace-working-copy-D8-s_Sgh.js +1 -0
- package/dist/index.html +1 -1
- package/frontend/farming-net/app.css +625 -0
- package/frontend/farming-net/app.js +268 -0
- package/frontend/farming-net/index.html +86 -0
- package/frontend/reading-anchor.js +198 -0
- package/frontend/session-bridge.js +12 -3
- package/frontend/session-modal-bridge.js +5 -12
- package/frontend/skins/crt/app.js +1976 -793
- package/frontend/skins/crt/index.html +313 -23
- package/frontend/skins/crt/styles/billing.css +294 -223
- package/frontend/skins/crt/styles/monochrome-green.css +7 -2
- package/frontend/terminal-replay.js +372 -0
- package/package.json +10 -3
- package/shared/browser-protocol.d.ts +5 -0
- package/shared/browser-protocol.js +130 -0
- package/dist/assets/App-8dYAM6ql.js +0 -124
- package/dist/assets/FileEditorPane-RWiFD2cq.js +0 -5
- package/dist/assets/IconGlyphs-DfL0EBnj.js +0 -1
- package/dist/assets/ProjectFilesSection-Q4PDsWmM.js +0 -12
- package/dist/assets/code-dark-CDkOQAtK.css +0 -1
- package/dist/assets/file-icons-EFUGSSwf.js +0 -1
- package/dist/assets/main-D073SnW4.css +0 -1
- package/dist/assets/qoder-C9LmmOSf.svg +0 -1
- package/dist/assets/qoder-Cf9gl0Y5.svg +0 -1
- package/dist/assets/qoder-gHCinseV.svg +0 -1
- package/dist/assets/workspace-view-state-CTyDzk2D.js +0 -1
- package/dist/assets/zsh-CLpveKlF.svg +0 -1
- package/dist/assets/zsh-FxSpMPbz.svg +0 -1
- /package/dist/assets/{api-D1lyBYIQ.js → api-D8nyOEbz.js} +0 -0
- /package/dist/assets/{core-ZlAPicox.js → core-D0LFJkDt.js} +0 -0
- /package/dist/assets/{useWorkspaceMenuKeyboard-CneKAZUJ.js → useWorkspaceMenuKeyboard-Brws6Ar9.js} +0 -0
package/backend/agent-manager.js
CHANGED
|
@@ -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 {
|
|
11
|
-
|
|
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
|
-
|
|
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?.
|
|
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
|
|
226
|
+
&& runtimeKind(metadata) === 'app-server'
|
|
107
227
|
&& hasUsableCodexAppServerHome(metadata);
|
|
108
228
|
}
|
|
109
229
|
|
|
110
230
|
function isJsonCliAgent(agent) {
|
|
111
|
-
return agent
|
|
231
|
+
return runtimeKind(agent) === 'json';
|
|
112
232
|
}
|
|
113
233
|
|
|
114
234
|
function isAcpAgent(agent) {
|
|
115
|
-
return agent
|
|
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
|
-
|
|
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
|
|
253
|
-
|
|
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
|
|
271
|
-
|
|
272
|
-
|
|
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
|
|
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
|
|
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.
|
|
351
|
-
this.
|
|
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
|
-
|
|
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
|
-
:
|
|
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
|
-
|
|
397
|
-
|
|
398
|
-
|
|
627
|
+
const runtime = runtimeBindingOf(agent, 'json');
|
|
628
|
+
if (!runtime) return;
|
|
629
|
+
runtime.state = state || '';
|
|
630
|
+
runtime.error = error || '';
|
|
399
631
|
if (sessionId) {
|
|
400
|
-
agent.
|
|
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
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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.
|
|
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
|
-
|
|
451
|
-
|
|
452
|
-
|
|
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
|
-
|
|
684
|
+
const runtime = runtimeBindingOf(agent, 'app-server');
|
|
685
|
+
if (!runtime) return;
|
|
462
686
|
|
|
463
687
|
if (typeof patch.threadId === 'string' && patch.threadId) {
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
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')
|
|
474
|
-
if (typeof patch.state === 'string' && patch.state)
|
|
475
|
-
if (typeof patch.error === 'string')
|
|
476
|
-
if (typeof patch.pendingRequestId === 'string')
|
|
477
|
-
if (typeof patch.pendingRequestMethod === 'string')
|
|
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
|
-
|
|
701
|
+
runtime.pendingRequest = patch.pendingRequest || null;
|
|
480
702
|
}
|
|
481
703
|
if (Object.prototype.hasOwnProperty.call(patch, 'notice')) {
|
|
482
|
-
|
|
704
|
+
runtime.notice = patch.notice || null;
|
|
483
705
|
}
|
|
484
706
|
if (Object.prototype.hasOwnProperty.call(patch, 'goal')) {
|
|
485
|
-
|
|
707
|
+
runtime.goal = patch.goal || null;
|
|
486
708
|
}
|
|
487
709
|
if (typeof patch.transcriptUpdatedAt === 'string') {
|
|
488
|
-
|
|
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', ({
|
|
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.
|
|
740
|
+
this.providerSessionService.observe(sessionId, { force: true });
|
|
508
741
|
this.emit('update');
|
|
509
742
|
});
|
|
510
743
|
|
|
511
|
-
this.engineBridge.on('session-output', ({
|
|
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
|
-
|
|
518
|
-
|
|
519
|
-
|
|
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:
|
|
527
|
-
|
|
528
|
-
this.
|
|
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-
|
|
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:
|
|
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
|
-
|
|
597
|
-
|
|
598
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
719
|
-
this.emit('update');
|
|
1041
|
+
this.emit('agent-update', { agentId: sessionId, patch: terminalMetadataPatch(agent) });
|
|
720
1042
|
});
|
|
721
1043
|
|
|
722
|
-
this.engineBridge.on('session-exited', ({
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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
|
-
|
|
846
|
-
|
|
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 || !
|
|
851
|
-
|
|
1452
|
+
if (!agentId || !sessionId || !providerSupportsRuntime(provider, 'acp')) continue;
|
|
1453
|
+
const agent = this.agents.get(agentId);
|
|
852
1454
|
if (!agent) {
|
|
853
|
-
|
|
854
|
-
ensureAgentOrders(
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
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
|
|
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: '
|
|
1488
|
+
historyMode: 'checkpoint',
|
|
1489
|
+
providerHomeId: agent.providerHomeId || record.providerHomeId || 'default',
|
|
876
1490
|
approvalMode,
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
reasoningEffort:
|
|
881
|
-
|
|
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
|
-
|
|
891
|
-
|
|
892
|
-
|
|
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
|
-
|
|
899
|
-
|
|
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
|
|
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
|
|
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
|
|
1567
|
+
workspaceRoot: effectiveAgentWorkspaceRoot(agent),
|
|
965
1568
|
approvalMode: agent.launchPermissionMode || 'approve',
|
|
966
1569
|
});
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
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
|
-
|
|
1579
|
+
runtime.observerDeferred = false;
|
|
976
1580
|
agent.exitedAt = null;
|
|
977
|
-
this.
|
|
1581
|
+
this.providerSessionService.activate(agentId);
|
|
978
1582
|
this.ensurePersistentAgentSession(agent);
|
|
979
1583
|
} catch (error) {
|
|
980
|
-
|
|
981
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
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.
|
|
1829
|
-
this.
|
|
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
|
-
|
|
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
|
|
1978
|
-
|
|
1979
|
-
|
|
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
|
|
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
|
-
&&
|
|
2651
|
+
&& providerSupportsRuntime(structuredRuntimeProvider, 'json')
|
|
2169
2652
|
&& !useCodexAppServer
|
|
2170
2653
|
&& process.env.FARMING_E2E_FAKE_EXECUTABLES !== '1';
|
|
2171
2654
|
const useAcp = requestedAgentRuntimeMode === 'acp'
|
|
2172
|
-
&&
|
|
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:
|
|
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
|
-
|
|
2224
|
-
? (
|
|
2225
|
-
:
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
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:
|
|
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:
|
|
2319
|
-
|
|
2320
|
-
|
|
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 =
|
|
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
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
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
|
-
|
|
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:
|
|
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'
|
|
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:
|
|
2378
|
-
|
|
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 =
|
|
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
|
-
|
|
2397
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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(
|
|
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:
|
|
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
|
|
2478
|
-
threadId:
|
|
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
|
-
|
|
2499
|
-
|
|
2500
|
-
.catch(() => {})
|
|
2501
|
-
.then(() => this.sendComposerMessageNow(agentId, message));
|
|
3012
|
+
return this.enqueueInputOperation(agentId, () => this.sendComposerMessageNow(agentId, message));
|
|
3013
|
+
}
|
|
2502
3014
|
|
|
2503
|
-
|
|
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.
|
|
2508
|
-
this.
|
|
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
|
-
&&
|
|
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
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
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
|
-
|
|
2557
|
-
|
|
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
|
-
|
|
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 !==
|
|
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
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
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
|
-
|
|
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 (!(
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
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.
|
|
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
|
-
&&
|
|
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
|
-
|
|
3461
|
+
appServer.state = 'interrupting';
|
|
2859
3462
|
this.emit('update');
|
|
2860
3463
|
return;
|
|
2861
3464
|
}
|
|
2862
|
-
|
|
3465
|
+
appServer.error = 'Codex App Server interrupt is not available';
|
|
2863
3466
|
this.emit('update');
|
|
2864
3467
|
return;
|
|
2865
3468
|
} catch (error) {
|
|
2866
|
-
|
|
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))
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
3586
|
+
if (result.reason === 'session-unavailable') {
|
|
3587
|
+
this.markAgentSessionDead(agentId, 'Session not available');
|
|
3588
|
+
}
|
|
2943
3589
|
return result;
|
|
2944
3590
|
}
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
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 (
|
|
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
|
-
|
|
3040
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
&&
|
|
3871
|
+
&& currentKind === 'app-server'
|
|
3190
3872
|
&& nextMode === 'terminal';
|
|
3191
|
-
if (
|
|
3873
|
+
if (currentMode === nextMode && !leavesCodexAppServer) {
|
|
3192
3874
|
return { agentId, agentRuntimeMode: nextMode };
|
|
3193
3875
|
}
|
|
3194
|
-
const turnActive =
|
|
3195
|
-
? ['working', 'waiting-for-permission', 'interrupting'].includes(agent
|
|
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
|
-
|
|
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
|
-
&&
|
|
3887
|
+
&& currentMode === 'terminal'
|
|
3207
3888
|
&& agent.terminalInputReceived !== true
|
|
3208
3889
|
&& (
|
|
3209
3890
|
agent.providerSessionTemporary === true
|
|
3210
|
-
||
|
|
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 (
|
|
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
|
|
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.
|
|
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
|
|
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 =
|
|
3966
|
+
const originalMode = currentMode;
|
|
3282
3967
|
const originalOptions = {
|
|
3283
3968
|
...restartOptions,
|
|
3284
3969
|
agentRuntimeMode: originalMode,
|
|
3285
|
-
codexRuntimeMode:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 ||
|
|
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
|
|
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.
|
|
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
|
|
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:
|
|
3888
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
4160
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
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
|
|