@spexcode/transcript 0.7.0-next.1 → 0.7.0-next.2

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 CHANGED
@@ -27,6 +27,8 @@ export type ParsedEvent = {
27
27
  export type Parse = (value: unknown) => ParsedEvent | null;
28
28
  export declare function claudeEvent(value: unknown): ParsedEvent | null;
29
29
  export declare function codexEvent(value: unknown): ParsedEvent | null;
30
+ export declare function codexAppServerEvent(value: unknown): ParsedEvent | null;
31
+ export declare function codexAppServerStream(): Parse;
30
32
  export declare function piEvent(value: unknown): ParsedEvent | null;
31
33
  export declare function geminiEvent(value: unknown): ParsedEvent | null;
32
34
  export declare function openclawEvent(value: unknown): ParsedEvent | null;
package/dist/parsers.js CHANGED
@@ -120,6 +120,101 @@ export function codexEvent(value) {
120
120
  }
121
121
  return null;
122
122
  }
123
+ // Codex app-server notifications are a different native stream from rollout lines. Keep this mapping stateless:
124
+ // a caller that needs streamed prose uses codexAppServerStream below, while file and in-memory sources still
125
+ // share the same one-record parser contract.
126
+ export function codexAppServerEvent(value) {
127
+ const entry = object(value);
128
+ const params = object(entry?.params);
129
+ if (!entry || !params || typeof entry.method !== 'string')
130
+ return null;
131
+ const item = object(params.item);
132
+ const method = entry.method;
133
+ const recognized = method === 'item/agentMessage/delta' || method === 'item/started' || method === 'item/completed';
134
+ if (!recognized)
135
+ return null;
136
+ const eventAt = timestamp(params.emittedAtMs) ?? timestamp(params.startedAtMs) ?? timestamp(params.completedAtMs);
137
+ if (eventAt === null)
138
+ return { at: null, turn: null };
139
+ if (method === 'item/agentMessage/delta') {
140
+ const id = string(params.itemId);
141
+ const delta = string(params.delta);
142
+ return id && delta !== null
143
+ ? { at: eventAt, turn: { id, at: eventAt, role: 'assistant', text: delta, tools: [] } }
144
+ : null;
145
+ }
146
+ if ((method !== 'item/started' && method !== 'item/completed') || !item)
147
+ return null;
148
+ const id = string(item.id);
149
+ const type = string(item.type);
150
+ if (!id || !type)
151
+ return null;
152
+ if (type === 'userMessage') {
153
+ const text = blockText(item.content);
154
+ return text ? { at: eventAt, turn: { id, at: eventAt, role: 'user', text, tools: [] } } : null;
155
+ }
156
+ if (type === 'agentMessage') {
157
+ const text = string(item.text);
158
+ return { at: eventAt, turn: { id, at: eventAt, role: 'assistant', text: text ?? undefined, tools: [] } };
159
+ }
160
+ const toolTypes = new Set(['commandExecution', 'functionCall', 'customToolCall', 'mcpToolCall', 'dynamicToolCall']);
161
+ if (!toolTypes.has(type))
162
+ return null;
163
+ if (method === 'item/started') {
164
+ const name = type === 'commandExecution' ? 'command'
165
+ : string(item.name) ?? string(item.tool) ?? (type === 'mcpToolCall' ? 'mcp' : 'tool');
166
+ const input = item.arguments !== undefined ? item.arguments
167
+ : item.input !== undefined ? item.input
168
+ : item.command !== undefined ? item.command
169
+ : undefined;
170
+ return { at: eventAt, turn: { id, at: eventAt, role: 'assistant', tools: [{ id, name, input: input === undefined ? undefined : compact(input), outputLines: 0, outputBytes: 0 }] } };
171
+ }
172
+ let output = undefined;
173
+ if (type === 'commandExecution')
174
+ output = item.aggregatedOutput;
175
+ else if (type === 'functionCall' || type === 'customToolCall')
176
+ output = item.output ?? item.result;
177
+ else if (type === 'mcpToolCall')
178
+ output = item.result ?? item.error;
179
+ else if (type === 'dynamicToolCall')
180
+ output = item.contentItems ?? item.output ?? item.error;
181
+ return output === undefined || output === null
182
+ ? { at: eventAt, turn: null }
183
+ : { at: eventAt, turn: null, toolOutputs: [{ id, text: compact(output) }] };
184
+ }
185
+ // Agent-message deltas are fragments of one native item. The closure remembers only that item's text and
186
+ // re-emits its native id, allowing IntervalCollector to replace the earlier turn in place.
187
+ export function codexAppServerStream() {
188
+ const textByItem = new Map();
189
+ return (value) => {
190
+ const parsed = codexAppServerEvent(value);
191
+ if (!parsed)
192
+ return null;
193
+ const entry = object(value);
194
+ const params = object(entry?.params);
195
+ const item = object(params?.item);
196
+ if (entry?.method === 'item/agentMessage/delta') {
197
+ const id = string(params?.itemId);
198
+ if (!id || !parsed.turn)
199
+ return parsed;
200
+ const text = (textByItem.get(id) ?? '') + (parsed.turn.text ?? '');
201
+ textByItem.set(id, text);
202
+ return { ...parsed, turn: { ...parsed.turn, text } };
203
+ }
204
+ if (item && item.type === 'agentMessage') {
205
+ const id = string(item.id);
206
+ if (id) {
207
+ const text = string(item.text);
208
+ if (text !== null || !textByItem.has(id))
209
+ textByItem.set(id, text ?? '');
210
+ const turn = parsed.turn;
211
+ if (turn)
212
+ return { ...parsed, turn: { ...turn, text: textByItem.get(id) || undefined } };
213
+ }
214
+ }
215
+ return parsed;
216
+ };
217
+ }
123
218
  const blockText = (content) => typeof content === 'string'
124
219
  ? string(content)
125
220
  : items(content).map((block) => string(object(block)?.text)).filter(Boolean).join('\n') || null;
@@ -366,14 +461,31 @@ export class IntervalCollector {
366
461
  this.synthesized.set(base, seen + 1);
367
462
  turn.id = seen ? `${base}#${seen}` : base;
368
463
  }
369
- this.turns.push(turn);
370
- for (const tool of turn.tools)
371
- this.byTool.set(tool.id, tool);
372
- if (this.turns.length > MAX_TURNS) {
373
- const dropped = this.turns.shift();
374
- for (const tool of dropped.tools)
375
- this.evicted.add(tool.id);
376
- this.omittedTurns++;
464
+ const existingAt = this.turns.findIndex((candidate) => candidate.id === turn.id);
465
+ if (existingAt >= 0) {
466
+ const existing = this.turns[existingAt];
467
+ const incomingTools = new Map(turn.tools.map((tool) => [tool.id, tool]));
468
+ const tools = existing.tools.map((tool) => {
469
+ const incoming = incomingTools.get(tool.id);
470
+ incomingTools.delete(tool.id);
471
+ return incoming ? { ...tool, ...incoming, output: incoming.output ?? tool.output } : tool;
472
+ });
473
+ tools.push(...incomingTools.values());
474
+ const replacement = { ...existing, ...turn, text: turn.text ?? existing.text, tools };
475
+ this.turns[existingAt] = replacement;
476
+ for (const tool of replacement.tools)
477
+ this.byTool.set(tool.id, tool);
478
+ }
479
+ else {
480
+ this.turns.push(turn);
481
+ for (const tool of turn.tools)
482
+ this.byTool.set(tool.id, tool);
483
+ if (this.turns.length > MAX_TURNS) {
484
+ const dropped = this.turns.shift();
485
+ for (const tool of dropped.tools)
486
+ this.evicted.add(tool.id);
487
+ this.omittedTurns++;
488
+ }
377
489
  }
378
490
  }
379
491
  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
- export function codexRolloutPath(threadId, root = codexSessionsDir()) {
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
- return null;
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spexcode/transcript",
3
- "version": "0.7.0-next.1",
3
+ "version": "0.7.0-next.2",
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": [