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.
- package/README.md +6 -1
- package/bin/create-walle.js +195 -30
- package/bin/mcp-inject.js +18 -53
- package/package.json +3 -1
- package/template/claude-task-manager/approval-agent.js +7 -0
- package/template/claude-task-manager/docs/session-standup-command-center-design.md +242 -0
- package/template/claude-task-manager/git-utils.js +111 -3
- package/template/claude-task-manager/lib/session-history.js +144 -16
- package/template/claude-task-manager/lib/session-standup.js +409 -0
- package/template/claude-task-manager/lib/standup-attention.js +200 -0
- package/template/claude-task-manager/lib/status-hooks.js +8 -2
- package/template/claude-task-manager/lib/update-telemetry.js +114 -0
- package/template/claude-task-manager/lib/walle-default-model.js +55 -0
- package/template/claude-task-manager/lib/walle-mcp-auto-config.js +62 -0
- package/template/claude-task-manager/lib/walle-supervisor.js +83 -19
- package/template/claude-task-manager/lib/worktree-cwd.js +82 -0
- package/template/claude-task-manager/providers/codex-mcp.js +104 -0
- package/template/claude-task-manager/providers/index.js +2 -0
- package/template/claude-task-manager/public/css/setup.css +2 -1
- package/template/claude-task-manager/public/css/walle.css +5 -0
- package/template/claude-task-manager/public/index.html +1596 -283
- package/template/claude-task-manager/public/js/session-search-utils.js +171 -1
- package/template/claude-task-manager/public/js/setup.js +62 -19
- package/template/claude-task-manager/public/js/stream-view.js +55 -6
- package/template/claude-task-manager/public/js/walle-session.js +73 -16
- package/template/claude-task-manager/public/js/walle.js +34 -2
- package/template/claude-task-manager/server.js +780 -177
- package/template/claude-task-manager/session-integrity.js +58 -15
- package/template/claude-task-manager/workers/approval-widget-validator.js +15 -5
- package/template/claude-task-manager/workers/state-detectors/codex.js +6 -0
- package/template/package.json +1 -1
- package/template/wall-e/agent.js +36 -7
- package/template/wall-e/api-walle.js +72 -20
- package/template/wall-e/coding/stream-processor.js +22 -2
- package/template/wall-e/coding-orchestrator.js +26 -6
- package/template/wall-e/eval/agent-runner.js +16 -4
- package/template/wall-e/eval/benchmark-generator.js +21 -1
- package/template/wall-e/eval/benchmarks/coding-agent.json +0 -596
- package/template/wall-e/eval/codex-cli-baseline.js +633 -0
- package/template/wall-e/eval/eval-orchestrator.js +3 -3
- package/template/wall-e/eval/run-agent-benchmarks.js +11 -3
- package/template/wall-e/eval/run-codex-cli-baseline.js +177 -0
- package/template/wall-e/lib/mcp-integration.js +220 -0
- package/template/wall-e/llm/ollama.js +47 -8
- package/template/wall-e/llm/ollama.plugin.json +1 -1
- package/template/wall-e/llm/tool-adapter.js +1 -0
- package/template/wall-e/loops/ingest.js +42 -8
- package/template/wall-e/mcp-server.js +272 -10
- package/template/wall-e/memory/ctm-session-context.js +910 -0
- package/template/wall-e/server.js +26 -1
- package/template/wall-e/skills/_bundled/scan-ctm-sessions/SKILL.md +20 -0
- package/template/wall-e/skills/_bundled/scan-ctm-sessions/run.js +43 -0
- package/template/wall-e/skills/skill-planner.js +52 -3
- package/template/wall-e/tools/builtin-middleware.js +55 -2
- package/template/wall-e/tools/shell-policy.js +1 -1
- package/template/wall-e/tools/slack-owner.js +104 -0
- package/template/website/index.html +2 -2
- 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
|
|
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',
|
|
1056
|
+
execFile('gh', prCommand.args,
|
|
955
1057
|
{ cwd: wt.path, timeout: 60000 }, (err, stdout, stderr) => {
|
|
956
|
-
if (err)
|
|
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
|
|
31
|
-
if (!
|
|
32
|
-
const
|
|
33
|
-
if (!
|
|
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
|
|
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() &&
|
|
99
|
+
if (entry.isFile() && codexRolloutIdFromPath(fullPath) === wanted) {
|
|
47
100
|
matches.push(fullPath);
|
|
48
101
|
}
|
|
49
102
|
}
|
|
50
103
|
}
|
|
51
|
-
return matches
|
|
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
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
371
|
+
WHERE cwd IN (${cwdPlaceholders}) AND created_at >= ? AND created_at <= ?
|
|
276
372
|
ORDER BY ABS(created_at - ?) ASC, created_at ASC`
|
|
277
|
-
).all(
|
|
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,
|
|
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,
|