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.
Files changed (107) hide show
  1. package/README.md +42 -28
  2. package/README.zh_cn.md +42 -28
  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 +1592 -741
  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 +54 -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 +35 -0
  20. package/backend/control-api.js +192 -17
  21. package/backend/executable-discovery.js +2 -0
  22. package/backend/farming-net-pass.js +285 -0
  23. package/backend/farming-net-registry.js +112 -0
  24. package/backend/farming-net-server.js +298 -0
  25. package/backend/farming-session-store.js +5 -13
  26. package/backend/git-worktree-info.js +181 -0
  27. package/backend/local-session-engine.js +411 -186
  28. package/backend/main-page-session.js +5 -2
  29. package/backend/native-pty-controller-generation.js +106 -0
  30. package/backend/native-pty-host-client.js +275 -7
  31. package/backend/native-pty-host-identity.js +86 -0
  32. package/backend/native-pty-host.js +813 -114
  33. package/backend/native-session-engine.js +100 -28
  34. package/backend/packaged-node-pty.js +22 -2
  35. package/backend/provider-adapters.js +253 -0
  36. package/backend/provider-session-service.js +241 -0
  37. package/backend/runtime-observation.js +81 -0
  38. package/backend/server.js +354 -84
  39. package/backend/session-engine-bridge.js +21 -2
  40. package/backend/session-engine-router.js +1 -1
  41. package/backend/session-engine.js +1 -1
  42. package/backend/session-stream-protocol.js +185 -0
  43. package/backend/storage-layout.js +55 -0
  44. package/backend/terminal-attach-checkpoint.js +74 -0
  45. package/backend/terminal-exit-quiescence.js +39 -0
  46. package/backend/terminal-reducer-flow-control.js +97 -0
  47. package/backend/terminal-screen-state.js +11 -2
  48. package/backend/terminal-screen-worker-pool.js +59 -6
  49. package/backend/terminal-screen-worker-thread.js +97 -57
  50. package/backend/terminal-screen-worker.js +133 -51
  51. package/backend/terminal-state-serialization.js +127 -0
  52. package/backend/terminal-status.js +23 -4
  53. package/backend/usage-monitor.js +176 -17
  54. package/backend/workspace-directory.js +232 -0
  55. package/backend/workspace-file-router.js +182 -76
  56. package/backend/workspace-file-service.js +319 -4
  57. package/backend/workspace-root-registry.js +164 -0
  58. package/dist/assets/App-DWsa7DXG.js +206 -0
  59. package/dist/assets/{FileEditorMarkdownPreview-elKWc8Im.js → FileEditorMarkdownPreview-UlVd0O4D.js} +92 -92
  60. package/dist/assets/FileEditorPane-Dxx4rKOg.js +2 -0
  61. package/dist/assets/IconGlyphs-AU22gPDY.js +1 -0
  62. package/dist/assets/ProjectFilesSection-DE7Nb9Jk.js +12 -0
  63. package/dist/assets/ReviewPage-CeDKDyZh.js +3 -0
  64. package/dist/assets/code-dark-DPiuyeSp.css +1 -0
  65. package/dist/assets/file-icons-Bw2qd5iT.js +1 -0
  66. package/dist/assets/{index-BrbljRqn.js → index-CUFPJoa5.js} +3 -3
  67. package/dist/assets/main-Bqtb5kuz.css +1 -0
  68. package/dist/assets/{monaco.contribution-CyLosfZI.js → monaco.contribution-BXz-lfFB.js} +2 -2
  69. package/dist/assets/{tsMode-DGZTlTj8.js → tsMode-B0oUTcXQ.js} +1 -1
  70. package/dist/assets/workspace-editor-model-BQol4qbA.js +1 -0
  71. package/dist/assets/workspace-editor-monaco-BICJESG0.js +4 -0
  72. package/dist/assets/workspace-editor-monaco-DRJ7IaXk.js +1 -0
  73. package/dist/assets/workspace-view-state-DvYG_9PH.js +7 -0
  74. package/dist/assets/workspace-working-copy-D8-s_Sgh.js +1 -0
  75. package/dist/farming-2/site.webmanifest +2 -0
  76. package/dist/index.html +1 -1
  77. package/frontend/farming-net/app.css +625 -0
  78. package/frontend/farming-net/app.js +268 -0
  79. package/frontend/farming-net/index.html +86 -0
  80. package/frontend/reading-anchor.js +198 -0
  81. package/frontend/session-bridge.js +12 -3
  82. package/frontend/session-modal-bridge.js +5 -12
  83. package/frontend/skins/crt/app.js +1978 -793
  84. package/frontend/skins/crt/index.html +313 -23
  85. package/frontend/skins/crt/styles/billing.css +294 -223
  86. package/frontend/skins/crt/styles/monochrome-green.css +7 -2
  87. package/frontend/terminal-replay.js +372 -0
  88. package/package.json +10 -3
  89. package/shared/browser-protocol.d.ts +5 -0
  90. package/shared/browser-protocol.js +130 -0
  91. package/dist/assets/App-8dYAM6ql.js +0 -124
  92. package/dist/assets/FileEditorPane-RWiFD2cq.js +0 -5
  93. package/dist/assets/IconGlyphs-DfL0EBnj.js +0 -1
  94. package/dist/assets/ProjectFilesSection-Q4PDsWmM.js +0 -12
  95. package/dist/assets/ReviewPage-BaXu1ZdX.js +0 -3
  96. package/dist/assets/code-dark-CDkOQAtK.css +0 -1
  97. package/dist/assets/file-icons-EFUGSSwf.js +0 -1
  98. package/dist/assets/main-D073SnW4.css +0 -1
  99. package/dist/assets/qoder-C9LmmOSf.svg +0 -1
  100. package/dist/assets/qoder-Cf9gl0Y5.svg +0 -1
  101. package/dist/assets/qoder-gHCinseV.svg +0 -1
  102. package/dist/assets/workspace-view-state-CTyDzk2D.js +0 -1
  103. package/dist/assets/zsh-CLpveKlF.svg +0 -1
  104. package/dist/assets/zsh-FxSpMPbz.svg +0 -1
  105. /package/dist/assets/{api-D1lyBYIQ.js → api-D8nyOEbz.js} +0 -0
  106. /package/dist/assets/{core-ZlAPicox.js → core-D0LFJkDt.js} +0 -0
  107. /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
- const execFileAsync = promisify(execFile);
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: 30 * 60_000,
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
- const catalog = await codexModelOptionsCache.get('catalog');
644
- res.json(catalog);
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, { fresh: req.query.fresh === '1' });
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.codexRuntimeMode === 'app-server'
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
- scheduleBroadcastState();
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 = await findAgentSession(session.provider, session.sessionId, { limit: 200, providerHomeId: session.providerHomeId || 'default', providerHomes: configuredProviderHomes() });
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
- broadcastState();
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.resizeAgentSession(data.agentId, data.cols, data.rows);
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.getAgentWorkspaceRoot(data.agentId);
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
- ...agentManager.getState(),
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.send(message);
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
- 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
- }
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
- 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
- }
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;