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.
Files changed (83) hide show
  1. package/README.md +27 -90
  2. package/README.zh_cn.md +17 -90
  3. package/THIRD_PARTY_NOTICES.md +25 -6
  4. package/backend/acp/packaged-claude-acp.js +13 -0
  5. package/backend/acp/packaged-codex-acp.js +2 -2
  6. package/backend/acp-runtime.js +38 -17
  7. package/backend/acp-session-state.js +78 -88
  8. package/backend/acp-transcript.js +174 -9
  9. package/backend/agent-manager.js +27 -2
  10. package/backend/agent-session-history.js +45 -12
  11. package/backend/codex-session-history.js +1 -1
  12. package/backend/codex-transcript.js +0 -202
  13. package/backend/config-manager.js +44 -16
  14. package/backend/data/LICENSE.agent-browser +201 -0
  15. package/backend/data/runtime-dependency-manifest.json +199 -0
  16. package/backend/data/runtime-dependency-sources.json +5 -0
  17. package/backend/executable-discovery.js +7 -0
  18. package/backend/farming-agent-bootstrap.zh_cn.md +4 -2
  19. package/backend/farming-app-cli.js +161 -65
  20. package/backend/farming-cli.js +7 -59
  21. package/backend/index-html.js +31 -0
  22. package/backend/main-agent-skills.js +7 -28
  23. package/backend/npm-update-helper.js +43 -6
  24. package/backend/provider-adapters.js +10 -0
  25. package/backend/runtime-dependency-manager.js +759 -0
  26. package/backend/runtime-executable-invocation.js +20 -0
  27. package/backend/server-process-identity.js +47 -0
  28. package/backend/server.js +145 -204
  29. package/backend/slash-command-discovery.js +138 -1
  30. package/backend/storage-layout.js +30 -0
  31. package/backend/update-service.js +60 -914
  32. package/config/farming.deploy.env.example +1 -1
  33. package/config/farming.install.env.example +5 -1
  34. package/dist/acp/LICENSE.claude-agent-acp +191 -0
  35. package/dist/acp/LICENSE.claude-agent-sdk +1 -0
  36. package/dist/acp/claude-agent-acp-0.59.0.mjs +41606 -0
  37. package/dist/acp/{codex-acp-1.1.4.js → codex-acp-1.1.4.mjs} +387 -7
  38. package/dist/assets/{App-CM7Cy-l4.css → App-BKNUILuL.css} +1 -1
  39. package/dist/assets/App-Z1N7T0eM.js +695 -0
  40. package/dist/assets/{FileEditorMarkdownPreview-DYOMm-b1.js → FileEditorMarkdownPreview-Cs9cLv8a.js} +1 -1
  41. package/dist/assets/FileEditorPane-CNtuGjQ3.js +2 -0
  42. package/dist/assets/IconGlyphs-BClgag6E.js +1 -0
  43. package/dist/assets/ProjectFilesSection-K3GHuQ4U.js +12 -0
  44. package/dist/assets/ReviewPage-BRuxFaH5.js +3 -0
  45. package/dist/assets/code-dark-kLsQLvpf.css +1 -0
  46. package/dist/assets/{index-BzQO3pUr.js → index-D-zetSol.js} +2 -2
  47. package/dist/assets/main-DUTz-Rd2.css +1 -0
  48. package/dist/assets/{workspace-editor-monaco-tXA-udGx.js → workspace-editor-monaco-CJNwbpCy.js} +1 -1
  49. package/dist/assets/workspace-editor-monaco-CO9w8YV0.js +1 -0
  50. package/dist/assets/workspace-view-state-BjKPYp0E.js +7 -0
  51. package/dist/index.html +33 -3
  52. package/extensions/browser/backend/agent-browser-runtime.js +1041 -0
  53. package/extensions/browser/backend/browser-mcp-server.js +352 -0
  54. package/extensions/browser/backend/browser-resource-manager.js +921 -114
  55. package/extensions/browser/backend/browser-resource-store.js +47 -6
  56. package/extensions/browser/backend/browser-router.js +15 -2
  57. package/extensions/browser/backend/executable-discovery.js +164 -17
  58. package/extensions/browser/backend/managed-chromium-installer.js +593 -0
  59. package/extensions/browser/bin/farming-browser +384 -30
  60. package/extensions/browser/frontend/BrowserSidebarPortals.tsx +17 -19
  61. package/extensions/browser/frontend/BrowserViewer.tsx +217 -46
  62. package/extensions/browser/frontend/browser.css +68 -17
  63. package/extensions/browser/frontend/types.ts +13 -0
  64. package/extensions/browser/frontend/useBrowserResources.ts +18 -3
  65. package/index.html +32 -2
  66. package/package.json +10 -6
  67. package/shared/backend-connection-status.d.ts +12 -0
  68. package/shared/backend-connection-status.js +35 -4
  69. package/backend/agent-activity.js +0 -35
  70. package/backend/agent-memory-report.js +0 -583
  71. package/backend/bundle-update-helper.js +0 -113
  72. package/backend/codex-rollout-follower.js +0 -326
  73. package/dist/assets/App-CPG4WcjO.js +0 -658
  74. package/dist/assets/FileEditorPane-DXXq82aM.js +0 -2
  75. package/dist/assets/IconGlyphs-DrdPjYXr.js +0 -1
  76. package/dist/assets/ProjectFilesSection-CP66XVPl.js +0 -12
  77. package/dist/assets/ReviewPage-w21MEPD6.js +0 -3
  78. package/dist/assets/code-dark-BDjwj7Um.css +0 -1
  79. package/dist/assets/main-BlVUXfxa.css +0 -1
  80. package/dist/assets/workspace-editor-monaco-CFEfFNcA.js +0 -1
  81. package/dist/assets/workspace-view-state-BS_V8JVs.js +0 -7
  82. package/extensions/browser/backend/cdp-browser-runtime.js +0 -648
  83. 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 MAX_CODEX_TRANSCRIPT_TURNS = 1000;
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 = rewriteIndexHtmlForBasePath(html, BASE_PATH);
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, blockingAgents: blockers } });
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: DEFAULT_CODEX_TRANSCRIPT_MAX_TURNS,
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(MAX_CODEX_TRANSCRIPT_TURNS, Math.max(20, requestedMaxTurns))
1153
- : DEFAULT_CODEX_TRANSCRIPT_MAX_TURNS;
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
- if (changesBrowserExtension && browserExtensionEnabled && !browserResourceManager.capability().browser) {
2215
- res.status(400).json({
2216
- error: 'Install a Chromium-based browser to use the system Browser in Farming',
2217
- code: 'BROWSER_EXECUTABLE_NOT_FOUND',
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
- return;
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
- changesBrowserExtension
2223
- && !browserExtensionEnabled
2224
- && configManager.getSettings().browserExtensionEnabled === true
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 runTerminalRuntimeStartupCleanup().finally(() => {
3225
- if (shutdownStarted) return;
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
  };