farming-code 2.2.19 → 2.2.22

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 (53) hide show
  1. package/README.md +5 -5
  2. package/README.zh_cn.md +5 -5
  3. package/THIRD_PARTY_NOTICES.md +5 -8
  4. package/backend/acp/client-services.js +37 -5
  5. package/backend/acp/patch-decisions.js +19 -3
  6. package/backend/acp-runtime.js +1201 -198
  7. package/backend/acp-transcript.js +66 -0
  8. package/backend/agent-lifecycle-journal.js +133 -0
  9. package/backend/agent-manager.js +2715 -354
  10. package/backend/agent-order.js +8 -4
  11. package/backend/agent-runtime-binding.js +1 -0
  12. package/backend/bundle-update-helper.js +5 -1
  13. package/backend/config-manager.js +16 -0
  14. package/backend/control-api.js +146 -20
  15. package/backend/farming-app-cli.js +335 -10
  16. package/backend/farming-session-store.js +283 -36
  17. package/backend/json-cli-runtime.js +120 -6
  18. package/backend/npm-update-helper.js +97 -18
  19. package/backend/provider-adapters.js +18 -6
  20. package/backend/review-session-service.js +31 -17
  21. package/backend/server.js +337 -103
  22. package/backend/storage-layout.js +10 -0
  23. package/backend/terminal-screen-worker-pool.js +15 -6
  24. package/backend/theme-manager.js +23 -5
  25. package/backend/update-service.js +239 -27
  26. package/backend/usage-monitor.js +11 -0
  27. package/backend/workspace-file-router.js +9 -3
  28. package/backend/workspace-file-service.js +292 -62
  29. package/dist/acp/codex-acp-1.1.4.js +32 -1
  30. package/dist/assets/App-CmZdNavW.js +210 -0
  31. package/dist/assets/{FileEditorMarkdownPreview-R0jwwLME.js → FileEditorMarkdownPreview-KbnE8VC8.js} +1 -1
  32. package/dist/assets/FileEditorPane-5_HUejjd.js +2 -0
  33. package/dist/assets/IconGlyphs-C2WFcoJS.js +1 -0
  34. package/dist/assets/ProjectFilesSection-C1RAJWOE.js +12 -0
  35. package/dist/assets/{ReviewPage-DBKNOWsJ.js → ReviewPage-L1vh1Ggs.js} +3 -3
  36. package/dist/assets/code-dark-BKtvf0Xc.css +1 -0
  37. package/dist/assets/file-icons-CHWt5nSy.js +1 -0
  38. package/dist/assets/{index-DQdtPfCk.js → index-RvUiMpGP.js} +2 -2
  39. package/dist/assets/main-XMXgyJJt.css +1 -0
  40. package/dist/assets/workspace-view-state-BqB4HM7r.js +7 -0
  41. package/dist/index.html +1 -1
  42. package/frontend/skins/crt/app.js +365 -107
  43. package/frontend/skins/crt/index.html +5 -10
  44. package/frontend/skins/crt/styles/billing.css +6 -1
  45. package/package.json +9 -3
  46. package/dist/assets/App-CbXYBccJ.js +0 -210
  47. package/dist/assets/FileEditorPane-CJs6kfZO.js +0 -2
  48. package/dist/assets/IconGlyphs-CZzDr5o2.js +0 -1
  49. package/dist/assets/ProjectFilesSection-DWCV07lA.js +0 -12
  50. package/dist/assets/code-dark-qIBiLBWV.css +0 -1
  51. package/dist/assets/file-icons-DpbrD5ZP.js +0 -1
  52. package/dist/assets/main-yzZEfgft.css +0 -1
  53. package/dist/assets/workspace-view-state-BpPINu84.js +0 -7
package/backend/server.js CHANGED
@@ -18,7 +18,6 @@ const { listAvailableAgents, resolveCompatibleCodexExecutable } = require('./exe
18
18
  const { readClaudeSettingsSummary } = require('./claude-settings');
19
19
  const { listCodexModelOptions } = require('./codex-models');
20
20
  const { listCodexSessions } = require('./codex-session-history');
21
- const { unarchiveCodexSession } = require('./codex-session-archive');
22
21
  const {
23
22
  buildAgentSessionResumeCommand,
24
23
  findAgentSession,
@@ -32,6 +31,7 @@ const {
32
31
  const {
33
32
  findActiveAgentClaimingSession,
34
33
  mainPageAgentSessionKey,
34
+ mainPageAgentSessionFromKey,
35
35
  mainPageAgentSessionsToAutoResume,
36
36
  resumedAgentSource,
37
37
  } = require('./main-page-session');
@@ -112,6 +112,20 @@ const agentManager = new AgentManager(configManager, {
112
112
  authDisabled: !authEnabled,
113
113
  cliBinDir: resolveCliBinDir(),
114
114
  });
115
+
116
+ async function requireAgentRecoveryForHttp(res) {
117
+ try {
118
+ await agentManager.whenRecovered();
119
+ return true;
120
+ } catch (error) {
121
+ res.status(503).json({
122
+ error: error?.message || 'Agent lifecycle recovery is unavailable',
123
+ retryable: true,
124
+ });
125
+ return false;
126
+ }
127
+ }
128
+
115
129
  const themeManager = new ThemeManager({ configDir: configManager.farmingDir });
116
130
  const workspaceFileService = new WorkspaceFileService();
117
131
  const workspaceRootRegistry = new WorkspaceRootRegistry(agentManager);
@@ -744,8 +758,12 @@ app.get(routePath(BASE_PATH, '/api/claude/settings'), (req, res) => {
744
758
  app.get(routePath(BASE_PATH, '/api/usage'), async (req, res) => {
745
759
  try {
746
760
  const fresh = req.query.fresh === '1';
761
+ const live = req.query.live === '1';
747
762
  if (fresh) usageMonitor.invalidateDailyCache();
748
- const usage = await usageSummaryCache.get('summary', fresh ? { force: true } : {});
763
+ const usage = await usageSummaryCache.get(
764
+ 'summary',
765
+ fresh ? { force: true } : live ? { maxAgeMs: 15_000 } : {},
766
+ );
749
767
  res.json({ usage });
750
768
  } catch (error) {
751
769
  res.status(500).json({ error: error.message || 'Failed to read usage information' });
@@ -813,16 +831,6 @@ app.get(routePath(BASE_PATH, '/api/update'), async (req, res) => {
813
831
  });
814
832
 
815
833
  app.post(routePath(BASE_PATH, '/api/update/install'), express.json(), async (req, res) => {
816
- const blockers = blockingUpdateAgents();
817
- const force = req.body && req.body.force === true;
818
- if (blockers.length > 0 && !force) {
819
- res.status(409).json({
820
- error: 'Cannot upgrade while non-recoverable project agents are running',
821
- blockingAgents: blockers,
822
- });
823
- return;
824
- }
825
-
826
834
  try {
827
835
  const state = await updateService.startInstall({
828
836
  assetName: req.body && typeof req.body.assetName === 'string' ? req.body.assetName : '',
@@ -830,7 +838,7 @@ app.post(routePath(BASE_PATH, '/api/update/install'), express.json(), async (req
830
838
  res.status(202).json({
831
839
  update: {
832
840
  state,
833
- blockingAgents: blockers,
841
+ blockingAgents: blockingUpdateAgents(),
834
842
  },
835
843
  });
836
844
  } catch (error) {
@@ -838,6 +846,25 @@ app.post(routePath(BASE_PATH, '/api/update/install'), express.json(), async (req
838
846
  }
839
847
  });
840
848
 
849
+ app.post(routePath(BASE_PATH, '/api/update/restart'), express.json(), async (req, res) => {
850
+ const blockers = blockingUpdateAgents();
851
+ const force = req.body && req.body.force === true;
852
+ if (blockers.length > 0 && !force) {
853
+ res.status(409).json({
854
+ error: 'Cannot restart for update while non-recoverable project agents are running',
855
+ blockingAgents: blockers,
856
+ });
857
+ return;
858
+ }
859
+
860
+ try {
861
+ const state = await updateService.applyPreparedUpdate();
862
+ res.status(202).json({ update: { state, blockingAgents: blockers } });
863
+ } catch (error) {
864
+ res.status(500).json({ error: error.message || 'Failed to restart for update' });
865
+ }
866
+ });
867
+
841
868
  function warmCodexExecutableVersionCache() {
842
869
  const startedAt = Date.now();
843
870
  try {
@@ -957,6 +984,40 @@ app.patch(routePath(BASE_PATH, '/api/agent-sessions/:provider/:sessionId'), expr
957
984
  res.json({ sessionKey, pinned: req.body.pinned });
958
985
  });
959
986
 
987
+ app.post(routePath(BASE_PATH, '/api/main-page-agent-sessions'), express.json(), (req, res) => {
988
+ const operation = req.body?.operation;
989
+ const requestedKeys = Array.isArray(req.body?.sessionKeys) ? req.body.sessionKeys : [];
990
+ const sessionKeys = [...new Set(requestedKeys.map(key => String(key || '').trim()).filter(Boolean))];
991
+ if (
992
+ !['add', 'remove'].includes(operation)
993
+ || sessionKeys.length === 0
994
+ || sessionKeys.length > 50
995
+ || sessionKeys.some(key => !mainPageAgentSessionFromKey(key))
996
+ ) {
997
+ res.status(400).json({ error: 'A valid main-page Agent session mutation is required' });
998
+ return;
999
+ }
1000
+
1001
+ if (operation === 'add') {
1002
+ [...sessionKeys].reverse().forEach(sessionKey => {
1003
+ const session = mainPageAgentSessionFromKey(sessionKey);
1004
+ configManager.rememberMainPageSessionKey(sessionKey, {
1005
+ provider: session.provider,
1006
+ providerSessionId: session.sessionId,
1007
+ providerSessionKey: sessionKey,
1008
+ providerHomeId: session.providerHomeId || 'default',
1009
+ });
1010
+ });
1011
+ } else {
1012
+ configManager.removeMainPageSessionKeys(sessionKeys);
1013
+ }
1014
+
1015
+ const mainPageSessionKeys = configManager.getMainPageSessionKeys();
1016
+ agentSessionsCache.invalidate();
1017
+ res.json({ success: true, mainPageSessionKeys });
1018
+ scheduleBroadcastState();
1019
+ });
1020
+
960
1021
  app.get(routePath(BASE_PATH, '/api/themes'), (req, res) => {
961
1022
  const currentTheme = configManager.getSettings().theme || 'terminal';
962
1023
  res.json({
@@ -1309,13 +1370,60 @@ app.post(routePath(BASE_PATH, '/api/agents/:agentId/codex-terminal-profile'), ex
1309
1370
  });
1310
1371
 
1311
1372
  app.patch(routePath(BASE_PATH, '/api/agents/:agentId'), express.json(), async (req, res) => {
1373
+ if (!await requireAgentRecoveryForHttp(res)) return;
1312
1374
  const body = req.body || {};
1313
1375
  const updates = {};
1376
+ const providedPatchFields = [
1377
+ 'customTitle',
1378
+ 'task',
1379
+ 'pinned',
1380
+ 'unread',
1381
+ 'archived',
1382
+ 'readAttentionSeq',
1383
+ 'readOutputEpoch',
1384
+ 'readOutputSeq',
1385
+ 'launchPermissionMode',
1386
+ 'agentRuntimeMode',
1387
+ ].filter(field => Object.prototype.hasOwnProperty.call(body, field));
1388
+ const lifecyclePatchFields = providedPatchFields.filter(field => (
1389
+ field === 'launchPermissionMode'
1390
+ || field === 'agentRuntimeMode'
1391
+ || (field === 'archived' && body.archived === true)
1392
+ ));
1393
+ if (lifecyclePatchFields.length > 0 && providedPatchFields.length > 1) {
1394
+ res.status(400).json({
1395
+ error: 'Archive, permission restart, and runtime switch must be requested separately from other Agent updates',
1396
+ });
1397
+ return;
1398
+ }
1399
+ const ordinaryPatchGroups = [
1400
+ providedPatchFields.includes('customTitle') ? 'customTitle' : '',
1401
+ providedPatchFields.includes('task') ? 'task' : '',
1402
+ providedPatchFields.some(field => [
1403
+ 'pinned',
1404
+ 'unread',
1405
+ 'archived',
1406
+ 'readAttentionSeq',
1407
+ 'readOutputEpoch',
1408
+ 'readOutputSeq',
1409
+ ].includes(field)) ? 'flags' : '',
1410
+ ].filter(Boolean);
1411
+ if (ordinaryPatchGroups.length > 1) {
1412
+ res.status(400).json({
1413
+ error: 'Agent title, task, and flags must be updated in separate requests',
1414
+ });
1415
+ return;
1416
+ }
1417
+
1418
+ await agentManager.whenAgentLifecycleIdle(req.params.agentId);
1314
1419
 
1315
1420
  if (typeof body.customTitle === 'string') {
1316
1421
  const result = agentManager.renameAgent(req.params.agentId, body.customTitle);
1317
1422
  if (result.error) {
1318
- res.status(404).json({ error: result.error });
1423
+ const status = result.error === 'Agent not found'
1424
+ ? 404
1425
+ : (result.error.startsWith('Failed to ') ? 500 : 409);
1426
+ res.status(status).json({ error: result.error });
1319
1427
  return;
1320
1428
  }
1321
1429
  updates.customTitle = result.customTitle;
@@ -1324,7 +1432,10 @@ app.patch(routePath(BASE_PATH, '/api/agents/:agentId'), express.json(), async (r
1324
1432
  if (typeof body.task === 'string') {
1325
1433
  const result = agentManager.setAgentTask(req.params.agentId, body.task);
1326
1434
  if (result.error) {
1327
- res.status(404).json({ error: result.error });
1435
+ const status = result.error === 'Agent not found'
1436
+ ? 404
1437
+ : (result.error.startsWith('Failed to ') ? 500 : 409);
1438
+ res.status(status).json({ error: result.error });
1328
1439
  return;
1329
1440
  }
1330
1441
  updates.task = result.task;
@@ -1352,8 +1463,10 @@ app.patch(routePath(BASE_PATH, '/api/agents/:agentId'), express.json(), async (r
1352
1463
  if (flagPatch.archived === true) {
1353
1464
  const result = await agentManager.archiveAgent(req.params.agentId);
1354
1465
  if (result.error) {
1355
- const status = result.error === 'Agent not found' ? 404 : 400;
1356
- res.status(status).json({ error: result.error });
1466
+ const status = result.stopped === true
1467
+ ? 409
1468
+ : (result.error === 'Agent not found' ? 404 : 400);
1469
+ res.status(status).json(result);
1357
1470
  return;
1358
1471
  }
1359
1472
  Object.assign(updates, result);
@@ -1365,7 +1478,9 @@ app.patch(routePath(BASE_PATH, '/api/agents/:agentId'), express.json(), async (r
1365
1478
  if (Object.keys(flagPatch).length > 0) {
1366
1479
  const result = agentManager.updateAgentFlags(req.params.agentId, flagPatch);
1367
1480
  if (result.error) {
1368
- const status = result.error === 'Agent not found' ? 404 : 400;
1481
+ const status = result.error === 'Agent not found'
1482
+ ? 404
1483
+ : (result.error.startsWith('Failed to ') ? 500 : 409);
1369
1484
  res.status(status).json({ error: result.error });
1370
1485
  return;
1371
1486
  }
@@ -1411,13 +1526,17 @@ app.patch(routePath(BASE_PATH, '/api/agents/:agentId'), express.json(), async (r
1411
1526
  res.json({ agentId: req.params.agentId, ...updates });
1412
1527
  });
1413
1528
 
1414
- app.post(routePath(BASE_PATH, '/api/agents/:agentId/reorder'), express.json(), (req, res) => {
1529
+ app.post(routePath(BASE_PATH, '/api/agents/:agentId/reorder'), express.json(), async (req, res) => {
1530
+ if (!await requireAgentRecoveryForHttp(res)) return;
1531
+ await agentManager.whenAgentLifecycleIdle(req.params.agentId);
1415
1532
  const result = agentManager.reorderAgent(req.params.agentId, {
1416
1533
  beforeAgentId: req.body?.beforeAgentId,
1417
1534
  afterAgentId: req.body?.afterAgentId,
1418
1535
  });
1419
1536
  if (result.error) {
1420
- const status = result.error === 'Agent not found' ? 404 : 400;
1537
+ const status = result.error === 'Agent not found'
1538
+ ? 404
1539
+ : (result.error.startsWith('Failed to ') ? 500 : 409);
1421
1540
  res.status(status).json({ error: result.error });
1422
1541
  return;
1423
1542
  }
@@ -1426,8 +1545,21 @@ app.post(routePath(BASE_PATH, '/api/agents/:agentId/reorder'), express.json(), (
1426
1545
  });
1427
1546
 
1428
1547
  app.post(routePath(BASE_PATH, '/api/agents/:agentId/fork'), express.json(), async (req, res) => {
1548
+ if (!await requireAgentRecoveryForHttp(res)) return;
1429
1549
  const mode = req.body && typeof req.body.mode === 'string' ? req.body.mode : 'same-worktree';
1430
- const result = await agentManager.forkAgent(req.params.agentId, mode);
1550
+ const targetRuntime = req.body && typeof req.body.targetRuntime === 'string'
1551
+ ? req.body.targetRuntime
1552
+ : '';
1553
+ if (targetRuntime && targetRuntime !== 'chat') {
1554
+ res.status(400).json({ error: 'Unsupported Fork target runtime' });
1555
+ return;
1556
+ }
1557
+ const result = await agentManager.forkAgent(req.params.agentId, mode, {
1558
+ ...(targetRuntime ? { targetRuntime } : {}),
1559
+ ...(Number.isSafeInteger(req.body?.expectedRevision)
1560
+ ? { expectedRevision: req.body.expectedRevision }
1561
+ : {}),
1562
+ });
1431
1563
  if (result.error) {
1432
1564
  const status = result.error === 'Agent not found' ? 404 : 400;
1433
1565
  res.status(status).json({ error: result.error });
@@ -1533,6 +1665,16 @@ app.post(routePath(BASE_PATH, '/api/projects/pin'), express.json(), (req, res) =
1533
1665
  }
1534
1666
  });
1535
1667
 
1668
+ app.patch(routePath(BASE_PATH, '/api/projects/name'), express.json(), (req, res) => {
1669
+ try {
1670
+ const result = configManager.setProjectName(req.body?.workspace, req.body?.name);
1671
+ scheduleBroadcastState();
1672
+ res.json(result);
1673
+ } catch (error) {
1674
+ res.status(400).json({ error: error.message || 'Failed to rename Project' });
1675
+ }
1676
+ });
1677
+
1536
1678
  app.post(routePath(BASE_PATH, '/api/projects/create-worktree'), express.json(), async (req, res) => {
1537
1679
  let created = null;
1538
1680
  try {
@@ -1564,6 +1706,7 @@ app.post(routePath(BASE_PATH, '/api/projects/create-worktree'), express.json(),
1564
1706
  });
1565
1707
 
1566
1708
  app.post(routePath(BASE_PATH, '/api/projects/delete-worktree'), express.json(), async (req, res) => {
1709
+ if (!await requireAgentRecoveryForHttp(res)) return;
1567
1710
  const body = req.body || {};
1568
1711
  const workspace = typeof body.workspace === 'string' ? body.workspace : '';
1569
1712
  const inspected = await agentManager.inspectForkWorktreeProject(workspace);
@@ -1580,42 +1723,22 @@ app.post(routePath(BASE_PATH, '/api/projects/delete-worktree'), express.json(),
1580
1723
  return;
1581
1724
  }
1582
1725
 
1583
- const wasPinned = (configManager.getSettings().pinnedProjectWorkspaces || []).includes(inspected.workspace);
1584
- let membership;
1585
- try {
1586
- membership = configManager.removeProjectWorkspace(inspected.workspace);
1587
- } catch (error) {
1588
- res.status(500).json({
1589
- ...inspected,
1590
- error: error.message || 'Project removal failed; the worktree was not deleted',
1591
- });
1592
- return;
1593
- }
1594
1726
  const result = await agentManager.deleteForkWorktreeProject(inspected.workspace, { force: body.force === true });
1595
1727
  if (result.error) {
1596
- let rollbackError = '';
1597
- try {
1598
- configManager.mountProjectWorkspace(inspected.workspace);
1599
- if (wasPinned) configManager.setProjectWorkspacePinned(inspected.workspace, true);
1600
- } catch (error) {
1601
- rollbackError = error.message || String(error);
1602
- }
1603
1728
  broadcastState();
1604
1729
  const status = result.requiresForce ? 409 : 400;
1605
- res.status(status).json({
1606
- ...result,
1607
- ...(rollbackError ? {
1608
- error: `${result.error}. Project restore failed: ${rollbackError}`,
1609
- rollbackError,
1610
- } : {}),
1611
- });
1730
+ res.status(status).json(result);
1612
1731
  return;
1613
1732
  }
1614
- if (result.cleanupError) {
1733
+
1734
+ let membership;
1735
+ try {
1736
+ membership = configManager.removeProjectWorkspace(inspected.workspace);
1737
+ } catch (error) {
1615
1738
  broadcastState();
1616
1739
  res.status(500).json({
1617
1740
  ...result,
1618
- error: `Worktree was deleted, but Agent cleanup failed: ${result.cleanupError}`,
1741
+ error: `Worktree was deleted, but Project membership cleanup failed: ${error.message || error}`,
1619
1742
  });
1620
1743
  return;
1621
1744
  }
@@ -1758,30 +1881,36 @@ async function resumeAgentSessionById(provider, rawSessionId, options = {}) {
1758
1881
  providerHomeId,
1759
1882
  providerHomes: configuredProviderHomes(),
1760
1883
  });
1884
+ if (
1885
+ options.allowUnarchiveArchived === true
1886
+ && normalizedProvider === 'codex'
1887
+ && !requestedAsMain
1888
+ ) {
1889
+ const providerHomes = configuredProviderHomes();
1890
+ const configuredHomePath = (providerHomes.codex || [])
1891
+ .find(home => home.id === providerHomeId)?.path || '';
1892
+ const unarchiveResult = await agentManager.ensureCodexSessionAvailable(sessionId, {
1893
+ providerHomeId,
1894
+ providerHomePath: session?.providerHomePath || configuredHomePath,
1895
+ providerHomes,
1896
+ cwd: session?.cwd || session?.workspace || '',
1897
+ });
1898
+ if (unarchiveResult?.error) return unarchiveResult;
1899
+ session = await findAgentSession(normalizedProvider, sessionId, {
1900
+ limit: 1000,
1901
+ providerLimit: 1000,
1902
+ scanLimit: 5000,
1903
+ providerHomeId,
1904
+ providerHomes,
1905
+ }) || (session ? { ...session, archived: false } : session);
1906
+ }
1761
1907
  if (session && session.archived && !shouldFork) {
1762
- if (options.allowUnarchiveArchived === true && normalizedProvider === 'codex' && !requestedAsMain) {
1763
- const unarchiveResult = await unarchiveCodexSession(sessionId, session);
1764
- if (unarchiveResult.error) {
1765
- return unarchiveResult;
1766
- }
1767
- session = await findAgentSession(normalizedProvider, sessionId, {
1768
- limit: 1000,
1769
- providerLimit: 1000,
1770
- scanLimit: 5000,
1771
- providerHomeId,
1772
- providerHomes: configuredProviderHomes(),
1773
- }) || {
1774
- ...session,
1775
- archived: false,
1776
- };
1777
- } else {
1778
- forgetMainPageAgentSession(normalizedProvider, sessionId, providerHomeId);
1779
- return {
1780
- error: `${session.providerName || normalizedProvider} session is archived. Unarchive it before resuming.`,
1781
- status: 409,
1782
- archived: true,
1783
- };
1784
- }
1908
+ forgetMainPageAgentSession(normalizedProvider, sessionId, providerHomeId);
1909
+ return {
1910
+ error: `${session.providerName || normalizedProvider} session is archived. Unarchive it before resuming.`,
1911
+ status: 409,
1912
+ archived: true,
1913
+ };
1785
1914
  }
1786
1915
  if (!shouldFork && !requestedAsMain) {
1787
1916
  const claimingAgent = findActiveAgentClaimingSession(agentManager.getState().agents, normalizedProvider, {
@@ -2105,6 +2234,7 @@ async function autoResumeMainPageAgentSessions() {
2105
2234
 
2106
2235
  app.post(routePath(BASE_PATH, '/api/settings'), express.json(), (req, res) => {
2107
2236
  const settingsPatch = { ...(req.body || {}) };
2237
+ delete settingsPatch.mainPageSessionKeys;
2108
2238
  delete settingsPatch.projectWorkspaces;
2109
2239
  delete settingsPatch.pinnedProjectWorkspaces;
2110
2240
  configManager.updateSettings(settingsPatch);
@@ -2242,7 +2372,11 @@ function restartMainAgent(ws, command) {
2242
2372
  agent.id === state.mainAgentId || agent.isMain === true
2243
2373
  ));
2244
2374
  if (currentMain) {
2245
- await agentManager.killAgent(currentMain.id);
2375
+ const killed = await agentManager.killAgent(currentMain.id);
2376
+ if (killed?.error) {
2377
+ ws.send(JSON.stringify({ type: 'error', message: killed.error }));
2378
+ return;
2379
+ }
2246
2380
  }
2247
2381
 
2248
2382
  await agentManager.startAgent(normalizedCommand, null, (agentId, error) => {
@@ -2264,6 +2398,39 @@ function restartMainAgent(ws, command) {
2264
2398
  })();
2265
2399
  }
2266
2400
 
2401
+ async function killAgentFromMessage(ws, agentId, options = {}) {
2402
+ try {
2403
+ const requested = await agentManager.requestKillAgent(agentId, {
2404
+ acknowledgeUnprovenAcpExit: options.acknowledgeUnprovenAcpExit === true,
2405
+ });
2406
+ const result = requested.result;
2407
+ if (result?.error) {
2408
+ ws.send(JSON.stringify({ type: 'error', message: result.error }));
2409
+ } else if (result?.accepted) {
2410
+ ws.send(JSON.stringify({ type: 'agent-delete-accepted', ...result }));
2411
+ void requested.completion.then(completed => {
2412
+ if (completed?.error && ws.readyState === WebSocket.OPEN) {
2413
+ ws.send(JSON.stringify({ type: 'error', message: completed.error }));
2414
+ }
2415
+ }).catch(error => {
2416
+ if (ws.readyState === WebSocket.OPEN) {
2417
+ ws.send(JSON.stringify({
2418
+ type: 'error',
2419
+ message: error instanceof Error ? error.message : 'Failed to stop Agent',
2420
+ }));
2421
+ }
2422
+ }).finally(() => broadcastState());
2423
+ }
2424
+ } catch (error) {
2425
+ ws.send(JSON.stringify({
2426
+ type: 'error',
2427
+ message: error instanceof Error ? error.message : 'Failed to stop Agent',
2428
+ }));
2429
+ } finally {
2430
+ broadcastState();
2431
+ }
2432
+ }
2433
+
2267
2434
  async function sendInputMessage(ws, data) {
2268
2435
  const targetAgentId = resolveInputTargetAgentId(ws, data);
2269
2436
  if (!targetAgentId) return;
@@ -2400,6 +2567,7 @@ function handleMessage(ws, data) {
2400
2567
  task: typeof data.task === 'string' ? data.task : '',
2401
2568
  workflowTemplate: typeof data.workflowTemplate === 'string' ? data.workflowTemplate : '',
2402
2569
  customTitle: typeof data.customTitle === 'string' ? data.customTitle : '',
2570
+ createRequestId: typeof data.requestId === 'string' ? data.requestId : '',
2403
2571
  codexApprovalMode: typeof data.codexApprovalMode === 'string' ? data.codexApprovalMode : undefined,
2404
2572
  agentRuntimeMode: ['json', 'acp', 'chat'].includes(data.agentRuntimeMode) ? data.agentRuntimeMode : 'terminal',
2405
2573
  acpHistoryMode: data.acpHistoryMode === 'resume' ? 'resume' : 'load',
@@ -2482,8 +2650,9 @@ function handleMessage(ws, data) {
2482
2650
  break;
2483
2651
 
2484
2652
  case 'kill-agent':
2485
- agentManager.killAgent(data.agentId);
2486
- broadcastState();
2653
+ void killAgentFromMessage(ws, data.agentId, {
2654
+ acknowledgeUnprovenAcpExit: data.acknowledgeUnprovenAcpExit === true,
2655
+ });
2487
2656
  break;
2488
2657
 
2489
2658
  case 'restart-main-agent':
@@ -2498,10 +2667,26 @@ function handleMessage(ws, data) {
2498
2667
  const { resolveInputTargetAgentId } = require('./input-routing');
2499
2668
 
2500
2669
  function sendWorkspaceFileWatchError(ws, error) {
2670
+ if (ws.readyState !== WebSocket.OPEN) return;
2501
2671
  const message = error instanceof WorkspaceFileError ? error.message : 'failed to watch workspace files';
2502
2672
  ws.send(JSON.stringify({ type: 'error', message }));
2503
2673
  }
2504
2674
 
2675
+ function isCurrentWorkspaceFileWatch(ws, agentId, record) {
2676
+ return Boolean(
2677
+ !record.cancelled
2678
+ && ws.workspaceFileUnsubscribes?.get(agentId) === record
2679
+ );
2680
+ }
2681
+
2682
+ function closeWorkspaceFileWatchRecord(record) {
2683
+ record.cancelled = true;
2684
+ if (!record.unsubscribe) return;
2685
+ Promise.resolve(record.unsubscribe()).catch((error) => {
2686
+ console.error('Failed to clear workspace file watch:', error);
2687
+ });
2688
+ }
2689
+
2505
2690
  function clearWorkspaceFileWatch(ws, agentId = null) {
2506
2691
  const watches = ws.workspaceFileUnsubscribes;
2507
2692
  if (!watches) return;
@@ -2510,12 +2695,10 @@ function clearWorkspaceFileWatch(ws, agentId = null) {
2510
2695
  ? [[agentId, watches.get(agentId)]]
2511
2696
  : Array.from(watches.entries());
2512
2697
 
2513
- entries.forEach(([watchedAgentId, unsubscribe]) => {
2514
- if (!unsubscribe) return;
2515
- watches.delete(watchedAgentId);
2516
- Promise.resolve(unsubscribe()).catch((error) => {
2517
- console.error('Failed to clear workspace file watch:', error);
2518
- });
2698
+ entries.forEach(([watchedAgentId, record]) => {
2699
+ if (!record) return;
2700
+ if (watches.get(watchedAgentId) === record) watches.delete(watchedAgentId);
2701
+ closeWorkspaceFileWatchRecord(record);
2519
2702
  });
2520
2703
 
2521
2704
  if (watches.size === 0) {
@@ -2531,33 +2714,62 @@ async function watchWorkspaceFiles(ws, data) {
2531
2714
  if (!ws.workspaceFileUnsubscribes) {
2532
2715
  ws.workspaceFileUnsubscribes = new Map();
2533
2716
  }
2534
- if (ws.workspaceFileUnsubscribes.has(data.agentId)) {
2535
- ws.send(JSON.stringify({
2536
- type: 'workspace-file-watch',
2537
- agentId: data.agentId,
2538
- watching: true,
2539
- }));
2717
+ const watches = ws.workspaceFileUnsubscribes;
2718
+ const existing = watches.get(data.agentId);
2719
+ if (existing) {
2720
+ const watching = await existing.ready;
2721
+ if (watching && isCurrentWorkspaceFileWatch(ws, data.agentId, existing) && ws.readyState === WebSocket.OPEN) {
2722
+ ws.send(JSON.stringify({
2723
+ type: 'workspace-file-watch',
2724
+ agentId: data.agentId,
2725
+ watching: true,
2726
+ }));
2727
+ }
2540
2728
  return;
2541
2729
  }
2542
2730
 
2543
2731
  const root = resolveWorkspaceRoot(agentManager, data.agentId);
2544
-
2545
- const unsubscribe = await workspaceFileService.subscribe(root, (event) => {
2546
- if (ws.readyState !== WebSocket.OPEN) return;
2547
- ws.send(JSON.stringify({
2548
- type: 'workspace-file-event',
2549
- event: {
2732
+ const record = {
2733
+ cancelled: false,
2734
+ unsubscribe: null,
2735
+ ready: null,
2736
+ };
2737
+ record.ready = (async () => {
2738
+ const unsubscribe = await workspaceFileService.subscribe(root, (event) => {
2739
+ if (!isCurrentWorkspaceFileWatch(ws, data.agentId, record) || ws.readyState !== WebSocket.OPEN) return;
2740
+ ws.send(JSON.stringify({
2741
+ type: 'workspace-file-event',
2742
+ event: {
2743
+ agentId: data.agentId,
2744
+ ...event,
2745
+ },
2746
+ }));
2747
+ });
2748
+ record.unsubscribe = unsubscribe;
2749
+ if (!isCurrentWorkspaceFileWatch(ws, data.agentId, record) || ws.readyState !== WebSocket.OPEN) {
2750
+ closeWorkspaceFileWatchRecord(record);
2751
+ return false;
2752
+ }
2753
+ return true;
2754
+ })();
2755
+ watches.set(data.agentId, record);
2756
+ try {
2757
+ const watching = await record.ready;
2758
+ if (watching && isCurrentWorkspaceFileWatch(ws, data.agentId, record) && ws.readyState === WebSocket.OPEN) {
2759
+ ws.send(JSON.stringify({
2760
+ type: 'workspace-file-watch',
2550
2761
  agentId: data.agentId,
2551
- ...event,
2552
- },
2553
- }));
2554
- });
2555
- ws.workspaceFileUnsubscribes.set(data.agentId, unsubscribe);
2556
- ws.send(JSON.stringify({
2557
- type: 'workspace-file-watch',
2558
- agentId: data.agentId,
2559
- watching: true,
2560
- }));
2762
+ watching: true,
2763
+ }));
2764
+ }
2765
+ } catch (error) {
2766
+ if (watches.get(data.agentId) === record) watches.delete(data.agentId);
2767
+ closeWorkspaceFileWatchRecord(record);
2768
+ if (watches.size === 0 && ws.workspaceFileUnsubscribes === watches) {
2769
+ ws.workspaceFileUnsubscribes = null;
2770
+ }
2771
+ throw error;
2772
+ }
2561
2773
  } catch (error) {
2562
2774
  sendWorkspaceFileWatchError(ws, error);
2563
2775
  }
@@ -2900,18 +3112,40 @@ async function shutdownServer(options = {}) {
2900
3112
  ? options.preserveTerminalHost === true
2901
3113
  : process.env.FARMING_NATIVE_PTY_HOST_PERSIST !== '0';
2902
3114
 
3115
+ try {
3116
+ await agentManager.dispose({ preserveTerminalHost });
3117
+ } catch (error) {
3118
+ const teardownFrozen = agentManager.disposing === true;
3119
+ shutdownStarted = false;
3120
+ console.error('Farming shutdown blocked because Agent runtime cleanup could not be verified:', error);
3121
+ if (teardownFrozen) {
3122
+ clearBroadcastTimers();
3123
+ tokenAuth.cleanup();
3124
+ await Promise.allSettled([
3125
+ closeWebSocketServer(),
3126
+ workspaceFileService.dispose(),
3127
+ closeHttpServer(),
3128
+ ]);
3129
+ }
3130
+ if (options.exit === true) process.exitCode = 1;
3131
+ return {
3132
+ error: error?.message || 'Agent runtime cleanup could not be verified',
3133
+ ...(teardownFrozen ? { frozen: true } : {}),
3134
+ };
3135
+ }
3136
+
2903
3137
  clearBroadcastTimers();
2904
3138
  tokenAuth.cleanup();
2905
3139
  await Promise.allSettled([
2906
3140
  closeWebSocketServer(),
2907
3141
  workspaceFileService.dispose(),
2908
- agentManager.dispose({ preserveTerminalHost }),
2909
3142
  closeHttpServer(),
2910
3143
  ]);
2911
3144
 
2912
3145
  if (options.exit === true) {
2913
3146
  process.exit(options.exitCode || 0);
2914
3147
  }
3148
+ return { stopped: true };
2915
3149
  }
2916
3150
 
2917
3151
  agentManager.onSessionStream((stream) => {