deepline 0.2.51 → 0.2.53

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.
@@ -819,7 +819,7 @@ export type MonitorsNamespace = {
819
819
  definition: MonitorDefinition,
820
820
  options?: { dryRun?: boolean },
821
821
  ) => Promise<MonitorDeployResult>;
822
- /** List deployed monitors (active by default). `includeConsumers` requires limit <= 20. */
822
+ /** List deployed monitors (active by default). `includeConsumers` requires a limit of 20 or fewer. */
823
823
  list: (options?: MonitorsListOptions) => Promise<MonitorsListResult>;
824
824
  /** Fetch one deployed monitor by public key with bounded current listener health. */
825
825
  get: (key: string) => Promise<MonitorDetail>;
@@ -160,7 +160,7 @@ export const SDK_RELEASE = {
160
160
  // 0.2.0 makes Dataset Handles uniformly async-only after 0.1.320 briefly
161
161
  // exposed storage-dependent synchronous access. This deliberate minor
162
162
  // release keeps lazy paging semantics independent of row residency.
163
- version: '0.2.51',
163
+ version: '0.2.53',
164
164
  contracts: {
165
165
  api: {
166
166
  name: 'sdk-http-api',
package/dist/cli/index.js CHANGED
@@ -1044,7 +1044,7 @@ var SDK_RELEASE = {
1044
1044
  // 0.2.0 makes Dataset Handles uniformly async-only after 0.1.320 briefly
1045
1045
  // exposed storage-dependent synchronous access. This deliberate minor
1046
1046
  // release keeps lazy paging semantics independent of row residency.
1047
- version: "0.2.51",
1047
+ version: "0.2.53",
1048
1048
  contracts: {
1049
1049
  api: {
1050
1050
  name: "sdk-http-api",
@@ -30191,9 +30191,46 @@ function selectiveCompactToolResults(raw) {
30191
30191
  return Buffer.from(lines.length > 0 ? `${lines.join("\n")}
30192
30192
  ` : "", "utf8");
30193
30193
  }
30194
+ function compactCodexEvents(raw) {
30195
+ const rawLines = normalizedJsonLines(raw);
30196
+ const parsedEvents = rawLines.map(parseJsonLine);
30197
+ const isCodex = parsedEvents.some(
30198
+ (event) => event && typeof event === "object" && event.type === "session_meta"
30199
+ );
30200
+ if (!isCodex) return raw;
30201
+ let lastTokenCountIndex = -1;
30202
+ parsedEvents.forEach((event, index) => {
30203
+ if (!event || typeof event !== "object") return;
30204
+ const payload = event.payload;
30205
+ if (payload && typeof payload === "object" && payload.type === "token_count") {
30206
+ lastTokenCountIndex = index;
30207
+ }
30208
+ });
30209
+ const lines = [];
30210
+ parsedEvents.forEach((event, index) => {
30211
+ if (!event || typeof event !== "object") {
30212
+ lines.push(rawLines[index] ?? "");
30213
+ return;
30214
+ }
30215
+ const record = event;
30216
+ if (record.type === "world_state") return;
30217
+ const payload = record.payload;
30218
+ const payloadType = payload && typeof payload === "object" ? String(payload.type ?? "") : "";
30219
+ if (payloadType === "token_count" && index !== lastTokenCountIndex) return;
30220
+ if (payloadType === "custom_tool_call_output" || payloadType === "function_call_output" || payloadType === "tool_search_output") {
30221
+ const record_payload = payload;
30222
+ record_payload.output = compactEventValue(record_payload.output);
30223
+ lines.push(JSON.stringify(record));
30224
+ return;
30225
+ }
30226
+ lines.push(rawLines[index] ?? "");
30227
+ });
30228
+ return Buffer.from(lines.length > 0 ? `${lines.join("\n")}
30229
+ ` : "", "utf8");
30230
+ }
30194
30231
  function prepareSessionBuffer(raw) {
30195
30232
  return selectiveCompactToolResults(
30196
- dedupConsecutiveEvents(stripNoiseEvents(raw))
30233
+ dedupConsecutiveEvents(compactCodexEvents(stripNoiseEvents(raw)))
30197
30234
  );
30198
30235
  }
30199
30236
  function buildSessionUploadContent(raw) {
@@ -1030,7 +1030,7 @@ var SDK_RELEASE = {
1030
1030
  // 0.2.0 makes Dataset Handles uniformly async-only after 0.1.320 briefly
1031
1031
  // exposed storage-dependent synchronous access. This deliberate minor
1032
1032
  // release keeps lazy paging semantics independent of row residency.
1033
- version: "0.2.51",
1033
+ version: "0.2.53",
1034
1034
  contracts: {
1035
1035
  api: {
1036
1036
  name: "sdk-http-api",
@@ -30244,9 +30244,46 @@ function selectiveCompactToolResults(raw) {
30244
30244
  return Buffer.from(lines.length > 0 ? `${lines.join("\n")}
30245
30245
  ` : "", "utf8");
30246
30246
  }
30247
+ function compactCodexEvents(raw) {
30248
+ const rawLines = normalizedJsonLines(raw);
30249
+ const parsedEvents = rawLines.map(parseJsonLine);
30250
+ const isCodex = parsedEvents.some(
30251
+ (event) => event && typeof event === "object" && event.type === "session_meta"
30252
+ );
30253
+ if (!isCodex) return raw;
30254
+ let lastTokenCountIndex = -1;
30255
+ parsedEvents.forEach((event, index) => {
30256
+ if (!event || typeof event !== "object") return;
30257
+ const payload = event.payload;
30258
+ if (payload && typeof payload === "object" && payload.type === "token_count") {
30259
+ lastTokenCountIndex = index;
30260
+ }
30261
+ });
30262
+ const lines = [];
30263
+ parsedEvents.forEach((event, index) => {
30264
+ if (!event || typeof event !== "object") {
30265
+ lines.push(rawLines[index] ?? "");
30266
+ return;
30267
+ }
30268
+ const record = event;
30269
+ if (record.type === "world_state") return;
30270
+ const payload = record.payload;
30271
+ const payloadType = payload && typeof payload === "object" ? String(payload.type ?? "") : "";
30272
+ if (payloadType === "token_count" && index !== lastTokenCountIndex) return;
30273
+ if (payloadType === "custom_tool_call_output" || payloadType === "function_call_output" || payloadType === "tool_search_output") {
30274
+ const record_payload = payload;
30275
+ record_payload.output = compactEventValue(record_payload.output);
30276
+ lines.push(JSON.stringify(record));
30277
+ return;
30278
+ }
30279
+ lines.push(rawLines[index] ?? "");
30280
+ });
30281
+ return Buffer.from(lines.length > 0 ? `${lines.join("\n")}
30282
+ ` : "", "utf8");
30283
+ }
30247
30284
  function prepareSessionBuffer(raw) {
30248
30285
  return selectiveCompactToolResults(
30249
- dedupConsecutiveEvents(stripNoiseEvents(raw))
30286
+ dedupConsecutiveEvents(compactCodexEvents(stripNoiseEvents(raw)))
30250
30287
  );
30251
30288
  }
30252
30289
  function buildSessionUploadContent(raw) {
package/dist/index.d.mts CHANGED
@@ -2201,7 +2201,7 @@ type MonitorsNamespace = {
2201
2201
  deploy: (definition: MonitorDefinition, options?: {
2202
2202
  dryRun?: boolean;
2203
2203
  }) => Promise<MonitorDeployResult>;
2204
- /** List deployed monitors (active by default). `includeConsumers` requires limit <= 20. */
2204
+ /** List deployed monitors (active by default). `includeConsumers` requires a limit of 20 or fewer. */
2205
2205
  list: (options?: MonitorsListOptions) => Promise<MonitorsListResult>;
2206
2206
  /** Fetch one deployed monitor by public key with bounded current listener health. */
2207
2207
  get: (key: string) => Promise<MonitorDetail>;
package/dist/index.d.ts CHANGED
@@ -2201,7 +2201,7 @@ type MonitorsNamespace = {
2201
2201
  deploy: (definition: MonitorDefinition, options?: {
2202
2202
  dryRun?: boolean;
2203
2203
  }) => Promise<MonitorDeployResult>;
2204
- /** List deployed monitors (active by default). `includeConsumers` requires limit <= 20. */
2204
+ /** List deployed monitors (active by default). `includeConsumers` requires a limit of 20 or fewer. */
2205
2205
  list: (options?: MonitorsListOptions) => Promise<MonitorsListResult>;
2206
2206
  /** Fetch one deployed monitor by public key with bounded current listener health. */
2207
2207
  get: (key: string) => Promise<MonitorDetail>;
package/dist/index.js CHANGED
@@ -763,7 +763,7 @@ var SDK_RELEASE = {
763
763
  // 0.2.0 makes Dataset Handles uniformly async-only after 0.1.320 briefly
764
764
  // exposed storage-dependent synchronous access. This deliberate minor
765
765
  // release keeps lazy paging semantics independent of row residency.
766
- version: "0.2.51",
766
+ version: "0.2.53",
767
767
  contracts: {
768
768
  api: {
769
769
  name: "sdk-http-api",
package/dist/index.mjs CHANGED
@@ -689,7 +689,7 @@ var SDK_RELEASE = {
689
689
  // 0.2.0 makes Dataset Handles uniformly async-only after 0.1.320 briefly
690
690
  // exposed storage-dependent synchronous access. This deliberate minor
691
691
  // release keeps lazy paging semantics independent of row residency.
692
- version: "0.2.51",
692
+ version: "0.2.53",
693
693
  contracts: {
694
694
  api: {
695
695
  name: "sdk-http-api",
@@ -341,10 +341,11 @@ body {
341
341
  min-width: 70px;
342
342
  }
343
343
  .tool-name.bash { color: var(--blue); }
344
- .tool-name.read, .tool-name.write, .tool-name.edit { color: var(--purple); }
344
+ .tool-name.read, .tool-name.write, .tool-name.edit, .tool-name.delete { color: var(--purple); }
345
345
  .tool-name.skill, .tool-name.task { color: var(--orange); }
346
- .tool-name.grep, .tool-name.glob { color: var(--cyan); }
346
+ .tool-name.grep, .tool-name.glob, .tool-name.toolsearch { color: var(--cyan); }
347
347
  .tool-name.webfetch, .tool-name.websearch { color: var(--yellow); }
348
+ .tool-name.wait, .tool-name.stdin { color: var(--text-dim); }
348
349
  .tool-cmd {
349
350
  color: var(--text-dim);
350
351
  overflow: hidden;
@@ -881,6 +882,14 @@ body {
881
882
  .expectation-result.pass { color: var(--green); }
882
883
  .expectation-result.fail { color: var(--red); }
883
884
  .expectation-result.unknown { color: var(--yellow); }
885
+ .expectation-failure-reason {
886
+ margin-top: 5px;
887
+ color: color-mix(in srgb, var(--red) 80%, var(--text-dim));
888
+ font-size: 10px;
889
+ font-weight: 400;
890
+ line-height: 1.4;
891
+ overflow-wrap: anywhere;
892
+ }
884
893
  .infra-signals {
885
894
  margin-top: 10px;
886
895
  border: 1px solid color-mix(in srgb, var(--red) 45%, var(--viewer-border));
@@ -899,6 +908,52 @@ body {
899
908
  .lane-prompt, .lane-result { border-bottom: 1px solid var(--viewer-border); }
900
909
  .lane-prompt summary, .lane-result summary { cursor: pointer; padding: 8px 13px; color: var(--text-dim); font-size: 11px; font-weight: 650; }
901
910
  .lane-prompt .prompt-text, .lane-result .md-rendered { padding: 0 13px 12px; font-size: 12px; }
911
+
912
+ .final-artifacts {
913
+ margin: 18px 0 4px;
914
+ border: 1px solid var(--viewer-border);
915
+ border-radius: 8px;
916
+ background: var(--bg-secondary);
917
+ overflow: hidden;
918
+ }
919
+ .final-artifacts-heading {
920
+ padding: 12px 14px;
921
+ border-bottom: 1px solid var(--viewer-border);
922
+ background: var(--bg-tertiary);
923
+ }
924
+ .final-artifacts-heading > div { display: flex; align-items: center; gap: 8px; }
925
+ .final-artifacts-heading span { font-size: 12px; font-weight: 700; color: var(--text); text-transform: uppercase; letter-spacing: .04em; }
926
+ .final-artifacts-heading strong { color: var(--viewer-primary); font-size: 12px; }
927
+ .final-artifacts-heading p { margin: 4px 0 0; color: var(--text-dim); font-size: 11px; }
928
+ .final-artifact-file { border-bottom: 1px solid var(--viewer-border); }
929
+ .final-artifact-file:last-child { border-bottom: 0; }
930
+ .final-artifact-file summary {
931
+ display: flex;
932
+ justify-content: space-between;
933
+ gap: 16px;
934
+ cursor: pointer;
935
+ padding: 10px 14px;
936
+ color: var(--text);
937
+ font-size: 12px;
938
+ }
939
+ .final-artifact-file summary span { color: var(--text-dim); font-weight: 400; }
940
+ .final-artifact-meta { padding: 0 14px 9px; color: var(--text-dim); font-size: 10px; }
941
+ .final-artifact-meta code { color: var(--text-dim); overflow-wrap: anywhere; }
942
+ .final-artifact-meta span { margin-left: 10px; color: var(--viewer-primary); }
943
+ .final-artifact-file pre {
944
+ max-height: 560px;
945
+ overflow: auto;
946
+ margin: 0;
947
+ padding: 13px 14px;
948
+ border-top: 1px solid var(--viewer-border);
949
+ background: var(--bg);
950
+ color: var(--text);
951
+ white-space: pre;
952
+ font-size: 11px;
953
+ line-height: 1.5;
954
+ }
955
+ .final-artifact-omitted { padding: 0 14px 12px; color: var(--text-dim); font-size: 11px; }
956
+ .lane-session .final-artifacts { margin: 0; border-width: 1px 0 0; border-radius: 0; }
902
957
  .lane-timeline { padding: 10px 7px 16px; }
903
958
  .lane-timeline .tool-row { gap: 8px; padding-left: 5px; padding-right: 5px; font-size: 11px; }
904
959
  .lane-timeline .tool-name { min-width: 58px; }
@@ -78,7 +78,7 @@ function extractSessionMetaCodex(events) {
78
78
  for (const d of events) {
79
79
  if (d.type !== 'session_meta') continue;
80
80
  const p = d.payload || {};
81
- meta.model = p.model || p.model_provider || null;
81
+ meta.model = p.model || null;
82
82
  meta.session_id = p.id || null;
83
83
  meta.cwd = p.cwd || null;
84
84
  meta.environment = {
@@ -89,9 +89,42 @@ function extractSessionMetaCodex(events) {
89
89
  };
90
90
  break;
91
91
  }
92
+ // session_meta usually carries only model_provider ("openai"); the real model
93
+ // name lives on the first turn_context.
94
+ if (!meta.model) {
95
+ for (const d of events) {
96
+ if (d.type !== 'turn_context') continue;
97
+ const p = d.payload || {};
98
+ if (typeof p.model === 'string' && p.model) { meta.model = p.model; break; }
99
+ }
100
+ }
101
+ if (!meta.model) {
102
+ for (const d of events) {
103
+ if (d.type !== 'session_meta') continue;
104
+ meta.model = (d.payload || {}).model_provider || null;
105
+ break;
106
+ }
107
+ }
92
108
  return meta;
93
109
  }
94
110
 
111
+ function extractResultCodex(events) {
112
+ let result = null;
113
+ for (const d of events) {
114
+ const p = d.payload || {};
115
+ if (d.type !== 'event_msg' || p.type !== 'task_complete') continue;
116
+ result = {
117
+ is_error: false,
118
+ duration_ms: Number(p.duration_ms) || 0,
119
+ duration_api_ms: 0,
120
+ num_turns: null,
121
+ result_text: String(p.last_agent_message || '').slice(0, 2000),
122
+ total_cost_usd: null,
123
+ };
124
+ }
125
+ return result;
126
+ }
127
+
95
128
  function extractResultStream(events) {
96
129
  for (let i = events.length - 1; i >= 0; i--) {
97
130
  const d = events[i];
@@ -192,15 +225,43 @@ function codexContentText(content) {
192
225
  return parts.join('\n').trim();
193
226
  }
194
227
 
228
+ // Codex injects several synthetic "user" turns before the real prompt (plugin
229
+ // catalog, AGENTS.md, environment context). They are transport, not intent —
230
+ // the Claude path skips its own equivalent ("Base directory for this skill:").
231
+ const CODEX_SYNTHETIC_PREFIXES = [
232
+ '<recommended_plugins>',
233
+ '<environment_context>',
234
+ '<INSTRUCTIONS>',
235
+ '<user_instructions>',
236
+ '<skills_instructions>',
237
+ '<permissions instructions>',
238
+ '# AGENTS.md instructions',
239
+ ];
240
+
241
+ function isCodexSyntheticMessage(text) {
242
+ const trimmed = String(text || '').replace(/^\s+/, '');
243
+ if (!trimmed) return true;
244
+ for (const prefix of CODEX_SYNTHETIC_PREFIXES) {
245
+ if (trimmed.startsWith(prefix)) return true;
246
+ }
247
+ return false;
248
+ }
249
+
195
250
  function extractPromptCodex(events) {
251
+ // event_msg carries the prompt the user actually typed; response_item
252
+ // user messages also include the injected preambles, so they are a fallback.
196
253
  for (const d of events) {
197
254
  const p = d.payload || {};
198
- if (d.type === 'event_msg' && p.type === 'user_message' && typeof p.message === 'string') {
255
+ if (d.type === 'event_msg' && p.type === 'user_message'
256
+ && typeof p.message === 'string' && !isCodexSyntheticMessage(p.message)) {
199
257
  return p.message;
200
258
  }
259
+ }
260
+ for (const d of events) {
261
+ const p = d.payload || {};
201
262
  if (d.type === 'response_item' && p.type === 'message' && p.role === 'user') {
202
263
  const text = codexContentText(p.content);
203
- if (text) return text;
264
+ if (text && !isCodexSyntheticMessage(text)) return text;
204
265
  }
205
266
  }
206
267
  return null;
@@ -476,33 +537,395 @@ function codexToolName(p) {
476
537
  return p.name || p.tool_name || p.type || 'tool';
477
538
  }
478
539
 
479
- function codexToolCommand(name, input) {
480
- const normalized = String(name || '').toLowerCase();
481
- if (typeof input.cmd === 'string') return input.cmd;
482
- if (typeof input.command === 'string') return input.command;
483
- if (typeof input.query === 'string') return input.query;
484
- if (typeof input.url === 'string') return input.url;
485
- if (typeof input.path === 'string') return input.path;
486
- if (typeof input.file_path === 'string') return input.file_path;
487
- if (typeof input.arguments === 'string') return input.arguments.slice(0, 120);
488
- if (normalized === 'apply patch') return 'apply patch';
489
- return JSON.stringify(input).slice(0, 120);
490
- }
491
-
492
540
  function codexSkillReadName(command) {
493
- const match = String(command || '').match(/\/(?:\.agents|\.claude|\.codex)\/skills\/([^/\s'"]+)\/SKILL\.md\b/);
541
+ // The path is often workspace-relative (".agents/skills/x/SKILL.md"), so do
542
+ // not require a leading slash.
543
+ const match = String(command || '')
544
+ .match(/(?:^|[/\s'"])\.(?:agents|claude|codex)\/skills\/([^/\s'"]+)\/SKILL\.md\b/);
494
545
  return match ? match[1] : null;
495
546
  }
496
547
 
548
+ // ---------------------------------------------------------------------------
549
+ // Codex tool-call normalization
550
+ //
551
+ // Subscription Codex does not send structured tool arguments. It sends a
552
+ // JavaScript snippet that calls tools.exec_command / tools.apply_patch and
553
+ // prints the result, and returns the result as an array of content blocks
554
+ // wrapped in a "Script completed / Wall time / Output:" preamble.
555
+ //
556
+ // Everything below unwraps that into the same {tool, command, input,
557
+ // result_content, is_error} shape buildTimeline() produces for Claude
558
+ // transcripts, so both agents share renderTimeline(), toolCommandSummary(),
559
+ // formatToolInputHtml() and buildFileTouchMap().
560
+ // ---------------------------------------------------------------------------
561
+
562
+ const CODEX_SHELL_TOOLS = [
563
+ 'exec', 'exec_command', 'execcommand', 'shell', 'bash',
564
+ 'unified_exec', 'local_shell', 'run_command', 'container.exec',
565
+ ];
566
+
567
+ // Walks a JS source string from an opening bracket to its match, respecting
568
+ // string literals and escapes. Returns the index just past the closer, or -1.
569
+ function codexScanBalanced(src, start) {
570
+ const closers = {'(': ')', '[': ']', '{': '}'};
571
+ const stack = [];
572
+ let quote = null;
573
+ for (let i = start; i < src.length; i++) {
574
+ const ch = src[i];
575
+ if (quote) {
576
+ if (ch === '\\') { i++; continue; }
577
+ if (ch === quote) quote = null;
578
+ continue;
579
+ }
580
+ if (ch === '"' || ch === "'" || ch === '`') { quote = ch; continue; }
581
+ if (closers[ch]) { stack.push(closers[ch]); continue; }
582
+ if (ch === ')' || ch === ']' || ch === '}') {
583
+ if (!stack.length || stack.pop() !== ch) return -1;
584
+ if (!stack.length) return i + 1;
585
+ }
586
+ }
587
+ return -1;
588
+ }
589
+
590
+ function codexScanString(src, start) {
591
+ const quote = src[start];
592
+ for (let i = start + 1; i < src.length; i++) {
593
+ if (src[i] === '\\') { i++; continue; }
594
+ if (src[i] === quote) return i + 1;
595
+ }
596
+ return -1;
597
+ }
598
+
599
+ function codexParseLiteral(text) {
600
+ const trimmed = String(text || '').trim();
601
+ if (!trimmed) return null;
602
+ const first = trimmed[0];
603
+ if (first === '{' || first === '[' || first === '"') {
604
+ try { return JSON.parse(trimmed); } catch(e) { /* not a JSON literal */ }
605
+ }
606
+ if (first === "'" && trimmed[trimmed.length - 1] === "'") {
607
+ const body = trimmed.slice(1, -1).replace(/\\'/g, "'").replace(/"/g, '\\"');
608
+ try { return JSON.parse('"' + body + '"'); } catch(e) { /* not a JS string */ }
609
+ }
610
+ return null;
611
+ }
612
+
613
+ // Codex often binds the patch or args to a const first, then passes the name.
614
+ function codexLocalConstants(src) {
615
+ const constants = {};
616
+ const re = /(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*/g;
617
+ let match;
618
+ while ((match = re.exec(src))) {
619
+ const valueStart = match.index + match[0].length;
620
+ const ch = src[valueStart];
621
+ let end = -1;
622
+ if (ch === '{' || ch === '[') end = codexScanBalanced(src, valueStart);
623
+ else if (ch === '"' || ch === "'") end = codexScanString(src, valueStart);
624
+ if (end < 0) continue;
625
+ const parsed = codexParseLiteral(src.slice(valueStart, end));
626
+ if (parsed !== null) constants[match[1]] = parsed;
627
+ re.lastIndex = end;
628
+ }
629
+ return constants;
630
+ }
631
+
632
+ // Spans of string literals in the snippet. A patch payload is itself a string,
633
+ // and the file it writes can contain `tools.execute(...)` in its own source —
634
+ // matching that would invent tool calls the agent never made.
635
+ function codexStringSpans(src) {
636
+ const spans = [];
637
+ for (let i = 0; i < src.length; i++) {
638
+ const ch = src[i];
639
+ if (ch !== '"' && ch !== "'" && ch !== '`') continue;
640
+ const end = codexScanString(src, i);
641
+ if (end < 0) break;
642
+ spans.push([i, end]);
643
+ i = end - 1;
644
+ }
645
+ return spans;
646
+ }
647
+
648
+ function parseCodexExecScript(source) {
649
+ const src = String(source || '');
650
+ if (src.indexOf('tools.') < 0) return [];
651
+ const constants = codexLocalConstants(src);
652
+ const stringSpans = codexStringSpans(src);
653
+ const insideString = (index) =>
654
+ stringSpans.some(([start, end]) => index >= start && index < end);
655
+ const calls = [];
656
+ const re = /tools\.([A-Za-z_$][\w$]*)\s*\(/g;
657
+ let match;
658
+ while ((match = re.exec(src))) {
659
+ if (insideString(match.index)) continue;
660
+ const openParen = match.index + match[0].length - 1;
661
+ const end = codexScanBalanced(src, openParen);
662
+ if (end < 0) continue;
663
+ const argText = src.slice(openParen + 1, end - 1).trim();
664
+ let args = codexParseLiteral(argText);
665
+ if (args === null && /^[A-Za-z_$][\w$]*$/.test(argText) && Object.prototype.hasOwnProperty.call(constants, argText)) {
666
+ args = constants[argText];
667
+ }
668
+ if (args === null) args = argText ? {source: argText} : {};
669
+ calls.push({tool: match[1], args});
670
+ re.lastIndex = end;
671
+ }
672
+ return calls;
673
+ }
674
+
675
+ function codexCommandFromArgs(args) {
676
+ if (typeof args === 'string') return args;
677
+ if (!args || typeof args !== 'object') return '';
678
+ if (typeof args.cmd === 'string') return args.cmd;
679
+ if (typeof args.command === 'string') return args.command;
680
+ if (Array.isArray(args.cmd)) return args.cmd.join(' ');
681
+ if (Array.isArray(args.command)) return args.command.join(' ');
682
+ if (typeof args.script === 'string') return args.script;
683
+ return '';
684
+ }
685
+
686
+ function codexPatchText(args) {
687
+ if (typeof args === 'string') return args;
688
+ if (!args || typeof args !== 'object') return '';
689
+ for (const key of ['patch', 'input', 'source', 'diff', 'content']) {
690
+ if (typeof args[key] === 'string') return args[key];
691
+ }
692
+ return '';
693
+ }
694
+
695
+ function parseCodexPatch(patchText) {
696
+ const text = String(patchText || '');
697
+ const ops = [];
698
+ if (text.indexOf('***') < 0) return ops;
699
+ let current = null;
700
+ const flush = () => { if (current) ops.push(current); current = null; };
701
+ for (const line of text.split('\n')) {
702
+ const header = line.match(/^\*\*\* (Add|Update|Delete) File:\s*(.+)$/);
703
+ if (header) {
704
+ flush();
705
+ current = {op: header[1].toLowerCase(), file_path: header[2].trim(), added: [], removed: []};
706
+ continue;
707
+ }
708
+ if (/^\*\*\* (Begin|End) Patch/.test(line)) { flush(); continue; }
709
+ if (!current) continue;
710
+ if (line[0] === '+') current.added.push(line.slice(1));
711
+ else if (line[0] === '-') current.removed.push(line.slice(1));
712
+ }
713
+ flush();
714
+ return ops;
715
+ }
716
+
717
+ // Maps one codex tool invocation onto one or more Claude-shaped entries.
718
+ // `seenSkills` makes the first read of a skill's SKILL.md the Skill invocation
719
+ // (Claude emits exactly one Skill call per skill); later paged reads of the same
720
+ // file stay Bash, so sequential `sed -n '241,520p'` reads are not mistaken for a
721
+ // retry loop by annotateTimeline().
722
+ function codexToolIdentity(rawName, args, seenSkills) {
723
+ const name = String(rawName || '').toLowerCase();
724
+
725
+ if (CODEX_SHELL_TOOLS.indexOf(name) >= 0) {
726
+ const command = codexCommandFromArgs(args);
727
+ if (!command) return [{tool: 'Bash', input: parseCodexArguments(args)}];
728
+ const skill = codexSkillReadName(command);
729
+ if (skill && (!seenSkills || !seenSkills.has(skill))) {
730
+ if (seenSkills) seenSkills.add(skill);
731
+ return [{tool: 'Skill', input: {skill, command}, command: skill}];
732
+ }
733
+ return [{tool: 'Bash', input: {command}}];
734
+ }
735
+
736
+ if (name === 'apply_patch' || name === 'applypatch' || name === 'edit_file') {
737
+ const ops = parseCodexPatch(codexPatchText(args));
738
+ if (ops.length) {
739
+ return ops.map(op => {
740
+ if (op.op === 'add') {
741
+ return {tool: 'Write', input: {file_path: op.file_path, content: op.added.join('\n')}};
742
+ }
743
+ if (op.op === 'delete') {
744
+ return {tool: 'Delete', input: {file_path: op.file_path}, command: op.file_path};
745
+ }
746
+ return {tool: 'Edit', input: {
747
+ file_path: op.file_path,
748
+ old_string: op.removed.join('\n'),
749
+ new_string: op.added.join('\n'),
750
+ }};
751
+ });
752
+ }
753
+ return [{tool: 'Write', input: parseCodexArguments(args)}];
754
+ }
755
+
756
+ if (name === 'wait') {
757
+ const input = parseCodexArguments(args);
758
+ const cell = input.cell_id != null ? String(input.cell_id) : '';
759
+ return [{tool: 'Wait', input, command: cell ? 'cell ' + cell : 'wait'}];
760
+ }
761
+
762
+ // Feeds keystrokes to a still-running exec cell; the Wait row shows the result.
763
+ if (name === 'write_stdin' || name === 'writestdin') {
764
+ const input = parseCodexArguments(args);
765
+ const cell = input.session_id != null ? String(input.session_id) : '';
766
+ return [{tool: 'Stdin', input, command: cell ? 'cell ' + cell : 'stdin'}];
767
+ }
768
+
769
+ if (name === 'read_file' || name === 'view_file' || name === 'read') {
770
+ const input = parseCodexArguments(args);
771
+ return [{tool: 'Read', input: {file_path: input.file_path || input.path || ''}}];
772
+ }
773
+
774
+ if (name === 'write_file' || name === 'create_file') {
775
+ const input = parseCodexArguments(args);
776
+ return [{tool: 'Write', input: {
777
+ file_path: input.file_path || input.path || '',
778
+ content: input.content || input.contents || '',
779
+ }}];
780
+ }
781
+
782
+ // Codex's web tool batches work into one call: `search_query` is a list of
783
+ // searches, `open` a list of pages. Each becomes its own step, the same way
784
+ // Claude issues one WebSearch/WebFetch tool_use per query or URL.
785
+ if (name === 'web__run' || name === 'web_run' || name === 'webrun') {
786
+ const input = parseCodexArguments(args);
787
+ const entries = [];
788
+ for (const item of asArray(input.search_query)) {
789
+ const query = typeof item === 'string' ? item : (item && (item.q || item.query)) || '';
790
+ if (query) entries.push({tool: 'WebSearch', input: {query}});
791
+ }
792
+ for (const item of asArray(input.open)) {
793
+ const url = typeof item === 'string' ? item : (item && (item.ref_id || item.url)) || '';
794
+ if (url) entries.push({tool: 'WebFetch', input: {url}});
795
+ }
796
+ if (entries.length) return entries;
797
+ return [{tool: 'WebSearch', input}];
798
+ }
799
+
800
+ if (name === 'web_search' || name === 'websearch' || name === 'web_search_call') {
801
+ const input = parseCodexArguments(args);
802
+ return [{tool: 'WebSearch', input: {query: input.query || input.q || ''}}];
803
+ }
804
+
805
+ if (name === 'tool_search' || name === 'tool_search_call') {
806
+ const input = parseCodexArguments(args);
807
+ return [{tool: 'ToolSearch', input: {query: input.query || input.q || ''}}];
808
+ }
809
+
810
+ return [{tool: normalizeStepToolName(rawName), input: parseCodexArguments(args)}];
811
+ }
812
+
813
+ function codexCallEntries(p, seenSkills) {
814
+ const rawArgs = p.arguments != null ? p.arguments : p.input;
815
+
816
+ // Unwrap the JS wrapper first; it is the only place the real command lives.
817
+ if (typeof rawArgs === 'string') {
818
+ const nested = parseCodexExecScript(rawArgs);
819
+ if (nested.length) {
820
+ const entries = [];
821
+ for (const call of nested) {
822
+ for (const entry of codexToolIdentity(call.tool, call.args, seenSkills)) entries.push(entry);
823
+ }
824
+ if (entries.length) return entries;
825
+ }
826
+ }
827
+
828
+ if (p.type === 'web_search_call' || p.type === 'tool_search_call') {
829
+ return codexToolIdentity(p.type, rawArgs != null ? rawArgs : (p.action || p.query || {}), seenSkills);
830
+ }
831
+ return codexToolIdentity(codexToolName(p), rawArgs, seenSkills);
832
+ }
833
+
834
+ // A wrapper that fans out with Promise.all prints the outputs joined by a
835
+ // separator of its own choosing, e.g. `.join("\n\n---NEXT---\n\n")`. Recover the
836
+ // separator from the source so each parallel step shows only its own output.
837
+ function codexSplitJoinedOutput(source, text, count) {
838
+ if (count < 2 || !text) return null;
839
+ const match = String(source || '').match(/\.join\(\s*("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*')\s*\)/);
840
+ if (!match) return null;
841
+ const separator = codexParseLiteral(match[1]);
842
+ if (typeof separator !== 'string' || !separator.trim()) return null;
843
+ const parts = String(text).split(separator);
844
+ return parts.length === count ? parts : null;
845
+ }
846
+
847
+ function codexOutputBlocksText(output) {
848
+ if (typeof output === 'string') return output;
849
+ if (!Array.isArray(output)) return '';
850
+ const parts = [];
851
+ for (const block of output) {
852
+ if (typeof block === 'string') { parts.push(block); continue; }
853
+ if (!block || typeof block !== 'object') continue;
854
+ if (typeof block.text === 'string') parts.push(block.text);
855
+ else if (typeof block.output_text === 'string') parts.push(block.output_text);
856
+ else if (typeof block.content === 'string') parts.push(block.content);
857
+ }
858
+ return parts.join('');
859
+ }
860
+
497
861
  function codexToolOutputText(p) {
498
862
  if (typeof p.output === 'string') return p.output;
863
+ if (Array.isArray(p.output)) return codexOutputBlocksText(p.output);
499
864
  if (typeof p.content === 'string') return p.content;
865
+ if (Array.isArray(p.content)) return codexOutputBlocksText(p.content);
500
866
  if (typeof p.message === 'string') return p.message;
501
867
  if (p.output && typeof p.output === 'object') return JSON.stringify(p.output, null, 2);
502
868
  if (p.content && typeof p.content === 'object') return JSON.stringify(p.content, null, 2);
503
869
  return '';
504
870
  }
505
871
 
872
+ // Strips the wrapper's "Script completed / Wall time / Output:" preamble and,
873
+ // when the snippet printed JSON.stringify({exit_code, output}), unwraps that too.
874
+ function normalizeCodexOutput(rawText) {
875
+ let text = String(rawText || '');
876
+ let pending = false;
877
+ let wallS = null;
878
+ let exitCode = null;
879
+
880
+ const preamble = text.match(/^Script (completed|running with cell ID [^\n]*)\n(?:Wall time ([\d.]+) seconds\n)?Output:\n?/);
881
+ if (preamble) {
882
+ pending = preamble[1] !== 'completed';
883
+ wallS = preamble[2] ? Number(preamble[2]) : null;
884
+ text = text.slice(preamble[0].length);
885
+ }
886
+
887
+ const trimmed = text.trim();
888
+ if (trimmed.startsWith('{') && trimmed.indexOf('"exit_code"') >= 0) {
889
+ try {
890
+ const parsed = JSON.parse(trimmed);
891
+ if (parsed && typeof parsed === 'object' && parsed.exit_code != null) {
892
+ exitCode = Number(parsed.exit_code);
893
+ text = typeof parsed.output === 'string' ? parsed.output : JSON.stringify(parsed, null, 2);
894
+ }
895
+ } catch(e) { /* not the wrapper's JSON envelope */ }
896
+ }
897
+
898
+ if (pending) {
899
+ const note = '(backgrounded' + (wallS != null ? ' after ' + wallS + 's' : '')
900
+ + ' — output arrives with the following Wait call)';
901
+ text = text.trim() ? note + '\n\n' + text : note;
902
+ }
903
+ return {text, pending, wall_s: wallS, exit_code: exitCode};
904
+ }
905
+
906
+ // Same markers scripts/eval-extract-metrics.py uses: the wrapper always reports
907
+ // "Script completed", so failure has to be read out of the command output.
908
+ function codexTextIndicatesFailure(text) {
909
+ const s = String(text || '');
910
+ const exited = s.match(/Process exited with code\s+(\d+)/);
911
+ if (exited) return Number(exited[1]) !== 0;
912
+ const exitCode = s.match(/^\s*exit_code:\s*(\d+)/m);
913
+ if (exitCode) return Number(exitCode[1]) !== 0;
914
+ if (/^Error:/m.test(s)) return true;
915
+ return false;
916
+ }
917
+
918
+ function codexOutputResult(p) {
919
+ const normalized = normalizeCodexOutput(codexToolOutputText(p));
920
+ let isError;
921
+ if (p.is_error === true || p.status === 'error' || p.status === 'failed') isError = true;
922
+ else if (normalized.exit_code != null) isError = normalized.exit_code !== 0;
923
+ else if (normalized.pending) isError = null;
924
+ else if (codexTextIndicatesFailure(normalized.text)) isError = true;
925
+ else isError = false;
926
+ return {text: normalized.text, is_error: isError};
927
+ }
928
+
506
929
  function buildCodexTimeline(events, firstTs) {
507
930
  const outputsByCallId = {};
508
931
  const hasEventUserMessages = events.some(d => {
@@ -519,12 +942,25 @@ function buildCodexTimeline(events, firstTs) {
519
942
  if (p.type !== 'function_call_output' && p.type !== 'custom_tool_call_output' && p.type !== 'tool_search_output') continue;
520
943
  const callId = p.call_id || p.id || '';
521
944
  if (!callId) continue;
522
- outputsByCallId[callId] = {
523
- text: codexToolOutputText(p),
524
- is_error: p.is_error === true || p.status === 'error' || p.status === 'failed',
525
- };
945
+ outputsByCallId[callId] = codexOutputResult(p);
526
946
  }
527
947
 
948
+ // patch_apply_end carries the authoritative result of an apply_patch, but its
949
+ // call_id ("exec-<uuid>") does not match the originating call ("call_<id>"),
950
+ // so it is consumed in document order instead.
951
+ const patchResults = [];
952
+ for (const d of events) {
953
+ const p = d.payload || {};
954
+ if (d.type === 'event_msg' && p.type === 'patch_apply_end') {
955
+ patchResults.push({
956
+ text: String(p.stdout || '') + (p.stderr ? '\n' + p.stderr : ''),
957
+ is_error: p.success === false,
958
+ });
959
+ }
960
+ }
961
+ let patchResultIndex = 0;
962
+ const seenSkills = new Set();
963
+
528
964
  const timeline = [];
529
965
  let step = 0;
530
966
  let eventIndex = 0;
@@ -549,7 +985,9 @@ function buildCodexTimeline(events, firstTs) {
549
985
  if (p.role === 'user') {
550
986
  if (hasEventUserMessages) continue;
551
987
  const text = codexContentText(p.content);
552
- if (text) timeline.push({kind: 'user_message', text, event_index: eventIndex, elapsed});
988
+ if (text && !isCodexSyntheticMessage(text)) {
989
+ timeline.push({kind: 'user_message', text, event_index: eventIndex, elapsed});
990
+ }
553
991
  } else if (p.role === 'assistant') {
554
992
  if (hasEventAgentMessages) continue;
555
993
  const text = codexContentText(p.content);
@@ -572,31 +1010,44 @@ function buildCodexTimeline(events, firstTs) {
572
1010
  || p.type === 'web_search_call';
573
1011
  if (!isToolCall) continue;
574
1012
 
575
- step++;
576
- const input = parseCodexArguments(p.arguments || p.input);
577
1013
  const callId = p.call_id || p.id || '';
578
1014
  const output = outputsByCallId[callId] || null;
579
- let tool = normalizeStepToolName(codexToolName(p));
580
- let command = codexToolCommand(tool, input);
581
- const skillName = codexSkillReadName(command);
582
- if (skillName) {
583
- tool = 'Skill';
584
- command = skillName;
585
- input.skill = skillName;
586
- input.command = input.command || input.cmd || command;
1015
+ const entries = codexCallEntries(p, seenSkills);
1016
+
1017
+ // One record can carry several commands (Promise.all of exec_command, or a
1018
+ // multi-file patch). Each becomes its own step flagged `parallel`, mirroring
1019
+ // how buildTimeline() expands multiple tool_use blocks from one Claude
1020
+ // assistant message. scripts/eval-extract-metrics.py still counts one call
1021
+ // per record, so the viewer's tool-call total can legitimately exceed the
1022
+ // tool_calls value in results.jsonl. Do not "reconcile" one to the other.
1023
+ const parallel = entries.length > 1;
1024
+ const rawArgs = p.arguments != null ? p.arguments : p.input;
1025
+ const splitOutputs = output
1026
+ ? codexSplitJoinedOutput(rawArgs, output.text, entries.length)
1027
+ : null;
1028
+
1029
+ for (let entryIndex = 0; entryIndex < entries.length; entryIndex++) {
1030
+ const entry = entries[entryIndex];
1031
+ step++;
1032
+ const isPatch = entry.tool === 'Write' || entry.tool === 'Edit' || entry.tool === 'Delete';
1033
+ const patchResult = isPatch && patchResultIndex < patchResults.length
1034
+ ? patchResults[patchResultIndex++]
1035
+ : null;
1036
+ const resolved = patchResult
1037
+ || (splitOutputs ? {text: splitOutputs[entryIndex], is_error: output.is_error} : output);
1038
+ timeline.push({
1039
+ kind: 'tool_call',
1040
+ step,
1041
+ tool: entry.tool,
1042
+ command: entry.command != null ? entry.command : toolCommandSummary(entry.tool, entry.input),
1043
+ input: entry.input,
1044
+ result_content: resolved ? resolved.text : null,
1045
+ is_error: resolved ? resolved.is_error : null,
1046
+ parallel,
1047
+ event_index: eventIndex,
1048
+ elapsed,
1049
+ });
587
1050
  }
588
- timeline.push({
589
- kind: 'tool_call',
590
- step,
591
- tool,
592
- command,
593
- input,
594
- result_content: output ? output.text : null,
595
- is_error: output ? output.is_error : null,
596
- parallel: false,
597
- event_index: eventIndex,
598
- elapsed,
599
- });
600
1051
  }
601
1052
  return timeline;
602
1053
  }
@@ -831,7 +1282,7 @@ function processRawSession(raw) {
831
1282
  } else if (isCodex) {
832
1283
  processedEvents = events;
833
1284
  meta = extractSessionMetaCodex(events);
834
- result = null;
1285
+ result = extractResultCodex(events);
835
1286
  const [dur, fts] = computeCodexDuration(events);
836
1287
  totalDurationS = Math.round(dur * 10) / 10;
837
1288
  firstTs = fts;
@@ -1346,7 +1797,10 @@ function renderExpectationMiniTable(session) {
1346
1797
  const rows = expectations.map(item => {
1347
1798
  const result = String(item.result || '—').toUpperCase();
1348
1799
  const resultClass = result === 'PASS' ? 'pass' : (result === 'FAIL' ? 'fail' : 'unknown');
1349
- return `<tr title="${esc(item.reason || '')}"><td>${esc(item.expectation || '—')}</td><td>${esc(item.category || 'other')}</td><td><span class="expectation-result ${resultClass}">${esc(result)}</span></td></tr>`;
1800
+ const reason = result === 'FAIL' && item.reason
1801
+ ? `<div class="expectation-failure-reason">${esc(item.reason)}</div>`
1802
+ : '';
1803
+ return `<tr><td>${esc(item.expectation || '—')}${reason}</td><td>${esc(item.category || 'other')}</td><td><span class="expectation-result ${resultClass}">${esc(result)}</span></td></tr>`;
1350
1804
  }).join('');
1351
1805
  return `<div class="expectations-mini ${statusClass}">
1352
1806
  <div class="expectations-mini-heading"><span>Eval results</span><strong>${passed}/${total} passed</strong></div>
@@ -1370,6 +1824,51 @@ function renderInfraSignals(metric) {
1370
1824
  return `<div class="infra-signals"><div class="infra-signals-heading">Infrastructure signals</div><ul>${rows}</ul></div>`;
1371
1825
  }
1372
1826
 
1827
+ function formatFileSize(bytes) {
1828
+ const value = Number(bytes || 0);
1829
+ if (value < 1024) return value + ' B';
1830
+ if (value < 1024 * 1024) return (value / 1024).toFixed(1) + ' KB';
1831
+ return (value / (1024 * 1024)).toFixed(1) + ' MB';
1832
+ }
1833
+
1834
+ function artifactDisplayContent(artifact) {
1835
+ const content = typeof artifact.content === 'string' ? artifact.content : null;
1836
+ if (content == null) return null;
1837
+ const language = String(artifact.language || '').toLowerCase();
1838
+ try {
1839
+ if (language === 'json') return JSON.stringify(JSON.parse(content), null, 2);
1840
+ if (language === 'jsonl') {
1841
+ return content.split('\n').filter(line => line.trim()).map(line => JSON.stringify(JSON.parse(line), null, 2)).join('\n');
1842
+ }
1843
+ } catch (_) {
1844
+ return content;
1845
+ }
1846
+ return content;
1847
+ }
1848
+
1849
+ function renderFinalArtifacts(session) {
1850
+ const metric = session.evalMetric || {};
1851
+ const artifacts = Array.isArray(metric.final_artifacts) ? metric.final_artifacts : [];
1852
+ if (!artifacts.length) return '';
1853
+ const files = artifacts.map((artifact, index) => {
1854
+ const content = artifactDisplayContent(artifact);
1855
+ const digest = String(artifact.sha256 || '');
1856
+ const formatted = content != null && ['json', 'jsonl'].includes(String(artifact.language || '').toLowerCase());
1857
+ const omission = artifact.content_omitted_reason
1858
+ ? `<div class="final-artifact-omitted">Content not embedded: ${esc(artifact.content_omitted_reason)}</div>`
1859
+ : '';
1860
+ return `<details class="final-artifact-file" ${index === 0 ? 'open' : ''}>
1861
+ <summary><strong>${esc(artifact.path || 'artifact')}</strong><span>${esc(formatFileSize(artifact.size_bytes))}</span></summary>
1862
+ <div class="final-artifact-meta">SHA-256 <code>${esc(digest)}</code>${formatted ? '<span>JSON formatted for display</span>' : ''}</div>
1863
+ ${content != null ? `<pre><code class="language-${esc(artifact.language || 'text')}">${esc(content)}</code></pre>` : omission}
1864
+ </details>`;
1865
+ }).join('');
1866
+ return `<section class="final-artifacts">
1867
+ <div class="final-artifacts-heading"><div><span>Final files</span><strong>${artifacts.length}</strong></div><p>Captured from the run directory after the agent exited.</p></div>
1868
+ ${files}
1869
+ </section>`;
1870
+ }
1871
+
1373
1872
  function renderLaneSession(session) {
1374
1873
  const i = session.sourceIndex;
1375
1874
  const d = session.dimensions;
@@ -1397,6 +1896,7 @@ function renderLaneSession(session) {
1397
1896
  if (session.result && session.result.result_text) {
1398
1897
  html += `<details class="lane-result"><summary>Final output</summary><div class="md-rendered">${renderMarkdown(session.result.result_text)}</div></details>`;
1399
1898
  }
1899
+ html += renderFinalArtifacts(session);
1400
1900
  return html + '</article>';
1401
1901
  }
1402
1902
 
@@ -1576,6 +2076,8 @@ function renderSession(i) {
1576
2076
  </div>`;
1577
2077
  }
1578
2078
 
2079
+ html += renderFinalArtifacts(s);
2080
+
1579
2081
  main.innerHTML = html;
1580
2082
  }
1581
2083
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepline",
3
- "version": "0.2.51",
3
+ "version": "0.2.53",
4
4
  "description": "Deepline SDK + CLI — B2B data enrichment powered by durable cloud execution",
5
5
  "license": "MIT",
6
6
  "repository": {