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.
- 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 +337 -103
- package/backend/storage-layout.js +10 -0
- package/backend/terminal-screen-worker-pool.js +15 -6
- package/backend/theme-manager.js +23 -5
- package/backend/update-service.js +239 -27
- package/backend/usage-monitor.js +11 -0
- 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-DQdtPfCk.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 +365 -107
- package/frontend/skins/crt/index.html +5 -10
- package/frontend/skins/crt/styles/billing.css +6 -1
- package/package.json +9 -3
- package/dist/assets/App-CbXYBccJ.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-qIBiLBWV.css +0 -1
- package/dist/assets/file-icons-DpbrD5ZP.js +0 -1
- package/dist/assets/main-yzZEfgft.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);
|
|
@@ -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(
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
1356
|
-
|
|
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'
|
|
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'
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
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
|
-
|
|
2486
|
-
|
|
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,
|
|
2514
|
-
if (!
|
|
2515
|
-
watches.delete(watchedAgentId);
|
|
2516
|
-
|
|
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
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
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
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
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
|
-
|
|
2552
|
-
}
|
|
2553
|
-
}
|
|
2554
|
-
})
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
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) => {
|