farming-code 2.2.20 → 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 (49) 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 +332 -102
  22. package/backend/storage-layout.js +10 -0
  23. package/backend/theme-manager.js +23 -5
  24. package/backend/update-service.js +239 -27
  25. package/backend/workspace-file-router.js +9 -3
  26. package/backend/workspace-file-service.js +292 -62
  27. package/dist/acp/codex-acp-1.1.4.js +32 -1
  28. package/dist/assets/App-CmZdNavW.js +210 -0
  29. package/dist/assets/{FileEditorMarkdownPreview-R0jwwLME.js → FileEditorMarkdownPreview-KbnE8VC8.js} +1 -1
  30. package/dist/assets/FileEditorPane-5_HUejjd.js +2 -0
  31. package/dist/assets/IconGlyphs-C2WFcoJS.js +1 -0
  32. package/dist/assets/ProjectFilesSection-C1RAJWOE.js +12 -0
  33. package/dist/assets/{ReviewPage-DBKNOWsJ.js → ReviewPage-L1vh1Ggs.js} +3 -3
  34. package/dist/assets/code-dark-BKtvf0Xc.css +1 -0
  35. package/dist/assets/file-icons-CHWt5nSy.js +1 -0
  36. package/dist/assets/{index-DuMjesL2.js → index-RvUiMpGP.js} +2 -2
  37. package/dist/assets/main-XMXgyJJt.css +1 -0
  38. package/dist/assets/workspace-view-state-BqB4HM7r.js +7 -0
  39. package/dist/index.html +1 -1
  40. package/frontend/skins/crt/app.js +199 -81
  41. package/package.json +9 -3
  42. package/dist/assets/App-BupehIbc.js +0 -210
  43. package/dist/assets/FileEditorPane-CJs6kfZO.js +0 -2
  44. package/dist/assets/IconGlyphs-CZzDr5o2.js +0 -1
  45. package/dist/assets/ProjectFilesSection-DWCV07lA.js +0 -12
  46. package/dist/assets/code-dark-BAt0Q_Op.css +0 -1
  47. package/dist/assets/file-icons-DpbrD5ZP.js +0 -1
  48. package/dist/assets/main-y2aK_56c.css +0 -1
  49. 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);
@@ -817,16 +831,6 @@ app.get(routePath(BASE_PATH, '/api/update'), async (req, res) => {
817
831
  });
818
832
 
819
833
  app.post(routePath(BASE_PATH, '/api/update/install'), express.json(), async (req, res) => {
820
- const blockers = blockingUpdateAgents();
821
- const force = req.body && req.body.force === true;
822
- if (blockers.length > 0 && !force) {
823
- res.status(409).json({
824
- error: 'Cannot upgrade while non-recoverable project agents are running',
825
- blockingAgents: blockers,
826
- });
827
- return;
828
- }
829
-
830
834
  try {
831
835
  const state = await updateService.startInstall({
832
836
  assetName: req.body && typeof req.body.assetName === 'string' ? req.body.assetName : '',
@@ -834,7 +838,7 @@ app.post(routePath(BASE_PATH, '/api/update/install'), express.json(), async (req
834
838
  res.status(202).json({
835
839
  update: {
836
840
  state,
837
- blockingAgents: blockers,
841
+ blockingAgents: blockingUpdateAgents(),
838
842
  },
839
843
  });
840
844
  } catch (error) {
@@ -842,6 +846,25 @@ app.post(routePath(BASE_PATH, '/api/update/install'), express.json(), async (req
842
846
  }
843
847
  });
844
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
+
845
868
  function warmCodexExecutableVersionCache() {
846
869
  const startedAt = Date.now();
847
870
  try {
@@ -961,6 +984,40 @@ app.patch(routePath(BASE_PATH, '/api/agent-sessions/:provider/:sessionId'), expr
961
984
  res.json({ sessionKey, pinned: req.body.pinned });
962
985
  });
963
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
+
964
1021
  app.get(routePath(BASE_PATH, '/api/themes'), (req, res) => {
965
1022
  const currentTheme = configManager.getSettings().theme || 'terminal';
966
1023
  res.json({
@@ -1313,13 +1370,60 @@ app.post(routePath(BASE_PATH, '/api/agents/:agentId/codex-terminal-profile'), ex
1313
1370
  });
1314
1371
 
1315
1372
  app.patch(routePath(BASE_PATH, '/api/agents/:agentId'), express.json(), async (req, res) => {
1373
+ if (!await requireAgentRecoveryForHttp(res)) return;
1316
1374
  const body = req.body || {};
1317
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);
1318
1419
 
1319
1420
  if (typeof body.customTitle === 'string') {
1320
1421
  const result = agentManager.renameAgent(req.params.agentId, body.customTitle);
1321
1422
  if (result.error) {
1322
- 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 });
1323
1427
  return;
1324
1428
  }
1325
1429
  updates.customTitle = result.customTitle;
@@ -1328,7 +1432,10 @@ app.patch(routePath(BASE_PATH, '/api/agents/:agentId'), express.json(), async (r
1328
1432
  if (typeof body.task === 'string') {
1329
1433
  const result = agentManager.setAgentTask(req.params.agentId, body.task);
1330
1434
  if (result.error) {
1331
- 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 });
1332
1439
  return;
1333
1440
  }
1334
1441
  updates.task = result.task;
@@ -1356,8 +1463,10 @@ app.patch(routePath(BASE_PATH, '/api/agents/:agentId'), express.json(), async (r
1356
1463
  if (flagPatch.archived === true) {
1357
1464
  const result = await agentManager.archiveAgent(req.params.agentId);
1358
1465
  if (result.error) {
1359
- const status = result.error === 'Agent not found' ? 404 : 400;
1360
- 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);
1361
1470
  return;
1362
1471
  }
1363
1472
  Object.assign(updates, result);
@@ -1369,7 +1478,9 @@ app.patch(routePath(BASE_PATH, '/api/agents/:agentId'), express.json(), async (r
1369
1478
  if (Object.keys(flagPatch).length > 0) {
1370
1479
  const result = agentManager.updateAgentFlags(req.params.agentId, flagPatch);
1371
1480
  if (result.error) {
1372
- 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);
1373
1484
  res.status(status).json({ error: result.error });
1374
1485
  return;
1375
1486
  }
@@ -1415,13 +1526,17 @@ app.patch(routePath(BASE_PATH, '/api/agents/:agentId'), express.json(), async (r
1415
1526
  res.json({ agentId: req.params.agentId, ...updates });
1416
1527
  });
1417
1528
 
1418
- 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);
1419
1532
  const result = agentManager.reorderAgent(req.params.agentId, {
1420
1533
  beforeAgentId: req.body?.beforeAgentId,
1421
1534
  afterAgentId: req.body?.afterAgentId,
1422
1535
  });
1423
1536
  if (result.error) {
1424
- 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);
1425
1540
  res.status(status).json({ error: result.error });
1426
1541
  return;
1427
1542
  }
@@ -1430,8 +1545,21 @@ app.post(routePath(BASE_PATH, '/api/agents/:agentId/reorder'), express.json(), (
1430
1545
  });
1431
1546
 
1432
1547
  app.post(routePath(BASE_PATH, '/api/agents/:agentId/fork'), express.json(), async (req, res) => {
1548
+ if (!await requireAgentRecoveryForHttp(res)) return;
1433
1549
  const mode = req.body && typeof req.body.mode === 'string' ? req.body.mode : 'same-worktree';
1434
- 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
+ });
1435
1563
  if (result.error) {
1436
1564
  const status = result.error === 'Agent not found' ? 404 : 400;
1437
1565
  res.status(status).json({ error: result.error });
@@ -1537,6 +1665,16 @@ app.post(routePath(BASE_PATH, '/api/projects/pin'), express.json(), (req, res) =
1537
1665
  }
1538
1666
  });
1539
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
+
1540
1678
  app.post(routePath(BASE_PATH, '/api/projects/create-worktree'), express.json(), async (req, res) => {
1541
1679
  let created = null;
1542
1680
  try {
@@ -1568,6 +1706,7 @@ app.post(routePath(BASE_PATH, '/api/projects/create-worktree'), express.json(),
1568
1706
  });
1569
1707
 
1570
1708
  app.post(routePath(BASE_PATH, '/api/projects/delete-worktree'), express.json(), async (req, res) => {
1709
+ if (!await requireAgentRecoveryForHttp(res)) return;
1571
1710
  const body = req.body || {};
1572
1711
  const workspace = typeof body.workspace === 'string' ? body.workspace : '';
1573
1712
  const inspected = await agentManager.inspectForkWorktreeProject(workspace);
@@ -1584,42 +1723,22 @@ app.post(routePath(BASE_PATH, '/api/projects/delete-worktree'), express.json(),
1584
1723
  return;
1585
1724
  }
1586
1725
 
1587
- const wasPinned = (configManager.getSettings().pinnedProjectWorkspaces || []).includes(inspected.workspace);
1588
- let membership;
1589
- try {
1590
- membership = configManager.removeProjectWorkspace(inspected.workspace);
1591
- } catch (error) {
1592
- res.status(500).json({
1593
- ...inspected,
1594
- error: error.message || 'Project removal failed; the worktree was not deleted',
1595
- });
1596
- return;
1597
- }
1598
1726
  const result = await agentManager.deleteForkWorktreeProject(inspected.workspace, { force: body.force === true });
1599
1727
  if (result.error) {
1600
- let rollbackError = '';
1601
- try {
1602
- configManager.mountProjectWorkspace(inspected.workspace);
1603
- if (wasPinned) configManager.setProjectWorkspacePinned(inspected.workspace, true);
1604
- } catch (error) {
1605
- rollbackError = error.message || String(error);
1606
- }
1607
1728
  broadcastState();
1608
1729
  const status = result.requiresForce ? 409 : 400;
1609
- res.status(status).json({
1610
- ...result,
1611
- ...(rollbackError ? {
1612
- error: `${result.error}. Project restore failed: ${rollbackError}`,
1613
- rollbackError,
1614
- } : {}),
1615
- });
1730
+ res.status(status).json(result);
1616
1731
  return;
1617
1732
  }
1618
- if (result.cleanupError) {
1733
+
1734
+ let membership;
1735
+ try {
1736
+ membership = configManager.removeProjectWorkspace(inspected.workspace);
1737
+ } catch (error) {
1619
1738
  broadcastState();
1620
1739
  res.status(500).json({
1621
1740
  ...result,
1622
- error: `Worktree was deleted, but Agent cleanup failed: ${result.cleanupError}`,
1741
+ error: `Worktree was deleted, but Project membership cleanup failed: ${error.message || error}`,
1623
1742
  });
1624
1743
  return;
1625
1744
  }
@@ -1762,30 +1881,36 @@ async function resumeAgentSessionById(provider, rawSessionId, options = {}) {
1762
1881
  providerHomeId,
1763
1882
  providerHomes: configuredProviderHomes(),
1764
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
+ }
1765
1907
  if (session && session.archived && !shouldFork) {
1766
- if (options.allowUnarchiveArchived === true && normalizedProvider === 'codex' && !requestedAsMain) {
1767
- const unarchiveResult = await unarchiveCodexSession(sessionId, session);
1768
- if (unarchiveResult.error) {
1769
- return unarchiveResult;
1770
- }
1771
- session = await findAgentSession(normalizedProvider, sessionId, {
1772
- limit: 1000,
1773
- providerLimit: 1000,
1774
- scanLimit: 5000,
1775
- providerHomeId,
1776
- providerHomes: configuredProviderHomes(),
1777
- }) || {
1778
- ...session,
1779
- archived: false,
1780
- };
1781
- } else {
1782
- forgetMainPageAgentSession(normalizedProvider, sessionId, providerHomeId);
1783
- return {
1784
- error: `${session.providerName || normalizedProvider} session is archived. Unarchive it before resuming.`,
1785
- status: 409,
1786
- archived: true,
1787
- };
1788
- }
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
+ };
1789
1914
  }
1790
1915
  if (!shouldFork && !requestedAsMain) {
1791
1916
  const claimingAgent = findActiveAgentClaimingSession(agentManager.getState().agents, normalizedProvider, {
@@ -2109,6 +2234,7 @@ async function autoResumeMainPageAgentSessions() {
2109
2234
 
2110
2235
  app.post(routePath(BASE_PATH, '/api/settings'), express.json(), (req, res) => {
2111
2236
  const settingsPatch = { ...(req.body || {}) };
2237
+ delete settingsPatch.mainPageSessionKeys;
2112
2238
  delete settingsPatch.projectWorkspaces;
2113
2239
  delete settingsPatch.pinnedProjectWorkspaces;
2114
2240
  configManager.updateSettings(settingsPatch);
@@ -2246,7 +2372,11 @@ function restartMainAgent(ws, command) {
2246
2372
  agent.id === state.mainAgentId || agent.isMain === true
2247
2373
  ));
2248
2374
  if (currentMain) {
2249
- 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
+ }
2250
2380
  }
2251
2381
 
2252
2382
  await agentManager.startAgent(normalizedCommand, null, (agentId, error) => {
@@ -2268,6 +2398,39 @@ function restartMainAgent(ws, command) {
2268
2398
  })();
2269
2399
  }
2270
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
+
2271
2434
  async function sendInputMessage(ws, data) {
2272
2435
  const targetAgentId = resolveInputTargetAgentId(ws, data);
2273
2436
  if (!targetAgentId) return;
@@ -2404,6 +2567,7 @@ function handleMessage(ws, data) {
2404
2567
  task: typeof data.task === 'string' ? data.task : '',
2405
2568
  workflowTemplate: typeof data.workflowTemplate === 'string' ? data.workflowTemplate : '',
2406
2569
  customTitle: typeof data.customTitle === 'string' ? data.customTitle : '',
2570
+ createRequestId: typeof data.requestId === 'string' ? data.requestId : '',
2407
2571
  codexApprovalMode: typeof data.codexApprovalMode === 'string' ? data.codexApprovalMode : undefined,
2408
2572
  agentRuntimeMode: ['json', 'acp', 'chat'].includes(data.agentRuntimeMode) ? data.agentRuntimeMode : 'terminal',
2409
2573
  acpHistoryMode: data.acpHistoryMode === 'resume' ? 'resume' : 'load',
@@ -2486,8 +2650,9 @@ function handleMessage(ws, data) {
2486
2650
  break;
2487
2651
 
2488
2652
  case 'kill-agent':
2489
- agentManager.killAgent(data.agentId);
2490
- broadcastState();
2653
+ void killAgentFromMessage(ws, data.agentId, {
2654
+ acknowledgeUnprovenAcpExit: data.acknowledgeUnprovenAcpExit === true,
2655
+ });
2491
2656
  break;
2492
2657
 
2493
2658
  case 'restart-main-agent':
@@ -2502,10 +2667,26 @@ function handleMessage(ws, data) {
2502
2667
  const { resolveInputTargetAgentId } = require('./input-routing');
2503
2668
 
2504
2669
  function sendWorkspaceFileWatchError(ws, error) {
2670
+ if (ws.readyState !== WebSocket.OPEN) return;
2505
2671
  const message = error instanceof WorkspaceFileError ? error.message : 'failed to watch workspace files';
2506
2672
  ws.send(JSON.stringify({ type: 'error', message }));
2507
2673
  }
2508
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
+
2509
2690
  function clearWorkspaceFileWatch(ws, agentId = null) {
2510
2691
  const watches = ws.workspaceFileUnsubscribes;
2511
2692
  if (!watches) return;
@@ -2514,12 +2695,10 @@ function clearWorkspaceFileWatch(ws, agentId = null) {
2514
2695
  ? [[agentId, watches.get(agentId)]]
2515
2696
  : Array.from(watches.entries());
2516
2697
 
2517
- entries.forEach(([watchedAgentId, unsubscribe]) => {
2518
- if (!unsubscribe) return;
2519
- watches.delete(watchedAgentId);
2520
- Promise.resolve(unsubscribe()).catch((error) => {
2521
- console.error('Failed to clear workspace file watch:', error);
2522
- });
2698
+ entries.forEach(([watchedAgentId, record]) => {
2699
+ if (!record) return;
2700
+ if (watches.get(watchedAgentId) === record) watches.delete(watchedAgentId);
2701
+ closeWorkspaceFileWatchRecord(record);
2523
2702
  });
2524
2703
 
2525
2704
  if (watches.size === 0) {
@@ -2535,33 +2714,62 @@ async function watchWorkspaceFiles(ws, data) {
2535
2714
  if (!ws.workspaceFileUnsubscribes) {
2536
2715
  ws.workspaceFileUnsubscribes = new Map();
2537
2716
  }
2538
- if (ws.workspaceFileUnsubscribes.has(data.agentId)) {
2539
- ws.send(JSON.stringify({
2540
- type: 'workspace-file-watch',
2541
- agentId: data.agentId,
2542
- watching: true,
2543
- }));
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
+ }
2544
2728
  return;
2545
2729
  }
2546
2730
 
2547
2731
  const root = resolveWorkspaceRoot(agentManager, data.agentId);
2548
-
2549
- const unsubscribe = await workspaceFileService.subscribe(root, (event) => {
2550
- if (ws.readyState !== WebSocket.OPEN) return;
2551
- ws.send(JSON.stringify({
2552
- type: 'workspace-file-event',
2553
- 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',
2554
2761
  agentId: data.agentId,
2555
- ...event,
2556
- },
2557
- }));
2558
- });
2559
- ws.workspaceFileUnsubscribes.set(data.agentId, unsubscribe);
2560
- ws.send(JSON.stringify({
2561
- type: 'workspace-file-watch',
2562
- agentId: data.agentId,
2563
- watching: true,
2564
- }));
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
+ }
2565
2773
  } catch (error) {
2566
2774
  sendWorkspaceFileWatchError(ws, error);
2567
2775
  }
@@ -2904,18 +3112,40 @@ async function shutdownServer(options = {}) {
2904
3112
  ? options.preserveTerminalHost === true
2905
3113
  : process.env.FARMING_NATIVE_PTY_HOST_PERSIST !== '0';
2906
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
+
2907
3137
  clearBroadcastTimers();
2908
3138
  tokenAuth.cleanup();
2909
3139
  await Promise.allSettled([
2910
3140
  closeWebSocketServer(),
2911
3141
  workspaceFileService.dispose(),
2912
- agentManager.dispose({ preserveTerminalHost }),
2913
3142
  closeHttpServer(),
2914
3143
  ]);
2915
3144
 
2916
3145
  if (options.exit === true) {
2917
3146
  process.exit(options.exitCode || 0);
2918
3147
  }
3148
+ return { stopped: true };
2919
3149
  }
2920
3150
 
2921
3151
  agentManager.onSessionStream((stream) => {
@@ -25,6 +25,10 @@ function acpCheckpointsDir(configDir) {
25
25
  return path.join(sessionsDir(configDir), 'acp-checkpoints');
26
26
  }
27
27
 
28
+ function agentStateFile(configDir, agentRecordId) {
29
+ return path.join(sessionsDir(configDir), `${agentRecordId}.state.json`);
30
+ }
31
+
28
32
  function historyDir(configDir) {
29
33
  return path.join(configDir, 'history');
30
34
  }
@@ -89,6 +93,10 @@ function updateLogFile(configDir) {
89
93
  return path.join(configDir, 'farming-update.log');
90
94
  }
91
95
 
96
+ function updateStagingDir(configDir) {
97
+ return path.join(configDir, 'updates');
98
+ }
99
+
92
100
  function farmingNetInstancesFile(configDir) {
93
101
  return path.join(configDir, 'instances.json');
94
102
  }
@@ -115,6 +123,7 @@ function farmingNetTrustFile(configDir) {
115
123
 
116
124
  module.exports = {
117
125
  acpCheckpointsDir,
126
+ agentStateFile,
118
127
  farmingNetInstancesFile,
119
128
  farmingNetServerLogFile,
120
129
  farmingNetServerStateFile,
@@ -140,6 +149,7 @@ module.exports = {
140
149
  settingsFile,
141
150
  themeSettingsFile,
142
151
  updateLogFile,
152
+ updateStagingDir,
143
153
  updateStateFile,
144
154
  usageHistoryCacheFile,
145
155
  };