@spexcode/transcript 0.7.0-next.11 → 0.7.0-next.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/parsers.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type TranscriptRange, type TranscriptRead } from './turns.js';
1
+ import { type TranscriptRange, type TranscriptRead, type TurnOutcome } from './turns.js';
2
2
  export declare const MAX_TURNS = 200;
3
3
  export declare const MAX_OUTPUT_BYTES: number;
4
4
  export type MutableTool = {
@@ -16,6 +16,8 @@ export type MutableTurn = {
16
16
  role: 'user' | 'assistant';
17
17
  text?: string;
18
18
  tools: MutableTool[];
19
+ outcome?: TurnOutcome;
20
+ error?: string;
19
21
  };
20
22
  export type ToolOutcome = 'failed' | 'rejected';
21
23
  export type ParsedEvent = {
@@ -34,7 +36,7 @@ export declare function codexAppServerEvent(value: unknown): ParsedEvent | null;
34
36
  export declare function codexAppServerStream(): Parse;
35
37
  export declare function piEvent(value: unknown): ParsedEvent | null;
36
38
  export declare function geminiEvent(value: unknown): ParsedEvent | null;
37
- export declare function openclawEvent(value: unknown): ParsedEvent | null;
39
+ export declare const openclawEvent: typeof piEvent;
38
40
  export declare function hermesEvents(value: unknown): ParsedEvent[];
39
41
  export declare function opencodeEvents(value: unknown): ParsedEvent[];
40
42
  export declare class IntervalCollector {
package/dist/parsers.js CHANGED
@@ -183,6 +183,18 @@ export function codexEvent(value) {
183
183
  const rawInput = payload.input === undefined && payload.arguments === undefined ? undefined : compact(payload.input ?? payload.arguments);
184
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 }] } };
185
185
  }
186
+ // THE ROLLOUT PUTS THE VERDICT IN A THIRD RECORD. Unlike every other source here, a codex result item
187
+ // carries no failure field at all — `function_call_output` and `custom_tool_call_output` have only
188
+ // `{call_id, output}`. The harness records how the command ended in a separate `event_msg`, joined by the
189
+ // same `call_id`: `exec_command_end.status` (completed | failed, 6,194 failed of 83,990 in the rollouts on
190
+ // this box, matching exactly the nonzero exit codes) and `patch_apply_end.success`. Without this second
191
+ // join every failed command in a codex transcript reads as an ordinary one. The outcome is carried alone,
192
+ // with no text, so it lands on the call the output record already filled.
193
+ if (entry.type === 'event_msg' && (type === 'exec_command_end' || type === 'patch_apply_end')) {
194
+ const id = string(payload.call_id);
195
+ const failed = string(payload.status) === 'failed' || payload.success === false;
196
+ return id && failed ? { at: eventAt, turn: null, toolOutputs: [{ id, text: '', outcome: 'failed' }] } : { at: eventAt, turn: null };
197
+ }
186
198
  if (entry.type === 'response_item' && (type === 'custom_tool_call_output' || type === 'function_call_output')) {
187
199
  const id = string(payload.call_id ?? payload.id);
188
200
  const output = payload.output ?? payload.result ?? '';
@@ -203,7 +215,12 @@ export function codexAppServerEvent(value) {
203
215
  const recognized = method === 'item/agentMessage/delta' || method === 'item/started' || method === 'item/completed';
204
216
  if (!recognized)
205
217
  return null;
206
- const eventAt = timestamp(params.emittedAtMs) ?? timestamp(params.startedAtMs) ?? timestamp(params.completedAtMs);
218
+ // `emittedAtMs` is a SIBLING of `method` and `params`, not a field inside them — the app-server's own
219
+ // generated envelope type puts it there, and every line of the capture in `fixtures/codex-app-server` has
220
+ // exactly the keys `method`, `params`, `emittedAtMs`. Reading it from `params` found nothing, which made
221
+ // every `item/agentMessage/delta` clockless and therefore dropped: the streaming path parsed and produced
222
+ // nothing at all. The lifecycle clocks below do sit in `params`.
223
+ const eventAt = timestamp(entry.emittedAtMs) ?? timestamp(params.startedAtMs) ?? timestamp(params.completedAtMs);
207
224
  if (eventAt === null)
208
225
  return { at: null, turn: null };
209
226
  if (method === 'item/agentMessage/delta') {
@@ -313,6 +330,16 @@ export function piEvent(value) {
313
330
  turn.tools.push({ id, name: string(block.name) ?? 'tool', input: block.arguments === undefined ? undefined : compact(block.arguments), outputLines: 0, outputBytes: 0 });
314
331
  }
315
332
  }
333
+ // the producer's verdict on the TURN (`StopReason` in pi's shipped types): `error` is the provider's own
334
+ // failure, `aborted` the turn a stop ended. These are exactly the turns that carry no text and no calls —
335
+ // 13 of 578 assistant messages in the sessions on this box — so without them the turn is an empty gap.
336
+ const stop = string(message.stopReason);
337
+ if (stop === 'error' || stop === 'aborted') {
338
+ turn.outcome = stop === 'aborted' ? 'cancelled' : 'failed';
339
+ const reason = string(message.errorMessage);
340
+ if (reason)
341
+ turn.error = reason;
342
+ }
316
343
  return { at: eventAt, turn };
317
344
  }
318
345
  if (message.role === 'toolResult') {
@@ -360,37 +387,23 @@ export function geminiEvent(value) {
360
387
  }
361
388
  return null;
362
389
  }
363
- export function openclawEvent(value) {
364
- const entry = object(value);
365
- const message = object(entry?.message);
366
- if (!entry || entry.type !== 'message' || !message)
367
- return null;
368
- const eventAt = at(message) ?? at(entry);
369
- if (eventAt === null)
370
- return { at: null, turn: null };
371
- if (message.role === 'user') {
372
- const text = blockText(message.content);
373
- return text ? { at: eventAt, turn: { id: idOf(entry) ?? idOf(message), at: eventAt, role: 'user', text, tools: [] } } : null;
374
- }
375
- if (message.role === 'toolResult') {
376
- const id = string(message.toolCallId);
377
- return id ? { at: eventAt, turn: null, toolOutputs: [{ id, text: blockText(message.content) ?? compact(message.content ?? ''), ...(message.isError === true ? { outcome: 'failed' } : {}) }] } : null;
378
- }
379
- if (message.role === 'assistant') {
380
- const turn = { id: idOf(entry) ?? idOf(message), at: eventAt, role: 'assistant', tools: [] };
381
- for (const blockValue of items(message.content)) {
382
- const block = object(blockValue);
383
- if (block?.type === 'text')
384
- turn.text = [turn.text, string(block.text)].filter(Boolean).join('\n') || undefined;
385
- if (block?.type === 'toolCall') {
386
- const id = string(block.id) ?? `tool-${turn.tools.length}`;
387
- turn.tools.push({ id, name: string(block.name) ?? 'tool', input: block.arguments === undefined ? undefined : compact(block.arguments), outputLines: 0, outputBytes: 0 });
388
- }
389
- }
390
- return turn.text || turn.tools.length ? { at: eventAt, turn } : null;
391
- }
392
- return null;
393
- }
390
+ // OPENCLAW WRITES PI'S FORMAT. Verified field by field against pi's shipped schema: the `{"type":"session",
391
+ // "version":3}` header, the 8-char hex id tree, the `user|assistant|toolResult` roles, the `text`/`thinking`/
392
+ // `toolCall` blocks, `toolResult`'s exact key set. The only OpenClaw-specific things are its own `customType`
393
+ // namespace and where the file lives — neither of which a parser reads. So this is not a second parser: two
394
+ // hand-written copies had already drifted into two defects (a different clock preference on identical bytes,
395
+ // and dropping every turn with no text and no calls, which is precisely the failed and aborted ones). Where a
396
+ // harness differs only in where its file sits, the adapter row is the LOCATOR ([[transcript-reader]]), never a
397
+ // second copy of the parse.
398
+ export const openclawEvent = piEvent;
399
+ // HERMES COUNTS IN SECONDS. Its export writes `timestamp` as a float epoch SECOND (`1787942674.556185`),
400
+ // where every other harness here writes milliseconds and `at()` reads a bare number as one. Unconverted, a
401
+ // Hermes turn lands in January 1970 and every interval read of a real thread — the session API hands `from`
402
+ // and `to` as epoch ms — comes back empty. The unit is this producer's, so the conversion is this adapter's.
403
+ const hermesAt = (message) => {
404
+ const seconds = at(message);
405
+ return seconds === null ? null : Math.round(seconds * 1000);
406
+ };
394
407
  export function hermesEvents(value) {
395
408
  const root = object(value);
396
409
  const events = [];
@@ -398,7 +411,7 @@ export function hermesEvents(value) {
398
411
  const message = object(messageValue);
399
412
  if (!message)
400
413
  continue;
401
- const eventAt = at(message);
414
+ const eventAt = hermesAt(message);
402
415
  if (eventAt === null) {
403
416
  events.push({ at: null, turn: null });
404
417
  continue;
package/dist/turns.d.ts CHANGED
@@ -11,12 +11,15 @@ export type TranscriptTool = Readonly<{
11
11
  outputBytes: number;
12
12
  outcome?: 'failed' | 'rejected';
13
13
  }>;
14
+ export type TurnOutcome = 'failed' | 'cancelled';
14
15
  export type TranscriptTurn = Readonly<{
15
16
  id: string;
16
17
  at: number;
17
18
  role: 'user' | 'assistant';
18
19
  text?: string;
19
20
  tools?: readonly TranscriptTool[];
21
+ outcome?: TurnOutcome;
22
+ error?: string;
20
23
  }>;
21
24
  export type TranscriptRead = Readonly<{
22
25
  revision: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spexcode/transcript",
3
- "version": "0.7.0-next.11",
3
+ "version": "0.7.0-next.12",
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": [