create-walle 0.9.13 → 0.9.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +6 -1
  2. package/bin/create-walle.js +195 -30
  3. package/bin/mcp-inject.js +18 -53
  4. package/package.json +3 -1
  5. package/template/claude-task-manager/approval-agent.js +7 -0
  6. package/template/claude-task-manager/docs/session-standup-command-center-design.md +242 -0
  7. package/template/claude-task-manager/git-utils.js +111 -3
  8. package/template/claude-task-manager/lib/session-history.js +144 -16
  9. package/template/claude-task-manager/lib/session-standup.js +409 -0
  10. package/template/claude-task-manager/lib/standup-attention.js +200 -0
  11. package/template/claude-task-manager/lib/status-hooks.js +8 -2
  12. package/template/claude-task-manager/lib/update-telemetry.js +114 -0
  13. package/template/claude-task-manager/lib/walle-default-model.js +55 -0
  14. package/template/claude-task-manager/lib/walle-mcp-auto-config.js +62 -0
  15. package/template/claude-task-manager/lib/walle-supervisor.js +83 -19
  16. package/template/claude-task-manager/lib/worktree-cwd.js +82 -0
  17. package/template/claude-task-manager/providers/codex-mcp.js +104 -0
  18. package/template/claude-task-manager/providers/index.js +2 -0
  19. package/template/claude-task-manager/public/css/setup.css +2 -1
  20. package/template/claude-task-manager/public/css/walle.css +5 -0
  21. package/template/claude-task-manager/public/index.html +1596 -283
  22. package/template/claude-task-manager/public/js/session-search-utils.js +171 -1
  23. package/template/claude-task-manager/public/js/setup.js +62 -19
  24. package/template/claude-task-manager/public/js/stream-view.js +55 -6
  25. package/template/claude-task-manager/public/js/walle-session.js +73 -16
  26. package/template/claude-task-manager/public/js/walle.js +34 -2
  27. package/template/claude-task-manager/server.js +780 -177
  28. package/template/claude-task-manager/session-integrity.js +58 -15
  29. package/template/claude-task-manager/workers/approval-widget-validator.js +15 -5
  30. package/template/claude-task-manager/workers/state-detectors/codex.js +6 -0
  31. package/template/package.json +1 -1
  32. package/template/wall-e/agent.js +36 -7
  33. package/template/wall-e/api-walle.js +72 -20
  34. package/template/wall-e/coding/stream-processor.js +22 -2
  35. package/template/wall-e/coding-orchestrator.js +26 -6
  36. package/template/wall-e/eval/agent-runner.js +16 -4
  37. package/template/wall-e/eval/benchmark-generator.js +21 -1
  38. package/template/wall-e/eval/benchmarks/coding-agent.json +0 -596
  39. package/template/wall-e/eval/codex-cli-baseline.js +633 -0
  40. package/template/wall-e/eval/eval-orchestrator.js +3 -3
  41. package/template/wall-e/eval/run-agent-benchmarks.js +11 -3
  42. package/template/wall-e/eval/run-codex-cli-baseline.js +177 -0
  43. package/template/wall-e/lib/mcp-integration.js +220 -0
  44. package/template/wall-e/llm/ollama.js +47 -8
  45. package/template/wall-e/llm/ollama.plugin.json +1 -1
  46. package/template/wall-e/llm/tool-adapter.js +1 -0
  47. package/template/wall-e/loops/ingest.js +42 -8
  48. package/template/wall-e/mcp-server.js +272 -10
  49. package/template/wall-e/memory/ctm-session-context.js +910 -0
  50. package/template/wall-e/server.js +26 -1
  51. package/template/wall-e/skills/_bundled/scan-ctm-sessions/SKILL.md +20 -0
  52. package/template/wall-e/skills/_bundled/scan-ctm-sessions/run.js +43 -0
  53. package/template/wall-e/skills/skill-planner.js +52 -3
  54. package/template/wall-e/tools/builtin-middleware.js +55 -2
  55. package/template/wall-e/tools/shell-policy.js +1 -1
  56. package/template/wall-e/tools/slack-owner.js +104 -0
  57. package/template/website/index.html +2 -2
  58. package/template/builder-journal.md +0 -17
@@ -0,0 +1,910 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const os = require('node:os');
5
+ const path = require('node:path');
6
+ const crypto = require('node:crypto');
7
+ const Database = require('better-sqlite3');
8
+
9
+ const DEFAULT_SEARCH_LIMIT = 10;
10
+ const MAX_SEARCH_LIMIT = 50;
11
+ const DEFAULT_CONTEXT_LIMIT = 200;
12
+ const MAX_CONTEXT_LIMIT = 5000;
13
+ const DEFAULT_TOKEN_BUDGET = 12000;
14
+
15
+ function resolveCtmDbPath(env = process.env) {
16
+ if (env.CTM_DB_PATH) return env.CTM_DB_PATH;
17
+ if (env.CTM_DATA_DIR) return path.join(env.CTM_DATA_DIR, 'task-manager.db');
18
+ if (env.WALLE_DEV_DIR) return path.join(env.WALLE_DEV_DIR, 'task-manager.db');
19
+ return path.join(env.HOME || os.homedir(), '.walle', 'data', 'task-manager.db');
20
+ }
21
+
22
+ function openCtmDb({ dbPath, readonly = true } = {}) {
23
+ const resolved = path.resolve(dbPath || resolveCtmDbPath());
24
+ if (!fs.existsSync(resolved)) {
25
+ return { db: null, dbPath: resolved, close: () => {}, unavailable: true };
26
+ }
27
+ const db = new Database(resolved, { readonly, fileMustExist: true });
28
+ db.pragma('busy_timeout = 2500');
29
+ return { db, dbPath: resolved, close: () => db.close(), unavailable: false };
30
+ }
31
+
32
+ function withCtmDb(options, fn) {
33
+ if (options?.db) {
34
+ return fn(options.db, { dbPath: options.dbPath || '', close: () => {}, owned: false });
35
+ }
36
+ const handle = openCtmDb(options);
37
+ if (!handle.db) {
38
+ return {
39
+ ok: false,
40
+ source: 'ctm-db',
41
+ unavailable: true,
42
+ db_path: handle.dbPath,
43
+ reason: 'ctm_db_not_found',
44
+ };
45
+ }
46
+ try {
47
+ return fn(handle.db, { dbPath: handle.dbPath, close: handle.close, owned: true });
48
+ } finally {
49
+ handle.close();
50
+ }
51
+ }
52
+
53
+ function searchCtmSessions({ query, limit = DEFAULT_SEARCH_LIMIT, db, dbPath } = {}) {
54
+ const cleanQuery = String(query || '').trim();
55
+ if (!cleanQuery) return { ok: false, source: 'ctm-db', results: [], reason: 'query_required' };
56
+ const requestedLimit = clampLimit(limit, DEFAULT_SEARCH_LIMIT, MAX_SEARCH_LIMIT);
57
+
58
+ return withCtmDb({ db, dbPath }, (conn, handle) => {
59
+ if (!hasAnySessionTable(conn)) {
60
+ return { ok: false, source: 'ctm-db', db_path: handle.dbPath, results: [], reason: 'ctm_session_tables_missing' };
61
+ }
62
+
63
+ const rawResults = [];
64
+ rawResults.push(...searchSessionMessagesFts(conn, cleanQuery, requestedLimit * 4));
65
+ rawResults.push(...searchSessionMessagesLike(conn, cleanQuery, requestedLimit * 4));
66
+ rawResults.push(...searchSessionMetadata(conn, cleanQuery, requestedLimit * 2));
67
+
68
+ const results = dedupeItems(rawResults, {
69
+ limit: requestedLimit,
70
+ contentFields: ['snippet', 'content'],
71
+ keyPrefix: (item) => `${item.session_id || ''}:${item.role || ''}`,
72
+ });
73
+
74
+ return {
75
+ ok: true,
76
+ source: 'ctm-db',
77
+ db_path: handle.dbPath,
78
+ query: cleanQuery,
79
+ count: results.length,
80
+ results,
81
+ };
82
+ });
83
+ }
84
+
85
+ function getCtmSessionContext({
86
+ session_id,
87
+ source_id,
88
+ session_ids,
89
+ ids,
90
+ limit = DEFAULT_CONTEXT_LIMIT,
91
+ cursor = 0,
92
+ include_raw = false,
93
+ dedupe = true,
94
+ format = 'messages',
95
+ db,
96
+ dbPath,
97
+ } = {}) {
98
+ const identifiers = normalizeIdentifiers(session_ids || ids || session_id || source_id);
99
+ if (identifiers.length === 0) {
100
+ return { ok: false, source: 'ctm-db', reason: 'session_id_required', sessions: [], messages: [] };
101
+ }
102
+
103
+ return withCtmDb({ db, dbPath }, (conn, handle) => {
104
+ if (!hasAnySessionTable(conn)) {
105
+ return { ok: false, source: 'ctm-db', db_path: handle.dbPath, reason: 'ctm_session_tables_missing', sessions: [], messages: [] };
106
+ }
107
+
108
+ const bundles = identifiers.map((id) => resolveSessionBundle(conn, id)).filter(Boolean);
109
+ const seenBundles = new Set();
110
+ const uniqueBundles = bundles.filter((bundle) => {
111
+ const key = bundle.ctm_session_id || bundle.requested_id;
112
+ if (seenBundles.has(key)) return false;
113
+ seenBundles.add(key);
114
+ return true;
115
+ });
116
+
117
+ const sessions = [];
118
+ const allMessages = [];
119
+ for (const bundle of uniqueBundles) {
120
+ const conversations = getConversationRows(conn, bundle);
121
+ const sessionMessages = [];
122
+ for (const row of conversations) {
123
+ const messages = parseConversationMessages(row);
124
+ for (let i = 0; i < messages.length; i += 1) {
125
+ const normalized = normalizeMessage(messages[i], {
126
+ row,
127
+ bundle,
128
+ index: i,
129
+ includeRaw: include_raw,
130
+ });
131
+ if (normalized.content || include_raw) sessionMessages.push(normalized);
132
+ }
133
+ }
134
+
135
+ const dedupedSessionMessages = dedupe ? dedupeMessages(sessionMessages) : sessionMessages;
136
+ sessions.push(formatSessionSummary(bundle, conversations, dedupedSessionMessages));
137
+ allMessages.push(...dedupedSessionMessages);
138
+ }
139
+
140
+ const orderedMessages = orderMessages(dedupe ? dedupeMessages(allMessages) : allMessages);
141
+ const start = Math.max(0, Number.parseInt(cursor, 10) || 0);
142
+ const cap = normalizeContextLimit(limit);
143
+ const pagedMessages = cap === 0 ? orderedMessages.slice(start) : orderedMessages.slice(start, start + cap);
144
+ const nextCursor = cap !== 0 && start + cap < orderedMessages.length ? start + cap : null;
145
+ const result = {
146
+ ok: true,
147
+ source: 'ctm-db',
148
+ db_path: handle.dbPath,
149
+ requested_ids: identifiers,
150
+ sessions,
151
+ count: pagedMessages.length,
152
+ total_count: orderedMessages.length,
153
+ cursor: start,
154
+ next_cursor: nextCursor,
155
+ messages: pagedMessages,
156
+ transfer: {
157
+ full_context_supported: true,
158
+ served_from: 'session_conversations/session_messages',
159
+ jsonl_fallback_used: false,
160
+ deduped: Boolean(dedupe),
161
+ },
162
+ };
163
+
164
+ if (format === 'markdown' || format === 'compact') {
165
+ result.text = renderContextMarkdown(result, { compact: format === 'compact' });
166
+ }
167
+ return result;
168
+ });
169
+ }
170
+
171
+ function buildContextPack({
172
+ task,
173
+ query,
174
+ session_ids,
175
+ ids,
176
+ limit = 5,
177
+ token_budget = DEFAULT_TOKEN_BUDGET,
178
+ include_raw = false,
179
+ mode = 'auto',
180
+ db,
181
+ dbPath,
182
+ } = {}) {
183
+ const identifiers = normalizeIdentifiers(session_ids || ids);
184
+ const effectiveQuery = String(query || task || '').trim();
185
+ const maxSessions = clampLimit(limit, 5, MAX_SEARCH_LIMIT);
186
+ const budgetChars = Math.max(1000, Math.min(Number(token_budget || DEFAULT_TOKEN_BUDGET), 200000) * 4);
187
+
188
+ return withCtmDb({ db, dbPath }, (conn, handle) => {
189
+ const selectedIds = [];
190
+ const search = effectiveQuery
191
+ ? searchCtmSessions({ query: effectiveQuery, limit: maxSessions, db: conn, dbPath: handle.dbPath })
192
+ : { ok: true, results: [] };
193
+
194
+ for (const id of identifiers) selectedIds.push(id);
195
+ for (const item of search.results || []) {
196
+ if (item.session_id) selectedIds.push(item.session_id);
197
+ if (item.agent_session_id) selectedIds.push(item.agent_session_id);
198
+ }
199
+
200
+ const uniqueIds = [...new Set(selectedIds)].slice(0, maxSessions);
201
+ const context = getCtmSessionContext({
202
+ session_ids: uniqueIds,
203
+ limit: mode === 'full' ? 0 : 1000,
204
+ include_raw,
205
+ dedupe: true,
206
+ db: conn,
207
+ dbPath: handle.dbPath,
208
+ });
209
+
210
+ const pack = trimContextToBudget(context, budgetChars);
211
+ return {
212
+ ok: true,
213
+ source: 'ctm-db',
214
+ db_path: handle.dbPath,
215
+ task: task || '',
216
+ query: effectiveQuery,
217
+ mode,
218
+ search_results: search.results || [],
219
+ selected_session_ids: uniqueIds,
220
+ sessions: pack.sessions,
221
+ messages: pack.messages,
222
+ text: renderContextMarkdown(pack, { compact: mode !== 'full' }),
223
+ transfer: {
224
+ full_context_supported: true,
225
+ served_from: 'session_conversations/session_messages',
226
+ jsonl_fallback_used: false,
227
+ deduped: true,
228
+ token_budget: Number(token_budget || DEFAULT_TOKEN_BUDGET),
229
+ truncated: pack.truncated,
230
+ },
231
+ };
232
+ });
233
+ }
234
+
235
+ function hasAnySessionTable(db) {
236
+ return hasTable(db, 'session_conversations') || hasTable(db, 'session_messages') || hasTable(db, 'ctm_sessions') || hasTable(db, 'agent_sessions');
237
+ }
238
+
239
+ function hasTable(db, name) {
240
+ try {
241
+ return Boolean(db.prepare("SELECT name FROM sqlite_master WHERE type IN ('table', 'view') AND name = ?").get(name));
242
+ } catch {
243
+ return false;
244
+ }
245
+ }
246
+
247
+ function tableColumns(db, table) {
248
+ try {
249
+ return db.prepare(`PRAGMA table_info(${quoteIdent(table)})`).all().map((row) => row.name);
250
+ } catch {
251
+ return [];
252
+ }
253
+ }
254
+
255
+ function idColumnFor(db, table) {
256
+ const cols = tableColumns(db, table);
257
+ if (cols.includes('ctm_session_id')) return 'ctm_session_id';
258
+ if (cols.includes('session_id')) return 'session_id';
259
+ return null;
260
+ }
261
+
262
+ function quoteIdent(value) {
263
+ return `"${String(value).replace(/"/g, '""')}"`;
264
+ }
265
+
266
+ function searchSessionMessagesFts(db, query, limit) {
267
+ if (!hasTable(db, 'session_messages') || !hasTable(db, 'session_messages_fts')) return [];
268
+ const msgSessionCol = idColumnFor(db, 'session_messages');
269
+ if (!msgSessionCol) return [];
270
+ const ftsQueries = buildFtsQueries(query);
271
+ if (!ftsQueries.length) return [];
272
+
273
+ try {
274
+ const stmt = db.prepare(`
275
+ SELECT sm.${quoteIdent(msgSessionCol)} AS conversation_id,
276
+ sm.id AS message_rowid,
277
+ sm.role,
278
+ sm.message_index,
279
+ sm.content,
280
+ sm.created_at,
281
+ fts.rank AS rank
282
+ FROM session_messages_fts fts
283
+ JOIN session_messages sm ON sm.id = fts.rowid
284
+ WHERE session_messages_fts MATCH ?
285
+ ORDER BY fts.rank
286
+ LIMIT ?
287
+ `);
288
+ let rows = [];
289
+ for (const ftsQuery of ftsQueries) {
290
+ rows = stmt.all(ftsQuery, limit);
291
+ if (rows.length) break;
292
+ }
293
+ return rows.map((row, index) => enrichSearchRow(db, row, {
294
+ source_table: 'session_messages_fts',
295
+ rank: index,
296
+ score: row.rank,
297
+ }));
298
+ } catch {
299
+ return [];
300
+ }
301
+ }
302
+
303
+ function searchSessionMessagesLike(db, query, limit) {
304
+ if (!hasTable(db, 'session_messages')) return [];
305
+ const msgSessionCol = idColumnFor(db, 'session_messages');
306
+ if (!msgSessionCol) return [];
307
+ const like = `%${escapeLike(query)}%`;
308
+ try {
309
+ const rows = db.prepare(`
310
+ SELECT ${quoteIdent(msgSessionCol)} AS conversation_id,
311
+ id AS message_rowid,
312
+ role,
313
+ message_index,
314
+ content,
315
+ created_at
316
+ FROM session_messages
317
+ WHERE content LIKE ? ESCAPE '\\'
318
+ ORDER BY created_at DESC, id DESC
319
+ LIMIT ?
320
+ `).all(like, limit);
321
+ return rows.map((row, index) => enrichSearchRow(db, row, {
322
+ source_table: 'session_messages',
323
+ rank: index + 1000,
324
+ }));
325
+ } catch {
326
+ return [];
327
+ }
328
+ }
329
+
330
+ function searchSessionMetadata(db, query, limit) {
331
+ const results = [];
332
+ const like = `%${escapeLike(query)}%`;
333
+ if (hasTable(db, 'session_conversations')) {
334
+ const convIdCol = idColumnFor(db, 'session_conversations');
335
+ if (convIdCol) {
336
+ const cols = tableColumns(db, 'session_conversations');
337
+ const selectCols = [
338
+ `${quoteIdent(convIdCol)} AS conversation_id`,
339
+ cols.includes('project_path') ? 'project_path' : "'' AS project_path",
340
+ cols.includes('title') ? 'title' : "'' AS title",
341
+ cols.includes('first_message') ? 'first_message' : "'' AS first_message",
342
+ cols.includes('git_branch') ? 'git_branch' : "'' AS git_branch",
343
+ cols.includes('imported_at') ? 'imported_at' : "'' AS imported_at",
344
+ ];
345
+ const predicates = ['1=0'];
346
+ const params = [];
347
+ for (const col of ['title', 'first_message', 'project_path']) {
348
+ if (cols.includes(col)) {
349
+ predicates.push(`${quoteIdent(col)} LIKE ? ESCAPE '\\'`);
350
+ params.push(like);
351
+ }
352
+ }
353
+ try {
354
+ const rows = db.prepare(`
355
+ SELECT ${selectCols.join(', ')}
356
+ FROM session_conversations
357
+ WHERE ${predicates.join(' OR ')}
358
+ ORDER BY imported_at DESC
359
+ LIMIT ?
360
+ `).all(...params, limit);
361
+ results.push(...rows.map((row, index) => enrichMetadataRow(db, row, index)));
362
+ } catch {}
363
+ }
364
+ }
365
+
366
+ if (hasTable(db, 'ctm_sessions')) {
367
+ try {
368
+ const rows = db.prepare(`
369
+ SELECT id AS ctm_session_id, provider, project_path, cwd, title, updated_at
370
+ FROM ctm_sessions
371
+ WHERE id = ? OR title LIKE ? ESCAPE '\\' OR project_path LIKE ? ESCAPE '\\' OR cwd LIKE ? ESCAPE '\\'
372
+ ORDER BY updated_at DESC
373
+ LIMIT ?
374
+ `).all(query, like, like, like, limit);
375
+ results.push(...rows.map((row, index) => ({
376
+ source: 'ctm-db',
377
+ source_table: 'ctm_sessions',
378
+ session_id: row.ctm_session_id,
379
+ ctm_session_id: row.ctm_session_id,
380
+ provider: row.provider || '',
381
+ cwd: row.cwd || row.project_path || '',
382
+ title: row.title || '',
383
+ timestamp: row.updated_at || '',
384
+ snippet: row.title || row.cwd || row.project_path || row.ctm_session_id,
385
+ rank: index + 2000,
386
+ })));
387
+ } catch {}
388
+ }
389
+
390
+ if (hasTable(db, 'agent_sessions')) {
391
+ try {
392
+ const rows = db.prepare(`
393
+ SELECT agent_session_id, ctm_session_id, provider, project_path, first_message, modified_at, model, git_branch
394
+ FROM agent_sessions
395
+ WHERE agent_session_id = ? OR ctm_session_id = ? OR first_message LIKE ? ESCAPE '\\' OR project_path LIKE ? ESCAPE '\\'
396
+ ORDER BY modified_at DESC, updated_at DESC
397
+ LIMIT ?
398
+ `).all(query, query, like, like, limit);
399
+ results.push(...rows.map((row, index) => ({
400
+ source: 'ctm-db',
401
+ source_table: 'agent_sessions',
402
+ session_id: row.ctm_session_id || row.agent_session_id,
403
+ ctm_session_id: row.ctm_session_id || '',
404
+ agent_session_id: row.agent_session_id || '',
405
+ provider: row.provider || '',
406
+ cwd: row.project_path || '',
407
+ branch: row.git_branch || '',
408
+ model: row.model || '',
409
+ timestamp: row.modified_at || '',
410
+ snippet: row.first_message || row.project_path || row.agent_session_id,
411
+ rank: index + 3000,
412
+ })));
413
+ } catch {}
414
+ }
415
+
416
+ return results;
417
+ }
418
+
419
+ function enrichSearchRow(db, row, extra = {}) {
420
+ const resolved = resolveConversationOwner(db, row.conversation_id);
421
+ return {
422
+ source: 'ctm-db',
423
+ source_table: extra.source_table,
424
+ session_id: resolved.ctm_session_id || row.conversation_id,
425
+ ctm_session_id: resolved.ctm_session_id || '',
426
+ agent_session_id: resolved.agent_session_id || '',
427
+ conversation_id: row.conversation_id,
428
+ role: row.role || '',
429
+ message_index: row.message_index,
430
+ timestamp: row.created_at || resolved.modified_at || resolved.updated_at || '',
431
+ cwd: resolved.cwd || resolved.project_path || '',
432
+ branch: resolved.git_branch || '',
433
+ title: resolved.title || resolved.first_message || '',
434
+ provider: resolved.provider || '',
435
+ model: resolved.model || '',
436
+ snippet: snippetAround(row.content || '', 800),
437
+ content_hash: contentFingerprint(row.content || ''),
438
+ rank: extra.rank || 0,
439
+ };
440
+ }
441
+
442
+ function enrichMetadataRow(db, row, rank) {
443
+ const resolved = resolveConversationOwner(db, row.conversation_id);
444
+ return {
445
+ source: 'ctm-db',
446
+ source_table: 'session_conversations',
447
+ session_id: resolved.ctm_session_id || row.conversation_id,
448
+ ctm_session_id: resolved.ctm_session_id || '',
449
+ agent_session_id: resolved.agent_session_id || '',
450
+ conversation_id: row.conversation_id,
451
+ timestamp: row.imported_at || resolved.modified_at || '',
452
+ cwd: resolved.cwd || row.project_path || '',
453
+ branch: resolved.git_branch || row.git_branch || '',
454
+ title: resolved.title || row.title || row.first_message || '',
455
+ provider: resolved.provider || '',
456
+ model: resolved.model || '',
457
+ snippet: row.title || row.first_message || row.project_path || row.conversation_id,
458
+ content_hash: contentFingerprint([row.title, row.first_message, row.project_path].filter(Boolean).join('\n')),
459
+ rank: rank + 1500,
460
+ };
461
+ }
462
+
463
+ function resolveConversationOwner(db, conversationId) {
464
+ const id = String(conversationId || '');
465
+ const empty = { ctm_session_id: id, agent_session_id: '', provider: '', project_path: '', cwd: '', title: '' };
466
+ if (!id) return empty;
467
+
468
+ let agent = null;
469
+ if (hasTable(db, 'agent_sessions')) {
470
+ try {
471
+ agent = db.prepare('SELECT * FROM agent_sessions WHERE agent_session_id = ?').get(id) || null;
472
+ } catch {}
473
+ }
474
+ const ctmId = agent?.ctm_session_id || id;
475
+ let ctm = null;
476
+ if (hasTable(db, 'ctm_sessions')) {
477
+ try {
478
+ ctm = db.prepare('SELECT * FROM ctm_sessions WHERE id = ?').get(ctmId) || null;
479
+ } catch {}
480
+ }
481
+ if (!agent && hasTable(db, 'agent_sessions')) {
482
+ try {
483
+ agent = db.prepare('SELECT * FROM agent_sessions WHERE ctm_session_id = ? ORDER BY modified_at DESC, created_at DESC LIMIT 1').get(ctmId) || null;
484
+ } catch {}
485
+ }
486
+ return {
487
+ ctm_session_id: ctm?.id || agent?.ctm_session_id || ctmId,
488
+ agent_session_id: agent?.agent_session_id || (agent?.ctm_session_id ? id : ''),
489
+ provider: ctm?.provider || agent?.provider || '',
490
+ project_path: ctm?.project_path || agent?.project_path || '',
491
+ cwd: ctm?.cwd || ctm?.project_path || agent?.project_path || '',
492
+ title: ctm?.title || agent?.first_message || '',
493
+ first_message: agent?.first_message || '',
494
+ git_branch: agent?.git_branch || '',
495
+ model: agent?.model || '',
496
+ modified_at: agent?.modified_at || '',
497
+ updated_at: ctm?.updated_at || '',
498
+ };
499
+ }
500
+
501
+ function resolveSessionBundle(db, rawId) {
502
+ const candidates = candidateIds(rawId);
503
+ for (const candidate of candidates) {
504
+ const bundle = resolveSessionBundleExact(db, candidate, rawId);
505
+ if (bundle) return bundle;
506
+ }
507
+ return null;
508
+ }
509
+
510
+ function resolveSessionBundleExact(db, id, requestedId = id) {
511
+ const requested = String(requestedId || id || '').trim();
512
+ const cleanId = String(id || '').trim();
513
+ if (!cleanId) return null;
514
+
515
+ let ctm = null;
516
+ let agent = null;
517
+ if (hasTable(db, 'ctm_sessions')) {
518
+ try { ctm = db.prepare('SELECT * FROM ctm_sessions WHERE id = ?').get(cleanId) || null; } catch {}
519
+ }
520
+ if (hasTable(db, 'agent_sessions')) {
521
+ try { agent = db.prepare('SELECT * FROM agent_sessions WHERE agent_session_id = ?').get(cleanId) || null; } catch {}
522
+ }
523
+
524
+ const ctmId = ctm?.id || agent?.ctm_session_id || cleanId;
525
+ if (!ctm && hasTable(db, 'ctm_sessions')) {
526
+ try { ctm = db.prepare('SELECT * FROM ctm_sessions WHERE id = ?').get(ctmId) || null; } catch {}
527
+ }
528
+
529
+ let agents = [];
530
+ if (hasTable(db, 'agent_sessions')) {
531
+ try {
532
+ agents = db.prepare('SELECT * FROM agent_sessions WHERE ctm_session_id = ? ORDER BY modified_at DESC, created_at DESC').all(ctmId);
533
+ } catch {}
534
+ }
535
+ if (agent && !agents.some((row) => row.agent_session_id === agent.agent_session_id)) agents.unshift(agent);
536
+
537
+ const conversationIds = new Set();
538
+ for (const idCandidate of [cleanId, ctmId, ...agents.map((row) => row.agent_session_id)]) {
539
+ if (idCandidate && hasConversation(db, idCandidate)) conversationIds.add(idCandidate);
540
+ }
541
+
542
+ if (!ctm && !agent && conversationIds.size === 0) return null;
543
+ return {
544
+ requested_id: requested,
545
+ ctm_session_id: ctmId,
546
+ agent_session_ids: agents.map((row) => row.agent_session_id).filter(Boolean),
547
+ conversation_ids: [...conversationIds],
548
+ provider: ctm?.provider || agent?.provider || '',
549
+ project_path: ctm?.project_path || agent?.project_path || '',
550
+ cwd: ctm?.cwd || ctm?.project_path || agent?.project_path || '',
551
+ title: ctm?.title || agent?.first_message || '',
552
+ starred: Boolean(ctm?.starred),
553
+ user_renamed: Boolean(ctm?.user_renamed),
554
+ created_at: ctm?.created_at || agent?.created_at || '',
555
+ updated_at: ctm?.updated_at || agent?.updated_at || agent?.modified_at || '',
556
+ agents,
557
+ };
558
+ }
559
+
560
+ function hasConversation(db, sessionId) {
561
+ if (!hasTable(db, 'session_conversations')) return false;
562
+ const idCol = idColumnFor(db, 'session_conversations');
563
+ if (!idCol) return false;
564
+ try {
565
+ return Boolean(db.prepare(`SELECT 1 FROM session_conversations WHERE ${quoteIdent(idCol)} = ? LIMIT 1`).get(sessionId));
566
+ } catch {
567
+ return false;
568
+ }
569
+ }
570
+
571
+ function getConversationRows(db, bundle) {
572
+ if (!hasTable(db, 'session_conversations')) return [];
573
+ const idCol = idColumnFor(db, 'session_conversations');
574
+ if (!idCol) return [];
575
+ const ids = bundle.conversation_ids && bundle.conversation_ids.length
576
+ ? bundle.conversation_ids
577
+ : [bundle.ctm_session_id, ...bundle.agent_session_ids].filter(Boolean);
578
+ const rows = [];
579
+ const seen = new Set();
580
+ for (const id of ids) {
581
+ if (seen.has(id)) continue;
582
+ seen.add(id);
583
+ try {
584
+ const row = db.prepare(`SELECT *, ${quoteIdent(idCol)} AS _conversation_id FROM session_conversations WHERE ${quoteIdent(idCol)} = ?`).get(id);
585
+ if (row) rows.push(row);
586
+ } catch {}
587
+ }
588
+ return rows.sort((a, b) => compareTimestamp(a.session_created_at || a.imported_at, b.session_created_at || b.imported_at));
589
+ }
590
+
591
+ function parseConversationMessages(row) {
592
+ try {
593
+ const parsed = JSON.parse(row?.messages || '[]');
594
+ return Array.isArray(parsed) ? parsed : [];
595
+ } catch {
596
+ return [];
597
+ }
598
+ }
599
+
600
+ function normalizeMessage(raw, { row, bundle, index, includeRaw }) {
601
+ const role = normalizeRole(raw);
602
+ const content = messageText(raw);
603
+ const conversationId = row?._conversation_id || row?.ctm_session_id || row?.session_id || bundle.ctm_session_id;
604
+ return {
605
+ source: 'ctm-db',
606
+ source_table: 'session_conversations',
607
+ session_id: bundle.ctm_session_id,
608
+ ctm_session_id: bundle.ctm_session_id,
609
+ agent_session_id: conversationId !== bundle.ctm_session_id ? conversationId : '',
610
+ conversation_id: conversationId,
611
+ message_index: Number.isInteger(raw?.message_index) ? raw.message_index : index,
612
+ role,
613
+ timestamp: raw?.timestamp || raw?.created_at || row?.session_created_at || row?.imported_at || '',
614
+ content,
615
+ content_hash: contentFingerprint(`${role}\n${content}`),
616
+ metadata: compactObject({
617
+ provider: bundle.provider || row?.model_provider || '',
618
+ model: row?.model_id || '',
619
+ title: bundle.title || row?.title || '',
620
+ cwd: bundle.cwd || row?.project_path || '',
621
+ branch: row?.git_branch || '',
622
+ tool_calls: raw?.tool_calls || raw?.toolCalls,
623
+ files: raw?.files || raw?.filesEdited,
624
+ }),
625
+ raw: includeRaw ? raw : undefined,
626
+ };
627
+ }
628
+
629
+ function normalizeRole(raw) {
630
+ const role = raw?.role || raw?.speaker || raw?.type || '';
631
+ if (role === 'human') return 'user';
632
+ if (role === 'ai') return 'assistant';
633
+ if (role === 'tool_result') return 'tool';
634
+ return String(role || 'unknown').toLowerCase();
635
+ }
636
+
637
+ function messageText(raw) {
638
+ if (!raw) return '';
639
+ if (typeof raw.content === 'string') return raw.content.trim();
640
+ if (Array.isArray(raw.content)) {
641
+ return raw.content.map((part) => {
642
+ if (!part) return '';
643
+ if (typeof part === 'string') return part;
644
+ if (typeof part.text === 'string') return part.text;
645
+ if (typeof part.content === 'string') return part.content;
646
+ if (typeof part.input === 'object') return JSON.stringify(part.input);
647
+ return '';
648
+ }).filter(Boolean).join('\n').trim();
649
+ }
650
+ if (typeof raw.text === 'string') return raw.text.trim();
651
+ if (typeof raw.message === 'string') return raw.message.trim();
652
+ if (raw.message && typeof raw.message.content === 'string') return raw.message.content.trim();
653
+ return '';
654
+ }
655
+
656
+ function formatSessionSummary(bundle, conversations, messages) {
657
+ const firstConversation = conversations[0] || {};
658
+ return {
659
+ session_id: bundle.ctm_session_id,
660
+ ctm_session_id: bundle.ctm_session_id,
661
+ agent_session_ids: bundle.agent_session_ids || [],
662
+ conversation_ids: conversations.map((row) => row._conversation_id || row.ctm_session_id || row.session_id).filter(Boolean),
663
+ provider: bundle.provider || firstConversation.model_provider || '',
664
+ cwd: bundle.cwd || firstConversation.project_path || '',
665
+ branch: firstConversation.git_branch || bundle.agents?.find((a) => a.git_branch)?.git_branch || '',
666
+ title: bundle.title || firstConversation.title || firstConversation.first_message || '',
667
+ created_at: bundle.created_at || firstConversation.session_created_at || '',
668
+ updated_at: bundle.updated_at || firstConversation.imported_at || '',
669
+ message_count: messages.length,
670
+ user_msg_count: firstConversation.user_msg_count || messages.filter((msg) => msg.role === 'user').length,
671
+ assistant_msg_count: firstConversation.assistant_msg_count || messages.filter((msg) => msg.role === 'assistant').length,
672
+ };
673
+ }
674
+
675
+ function orderMessages(messages) {
676
+ return [...messages].sort((a, b) => {
677
+ const t = compareTimestamp(a.timestamp, b.timestamp);
678
+ if (t !== 0) return t;
679
+ return (a.message_index || 0) - (b.message_index || 0);
680
+ });
681
+ }
682
+
683
+ function compareTimestamp(a, b) {
684
+ const at = Date.parse(a || '');
685
+ const bt = Date.parse(b || '');
686
+ if (Number.isFinite(at) && Number.isFinite(bt) && at !== bt) return at - bt;
687
+ if (Number.isFinite(at) && !Number.isFinite(bt)) return -1;
688
+ if (!Number.isFinite(at) && Number.isFinite(bt)) return 1;
689
+ return 0;
690
+ }
691
+
692
+ function dedupeMessages(messages) {
693
+ return dedupeItems(messages, {
694
+ contentFields: ['content'],
695
+ keyPrefix: (item) => item.role || '',
696
+ minContentLength: 12,
697
+ });
698
+ }
699
+
700
+ function dedupeItems(items, {
701
+ limit = 0,
702
+ contentFields = ['content'],
703
+ keyPrefix = () => '',
704
+ minContentLength = 0,
705
+ } = {}) {
706
+ const seen = new Map();
707
+ for (const item of items || []) {
708
+ const content = contentFields.map((field) => item?.[field]).find((value) => typeof value === 'string' && value.trim()) || '';
709
+ const normalized = normalizeContent(content);
710
+ const prefix = typeof keyPrefix === 'function' ? keyPrefix(item) : String(keyPrefix || '');
711
+ const key = normalized.length >= minContentLength
712
+ ? `${prefix}:${contentFingerprint(normalized)}`
713
+ : `${prefix}:${item.session_id || ''}:${item.conversation_id || ''}:${item.message_index ?? item.id ?? normalized}`;
714
+
715
+ if (!seen.has(key)) {
716
+ seen.set(key, { ...item });
717
+ continue;
718
+ }
719
+ const existing = seen.get(key);
720
+ seen.set(key, mergeDuplicateItem(existing, item));
721
+ }
722
+ const deduped = [...seen.values()].sort((a, b) => (a.rank || 0) - (b.rank || 0));
723
+ return limit ? deduped.slice(0, limit) : deduped;
724
+ }
725
+
726
+ function mergeDuplicateItem(existing, next) {
727
+ const merged = { ...existing };
728
+ const provenance = [
729
+ ...(Array.isArray(existing.provenance) ? existing.provenance : [provenanceOf(existing)]),
730
+ provenanceOf(next),
731
+ ].filter(Boolean);
732
+ merged.provenance = dedupeProvenance(provenance);
733
+ if (!merged.agent_session_id && next.agent_session_id) merged.agent_session_id = next.agent_session_id;
734
+ if (!merged.ctm_session_id && next.ctm_session_id) merged.ctm_session_id = next.ctm_session_id;
735
+ if (!merged.conversation_id && next.conversation_id) merged.conversation_id = next.conversation_id;
736
+ if (!merged.timestamp && next.timestamp) merged.timestamp = next.timestamp;
737
+ if ((String(next.snippet || '').length > String(merged.snippet || '').length) && String(next.snippet || '').length <= 1200) {
738
+ merged.snippet = next.snippet;
739
+ }
740
+ return merged;
741
+ }
742
+
743
+ function provenanceOf(item) {
744
+ if (!item) return null;
745
+ return compactObject({
746
+ source: item.source,
747
+ source_table: item.source_table,
748
+ session_id: item.session_id,
749
+ ctm_session_id: item.ctm_session_id,
750
+ agent_session_id: item.agent_session_id,
751
+ conversation_id: item.conversation_id,
752
+ message_index: item.message_index,
753
+ id: item.id,
754
+ });
755
+ }
756
+
757
+ function dedupeProvenance(items) {
758
+ const seen = new Set();
759
+ const out = [];
760
+ for (const item of items) {
761
+ const key = JSON.stringify(item);
762
+ if (seen.has(key)) continue;
763
+ seen.add(key);
764
+ out.push(item);
765
+ }
766
+ return out;
767
+ }
768
+
769
+ function normalizeContent(value) {
770
+ return String(value || '')
771
+ .replace(/\r\n/g, '\n')
772
+ .replace(/\s+/g, ' ')
773
+ .trim()
774
+ .toLowerCase();
775
+ }
776
+
777
+ function contentFingerprint(value) {
778
+ return crypto.createHash('sha256').update(String(value || '')).digest('hex').slice(0, 16);
779
+ }
780
+
781
+ function buildFtsQueries(query) {
782
+ const terms = String(query || '')
783
+ .match(/[A-Za-z0-9_./:-]+/g)
784
+ ?.map((term) => term.replace(/^[-:.\\/]+|[-:.\\/]+$/g, ''))
785
+ .filter((term) => term.length > 1)
786
+ .slice(0, 12) || [];
787
+ if (!terms.length) return [];
788
+ const quoted = terms.map((term) => `"${term.replace(/"/g, '""')}"`);
789
+ const andQuery = quoted.join(' AND ');
790
+ const orQuery = quoted.join(' OR ');
791
+ return andQuery === orQuery ? [andQuery] : [andQuery, orQuery];
792
+ }
793
+
794
+ function escapeLike(value) {
795
+ return String(value || '').replace(/[\\%_]/g, (match) => `\\${match}`);
796
+ }
797
+
798
+ function snippetAround(content, max = 800) {
799
+ const text = String(content || '').replace(/\s+/g, ' ').trim();
800
+ if (text.length <= max) return text;
801
+ return `${text.slice(0, Math.max(0, max - 1)).trim()}...`;
802
+ }
803
+
804
+ function normalizeIdentifiers(value) {
805
+ if (Array.isArray(value)) return value.flatMap((item) => normalizeIdentifiers(item));
806
+ if (value == null) return [];
807
+ return String(value).split(',').map((part) => part.trim()).filter(Boolean);
808
+ }
809
+
810
+ function candidateIds(id) {
811
+ const raw = String(id || '').trim();
812
+ if (!raw) return [];
813
+ const out = [raw];
814
+ const parts = raw.split(':').filter(Boolean);
815
+ if (parts.length > 1) {
816
+ out.push(parts[parts.length - 1]);
817
+ if (parts.length >= 4 && parts[0] === 'diary') out.push(parts.slice(2, -1).join(':'));
818
+ if (parts.length >= 2) out.push(parts.slice(1).join(':'));
819
+ }
820
+ return [...new Set(out.filter(Boolean))];
821
+ }
822
+
823
+ function clampLimit(value, fallback, max) {
824
+ const n = Number(value || fallback);
825
+ if (!Number.isFinite(n)) return fallback;
826
+ return Math.min(Math.max(Math.trunc(n), 1), max);
827
+ }
828
+
829
+ function normalizeContextLimit(value) {
830
+ if (value === 0 || value === '0' || value === 'all' || value === 'full') return 0;
831
+ return clampLimit(value, DEFAULT_CONTEXT_LIMIT, MAX_CONTEXT_LIMIT);
832
+ }
833
+
834
+ function trimContextToBudget(context, budgetChars) {
835
+ const sessions = context.sessions || [];
836
+ const messages = [];
837
+ let used = JSON.stringify(sessions).length;
838
+ let truncated = false;
839
+ for (const message of context.messages || []) {
840
+ const size = String(message.content || '').length + 200;
841
+ if (used + size > budgetChars) {
842
+ truncated = true;
843
+ break;
844
+ }
845
+ messages.push(message);
846
+ used += size;
847
+ }
848
+ return {
849
+ ...context,
850
+ sessions,
851
+ messages,
852
+ count: messages.length,
853
+ truncated,
854
+ };
855
+ }
856
+
857
+ function renderContextMarkdown(context, { compact = false } = {}) {
858
+ const lines = [];
859
+ lines.push('# Wall-E Session Context');
860
+ lines.push('');
861
+ if (context.task) {
862
+ lines.push(`Task: ${context.task}`);
863
+ lines.push('');
864
+ }
865
+ if (context.sessions?.length) {
866
+ lines.push('## Sessions');
867
+ for (const session of context.sessions) {
868
+ const title = session.title ? ` - ${session.title}` : '';
869
+ const cwd = session.cwd ? ` (${session.cwd})` : '';
870
+ lines.push(`- ${session.session_id}${title}${cwd}; messages=${session.message_count}`);
871
+ }
872
+ lines.push('');
873
+ }
874
+ lines.push('## Messages');
875
+ const maxContent = compact ? 1200 : 8000;
876
+ for (const msg of context.messages || []) {
877
+ const label = [msg.role || 'unknown', msg.timestamp || '', msg.session_id || ''].filter(Boolean).join(' | ');
878
+ lines.push(`### ${label}`);
879
+ const content = String(msg.content || '');
880
+ lines.push(content.length > maxContent ? `${content.slice(0, maxContent).trim()}...` : content);
881
+ lines.push('');
882
+ }
883
+ if (context.transfer?.truncated || context.truncated) {
884
+ lines.push('_Context pack truncated to fit the requested budget._');
885
+ }
886
+ return lines.join('\n').trim();
887
+ }
888
+
889
+ function compactObject(obj) {
890
+ const out = {};
891
+ for (const [key, value] of Object.entries(obj || {})) {
892
+ if (value === undefined || value === null || value === '') continue;
893
+ if (Array.isArray(value) && value.length === 0) continue;
894
+ out[key] = value;
895
+ }
896
+ return out;
897
+ }
898
+
899
+ module.exports = {
900
+ resolveCtmDbPath,
901
+ openCtmDb,
902
+ searchCtmSessions,
903
+ getCtmSessionContext,
904
+ buildContextPack,
905
+ dedupeItems,
906
+ dedupeMessages,
907
+ normalizeContent,
908
+ contentFingerprint,
909
+ renderContextMarkdown,
910
+ };