farming-code 2.2.7 → 2.2.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +110 -199
- package/README.zh_cn.md +106 -237
- 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 +1496 -737
- 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-B8SHldPE.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-iA2BU9P6.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-DpYuBonn.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()
|
|
@@ -2140,6 +2623,16 @@ class AgentManager extends EventEmitter {
|
|
|
2140
2623
|
resolvedProviderHomeId = defaultCodexHome.id || 'default';
|
|
2141
2624
|
}
|
|
2142
2625
|
}
|
|
2626
|
+
const requestedAgentRuntimeMode = ['json', 'acp'].includes(options.agentRuntimeMode)
|
|
2627
|
+
? options.agentRuntimeMode
|
|
2628
|
+
: 'terminal';
|
|
2629
|
+
// A fresh structured runtime does not need a provider CLI resume id yet:
|
|
2630
|
+
// ACP/JSON creates the provider session and writes the resulting id back
|
|
2631
|
+
// after connecting. OpenCode is the important case here because its
|
|
2632
|
+
// terminal CLI does not accept a pre-generated id for a fresh session.
|
|
2633
|
+
const structuredRuntimeProvider = ['json', 'acp'].includes(requestedAgentRuntimeMode)
|
|
2634
|
+
? homeProvider
|
|
2635
|
+
: providerSessionPlan.provider;
|
|
2143
2636
|
const requestedCodexRuntimeMode = options.codexRuntimeMode === 'app-server' || options.codexRuntimeMode === 'cli'
|
|
2144
2637
|
? options.codexRuntimeMode
|
|
2145
2638
|
: (this.configManager && typeof this.configManager.getCodexRuntimeMode === 'function'
|
|
@@ -2154,20 +2647,19 @@ class AgentManager extends EventEmitter {
|
|
|
2154
2647
|
// on the path they are designed to exercise.
|
|
2155
2648
|
&& process.env.FARMING_E2E_FAKE_EXECUTABLES !== '1'
|
|
2156
2649
|
&& normalizeCodexRuntimeMode(requestedCodexRuntimeMode) === 'app-server';
|
|
2157
|
-
const requestedAgentRuntimeMode = ['json', 'acp'].includes(options.agentRuntimeMode)
|
|
2158
|
-
? options.agentRuntimeMode
|
|
2159
|
-
: 'terminal';
|
|
2160
2650
|
const useJsonCli = requestedAgentRuntimeMode === 'json'
|
|
2161
|
-
&&
|
|
2651
|
+
&& providerSupportsRuntime(structuredRuntimeProvider, 'json')
|
|
2162
2652
|
&& !useCodexAppServer
|
|
2163
2653
|
&& process.env.FARMING_E2E_FAKE_EXECUTABLES !== '1';
|
|
2164
2654
|
const useAcp = requestedAgentRuntimeMode === 'acp'
|
|
2165
|
-
&&
|
|
2655
|
+
&& providerSupportsRuntime(structuredRuntimeProvider, 'acp')
|
|
2166
2656
|
&& !useCodexAppServer
|
|
2167
2657
|
&& (
|
|
2168
2658
|
process.env.FARMING_E2E_FAKE_EXECUTABLES !== '1'
|
|
2169
2659
|
|| process.env.FARMING_E2E_FAKE_ACP_AGENT === '1'
|
|
2170
2660
|
);
|
|
2661
|
+
const acpGeneratedFreshSession = useAcp
|
|
2662
|
+
&& isFreshAcpSessionSource(structuredRuntimeProvider, providerSessionPlan.source);
|
|
2171
2663
|
let codexAppServerHomePath = '';
|
|
2172
2664
|
if (useCodexAppServer) {
|
|
2173
2665
|
try {
|
|
@@ -2203,44 +2695,27 @@ class AgentManager extends EventEmitter {
|
|
|
2203
2695
|
task: typeof options.task === 'string' ? options.task : '',
|
|
2204
2696
|
workflowTemplate: typeof options.workflowTemplate === 'string' ? options.workflowTemplate : '',
|
|
2205
2697
|
source: typeof options.source === 'string' ? options.source : 'ui',
|
|
2206
|
-
providerSessionProvider: providerSessionPlan.provider || '',
|
|
2698
|
+
providerSessionProvider: useAcp || useJsonCli ? structuredRuntimeProvider : (providerSessionPlan.provider || ''),
|
|
2207
2699
|
providerHomeId: resolvedProviderHomeId,
|
|
2208
2700
|
providerHomePath,
|
|
2209
2701
|
providerSessionId: providerSessionPlan.id || '',
|
|
2210
|
-
providerSessionKey:
|
|
2702
|
+
providerSessionKey: mainPageAgentSessionKey(providerSessionPlan.provider, providerSessionPlan.id, providerHome ? providerHome.id : providerHomeId),
|
|
2211
2703
|
providerSessionTemporary: providerSessionPlan.temporary === true,
|
|
2212
2704
|
providerSessionSource: providerSessionPlan.source || '',
|
|
2213
2705
|
providerSessionResolvedAt: providerSessionPlan.temporary === true ? null : Date.now(),
|
|
2214
2706
|
providerSessionTitle: typeof options.providerSessionTitle === 'string' ? options.providerSessionTitle.trim().slice(0, 160) : '',
|
|
2707
|
+
providerSessionWorkspace: '',
|
|
2215
2708
|
terminalInputReceived: false,
|
|
2216
|
-
|
|
2217
|
-
? (
|
|
2218
|
-
:
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
acpStopReason: '',
|
|
2227
|
-
acpPendingPermission: null,
|
|
2228
|
-
acpPendingPermissions: [],
|
|
2229
|
-
acpPendingElicitation: null,
|
|
2230
|
-
acpPendingElicitations: [],
|
|
2231
|
-
acpActiveElicitations: [],
|
|
2232
|
-
acpSessionUpdatedAt: '',
|
|
2233
|
-
acpSessionRevision: 0,
|
|
2234
|
-
codexAppServerHomePath,
|
|
2235
|
-
codexAppServerState: useCodexAppServer ? 'connecting' : '',
|
|
2236
|
-
codexAppServerEndpoint: '',
|
|
2237
|
-
codexAppServerThreadId: '',
|
|
2238
|
-
codexAppServerTurnId: '',
|
|
2239
|
-
codexAppServerError: '',
|
|
2240
|
-
codexAppServerPendingRequestId: '',
|
|
2241
|
-
codexAppServerPendingRequestMethod: '',
|
|
2242
|
-
codexAppServerPendingRequest: null,
|
|
2243
|
-
codexCliObserverDeferred: false,
|
|
2709
|
+
runtimeBinding: useCodexAppServer
|
|
2710
|
+
? runtimeBindingFor('app-server', { state: 'connecting', homePath: codexAppServerHomePath })
|
|
2711
|
+
: (useAcp
|
|
2712
|
+
? runtimeBindingFor('acp', { state: 'connecting' })
|
|
2713
|
+
: (useJsonCli
|
|
2714
|
+
? runtimeBindingFor('json', {
|
|
2715
|
+
state: 'idle',
|
|
2716
|
+
events: Array.isArray(options.jsonCliEvents) ? options.jsonCliEvents : [],
|
|
2717
|
+
})
|
|
2718
|
+
: runtimeBindingFor('terminal'))),
|
|
2244
2719
|
forkedFromProviderSessionId: providerSessionPlan.forkedFromProviderSessionId || '',
|
|
2245
2720
|
restartedFromAgentId: typeof options.restartedFromAgentId === 'string' ? options.restartedFromAgentId : '',
|
|
2246
2721
|
restartedFromAgentIds: Array.isArray(options.restartedFromAgentIds)
|
|
@@ -2249,7 +2724,7 @@ class AgentManager extends EventEmitter {
|
|
|
2249
2724
|
runtimeSwitchVerifiedSessionId: typeof options.runtimeSwitchVerifiedSessionId === 'string'
|
|
2250
2725
|
? options.runtimeSwitchVerifiedSessionId
|
|
2251
2726
|
: '',
|
|
2252
|
-
persistentSessionId: '',
|
|
2727
|
+
persistentSessionId: typeof options.persistentSessionId === 'string' ? options.persistentSessionId : '',
|
|
2253
2728
|
customTitle: typeof options.customTitle === 'string' ? options.customTitle.trim().slice(0, 80) : '',
|
|
2254
2729
|
terminalBusy: null,
|
|
2255
2730
|
shellCwd: '',
|
|
@@ -2269,7 +2744,10 @@ class AgentManager extends EventEmitter {
|
|
|
2269
2744
|
attentionUpdatedAt: null,
|
|
2270
2745
|
readAttentionAt: null,
|
|
2271
2746
|
attentionReason: '',
|
|
2747
|
+
attentionOutputEpoch: '',
|
|
2272
2748
|
attentionOutputSeq: null,
|
|
2749
|
+
readOutputEpoch: '',
|
|
2750
|
+
readOutputSeq: null,
|
|
2273
2751
|
unread: false,
|
|
2274
2752
|
archived: false,
|
|
2275
2753
|
archivedAt: null,
|
|
@@ -2286,6 +2764,7 @@ class AgentManager extends EventEmitter {
|
|
|
2286
2764
|
|
|
2287
2765
|
ensureAgentOrders(agentRecord, Array.from(this.agents.values()));
|
|
2288
2766
|
this.agents.set(agentId, agentRecord);
|
|
2767
|
+
void this.refreshAgentWorktree(agentId);
|
|
2289
2768
|
|
|
2290
2769
|
this.lastActivity.set(agentId, Date.now());
|
|
2291
2770
|
|
|
@@ -2294,9 +2773,10 @@ class AgentManager extends EventEmitter {
|
|
|
2294
2773
|
try {
|
|
2295
2774
|
if (useCodexAppServer) {
|
|
2296
2775
|
try {
|
|
2776
|
+
const appServer = runtimeBindingOf(agentRecord, 'app-server');
|
|
2297
2777
|
const prepared = await this.codexAppServerRuntime.prepareAgent({
|
|
2298
2778
|
agentId,
|
|
2299
|
-
codexHome:
|
|
2779
|
+
codexHome: appServer.homePath,
|
|
2300
2780
|
executable: spawnProgram,
|
|
2301
2781
|
env: this.buildAgentEnv(agentId, agentRecord),
|
|
2302
2782
|
cwd: workspace,
|
|
@@ -2308,19 +2788,13 @@ class AgentManager extends EventEmitter {
|
|
|
2308
2788
|
? this.configManager.getCodexApprovalMode()
|
|
2309
2789
|
: 'approve'
|
|
2310
2790
|
),
|
|
2311
|
-
model:
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
reasoningEffort: this.configManager && this.configManager.getCodexReasoningEffort
|
|
2315
|
-
? this.configManager.getCodexReasoningEffort()
|
|
2316
|
-
: 'xhigh',
|
|
2317
|
-
serviceTier: this.configManager && this.configManager.getCodexServiceTier
|
|
2318
|
-
? this.configManager.getCodexServiceTier()
|
|
2319
|
-
: 'default',
|
|
2791
|
+
model: codexModel,
|
|
2792
|
+
reasoningEffort: codexReasoningEffort,
|
|
2793
|
+
serviceTier: codexServiceTier,
|
|
2320
2794
|
developerInstructions: wantsMain ? renderMainAgentBootstrap() : '',
|
|
2321
2795
|
});
|
|
2322
2796
|
agentRecord.providerSessionId = prepared.threadId;
|
|
2323
|
-
agentRecord.providerSessionKey =
|
|
2797
|
+
agentRecord.providerSessionKey = mainPageAgentSessionKey(
|
|
2324
2798
|
'codex',
|
|
2325
2799
|
prepared.threadId,
|
|
2326
2800
|
agentRecord.providerHomeId || 'default'
|
|
@@ -2328,13 +2802,13 @@ class AgentManager extends EventEmitter {
|
|
|
2328
2802
|
agentRecord.providerSessionTemporary = false;
|
|
2329
2803
|
agentRecord.providerSessionSource = prepared.resumed ? 'app-server-resume' : 'app-server-start';
|
|
2330
2804
|
agentRecord.providerSessionResolvedAt = Date.now();
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2805
|
+
appServer.state = 'idle';
|
|
2806
|
+
appServer.endpoint = prepared.endpoint;
|
|
2807
|
+
appServer.threadId = prepared.threadId;
|
|
2808
|
+
appServer.error = '';
|
|
2335
2809
|
// App Server mode has no embedded CLI observer. Its chat and
|
|
2336
2810
|
// lifecycle both come from the structured App Server connection.
|
|
2337
|
-
|
|
2811
|
+
appServer.observerDeferred = false;
|
|
2338
2812
|
} catch (error) {
|
|
2339
2813
|
const reason = error && error.message ? error.message : String(error);
|
|
2340
2814
|
throw new Error(`Codex App Server failed to start: ${reason}`, { cause: error });
|
|
@@ -2342,52 +2816,51 @@ class AgentManager extends EventEmitter {
|
|
|
2342
2816
|
}
|
|
2343
2817
|
|
|
2344
2818
|
if (useJsonCli) {
|
|
2819
|
+
const jsonRuntime = runtimeBindingOf(agentRecord, 'json');
|
|
2345
2820
|
this.jsonCliRuntime.registerAgent({
|
|
2346
2821
|
agentId,
|
|
2347
|
-
provider:
|
|
2822
|
+
provider: structuredRuntimeProvider,
|
|
2348
2823
|
executable: spawnProgram,
|
|
2349
2824
|
env: this.buildAgentEnv(agentId, agentRecord),
|
|
2350
2825
|
cwd: workspace,
|
|
2351
2826
|
sessionId: agentRecord.providerSessionTemporary ? '' : agentRecord.providerSessionId,
|
|
2352
2827
|
approvalMode: agentRecord.launchPermissionMode || 'approve',
|
|
2353
2828
|
autoApprove: options.dangerouslySkipPermissions === true,
|
|
2354
|
-
initialEvents:
|
|
2829
|
+
initialEvents: jsonRuntime.events,
|
|
2355
2830
|
});
|
|
2356
2831
|
}
|
|
2357
2832
|
|
|
2358
2833
|
if (useAcp) {
|
|
2834
|
+
const acpRuntime = runtimeBindingOf(agentRecord, 'acp');
|
|
2359
2835
|
const prepared = await this.acpRuntime.prepareAgent({
|
|
2360
2836
|
agentId,
|
|
2361
|
-
provider:
|
|
2837
|
+
provider: structuredRuntimeProvider,
|
|
2362
2838
|
executable: spawnProgram,
|
|
2363
2839
|
env: this.buildAgentEnv(agentId, agentRecord),
|
|
2364
2840
|
cwd: workspace,
|
|
2365
|
-
sessionId: options.acpStartFresh === true || agentRecord.providerSessionTemporary
|
|
2841
|
+
sessionId: options.acpStartFresh === true || agentRecord.providerSessionTemporary || acpGeneratedFreshSession
|
|
2366
2842
|
? ''
|
|
2367
2843
|
: agentRecord.providerSessionId,
|
|
2368
|
-
historyMode: options.acpHistoryMode === 'resume'
|
|
2844
|
+
historyMode: options.acpHistoryMode === 'resume'
|
|
2845
|
+
? 'resume'
|
|
2846
|
+
: (options.acpHistoryMode === 'load' ? 'load' : 'checkpoint'),
|
|
2847
|
+
providerHomeId: agentRecord.providerHomeId || 'default',
|
|
2369
2848
|
approvalMode: agentRecord.launchPermissionMode || 'approve',
|
|
2370
|
-
model:
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
reasoningEffort: this.configManager && this.configManager.getCodexReasoningEffort
|
|
2374
|
-
? this.configManager.getCodexReasoningEffort()
|
|
2375
|
-
: '',
|
|
2376
|
-
serviceTier: this.configManager && this.configManager.getCodexServiceTier
|
|
2377
|
-
? this.configManager.getCodexServiceTier()
|
|
2378
|
-
: '',
|
|
2849
|
+
model: codexModel,
|
|
2850
|
+
reasoningEffort: codexReasoningEffort,
|
|
2851
|
+
serviceTier: codexServiceTier,
|
|
2379
2852
|
});
|
|
2380
2853
|
agentRecord.providerSessionId = prepared.sessionId;
|
|
2381
|
-
agentRecord.providerSessionKey =
|
|
2382
|
-
|
|
2854
|
+
agentRecord.providerSessionKey = mainPageAgentSessionKey(
|
|
2855
|
+
structuredRuntimeProvider,
|
|
2383
2856
|
prepared.sessionId,
|
|
2384
2857
|
agentRecord.providerHomeId || 'default'
|
|
2385
2858
|
);
|
|
2386
2859
|
agentRecord.providerSessionTemporary = false;
|
|
2387
2860
|
agentRecord.providerSessionSource = `acp-${prepared.historyMode}`;
|
|
2388
2861
|
agentRecord.providerSessionResolvedAt = Date.now();
|
|
2389
|
-
|
|
2390
|
-
|
|
2862
|
+
acpRuntime.state = 'idle';
|
|
2863
|
+
acpRuntime.error = '';
|
|
2391
2864
|
}
|
|
2392
2865
|
|
|
2393
2866
|
agentRecord.persistentSessionId = this.ensurePersistentAgentSession(agentRecord);
|
|
@@ -2397,6 +2870,7 @@ class AgentManager extends EventEmitter {
|
|
|
2397
2870
|
args,
|
|
2398
2871
|
cwd: workspace,
|
|
2399
2872
|
category: resolution.spec ? resolution.spec.category : 'shell',
|
|
2873
|
+
reviveState: options.reviveTerminalState || null,
|
|
2400
2874
|
};
|
|
2401
2875
|
await this.createAgentEngineSession(agentRecord, resolution.engine, engineLaunch);
|
|
2402
2876
|
}
|
|
@@ -2412,18 +2886,33 @@ class AgentManager extends EventEmitter {
|
|
|
2412
2886
|
}
|
|
2413
2887
|
}
|
|
2414
2888
|
|
|
2415
|
-
this.
|
|
2889
|
+
this.providerSessionService.activate(agentId);
|
|
2416
2890
|
if (callback) callback(agentId);
|
|
2417
2891
|
this.emit('update');
|
|
2418
2892
|
return agentId;
|
|
2419
2893
|
} catch (error) {
|
|
2420
2894
|
console.error('Failed to start agent:', error);
|
|
2895
|
+
if (options.restoreRuntimeAgentIdOnFailure && agentRecord.persistentSessionId) {
|
|
2896
|
+
const failedAgentId = agentRecord.id;
|
|
2897
|
+
try {
|
|
2898
|
+
agentRecord.id = options.restoreRuntimeAgentIdOnFailure;
|
|
2899
|
+
this.ensurePersistentAgentSession(agentRecord);
|
|
2900
|
+
} catch (rollbackError) {
|
|
2901
|
+
console.error(
|
|
2902
|
+
'Failed to roll back persisted Agent runtime id after restart failure:',
|
|
2903
|
+
rollbackError && (rollbackError.message || rollbackError)
|
|
2904
|
+
);
|
|
2905
|
+
} finally {
|
|
2906
|
+
agentRecord.id = failedAgentId;
|
|
2907
|
+
}
|
|
2908
|
+
}
|
|
2421
2909
|
this.agents.delete(agentId);
|
|
2422
2910
|
this.lastActivity.delete(agentId);
|
|
2423
2911
|
this.lastActivityUpdate.delete(agentId);
|
|
2424
2912
|
this.outputEvents.delete(agentId);
|
|
2913
|
+
this.agentUsageRateCache.delete(agentId);
|
|
2425
2914
|
this.lastResizeByAgent.delete(agentId);
|
|
2426
|
-
this.
|
|
2915
|
+
this.providerSessionService.stop(agentId);
|
|
2427
2916
|
if (this.codexAppServerRuntime && typeof this.codexAppServerRuntime.unregisterAgent === 'function') {
|
|
2428
2917
|
this.codexAppServerRuntime.unregisterAgent(agentId);
|
|
2429
2918
|
}
|
|
@@ -2439,15 +2928,15 @@ class AgentManager extends EventEmitter {
|
|
|
2439
2928
|
}
|
|
2440
2929
|
}
|
|
2441
2930
|
|
|
2442
|
-
async
|
|
2931
|
+
async enqueueInputOperation(agentId, operation) {
|
|
2443
2932
|
const previous = this.inputQueues.get(agentId) || Promise.resolve();
|
|
2444
2933
|
const next = previous
|
|
2445
2934
|
.catch(() => {})
|
|
2446
|
-
.then(
|
|
2935
|
+
.then(operation);
|
|
2447
2936
|
|
|
2448
2937
|
this.inputQueues.set(agentId, next);
|
|
2449
2938
|
try {
|
|
2450
|
-
await next;
|
|
2939
|
+
return await next;
|
|
2451
2940
|
} finally {
|
|
2452
2941
|
if (this.inputQueues.get(agentId) === next) {
|
|
2453
2942
|
this.inputQueues.delete(agentId);
|
|
@@ -2455,20 +2944,52 @@ class AgentManager extends EventEmitter {
|
|
|
2455
2944
|
}
|
|
2456
2945
|
}
|
|
2457
2946
|
|
|
2947
|
+
async enqueueInputOperationUntilReleased(agentId, operation) {
|
|
2948
|
+
const previous = this.inputQueues.get(agentId) || Promise.resolve();
|
|
2949
|
+
let released = false;
|
|
2950
|
+
let resolveReleased;
|
|
2951
|
+
const releasedPromise = new Promise(resolve => {
|
|
2952
|
+
resolveReleased = resolve;
|
|
2953
|
+
});
|
|
2954
|
+
const release = () => {
|
|
2955
|
+
if (released) return;
|
|
2956
|
+
released = true;
|
|
2957
|
+
resolveReleased();
|
|
2958
|
+
};
|
|
2959
|
+
|
|
2960
|
+
const ready = previous.catch(() => {});
|
|
2961
|
+
const completion = ready.then(() => operation(release));
|
|
2962
|
+
completion.catch(() => release());
|
|
2963
|
+
const boundary = ready.then(() => releasedPromise);
|
|
2964
|
+
this.inputQueues.set(agentId, boundary);
|
|
2965
|
+
boundary.then(() => {
|
|
2966
|
+
if (this.inputQueues.get(agentId) === boundary) {
|
|
2967
|
+
this.inputQueues.delete(agentId);
|
|
2968
|
+
}
|
|
2969
|
+
});
|
|
2970
|
+
return completion;
|
|
2971
|
+
}
|
|
2972
|
+
|
|
2973
|
+
async sendInput(agentId, input, options = {}) {
|
|
2974
|
+
return this.enqueueInputOperation(agentId, () => this.sendInputNow(agentId, input, options));
|
|
2975
|
+
}
|
|
2976
|
+
|
|
2458
2977
|
codexAppServerOptionsForAgent(agent, message = '') {
|
|
2459
2978
|
const codexResolution = resolveCompatibleCodexExecutable(agent.requiredCliVersion || '');
|
|
2460
2979
|
if (!codexResolution.compatible) {
|
|
2461
2980
|
throw new Error(codexResolution.error || 'Codex CLI is not compatible with App Server mode');
|
|
2462
2981
|
}
|
|
2463
2982
|
|
|
2983
|
+
const runtime = runtimeBindingOf(agent, 'app-server');
|
|
2984
|
+
if (!runtime) throw new Error('Agent is not using the Codex App Server runtime');
|
|
2464
2985
|
return {
|
|
2465
2986
|
agentId: agent.id,
|
|
2466
|
-
codexHome:
|
|
2987
|
+
codexHome: runtime.homePath,
|
|
2467
2988
|
executable: codexResolution.path || resolveAgentExecutable(agent.command) || agent.command,
|
|
2468
2989
|
env: this.buildAgentEnv(agent.id, agent),
|
|
2469
2990
|
cwd: agent.cwd,
|
|
2470
|
-
workspaceRoot: agent
|
|
2471
|
-
threadId:
|
|
2991
|
+
workspaceRoot: effectiveAgentWorkspaceRoot(agent),
|
|
2992
|
+
threadId: runtime.threadId || agent.providerSessionId || '',
|
|
2472
2993
|
approvalMode: agent.launchPermissionMode || (
|
|
2473
2994
|
this.configManager && this.configManager.getCodexApprovalMode
|
|
2474
2995
|
? this.configManager.getCodexApprovalMode()
|
|
@@ -2488,21 +3009,101 @@ class AgentManager extends EventEmitter {
|
|
|
2488
3009
|
}
|
|
2489
3010
|
|
|
2490
3011
|
async sendComposerMessage(agentId, message) {
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
.catch(() => {})
|
|
2494
|
-
.then(() => this.sendComposerMessageNow(agentId, message));
|
|
3012
|
+
return this.enqueueInputOperation(agentId, () => this.sendComposerMessageNow(agentId, message));
|
|
3013
|
+
}
|
|
2495
3014
|
|
|
2496
|
-
|
|
3015
|
+
async setCodexTerminalProfile(agentId, profile, options = {}) {
|
|
3016
|
+
const previous = this.codexTerminalProfileQueues.get(agentId) || Promise.resolve();
|
|
3017
|
+
const start = () => this.enqueueInputOperationUntilReleased(
|
|
3018
|
+
agentId,
|
|
3019
|
+
releaseInput => this.setCodexTerminalProfileNow(agentId, profile, {
|
|
3020
|
+
...options,
|
|
3021
|
+
onInputSafe: releaseInput,
|
|
3022
|
+
}),
|
|
3023
|
+
);
|
|
3024
|
+
const next = this.codexTerminalProfileQueues.has(agentId)
|
|
3025
|
+
? previous.catch(() => {}).then(start)
|
|
3026
|
+
: start();
|
|
3027
|
+
this.codexTerminalProfileQueues.set(agentId, next);
|
|
2497
3028
|
try {
|
|
2498
3029
|
return await next;
|
|
2499
3030
|
} finally {
|
|
2500
|
-
if (this.
|
|
2501
|
-
this.
|
|
3031
|
+
if (this.codexTerminalProfileQueues.get(agentId) === next) {
|
|
3032
|
+
this.codexTerminalProfileQueues.delete(agentId);
|
|
2502
3033
|
}
|
|
2503
3034
|
}
|
|
2504
3035
|
}
|
|
2505
3036
|
|
|
3037
|
+
async setCodexTerminalProfileNow(agentId, profile, options = {}) {
|
|
3038
|
+
const agent = this.agents.get(agentId);
|
|
3039
|
+
if (!agent) throw new Error('Agent not found');
|
|
3040
|
+
if (
|
|
3041
|
+
agentProgramName(agent.command).toLowerCase() !== 'codex'
|
|
3042
|
+
|| isCodexAppServerAgent(agent)
|
|
3043
|
+
|| isJsonCliAgent(agent)
|
|
3044
|
+
|| isAcpAgent(agent)
|
|
3045
|
+
|| runtimeKind(agent) !== 'terminal'
|
|
3046
|
+
) {
|
|
3047
|
+
throw new Error('This Agent is not using Codex Terminal');
|
|
3048
|
+
}
|
|
3049
|
+
if (!isRunningAgentRuntimeStatus(agent.status)) {
|
|
3050
|
+
throw new Error('Codex Terminal is not running');
|
|
3051
|
+
}
|
|
3052
|
+
if (this.isAgentAttentionTurnActive(agent)) {
|
|
3053
|
+
throw new Error('Wait for the active Codex Terminal turn to finish before changing its model');
|
|
3054
|
+
}
|
|
3055
|
+
|
|
3056
|
+
const applied = await applyCodexTerminalProfile({
|
|
3057
|
+
profile,
|
|
3058
|
+
timeoutMs: options.timeoutMs,
|
|
3059
|
+
signal: options.signal,
|
|
3060
|
+
onInputSafe: options.onInputSafe,
|
|
3061
|
+
readPreview: async () => {
|
|
3062
|
+
const view = await this.getAgentSessionView(agentId);
|
|
3063
|
+
if (!view) throw new Error('Agent not found');
|
|
3064
|
+
return view.previewText;
|
|
3065
|
+
},
|
|
3066
|
+
readOutput: async () => String(await this.getAgentSessionText(agentId) || ''),
|
|
3067
|
+
// `/model` and `/fast on|off` are Farming-owned control traffic. They must not
|
|
3068
|
+
// make a fresh Terminal look user-authored, because that would remove
|
|
3069
|
+
// the safe fresh-session path into ACP Chat before the provider has
|
|
3070
|
+
// materialized a resumable history record.
|
|
3071
|
+
sendInput: async input => this.sendInputNow(agentId, input, { markUserInput: false }),
|
|
3072
|
+
});
|
|
3073
|
+
agent.codexTerminalProfile = {
|
|
3074
|
+
model: applied.model,
|
|
3075
|
+
reasoningEffort: applied.effort,
|
|
3076
|
+
serviceTier: applied.serviceTier,
|
|
3077
|
+
source: 'terminal-command',
|
|
3078
|
+
};
|
|
3079
|
+
|
|
3080
|
+
// The HTTP response confirms the terminal has already reached this profile.
|
|
3081
|
+
// Publish that confirmation immediately instead of waiting for a later PTY
|
|
3082
|
+
// preview tick; otherwise the browser's bounded optimistic state can expire
|
|
3083
|
+
// and briefly fall back to the pre-command footer.
|
|
3084
|
+
const view = options.signal?.aborted
|
|
3085
|
+
? null
|
|
3086
|
+
: await this.getAgentSessionView(agentId);
|
|
3087
|
+
if (view) {
|
|
3088
|
+
agent.previewText = view.previewText || agent.previewText || '';
|
|
3089
|
+
agent.previewSnapshot = view.previewSnapshot || agent.previewSnapshot || null;
|
|
3090
|
+
agent.previewCols = view.previewCols || agent.previewCols || 80;
|
|
3091
|
+
agent.previewRows = view.previewRows || agent.previewRows || 30;
|
|
3092
|
+
this.emit('session-preview-update', {
|
|
3093
|
+
agentId,
|
|
3094
|
+
previewText: agent.previewText,
|
|
3095
|
+
cols: agent.previewCols,
|
|
3096
|
+
rows: agent.previewRows,
|
|
3097
|
+
previewSnapshot: agent.previewSnapshot,
|
|
3098
|
+
codexTerminalProfile: agent.codexTerminalProfile,
|
|
3099
|
+
terminalStatus: view.terminalStatus,
|
|
3100
|
+
runtimeObservation: deriveRuntimeObservation({ ...agent, terminalStatus: view.terminalStatus }),
|
|
3101
|
+
});
|
|
3102
|
+
}
|
|
3103
|
+
this.emit('update');
|
|
3104
|
+
return applied;
|
|
3105
|
+
}
|
|
3106
|
+
|
|
2506
3107
|
async sendComposerMessageNow(agentId, message) {
|
|
2507
3108
|
const agent = this.agents.get(agentId);
|
|
2508
3109
|
if (!agent) throw new Error('Agent not found');
|
|
@@ -2516,19 +3117,20 @@ class AgentManager extends EventEmitter {
|
|
|
2516
3117
|
throw new Error('Composer message is empty');
|
|
2517
3118
|
}
|
|
2518
3119
|
|
|
3120
|
+
const appServer = runtimeBindingOf(agent, 'app-server');
|
|
2519
3121
|
if (
|
|
2520
3122
|
agent.providerSessionProvider === 'codex'
|
|
2521
|
-
&&
|
|
3123
|
+
&& appServer
|
|
2522
3124
|
&& this.codexAppServerRuntime
|
|
2523
3125
|
&& typeof this.codexAppServerRuntime.submitComposerMessage === 'function'
|
|
2524
3126
|
) {
|
|
2525
3127
|
const result = await this.codexAppServerRuntime.submitComposerMessage(
|
|
2526
3128
|
this.codexAppServerOptionsForAgent(agent, text)
|
|
2527
3129
|
);
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
3130
|
+
appServer.threadId = result.threadId;
|
|
3131
|
+
appServer.turnId = result.turnId;
|
|
3132
|
+
appServer.state = 'working';
|
|
3133
|
+
appServer.error = '';
|
|
2532
3134
|
this.lastActivity.set(agentId, Date.now());
|
|
2533
3135
|
this.emit('update');
|
|
2534
3136
|
return result;
|
|
@@ -2546,8 +3148,9 @@ class AgentManager extends EventEmitter {
|
|
|
2546
3148
|
|
|
2547
3149
|
if (isAcpAgent(agent)) {
|
|
2548
3150
|
const result = await this.acpRuntime.prompt(agentId, prompt);
|
|
2549
|
-
|
|
2550
|
-
|
|
3151
|
+
const runtime = runtimeBindingOf(agent, 'acp');
|
|
3152
|
+
runtime.state = 'idle';
|
|
3153
|
+
runtime.stopReason = result.stopReason || '';
|
|
2551
3154
|
this.ensurePersistentAgentSession(agent);
|
|
2552
3155
|
return { kind: 'acp', ...result };
|
|
2553
3156
|
}
|
|
@@ -2559,11 +3162,12 @@ class AgentManager extends EventEmitter {
|
|
|
2559
3162
|
respondToCodexAppServerRequest(agentId, requestId, result, options = {}) {
|
|
2560
3163
|
const agent = this.agents.get(agentId);
|
|
2561
3164
|
if (!agent) throw new Error('Agent not found');
|
|
2562
|
-
|
|
3165
|
+
const runtime = runtimeBindingOf(agent, 'app-server');
|
|
3166
|
+
if (agent.providerSessionProvider !== 'codex' || !runtime) {
|
|
2563
3167
|
throw new Error('This Agent is not using the Codex App Server runtime');
|
|
2564
3168
|
}
|
|
2565
3169
|
const resolvedRequestId = String(requestId || '');
|
|
2566
|
-
if (!resolvedRequestId || resolvedRequestId !==
|
|
3170
|
+
if (!resolvedRequestId || resolvedRequestId !== runtime.pendingRequestId) {
|
|
2567
3171
|
throw new Error('Codex App Server request is no longer pending');
|
|
2568
3172
|
}
|
|
2569
3173
|
if (!this.codexAppServerRuntime) throw new Error('Codex App Server runtime is unavailable');
|
|
@@ -2575,10 +3179,10 @@ class AgentManager extends EventEmitter {
|
|
|
2575
3179
|
{ code: -32000, message: String(options.reason || 'Rejected by Farming') }
|
|
2576
3180
|
)
|
|
2577
3181
|
: this.codexAppServerRuntime.resolveAgentServerRequest(agentId, resolvedRequestId, result);
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
3182
|
+
runtime.pendingRequestId = '';
|
|
3183
|
+
runtime.pendingRequestMethod = '';
|
|
3184
|
+
runtime.pendingRequest = null;
|
|
3185
|
+
runtime.state = runtime.turnId ? 'working' : 'idle';
|
|
2582
3186
|
this.emit('update');
|
|
2583
3187
|
return response;
|
|
2584
3188
|
}
|
|
@@ -2586,11 +3190,12 @@ class AgentManager extends EventEmitter {
|
|
|
2586
3190
|
assertCodexAppServerGoalAgent(agentId) {
|
|
2587
3191
|
const agent = this.agents.get(agentId);
|
|
2588
3192
|
if (!agent) throw new Error('Agent not found');
|
|
2589
|
-
|
|
3193
|
+
const runtime = runtimeBindingOf(agent, 'app-server');
|
|
3194
|
+
if (agent.providerSessionProvider !== 'codex' || !runtime) {
|
|
2590
3195
|
throw new Error('This Agent is not using the Codex App Server runtime');
|
|
2591
3196
|
}
|
|
2592
3197
|
if (!this.codexAppServerRuntime) throw new Error('Codex App Server runtime is unavailable');
|
|
2593
|
-
if (!(
|
|
3198
|
+
if (!(runtime.threadId || agent.providerSessionId)) {
|
|
2594
3199
|
throw new Error('Codex App Server thread id is not available yet');
|
|
2595
3200
|
}
|
|
2596
3201
|
return agent;
|
|
@@ -2599,7 +3204,7 @@ class AgentManager extends EventEmitter {
|
|
|
2599
3204
|
async getCodexAppServerGoal(agentId) {
|
|
2600
3205
|
const agent = this.assertCodexAppServerGoalAgent(agentId);
|
|
2601
3206
|
const goal = await this.codexAppServerRuntime.getAgentGoal(this.codexAppServerOptionsForAgent(agent));
|
|
2602
|
-
agent.
|
|
3207
|
+
runtimeBindingOf(agent, 'app-server').goal = goal || null;
|
|
2603
3208
|
this.emit('update');
|
|
2604
3209
|
return goal || null;
|
|
2605
3210
|
}
|
|
@@ -2630,7 +3235,11 @@ class AgentManager extends EventEmitter {
|
|
|
2630
3235
|
const agent = this.agents.get(agentId);
|
|
2631
3236
|
if (!agent) throw new Error('Agent not found');
|
|
2632
3237
|
if (!isAcpAgent(agent)) throw new Error('Agent is not using the ACP runtime');
|
|
2633
|
-
|
|
3238
|
+
const transcript = this.acpRuntime.getTranscriptSession(agentId, options);
|
|
3239
|
+
return {
|
|
3240
|
+
...transcript,
|
|
3241
|
+
entries: transcript.entries.map(entry => entry?.type === 'tool' ? acpTranscriptToolEntry(entry) : entry),
|
|
3242
|
+
};
|
|
2634
3243
|
}
|
|
2635
3244
|
|
|
2636
3245
|
getAcpToolDetail(agentId, toolCallId) {
|
|
@@ -2771,7 +3380,7 @@ class AgentManager extends EventEmitter {
|
|
|
2771
3380
|
...(status !== undefined ? { status } : {}),
|
|
2772
3381
|
...(Object.prototype.hasOwnProperty.call(patch, 'tokenBudget') ? { tokenBudget } : {}),
|
|
2773
3382
|
});
|
|
2774
|
-
agent.
|
|
3383
|
+
runtimeBindingOf(agent, 'app-server').goal = goal || null;
|
|
2775
3384
|
this.emit('update');
|
|
2776
3385
|
return goal || null;
|
|
2777
3386
|
}
|
|
@@ -2779,43 +3388,43 @@ class AgentManager extends EventEmitter {
|
|
|
2779
3388
|
async clearCodexAppServerGoal(agentId) {
|
|
2780
3389
|
const agent = this.assertCodexAppServerGoalAgent(agentId);
|
|
2781
3390
|
await this.codexAppServerRuntime.clearAgentGoal(this.codexAppServerOptionsForAgent(agent));
|
|
2782
|
-
agent.
|
|
3391
|
+
runtimeBindingOf(agent, 'app-server').goal = null;
|
|
2783
3392
|
this.emit('update');
|
|
2784
3393
|
return null;
|
|
2785
3394
|
}
|
|
2786
3395
|
|
|
2787
|
-
async sendInputNow(agentId, input) {
|
|
3396
|
+
async sendInputNow(agentId, input, { markUserInput = true, expectedRuntimeEpoch = '' } = {}) {
|
|
2788
3397
|
const agent = this.agents.get(agentId);
|
|
2789
3398
|
if (!agent) return;
|
|
2790
3399
|
// The observer PTY is optional telemetry for App Server Agents. Its
|
|
2791
3400
|
// disappearance must never redefine the App Server Agent as dead.
|
|
2792
3401
|
if (isCodexAppServerAgent(agent) || isJsonCliAgent(agent) || isAcpAgent(agent)) return;
|
|
3402
|
+
if (expectedRuntimeEpoch && agent.runtimeEpoch !== expectedRuntimeEpoch) {
|
|
3403
|
+
return { status: 'input-rejected', reason: 'runtime-epoch-mismatch' };
|
|
3404
|
+
}
|
|
2793
3405
|
|
|
2794
3406
|
const engine = this.engineBridge.getEngine(agent.engineName);
|
|
2795
3407
|
if (!engine) return;
|
|
2796
3408
|
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
await engine.sendInput(agentId, input);
|
|
2806
|
-
this.observeAgentStateChange(agentId, { force: true });
|
|
2807
|
-
return;
|
|
2808
|
-
} catch (error) {
|
|
2809
|
-
const delay = INPUT_SESSION_RETRY_DELAYS_MS[attempt];
|
|
2810
|
-
if (!isSessionNotAvailableError(error) || delay === undefined) {
|
|
2811
|
-
console.error('Failed to send input:', error);
|
|
2812
|
-
if (isSessionNotAvailableError(error)) {
|
|
2813
|
-
this.markAgentSessionDead(agentId, error);
|
|
2814
|
-
}
|
|
2815
|
-
return;
|
|
2816
|
-
}
|
|
2817
|
-
await sleep(delay);
|
|
3409
|
+
try {
|
|
3410
|
+
const submittedUserInput = markUserInput && hasSubmittedTerminalInput(input);
|
|
3411
|
+
const result = await engine.sendInput(agentId, input, { expectedRuntimeEpoch });
|
|
3412
|
+
if (submittedUserInput && agent.terminalInputReceived !== true) {
|
|
3413
|
+
agent.terminalInputReceived = true;
|
|
3414
|
+
this.ensurePersistentAgentSession(agent);
|
|
3415
|
+
this.updateEngineProviderSessionMetadata(agent);
|
|
3416
|
+
this.emit('agent-update', { agentId, patch: { terminalInputReceived: true } });
|
|
2818
3417
|
}
|
|
3418
|
+
if (submittedUserInput) {
|
|
3419
|
+
this.providerSessionService.observe(agentId, { force: true });
|
|
3420
|
+
}
|
|
3421
|
+
return result;
|
|
3422
|
+
} catch (error) {
|
|
3423
|
+
console.error('Failed to send input:', error);
|
|
3424
|
+
if (isSessionNotAvailableError(error)) {
|
|
3425
|
+
this.markAgentSessionDead(agentId, error);
|
|
3426
|
+
}
|
|
3427
|
+
return;
|
|
2819
3428
|
}
|
|
2820
3429
|
}
|
|
2821
3430
|
|
|
@@ -2830,33 +3439,34 @@ class AgentManager extends EventEmitter {
|
|
|
2830
3439
|
agent.terminalBusy = false;
|
|
2831
3440
|
agent.exitedAt = Date.now();
|
|
2832
3441
|
agent.output = trimSessionOutput(`${agent.output || ''}\n${message}`);
|
|
2833
|
-
this.
|
|
3442
|
+
this.providerSessionService.observe(agentId, { force: true });
|
|
2834
3443
|
this.emit('update');
|
|
2835
3444
|
}
|
|
2836
3445
|
|
|
2837
|
-
async interruptAgent(agentId) {
|
|
3446
|
+
async interruptAgent(agentId, options = {}) {
|
|
2838
3447
|
const agent = this.agents.get(agentId);
|
|
2839
3448
|
if (!agent) return;
|
|
3449
|
+
const appServer = runtimeBindingOf(agent, 'app-server');
|
|
2840
3450
|
|
|
2841
3451
|
try {
|
|
2842
3452
|
if (
|
|
2843
3453
|
agent.providerSessionProvider === 'codex'
|
|
2844
|
-
&&
|
|
3454
|
+
&& appServer
|
|
2845
3455
|
&& this.codexAppServerRuntime
|
|
2846
3456
|
&& typeof this.codexAppServerRuntime.interruptAgent === 'function'
|
|
2847
3457
|
) {
|
|
2848
3458
|
try {
|
|
2849
3459
|
const interrupted = await this.codexAppServerRuntime.interruptAgent(agentId);
|
|
2850
3460
|
if (interrupted) {
|
|
2851
|
-
|
|
3461
|
+
appServer.state = 'interrupting';
|
|
2852
3462
|
this.emit('update');
|
|
2853
3463
|
return;
|
|
2854
3464
|
}
|
|
2855
|
-
|
|
3465
|
+
appServer.error = 'Codex App Server interrupt is not available';
|
|
2856
3466
|
this.emit('update');
|
|
2857
3467
|
return;
|
|
2858
3468
|
} catch (error) {
|
|
2859
|
-
|
|
3469
|
+
appServer.error = `Codex App Server interrupt failed: ${error && (error.message || error)}`;
|
|
2860
3470
|
this.emit('update');
|
|
2861
3471
|
return;
|
|
2862
3472
|
}
|
|
@@ -2874,9 +3484,9 @@ class AgentManager extends EventEmitter {
|
|
|
2874
3484
|
|
|
2875
3485
|
const input = interruptInputForAgent(agent);
|
|
2876
3486
|
if (engine.interruptSession) {
|
|
2877
|
-
await engine.interruptSession(agentId, input);
|
|
3487
|
+
return await engine.interruptSession(agentId, input, options);
|
|
2878
3488
|
} else {
|
|
2879
|
-
await engine.sendInput(agentId, input);
|
|
3489
|
+
return await engine.sendInput(agentId, input, options);
|
|
2880
3490
|
}
|
|
2881
3491
|
} catch (error) {
|
|
2882
3492
|
console.error('Failed to interrupt agent:', error);
|
|
@@ -2886,10 +3496,48 @@ class AgentManager extends EventEmitter {
|
|
|
2886
3496
|
}
|
|
2887
3497
|
}
|
|
2888
3498
|
|
|
3499
|
+
agentSupportsTerminalInput(agentId) {
|
|
3500
|
+
const agent = this.agents.get(agentId);
|
|
3501
|
+
if (!agent) return false;
|
|
3502
|
+
return !isCodexAppServerAgent(agent) && !isJsonCliAgent(agent) && !isAcpAgent(agent);
|
|
3503
|
+
}
|
|
3504
|
+
|
|
3505
|
+
async getAgentSessionAttachCheckpoint(agentId) {
|
|
3506
|
+
const agent = this.agents.get(agentId);
|
|
3507
|
+
if (!agent || isCodexAppServerAgent(agent) || isAcpAgent(agent) || isJsonCliAgent(agent)) {
|
|
3508
|
+
return null;
|
|
3509
|
+
}
|
|
3510
|
+
try {
|
|
3511
|
+
return await this.engineBridge.getSessionAttachCheckpoint(agent.engineName, agentId);
|
|
3512
|
+
} catch (error) {
|
|
3513
|
+
console.error('Failed to read agent terminal attach checkpoint:', error);
|
|
3514
|
+
return null;
|
|
3515
|
+
}
|
|
3516
|
+
}
|
|
3517
|
+
|
|
3518
|
+
requestAgentSessionResize(agentId, cols, rows) {
|
|
3519
|
+
this.pendingResizeByAgent.set(agentId, { cols, rows });
|
|
3520
|
+
if (this.resizeDrains.has(agentId)) return true;
|
|
3521
|
+
|
|
3522
|
+
const drain = (async () => {
|
|
3523
|
+
while (this.pendingResizeByAgent.has(agentId)) {
|
|
3524
|
+
const next = this.pendingResizeByAgent.get(agentId);
|
|
3525
|
+
this.pendingResizeByAgent.delete(agentId);
|
|
3526
|
+
await this.resizeAgentSession(agentId, next.cols, next.rows);
|
|
3527
|
+
}
|
|
3528
|
+
})().finally(() => {
|
|
3529
|
+
this.resizeDrains.delete(agentId);
|
|
3530
|
+
});
|
|
3531
|
+
this.resizeDrains.set(agentId, drain);
|
|
3532
|
+
return true;
|
|
3533
|
+
}
|
|
3534
|
+
|
|
2889
3535
|
async resizeAgentSession(agentId, cols, rows) {
|
|
2890
3536
|
const agent = this.agents.get(agentId);
|
|
2891
|
-
if (!agent) return;
|
|
2892
|
-
if (isCodexAppServerAgent(agent) || isAcpAgent(agent) || isJsonCliAgent(agent))
|
|
3537
|
+
if (!agent) return { status: 'resize-rejected', reason: 'session-unavailable', resized: false };
|
|
3538
|
+
if (isCodexAppServerAgent(agent) || isAcpAgent(agent) || isJsonCliAgent(agent)) {
|
|
3539
|
+
return { status: 'resize-rejected', reason: 'unsupported-session', resized: false };
|
|
3540
|
+
}
|
|
2893
3541
|
|
|
2894
3542
|
const nextCols = Math.floor(Number(cols));
|
|
2895
3543
|
const nextRows = Math.floor(Number(rows));
|
|
@@ -2899,49 +3547,50 @@ class AgentManager extends EventEmitter {
|
|
|
2899
3547
|
nextCols < MIN_TERMINAL_RESIZE_COLS ||
|
|
2900
3548
|
nextRows < MIN_TERMINAL_RESIZE_ROWS
|
|
2901
3549
|
) {
|
|
2902
|
-
return;
|
|
3550
|
+
return { status: 'resize-rejected', reason: 'invalid-dimensions', resized: false };
|
|
2903
3551
|
}
|
|
2904
3552
|
|
|
2905
3553
|
try {
|
|
2906
|
-
const previousSize = this.lastResizeByAgent.get(agentId);
|
|
2907
|
-
if (previousSize && previousSize.cols === nextCols && previousSize.rows === nextRows) {
|
|
2908
|
-
return;
|
|
2909
|
-
}
|
|
2910
|
-
|
|
2911
3554
|
const engine = this.engineBridge.getEngine(agent.engineName);
|
|
2912
|
-
if (!engine || !engine.resizeSession)
|
|
3555
|
+
if (!engine || !engine.resizeSession) {
|
|
3556
|
+
return { status: 'resize-rejected', reason: 'unsupported-engine', resized: false };
|
|
3557
|
+
}
|
|
2913
3558
|
|
|
2914
3559
|
const result = await engine.resizeSession(agentId, nextCols, nextRows);
|
|
2915
|
-
if (result && result.resized === false) {
|
|
3560
|
+
if (result && result.resized === false && result.reason === 'session-unavailable') {
|
|
2916
3561
|
this.markAgentSessionDead(agentId, 'Session not available');
|
|
2917
|
-
return;
|
|
2918
3562
|
}
|
|
2919
|
-
|
|
3563
|
+
if (result && result.status === 'resize-committed') {
|
|
3564
|
+
this.lastResizeByAgent.set(agentId, { cols: nextCols, rows: nextRows });
|
|
3565
|
+
}
|
|
3566
|
+
return result;
|
|
2920
3567
|
} catch (error) {
|
|
2921
3568
|
console.error('Failed to resize agent session:', error);
|
|
3569
|
+
return { status: 'resize-rejected', reason: 'resize-failed', resized: false };
|
|
2922
3570
|
}
|
|
2923
3571
|
}
|
|
2924
3572
|
|
|
2925
|
-
async clearAgentSessionBuffer(agentId) {
|
|
3573
|
+
async clearAgentSessionBuffer(agentId, options = {}) {
|
|
2926
3574
|
const agent = this.agents.get(agentId);
|
|
2927
3575
|
if (!agent) return { cleared: false };
|
|
2928
3576
|
if (isCodexAppServerAgent(agent) || isAcpAgent(agent) || isJsonCliAgent(agent)) return { cleared: false };
|
|
3577
|
+
if (options.expectedRuntimeEpoch && agent.runtimeEpoch !== options.expectedRuntimeEpoch) {
|
|
3578
|
+
return { cleared: false, reason: 'runtime-epoch-mismatch' };
|
|
3579
|
+
}
|
|
2929
3580
|
|
|
2930
3581
|
try {
|
|
2931
3582
|
const engine = this.engineBridge.getEngine(agent.engineName);
|
|
2932
3583
|
if (!engine || !engine.clearBuffer) return { cleared: false };
|
|
2933
|
-
const result = await engine.clearBuffer(agentId);
|
|
3584
|
+
const result = await engine.clearBuffer(agentId, options);
|
|
2934
3585
|
if (result && result.cleared === false) {
|
|
2935
|
-
|
|
3586
|
+
if (result.reason === 'session-unavailable') {
|
|
3587
|
+
this.markAgentSessionDead(agentId, 'Session not available');
|
|
3588
|
+
}
|
|
2936
3589
|
return result;
|
|
2937
3590
|
}
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
this.outputEvents.delete(agentId);
|
|
2942
|
-
this.lastActivity.set(agentId, Date.now());
|
|
2943
|
-
this.observeAgentStateChange(agentId, { force: true });
|
|
2944
|
-
this.emit('update');
|
|
3591
|
+
// The ordered clear transition is the single metadata writer. Output
|
|
3592
|
+
// committed immediately after clear must not be erased by this RPC
|
|
3593
|
+
// response path racing the transition stream.
|
|
2945
3594
|
return result || { cleared: true };
|
|
2946
3595
|
} catch (error) {
|
|
2947
3596
|
console.error('Failed to clear agent session buffer:', error);
|
|
@@ -2983,9 +3632,12 @@ class AgentManager extends EventEmitter {
|
|
|
2983
3632
|
}
|
|
2984
3633
|
|
|
2985
3634
|
const updates = {};
|
|
3635
|
+
let structuralUpdateChanged = false;
|
|
3636
|
+
let readUpdateChanged = false;
|
|
2986
3637
|
if (typeof flags.pinned === 'boolean') {
|
|
2987
3638
|
const wasPinned = agent.pinned === true;
|
|
2988
3639
|
agent.pinned = flags.pinned;
|
|
3640
|
+
structuralUpdateChanged = structuralUpdateChanged || wasPinned !== agent.pinned;
|
|
2989
3641
|
updates.pinned = agent.pinned;
|
|
2990
3642
|
if (!wasPinned && agent.pinned) {
|
|
2991
3643
|
agent.pinnedOrder = nextPinnedOrder(Array.from(this.agents.values()));
|
|
@@ -2993,22 +3645,36 @@ class AgentManager extends EventEmitter {
|
|
|
2993
3645
|
updates.pinnedOrder = finiteOrder(agent.pinnedOrder);
|
|
2994
3646
|
}
|
|
2995
3647
|
|
|
3648
|
+
if (
|
|
3649
|
+
typeof flags.readOutputEpoch === 'string'
|
|
3650
|
+
&& typeof flags.readOutputSeq === 'number'
|
|
3651
|
+
&& Number.isFinite(flags.readOutputSeq)
|
|
3652
|
+
) {
|
|
3653
|
+
const result = this.markAgentReadOutputCut(agentId, flags.readOutputEpoch, flags.readOutputSeq);
|
|
3654
|
+
if (result.error) return result;
|
|
3655
|
+
updates.readOutputEpoch = result.readOutputEpoch;
|
|
3656
|
+
updates.readOutputSeq = result.readOutputSeq;
|
|
3657
|
+
readUpdateChanged = readUpdateChanged || result.changed;
|
|
3658
|
+
}
|
|
3659
|
+
|
|
2996
3660
|
if (typeof flags.unread === 'boolean') {
|
|
2997
3661
|
const result = flags.unread
|
|
2998
3662
|
? this.markAgentUnreadCursor(agentId)
|
|
2999
|
-
: this.markAgentReadCursor(agentId);
|
|
3663
|
+
: this.markAgentReadCursor(agentId, undefined, { emitUpdate: false });
|
|
3000
3664
|
if (result.error) return result;
|
|
3001
3665
|
updates.unread = result.unread;
|
|
3002
3666
|
updates.attentionSeq = result.attentionSeq;
|
|
3003
3667
|
updates.readAttentionSeq = result.readAttentionSeq;
|
|
3668
|
+
readUpdateChanged = readUpdateChanged || result.changed === true;
|
|
3004
3669
|
}
|
|
3005
3670
|
|
|
3006
3671
|
if (typeof flags.readAttentionSeq === 'number' && Number.isFinite(flags.readAttentionSeq)) {
|
|
3007
|
-
const result = this.markAgentReadCursor(agentId, flags.readAttentionSeq);
|
|
3672
|
+
const result = this.markAgentReadCursor(agentId, flags.readAttentionSeq, { emitUpdate: false });
|
|
3008
3673
|
if (result.error) return result;
|
|
3009
3674
|
updates.unread = result.unread;
|
|
3010
3675
|
updates.attentionSeq = result.attentionSeq;
|
|
3011
3676
|
updates.readAttentionSeq = result.readAttentionSeq;
|
|
3677
|
+
readUpdateChanged = readUpdateChanged || result.changed === true;
|
|
3012
3678
|
}
|
|
3013
3679
|
|
|
3014
3680
|
if (flags.archived === true) {
|
|
@@ -3019,18 +3685,38 @@ class AgentManager extends EventEmitter {
|
|
|
3019
3685
|
}
|
|
3020
3686
|
|
|
3021
3687
|
if (flags.archived === false) {
|
|
3688
|
+
structuralUpdateChanged = structuralUpdateChanged || agent.archived === true || agent.archivedAt !== null;
|
|
3022
3689
|
agent.archived = false;
|
|
3023
3690
|
agent.archivedAt = null;
|
|
3024
3691
|
updates.archived = agent.archived;
|
|
3025
3692
|
updates.archivedAt = agent.archivedAt;
|
|
3026
3693
|
}
|
|
3027
3694
|
|
|
3028
|
-
if (
|
|
3695
|
+
if (
|
|
3696
|
+
typeof flags.pinned === 'boolean'
|
|
3697
|
+
|| (typeof flags.readOutputEpoch === 'string' && typeof flags.readOutputSeq === 'number')
|
|
3698
|
+
) {
|
|
3029
3699
|
this.ensurePersistentAgentSession(agent);
|
|
3030
3700
|
this.updateEngineProviderSessionMetadata(agent);
|
|
3031
3701
|
}
|
|
3032
|
-
|
|
3033
|
-
|
|
3702
|
+
if (structuralUpdateChanged) {
|
|
3703
|
+
this.emit('update');
|
|
3704
|
+
} else if (readUpdateChanged) {
|
|
3705
|
+
this.emit('agent-read', {
|
|
3706
|
+
agentId,
|
|
3707
|
+
unread: agent.unread === true,
|
|
3708
|
+
attentionSeq: finiteNonNegativeInteger(agent.attentionSeq),
|
|
3709
|
+
readAttentionSeq: finiteNonNegativeInteger(agent.readAttentionSeq),
|
|
3710
|
+
readOutputEpoch: typeof agent.readOutputEpoch === 'string' ? agent.readOutputEpoch : '',
|
|
3711
|
+
readOutputSeq: finiteNumberOrNull(agent.readOutputSeq),
|
|
3712
|
+
});
|
|
3713
|
+
}
|
|
3714
|
+
return {
|
|
3715
|
+
agentId,
|
|
3716
|
+
...updates,
|
|
3717
|
+
changed: structuralUpdateChanged || readUpdateChanged,
|
|
3718
|
+
requiresState: structuralUpdateChanged,
|
|
3719
|
+
};
|
|
3034
3720
|
}
|
|
3035
3721
|
|
|
3036
3722
|
reorderProjectAgent(agentId, { beforeAgentId = '', afterAgentId = '' } = {}) {
|
|
@@ -3091,7 +3777,7 @@ class AgentManager extends EventEmitter {
|
|
|
3091
3777
|
|
|
3092
3778
|
if (
|
|
3093
3779
|
agent.providerSessionProvider === 'codex'
|
|
3094
|
-
&& agent
|
|
3780
|
+
&& runtimeKind(agent) === 'app-server'
|
|
3095
3781
|
) {
|
|
3096
3782
|
return this.updateCodexAppServerPermissionMode(agentId, mode);
|
|
3097
3783
|
}
|
|
@@ -3102,7 +3788,8 @@ class AgentManager extends EventEmitter {
|
|
|
3102
3788
|
async updateCodexAppServerPermissionMode(agentId, mode) {
|
|
3103
3789
|
const agent = this.agents.get(agentId);
|
|
3104
3790
|
if (!agent) return { error: 'Agent not found' };
|
|
3105
|
-
|
|
3791
|
+
const runtime = runtimeBindingOf(agent, 'app-server');
|
|
3792
|
+
if (agent.providerSessionProvider !== 'codex' || !runtime) {
|
|
3106
3793
|
return { error: 'Agent is not using the Codex App Server runtime' };
|
|
3107
3794
|
}
|
|
3108
3795
|
|
|
@@ -3122,7 +3809,7 @@ class AgentManager extends EventEmitter {
|
|
|
3122
3809
|
}
|
|
3123
3810
|
|
|
3124
3811
|
agent.launchPermissionMode = nextMode;
|
|
3125
|
-
|
|
3812
|
+
runtime.error = '';
|
|
3126
3813
|
this.updateEngineProviderSessionMetadata(agent);
|
|
3127
3814
|
this.ensurePersistentAgentSession(agent, { launchPermissionMode: nextMode });
|
|
3128
3815
|
this.emit('update');
|
|
@@ -3178,29 +3865,30 @@ class AgentManager extends EventEmitter {
|
|
|
3178
3865
|
const nextMode = ['terminal', 'json', 'acp'].includes(mode) ? mode : '';
|
|
3179
3866
|
if (!nextMode) return { error: 'Unsupported Agent runtime mode' };
|
|
3180
3867
|
const provider = agent.providerSessionProvider || '';
|
|
3868
|
+
const currentKind = runtimeKind(agent);
|
|
3869
|
+
const currentMode = ['acp', 'json'].includes(currentKind) ? currentKind : 'terminal';
|
|
3181
3870
|
const leavesCodexAppServer = provider === 'codex'
|
|
3182
|
-
&&
|
|
3871
|
+
&& currentKind === 'app-server'
|
|
3183
3872
|
&& nextMode === 'terminal';
|
|
3184
|
-
if (
|
|
3873
|
+
if (currentMode === nextMode && !leavesCodexAppServer) {
|
|
3185
3874
|
return { agentId, agentRuntimeMode: nextMode };
|
|
3186
3875
|
}
|
|
3187
|
-
const turnActive =
|
|
3188
|
-
? ['working', 'waiting-for-permission', 'interrupting'].includes(agent
|
|
3876
|
+
const turnActive = currentKind === 'acp'
|
|
3877
|
+
? ['working', 'waiting-for-permission', 'interrupting'].includes(runtimeBindingOf(agent, 'acp').state)
|
|
3189
3878
|
: this.isAgentAttentionTurnActive(agent);
|
|
3190
3879
|
if (turnActive) {
|
|
3191
3880
|
return { error: 'Interrupt the active Agent turn before switching Chat and Terminal.' };
|
|
3192
3881
|
}
|
|
3193
|
-
|
|
3194
|
-
if (!supportedProviders.includes(provider)) {
|
|
3882
|
+
if (!providerSupportsRuntime(provider, nextMode)) {
|
|
3195
3883
|
return { error: `Agent does not support the ${nextMode.toUpperCase()} runtime` };
|
|
3196
3884
|
}
|
|
3197
3885
|
const sessionId = String(agent.providerSessionId || '').trim();
|
|
3198
3886
|
const canStartFreshAcpSession = nextMode === 'acp'
|
|
3199
|
-
&&
|
|
3887
|
+
&& currentMode === 'terminal'
|
|
3200
3888
|
&& agent.terminalInputReceived !== true
|
|
3201
3889
|
&& (
|
|
3202
3890
|
agent.providerSessionTemporary === true
|
|
3203
|
-
||
|
|
3891
|
+
|| isFreshAcpSessionSource(provider, agent.providerSessionSource || '')
|
|
3204
3892
|
);
|
|
3205
3893
|
if (!isSafeProviderSessionId(sessionId) && !canStartFreshAcpSession) {
|
|
3206
3894
|
return { error: 'Runtime switching requires a resumable provider session. Send the first message and try again.' };
|
|
@@ -3212,7 +3900,7 @@ class AgentManager extends EventEmitter {
|
|
|
3212
3900
|
// fail. Historical/terminal sessions still use the provider lookup as a
|
|
3213
3901
|
// stale-session guard.
|
|
3214
3902
|
let liveAcpSession = false;
|
|
3215
|
-
if (
|
|
3903
|
+
if (currentKind === 'acp' && this.acpRuntime && typeof this.acpRuntime.getSession === 'function') {
|
|
3216
3904
|
try {
|
|
3217
3905
|
const snapshot = this.acpRuntime.getSession(agentId, { maxEntries: 0 });
|
|
3218
3906
|
liveAcpSession = String(snapshot?.sessionId || '') === sessionId;
|
|
@@ -3232,7 +3920,8 @@ class AgentManager extends EventEmitter {
|
|
|
3232
3920
|
const command = startsFreshAcpSession
|
|
3233
3921
|
? (agent.forkCommand || agent.command)
|
|
3234
3922
|
: buildAgentSessionResumeCommand(provider, sessionId, {
|
|
3235
|
-
cwd: agent
|
|
3923
|
+
cwd: effectiveAgentWorkspaceRoot(agent),
|
|
3924
|
+
providerHomePath: agent.providerHomePath || '',
|
|
3236
3925
|
});
|
|
3237
3926
|
if (!command) return { error: 'Failed to build provider resume command' };
|
|
3238
3927
|
const preserved = {
|
|
@@ -3243,13 +3932,13 @@ class AgentManager extends EventEmitter {
|
|
|
3243
3932
|
unread: agent.unread === true,
|
|
3244
3933
|
jsonCliEvents: isJsonCliAgent(agent)
|
|
3245
3934
|
? this.jsonCliRuntime.getEvents(agentId)
|
|
3246
|
-
: (Array.isArray(agent.
|
|
3935
|
+
: (Array.isArray(agent.runtimeResumeState?.jsonEvents) ? agent.runtimeResumeState.jsonEvents : []),
|
|
3247
3936
|
};
|
|
3248
3937
|
const restartOptions = {
|
|
3249
3938
|
wantsMain: agent.wantsMain === true,
|
|
3250
3939
|
task: agent.task || agent.providerSessionTitle || '',
|
|
3251
3940
|
workflowTemplate: agent.workflowTemplate || '',
|
|
3252
|
-
projectWorkspace: agent
|
|
3941
|
+
projectWorkspace: effectiveAgentWorkspaceRoot(agent),
|
|
3253
3942
|
source: startsFreshAcpSession
|
|
3254
3943
|
? 'ui-runtime-switch-fresh'
|
|
3255
3944
|
: resumedAgentSource(provider, sessionId, agent.providerHomeId || ''),
|
|
@@ -3270,12 +3959,15 @@ class AgentManager extends EventEmitter {
|
|
|
3270
3959
|
codexApprovalMode: agent.launchPermissionMode || undefined,
|
|
3271
3960
|
jsonCliEvents: preserved.jsonCliEvents,
|
|
3272
3961
|
runtimeSwitchVerifiedSessionId: startsFreshAcpSession ? '' : sessionId,
|
|
3962
|
+
...(provider === 'codex' && !startsFreshAcpSession
|
|
3963
|
+
? preserveCodexSessionProfileOptions()
|
|
3964
|
+
: {}),
|
|
3273
3965
|
};
|
|
3274
|
-
const originalMode =
|
|
3966
|
+
const originalMode = currentMode;
|
|
3275
3967
|
const originalOptions = {
|
|
3276
3968
|
...restartOptions,
|
|
3277
3969
|
agentRuntimeMode: originalMode,
|
|
3278
|
-
codexRuntimeMode:
|
|
3970
|
+
codexRuntimeMode: currentKind === 'app-server' ? 'app-server' : 'cli',
|
|
3279
3971
|
acpStartFresh: false,
|
|
3280
3972
|
};
|
|
3281
3973
|
const startReplacement = options => new Promise(resolve => {
|
|
@@ -3288,7 +3980,7 @@ class AgentManager extends EventEmitter {
|
|
|
3288
3980
|
try {
|
|
3289
3981
|
const started = this.startAgent(
|
|
3290
3982
|
command,
|
|
3291
|
-
agent
|
|
3983
|
+
effectiveAgentWorkspaceRoot(agent) || null,
|
|
3292
3984
|
(restartedAgentId, error) => finish(restartedAgentId, error),
|
|
3293
3985
|
options
|
|
3294
3986
|
);
|
|
@@ -3395,11 +4087,11 @@ class AgentManager extends EventEmitter {
|
|
|
3395
4087
|
if (!hasResumableSession && !startsFreshCodexSession) {
|
|
3396
4088
|
return { error: 'Permission changes require a resumable provider session. Try again after the session id is available.' };
|
|
3397
4089
|
}
|
|
3398
|
-
|
|
3399
4090
|
const command = startsFreshCodexSession
|
|
3400
4091
|
? 'codex'
|
|
3401
4092
|
: buildAgentSessionResumeCommand(provider, sessionId, {
|
|
3402
|
-
cwd: agent
|
|
4093
|
+
cwd: effectiveAgentWorkspaceRoot(agent),
|
|
4094
|
+
providerHomePath: agent.providerHomePath || '',
|
|
3403
4095
|
});
|
|
3404
4096
|
if (!command) {
|
|
3405
4097
|
return { error: 'Failed to build provider resume command' };
|
|
@@ -3410,7 +4102,7 @@ class AgentManager extends EventEmitter {
|
|
|
3410
4102
|
task: agent.task || agent.providerSessionTitle || '',
|
|
3411
4103
|
workflowTemplate: agent.workflowTemplate || '',
|
|
3412
4104
|
requiredCliVersion: provider === 'codex' ? (agent.requiredCliVersion || '') : '',
|
|
3413
|
-
projectWorkspace: agent
|
|
4105
|
+
projectWorkspace: effectiveAgentWorkspaceRoot(agent),
|
|
3414
4106
|
source: startsFreshCodexSession
|
|
3415
4107
|
? 'ui'
|
|
3416
4108
|
: (resumedSessionFromSource(agent.source)
|
|
@@ -3428,6 +4120,9 @@ class AgentManager extends EventEmitter {
|
|
|
3428
4120
|
projectOrder: finiteOrder(agent.projectOrder),
|
|
3429
4121
|
pinnedOrder: finiteOrder(agent.pinnedOrder),
|
|
3430
4122
|
...(provider === 'codex' ? { codexApprovalMode: nextMode } : { claudePermissionMode: nextMode }),
|
|
4123
|
+
...(provider === 'codex' && hasResumableSession
|
|
4124
|
+
? preserveCodexSessionProfileOptions()
|
|
4125
|
+
: {}),
|
|
3431
4126
|
};
|
|
3432
4127
|
const preserved = {
|
|
3433
4128
|
pinned: agent.pinned === true,
|
|
@@ -3451,7 +4146,7 @@ class AgentManager extends EventEmitter {
|
|
|
3451
4146
|
}
|
|
3452
4147
|
|
|
3453
4148
|
return new Promise((resolve) => {
|
|
3454
|
-
const startResult = this.startAgent(command, agent
|
|
4149
|
+
const startResult = this.startAgent(command, effectiveAgentWorkspaceRoot(agent) || null, (restartedAgentId, error) => {
|
|
3455
4150
|
if (error) {
|
|
3456
4151
|
this.emit('update');
|
|
3457
4152
|
resolve({ error });
|
|
@@ -3601,7 +4296,7 @@ class AgentManager extends EventEmitter {
|
|
|
3601
4296
|
const resolvedWorkspace = path.resolve(workspace);
|
|
3602
4297
|
return Array.from(this.agents.values()).filter(agent => {
|
|
3603
4298
|
if (!agent || agent.isMain) return false;
|
|
3604
|
-
const agentWorkspace = this.expandWorkspacePath(agent
|
|
4299
|
+
const agentWorkspace = this.expandWorkspacePath(effectiveAgentWorkspaceRoot(agent));
|
|
3605
4300
|
if (!agentWorkspace) return false;
|
|
3606
4301
|
return path.resolve(agentWorkspace) === resolvedWorkspace;
|
|
3607
4302
|
});
|
|
@@ -3614,7 +4309,7 @@ class AgentManager extends EventEmitter {
|
|
|
3614
4309
|
|
|
3615
4310
|
const keysToRemove = new Set();
|
|
3616
4311
|
agents.forEach(agent => {
|
|
3617
|
-
const providerSessionKey = agent.providerSessionKey ||
|
|
4312
|
+
const providerSessionKey = agent.providerSessionKey || mainPageAgentSessionKey(
|
|
3618
4313
|
agent.providerSessionProvider,
|
|
3619
4314
|
agent.providerSessionId,
|
|
3620
4315
|
agent.providerHomeId || ''
|
|
@@ -3686,8 +4381,21 @@ class AgentManager extends EventEmitter {
|
|
|
3686
4381
|
if (!agent) {
|
|
3687
4382
|
return { error: 'Agent not found' };
|
|
3688
4383
|
}
|
|
4384
|
+
if (!['same-worktree', 'new-worktree'].includes(mode)) {
|
|
4385
|
+
return { error: 'Unsupported fork mode' };
|
|
4386
|
+
}
|
|
4387
|
+
|
|
4388
|
+
const sourceWorkspace = effectiveAgentWorkspaceRoot(agent);
|
|
4389
|
+
const resumedSession = agent.providerSessionProvider
|
|
4390
|
+
&& agent.providerSessionId
|
|
4391
|
+
&& agent.providerSessionTemporary !== true
|
|
4392
|
+
? { provider: agent.providerSessionProvider, providerHomeId: agent.providerHomeId || 'default', sessionId: agent.providerSessionId }
|
|
4393
|
+
: resumedSessionFromSource(agent.source);
|
|
4394
|
+
if (resumedSession?.provider === 'codex') {
|
|
4395
|
+
const availability = await this.ensureCodexSessionAvailableForFork(agent, resumedSession, sourceWorkspace);
|
|
4396
|
+
if (availability?.error) return availability;
|
|
4397
|
+
}
|
|
3689
4398
|
|
|
3690
|
-
const sourceWorkspace = agent.projectWorkspace || agent.cwd;
|
|
3691
4399
|
let targetWorkspace = sourceWorkspace;
|
|
3692
4400
|
if (mode === 'new-worktree') {
|
|
3693
4401
|
try {
|
|
@@ -3695,19 +4403,13 @@ class AgentManager extends EventEmitter {
|
|
|
3695
4403
|
} catch (error) {
|
|
3696
4404
|
return { error: error.message || 'Failed to create git worktree' };
|
|
3697
4405
|
}
|
|
3698
|
-
} else if (mode !== 'same-worktree') {
|
|
3699
|
-
return { error: 'Unsupported fork mode' };
|
|
3700
4406
|
}
|
|
3701
4407
|
|
|
3702
|
-
const resumedSession = agent.providerSessionProvider
|
|
3703
|
-
&& agent.providerSessionId
|
|
3704
|
-
&& agent.providerSessionTemporary !== true
|
|
3705
|
-
? { provider: agent.providerSessionProvider, providerHomeId: agent.providerHomeId || 'default', sessionId: agent.providerSessionId }
|
|
3706
|
-
: resumedSessionFromSource(agent.source);
|
|
3707
4408
|
const forkCommand = resumedSession
|
|
3708
4409
|
? buildAgentSessionResumeCommand(resumedSession.provider, resumedSession.sessionId, {
|
|
3709
4410
|
fork: true,
|
|
3710
4411
|
cwd: targetWorkspace,
|
|
4412
|
+
providerHomePath: agent.providerHomePath || '',
|
|
3711
4413
|
})
|
|
3712
4414
|
: (agent.forkCommand || agent.command);
|
|
3713
4415
|
|
|
@@ -3734,10 +4436,62 @@ class AgentManager extends EventEmitter {
|
|
|
3734
4436
|
source: mode === 'new-worktree' ? 'ui-fork-new-worktree' : 'ui-fork-same-worktree',
|
|
3735
4437
|
providerHomeId: agent.providerHomeId || (resumedSession && resumedSession.providerHomeId) || '',
|
|
3736
4438
|
providerHomePath: agent.providerHomePath || '',
|
|
4439
|
+
...(resumedSession?.provider === 'codex'
|
|
4440
|
+
? preserveCodexSessionProfileOptions()
|
|
4441
|
+
: {}),
|
|
3737
4442
|
});
|
|
3738
4443
|
});
|
|
3739
4444
|
}
|
|
3740
4445
|
|
|
4446
|
+
async ensureCodexSessionAvailableForFork(agent, resumedSession, sourceWorkspace) {
|
|
4447
|
+
const providerHomeId = agent.providerHomeId || resumedSession.providerHomeId || 'default';
|
|
4448
|
+
const providerHomePath = agent.providerHomePath || '';
|
|
4449
|
+
const sessionId = resumedSession.sessionId;
|
|
4450
|
+
const unarchiveKey = `${providerHomePath || providerHomeId}:${sessionId}`;
|
|
4451
|
+
const inFlight = this.codexSessionUnarchiveInFlight.get(unarchiveKey);
|
|
4452
|
+
if (inFlight) return inFlight;
|
|
4453
|
+
|
|
4454
|
+
const unarchivePromise = (async () => {
|
|
4455
|
+
let session;
|
|
4456
|
+
try {
|
|
4457
|
+
session = await findAgentSession('codex', sessionId, {
|
|
4458
|
+
limit: 1000,
|
|
4459
|
+
providerLimit: 1000,
|
|
4460
|
+
scanLimit: 5000,
|
|
4461
|
+
providerHomeId,
|
|
4462
|
+
providerHomes: providerHomePath
|
|
4463
|
+
? { codex: [{ id: providerHomeId, path: providerHomePath }] }
|
|
4464
|
+
: undefined,
|
|
4465
|
+
});
|
|
4466
|
+
} catch (error) {
|
|
4467
|
+
return {
|
|
4468
|
+
error: `Failed to inspect Codex session before forking: ${error && (error.message || error)}`,
|
|
4469
|
+
};
|
|
4470
|
+
}
|
|
4471
|
+
if (!session || session.archived !== true) return null;
|
|
4472
|
+
|
|
4473
|
+
const result = await this.unarchiveCodexSession(sessionId, {
|
|
4474
|
+
...session,
|
|
4475
|
+
// Fork is an action on the live Farming Agent. Its current workspace is
|
|
4476
|
+
// authoritative even when the older provider history cwd no longer exists.
|
|
4477
|
+
cwd: sourceWorkspace || session.cwd || session.workspace,
|
|
4478
|
+
providerHomePath: session.providerHomePath || providerHomePath,
|
|
4479
|
+
});
|
|
4480
|
+
if (!result?.error) return null;
|
|
4481
|
+
return {
|
|
4482
|
+
error: `Codex session ${sessionId} is archived and could not be unarchived before forking: ${result.error}`,
|
|
4483
|
+
};
|
|
4484
|
+
})();
|
|
4485
|
+
this.codexSessionUnarchiveInFlight.set(unarchiveKey, unarchivePromise);
|
|
4486
|
+
try {
|
|
4487
|
+
return await unarchivePromise;
|
|
4488
|
+
} finally {
|
|
4489
|
+
if (this.codexSessionUnarchiveInFlight.get(unarchiveKey) === unarchivePromise) {
|
|
4490
|
+
this.codexSessionUnarchiveInFlight.delete(unarchiveKey);
|
|
4491
|
+
}
|
|
4492
|
+
}
|
|
4493
|
+
}
|
|
4494
|
+
|
|
3741
4495
|
recordTaskHistory(agent, options = {}) {
|
|
3742
4496
|
if (!agent || agent.id === this.mainAgentId) return;
|
|
3743
4497
|
if (!isSupportedHistoryAgent(agent.forkCommand || agent.command || '')) return;
|
|
@@ -3751,7 +4505,7 @@ class AgentManager extends EventEmitter {
|
|
|
3751
4505
|
agentId: agent.id,
|
|
3752
4506
|
command: agent.command || '',
|
|
3753
4507
|
cwd: agent.cwd || '',
|
|
3754
|
-
projectWorkspace: agent
|
|
4508
|
+
projectWorkspace: effectiveAgentWorkspaceRoot(agent),
|
|
3755
4509
|
title: agent.customTitle || agent.sessionTitle || agent.task || '',
|
|
3756
4510
|
customTitle: agent.customTitle || '',
|
|
3757
4511
|
task: agent.task || '',
|
|
@@ -3810,9 +4564,9 @@ class AgentManager extends EventEmitter {
|
|
|
3810
4564
|
this.lastActivity.delete(agentId);
|
|
3811
4565
|
this.lastActivityUpdate.delete(agentId);
|
|
3812
4566
|
this.outputEvents.delete(agentId);
|
|
4567
|
+
this.agentUsageRateCache.delete(agentId);
|
|
3813
4568
|
this.lastResizeByAgent.delete(agentId);
|
|
3814
|
-
this.
|
|
3815
|
-
this.stopProviderSessionTitleResolver(agentId);
|
|
4569
|
+
this.providerSessionService.stop(agentId);
|
|
3816
4570
|
if (this.codexAppServerRuntime && typeof this.codexAppServerRuntime.unregisterAgent === 'function') {
|
|
3817
4571
|
this.codexAppServerRuntime.unregisterAgent(agentId);
|
|
3818
4572
|
}
|
|
@@ -3866,19 +4620,20 @@ class AgentManager extends EventEmitter {
|
|
|
3866
4620
|
return null;
|
|
3867
4621
|
}
|
|
3868
4622
|
|
|
3869
|
-
return agent
|
|
4623
|
+
return effectiveAgentWorkspaceRoot(agent);
|
|
3870
4624
|
}
|
|
3871
4625
|
|
|
3872
4626
|
getAgentProviderSession(agentId) {
|
|
3873
4627
|
const agent = this.agents.get(agentId);
|
|
3874
4628
|
if (!agent) return null;
|
|
4629
|
+
const appServer = runtimeBindingOf(agent, 'app-server');
|
|
3875
4630
|
return {
|
|
3876
4631
|
provider: agent.providerSessionProvider || '',
|
|
3877
4632
|
sessionId: agent.providerSessionId || '',
|
|
3878
4633
|
providerHomeId: agent.providerHomeId || '',
|
|
3879
4634
|
providerHomePath: agent.providerHomePath || '',
|
|
3880
|
-
codexAppServerHomePath:
|
|
3881
|
-
|
|
4635
|
+
codexAppServerHomePath: appServer?.homePath || '',
|
|
4636
|
+
runtimeBinding: publicRuntimeBinding(agent),
|
|
3882
4637
|
temporary: agent.providerSessionTemporary === true,
|
|
3883
4638
|
title: agent.providerSessionTitle || '',
|
|
3884
4639
|
};
|
|
@@ -3958,6 +4713,7 @@ class AgentManager extends EventEmitter {
|
|
|
3958
4713
|
engineName: agent.engineName || '',
|
|
3959
4714
|
cwd: agent.cwd,
|
|
3960
4715
|
projectWorkspace: agent.projectWorkspace || '',
|
|
4716
|
+
gitWorktree: publicAgentGitWorktree(agent),
|
|
3961
4717
|
status: sessionState && sessionState.status === 'exited'
|
|
3962
4718
|
? agent.status
|
|
3963
4719
|
: (isLiveEngineSessionState(sessionState) ? 'running' : agent.status),
|
|
@@ -3982,27 +4738,9 @@ class AgentManager extends EventEmitter {
|
|
|
3982
4738
|
providerSessionSource: agent.providerSessionSource || '',
|
|
3983
4739
|
providerSessionResolvedAt: agent.providerSessionResolvedAt || null,
|
|
3984
4740
|
providerSessionTitle: agent.providerSessionTitle || '',
|
|
4741
|
+
providerSessionWorkspace: agent.providerSessionWorkspace || '',
|
|
3985
4742
|
terminalInputReceived: agent.terminalInputReceived === true,
|
|
3986
|
-
|
|
3987
|
-
agentRuntimeMode: agent.agentRuntimeMode || 'terminal',
|
|
3988
|
-
acpState: agent.acpState || '',
|
|
3989
|
-
acpError: agent.acpError || '',
|
|
3990
|
-
acpStopReason: agent.acpStopReason || '',
|
|
3991
|
-
acpPendingPermission: agent.acpPendingPermission || null,
|
|
3992
|
-
acpPendingPermissions: Array.isArray(agent.acpPendingPermissions) ? agent.acpPendingPermissions : [],
|
|
3993
|
-
acpPendingElicitation: agent.acpPendingElicitation || null,
|
|
3994
|
-
acpPendingElicitations: Array.isArray(agent.acpPendingElicitations) ? agent.acpPendingElicitations : [],
|
|
3995
|
-
acpActiveElicitations: Array.isArray(agent.acpActiveElicitations) ? agent.acpActiveElicitations : [],
|
|
3996
|
-
acpSessionUpdatedAt: agent.acpSessionUpdatedAt || '',
|
|
3997
|
-
acpSessionRevision: Number(agent.acpSessionRevision) || 0,
|
|
3998
|
-
codexAppServerState: agent.codexAppServerState || '',
|
|
3999
|
-
codexAppServerEndpoint: agent.codexAppServerEndpoint || '',
|
|
4000
|
-
codexAppServerThreadId: agent.codexAppServerThreadId || '',
|
|
4001
|
-
codexAppServerTurnId: agent.codexAppServerTurnId || '',
|
|
4002
|
-
codexAppServerError: agent.codexAppServerError || '',
|
|
4003
|
-
codexAppServerPendingRequestId: agent.codexAppServerPendingRequestId || '',
|
|
4004
|
-
codexAppServerPendingRequestMethod: agent.codexAppServerPendingRequestMethod || '',
|
|
4005
|
-
codexAppServerPendingRequest: agent.codexAppServerPendingRequest || null,
|
|
4743
|
+
runtimeBinding: publicRuntimeBinding(agent),
|
|
4006
4744
|
forkedFromProviderSessionId: agent.forkedFromProviderSessionId || '',
|
|
4007
4745
|
customTitle: agent.customTitle || '',
|
|
4008
4746
|
pinned: agent.pinned === true,
|
|
@@ -4013,12 +4751,21 @@ class AgentManager extends EventEmitter {
|
|
|
4013
4751
|
attentionUpdatedAt: finiteNumberOrNull(agent.attentionUpdatedAt),
|
|
4014
4752
|
readAttentionAt: finiteNumberOrNull(agent.readAttentionAt),
|
|
4015
4753
|
attentionReason: agent.attentionReason || '',
|
|
4754
|
+
attentionOutputEpoch: agent.attentionOutputEpoch || '',
|
|
4016
4755
|
attentionOutputSeq: finiteNumberOrNull(agent.attentionOutputSeq),
|
|
4756
|
+
readOutputEpoch: agent.readOutputEpoch || '',
|
|
4757
|
+
readOutputSeq: finiteNumberOrNull(agent.readOutputSeq),
|
|
4017
4758
|
unread: agentAttentionUnread(agent),
|
|
4018
4759
|
archived: agent.archived === true,
|
|
4019
4760
|
archivedAt: agent.archivedAt || null,
|
|
4020
4761
|
sessionSource: this.getEngineSessionSource(agent.engineName),
|
|
4762
|
+
runtimeEpoch: sessionState && typeof sessionState.runtimeEpoch === 'string'
|
|
4763
|
+
? sessionState.runtimeEpoch
|
|
4764
|
+
: (agent.runtimeEpoch || ''),
|
|
4021
4765
|
outputSeq: sessionState && Number.isFinite(sessionState.outputSeq) ? sessionState.outputSeq : null,
|
|
4766
|
+
stateRevision: sessionState && Number.isFinite(sessionState.stateRevision)
|
|
4767
|
+
? sessionState.stateRevision
|
|
4768
|
+
: null,
|
|
4022
4769
|
isMain,
|
|
4023
4770
|
activityLevel: isMain ? 'warm' : this.calculateActivityLevel(lastActivity, now),
|
|
4024
4771
|
lastActivity,
|
|
@@ -4030,6 +4777,7 @@ class AgentManager extends EventEmitter {
|
|
|
4030
4777
|
output: (sessionState && typeof sessionState.output === 'string') ? sessionState.output : fallbackOutput,
|
|
4031
4778
|
renderOutput: (sessionState && typeof sessionState.renderOutput === 'string') ? sessionState.renderOutput : fallbackOutput,
|
|
4032
4779
|
previewText,
|
|
4780
|
+
codexTerminalProfile: activeCodexTerminalProfile(agent, previewText),
|
|
4033
4781
|
previewSnapshot: (sessionState && sessionState.previewSnapshot) || agent.previewSnapshot || null,
|
|
4034
4782
|
previewCols: (sessionState && Number.isFinite(sessionState.previewCols) && sessionState.previewCols > 0)
|
|
4035
4783
|
? sessionState.previewCols
|
|
@@ -4037,10 +4785,28 @@ class AgentManager extends EventEmitter {
|
|
|
4037
4785
|
previewRows: (sessionState && Number.isFinite(sessionState.previewRows) && sessionState.previewRows > 0)
|
|
4038
4786
|
? sessionState.previewRows
|
|
4039
4787
|
: (agent.previewRows || 30),
|
|
4040
|
-
usageRate: this.
|
|
4788
|
+
usageRate: this.getAgentUsageRate(agent.id),
|
|
4041
4789
|
};
|
|
4042
4790
|
}
|
|
4043
4791
|
|
|
4792
|
+
getAgentUsageRate(agentId, options = {}) {
|
|
4793
|
+
const now = options.now || Date.now();
|
|
4794
|
+
const windowMs = options.windowMs || AGENT_USAGE_RATE_WINDOW_MS;
|
|
4795
|
+
const cached = this.agentUsageRateCache.get(agentId);
|
|
4796
|
+
if (
|
|
4797
|
+
cached
|
|
4798
|
+
&& cached.windowMs === windowMs
|
|
4799
|
+
&& now >= cached.sampledAt
|
|
4800
|
+
&& now - cached.sampledAt < AGENT_USAGE_RATE_REFRESH_MS
|
|
4801
|
+
) {
|
|
4802
|
+
return cached.value;
|
|
4803
|
+
}
|
|
4804
|
+
|
|
4805
|
+
const value = this.calculateAgentUsageRate(agentId, { now, windowMs });
|
|
4806
|
+
this.agentUsageRateCache.set(agentId, { windowMs, sampledAt: now, value });
|
|
4807
|
+
return value;
|
|
4808
|
+
}
|
|
4809
|
+
|
|
4044
4810
|
calculateAgentUsageRate(agentId, options = {}) {
|
|
4045
4811
|
const now = options.now || Date.now();
|
|
4046
4812
|
const windowMs = options.windowMs || AGENT_USAGE_RATE_WINDOW_MS;
|
|
@@ -4077,7 +4843,7 @@ class AgentManager extends EventEmitter {
|
|
|
4077
4843
|
cwd: agent.cwd,
|
|
4078
4844
|
isMain: this.isMainAgentRecord(agent.id, agent),
|
|
4079
4845
|
status: agent.status,
|
|
4080
|
-
usageRate: this.
|
|
4846
|
+
usageRate: this.getAgentUsageRate(agent.id, { now, windowMs }),
|
|
4081
4847
|
}));
|
|
4082
4848
|
const totalOutputBytes = agents.reduce((sum, agent) => sum + agent.usageRate.outputBytes, 0);
|
|
4083
4849
|
const estimatedOutputTokens = agents.reduce((sum, agent) => sum + agent.usageRate.estimatedOutputTokens, 0);
|
|
@@ -4119,12 +4885,17 @@ class AgentManager extends EventEmitter {
|
|
|
4119
4885
|
engineName: agent.engineName || '',
|
|
4120
4886
|
cwd: agent.cwd,
|
|
4121
4887
|
projectWorkspace: agent.projectWorkspace || '',
|
|
4888
|
+
gitWorktree: publicAgentGitWorktree(agent),
|
|
4122
4889
|
output: agent.output.slice(-2000),
|
|
4123
4890
|
previewText: agent.previewText || '',
|
|
4891
|
+
codexTerminalProfile: activeCodexTerminalProfile(agent, agent.previewText || ''),
|
|
4124
4892
|
previewCols: agent.previewCols || 80,
|
|
4125
4893
|
previewRows: agent.previewRows || 30,
|
|
4126
4894
|
sessionTitle: agent.sessionTitle || '',
|
|
4127
4895
|
sessionSource: this.getEngineSessionSource(agent.engineName),
|
|
4896
|
+
runtimeEpoch: agent.runtimeEpoch || '',
|
|
4897
|
+
outputSeq: finiteNumberOrNull(agent.lastOutputSeq),
|
|
4898
|
+
stateRevision: finiteNumberOrNull(agent.stateRevision),
|
|
4128
4899
|
status: agent.status,
|
|
4129
4900
|
terminalBusy,
|
|
4130
4901
|
terminalStatus,
|
|
@@ -4148,33 +4919,11 @@ class AgentManager extends EventEmitter {
|
|
|
4148
4919
|
providerSessionSource: agent.providerSessionSource || '',
|
|
4149
4920
|
providerSessionResolvedAt: agent.providerSessionResolvedAt || null,
|
|
4150
4921
|
providerSessionTitle: agent.providerSessionTitle || '',
|
|
4922
|
+
providerSessionWorkspace: agent.providerSessionWorkspace || '',
|
|
4923
|
+
providerCapabilities: providerCapabilities(agent.providerSessionProvider),
|
|
4151
4924
|
terminalInputReceived: agent.terminalInputReceived === true,
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
jsonCliState: agent.jsonCliState || '',
|
|
4155
|
-
jsonCliError: agent.jsonCliError || '',
|
|
4156
|
-
jsonCliTranscriptUpdatedAt: agent.jsonCliTranscriptUpdatedAt || '',
|
|
4157
|
-
acpState: agent.acpState || '',
|
|
4158
|
-
acpError: agent.acpError || '',
|
|
4159
|
-
acpStopReason: agent.acpStopReason || '',
|
|
4160
|
-
acpPendingPermission: agent.acpPendingPermission || null,
|
|
4161
|
-
acpPendingPermissions: Array.isArray(agent.acpPendingPermissions) ? agent.acpPendingPermissions : [],
|
|
4162
|
-
acpPendingElicitation: agent.acpPendingElicitation || null,
|
|
4163
|
-
acpPendingElicitations: Array.isArray(agent.acpPendingElicitations) ? agent.acpPendingElicitations : [],
|
|
4164
|
-
acpActiveElicitations: Array.isArray(agent.acpActiveElicitations) ? agent.acpActiveElicitations : [],
|
|
4165
|
-
acpSessionUpdatedAt: agent.acpSessionUpdatedAt || '',
|
|
4166
|
-
acpSessionRevision: Number(agent.acpSessionRevision) || 0,
|
|
4167
|
-
codexAppServerState: agent.codexAppServerState || '',
|
|
4168
|
-
codexAppServerEndpoint: agent.codexAppServerEndpoint || '',
|
|
4169
|
-
codexAppServerThreadId: agent.codexAppServerThreadId || '',
|
|
4170
|
-
codexAppServerTurnId: agent.codexAppServerTurnId || '',
|
|
4171
|
-
codexAppServerError: agent.codexAppServerError || '',
|
|
4172
|
-
codexAppServerPendingRequestId: agent.codexAppServerPendingRequestId || '',
|
|
4173
|
-
codexAppServerPendingRequestMethod: agent.codexAppServerPendingRequestMethod || '',
|
|
4174
|
-
codexAppServerPendingRequest: agent.codexAppServerPendingRequest || null,
|
|
4175
|
-
codexAppServerNotice: agent.codexAppServerNotice || null,
|
|
4176
|
-
codexAppServerGoal: agent.codexAppServerGoal || null,
|
|
4177
|
-
codexCliObserverDeferred: agent.codexCliObserverDeferred === true,
|
|
4925
|
+
runtimeBinding: publicRuntimeBinding(agent),
|
|
4926
|
+
runtimeObservation: deriveRuntimeObservation(agent),
|
|
4178
4927
|
forkedFromProviderSessionId: agent.forkedFromProviderSessionId || '',
|
|
4179
4928
|
restartedFromAgentId: agent.restartedFromAgentId || '',
|
|
4180
4929
|
restartedFromAgentIds: Array.isArray(agent.restartedFromAgentIds) ? agent.restartedFromAgentIds : [],
|
|
@@ -4188,11 +4937,14 @@ class AgentManager extends EventEmitter {
|
|
|
4188
4937
|
attentionUpdatedAt: finiteNumberOrNull(agent.attentionUpdatedAt),
|
|
4189
4938
|
readAttentionAt: finiteNumberOrNull(agent.readAttentionAt),
|
|
4190
4939
|
attentionReason: agent.attentionReason || '',
|
|
4940
|
+
attentionOutputEpoch: agent.attentionOutputEpoch || '',
|
|
4191
4941
|
attentionOutputSeq: finiteNumberOrNull(agent.attentionOutputSeq),
|
|
4942
|
+
readOutputEpoch: agent.readOutputEpoch || '',
|
|
4943
|
+
readOutputSeq: finiteNumberOrNull(agent.readOutputSeq),
|
|
4192
4944
|
unread: agentAttentionUnread(agent),
|
|
4193
4945
|
archived: agent.archived === true,
|
|
4194
4946
|
archivedAt: agent.archivedAt || null,
|
|
4195
|
-
canForkNewWorktree: this.canCreateForkWorktree(agent
|
|
4947
|
+
canForkNewWorktree: this.canCreateForkWorktree(effectiveAgentWorkspaceRoot(agent)),
|
|
4196
4948
|
startedAt: agent.startedAt || null,
|
|
4197
4949
|
exitedAt: agent.exitedAt || null,
|
|
4198
4950
|
// Main agent is exempt from activity/attention/zombie scoring
|
|
@@ -4200,7 +4952,7 @@ class AgentManager extends EventEmitter {
|
|
|
4200
4952
|
lastActivity,
|
|
4201
4953
|
attentionScore: isMain ? 0 : this.calculateAttentionScore(id, now),
|
|
4202
4954
|
isZombie: isMain ? false : this.isZombie(id, now),
|
|
4203
|
-
usageRate: this.
|
|
4955
|
+
usageRate: this.getAgentUsageRate(id, { now })
|
|
4204
4956
|
});
|
|
4205
4957
|
}
|
|
4206
4958
|
|
|
@@ -4273,6 +5025,10 @@ class AgentManager extends EventEmitter {
|
|
|
4273
5025
|
this.on('update', callback);
|
|
4274
5026
|
}
|
|
4275
5027
|
|
|
5028
|
+
onAgentActivity(callback) {
|
|
5029
|
+
this.on('agent-activity', callback);
|
|
5030
|
+
}
|
|
5031
|
+
|
|
4276
5032
|
onSessionStream(callback) {
|
|
4277
5033
|
this.on('session-stream', callback);
|
|
4278
5034
|
}
|
|
@@ -4288,17 +5044,20 @@ class AgentManager extends EventEmitter {
|
|
|
4288
5044
|
continue;
|
|
4289
5045
|
}
|
|
4290
5046
|
|
|
5047
|
+
const terminalStatus = deriveAgentTerminalStatus(agent, {
|
|
5048
|
+
previewText: agent.previewText || '',
|
|
5049
|
+
title: agent.sessionTitle || '',
|
|
5050
|
+
terminalBusy: typeof agent.terminalBusy === 'boolean' ? agent.terminalBusy : null,
|
|
5051
|
+
});
|
|
4291
5052
|
previews.push({
|
|
4292
5053
|
agentId: agent.id,
|
|
4293
5054
|
previewText: agent.previewText || '',
|
|
4294
5055
|
cols: agent.previewCols || 80,
|
|
4295
5056
|
rows: agent.previewRows || 30,
|
|
4296
5057
|
previewSnapshot: agent.previewSnapshot || null,
|
|
4297
|
-
|
|
4298
|
-
|
|
4299
|
-
|
|
4300
|
-
terminalBusy: typeof agent.terminalBusy === 'boolean' ? agent.terminalBusy : null,
|
|
4301
|
-
}),
|
|
5058
|
+
codexTerminalProfile: activeCodexTerminalProfile(agent, agent.previewText || ''),
|
|
5059
|
+
terminalStatus,
|
|
5060
|
+
runtimeObservation: deriveRuntimeObservation({ ...agent, terminalStatus }),
|
|
4302
5061
|
});
|
|
4303
5062
|
}
|
|
4304
5063
|
|