remote-codex 0.1.5 → 0.1.7
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/apps/supervisor-api/dist/index.js +7749 -5501
- package/apps/supervisor-web/dist/assets/{highlighted-body-OFNGDK62-D-RjOTTL.js → highlighted-body-OFNGDK62-0cYcfOfd.js} +1 -1
- package/apps/supervisor-web/dist/assets/index-CbIt0KnL.css +32 -0
- package/apps/supervisor-web/dist/assets/index-nH6a8Wwn.js +377 -0
- package/apps/supervisor-web/dist/assets/{xterm-D8iZbRww.js → xterm-DisVWgDR.js} +1 -1
- package/apps/supervisor-web/dist/index.html +2 -2
- package/package.json +5 -1
- package/packages/agent-runtime/src/index.ts +2 -0
- package/packages/agent-runtime/src/registry.ts +44 -0
- package/packages/agent-runtime/src/types.ts +531 -0
- package/packages/codex/src/appServerManager.test.ts +328 -0
- package/packages/codex/src/appServerManager.ts +656 -0
- package/packages/codex/src/historyItems.ts +1185 -0
- package/packages/codex/src/hookHistory.ts +224 -0
- package/packages/codex/src/index.ts +6 -0
- package/packages/codex/src/jsonrpc.test.ts +58 -0
- package/packages/codex/src/jsonrpc.ts +198 -0
- package/packages/codex/src/requestMapper.test.ts +127 -0
- package/packages/codex/src/requestMapper.ts +511 -0
- package/packages/codex/src/runtimeAdapter.ts +692 -0
- package/packages/codex/src/types.ts +403 -0
- package/packages/db/migrations/0014_thread_history_items.sql +12 -0
- package/packages/db/migrations/0015_agent_provider_fields.sql +14 -0
- package/packages/db/migrations/0016_remove_codex_thread_goal_id.sql +46 -0
- package/packages/db/migrations/0017_remove_codex_thread_columns.sql +85 -0
- package/packages/db/src/client.ts +53 -0
- package/packages/db/src/index.ts +5 -0
- package/packages/db/src/migrate.test.ts +36 -0
- package/packages/db/src/migrate.ts +84 -0
- package/packages/db/src/repositories.ts +893 -0
- package/packages/db/src/schema.ts +177 -0
- package/packages/db/src/seed.ts +51 -0
- package/packages/shared/src/index.ts +878 -0
- package/scripts/service-manager.mjs +6 -4
- package/apps/supervisor-web/dist/assets/index-CdG3ogmZ.js +0 -376
- package/apps/supervisor-web/dist/assets/index-QM8NQf3e.css +0 -32
|
@@ -0,0 +1,1185 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CodexTurnRecord,
|
|
3
|
+
CodexTurnItem,
|
|
4
|
+
} from './types';
|
|
5
|
+
import type { AgentTurn } from '../../agent-runtime/src/index';
|
|
6
|
+
import type {
|
|
7
|
+
ThreadHistoryItemDetailDto,
|
|
8
|
+
ThreadHistoryItemDto,
|
|
9
|
+
ThreadTurnDto,
|
|
10
|
+
} from '../../shared/src/index';
|
|
11
|
+
|
|
12
|
+
import { parseCodexHookPromptText } from './hookHistory';
|
|
13
|
+
|
|
14
|
+
const DEFERRED_COMMAND_DETAIL_TITLE = 'Command Output';
|
|
15
|
+
const DEFERRED_TOOL_DETAIL_TITLE = 'Tool Call Details';
|
|
16
|
+
|
|
17
|
+
export type TurnItemOrderSnapshot = Map<string, Map<string, number>>;
|
|
18
|
+
|
|
19
|
+
interface WebSearchSourceRecord {
|
|
20
|
+
title: string | null;
|
|
21
|
+
url: string | null;
|
|
22
|
+
snippet: string | null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function parseUuidV7Timestamp(id: string): string | null {
|
|
26
|
+
const normalized = id.replace(/-/g, '');
|
|
27
|
+
if (!/^[0-9a-f]{32}$/i.test(normalized) || normalized[12]?.toLowerCase() !== '7') {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const millis = Number.parseInt(normalized.slice(0, 12), 16);
|
|
32
|
+
if (!Number.isFinite(millis)) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return new Date(millis).toISOString();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
40
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function stringOrNull(value: unknown) {
|
|
44
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function numberOrNull(value: unknown) {
|
|
48
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (typeof value === 'string') {
|
|
53
|
+
const normalized = value.trim();
|
|
54
|
+
if (!normalized) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const parsed = Number(normalized);
|
|
59
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function stringArray(value: unknown) {
|
|
66
|
+
if (!Array.isArray(value)) {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return value
|
|
71
|
+
.map((entry) => stringOrNull(entry))
|
|
72
|
+
.filter((entry): entry is string => Boolean(entry));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function uniqueStrings(values: Array<string | null | undefined>) {
|
|
76
|
+
return [...new Set(values.filter((value): value is string => Boolean(value?.trim())))];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function normalizeTextLines(text: string) {
|
|
80
|
+
const lines = text.replace(/\r\n/g, '\n').split('\n');
|
|
81
|
+
|
|
82
|
+
while (lines.length > 1 && lines.at(-1)?.trim() === '') {
|
|
83
|
+
lines.pop();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return lines;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function textFromContentEntries(
|
|
90
|
+
content: CodexTurnItem['content'],
|
|
91
|
+
fallback: string | null = null,
|
|
92
|
+
) {
|
|
93
|
+
const text =
|
|
94
|
+
content
|
|
95
|
+
?.map((entry) => {
|
|
96
|
+
if (typeof entry.text === 'string') {
|
|
97
|
+
return entry.text;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return `[${entry.type}]`;
|
|
101
|
+
})
|
|
102
|
+
.join('\n')
|
|
103
|
+
.trim();
|
|
104
|
+
|
|
105
|
+
return text || fallback;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function codexItemText(item: CodexTurnItem, fallback = '') {
|
|
109
|
+
const contentText = textFromContentEntries(item.content);
|
|
110
|
+
const directText = typeof item.text === 'string' && item.text.trim() ? item.text : null;
|
|
111
|
+
|
|
112
|
+
if (
|
|
113
|
+
contentText &&
|
|
114
|
+
(!directText ||
|
|
115
|
+
contentText.includes('\n') ||
|
|
116
|
+
(Array.isArray(item.content) && item.content.length > 1))
|
|
117
|
+
) {
|
|
118
|
+
return contentText;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return directText ?? contentText ?? fallback;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function summarizeCommandText(text: string) {
|
|
125
|
+
const lines = normalizeTextLines(text);
|
|
126
|
+
return lines.find((line) => line.trim().length > 0) ?? lines[0] ?? 'Command output';
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function textFromUnknown(value: unknown): string | null {
|
|
130
|
+
if (typeof value === 'string') {
|
|
131
|
+
return value.trim() ? value : null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (Array.isArray(value)) {
|
|
135
|
+
const parts: string[] = value
|
|
136
|
+
.map((entry) => textFromUnknown(entry))
|
|
137
|
+
.filter((entry): entry is string => Boolean(entry));
|
|
138
|
+
return parts.length > 0 ? parts.join(' ') : null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function safeJsonStringify(value: unknown) {
|
|
145
|
+
try {
|
|
146
|
+
const serialized = JSON.stringify(value, null, 2);
|
|
147
|
+
return serialized && serialized !== 'null' ? serialized : null;
|
|
148
|
+
} catch {
|
|
149
|
+
return String(value);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function summarizeToolCallText(text: string) {
|
|
154
|
+
const lines = normalizeTextLines(text);
|
|
155
|
+
return lines.find((line) => line.trim().length > 0) ?? lines[0] ?? 'Tool call';
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function deferCommandHistoryItem(
|
|
159
|
+
item: ThreadHistoryItemDto & { kind: 'commandExecution' },
|
|
160
|
+
deferredDetails: Map<string, ThreadHistoryItemDetailDto>,
|
|
161
|
+
): ThreadHistoryItemDto {
|
|
162
|
+
const fullText = item.detailText?.trim() || item.text || 'Command output';
|
|
163
|
+
deferredDetails.set(item.id, {
|
|
164
|
+
id: item.id,
|
|
165
|
+
kind: item.kind,
|
|
166
|
+
title: DEFERRED_COMMAND_DETAIL_TITLE,
|
|
167
|
+
text: fullText,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
...item,
|
|
172
|
+
text: summarizeCommandText(fullText),
|
|
173
|
+
detailText: null,
|
|
174
|
+
hasDeferredDetail: true,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function formatCommandHistoryItem(
|
|
179
|
+
item: CodexTurnItem,
|
|
180
|
+
deferredDetails?: Map<string, ThreadHistoryItemDetailDto>,
|
|
181
|
+
): ThreadHistoryItemDto {
|
|
182
|
+
const nestedRecords = [
|
|
183
|
+
item,
|
|
184
|
+
isRecord(item.action) ? item.action : null,
|
|
185
|
+
isRecord(item.result) ? item.result : null,
|
|
186
|
+
].filter((candidate): candidate is Record<string, unknown> => Boolean(candidate));
|
|
187
|
+
const commandText =
|
|
188
|
+
textFromUnknown(valueFromNestedRecords(nestedRecords, ['command', 'cmd', 'argv'])) ??
|
|
189
|
+
stringOrNull(item.text) ??
|
|
190
|
+
'Command output';
|
|
191
|
+
const outputText =
|
|
192
|
+
textFromUnknown(
|
|
193
|
+
valueFromNestedRecords(nestedRecords, [
|
|
194
|
+
'aggregatedOutput',
|
|
195
|
+
'aggregated_output',
|
|
196
|
+
'output',
|
|
197
|
+
'stdout',
|
|
198
|
+
'stderr',
|
|
199
|
+
'text',
|
|
200
|
+
]),
|
|
201
|
+
) ?? null;
|
|
202
|
+
const detailText = [commandText, outputText].filter(Boolean).join('\n\n');
|
|
203
|
+
const historyItem: ThreadHistoryItemDto & { kind: 'commandExecution' } = {
|
|
204
|
+
id: item.id,
|
|
205
|
+
kind: 'commandExecution',
|
|
206
|
+
text: detailText,
|
|
207
|
+
status: item.status ?? null,
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
return deferredDetails
|
|
211
|
+
? deferCommandHistoryItem(historyItem, deferredDetails)
|
|
212
|
+
: historyItem;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function deferToolCallHistoryItem(
|
|
216
|
+
item: ThreadHistoryItemDto & { kind: 'toolCall' },
|
|
217
|
+
deferredDetails: Map<string, ThreadHistoryItemDetailDto>,
|
|
218
|
+
): ThreadHistoryItemDto {
|
|
219
|
+
const fullText = item.detailText?.trim() || item.text || 'Tool call';
|
|
220
|
+
deferredDetails.set(item.id, {
|
|
221
|
+
id: item.id,
|
|
222
|
+
kind: item.kind,
|
|
223
|
+
title: DEFERRED_TOOL_DETAIL_TITLE,
|
|
224
|
+
text: fullText,
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
...item,
|
|
229
|
+
text: summarizeToolCallText(fullText),
|
|
230
|
+
detailText: null,
|
|
231
|
+
hasDeferredDetail: true,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function extractToolCallRecords(item: CodexTurnItem) {
|
|
236
|
+
const action = isRecord(item.action) ? item.action : null;
|
|
237
|
+
const result = isRecord(item.result) ? item.result : null;
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
action,
|
|
241
|
+
result,
|
|
242
|
+
input: action && isRecord(action.input) ? action.input : null,
|
|
243
|
+
output: result && isRecord(result.output) ? result.output : null,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function valueFromNestedRecords(
|
|
248
|
+
records: Array<Record<string, unknown> | null | undefined>,
|
|
249
|
+
keys: string[],
|
|
250
|
+
) {
|
|
251
|
+
for (const record of records) {
|
|
252
|
+
if (!record) {
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
for (const key of keys) {
|
|
257
|
+
const value = record[key];
|
|
258
|
+
if (value !== undefined && value !== null) {
|
|
259
|
+
return value;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function formatToolCallHistoryItem(
|
|
268
|
+
item: CodexTurnItem,
|
|
269
|
+
deferredDetails?: Map<string, ThreadHistoryItemDetailDto>,
|
|
270
|
+
): ThreadHistoryItemDto {
|
|
271
|
+
const { action, result, input, output } = extractToolCallRecords(item);
|
|
272
|
+
const nestedRecords = [item, action, result, input, output];
|
|
273
|
+
const toolName = uniqueStrings([
|
|
274
|
+
stringOrNull(valueFromNestedRecords(nestedRecords, [
|
|
275
|
+
'tool',
|
|
276
|
+
'toolName',
|
|
277
|
+
'tool_name',
|
|
278
|
+
'name',
|
|
279
|
+
'title',
|
|
280
|
+
'functionName',
|
|
281
|
+
'function_name',
|
|
282
|
+
])),
|
|
283
|
+
])[0] ?? null;
|
|
284
|
+
const serverName = uniqueStrings([
|
|
285
|
+
stringOrNull(valueFromNestedRecords(nestedRecords, [
|
|
286
|
+
'server',
|
|
287
|
+
'serverName',
|
|
288
|
+
'server_name',
|
|
289
|
+
'mcpServer',
|
|
290
|
+
'mcp_server',
|
|
291
|
+
'namespace',
|
|
292
|
+
])),
|
|
293
|
+
])[0] ?? null;
|
|
294
|
+
const status = stringOrNull(item.status) ?? stringOrNull(item.phase) ?? null;
|
|
295
|
+
const summaryLine = serverName && toolName
|
|
296
|
+
? `${serverName}/${toolName}`
|
|
297
|
+
: toolName ?? serverName ?? stringOrNull(item.text) ?? item.type;
|
|
298
|
+
|
|
299
|
+
const detailLines = [summaryLine];
|
|
300
|
+
if (status) {
|
|
301
|
+
detailLines.push(`Status: ${status}`);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const text = stringOrNull(item.text);
|
|
305
|
+
if (text && text !== summaryLine) {
|
|
306
|
+
detailLines.push('', text);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const argumentPayload = input ?? action;
|
|
310
|
+
const resultPayload = output ?? result;
|
|
311
|
+
const argumentText = safeJsonStringify(argumentPayload);
|
|
312
|
+
const resultText = safeJsonStringify(resultPayload);
|
|
313
|
+
|
|
314
|
+
if (argumentText) {
|
|
315
|
+
detailLines.push('', 'Arguments', argumentText);
|
|
316
|
+
}
|
|
317
|
+
if (resultText) {
|
|
318
|
+
detailLines.push('', 'Result', resultText);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const historyItem: ThreadHistoryItemDto = {
|
|
322
|
+
id: item.id,
|
|
323
|
+
kind: 'toolCall',
|
|
324
|
+
text: summaryLine,
|
|
325
|
+
previewText: summaryLine,
|
|
326
|
+
detailText: detailLines.join('\n'),
|
|
327
|
+
status,
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
if (
|
|
331
|
+
deferredDetails &&
|
|
332
|
+
(Boolean(argumentText) ||
|
|
333
|
+
Boolean(resultText) ||
|
|
334
|
+
(historyItem.detailText?.length ?? 0) > 240)
|
|
335
|
+
) {
|
|
336
|
+
return deferToolCallHistoryItem(
|
|
337
|
+
historyItem as ThreadHistoryItemDto & { kind: 'toolCall' },
|
|
338
|
+
deferredDetails,
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return historyItem;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export function deferLargeHistoryItemDetails(
|
|
346
|
+
turn: ThreadTurnDto,
|
|
347
|
+
deferredDetails: Map<string, ThreadHistoryItemDetailDto>,
|
|
348
|
+
): ThreadTurnDto {
|
|
349
|
+
return {
|
|
350
|
+
...turn,
|
|
351
|
+
items: turn.items.map((item) =>
|
|
352
|
+
item.kind === 'commandExecution'
|
|
353
|
+
? deferCommandHistoryItem(
|
|
354
|
+
item as ThreadHistoryItemDto & { kind: 'commandExecution' },
|
|
355
|
+
deferredDetails,
|
|
356
|
+
)
|
|
357
|
+
: item.kind === 'toolCall'
|
|
358
|
+
? deferToolCallHistoryItem(
|
|
359
|
+
item as ThreadHistoryItemDto & { kind: 'toolCall' },
|
|
360
|
+
deferredDetails,
|
|
361
|
+
)
|
|
362
|
+
: item,
|
|
363
|
+
),
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export function shouldPersistLiveHistoryItem(item: ThreadHistoryItemDto) {
|
|
368
|
+
return (
|
|
369
|
+
item.kind === 'commandExecution' ||
|
|
370
|
+
item.kind === 'fileChange' ||
|
|
371
|
+
item.kind === 'hook' ||
|
|
372
|
+
item.kind === 'toolCall' ||
|
|
373
|
+
item.kind === 'webSearch'
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export function parseStoredHistoryItem(value: string): ThreadHistoryItemDto | null {
|
|
378
|
+
try {
|
|
379
|
+
const parsed = JSON.parse(value);
|
|
380
|
+
if (
|
|
381
|
+
parsed &&
|
|
382
|
+
typeof parsed === 'object' &&
|
|
383
|
+
typeof parsed.id === 'string' &&
|
|
384
|
+
typeof parsed.kind === 'string' &&
|
|
385
|
+
typeof parsed.text === 'string'
|
|
386
|
+
) {
|
|
387
|
+
return parsed as ThreadHistoryItemDto;
|
|
388
|
+
}
|
|
389
|
+
} catch {
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function hasHistoryItemSequence(item: ThreadHistoryItemDto) {
|
|
397
|
+
return typeof item.sequence === 'number' && Number.isFinite(item.sequence);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function historyItemSequence(item: ThreadHistoryItemDto) {
|
|
401
|
+
return hasHistoryItemSequence(item) ? item.sequence! : Number.POSITIVE_INFINITY;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
export function sortHistoryItemsBySequence<T extends ThreadHistoryItemDto>(items: T[]): T[] {
|
|
405
|
+
if (!items.some(hasHistoryItemSequence)) {
|
|
406
|
+
return items;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return items
|
|
410
|
+
.map((item, index) => ({ item, index }))
|
|
411
|
+
.sort((left, right) => {
|
|
412
|
+
const sequenceDelta =
|
|
413
|
+
historyItemSequence(left.item) - historyItemSequence(right.item);
|
|
414
|
+
return sequenceDelta === 0 ? left.index - right.index : sequenceDelta;
|
|
415
|
+
})
|
|
416
|
+
.map((entry) => entry.item);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function sortTurnItemsByRecordedSequence(items: ThreadHistoryItemDto[]) {
|
|
420
|
+
const leadingItems: ThreadHistoryItemDto[] = [];
|
|
421
|
+
let index = 0;
|
|
422
|
+
|
|
423
|
+
while (
|
|
424
|
+
index < items.length &&
|
|
425
|
+
items[index]?.kind === 'userMessage' &&
|
|
426
|
+
!hasHistoryItemSequence(items[index]!)
|
|
427
|
+
) {
|
|
428
|
+
leadingItems.push(items[index]!);
|
|
429
|
+
index += 1;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return [...leadingItems, ...sortHistoryItemsBySequence(items.slice(index))];
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function mergeHistoryItemsBySequence(
|
|
436
|
+
items: ThreadHistoryItemDto[],
|
|
437
|
+
missingItems: ThreadHistoryItemDto[],
|
|
438
|
+
) {
|
|
439
|
+
if (missingItems.length === 0) {
|
|
440
|
+
return items;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (!missingItems.some(hasHistoryItemSequence)) {
|
|
444
|
+
return [...items, ...missingItems];
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const mergedItems = [...items];
|
|
448
|
+
const orderedMissingItems = sortHistoryItemsBySequence(missingItems);
|
|
449
|
+
for (const missingItem of orderedMissingItems) {
|
|
450
|
+
if (!hasHistoryItemSequence(missingItem)) {
|
|
451
|
+
mergedItems.push(missingItem);
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const firstGreaterIndex = mergedItems.findIndex(
|
|
456
|
+
(item) =>
|
|
457
|
+
hasHistoryItemSequence(item) &&
|
|
458
|
+
historyItemSequence(item) > historyItemSequence(missingItem),
|
|
459
|
+
);
|
|
460
|
+
if (firstGreaterIndex >= 0) {
|
|
461
|
+
mergedItems.splice(firstGreaterIndex, 0, missingItem);
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const lastLowerIndex = mergedItems.findLastIndex(
|
|
466
|
+
(item) =>
|
|
467
|
+
hasHistoryItemSequence(item) &&
|
|
468
|
+
historyItemSequence(item) < historyItemSequence(missingItem),
|
|
469
|
+
);
|
|
470
|
+
if (lastLowerIndex >= 0) {
|
|
471
|
+
mergedItems.splice(lastLowerIndex + 1, 0, missingItem);
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
mergedItems.push(missingItem);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
return mergedItems;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
export function mergePersistedHistoryItemsIntoTurns(
|
|
482
|
+
turns: ThreadTurnDto[],
|
|
483
|
+
persistedItemsByTurnId: Map<string, ThreadHistoryItemDto[]>,
|
|
484
|
+
deferredDetails: Map<string, ThreadHistoryItemDetailDto>,
|
|
485
|
+
): ThreadTurnDto[] {
|
|
486
|
+
if (persistedItemsByTurnId.size === 0) {
|
|
487
|
+
return turns;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
return turns.map((turn) => {
|
|
491
|
+
if (turn.status === 'inProgress') {
|
|
492
|
+
return turn;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const persistedItems = persistedItemsByTurnId.get(turn.id);
|
|
496
|
+
if (!persistedItems || persistedItems.length === 0) {
|
|
497
|
+
return turn;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
let changed = false;
|
|
501
|
+
const persistedItemsById = new Map(persistedItems.map((item) => [item.id, item]));
|
|
502
|
+
const nextItems = turn.items.map((item) => {
|
|
503
|
+
const persistedItem = persistedItemsById.get(item.id);
|
|
504
|
+
if (
|
|
505
|
+
!persistedItem ||
|
|
506
|
+
item.kind !== persistedItem.kind ||
|
|
507
|
+
(persistedItem.kind !== 'commandExecution' && persistedItem.kind !== 'toolCall')
|
|
508
|
+
) {
|
|
509
|
+
return item;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const existingText = item.detailText?.trim() || item.text.trim();
|
|
513
|
+
const persistedText = persistedItem.detailText?.trim() || persistedItem.text.trim();
|
|
514
|
+
if (persistedText.length <= existingText.length) {
|
|
515
|
+
return item;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
changed = true;
|
|
519
|
+
persistedItemsById.delete(item.id);
|
|
520
|
+
return persistedItem.kind === 'commandExecution'
|
|
521
|
+
? deferCommandHistoryItem(
|
|
522
|
+
persistedItem as ThreadHistoryItemDto & { kind: 'commandExecution' },
|
|
523
|
+
deferredDetails,
|
|
524
|
+
)
|
|
525
|
+
: deferToolCallHistoryItem(
|
|
526
|
+
persistedItem as ThreadHistoryItemDto & { kind: 'toolCall' },
|
|
527
|
+
deferredDetails,
|
|
528
|
+
);
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
const existingItemIds = new Set(nextItems.map((item) => item.id));
|
|
532
|
+
const missingItems = [...persistedItemsById.values()]
|
|
533
|
+
.filter((item) => !existingItemIds.has(item.id))
|
|
534
|
+
.map((item) =>
|
|
535
|
+
item.kind === 'commandExecution'
|
|
536
|
+
? deferCommandHistoryItem(
|
|
537
|
+
item as ThreadHistoryItemDto & { kind: 'commandExecution' },
|
|
538
|
+
deferredDetails,
|
|
539
|
+
)
|
|
540
|
+
: item.kind === 'toolCall'
|
|
541
|
+
? deferToolCallHistoryItem(
|
|
542
|
+
item as ThreadHistoryItemDto & { kind: 'toolCall' },
|
|
543
|
+
deferredDetails,
|
|
544
|
+
)
|
|
545
|
+
: item,
|
|
546
|
+
);
|
|
547
|
+
if (missingItems.length === 0 && !changed) {
|
|
548
|
+
return turn;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
return {
|
|
552
|
+
...turn,
|
|
553
|
+
items: mergeHistoryItemsBySequence(nextItems, missingItems),
|
|
554
|
+
};
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function extractWebSearchQueries(item: CodexTurnItem) {
|
|
559
|
+
const action = isRecord(item.action) ? item.action : null;
|
|
560
|
+
const result = isRecord(item.result) ? item.result : null;
|
|
561
|
+
|
|
562
|
+
return uniqueStrings([
|
|
563
|
+
stringOrNull(item.query),
|
|
564
|
+
...stringArray(item.queries),
|
|
565
|
+
action ? stringOrNull(action.query) : null,
|
|
566
|
+
...(action ? stringArray(action.queries) : []),
|
|
567
|
+
action && isRecord(action.input) ? stringOrNull(action.input.query) : null,
|
|
568
|
+
result ? stringOrNull(result.query) : null,
|
|
569
|
+
...(result ? stringArray(result.queries) : []),
|
|
570
|
+
]);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function normalizeWebSearchSource(value: unknown): WebSearchSourceRecord | null {
|
|
574
|
+
if (!isRecord(value)) {
|
|
575
|
+
return null;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
const title = stringOrNull(value.title) ?? stringOrNull(value.name);
|
|
579
|
+
const url = stringOrNull(value.url) ?? stringOrNull(value.link);
|
|
580
|
+
const snippet =
|
|
581
|
+
stringOrNull(value.snippet) ??
|
|
582
|
+
stringOrNull(value.description) ??
|
|
583
|
+
stringOrNull(value.text);
|
|
584
|
+
|
|
585
|
+
if (!title && !url && !snippet) {
|
|
586
|
+
return null;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
return { title, url, snippet };
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function extractWebSearchSources(item: CodexTurnItem) {
|
|
593
|
+
const action = isRecord(item.action) ? item.action : null;
|
|
594
|
+
const result = isRecord(item.result) ? item.result : null;
|
|
595
|
+
|
|
596
|
+
const candidates: unknown[] = [
|
|
597
|
+
item.sources,
|
|
598
|
+
action?.sources,
|
|
599
|
+
result?.sources,
|
|
600
|
+
result?.results,
|
|
601
|
+
action?.results,
|
|
602
|
+
item.results,
|
|
603
|
+
item.searchResults,
|
|
604
|
+
item.webResults,
|
|
605
|
+
];
|
|
606
|
+
|
|
607
|
+
const sources: WebSearchSourceRecord[] = [];
|
|
608
|
+
|
|
609
|
+
for (const candidate of candidates) {
|
|
610
|
+
if (!Array.isArray(candidate)) {
|
|
611
|
+
continue;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
for (const entry of candidate) {
|
|
615
|
+
const normalized = normalizeWebSearchSource(entry);
|
|
616
|
+
if (normalized) {
|
|
617
|
+
sources.push(normalized);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
return sources.filter((source, index, allSources) => {
|
|
623
|
+
return (
|
|
624
|
+
index ===
|
|
625
|
+
allSources.findIndex(
|
|
626
|
+
(entry) =>
|
|
627
|
+
entry.title === source.title &&
|
|
628
|
+
entry.url === source.url &&
|
|
629
|
+
entry.snippet === source.snippet,
|
|
630
|
+
)
|
|
631
|
+
);
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function stringifyPayload(value: unknown) {
|
|
636
|
+
try {
|
|
637
|
+
return JSON.stringify(value, null, 2);
|
|
638
|
+
} catch {
|
|
639
|
+
return null;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function extractImageAssetPath(item: CodexTurnItem) {
|
|
644
|
+
const candidates: unknown[] = [
|
|
645
|
+
item.path,
|
|
646
|
+
item.imagePath,
|
|
647
|
+
item.filePath,
|
|
648
|
+
isRecord(item.action) ? item.action.path : null,
|
|
649
|
+
isRecord(item.action) ? item.action.imagePath : null,
|
|
650
|
+
isRecord(item.result) ? item.result.path : null,
|
|
651
|
+
isRecord(item.result) ? item.result.imagePath : null,
|
|
652
|
+
];
|
|
653
|
+
|
|
654
|
+
for (const candidate of candidates) {
|
|
655
|
+
const normalized = stringOrNull(candidate);
|
|
656
|
+
if (normalized) {
|
|
657
|
+
return normalized;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
return null;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function formatImageHistoryItem(item: CodexTurnItem): ThreadHistoryItemDto {
|
|
665
|
+
const assetPath = extractImageAssetPath(item);
|
|
666
|
+
const text =
|
|
667
|
+
stringOrNull(item.text) ??
|
|
668
|
+
assetPath ??
|
|
669
|
+
'Image view';
|
|
670
|
+
|
|
671
|
+
return {
|
|
672
|
+
id: item.id,
|
|
673
|
+
kind: 'image',
|
|
674
|
+
text,
|
|
675
|
+
previewText: text,
|
|
676
|
+
detailText: assetPath,
|
|
677
|
+
assetPath,
|
|
678
|
+
status: item.status ?? null,
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function formatWebSearchHistoryItem(item: CodexTurnItem): ThreadHistoryItemDto {
|
|
683
|
+
const queries = extractWebSearchQueries(item);
|
|
684
|
+
const sources = extractWebSearchSources(item);
|
|
685
|
+
const supplementalText = stringOrNull(item.text);
|
|
686
|
+
const previewText =
|
|
687
|
+
queries.length > 0
|
|
688
|
+
? queries.length <= 2
|
|
689
|
+
? queries.join('\n')
|
|
690
|
+
: `${queries[0]}\n${queries[1]}\n+${queries.length - 2} more queries`
|
|
691
|
+
: supplementalText ?? 'Web search';
|
|
692
|
+
|
|
693
|
+
const detailLines: string[] = [];
|
|
694
|
+
|
|
695
|
+
if (queries.length > 0) {
|
|
696
|
+
detailLines.push(queries.length === 1 ? 'Search query' : 'Search queries', '');
|
|
697
|
+
detailLines.push(...queries.map((query) => `- ${query}`), '');
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
if (sources.length > 0) {
|
|
701
|
+
detailLines.push('Sources', '');
|
|
702
|
+
for (const source of sources) {
|
|
703
|
+
detailLines.push(`- ${source.title ?? 'Untitled source'}`);
|
|
704
|
+
if (source.url) {
|
|
705
|
+
detailLines.push(` ${source.url}`);
|
|
706
|
+
}
|
|
707
|
+
if (source.snippet) {
|
|
708
|
+
detailLines.push(` ${source.snippet}`);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
detailLines.push('');
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
if (supplementalText && !queries.includes(supplementalText)) {
|
|
715
|
+
detailLines.push('Additional text', '', supplementalText, '');
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
if (sources.length === 0) {
|
|
719
|
+
const rawPayload = stringifyPayload(item);
|
|
720
|
+
if (rawPayload) {
|
|
721
|
+
detailLines.push('Raw payload', '', rawPayload, '');
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
return {
|
|
726
|
+
id: item.id,
|
|
727
|
+
kind: 'webSearch',
|
|
728
|
+
text: previewText,
|
|
729
|
+
previewText,
|
|
730
|
+
detailText: detailLines.join('\n').trim() || null,
|
|
731
|
+
status: item.status ?? null,
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
function isRunningItemStatus(status: string | null | undefined) {
|
|
736
|
+
if (!status) {
|
|
737
|
+
return false;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
const normalized = status.toLowerCase();
|
|
741
|
+
return (
|
|
742
|
+
normalized.includes('running') ||
|
|
743
|
+
normalized.includes('inprogress') ||
|
|
744
|
+
normalized.includes('in_progress') ||
|
|
745
|
+
normalized.includes('compacting')
|
|
746
|
+
);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
function formatContextCompactionHistoryItem(item: CodexTurnItem): ThreadHistoryItemDto {
|
|
750
|
+
const rawText =
|
|
751
|
+
stringOrNull(item.text) ??
|
|
752
|
+
(Array.isArray(item.summary) ? item.summary.filter(Boolean).join('\n') : null);
|
|
753
|
+
const status = stringOrNull(item.status) ?? stringOrNull(item.phase) ?? null;
|
|
754
|
+
const previewText = isRunningItemStatus(status)
|
|
755
|
+
? 'Compacting context'
|
|
756
|
+
: 'Context compacted';
|
|
757
|
+
const detailText = rawText && rawText !== previewText ? rawText : null;
|
|
758
|
+
|
|
759
|
+
return {
|
|
760
|
+
id: item.id,
|
|
761
|
+
kind: 'contextCompaction',
|
|
762
|
+
text: previewText,
|
|
763
|
+
previewText,
|
|
764
|
+
detailText,
|
|
765
|
+
status,
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
function countUnifiedDiffStats(diffText: string) {
|
|
770
|
+
let additions = 0;
|
|
771
|
+
let deletions = 0;
|
|
772
|
+
|
|
773
|
+
for (const line of diffText.replace(/\r\n/g, '\n').split('\n')) {
|
|
774
|
+
if (line.startsWith('+++') || line.startsWith('---')) {
|
|
775
|
+
continue;
|
|
776
|
+
}
|
|
777
|
+
if (line.startsWith('+')) {
|
|
778
|
+
additions += 1;
|
|
779
|
+
continue;
|
|
780
|
+
}
|
|
781
|
+
if (line.startsWith('-')) {
|
|
782
|
+
deletions += 1;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
return { additions, deletions };
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function extractPathFromDiffText(diffText: string) {
|
|
790
|
+
for (const line of diffText.replace(/\r\n/g, '\n').split('\n')) {
|
|
791
|
+
if (!line.startsWith('+++ ')) {
|
|
792
|
+
continue;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
const candidate = line.slice(4).trim();
|
|
796
|
+
if (!candidate || candidate === '/dev/null') {
|
|
797
|
+
continue;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
return candidate.replace(/^b\//, '');
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
return null;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function extractFileChangeEntries(item: CodexTurnItem) {
|
|
807
|
+
const candidateArrays: unknown[] = [
|
|
808
|
+
item.changes,
|
|
809
|
+
item.files,
|
|
810
|
+
isRecord(item.result) ? item.result.changes : null,
|
|
811
|
+
isRecord(item.result) ? item.result.files : null,
|
|
812
|
+
isRecord(item.action) ? item.action.changes : null,
|
|
813
|
+
isRecord(item.action) ? item.action.files : null,
|
|
814
|
+
];
|
|
815
|
+
|
|
816
|
+
const normalizedEntries = new Map<
|
|
817
|
+
string,
|
|
818
|
+
{ path: string | null; additions: number; deletions: number }
|
|
819
|
+
>();
|
|
820
|
+
|
|
821
|
+
function valueFromRecords(
|
|
822
|
+
records: Array<Record<string, unknown>>,
|
|
823
|
+
keys: string[],
|
|
824
|
+
) {
|
|
825
|
+
for (const record of records) {
|
|
826
|
+
for (const key of keys) {
|
|
827
|
+
const value = record[key];
|
|
828
|
+
if (value !== undefined && value !== null) {
|
|
829
|
+
return value;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
return null;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function normalizeEntry(entry: unknown) {
|
|
838
|
+
if (typeof entry === 'string') {
|
|
839
|
+
return {
|
|
840
|
+
path: entry.trim() || null,
|
|
841
|
+
additions: 0,
|
|
842
|
+
deletions: 0,
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
if (!isRecord(entry)) {
|
|
847
|
+
return null;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
const nestedRecords = [
|
|
851
|
+
entry,
|
|
852
|
+
isRecord(entry.result) ? entry.result : null,
|
|
853
|
+
isRecord(entry.action) ? entry.action : null,
|
|
854
|
+
isRecord(entry.stats) ? entry.stats : null,
|
|
855
|
+
isRecord(entry.summary) ? entry.summary : null,
|
|
856
|
+
isRecord(entry.diff) ? entry.diff : null,
|
|
857
|
+
].filter((candidate): candidate is Record<string, unknown> => Boolean(candidate));
|
|
858
|
+
|
|
859
|
+
const path = uniqueStrings([
|
|
860
|
+
stringOrNull(valueFromRecords(nestedRecords, ['path', 'filePath', 'targetPath'])),
|
|
861
|
+
stringOrNull(
|
|
862
|
+
valueFromRecords(nestedRecords, [
|
|
863
|
+
'relativePath',
|
|
864
|
+
'relative_path',
|
|
865
|
+
'filename',
|
|
866
|
+
'file',
|
|
867
|
+
'newPath',
|
|
868
|
+
'new_path',
|
|
869
|
+
'oldPath',
|
|
870
|
+
'old_path',
|
|
871
|
+
]),
|
|
872
|
+
),
|
|
873
|
+
])[0] ?? null;
|
|
874
|
+
|
|
875
|
+
const explicitAdditions =
|
|
876
|
+
numberOrNull(
|
|
877
|
+
valueFromRecords(nestedRecords, [
|
|
878
|
+
'additions',
|
|
879
|
+
'added',
|
|
880
|
+
'insertions',
|
|
881
|
+
'linesAdded',
|
|
882
|
+
'lines_added',
|
|
883
|
+
'addedLines',
|
|
884
|
+
'added_lines',
|
|
885
|
+
'numAdded',
|
|
886
|
+
'num_added',
|
|
887
|
+
]),
|
|
888
|
+
) ?? 0;
|
|
889
|
+
const explicitDeletions =
|
|
890
|
+
numberOrNull(
|
|
891
|
+
valueFromRecords(nestedRecords, [
|
|
892
|
+
'deletions',
|
|
893
|
+
'removed',
|
|
894
|
+
'deleted',
|
|
895
|
+
'linesRemoved',
|
|
896
|
+
'lines_removed',
|
|
897
|
+
'removedLines',
|
|
898
|
+
'removed_lines',
|
|
899
|
+
'numRemoved',
|
|
900
|
+
'num_removed',
|
|
901
|
+
]),
|
|
902
|
+
) ?? 0;
|
|
903
|
+
const diffText = stringOrNull(
|
|
904
|
+
valueFromRecords(nestedRecords, ['diff', 'patch', 'unifiedDiff', 'unified_diff']),
|
|
905
|
+
);
|
|
906
|
+
const diffStats =
|
|
907
|
+
explicitAdditions === 0 && explicitDeletions === 0 && diffText
|
|
908
|
+
? countUnifiedDiffStats(diffText)
|
|
909
|
+
: null;
|
|
910
|
+
const additions = explicitAdditions || diffStats?.additions || 0;
|
|
911
|
+
const deletions = explicitDeletions || diffStats?.deletions || 0;
|
|
912
|
+
const normalizedPath = path ?? (diffText ? extractPathFromDiffText(diffText) : null);
|
|
913
|
+
|
|
914
|
+
if (!normalizedPath && additions === 0 && deletions === 0) {
|
|
915
|
+
return null;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
return {
|
|
919
|
+
path: normalizedPath,
|
|
920
|
+
additions,
|
|
921
|
+
deletions,
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
for (const candidateArray of candidateArrays) {
|
|
926
|
+
if (!Array.isArray(candidateArray)) {
|
|
927
|
+
continue;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
for (const entry of candidateArray) {
|
|
931
|
+
const normalized = normalizeEntry(entry);
|
|
932
|
+
if (!normalized) {
|
|
933
|
+
continue;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
const key = normalized.path ?? `unknown:${normalizedEntries.size}`;
|
|
937
|
+
const current = normalizedEntries.get(key);
|
|
938
|
+
if (current) {
|
|
939
|
+
current.additions += normalized.additions;
|
|
940
|
+
current.deletions += normalized.deletions;
|
|
941
|
+
continue;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
normalizedEntries.set(key, normalized);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
return [...normalizedEntries.values()];
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
function formatFileChangeHistoryItem(item: CodexTurnItem): ThreadHistoryItemDto {
|
|
952
|
+
const entries = extractFileChangeEntries(item);
|
|
953
|
+
const fallbackText = stringOrNull(item.text) ?? 'File changes applied.';
|
|
954
|
+
|
|
955
|
+
if (entries.length === 0) {
|
|
956
|
+
return {
|
|
957
|
+
id: item.id,
|
|
958
|
+
kind: 'fileChange',
|
|
959
|
+
text: fallbackText,
|
|
960
|
+
previewText: fallbackText,
|
|
961
|
+
status: item.status ?? null,
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
const additions = entries.reduce((sum, entry) => sum + entry.additions, 0);
|
|
966
|
+
const deletions = entries.reduce((sum, entry) => sum + entry.deletions, 0);
|
|
967
|
+
const summaryParts = [
|
|
968
|
+
`${entries.length} ${entries.length === 1 ? 'file' : 'files'} changed`,
|
|
969
|
+
];
|
|
970
|
+
if (additions > 0) {
|
|
971
|
+
summaryParts.push(`+${additions}`);
|
|
972
|
+
}
|
|
973
|
+
if (deletions > 0) {
|
|
974
|
+
summaryParts.push(`-${deletions}`);
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
const previewText = summaryParts.join(' · ');
|
|
978
|
+
const fileNames = entries
|
|
979
|
+
.map((entry) => entry.path)
|
|
980
|
+
.filter((entry): entry is string => Boolean(entry));
|
|
981
|
+
const primaryFileName = fileNames[0] ?? previewText;
|
|
982
|
+
const compactPathText =
|
|
983
|
+
fileNames.length === 0
|
|
984
|
+
? previewText
|
|
985
|
+
: fileNames.length === 1
|
|
986
|
+
? primaryFileName
|
|
987
|
+
: `${primaryFileName}, +${fileNames.length - 1} more`;
|
|
988
|
+
const detailLines = entries.map((entry) => {
|
|
989
|
+
const counts: string[] = [];
|
|
990
|
+
if (entry.additions > 0) {
|
|
991
|
+
counts.push(`+${entry.additions}`);
|
|
992
|
+
}
|
|
993
|
+
if (entry.deletions > 0) {
|
|
994
|
+
counts.push(`-${entry.deletions}`);
|
|
995
|
+
}
|
|
996
|
+
return `- ${entry.path ?? 'Unknown file'}${counts.length > 0 ? ` (${counts.join(' ')})` : ''}`;
|
|
997
|
+
});
|
|
998
|
+
|
|
999
|
+
if (fallbackText !== 'File changes applied.' && fallbackText !== previewText) {
|
|
1000
|
+
detailLines.push('', fallbackText);
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
return {
|
|
1004
|
+
id: item.id,
|
|
1005
|
+
kind: 'fileChange',
|
|
1006
|
+
text: compactPathText,
|
|
1007
|
+
previewText,
|
|
1008
|
+
detailText: detailLines.join('\n'),
|
|
1009
|
+
status: item.status ?? null,
|
|
1010
|
+
changedFiles: entries.length,
|
|
1011
|
+
addedLines: additions,
|
|
1012
|
+
removedLines: deletions,
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
function itemToHistoryItem(
|
|
1017
|
+
item: CodexTurnItem,
|
|
1018
|
+
deferredDetails?: Map<string, ThreadHistoryItemDetailDto>,
|
|
1019
|
+
): ThreadHistoryItemDto {
|
|
1020
|
+
const hookPrompt = parseCodexHookPromptText(codexItemText(item));
|
|
1021
|
+
if (hookPrompt) {
|
|
1022
|
+
return {
|
|
1023
|
+
id: `hook-prompt:${hookPrompt.hookRunId ?? item.id}`,
|
|
1024
|
+
kind: 'hook',
|
|
1025
|
+
text: `${hookPrompt.eventLabel} hook`,
|
|
1026
|
+
previewText: hookPrompt.output || `${hookPrompt.eventLabel} hook`,
|
|
1027
|
+
detailText: hookPrompt.output || null,
|
|
1028
|
+
status: 'Completed',
|
|
1029
|
+
hookEventName: hookPrompt.eventName,
|
|
1030
|
+
hookEventLabel: hookPrompt.eventLabel,
|
|
1031
|
+
hookHandlerType: 'command',
|
|
1032
|
+
hookScope: 'turn',
|
|
1033
|
+
hookSource: hookPrompt.sourcePath ? 'project' : null,
|
|
1034
|
+
hookSourcePath: hookPrompt.sourcePath,
|
|
1035
|
+
hookStatusMessage: null,
|
|
1036
|
+
hookOutputEntries: hookPrompt.outputEntries,
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
switch (item.type) {
|
|
1041
|
+
case 'userMessage':
|
|
1042
|
+
return {
|
|
1043
|
+
id: item.id,
|
|
1044
|
+
kind: 'userMessage',
|
|
1045
|
+
text: codexItemText(item),
|
|
1046
|
+
};
|
|
1047
|
+
case 'agentMessage':
|
|
1048
|
+
return {
|
|
1049
|
+
id: item.id,
|
|
1050
|
+
kind: 'agentMessage',
|
|
1051
|
+
text: codexItemText(item),
|
|
1052
|
+
};
|
|
1053
|
+
case 'text':
|
|
1054
|
+
return {
|
|
1055
|
+
id: item.id,
|
|
1056
|
+
kind: 'agentMessage',
|
|
1057
|
+
text: codexItemText(item),
|
|
1058
|
+
};
|
|
1059
|
+
case 'plan':
|
|
1060
|
+
return {
|
|
1061
|
+
id: item.id,
|
|
1062
|
+
kind: 'plan',
|
|
1063
|
+
text: codexItemText(item),
|
|
1064
|
+
};
|
|
1065
|
+
case 'contextCompaction':
|
|
1066
|
+
case 'context_compaction':
|
|
1067
|
+
return formatContextCompactionHistoryItem(item);
|
|
1068
|
+
case 'reasoning':
|
|
1069
|
+
return {
|
|
1070
|
+
id: item.id,
|
|
1071
|
+
kind: 'reasoning',
|
|
1072
|
+
text: [item.summary?.join('\n') ?? '', item.text ?? ''].filter(Boolean).join('\n\n'),
|
|
1073
|
+
};
|
|
1074
|
+
case 'commandExecution':
|
|
1075
|
+
return formatCommandHistoryItem(item, deferredDetails);
|
|
1076
|
+
case 'webSearch':
|
|
1077
|
+
case 'web_search':
|
|
1078
|
+
case 'webSearchCall':
|
|
1079
|
+
case 'web_search_call':
|
|
1080
|
+
return formatWebSearchHistoryItem(item);
|
|
1081
|
+
case 'imageView':
|
|
1082
|
+
case 'image_view':
|
|
1083
|
+
case 'viewImage':
|
|
1084
|
+
case 'view_image':
|
|
1085
|
+
return formatImageHistoryItem(item);
|
|
1086
|
+
case 'fileChange':
|
|
1087
|
+
return formatFileChangeHistoryItem(item);
|
|
1088
|
+
case 'mcpToolCall':
|
|
1089
|
+
case 'dynamicToolCall':
|
|
1090
|
+
case 'collabAgentToolCall':
|
|
1091
|
+
return formatToolCallHistoryItem(item, deferredDetails);
|
|
1092
|
+
default:
|
|
1093
|
+
return {
|
|
1094
|
+
id: item.id,
|
|
1095
|
+
kind: 'other',
|
|
1096
|
+
text: codexItemText(item, item.type),
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
export function liveCodexItemToHistoryItem(
|
|
1102
|
+
item: CodexTurnItem,
|
|
1103
|
+
phase: 'started' | 'completed',
|
|
1104
|
+
): ThreadHistoryItemDto | null {
|
|
1105
|
+
const historyItem = itemToHistoryItem(item);
|
|
1106
|
+
|
|
1107
|
+
if (
|
|
1108
|
+
historyItem.kind !== 'commandExecution' &&
|
|
1109
|
+
historyItem.kind !== 'toolCall' &&
|
|
1110
|
+
historyItem.kind !== 'fileChange' &&
|
|
1111
|
+
historyItem.kind !== 'webSearch'
|
|
1112
|
+
) {
|
|
1113
|
+
return null;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
return {
|
|
1117
|
+
...historyItem,
|
|
1118
|
+
status:
|
|
1119
|
+
historyItem.status ??
|
|
1120
|
+
(phase === 'started' ? 'running' : 'completed'),
|
|
1121
|
+
};
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
export function agentTurnToThreadTurnDto(
|
|
1125
|
+
turn: AgentTurn,
|
|
1126
|
+
deferredDetails?: Map<string, ThreadHistoryItemDetailDto>,
|
|
1127
|
+
): ThreadTurnDto {
|
|
1128
|
+
const baseTurn: ThreadTurnDto = {
|
|
1129
|
+
id: turn.providerTurnId,
|
|
1130
|
+
startedAt: parseUuidV7Timestamp(turn.providerTurnId),
|
|
1131
|
+
status: turn.status,
|
|
1132
|
+
error: turn.error?.message ?? null,
|
|
1133
|
+
items: turn.items,
|
|
1134
|
+
};
|
|
1135
|
+
|
|
1136
|
+
return deferredDetails ? deferLargeHistoryItemDetails(baseTurn, deferredDetails) : baseTurn;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
export function codexTurnToAgentTurn(turn: CodexTurnRecord): AgentTurn {
|
|
1140
|
+
return {
|
|
1141
|
+
providerTurnId: turn.id,
|
|
1142
|
+
rawTurnId: turn.id,
|
|
1143
|
+
status: turn.status,
|
|
1144
|
+
error: turn.error,
|
|
1145
|
+
items: turn.items.map((item) => itemToHistoryItem(item)),
|
|
1146
|
+
rawTurn: turn,
|
|
1147
|
+
};
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
function applyRecordedTurnItemOrder(
|
|
1151
|
+
turn: ThreadTurnDto,
|
|
1152
|
+
turnItemOrder: TurnItemOrderSnapshot,
|
|
1153
|
+
): ThreadTurnDto {
|
|
1154
|
+
const itemOrder = turnItemOrder.get(turn.id);
|
|
1155
|
+
if (!itemOrder || itemOrder.size === 0) {
|
|
1156
|
+
return turn;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
let changed = false;
|
|
1160
|
+
const items = turn.items.map((item) => {
|
|
1161
|
+
const sequence = itemOrder.get(item.id);
|
|
1162
|
+
if (sequence === undefined || item.sequence === sequence) {
|
|
1163
|
+
return item;
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
changed = true;
|
|
1167
|
+
return {
|
|
1168
|
+
...item,
|
|
1169
|
+
sequence,
|
|
1170
|
+
};
|
|
1171
|
+
});
|
|
1172
|
+
|
|
1173
|
+
return changed ? { ...turn, items: sortTurnItemsByRecordedSequence(items) } : turn;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
export function applyRecordedTurnItemOrders(
|
|
1177
|
+
turns: ThreadTurnDto[],
|
|
1178
|
+
turnItemOrder: TurnItemOrderSnapshot,
|
|
1179
|
+
): ThreadTurnDto[] {
|
|
1180
|
+
if (turnItemOrder.size === 0) {
|
|
1181
|
+
return turns;
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
return turns.map((turn) => applyRecordedTurnItemOrder(turn, turnItemOrder));
|
|
1185
|
+
}
|