create-walle 0.9.13 → 0.9.14

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 (58) hide show
  1. package/README.md +6 -1
  2. package/bin/create-walle.js +195 -30
  3. package/bin/mcp-inject.js +18 -53
  4. package/package.json +3 -1
  5. package/template/claude-task-manager/approval-agent.js +7 -0
  6. package/template/claude-task-manager/docs/session-standup-command-center-design.md +242 -0
  7. package/template/claude-task-manager/git-utils.js +111 -3
  8. package/template/claude-task-manager/lib/session-history.js +144 -16
  9. package/template/claude-task-manager/lib/session-standup.js +409 -0
  10. package/template/claude-task-manager/lib/standup-attention.js +200 -0
  11. package/template/claude-task-manager/lib/status-hooks.js +8 -2
  12. package/template/claude-task-manager/lib/update-telemetry.js +114 -0
  13. package/template/claude-task-manager/lib/walle-default-model.js +55 -0
  14. package/template/claude-task-manager/lib/walle-mcp-auto-config.js +62 -0
  15. package/template/claude-task-manager/lib/walle-supervisor.js +83 -19
  16. package/template/claude-task-manager/lib/worktree-cwd.js +82 -0
  17. package/template/claude-task-manager/providers/codex-mcp.js +104 -0
  18. package/template/claude-task-manager/providers/index.js +2 -0
  19. package/template/claude-task-manager/public/css/setup.css +2 -1
  20. package/template/claude-task-manager/public/css/walle.css +5 -0
  21. package/template/claude-task-manager/public/index.html +1596 -283
  22. package/template/claude-task-manager/public/js/session-search-utils.js +171 -1
  23. package/template/claude-task-manager/public/js/setup.js +62 -19
  24. package/template/claude-task-manager/public/js/stream-view.js +55 -6
  25. package/template/claude-task-manager/public/js/walle-session.js +73 -16
  26. package/template/claude-task-manager/public/js/walle.js +34 -2
  27. package/template/claude-task-manager/server.js +780 -177
  28. package/template/claude-task-manager/session-integrity.js +58 -15
  29. package/template/claude-task-manager/workers/approval-widget-validator.js +15 -5
  30. package/template/claude-task-manager/workers/state-detectors/codex.js +6 -0
  31. package/template/package.json +1 -1
  32. package/template/wall-e/agent.js +36 -7
  33. package/template/wall-e/api-walle.js +72 -20
  34. package/template/wall-e/coding/stream-processor.js +22 -2
  35. package/template/wall-e/coding-orchestrator.js +26 -6
  36. package/template/wall-e/eval/agent-runner.js +16 -4
  37. package/template/wall-e/eval/benchmark-generator.js +21 -1
  38. package/template/wall-e/eval/benchmarks/coding-agent.json +0 -596
  39. package/template/wall-e/eval/codex-cli-baseline.js +633 -0
  40. package/template/wall-e/eval/eval-orchestrator.js +3 -3
  41. package/template/wall-e/eval/run-agent-benchmarks.js +11 -3
  42. package/template/wall-e/eval/run-codex-cli-baseline.js +177 -0
  43. package/template/wall-e/lib/mcp-integration.js +220 -0
  44. package/template/wall-e/llm/ollama.js +47 -8
  45. package/template/wall-e/llm/ollama.plugin.json +1 -1
  46. package/template/wall-e/llm/tool-adapter.js +1 -0
  47. package/template/wall-e/loops/ingest.js +42 -8
  48. package/template/wall-e/mcp-server.js +272 -10
  49. package/template/wall-e/memory/ctm-session-context.js +910 -0
  50. package/template/wall-e/server.js +26 -1
  51. package/template/wall-e/skills/_bundled/scan-ctm-sessions/SKILL.md +20 -0
  52. package/template/wall-e/skills/_bundled/scan-ctm-sessions/run.js +43 -0
  53. package/template/wall-e/skills/skill-planner.js +52 -3
  54. package/template/wall-e/tools/builtin-middleware.js +55 -2
  55. package/template/wall-e/tools/shell-policy.js +1 -1
  56. package/template/wall-e/tools/slack-owner.js +104 -0
  57. package/template/website/index.html +2 -2
  58. package/template/builder-journal.md +0 -17
@@ -457,6 +457,106 @@ async function _gitSafe(cwd, args, timeoutMs = 5000) {
457
457
  });
458
458
  }
459
459
 
460
+ function _parseGitHubRemoteUrl(remoteUrl) {
461
+ const raw = String(remoteUrl || '').trim();
462
+ if (!raw) return null;
463
+
464
+ let host = 'github.com';
465
+ let owner = '';
466
+ let repo = '';
467
+ let match = raw.match(/^git@([^:]+):([^/]+)\/(.+)$/);
468
+ if (match) {
469
+ host = match[1];
470
+ owner = match[2];
471
+ repo = match[3];
472
+ } else {
473
+ match = raw.match(/^ssh:\/\/(?:[^@/]+@)?([^/]+)\/([^/]+)\/(.+)$/);
474
+ if (match) {
475
+ host = match[1];
476
+ owner = match[2];
477
+ repo = match[3];
478
+ } else {
479
+ match = raw.match(/^https?:\/\/([^/]+)\/([^/]+)\/(.+)$/);
480
+ if (!match) return null;
481
+ host = match[1];
482
+ owner = match[2];
483
+ repo = match[3];
484
+ }
485
+ }
486
+
487
+ repo = repo.replace(/(?:\.git)?\/?$/, '');
488
+ if (!host || !owner || !repo || repo.includes('/')) return null;
489
+ const nameWithOwner = `${owner}/${repo}`;
490
+ return {
491
+ host,
492
+ owner,
493
+ repo,
494
+ nameWithOwner,
495
+ ghRepo: host === 'github.com' ? nameWithOwner : `${host}/${nameWithOwner}`,
496
+ };
497
+ }
498
+
499
+ async function _githubRemoteSpec(cwd, remoteName) {
500
+ if (!remoteName) return null;
501
+ const urls = [
502
+ await _gitSafe(cwd, ['remote', 'get-url', remoteName]),
503
+ await _gitSafe(cwd, ['remote', 'get-url', '--push', remoteName]),
504
+ ].filter(Boolean);
505
+ for (const url of urls) {
506
+ const spec = _parseGitHubRemoteUrl(url);
507
+ if (spec) return { ...spec, remote: remoteName, url };
508
+ }
509
+ return null;
510
+ }
511
+
512
+ async function _selectPrBaseRemote(cwd, base, remoteNames) {
513
+ const remotes = Array.isArray(remoteNames) ? remoteNames : [];
514
+ if (remotes.includes('upstream') && await _githubRemoteSpec(cwd, 'upstream')) {
515
+ return 'upstream';
516
+ }
517
+
518
+ const configured = await _gitSafe(cwd, ['config', '--get', `branch.${base}.remote`]);
519
+ if (configured && configured !== '.' && remotes.includes(configured)
520
+ && await _githubRemoteSpec(cwd, configured)) {
521
+ return configured;
522
+ }
523
+
524
+ if (remotes.includes('origin') && await _githubRemoteSpec(cwd, 'origin')) {
525
+ return 'origin';
526
+ }
527
+
528
+ for (const remote of remotes) {
529
+ if (await _githubRemoteSpec(cwd, remote)) return remote;
530
+ }
531
+ return null;
532
+ }
533
+
534
+ async function _buildGhPrCreateArgs(cwd, branchName, opts) {
535
+ opts = opts || {};
536
+ const base = opts.base || 'main';
537
+ const title = opts.title || branchName;
538
+ const body = opts.body || `Branch \`${branchName}\` opened.`;
539
+ const remoteOut = await _gitSafe(cwd, ['remote']);
540
+ const remoteNames = remoteOut ? remoteOut.split('\n').map(s => s.trim()).filter(Boolean) : [];
541
+ const pushRemote = remoteNames.includes('origin') ? 'origin' : remoteNames[0];
542
+ const baseRemote = await _selectPrBaseRemote(cwd, base, remoteNames);
543
+ const baseSpec = await _githubRemoteSpec(cwd, baseRemote);
544
+ const pushSpec = await _githubRemoteSpec(cwd, pushRemote);
545
+
546
+ let head = branchName;
547
+ if (baseSpec && pushSpec
548
+ && (baseSpec.host !== pushSpec.host
549
+ || baseSpec.owner !== pushSpec.owner
550
+ || baseSpec.repo !== pushSpec.repo)) {
551
+ head = `${pushSpec.owner}:${branchName}`;
552
+ }
553
+
554
+ const args = ['pr', 'create'];
555
+ if (baseSpec) args.push('--repo', baseSpec.ghRepo);
556
+ args.push('--base', base, '--head', head, '--title', title, '--body', body);
557
+ return { args, baseRemote, pushRemote, baseSpec, pushSpec, head };
558
+ }
559
+
460
560
  function _isGhostPath(p) {
461
561
  if (!p) return true;
462
562
  if (p.includes('/~/')) return true; // literal-tilde corruption
@@ -949,11 +1049,18 @@ async function pushAndCreatePR(cwd, branchName, opts) {
949
1049
  body = log ? `## Summary\n\n${log}\n` : `Branch \`${branchName}\` opened.`;
950
1050
  }
951
1051
 
952
- // gh pr create use --json to get the URL back, fall back to scraping stdout.
1052
+ // gh pr create. Do not let gh infer the repository from `origin`: in forked
1053
+ // or stale-remote checkouts that can point it at an inaccessible repo.
1054
+ const prCommand = await _buildGhPrCreateArgs(wt.path, branchName, { base, title, body });
953
1055
  const ghOut = await new Promise((resolve, reject) => {
954
- execFile('gh', ['pr', 'create', '--base', base, '--head', branchName, '--title', title, '--body', body],
1056
+ execFile('gh', prCommand.args,
955
1057
  { cwd: wt.path, timeout: 60000 }, (err, stdout, stderr) => {
956
- if (err) return reject(new Error(`gh pr create failed: ${stderr || err.message}`));
1058
+ if (err) {
1059
+ const target = prCommand.baseSpec
1060
+ ? `${prCommand.baseSpec.ghRepo} from ${prCommand.head}`
1061
+ : `the repository inferred by gh from ${wt.path}`;
1062
+ return reject(new Error(`gh pr create failed for ${target}: ${stderr || err.message}`));
1063
+ }
957
1064
  resolve((stdout || '').trim());
958
1065
  });
959
1066
  });
@@ -971,4 +1078,5 @@ module.exports = {
971
1078
  // Internal helpers exposed for tests
972
1079
  _classifyState, _buildSummary, _isGhostPath, _isCanonicalPath, STATE_SORT_RANK,
973
1080
  _classifyMainRemote, _recommendedAction, _syncAllEligibility, _sameWorktreePath,
1081
+ _parseGitHubRemoteUrl, _githubRemoteSpec, _buildGhPrCreateArgs,
974
1082
  };
@@ -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);
@@ -502,7 +598,7 @@ function findCodexThreadForCtmSession({
502
598
  if (explicitId) {
503
599
  const direct = getCodexThreadById(explicitId, homeDir);
504
600
  if (direct) return direct;
505
- const files = findCodexSessionFiles(explicitId, path.join(homeDir, '.codex', 'sessions'));
601
+ const files = findCodexSessionFiles(explicitId, null, homeDir);
506
602
  if (files.length > 0) {
507
603
  return {
508
604
  id: explicitId,
@@ -535,10 +631,41 @@ function findCodexThreadForCtmSession({
535
631
  });
536
632
  }
537
633
 
634
+ // Scan ~/.codex/sessions/**/*.jsonl and return session metadata rows sorted by mtime DESC.
635
+ // Used as a fallback when state_5.sqlite is unavailable or corrupt.
636
+ function listCodexSessionsFromRollouts(homeDir = process.env.HOME, limit = 50) {
637
+ const root = path.join(homeDir, '.codex', 'sessions');
638
+ if (!fs.existsSync(root)) return [];
639
+ const results = [];
640
+ const stack = [root];
641
+ while (stack.length > 0) {
642
+ const dir = stack.pop();
643
+ let entries = [];
644
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { continue; }
645
+ for (const entry of entries) {
646
+ const fullPath = path.join(dir, entry.name);
647
+ if (entry.isDirectory()) { stack.push(fullPath); continue; }
648
+ if (!entry.isFile() || !entry.name.endsWith('.jsonl')) continue;
649
+ const row = _readCodexRolloutMetadata(fullPath);
650
+ if (!row) continue;
651
+ try {
652
+ const st = fs.statSync(fullPath);
653
+ row.updated_at = Math.max(row.updated_at || 0, Math.floor(st.mtimeMs / 1000));
654
+ row._jsonlPath = fullPath;
655
+ } catch {}
656
+ results.push(row);
657
+ }
658
+ }
659
+ results.sort((a, b) => (b.updated_at || 0) - (a.updated_at || 0));
660
+ return results.slice(0, limit);
661
+ }
662
+
538
663
  module.exports = {
539
664
  cleanCodexUserText,
540
665
  codexInputText,
541
666
  codexMessageFromEntry,
667
+ codexRolloutFileInfo,
668
+ codexRolloutIdFromPath,
542
669
  codexUserKey,
543
670
  extractResumeTarget,
544
671
  findCodexSessionFiles,
@@ -548,6 +675,7 @@ module.exports = {
548
675
  getCodexThreadResumeCwd,
549
676
  getCodexThreadById,
550
677
  getResumeSpec,
678
+ listCodexSessionsFromRollouts,
551
679
  parseSessionStartMs,
552
680
  parseCodexJsonlIntoMessages,
553
681
  parseCodexJsonlFileIntoMessages,