create-walle 0.9.13 → 0.9.15

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 (98) hide show
  1. package/README.md +8 -3
  2. package/bin/create-walle.js +232 -32
  3. package/bin/mcp-inject.js +18 -53
  4. package/package.json +3 -1
  5. package/template/claude-task-manager/api-prompts.js +11 -2
  6. package/template/claude-task-manager/approval-agent.js +7 -0
  7. package/template/claude-task-manager/db.js +94 -75
  8. package/template/claude-task-manager/docs/session-standup-command-center-design.md +242 -0
  9. package/template/claude-task-manager/docs/session-tooltip-freshness-design.md +224 -0
  10. package/template/claude-task-manager/docs/session-ux-issue-review-2026-05-01.md +369 -0
  11. package/template/claude-task-manager/fuzzy-utils.js +10 -2
  12. package/template/claude-task-manager/git-utils.js +140 -10
  13. package/template/claude-task-manager/lib/agent-capabilities.js +1 -1
  14. package/template/claude-task-manager/lib/agent-presets.js +38 -5
  15. package/template/claude-task-manager/lib/codex-terminal-final.js +53 -0
  16. package/template/claude-task-manager/lib/ctm-session-context-api.js +222 -0
  17. package/template/claude-task-manager/lib/session-diagnostics.js +56 -0
  18. package/template/claude-task-manager/lib/session-history.js +309 -16
  19. package/template/claude-task-manager/lib/session-standup.js +409 -0
  20. package/template/claude-task-manager/lib/session-stream.js +253 -20
  21. package/template/claude-task-manager/lib/standup-attention.js +200 -0
  22. package/template/claude-task-manager/lib/status-hooks.js +8 -2
  23. package/template/claude-task-manager/lib/update-telemetry.js +114 -0
  24. package/template/claude-task-manager/lib/walle-ctm-history.js +49 -6
  25. package/template/claude-task-manager/lib/walle-default-model.js +55 -0
  26. package/template/claude-task-manager/lib/walle-mcp-auto-config.js +66 -0
  27. package/template/claude-task-manager/lib/walle-supervisor.js +86 -19
  28. package/template/claude-task-manager/lib/walle-transcript.js +1 -3
  29. package/template/claude-task-manager/lib/worktree-cwd.js +82 -0
  30. package/template/claude-task-manager/package.json +1 -0
  31. package/template/claude-task-manager/providers/codex-mcp.js +104 -0
  32. package/template/claude-task-manager/providers/index.js +2 -0
  33. package/template/claude-task-manager/public/css/setup.css +2 -1
  34. package/template/claude-task-manager/public/css/walle.css +71 -0
  35. package/template/claude-task-manager/public/index.html +2388 -429
  36. package/template/claude-task-manager/public/js/message-renderer.js +314 -35
  37. package/template/claude-task-manager/public/js/session-search-utils.js +185 -3
  38. package/template/claude-task-manager/public/js/session-status-precedence.js +125 -0
  39. package/template/claude-task-manager/public/js/setup.js +62 -19
  40. package/template/claude-task-manager/public/js/stream-view.js +396 -55
  41. package/template/claude-task-manager/public/js/terminal-restore-state.js +57 -0
  42. package/template/claude-task-manager/public/js/walle-session.js +234 -26
  43. package/template/claude-task-manager/public/js/walle.js +143 -2
  44. package/template/claude-task-manager/server.js +1402 -433
  45. package/template/claude-task-manager/session-integrity.js +77 -28
  46. package/template/claude-task-manager/workers/approval-widget-validator.js +15 -5
  47. package/template/claude-task-manager/workers/scrollback-worker.js +5 -6
  48. package/template/claude-task-manager/workers/state-detectors/codex.js +6 -0
  49. package/template/package.json +1 -1
  50. package/template/wall-e/agent-runners/claude-code.js +2 -0
  51. package/template/wall-e/agent.js +63 -8
  52. package/template/wall-e/api-walle.js +330 -52
  53. package/template/wall-e/brain.js +291 -42
  54. package/template/wall-e/chat.js +172 -15
  55. package/template/wall-e/coding/compaction-service.js +19 -5
  56. package/template/wall-e/coding/stream-processor.js +22 -2
  57. package/template/wall-e/coding/workspace-replay.js +1 -4
  58. package/template/wall-e/coding-orchestrator.js +250 -80
  59. package/template/wall-e/compat.js +0 -28
  60. package/template/wall-e/context/context-builder.js +3 -1
  61. package/template/wall-e/embeddings.js +2 -7
  62. package/template/wall-e/eval/agent-runner.js +30 -9
  63. package/template/wall-e/eval/benchmark-generator.js +21 -1
  64. package/template/wall-e/eval/benchmarks/chat-eval.json +66 -6
  65. package/template/wall-e/eval/benchmarks/coding-agent.json +0 -596
  66. package/template/wall-e/eval/cc-replay.js +1 -0
  67. package/template/wall-e/eval/codex-cli-baseline.js +633 -0
  68. package/template/wall-e/eval/debug-agent003.js +1 -0
  69. package/template/wall-e/eval/eval-orchestrator.js +3 -3
  70. package/template/wall-e/eval/run-agent-benchmarks.js +11 -3
  71. package/template/wall-e/eval/run-codex-cli-baseline.js +177 -0
  72. package/template/wall-e/eval/run-model-comparison.js +1 -0
  73. package/template/wall-e/eval/swebench-adapter.js +1 -0
  74. package/template/wall-e/evaluation/quorum-evaluator.js +0 -1
  75. package/template/wall-e/extraction/knowledge-extractor.js +1 -2
  76. package/template/wall-e/lib/mcp-integration.js +336 -0
  77. package/template/wall-e/llm/ollama.js +47 -8
  78. package/template/wall-e/llm/ollama.plugin.json +1 -1
  79. package/template/wall-e/llm/tool-adapter.js +1 -0
  80. package/template/wall-e/loops/ingest.js +42 -8
  81. package/template/wall-e/loops/initiative.js +87 -2
  82. package/template/wall-e/mcp-server.js +872 -19
  83. package/template/wall-e/memory/ctm-context-client.js +230 -0
  84. package/template/wall-e/memory/ctm-session-context.js +1376 -0
  85. package/template/wall-e/prompts/coding/memory-protocol.md +6 -0
  86. package/template/wall-e/server.js +30 -1
  87. package/template/wall-e/skills/_bundled/memory-search/SKILL.md +8 -0
  88. package/template/wall-e/skills/_bundled/scan-ctm-sessions/SKILL.md +20 -0
  89. package/template/wall-e/skills/_bundled/scan-ctm-sessions/run.js +43 -0
  90. package/template/wall-e/skills/_bundled/slack-mentions/run.js +471 -188
  91. package/template/wall-e/skills/skill-planner.js +86 -4
  92. package/template/wall-e/slack/socket-mode-listener.js +276 -0
  93. package/template/wall-e/telemetry.js +70 -2
  94. package/template/wall-e/tools/builtin-middleware.js +55 -2
  95. package/template/wall-e/tools/shell-policy.js +1 -1
  96. package/template/wall-e/tools/slack-owner.js +104 -0
  97. package/template/website/index.html +4 -4
  98. package/template/builder-journal.md +0 -17
@@ -7,6 +7,8 @@ const { getResumeSpec: getAgentResumeSpec, normalizeAgentType } = require('./age
7
7
 
8
8
  const DEFAULT_CODEX_LOOKBACK_SEC = 10;
9
9
  const DEFAULT_CODEX_MAX_DELAY_SEC = 180;
10
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
11
+ const CODEX_ROLLOUT_BASENAME_RE = /^rollout-(\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2})-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl(?:\.bak)?$/i;
10
12
 
11
13
  function getResumeSpec(agentType, resumeId) {
12
14
  return getAgentResumeSpec(agentType || 'claude', resumeId);
@@ -27,12 +29,63 @@ function extractResumeTarget(agentType, args) {
27
29
  return idx >= 0 && list[idx + 1] ? { index: idx, valueIndex: idx + 1, sessionId: list[idx + 1] } : null;
28
30
  }
29
31
 
30
- function findCodexSessionFiles(threadId, sessionsRoot) {
31
- if (!threadId) return [];
32
- const root = sessionsRoot || path.join(process.env.HOME, '.codex', 'sessions');
33
- if (!fs.existsSync(root)) return [];
32
+ function codexRolloutFileInfo(filePath) {
33
+ if (!filePath) return null;
34
+ const match = path.basename(filePath).match(CODEX_ROLLOUT_BASENAME_RE);
35
+ if (!match) return null;
36
+ return {
37
+ rolloutPrefix: match[1],
38
+ threadId: match[2].toLowerCase(),
39
+ };
40
+ }
41
+
42
+ function codexRolloutIdFromPath(filePath) {
43
+ return codexRolloutFileInfo(filePath)?.threadId || '';
44
+ }
45
+
46
+ function codexSessionSearchRoots(sessionsRoot, homeDir = process.env.HOME) {
47
+ if (sessionsRoot) return fs.existsSync(sessionsRoot) ? [sessionsRoot] : [];
48
+ return [
49
+ path.join(homeDir, '.codex', 'sessions'),
50
+ path.join(homeDir, '.codex', 'archived_sessions'),
51
+ ].filter((root) => fs.existsSync(root));
52
+ }
53
+
54
+ function sortCodexRolloutFiles(files) {
55
+ return [...new Set(files)]
56
+ .map((filePath) => ({ filePath, info: codexRolloutFileInfo(filePath) }))
57
+ .filter((entry) => entry.info)
58
+ .sort((a, b) => {
59
+ const byPrefix = b.info.rolloutPrefix.localeCompare(a.info.rolloutPrefix);
60
+ return byPrefix || b.filePath.localeCompare(a.filePath);
61
+ })
62
+ .map((entry) => entry.filePath);
63
+ }
64
+
65
+ function findCodexStateRolloutPath(threadId, homeDir = process.env.HOME) {
66
+ if (!UUID_RE.test(String(threadId || ''))) return '';
67
+ let db = null;
68
+ try {
69
+ db = openCodexStateDb(homeDir);
70
+ if (!db) return '';
71
+ const row = db.prepare('SELECT rollout_path FROM threads WHERE id = ?').get(threadId);
72
+ const rolloutPath = row?.rollout_path || '';
73
+ if (!rolloutPath || !fs.existsSync(rolloutPath)) return '';
74
+ return codexRolloutIdFromPath(rolloutPath) === threadId.toLowerCase() ? rolloutPath : '';
75
+ } catch {
76
+ return '';
77
+ } finally {
78
+ try { if (db) db.close(); } catch {}
79
+ }
80
+ }
81
+
82
+ function findCodexSessionFiles(threadId, sessionsRoot, homeDir = process.env.HOME) {
83
+ const wanted = String(threadId || '').trim().toLowerCase();
84
+ if (!UUID_RE.test(wanted)) return [];
34
85
  const matches = [];
35
- const stack = [root];
86
+ const indexedPath = sessionsRoot ? '' : findCodexStateRolloutPath(wanted, homeDir);
87
+ if (indexedPath) matches.push(indexedPath);
88
+ const stack = codexSessionSearchRoots(sessionsRoot, homeDir);
36
89
  while (stack.length > 0) {
37
90
  const dir = stack.pop();
38
91
  let entries = [];
@@ -43,12 +96,12 @@ function findCodexSessionFiles(threadId, sessionsRoot) {
43
96
  stack.push(fullPath);
44
97
  continue;
45
98
  }
46
- if (entry.isFile() && entry.name.endsWith('.jsonl') && entry.name.includes(threadId)) {
99
+ if (entry.isFile() && codexRolloutIdFromPath(fullPath) === wanted) {
47
100
  matches.push(fullPath);
48
101
  }
49
102
  }
50
103
  }
51
- return matches.sort();
104
+ return sortCodexRolloutFiles(matches);
52
105
  }
53
106
 
54
107
  function parseSessionStartMs(value) {
@@ -66,6 +119,46 @@ function parseSessionStartMs(value) {
66
119
  return Number.isFinite(ms) ? ms : null;
67
120
  }
68
121
 
122
+ function cleanCodexCwdPath(value) {
123
+ const raw = String(value || '').trim();
124
+ if (!raw) return '';
125
+ const normalized = path.normalize(path.isAbsolute(raw) ? raw : path.resolve(raw));
126
+ return normalized.replace(/\/+$/, '') || path.parse(normalized).root;
127
+ }
128
+
129
+ function realpathCodexCwdPath(value) {
130
+ const cleaned = cleanCodexCwdPath(value);
131
+ if (!cleaned) return '';
132
+ try {
133
+ const realpath = fs.realpathSync.native ? fs.realpathSync.native(cleaned) : fs.realpathSync(cleaned);
134
+ return cleanCodexCwdPath(realpath);
135
+ } catch {
136
+ // The cwd may point at an already-removed worktree; fall back to lexical aliases.
137
+ return cleaned;
138
+ }
139
+ }
140
+
141
+ function codexCwdVariants(value) {
142
+ const variants = new Set();
143
+ const add = (candidate) => {
144
+ const cleaned = cleanCodexCwdPath(candidate);
145
+ if (!cleaned) return;
146
+ variants.add(cleaned);
147
+ if (cleaned === '/tmp') variants.add('/private/tmp');
148
+ else if (cleaned.startsWith('/tmp/')) variants.add(`/private${cleaned}`);
149
+ else if (cleaned === '/private/tmp') variants.add('/tmp');
150
+ else if (cleaned.startsWith('/private/tmp/')) variants.add(`/tmp/${cleaned.slice('/private/tmp/'.length)}`);
151
+ };
152
+ add(value);
153
+ add(realpathCodexCwdPath(value));
154
+ return [...variants];
155
+ }
156
+
157
+ function codexCwdMatches(a, b) {
158
+ const bVariants = new Set(codexCwdVariants(b));
159
+ return codexCwdVariants(a).some((variant) => bVariants.has(variant));
160
+ }
161
+
69
162
  function openCodexStateDb(homeDir = process.env.HOME) {
70
163
  const Database = require('better-sqlite3');
71
164
  const dbPath = path.join(homeDir, '.codex', 'state_5.sqlite');
@@ -80,7 +173,7 @@ function getCodexThreadById(threadId, homeDir = process.env.HOME) {
80
173
  db = openCodexStateDb(homeDir);
81
174
  if (!db) return null;
82
175
  return db.prepare(
83
- 'SELECT id, title, first_user_message, model, cwd, git_branch, created_at, updated_at FROM threads WHERE id = ?'
176
+ 'SELECT * FROM threads WHERE id = ?'
84
177
  ).get(threadId) || null;
85
178
  } catch {
86
179
  return null;
@@ -98,7 +191,7 @@ function getCodexThreadResumeCwd(threadId, {
98
191
  const thread = getCodexThreadById(threadId, homeDir);
99
192
  if (thread?.cwd) return thread.cwd;
100
193
 
101
- const files = findCodexSessionFiles(threadId, path.join(homeDir, '.codex', 'sessions'));
194
+ const files = findCodexSessionFiles(threadId, null, homeDir);
102
195
  for (const filePath of files) {
103
196
  const row = _readCodexRolloutMetadata(filePath);
104
197
  if (row?.cwd) return row.cwd;
@@ -124,8 +217,7 @@ function readFilePrefix(filePath, maxBytes = 64 * 1024) {
124
217
  function codexThreadPreciseCreatedMs(row, homeDir = process.env.HOME) {
125
218
  const fallback = Number.isFinite(row?.created_at) ? row.created_at * 1000 : null;
126
219
  if (!row?.id) return fallback;
127
- const sessionsRoot = path.join(homeDir, '.codex', 'sessions');
128
- const files = findCodexSessionFiles(row.id, sessionsRoot);
220
+ const files = findCodexSessionFiles(row.id, null, homeDir);
129
221
  for (const filePath of files) {
130
222
  const prefix = readFilePrefix(filePath);
131
223
  if (!prefix) continue;
@@ -142,7 +234,8 @@ function codexThreadPreciseCreatedMs(row, homeDir = process.env.HOME) {
142
234
  }
143
235
 
144
236
  function _readCodexRolloutMetadata(filePath) {
145
- const prefix = readFilePrefix(filePath, 256 * 1024);
237
+ // 512KB covers the large turn_context blob that Codex injects before the user_message event
238
+ const prefix = readFilePrefix(filePath, 512 * 1024);
146
239
  if (!prefix) return null;
147
240
  let meta = null;
148
241
  let firstUser = '';
@@ -228,7 +321,7 @@ function findCodexThreadFromRolloutsForSession({
228
321
  }
229
322
  if (!entry.isFile() || !entry.name.endsWith('.jsonl')) continue;
230
323
  const row = _readCodexRolloutMetadata(fullPath);
231
- if (!row || row.cwd !== cwd) continue;
324
+ if (!row || !codexCwdMatches(row.cwd, cwd)) continue;
232
325
  const ms = row._preciseCreatedMs;
233
326
  if (ms < minMs || ms > maxMs) continue;
234
327
  matches.push({ row, distance: Math.abs(ms - startMs) });
@@ -257,6 +350,7 @@ function findCodexThreadForSession({
257
350
  const startMs = parseSessionStartMs(createdAtMs);
258
351
  if (!Number.isFinite(startMs)) return null;
259
352
  const startSec = Math.floor(startMs / 1000);
353
+ const cwdVariants = codexCwdVariants(cwd);
260
354
  const fallbackFromRollouts = () => findCodexThreadFromRolloutsForSession({
261
355
  cwd,
262
356
  createdAtMs,
@@ -265,16 +359,18 @@ function findCodexThreadForSession({
265
359
  maxDelaySec,
266
360
  allowAmbiguous,
267
361
  });
362
+ if (cwdVariants.length === 0) return null;
268
363
  let db = null;
269
364
  try {
270
365
  db = openCodexStateDb(homeDir);
271
366
  if (!db) return fallbackFromRollouts();
367
+ const cwdPlaceholders = cwdVariants.map(() => '?').join(', ');
272
368
  const rows = db.prepare(
273
369
  `SELECT rowid, id, title, first_user_message, model, cwd, git_branch, created_at, updated_at
274
370
  FROM threads
275
- WHERE cwd = ? AND created_at >= ? AND created_at <= ?
371
+ WHERE cwd IN (${cwdPlaceholders}) AND created_at >= ? AND created_at <= ?
276
372
  ORDER BY ABS(created_at - ?) ASC, created_at ASC`
277
- ).all(cwd, startSec - lookbackSec, startSec + maxDelaySec, startSec);
373
+ ).all(...cwdVariants, startSec - lookbackSec, startSec + maxDelaySec, startSec);
278
374
  if (rows.length === 0) return fallbackFromRollouts();
279
375
  const best = rows[0];
280
376
  const bestDistance = Math.abs((best.created_at || 0) - startSec);
@@ -482,6 +578,170 @@ function parseCodexJsonlFileIntoMessages(filePath, messages, options = {}) {
482
578
  };
483
579
  }
484
580
 
581
+ function normalizeCodexSearchValue(value) {
582
+ return String(value || '')
583
+ .trim()
584
+ .toLowerCase()
585
+ .replace(/(^|[\s([{])[$/]+(?=[a-z0-9_-])/g, '$1')
586
+ .replace(/\s+/g, ' ');
587
+ }
588
+
589
+ function codexSearchSnippet(text, terms) {
590
+ const raw = String(text || '').replace(/\s+/g, ' ').trim();
591
+ const normalized = normalizeCodexSearchValue(raw);
592
+ let idx = -1;
593
+ for (const term of terms || []) {
594
+ idx = normalized.indexOf(term);
595
+ if (idx >= 0) break;
596
+ }
597
+ if (idx < 0) return raw.slice(0, 180);
598
+ const start = Math.max(0, idx - 60);
599
+ const end = Math.min(raw.length, idx + 140);
600
+ return (start > 0 ? '...' : '') + raw.slice(start, end).trim() + (end < raw.length ? '...' : '');
601
+ }
602
+
603
+ function codexSearchMatchForMessage(msg, terms, order) {
604
+ const text = msg?.text || '';
605
+ const normalized = normalizeCodexSearchValue(text);
606
+ if (!terms.every(term => normalized.includes(term))) return null;
607
+
608
+ const rawLower = String(text).toLowerCase();
609
+ let score = msg.role === 'user' ? 20 : 0;
610
+ for (const term of terms) {
611
+ if (rawLower.includes(`$${term}`) || rawLower.includes(`/${term}`)) score += 50;
612
+ const wordRe = new RegExp(`(^|[^a-z0-9_-])${term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([^a-z0-9_-]|$)`, 'i');
613
+ if (wordRe.test(text)) score += 10;
614
+ }
615
+
616
+ return {
617
+ role: msg.role,
618
+ timestamp: msg.timestamp || '',
619
+ snippet: codexSearchSnippet(text, terms),
620
+ _rankScore: score,
621
+ _order: order,
622
+ };
623
+ }
624
+
625
+ function searchCodexRolloutFileMessages(filePath, terms, row) {
626
+ const state = { seenUsers: new Set(), sessionMeta: null };
627
+ const matches = [];
628
+ const chunkSize = 1024 * 1024;
629
+ const buf = Buffer.alloc(chunkSize);
630
+ const decoder = new StringDecoder('utf8');
631
+ let partial = '';
632
+ let fd = null;
633
+ let order = 0;
634
+
635
+ try {
636
+ fd = fs.openSync(filePath, 'r');
637
+ while (true) {
638
+ const bytesRead = fs.readSync(fd, buf, 0, buf.length, null);
639
+ if (bytesRead <= 0) break;
640
+ const text = partial + decoder.write(buf.subarray(0, bytesRead));
641
+ let start = 0;
642
+ while (true) {
643
+ const end = text.indexOf('\n', start);
644
+ if (end === -1) break;
645
+ const line = text.slice(start, end);
646
+ if (line) {
647
+ let entry;
648
+ try { entry = JSON.parse(line); } catch { entry = null; }
649
+ if (entry) {
650
+ if (!state.sessionMeta) {
651
+ const meta = codexSessionMetaFromEntry(entry);
652
+ if (meta?.id) state.sessionMeta = meta;
653
+ }
654
+ const msg = codexMessageFromEntry(entry);
655
+ if (msg) {
656
+ if (msg.role === 'user') {
657
+ const key = codexUserKey(msg.text);
658
+ if (!key || state.seenUsers.has(key)) {
659
+ start = end + 1;
660
+ continue;
661
+ }
662
+ state.seenUsers.add(key);
663
+ }
664
+ const match = codexSearchMatchForMessage(msg, terms, order++);
665
+ if (match) matches.push(match);
666
+ }
667
+ }
668
+ }
669
+ start = end + 1;
670
+ }
671
+ partial = text.slice(start);
672
+ }
673
+ const tail = decoder.end();
674
+ if (tail) partial += tail;
675
+ if (partial && partial.trim()) {
676
+ let entry;
677
+ try { entry = JSON.parse(partial); } catch { entry = null; }
678
+ if (entry) {
679
+ if (!state.sessionMeta) {
680
+ const meta = codexSessionMetaFromEntry(entry);
681
+ if (meta?.id) state.sessionMeta = meta;
682
+ }
683
+ const msg = codexMessageFromEntry(entry);
684
+ if (msg) {
685
+ let shouldConsider = true;
686
+ if (msg.role === 'user') {
687
+ const key = codexUserKey(msg.text);
688
+ if (!key || state.seenUsers.has(key)) {
689
+ shouldConsider = false;
690
+ } else {
691
+ state.seenUsers.add(key);
692
+ }
693
+ }
694
+ const match = shouldConsider ? codexSearchMatchForMessage(msg, terms, order++) : null;
695
+ if (match) matches.push(match);
696
+ }
697
+ }
698
+ }
699
+ } finally {
700
+ try { if (fd != null) fs.closeSync(fd); } catch {}
701
+ }
702
+
703
+ if (matches.length === 0) return null;
704
+ const meta = state.sessionMeta || {};
705
+ const id = row?.id || meta.id || codexRolloutIdFromPath(filePath);
706
+ if (!id) return null;
707
+ let stat = null;
708
+ try { stat = fs.statSync(filePath); } catch {}
709
+ const updatedAt = stat ? Math.floor(stat.mtimeMs / 1000) : (row?.updated_at || 0);
710
+ return {
711
+ id,
712
+ title: row?.title || row?.first_user_message || '',
713
+ first_user_message: row?.first_user_message || '',
714
+ model: row?.model || meta.model || '',
715
+ cwd: row?.cwd || meta.cwd || '',
716
+ git_branch: row?.git_branch || meta.git_branch || '',
717
+ created_at: row?.created_at || Math.floor(parseSessionStartMs(meta.timestamp) / 1000) || 0,
718
+ updated_at: updatedAt,
719
+ _jsonlPath: filePath,
720
+ _fileSize: stat?.size || 0,
721
+ _match: matches.sort((a, b) => (b._rankScore || 0) - (a._rankScore || 0) || (b._order || 0) - (a._order || 0))[0],
722
+ _matchCount: matches.length,
723
+ };
724
+ }
725
+
726
+ function searchCodexRolloutMessages(query, {
727
+ homeDir = process.env.HOME,
728
+ maxFiles = 100,
729
+ limit = 50,
730
+ } = {}) {
731
+ const terms = normalizeCodexSearchValue(query).split(/\s+/).filter(t => t.length >= 2);
732
+ if (terms.length === 0) return [];
733
+ const rows = listCodexSessionsFromRollouts(homeDir, maxFiles);
734
+ const hits = [];
735
+ for (const row of rows) {
736
+ if (hits.length >= limit) break;
737
+ const filePath = row._jsonlPath;
738
+ if (!filePath) continue;
739
+ const hit = searchCodexRolloutFileMessages(filePath, terms, row);
740
+ if (hit) hits.push(hit);
741
+ }
742
+ return hits.sort((a, b) => (b.updated_at || 0) - (a.updated_at || 0)).slice(0, limit);
743
+ }
744
+
485
745
  function commandLooksLikeCodex(value) {
486
746
  return /(^|\/)codex(\s|$)/i.test(String(value || ''));
487
747
  }
@@ -502,7 +762,7 @@ function findCodexThreadForCtmSession({
502
762
  if (explicitId) {
503
763
  const direct = getCodexThreadById(explicitId, homeDir);
504
764
  if (direct) return direct;
505
- const files = findCodexSessionFiles(explicitId, path.join(homeDir, '.codex', 'sessions'));
765
+ const files = findCodexSessionFiles(explicitId, null, homeDir);
506
766
  if (files.length > 0) {
507
767
  return {
508
768
  id: explicitId,
@@ -535,10 +795,41 @@ function findCodexThreadForCtmSession({
535
795
  });
536
796
  }
537
797
 
798
+ // Scan ~/.codex/sessions/**/*.jsonl and return session metadata rows sorted by mtime DESC.
799
+ // Used as a fallback when state_5.sqlite is unavailable or corrupt.
800
+ function listCodexSessionsFromRollouts(homeDir = process.env.HOME, limit = 50) {
801
+ const root = path.join(homeDir, '.codex', 'sessions');
802
+ if (!fs.existsSync(root)) return [];
803
+ const results = [];
804
+ const stack = [root];
805
+ while (stack.length > 0) {
806
+ const dir = stack.pop();
807
+ let entries = [];
808
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { continue; }
809
+ for (const entry of entries) {
810
+ const fullPath = path.join(dir, entry.name);
811
+ if (entry.isDirectory()) { stack.push(fullPath); continue; }
812
+ if (!entry.isFile() || !entry.name.endsWith('.jsonl')) continue;
813
+ const row = _readCodexRolloutMetadata(fullPath);
814
+ if (!row) continue;
815
+ try {
816
+ const st = fs.statSync(fullPath);
817
+ row.updated_at = Math.max(row.updated_at || 0, Math.floor(st.mtimeMs / 1000));
818
+ row._jsonlPath = fullPath;
819
+ } catch {}
820
+ results.push(row);
821
+ }
822
+ }
823
+ results.sort((a, b) => (b.updated_at || 0) - (a.updated_at || 0));
824
+ return results.slice(0, limit);
825
+ }
826
+
538
827
  module.exports = {
539
828
  cleanCodexUserText,
540
829
  codexInputText,
541
830
  codexMessageFromEntry,
831
+ codexRolloutFileInfo,
832
+ codexRolloutIdFromPath,
542
833
  codexUserKey,
543
834
  extractResumeTarget,
544
835
  findCodexSessionFiles,
@@ -548,6 +839,8 @@ module.exports = {
548
839
  getCodexThreadResumeCwd,
549
840
  getCodexThreadById,
550
841
  getResumeSpec,
842
+ listCodexSessionsFromRollouts,
843
+ searchCodexRolloutMessages,
551
844
  parseSessionStartMs,
552
845
  parseCodexJsonlIntoMessages,
553
846
  parseCodexJsonlFileIntoMessages,