@spexcode/transcript 0.7.0-next.1 → 0.7.0-next.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/dist/parsers.d.ts +5 -0
- package/dist/parsers.js +213 -24
- package/dist/readers.d.ts +1 -1
- package/dist/readers.js +7 -3
- package/dist/turns.d.ts +1 -0
- package/package.json +1 -1
package/dist/parsers.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export type MutableTool = {
|
|
|
8
8
|
output?: string;
|
|
9
9
|
outputLines: number;
|
|
10
10
|
outputBytes: number;
|
|
11
|
+
outcome?: ToolOutcome;
|
|
11
12
|
};
|
|
12
13
|
export type MutableTurn = {
|
|
13
14
|
id: string | null;
|
|
@@ -16,17 +17,21 @@ export type MutableTurn = {
|
|
|
16
17
|
text?: string;
|
|
17
18
|
tools: MutableTool[];
|
|
18
19
|
};
|
|
20
|
+
export type ToolOutcome = 'failed' | 'rejected';
|
|
19
21
|
export type ParsedEvent = {
|
|
20
22
|
at: number | null;
|
|
21
23
|
turn: MutableTurn | null;
|
|
22
24
|
toolOutputs?: readonly {
|
|
23
25
|
id: string;
|
|
24
26
|
text: string;
|
|
27
|
+
outcome?: ToolOutcome;
|
|
25
28
|
}[];
|
|
26
29
|
};
|
|
27
30
|
export type Parse = (value: unknown) => ParsedEvent | null;
|
|
28
31
|
export declare function claudeEvent(value: unknown): ParsedEvent | null;
|
|
29
32
|
export declare function codexEvent(value: unknown): ParsedEvent | null;
|
|
33
|
+
export declare function codexAppServerEvent(value: unknown): ParsedEvent | null;
|
|
34
|
+
export declare function codexAppServerStream(): Parse;
|
|
30
35
|
export declare function piEvent(value: unknown): ParsedEvent | null;
|
|
31
36
|
export declare function geminiEvent(value: unknown): ParsedEvent | null;
|
|
32
37
|
export declare function openclawEvent(value: unknown): ParsedEvent | null;
|
package/dist/parsers.js
CHANGED
|
@@ -50,12 +50,69 @@ const compact = (value) => {
|
|
|
50
50
|
return String(value);
|
|
51
51
|
}
|
|
52
52
|
};
|
|
53
|
+
// A RESULT IS WHAT THE TOOL SAID, NOT ITS WIRE SHAPE. Every harness that records a result as content blocks
|
|
54
|
+
// (Claude's tool_result content, Codex's input_text output blocks, MCP results everywhere) means the text of
|
|
55
|
+
// those blocks, with their line breaks; encoding the block list itself as JSON would show the reader escaped
|
|
56
|
+
// newlines inside a JSON shell. A block that is not text — an image, a reference — is named, not dumped.
|
|
57
|
+
const resultText = (value) => {
|
|
58
|
+
if (typeof value === 'string')
|
|
59
|
+
return value;
|
|
60
|
+
if (!Array.isArray(value))
|
|
61
|
+
return compact(value);
|
|
62
|
+
return value.map((blockValue) => {
|
|
63
|
+
const block = object(blockValue);
|
|
64
|
+
if (!block)
|
|
65
|
+
return compact(blockValue);
|
|
66
|
+
const text = string(block.text);
|
|
67
|
+
if (text !== null)
|
|
68
|
+
return text;
|
|
69
|
+
const type = string(block.type);
|
|
70
|
+
if (type === 'image')
|
|
71
|
+
return '[image]';
|
|
72
|
+
if (type)
|
|
73
|
+
return `[${type}]`;
|
|
74
|
+
return compact(blockValue);
|
|
75
|
+
}).join('\n');
|
|
76
|
+
};
|
|
77
|
+
// CODEX CODE-MODE: the `exec` tool's input is a JS program that calls `tools.exec_command({cmd:"…"})`; the shell
|
|
78
|
+
// command is what actually ran, the JS around it is transport the model writes to reach the sandbox. Surface the
|
|
79
|
+
// command(s) so a GENERIC renderer shows what ran, not the wrapper. This is codex-adapter knowledge and lives
|
|
80
|
+
// here at the bottom of the stack — never leak a code-mode literal up into the shared vocabulary or the UI.
|
|
81
|
+
const CODEX_EXEC_CMD = /\bcmd\s*:\s*"((?:[^"\\]|\\.)*)"/g;
|
|
82
|
+
function codexExecCommand(input) {
|
|
83
|
+
if (typeof input !== 'string' || !input.includes('exec_command'))
|
|
84
|
+
return input;
|
|
85
|
+
const commands = [];
|
|
86
|
+
for (const match of input.matchAll(CODEX_EXEC_CMD)) {
|
|
87
|
+
try {
|
|
88
|
+
commands.push(JSON.parse(`"${match[1]}"`));
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
commands.push(match[1].replace(/\\(["\\])/g, '$1'));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return commands.length ? commands.join('\n') : input;
|
|
95
|
+
}
|
|
53
96
|
const lineCount = (value) => value ? value.split(/\r?\n/).length : 0;
|
|
54
97
|
// --- the seven native shapes -------------------------------------------------------------------------------
|
|
55
98
|
export function claudeEvent(value) {
|
|
56
99
|
const entry = object(value);
|
|
57
|
-
|
|
58
|
-
|
|
100
|
+
if (!entry)
|
|
101
|
+
return null;
|
|
102
|
+
// A message steered into a RUNNING turn (stream-json `type:user` on stdin, a queued command in the TUI) is not
|
|
103
|
+
// recorded as a `user` message: Claude writes it as an `attachment` of type `queued_command` carrying the prompt
|
|
104
|
+
// blocks. It is the person's turn all the same, and the one place a steer becomes observable — hooks never fire
|
|
105
|
+
// for it — so the reader draws it as a user turn at the moment it entered the conversation.
|
|
106
|
+
if (entry.type === 'attachment') {
|
|
107
|
+
const attachment = object(entry.attachment);
|
|
108
|
+
const queuedAt = at(entry);
|
|
109
|
+
if (attachment?.type !== 'queued_command' || queuedAt === null)
|
|
110
|
+
return null;
|
|
111
|
+
const text = items(attachment.prompt).map((block) => string(object(block)?.text)).filter(Boolean).join('\n') || null;
|
|
112
|
+
return text ? { at: queuedAt, turn: { id: idOf(entry), at: queuedAt, role: 'user', text, tools: [] } } : null;
|
|
113
|
+
}
|
|
114
|
+
const message = object(entry.message);
|
|
115
|
+
if (!message)
|
|
59
116
|
return null;
|
|
60
117
|
const eventAt = at(entry) ?? at(message);
|
|
61
118
|
if (eventAt === null)
|
|
@@ -68,7 +125,7 @@ export function claudeEvent(value) {
|
|
|
68
125
|
const outputs = blocks.flatMap((block) => {
|
|
69
126
|
const b = object(block);
|
|
70
127
|
const id = string(b?.tool_use_id);
|
|
71
|
-
return b?.type === 'tool_result' && id ? [{ id, text:
|
|
128
|
+
return b?.type === 'tool_result' && id ? [{ id, text: resultText(b?.content ?? ''), ...(b?.is_error === true ? { outcome: 'failed' } : {}) }] : [];
|
|
72
129
|
});
|
|
73
130
|
if (outputs.length)
|
|
74
131
|
return { at: eventAt, turn: null, toolOutputs: outputs };
|
|
@@ -90,6 +147,9 @@ export function claudeEvent(value) {
|
|
|
90
147
|
}
|
|
91
148
|
return null;
|
|
92
149
|
}
|
|
150
|
+
const blockText = (content) => typeof content === 'string'
|
|
151
|
+
? string(content)
|
|
152
|
+
: items(content).map((block) => string(object(block)?.text)).filter(Boolean).join('\n') || null;
|
|
93
153
|
export function codexEvent(value) {
|
|
94
154
|
const entry = object(value);
|
|
95
155
|
const payload = object(entry?.payload);
|
|
@@ -99,30 +159,137 @@ export function codexEvent(value) {
|
|
|
99
159
|
if (eventAt === null)
|
|
100
160
|
return { at: null, turn: null };
|
|
101
161
|
const type = string(payload.type);
|
|
102
|
-
|
|
103
|
-
|
|
162
|
+
// THE PERSON'S MESSAGE is the `user_message` event. Codex also records it as a `response_item` message (the API
|
|
163
|
+
// form, `input_text` blocks) beside harness injections that no person typed (the AGENTS.md instructions), so
|
|
164
|
+
// that form is not a turn — the event is, once.
|
|
165
|
+
if (entry.type === 'event_msg' && type === 'user_message') {
|
|
104
166
|
const text = typeof payload.message === 'string' ? payload.message : compact(payload.message ?? payload.content ?? '');
|
|
105
167
|
return text ? { at: eventAt, turn: { id: idOf(payload) ?? idOf(entry), at: eventAt, role: 'user', text, tools: [] } } : null;
|
|
106
168
|
}
|
|
107
|
-
|
|
169
|
+
if (entry.type === 'response_item' && (type === 'message' || type === 'input_message') && payload.role === 'user')
|
|
170
|
+
return { at: eventAt, turn: null };
|
|
171
|
+
// WHAT THE AGENT SAID is the `agent_message` event — commentary and the final answer alike. Codex 0.146 also
|
|
172
|
+
// records the same prose as a `response_item` message (`output_text` blocks) beside it, so that form is not read:
|
|
173
|
+
// reading both would say every sentence twice. An empty event (a final answer that was a tool call) is a clock,
|
|
174
|
+
// not a turn. Structured reasoning stays private.
|
|
108
175
|
if (entry.type === 'event_msg' && type === 'agent_message') {
|
|
109
176
|
const text = string(payload.message ?? payload.text);
|
|
110
|
-
return text ? { at: eventAt, turn: { id: idOf(payload) ?? idOf(entry), at: eventAt, role: 'assistant', text, tools: [] } } : null;
|
|
177
|
+
return text ? { at: eventAt, turn: { id: idOf(payload) ?? idOf(entry), at: eventAt, role: 'assistant', text, tools: [] } } : { at: eventAt, turn: null };
|
|
111
178
|
}
|
|
179
|
+
if (entry.type === 'response_item' && type === 'message' && payload.role === 'assistant')
|
|
180
|
+
return { at: eventAt, turn: null };
|
|
112
181
|
if (entry.type === 'response_item' && (type === 'custom_tool_call' || type === 'function_call')) {
|
|
113
182
|
const id = string(payload.call_id ?? payload.id) ?? 'tool';
|
|
114
|
-
|
|
183
|
+
const rawInput = payload.input === undefined && payload.arguments === undefined ? undefined : compact(payload.input ?? payload.arguments);
|
|
184
|
+
return { at: eventAt, turn: { id: idOf(payload) ?? idOf(entry), at: eventAt, role: 'assistant', tools: [{ id, name: string(payload.name ?? payload.tool_name) ?? 'tool', input: codexExecCommand(rawInput), outputLines: 0, outputBytes: 0 }] } };
|
|
115
185
|
}
|
|
116
186
|
if (entry.type === 'response_item' && (type === 'custom_tool_call_output' || type === 'function_call_output')) {
|
|
117
187
|
const id = string(payload.call_id ?? payload.id);
|
|
118
188
|
const output = payload.output ?? payload.result ?? '';
|
|
119
|
-
return id ? { at: eventAt, turn: null, toolOutputs: [{ id, text:
|
|
189
|
+
return id ? { at: eventAt, turn: null, toolOutputs: [{ id, text: resultText(output) }] } : null;
|
|
120
190
|
}
|
|
121
191
|
return null;
|
|
122
192
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
193
|
+
// Codex app-server notifications are a different native stream from rollout lines. Keep this mapping stateless:
|
|
194
|
+
// a caller that needs streamed prose uses codexAppServerStream below, while file and in-memory sources still
|
|
195
|
+
// share the same one-record parser contract.
|
|
196
|
+
export function codexAppServerEvent(value) {
|
|
197
|
+
const entry = object(value);
|
|
198
|
+
const params = object(entry?.params);
|
|
199
|
+
if (!entry || !params || typeof entry.method !== 'string')
|
|
200
|
+
return null;
|
|
201
|
+
const item = object(params.item);
|
|
202
|
+
const method = entry.method;
|
|
203
|
+
const recognized = method === 'item/agentMessage/delta' || method === 'item/started' || method === 'item/completed';
|
|
204
|
+
if (!recognized)
|
|
205
|
+
return null;
|
|
206
|
+
const eventAt = timestamp(params.emittedAtMs) ?? timestamp(params.startedAtMs) ?? timestamp(params.completedAtMs);
|
|
207
|
+
if (eventAt === null)
|
|
208
|
+
return { at: null, turn: null };
|
|
209
|
+
if (method === 'item/agentMessage/delta') {
|
|
210
|
+
const id = string(params.itemId);
|
|
211
|
+
const delta = string(params.delta);
|
|
212
|
+
return id && delta !== null
|
|
213
|
+
? { at: eventAt, turn: { id, at: eventAt, role: 'assistant', text: delta, tools: [] } }
|
|
214
|
+
: null;
|
|
215
|
+
}
|
|
216
|
+
if ((method !== 'item/started' && method !== 'item/completed') || !item)
|
|
217
|
+
return null;
|
|
218
|
+
const id = string(item.id);
|
|
219
|
+
const type = string(item.type);
|
|
220
|
+
if (!id || !type)
|
|
221
|
+
return null;
|
|
222
|
+
if (type === 'userMessage') {
|
|
223
|
+
const text = blockText(item.content);
|
|
224
|
+
return text ? { at: eventAt, turn: { id, at: eventAt, role: 'user', text, tools: [] } } : null;
|
|
225
|
+
}
|
|
226
|
+
if (type === 'agentMessage') {
|
|
227
|
+
const text = string(item.text);
|
|
228
|
+
return { at: eventAt, turn: { id, at: eventAt, role: 'assistant', text: text ?? undefined, tools: [] } };
|
|
229
|
+
}
|
|
230
|
+
const toolTypes = new Set(['commandExecution', 'functionCall', 'customToolCall', 'mcpToolCall', 'dynamicToolCall']);
|
|
231
|
+
if (!toolTypes.has(type))
|
|
232
|
+
return null;
|
|
233
|
+
if (method === 'item/started') {
|
|
234
|
+
const name = type === 'commandExecution' ? 'command'
|
|
235
|
+
: string(item.name) ?? string(item.tool) ?? (type === 'mcpToolCall' ? 'mcp' : 'tool');
|
|
236
|
+
const input = item.arguments !== undefined ? item.arguments
|
|
237
|
+
: item.input !== undefined ? item.input
|
|
238
|
+
: item.command !== undefined ? item.command
|
|
239
|
+
: undefined;
|
|
240
|
+
return { at: eventAt, turn: { id, at: eventAt, role: 'assistant', tools: [{ id, name, input: input === undefined ? undefined : compact(input), outputLines: 0, outputBytes: 0 }] } };
|
|
241
|
+
}
|
|
242
|
+
let output = undefined;
|
|
243
|
+
if (type === 'commandExecution')
|
|
244
|
+
output = item.aggregatedOutput;
|
|
245
|
+
else if (type === 'functionCall' || type === 'customToolCall')
|
|
246
|
+
output = item.output ?? item.result;
|
|
247
|
+
else if (type === 'mcpToolCall')
|
|
248
|
+
output = item.result ?? item.error;
|
|
249
|
+
else if (type === 'dynamicToolCall')
|
|
250
|
+
output = item.contentItems ?? item.output ?? item.error;
|
|
251
|
+
// the item status is the app-server's own verdict: `failed`, or `declined` when the person refused the call —
|
|
252
|
+
// a declined call has no output, so the empty result is what ends its "running"
|
|
253
|
+
const status = string(item.status);
|
|
254
|
+
const outcome = status === 'failed' ? 'failed' : status === 'declined' ? 'rejected' : undefined;
|
|
255
|
+
if (output === undefined || output === null) {
|
|
256
|
+
return outcome ? { at: eventAt, turn: null, toolOutputs: [{ id, text: '', outcome }] } : { at: eventAt, turn: null };
|
|
257
|
+
}
|
|
258
|
+
return { at: eventAt, turn: null, toolOutputs: [{ id, text: resultText(output), ...(outcome ? { outcome } : {}) }] };
|
|
259
|
+
}
|
|
260
|
+
// Agent-message deltas are fragments of one native item. The closure remembers only that item's text and
|
|
261
|
+
// re-emits its native id, allowing IntervalCollector to replace the earlier turn in place.
|
|
262
|
+
export function codexAppServerStream() {
|
|
263
|
+
const textByItem = new Map();
|
|
264
|
+
return (value) => {
|
|
265
|
+
const parsed = codexAppServerEvent(value);
|
|
266
|
+
if (!parsed)
|
|
267
|
+
return null;
|
|
268
|
+
const entry = object(value);
|
|
269
|
+
const params = object(entry?.params);
|
|
270
|
+
const item = object(params?.item);
|
|
271
|
+
if (entry?.method === 'item/agentMessage/delta') {
|
|
272
|
+
const id = string(params?.itemId);
|
|
273
|
+
if (!id || !parsed.turn)
|
|
274
|
+
return parsed;
|
|
275
|
+
const text = (textByItem.get(id) ?? '') + (parsed.turn.text ?? '');
|
|
276
|
+
textByItem.set(id, text);
|
|
277
|
+
return { ...parsed, turn: { ...parsed.turn, text } };
|
|
278
|
+
}
|
|
279
|
+
if (item && item.type === 'agentMessage') {
|
|
280
|
+
const id = string(item.id);
|
|
281
|
+
if (id) {
|
|
282
|
+
const text = string(item.text);
|
|
283
|
+
if (text !== null || !textByItem.has(id))
|
|
284
|
+
textByItem.set(id, text ?? '');
|
|
285
|
+
const turn = parsed.turn;
|
|
286
|
+
if (turn)
|
|
287
|
+
return { ...parsed, turn: { ...turn, text: textByItem.get(id) || undefined } };
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return parsed;
|
|
291
|
+
};
|
|
292
|
+
}
|
|
126
293
|
export function piEvent(value) {
|
|
127
294
|
const entry = object(value);
|
|
128
295
|
const message = object(entry?.message);
|
|
@@ -150,7 +317,7 @@ export function piEvent(value) {
|
|
|
150
317
|
}
|
|
151
318
|
if (message.role === 'toolResult') {
|
|
152
319
|
const id = string(message.toolCallId);
|
|
153
|
-
return id ? { at: eventAt, turn: null, toolOutputs: [{ id, text: blockText(message.content) ?? compact(message.content ?? '') }] } : null;
|
|
320
|
+
return id ? { at: eventAt, turn: null, toolOutputs: [{ id, text: blockText(message.content) ?? compact(message.content ?? ''), ...(message.isError === true ? { outcome: 'failed' } : {}) }] } : null;
|
|
154
321
|
}
|
|
155
322
|
return null;
|
|
156
323
|
}
|
|
@@ -185,7 +352,7 @@ export function geminiEvent(value) {
|
|
|
185
352
|
for (const callValue of items(message.toolCalls)) {
|
|
186
353
|
const call = object(callValue);
|
|
187
354
|
const id = string(call?.id) ?? `tool-${turn.tools.length}`;
|
|
188
|
-
turn.tools.push({ id, name: string(call?.name) ?? 'tool', input: call?.args === undefined ? undefined : compact(call.args), outputLines: 0, outputBytes: 0 });
|
|
355
|
+
turn.tools.push({ id, name: string(call?.name) ?? 'tool', input: call?.args === undefined ? undefined : compact(call.args), outputLines: 0, outputBytes: 0, ...(call?.status === 'error' ? { outcome: 'failed' } : {}) });
|
|
189
356
|
}
|
|
190
357
|
if (!turn.text && !turn.tools.length)
|
|
191
358
|
return null;
|
|
@@ -207,7 +374,7 @@ export function openclawEvent(value) {
|
|
|
207
374
|
}
|
|
208
375
|
if (message.role === 'toolResult') {
|
|
209
376
|
const id = string(message.toolCallId);
|
|
210
|
-
return id ? { at: eventAt, turn: null, toolOutputs: [{ id, text: blockText(message.content) ?? compact(message.content ?? '') }] } : null;
|
|
377
|
+
return id ? { at: eventAt, turn: null, toolOutputs: [{ id, text: blockText(message.content) ?? compact(message.content ?? ''), ...(message.isError === true ? { outcome: 'failed' } : {}) }] } : null;
|
|
211
378
|
}
|
|
212
379
|
if (message.role === 'assistant') {
|
|
213
380
|
const turn = { id: idOf(entry) ?? idOf(message), at: eventAt, role: 'assistant', tools: [] };
|
|
@@ -291,11 +458,14 @@ export function opencodeEvents(value) {
|
|
|
291
458
|
const state = object(part.state);
|
|
292
459
|
const status = (string(state?.status) ?? '').toLowerCase();
|
|
293
460
|
const tool = { id: string(part.callID ?? part.id) ?? `tool-${turn.tools.length}`, name: string(part.tool) ?? 'tool', input: state?.input === undefined ? undefined : compact(state.input), outputLines: 0, outputBytes: 0 };
|
|
294
|
-
|
|
461
|
+
// the terminal states of OpenCode's ToolState union — a call still pending or running has no result yet
|
|
462
|
+
if (/completed|error/.test(status)) {
|
|
295
463
|
const output = compact(state?.output ?? state?.error ?? '');
|
|
296
464
|
tool.output = output.slice(0, MAX_OUTPUT_BYTES);
|
|
297
465
|
tool.outputBytes = Buffer.byteLength(output);
|
|
298
466
|
tool.outputLines = lineCount(output);
|
|
467
|
+
if (status === 'error')
|
|
468
|
+
tool.outcome = 'failed';
|
|
299
469
|
}
|
|
300
470
|
turn.tools.push(tool);
|
|
301
471
|
}
|
|
@@ -348,6 +518,8 @@ export class IntervalCollector {
|
|
|
348
518
|
tool.outputLines += lineCount(output.text);
|
|
349
519
|
if (tool.output === undefined)
|
|
350
520
|
tool.output = '';
|
|
521
|
+
if (output.outcome)
|
|
522
|
+
tool.outcome = output.outcome;
|
|
351
523
|
const remaining = Math.max(0, MAX_OUTPUT_BYTES - Buffer.byteLength(tool.output));
|
|
352
524
|
tool.output += output.text.slice(0, remaining);
|
|
353
525
|
if (bytes > remaining)
|
|
@@ -366,14 +538,31 @@ export class IntervalCollector {
|
|
|
366
538
|
this.synthesized.set(base, seen + 1);
|
|
367
539
|
turn.id = seen ? `${base}#${seen}` : base;
|
|
368
540
|
}
|
|
369
|
-
this.turns.
|
|
370
|
-
|
|
371
|
-
this.
|
|
372
|
-
|
|
373
|
-
const
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
541
|
+
const existingAt = this.turns.findIndex((candidate) => candidate.id === turn.id);
|
|
542
|
+
if (existingAt >= 0) {
|
|
543
|
+
const existing = this.turns[existingAt];
|
|
544
|
+
const incomingTools = new Map(turn.tools.map((tool) => [tool.id, tool]));
|
|
545
|
+
const tools = existing.tools.map((tool) => {
|
|
546
|
+
const incoming = incomingTools.get(tool.id);
|
|
547
|
+
incomingTools.delete(tool.id);
|
|
548
|
+
return incoming ? { ...tool, ...incoming, output: incoming.output ?? tool.output } : tool;
|
|
549
|
+
});
|
|
550
|
+
tools.push(...incomingTools.values());
|
|
551
|
+
const replacement = { ...existing, ...turn, text: turn.text ?? existing.text, tools };
|
|
552
|
+
this.turns[existingAt] = replacement;
|
|
553
|
+
for (const tool of replacement.tools)
|
|
554
|
+
this.byTool.set(tool.id, tool);
|
|
555
|
+
}
|
|
556
|
+
else {
|
|
557
|
+
this.turns.push(turn);
|
|
558
|
+
for (const tool of turn.tools)
|
|
559
|
+
this.byTool.set(tool.id, tool);
|
|
560
|
+
if (this.turns.length > MAX_TURNS) {
|
|
561
|
+
const dropped = this.turns.shift();
|
|
562
|
+
for (const tool of dropped.tools)
|
|
563
|
+
this.evicted.add(tool.id);
|
|
564
|
+
this.omittedTurns++;
|
|
565
|
+
}
|
|
377
566
|
}
|
|
378
567
|
}
|
|
379
568
|
return this.pastRange;
|
package/dist/readers.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type TranscriptReader } from './turns.js';
|
|
2
2
|
export declare function claudeTranscriptPath(threadId: string, root?: string): string | null;
|
|
3
|
-
export declare function codexRolloutPath(threadId: string, root?: string): string | null;
|
|
3
|
+
export declare function codexRolloutPath(threadId: string, root?: string, archive?: string): string | null;
|
|
4
4
|
export declare function piSessionPath(threadId: string, root?: string): string | null;
|
|
5
5
|
export declare function geminiTranscriptPath(threadId: string, root?: string): string | null;
|
|
6
6
|
export declare function openclawTranscriptPath(threadId: string, root?: string): string | null;
|
package/dist/readers.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process';
|
|
2
2
|
import { closeSync, openSync, readFileSync, readSync, readdirSync, statSync } from 'node:fs';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
|
-
import { join } from 'node:path';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
5
|
import { TranscriptReadError } from './turns.js';
|
|
6
6
|
import { IntervalCollector, claudeEvent, codexEvent, geminiEvent, hermesEvents, openclawEvent, opencodeEvents, piEvent } from './parsers.js';
|
|
7
7
|
// THE NATIVE-THREAD READERS. Each harness keeps its conversation somewhere private — Claude's project JSONL,
|
|
@@ -31,7 +31,10 @@ export function claudeTranscriptPath(threadId, root = projectTranscriptRoot()) {
|
|
|
31
31
|
const codexSessionsDir = () => join(process.env.CODEX_HOME || join(homedir(), '.codex'), 'sessions');
|
|
32
32
|
// Walk newest day first and return on the first hit; the walk is exhaustive rather than capped, because
|
|
33
33
|
// future-dated junk under sessions/ sorts above every real day and a cap once masked every real rollout.
|
|
34
|
-
|
|
34
|
+
// A thread Codex has ARCHIVED (the app-server's thread/archive, which a closed session runs) keeps its
|
|
35
|
+
// rollout, moved out of the dated tree into the flat `archived_sessions/` beside it — a closed session's
|
|
36
|
+
// conversation is still on disk and still readable, so the locator looks there second.
|
|
37
|
+
export function codexRolloutPath(threadId, root = codexSessionsDir(), archive = join(dirname(root), 'archived_sessions')) {
|
|
35
38
|
for (const year of children(root))
|
|
36
39
|
for (const month of children(join(root, year)))
|
|
37
40
|
for (const day of children(join(root, year, month))) {
|
|
@@ -40,7 +43,8 @@ export function codexRolloutPath(threadId, root = codexSessionsDir()) {
|
|
|
40
43
|
if (file)
|
|
41
44
|
return join(dir, file);
|
|
42
45
|
}
|
|
43
|
-
|
|
46
|
+
const archived = children(archive).find((name) => name.includes(threadId));
|
|
47
|
+
return archived ? join(archive, archived) : null;
|
|
44
48
|
}
|
|
45
49
|
const piSessionsRoot = () => join(process.env.SPEXCODE_PI_AGENT_DIR || join(homedir(), '.pi', 'agent'), 'sessions');
|
|
46
50
|
const piSessionPaths = new Map();
|
package/dist/turns.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spexcode/transcript",
|
|
3
|
-
"version": "0.7.0-next.
|
|
3
|
+
"version": "0.7.0-next.11",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Normalized agent transcripts: one parser per harness, a bounded interval reader over a native thread file or an in-memory event stream, and the full/delta frame protocol every transport and renderer share.",
|
|
6
6
|
"files": [
|