farming-code 2.2.28 → 2.2.30
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 +27 -90
- package/README.zh_cn.md +17 -90
- package/THIRD_PARTY_NOTICES.md +25 -6
- package/backend/acp/packaged-claude-acp.js +13 -0
- package/backend/acp/packaged-codex-acp.js +2 -2
- package/backend/acp-runtime.js +38 -17
- package/backend/acp-session-state.js +78 -88
- package/backend/acp-transcript.js +174 -9
- package/backend/agent-manager.js +27 -2
- package/backend/agent-session-history.js +45 -12
- package/backend/codex-session-history.js +1 -1
- package/backend/codex-transcript.js +0 -202
- package/backend/config-manager.js +44 -16
- package/backend/data/LICENSE.agent-browser +201 -0
- package/backend/data/runtime-dependency-manifest.json +199 -0
- package/backend/data/runtime-dependency-sources.json +5 -0
- package/backend/executable-discovery.js +7 -0
- package/backend/farming-agent-bootstrap.zh_cn.md +4 -2
- package/backend/farming-app-cli.js +161 -65
- package/backend/farming-cli.js +7 -59
- package/backend/index-html.js +31 -0
- package/backend/main-agent-skills.js +7 -28
- package/backend/npm-update-helper.js +43 -6
- package/backend/provider-adapters.js +10 -0
- package/backend/runtime-dependency-manager.js +759 -0
- package/backend/runtime-executable-invocation.js +20 -0
- package/backend/server-process-identity.js +47 -0
- package/backend/server.js +145 -204
- package/backend/slash-command-discovery.js +138 -1
- package/backend/storage-layout.js +30 -0
- package/backend/update-service.js +60 -914
- package/config/farming.deploy.env.example +1 -1
- package/config/farming.install.env.example +5 -1
- package/dist/acp/LICENSE.claude-agent-acp +191 -0
- package/dist/acp/LICENSE.claude-agent-sdk +1 -0
- package/dist/acp/claude-agent-acp-0.59.0.mjs +41606 -0
- package/dist/acp/{codex-acp-1.1.4.js → codex-acp-1.1.4.mjs} +387 -7
- package/dist/assets/{App-CM7Cy-l4.css → App-BKNUILuL.css} +1 -1
- package/dist/assets/App-Z1N7T0eM.js +695 -0
- package/dist/assets/{FileEditorMarkdownPreview-DYOMm-b1.js → FileEditorMarkdownPreview-Cs9cLv8a.js} +1 -1
- package/dist/assets/FileEditorPane-CNtuGjQ3.js +2 -0
- package/dist/assets/IconGlyphs-BClgag6E.js +1 -0
- package/dist/assets/ProjectFilesSection-K3GHuQ4U.js +12 -0
- package/dist/assets/ReviewPage-BRuxFaH5.js +3 -0
- package/dist/assets/code-dark-kLsQLvpf.css +1 -0
- package/dist/assets/{index-BzQO3pUr.js → index-D-zetSol.js} +2 -2
- package/dist/assets/main-DUTz-Rd2.css +1 -0
- package/dist/assets/{workspace-editor-monaco-tXA-udGx.js → workspace-editor-monaco-CJNwbpCy.js} +1 -1
- package/dist/assets/workspace-editor-monaco-CO9w8YV0.js +1 -0
- package/dist/assets/workspace-view-state-BjKPYp0E.js +7 -0
- package/dist/index.html +33 -3
- package/extensions/browser/backend/agent-browser-runtime.js +1041 -0
- package/extensions/browser/backend/browser-mcp-server.js +352 -0
- package/extensions/browser/backend/browser-resource-manager.js +921 -114
- package/extensions/browser/backend/browser-resource-store.js +47 -6
- package/extensions/browser/backend/browser-router.js +15 -2
- package/extensions/browser/backend/executable-discovery.js +164 -17
- package/extensions/browser/backend/managed-chromium-installer.js +593 -0
- package/extensions/browser/bin/farming-browser +384 -30
- package/extensions/browser/frontend/BrowserSidebarPortals.tsx +17 -19
- package/extensions/browser/frontend/BrowserViewer.tsx +217 -46
- package/extensions/browser/frontend/browser.css +68 -17
- package/extensions/browser/frontend/types.ts +13 -0
- package/extensions/browser/frontend/useBrowserResources.ts +18 -3
- package/index.html +32 -2
- package/package.json +10 -6
- package/shared/backend-connection-status.d.ts +12 -0
- package/shared/backend-connection-status.js +35 -4
- package/backend/agent-activity.js +0 -35
- package/backend/agent-memory-report.js +0 -583
- package/backend/bundle-update-helper.js +0 -113
- package/backend/codex-rollout-follower.js +0 -326
- package/dist/assets/App-CPG4WcjO.js +0 -658
- package/dist/assets/FileEditorPane-DXXq82aM.js +0 -2
- package/dist/assets/IconGlyphs-DrdPjYXr.js +0 -1
- package/dist/assets/ProjectFilesSection-CP66XVPl.js +0 -12
- package/dist/assets/ReviewPage-w21MEPD6.js +0 -3
- package/dist/assets/code-dark-BDjwj7Um.css +0 -1
- package/dist/assets/main-BlVUXfxa.css +0 -1
- package/dist/assets/workspace-editor-monaco-CFEfFNcA.js +0 -1
- package/dist/assets/workspace-view-state-BS_V8JVs.js +0 -7
- package/extensions/browser/backend/cdp-browser-runtime.js +0 -648
- package/extensions/browser/backend/cdp-client.js +0 -167
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
function runtimeExecutableInvocation(
|
|
2
|
+
executablePath,
|
|
3
|
+
args = [],
|
|
4
|
+
env = process.env,
|
|
5
|
+
platform = process.platform,
|
|
6
|
+
) {
|
|
7
|
+
if (
|
|
8
|
+
platform === 'linux'
|
|
9
|
+
&& env.FARMING_NODE_LD
|
|
10
|
+
&& env.FARMING_NODE_LIBRARY_PATH
|
|
11
|
+
) {
|
|
12
|
+
return {
|
|
13
|
+
command: env.FARMING_NODE_LD,
|
|
14
|
+
args: ['--library-path', env.FARMING_NODE_LIBRARY_PATH, executablePath, ...args],
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
return { command: executablePath, args };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
module.exports = { runtimeExecutableInvocation };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const { execFileSync } = require('child_process');
|
|
2
|
+
|
|
3
|
+
const SERVER_PROCESS_IDENTITY_FORMAT = 'ps-lstart-c-utc-v1';
|
|
4
|
+
|
|
5
|
+
function readServerProcessIdentity(pid) {
|
|
6
|
+
const processId = Number(pid);
|
|
7
|
+
if (!Number.isSafeInteger(processId) || processId <= 0 || process.platform === 'win32') return null;
|
|
8
|
+
let stdout;
|
|
9
|
+
try {
|
|
10
|
+
stdout = execFileSync(
|
|
11
|
+
'/bin/ps',
|
|
12
|
+
['-p', String(processId), '-o', 'pid=', '-o', 'pgid=', '-o', 'lstart='],
|
|
13
|
+
{
|
|
14
|
+
encoding: 'utf8',
|
|
15
|
+
timeout: 1_000,
|
|
16
|
+
maxBuffer: 16_384,
|
|
17
|
+
env: { ...process.env, LANG: 'C', LC_ALL: 'C', TZ: 'UTC' },
|
|
18
|
+
},
|
|
19
|
+
);
|
|
20
|
+
} catch (error) {
|
|
21
|
+
if (error?.status === 1 || error?.code === 'ESRCH') return null;
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
const match = String(stdout || '').trim().match(/^(\d+)\s+(\d+)\s+(.+)$/);
|
|
25
|
+
if (!match || Number(match[1]) !== processId) return null;
|
|
26
|
+
return {
|
|
27
|
+
pid: processId,
|
|
28
|
+
processGroupId: Number(match[2]),
|
|
29
|
+
startedAt: match[3].trim(),
|
|
30
|
+
format: SERVER_PROCESS_IDENTITY_FORMAT,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function matchingProcessIdentity(expected, current) {
|
|
35
|
+
return Boolean(
|
|
36
|
+
current
|
|
37
|
+
&& current.pid === Number(expected?.pid)
|
|
38
|
+
&& current.processGroupId === Number(expected?.processGroupId)
|
|
39
|
+
&& current.startedAt === String(expected?.startedAt || '')
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = {
|
|
44
|
+
SERVER_PROCESS_IDENTITY_FORMAT,
|
|
45
|
+
matchingProcessIdentity,
|
|
46
|
+
readServerProcessIdentity,
|
|
47
|
+
};
|
package/backend/server.js
CHANGED
|
@@ -48,12 +48,10 @@ const {
|
|
|
48
48
|
} = require('../extensions/browser/backend');
|
|
49
49
|
const { UsageMonitor } = require('./usage-monitor');
|
|
50
50
|
const { CodexContextWindowReader } = require('./codex-context-window');
|
|
51
|
-
const { DEFAULT_MAX_TURNS: DEFAULT_CODEX_TRANSCRIPT_MAX_TURNS, readCodexTranscript } = require('./codex-transcript');
|
|
52
51
|
const { AsyncCache } = require('./async-cache');
|
|
53
52
|
const { getMainAgentSkillsCatalog } = require('./main-agent-skills');
|
|
54
|
-
const { discoverSlashCommands } = require('./slash-command-discovery');
|
|
53
|
+
const { discoverAgentExtensions, discoverSlashCommands } = require('./slash-command-discovery');
|
|
55
54
|
const { FarmingUpdateService } = require('./update-service');
|
|
56
|
-
const { isRestartBlockingAgent } = require('./agent-activity');
|
|
57
55
|
const { inputPartsFromMessage } = require('./input-parts');
|
|
58
56
|
const { cleanupTerminalRuntime } = require('./terminal-runtime-cleanup');
|
|
59
57
|
const { QrShareTicketStore, SHARE_TICKET_TTL_MS } = require('./qr-share-tickets');
|
|
@@ -65,11 +63,13 @@ const { ReviewSessionStore } = require('./review-session-store');
|
|
|
65
63
|
const { ReviewSessionService } = require('./review-session-service');
|
|
66
64
|
const { createReviewSessionRouter } = require('./review-session-router');
|
|
67
65
|
const {
|
|
66
|
+
applyIndexHtmlAppearance,
|
|
68
67
|
normalizeBasePath,
|
|
69
68
|
routePath,
|
|
70
69
|
rewriteIndexHtmlForBasePath,
|
|
71
70
|
appendIndexHtmlAssetToken,
|
|
72
71
|
} = require('./index-html');
|
|
72
|
+
const { decodeAcpTranscriptMedia } = require('./acp-transcript');
|
|
73
73
|
const {
|
|
74
74
|
coalesceSessionStream,
|
|
75
75
|
deliverSessionStreamToClients,
|
|
@@ -89,7 +89,8 @@ const tokenAuth = new TokenAuth({ basePath: BASE_PATH || '/' });
|
|
|
89
89
|
const authEnabled = tokenAuth.isEnabled();
|
|
90
90
|
const WS_PATH = routePath(BASE_PATH, '/ws');
|
|
91
91
|
const encodeCookieToken = TokenAuth.encodeCookieToken;
|
|
92
|
-
const
|
|
92
|
+
const DEFAULT_TRANSCRIPT_MAX_TURNS = 240;
|
|
93
|
+
const MAX_TRANSCRIPT_TURNS = 1000;
|
|
93
94
|
const INTERACTIVE_REFRESH_CACHE_MAX_AGE_MS = 3_000;
|
|
94
95
|
const execFileAsync = promisify(execFile);
|
|
95
96
|
|
|
@@ -136,15 +137,18 @@ const workspaceRootRegistry = new WorkspaceRootRegistry(agentManager);
|
|
|
136
137
|
const browserResourceManager = new BrowserResourceManager({
|
|
137
138
|
configDir: configManager.farmingDir,
|
|
138
139
|
isEnabled: () => configManager.getSettings().browserExtensionEnabled === true,
|
|
140
|
+
getBrowserSettings: () => configManager.getSettings(),
|
|
141
|
+
});
|
|
142
|
+
const browserRuntimeRecoveryPromise = browserResourceManager.init().catch(error => {
|
|
143
|
+
console.warn('Failed to recover Browser runtimes:', error?.message || error);
|
|
144
|
+
return null;
|
|
139
145
|
});
|
|
140
|
-
browserResourceManager.init();
|
|
141
146
|
const updateService = new FarmingUpdateService({
|
|
142
147
|
rootDir: path.join(__dirname, '..'),
|
|
143
148
|
configDir: configManager.farmingDir,
|
|
144
149
|
platform: process.platform,
|
|
145
150
|
arch: process.arch,
|
|
146
151
|
packagedRuntime: Boolean(process.pkg || process.env.FARMING_PACKAGED_RUNTIME === '1'),
|
|
147
|
-
getUpdateUrl: () => configManager.getSettings().updateUrl || '',
|
|
148
152
|
});
|
|
149
153
|
const usageMonitor = new UsageMonitor({
|
|
150
154
|
agentManager,
|
|
@@ -255,6 +259,22 @@ function getAvailableAgentsForRequest() {
|
|
|
255
259
|
supported: true,
|
|
256
260
|
interactive: true,
|
|
257
261
|
},
|
|
262
|
+
{
|
|
263
|
+
name: 'opencode',
|
|
264
|
+
command: 'opencode',
|
|
265
|
+
description: 'OpenCode - AI coding assistant',
|
|
266
|
+
category: 'coding',
|
|
267
|
+
supported: true,
|
|
268
|
+
interactive: true,
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
name: 'qoder',
|
|
272
|
+
command: 'qodercli',
|
|
273
|
+
description: 'Qoder - AI coding assistant',
|
|
274
|
+
category: 'coding',
|
|
275
|
+
supported: true,
|
|
276
|
+
interactive: true,
|
|
277
|
+
},
|
|
258
278
|
{
|
|
259
279
|
name: 'bash',
|
|
260
280
|
command: 'bash',
|
|
@@ -574,7 +594,10 @@ app.get([
|
|
|
574
594
|
}
|
|
575
595
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
576
596
|
res.setHeader('Cache-Control', 'no-cache');
|
|
577
|
-
const rewrittenHtml =
|
|
597
|
+
const rewrittenHtml = applyIndexHtmlAppearance(
|
|
598
|
+
rewriteIndexHtmlForBasePath(html, BASE_PATH),
|
|
599
|
+
configManager.getSettings().appearance
|
|
600
|
+
);
|
|
578
601
|
const requestUrl = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
|
|
579
602
|
const requestToken = requestUrl.searchParams.has('token') ? tokenAuth.extractToken(req) : '';
|
|
580
603
|
const htmlWithAssetToken = authEnabled && requestToken && tokenAuth.verify(requestToken)
|
|
@@ -613,6 +636,40 @@ app.get(routePath(BASE_PATH, '/api/skills'), (_req, res) => {
|
|
|
613
636
|
res.json({ skills: getMainAgentSkillsCatalog() });
|
|
614
637
|
});
|
|
615
638
|
|
|
639
|
+
app.get(routePath(BASE_PATH, '/api/agent-extensions'), (_req, res) => {
|
|
640
|
+
const agents = getAvailableAgentsForRequest()
|
|
641
|
+
.filter(agent => agent.category === 'coding')
|
|
642
|
+
.map(agent => {
|
|
643
|
+
const provider = String(agent.name || agent.command || '').trim().toLowerCase();
|
|
644
|
+
const configuredHomes = configManager.getAgentHomes(provider);
|
|
645
|
+
const homes = configuredHomes.length > 0
|
|
646
|
+
? configuredHomes
|
|
647
|
+
: [{ id: 'default', path: '' }];
|
|
648
|
+
return {
|
|
649
|
+
id: provider,
|
|
650
|
+
name: agent.name,
|
|
651
|
+
description: agent.description || '',
|
|
652
|
+
discoverySupported: provider === 'codex' || provider === 'claude',
|
|
653
|
+
homes: homes.map(home => ({
|
|
654
|
+
id: home.id,
|
|
655
|
+
extensions: discoverAgentExtensions({
|
|
656
|
+
provider,
|
|
657
|
+
providerHomePath: home.path,
|
|
658
|
+
}).map(extension => ({
|
|
659
|
+
id: extension.command,
|
|
660
|
+
command: extension.command,
|
|
661
|
+
name: extension.label,
|
|
662
|
+
description: extension.description,
|
|
663
|
+
kind: extension.source === 'custom' ? 'command' : extension.source || 'command',
|
|
664
|
+
scope: extension.scope || '',
|
|
665
|
+
})),
|
|
666
|
+
})),
|
|
667
|
+
};
|
|
668
|
+
});
|
|
669
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
670
|
+
res.json({ agents });
|
|
671
|
+
});
|
|
672
|
+
|
|
616
673
|
app.get(routePath(BASE_PATH, '/api/slash-commands'), (req, res) => {
|
|
617
674
|
const provider = typeof req.query.provider === 'string' ? req.query.provider : '';
|
|
618
675
|
const workspace = typeof req.query.workspace === 'string' ? req.query.workspace : '';
|
|
@@ -815,26 +872,10 @@ app.post(routePath(BASE_PATH, '/api/codex/context-windows'), express.json(), asy
|
|
|
815
872
|
}
|
|
816
873
|
});
|
|
817
874
|
|
|
818
|
-
function blockingUpdateAgents() {
|
|
819
|
-
return agentManager.getState().agents
|
|
820
|
-
.filter(isRestartBlockingAgent)
|
|
821
|
-
.map(agent => ({
|
|
822
|
-
id: agent.id,
|
|
823
|
-
command: agent.command,
|
|
824
|
-
task: agent.task || agent.customTitle || agent.sessionTitle || '',
|
|
825
|
-
cwd: agent.cwd,
|
|
826
|
-
}));
|
|
827
|
-
}
|
|
828
|
-
|
|
829
875
|
app.get(routePath(BASE_PATH, '/api/update'), async (req, res) => {
|
|
830
876
|
try {
|
|
831
877
|
const update = await updateService.check({ force: req.query.force === '1' });
|
|
832
|
-
res.json({
|
|
833
|
-
update: {
|
|
834
|
-
...update,
|
|
835
|
-
blockingAgents: blockingUpdateAgents(),
|
|
836
|
-
},
|
|
837
|
-
});
|
|
878
|
+
res.json({ update });
|
|
838
879
|
} catch (error) {
|
|
839
880
|
res.status(502).json({ error: error.message || 'Failed to check for updates' });
|
|
840
881
|
}
|
|
@@ -845,31 +886,16 @@ app.post(routePath(BASE_PATH, '/api/update/install'), express.json(), async (req
|
|
|
845
886
|
const state = await updateService.startInstall({
|
|
846
887
|
assetName: req.body && typeof req.body.assetName === 'string' ? req.body.assetName : '',
|
|
847
888
|
});
|
|
848
|
-
res.status(202).json({
|
|
849
|
-
update: {
|
|
850
|
-
state,
|
|
851
|
-
blockingAgents: blockingUpdateAgents(),
|
|
852
|
-
},
|
|
853
|
-
});
|
|
889
|
+
res.status(202).json({ update: { state } });
|
|
854
890
|
} catch (error) {
|
|
855
891
|
res.status(500).json({ error: error.message || 'Failed to start update' });
|
|
856
892
|
}
|
|
857
893
|
});
|
|
858
894
|
|
|
859
895
|
app.post(routePath(BASE_PATH, '/api/update/restart'), express.json(), async (req, res) => {
|
|
860
|
-
const blockers = blockingUpdateAgents();
|
|
861
|
-
const force = req.body && req.body.force === true;
|
|
862
|
-
if (blockers.length > 0 && !force) {
|
|
863
|
-
res.status(409).json({
|
|
864
|
-
error: 'Cannot restart for update while non-recoverable project agents are running',
|
|
865
|
-
blockingAgents: blockers,
|
|
866
|
-
});
|
|
867
|
-
return;
|
|
868
|
-
}
|
|
869
|
-
|
|
870
896
|
try {
|
|
871
897
|
const state = await updateService.applyPreparedUpdate();
|
|
872
|
-
res.status(202).json({ update: { state
|
|
898
|
+
res.status(202).json({ update: { state } });
|
|
873
899
|
} catch (error) {
|
|
874
900
|
res.status(500).json({ error: error.message || 'Failed to restart for update' });
|
|
875
901
|
}
|
|
@@ -1078,51 +1104,10 @@ app.get(routePath(BASE_PATH, '/api/agents/:agentId/session-view'), async (req, r
|
|
|
1078
1104
|
res.json({ session: sessionView });
|
|
1079
1105
|
});
|
|
1080
1106
|
|
|
1081
|
-
app.get(routePath(BASE_PATH, '/api/agents/:agentId/codex-transcript'), async (req, res) => {
|
|
1082
|
-
// Legacy JSONL transcript reader. ACP Chat uses the dedicated ACP endpoint;
|
|
1083
|
-
// Terminal Agents stay in their terminal UI.
|
|
1084
|
-
const providerSession = agentManager.getAgentProviderSession(req.params.agentId);
|
|
1085
|
-
if (!providerSession) {
|
|
1086
|
-
res.status(404).json({ error: 'Agent not found' });
|
|
1087
|
-
return;
|
|
1088
|
-
}
|
|
1089
|
-
if (
|
|
1090
|
-
providerSession.provider !== 'codex'
|
|
1091
|
-
|| providerSession.temporary
|
|
1092
|
-
|| !providerSession.sessionId
|
|
1093
|
-
|| providerSession.sessionId.startsWith('tmp_uuid')
|
|
1094
|
-
) {
|
|
1095
|
-
res.json({
|
|
1096
|
-
transcript: {
|
|
1097
|
-
available: false,
|
|
1098
|
-
reason: 'not-codex-provider-session',
|
|
1099
|
-
sessionId: providerSession.sessionId || '',
|
|
1100
|
-
turns: [],
|
|
1101
|
-
},
|
|
1102
|
-
});
|
|
1103
|
-
return;
|
|
1104
|
-
}
|
|
1105
|
-
|
|
1106
|
-
try {
|
|
1107
|
-
const requestedMaxTurns = Number.parseInt(String(req.query.maxTurns || ''), 10);
|
|
1108
|
-
const maxTurns = Number.isFinite(requestedMaxTurns)
|
|
1109
|
-
? Math.min(MAX_CODEX_TRANSCRIPT_TURNS, Math.max(20, requestedMaxTurns))
|
|
1110
|
-
: DEFAULT_CODEX_TRANSCRIPT_MAX_TURNS;
|
|
1111
|
-
const transcript = await readCodexTranscript(providerSession.sessionId, {
|
|
1112
|
-
maxTurns,
|
|
1113
|
-
codexHome: providerSession.providerHomePath || '',
|
|
1114
|
-
});
|
|
1115
|
-
res.json({ transcript });
|
|
1116
|
-
} catch (error) {
|
|
1117
|
-
console.error('Failed to read Codex transcript:', error);
|
|
1118
|
-
res.status(500).json({ error: error.message || 'Failed to read Codex transcript' });
|
|
1119
|
-
}
|
|
1120
|
-
});
|
|
1121
|
-
|
|
1122
1107
|
app.get(routePath(BASE_PATH, '/api/agents/:agentId/json-cli-transcript'), async (req, res) => {
|
|
1123
1108
|
try {
|
|
1124
1109
|
const transcript = agentManager.getJsonCliTranscript(req.params.agentId, {
|
|
1125
|
-
maxTurns:
|
|
1110
|
+
maxTurns: DEFAULT_TRANSCRIPT_MAX_TURNS,
|
|
1126
1111
|
});
|
|
1127
1112
|
res.json({ transcript });
|
|
1128
1113
|
} catch (error) {
|
|
@@ -1149,11 +1134,20 @@ app.get(routePath(BASE_PATH, '/api/agents/:agentId/acp-transcript'), async (req,
|
|
|
1149
1134
|
try {
|
|
1150
1135
|
const requestedMaxTurns = Number.parseInt(String(req.query.maxTurns || ''), 10);
|
|
1151
1136
|
const maxTurns = Number.isFinite(requestedMaxTurns)
|
|
1152
|
-
? Math.min(
|
|
1153
|
-
:
|
|
1137
|
+
? Math.min(MAX_TRANSCRIPT_TURNS, Math.max(20, requestedMaxTurns))
|
|
1138
|
+
: DEFAULT_TRANSCRIPT_MAX_TURNS;
|
|
1154
1139
|
const requestedRevision = Number.parseInt(String(req.query.sinceRevision || ''), 10);
|
|
1140
|
+
const externalMedia = req.query.media === 'external-v1';
|
|
1155
1141
|
res.json({ transcript: agentManager.getAcpTranscript(req.params.agentId, {
|
|
1156
1142
|
maxTurns,
|
|
1143
|
+
...(externalMedia
|
|
1144
|
+
? {
|
|
1145
|
+
mediaPathPrefix: routePath(
|
|
1146
|
+
BASE_PATH,
|
|
1147
|
+
`/api/agents/${encodeURIComponent(req.params.agentId)}/acp-media`
|
|
1148
|
+
),
|
|
1149
|
+
}
|
|
1150
|
+
: {}),
|
|
1157
1151
|
...(Number.isFinite(requestedRevision) && requestedRevision >= 0
|
|
1158
1152
|
? { sinceRevision: requestedRevision }
|
|
1159
1153
|
: {}),
|
|
@@ -1164,6 +1158,32 @@ app.get(routePath(BASE_PATH, '/api/agents/:agentId/acp-transcript'), async (req,
|
|
|
1164
1158
|
}
|
|
1165
1159
|
});
|
|
1166
1160
|
|
|
1161
|
+
app.get(routePath(BASE_PATH, '/api/agents/:agentId/acp-media/:entryId/:mediaId'), (req, res) => {
|
|
1162
|
+
try {
|
|
1163
|
+
const media = agentManager.getAcpTranscriptMedia(
|
|
1164
|
+
req.params.agentId,
|
|
1165
|
+
req.params.entryId,
|
|
1166
|
+
req.params.mediaId
|
|
1167
|
+
);
|
|
1168
|
+
const decoded = decodeAcpTranscriptMedia(media);
|
|
1169
|
+
if (!decoded) {
|
|
1170
|
+
res.status(415).json({ error: 'unsupported ACP transcript media' });
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1173
|
+
res.setHeader('Cache-Control', 'private, max-age=31536000, immutable');
|
|
1174
|
+
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
1175
|
+
res.type(decoded.mimeType).send(decoded.content);
|
|
1176
|
+
} catch (error) {
|
|
1177
|
+
const message = error && error.message ? error.message : 'Failed to read ACP transcript media';
|
|
1178
|
+
const status = message === 'Agent not found'
|
|
1179
|
+
|| message === 'ACP transcript entry not found'
|
|
1180
|
+
|| message === 'ACP transcript media not found'
|
|
1181
|
+
? 404
|
|
1182
|
+
: 409;
|
|
1183
|
+
res.status(status).json({ error: message });
|
|
1184
|
+
}
|
|
1185
|
+
});
|
|
1186
|
+
|
|
1167
1187
|
app.get(routePath(BASE_PATH, '/api/agents/:agentId/acp-tool-details/:toolCallId'), async (req, res) => {
|
|
1168
1188
|
try {
|
|
1169
1189
|
res.json(agentManager.getAcpToolDetail(req.params.agentId, req.params.toolCallId));
|
|
@@ -2209,19 +2229,44 @@ app.post(routePath(BASE_PATH, '/api/settings'), express.json(), async (req, res)
|
|
|
2209
2229
|
delete settingsPatch.mainPageSessionKeys;
|
|
2210
2230
|
delete settingsPatch.projectWorkspaces;
|
|
2211
2231
|
delete settingsPatch.pinnedProjectWorkspaces;
|
|
2232
|
+
const browserConfigurationKeys = [
|
|
2233
|
+
'browserSource',
|
|
2234
|
+
'browserExecutablePath',
|
|
2235
|
+
'browserExternalCdpUrl',
|
|
2236
|
+
];
|
|
2212
2237
|
const changesBrowserExtension = Object.prototype.hasOwnProperty.call(settingsPatch, 'browserExtensionEnabled');
|
|
2238
|
+
const changesBrowserConfiguration = browserConfigurationKeys.some(key =>
|
|
2239
|
+
Object.prototype.hasOwnProperty.call(settingsPatch, key)
|
|
2240
|
+
);
|
|
2241
|
+
const currentSettings = configManager.getSettings();
|
|
2213
2242
|
const browserExtensionEnabled = settingsPatch.browserExtensionEnabled === true;
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2243
|
+
const desiredBrowserEnabled = changesBrowserExtension
|
|
2244
|
+
? browserExtensionEnabled
|
|
2245
|
+
: currentSettings.browserExtensionEnabled === true;
|
|
2246
|
+
if ((changesBrowserExtension && browserExtensionEnabled) || changesBrowserConfiguration) {
|
|
2247
|
+
const desiredSelection = browserResourceManager.browserSelection({
|
|
2248
|
+
...currentSettings,
|
|
2249
|
+
...settingsPatch,
|
|
2218
2250
|
});
|
|
2219
|
-
|
|
2251
|
+
const probe = await browserResourceManager.probeCapability(desiredSelection);
|
|
2252
|
+
if (
|
|
2253
|
+
(changesBrowserConfiguration || desiredBrowserEnabled)
|
|
2254
|
+
&& (!probe.runtimeCapability || probe.runtimeCapability.error)
|
|
2255
|
+
) {
|
|
2256
|
+
res.status(400).json({
|
|
2257
|
+
error: probe.runtimeCapability?.error
|
|
2258
|
+
|| 'Install Farming-managed Chromium, choose a system Chromium browser, or configure a loopback external CDP endpoint',
|
|
2259
|
+
code: 'BROWSER_EXECUTABLE_NOT_FOUND',
|
|
2260
|
+
});
|
|
2261
|
+
return;
|
|
2262
|
+
}
|
|
2220
2263
|
}
|
|
2221
2264
|
if (
|
|
2222
|
-
|
|
2223
|
-
&&
|
|
2224
|
-
|
|
2265
|
+
currentSettings.browserExtensionEnabled === true
|
|
2266
|
+
&& (
|
|
2267
|
+
(changesBrowserExtension && !browserExtensionEnabled)
|
|
2268
|
+
|| changesBrowserConfiguration
|
|
2269
|
+
)
|
|
2225
2270
|
) {
|
|
2226
2271
|
try {
|
|
2227
2272
|
await browserResourceManager.stopAll();
|
|
@@ -2234,6 +2279,9 @@ app.post(routePath(BASE_PATH, '/api/settings'), express.json(), async (req, res)
|
|
|
2234
2279
|
}
|
|
2235
2280
|
}
|
|
2236
2281
|
configManager.updateSettings(settingsPatch);
|
|
2282
|
+
if (changesBrowserExtension || changesBrowserConfiguration) {
|
|
2283
|
+
await browserResourceManager.refreshCapability();
|
|
2284
|
+
}
|
|
2237
2285
|
agentSessionsCache.invalidate();
|
|
2238
2286
|
res.json({
|
|
2239
2287
|
success: true,
|
|
@@ -2857,7 +2905,6 @@ const pendingAgentUpdates = new Map();
|
|
|
2857
2905
|
const pendingSessionStreams = new Map();
|
|
2858
2906
|
let sessionStreamBroadcastTimer = null;
|
|
2859
2907
|
let lastSessionStreamBroadcastAt = 0;
|
|
2860
|
-
let shutdownStarted = false;
|
|
2861
2908
|
|
|
2862
2909
|
function broadcastState() {
|
|
2863
2910
|
lastStateBroadcastAt = Date.now();
|
|
@@ -3072,106 +3119,6 @@ function scheduleSessionStreamBroadcast(stream) {
|
|
|
3072
3119
|
}
|
|
3073
3120
|
}
|
|
3074
3121
|
|
|
3075
|
-
function clearBroadcastTimers() {
|
|
3076
|
-
if (stateBroadcastTimer) {
|
|
3077
|
-
clearTimeout(stateBroadcastTimer);
|
|
3078
|
-
stateBroadcastTimer = null;
|
|
3079
|
-
}
|
|
3080
|
-
for (const entry of pendingPreviewBroadcasts.values()) {
|
|
3081
|
-
if (entry && entry.timer) {
|
|
3082
|
-
clearTimeout(entry.timer);
|
|
3083
|
-
}
|
|
3084
|
-
}
|
|
3085
|
-
pendingPreviewBroadcasts.clear();
|
|
3086
|
-
for (const entry of pendingAgentActivityBroadcasts.values()) {
|
|
3087
|
-
if (entry && entry.timer) clearTimeout(entry.timer);
|
|
3088
|
-
}
|
|
3089
|
-
pendingAgentActivityBroadcasts.clear();
|
|
3090
|
-
for (const entry of pendingAgentUpdates.values()) {
|
|
3091
|
-
if (entry && entry.timer) clearTimeout(entry.timer);
|
|
3092
|
-
}
|
|
3093
|
-
pendingAgentUpdates.clear();
|
|
3094
|
-
if (sessionStreamBroadcastTimer) {
|
|
3095
|
-
clearTimeout(sessionStreamBroadcastTimer);
|
|
3096
|
-
sessionStreamBroadcastTimer = null;
|
|
3097
|
-
}
|
|
3098
|
-
pendingSessionStreams.clear();
|
|
3099
|
-
}
|
|
3100
|
-
|
|
3101
|
-
function closeHttpServer() {
|
|
3102
|
-
return new Promise(resolve => {
|
|
3103
|
-
if (!server.listening) {
|
|
3104
|
-
resolve();
|
|
3105
|
-
return;
|
|
3106
|
-
}
|
|
3107
|
-
server.close(error => {
|
|
3108
|
-
if (error && error.code !== 'ERR_SERVER_NOT_RUNNING') {
|
|
3109
|
-
console.warn('Failed to close HTTP server:', error.message || error);
|
|
3110
|
-
}
|
|
3111
|
-
resolve();
|
|
3112
|
-
});
|
|
3113
|
-
});
|
|
3114
|
-
}
|
|
3115
|
-
|
|
3116
|
-
function closeWebSocketServer() {
|
|
3117
|
-
for (const client of wss.clients) {
|
|
3118
|
-
clearWorkspaceFileWatch(client);
|
|
3119
|
-
try {
|
|
3120
|
-
client.close(1001, 'Farming server shutting down');
|
|
3121
|
-
} catch {
|
|
3122
|
-
// ignore shutdown races
|
|
3123
|
-
}
|
|
3124
|
-
}
|
|
3125
|
-
return new Promise(resolve => {
|
|
3126
|
-
wss.close(() => resolve());
|
|
3127
|
-
setTimeout(resolve, 250).unref?.();
|
|
3128
|
-
});
|
|
3129
|
-
}
|
|
3130
|
-
|
|
3131
|
-
async function shutdownServer(options = {}) {
|
|
3132
|
-
if (shutdownStarted) return;
|
|
3133
|
-
shutdownStarted = true;
|
|
3134
|
-
const preserveTerminalHost = options.preserveTerminalHost !== undefined
|
|
3135
|
-
? options.preserveTerminalHost === true
|
|
3136
|
-
: process.env.FARMING_NATIVE_PTY_HOST_PERSIST !== '0';
|
|
3137
|
-
|
|
3138
|
-
try {
|
|
3139
|
-
await browserResourceManager.dispose();
|
|
3140
|
-
await agentManager.dispose({ preserveTerminalHost });
|
|
3141
|
-
} catch (error) {
|
|
3142
|
-
const teardownFrozen = agentManager.disposing === true;
|
|
3143
|
-
shutdownStarted = false;
|
|
3144
|
-
console.error('Farming shutdown blocked because runtime cleanup could not be verified:', error);
|
|
3145
|
-
if (teardownFrozen) {
|
|
3146
|
-
clearBroadcastTimers();
|
|
3147
|
-
tokenAuth.cleanup();
|
|
3148
|
-
await Promise.allSettled([
|
|
3149
|
-
closeWebSocketServer(),
|
|
3150
|
-
workspaceFileService.dispose(),
|
|
3151
|
-
closeHttpServer(),
|
|
3152
|
-
]);
|
|
3153
|
-
}
|
|
3154
|
-
if (options.exit === true) process.exitCode = 1;
|
|
3155
|
-
return {
|
|
3156
|
-
error: error?.message || 'Runtime cleanup could not be verified',
|
|
3157
|
-
...(teardownFrozen ? { frozen: true } : {}),
|
|
3158
|
-
};
|
|
3159
|
-
}
|
|
3160
|
-
|
|
3161
|
-
clearBroadcastTimers();
|
|
3162
|
-
tokenAuth.cleanup();
|
|
3163
|
-
await Promise.allSettled([
|
|
3164
|
-
closeWebSocketServer(),
|
|
3165
|
-
workspaceFileService.dispose(),
|
|
3166
|
-
closeHttpServer(),
|
|
3167
|
-
]);
|
|
3168
|
-
|
|
3169
|
-
if (options.exit === true) {
|
|
3170
|
-
process.exit(options.exitCode || 0);
|
|
3171
|
-
}
|
|
3172
|
-
return { stopped: true };
|
|
3173
|
-
}
|
|
3174
|
-
|
|
3175
3122
|
agentManager.onSessionStream((stream) => {
|
|
3176
3123
|
scheduleSessionStreamBroadcast(stream);
|
|
3177
3124
|
});
|
|
@@ -3221,8 +3168,10 @@ function startServer() {
|
|
|
3221
3168
|
if (serverStarted) return server;
|
|
3222
3169
|
serverStarted = true;
|
|
3223
3170
|
|
|
3224
|
-
void
|
|
3225
|
-
|
|
3171
|
+
void Promise.all([
|
|
3172
|
+
runTerminalRuntimeStartupCleanup(),
|
|
3173
|
+
browserRuntimeRecoveryPromise,
|
|
3174
|
+
]).finally(() => {
|
|
3226
3175
|
server.listen(PORT, () => {
|
|
3227
3176
|
const token = tokenAuth.getToken();
|
|
3228
3177
|
const localIPs = getLocalIPs();
|
|
@@ -3252,13 +3201,6 @@ function startServer() {
|
|
|
3252
3201
|
});
|
|
3253
3202
|
});
|
|
3254
3203
|
|
|
3255
|
-
process.on('SIGINT', () => {
|
|
3256
|
-
void shutdownServer({ exit: true });
|
|
3257
|
-
});
|
|
3258
|
-
process.on('SIGTERM', () => {
|
|
3259
|
-
void shutdownServer({ exit: true });
|
|
3260
|
-
});
|
|
3261
|
-
|
|
3262
3204
|
return server;
|
|
3263
3205
|
}
|
|
3264
3206
|
|
|
@@ -3279,6 +3221,5 @@ module.exports = {
|
|
|
3279
3221
|
rewriteIndexHtmlForBasePath,
|
|
3280
3222
|
appendIndexHtmlAssetToken,
|
|
3281
3223
|
startServer,
|
|
3282
|
-
shutdownServer,
|
|
3283
3224
|
runTerminalRuntimeStartupCleanup,
|
|
3284
3225
|
};
|