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.
Files changed (167) hide show
  1. package/README.md +3 -3
  2. package/package.json +2 -2
  3. package/template/bin/dev.sh +7 -1
  4. package/template/bin/setup.js +53 -9
  5. package/template/bin/sync-images.js +53 -0
  6. package/template/builder-journal.md +17 -0
  7. package/template/claude-task-manager/api-prompts.js +98 -13
  8. package/template/claude-task-manager/api-reviews.js +82 -5
  9. package/template/claude-task-manager/db.js +32 -5
  10. package/template/claude-task-manager/docs/session-capture-foundation-design.md +1273 -0
  11. package/template/claude-task-manager/lib/claude-desktop-sessions.js +696 -0
  12. package/template/claude-task-manager/lib/coding-agent-models.js +49 -1
  13. package/template/claude-task-manager/lib/session-capture.js +421 -0
  14. package/template/claude-task-manager/lib/session-history.js +135 -15
  15. package/template/claude-task-manager/lib/session-jobs.js +10 -5
  16. package/template/claude-task-manager/lib/session-stream.js +87 -19
  17. package/template/claude-task-manager/lib/setup-provider-config.js +115 -0
  18. package/template/claude-task-manager/lib/walle-ctm-history.js +72 -0
  19. package/template/claude-task-manager/lib/walle-session-context.js +61 -0
  20. package/template/claude-task-manager/lib/walle-transcript.js +176 -0
  21. package/template/claude-task-manager/public/css/setup.css +35 -8
  22. package/template/claude-task-manager/public/css/walle-session.css +56 -0
  23. package/template/claude-task-manager/public/css/walle.css +120 -0
  24. package/template/claude-task-manager/public/index.html +814 -181
  25. package/template/claude-task-manager/public/js/message-renderer.js +148 -19
  26. package/template/claude-task-manager/public/js/reviews.js +120 -62
  27. package/template/claude-task-manager/public/js/setup.js +75 -31
  28. package/template/claude-task-manager/public/js/stream-view.js +115 -55
  29. package/template/claude-task-manager/public/js/walle-session.js +84 -2
  30. package/template/claude-task-manager/public/js/walle.js +308 -54
  31. package/template/claude-task-manager/server.js +1092 -146
  32. package/template/claude-task-manager/session-integrity.js +181 -54
  33. package/template/claude-task-manager/session-utils.js +123 -41
  34. package/template/claude-task-manager/workers/state-detectors/codex.js +5 -2
  35. package/template/package.json +1 -1
  36. package/template/wall-e/adapters/ctm.js +39 -18
  37. package/template/wall-e/agent-runners/contract.js +17 -0
  38. package/template/wall-e/agent-runners/index.js +22 -0
  39. package/template/wall-e/agent-runtime/harness.js +212 -0
  40. package/template/wall-e/agent-runtime/index.js +8 -0
  41. package/template/wall-e/agent-runtime/registry.js +67 -0
  42. package/template/wall-e/agent-runtime/session-store.js +179 -0
  43. package/template/wall-e/agent-runtime/spawn.js +208 -0
  44. package/template/wall-e/api-walle.js +174 -7
  45. package/template/wall-e/brain.js +266 -28
  46. package/template/wall-e/channels/policy.js +88 -0
  47. package/template/wall-e/channels/registry.js +15 -1
  48. package/template/wall-e/channels/reply-dispatcher.js +70 -0
  49. package/template/wall-e/channels/session-bindings.js +51 -0
  50. package/template/wall-e/chat/code-review-context.js +29 -0
  51. package/template/wall-e/chat.js +188 -42
  52. package/template/wall-e/coding/acp-adapter.js +188 -0
  53. package/template/wall-e/coding/agent-catalog.js +129 -0
  54. package/template/wall-e/coding/compaction-service.js +247 -0
  55. package/template/wall-e/coding/execution-trace.js +3 -0
  56. package/template/wall-e/coding/instruction-service.js +224 -0
  57. package/template/wall-e/coding/model-message.js +67 -0
  58. package/template/wall-e/coding/permission-rules-store.js +111 -0
  59. package/template/wall-e/coding/permission-service.js +266 -0
  60. package/template/wall-e/coding/prompt-bundle.js +67 -0
  61. package/template/wall-e/coding/prompt-runtime.js +243 -0
  62. package/template/wall-e/coding/provider-transform.js +188 -0
  63. package/template/wall-e/coding/runtime-mode.js +132 -0
  64. package/template/wall-e/coding/snapshot-service.js +155 -0
  65. package/template/wall-e/coding/stream-processor.js +268 -0
  66. package/template/wall-e/coding/task-tool.js +255 -0
  67. package/template/wall-e/coding/tool-registry.js +361 -0
  68. package/template/wall-e/coding/transcript-writer.js +143 -0
  69. package/template/wall-e/coding/workspace-replay.js +324 -0
  70. package/template/wall-e/coding-context.js +4 -22
  71. package/template/wall-e/coding-orchestrator.js +307 -18
  72. package/template/wall-e/coding-prompts.js +44 -3
  73. package/template/wall-e/context/context-builder.js +43 -1
  74. package/template/wall-e/context/topic-matcher.js +1 -1
  75. package/template/wall-e/eval/agent-runner.js +59 -13
  76. package/template/wall-e/eval/benchmarks/memory-retrieval.json +155 -57
  77. package/template/wall-e/eval/benchmarks.js +100 -16
  78. package/template/wall-e/eval/eval-orchestrator.js +218 -8
  79. package/template/wall-e/eval/harvester.js +62 -5
  80. package/template/wall-e/eval/head-to-head.js +23 -2
  81. package/template/wall-e/eval/humaneval-adapter.js +30 -5
  82. package/template/wall-e/eval/livecodebench-adapter.js +29 -5
  83. package/template/wall-e/eval/manifest.js +186 -0
  84. package/template/wall-e/eval/run-agent-benchmarks.js +66 -2
  85. package/template/wall-e/eval/session-retrieval-benchmark.js +150 -0
  86. package/template/wall-e/eval/session-transcripts.js +57 -4
  87. package/template/wall-e/eval/swebench-adapter.js +109 -3
  88. package/template/wall-e/evaluation/agent-router.js +53 -1
  89. package/template/wall-e/evaluation/coding-quorum.js +48 -1
  90. package/template/wall-e/evaluation/router.js +4 -2
  91. package/template/wall-e/evaluation/tier-selector.js +11 -1
  92. package/template/wall-e/extraction/contradiction.js +2 -2
  93. package/template/wall-e/extraction/indexer.js +2 -1
  94. package/template/wall-e/extraction/knowledge-extractor.js +2 -2
  95. package/template/wall-e/hooks/cli.js +92 -0
  96. package/template/wall-e/hooks/discovery.js +119 -0
  97. package/template/wall-e/hooks/index.js +7 -0
  98. package/template/wall-e/hooks/manifest.js +55 -0
  99. package/template/wall-e/hooks/runtime.js +84 -0
  100. package/template/wall-e/hooks/session-memory.js +225 -0
  101. package/template/wall-e/http/auth.js +6 -2
  102. package/template/wall-e/http/chat-api.js +54 -8
  103. package/template/wall-e/integrations/claude-plugin/hooks/hooks.json +27 -0
  104. package/template/wall-e/integrations/claude-plugin/hooks/walle-precompact-hook.sh +5 -0
  105. package/template/wall-e/integrations/claude-plugin/hooks/walle-stop-hook.sh +5 -0
  106. package/template/wall-e/integrations/codex-plugin/hooks/walle-hook.sh +7 -0
  107. package/template/wall-e/integrations/codex-plugin/hooks.json +37 -0
  108. package/template/wall-e/listening/calendar.js +3 -1
  109. package/template/wall-e/llm/client.js +64 -10
  110. package/template/wall-e/llm/google.js +39 -5
  111. package/template/wall-e/llm/ollama.js +1 -1
  112. package/template/wall-e/llm/ollama.plugin.json +1 -1
  113. package/template/wall-e/llm/provider-availability.js +10 -0
  114. package/template/wall-e/llm/provider-error.js +269 -0
  115. package/template/wall-e/llm/tool-adapter.js +48 -12
  116. package/template/wall-e/loops/boot.js +2 -1
  117. package/template/wall-e/loops/initiative.js +2 -2
  118. package/template/wall-e/loops/tasks.js +8 -47
  119. package/template/wall-e/loops/workspace-prompts.js +20 -0
  120. package/template/wall-e/mcp-server.js +442 -1
  121. package/template/wall-e/memory/session-ingest-service.js +159 -0
  122. package/template/wall-e/memory/source-indexer.js +289 -0
  123. package/template/wall-e/plugins/discovery.js +83 -0
  124. package/template/wall-e/plugins/manifest-loader.js +50 -10
  125. package/template/wall-e/plugins/manifest-schema.js +69 -0
  126. package/template/wall-e/plugins/model-catalog.js +55 -0
  127. package/template/wall-e/prompts/coding/base.txt +2 -0
  128. package/template/wall-e/prompts/coding/deepseek.txt +1 -0
  129. package/template/wall-e/prompts/coding/memory-protocol.md +9 -0
  130. package/template/wall-e/prompts/coding/plan.txt +1 -0
  131. package/template/wall-e/runtime/execution-trace.js +220 -0
  132. package/template/wall-e/security/audit.js +266 -0
  133. package/template/wall-e/security/ssrf.js +236 -0
  134. package/template/wall-e/session-files.js +303 -0
  135. package/template/wall-e/skills/_bundled/slack-backfill/SKILL.md +3 -0
  136. package/template/wall-e/skills/_bundled/slack-sync/SKILL.md +3 -0
  137. package/template/wall-e/skills/internal-skill-registry.js +2 -2
  138. package/template/wall-e/skills/script-skill-runner.js +143 -0
  139. package/template/wall-e/skills/skill-executor.js +5 -6
  140. package/template/wall-e/skills/skill-fallback.js +3 -1
  141. package/template/wall-e/skills/skill-harness-registry.js +7 -8
  142. package/template/wall-e/skills/skill-planner.js +52 -4
  143. package/template/wall-e/skills/slack-ingest.js +11 -3
  144. package/template/wall-e/sources/base.js +90 -0
  145. package/template/wall-e/sources/builtin.js +33 -0
  146. package/template/wall-e/sources/claude-code-jsonl.js +78 -0
  147. package/template/wall-e/sources/codex-jsonl.js +125 -0
  148. package/template/wall-e/sources/coding-session-utils.js +117 -0
  149. package/template/wall-e/sources/contract-suite.js +59 -0
  150. package/template/wall-e/sources/gemini-jsonl.js +85 -0
  151. package/template/wall-e/sources/index.js +9 -0
  152. package/template/wall-e/sources/jsonl-utils.js +181 -0
  153. package/template/wall-e/sources/record-types.js +252 -0
  154. package/template/wall-e/sources/registry.js +92 -0
  155. package/template/wall-e/sources/transforms.js +100 -0
  156. package/template/wall-e/sources/walle-jsonl.js +108 -0
  157. package/template/wall-e/tools/coding-middleware.js +31 -1
  158. package/template/wall-e/tools/file-tracker.js +25 -1
  159. package/template/wall-e/tools/local-tools.js +75 -47
  160. package/template/wall-e/tools/session-sharing.js +68 -1
  161. package/template/wall-e/tools/shell-analyzer.js +1 -1
  162. package/template/wall-e/tools/shell-policy.js +47 -0
  163. package/template/wall-e/tools/snapshot.js +42 -0
  164. package/template/wall-e/training/harvester.js +62 -5
  165. package/template/wall-e/utils/repair.js +253 -1
  166. package/template/website/index.html +3 -3
  167. 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 file birthtime
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 uuid = path.basename(filePath).replace(/\.jsonl(\.bak)?$/, '');
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 file birthtime)
123
- if (row.created_at && file.stat.birthtime) {
124
- const dbMs = new Date(row.created_at.replace(' ', 'T') + 'Z').getTime();
125
- const fileMs = file.stat.birthtime.getTime();
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
- if (drift > 300000) { // >5 minutes off
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
- file_birthtime: file.stat.birthtime.toISOString(),
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: 'Timestamps are significantly off may indicate wrong file mapping.',
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 fd = fs.openSync(file.filePath, 'r');
213
- const buf = Buffer.alloc(Math.min(file.stat.size, 32768));
214
- const bytesRead = fs.readSync(fd, buf, 0, buf.length, 0);
215
- fs.closeSync(fd);
216
- const chunk = buf.toString('utf8', 0, bytesRead);
217
- const lines = chunk.split('\n');
218
- let fileFirstMsg = '';
219
- for (const line of lines) {
220
- if (!line.trim()) continue;
221
- try {
222
- const entry = JSON.parse(line);
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: fileFirstMsg.slice(0, 100),
247
- word_similarity: Math.round(similarity * 100) + '%',
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 uuid = path.basename(filePath).replace(/\.jsonl(\.bak)?$/, '');
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 file birthtime
429
- const fileId = issue.details.file_id;
430
- const file = fileIndex[fileId];
431
- if (!file) { result.skipped++; break; }
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 && issues.some(i => i.severity !== 'info')) {
663
- recovery = recoverMismatches(db, issues, getAllSessionFiles);
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
- if (modelId.startsWith('claude-')) modelProvider = 'anthropic';
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 content = entry.message.content;
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 content = entry.message.content;
104
- if (Array.isArray(content)) {
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 (!fs.existsSync(claudeProjectsDir)) return results;
180
-
181
- for (const projectEntry of fs.readdirSync(claudeProjectsDir)) {
182
- const projectDir = path.join(claudeProjectsDir, projectEntry);
183
- let stat;
184
- try { stat = fs.statSync(projectDir); } catch { continue; }
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
- const projectPath = decodeProjectEntry(projectEntry);
188
- let files;
189
- try { files = fs.readdirSync(projectDir); } catch { continue; }
266
+ const projectPath = decodeProjectEntry(projectEntry);
267
+ let files;
268
+ try { files = fs.readdirSync(projectDir); } catch { continue; }
190
269
 
191
- const fileSet = new Set(files);
192
- for (const file of files) {
193
- if (!file.endsWith('.jsonl') && !file.endsWith('.jsonl.bak')) continue;
194
- if (file.endsWith('.jsonl.bak') && fileSet.has(file.replace(/\.bak$/, ''))) continue;
195
- const filePath = path.join(projectDir, file);
196
- results.push({ filePath, projectPath, projectEntry });
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 { return results; }
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 <= 80 &&
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
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "walle",
3
- "version": "0.9.11",
3
+ "version": "0.9.13",
4
4
  "private": true,
5
5
  "description": "Wall-E — your personal digital twin",
6
6
  "scripts": {