farming-code 2.2.7 → 2.2.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +110 -199
- package/README.zh_cn.md +106 -237
- package/THIRD_PARTY_NOTICES.md +11 -2
- package/backend/acp-checkpoint-store.js +198 -0
- package/backend/acp-runtime.js +306 -83
- package/backend/acp-session-state.js +202 -6
- package/backend/acp-transcript.js +112 -0
- package/backend/agent-activity.js +6 -157
- package/backend/agent-manager.js +1496 -737
- package/backend/agent-provider-session.js +17 -242
- package/backend/agent-runtime-binding.js +219 -0
- package/backend/agent-session-history.js +66 -1
- package/backend/auth.js +79 -6
- package/backend/codex-models.js +81 -84
- package/backend/codex-session-archive.js +45 -0
- package/backend/codex-terminal-profile.js +500 -0
- package/backend/codex-transcript-sanitizer.js +12 -0
- package/backend/codex-transcript.js +230 -8
- package/backend/config-manager.js +30 -0
- package/backend/control-api.js +192 -17
- package/backend/farming-net-pass.js +285 -0
- package/backend/farming-net-registry.js +112 -0
- package/backend/farming-net-server.js +298 -0
- package/backend/farming-session-store.js +5 -13
- package/backend/git-worktree-info.js +181 -0
- package/backend/local-session-engine.js +411 -186
- package/backend/main-page-session.js +5 -2
- package/backend/native-pty-controller-generation.js +106 -0
- package/backend/native-pty-host-client.js +275 -7
- package/backend/native-pty-host-identity.js +86 -0
- package/backend/native-pty-host.js +813 -114
- package/backend/native-session-engine.js +100 -28
- package/backend/packaged-node-pty.js +22 -2
- package/backend/provider-adapters.js +253 -0
- package/backend/provider-session-service.js +241 -0
- package/backend/runtime-observation.js +81 -0
- package/backend/server.js +294 -86
- package/backend/session-engine-bridge.js +21 -2
- package/backend/session-engine-router.js +1 -1
- package/backend/session-engine.js +1 -1
- package/backend/session-stream-protocol.js +185 -0
- package/backend/storage-layout.js +55 -0
- package/backend/terminal-attach-checkpoint.js +74 -0
- package/backend/terminal-exit-quiescence.js +39 -0
- package/backend/terminal-reducer-flow-control.js +97 -0
- package/backend/terminal-screen-state.js +11 -2
- package/backend/terminal-screen-worker-pool.js +59 -6
- package/backend/terminal-screen-worker-thread.js +97 -57
- package/backend/terminal-screen-worker.js +133 -51
- package/backend/terminal-state-serialization.js +127 -0
- package/backend/terminal-status.js +23 -4
- package/backend/usage-monitor.js +81 -4
- package/backend/workspace-directory.js +152 -0
- package/backend/workspace-file-router.js +182 -76
- package/backend/workspace-file-service.js +319 -4
- package/backend/workspace-root-registry.js +164 -0
- package/dist/assets/App-CHYpgECZ.js +208 -0
- package/dist/assets/{FileEditorMarkdownPreview-elKWc8Im.js → FileEditorMarkdownPreview--Cri80nQ.js} +92 -92
- package/dist/assets/FileEditorPane-DGJlEKdf.js +2 -0
- package/dist/assets/IconGlyphs-Cc14sepw.js +1 -0
- package/dist/assets/ProjectFilesSection-Cz1wG4NT.js +12 -0
- package/dist/assets/{ReviewPage-BaXu1ZdX.js → ReviewPage-LonRc6Fz.js} +1 -1
- package/dist/assets/code-dark-B1pAtO_P.css +1 -0
- package/dist/assets/file-icons-Bw2qd5iT.js +1 -0
- package/dist/assets/{index-B8SHldPE.js → index-miO2r9IZ.js} +3 -3
- package/dist/assets/main-DNe0jPw-.css +1 -0
- package/dist/assets/workspace-editor-model-BQol4qbA.js +1 -0
- package/dist/assets/workspace-editor-monaco-5gFbeDPF.js +1 -0
- package/dist/assets/workspace-editor-monaco-xPv8JzHH.js +4 -0
- package/dist/assets/workspace-view-state-DvYG_9PH.js +7 -0
- package/dist/assets/workspace-working-copy-D8-s_Sgh.js +1 -0
- package/dist/index.html +1 -1
- package/frontend/farming-net/app.css +625 -0
- package/frontend/farming-net/app.js +268 -0
- package/frontend/farming-net/index.html +86 -0
- package/frontend/reading-anchor.js +198 -0
- package/frontend/session-bridge.js +12 -3
- package/frontend/session-modal-bridge.js +5 -12
- package/frontend/skins/crt/app.js +1976 -793
- package/frontend/skins/crt/index.html +313 -23
- package/frontend/skins/crt/styles/billing.css +294 -223
- package/frontend/skins/crt/styles/monochrome-green.css +7 -2
- package/frontend/terminal-replay.js +372 -0
- package/package.json +10 -3
- package/shared/browser-protocol.d.ts +5 -0
- package/shared/browser-protocol.js +130 -0
- package/dist/assets/App-iA2BU9P6.js +0 -124
- package/dist/assets/FileEditorPane-RWiFD2cq.js +0 -5
- package/dist/assets/IconGlyphs-DfL0EBnj.js +0 -1
- package/dist/assets/ProjectFilesSection-Q4PDsWmM.js +0 -12
- package/dist/assets/code-dark-CDkOQAtK.css +0 -1
- package/dist/assets/file-icons-EFUGSSwf.js +0 -1
- package/dist/assets/main-DpYuBonn.css +0 -1
- package/dist/assets/qoder-C9LmmOSf.svg +0 -1
- package/dist/assets/qoder-Cf9gl0Y5.svg +0 -1
- package/dist/assets/qoder-gHCinseV.svg +0 -1
- package/dist/assets/workspace-view-state-CTyDzk2D.js +0 -1
- package/dist/assets/zsh-CLpveKlF.svg +0 -1
- package/dist/assets/zsh-FxSpMPbz.svg +0 -1
- /package/dist/assets/{api-D1lyBYIQ.js → api-D8nyOEbz.js} +0 -0
- /package/dist/assets/{core-ZlAPicox.js → core-D0LFJkDt.js} +0 -0
- /package/dist/assets/{useWorkspaceMenuKeyboard-CneKAZUJ.js → useWorkspaceMenuKeyboard-Brws6Ar9.js} +0 -0
package/backend/server.js
CHANGED
|
@@ -5,10 +5,9 @@ const path = require('path');
|
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const os = require('os');
|
|
7
7
|
const crypto = require('crypto');
|
|
8
|
-
const { execFile } = require('child_process');
|
|
9
|
-
const { promisify } = require('util');
|
|
10
8
|
const { URLSearchParams, pathToFileURL } = require('url');
|
|
11
9
|
const AgentManager = require('./agent-manager');
|
|
10
|
+
const { runtimeKind } = require('./agent-runtime-binding');
|
|
12
11
|
const ConfigManager = require('./config-manager');
|
|
13
12
|
const ThemeManager = require('./theme-manager');
|
|
14
13
|
const TokenAuth = require('./auth');
|
|
@@ -17,6 +16,7 @@ const { listAvailableAgents, resolveCompatibleCodexExecutable } = require('./exe
|
|
|
17
16
|
const { readClaudeSettingsSummary } = require('./claude-settings');
|
|
18
17
|
const { listCodexModelOptions } = require('./codex-models');
|
|
19
18
|
const { listCodexSessions } = require('./codex-session-history');
|
|
19
|
+
const { unarchiveCodexSession } = require('./codex-session-archive');
|
|
20
20
|
const {
|
|
21
21
|
buildAgentSessionResumeCommand,
|
|
22
22
|
findAgentSession,
|
|
@@ -24,6 +24,7 @@ const {
|
|
|
24
24
|
listAgentSessions,
|
|
25
25
|
normalizeProvider,
|
|
26
26
|
paginateAgentSessions,
|
|
27
|
+
resolveCodexResumeModelProvider,
|
|
27
28
|
searchAgentSessions,
|
|
28
29
|
} = require('./agent-session-history');
|
|
29
30
|
const {
|
|
@@ -33,9 +34,11 @@ const {
|
|
|
33
34
|
resumedAgentSource,
|
|
34
35
|
} = require('./main-page-session');
|
|
35
36
|
const { discoverAgentWorkspaces } = require('./workspace-discovery');
|
|
37
|
+
const { createWorkspaceDirectoryRouter } = require('./workspace-directory');
|
|
36
38
|
const { createControlRouter } = require('./control-api');
|
|
37
39
|
const { WorkspaceFileService, WorkspaceFileError } = require('./workspace-file-service');
|
|
38
|
-
const { createWorkspaceFileRouter } = require('./workspace-file-router');
|
|
40
|
+
const { createWorkspaceFileRouter, resolveWorkspaceRoot } = require('./workspace-file-router');
|
|
41
|
+
const { WorkspaceRootRegistry, rootIdForPath } = require('./workspace-root-registry');
|
|
39
42
|
const { UsageMonitor } = require('./usage-monitor');
|
|
40
43
|
const { CodexContextWindowReader } = require('./codex-context-window');
|
|
41
44
|
const { DEFAULT_MAX_TURNS: DEFAULT_CODEX_TRANSCRIPT_MAX_TURNS, readCodexTranscript } = require('./codex-transcript');
|
|
@@ -61,8 +64,18 @@ const {
|
|
|
61
64
|
rewriteIndexHtmlForBasePath,
|
|
62
65
|
appendIndexHtmlAssetToken,
|
|
63
66
|
} = require('./index-html');
|
|
64
|
-
|
|
65
|
-
|
|
67
|
+
const {
|
|
68
|
+
coalesceSessionStream,
|
|
69
|
+
deliverSessionStreamToClients,
|
|
70
|
+
shouldBroadcastSessionStreamImmediately,
|
|
71
|
+
} = require('./session-stream-protocol');
|
|
72
|
+
const {
|
|
73
|
+
MIN_PROTOCOL_VERSION,
|
|
74
|
+
PROTOCOL_VERSION,
|
|
75
|
+
protocolCompatible,
|
|
76
|
+
sanitizeAgentUpdatePatch,
|
|
77
|
+
validateClientMessage,
|
|
78
|
+
} = require('../shared/browser-protocol');
|
|
66
79
|
|
|
67
80
|
const BASE_PATH = normalizeBasePath(process.env.FARMING_BASE_PATH || '/');
|
|
68
81
|
const PORT = process.env.PORT || 3000;
|
|
@@ -98,6 +111,7 @@ const agentManager = new AgentManager(configManager, {
|
|
|
98
111
|
});
|
|
99
112
|
const themeManager = new ThemeManager({ configDir: configManager.farmingDir });
|
|
100
113
|
const workspaceFileService = new WorkspaceFileService();
|
|
114
|
+
const workspaceRootRegistry = new WorkspaceRootRegistry(agentManager);
|
|
101
115
|
const updateService = new FarmingUpdateService({
|
|
102
116
|
rootDir: path.join(__dirname, '..'),
|
|
103
117
|
configDir: configManager.farmingDir,
|
|
@@ -115,7 +129,7 @@ const usageSummaryCache = new AsyncCache(() => usageMonitor.getUsageSummary(), {
|
|
|
115
129
|
});
|
|
116
130
|
const codexModelOptionsCache = new AsyncCache(() => listCodexModelOptions(), {
|
|
117
131
|
ttlMs: 5 * 60_000,
|
|
118
|
-
staleMs:
|
|
132
|
+
staleMs: 5 * 60_000,
|
|
119
133
|
});
|
|
120
134
|
function configuredProviderHomes() {
|
|
121
135
|
const settings = configManager.getSettings();
|
|
@@ -367,11 +381,13 @@ function shareTargetQueryFromBody(body) {
|
|
|
367
381
|
const agentId = shareTargetString(target.agentId, 160);
|
|
368
382
|
const absolutePath = shareTargetString(target.absolutePath, 2048);
|
|
369
383
|
const projectLabel = shareTargetString(target.projectLabel, 160);
|
|
384
|
+
const readingAnchor = shareTargetString(target.readingAnchor, 1800);
|
|
370
385
|
if (!kind || kind === 'agent' && !agentId || kind !== 'agent' && !absolutePath && !agentId && !projectLabel) return '';
|
|
371
386
|
|
|
372
387
|
const params = new URLSearchParams();
|
|
373
388
|
params.set('ftarget', kind);
|
|
374
389
|
if (agentId) params.set('agent', agentId);
|
|
390
|
+
if (kind === 'agent' && readingAnchor && /^[A-Za-z0-9_-]+$/.test(readingAnchor)) params.set('fra', readingAnchor);
|
|
375
391
|
if (absolutePath) params.set('path', absolutePath);
|
|
376
392
|
if (projectLabel) params.set('project', projectLabel);
|
|
377
393
|
|
|
@@ -478,14 +494,28 @@ app.use(`${crtEntryPath}/shared`, express.static(frontendDir, { index: false }))
|
|
|
478
494
|
app.use(`${crtEntryPath}/`, express.static(crtFrontendDir, { index: false }));
|
|
479
495
|
app.use(BASE_PATH || '/', express.static(staticAppDir, { index: false }));
|
|
480
496
|
|
|
481
|
-
app.use(routePath(BASE_PATH, '/api/files'), createWorkspaceFileRouter(agentManager, workspaceFileService
|
|
497
|
+
app.use(routePath(BASE_PATH, '/api/files'), createWorkspaceFileRouter(agentManager, workspaceFileService, {
|
|
498
|
+
rootRegistry: workspaceRootRegistry,
|
|
499
|
+
}));
|
|
482
500
|
|
|
483
501
|
app.use(routePath(BASE_PATH, '/api/review-sessions'), createReviewSessionRouter(reviewSessionService));
|
|
484
502
|
app.use(routePath(BASE_PATH, '/api/reviews'), createReviewDiffRouter(reviewDiffService, reviewSessionService));
|
|
485
503
|
app.use(routePath(BASE_PATH, '/api/reviews'), createReviewStateRouter(reviewStateStore));
|
|
486
504
|
|
|
505
|
+
if (process.env.NODE_ENV === 'test' && process.env.FARMING_E2E_FAKE_EXECUTABLES === '1') {
|
|
506
|
+
app.post(routePath(BASE_PATH, '/api/control/e2e/close-websockets'), express.json(), (_req, res) => {
|
|
507
|
+
const clients = [...wss.clients].filter(client => client.readyState === WebSocket.OPEN);
|
|
508
|
+
res.json({ closing: clients.length, code: 1013 });
|
|
509
|
+
setImmediate(() => {
|
|
510
|
+
clients.forEach(client => client.close(1013, 'terminal stream backpressure test'));
|
|
511
|
+
});
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
|
|
487
515
|
app.use(routePath(BASE_PATH, '/api/control'), createControlRouter(agentManager, {
|
|
488
516
|
notifyUpdate: broadcastState,
|
|
517
|
+
allowConcurrentTestControl: process.env.NODE_ENV === 'test'
|
|
518
|
+
&& process.env.FARMING_E2E_FAKE_EXECUTABLES === '1',
|
|
489
519
|
}));
|
|
490
520
|
|
|
491
521
|
app.use(routePath(BASE_PATH, '/api/app-server'), createAppServerApiRouter({
|
|
@@ -541,6 +571,8 @@ app.get(routePath(BASE_PATH, '/api/workspaces/complete'), async (req, res) => {
|
|
|
541
571
|
}
|
|
542
572
|
});
|
|
543
573
|
|
|
574
|
+
app.use(routePath(BASE_PATH, '/api/workspaces'), createWorkspaceDirectoryRouter());
|
|
575
|
+
|
|
544
576
|
app.get(routePath(BASE_PATH, '/api/skills'), (_req, res) => {
|
|
545
577
|
res.json({ skills: getMainAgentSkillsCatalog() });
|
|
546
578
|
});
|
|
@@ -640,8 +672,16 @@ app.post(
|
|
|
640
672
|
);
|
|
641
673
|
|
|
642
674
|
app.get(routePath(BASE_PATH, '/api/codex/models'), async (req, res) => {
|
|
643
|
-
|
|
644
|
-
|
|
675
|
+
try {
|
|
676
|
+
const catalog = await codexModelOptionsCache.get('catalog');
|
|
677
|
+
res.json(catalog);
|
|
678
|
+
} catch (error) {
|
|
679
|
+
const timedOut = error && error.code === 'CODEX_MODELS_TIMEOUT';
|
|
680
|
+
res.status(timedOut ? 504 : 502).json({
|
|
681
|
+
error: error && error.message ? error.message : 'Failed to load Codex model catalog',
|
|
682
|
+
code: error && error.code ? error.code : 'CODEX_MODELS_FAILED',
|
|
683
|
+
});
|
|
684
|
+
}
|
|
645
685
|
});
|
|
646
686
|
|
|
647
687
|
app.get(routePath(BASE_PATH, '/api/claude/settings'), (req, res) => {
|
|
@@ -662,7 +702,10 @@ app.get(routePath(BASE_PATH, '/api/usage'), async (req, res) => {
|
|
|
662
702
|
app.get(routePath(BASE_PATH, '/api/usage/day'), async (req, res) => {
|
|
663
703
|
try {
|
|
664
704
|
const date = String(req.query.date || '').trim();
|
|
665
|
-
const detail = await usageMonitor.getUsageDay(date, {
|
|
705
|
+
const detail = await usageMonitor.getUsageDay(date, {
|
|
706
|
+
fresh: req.query.fresh === '1',
|
|
707
|
+
live: req.query.live === '1',
|
|
708
|
+
});
|
|
666
709
|
res.json({ detail });
|
|
667
710
|
} catch (error) {
|
|
668
711
|
const invalidDate = error instanceof RangeError;
|
|
@@ -943,7 +986,7 @@ app.get(routePath(BASE_PATH, '/api/agents/:agentId/codex-transcript'), async (re
|
|
|
943
986
|
: DEFAULT_CODEX_TRANSCRIPT_MAX_TURNS;
|
|
944
987
|
const transcript = await readCodexTranscript(providerSession.sessionId, {
|
|
945
988
|
maxTurns,
|
|
946
|
-
codexHome: providerSession
|
|
989
|
+
codexHome: runtimeKind(providerSession) === 'app-server'
|
|
947
990
|
? (providerSession.codexAppServerHomePath || providerSession.providerHomePath || '')
|
|
948
991
|
: (providerSession.providerHomePath || ''),
|
|
949
992
|
});
|
|
@@ -1222,6 +1265,38 @@ app.delete(routePath(BASE_PATH, '/api/agents/:agentId/codex-goal'), async (req,
|
|
|
1222
1265
|
}
|
|
1223
1266
|
});
|
|
1224
1267
|
|
|
1268
|
+
app.post(routePath(BASE_PATH, '/api/agents/:agentId/codex-terminal-profile'), express.json(), async (req, res) => {
|
|
1269
|
+
const requestAbort = new globalThis.AbortController();
|
|
1270
|
+
const abortRequest = () => requestAbort.abort(new Error('Terminal profile request was canceled'));
|
|
1271
|
+
const abortClosedResponse = () => {
|
|
1272
|
+
if (!res.writableEnded) abortRequest();
|
|
1273
|
+
};
|
|
1274
|
+
req.once('aborted', abortRequest);
|
|
1275
|
+
res.once('close', abortClosedResponse);
|
|
1276
|
+
try {
|
|
1277
|
+
const profile = await agentManager.setCodexTerminalProfile(req.params.agentId, {
|
|
1278
|
+
model: req.body?.model,
|
|
1279
|
+
effort: req.body?.effort,
|
|
1280
|
+
serviceTier: req.body?.serviceTier,
|
|
1281
|
+
}, {
|
|
1282
|
+
signal: requestAbort.signal,
|
|
1283
|
+
timeoutMs: 44_000,
|
|
1284
|
+
});
|
|
1285
|
+
res.json({ profile });
|
|
1286
|
+
} catch (error) {
|
|
1287
|
+
const message = error && error.message ? error.message : 'Failed to update Codex Terminal profile';
|
|
1288
|
+
const status = message === 'Agent not found'
|
|
1289
|
+
? 404
|
|
1290
|
+
: /^A valid Codex /.test(message)
|
|
1291
|
+
? 400
|
|
1292
|
+
: 409;
|
|
1293
|
+
if (!res.headersSent && !res.destroyed) res.status(status).json({ error: message });
|
|
1294
|
+
} finally {
|
|
1295
|
+
req.off('aborted', abortRequest);
|
|
1296
|
+
res.off('close', abortClosedResponse);
|
|
1297
|
+
}
|
|
1298
|
+
});
|
|
1299
|
+
|
|
1225
1300
|
app.patch(routePath(BASE_PATH, '/api/agents/:agentId'), express.json(), async (req, res) => {
|
|
1226
1301
|
const body = req.body || {};
|
|
1227
1302
|
const updates = {};
|
|
@@ -1253,6 +1328,15 @@ app.patch(routePath(BASE_PATH, '/api/agents/:agentId'), express.json(), async (r
|
|
|
1253
1328
|
if (typeof body.readAttentionSeq === 'number' && Number.isFinite(body.readAttentionSeq)) {
|
|
1254
1329
|
flagPatch.readAttentionSeq = body.readAttentionSeq;
|
|
1255
1330
|
}
|
|
1331
|
+
if (
|
|
1332
|
+
typeof body.readOutputEpoch === 'string'
|
|
1333
|
+
&& body.readOutputEpoch
|
|
1334
|
+
&& typeof body.readOutputSeq === 'number'
|
|
1335
|
+
&& Number.isFinite(body.readOutputSeq)
|
|
1336
|
+
) {
|
|
1337
|
+
flagPatch.readOutputEpoch = body.readOutputEpoch;
|
|
1338
|
+
flagPatch.readOutputSeq = body.readOutputSeq;
|
|
1339
|
+
}
|
|
1256
1340
|
|
|
1257
1341
|
if (flagPatch.archived === true) {
|
|
1258
1342
|
const result = await agentManager.archiveAgent(req.params.agentId);
|
|
@@ -1266,6 +1350,7 @@ app.patch(routePath(BASE_PATH, '/api/agents/:agentId'), express.json(), async (r
|
|
|
1266
1350
|
delete flagPatch.archived;
|
|
1267
1351
|
}
|
|
1268
1352
|
|
|
1353
|
+
let flagUpdateRequiresState = false;
|
|
1269
1354
|
if (Object.keys(flagPatch).length > 0) {
|
|
1270
1355
|
const result = agentManager.updateAgentFlags(req.params.agentId, flagPatch);
|
|
1271
1356
|
if (result.error) {
|
|
@@ -1275,6 +1360,7 @@ app.patch(routePath(BASE_PATH, '/api/agents/:agentId'), express.json(), async (r
|
|
|
1275
1360
|
}
|
|
1276
1361
|
Object.assign(updates, result);
|
|
1277
1362
|
delete updates.agentId;
|
|
1363
|
+
flagUpdateRequiresState = result.requiresState === true;
|
|
1278
1364
|
}
|
|
1279
1365
|
|
|
1280
1366
|
if (typeof body.launchPermissionMode === 'string') {
|
|
@@ -1304,11 +1390,13 @@ app.patch(routePath(BASE_PATH, '/api/agents/:agentId'), express.json(), async (r
|
|
|
1304
1390
|
}
|
|
1305
1391
|
|
|
1306
1392
|
if (Object.keys(updates).length === 0) {
|
|
1307
|
-
res.status(400).json({ error: 'customTitle, task, pinned, unread, archived, readAttentionSeq, launchPermissionMode, or agentRuntimeMode is required' });
|
|
1393
|
+
res.status(400).json({ error: 'customTitle, task, pinned, unread, archived, readAttentionSeq, readOutputEpoch/readOutputSeq, launchPermissionMode, or agentRuntimeMode is required' });
|
|
1308
1394
|
return;
|
|
1309
1395
|
}
|
|
1310
1396
|
|
|
1311
|
-
|
|
1397
|
+
if (flagUpdateRequiresState || typeof body.task === 'string' || typeof body.customTitle === 'string' || typeof body.launchPermissionMode === 'string' || typeof body.agentRuntimeMode === 'string') {
|
|
1398
|
+
scheduleBroadcastState();
|
|
1399
|
+
}
|
|
1312
1400
|
res.json({ agentId: req.params.agentId, ...updates });
|
|
1313
1401
|
});
|
|
1314
1402
|
|
|
@@ -1427,38 +1515,6 @@ function forgetMainPageAgentSession(provider, sessionId, providerHomeId = '') {
|
|
|
1427
1515
|
});
|
|
1428
1516
|
}
|
|
1429
1517
|
|
|
1430
|
-
async function unarchiveCodexSession(sessionId, session = {}) {
|
|
1431
|
-
const codexResolution = resolveCompatibleCodexExecutable(session.cliVersion || '');
|
|
1432
|
-
if (!codexResolution.compatible) {
|
|
1433
|
-
return {
|
|
1434
|
-
error: codexResolution.error || 'Codex CLI is not compatible with this session',
|
|
1435
|
-
status: 400,
|
|
1436
|
-
};
|
|
1437
|
-
}
|
|
1438
|
-
|
|
1439
|
-
try {
|
|
1440
|
-
await execFileAsync(codexResolution.path || 'codex', ['unarchive', sessionId], {
|
|
1441
|
-
cwd: session.cwd || session.workspace || os.homedir(),
|
|
1442
|
-
env: session.providerHomePath
|
|
1443
|
-
? { ...process.env, CODEX_HOME: session.providerHomePath }
|
|
1444
|
-
: process.env,
|
|
1445
|
-
timeout: 30_000,
|
|
1446
|
-
maxBuffer: 1024 * 1024,
|
|
1447
|
-
});
|
|
1448
|
-
return { unarchived: true };
|
|
1449
|
-
} catch (error) {
|
|
1450
|
-
const message = [
|
|
1451
|
-
error && error.stdout ? String(error.stdout).trim() : '',
|
|
1452
|
-
error && error.stderr ? String(error.stderr).trim() : '',
|
|
1453
|
-
error && error.message ? String(error.message).trim() : '',
|
|
1454
|
-
].filter(Boolean).join('\n') || 'failed to unarchive Codex session';
|
|
1455
|
-
return {
|
|
1456
|
-
error: message,
|
|
1457
|
-
status: 409,
|
|
1458
|
-
};
|
|
1459
|
-
}
|
|
1460
|
-
}
|
|
1461
|
-
|
|
1462
1518
|
async function resumeAgentSessionById(provider, rawSessionId, options = {}) {
|
|
1463
1519
|
const normalizedProvider = normalizeProvider(provider);
|
|
1464
1520
|
const sessionId = String(rawSessionId || '').trim();
|
|
@@ -1551,6 +1607,9 @@ async function resumeAgentSessionById(provider, rawSessionId, options = {}) {
|
|
|
1551
1607
|
const command = buildAgentSessionResumeCommand(normalizedProvider, sessionId, {
|
|
1552
1608
|
fork: shouldFork,
|
|
1553
1609
|
cwd: workingDirectory,
|
|
1610
|
+
modelProvider: normalizedProvider === 'codex'
|
|
1611
|
+
? resolveCodexResumeModelProvider(session ? session.providerHomePath : '')
|
|
1612
|
+
: '',
|
|
1554
1613
|
});
|
|
1555
1614
|
|
|
1556
1615
|
if (!command) {
|
|
@@ -1584,6 +1643,7 @@ async function resumeAgentSessionById(provider, rawSessionId, options = {}) {
|
|
|
1584
1643
|
providerHomeId: resolvedProviderHomeId,
|
|
1585
1644
|
providerHomePath: session ? (session.providerHomePath || '') : '',
|
|
1586
1645
|
autoReadInitialAttention: options.autoReadInitialAttention === true,
|
|
1646
|
+
preserveProviderSessionProfile: normalizedProvider === 'codex',
|
|
1587
1647
|
});
|
|
1588
1648
|
Promise.resolve(startResult).catch((error) => {
|
|
1589
1649
|
resolve({ error: error.message || 'failed to resume agent session', status: 500 });
|
|
@@ -1653,10 +1713,29 @@ async function autoResumeMainPageAgentSessions() {
|
|
|
1653
1713
|
const sessions = mainPageAgentSessionsToAutoResume(configManager.getSettings());
|
|
1654
1714
|
if (sessions.length === 0) return;
|
|
1655
1715
|
|
|
1716
|
+
let knownSessions;
|
|
1717
|
+
try {
|
|
1718
|
+
knownSessions = await agentSessionsCache.get(
|
|
1719
|
+
JSON.stringify(configManager.getSettings().agentHomes || {}),
|
|
1720
|
+
{ maxAgeMs: INTERACTIVE_REFRESH_CACHE_MAX_AGE_MS }
|
|
1721
|
+
);
|
|
1722
|
+
} catch (error) {
|
|
1723
|
+
console.warn('Failed to load Agent session catalog for auto-resume:', error && (error.message || error));
|
|
1724
|
+
return;
|
|
1725
|
+
}
|
|
1726
|
+
const knownSessionByKey = new Map(knownSessions.map(session => [
|
|
1727
|
+
mainPageAgentSessionKey(session.provider, session.id, session.providerHomeId || 'default'),
|
|
1728
|
+
session,
|
|
1729
|
+
]));
|
|
1730
|
+
|
|
1656
1731
|
let resumedCount = 0;
|
|
1657
1732
|
for (const session of sessions) {
|
|
1658
1733
|
try {
|
|
1659
|
-
const sessionDetails =
|
|
1734
|
+
const sessionDetails = knownSessionByKey.get(mainPageAgentSessionKey(
|
|
1735
|
+
session.provider,
|
|
1736
|
+
session.sessionId,
|
|
1737
|
+
session.providerHomeId || 'default'
|
|
1738
|
+
));
|
|
1660
1739
|
if (!sessionDetails) {
|
|
1661
1740
|
console.warn('Dropping stale main-page session from auto-resume:', session.provider, session.sessionId);
|
|
1662
1741
|
forgetMainPageAgentSession(session.provider, session.sessionId, session.providerHomeId || 'default');
|
|
@@ -1705,7 +1784,7 @@ app.post(routePath(BASE_PATH, '/api/settings'), express.json(), (req, res) => {
|
|
|
1705
1784
|
success: true,
|
|
1706
1785
|
settings: configManager.getSettings()
|
|
1707
1786
|
});
|
|
1708
|
-
|
|
1787
|
+
scheduleBroadcastState();
|
|
1709
1788
|
});
|
|
1710
1789
|
|
|
1711
1790
|
app.post(routePath(BASE_PATH, '/api/themes/:themeId/set'), express.json(), (req, res) => {
|
|
@@ -1773,13 +1852,30 @@ wss.on('connection', (ws, req) => {
|
|
|
1773
1852
|
}
|
|
1774
1853
|
|
|
1775
1854
|
console.log('Client connected');
|
|
1855
|
+
ws.connectionId = crypto.randomUUID();
|
|
1776
1856
|
|
|
1777
1857
|
ws.on('message', (message) => {
|
|
1778
1858
|
try {
|
|
1779
1859
|
const data = JSON.parse(message);
|
|
1860
|
+
const validation = validateClientMessage(data);
|
|
1861
|
+
if (!validation.ok) {
|
|
1862
|
+
ws.send(JSON.stringify({
|
|
1863
|
+
type: 'protocol-error',
|
|
1864
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
1865
|
+
requestId: typeof data?.requestId === 'string' ? data.requestId : '',
|
|
1866
|
+
message: validation.error,
|
|
1867
|
+
}));
|
|
1868
|
+
return;
|
|
1869
|
+
}
|
|
1780
1870
|
handleMessage(ws, data);
|
|
1781
1871
|
} catch (e) {
|
|
1782
1872
|
console.error('Failed to parse message:', e);
|
|
1873
|
+
ws.send(JSON.stringify({
|
|
1874
|
+
type: 'protocol-error',
|
|
1875
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
1876
|
+
requestId: '',
|
|
1877
|
+
message: 'message must be valid JSON',
|
|
1878
|
+
}));
|
|
1783
1879
|
}
|
|
1784
1880
|
});
|
|
1785
1881
|
|
|
@@ -1788,6 +1884,11 @@ wss.on('connection', (ws, req) => {
|
|
|
1788
1884
|
console.log('Client disconnected');
|
|
1789
1885
|
});
|
|
1790
1886
|
|
|
1887
|
+
ws.send(JSON.stringify({
|
|
1888
|
+
type: 'protocol-hello',
|
|
1889
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
1890
|
+
minProtocolVersion: MIN_PROTOCOL_VERSION,
|
|
1891
|
+
}));
|
|
1791
1892
|
sendState(ws);
|
|
1792
1893
|
});
|
|
1793
1894
|
|
|
@@ -1872,6 +1973,15 @@ async function sendComposerInputMessage(ws, data) {
|
|
|
1872
1973
|
}
|
|
1873
1974
|
}
|
|
1874
1975
|
if (!targetAgentId || content.length === 0) return;
|
|
1976
|
+
const targetAgent = agentManager.getState().agents.find(agent => agent.id === targetAgentId);
|
|
1977
|
+
const structuredRuntime = targetAgent && runtimeKind(targetAgent) !== 'terminal';
|
|
1978
|
+
if (!structuredRuntime) {
|
|
1979
|
+
ws.send(JSON.stringify({
|
|
1980
|
+
type: 'error',
|
|
1981
|
+
message: 'Terminal Composer input requires the active terminal owner',
|
|
1982
|
+
}));
|
|
1983
|
+
return;
|
|
1984
|
+
}
|
|
1875
1985
|
try {
|
|
1876
1986
|
await agentManager.sendComposerMessage(targetAgentId, content);
|
|
1877
1987
|
} catch (error) {
|
|
@@ -1901,6 +2011,13 @@ async function respondToAppServerRequest(ws, data) {
|
|
|
1901
2011
|
|
|
1902
2012
|
function handleMessage(ws, data) {
|
|
1903
2013
|
switch (data.type) {
|
|
2014
|
+
case 'protocol-hello':
|
|
2015
|
+
if (!protocolCompatible(data.protocolVersion)) {
|
|
2016
|
+
ws.close(4002, `Unsupported Farming protocol version ${data.protocolVersion}`);
|
|
2017
|
+
return;
|
|
2018
|
+
}
|
|
2019
|
+
ws.protocolVersion = data.protocolVersion;
|
|
2020
|
+
break;
|
|
1904
2021
|
case 'start-agent': {
|
|
1905
2022
|
const workspace = data.workspace || null;
|
|
1906
2023
|
agentManager.startAgent(data.command, workspace, (agentId, error) => {
|
|
@@ -1964,7 +2081,7 @@ function handleMessage(ws, data) {
|
|
|
1964
2081
|
|
|
1965
2082
|
case 'interrupt-agent':
|
|
1966
2083
|
if (data.agentId) {
|
|
1967
|
-
agentManager.interruptAgent(data.agentId);
|
|
2084
|
+
void agentManager.interruptAgent(data.agentId);
|
|
1968
2085
|
}
|
|
1969
2086
|
break;
|
|
1970
2087
|
|
|
@@ -1983,10 +2100,14 @@ function handleMessage(ws, data) {
|
|
|
1983
2100
|
|
|
1984
2101
|
case 'resize-agent':
|
|
1985
2102
|
if (data.agentId && Number.isFinite(data.cols) && Number.isFinite(data.rows)) {
|
|
1986
|
-
agentManager.
|
|
2103
|
+
agentManager.requestAgentSessionResize(data.agentId, data.cols, data.rows);
|
|
1987
2104
|
}
|
|
1988
2105
|
break;
|
|
1989
2106
|
|
|
2107
|
+
case 'clear-terminal':
|
|
2108
|
+
if (data.agentId) void agentManager.clearAgentSessionBuffer(data.agentId);
|
|
2109
|
+
break;
|
|
2110
|
+
|
|
1990
2111
|
case 'watch-workspace-files':
|
|
1991
2112
|
watchWorkspaceFiles(ws, data);
|
|
1992
2113
|
break;
|
|
@@ -2054,10 +2175,7 @@ async function watchWorkspaceFiles(ws, data) {
|
|
|
2054
2175
|
return;
|
|
2055
2176
|
}
|
|
2056
2177
|
|
|
2057
|
-
const root = agentManager
|
|
2058
|
-
if (!root) {
|
|
2059
|
-
throw new WorkspaceFileError('agent not found', 404);
|
|
2060
|
-
}
|
|
2178
|
+
const root = resolveWorkspaceRoot(agentManager, data.agentId);
|
|
2061
2179
|
|
|
2062
2180
|
const unsubscribe = await workspaceFileService.subscribe(root, (event) => {
|
|
2063
2181
|
if (ws.readyState !== WebSocket.OPEN) return;
|
|
@@ -2081,8 +2199,14 @@ async function watchWorkspaceFiles(ws, data) {
|
|
|
2081
2199
|
}
|
|
2082
2200
|
|
|
2083
2201
|
function buildStatePayload() {
|
|
2202
|
+
const state = agentManager.getState();
|
|
2084
2203
|
return {
|
|
2085
|
-
...
|
|
2204
|
+
...state,
|
|
2205
|
+
agents: state.agents.map(agent => ({
|
|
2206
|
+
...agent,
|
|
2207
|
+
workspaceRootId: rootIdForPath(agent.projectWorkspace || agent.gitWorktree?.workspace || agent.cwd),
|
|
2208
|
+
})),
|
|
2209
|
+
workspaceRoots: workspaceRootRegistry.list(),
|
|
2086
2210
|
mainPageSessionKeys: typeof configManager.getMainPageSessionKeys === 'function'
|
|
2087
2211
|
? configManager.getMainPageSessionKeys()
|
|
2088
2212
|
: (Array.isArray(configManager.getSettings().mainPageSessionKeys) ? configManager.getSettings().mainPageSessionKeys : []),
|
|
@@ -2097,22 +2221,39 @@ function sendState(ws) {
|
|
|
2097
2221
|
});
|
|
2098
2222
|
}
|
|
2099
2223
|
|
|
2224
|
+
function previewForClient(preview, client) {
|
|
2225
|
+
if (!preview || !client || client.focusedAgentId !== preview.agentId || !preview.previewSnapshot) {
|
|
2226
|
+
return preview;
|
|
2227
|
+
}
|
|
2228
|
+
// The active terminal is hydrated from its authoritative session-view and
|
|
2229
|
+
// receives live session-output. Sending its sidebar snapshot again adds a
|
|
2230
|
+
// large, unrelated React update to every keystroke in a full-screen TUI.
|
|
2231
|
+
return {
|
|
2232
|
+
...preview,
|
|
2233
|
+
previewSnapshot: null,
|
|
2234
|
+
};
|
|
2235
|
+
}
|
|
2236
|
+
|
|
2100
2237
|
function sendPreview(ws, preview) {
|
|
2101
2238
|
ws.send(JSON.stringify({
|
|
2102
2239
|
type: 'session-preview',
|
|
2103
|
-
preview,
|
|
2240
|
+
preview: previewForClient(preview, ws),
|
|
2104
2241
|
}));
|
|
2105
2242
|
}
|
|
2106
2243
|
|
|
2107
2244
|
const STATE_BROADCAST_INTERVAL_MS = 120;
|
|
2108
2245
|
const PREVIEW_BROADCAST_INTERVAL_MS = 500;
|
|
2109
2246
|
const SESSION_STREAM_BROADCAST_INTERVAL_MS = 33;
|
|
2247
|
+
const AGENT_ACTIVITY_BROADCAST_DELAY_MS = SESSION_STREAM_BROADCAST_INTERVAL_MS;
|
|
2110
2248
|
const MAX_SESSION_STREAM_CLIENT_BUFFERED_AMOUNT = 4 * 1024 * 1024;
|
|
2111
2249
|
let stateBroadcastTimer = null;
|
|
2112
2250
|
let lastStateBroadcastAt = 0;
|
|
2113
2251
|
const pendingPreviewBroadcasts = new Map();
|
|
2252
|
+
const pendingAgentActivityBroadcasts = new Map();
|
|
2253
|
+
const pendingAgentUpdates = new Map();
|
|
2114
2254
|
const pendingSessionStreams = new Map();
|
|
2115
2255
|
let sessionStreamBroadcastTimer = null;
|
|
2256
|
+
let lastSessionStreamBroadcastAt = 0;
|
|
2116
2257
|
let shutdownStarted = false;
|
|
2117
2258
|
|
|
2118
2259
|
function broadcastState() {
|
|
@@ -2146,16 +2287,11 @@ function scheduleBroadcastState() {
|
|
|
2146
2287
|
}
|
|
2147
2288
|
|
|
2148
2289
|
function broadcastSessionPreview(preview) {
|
|
2149
|
-
const message = JSON.stringify({
|
|
2150
|
-
type: 'session-preview',
|
|
2151
|
-
preview,
|
|
2152
|
-
});
|
|
2153
|
-
|
|
2154
2290
|
wss.clients.forEach((client) => {
|
|
2155
2291
|
const previewAllowed = client.previewScope !== 'none'
|
|
2156
2292
|
&& (client.previewScope !== 'focused' || client.focusedAgentId === preview.agentId);
|
|
2157
2293
|
if (client.readyState === WebSocket.OPEN && previewAllowed) {
|
|
2158
|
-
client
|
|
2294
|
+
sendPreview(client, preview);
|
|
2159
2295
|
}
|
|
2160
2296
|
});
|
|
2161
2297
|
}
|
|
@@ -2214,6 +2350,78 @@ agentManager.on('provider-session-updated', () => {
|
|
|
2214
2350
|
scheduleBroadcastState();
|
|
2215
2351
|
});
|
|
2216
2352
|
|
|
2353
|
+
function broadcastAgentActivity(activity) {
|
|
2354
|
+
const message = JSON.stringify({
|
|
2355
|
+
type: 'agent-activity',
|
|
2356
|
+
activity,
|
|
2357
|
+
});
|
|
2358
|
+
wss.clients.forEach((client) => {
|
|
2359
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
2360
|
+
client.send(message);
|
|
2361
|
+
}
|
|
2362
|
+
});
|
|
2363
|
+
}
|
|
2364
|
+
|
|
2365
|
+
function scheduleAgentActivityBroadcast(activity) {
|
|
2366
|
+
if (!activity || !activity.agentId) return;
|
|
2367
|
+
const agentId = activity.agentId;
|
|
2368
|
+
const existing = pendingAgentActivityBroadcasts.get(agentId);
|
|
2369
|
+
if (existing) {
|
|
2370
|
+
existing.activity = activity;
|
|
2371
|
+
return;
|
|
2372
|
+
}
|
|
2373
|
+
const entry = {
|
|
2374
|
+
activity,
|
|
2375
|
+
timer: setTimeout(() => {
|
|
2376
|
+
pendingAgentActivityBroadcasts.delete(agentId);
|
|
2377
|
+
broadcastAgentActivity(entry.activity);
|
|
2378
|
+
}, AGENT_ACTIVITY_BROADCAST_DELAY_MS),
|
|
2379
|
+
};
|
|
2380
|
+
entry.timer.unref?.();
|
|
2381
|
+
pendingAgentActivityBroadcasts.set(agentId, entry);
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2384
|
+
agentManager.onAgentActivity(scheduleAgentActivityBroadcast);
|
|
2385
|
+
|
|
2386
|
+
function scheduleAgentUpdate(update) {
|
|
2387
|
+
const patch = sanitizeAgentUpdatePatch(update?.patch);
|
|
2388
|
+
if (!update?.agentId || !patch) return;
|
|
2389
|
+
const existing = pendingAgentUpdates.get(update.agentId);
|
|
2390
|
+
if (existing) {
|
|
2391
|
+
Object.assign(existing.patch, patch);
|
|
2392
|
+
return;
|
|
2393
|
+
}
|
|
2394
|
+
const entry = {
|
|
2395
|
+
patch,
|
|
2396
|
+
timer: setTimeout(() => {
|
|
2397
|
+
pendingAgentUpdates.delete(update.agentId);
|
|
2398
|
+
const message = JSON.stringify({
|
|
2399
|
+
type: 'agent-update',
|
|
2400
|
+
update: { agentId: update.agentId, patch: entry.patch },
|
|
2401
|
+
});
|
|
2402
|
+
wss.clients.forEach((client) => {
|
|
2403
|
+
if (client.readyState === WebSocket.OPEN) client.send(message);
|
|
2404
|
+
});
|
|
2405
|
+
}, AGENT_ACTIVITY_BROADCAST_DELAY_MS),
|
|
2406
|
+
};
|
|
2407
|
+
entry.timer.unref?.();
|
|
2408
|
+
pendingAgentUpdates.set(update.agentId, entry);
|
|
2409
|
+
}
|
|
2410
|
+
|
|
2411
|
+
agentManager.on('agent-update', scheduleAgentUpdate);
|
|
2412
|
+
|
|
2413
|
+
function broadcastAgentRead(read) {
|
|
2414
|
+
if (!read || !read.agentId) return;
|
|
2415
|
+
const message = JSON.stringify({ type: 'agent-read', read });
|
|
2416
|
+
wss.clients.forEach((client) => {
|
|
2417
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
2418
|
+
client.send(message);
|
|
2419
|
+
}
|
|
2420
|
+
});
|
|
2421
|
+
}
|
|
2422
|
+
|
|
2423
|
+
agentManager.on('agent-read', broadcastAgentRead);
|
|
2424
|
+
|
|
2217
2425
|
function sessionStreamKey(stream) {
|
|
2218
2426
|
return `${stream.agentId || ''}\0${stream.sessionSource || ''}`;
|
|
2219
2427
|
}
|
|
@@ -2223,13 +2431,10 @@ function broadcastSessionStream(stream) {
|
|
|
2223
2431
|
type: 'session-output',
|
|
2224
2432
|
stream
|
|
2225
2433
|
});
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
if (client.bufferedAmount > MAX_SESSION_STREAM_CLIENT_BUFFERED_AMOUNT) return;
|
|
2231
|
-
client.send(message);
|
|
2232
|
-
}
|
|
2434
|
+
deliverSessionStreamToClients(wss.clients, stream, {
|
|
2435
|
+
openState: WebSocket.OPEN,
|
|
2436
|
+
maxBufferedAmount: MAX_SESSION_STREAM_CLIENT_BUFFERED_AMOUNT,
|
|
2437
|
+
message,
|
|
2233
2438
|
});
|
|
2234
2439
|
}
|
|
2235
2440
|
|
|
@@ -2237,31 +2442,26 @@ function flushSessionStreams() {
|
|
|
2237
2442
|
sessionStreamBroadcastTimer = null;
|
|
2238
2443
|
const streams = Array.from(pendingSessionStreams.values());
|
|
2239
2444
|
pendingSessionStreams.clear();
|
|
2445
|
+
if (streams.length > 0) lastSessionStreamBroadcastAt = Date.now();
|
|
2240
2446
|
streams.forEach(broadcastSessionStream);
|
|
2241
2447
|
}
|
|
2242
2448
|
|
|
2243
2449
|
function scheduleSessionStreamBroadcast(stream) {
|
|
2244
2450
|
if (!stream || !stream.agentId) return;
|
|
2451
|
+
const now = Date.now();
|
|
2452
|
+
if (shouldBroadcastSessionStreamImmediately({
|
|
2453
|
+
pendingCount: pendingSessionStreams.size,
|
|
2454
|
+
lastBroadcastAt: lastSessionStreamBroadcastAt,
|
|
2455
|
+
now,
|
|
2456
|
+
intervalMs: SESSION_STREAM_BROADCAST_INTERVAL_MS,
|
|
2457
|
+
})) {
|
|
2458
|
+
lastSessionStreamBroadcastAt = now;
|
|
2459
|
+
broadcastSessionStream(coalesceSessionStream(null, stream));
|
|
2460
|
+
return;
|
|
2461
|
+
}
|
|
2245
2462
|
const key = sessionStreamKey(stream);
|
|
2246
2463
|
const existing = pendingSessionStreams.get(key);
|
|
2247
|
-
|
|
2248
|
-
const outputSeq = Number.isFinite(stream.outputSeq) ? stream.outputSeq : undefined;
|
|
2249
|
-
|
|
2250
|
-
if (existing) {
|
|
2251
|
-
pendingSessionStreams.set(key, {
|
|
2252
|
-
...stream,
|
|
2253
|
-
data: stream.replace === true ? data : `${existing.data || ''}${data}`,
|
|
2254
|
-
replace: existing.replace || stream.replace === true,
|
|
2255
|
-
outputSeq: outputSeq ?? existing.outputSeq,
|
|
2256
|
-
});
|
|
2257
|
-
} else {
|
|
2258
|
-
pendingSessionStreams.set(key, {
|
|
2259
|
-
...stream,
|
|
2260
|
-
data,
|
|
2261
|
-
replace: stream.replace === true,
|
|
2262
|
-
outputSeq,
|
|
2263
|
-
});
|
|
2264
|
-
}
|
|
2464
|
+
pendingSessionStreams.set(key, coalesceSessionStream(existing, stream));
|
|
2265
2465
|
|
|
2266
2466
|
if (!sessionStreamBroadcastTimer) {
|
|
2267
2467
|
sessionStreamBroadcastTimer = setTimeout(flushSessionStreams, SESSION_STREAM_BROADCAST_INTERVAL_MS);
|
|
@@ -2280,6 +2480,14 @@ function clearBroadcastTimers() {
|
|
|
2280
2480
|
}
|
|
2281
2481
|
}
|
|
2282
2482
|
pendingPreviewBroadcasts.clear();
|
|
2483
|
+
for (const entry of pendingAgentActivityBroadcasts.values()) {
|
|
2484
|
+
if (entry && entry.timer) clearTimeout(entry.timer);
|
|
2485
|
+
}
|
|
2486
|
+
pendingAgentActivityBroadcasts.clear();
|
|
2487
|
+
for (const entry of pendingAgentUpdates.values()) {
|
|
2488
|
+
if (entry && entry.timer) clearTimeout(entry.timer);
|
|
2489
|
+
}
|
|
2490
|
+
pendingAgentUpdates.clear();
|
|
2283
2491
|
if (sessionStreamBroadcastTimer) {
|
|
2284
2492
|
clearTimeout(sessionStreamBroadcastTimer);
|
|
2285
2493
|
sessionStreamBroadcastTimer = null;
|