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.
- package/README.md +5 -5
- package/README.zh_cn.md +5 -5
- package/THIRD_PARTY_NOTICES.md +5 -8
- package/backend/acp/client-services.js +37 -5
- package/backend/acp/patch-decisions.js +19 -3
- package/backend/acp-runtime.js +1201 -198
- package/backend/acp-transcript.js +66 -0
- package/backend/agent-lifecycle-journal.js +133 -0
- package/backend/agent-manager.js +2715 -354
- package/backend/agent-order.js +8 -4
- package/backend/agent-runtime-binding.js +1 -0
- package/backend/bundle-update-helper.js +5 -1
- package/backend/config-manager.js +16 -0
- package/backend/control-api.js +146 -20
- package/backend/farming-app-cli.js +335 -10
- package/backend/farming-session-store.js +283 -36
- package/backend/json-cli-runtime.js +120 -6
- package/backend/npm-update-helper.js +97 -18
- package/backend/provider-adapters.js +18 -6
- package/backend/review-session-service.js +31 -17
- package/backend/server.js +332 -102
- package/backend/storage-layout.js +10 -0
- package/backend/theme-manager.js +23 -5
- package/backend/update-service.js +239 -27
- package/backend/workspace-file-router.js +9 -3
- package/backend/workspace-file-service.js +292 -62
- package/dist/acp/codex-acp-1.1.4.js +32 -1
- package/dist/assets/App-CmZdNavW.js +210 -0
- package/dist/assets/{FileEditorMarkdownPreview-R0jwwLME.js → FileEditorMarkdownPreview-KbnE8VC8.js} +1 -1
- package/dist/assets/FileEditorPane-5_HUejjd.js +2 -0
- package/dist/assets/IconGlyphs-C2WFcoJS.js +1 -0
- package/dist/assets/ProjectFilesSection-C1RAJWOE.js +12 -0
- package/dist/assets/{ReviewPage-DBKNOWsJ.js → ReviewPage-L1vh1Ggs.js} +3 -3
- package/dist/assets/code-dark-BKtvf0Xc.css +1 -0
- package/dist/assets/file-icons-CHWt5nSy.js +1 -0
- package/dist/assets/{index-DuMjesL2.js → index-RvUiMpGP.js} +2 -2
- package/dist/assets/main-XMXgyJJt.css +1 -0
- package/dist/assets/workspace-view-state-BqB4HM7r.js +7 -0
- package/dist/index.html +1 -1
- package/frontend/skins/crt/app.js +199 -81
- package/package.json +9 -3
- package/dist/assets/App-BupehIbc.js +0 -210
- package/dist/assets/FileEditorPane-CJs6kfZO.js +0 -2
- package/dist/assets/IconGlyphs-CZzDr5o2.js +0 -1
- package/dist/assets/ProjectFilesSection-DWCV07lA.js +0 -12
- package/dist/assets/code-dark-BAt0Q_Op.css +0 -1
- package/dist/assets/file-icons-DpbrD5ZP.js +0 -1
- package/dist/assets/main-y2aK_56c.css +0 -1
- 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:
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
1360
|
-
|
|
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'
|
|
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'
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
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
|
-
|
|
2490
|
-
|
|
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,
|
|
2518
|
-
if (!
|
|
2519
|
-
watches.delete(watchedAgentId);
|
|
2520
|
-
|
|
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
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
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
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
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
|
-
|
|
2556
|
-
}
|
|
2557
|
-
}
|
|
2558
|
-
})
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
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
|
};
|