farming-code 2.2.8 → 2.2.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -23
- package/README.zh_cn.md +36 -23
- package/THIRD_PARTY_NOTICES.md +11 -2
- package/backend/acp-checkpoint-store.js +198 -0
- package/backend/acp-runtime.js +306 -83
- package/backend/acp-session-state.js +202 -6
- package/backend/acp-transcript.js +112 -0
- package/backend/agent-activity.js +6 -157
- package/backend/agent-manager.js +1482 -730
- package/backend/agent-provider-session.js +17 -242
- package/backend/agent-runtime-binding.js +219 -0
- package/backend/agent-session-history.js +66 -1
- package/backend/auth.js +79 -6
- package/backend/codex-models.js +81 -84
- package/backend/codex-session-archive.js +45 -0
- package/backend/codex-terminal-profile.js +500 -0
- package/backend/codex-transcript-sanitizer.js +12 -0
- package/backend/codex-transcript.js +230 -8
- package/backend/config-manager.js +30 -0
- package/backend/control-api.js +192 -17
- package/backend/farming-net-pass.js +285 -0
- package/backend/farming-net-registry.js +112 -0
- package/backend/farming-net-server.js +298 -0
- package/backend/farming-session-store.js +5 -13
- package/backend/git-worktree-info.js +181 -0
- package/backend/local-session-engine.js +411 -186
- package/backend/main-page-session.js +5 -2
- package/backend/native-pty-controller-generation.js +106 -0
- package/backend/native-pty-host-client.js +275 -7
- package/backend/native-pty-host-identity.js +86 -0
- package/backend/native-pty-host.js +813 -114
- package/backend/native-session-engine.js +100 -28
- package/backend/packaged-node-pty.js +22 -2
- package/backend/provider-adapters.js +253 -0
- package/backend/provider-session-service.js +241 -0
- package/backend/runtime-observation.js +81 -0
- package/backend/server.js +294 -86
- package/backend/session-engine-bridge.js +21 -2
- package/backend/session-engine-router.js +1 -1
- package/backend/session-engine.js +1 -1
- package/backend/session-stream-protocol.js +185 -0
- package/backend/storage-layout.js +55 -0
- package/backend/terminal-attach-checkpoint.js +74 -0
- package/backend/terminal-exit-quiescence.js +39 -0
- package/backend/terminal-reducer-flow-control.js +97 -0
- package/backend/terminal-screen-state.js +11 -2
- package/backend/terminal-screen-worker-pool.js +59 -6
- package/backend/terminal-screen-worker-thread.js +97 -57
- package/backend/terminal-screen-worker.js +133 -51
- package/backend/terminal-state-serialization.js +127 -0
- package/backend/terminal-status.js +23 -4
- package/backend/usage-monitor.js +81 -4
- package/backend/workspace-directory.js +152 -0
- package/backend/workspace-file-router.js +182 -76
- package/backend/workspace-file-service.js +319 -4
- package/backend/workspace-root-registry.js +164 -0
- package/dist/assets/App-CHYpgECZ.js +208 -0
- package/dist/assets/{FileEditorMarkdownPreview-elKWc8Im.js → FileEditorMarkdownPreview--Cri80nQ.js} +92 -92
- package/dist/assets/FileEditorPane-DGJlEKdf.js +2 -0
- package/dist/assets/IconGlyphs-Cc14sepw.js +1 -0
- package/dist/assets/ProjectFilesSection-Cz1wG4NT.js +12 -0
- package/dist/assets/{ReviewPage-BaXu1ZdX.js → ReviewPage-LonRc6Fz.js} +1 -1
- package/dist/assets/code-dark-B1pAtO_P.css +1 -0
- package/dist/assets/file-icons-Bw2qd5iT.js +1 -0
- package/dist/assets/{index-BrbljRqn.js → index-miO2r9IZ.js} +3 -3
- package/dist/assets/main-DNe0jPw-.css +1 -0
- package/dist/assets/workspace-editor-model-BQol4qbA.js +1 -0
- package/dist/assets/workspace-editor-monaco-5gFbeDPF.js +1 -0
- package/dist/assets/workspace-editor-monaco-xPv8JzHH.js +4 -0
- package/dist/assets/workspace-view-state-DvYG_9PH.js +7 -0
- package/dist/assets/workspace-working-copy-D8-s_Sgh.js +1 -0
- package/dist/index.html +1 -1
- package/frontend/farming-net/app.css +625 -0
- package/frontend/farming-net/app.js +268 -0
- package/frontend/farming-net/index.html +86 -0
- package/frontend/reading-anchor.js +198 -0
- package/frontend/session-bridge.js +12 -3
- package/frontend/session-modal-bridge.js +5 -12
- package/frontend/skins/crt/app.js +1976 -793
- package/frontend/skins/crt/index.html +313 -23
- package/frontend/skins/crt/styles/billing.css +294 -223
- package/frontend/skins/crt/styles/monochrome-green.css +7 -2
- package/frontend/terminal-replay.js +372 -0
- package/package.json +10 -3
- package/shared/browser-protocol.d.ts +5 -0
- package/shared/browser-protocol.js +130 -0
- package/dist/assets/App-8dYAM6ql.js +0 -124
- package/dist/assets/FileEditorPane-RWiFD2cq.js +0 -5
- package/dist/assets/IconGlyphs-DfL0EBnj.js +0 -1
- package/dist/assets/ProjectFilesSection-Q4PDsWmM.js +0 -12
- package/dist/assets/code-dark-CDkOQAtK.css +0 -1
- package/dist/assets/file-icons-EFUGSSwf.js +0 -1
- package/dist/assets/main-D073SnW4.css +0 -1
- package/dist/assets/qoder-C9LmmOSf.svg +0 -1
- package/dist/assets/qoder-Cf9gl0Y5.svg +0 -1
- package/dist/assets/qoder-gHCinseV.svg +0 -1
- package/dist/assets/workspace-view-state-CTyDzk2D.js +0 -1
- package/dist/assets/zsh-CLpveKlF.svg +0 -1
- package/dist/assets/zsh-FxSpMPbz.svg +0 -1
- /package/dist/assets/{api-D1lyBYIQ.js → api-D8nyOEbz.js} +0 -0
- /package/dist/assets/{core-ZlAPicox.js → core-D0LFJkDt.js} +0 -0
- /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
|
-
|
|
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
|
-
&&
|
|
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 =
|
|
330
|
-
|
|
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
|
|
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
|
|
176
|
-
if (
|
|
177
|
-
if (
|
|
178
|
-
|
|
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 = {
|