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

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.
Files changed (2) hide show
  1. package/dist/parsers.js +32 -9
  2. package/package.json +1 -1
package/dist/parsers.js CHANGED
@@ -54,8 +54,22 @@ const lineCount = (value) => value ? value.split(/\r?\n/).length : 0;
54
54
  // --- the seven native shapes -------------------------------------------------------------------------------
55
55
  export function claudeEvent(value) {
56
56
  const entry = object(value);
57
- const message = object(entry?.message);
58
- if (!entry || !message)
57
+ if (!entry)
58
+ return null;
59
+ // A message steered into a RUNNING turn (stream-json `type:user` on stdin, a queued command in the TUI) is not
60
+ // recorded as a `user` message: Claude writes it as an `attachment` of type `queued_command` carrying the prompt
61
+ // blocks. It is the person's turn all the same, and the one place a steer becomes observable — hooks never fire
62
+ // for it — so the reader draws it as a user turn at the moment it entered the conversation.
63
+ if (entry.type === 'attachment') {
64
+ const attachment = object(entry.attachment);
65
+ const queuedAt = at(entry);
66
+ if (attachment?.type !== 'queued_command' || queuedAt === null)
67
+ return null;
68
+ const text = items(attachment.prompt).map((block) => string(object(block)?.text)).filter(Boolean).join('\n') || null;
69
+ return text ? { at: queuedAt, turn: { id: idOf(entry), at: queuedAt, role: 'user', text, tools: [] } } : null;
70
+ }
71
+ const message = object(entry.message);
72
+ if (!message)
59
73
  return null;
60
74
  const eventAt = at(entry) ?? at(message);
61
75
  if (eventAt === null)
@@ -90,6 +104,9 @@ export function claudeEvent(value) {
90
104
  }
91
105
  return null;
92
106
  }
107
+ const blockText = (content) => typeof content === 'string'
108
+ ? string(content)
109
+ : items(content).map((block) => string(object(block)?.text)).filter(Boolean).join('\n') || null;
93
110
  export function codexEvent(value) {
94
111
  const entry = object(value);
95
112
  const payload = object(entry?.payload);
@@ -99,16 +116,25 @@ export function codexEvent(value) {
99
116
  if (eventAt === null)
100
117
  return { at: null, turn: null };
101
118
  const type = string(payload.type);
102
- if ((entry.type === 'event_msg' && type === 'user_message')
103
- || (entry.type === 'response_item' && (type === 'message' || type === 'input_message') && payload.role === 'user')) {
119
+ // THE PERSON'S MESSAGE is the `user_message` event. Codex also records it as a `response_item` message (the API
120
+ // form, `input_text` blocks) beside harness injections that no person typed (the AGENTS.md instructions), so
121
+ // that form is not a turn — the event is, once.
122
+ if (entry.type === 'event_msg' && type === 'user_message') {
104
123
  const text = typeof payload.message === 'string' ? payload.message : compact(payload.message ?? payload.content ?? '');
105
124
  return text ? { at: eventAt, turn: { id: idOf(payload) ?? idOf(entry), at: eventAt, role: 'user', text, tools: [] } } : null;
106
125
  }
107
- // commentary AND the final answer are both what the agent said; only structured reasoning stays private
126
+ if (entry.type === 'response_item' && (type === 'message' || type === 'input_message') && payload.role === 'user')
127
+ return { at: eventAt, turn: null };
128
+ // WHAT THE AGENT SAID is the `agent_message` event — commentary and the final answer alike. Codex 0.146 also
129
+ // records the same prose as a `response_item` message (`output_text` blocks) beside it, so that form is not read:
130
+ // reading both would say every sentence twice. An empty event (a final answer that was a tool call) is a clock,
131
+ // not a turn. Structured reasoning stays private.
108
132
  if (entry.type === 'event_msg' && type === 'agent_message') {
109
133
  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;
134
+ return text ? { at: eventAt, turn: { id: idOf(payload) ?? idOf(entry), at: eventAt, role: 'assistant', text, tools: [] } } : { at: eventAt, turn: null };
111
135
  }
136
+ if (entry.type === 'response_item' && type === 'message' && payload.role === 'assistant')
137
+ return { at: eventAt, turn: null };
112
138
  if (entry.type === 'response_item' && (type === 'custom_tool_call' || type === 'function_call')) {
113
139
  const id = string(payload.call_id ?? payload.id) ?? 'tool';
114
140
  return { at: eventAt, turn: { id: idOf(payload) ?? idOf(entry), at: eventAt, role: 'assistant', tools: [{ id, name: string(payload.name ?? payload.tool_name) ?? 'tool', input: payload.input === undefined && payload.arguments === undefined ? undefined : compact(payload.input ?? payload.arguments), outputLines: 0, outputBytes: 0 }] } };
@@ -215,9 +241,6 @@ export function codexAppServerStream() {
215
241
  return parsed;
216
242
  };
217
243
  }
218
- const blockText = (content) => typeof content === 'string'
219
- ? string(content)
220
- : items(content).map((block) => string(object(block)?.text)).filter(Boolean).join('\n') || null;
221
244
  export function piEvent(value) {
222
245
  const entry = object(value);
223
246
  const message = object(entry?.message);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spexcode/transcript",
3
- "version": "0.7.0-next.2",
3
+ "version": "0.7.0-next.4",
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": [