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.
Files changed (102) hide show
  1. package/README.md +110 -199
  2. package/README.zh_cn.md +106 -237
  3. package/THIRD_PARTY_NOTICES.md +11 -2
  4. package/backend/acp-checkpoint-store.js +198 -0
  5. package/backend/acp-runtime.js +306 -83
  6. package/backend/acp-session-state.js +202 -6
  7. package/backend/acp-transcript.js +112 -0
  8. package/backend/agent-activity.js +6 -157
  9. package/backend/agent-manager.js +1496 -737
  10. package/backend/agent-provider-session.js +17 -242
  11. package/backend/agent-runtime-binding.js +219 -0
  12. package/backend/agent-session-history.js +66 -1
  13. package/backend/auth.js +79 -6
  14. package/backend/codex-models.js +81 -84
  15. package/backend/codex-session-archive.js +45 -0
  16. package/backend/codex-terminal-profile.js +500 -0
  17. package/backend/codex-transcript-sanitizer.js +12 -0
  18. package/backend/codex-transcript.js +230 -8
  19. package/backend/config-manager.js +30 -0
  20. package/backend/control-api.js +192 -17
  21. package/backend/farming-net-pass.js +285 -0
  22. package/backend/farming-net-registry.js +112 -0
  23. package/backend/farming-net-server.js +298 -0
  24. package/backend/farming-session-store.js +5 -13
  25. package/backend/git-worktree-info.js +181 -0
  26. package/backend/local-session-engine.js +411 -186
  27. package/backend/main-page-session.js +5 -2
  28. package/backend/native-pty-controller-generation.js +106 -0
  29. package/backend/native-pty-host-client.js +275 -7
  30. package/backend/native-pty-host-identity.js +86 -0
  31. package/backend/native-pty-host.js +813 -114
  32. package/backend/native-session-engine.js +100 -28
  33. package/backend/packaged-node-pty.js +22 -2
  34. package/backend/provider-adapters.js +253 -0
  35. package/backend/provider-session-service.js +241 -0
  36. package/backend/runtime-observation.js +81 -0
  37. package/backend/server.js +294 -86
  38. package/backend/session-engine-bridge.js +21 -2
  39. package/backend/session-engine-router.js +1 -1
  40. package/backend/session-engine.js +1 -1
  41. package/backend/session-stream-protocol.js +185 -0
  42. package/backend/storage-layout.js +55 -0
  43. package/backend/terminal-attach-checkpoint.js +74 -0
  44. package/backend/terminal-exit-quiescence.js +39 -0
  45. package/backend/terminal-reducer-flow-control.js +97 -0
  46. package/backend/terminal-screen-state.js +11 -2
  47. package/backend/terminal-screen-worker-pool.js +59 -6
  48. package/backend/terminal-screen-worker-thread.js +97 -57
  49. package/backend/terminal-screen-worker.js +133 -51
  50. package/backend/terminal-state-serialization.js +127 -0
  51. package/backend/terminal-status.js +23 -4
  52. package/backend/usage-monitor.js +81 -4
  53. package/backend/workspace-directory.js +152 -0
  54. package/backend/workspace-file-router.js +182 -76
  55. package/backend/workspace-file-service.js +319 -4
  56. package/backend/workspace-root-registry.js +164 -0
  57. package/dist/assets/App-CHYpgECZ.js +208 -0
  58. package/dist/assets/{FileEditorMarkdownPreview-elKWc8Im.js → FileEditorMarkdownPreview--Cri80nQ.js} +92 -92
  59. package/dist/assets/FileEditorPane-DGJlEKdf.js +2 -0
  60. package/dist/assets/IconGlyphs-Cc14sepw.js +1 -0
  61. package/dist/assets/ProjectFilesSection-Cz1wG4NT.js +12 -0
  62. package/dist/assets/{ReviewPage-BaXu1ZdX.js → ReviewPage-LonRc6Fz.js} +1 -1
  63. package/dist/assets/code-dark-B1pAtO_P.css +1 -0
  64. package/dist/assets/file-icons-Bw2qd5iT.js +1 -0
  65. package/dist/assets/{index-B8SHldPE.js → index-miO2r9IZ.js} +3 -3
  66. package/dist/assets/main-DNe0jPw-.css +1 -0
  67. package/dist/assets/workspace-editor-model-BQol4qbA.js +1 -0
  68. package/dist/assets/workspace-editor-monaco-5gFbeDPF.js +1 -0
  69. package/dist/assets/workspace-editor-monaco-xPv8JzHH.js +4 -0
  70. package/dist/assets/workspace-view-state-DvYG_9PH.js +7 -0
  71. package/dist/assets/workspace-working-copy-D8-s_Sgh.js +1 -0
  72. package/dist/index.html +1 -1
  73. package/frontend/farming-net/app.css +625 -0
  74. package/frontend/farming-net/app.js +268 -0
  75. package/frontend/farming-net/index.html +86 -0
  76. package/frontend/reading-anchor.js +198 -0
  77. package/frontend/session-bridge.js +12 -3
  78. package/frontend/session-modal-bridge.js +5 -12
  79. package/frontend/skins/crt/app.js +1976 -793
  80. package/frontend/skins/crt/index.html +313 -23
  81. package/frontend/skins/crt/styles/billing.css +294 -223
  82. package/frontend/skins/crt/styles/monochrome-green.css +7 -2
  83. package/frontend/terminal-replay.js +372 -0
  84. package/package.json +10 -3
  85. package/shared/browser-protocol.d.ts +5 -0
  86. package/shared/browser-protocol.js +130 -0
  87. package/dist/assets/App-iA2BU9P6.js +0 -124
  88. package/dist/assets/FileEditorPane-RWiFD2cq.js +0 -5
  89. package/dist/assets/IconGlyphs-DfL0EBnj.js +0 -1
  90. package/dist/assets/ProjectFilesSection-Q4PDsWmM.js +0 -12
  91. package/dist/assets/code-dark-CDkOQAtK.css +0 -1
  92. package/dist/assets/file-icons-EFUGSSwf.js +0 -1
  93. package/dist/assets/main-DpYuBonn.css +0 -1
  94. package/dist/assets/qoder-C9LmmOSf.svg +0 -1
  95. package/dist/assets/qoder-Cf9gl0Y5.svg +0 -1
  96. package/dist/assets/qoder-gHCinseV.svg +0 -1
  97. package/dist/assets/workspace-view-state-CTyDzk2D.js +0 -1
  98. package/dist/assets/zsh-CLpveKlF.svg +0 -1
  99. package/dist/assets/zsh-FxSpMPbz.svg +0 -1
  100. /package/dist/assets/{api-D1lyBYIQ.js → api-D8nyOEbz.js} +0 -0
  101. /package/dist/assets/{core-ZlAPicox.js → core-D0LFJkDt.js} +0 -0
  102. /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
- const execFileAsync = promisify(execFile);
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: 30 * 60_000,
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
- const catalog = await codexModelOptionsCache.get('catalog');
644
- res.json(catalog);
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, { fresh: req.query.fresh === '1' });
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.codexRuntimeMode === 'app-server'
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
- scheduleBroadcastState();
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 = await findAgentSession(session.provider, session.sessionId, { limit: 200, providerHomeId: session.providerHomeId || 'default', providerHomes: configuredProviderHomes() });
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
- broadcastState();
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.resizeAgentSession(data.agentId, data.cols, data.rows);
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.getAgentWorkspaceRoot(data.agentId);
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
- ...agentManager.getState(),
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.send(message);
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
- wss.clients.forEach((client) => {
2228
- if (client.readyState === WebSocket.OPEN) {
2229
- if (client.streamScope === 'focused' && client.focusedAgentId !== stream.agentId) return;
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
- const data = typeof stream.data === 'string' ? stream.data : String(stream.data || '');
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;