farming-code 2.2.29 → 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 (38) hide show
  1. package/README.md +25 -66
  2. package/README.zh_cn.md +15 -66
  3. package/backend/acp-runtime.js +1 -15
  4. package/backend/acp-session-state.js +78 -88
  5. package/backend/agent-session-history.js +45 -12
  6. package/backend/codex-session-history.js +1 -1
  7. package/backend/codex-transcript.js +0 -202
  8. package/backend/config-manager.js +3 -19
  9. package/backend/farming-app-cli.js +1 -1
  10. package/backend/farming-cli.js +3 -57
  11. package/backend/index-html.js +21 -3
  12. package/backend/main-agent-skills.js +1 -18
  13. package/backend/server.js +6 -48
  14. package/backend/storage-layout.js +15 -0
  15. package/backend/update-service.js +55 -935
  16. package/config/farming.deploy.env.example +1 -1
  17. package/config/farming.install.env.example +1 -1
  18. package/dist/acp/codex-acp-1.1.4.mjs +387 -7
  19. package/dist/assets/App-Z1N7T0eM.js +695 -0
  20. package/dist/assets/{ProjectFilesSection-Cf2BEcAN.js → ProjectFilesSection-K3GHuQ4U.js} +2 -2
  21. package/dist/assets/code-dark-kLsQLvpf.css +1 -0
  22. package/dist/assets/{index-CQNaZc3l.js → index-D-zetSol.js} +2 -2
  23. package/dist/assets/main-DUTz-Rd2.css +1 -0
  24. package/dist/index.html +2 -1
  25. package/extensions/browser/backend/browser-resource-manager.js +27 -5
  26. package/extensions/browser/backend/browser-router.js +8 -0
  27. package/extensions/browser/backend/executable-discovery.js +26 -12
  28. package/extensions/browser/backend/managed-chromium-installer.js +593 -0
  29. package/extensions/browser/frontend/types.ts +8 -1
  30. package/extensions/browser/frontend/useBrowserResources.ts +8 -0
  31. package/index.html +1 -0
  32. package/package.json +1 -1
  33. package/backend/agent-memory-report.js +0 -583
  34. package/backend/bundle-update-helper.js +0 -113
  35. package/backend/codex-rollout-follower.js +0 -326
  36. package/dist/assets/App-Ex2oVVXO.js +0 -693
  37. package/dist/assets/code-dark-BEZTQwYZ.css +0 -1
  38. package/dist/assets/main-DK9Sy7-c.css +0 -1
@@ -105,6 +105,18 @@ function compareAgentSessions(a, b) {
105
105
  return timeDelta || agentSessionIdentity(a).localeCompare(agentSessionIdentity(b));
106
106
  }
107
107
 
108
+ function dedupeAgentSessions(sessions) {
109
+ const sessionsByIdentity = new Map();
110
+ for (const session of sessions) {
111
+ const identity = agentSessionIdentity(session);
112
+ const current = sessionsByIdentity.get(identity);
113
+ if (!current || timestampMs(session?.updatedAt || session?.createdAt) > timestampMs(current?.updatedAt || current?.createdAt)) {
114
+ sessionsByIdentity.set(identity, session);
115
+ }
116
+ }
117
+ return Array.from(sessionsByIdentity.values());
118
+ }
119
+
108
120
  function encodeAgentSessionCursor(session) {
109
121
  if (!session?.provider || !session?.id) return '';
110
122
  return Buffer.from(JSON.stringify({
@@ -140,7 +152,10 @@ function paginateAgentSessions(sessions, options = {}) {
140
152
  if (cursorValue) {
141
153
  const cursor = decodeAgentSessionCursor(cursorValue);
142
154
  if (!cursor) return { sessions: [], nextCursor: '', hasMore: false, invalidCursor: true };
143
- const exactIndex = sessions.findIndex(session => agentSessionIdentity(session) === agentSessionIdentity(cursor));
155
+ const exactIndex = sessions.findIndex(session => (
156
+ agentSessionIdentity(session) === agentSessionIdentity(cursor)
157
+ && String(session.updatedAt || session.createdAt || '') === cursor.updatedAt
158
+ ));
144
159
  if (exactIndex >= 0) {
145
160
  start = exactIndex + 1;
146
161
  } else {
@@ -164,7 +179,7 @@ function searchAgentSessions(sessions, query, options = {}) {
164
179
  ? Math.max(1, Math.min(MAX_AGENT_SESSION_HISTORY_LIMIT, Math.floor(options.limit)))
165
180
  : DEFAULT_LIMIT;
166
181
  if (!normalizedQuery) {
167
- return { sessions: [], total: 0, query: '', scope: 'title-project' };
182
+ return { sessions: [], total: 0, query: '', scope: 'id-title-project' };
168
183
  }
169
184
 
170
185
  const projectNames = options.projectNames && typeof options.projectNames === 'object'
@@ -173,6 +188,7 @@ function searchAgentSessions(sessions, query, options = {}) {
173
188
  const matches = sessions.filter(session => {
174
189
  const projectPaths = [session?.workspace, session?.cwd];
175
190
  return [
191
+ session?.id,
176
192
  session?.title,
177
193
  ...projectPaths,
178
194
  ...projectPaths.map(workspace => projectNames[String(workspace || '')]),
@@ -183,7 +199,7 @@ function searchAgentSessions(sessions, query, options = {}) {
183
199
  sessions: matches.slice(0, limit),
184
200
  total: matches.length,
185
201
  query: normalizedQuery,
186
- scope: 'title-project',
202
+ scope: 'id-title-project',
187
203
  };
188
204
  }
189
205
 
@@ -340,7 +356,13 @@ function scheduleFromClaudeEvent(event, sessionId) {
340
356
  return null;
341
357
  }
342
358
 
343
- async function collectRecentFiles(root, extension, limit, acceptFile = () => true) {
359
+ async function collectRecentFiles(
360
+ root,
361
+ extension,
362
+ limit,
363
+ acceptFile = () => true,
364
+ descendDirectory = () => true
365
+ ) {
344
366
  const directories = [root];
345
367
  let files = [];
346
368
  let visitedDirectories = 0;
@@ -362,7 +384,7 @@ async function collectRecentFiles(root, extension, limit, acceptFile = () => tru
362
384
  for (const entry of entries) {
363
385
  const fullPath = path.join(directory, entry.name);
364
386
  if (entry.isDirectory()) {
365
- directories.push(fullPath);
387
+ if (descendDirectory(fullPath)) directories.push(fullPath);
366
388
  } else if (entry.isFile() && entry.name.endsWith(extension) && acceptFile(fullPath)) {
367
389
  let mtimeMs = 0;
368
390
  try {
@@ -508,7 +530,17 @@ async function listClaudeSessions(options = {}) {
508
530
  ? Math.max(limit, Math.min(MAX_AGENT_SESSION_SCAN_LIMIT, Math.floor(options.scanLimit)))
509
531
  : DEFAULT_SCAN_LIMIT;
510
532
  const promptHistory = await readClaudePromptHistory(claudeHome);
511
- const sessionFiles = await collectRecentFiles(path.join(claudeHome, 'projects'), '.jsonl', scanLimit);
533
+ const projectsRoot = path.join(claudeHome, 'projects');
534
+ // Claude child-agent transcripts live below <project>/<session-id>/subagents
535
+ // and inherit the parent session id. Only direct project session files are
536
+ // resumable roots and should count as History entries.
537
+ const sessionFiles = await collectRecentFiles(
538
+ projectsRoot,
539
+ '.jsonl',
540
+ scanLimit,
541
+ filePath => path.relative(projectsRoot, filePath).split(path.sep).length === 2,
542
+ directoryPath => path.relative(projectsRoot, directoryPath).split(path.sep).length === 1
543
+ );
512
544
 
513
545
  const sessions = [];
514
546
  for (const { filePath, mtimeMs } of sessionFiles) {
@@ -542,7 +574,7 @@ async function listClaudeSessions(options = {}) {
542
574
  }
543
575
  }
544
576
 
545
- return sessions
577
+ return dedupeAgentSessions(sessions)
546
578
  .sort((a, b) => timestampMs(b.updatedAt) - timestampMs(a.updatedAt))
547
579
  .slice(0, limit);
548
580
  }
@@ -674,7 +706,8 @@ async function listQoderSessions(options = {}) {
674
706
  projectsRoot,
675
707
  '.jsonl',
676
708
  scanLimit,
677
- filePath => path.relative(projectsRoot, filePath).split(path.sep).length === 2
709
+ filePath => path.relative(projectsRoot, filePath).split(path.sep).length === 2,
710
+ directoryPath => path.relative(projectsRoot, directoryPath).split(path.sep).length === 1
678
711
  );
679
712
 
680
713
  const sessions = [];
@@ -709,7 +742,7 @@ async function listQoderSessions(options = {}) {
709
742
  }
710
743
  }
711
744
 
712
- return sessions
745
+ return dedupeAgentSessions(sessions)
713
746
  .sort((a, b) => timestampMs(b.updatedAt) - timestampMs(a.updatedAt))
714
747
  .slice(0, limit);
715
748
  }
@@ -755,7 +788,7 @@ async function listOpenCodeSessions(options = {}) {
755
788
  }
756
789
  if (!Array.isArray(rawSessions)) return [];
757
790
 
758
- return rawSessions
791
+ return dedupeAgentSessions(rawSessions
759
792
  .map(raw => {
760
793
  const id = firstTrimmedString(raw?.id);
761
794
  const cwd = normalizePathValue(raw?.directory || '');
@@ -777,7 +810,7 @@ async function listOpenCodeSessions(options = {}) {
777
810
  capabilities: ['resume', 'fork'],
778
811
  };
779
812
  })
780
- .filter(session => session && isVisibleAgentSession(session))
813
+ .filter(session => session && isVisibleAgentSession(session)))
781
814
  .sort((a, b) => timestampMs(b.updatedAt) - timestampMs(a.updatedAt))
782
815
  .slice(0, limit);
783
816
  }
@@ -912,7 +945,7 @@ async function listAgentSessions(options = {}) {
912
945
  })));
913
946
  }
914
947
 
915
- return sessions
948
+ return dedupeAgentSessions(sessions)
916
949
  .sort(compareAgentSessions)
917
950
  .slice(0, limit);
918
951
  }
@@ -6,7 +6,7 @@ const { stripCodexInternalContextBlocks } = require('./codex-transcript-sanitize
6
6
 
7
7
  const DEFAULT_LIMIT = 40;
8
8
  const DEFAULT_SCAN_LIMIT = 400;
9
- const MAX_SESSION_HISTORY_LIMIT = 1000;
9
+ const MAX_SESSION_HISTORY_LIMIT = 5000;
10
10
  const MAX_SESSION_HISTORY_SCAN_LIMIT = 5000;
11
11
  const MAX_SCAN_DIRECTORIES = 2000;
12
12
  const SESSION_INDEX_TAIL_BYTES = 4 * 1024 * 1024;
@@ -1,9 +1,5 @@
1
1
  const fs = require('fs');
2
- const fsp = require('fs/promises');
3
- const os = require('os');
4
2
  const path = require('path');
5
- const { fileURLToPath } = require('url');
6
- const { findCodexRolloutFile } = require('./codex-rollout-follower');
7
3
  const {
8
4
  heartbeatAssistantMessage,
9
5
  heartbeatUserMessage,
@@ -11,10 +7,8 @@ const {
11
7
  stripCodexInternalContextBlocks,
12
8
  } = require('./codex-transcript-sanitizer');
13
9
 
14
- // @deprecated `readCodexTranscript` below is the legacy JSONL file reader.
15
10
  // The generic event-to-turn projection remains for JSON compatibility feeds;
16
11
  // live structured Chat is reduced from ACP session updates instead.
17
- const DEFAULT_MAX_READ_BYTES = 32 * 1024 * 1024;
18
12
  const DEFAULT_MAX_TURNS = 240;
19
13
  const USER_MESSAGE_BEGIN = '## My request for Codex:';
20
14
  const MAX_USER_IMAGES_PER_TURN = 6;
@@ -308,71 +302,6 @@ function stripRenderedComposerAttachmentBlocks(value, renderedKinds) {
308
302
  .trim();
309
303
  }
310
304
 
311
- function codexHistoryImageTargets(value) {
312
- const text = String(value || '');
313
- const targets = [];
314
- let match;
315
- CODEX_HISTORY_IMAGE_LINK_PATTERN.lastIndex = 0;
316
- while ((match = CODEX_HISTORY_IMAGE_LINK_PATTERN.exec(text))) {
317
- const target = String(match[1] || '').trim();
318
- if (!target || targets.includes(target)) continue;
319
- targets.push(target);
320
- if (targets.length >= MAX_USER_IMAGES_PER_TURN) break;
321
- }
322
- return targets;
323
- }
324
-
325
- function localImagePathFromTarget(value) {
326
- const target = String(value || '').trim();
327
- if (!target || target.includes('\0') || /^data:/i.test(target)) return '';
328
- if (/^file:\/\//i.test(target)) {
329
- try {
330
- return fileURLToPath(target);
331
- } catch {
332
- return '';
333
- }
334
- }
335
- return path.isAbsolute(target) ? target : '';
336
- }
337
-
338
- function localImagePathsFromUserContent(content) {
339
- const paths = [];
340
- const append = (value) => {
341
- const filePath = localImagePathFromTarget(value);
342
- if (!filePath || !LOCAL_IMAGE_MIME_BY_EXT[path.extname(filePath).toLowerCase()] || paths.includes(filePath)) return;
343
- paths.push(filePath);
344
- };
345
- for (const part of Array.isArray(content) ? content : []) {
346
- if (!part || typeof part !== 'object') continue;
347
- if (['local_image', 'localImage'].includes(part.type)) append(part.path || part.file || part.url);
348
- const text = typeof part.text === 'string' ? part.text : '';
349
- for (const match of text.matchAll(/<image\b[^>]*\bpath=(['"])(.*?)\1/gi)) append(match[2]);
350
- for (const match of text.matchAll(/^##\s+[^\n:]+:\s*(.+)$/gim)) append(match[1]);
351
- }
352
- return paths.slice(0, MAX_USER_IMAGES_PER_TURN);
353
- }
354
-
355
- function dataImageUrlsFromUserContent(content) {
356
- const urls = [];
357
- for (const part of Array.isArray(content) ? content : []) {
358
- if (!part || typeof part !== 'object' || !['input_image', 'inputImage', 'image'].includes(part.type)) continue;
359
- const value = String(part.image_url || part.imageUrl || part.url || part.data || '');
360
- if (!/^data:image\/(?:gif|jpe?g|png|svg\+xml|webp);base64,/i.test(value)) continue;
361
- if (value.length > MAX_USER_IMAGE_URL_LENGTH || urls.includes(value)) continue;
362
- urls.push(value);
363
- if (urls.length >= MAX_USER_IMAGES_PER_TURN) break;
364
- }
365
- return urls;
366
- }
367
-
368
- function appendHistoryImageDataFromContent(imageDataByPath, content) {
369
- const paths = localImagePathsFromUserContent(content);
370
- const urls = dataImageUrlsFromUserContent(content);
371
- for (let index = 0; index < Math.min(paths.length, urls.length); index += 1) {
372
- if (!imageDataByPath.has(paths[index])) imageDataByPath.set(paths[index], urls[index]);
373
- }
374
- }
375
-
376
305
  function stripRenderedCodexHistoryAttachmentLinks(value, renderedKinds) {
377
306
  const text = normalizeText(value);
378
307
  if (!text || !renderedKinds.has('image')) return text;
@@ -2416,133 +2345,6 @@ function buildTranscriptFromLines(lines, options = {}) {
2416
2345
  .slice(-maxTurns);
2417
2346
  }
2418
2347
 
2419
- async function readTailLines(filePath, maxReadBytes = DEFAULT_MAX_READ_BYTES) {
2420
- const stat = await fsp.stat(filePath);
2421
- const start = Math.max(0, stat.size - maxReadBytes);
2422
- const handle = await fsp.open(filePath, 'r');
2423
- try {
2424
- const buffer = Buffer.alloc(stat.size - start);
2425
- const result = await handle.read(buffer, 0, buffer.length, start);
2426
- let text = buffer.subarray(0, result.bytesRead).toString('utf8');
2427
- if (start > 0) {
2428
- const firstNewline = text.indexOf('\n');
2429
- text = firstNewline >= 0 ? text.slice(firstNewline + 1) : '';
2430
- }
2431
- return {
2432
- truncated: start > 0,
2433
- lines: text.split('\n'),
2434
- };
2435
- } finally {
2436
- await handle.close().catch(() => {});
2437
- }
2438
- }
2439
-
2440
- function isUserMessageLine(line) {
2441
- if (!line.trim()) return false;
2442
- try {
2443
- const event = JSON.parse(line);
2444
- const { type: eventType, payload } = normalizeEventEnvelope(event);
2445
- if (!payload) return false;
2446
- if (eventType === 'event_msg') {
2447
- if (payload.type !== 'user_message') return false;
2448
- const text = textFromUserMessagePayload(payload);
2449
- const images = imagesFromUserMessagePayload(payload);
2450
- const files = extractComposerFileAttachments(text).files;
2451
- return Boolean(visibleUserMessageText(text, {
2452
- renderedAttachmentKinds: renderedAttachmentKindsForAttachments({ images, files }),
2453
- }) || images.length > 0 || files.length > 0);
2454
- }
2455
- if (eventType === 'response_item') {
2456
- if (payload.type !== 'message' || payload.role !== 'user') return false;
2457
- const text = textFromContent(payload.content);
2458
- const images = imagesFromContent(payload.content);
2459
- const files = extractComposerFileAttachments(text).files;
2460
- return Boolean(visibleUserMessageText(text, {
2461
- renderedAttachmentKinds: renderedAttachmentKindsForAttachments({ images, files }),
2462
- }) || images.length > 0 || files.length > 0);
2463
- }
2464
- if (eventType === 'item/started' || eventType === 'item/completed' || eventType === 'item.started' || eventType === 'item.completed') {
2465
- if (turnItemType(payload.item) !== 'usermessage') return false;
2466
- const text = textFromUserInput(payload.item?.content);
2467
- const images = imagesFromContent(payload.item?.content);
2468
- const audios = audiosFromContent(payload.item?.content);
2469
- const files = extractComposerFileAttachments(text).files;
2470
- return Boolean(visibleUserMessageText(text, {
2471
- renderedAttachmentKinds: renderedAttachmentKindsForAttachments({ images, audios, files }),
2472
- }) || images.length > 0 || audios.length > 0 || files.length > 0);
2473
- }
2474
- return false;
2475
- } catch {
2476
- return false;
2477
- }
2478
- }
2479
-
2480
- function dropLeadingPartialTurn(lines) {
2481
- const firstUserIndex = lines.findIndex(isUserMessageLine);
2482
- return firstUserIndex > 0 ? lines.slice(firstUserIndex) : lines;
2483
- }
2484
-
2485
- async function readCodexTranscript(sessionId, options = {}) {
2486
- const normalizedSessionId = String(sessionId || '').trim();
2487
- if (!normalizedSessionId) {
2488
- return { available: false, reason: 'missing-session-id', sessionId: '', turns: [] };
2489
- }
2490
-
2491
- const filePath = findCodexRolloutFile(normalizedSessionId, {
2492
- codexHome: options.codexHome || path.join(os.homedir(), '.codex'),
2493
- });
2494
- if (!filePath) {
2495
- return { available: false, reason: 'history-not-found', sessionId: normalizedSessionId, turns: [] };
2496
- }
2497
-
2498
- const tail = await readTailLines(filePath, options.maxReadBytes);
2499
- const lines = tail.truncated ? dropLeadingPartialTurn(tail.lines) : tail.lines;
2500
- const stat = fs.statSync(filePath);
2501
- const maxTurns = Number.isFinite(options.maxTurns) ? Math.max(1, Math.floor(options.maxTurns)) : DEFAULT_MAX_TURNS;
2502
- const allTurns = buildTranscriptFromLines(lines, { maxTurns: Number.MAX_SAFE_INTEGER });
2503
- const turns = allTurns.slice(-maxTurns);
2504
- return {
2505
- available: true,
2506
- sessionId: normalizedSessionId,
2507
- filePath,
2508
- updatedAt: new Date(stat.mtimeMs).toISOString(),
2509
- source: 'codex-rollout-jsonl',
2510
- hasMoreBefore: allTurns.length > maxTurns || tail.truncated,
2511
- turnLimit: maxTurns,
2512
- turns,
2513
- };
2514
- }
2515
-
2516
- async function readCodexHistoryImageData(sessionId, options = {}) {
2517
- const normalizedSessionId = String(sessionId || '').trim();
2518
- if (!normalizedSessionId) return new Map();
2519
- const filePath = findCodexRolloutFile(normalizedSessionId, {
2520
- codexHome: options.codexHome || path.join(os.homedir(), '.codex'),
2521
- });
2522
- if (!filePath) return new Map();
2523
- const tail = await readTailLines(filePath, options.maxReadBytes);
2524
- const imageDataByPath = new Map();
2525
- for (const line of tail.lines) {
2526
- if (!line.trim()) continue;
2527
- try {
2528
- const event = JSON.parse(line);
2529
- const { type: eventType, payload } = normalizeEventEnvelope(event);
2530
- if (!payload) continue;
2531
- if (eventType === 'response_item' && payload.type === 'message' && payload.role === 'user') {
2532
- appendHistoryImageDataFromContent(imageDataByPath, payload.content);
2533
- continue;
2534
- }
2535
- if (['item/started', 'item/completed', 'item.started', 'item.completed'].includes(eventType)) {
2536
- const item = payload.item;
2537
- if (turnItemType(item) === 'usermessage') appendHistoryImageDataFromContent(imageDataByPath, item?.content);
2538
- }
2539
- } catch {
2540
- // Ignore malformed or partial tail records; history replay remains usable without image recovery.
2541
- }
2542
- }
2543
- return imageDataByPath;
2544
- }
2545
-
2546
2348
  function buildTranscriptFromEvents(events, options = {}) {
2547
2349
  const lines = Array.isArray(events)
2548
2350
  ? events.filter(event => event && typeof event === 'object').map(event => JSON.stringify(event))
@@ -2554,10 +2356,6 @@ module.exports = {
2554
2356
  DEFAULT_MAX_TURNS,
2555
2357
  buildTranscriptFromEvents,
2556
2358
  buildTranscriptFromLines,
2557
- codexHistoryImageTargets,
2558
- dropLeadingPartialTurn,
2559
- readCodexHistoryImageData,
2560
- readCodexTranscript,
2561
2359
  stripUserMessagePrefix,
2562
2360
  textFromContent,
2563
2361
  visibleUserMessageText,
@@ -59,9 +59,6 @@ const DEFAULT_AGENT_HOMES = {
59
59
  qoder: [{ id: 'default', path: '~/.qoder' }],
60
60
  };
61
61
 
62
- const DEFAULT_UPDATE_URL = 'https://github.com/zhuwenzhuang/farming/releases/latest';
63
- const LEGACY_DEFAULT_UPDATE_URL = 'https://github.com/zhuwenzhuang/farming/releases/latest/download/manifest.json';
64
- const API_DEFAULT_UPDATE_URL = 'https://api.github.com/repos/zhuwenzhuang/farming/releases/latest';
65
62
  const LEGACY_DEFAULT_WORKSPACE_FILE_SEARCH_TIMEOUT_MS = 3000;
66
63
  const DEFAULT_SEARCH_TIMEOUT_MS = 15000;
67
64
  const MIN_SEARCH_TIMEOUT_MS = 3000;
@@ -98,7 +95,6 @@ const PERSISTED_SETTING_KEYS = new Set([
98
95
  'defaultLaunchAgent',
99
96
  'agentLaunchProfiles',
100
97
  'agentHomes',
101
- 'updateUrl',
102
98
  'searchTimeoutMs',
103
99
  'codexApprovalMode',
104
100
  'codexModel',
@@ -337,7 +333,6 @@ class ConfigManager {
337
333
  claude: cloneLaunchProfile(DEFAULT_CLAUDE_LAUNCH_PROFILE),
338
334
  },
339
335
  agentHomes: cloneAgentHomes(DEFAULT_AGENT_HOMES),
340
- updateUrl: DEFAULT_UPDATE_URL,
341
336
  searchTimeoutMs: DEFAULT_SEARCH_TIMEOUT_MS,
342
337
  codexApprovalMode: 'approve',
343
338
  codexModel: 'gpt-5.5',
@@ -379,7 +374,6 @@ class ConfigManager {
379
374
  claude: cloneLaunchProfile(DEFAULT_CLAUDE_LAUNCH_PROFILE),
380
375
  },
381
376
  agentHomes: cloneAgentHomes(DEFAULT_AGENT_HOMES),
382
- updateUrl: DEFAULT_UPDATE_URL,
383
377
  searchTimeoutMs: DEFAULT_SEARCH_TIMEOUT_MS,
384
378
  codexApprovalMode: 'approve',
385
379
  codexModel: 'gpt-5.5',
@@ -417,10 +411,7 @@ class ConfigManager {
417
411
  this.settings.projectOperations = this.normalizeProjectOperations(this.settings.projectOperations);
418
412
  this.settings.instanceName = this.normalizeInstanceName(this.settings.instanceName);
419
413
  this.settings.agentHomes = this.normalizeAgentHomes(this.settings.agentHomes);
420
- if (this.settings.updateUrl === LEGACY_DEFAULT_UPDATE_URL || this.settings.updateUrl === API_DEFAULT_UPDATE_URL) {
421
- this.settings.updateUrl = DEFAULT_UPDATE_URL;
422
- }
423
- this.settings.updateUrl = this.normalizeUpdateUrl(this.settings.updateUrl);
414
+ delete this.settings.updateUrl;
424
415
  this.settings.searchTimeoutMs = this.normalizeSearchTimeoutMs(this.settings.searchTimeoutMs);
425
416
  delete this.settings.codexRuntimeMode;
426
417
  const legacyMainPageSessionKeys = this.normalizeMainPageSessionKeys(this.settings.mainPageSessionKeys);
@@ -609,19 +600,13 @@ class ConfigManager {
609
600
  }
610
601
 
611
602
  normalizeBrowserSource(source) {
612
- return source === 'external-cdp' ? source : 'system';
603
+ return ['external-cdp', 'managed'].includes(source) ? source : 'system';
613
604
  }
614
605
 
615
606
  normalizeBrowserSetting(value) {
616
607
  return String(value || '').trim().slice(0, 2000);
617
608
  }
618
609
 
619
- normalizeUpdateUrl(value) {
620
- const url = String(value || '').trim();
621
- if (!url) return DEFAULT_UPDATE_URL;
622
- return /^https?:\/\//i.test(url) ? url.slice(0, 2000) : DEFAULT_UPDATE_URL;
623
- }
624
-
625
610
  normalizeClaudePermissionMode(mode) {
626
611
  return ['acceptEdits', 'auto', 'bypassPermissions', 'default', 'dontAsk', 'plan'].includes(mode)
627
612
  ? mode
@@ -1094,7 +1079,7 @@ class ConfigManager {
1094
1079
  nextSettings.projectOperations = this.normalizeProjectOperations(nextSettings.projectOperations);
1095
1080
  nextSettings.instanceName = this.normalizeInstanceName(nextSettings.instanceName);
1096
1081
  nextSettings.agentHomes = this.normalizeAgentHomes(nextSettings.agentHomes);
1097
- nextSettings.updateUrl = this.normalizeUpdateUrl(nextSettings.updateUrl);
1082
+ delete nextSettings.updateUrl;
1098
1083
  nextSettings.searchTimeoutMs = this.normalizeSearchTimeoutMs(nextSettings.searchTimeoutMs);
1099
1084
  delete nextSettings.codexRuntimeMode;
1100
1085
  delete nextSettings.mainPageSessionKeys;
@@ -1126,7 +1111,6 @@ class ConfigManager {
1126
1111
  }
1127
1112
 
1128
1113
  module.exports = ConfigManager;
1129
- module.exports.DEFAULT_UPDATE_URL = DEFAULT_UPDATE_URL;
1130
1114
  module.exports.DEFAULT_SEARCH_TIMEOUT_MS = DEFAULT_SEARCH_TIMEOUT_MS;
1131
1115
  module.exports.DEFAULT_CRT_TERMINAL_FONT_SIZE = DEFAULT_CRT_TERMINAL_FONT_SIZE;
1132
1116
  module.exports.MIN_CRT_TERMINAL_FONT_SIZE = MIN_CRT_TERMINAL_FONT_SIZE;
@@ -27,7 +27,7 @@ const DEFAULT_SERVER_START_STABILITY_MS = 1_500;
27
27
  const DEFAULT_SERVER_STOP_TIMEOUT_MS = 30_000;
28
28
  const SERVER_STOP_POLL_MS = 100;
29
29
  const SERVER_COMMANDS = new Set(['start', 'serve', 'daemon', 'stop', 'status', 'logs', 'url', 'help']);
30
- const CONTROL_COMMANDS = new Set(['skills', 'capabilities', 'memory', 'report', 'list', 'spawn', 'output', 'send', 'kill']);
30
+ const CONTROL_COMMANDS = new Set(['skills', 'capabilities', 'list', 'spawn', 'output', 'send', 'kill']);
31
31
  const SERVER_BACKED_CONTROL_COMMANDS = new Set(['capabilities', 'list', 'spawn', 'output', 'send', 'kill']);
32
32
 
33
33
  function defaultConfigDir(env = process.env) {
@@ -1,7 +1,6 @@
1
1
  const fs = require('fs');
2
2
  const http = require('http');
3
3
  const https = require('https');
4
- const { buildMemoryReport, formatMemoryReport } = require('./agent-memory-report');
5
4
  const { renderMainAgentSkills } = require('./main-agent-skills');
6
5
  const storageLayout = require('./storage-layout');
7
6
 
@@ -9,7 +8,6 @@ function usage() {
9
8
  return `Usage:
10
9
  farming skills
11
10
  farming capabilities [--json]
12
- farming memory report [--period today|yesterday|week] [--since <time>] [--until <time>] [--home <path>] [--json]
13
11
  farming list [--json] [--parent <agentId>]
14
12
  farming spawn [--workspace <path>] [--task <text>] [--parent <agentId>] [--json] -- <command...>
15
13
  farming output <agentId> [--tail <chars>]
@@ -18,8 +16,6 @@ function usage() {
18
16
 
19
17
  Examples:
20
18
  farming spawn --workspace /repo --task "Inspect this module for bugs" -- claude
21
- farming memory report --period today
22
- farming memory report --period week --json
23
19
  farming skills
24
20
  farming capabilities
25
21
  farming list --parent "$FARMING_AGENT_ID"
@@ -85,43 +81,6 @@ function parseArgs(argv) {
85
81
  return { command, options: { json: rest.includes('--json') } };
86
82
  }
87
83
 
88
- if (command === 'memory' || command === 'report') {
89
- const subcommand = command === 'memory' ? rest.shift() : 'report';
90
- if (subcommand !== 'report') {
91
- throw new Error('memory requires the report subcommand');
92
- }
93
-
94
- const options = {
95
- period: 'today',
96
- since: '',
97
- until: '',
98
- homeDir: '',
99
- json: false,
100
- };
101
-
102
- for (let i = 0; i < rest.length; i++) {
103
- if (rest[i] === '--period') {
104
- options.period = splitOptionValue(rest, i, '--period');
105
- i++;
106
- } else if (rest[i] === '--since') {
107
- options.since = splitOptionValue(rest, i, '--since');
108
- i++;
109
- } else if (rest[i] === '--until') {
110
- options.until = splitOptionValue(rest, i, '--until');
111
- i++;
112
- } else if (rest[i] === '--home') {
113
- options.homeDir = splitOptionValue(rest, i, '--home');
114
- i++;
115
- } else if (rest[i] === '--json') {
116
- options.json = true;
117
- } else {
118
- throw new Error(`Unknown option: ${rest[i]}`);
119
- }
120
- }
121
-
122
- return { command: 'memory-report', options };
123
- }
124
-
125
84
  if (command === 'list') {
126
85
  const options = { json: false, parent: '' };
127
86
  for (let i = 0; i < rest.length; i++) {
@@ -316,7 +275,9 @@ function farmingCapabilities(browser) {
316
275
  summary: state === 'available'
317
276
  ? (browser?.browser?.kind === 'external-cdp'
318
277
  ? 'Default browser path for web tasks. An externally managed CDP Browser can be created or attached as a shared, user-visible Farming Resource.'
319
- : 'Default browser path for web tasks. An installed system Chromium Browser can be created or attached as a shared, user-visible Farming Resource.')
278
+ : browser?.browser?.kind === 'managed-chromium'
279
+ ? 'Default browser path for web tasks. Farming-managed Chromium can be created or attached as a shared, user-visible Farming Resource.'
280
+ : 'Default browser path for web tasks. An installed system Chromium Browser can be created or attached as a shared, user-visible Farming Resource.')
320
281
  : (browser?.message || 'Browser integration is unavailable in Farming.'),
321
282
  commands: state === 'available'
322
283
  ? {
@@ -361,21 +322,6 @@ async function run(argv = process.argv.slice(2), io = process) {
361
322
  return 0;
362
323
  }
363
324
 
364
- if (parsed.command === 'memory-report') {
365
- const report = buildMemoryReport({
366
- period: parsed.options.period,
367
- since: parsed.options.since,
368
- until: parsed.options.until,
369
- homeDir: parsed.options.homeDir || undefined,
370
- });
371
- if (parsed.options.json) {
372
- io.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
373
- } else {
374
- io.stdout.write(`${formatMemoryReport(report)}\n`);
375
- }
376
- return 0;
377
- }
378
-
379
325
  if (parsed.command === 'list') {
380
326
  const query = parsed.options.parent ? `?parent=${encodeURIComponent(parsed.options.parent)}` : '';
381
327
  const state = await request(`/api/control/agents${query}`);
@@ -43,14 +43,32 @@ function appendIndexHtmlAssetToken(html, token) {
43
43
 
44
44
  function applyIndexHtmlAppearance(html, appearance) {
45
45
  const normalizedAppearance = ['light', 'dark'].includes(appearance) ? appearance : 'system';
46
- const source = String(html || '');
46
+ let source = String(html || '');
47
47
  if (/\bdata-appearance-preference="[^"]*"/i.test(source)) {
48
- return source.replace(
48
+ source = source.replace(
49
49
  /\bdata-appearance-preference="[^"]*"/i,
50
50
  `data-appearance-preference="${normalizedAppearance}"`
51
51
  );
52
+ } else {
53
+ source = source.replace(
54
+ /<html\b/i,
55
+ `<html data-appearance-preference="${normalizedAppearance}"`
56
+ );
52
57
  }
53
- return source.replace(/<html\b/i, `<html data-appearance-preference="${normalizedAppearance}"`);
58
+
59
+ const colorScheme = normalizedAppearance === 'system'
60
+ ? 'light dark'
61
+ : normalizedAppearance;
62
+ const themeColor = normalizedAppearance === 'dark' ? '#181818' : '#ffffff';
63
+ return source
64
+ .replace(
65
+ /(<meta\s+name="color-scheme"\s+content=")[^"]*(")/i,
66
+ `$1${colorScheme}$2`
67
+ )
68
+ .replace(
69
+ /(<meta\s+name="theme-color"\s+content=")[^"]*(")/i,
70
+ `$1${themeColor}$2`
71
+ );
54
72
  }
55
73
 
56
74
  module.exports = {
@@ -4,7 +4,7 @@ const path = require('path');
4
4
  const MANAGED_BLOCK_START = '<!-- FARMING_MAIN_AGENT_SKILLS:START -->';
5
5
  const MANAGED_BLOCK_END = '<!-- FARMING_MAIN_AGENT_SKILLS:END -->';
6
6
 
7
- const REMOVED_MAIN_AGENT_SKILL_IDS = [];
7
+ const REMOVED_MAIN_AGENT_SKILL_IDS = ['memory-report'];
8
8
 
9
9
  const MAIN_AGENT_OPERATING_GUIDE = [
10
10
  {
@@ -19,7 +19,6 @@ const MAIN_AGENT_OPERATING_GUIDE = [
19
19
  lines: [
20
20
  'On startup or resume, orient yourself with `farming list --json` and inspect `FARMING_PROJECT_WORKSPACE` / `FARMING_MAIN_WORKSPACE` before proposing work.',
21
21
  'Do not spawn child agents just because you can. Wait for a concrete user goal, or ask one short clarification if the goal is too vague to route safely.',
22
- 'Use `farming memory report` only when recent local context would change what you do next.',
23
22
  ],
24
23
  },
25
24
  {
@@ -69,18 +68,6 @@ const MAIN_AGENT_SKILLS = [
69
68
  'farming browser help <lifecycle|navigation|interaction|inspection|debugging|state|files>',
70
69
  ],
71
70
  },
72
- {
73
- id: 'memory-report',
74
- name: '记忆读取总结',
75
- trigger: '用户要求总结今日、昨日、本周做过什么,或需要先了解这台机器上各 agent 的近期工作记忆时',
76
- summary: '只读扫描本机 Claude/Qwen/Codex 本地历史线索,按时间段生成 Farming 日报或周报。',
77
- commands: [
78
- 'farming memory report --period today',
79
- 'farming memory report --period yesterday',
80
- 'farming memory report --period week',
81
- 'farming memory report --since <YYYY-MM-DD> --until <YYYY-MM-DD>',
82
- ],
83
- },
84
71
  {
85
72
  id: 'pest-control',
86
73
  name: '牧场除虫计划',
@@ -150,7 +137,6 @@ function renderMainAgentSkills() {
150
137
 
151
138
  lines.push('Rules:');
152
139
  lines.push('- Use `farming browser` only for Browser Resources already in the user-selected Project scope; follow `farming browser help workflow`, and reveal only the help topic or command needed for the current step.');
153
- lines.push('- Use `farming memory report` when you need recent context from local agent memories.');
154
140
  lines.push('- Use “牧场除虫计划” when the user asks for systematic bug hunting across a directory or module tree.');
155
141
  lines.push('- Before spawning child agents for pest control, map modules and module protocols first; do not send overlapping or vague tasks.');
156
142
  lines.push('- Keep child agents scoped to their assigned module and require evidence, reproduction notes, or tests for each bug claim.');
@@ -174,9 +160,6 @@ function renderCanonicalAgentsFile() {
174
160
  '',
175
161
  'Useful commands:',
176
162
  '- `farming skills`',
177
- '- `farming memory report --period today`',
178
- '- `farming memory report --period yesterday`',
179
- '- `farming memory report --period week`',
180
163
  '- `farming spawn --workspace <repo> --task "<module bug hunt task>" -- <coding-agent-command>`',
181
164
  '- `farming list --parent "$FARMING_AGENT_ID"`',
182
165
  '- `farming output <agent-id> --tail 2000`',