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/server.js
CHANGED
|
@@ -9,6 +9,7 @@ const { execFile } = require('child_process');
|
|
|
9
9
|
const { promisify } = require('util');
|
|
10
10
|
const { URLSearchParams, pathToFileURL } = require('url');
|
|
11
11
|
const AgentManager = require('./agent-manager');
|
|
12
|
+
const { runtimeKind } = require('./agent-runtime-binding');
|
|
12
13
|
const ConfigManager = require('./config-manager');
|
|
13
14
|
const ThemeManager = require('./theme-manager');
|
|
14
15
|
const TokenAuth = require('./auth');
|
|
@@ -17,6 +18,7 @@ const { listAvailableAgents, resolveCompatibleCodexExecutable } = require('./exe
|
|
|
17
18
|
const { readClaudeSettingsSummary } = require('./claude-settings');
|
|
18
19
|
const { listCodexModelOptions } = require('./codex-models');
|
|
19
20
|
const { listCodexSessions } = require('./codex-session-history');
|
|
21
|
+
const { unarchiveCodexSession } = require('./codex-session-archive');
|
|
20
22
|
const {
|
|
21
23
|
buildAgentSessionResumeCommand,
|
|
22
24
|
findAgentSession,
|
|
@@ -24,6 +26,7 @@ const {
|
|
|
24
26
|
listAgentSessions,
|
|
25
27
|
normalizeProvider,
|
|
26
28
|
paginateAgentSessions,
|
|
29
|
+
resolveCodexResumeModelProvider,
|
|
27
30
|
searchAgentSessions,
|
|
28
31
|
} = require('./agent-session-history');
|
|
29
32
|
const {
|
|
@@ -33,9 +36,11 @@ const {
|
|
|
33
36
|
resumedAgentSource,
|
|
34
37
|
} = require('./main-page-session');
|
|
35
38
|
const { discoverAgentWorkspaces } = require('./workspace-discovery');
|
|
39
|
+
const { createWorkspaceDirectoryRouter } = require('./workspace-directory');
|
|
36
40
|
const { createControlRouter } = require('./control-api');
|
|
37
41
|
const { WorkspaceFileService, WorkspaceFileError } = require('./workspace-file-service');
|
|
38
|
-
const { createWorkspaceFileRouter } = require('./workspace-file-router');
|
|
42
|
+
const { createWorkspaceFileRouter, resolveWorkspaceRoot } = require('./workspace-file-router');
|
|
43
|
+
const { WorkspaceRootRegistry, rootIdForPath } = require('./workspace-root-registry');
|
|
39
44
|
const { UsageMonitor } = require('./usage-monitor');
|
|
40
45
|
const { CodexContextWindowReader } = require('./codex-context-window');
|
|
41
46
|
const { DEFAULT_MAX_TURNS: DEFAULT_CODEX_TRANSCRIPT_MAX_TURNS, readCodexTranscript } = require('./codex-transcript');
|
|
@@ -61,8 +66,18 @@ const {
|
|
|
61
66
|
rewriteIndexHtmlForBasePath,
|
|
62
67
|
appendIndexHtmlAssetToken,
|
|
63
68
|
} = require('./index-html');
|
|
64
|
-
|
|
65
|
-
|
|
69
|
+
const {
|
|
70
|
+
coalesceSessionStream,
|
|
71
|
+
deliverSessionStreamToClients,
|
|
72
|
+
shouldBroadcastSessionStreamImmediately,
|
|
73
|
+
} = require('./session-stream-protocol');
|
|
74
|
+
const {
|
|
75
|
+
MIN_PROTOCOL_VERSION,
|
|
76
|
+
PROTOCOL_VERSION,
|
|
77
|
+
protocolCompatible,
|
|
78
|
+
sanitizeAgentUpdatePatch,
|
|
79
|
+
validateClientMessage,
|
|
80
|
+
} = require('../shared/browser-protocol');
|
|
66
81
|
|
|
67
82
|
const BASE_PATH = normalizeBasePath(process.env.FARMING_BASE_PATH || '/');
|
|
68
83
|
const PORT = process.env.PORT || 3000;
|
|
@@ -72,6 +87,7 @@ const WS_PATH = routePath(BASE_PATH, '/ws');
|
|
|
72
87
|
const encodeCookieToken = TokenAuth.encodeCookieToken;
|
|
73
88
|
const MAX_CODEX_TRANSCRIPT_TURNS = 1000;
|
|
74
89
|
const INTERACTIVE_REFRESH_CACHE_MAX_AGE_MS = 3_000;
|
|
90
|
+
const execFileAsync = promisify(execFile);
|
|
75
91
|
|
|
76
92
|
const app = express();
|
|
77
93
|
const server = http.createServer(app);
|
|
@@ -98,6 +114,7 @@ const agentManager = new AgentManager(configManager, {
|
|
|
98
114
|
});
|
|
99
115
|
const themeManager = new ThemeManager({ configDir: configManager.farmingDir });
|
|
100
116
|
const workspaceFileService = new WorkspaceFileService();
|
|
117
|
+
const workspaceRootRegistry = new WorkspaceRootRegistry(agentManager);
|
|
101
118
|
const updateService = new FarmingUpdateService({
|
|
102
119
|
rootDir: path.join(__dirname, '..'),
|
|
103
120
|
configDir: configManager.farmingDir,
|
|
@@ -115,7 +132,7 @@ const usageSummaryCache = new AsyncCache(() => usageMonitor.getUsageSummary(), {
|
|
|
115
132
|
});
|
|
116
133
|
const codexModelOptionsCache = new AsyncCache(() => listCodexModelOptions(), {
|
|
117
134
|
ttlMs: 5 * 60_000,
|
|
118
|
-
staleMs:
|
|
135
|
+
staleMs: 5 * 60_000,
|
|
119
136
|
});
|
|
120
137
|
function configuredProviderHomes() {
|
|
121
138
|
const settings = configManager.getSettings();
|
|
@@ -367,11 +384,13 @@ function shareTargetQueryFromBody(body) {
|
|
|
367
384
|
const agentId = shareTargetString(target.agentId, 160);
|
|
368
385
|
const absolutePath = shareTargetString(target.absolutePath, 2048);
|
|
369
386
|
const projectLabel = shareTargetString(target.projectLabel, 160);
|
|
387
|
+
const readingAnchor = shareTargetString(target.readingAnchor, 1800);
|
|
370
388
|
if (!kind || kind === 'agent' && !agentId || kind !== 'agent' && !absolutePath && !agentId && !projectLabel) return '';
|
|
371
389
|
|
|
372
390
|
const params = new URLSearchParams();
|
|
373
391
|
params.set('ftarget', kind);
|
|
374
392
|
if (agentId) params.set('agent', agentId);
|
|
393
|
+
if (kind === 'agent' && readingAnchor && /^[A-Za-z0-9_-]+$/.test(readingAnchor)) params.set('fra', readingAnchor);
|
|
375
394
|
if (absolutePath) params.set('path', absolutePath);
|
|
376
395
|
if (projectLabel) params.set('project', projectLabel);
|
|
377
396
|
|
|
@@ -478,14 +497,28 @@ app.use(`${crtEntryPath}/shared`, express.static(frontendDir, { index: false }))
|
|
|
478
497
|
app.use(`${crtEntryPath}/`, express.static(crtFrontendDir, { index: false }));
|
|
479
498
|
app.use(BASE_PATH || '/', express.static(staticAppDir, { index: false }));
|
|
480
499
|
|
|
481
|
-
app.use(routePath(BASE_PATH, '/api/files'), createWorkspaceFileRouter(agentManager, workspaceFileService
|
|
500
|
+
app.use(routePath(BASE_PATH, '/api/files'), createWorkspaceFileRouter(agentManager, workspaceFileService, {
|
|
501
|
+
rootRegistry: workspaceRootRegistry,
|
|
502
|
+
}));
|
|
482
503
|
|
|
483
504
|
app.use(routePath(BASE_PATH, '/api/review-sessions'), createReviewSessionRouter(reviewSessionService));
|
|
484
505
|
app.use(routePath(BASE_PATH, '/api/reviews'), createReviewDiffRouter(reviewDiffService, reviewSessionService));
|
|
485
506
|
app.use(routePath(BASE_PATH, '/api/reviews'), createReviewStateRouter(reviewStateStore));
|
|
486
507
|
|
|
508
|
+
if (process.env.NODE_ENV === 'test' && process.env.FARMING_E2E_FAKE_EXECUTABLES === '1') {
|
|
509
|
+
app.post(routePath(BASE_PATH, '/api/control/e2e/close-websockets'), express.json(), (_req, res) => {
|
|
510
|
+
const clients = [...wss.clients].filter(client => client.readyState === WebSocket.OPEN);
|
|
511
|
+
res.json({ closing: clients.length, code: 1013 });
|
|
512
|
+
setImmediate(() => {
|
|
513
|
+
clients.forEach(client => client.close(1013, 'terminal stream backpressure test'));
|
|
514
|
+
});
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
|
|
487
518
|
app.use(routePath(BASE_PATH, '/api/control'), createControlRouter(agentManager, {
|
|
488
519
|
notifyUpdate: broadcastState,
|
|
520
|
+
allowConcurrentTestControl: process.env.NODE_ENV === 'test'
|
|
521
|
+
&& process.env.FARMING_E2E_FAKE_EXECUTABLES === '1',
|
|
489
522
|
}));
|
|
490
523
|
|
|
491
524
|
app.use(routePath(BASE_PATH, '/api/app-server'), createAppServerApiRouter({
|
|
@@ -541,6 +574,8 @@ app.get(routePath(BASE_PATH, '/api/workspaces/complete'), async (req, res) => {
|
|
|
541
574
|
}
|
|
542
575
|
});
|
|
543
576
|
|
|
577
|
+
app.use(routePath(BASE_PATH, '/api/workspaces'), createWorkspaceDirectoryRouter());
|
|
578
|
+
|
|
544
579
|
app.get(routePath(BASE_PATH, '/api/skills'), (_req, res) => {
|
|
545
580
|
res.json({ skills: getMainAgentSkillsCatalog() });
|
|
546
581
|
});
|
|
@@ -640,8 +675,16 @@ app.post(
|
|
|
640
675
|
);
|
|
641
676
|
|
|
642
677
|
app.get(routePath(BASE_PATH, '/api/codex/models'), async (req, res) => {
|
|
643
|
-
|
|
644
|
-
|
|
678
|
+
try {
|
|
679
|
+
const catalog = await codexModelOptionsCache.get('catalog');
|
|
680
|
+
res.json(catalog);
|
|
681
|
+
} catch (error) {
|
|
682
|
+
const timedOut = error && error.code === 'CODEX_MODELS_TIMEOUT';
|
|
683
|
+
res.status(timedOut ? 504 : 502).json({
|
|
684
|
+
error: error && error.message ? error.message : 'Failed to load Codex model catalog',
|
|
685
|
+
code: error && error.code ? error.code : 'CODEX_MODELS_FAILED',
|
|
686
|
+
});
|
|
687
|
+
}
|
|
645
688
|
});
|
|
646
689
|
|
|
647
690
|
app.get(routePath(BASE_PATH, '/api/claude/settings'), (req, res) => {
|
|
@@ -662,7 +705,10 @@ app.get(routePath(BASE_PATH, '/api/usage'), async (req, res) => {
|
|
|
662
705
|
app.get(routePath(BASE_PATH, '/api/usage/day'), async (req, res) => {
|
|
663
706
|
try {
|
|
664
707
|
const date = String(req.query.date || '').trim();
|
|
665
|
-
const detail = await usageMonitor.getUsageDay(date, {
|
|
708
|
+
const detail = await usageMonitor.getUsageDay(date, {
|
|
709
|
+
fresh: req.query.fresh === '1',
|
|
710
|
+
live: req.query.live === '1',
|
|
711
|
+
});
|
|
666
712
|
res.json({ detail });
|
|
667
713
|
} catch (error) {
|
|
668
714
|
const invalidDate = error instanceof RangeError;
|
|
@@ -943,7 +989,7 @@ app.get(routePath(BASE_PATH, '/api/agents/:agentId/codex-transcript'), async (re
|
|
|
943
989
|
: DEFAULT_CODEX_TRANSCRIPT_MAX_TURNS;
|
|
944
990
|
const transcript = await readCodexTranscript(providerSession.sessionId, {
|
|
945
991
|
maxTurns,
|
|
946
|
-
codexHome: providerSession
|
|
992
|
+
codexHome: runtimeKind(providerSession) === 'app-server'
|
|
947
993
|
? (providerSession.codexAppServerHomePath || providerSession.providerHomePath || '')
|
|
948
994
|
: (providerSession.providerHomePath || ''),
|
|
949
995
|
});
|
|
@@ -1222,6 +1268,38 @@ app.delete(routePath(BASE_PATH, '/api/agents/:agentId/codex-goal'), async (req,
|
|
|
1222
1268
|
}
|
|
1223
1269
|
});
|
|
1224
1270
|
|
|
1271
|
+
app.post(routePath(BASE_PATH, '/api/agents/:agentId/codex-terminal-profile'), express.json(), async (req, res) => {
|
|
1272
|
+
const requestAbort = new globalThis.AbortController();
|
|
1273
|
+
const abortRequest = () => requestAbort.abort(new Error('Terminal profile request was canceled'));
|
|
1274
|
+
const abortClosedResponse = () => {
|
|
1275
|
+
if (!res.writableEnded) abortRequest();
|
|
1276
|
+
};
|
|
1277
|
+
req.once('aborted', abortRequest);
|
|
1278
|
+
res.once('close', abortClosedResponse);
|
|
1279
|
+
try {
|
|
1280
|
+
const profile = await agentManager.setCodexTerminalProfile(req.params.agentId, {
|
|
1281
|
+
model: req.body?.model,
|
|
1282
|
+
effort: req.body?.effort,
|
|
1283
|
+
serviceTier: req.body?.serviceTier,
|
|
1284
|
+
}, {
|
|
1285
|
+
signal: requestAbort.signal,
|
|
1286
|
+
timeoutMs: 44_000,
|
|
1287
|
+
});
|
|
1288
|
+
res.json({ profile });
|
|
1289
|
+
} catch (error) {
|
|
1290
|
+
const message = error && error.message ? error.message : 'Failed to update Codex Terminal profile';
|
|
1291
|
+
const status = message === 'Agent not found'
|
|
1292
|
+
? 404
|
|
1293
|
+
: /^A valid Codex /.test(message)
|
|
1294
|
+
? 400
|
|
1295
|
+
: 409;
|
|
1296
|
+
if (!res.headersSent && !res.destroyed) res.status(status).json({ error: message });
|
|
1297
|
+
} finally {
|
|
1298
|
+
req.off('aborted', abortRequest);
|
|
1299
|
+
res.off('close', abortClosedResponse);
|
|
1300
|
+
}
|
|
1301
|
+
});
|
|
1302
|
+
|
|
1225
1303
|
app.patch(routePath(BASE_PATH, '/api/agents/:agentId'), express.json(), async (req, res) => {
|
|
1226
1304
|
const body = req.body || {};
|
|
1227
1305
|
const updates = {};
|
|
@@ -1253,6 +1331,15 @@ app.patch(routePath(BASE_PATH, '/api/agents/:agentId'), express.json(), async (r
|
|
|
1253
1331
|
if (typeof body.readAttentionSeq === 'number' && Number.isFinite(body.readAttentionSeq)) {
|
|
1254
1332
|
flagPatch.readAttentionSeq = body.readAttentionSeq;
|
|
1255
1333
|
}
|
|
1334
|
+
if (
|
|
1335
|
+
typeof body.readOutputEpoch === 'string'
|
|
1336
|
+
&& body.readOutputEpoch
|
|
1337
|
+
&& typeof body.readOutputSeq === 'number'
|
|
1338
|
+
&& Number.isFinite(body.readOutputSeq)
|
|
1339
|
+
) {
|
|
1340
|
+
flagPatch.readOutputEpoch = body.readOutputEpoch;
|
|
1341
|
+
flagPatch.readOutputSeq = body.readOutputSeq;
|
|
1342
|
+
}
|
|
1256
1343
|
|
|
1257
1344
|
if (flagPatch.archived === true) {
|
|
1258
1345
|
const result = await agentManager.archiveAgent(req.params.agentId);
|
|
@@ -1266,6 +1353,7 @@ app.patch(routePath(BASE_PATH, '/api/agents/:agentId'), express.json(), async (r
|
|
|
1266
1353
|
delete flagPatch.archived;
|
|
1267
1354
|
}
|
|
1268
1355
|
|
|
1356
|
+
let flagUpdateRequiresState = false;
|
|
1269
1357
|
if (Object.keys(flagPatch).length > 0) {
|
|
1270
1358
|
const result = agentManager.updateAgentFlags(req.params.agentId, flagPatch);
|
|
1271
1359
|
if (result.error) {
|
|
@@ -1275,6 +1363,7 @@ app.patch(routePath(BASE_PATH, '/api/agents/:agentId'), express.json(), async (r
|
|
|
1275
1363
|
}
|
|
1276
1364
|
Object.assign(updates, result);
|
|
1277
1365
|
delete updates.agentId;
|
|
1366
|
+
flagUpdateRequiresState = result.requiresState === true;
|
|
1278
1367
|
}
|
|
1279
1368
|
|
|
1280
1369
|
if (typeof body.launchPermissionMode === 'string') {
|
|
@@ -1304,11 +1393,13 @@ app.patch(routePath(BASE_PATH, '/api/agents/:agentId'), express.json(), async (r
|
|
|
1304
1393
|
}
|
|
1305
1394
|
|
|
1306
1395
|
if (Object.keys(updates).length === 0) {
|
|
1307
|
-
res.status(400).json({ error: 'customTitle, task, pinned, unread, archived, readAttentionSeq, launchPermissionMode, or agentRuntimeMode is required' });
|
|
1396
|
+
res.status(400).json({ error: 'customTitle, task, pinned, unread, archived, readAttentionSeq, readOutputEpoch/readOutputSeq, launchPermissionMode, or agentRuntimeMode is required' });
|
|
1308
1397
|
return;
|
|
1309
1398
|
}
|
|
1310
1399
|
|
|
1311
|
-
|
|
1400
|
+
if (flagUpdateRequiresState || typeof body.task === 'string' || typeof body.customTitle === 'string' || typeof body.launchPermissionMode === 'string' || typeof body.agentRuntimeMode === 'string') {
|
|
1401
|
+
scheduleBroadcastState();
|
|
1402
|
+
}
|
|
1312
1403
|
res.json({ agentId: req.params.agentId, ...updates });
|
|
1313
1404
|
});
|
|
1314
1405
|
|
|
@@ -1339,6 +1430,65 @@ app.post(routePath(BASE_PATH, '/api/agents/:agentId/fork'), express.json(), asyn
|
|
|
1339
1430
|
res.status(201).json(result);
|
|
1340
1431
|
});
|
|
1341
1432
|
|
|
1433
|
+
function resolveProjectActionRoot(rootId) {
|
|
1434
|
+
const root = workspaceRootRegistry.resolve(rootId);
|
|
1435
|
+
if (!root || root.kind === 'global') {
|
|
1436
|
+
throw new WorkspaceFileError('project workspace is required', 400);
|
|
1437
|
+
}
|
|
1438
|
+
return root;
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
app.post(routePath(BASE_PATH, '/api/projects/reveal'), express.json(), async (req, res) => {
|
|
1442
|
+
try {
|
|
1443
|
+
const root = resolveProjectActionRoot(req.body?.rootId);
|
|
1444
|
+
const command = process.platform === 'darwin'
|
|
1445
|
+
? 'open'
|
|
1446
|
+
: process.platform === 'win32'
|
|
1447
|
+
? 'explorer'
|
|
1448
|
+
: 'xdg-open';
|
|
1449
|
+
await execFileAsync(command, [root.canonicalPath], {
|
|
1450
|
+
timeout: 15000,
|
|
1451
|
+
maxBuffer: 1024 * 1024,
|
|
1452
|
+
});
|
|
1453
|
+
res.json({ revealed: true, workspace: root.canonicalPath });
|
|
1454
|
+
} catch (error) {
|
|
1455
|
+
const status = error instanceof WorkspaceFileError ? error.statusCode : 500;
|
|
1456
|
+
res.status(status).json({ error: error.message || 'Failed to reveal project workspace' });
|
|
1457
|
+
}
|
|
1458
|
+
});
|
|
1459
|
+
|
|
1460
|
+
app.post(routePath(BASE_PATH, '/api/projects/create-worktree'), express.json(), async (req, res) => {
|
|
1461
|
+
let created = null;
|
|
1462
|
+
let previousProjects = null;
|
|
1463
|
+
try {
|
|
1464
|
+
const root = resolveProjectActionRoot(req.body?.rootId);
|
|
1465
|
+
created = await agentManager.createPermanentWorktree(root.canonicalPath);
|
|
1466
|
+
previousProjects = configManager.getSettings().projectWorkspaces || [];
|
|
1467
|
+
configManager.updateSettings({
|
|
1468
|
+
projectWorkspaces: [
|
|
1469
|
+
created.workspace,
|
|
1470
|
+
...previousProjects.filter(workspace => workspace !== created.workspace),
|
|
1471
|
+
],
|
|
1472
|
+
});
|
|
1473
|
+
res.status(201).json({
|
|
1474
|
+
workspace: created.workspace,
|
|
1475
|
+
branch: created.branch,
|
|
1476
|
+
projectWorkspaces: configManager.getSettings().projectWorkspaces,
|
|
1477
|
+
});
|
|
1478
|
+
} catch (error) {
|
|
1479
|
+
if (created) await agentManager.rollbackPermanentWorktree(created);
|
|
1480
|
+
if (previousProjects) {
|
|
1481
|
+
try {
|
|
1482
|
+
configManager.updateSettings({ projectWorkspaces: previousProjects });
|
|
1483
|
+
} catch {
|
|
1484
|
+
// Keep the original error visible; the failed settings write is already the root cause.
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
const status = error instanceof WorkspaceFileError ? error.statusCode : 400;
|
|
1488
|
+
res.status(status).json({ error: error.message || 'Failed to create permanent worktree' });
|
|
1489
|
+
}
|
|
1490
|
+
});
|
|
1491
|
+
|
|
1342
1492
|
app.post(routePath(BASE_PATH, '/api/projects/delete-worktree'), express.json(), async (req, res) => {
|
|
1343
1493
|
const body = req.body || {};
|
|
1344
1494
|
const workspace = typeof body.workspace === 'string' ? body.workspace : '';
|
|
@@ -1427,38 +1577,6 @@ function forgetMainPageAgentSession(provider, sessionId, providerHomeId = '') {
|
|
|
1427
1577
|
});
|
|
1428
1578
|
}
|
|
1429
1579
|
|
|
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
1580
|
async function resumeAgentSessionById(provider, rawSessionId, options = {}) {
|
|
1463
1581
|
const normalizedProvider = normalizeProvider(provider);
|
|
1464
1582
|
const sessionId = String(rawSessionId || '').trim();
|
|
@@ -1551,6 +1669,9 @@ async function resumeAgentSessionById(provider, rawSessionId, options = {}) {
|
|
|
1551
1669
|
const command = buildAgentSessionResumeCommand(normalizedProvider, sessionId, {
|
|
1552
1670
|
fork: shouldFork,
|
|
1553
1671
|
cwd: workingDirectory,
|
|
1672
|
+
modelProvider: normalizedProvider === 'codex'
|
|
1673
|
+
? resolveCodexResumeModelProvider(session ? session.providerHomePath : '')
|
|
1674
|
+
: '',
|
|
1554
1675
|
});
|
|
1555
1676
|
|
|
1556
1677
|
if (!command) {
|
|
@@ -1584,6 +1705,7 @@ async function resumeAgentSessionById(provider, rawSessionId, options = {}) {
|
|
|
1584
1705
|
providerHomeId: resolvedProviderHomeId,
|
|
1585
1706
|
providerHomePath: session ? (session.providerHomePath || '') : '',
|
|
1586
1707
|
autoReadInitialAttention: options.autoReadInitialAttention === true,
|
|
1708
|
+
preserveProviderSessionProfile: normalizedProvider === 'codex',
|
|
1587
1709
|
});
|
|
1588
1710
|
Promise.resolve(startResult).catch((error) => {
|
|
1589
1711
|
resolve({ error: error.message || 'failed to resume agent session', status: 500 });
|
|
@@ -1653,10 +1775,29 @@ async function autoResumeMainPageAgentSessions() {
|
|
|
1653
1775
|
const sessions = mainPageAgentSessionsToAutoResume(configManager.getSettings());
|
|
1654
1776
|
if (sessions.length === 0) return;
|
|
1655
1777
|
|
|
1778
|
+
let knownSessions;
|
|
1779
|
+
try {
|
|
1780
|
+
knownSessions = await agentSessionsCache.get(
|
|
1781
|
+
JSON.stringify(configManager.getSettings().agentHomes || {}),
|
|
1782
|
+
{ maxAgeMs: INTERACTIVE_REFRESH_CACHE_MAX_AGE_MS }
|
|
1783
|
+
);
|
|
1784
|
+
} catch (error) {
|
|
1785
|
+
console.warn('Failed to load Agent session catalog for auto-resume:', error && (error.message || error));
|
|
1786
|
+
return;
|
|
1787
|
+
}
|
|
1788
|
+
const knownSessionByKey = new Map(knownSessions.map(session => [
|
|
1789
|
+
mainPageAgentSessionKey(session.provider, session.id, session.providerHomeId || 'default'),
|
|
1790
|
+
session,
|
|
1791
|
+
]));
|
|
1792
|
+
|
|
1656
1793
|
let resumedCount = 0;
|
|
1657
1794
|
for (const session of sessions) {
|
|
1658
1795
|
try {
|
|
1659
|
-
const sessionDetails =
|
|
1796
|
+
const sessionDetails = knownSessionByKey.get(mainPageAgentSessionKey(
|
|
1797
|
+
session.provider,
|
|
1798
|
+
session.sessionId,
|
|
1799
|
+
session.providerHomeId || 'default'
|
|
1800
|
+
));
|
|
1660
1801
|
if (!sessionDetails) {
|
|
1661
1802
|
console.warn('Dropping stale main-page session from auto-resume:', session.provider, session.sessionId);
|
|
1662
1803
|
forgetMainPageAgentSession(session.provider, session.sessionId, session.providerHomeId || 'default');
|
|
@@ -1705,7 +1846,7 @@ app.post(routePath(BASE_PATH, '/api/settings'), express.json(), (req, res) => {
|
|
|
1705
1846
|
success: true,
|
|
1706
1847
|
settings: configManager.getSettings()
|
|
1707
1848
|
});
|
|
1708
|
-
|
|
1849
|
+
scheduleBroadcastState();
|
|
1709
1850
|
});
|
|
1710
1851
|
|
|
1711
1852
|
app.post(routePath(BASE_PATH, '/api/themes/:themeId/set'), express.json(), (req, res) => {
|
|
@@ -1773,13 +1914,30 @@ wss.on('connection', (ws, req) => {
|
|
|
1773
1914
|
}
|
|
1774
1915
|
|
|
1775
1916
|
console.log('Client connected');
|
|
1917
|
+
ws.connectionId = crypto.randomUUID();
|
|
1776
1918
|
|
|
1777
1919
|
ws.on('message', (message) => {
|
|
1778
1920
|
try {
|
|
1779
1921
|
const data = JSON.parse(message);
|
|
1922
|
+
const validation = validateClientMessage(data);
|
|
1923
|
+
if (!validation.ok) {
|
|
1924
|
+
ws.send(JSON.stringify({
|
|
1925
|
+
type: 'protocol-error',
|
|
1926
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
1927
|
+
requestId: typeof data?.requestId === 'string' ? data.requestId : '',
|
|
1928
|
+
message: validation.error,
|
|
1929
|
+
}));
|
|
1930
|
+
return;
|
|
1931
|
+
}
|
|
1780
1932
|
handleMessage(ws, data);
|
|
1781
1933
|
} catch (e) {
|
|
1782
1934
|
console.error('Failed to parse message:', e);
|
|
1935
|
+
ws.send(JSON.stringify({
|
|
1936
|
+
type: 'protocol-error',
|
|
1937
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
1938
|
+
requestId: '',
|
|
1939
|
+
message: 'message must be valid JSON',
|
|
1940
|
+
}));
|
|
1783
1941
|
}
|
|
1784
1942
|
});
|
|
1785
1943
|
|
|
@@ -1788,6 +1946,11 @@ wss.on('connection', (ws, req) => {
|
|
|
1788
1946
|
console.log('Client disconnected');
|
|
1789
1947
|
});
|
|
1790
1948
|
|
|
1949
|
+
ws.send(JSON.stringify({
|
|
1950
|
+
type: 'protocol-hello',
|
|
1951
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
1952
|
+
minProtocolVersion: MIN_PROTOCOL_VERSION,
|
|
1953
|
+
}));
|
|
1791
1954
|
sendState(ws);
|
|
1792
1955
|
});
|
|
1793
1956
|
|
|
@@ -1872,6 +2035,15 @@ async function sendComposerInputMessage(ws, data) {
|
|
|
1872
2035
|
}
|
|
1873
2036
|
}
|
|
1874
2037
|
if (!targetAgentId || content.length === 0) return;
|
|
2038
|
+
const targetAgent = agentManager.getState().agents.find(agent => agent.id === targetAgentId);
|
|
2039
|
+
const structuredRuntime = targetAgent && runtimeKind(targetAgent) !== 'terminal';
|
|
2040
|
+
if (!structuredRuntime) {
|
|
2041
|
+
ws.send(JSON.stringify({
|
|
2042
|
+
type: 'error',
|
|
2043
|
+
message: 'Terminal Composer input requires the active terminal owner',
|
|
2044
|
+
}));
|
|
2045
|
+
return;
|
|
2046
|
+
}
|
|
1875
2047
|
try {
|
|
1876
2048
|
await agentManager.sendComposerMessage(targetAgentId, content);
|
|
1877
2049
|
} catch (error) {
|
|
@@ -1901,6 +2073,13 @@ async function respondToAppServerRequest(ws, data) {
|
|
|
1901
2073
|
|
|
1902
2074
|
function handleMessage(ws, data) {
|
|
1903
2075
|
switch (data.type) {
|
|
2076
|
+
case 'protocol-hello':
|
|
2077
|
+
if (!protocolCompatible(data.protocolVersion)) {
|
|
2078
|
+
ws.close(4002, `Unsupported Farming protocol version ${data.protocolVersion}`);
|
|
2079
|
+
return;
|
|
2080
|
+
}
|
|
2081
|
+
ws.protocolVersion = data.protocolVersion;
|
|
2082
|
+
break;
|
|
1904
2083
|
case 'start-agent': {
|
|
1905
2084
|
const workspace = data.workspace || null;
|
|
1906
2085
|
agentManager.startAgent(data.command, workspace, (agentId, error) => {
|
|
@@ -1964,7 +2143,7 @@ function handleMessage(ws, data) {
|
|
|
1964
2143
|
|
|
1965
2144
|
case 'interrupt-agent':
|
|
1966
2145
|
if (data.agentId) {
|
|
1967
|
-
agentManager.interruptAgent(data.agentId);
|
|
2146
|
+
void agentManager.interruptAgent(data.agentId);
|
|
1968
2147
|
}
|
|
1969
2148
|
break;
|
|
1970
2149
|
|
|
@@ -1983,10 +2162,14 @@ function handleMessage(ws, data) {
|
|
|
1983
2162
|
|
|
1984
2163
|
case 'resize-agent':
|
|
1985
2164
|
if (data.agentId && Number.isFinite(data.cols) && Number.isFinite(data.rows)) {
|
|
1986
|
-
agentManager.
|
|
2165
|
+
agentManager.requestAgentSessionResize(data.agentId, data.cols, data.rows);
|
|
1987
2166
|
}
|
|
1988
2167
|
break;
|
|
1989
2168
|
|
|
2169
|
+
case 'clear-terminal':
|
|
2170
|
+
if (data.agentId) void agentManager.clearAgentSessionBuffer(data.agentId);
|
|
2171
|
+
break;
|
|
2172
|
+
|
|
1990
2173
|
case 'watch-workspace-files':
|
|
1991
2174
|
watchWorkspaceFiles(ws, data);
|
|
1992
2175
|
break;
|
|
@@ -2054,10 +2237,7 @@ async function watchWorkspaceFiles(ws, data) {
|
|
|
2054
2237
|
return;
|
|
2055
2238
|
}
|
|
2056
2239
|
|
|
2057
|
-
const root = agentManager
|
|
2058
|
-
if (!root) {
|
|
2059
|
-
throw new WorkspaceFileError('agent not found', 404);
|
|
2060
|
-
}
|
|
2240
|
+
const root = resolveWorkspaceRoot(agentManager, data.agentId);
|
|
2061
2241
|
|
|
2062
2242
|
const unsubscribe = await workspaceFileService.subscribe(root, (event) => {
|
|
2063
2243
|
if (ws.readyState !== WebSocket.OPEN) return;
|
|
@@ -2081,8 +2261,14 @@ async function watchWorkspaceFiles(ws, data) {
|
|
|
2081
2261
|
}
|
|
2082
2262
|
|
|
2083
2263
|
function buildStatePayload() {
|
|
2264
|
+
const state = agentManager.getState();
|
|
2084
2265
|
return {
|
|
2085
|
-
...
|
|
2266
|
+
...state,
|
|
2267
|
+
agents: state.agents.map(agent => ({
|
|
2268
|
+
...agent,
|
|
2269
|
+
workspaceRootId: rootIdForPath(agent.projectWorkspace || agent.gitWorktree?.workspace || agent.cwd),
|
|
2270
|
+
})),
|
|
2271
|
+
workspaceRoots: workspaceRootRegistry.list(),
|
|
2086
2272
|
mainPageSessionKeys: typeof configManager.getMainPageSessionKeys === 'function'
|
|
2087
2273
|
? configManager.getMainPageSessionKeys()
|
|
2088
2274
|
: (Array.isArray(configManager.getSettings().mainPageSessionKeys) ? configManager.getSettings().mainPageSessionKeys : []),
|
|
@@ -2097,22 +2283,39 @@ function sendState(ws) {
|
|
|
2097
2283
|
});
|
|
2098
2284
|
}
|
|
2099
2285
|
|
|
2286
|
+
function previewForClient(preview, client) {
|
|
2287
|
+
if (!preview || !client || client.focusedAgentId !== preview.agentId || !preview.previewSnapshot) {
|
|
2288
|
+
return preview;
|
|
2289
|
+
}
|
|
2290
|
+
// The active terminal is hydrated from its authoritative session-view and
|
|
2291
|
+
// receives live session-output. Sending its sidebar snapshot again adds a
|
|
2292
|
+
// large, unrelated React update to every keystroke in a full-screen TUI.
|
|
2293
|
+
return {
|
|
2294
|
+
...preview,
|
|
2295
|
+
previewSnapshot: null,
|
|
2296
|
+
};
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2100
2299
|
function sendPreview(ws, preview) {
|
|
2101
2300
|
ws.send(JSON.stringify({
|
|
2102
2301
|
type: 'session-preview',
|
|
2103
|
-
preview,
|
|
2302
|
+
preview: previewForClient(preview, ws),
|
|
2104
2303
|
}));
|
|
2105
2304
|
}
|
|
2106
2305
|
|
|
2107
2306
|
const STATE_BROADCAST_INTERVAL_MS = 120;
|
|
2108
2307
|
const PREVIEW_BROADCAST_INTERVAL_MS = 500;
|
|
2109
2308
|
const SESSION_STREAM_BROADCAST_INTERVAL_MS = 33;
|
|
2309
|
+
const AGENT_ACTIVITY_BROADCAST_DELAY_MS = SESSION_STREAM_BROADCAST_INTERVAL_MS;
|
|
2110
2310
|
const MAX_SESSION_STREAM_CLIENT_BUFFERED_AMOUNT = 4 * 1024 * 1024;
|
|
2111
2311
|
let stateBroadcastTimer = null;
|
|
2112
2312
|
let lastStateBroadcastAt = 0;
|
|
2113
2313
|
const pendingPreviewBroadcasts = new Map();
|
|
2314
|
+
const pendingAgentActivityBroadcasts = new Map();
|
|
2315
|
+
const pendingAgentUpdates = new Map();
|
|
2114
2316
|
const pendingSessionStreams = new Map();
|
|
2115
2317
|
let sessionStreamBroadcastTimer = null;
|
|
2318
|
+
let lastSessionStreamBroadcastAt = 0;
|
|
2116
2319
|
let shutdownStarted = false;
|
|
2117
2320
|
|
|
2118
2321
|
function broadcastState() {
|
|
@@ -2146,16 +2349,11 @@ function scheduleBroadcastState() {
|
|
|
2146
2349
|
}
|
|
2147
2350
|
|
|
2148
2351
|
function broadcastSessionPreview(preview) {
|
|
2149
|
-
const message = JSON.stringify({
|
|
2150
|
-
type: 'session-preview',
|
|
2151
|
-
preview,
|
|
2152
|
-
});
|
|
2153
|
-
|
|
2154
2352
|
wss.clients.forEach((client) => {
|
|
2155
2353
|
const previewAllowed = client.previewScope !== 'none'
|
|
2156
2354
|
&& (client.previewScope !== 'focused' || client.focusedAgentId === preview.agentId);
|
|
2157
2355
|
if (client.readyState === WebSocket.OPEN && previewAllowed) {
|
|
2158
|
-
client
|
|
2356
|
+
sendPreview(client, preview);
|
|
2159
2357
|
}
|
|
2160
2358
|
});
|
|
2161
2359
|
}
|
|
@@ -2214,6 +2412,78 @@ agentManager.on('provider-session-updated', () => {
|
|
|
2214
2412
|
scheduleBroadcastState();
|
|
2215
2413
|
});
|
|
2216
2414
|
|
|
2415
|
+
function broadcastAgentActivity(activity) {
|
|
2416
|
+
const message = JSON.stringify({
|
|
2417
|
+
type: 'agent-activity',
|
|
2418
|
+
activity,
|
|
2419
|
+
});
|
|
2420
|
+
wss.clients.forEach((client) => {
|
|
2421
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
2422
|
+
client.send(message);
|
|
2423
|
+
}
|
|
2424
|
+
});
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2427
|
+
function scheduleAgentActivityBroadcast(activity) {
|
|
2428
|
+
if (!activity || !activity.agentId) return;
|
|
2429
|
+
const agentId = activity.agentId;
|
|
2430
|
+
const existing = pendingAgentActivityBroadcasts.get(agentId);
|
|
2431
|
+
if (existing) {
|
|
2432
|
+
existing.activity = activity;
|
|
2433
|
+
return;
|
|
2434
|
+
}
|
|
2435
|
+
const entry = {
|
|
2436
|
+
activity,
|
|
2437
|
+
timer: setTimeout(() => {
|
|
2438
|
+
pendingAgentActivityBroadcasts.delete(agentId);
|
|
2439
|
+
broadcastAgentActivity(entry.activity);
|
|
2440
|
+
}, AGENT_ACTIVITY_BROADCAST_DELAY_MS),
|
|
2441
|
+
};
|
|
2442
|
+
entry.timer.unref?.();
|
|
2443
|
+
pendingAgentActivityBroadcasts.set(agentId, entry);
|
|
2444
|
+
}
|
|
2445
|
+
|
|
2446
|
+
agentManager.onAgentActivity(scheduleAgentActivityBroadcast);
|
|
2447
|
+
|
|
2448
|
+
function scheduleAgentUpdate(update) {
|
|
2449
|
+
const patch = sanitizeAgentUpdatePatch(update?.patch);
|
|
2450
|
+
if (!update?.agentId || !patch) return;
|
|
2451
|
+
const existing = pendingAgentUpdates.get(update.agentId);
|
|
2452
|
+
if (existing) {
|
|
2453
|
+
Object.assign(existing.patch, patch);
|
|
2454
|
+
return;
|
|
2455
|
+
}
|
|
2456
|
+
const entry = {
|
|
2457
|
+
patch,
|
|
2458
|
+
timer: setTimeout(() => {
|
|
2459
|
+
pendingAgentUpdates.delete(update.agentId);
|
|
2460
|
+
const message = JSON.stringify({
|
|
2461
|
+
type: 'agent-update',
|
|
2462
|
+
update: { agentId: update.agentId, patch: entry.patch },
|
|
2463
|
+
});
|
|
2464
|
+
wss.clients.forEach((client) => {
|
|
2465
|
+
if (client.readyState === WebSocket.OPEN) client.send(message);
|
|
2466
|
+
});
|
|
2467
|
+
}, AGENT_ACTIVITY_BROADCAST_DELAY_MS),
|
|
2468
|
+
};
|
|
2469
|
+
entry.timer.unref?.();
|
|
2470
|
+
pendingAgentUpdates.set(update.agentId, entry);
|
|
2471
|
+
}
|
|
2472
|
+
|
|
2473
|
+
agentManager.on('agent-update', scheduleAgentUpdate);
|
|
2474
|
+
|
|
2475
|
+
function broadcastAgentRead(read) {
|
|
2476
|
+
if (!read || !read.agentId) return;
|
|
2477
|
+
const message = JSON.stringify({ type: 'agent-read', read });
|
|
2478
|
+
wss.clients.forEach((client) => {
|
|
2479
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
2480
|
+
client.send(message);
|
|
2481
|
+
}
|
|
2482
|
+
});
|
|
2483
|
+
}
|
|
2484
|
+
|
|
2485
|
+
agentManager.on('agent-read', broadcastAgentRead);
|
|
2486
|
+
|
|
2217
2487
|
function sessionStreamKey(stream) {
|
|
2218
2488
|
return `${stream.agentId || ''}\0${stream.sessionSource || ''}`;
|
|
2219
2489
|
}
|
|
@@ -2223,13 +2493,10 @@ function broadcastSessionStream(stream) {
|
|
|
2223
2493
|
type: 'session-output',
|
|
2224
2494
|
stream
|
|
2225
2495
|
});
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
if (client.bufferedAmount > MAX_SESSION_STREAM_CLIENT_BUFFERED_AMOUNT) return;
|
|
2231
|
-
client.send(message);
|
|
2232
|
-
}
|
|
2496
|
+
deliverSessionStreamToClients(wss.clients, stream, {
|
|
2497
|
+
openState: WebSocket.OPEN,
|
|
2498
|
+
maxBufferedAmount: MAX_SESSION_STREAM_CLIENT_BUFFERED_AMOUNT,
|
|
2499
|
+
message,
|
|
2233
2500
|
});
|
|
2234
2501
|
}
|
|
2235
2502
|
|
|
@@ -2237,31 +2504,26 @@ function flushSessionStreams() {
|
|
|
2237
2504
|
sessionStreamBroadcastTimer = null;
|
|
2238
2505
|
const streams = Array.from(pendingSessionStreams.values());
|
|
2239
2506
|
pendingSessionStreams.clear();
|
|
2507
|
+
if (streams.length > 0) lastSessionStreamBroadcastAt = Date.now();
|
|
2240
2508
|
streams.forEach(broadcastSessionStream);
|
|
2241
2509
|
}
|
|
2242
2510
|
|
|
2243
2511
|
function scheduleSessionStreamBroadcast(stream) {
|
|
2244
2512
|
if (!stream || !stream.agentId) return;
|
|
2513
|
+
const now = Date.now();
|
|
2514
|
+
if (shouldBroadcastSessionStreamImmediately({
|
|
2515
|
+
pendingCount: pendingSessionStreams.size,
|
|
2516
|
+
lastBroadcastAt: lastSessionStreamBroadcastAt,
|
|
2517
|
+
now,
|
|
2518
|
+
intervalMs: SESSION_STREAM_BROADCAST_INTERVAL_MS,
|
|
2519
|
+
})) {
|
|
2520
|
+
lastSessionStreamBroadcastAt = now;
|
|
2521
|
+
broadcastSessionStream(coalesceSessionStream(null, stream));
|
|
2522
|
+
return;
|
|
2523
|
+
}
|
|
2245
2524
|
const key = sessionStreamKey(stream);
|
|
2246
2525
|
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
|
-
}
|
|
2526
|
+
pendingSessionStreams.set(key, coalesceSessionStream(existing, stream));
|
|
2265
2527
|
|
|
2266
2528
|
if (!sessionStreamBroadcastTimer) {
|
|
2267
2529
|
sessionStreamBroadcastTimer = setTimeout(flushSessionStreams, SESSION_STREAM_BROADCAST_INTERVAL_MS);
|
|
@@ -2280,6 +2542,14 @@ function clearBroadcastTimers() {
|
|
|
2280
2542
|
}
|
|
2281
2543
|
}
|
|
2282
2544
|
pendingPreviewBroadcasts.clear();
|
|
2545
|
+
for (const entry of pendingAgentActivityBroadcasts.values()) {
|
|
2546
|
+
if (entry && entry.timer) clearTimeout(entry.timer);
|
|
2547
|
+
}
|
|
2548
|
+
pendingAgentActivityBroadcasts.clear();
|
|
2549
|
+
for (const entry of pendingAgentUpdates.values()) {
|
|
2550
|
+
if (entry && entry.timer) clearTimeout(entry.timer);
|
|
2551
|
+
}
|
|
2552
|
+
pendingAgentUpdates.clear();
|
|
2283
2553
|
if (sessionStreamBroadcastTimer) {
|
|
2284
2554
|
clearTimeout(sessionStreamBroadcastTimer);
|
|
2285
2555
|
sessionStreamBroadcastTimer = null;
|