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