create-walle 0.9.11 → 0.9.13
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 +3 -3
- package/package.json +2 -2
- package/template/bin/dev.sh +7 -1
- package/template/bin/setup.js +53 -9
- package/template/bin/sync-images.js +53 -0
- package/template/builder-journal.md +17 -0
- package/template/claude-task-manager/api-prompts.js +98 -13
- package/template/claude-task-manager/api-reviews.js +82 -5
- package/template/claude-task-manager/db.js +32 -5
- package/template/claude-task-manager/docs/session-capture-foundation-design.md +1273 -0
- package/template/claude-task-manager/lib/claude-desktop-sessions.js +696 -0
- package/template/claude-task-manager/lib/coding-agent-models.js +49 -1
- package/template/claude-task-manager/lib/session-capture.js +421 -0
- package/template/claude-task-manager/lib/session-history.js +135 -15
- package/template/claude-task-manager/lib/session-jobs.js +10 -5
- package/template/claude-task-manager/lib/session-stream.js +87 -19
- package/template/claude-task-manager/lib/setup-provider-config.js +115 -0
- package/template/claude-task-manager/lib/walle-ctm-history.js +72 -0
- package/template/claude-task-manager/lib/walle-session-context.js +61 -0
- package/template/claude-task-manager/lib/walle-transcript.js +176 -0
- package/template/claude-task-manager/public/css/setup.css +35 -8
- package/template/claude-task-manager/public/css/walle-session.css +56 -0
- package/template/claude-task-manager/public/css/walle.css +120 -0
- package/template/claude-task-manager/public/index.html +814 -181
- package/template/claude-task-manager/public/js/message-renderer.js +148 -19
- package/template/claude-task-manager/public/js/reviews.js +120 -62
- package/template/claude-task-manager/public/js/setup.js +75 -31
- package/template/claude-task-manager/public/js/stream-view.js +115 -55
- package/template/claude-task-manager/public/js/walle-session.js +84 -2
- package/template/claude-task-manager/public/js/walle.js +308 -54
- package/template/claude-task-manager/server.js +1092 -146
- package/template/claude-task-manager/session-integrity.js +181 -54
- package/template/claude-task-manager/session-utils.js +123 -41
- package/template/claude-task-manager/workers/state-detectors/codex.js +5 -2
- package/template/package.json +1 -1
- package/template/wall-e/adapters/ctm.js +39 -18
- package/template/wall-e/agent-runners/contract.js +17 -0
- package/template/wall-e/agent-runners/index.js +22 -0
- package/template/wall-e/agent-runtime/harness.js +212 -0
- package/template/wall-e/agent-runtime/index.js +8 -0
- package/template/wall-e/agent-runtime/registry.js +67 -0
- package/template/wall-e/agent-runtime/session-store.js +179 -0
- package/template/wall-e/agent-runtime/spawn.js +208 -0
- package/template/wall-e/api-walle.js +174 -7
- package/template/wall-e/brain.js +266 -28
- package/template/wall-e/channels/policy.js +88 -0
- package/template/wall-e/channels/registry.js +15 -1
- package/template/wall-e/channels/reply-dispatcher.js +70 -0
- package/template/wall-e/channels/session-bindings.js +51 -0
- package/template/wall-e/chat/code-review-context.js +29 -0
- package/template/wall-e/chat.js +188 -42
- package/template/wall-e/coding/acp-adapter.js +188 -0
- package/template/wall-e/coding/agent-catalog.js +129 -0
- package/template/wall-e/coding/compaction-service.js +247 -0
- package/template/wall-e/coding/execution-trace.js +3 -0
- package/template/wall-e/coding/instruction-service.js +224 -0
- package/template/wall-e/coding/model-message.js +67 -0
- package/template/wall-e/coding/permission-rules-store.js +111 -0
- package/template/wall-e/coding/permission-service.js +266 -0
- package/template/wall-e/coding/prompt-bundle.js +67 -0
- package/template/wall-e/coding/prompt-runtime.js +243 -0
- package/template/wall-e/coding/provider-transform.js +188 -0
- package/template/wall-e/coding/runtime-mode.js +132 -0
- package/template/wall-e/coding/snapshot-service.js +155 -0
- package/template/wall-e/coding/stream-processor.js +268 -0
- package/template/wall-e/coding/task-tool.js +255 -0
- package/template/wall-e/coding/tool-registry.js +361 -0
- package/template/wall-e/coding/transcript-writer.js +143 -0
- package/template/wall-e/coding/workspace-replay.js +324 -0
- package/template/wall-e/coding-context.js +4 -22
- package/template/wall-e/coding-orchestrator.js +307 -18
- package/template/wall-e/coding-prompts.js +44 -3
- package/template/wall-e/context/context-builder.js +43 -1
- package/template/wall-e/context/topic-matcher.js +1 -1
- package/template/wall-e/eval/agent-runner.js +59 -13
- package/template/wall-e/eval/benchmarks/memory-retrieval.json +155 -57
- package/template/wall-e/eval/benchmarks.js +100 -16
- package/template/wall-e/eval/eval-orchestrator.js +218 -8
- package/template/wall-e/eval/harvester.js +62 -5
- package/template/wall-e/eval/head-to-head.js +23 -2
- package/template/wall-e/eval/humaneval-adapter.js +30 -5
- package/template/wall-e/eval/livecodebench-adapter.js +29 -5
- package/template/wall-e/eval/manifest.js +186 -0
- package/template/wall-e/eval/run-agent-benchmarks.js +66 -2
- package/template/wall-e/eval/session-retrieval-benchmark.js +150 -0
- package/template/wall-e/eval/session-transcripts.js +57 -4
- package/template/wall-e/eval/swebench-adapter.js +109 -3
- package/template/wall-e/evaluation/agent-router.js +53 -1
- package/template/wall-e/evaluation/coding-quorum.js +48 -1
- package/template/wall-e/evaluation/router.js +4 -2
- package/template/wall-e/evaluation/tier-selector.js +11 -1
- package/template/wall-e/extraction/contradiction.js +2 -2
- package/template/wall-e/extraction/indexer.js +2 -1
- package/template/wall-e/extraction/knowledge-extractor.js +2 -2
- package/template/wall-e/hooks/cli.js +92 -0
- package/template/wall-e/hooks/discovery.js +119 -0
- package/template/wall-e/hooks/index.js +7 -0
- package/template/wall-e/hooks/manifest.js +55 -0
- package/template/wall-e/hooks/runtime.js +84 -0
- package/template/wall-e/hooks/session-memory.js +225 -0
- package/template/wall-e/http/auth.js +6 -2
- package/template/wall-e/http/chat-api.js +54 -8
- package/template/wall-e/integrations/claude-plugin/hooks/hooks.json +27 -0
- package/template/wall-e/integrations/claude-plugin/hooks/walle-precompact-hook.sh +5 -0
- package/template/wall-e/integrations/claude-plugin/hooks/walle-stop-hook.sh +5 -0
- package/template/wall-e/integrations/codex-plugin/hooks/walle-hook.sh +7 -0
- package/template/wall-e/integrations/codex-plugin/hooks.json +37 -0
- package/template/wall-e/listening/calendar.js +3 -1
- package/template/wall-e/llm/client.js +64 -10
- package/template/wall-e/llm/google.js +39 -5
- package/template/wall-e/llm/ollama.js +1 -1
- package/template/wall-e/llm/ollama.plugin.json +1 -1
- package/template/wall-e/llm/provider-availability.js +10 -0
- package/template/wall-e/llm/provider-error.js +269 -0
- package/template/wall-e/llm/tool-adapter.js +48 -12
- package/template/wall-e/loops/boot.js +2 -1
- package/template/wall-e/loops/initiative.js +2 -2
- package/template/wall-e/loops/tasks.js +8 -47
- package/template/wall-e/loops/workspace-prompts.js +20 -0
- package/template/wall-e/mcp-server.js +442 -1
- package/template/wall-e/memory/session-ingest-service.js +159 -0
- package/template/wall-e/memory/source-indexer.js +289 -0
- package/template/wall-e/plugins/discovery.js +83 -0
- package/template/wall-e/plugins/manifest-loader.js +50 -10
- package/template/wall-e/plugins/manifest-schema.js +69 -0
- package/template/wall-e/plugins/model-catalog.js +55 -0
- package/template/wall-e/prompts/coding/base.txt +2 -0
- package/template/wall-e/prompts/coding/deepseek.txt +1 -0
- package/template/wall-e/prompts/coding/memory-protocol.md +9 -0
- package/template/wall-e/prompts/coding/plan.txt +1 -0
- package/template/wall-e/runtime/execution-trace.js +220 -0
- package/template/wall-e/security/audit.js +266 -0
- package/template/wall-e/security/ssrf.js +236 -0
- package/template/wall-e/session-files.js +303 -0
- package/template/wall-e/skills/_bundled/slack-backfill/SKILL.md +3 -0
- package/template/wall-e/skills/_bundled/slack-sync/SKILL.md +3 -0
- package/template/wall-e/skills/internal-skill-registry.js +2 -2
- package/template/wall-e/skills/script-skill-runner.js +143 -0
- package/template/wall-e/skills/skill-executor.js +5 -6
- package/template/wall-e/skills/skill-fallback.js +3 -1
- package/template/wall-e/skills/skill-harness-registry.js +7 -8
- package/template/wall-e/skills/skill-planner.js +52 -4
- package/template/wall-e/skills/slack-ingest.js +11 -3
- package/template/wall-e/sources/base.js +90 -0
- package/template/wall-e/sources/builtin.js +33 -0
- package/template/wall-e/sources/claude-code-jsonl.js +78 -0
- package/template/wall-e/sources/codex-jsonl.js +125 -0
- package/template/wall-e/sources/coding-session-utils.js +117 -0
- package/template/wall-e/sources/contract-suite.js +59 -0
- package/template/wall-e/sources/gemini-jsonl.js +85 -0
- package/template/wall-e/sources/index.js +9 -0
- package/template/wall-e/sources/jsonl-utils.js +181 -0
- package/template/wall-e/sources/record-types.js +252 -0
- package/template/wall-e/sources/registry.js +92 -0
- package/template/wall-e/sources/transforms.js +100 -0
- package/template/wall-e/sources/walle-jsonl.js +108 -0
- package/template/wall-e/tools/coding-middleware.js +31 -1
- package/template/wall-e/tools/file-tracker.js +25 -1
- package/template/wall-e/tools/local-tools.js +75 -47
- package/template/wall-e/tools/session-sharing.js +68 -1
- package/template/wall-e/tools/shell-analyzer.js +1 -1
- package/template/wall-e/tools/shell-policy.js +47 -0
- package/template/wall-e/tools/snapshot.js +42 -0
- package/template/wall-e/training/harvester.js +62 -5
- package/template/wall-e/utils/repair.js +253 -1
- package/template/website/index.html +3 -3
- package/template/wall-e/skills/_bundled/slack-mentions/.watched-threads.json +0 -18
|
@@ -10,15 +10,116 @@
|
|
|
10
10
|
// 3. Wrong mapping: CTM tab's agent_session_id points to wrong JSONL file
|
|
11
11
|
// 4. Orphan file: JSONL file on disk has no corresponding DB row
|
|
12
12
|
// 5. Duplicate claim: Multiple DB rows claim the same agent_session_id
|
|
13
|
-
// 6. Timestamp drift: DB created_at is significantly off from
|
|
13
|
+
// 6. Timestamp drift: DB created_at is significantly off from JSONL timestamp
|
|
14
14
|
|
|
15
15
|
const fs = require('fs');
|
|
16
16
|
const path = require('path');
|
|
17
|
+
const claudeDesktopSessions = require('./lib/claude-desktop-sessions');
|
|
17
18
|
|
|
18
19
|
const CLAUDE_PROJECTS_DIR = path.join(process.env.HOME, '.claude', 'projects');
|
|
19
20
|
|
|
20
21
|
// --- Detection ---
|
|
21
22
|
|
|
23
|
+
function dbTimestampFromIso(value) {
|
|
24
|
+
if (!value) return '';
|
|
25
|
+
const ms = new Date(value).getTime();
|
|
26
|
+
if (!Number.isFinite(ms)) return '';
|
|
27
|
+
return new Date(ms).toISOString().replace('T', ' ').replace(/\.\d+Z$/, '');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function parseDbTimestampMs(value) {
|
|
31
|
+
if (!value) return NaN;
|
|
32
|
+
const str = String(value).trim();
|
|
33
|
+
if (!str) return NaN;
|
|
34
|
+
const hasZone = /(?:Z|[+-]\d\d:?\d\d)$/.test(str);
|
|
35
|
+
return new Date(hasZone ? str : str.replace(' ', 'T') + 'Z').getTime();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isLowSignalIntegrityUserText(text) {
|
|
39
|
+
const s = String(text || '').trim();
|
|
40
|
+
if (!s) return true;
|
|
41
|
+
if (/^\[Request interrupted\b/i.test(s)) return true;
|
|
42
|
+
if (/^\[(?:request\s+)?interrupted\b/i.test(s)) return true;
|
|
43
|
+
if (/^<command-(?:message|name)>/i.test(s)) return true;
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function normalizedWordSet(text) {
|
|
48
|
+
return new Set(String(text || '')
|
|
49
|
+
.replace(/<[^>]+>/g, ' ')
|
|
50
|
+
.replace(/[^a-zA-Z0-9\s]/g, ' ')
|
|
51
|
+
.toLowerCase()
|
|
52
|
+
.split(/\s+/)
|
|
53
|
+
.filter(w => w.length > 3));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function wordSimilarity(a, b) {
|
|
57
|
+
const aWords = normalizedWordSet(a);
|
|
58
|
+
const bWords = normalizedWordSet(b);
|
|
59
|
+
if (aWords.size <= 3 || bWords.size <= 3) return null;
|
|
60
|
+
let overlap = 0;
|
|
61
|
+
for (const w of aWords) if (bWords.has(w)) overlap++;
|
|
62
|
+
return overlap / Math.min(aWords.size, bWords.size);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function textFromUserEntry(entry) {
|
|
66
|
+
if (!entry || entry.type !== 'user' || entry.message?.role !== 'user') return '';
|
|
67
|
+
const c = entry.message.content;
|
|
68
|
+
if (typeof c === 'string') return c;
|
|
69
|
+
if (Array.isArray(c)) return c.filter(b => b && b.type === 'text').map(b => b.text || '').join('\n');
|
|
70
|
+
return '';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function readJsonlHeaderSignals(filePath, stat, {
|
|
74
|
+
maxBytes = 128 * 1024,
|
|
75
|
+
maxUserMessages = 5,
|
|
76
|
+
} = {}) {
|
|
77
|
+
const result = { timestamp: '', rawTimestamp: '', userMessages: [] };
|
|
78
|
+
let fallbackTimestamp = '';
|
|
79
|
+
let fd = null;
|
|
80
|
+
try {
|
|
81
|
+
const sourcePath = claudeDesktopSessions.sourcePathForStat(filePath);
|
|
82
|
+
fd = fs.openSync(sourcePath, 'r');
|
|
83
|
+
const buf = Buffer.alloc(Math.min(stat.size || 0, maxBytes));
|
|
84
|
+
const bytesRead = fs.readSync(fd, buf, 0, buf.length, 0);
|
|
85
|
+
const chunk = buf.toString('utf8', 0, bytesRead);
|
|
86
|
+
const lines = chunk.split('\n');
|
|
87
|
+
for (const line of lines) {
|
|
88
|
+
if (!line.trim()) continue;
|
|
89
|
+
try {
|
|
90
|
+
const entry = JSON.parse(line);
|
|
91
|
+
if (!fallbackTimestamp && entry.timestamp) fallbackTimestamp = entry.timestamp;
|
|
92
|
+
const text = textFromUserEntry(entry);
|
|
93
|
+
if (text && !isLowSignalIntegrityUserText(text)) {
|
|
94
|
+
if (!result.timestamp && entry.timestamp) result.timestamp = entry.timestamp;
|
|
95
|
+
result.userMessages.push(text);
|
|
96
|
+
if (result.userMessages.length >= maxUserMessages && result.timestamp) break;
|
|
97
|
+
}
|
|
98
|
+
} catch {
|
|
99
|
+
if (!fallbackTimestamp) {
|
|
100
|
+
const ts = line.match(/"timestamp"\s*:\s*"([^"]+)"/);
|
|
101
|
+
if (ts) fallbackTimestamp = ts[1];
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
} catch {
|
|
106
|
+
// Integrity checks are advisory; unreadable files are handled elsewhere.
|
|
107
|
+
} finally {
|
|
108
|
+
if (fd !== null) {
|
|
109
|
+
try { fs.closeSync(fd); } catch {}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
result.rawTimestamp = fallbackTimestamp;
|
|
113
|
+
if (!result.timestamp) result.timestamp = fallbackTimestamp;
|
|
114
|
+
return result;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function getJsonlHeaderSignals(file) {
|
|
118
|
+
if (!file) return { timestamp: '', userMessages: [] };
|
|
119
|
+
if (!file._headerSignals) file._headerSignals = readJsonlHeaderSignals(file.filePath, file.stat);
|
|
120
|
+
return file._headerSignals;
|
|
121
|
+
}
|
|
122
|
+
|
|
22
123
|
/**
|
|
23
124
|
* Run a full integrity check between the sessions DB and JSONL files on disk.
|
|
24
125
|
* Returns an array of issue objects, each with { type, severity, sessionId, details, suggestion }.
|
|
@@ -55,9 +156,10 @@ function detectMismatches(db, getAllSessionFiles) {
|
|
|
55
156
|
const fileIndex = {}; // uuid -> { filePath, stat, projectEntry }
|
|
56
157
|
try {
|
|
57
158
|
for (const { filePath, projectEntry } of getAllSessionFiles()) {
|
|
58
|
-
const
|
|
159
|
+
const virtual = claudeDesktopSessions.parseVirtualSessionPath(filePath);
|
|
160
|
+
const uuid = virtual ? virtual.sessionId : path.basename(filePath).replace(/\.jsonl(\.bak)?$/, '');
|
|
59
161
|
try {
|
|
60
|
-
const stat = fs.statSync(filePath);
|
|
162
|
+
const stat = fs.statSync(claudeDesktopSessions.sourcePathForStat(filePath));
|
|
61
163
|
fileIndex[uuid] = { filePath, stat, projectEntry };
|
|
62
164
|
} catch {}
|
|
63
165
|
}
|
|
@@ -119,21 +221,30 @@ function detectMismatches(db, getAllSessionFiles) {
|
|
|
119
221
|
}
|
|
120
222
|
}
|
|
121
223
|
|
|
122
|
-
// Check 3: Timestamp drift (created_at vs
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
224
|
+
// Check 3: Timestamp drift (created_at vs JSONL timestamp)
|
|
225
|
+
// Filesystem birthtime is not reliable for Claude JSONL sessions: Dropbox,
|
|
226
|
+
// restores, rsync/copy, and fresh dev databases can all make birthtime mean
|
|
227
|
+
// "when this file arrived here", not "when the conversation started".
|
|
228
|
+
const headerSignals = getJsonlHeaderSignals(file);
|
|
229
|
+
if (row.created_at && headerSignals.timestamp) {
|
|
230
|
+
const dbMs = parseDbTimestampMs(row.created_at);
|
|
231
|
+
const fileMs = new Date(headerSignals.timestamp).getTime();
|
|
232
|
+
const rawMs = headerSignals.rawTimestamp ? new Date(headerSignals.rawTimestamp).getTime() : NaN;
|
|
126
233
|
const drift = Math.abs(dbMs - fileMs);
|
|
127
|
-
|
|
234
|
+
const rawDrift = Math.abs(dbMs - rawMs);
|
|
235
|
+
const matchesRawStart = Number.isFinite(rawMs) && rawDrift <= 300000;
|
|
236
|
+
if (Number.isFinite(dbMs) && Number.isFinite(fileMs) && drift > 300000 && !matchesRawStart) { // >5 minutes off
|
|
128
237
|
issues.push({
|
|
129
238
|
type: 'timestamp_drift', severity: 'info', sessionId: sid,
|
|
130
239
|
details: {
|
|
131
240
|
file_id: expectedFileId,
|
|
132
241
|
db_created_at: row.created_at,
|
|
133
|
-
|
|
242
|
+
file_created_at: dbTimestampFromIso(headerSignals.timestamp),
|
|
243
|
+
file_timestamp: headerSignals.timestamp,
|
|
244
|
+
raw_file_timestamp: headerSignals.rawTimestamp || '',
|
|
134
245
|
drift_seconds: Math.round(drift / 1000),
|
|
135
246
|
},
|
|
136
|
-
suggestion: '
|
|
247
|
+
suggestion: 'DB created_at differs from the JSONL conversation timestamp.',
|
|
137
248
|
});
|
|
138
249
|
}
|
|
139
250
|
}
|
|
@@ -151,7 +262,7 @@ function detectMismatches(db, getAllSessionFiles) {
|
|
|
151
262
|
// Prefix-copy rescue: if candidate's JSONL prefix contains a known
|
|
152
263
|
// sibling sessionId, treat as lineage and skip.
|
|
153
264
|
let prefixRescue = false;
|
|
154
|
-
if (row.jsonl_path && fs.existsSync(row.jsonl_path)) {
|
|
265
|
+
if (row.jsonl_path && !claudeDesktopSessions.isVirtualSessionPath(row.jsonl_path) && fs.existsSync(row.jsonl_path)) {
|
|
155
266
|
const known = knownAgentIdsForTab(db, row.id);
|
|
156
267
|
const prefixIds = readPrefixSessionIds(row.jsonl_path);
|
|
157
268
|
for (const pid of prefixIds) {
|
|
@@ -176,7 +287,7 @@ function detectMismatches(db, getAllSessionFiles) {
|
|
|
176
287
|
// Backward-compat: slug empty in DB. Try reading the JSONL directly
|
|
177
288
|
// so we don't miss the detection just because backfill hasn't caught up.
|
|
178
289
|
const canonical = canonicalSlugForTab(db, row.id);
|
|
179
|
-
if (canonical && fs.existsSync(row.jsonl_path)) {
|
|
290
|
+
if (canonical && !claudeDesktopSessions.isVirtualSessionPath(row.jsonl_path) && fs.existsSync(row.jsonl_path)) {
|
|
180
291
|
const hdr = readFirstSlug(row.jsonl_path);
|
|
181
292
|
if (hdr && hdr.slug && hdr.slug !== canonical) {
|
|
182
293
|
const known = knownAgentIdsForTab(db, row.id);
|
|
@@ -209,42 +320,24 @@ function detectMismatches(db, getAllSessionFiles) {
|
|
|
209
320
|
// the mapping is likely wrong.
|
|
210
321
|
if (row.first_message && row.first_message.length > 20) {
|
|
211
322
|
try {
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
if (entry.type === 'user' && entry.message?.role === 'user') {
|
|
224
|
-
const c = entry.message.content;
|
|
225
|
-
fileFirstMsg = typeof c === 'string' ? c
|
|
226
|
-
: Array.isArray(c) ? (c.find(b => b.type === 'text')?.text || '') : '';
|
|
227
|
-
if (fileFirstMsg) break;
|
|
228
|
-
}
|
|
229
|
-
} catch {}
|
|
230
|
-
}
|
|
231
|
-
if (fileFirstMsg && fileFirstMsg.length > 20) {
|
|
232
|
-
// Compare using word overlap — strip common boilerplate
|
|
233
|
-
const normalize = (s) => s.replace(/<[^>]+>/g, ' ').replace(/[^a-zA-Z0-9\s]/g, ' ').toLowerCase().split(/\s+/).filter(w => w.length > 3);
|
|
234
|
-
const dbWords = new Set(normalize(row.first_message));
|
|
235
|
-
const fileWords = new Set(normalize(fileFirstMsg));
|
|
236
|
-
if (dbWords.size > 3 && fileWords.size > 3) {
|
|
237
|
-
let overlap = 0;
|
|
238
|
-
for (const w of dbWords) if (fileWords.has(w)) overlap++;
|
|
239
|
-
const similarity = overlap / Math.min(dbWords.size, fileWords.size);
|
|
240
|
-
if (similarity < 0.1) { // <10% word overlap = likely wrong mapping
|
|
323
|
+
const fileUserMessages = getJsonlHeaderSignals(file).userMessages
|
|
324
|
+
.filter(m => m && m.length > 20)
|
|
325
|
+
.slice(0, 5);
|
|
326
|
+
if (fileUserMessages.length > 0) {
|
|
327
|
+
const similarities = fileUserMessages
|
|
328
|
+
.map(m => ({ text: m, score: wordSimilarity(row.first_message, m) }))
|
|
329
|
+
.filter(s => s.score !== null);
|
|
330
|
+
if (similarities.length > 0) {
|
|
331
|
+
similarities.sort((a, b) => b.score - a.score);
|
|
332
|
+
const best = similarities[0];
|
|
333
|
+
if (best.score < 0.1) { // <10% word overlap = likely wrong mapping
|
|
241
334
|
issues.push({
|
|
242
335
|
type: 'wrong_mapping', severity: 'error', sessionId: sid,
|
|
243
336
|
details: {
|
|
244
337
|
file_id: expectedFileId,
|
|
245
338
|
db_first_message: row.first_message.slice(0, 100),
|
|
246
|
-
file_first_message:
|
|
247
|
-
word_similarity: Math.round(
|
|
339
|
+
file_first_message: best.text.slice(0, 100),
|
|
340
|
+
word_similarity: Math.round(best.score * 100) + '%',
|
|
248
341
|
},
|
|
249
342
|
suggestion: 'Content mismatch — DB row may be linked to wrong JSONL file.',
|
|
250
343
|
});
|
|
@@ -346,9 +439,10 @@ function recoverMismatches(db, issues, getAllSessionFiles) {
|
|
|
346
439
|
const fileIndex = {};
|
|
347
440
|
try {
|
|
348
441
|
for (const { filePath, projectEntry } of getAllSessionFiles()) {
|
|
349
|
-
const
|
|
442
|
+
const virtual = claudeDesktopSessions.parseVirtualSessionPath(filePath);
|
|
443
|
+
const uuid = virtual ? virtual.sessionId : path.basename(filePath).replace(/\.jsonl(\.bak)?$/, '');
|
|
350
444
|
try {
|
|
351
|
-
const stat = fs.statSync(filePath);
|
|
445
|
+
const stat = fs.statSync(claudeDesktopSessions.sourcePathForStat(filePath));
|
|
352
446
|
fileIndex[uuid] = { filePath, stat, projectEntry };
|
|
353
447
|
} catch {}
|
|
354
448
|
}
|
|
@@ -425,12 +519,12 @@ function recoverMismatches(db, issues, getAllSessionFiles) {
|
|
|
425
519
|
}
|
|
426
520
|
|
|
427
521
|
case 'timestamp_drift': {
|
|
428
|
-
// Fix timestamp from
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
522
|
+
// Fix timestamp from JSONL timestamp. Do not use filesystem birthtime:
|
|
523
|
+
// it changes under copy/sync/restore and caused fresh dev DBs to
|
|
524
|
+
// rewrite conversation chronology to the wrong date.
|
|
525
|
+
const ts = issue.details?.file_created_at;
|
|
526
|
+
if (!ts) { result.skipped++; break; }
|
|
432
527
|
try {
|
|
433
|
-
const ts = file.stat.birthtime.toISOString().replace('T', ' ').replace(/\.\d+Z$/, '');
|
|
434
528
|
db.prepare('UPDATE ctm_sessions SET created_at = ?, updated_at = datetime(\'now\') WHERE id = ?')
|
|
435
529
|
.run(ts, issue.sessionId);
|
|
436
530
|
result.fixed++;
|
|
@@ -531,7 +625,7 @@ function recoverMismatches(db, issues, getAllSessionFiles) {
|
|
|
531
625
|
if (stRow && stRow.agent_session_id === badAgent) {
|
|
532
626
|
db.prepare('UPDATE startup_tasks SET agent_session_id = ? WHERE ctm_session_id = ?').run(rescue.agent_session_id, ctmId);
|
|
533
627
|
}
|
|
534
|
-
if (badRow && badRow.jsonl_path && fs.existsSync(badRow.jsonl_path)) {
|
|
628
|
+
if (badRow && badRow.jsonl_path && fs.existsSync(claudeDesktopSessions.sourcePathForStat(badRow.jsonl_path))) {
|
|
535
629
|
const existingCtm = db.prepare('SELECT id FROM ctm_sessions WHERE id = ?').get(badAgent);
|
|
536
630
|
if (!existingCtm) {
|
|
537
631
|
db.prepare("INSERT OR IGNORE INTO ctm_sessions (id, provider, created_at, updated_at) VALUES (?, 'claude', datetime('now'), datetime('now'))").run(badAgent);
|
|
@@ -564,6 +658,34 @@ function recoverMismatches(db, issues, getAllSessionFiles) {
|
|
|
564
658
|
|
|
565
659
|
// --- Telemetry Reporting ---
|
|
566
660
|
|
|
661
|
+
const INTEGRITY_ISSUE_TELEMETRY_TTL_MS = 60 * 60 * 1000;
|
|
662
|
+
const _lastIntegrityIssueTelemetry = new Map();
|
|
663
|
+
|
|
664
|
+
function integrityIssueTelemetryKey(issue) {
|
|
665
|
+
const details = issue && issue.details && typeof issue.details === 'object'
|
|
666
|
+
? Object.keys(issue.details).sort().map(k => `${k}:${String(issue.details[k]).slice(0, 80)}`).join('|')
|
|
667
|
+
: String(issue?.details || '');
|
|
668
|
+
return [
|
|
669
|
+
issue?.type || 'unknown',
|
|
670
|
+
issue?.severity || 'unknown',
|
|
671
|
+
issue?.sessionId || '',
|
|
672
|
+
details,
|
|
673
|
+
].join(':');
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function shouldReportIntegrityIssue(issue, now = Date.now()) {
|
|
677
|
+
const key = integrityIssueTelemetryKey(issue);
|
|
678
|
+
const last = _lastIntegrityIssueTelemetry.get(key) || 0;
|
|
679
|
+
if (now - last < INTEGRITY_ISSUE_TELEMETRY_TTL_MS) return false;
|
|
680
|
+
_lastIntegrityIssueTelemetry.set(key, now);
|
|
681
|
+
if (_lastIntegrityIssueTelemetry.size > 1000) {
|
|
682
|
+
for (const [k, ts] of _lastIntegrityIssueTelemetry) {
|
|
683
|
+
if (now - ts > INTEGRITY_ISSUE_TELEMETRY_TTL_MS) _lastIntegrityIssueTelemetry.delete(k);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
return true;
|
|
687
|
+
}
|
|
688
|
+
|
|
567
689
|
/**
|
|
568
690
|
* Report integrity check results to telemetry with full debug context.
|
|
569
691
|
*/
|
|
@@ -588,7 +710,7 @@ function reportToTelemetry(telemetry, issues, recovery) {
|
|
|
588
710
|
|
|
589
711
|
// Detail events for errors and warnings (not info — too noisy)
|
|
590
712
|
const serious = issues.filter(i => i.severity === 'error' || i.severity === 'critical');
|
|
591
|
-
for (const issue of serious.slice(0, 20)) { // Cap to prevent flood
|
|
713
|
+
for (const issue of serious.filter(issue => shouldReportIntegrityIssue(issue)).slice(0, 20)) { // Cap to prevent flood
|
|
592
714
|
telemetry.track('session_integrity_issue', {
|
|
593
715
|
type: issue.type,
|
|
594
716
|
severity: issue.severity,
|
|
@@ -659,8 +781,13 @@ function runIntegrityCheck(opts) {
|
|
|
659
781
|
const issues = detectMismatches(db, getAllSessionFiles);
|
|
660
782
|
|
|
661
783
|
let recovery = null;
|
|
662
|
-
if (autoRecover
|
|
663
|
-
|
|
784
|
+
if (autoRecover) {
|
|
785
|
+
const recoverable = issues.filter(i =>
|
|
786
|
+
i.severity !== 'info' || i.type === 'stale_metadata' || i.type === 'timestamp_drift'
|
|
787
|
+
);
|
|
788
|
+
if (recoverable.length > 0) {
|
|
789
|
+
recovery = recoverMismatches(db, recoverable, getAllSessionFiles);
|
|
790
|
+
}
|
|
664
791
|
}
|
|
665
792
|
|
|
666
793
|
// Additionally run the relink audit to surface tab↔JSONL cross-contamination.
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
const fs = require('fs');
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const os = require('os');
|
|
5
|
+
const walleTranscript = require('./lib/walle-transcript');
|
|
6
|
+
const claudeDesktopSessions = require('./lib/claude-desktop-sessions');
|
|
5
7
|
|
|
6
8
|
function decodeProjectEntry(entry) {
|
|
7
9
|
if (!entry.startsWith('-')) return entry;
|
|
@@ -23,7 +25,70 @@ function decodeProjectEntry(entry) {
|
|
|
23
25
|
return solve(1, '/' + parts[0]) || '/' + parts.join('/');
|
|
24
26
|
}
|
|
25
27
|
|
|
28
|
+
function inferModelProvider(modelId) {
|
|
29
|
+
if (!modelId || modelId.startsWith('<')) return '';
|
|
30
|
+
if (modelId.startsWith('claude-')) return 'anthropic';
|
|
31
|
+
if (modelId.startsWith('codex-')) return 'openai';
|
|
32
|
+
if (modelId.startsWith('gemini-')) return 'google';
|
|
33
|
+
if (modelId.startsWith('gpt-') || modelId.startsWith('o1-') || modelId.startsWith('o3-') || modelId.startsWith('o4-')) return 'openai';
|
|
34
|
+
if (modelId.startsWith('deepseek-')) return 'deepseek';
|
|
35
|
+
return 'unknown';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function messageText(messageOrContent) {
|
|
39
|
+
if (!messageOrContent) return '';
|
|
40
|
+
const content = messageOrContent.content !== undefined ? messageOrContent.content : messageOrContent;
|
|
41
|
+
return walleTranscript.extractText(content);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function pushWalleSessionFiles(results) {
|
|
45
|
+
const sessionsDir = walleTranscript.defaultSessionsDir(process.env);
|
|
46
|
+
let files;
|
|
47
|
+
try { files = fs.readdirSync(sessionsDir); } catch { return; }
|
|
48
|
+
const fileSet = new Set(files);
|
|
49
|
+
for (const file of files) {
|
|
50
|
+
if (!file.endsWith('.jsonl') && !file.endsWith('.jsonl.bak')) continue;
|
|
51
|
+
if (file.endsWith('.jsonl.bak') && fileSet.has(file.replace(/\.bak$/, ''))) continue;
|
|
52
|
+
const filePath = path.join(sessionsDir, file);
|
|
53
|
+
let stat;
|
|
54
|
+
try { stat = fs.statSync(filePath); } catch { continue; }
|
|
55
|
+
if (!stat.isFile()) continue;
|
|
56
|
+
results.push({
|
|
57
|
+
filePath,
|
|
58
|
+
projectPath: sessionsDir,
|
|
59
|
+
projectEntry: walleTranscript.WALLE_PROJECT_ENTRY,
|
|
60
|
+
sessionId: file.replace(/\.jsonl(\.bak)?$/, ''),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function pushWalleSessionFilesAsync(results) {
|
|
66
|
+
const sessionsDir = walleTranscript.defaultSessionsDir(process.env);
|
|
67
|
+
let files;
|
|
68
|
+
try { files = await fsp.readdir(sessionsDir); } catch { return; }
|
|
69
|
+
const fileSet = new Set(files);
|
|
70
|
+
for (const file of files) {
|
|
71
|
+
if (!file.endsWith('.jsonl') && !file.endsWith('.jsonl.bak')) continue;
|
|
72
|
+
if (file.endsWith('.jsonl.bak') && fileSet.has(file.replace(/\.bak$/, ''))) continue;
|
|
73
|
+
const filePath = path.join(sessionsDir, file);
|
|
74
|
+
let stat;
|
|
75
|
+
try { stat = await fsp.stat(filePath); } catch { continue; }
|
|
76
|
+
if (!stat.isFile()) continue;
|
|
77
|
+
results.push({
|
|
78
|
+
filePath,
|
|
79
|
+
projectPath: sessionsDir,
|
|
80
|
+
projectEntry: walleTranscript.WALLE_PROJECT_ENTRY,
|
|
81
|
+
sessionId: file.replace(/\.jsonl(\.bak)?$/, ''),
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
26
86
|
function parseSessionFile(filePath, projectPath, projectEntry) {
|
|
87
|
+
if (projectEntry === claudeDesktopSessions.DESKTOP_PROJECT_ENTRY ||
|
|
88
|
+
claudeDesktopSessions.isVirtualSessionPath(filePath)) {
|
|
89
|
+
return claudeDesktopSessions.parseSessionFile(filePath, projectPath, projectEntry);
|
|
90
|
+
}
|
|
91
|
+
|
|
27
92
|
const fileStat = fs.statSync(filePath);
|
|
28
93
|
const modifiedAt = fileStat.mtime.toISOString();
|
|
29
94
|
const sessionId = path.basename(filePath).replace(/\.jsonl(\.bak)?$/, '');
|
|
@@ -50,6 +115,7 @@ function parseSessionFile(filePath, projectPath, projectEntry) {
|
|
|
50
115
|
let allUserMessages = [];
|
|
51
116
|
let modelId = '';
|
|
52
117
|
let modelProvider = '';
|
|
118
|
+
let agent = 'claude';
|
|
53
119
|
|
|
54
120
|
// Claude Code users can issue `/rename Foo bar` as a user message to
|
|
55
121
|
// re-label the session. Upstream claude-code-history-viewer uses this
|
|
@@ -59,26 +125,28 @@ function parseSessionFile(filePath, projectPath, projectEntry) {
|
|
|
59
125
|
for (const line of lines) {
|
|
60
126
|
try {
|
|
61
127
|
const entry = JSON.parse(line);
|
|
128
|
+
const isWalle = entry.provider === 'walle' || entry.type === 'walle_part';
|
|
129
|
+
if (isWalle) agent = 'walle';
|
|
130
|
+
if (entry.type === 'session_meta' && isWalle) {
|
|
131
|
+
sessionCwd = entry.cwd || sessionCwd;
|
|
132
|
+
timestamp = entry.timestamp || timestamp;
|
|
133
|
+
version = entry.version || version;
|
|
134
|
+
gitBranch = entry.gitBranch || gitBranch;
|
|
135
|
+
if (!modelId && entry.modelId) modelId = entry.modelId;
|
|
136
|
+
if (!modelProvider && entry.modelProvider) modelProvider = entry.modelProvider;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
62
139
|
// Capture slug from any row that has it (Claude Code writes it on most rows).
|
|
63
140
|
// slug is the conversation-level identifier that persists across --resume boundaries.
|
|
64
141
|
if (!slug && typeof entry.slug === 'string' && entry.slug) slug = entry.slug;
|
|
65
142
|
// Extract model from assistant entries (JSONL has "model" inside entry.message)
|
|
66
|
-
const entryModel = entry.message?.model || entry.model;
|
|
143
|
+
const entryModel = entry.message?.model || entry.model || entry.modelId;
|
|
67
144
|
if (!modelId && entryModel && !entryModel.startsWith('<')) {
|
|
68
145
|
modelId = entryModel;
|
|
69
|
-
|
|
70
|
-
else if (modelId.startsWith('codex-')) modelProvider = 'openai';
|
|
71
|
-
else if (modelId.startsWith('gemini-')) modelProvider = 'google';
|
|
72
|
-
else if (modelId.startsWith('gpt-') || modelId.startsWith('o1-') || modelId.startsWith('o3-') || modelId.startsWith('o4-')) modelProvider = 'openai';
|
|
73
|
-
else modelProvider = 'unknown';
|
|
146
|
+
modelProvider = entry.modelProvider || inferModelProvider(modelId);
|
|
74
147
|
}
|
|
75
148
|
if (entry.type === 'user' && entry.message?.role === 'user') {
|
|
76
|
-
const
|
|
77
|
-
const text = typeof content === 'string'
|
|
78
|
-
? content
|
|
79
|
-
: Array.isArray(content)
|
|
80
|
-
? (content.find(c => c.type === 'text')?.text || '')
|
|
81
|
-
: '';
|
|
149
|
+
const text = messageText(entry.message);
|
|
82
150
|
userMsgCount++;
|
|
83
151
|
if (text) {
|
|
84
152
|
allUserMessages.push(text.slice(0, 200));
|
|
@@ -98,18 +166,27 @@ function parseSessionFile(filePath, projectPath, projectEntry) {
|
|
|
98
166
|
version = entry.version || version;
|
|
99
167
|
gitBranch = entry.gitBranch || gitBranch;
|
|
100
168
|
}
|
|
169
|
+
} else if (entry.type === 'user' && entry.provider === 'walle' && typeof entry.content === 'string') {
|
|
170
|
+
const text = entry.content;
|
|
171
|
+
userMsgCount++;
|
|
172
|
+
if (text) {
|
|
173
|
+
allUserMessages.push(text.slice(0, 200));
|
|
174
|
+
lastUserContent = text.slice(0, 200);
|
|
175
|
+
}
|
|
176
|
+
if (!firstUserMessage && text) {
|
|
177
|
+
firstUserMessage = text.slice(0, 200);
|
|
178
|
+
sessionCwd = entry.cwd || sessionCwd;
|
|
179
|
+
timestamp = entry.timestamp || timestamp;
|
|
180
|
+
version = entry.version || version;
|
|
181
|
+
gitBranch = entry.gitBranch || gitBranch;
|
|
182
|
+
}
|
|
101
183
|
} else if (entry.type === 'assistant' && entry.message?.role === 'assistant') {
|
|
102
184
|
if (!firstAssistantText) {
|
|
103
|
-
const
|
|
104
|
-
if (
|
|
105
|
-
for (const block of content) {
|
|
106
|
-
if (block.type === 'text' && block.text) {
|
|
107
|
-
firstAssistantText = block.text.slice(0, 200);
|
|
108
|
-
break;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
}
|
|
185
|
+
const text = messageText(entry.message);
|
|
186
|
+
if (text) firstAssistantText = text.slice(0, 200);
|
|
112
187
|
}
|
|
188
|
+
} else if (entry.type === 'assistant' && entry.provider === 'walle' && typeof entry.content === 'string') {
|
|
189
|
+
if (!firstAssistantText && entry.content) firstAssistantText = entry.content.slice(0, 200);
|
|
113
190
|
}
|
|
114
191
|
} catch {
|
|
115
192
|
// JSON.parse failed — line may be truncated (e.g. 1MB+ image block cut off
|
|
@@ -169,6 +246,9 @@ function parseSessionFile(filePath, projectPath, projectEntry) {
|
|
|
169
246
|
hostname: os.hostname(),
|
|
170
247
|
modelId,
|
|
171
248
|
modelProvider,
|
|
249
|
+
model: modelId,
|
|
250
|
+
agent,
|
|
251
|
+
jsonlPath: filePath,
|
|
172
252
|
};
|
|
173
253
|
}
|
|
174
254
|
|
|
@@ -176,26 +256,28 @@ function getAllSessionFiles() {
|
|
|
176
256
|
const claudeProjectsDir = path.join(process.env.HOME, '.claude', 'projects');
|
|
177
257
|
const results = [];
|
|
178
258
|
|
|
179
|
-
if (
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
if (!stat.isDirectory()) continue;
|
|
259
|
+
if (fs.existsSync(claudeProjectsDir)) {
|
|
260
|
+
for (const projectEntry of fs.readdirSync(claudeProjectsDir)) {
|
|
261
|
+
const projectDir = path.join(claudeProjectsDir, projectEntry);
|
|
262
|
+
let stat;
|
|
263
|
+
try { stat = fs.statSync(projectDir); } catch { continue; }
|
|
264
|
+
if (!stat.isDirectory()) continue;
|
|
186
265
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
266
|
+
const projectPath = decodeProjectEntry(projectEntry);
|
|
267
|
+
let files;
|
|
268
|
+
try { files = fs.readdirSync(projectDir); } catch { continue; }
|
|
190
269
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
270
|
+
const fileSet = new Set(files);
|
|
271
|
+
for (const file of files) {
|
|
272
|
+
if (!file.endsWith('.jsonl') && !file.endsWith('.jsonl.bak')) continue;
|
|
273
|
+
if (file.endsWith('.jsonl.bak') && fileSet.has(file.replace(/\.bak$/, ''))) continue;
|
|
274
|
+
const filePath = path.join(projectDir, file);
|
|
275
|
+
results.push({ filePath, projectPath, projectEntry, sessionId: file.replace(/\.jsonl(\.bak)?$/, '') });
|
|
276
|
+
}
|
|
197
277
|
}
|
|
198
278
|
}
|
|
279
|
+
pushWalleSessionFiles(results);
|
|
280
|
+
for (const entry of claudeDesktopSessions.listSessionFileEntries()) results.push(entry);
|
|
199
281
|
return results;
|
|
200
282
|
}
|
|
201
283
|
|
|
@@ -206,10 +288,8 @@ async function getAllSessionFilesAsync() {
|
|
|
206
288
|
const claudeProjectsDir = path.join(process.env.HOME, '.claude', 'projects');
|
|
207
289
|
const results = [];
|
|
208
290
|
|
|
209
|
-
try { await fsp.access(claudeProjectsDir); } catch { return results; }
|
|
210
|
-
|
|
211
291
|
let projectEntries;
|
|
212
|
-
try { projectEntries = await fsp.readdir(claudeProjectsDir); } catch {
|
|
292
|
+
try { projectEntries = await fsp.readdir(claudeProjectsDir); } catch { projectEntries = []; }
|
|
213
293
|
|
|
214
294
|
for (const projectEntry of projectEntries) {
|
|
215
295
|
const projectDir = path.join(claudeProjectsDir, projectEntry);
|
|
@@ -226,9 +306,11 @@ async function getAllSessionFilesAsync() {
|
|
|
226
306
|
if (!file.endsWith('.jsonl') && !file.endsWith('.jsonl.bak')) continue;
|
|
227
307
|
if (file.endsWith('.jsonl.bak') && fileSet.has(file.replace(/\.bak$/, ''))) continue;
|
|
228
308
|
const filePath = path.join(projectDir, file);
|
|
229
|
-
results.push({ filePath, projectPath, projectEntry });
|
|
309
|
+
results.push({ filePath, projectPath, projectEntry, sessionId: file.replace(/\.jsonl(\.bak)?$/, '') });
|
|
230
310
|
}
|
|
231
311
|
}
|
|
312
|
+
await pushWalleSessionFilesAsync(results);
|
|
313
|
+
for (const entry of claudeDesktopSessions.listSessionFileEntries()) results.push(entry);
|
|
232
314
|
return results;
|
|
233
315
|
}
|
|
234
316
|
|
|
@@ -11,9 +11,12 @@
|
|
|
11
11
|
const { baseDetector, stripAnsi } = require('./base');
|
|
12
12
|
|
|
13
13
|
const CODEX_STATUS_FRAGMENT_RE = /^[\s\d•◦·∙●○WwOoRrKkIiNnGg]+$/u;
|
|
14
|
+
const CODEX_BUSY_STATUS_LINE_RE = /^(?:working(?:\s*\([^)]*\))?(?:\s*[•◦·∙●○]\s*esc\s+to\s+interrupt)?|(?:working\s*)?esc\s+to\s+interrupt)$/iu;
|
|
14
15
|
const CODEX_BUSY_WORD = 'working';
|
|
16
|
+
const CODEX_BUSY_HINT_RE = /esc\s+to\s+interrupt/i;
|
|
15
17
|
|
|
16
18
|
function hasCodexBusyStatusFragment(text) {
|
|
19
|
+
if (CODEX_BUSY_HINT_RE.test(String(text || ''))) return true;
|
|
17
20
|
const letters = String(text || '').toLowerCase().replace(/[^a-z]/g, '');
|
|
18
21
|
if (letters.length < 3) return false;
|
|
19
22
|
if (letters.includes(CODEX_BUSY_WORD)) return true;
|
|
@@ -35,8 +38,8 @@ function isCursorAddressedRedraw(data) {
|
|
|
35
38
|
function isCodexStatusRedraw(data) {
|
|
36
39
|
const stripped = stripAnsi(data).trim();
|
|
37
40
|
return (
|
|
38
|
-
stripped.length <=
|
|
39
|
-
CODEX_STATUS_FRAGMENT_RE.test(stripped) &&
|
|
41
|
+
stripped.length <= 160 &&
|
|
42
|
+
(CODEX_STATUS_FRAGMENT_RE.test(stripped) || CODEX_BUSY_STATUS_LINE_RE.test(stripped)) &&
|
|
40
43
|
isCursorAddressedRedraw(data)
|
|
41
44
|
);
|
|
42
45
|
}
|