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.
Files changed (53) hide show
  1. package/README.md +5 -5
  2. package/README.zh_cn.md +5 -5
  3. package/THIRD_PARTY_NOTICES.md +5 -8
  4. package/backend/acp/client-services.js +37 -5
  5. package/backend/acp/patch-decisions.js +19 -3
  6. package/backend/acp-runtime.js +1201 -198
  7. package/backend/acp-transcript.js +66 -0
  8. package/backend/agent-lifecycle-journal.js +133 -0
  9. package/backend/agent-manager.js +2715 -354
  10. package/backend/agent-order.js +8 -4
  11. package/backend/agent-runtime-binding.js +1 -0
  12. package/backend/bundle-update-helper.js +5 -1
  13. package/backend/config-manager.js +16 -0
  14. package/backend/control-api.js +146 -20
  15. package/backend/farming-app-cli.js +335 -10
  16. package/backend/farming-session-store.js +283 -36
  17. package/backend/json-cli-runtime.js +120 -6
  18. package/backend/npm-update-helper.js +97 -18
  19. package/backend/provider-adapters.js +18 -6
  20. package/backend/review-session-service.js +31 -17
  21. package/backend/server.js +337 -103
  22. package/backend/storage-layout.js +10 -0
  23. package/backend/terminal-screen-worker-pool.js +15 -6
  24. package/backend/theme-manager.js +23 -5
  25. package/backend/update-service.js +239 -27
  26. package/backend/usage-monitor.js +11 -0
  27. package/backend/workspace-file-router.js +9 -3
  28. package/backend/workspace-file-service.js +292 -62
  29. package/dist/acp/codex-acp-1.1.4.js +32 -1
  30. package/dist/assets/App-CmZdNavW.js +210 -0
  31. package/dist/assets/{FileEditorMarkdownPreview-R0jwwLME.js → FileEditorMarkdownPreview-KbnE8VC8.js} +1 -1
  32. package/dist/assets/FileEditorPane-5_HUejjd.js +2 -0
  33. package/dist/assets/IconGlyphs-C2WFcoJS.js +1 -0
  34. package/dist/assets/ProjectFilesSection-C1RAJWOE.js +12 -0
  35. package/dist/assets/{ReviewPage-DBKNOWsJ.js → ReviewPage-L1vh1Ggs.js} +3 -3
  36. package/dist/assets/code-dark-BKtvf0Xc.css +1 -0
  37. package/dist/assets/file-icons-CHWt5nSy.js +1 -0
  38. package/dist/assets/{index-DQdtPfCk.js → index-RvUiMpGP.js} +2 -2
  39. package/dist/assets/main-XMXgyJJt.css +1 -0
  40. package/dist/assets/workspace-view-state-BqB4HM7r.js +7 -0
  41. package/dist/index.html +1 -1
  42. package/frontend/skins/crt/app.js +365 -107
  43. package/frontend/skins/crt/index.html +5 -10
  44. package/frontend/skins/crt/styles/billing.css +6 -1
  45. package/package.json +9 -3
  46. package/dist/assets/App-CbXYBccJ.js +0 -210
  47. package/dist/assets/FileEditorPane-CJs6kfZO.js +0 -2
  48. package/dist/assets/IconGlyphs-CZzDr5o2.js +0 -1
  49. package/dist/assets/ProjectFilesSection-DWCV07lA.js +0 -12
  50. package/dist/assets/code-dark-qIBiLBWV.css +0 -1
  51. package/dist/assets/file-icons-DpbrD5ZP.js +0 -1
  52. package/dist/assets/main-yzZEfgft.css +0 -1
  53. package/dist/assets/workspace-view-state-BpPINu84.js +0 -7
@@ -61,10 +61,11 @@ async function stopProcess(pid, timeoutMs = 15_000) {
61
61
 
62
62
  function validatePayload(payload) {
63
63
  if (!payload || typeof payload !== 'object') throw new Error('Missing npm update payload');
64
+ if (!['prepare', 'apply'].includes(payload.action)) throw new Error('Invalid npm update action');
64
65
  if (!/^[A-Za-z0-9@/._-]+$/.test(String(payload.packageName || ''))) throw new Error('Invalid npm package name');
65
66
  if (!/^[0-9A-Za-z.+-]+$/.test(String(payload.targetVersion || ''))) throw new Error('Invalid npm target version');
66
67
  if (!/^[0-9A-Za-z.+-]+$/.test(String(payload.previousVersion || ''))) throw new Error('Invalid npm previous version');
67
- for (const key of ['stateFile', 'logPath', 'cliPath', 'packageRoot', 'configDir']) {
68
+ for (const key of ['stateFile', 'logPath', 'cliPath', 'packageRoot', 'configDir', 'stagingPrefix', 'stagingPackageRoot']) {
68
69
  if (!path.isAbsolute(String(payload[key] || ''))) throw new Error(`Invalid npm update ${key}`);
69
70
  }
70
71
  if (payload.npmPrefix && !path.isAbsolute(String(payload.npmPrefix))) {
@@ -81,6 +82,13 @@ function validatePayload(payload) {
81
82
  throw new Error('Invalid npm update registry');
82
83
  }
83
84
  }
85
+ const expectedStagingRoot = path.join(payload.stagingPrefix, 'lib', 'node_modules', payload.packageName);
86
+ if (path.resolve(payload.stagingPackageRoot) !== path.resolve(expectedStagingRoot)) {
87
+ throw new Error('Invalid npm update stagingPackageRoot');
88
+ }
89
+ if (path.resolve(payload.stagingPackageRoot) === path.resolve(payload.packageRoot)) {
90
+ throw new Error('npm update staging root must differ from the running package root');
91
+ }
84
92
  return payload;
85
93
  }
86
94
 
@@ -93,6 +101,8 @@ function stateFor(payload, phase, extra = {}) {
93
101
  packageName: payload.packageName,
94
102
  startedAt: payload.startedAt,
95
103
  logPath: payload.logPath,
104
+ stagingPrefix: payload.stagingPrefix,
105
+ stagingPackageRoot: payload.stagingPackageRoot,
96
106
  ...extra,
97
107
  };
98
108
  }
@@ -118,13 +128,13 @@ function commandEnvironment() {
118
128
  return env;
119
129
  }
120
130
 
121
- async function installPackage(payload, version) {
131
+ async function installPackage(payload, version, npmPrefix = payload.npmPrefix) {
122
132
  const packageSpec = `${payload.packageName}@${version}`;
123
- return installPackageFromRegistry(payload, packageSpec);
133
+ return installPackageFromRegistry(payload, packageSpec, '', npmPrefix);
124
134
  }
125
135
 
126
- function verifyInstalledVersion(payload, expectedVersion) {
127
- const packageJsonPath = path.join(payload.packageRoot, 'package.json');
136
+ function verifyInstalledVersion(payload, expectedVersion, packageRoot = payload.packageRoot) {
137
+ const packageJsonPath = path.join(packageRoot, 'package.json');
128
138
  let metadata;
129
139
  try {
130
140
  metadata = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
@@ -153,10 +163,10 @@ function logSince(filePath, offset) {
153
163
  }
154
164
  }
155
165
 
156
- async function installPackageFromRegistry(payload, packageSpec, registryUrl = '') {
166
+ async function installPackageFromRegistry(payload, packageSpec, registryUrl = '', npmPrefix = payload.npmPrefix) {
157
167
  appendLog(payload.logPath, `Installing ${packageSpec}${registryUrl ? ' from the update-status registry' : ''}`);
158
168
  const args = ['install', '--global'];
159
- if (payload.npmPrefix) args.push('--prefix', payload.npmPrefix);
169
+ if (npmPrefix) args.push('--prefix', npmPrefix);
160
170
  if (registryUrl) args.push('--registry', registryUrl);
161
171
  args.push(packageSpec, '--no-audit', '--no-fund');
162
172
  const offset = logSize(payload.logPath);
@@ -169,7 +179,7 @@ async function installPackageFromRegistry(payload, packageSpec, registryUrl = ''
169
179
  } catch (error) {
170
180
  if (!registryUrl && payload.npmFallbackRegistryUrl && /(?:ETARGET|No matching version found)/.test(logSince(payload.logPath, offset))) {
171
181
  appendLog(payload.logPath, `Configured npm registry has no ${packageSpec}; retrying from the update-status registry`);
172
- return installPackageFromRegistry(payload, packageSpec, payload.npmFallbackRegistryUrl);
182
+ return installPackageFromRegistry(payload, packageSpec, payload.npmFallbackRegistryUrl, npmPrefix);
173
183
  }
174
184
  throw error;
175
185
  }
@@ -184,38 +194,100 @@ async function startServer(payload, version = payload.targetVersion) {
184
194
  });
185
195
  }
186
196
 
187
- async function runNpmUpdate(rawPayload) {
188
- const payload = validatePayload(rawPayload);
197
+ async function prepareNpmUpdate(payload) {
189
198
  try {
190
199
  writeJsonAtomic(payload.stateFile, stateFor(payload, 'installing'));
191
- await installPackage(payload, payload.targetVersion);
192
- verifyInstalledVersion(payload, payload.targetVersion);
200
+ fs.mkdirSync(payload.stagingPrefix, { recursive: true });
201
+ await installPackage(payload, payload.targetVersion, payload.stagingPrefix);
202
+ verifyInstalledVersion(payload, payload.targetVersion, payload.stagingPackageRoot);
203
+ const preparedAt = new Date().toISOString();
204
+ writeJsonAtomic(payload.stateFile, stateFor(payload, 'ready-to-restart', {
205
+ preparedAt,
206
+ }));
207
+ appendLog(payload.logPath, `Farming ${payload.targetVersion} is ready to restart`);
208
+ } catch (error) {
209
+ const message = error && error.message ? error.message : String(error);
210
+ appendLog(payload.logPath, `Update preparation failed: ${message}`);
211
+ try {
212
+ fs.rmSync(payload.stagingPrefix, { recursive: true, force: true });
213
+ } catch (cleanupError) {
214
+ appendLog(payload.logPath, `Update preparation cleanup failed: ${cleanupError.message || cleanupError}`);
215
+ }
216
+ writeJsonAtomic(payload.stateFile, stateFor(payload, 'failed', {
217
+ error: message,
218
+ completedAt: new Date().toISOString(),
219
+ }));
220
+ }
221
+ }
222
+
223
+ async function applyNpmUpdate(payload) {
224
+ const backupRoot = path.join(
225
+ path.dirname(payload.packageRoot),
226
+ `.${path.basename(payload.packageRoot)}.backup-${process.pid}`,
227
+ );
228
+ let stoppedOldServer = false;
229
+ let movedOldPackage = false;
230
+ let movedNewPackage = false;
231
+ try {
232
+ verifyInstalledVersion(payload, payload.previousVersion, payload.packageRoot);
233
+ verifyInstalledVersion(payload, payload.targetVersion, payload.stagingPackageRoot);
234
+ if (fs.existsSync(backupRoot)) throw new Error(`npm update backup already exists: ${backupRoot}`);
193
235
 
194
- writeJsonAtomic(payload.stateFile, stateFor(payload, 'restarting'));
236
+ writeJsonAtomic(payload.stateFile, stateFor(payload, 'restarting', {
237
+ preparedAt: payload.preparedAt,
238
+ }));
239
+ await new Promise(resolve => setTimeout(resolve, 1_000));
195
240
  await stopProcess(Number(payload.serverPid));
241
+ stoppedOldServer = true;
242
+ fs.renameSync(payload.packageRoot, backupRoot);
243
+ movedOldPackage = true;
244
+ fs.renameSync(payload.stagingPackageRoot, payload.packageRoot);
245
+ movedNewPackage = true;
196
246
  await startServer(payload);
197
247
 
198
248
  writeJsonAtomic(payload.stateFile, stateFor(payload, 'succeeded', {
249
+ preparedAt: payload.preparedAt,
199
250
  completedAt: new Date().toISOString(),
200
251
  }));
201
252
  appendLog(payload.logPath, `Farming updated to ${payload.targetVersion}`);
253
+ try {
254
+ fs.rmSync(backupRoot, { recursive: true, force: true });
255
+ fs.rmSync(payload.stagingPrefix, { recursive: true, force: true });
256
+ } catch (cleanupError) {
257
+ appendLog(payload.logPath, `Update cleanup failed: ${cleanupError.message || cleanupError}`);
258
+ }
202
259
  } catch (error) {
203
260
  const message = error && error.message ? error.message : String(error);
204
- appendLog(payload.logPath, `Update failed: ${message}`);
261
+ appendLog(payload.logPath, `Update apply failed: ${message}`);
205
262
 
206
- if (!isProcessRunning(Number(payload.serverPid))) {
263
+ if (stoppedOldServer || !isProcessRunning(Number(payload.serverPid))) {
207
264
  try {
208
- writeJsonAtomic(payload.stateFile, stateFor(payload, 'rolling-back', { error: message }));
209
- await installPackage(payload, payload.previousVersion);
210
- verifyInstalledVersion(payload, payload.previousVersion);
265
+ writeJsonAtomic(payload.stateFile, stateFor(payload, 'rolling-back', {
266
+ preparedAt: payload.preparedAt,
267
+ error: message,
268
+ }));
269
+ if (movedNewPackage && fs.existsSync(payload.packageRoot)) {
270
+ fs.mkdirSync(path.dirname(payload.stagingPackageRoot), { recursive: true });
271
+ fs.renameSync(payload.packageRoot, payload.stagingPackageRoot);
272
+ }
273
+ if (movedOldPackage && fs.existsSync(backupRoot)) {
274
+ fs.renameSync(backupRoot, payload.packageRoot);
275
+ }
276
+ verifyInstalledVersion(payload, payload.previousVersion, payload.packageRoot);
211
277
  await startServer(payload, payload.previousVersion);
212
278
  writeJsonAtomic(payload.stateFile, stateFor(payload, 'rolled-back', {
213
279
  version: payload.previousVersion,
214
280
  attemptedVersion: payload.targetVersion,
281
+ preparedAt: payload.preparedAt,
215
282
  error: message,
216
283
  completedAt: new Date().toISOString(),
217
284
  }));
218
285
  appendLog(payload.logPath, `Rolled back to ${payload.previousVersion}`);
286
+ try {
287
+ fs.rmSync(payload.stagingPrefix, { recursive: true, force: true });
288
+ } catch (cleanupError) {
289
+ appendLog(payload.logPath, `Rollback cleanup failed: ${cleanupError.message || cleanupError}`);
290
+ }
219
291
  return;
220
292
  } catch (rollbackError) {
221
293
  const rollbackMessage = rollbackError && rollbackError.message ? rollbackError.message : String(rollbackError);
@@ -228,12 +300,19 @@ async function runNpmUpdate(rawPayload) {
228
300
  }
229
301
 
230
302
  writeJsonAtomic(payload.stateFile, stateFor(payload, 'failed', {
303
+ preparedAt: payload.preparedAt,
231
304
  error: message,
232
305
  completedAt: new Date().toISOString(),
233
306
  }));
234
307
  }
235
308
  }
236
309
 
310
+ async function runNpmUpdate(rawPayload) {
311
+ const payload = validatePayload(rawPayload);
312
+ if (payload.action === 'prepare') return prepareNpmUpdate(payload);
313
+ return applyNpmUpdate(payload);
314
+ }
315
+
237
316
  if (require.main === module) {
238
317
  let payload;
239
318
  try {
@@ -221,7 +221,7 @@ const PROVIDER_ADAPTERS = Object.freeze([
221
221
  terminalResumeArgs: (args, sessionId) => ['resume', sessionId, ...args],
222
222
  acp: { packageName: '@agentclientprotocol/codex-acp', version: '1.1.4' },
223
223
  prepareAcpEnvironment: codexAcpEnvironment,
224
- capabilities: { runtimeSwitch: true, terminalProfile: true, goals: false },
224
+ capabilities: { runtimeSwitch: true, terminalProfile: true, goals: false, sessionFork: true },
225
225
  },
226
226
  {
227
227
  id: 'claude',
@@ -233,8 +233,12 @@ const PROVIDER_ADAPTERS = Object.freeze([
233
233
  commands: ['claude'],
234
234
  supportedRuntimes: ['terminal', 'acp'],
235
235
  planSession: (rawArgs, launchArgs) => explicitSessionPlan('claude', rawArgs, launchArgs),
236
- acp: { packageName: '@agentclientprotocol/claude-agent-acp', version: '0.59.0' },
237
- capabilities: { runtimeSwitch: true, terminalProfile: false, goals: false },
236
+ acp: {
237
+ packageName: '@agentclientprotocol/claude-agent-acp',
238
+ version: '0.59.0',
239
+ forkMode: 'target-process',
240
+ },
241
+ capabilities: { runtimeSwitch: true, terminalProfile: false, goals: false, sessionFork: true },
238
242
  },
239
243
  {
240
244
  id: 'opencode',
@@ -263,7 +267,7 @@ const PROVIDER_ADAPTERS = Object.freeze([
263
267
  args: ['acp', '--cwd', path.resolve(options.cwd || process.cwd())],
264
268
  }),
265
269
  },
266
- capabilities: { runtimeSwitch: true, terminalProfile: false, goals: false },
270
+ capabilities: { runtimeSwitch: true, terminalProfile: false, goals: false, sessionFork: true },
267
271
  },
268
272
  {
269
273
  id: 'qoder',
@@ -279,7 +283,7 @@ const PROVIDER_ADAPTERS = Object.freeze([
279
283
  version: 'native',
280
284
  launch: options => ({ command: options.executable || 'qodercli', args: ['--acp'] }),
281
285
  },
282
- capabilities: { runtimeSwitch: true, terminalProfile: false, goals: false },
286
+ capabilities: { runtimeSwitch: true, terminalProfile: false, goals: false, sessionFork: true },
283
287
  },
284
288
  ]);
285
289
 
@@ -303,10 +307,13 @@ function listProviderAdapters() {
303
307
  function providerCapabilities(provider) {
304
308
  const adapter = getProviderAdapter(provider);
305
309
  return {
306
- supportedRuntimes: adapter ? [...adapter.supportedRuntimes] : ['terminal'],
310
+ supportedRuntimes: adapter
311
+ ? adapter.supportedRuntimes.filter(runtime => runtime !== 'json')
312
+ : ['terminal'],
307
313
  runtimeSwitch: adapter?.capabilities?.runtimeSwitch === true,
308
314
  terminalProfile: adapter?.capabilities?.terminalProfile === true,
309
315
  goals: adapter?.capabilities?.goals === true,
316
+ sessionFork: adapter?.capabilities?.sessionFork === true,
310
317
  ...(adapter
311
318
  ? chatCapabilitiesForProvider(provider)
312
319
  : { chatRuntime: '', supportsChat: false, supportsSteer: false }),
@@ -317,6 +324,10 @@ function providerSupportsRuntime(provider, runtime) {
317
324
  return getProviderAdapter(provider)?.supportedRuntimes.includes(runtime) === true;
318
325
  }
319
326
 
327
+ function providerAcpForkMode(provider) {
328
+ return getProviderAdapter(provider)?.acp?.forkMode || 'source-then-load';
329
+ }
330
+
320
331
  function applyProviderHomeEnvironment(env, provider, homePath) {
321
332
  const key = getProviderAdapter(provider)?.homeEnvKey;
322
333
  if (key && homePath) env[key] = homePath;
@@ -332,6 +343,7 @@ module.exports = {
332
343
  applyProviderHomeEnvironment,
333
344
  isFreshAcpSessionSource,
334
345
  listProviderAdapters,
346
+ providerAcpForkMode,
335
347
  providerCapabilities,
336
348
  providerForProgram,
337
349
  providerSupportsRuntime,
@@ -178,6 +178,18 @@ class ReviewSessionService {
178
178
  this.reviewStateStore = reviewStateStore;
179
179
  this.resolveAgentRoot = options.resolveAgentRoot;
180
180
  this.resolveAcpReviewChanges = options.resolveAcpReviewChanges;
181
+ this.refreshQueues = new Map();
182
+ }
183
+
184
+ enqueueRefresh(reviewId, operation) {
185
+ const previous = this.refreshQueues.get(reviewId) || Promise.resolve();
186
+ const next = previous.catch(() => {}).then(operation);
187
+ this.refreshQueues.set(reviewId, next);
188
+ const cleanup = () => {
189
+ if (this.refreshQueues.get(reviewId) === next) this.refreshQueues.delete(reviewId);
190
+ };
191
+ next.then(cleanup, cleanup);
192
+ return next;
181
193
  }
182
194
 
183
195
  async git(root, args, options = {}) {
@@ -373,24 +385,26 @@ class ReviewSessionService {
373
385
 
374
386
  async refresh(reviewId) {
375
387
  if (!REVIEW_ID_PATTERN.test(reviewId)) throw new ReviewSessionError('review session id is invalid');
376
- const current = this.sessionStore.get(reviewId);
377
- if (!current) throw new ReviewSessionError('review session not found', 404);
378
- const paths = await this.capturePaths(current.root, current);
379
- const tree = await this.captureStableTree(current.root, paths);
380
- const previous = current.revisions[current.revisions.length - 1];
381
- if (previous.tree === tree) return { ...publicRevision(current, previous), changedPaths: [], unchanged: true };
382
- const nextNumber = previous.number + 1;
383
- const changedPaths = await this.changedPaths(current.root, previous.tree, tree);
384
- await this.keepRevision(current.root, reviewId, nextNumber, tree);
385
- const result = this.sessionStore.appendRevision(reviewId, tree);
386
- const next = result.session.revisions[result.session.revisions.length - 1];
387
- this.reviewStateStore?.inheritPatchset?.({
388
- changedPaths,
389
- nextPatchset: tree,
390
- previousPatchset: previous.tree,
391
- reviewId,
388
+ return this.enqueueRefresh(reviewId, async () => {
389
+ const current = this.sessionStore.get(reviewId);
390
+ if (!current) throw new ReviewSessionError('review session not found', 404);
391
+ const paths = await this.capturePaths(current.root, current);
392
+ const tree = await this.captureStableTree(current.root, paths);
393
+ const previous = current.revisions[current.revisions.length - 1];
394
+ if (previous.tree === tree) return { ...publicRevision(current, previous), changedPaths: [], unchanged: true };
395
+ const nextNumber = previous.number + 1;
396
+ const changedPaths = await this.changedPaths(current.root, previous.tree, tree);
397
+ await this.keepRevision(current.root, reviewId, nextNumber, tree);
398
+ const result = this.sessionStore.appendRevision(reviewId, tree);
399
+ const next = result.session.revisions[result.session.revisions.length - 1];
400
+ this.reviewStateStore?.inheritPatchset?.({
401
+ changedPaths,
402
+ nextPatchset: tree,
403
+ previousPatchset: previous.tree,
404
+ reviewId,
405
+ });
406
+ return { ...publicRevision(result.session, next), changedPaths, unchanged: false };
392
407
  });
393
- return { ...publicRevision(result.session, next), changedPaths, unchanged: false };
394
408
  }
395
409
 
396
410
  get(reviewId) {