farming-code 2.2.8 → 2.2.12

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 (107) hide show
  1. package/README.md +42 -28
  2. package/README.zh_cn.md +42 -28
  3. package/THIRD_PARTY_NOTICES.md +11 -2
  4. package/backend/acp-checkpoint-store.js +198 -0
  5. package/backend/acp-runtime.js +306 -83
  6. package/backend/acp-session-state.js +202 -6
  7. package/backend/acp-transcript.js +112 -0
  8. package/backend/agent-activity.js +6 -157
  9. package/backend/agent-manager.js +1592 -741
  10. package/backend/agent-provider-session.js +17 -242
  11. package/backend/agent-runtime-binding.js +219 -0
  12. package/backend/agent-session-history.js +66 -1
  13. package/backend/auth.js +79 -6
  14. package/backend/codex-models.js +81 -84
  15. package/backend/codex-session-archive.js +54 -0
  16. package/backend/codex-terminal-profile.js +500 -0
  17. package/backend/codex-transcript-sanitizer.js +12 -0
  18. package/backend/codex-transcript.js +230 -8
  19. package/backend/config-manager.js +35 -0
  20. package/backend/control-api.js +192 -17
  21. package/backend/executable-discovery.js +2 -0
  22. package/backend/farming-net-pass.js +285 -0
  23. package/backend/farming-net-registry.js +112 -0
  24. package/backend/farming-net-server.js +298 -0
  25. package/backend/farming-session-store.js +5 -13
  26. package/backend/git-worktree-info.js +181 -0
  27. package/backend/local-session-engine.js +411 -186
  28. package/backend/main-page-session.js +5 -2
  29. package/backend/native-pty-controller-generation.js +106 -0
  30. package/backend/native-pty-host-client.js +275 -7
  31. package/backend/native-pty-host-identity.js +86 -0
  32. package/backend/native-pty-host.js +813 -114
  33. package/backend/native-session-engine.js +100 -28
  34. package/backend/packaged-node-pty.js +22 -2
  35. package/backend/provider-adapters.js +253 -0
  36. package/backend/provider-session-service.js +241 -0
  37. package/backend/runtime-observation.js +81 -0
  38. package/backend/server.js +354 -84
  39. package/backend/session-engine-bridge.js +21 -2
  40. package/backend/session-engine-router.js +1 -1
  41. package/backend/session-engine.js +1 -1
  42. package/backend/session-stream-protocol.js +185 -0
  43. package/backend/storage-layout.js +55 -0
  44. package/backend/terminal-attach-checkpoint.js +74 -0
  45. package/backend/terminal-exit-quiescence.js +39 -0
  46. package/backend/terminal-reducer-flow-control.js +97 -0
  47. package/backend/terminal-screen-state.js +11 -2
  48. package/backend/terminal-screen-worker-pool.js +59 -6
  49. package/backend/terminal-screen-worker-thread.js +97 -57
  50. package/backend/terminal-screen-worker.js +133 -51
  51. package/backend/terminal-state-serialization.js +127 -0
  52. package/backend/terminal-status.js +23 -4
  53. package/backend/usage-monitor.js +176 -17
  54. package/backend/workspace-directory.js +232 -0
  55. package/backend/workspace-file-router.js +182 -76
  56. package/backend/workspace-file-service.js +319 -4
  57. package/backend/workspace-root-registry.js +164 -0
  58. package/dist/assets/App-DWsa7DXG.js +206 -0
  59. package/dist/assets/{FileEditorMarkdownPreview-elKWc8Im.js → FileEditorMarkdownPreview-UlVd0O4D.js} +92 -92
  60. package/dist/assets/FileEditorPane-Dxx4rKOg.js +2 -0
  61. package/dist/assets/IconGlyphs-AU22gPDY.js +1 -0
  62. package/dist/assets/ProjectFilesSection-DE7Nb9Jk.js +12 -0
  63. package/dist/assets/ReviewPage-CeDKDyZh.js +3 -0
  64. package/dist/assets/code-dark-DPiuyeSp.css +1 -0
  65. package/dist/assets/file-icons-Bw2qd5iT.js +1 -0
  66. package/dist/assets/{index-BrbljRqn.js → index-CUFPJoa5.js} +3 -3
  67. package/dist/assets/main-Bqtb5kuz.css +1 -0
  68. package/dist/assets/{monaco.contribution-CyLosfZI.js → monaco.contribution-BXz-lfFB.js} +2 -2
  69. package/dist/assets/{tsMode-DGZTlTj8.js → tsMode-B0oUTcXQ.js} +1 -1
  70. package/dist/assets/workspace-editor-model-BQol4qbA.js +1 -0
  71. package/dist/assets/workspace-editor-monaco-BICJESG0.js +4 -0
  72. package/dist/assets/workspace-editor-monaco-DRJ7IaXk.js +1 -0
  73. package/dist/assets/workspace-view-state-DvYG_9PH.js +7 -0
  74. package/dist/assets/workspace-working-copy-D8-s_Sgh.js +1 -0
  75. package/dist/farming-2/site.webmanifest +2 -0
  76. package/dist/index.html +1 -1
  77. package/frontend/farming-net/app.css +625 -0
  78. package/frontend/farming-net/app.js +268 -0
  79. package/frontend/farming-net/index.html +86 -0
  80. package/frontend/reading-anchor.js +198 -0
  81. package/frontend/session-bridge.js +12 -3
  82. package/frontend/session-modal-bridge.js +5 -12
  83. package/frontend/skins/crt/app.js +1978 -793
  84. package/frontend/skins/crt/index.html +313 -23
  85. package/frontend/skins/crt/styles/billing.css +294 -223
  86. package/frontend/skins/crt/styles/monochrome-green.css +7 -2
  87. package/frontend/terminal-replay.js +372 -0
  88. package/package.json +10 -3
  89. package/shared/browser-protocol.d.ts +5 -0
  90. package/shared/browser-protocol.js +130 -0
  91. package/dist/assets/App-8dYAM6ql.js +0 -124
  92. package/dist/assets/FileEditorPane-RWiFD2cq.js +0 -5
  93. package/dist/assets/IconGlyphs-DfL0EBnj.js +0 -1
  94. package/dist/assets/ProjectFilesSection-Q4PDsWmM.js +0 -12
  95. package/dist/assets/ReviewPage-BaXu1ZdX.js +0 -3
  96. package/dist/assets/code-dark-CDkOQAtK.css +0 -1
  97. package/dist/assets/file-icons-EFUGSSwf.js +0 -1
  98. package/dist/assets/main-D073SnW4.css +0 -1
  99. package/dist/assets/qoder-C9LmmOSf.svg +0 -1
  100. package/dist/assets/qoder-Cf9gl0Y5.svg +0 -1
  101. package/dist/assets/qoder-gHCinseV.svg +0 -1
  102. package/dist/assets/workspace-view-state-CTyDzk2D.js +0 -1
  103. package/dist/assets/zsh-CLpveKlF.svg +0 -1
  104. package/dist/assets/zsh-FxSpMPbz.svg +0 -1
  105. /package/dist/assets/{api-D1lyBYIQ.js → api-D8nyOEbz.js} +0 -0
  106. /package/dist/assets/{core-ZlAPicox.js → core-D0LFJkDt.js} +0 -0
  107. /package/dist/assets/{useWorkspaceMenuKeyboard-CneKAZUJ.js → useWorkspaceMenuKeyboard-Brws6Ar9.js} +0 -0
@@ -1,5 +1,23 @@
1
+ const fsp = require('fs/promises');
2
+ const path = require('path');
3
+ const { fileURLToPath } = require('url');
4
+
1
5
  const MAX_ACP_UPDATES = 2_000;
2
6
  const MAX_ACP_UPDATE_LOG_VALUE_CHARS = 32 * 1024;
7
+ const MAX_CODEX_HISTORY_IMAGES_PER_MESSAGE = 6;
8
+ const MAX_CODEX_HISTORY_IMAGE_BYTES = 5 * 1024 * 1024;
9
+ const CODEX_HISTORY_IMAGE_MIME_BY_EXT = Object.freeze({
10
+ '.gif': 'image/gif',
11
+ '.jpeg': 'image/jpeg',
12
+ '.jpg': 'image/jpeg',
13
+ '.png': 'image/png',
14
+ '.svg': 'image/svg+xml',
15
+ '.webp': 'image/webp',
16
+ });
17
+ const {
18
+ codexHistoryImageTargets,
19
+ visibleUserMessageText,
20
+ } = require('./codex-transcript');
3
21
  const {
4
22
  isCodexInjectedContextMessage,
5
23
  stripCodexInternalContextBlocks,
@@ -44,14 +62,78 @@ function contentText(content) {
44
62
  .join('');
45
63
  }
46
64
 
65
+ function localPathFromHistoryImageTarget(value) {
66
+ const target = String(value || '').trim();
67
+ if (!target || target.includes('\0') || /^data:/i.test(target)) return '';
68
+ if (/^file:\/\//i.test(target)) {
69
+ try {
70
+ return fileURLToPath(target);
71
+ } catch {
72
+ return '';
73
+ }
74
+ }
75
+ return path.isAbsolute(target) ? target : '';
76
+ }
77
+
78
+ function imageBlockFromDataUrl(value) {
79
+ const match = String(value || '').match(/^data:(image\/(?:gif|jpe?g|png|svg\+xml|webp));base64,([a-z0-9+/=]+)$/i);
80
+ if (!match) return null;
81
+ const data = match[2];
82
+ if (!data || Math.ceil(data.length * 3 / 4) > MAX_CODEX_HISTORY_IMAGE_BYTES) return null;
83
+ return { type: 'image', mimeType: match[1].toLowerCase(), data };
84
+ }
85
+
86
+ async function imageBlockFromLocalPath(filePath) {
87
+ const ext = path.extname(filePath).toLowerCase();
88
+ const mimeType = CODEX_HISTORY_IMAGE_MIME_BY_EXT[ext];
89
+ if (!mimeType) return null;
90
+ try {
91
+ const stat = await fsp.stat(filePath);
92
+ if (!stat.isFile() || stat.size <= 0 || stat.size > MAX_CODEX_HISTORY_IMAGE_BYTES) return null;
93
+ return { type: 'image', mimeType, data: (await fsp.readFile(filePath)).toString('base64') };
94
+ } catch {
95
+ return null;
96
+ }
97
+ }
98
+
47
99
  function isContextCompactionText(content) {
48
- return /^\*?Context compacted(?: to fit the model's context window)?\.?\*?$/i.test(contentText(content).trim());
100
+ const text = contentText(content).trim();
101
+ return /^\*?Context compacted(?: to fit the model's context window)?\.?\*?$/i.test(text)
102
+ || /^(?:#{1,3}\s*)?Handoff Summary(?:[ \t]*:|[ \t]*(?:\r?\n|$))/i.test(text)
103
+ || /^Another language model started to solve this problem and produced a summary\b/i.test(text);
49
104
  }
50
105
 
51
106
  function canMergeMessageIds(existing, incoming) {
52
107
  return !existing || !incoming || existing === incoming;
53
108
  }
54
109
 
110
+ function codexMessagePhase(meta) {
111
+ return String(meta?.codex?.phase || '');
112
+ }
113
+
114
+ function canMergeMessageChunks(existing, update) {
115
+ if (!canMergeMessageIds(existing?.messageId, String(update?.messageId || ''))) return false;
116
+ // Codex ACP history can omit message ids while still preserving the original
117
+ // commentary/final-answer boundary in metadata. Never merge across that
118
+ // boundary or the frontend loses the only authoritative answer signal.
119
+ return codexMessagePhase(existing?._meta) === codexMessagePhase(update?._meta);
120
+ }
121
+
122
+ function isCodexMirroredAssistantMessage(provider, existing, update, role, type) {
123
+ if (provider !== 'codex' || role !== 'assistant' || type !== 'message') return false;
124
+ if (!existing || existing.type !== type || existing.role !== role) return false;
125
+ if (codexMessagePhase(existing._meta) !== codexMessagePhase(update?._meta)) return false;
126
+ const existingId = String(existing.messageId || '');
127
+ const incomingId = String(update?.messageId || '');
128
+ // The App Server thread item has an id while the JSONL response-item
129
+ // fallback does not. If both have ids, keep them as distinct protocol
130
+ // messages even when their visible text happens to match.
131
+ if (existingId && incomingId) return false;
132
+ const existingText = stripCodexInternalContextBlocks(contentText(existing.content));
133
+ const incomingText = stripCodexInternalContextBlocks(contentText([update?.content]));
134
+ return Boolean(existingText) && existingText === incomingText;
135
+ }
136
+
55
137
  class AcpSessionState {
56
138
  constructor(options = {}) {
57
139
  this.provider = String(options.provider || '');
@@ -89,6 +171,41 @@ class AcpSessionState {
89
171
  this.sessionId = String(sessionId || this.sessionId);
90
172
  }
91
173
 
174
+ static fromCheckpoint(checkpoint, options = {}) {
175
+ if (!checkpoint || checkpoint.version !== 1 || !Array.isArray(checkpoint.entries)) return null;
176
+ const state = new AcpSessionState({
177
+ provider: options.provider || checkpoint.provider,
178
+ sessionId: options.sessionId || checkpoint.sessionId,
179
+ cwd: options.cwd || checkpoint.cwd,
180
+ maxUpdates: options.maxUpdates,
181
+ revisionBase: checkpoint.revision,
182
+ resetBeforeRevision: checkpoint.resetBeforeRevision,
183
+ });
184
+ state.entries = clone(checkpoint.entries);
185
+ state.toolEntries.clear();
186
+ state.compactionEntries.clear();
187
+ let maximumRevision = 0;
188
+ for (const entry of state.entries) {
189
+ maximumRevision = Math.max(maximumRevision, Number(entry?._revision || 0));
190
+ if (entry?.type === 'tool' && entry.id) state.toolEntries.set(String(entry.id), entry);
191
+ if (entry?.type === 'compaction' && entry.id) state.compactionEntries.set(String(entry.id), entry);
192
+ }
193
+ state.revision = Math.max(state.revision, maximumRevision);
194
+ state.sequence = Math.max(0, Math.floor(Number(checkpoint.sequence || 0)));
195
+ state.activePlanEntry = checkpoint.activePlanEntryId
196
+ ? state.entries.find(entry => entry?.id === checkpoint.activePlanEntryId) || null
197
+ : null;
198
+ state.plan = clone(checkpoint.plan ?? state.activePlanEntry?.plan ?? null);
199
+ state.usage = clone(checkpoint.usage ?? null);
200
+ state.availableCommands = clone(checkpoint.availableCommands || []);
201
+ state.currentModeId = String(checkpoint.currentModeId || '');
202
+ state.configOptions = clone(checkpoint.configOptions || []);
203
+ state.title = String(checkpoint.title || '');
204
+ state.updatedAt = String(checkpoint.updatedAt || '');
205
+ state.truncated = checkpoint.truncated === true;
206
+ return state;
207
+ }
208
+
92
209
  nextEntryId(prefix) {
93
210
  return `${prefix}-${++this.sequence}`;
94
211
  }
@@ -165,6 +282,44 @@ class AcpSessionState {
165
282
  // History replay uses the same reducer as live updates; nothing to close.
166
283
  }
167
284
 
285
+ hasCodexHistoryImageReferences() {
286
+ return this.provider === 'codex' && this.entries.some(entry => (
287
+ entry?.type === 'message'
288
+ && entry.role === 'user'
289
+ && codexHistoryImageTargets(contentText(entry.content)).length > 0
290
+ ));
291
+ }
292
+
293
+ async hydrateCodexHistoryAttachments(options = {}) {
294
+ if (this.provider !== 'codex') return 0;
295
+ const imageDataByPath = options.imageDataByPath instanceof Map ? options.imageDataByPath : new Map();
296
+ let hydrated = 0;
297
+ for (const entry of this.entries) {
298
+ if (entry?.type !== 'message' || entry.role !== 'user') continue;
299
+ const existingImages = (entry.content || []).filter(content => content?.type === 'image');
300
+ const remaining = MAX_CODEX_HISTORY_IMAGES_PER_MESSAGE - existingImages.length;
301
+ if (remaining <= 0) continue;
302
+ const targets = codexHistoryImageTargets(contentText(entry.content));
303
+ const seenPaths = new Set();
304
+ const blocks = [];
305
+ for (const target of targets) {
306
+ const filePath = localPathFromHistoryImageTarget(target);
307
+ if (!filePath || seenPaths.has(filePath)) continue;
308
+ seenPaths.add(filePath);
309
+ const fallback = imageDataByPath.get(filePath);
310
+ const block = await imageBlockFromLocalPath(filePath) || imageBlockFromDataUrl(fallback);
311
+ if (!block) continue;
312
+ blocks.push(block);
313
+ if (blocks.length >= remaining) break;
314
+ }
315
+ if (blocks.length === 0) continue;
316
+ entry.content.push(...blocks);
317
+ this.touchEntry(entry);
318
+ hydrated += blocks.length;
319
+ }
320
+ return hydrated;
321
+ }
322
+
168
323
  apply(notification) {
169
324
  if (!notification || notification.sessionId !== this.sessionId) return false;
170
325
  const update = notification.update;
@@ -243,9 +398,15 @@ class AcpSessionState {
243
398
  if (
244
399
  last?.type === type
245
400
  && last.role === role
246
- && canMergeMessageIds(last.messageId, messageId)
401
+ && canMergeMessageChunks(last, update)
247
402
  ) {
403
+ const mirroredAssistantMessage = isCodexMirroredAssistantMessage(this.provider, last, update, role, type);
248
404
  if (!last.messageId) last.messageId = messageId;
405
+ if (!last._meta && update._meta) last._meta = clone(update._meta);
406
+ if (mirroredAssistantMessage) {
407
+ this.touchEntry(last);
408
+ return;
409
+ }
249
410
  appendContent(last.content, update.content);
250
411
  this.touchEntry(last);
251
412
  return;
@@ -258,6 +419,7 @@ class AcpSessionState {
258
419
  role,
259
420
  messageId,
260
421
  content: [],
422
+ ...(update._meta ? { _meta: clone(update._meta) } : {}),
261
423
  });
262
424
  appendContent(this.entries[this.entries.length - 1].content, update.content);
263
425
  }
@@ -326,9 +488,10 @@ class AcpSessionState {
326
488
  ? Math.max(1, Math.floor(Number(options.maxTurns)))
327
489
  : 80;
328
490
  const requestedRevision = Number(options.sinceRevision);
329
- const resetRequired = this.resetBeforeRevision > 0
330
- && Number.isFinite(requestedRevision)
331
- && requestedRevision <= this.resetBeforeRevision;
491
+ const resetRequired = Number.isFinite(requestedRevision) && (
492
+ requestedRevision > this.revision
493
+ || (this.resetBeforeRevision > 0 && requestedRevision <= this.resetBeforeRevision)
494
+ );
332
495
  const delta = Number.isFinite(requestedRevision) && requestedRevision >= 0 && !resetRequired;
333
496
  let startIndex = 0;
334
497
 
@@ -397,8 +560,19 @@ class AcpSessionState {
397
560
  }
398
561
  entry.internal = internalSegment;
399
562
  if (!['message', 'thought'].includes(entry.type)) continue;
563
+ const renderedAttachmentKinds = [];
564
+ if (
565
+ entry.type === 'message'
566
+ && entry.role === 'user'
567
+ && ((entry.content || []).some(content => content?.type === 'image') || codexHistoryImageTargets(contentText(entry.content)).length > 0)
568
+ ) {
569
+ renderedAttachmentKinds.push('image');
570
+ }
400
571
  for (const content of entry.content || []) {
401
- if (content.type === 'text') content.text = stripCodexInternalContextBlocks(content.text);
572
+ if (content.type !== 'text') continue;
573
+ content.text = entry.type === 'message' && entry.role === 'user' && entry.internal !== true
574
+ ? visibleUserMessageText(content.text, { renderedAttachmentKinds })
575
+ : stripCodexInternalContextBlocks(content.text);
402
576
  }
403
577
  }
404
578
  return entries;
@@ -439,6 +613,28 @@ class AcpSessionState {
439
613
  if (options.includeUpdates === true) snapshot.updates = clone(this.updates);
440
614
  return snapshot;
441
615
  }
616
+
617
+ exportCheckpoint() {
618
+ return {
619
+ version: 1,
620
+ provider: this.provider,
621
+ sessionId: this.sessionId,
622
+ cwd: this.cwd,
623
+ sequence: this.sequence,
624
+ revision: this.revision,
625
+ resetBeforeRevision: this.resetBeforeRevision,
626
+ entries: clone(this.entries),
627
+ activePlanEntryId: this.activePlanEntry?.id || '',
628
+ plan: clone(this.plan),
629
+ usage: clone(this.usage),
630
+ availableCommands: clone(this.availableCommands),
631
+ currentModeId: this.currentModeId,
632
+ configOptions: clone(this.configOptions),
633
+ title: this.title,
634
+ updatedAt: this.updatedAt,
635
+ truncated: this.truncated,
636
+ };
637
+ }
442
638
  }
443
639
 
444
640
  module.exports = {
@@ -1,6 +1,8 @@
1
1
  const { createTwoFilesPatch, diffLines } = require('diff');
2
2
 
3
3
  const MAX_RENDERED_DIFF_CHARS = 64 * 1024;
4
+ const MAX_INLINE_TOOL_DETAIL_CHARS = 4 * 1024;
5
+ const MAX_EMBEDDED_RESOURCE_TEXT_CHARS = 4 * 1024;
4
6
 
5
7
  function diffBlocks(content) {
6
8
  return (Array.isArray(content) ? content : [])
@@ -162,8 +164,118 @@ function detailForTool(entry) {
162
164
  return sections.join('\n\n');
163
165
  }
164
166
 
167
+ function transcriptResource(resourceValue) {
168
+ const resource = resourceValue && typeof resourceValue === 'object' ? resourceValue : {};
169
+ const text = typeof resource.text === 'string' ? resource.text : '';
170
+ return {
171
+ name: resource.name,
172
+ uri: resource.uri,
173
+ mimeType: resource.mimeType,
174
+ ...(text
175
+ ? {
176
+ text: text.slice(0, MAX_EMBEDDED_RESOURCE_TEXT_CHARS),
177
+ ...(text.length > MAX_EMBEDDED_RESOURCE_TEXT_CHARS ? { textTruncated: true } : {}),
178
+ }
179
+ : {}),
180
+ };
181
+ }
182
+
183
+ function transcriptMediaBlocks(entry) {
184
+ const direct = Array.isArray(entry?.content) ? entry.content : [];
185
+ const output = entry?.rawOutput && typeof entry.rawOutput === 'object' ? entry.rawOutput : {};
186
+ const result = output?.result && typeof output.result === 'object' ? output.result : {};
187
+ const raw = Array.isArray(result.content)
188
+ ? result.content
189
+ : (Array.isArray(output.content) ? output.content : []);
190
+ const blocks = [...direct, ...raw].flatMap(block => {
191
+ if (!block || typeof block !== 'object') return [];
192
+ if (block.type === 'terminal') return [{ type: 'terminal', terminalId: String(block.terminalId || '') }];
193
+ if (['image', 'audio', 'resource_link'].includes(block.type)) return [JSON.parse(JSON.stringify(block))];
194
+ if (block.type === 'resource') {
195
+ return [{
196
+ type: 'resource',
197
+ resource: transcriptResource(block.resource),
198
+ }];
199
+ }
200
+ if (block.type !== 'content' || !block.content || typeof block.content !== 'object') return [];
201
+ const content = block.content;
202
+ if (['image', 'audio', 'resource_link'].includes(content.type)) {
203
+ return [{ type: 'content', content: JSON.parse(JSON.stringify(content)) }];
204
+ }
205
+ if (content.type === 'resource') {
206
+ return [{
207
+ type: 'content',
208
+ content: {
209
+ type: 'resource',
210
+ resource: transcriptResource(content.resource),
211
+ },
212
+ }];
213
+ }
214
+ return [];
215
+ });
216
+ const seenTerminals = new Set();
217
+ return blocks.filter(block => {
218
+ if (block.type !== 'terminal') return true;
219
+ if (!block.terminalId || seenTerminals.has(block.terminalId)) return false;
220
+ seenTerminals.add(block.terminalId);
221
+ return true;
222
+ });
223
+ }
224
+
225
+ function generatedMediaTool(entry) {
226
+ const title = String(entry?.title || '').trim().toLowerCase();
227
+ const id = String(entry?.id || '').trim().toLowerCase();
228
+ const output = entry?.rawOutput && typeof entry.rawOutput === 'object' ? entry.rawOutput : {};
229
+ return id.startsWith('ig_')
230
+ || title === 'image generation'
231
+ || title === 'audio generation'
232
+ || String(output.savedPath || '').includes('/generated_images/');
233
+ }
234
+
235
+ function acpTranscriptToolEntry(entry) {
236
+ if (!entry || entry.type !== 'tool') return entry;
237
+ const detail = detailForTool(entry);
238
+ const changes = patchChanges(entry.content).map(change => ({
239
+ path: change.path,
240
+ kind: change.kind,
241
+ added: change.added,
242
+ removed: change.removed,
243
+ ...(entry?._meta?.farming_patch_decisions?.[change.path]
244
+ ? { decision: entry._meta.farming_patch_decisions[change.path] }
245
+ : {}),
246
+ }));
247
+ const patchSummary = diffBlocks(entry.content)
248
+ .map(block => `${diffAction(block)} ${String(block.path || '').trim()}`)
249
+ .join('\n');
250
+ const meta = {};
251
+ if (entry?._meta?.subagent_session_info) {
252
+ meta.subagent_session_info = JSON.parse(JSON.stringify(entry._meta.subagent_session_info));
253
+ }
254
+ if (entry?._meta?.farming_patch_decisions) {
255
+ meta.farming_patch_decisions = JSON.parse(JSON.stringify(entry._meta.farming_patch_decisions));
256
+ }
257
+ return {
258
+ id: String(entry.id || ''),
259
+ type: 'tool',
260
+ title: String(entry.title || ''),
261
+ kind: String(entry.kind || 'other'),
262
+ status: String(entry.status || ''),
263
+ content: transcriptMediaBlocks(entry),
264
+ ...(Object.keys(meta).length > 0 ? { _meta: meta } : {}),
265
+ transcriptDetail: detail.length <= MAX_INLINE_TOOL_DETAIL_CHARS
266
+ ? detail
267
+ : `${detail.slice(0, MAX_INLINE_TOOL_DETAIL_CHARS)}\n\n[Open to load full detail]`,
268
+ transcriptDetailTruncated: detail.length > MAX_INLINE_TOOL_DETAIL_CHARS,
269
+ transcriptPatchSummary: patchSummary,
270
+ transcriptChanges: changes,
271
+ generatedMedia: generatedMediaTool(entry),
272
+ internal: entry.internal === true,
273
+ };
274
+ }
275
+
165
276
  module.exports = {
166
277
  acpToolChanges: entry => patchChanges(entry?.content, { includeDiff: true }),
167
278
  acpToolDetail: detailForTool,
168
279
  acpToolReviewChanges: entry => patchReviewChanges(entry?.content),
280
+ acpTranscriptToolEntry,
169
281
  };
@@ -1,3 +1,5 @@
1
+ const { deriveRuntimeObservation } = require('./runtime-observation');
2
+
1
3
  function agentKindForCommand(command) {
2
4
  const executable = String(command || '')
3
5
  .trim()
@@ -10,157 +12,6 @@ function agentKindForCommand(command) {
10
12
  return executable ? 'agent' : null;
11
13
  }
12
14
 
13
- function currentTerminalText(agent) {
14
- if (!agent) return '';
15
- const previewText = typeof agent.previewText === 'string' ? agent.previewText : '';
16
- if (previewText.trim()) return previewText.toLowerCase();
17
- return String(agent.output || '').slice(-1800).toLowerCase();
18
- }
19
-
20
- function lastIndexOfAny(text, needles) {
21
- return needles.reduce((last, needle) => Math.max(last, text.lastIndexOf(needle)), -1);
22
- }
23
-
24
- function lastCodexIdleFooterIndex(text) {
25
- const matches = Array.from(text.matchAll(/(?:^|\n)\s*(?:gpt|codex)[^\n]*(?:·|•)\s*(?:~|\/)[^\n]*$/gim));
26
- const lastMatch = matches.length > 0 ? matches[matches.length - 1] : undefined;
27
- return lastMatch && typeof lastMatch.index === 'number' ? lastMatch.index : -1;
28
- }
29
-
30
- function codexActiveIndex(text) {
31
- const activeTextIndex = lastIndexOfAny(text, [
32
- 'pursuing goal',
33
- 'esc to interrupt',
34
- 'press esc to interrupt',
35
- 'reconnecting',
36
- '/stop to close',
37
- 'background terminal running',
38
- ]);
39
- const workingIndex = /\bworking\b/.test(text) ? text.lastIndexOf('working') : -1;
40
- const stepMatches = Array.from(text.matchAll(/step\s+\d+\s*\/\s*\d+/g));
41
- const lastStepMatch = stepMatches.length > 0 ? stepMatches[stepMatches.length - 1] : undefined;
42
- const stepIndex = lastStepMatch && typeof lastStepMatch.index === 'number' ? lastStepMatch.index : -1;
43
- return Math.max(activeTextIndex, workingIndex, stepIndex);
44
- }
45
-
46
- function codexKindIndex(text) {
47
- const codexSpecificIndex = lastIndexOfAny(text, [
48
- 'pursuing goal',
49
- 'reconnecting',
50
- '/stop to close',
51
- 'background terminal running',
52
- 'messages to be submitted after next tool call',
53
- 'stream disconnected before completion',
54
- ]);
55
- const workingIndex = /\bworking\b/.test(text) ? text.lastIndexOf('working') : -1;
56
- const stepMatches = Array.from(text.matchAll(/step\s+\d+\s*\/\s*\d+/g));
57
- const lastStepMatch = stepMatches.length > 0 ? stepMatches[stepMatches.length - 1] : undefined;
58
- const stepIndex = lastStepMatch && typeof lastStepMatch.index === 'number' ? lastStepMatch.index : -1;
59
- return Math.max(codexSpecificIndex, workingIndex, stepIndex);
60
- }
61
-
62
- function codexBlockedIndex(text) {
63
- return lastIndexOfAny(text, [
64
- 'goal blocked',
65
- 'input exceeds the context window',
66
- 'please adjust your input and try again',
67
- ]);
68
- }
69
-
70
- function titleWithoutActivityPrefix(title) {
71
- return String(title || '')
72
- .trim()
73
- .replace(/^[\s**✳✱✲✶·•:.\u2800-\u28FF]+/u, '')
74
- .replace(/\s+/g, ' ')
75
- .toLowerCase();
76
- }
77
-
78
- function activeTerminalTitleKind(agent) {
79
- const title = typeof agent.sessionTitle === 'string' ? agent.sessionTitle.trim() : '';
80
- if (/^[\u2800-\u28ff]/u.test(title)) return 'codex';
81
- return null;
82
- }
83
-
84
- function genericTerminalTitleKind(agent) {
85
- const normalized = titleWithoutActivityPrefix(agent && agent.sessionTitle);
86
- if (!normalized) return null;
87
- if (/\bclaude(?:\s+code)?\b/.test(normalized)) return 'claude';
88
- if (/\bcodex\b/.test(normalized)) return 'codex';
89
- if (/^(?:bash|zsh|sh|fish)(?:\s|$)/.test(normalized)) return 'shell';
90
- return null;
91
- }
92
-
93
- function lastMatchIndex(text, pattern) {
94
- const matches = Array.from(text.matchAll(pattern));
95
- const lastMatch = matches.length > 0 ? matches[matches.length - 1] : undefined;
96
- return lastMatch && typeof lastMatch.index === 'number' ? lastMatch.index : -1;
97
- }
98
-
99
- function latestTerminalOutputKind(text) {
100
- const candidates = [];
101
- const codexIndex = Math.max(
102
- lastCodexIdleFooterIndex(text),
103
- codexKindIndex(text),
104
- lastMatchIndex(text, /(?:^|\n)\s*›\s/g)
105
- );
106
- if (codexIndex >= 0) candidates.push({ kind: 'codex', index: codexIndex });
107
-
108
- const claudeIndex = Math.max(
109
- lastIndexOfAny(text, ['claude code']),
110
- lastMatchIndex(text, /(?:thinking|claude)[\s\S]*(?:esc|escape|ctrl\+c|ctrl-c) to interrupt/g)
111
- );
112
- if (claudeIndex >= 0) candidates.push({ kind: 'claude', index: claudeIndex });
113
-
114
- const shellIndex = lastMatchIndex(text, /(?:^|\n)\s*(?:[\w./~:@-]+\s*)?[$%#]\s*$/gm);
115
- if (shellIndex >= 0) candidates.push({ kind: 'shell', index: shellIndex });
116
-
117
- const latest = candidates.sort((a, b) => b.index - a.index)[0];
118
- return latest ? latest.kind : null;
119
- }
120
-
121
- function inferAgentKind(agent) {
122
- if (!agent) return null;
123
- return activeTerminalTitleKind(agent)
124
- || latestTerminalOutputKind(currentTerminalText(agent))
125
- || (agent.terminalStatus && agent.terminalStatus.kind !== 'unknown' ? agent.terminalStatus.kind : null)
126
- || (agent.terminalBusy === true ? 'shell' : null)
127
- || genericTerminalTitleKind(agent)
128
- || agentKindForCommand(agent.command);
129
- }
130
-
131
- function isAgentTerminalBusy(agent) {
132
- if (agent && agent.terminalStatus) {
133
- if (agent.terminalStatus.activity === 'busy') return true;
134
- if (agent.terminalStatus.activity === 'idle' || agent.terminalStatus.activity === 'exited') return false;
135
- }
136
- return agent && agent.terminalBusy === true;
137
- }
138
-
139
- function isCodexRestartBlocking(agent) {
140
- const output = currentTerminalText(agent);
141
- if (!output) return false;
142
- if (output.includes('messages to be submitted after next tool call')) return true;
143
-
144
- const activeIndex = codexActiveIndex(output);
145
- if (activeIndex < 0) return false;
146
-
147
- const blockedIndex = codexBlockedIndex(output);
148
- if (blockedIndex >= activeIndex) return false;
149
-
150
- return lastCodexIdleFooterIndex(output) <= activeIndex;
151
- }
152
-
153
- function isClaudeRestartBlocking(agent) {
154
- const output = currentTerminalText(agent);
155
- return (
156
- output.includes('esc to interrupt') ||
157
- output.includes('escape to interrupt') ||
158
- output.includes('ctrl+c to interrupt') ||
159
- output.includes('ctrl-c to interrupt') ||
160
- output.includes('press esc to interrupt')
161
- );
162
- }
163
-
164
15
  function isRecoverableEngineAgent(agent) {
165
16
  return agent && agent.engineName === 'native';
166
17
  }
@@ -170,13 +21,11 @@ function isRestartBlockingAgent(agent) {
170
21
  if (agent.status === 'pending') return true;
171
22
  if (agent.status !== 'running') return false;
172
23
  if (isRecoverableEngineAgent(agent)) return false;
173
- if (isAgentTerminalBusy(agent)) return true;
174
24
 
175
- const kind = inferAgentKind(agent);
176
- if (kind === 'shell') return false;
177
- if (kind === 'codex') return isCodexRestartBlocking(agent);
178
- if (kind === 'claude') return isClaudeRestartBlocking(agent);
179
- return true;
25
+ const observation = deriveRuntimeObservation(agent);
26
+ if (observation.phase === 'starting' || observation.phase === 'working' || observation.phase === 'waiting') return true;
27
+ if (observation.phase === 'idle' || observation.phase === 'exited') return false;
28
+ return observation.kind !== 'shell';
180
29
  }
181
30
 
182
31
  module.exports = {