create-walle 0.10.0 → 0.10.1

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 (70) hide show
  1. package/README.md +84 -41
  2. package/lib/runtime-closure-policy.json +2 -2
  3. package/package.json +10 -4
  4. package/template/coding-task-manager/bin/capture-codex-rollout-records.js +417 -0
  5. package/template/coding-task-manager/bin/census-codex-lead-history.js +325 -0
  6. package/template/coding-task-manager/bin/seam-replay-codex.js +301 -0
  7. package/template/coding-task-manager/lib/client-diag-handler.js +20 -0
  8. package/template/coding-task-manager/lib/codex-history-terminal-renderer.js +33 -33
  9. package/template/coding-task-manager/lib/codex-local-links.js +267 -0
  10. package/template/coding-task-manager/lib/codex-markdown-lists.js +160 -0
  11. package/template/coding-task-manager/lib/codex-native-history-prefix.js +4 -0
  12. package/template/coding-task-manager/lib/codex-source-handoff.js +186 -0
  13. package/template/coding-task-manager/lib/db-owner-worker-client.js +22 -0
  14. package/template/coding-task-manager/lib/feature-manifest.js +0 -2
  15. package/template/coding-task-manager/lib/harness-history-transition.js +38 -11
  16. package/template/coding-task-manager/lib/headless-term-service.js +5 -6
  17. package/template/coding-task-manager/lib/history-transition-read-op.js +179 -23
  18. package/template/coding-task-manager/lib/kernel/diag-log.js +12 -0
  19. package/template/coding-task-manager/lib/lead-model-observer.js +17 -2
  20. package/template/coding-task-manager/lib/lead-model-switch.js +8 -0
  21. package/template/coding-task-manager/lib/lead-model-transition-runtime.js +42 -4
  22. package/template/coding-task-manager/lib/login-tripwire.js +17 -0
  23. package/template/coding-task-manager/lib/main-thread-lint-exemptions.json +4 -0
  24. package/template/coding-task-manager/lib/model-execution-recipe-metadata.js +16 -3
  25. package/template/coding-task-manager/lib/model-proxy/collaboration-codec.js +94 -0
  26. package/template/coding-task-manager/lib/model-proxy/responses-face.js +24 -2
  27. package/template/coding-task-manager/lib/native-history-control.js +60 -12
  28. package/template/coding-task-manager/lib/prompt-admission-trace.js +69 -0
  29. package/template/coding-task-manager/lib/raw-history-block.js +3 -1
  30. package/template/coding-task-manager/lib/read-pool-client.js +31 -22
  31. package/template/coding-task-manager/lib/read-pool-telemetry.js +14 -5
  32. package/template/coding-task-manager/lib/remote-command-runtime.js +5 -4
  33. package/template/coding-task-manager/lib/root-prompt-admission.js +10 -9
  34. package/template/coding-task-manager/lib/root-prompt-flow.js +8 -5
  35. package/template/coding-task-manager/lib/server-memory-sampler-runtime.js +10 -0
  36. package/template/coding-task-manager/lib/server-memory-sampler.js +41 -2
  37. package/template/coding-task-manager/lib/session-create/provider-process.js +4 -2
  38. package/template/coding-task-manager/lib/session-lifecycle.js +2 -50
  39. package/template/coding-task-manager/lib/session-model-action-preconditions.js +59 -3
  40. package/template/coding-task-manager/lib/session-resource-sampler.js +48 -0
  41. package/template/coding-task-manager/lib/session-status.js +2 -2
  42. package/template/coding-task-manager/lib/typing-interaction-trace.js +44 -2
  43. package/template/coding-task-manager/lib/typing-worker-stages.js +148 -1
  44. package/template/coding-task-manager/lib/usage-ledger/claude-parse.js +53 -2
  45. package/template/coding-task-manager/lib/usage-ledger/ingest.js +13 -4
  46. package/template/coding-task-manager/lib/usage-ledger/read.js +14 -0
  47. package/template/coding-task-manager/lib/usage-ledger/schema.js +49 -1
  48. package/template/coding-task-manager/lib/usage-ledger/store.js +337 -6
  49. package/template/coding-task-manager/lib/usage-ledger/task-read.js +123 -5
  50. package/template/coding-task-manager/lib/ws/input.js +1 -0
  51. package/template/coding-task-manager/protocol/message-table.json +0 -10
  52. package/template/coding-task-manager/protocol/messages.js +0 -10
  53. package/template/coding-task-manager/protocol/ws-schema.json +0 -20
  54. package/template/coding-task-manager/protocol/ws-schema.md +0 -2
  55. package/template/coding-task-manager/public/css/app.css +0 -1
  56. package/template/coding-task-manager/public/index.html +3 -17
  57. package/template/coding-task-manager/public/js/client-health-core.js +81 -0
  58. package/template/coding-task-manager/public/js/client-health-main.js +61 -0
  59. package/template/coding-task-manager/public/js/prompt-submission-client.js +5 -1
  60. package/template/coding-task-manager/public/js/protocol.js +0 -10
  61. package/template/coding-task-manager/public/js/row-join.js +14 -1
  62. package/template/coding-task-manager/public/js/session-usage-view.js +1 -0
  63. package/template/coding-task-manager/public/js/terminal-history-viewport.js +0 -2
  64. package/template/coding-task-manager/server.js +62 -186
  65. package/template/package-lock.json +2 -2
  66. package/template/package.json +1 -1
  67. package/template/wall-e/coding/host-capabilities.js +2 -2
  68. package/template/wall-e/fly.toml +1 -1
  69. package/template/wall-e/tools/local-tools.js +6 -1
  70. package/template/coding-task-manager/lib/giant-freeze-park.js +0 -31
@@ -31,6 +31,8 @@
31
31
  // blob"; we still guard internally so one bad row can't sink the whole render.
32
32
 
33
33
  const { takeMarkdownTable, isTableRowLine } = require('./codex-markdown-table');
34
+ const { createListTracker } = require('./codex-markdown-lists');
35
+ const { isLocalPathLikeLink, renderLocalLinkTarget } = require('./codex-local-links');
34
36
 
35
37
  const STYLE_TABLE_VERSION = 2;
36
38
 
@@ -340,14 +342,15 @@ function renderTextBlock(text, cols, style) {
340
342
  });
341
343
  }
342
344
 
343
- function stripMarkdownInline(text) {
345
+ // `ctx` is the render opts: `ctx.cwd` is the rollout session cwd the local-link rule shortens under.
346
+ function stripMarkdownInline(text, ctx) {
344
347
  let s = String(text == null ? '' : text);
345
- // Terminal projection: keep the information, remove Markdown control syntax.
346
- s = s.replace(/!\[([^\]]*)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g, (_m, alt, url) => {
347
- const label = String(alt || 'image').trim() || 'image';
348
- return `${label} (${url})`;
349
- });
350
- s = s.replace(/\[([^\]]+)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g, (_m, label, url) => `${label} (${url})`);
348
+ // Terminal projection: keep the information, remove Markdown control syntax. A local path-like
349
+ // destination draws Codex's display target (`local_links.rs`): `label (target)`, or `target` alone
350
+ // when the label is blank; every other destination keeps its URL.
351
+ const dest = (url) => (isLocalPathLikeLink(url) ? (renderLocalLinkTarget(url, ctx && ctx.cwd) || url) : url);
352
+ s = s.replace(/!\[([^\]]*)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g, (_m, alt, url) => `${String(alt || 'image').trim() || 'image'} (${dest(url)})`);
353
+ s = s.replace(/\[([^\]]*)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g, (_m, label, url) => (String(label).trim() ? `${label} (${dest(url)})` : dest(url)));
351
354
  s = s.replace(/<((?:https?:|file:)[^>\s]+)>/g, '$1');
352
355
  s = s.replace(/`([^`\n]+)`/g, '$1');
353
356
  s = s.replace(/\*\*\*([^*\n][\s\S]*?[^*\n])\*\*\*/g, '$1');
@@ -366,13 +369,13 @@ function isMarkdownTableSeparator(line) {
366
369
  return /^\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?$/.test(s);
367
370
  }
368
371
 
369
- function formatMarkdownTableRow(line) {
372
+ function formatMarkdownTableRow(line, ctx) {
370
373
  const raw = String(line || '').trim();
371
374
  const inner = raw.replace(/^\|/, '').replace(/\|$/, '');
372
- return inner.split('|').map((cell) => stripMarkdownInline(cell.trim())).join(' | ');
375
+ return inner.split('|').map((cell) => stripMarkdownInline(cell.trim(), ctx)).join(' | ');
373
376
  }
374
377
 
375
- function renderMarkdownTextBlock(text, cols, style) {
378
+ function renderMarkdownTextBlock(text, cols, style, ctx) {
376
379
  const prefix = style.prefix || '';
377
380
  const prefixW = strWidth(prefix);
378
381
  const baseBodySgr = style.bodySgr || [];
@@ -397,8 +400,12 @@ function renderMarkdownTextBlock(text, cols, style) {
397
400
  };
398
401
 
399
402
  const rawLines = String(text == null ? '' : text).replace(/\t/g, ' ').split('\n');
403
+ const lists = createListTracker();
400
404
  for (let lineIndex = 0; lineIndex < rawLines.length; lineIndex += 1) {
401
405
  const line = rawLines[lineIndex].replace(/\r$/, '');
406
+ // Fed for EVERY line (a blank line does not close the container stack, and a non-list line can
407
+ // be a continuation paragraph of the innermost item); fenced content is verbatim, never a list.
408
+ const item = inFence ? null : lists.consume(line);
402
409
  const fence = line.match(/^\s*(```+|~~~+)/);
403
410
  if (fence) {
404
411
  inFence = !inFence;
@@ -408,47 +415,39 @@ function renderMarkdownTextBlock(text, cols, style) {
408
415
  emitWrapped(line, [SGR.fgGray], { indent: 2, hang: 2 });
409
416
  continue;
410
417
  }
418
+ if (item && item.marker) {
419
+ // R-1: the marker already carries its own column, so indent 0 and the absolute hang; the
420
+ // message prefix (`• ` on the first row, ` ` after) is still applied by emitWrapped.
421
+ emitWrapped(item.marker + stripMarkdownInline(item.text, ctx), baseBodySgr, { indent: item.indent, hang: item.hang });
422
+ continue;
423
+ }
411
424
  if (!line.trim()) {
412
425
  out.push('');
413
426
  continue;
414
427
  }
415
428
  const heading = line.match(/^\s{0,3}#{1,6}\s+(.+?)\s*#*\s*$/);
416
429
  if (heading) {
417
- emitWrapped(stripMarkdownInline(heading[1]), [SGR.bold]);
430
+ emitWrapped(stripMarkdownInline(heading[1], ctx), [SGR.bold]);
418
431
  continue;
419
432
  }
420
433
  const quote = line.match(/^\s{0,3}>\s?(.*)$/);
421
434
  if (quote) {
422
- emitWrapped('| ' + stripMarkdownInline(quote[1]), [SGR.dim], { hang: 2 });
435
+ emitWrapped('| ' + stripMarkdownInline(quote[1], ctx), [SGR.dim], { hang: 2 });
423
436
  continue;
424
437
  }
425
- const tbl = takeMarkdownTable(rawLines, lineIndex, { width: cols - prefixW, indent: prefixW, lead: prefixPending ? stylize(prefix, style.prefixSgr) : null, strWidth, stylize, isSeparator: isMarkdownTableSeparator, strip: stripMarkdownInline, codes: { bold: [SGR.bold], dim: [SGR.dim], body: baseBodySgr } });
438
+ const tbl = takeMarkdownTable(rawLines, lineIndex, { width: cols - prefixW, indent: prefixW, lead: prefixPending ? stylize(prefix, style.prefixSgr) : null, strWidth, stylize, isSeparator: isMarkdownTableSeparator, strip: (cell) => stripMarkdownInline(cell, ctx), codes: { bold: [SGR.bold], dim: [SGR.dim], body: baseBodySgr } });
426
439
  if (tbl) { lineIndex = tbl.nextIndex - 1; out.push(...tbl.rows); prefixPending = false; continue; }
427
440
  if (isMarkdownTableSeparator(line)) continue;
428
441
  const tableRow = isTableRowLine(line);
429
442
  if (tableRow) {
430
- emitWrapped(formatMarkdownTableRow(line), baseBodySgr, { hang: 4 });
431
- continue;
432
- }
433
- const unordered = line.match(/^(\s*)([-*+])\s+(.+)$/);
434
- if (unordered) {
435
- const indent = Math.min(8, Math.floor(unordered[1].length / 2) * 2);
436
- const marker = '- ';
437
- emitWrapped(marker + stripMarkdownInline(unordered[3]), baseBodySgr, { indent, hang: indent + strWidth(marker) });
438
- continue;
439
- }
440
- const ordered = line.match(/^(\s*)(\d{1,4}[.)])\s+(.+)$/);
441
- if (ordered) {
442
- const indent = Math.min(8, Math.floor(ordered[1].length / 2) * 2);
443
- const marker = ordered[2] + ' ';
444
- emitWrapped(marker + stripMarkdownInline(ordered[3]), baseBodySgr, { indent, hang: indent + strWidth(marker) });
443
+ emitWrapped(formatMarkdownTableRow(line, ctx), baseBodySgr, { hang: 4 });
445
444
  continue;
446
445
  }
447
446
  if (/^\s{0,3}([-*_])(?:\s*\1){2,}\s*$/.test(line)) {
448
447
  emitWrapped('─'.repeat(Math.max(8, Math.min(40, cols - prefixW))), [SGR.dim]);
449
448
  continue;
450
449
  }
451
- emitWrapped(stripMarkdownInline(line), baseBodySgr);
450
+ emitWrapped(stripMarkdownInline(item ? item.text : line, ctx), baseBodySgr, item ? { indent: item.indent, hang: item.hang } : {}); // a list item's continuation paragraph hangs at its level
452
451
  }
453
452
 
454
453
  return out;
@@ -524,8 +523,9 @@ function jsonToolResultPreview(text, cols, style) {
524
523
  ].join('\n');
525
524
  }
526
525
 
527
- // Returns { lines: string[], known: boolean } for one message.
528
- function renderMessage(msg, cols, table, { fullHistory = false } = {}) {
526
+ // Returns { lines: string[], known: boolean } for one message. `ctx` is the render opts (its cwd
527
+ // drives the local-link display target).
528
+ function renderMessage(msg, cols, table, { fullHistory = false, ctx } = {}) {
529
529
  const meta = messageMeta(msg);
530
530
  const kind = meta && meta.kind ? meta.kind : null;
531
531
  const role = msg && msg.role;
@@ -533,7 +533,7 @@ function renderMessage(msg, cols, table, { fullHistory = false } = {}) {
533
533
  // Plain assistant / user text (no structured kind).
534
534
  if (!kind) {
535
535
  if (role === 'user') return { lines: renderTextBlock(String(msg.text || ''), cols, table.user), known: true };
536
- if (role === 'assistant') return { lines: renderMarkdownTextBlock(String(msg.text || ''), cols, table.assistant), known: true };
536
+ if (role === 'assistant') return { lines: renderMarkdownTextBlock(String(msg.text || ''), cols, table.assistant, ctx), known: true };
537
537
  return { lines: [], known: false };
538
538
  }
539
539
 
@@ -715,7 +715,7 @@ function renderOneMessage(msg, cols, table, fullHistory, ctx) {
715
715
  const meta = messageMeta(msg);
716
716
  kind = (meta && meta.kind) || (msg.role === 'user' ? 'user' : msg.role === 'assistant' ? 'assistant' : 'text');
717
717
  rendered = (table.owned === true && owned.renderOwnedMessage(msg, cols, table, ctx))
718
- || renderMessage(msg, cols, table, { fullHistory: fullHistory === true });
718
+ || renderMessage(msg, cols, table, { fullHistory: fullHistory === true, ctx });
719
719
  } catch { rendered = { lines: [], known: false }; } // one bad row must never sink the whole history
720
720
  return { kind, rendered };
721
721
  }
@@ -0,0 +1,267 @@
1
+ 'use strict';
2
+
3
+ // Port of codex-rs/tui/src/markdown_render/local_links.rs (plus the `#L..` fragment helper at
4
+ // codex-rs/utils/string/src/lib.rs:73-104).
5
+ //
6
+ // Codex keeps a local link's label and ALSO shows its target, shortening an absolute destination
7
+ // relative to the session working directory when that path lies strictly underneath it. A local
8
+ // link therefore draws as `label (target)`, or as `target` alone when the label is blank. Rows CTM
9
+ // paints must equal the rows the live Codex screen paints, so this is a display rule, not a
10
+ // filesystem lookup: it never resolves files, symlinks or the frontend home, and it never collapses
11
+ // `.` / `..`.
12
+ //
13
+ // Names follow the Rust 1:1 in camelCase. `renderLocalLinkTarget` returns null only for a `file://`
14
+ // URL that does not parse into a local path; plain path-like input always yields a target.
15
+
16
+ /** Path-like destinations: `file://`, absolute, `~/`, `./`, `../`, `\\`, or `X:/` / `X:\`. */
17
+ function isLocalPathLikeLink(destUrl) {
18
+ const value = String(destUrl == null ? '' : destUrl);
19
+ return value.startsWith('file://') || value.startsWith('/') || value.startsWith('~/')
20
+ || value.startsWith('./') || value.startsWith('../') || value.startsWith('\\\\')
21
+ || /^[A-Za-z]:[\\/]/.test(value);
22
+ }
23
+
24
+ /** Trailing `:line`, `:line:col` or `:a-b` run (an en dash is accepted, as in Codex's regex). */
25
+ const COLON_LOCATION_SUFFIX_RE = /:\d+(?::\d+)?(?:[-\u2013]\d+(?::\d+)?)?$/;
26
+ /** `L12`, `L12C3`, `L12-L14`, `L12C3-L14C9` — a markdown `#L..` location fragment. */
27
+ const HASH_LOCATION_SUFFIX_RE = /^L\d+(?:C\d+)?(?:-L\d+(?:C\d+)?)?$/;
28
+ const HEX_PAIR_RE = /^[0-9A-Fa-f]{2}$/;
29
+
30
+ /**
31
+ * A local link's display target, or null when a `file://` URL does not parse into a local path.
32
+ * The path is normalized and shortened relative to `cwd` when it lies strictly under it; the
33
+ * location suffix (`#L..` normalised to `:line[:col]`) is appended back.
34
+ */
35
+ function renderLocalLinkTarget(destUrl, cwd) {
36
+ const parsed = parseLocalLinkTarget(destUrl);
37
+ if (!parsed) return null;
38
+ let rendered = displayLocalLinkPath(parsed[0], cwd);
39
+ if (parsed[1] != null) rendered += parsed[1];
40
+ return rendered;
41
+ }
42
+
43
+ /** Split a local-link destination into `[normalizedPathText, locationSuffixOrNull]`, or null. */
44
+ function parseLocalLinkTarget(destUrl) {
45
+ const dest = String(destUrl == null ? '' : destUrl);
46
+ if (dest.startsWith('file://')) {
47
+ let url;
48
+ try { url = new URL(dest); } catch { return null; } // an unparseable file:// URL has no target
49
+ // `PathUri::parse(&url[..Position::AfterPath])`: the query and fragment are not part of the path.
50
+ const pathText = pathUriNativePath(`${url.protocol}//${url.host}${url.pathname}`);
51
+ if (pathText == null) return null;
52
+ const fragment = url.hash === '' ? null : url.hash.slice(1);
53
+ const locationSuffix = fragment == null ? null : normalizeHashLocationSuffixFragment(fragment);
54
+ return [normalizeLocalLinkPathText(pathText), locationSuffix];
55
+ }
56
+
57
+ let pathText = dest;
58
+ let locationSuffix = null;
59
+ // Prefer `#L..` when both forms are present so `path#L10` is not misread as a path ending in `:10`.
60
+ const hash = dest.lastIndexOf('#');
61
+ if (hash >= 0) {
62
+ const normalized = normalizeHashLocationSuffixFragment(dest.slice(hash + 1));
63
+ if (normalized != null) { pathText = dest.slice(0, hash); locationSuffix = normalized; }
64
+ }
65
+ if (locationSuffix == null) {
66
+ const suffix = extractColonLocationSuffix(pathText);
67
+ if (suffix != null) { pathText = pathText.slice(0, pathText.length - suffix.length); locationSuffix = suffix; }
68
+ }
69
+ return [normalizeLocalLinkPathText(urlDecode(pathText)), locationSuffix];
70
+ }
71
+
72
+ /** `#L12`-style fragment -> `:12[:3][-14[:9]]`, or null when the fragment is not a location. */
73
+ function normalizeHashLocationSuffixFragment(fragment) {
74
+ const text = String(fragment == null ? '' : fragment);
75
+ return HASH_LOCATION_SUFFIX_RE.test(text) ? normalizeMarkdownHashLocationSuffix(`#${text}`) : null;
76
+ }
77
+
78
+ /** `codex_utils_string::normalize_markdown_hash_location_suffix` (`#L74C3-L76C9` -> `:74:3-76:9`). */
79
+ function normalizeMarkdownHashLocationSuffix(suffix) {
80
+ const text = String(suffix == null ? '' : suffix);
81
+ if (!text.startsWith('#')) return null;
82
+ const fragment = text.slice(1);
83
+ const dash = fragment.indexOf('-');
84
+ const start = parseMarkdownHashLocationPoint(dash < 0 ? fragment : fragment.slice(0, dash));
85
+ if (!start) return null;
86
+ let normalized = `:${start.line}`;
87
+ if (start.column != null) normalized += `:${start.column}`;
88
+ if (dash >= 0) {
89
+ const end = parseMarkdownHashLocationPoint(fragment.slice(dash + 1));
90
+ if (!end) return null;
91
+ normalized += `-${end.line}`;
92
+ if (end.column != null) normalized += `:${end.column}`;
93
+ }
94
+ return normalized;
95
+ }
96
+
97
+ function parseMarkdownHashLocationPoint(point) {
98
+ const text = String(point == null ? '' : point);
99
+ if (!text.startsWith('L')) return null;
100
+ const rest = text.slice(1);
101
+ const column = rest.indexOf('C');
102
+ return column < 0 ? { line: rest, column: null } : { line: rest.slice(0, column), column: rest.slice(column + 1) };
103
+ }
104
+
105
+ /** A trailing `:line[:col][-line[:col]]` run at the very end of a plain path-like string. */
106
+ function extractColonLocationSuffix(pathText) {
107
+ const matched = COLON_LOCATION_SUFFIX_RE.exec(String(pathText == null ? '' : pathText));
108
+ return matched ? matched[0] : null;
109
+ }
110
+
111
+ /**
112
+ * Local-path display normalisation: forward slashes everywhere, and UNC-style `\\server\share`
113
+ * rewritten to `//server/share` so the prefix checks below see one stable spelling.
114
+ */
115
+ function normalizeLocalLinkPathText(pathText) {
116
+ const text = String(pathText == null ? '' : pathText);
117
+ if (text.startsWith('\\\\')) return `//${text.slice(2).replace(/\\/g, '/').replace(/^\/+/, '')}`;
118
+ return text.replace(/\\/g, '/');
119
+ }
120
+
121
+ function isAbsoluteLocalLinkPath(pathText) {
122
+ const text = String(pathText == null ? '' : pathText);
123
+ return text.startsWith('/') || text.startsWith('//') || /^[A-Za-z]:\//.test(text);
124
+ }
125
+
126
+ /** Trim trailing separators without destroying root semantics (`/`, `//`, `C:/` stay intact). */
127
+ function trimTrailingLocalPathSeparator(pathText) {
128
+ const text = String(pathText == null ? '' : pathText);
129
+ if (text === '/' || text === '//' || /^[A-Za-z]:\/$/.test(text)) return text;
130
+ return text.replace(/\/+$/, '');
131
+ }
132
+
133
+ /** The part of `pathText` strictly under `cwdText`, without a leading slash; null when it is not. */
134
+ function stripLocalPathPrefix(pathText, cwdText) {
135
+ const path = trimTrailingLocalPathSeparator(pathText);
136
+ const cwd = trimTrailingLocalPathSeparator(cwdText);
137
+ if (path === cwd) return null; // the path IS the cwd: keep it whole rather than render an empty target
138
+ if (cwd === '/' || cwd === '//') return path.startsWith('/') ? path.slice(1) : null;
139
+ if (!path.startsWith(cwd)) return null;
140
+ const rest = path.slice(cwd.length);
141
+ return rest.startsWith('/') ? rest.slice(1) : null;
142
+ }
143
+
144
+ /** Relative paths (including `~/...`) stay relative; absolute paths prefer the cwd-relative form. */
145
+ function displayLocalLinkPath(pathText, cwd) {
146
+ const path = normalizeLocalLinkPathText(pathText);
147
+ if (!isAbsoluteLocalLinkPath(path)) return path;
148
+ if (typeof cwd === 'string') {
149
+ const stripped = stripLocalPathPrefix(path, normalizeLocalLinkPathText(cwd));
150
+ if (stripped != null) return stripped;
151
+ }
152
+ return path;
153
+ }
154
+
155
+ // --- PathUri (codex-rs/utils/path-uri): only what a `file://` link destination needs ---
156
+
157
+ /** The native path a `file:` URI names, or null when it is not a usable local path URI. */
158
+ function pathUriNativePath(uri) {
159
+ let url;
160
+ try { url = new URL(uri); } catch { return null; }
161
+ if (url.protocol !== 'file:') return null;
162
+ if (url.username !== '' || url.password !== '' || url.port !== '') return null;
163
+ // A decoded NUL cannot be a native path (Codex rejects it the same way).
164
+ if (decodeBinaryUtf8(url.pathname).includes('\u0000')) return null;
165
+ const host = url.hostname === 'localhost' ? '' : url.hostname; // a localhost authority is dropped
166
+ const pathname = host === '' ? withNormalizedWindowsDriveLetter(url.pathname) : url.pathname;
167
+ const segments = pathSegments(pathname);
168
+ const rendered = inferPathConvention(host, segments) === 'windows'
169
+ ? renderWindowsPath(host, segments) : renderPosixPath(host, segments);
170
+ return rendered == null ? `file://${host}${pathname}` : rendered; // fallback: the canonical URI text
171
+ }
172
+
173
+ function pathSegments(pathname) {
174
+ const path = String(pathname);
175
+ return path.startsWith('/') ? path.slice(1).split('/') : path.split('/');
176
+ }
177
+
178
+ /** Uppercase a leading `c:` drive segment so later comparison is stable across spellings. */
179
+ function withNormalizedWindowsDriveLetter(pathname) {
180
+ const matched = /^(\/*)([^/]*)([\s\S]*)$/.exec(String(pathname));
181
+ if (!matched) return pathname;
182
+ const [, slashes, drive, rest] = matched;
183
+ if (!isWindowsDriveUriSegment(drive) || !/^[a-z]/.test(drive)) return pathname;
184
+ return `${slashes}${drive[0].toUpperCase()}${drive.slice(1)}${rest}`;
185
+ }
186
+
187
+ function isWindowsDriveUriSegment(segment) {
188
+ const text = String(segment == null ? '' : segment);
189
+ return /^[A-Za-z]:$/.test(text) || /^[A-Za-z]%3[Aa]$/.test(text);
190
+ }
191
+
192
+ /** An authority means Windows (UNC); otherwise a leading drive segment does, else POSIX. */
193
+ function inferPathConvention(host, segments) {
194
+ if (host !== '') return 'windows';
195
+ const first = segments.find((segment) => segment !== '');
196
+ return first != null && isWindowsDriveUriSegment(first) ? 'windows' : 'posix';
197
+ }
198
+
199
+ function renderPosixPath(host, segments) {
200
+ if (host !== '') return null; // a UNC authority cannot be spelled as a POSIX path
201
+ return segments.map((segment) => `/${decodeNativeSegment(segment)}`).join('');
202
+ }
203
+
204
+ function renderWindowsPath(host, segments) {
205
+ let rendered;
206
+ let rest;
207
+ if (host !== '') {
208
+ // `file://server/share/file` -> `\\server\share\file`; the share name must be present.
209
+ if (!segments.length) return null;
210
+ const share = decodeNativeSegment(segments[0]);
211
+ if (share === '') return null;
212
+ rendered = `\\\\${host}\\${share}`;
213
+ rest = segments.slice(1);
214
+ } else {
215
+ // Without an authority Windows needs a drive root: `file:///C:/src` -> `C:\src`.
216
+ if (!segments.length) return null;
217
+ const drive = decodeNativeSegment(segments[0]);
218
+ if (!/^[A-Za-z]:$/.test(drive)) return null;
219
+ rendered = drive;
220
+ rest = segments.slice(1);
221
+ }
222
+ for (const segment of rest) rendered += `\\${decodeNativeSegment(segment)}`;
223
+ // `file:///C:` and `file:///C:/` both name the drive root, never the drive-relative `C:`.
224
+ return /^[A-Za-z]:$/.test(rendered) ? `${rendered}\\` : rendered;
225
+ }
226
+
227
+ /** `urlencoding::decode_binary` + lossy UTF-8: a URI path segment as native path text. */
228
+ function decodeNativeSegment(segment) {
229
+ return decodeBinaryUtf8(segment);
230
+ }
231
+
232
+ /** `urlencoding::decode`: percent-decoding that fails (and keeps the input) on invalid UTF-8. */
233
+ function urlDecode(text) {
234
+ const decoded = decodeBinaryUtf8(text, true);
235
+ return decoded == null ? String(text) : decoded;
236
+ }
237
+
238
+ function decodeBinaryUtf8(text, strict = false) {
239
+ const bytes = [];
240
+ const value = String(text == null ? '' : text);
241
+ for (let index = 0; index < value.length; index += 1) {
242
+ const pair = value[index] === '%' ? value.slice(index + 1, index + 3) : '';
243
+ if (HEX_PAIR_RE.test(pair)) { bytes.push(parseInt(pair, 16)); index += 2; continue; }
244
+ const codePoint = value.codePointAt(index);
245
+ const character = String.fromCodePoint(codePoint);
246
+ if (character.length === 2) index += 1; // a surrogate pair is one code point
247
+ for (const byte of Buffer.from(character, 'utf8')) bytes.push(byte);
248
+ }
249
+ const buffer = Buffer.from(bytes);
250
+ if (!strict) return buffer.toString('utf8');
251
+ try { return new TextDecoder('utf-8', { fatal: true }).decode(buffer); } catch { return null; }
252
+ }
253
+
254
+ module.exports = {
255
+ isLocalPathLikeLink,
256
+ renderLocalLinkTarget,
257
+ // Exported for the ported Codex test table (same functions, same behaviour).
258
+ _internal: {
259
+ parseLocalLinkTarget,
260
+ displayLocalLinkPath,
261
+ normalizeLocalLinkPathText,
262
+ trimTrailingLocalPathSeparator,
263
+ stripLocalPathPrefix,
264
+ extractColonLocationSuffix,
265
+ normalizeMarkdownHashLocationSuffix,
266
+ },
267
+ };
@@ -0,0 +1,160 @@
1
+ 'use strict';
2
+
3
+ // Codex list markers, at parity with what the TUI paints, ported from
4
+ // codex-rs/tui/src/markdown_render.rs start_item (:1014-1090), prefix_spans (:2535-2565)
5
+ // codex-rs/tui/src/markdown_render/task_lists.rs task_list_marker
6
+ // codex-rs/tui/src/markdown_render_tests.rs list_unordered_single / list_nested / nested_five_levels_mixed_lists / ...
7
+ //
8
+ // Why this module exists: the reader joins CTM's rendered rows to the rows the live Codex screen
9
+ // actually shows, by identity of the row text. Codex draws unordered items as `• ` (PR #47322, on
10
+ // by default) and nests by a FIXED 4-column step, while the first CTM renderer drew `- ` indented by
11
+ // floor(spaces/2)*2 — so every list row missed the join and the reader drew it twice.
12
+ //
13
+ // The two facts that make the port non-obvious:
14
+ // * depth is the list nesting level; `width = depth*4 - 3`. The marker of an item at depth d is
15
+ // ONLY its own marker: prefix_spans skips every list context except the innermost, so an outer
16
+ // level contributes nothing to a nested row (no cumulative indent).
17
+ // * depth is NOT floor(spaces/2). It comes from a CommonMark container stack over the source
18
+ // lines; each open level remembers its item's CONTENT column, and a list line either opens a
19
+ // deeper level, lands as a sibling, or closes levels.
20
+ //
21
+ // Pure: no DOM, no clock, no I/O. Same input => same output. Tabs are already expanded to 4 spaces
22
+ // by the caller (renderMarkdownTextBlock does this before the line loop).
23
+
24
+ const BULLET = '•';
25
+ const UNCHECKED = '☐';
26
+ const CHECKED = '☑';
27
+
28
+ // Codex nests by a fixed 4-column step (markdown_render.rs:1042: `width = depth * 4 - 3`).
29
+ const HANG_STEP = 4;
30
+
31
+ // `- x` / `* x` / `+ x` and `1. x` / `1) x`. The content must be non-blank, so a bare rule or a
32
+ // lone marker keeps falling through to the renderer's paragraph path exactly as before.
33
+ const BULLET_LINE = /^( *)([-*+])( +)(\S.*)$/;
34
+ const ORDERED_LINE = /^( *)(\d{1,4})([.)])( +)(\S.*)$/;
35
+ // A GFM task checkbox, right after the marker (task_lists.rs:8-24). `[X]` counts as checked.
36
+ const TASK = /^\[([ xX])\](?: |$)/;
37
+
38
+ // `width` for a depth (markdown_render.rs:1042): depth 1 => 1, depth 2 => 5, depth 3 => 9 ...
39
+ function markerWidthFor(depth) {
40
+ return depth * HANG_STEP - 3;
41
+ }
42
+
43
+ // The column a wrapped (continuation) row of this item hangs at (markdown_render.rs:1057-1061):
44
+ // unordered `width + 1`, ordered `width + 2` whatever the digit count. A task item hangs at its whole
45
+ // marker's display width instead (itemOf).
46
+ function hangFor(entry) {
47
+ return entry.width + (entry.ordered ? 2 : 1);
48
+ }
49
+
50
+ // markdown_render.rs:1043-1056 + task_lists.rs:15-19: the task checkbox REPLACES a trailing `• `
51
+ // and keeps an ordered `N. ` prefix.
52
+ function markerFor(entry, index) {
53
+ const glyph = entry.task ? (entry.checked ? CHECKED : UNCHECKED) : BULLET;
54
+ if (!entry.ordered) return ' '.repeat(Math.max(0, entry.width - 1)) + glyph + ' ';
55
+ return String(index).padStart(entry.width) + '. ' + (entry.task ? glyph + ' ' : '');
56
+ }
57
+
58
+ // Builds the row a list item paints: the marker (which already carries its own leading columns)
59
+ // plus the source text. `indent` is 0 for an item row because the marker string holds its column,
60
+ // and `hang` is the absolute continuation column. The caller adds the message prefix in front
61
+ // (rule R-1.6) and strips inline markdown from `text` alone, so the marker never joins a match.
62
+ function itemOf(entry, index, text) {
63
+ const marker = markerFor(entry, index);
64
+ // A task item hangs at its WHOLE marker's display width (task_lists.rs:22), so `10. ☐ ` hangs 6;
65
+ // every char of a marker is one column wide.
66
+ return { marker, text, indent: 0, hang: entry.task ? marker.length : hangFor(entry) };
67
+ }
68
+
69
+ // A tracker owns the CommonMark container stack for one text block. `consume(line)` must be called
70
+ // with EVERY source line, in order, including blank ones: a blank line does not close the stack,
71
+ // and a non-list line may be a continuation paragraph of the innermost item.
72
+ function createListTracker() {
73
+ const stack = []; // innermost last; each entry = { ordered, task, checked, width, contentCol, next }
74
+
75
+ const push = (fields) => {
76
+ const entry = {
77
+ ordered: fields.ordered,
78
+ task: fields.task,
79
+ checked: fields.checked,
80
+ width: markerWidthFor(stack.length + 1),
81
+ contentCol: fields.contentCol,
82
+ next: fields.next,
83
+ };
84
+ stack.push(entry);
85
+ return entry;
86
+ };
87
+
88
+ return {
89
+ // -> null when the line is not a list item (or a continuation of one); otherwise the row
90
+ // { marker, text, indent, hang } the renderer draws. `marker` is '' for a continuation row.
91
+ consume(rawLine) {
92
+ const line = String(rawLine == null ? '' : rawLine);
93
+ if (!line.trim()) return null; // a blank line does not close (R-1.5)
94
+
95
+ const bullet = BULLET_LINE.exec(line);
96
+ const ordered = bullet ? null : ORDERED_LINE.exec(line);
97
+
98
+ if (!bullet && !ordered) {
99
+ const top = stack[stack.length - 1];
100
+ const indent = line.length - line.replace(/^ */, '').length;
101
+ if (top && indent >= top.contentCol) {
102
+ // A non-blank, non-list line indented to the item's content column continues that item,
103
+ // rendered at the level's hang (R-1.5).
104
+ const hang = hangFor(top);
105
+ return { marker: '', text: line.trim(), indent: hang, hang };
106
+ }
107
+ stack.length = 0; // any other non-blank line closes every level
108
+ return null;
109
+ }
110
+
111
+ const markerIndent = (bullet ? bullet[1] : ordered[1]).length;
112
+ // The marker LENGTH is `-` (1) or `N.` (digits + the bracket) — the checkbox is not part of
113
+ // it, so `- [ ] a` still has content column 2.
114
+ const markerLen = bullet ? 1 : ordered[2].length + 1;
115
+ const gap = (bullet ? bullet[3] : ordered[4]).length;
116
+ const contentCol = markerIndent + markerLen + gap;
117
+ const content = bullet ? bullet[4] : ordered[5];
118
+ const task = TASK.exec(content);
119
+ const isOrdered = !bullet;
120
+ const text = task ? content.slice(task[0].length) : content;
121
+ const fields = {
122
+ ordered: isOrdered,
123
+ task: !!task,
124
+ checked: !!task && task[1] !== ' ',
125
+ contentCol,
126
+ next: isOrdered ? Number(ordered[2]) : 0,
127
+ };
128
+
129
+ const top = stack[stack.length - 1];
130
+ let entry;
131
+ if (top && markerIndent >= top.contentCol) {
132
+ entry = push(fields); // deeper than the innermost item's content -> a nested level
133
+ } else {
134
+ // Pop while the marker column sits left of the content column of the level BELOW the top.
135
+ while (stack.length >= 2 && markerIndent < stack[stack.length - 2].contentCol) stack.pop();
136
+ const sibling = stack[stack.length - 1];
137
+ if (!sibling || sibling.ordered !== isOrdered) {
138
+ // A sibling of the other type starts a NEW list at this depth; its numbering restarts
139
+ // from its own first source number.
140
+ if (sibling) stack.pop();
141
+ entry = push(fields);
142
+ } else {
143
+ entry = sibling; // same list: keep the level, refresh the per-item task state
144
+ entry.task = fields.task;
145
+ entry.checked = fields.checked;
146
+ }
147
+ }
148
+
149
+ let index = 0;
150
+ if (entry.ordered) {
151
+ index = entry.next; // counts from the list's first source number, per sibling
152
+ entry.next += 1;
153
+ }
154
+
155
+ return itemOf(entry, index, text);
156
+ },
157
+ };
158
+ }
159
+
160
+ module.exports = { createListTracker };
@@ -191,6 +191,10 @@ function findCanonicalRolloutsByFilenameId(threadId, storageRoots) {
191
191
  const candidate = path.join(dir, entry.name);
192
192
  if (entry.isDirectory()) { stack.push({ dir: candidate, depth: depth + 1 }); continue; }
193
193
  if (!entry.isFile() || sessionHistory.codexRolloutIdFromPath(candidate).toLowerCase() !== wanted) continue;
194
+ // Codex rust-v0.163.0 rollout/src/compression.rs should_skip_compressed_sibling: one rollout is
195
+ // one physical file, and a `.jsonl.zst` is hidden while its plain `.jsonl` exists (Codex
196
+ // compresses an idle thread, then appends to a re-materialized plain file beside the stale copy).
197
+ if (candidate.endsWith('.jsonl.zst') && transcriptSource.statTranscriptSync(candidate.slice(0, -'.zst'.length)).exists) continue;
194
198
  const info = transcriptSource.statTranscriptSync(candidate);
195
199
  if (!info.exists) throw Object.assign(new Error('native history candidate changed during lookup'), { code: 'native_history_lineage_changed' });
196
200
  let canonical;