create-walle 0.9.14 → 0.9.15
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 +2 -2
- package/bin/create-walle.js +37 -2
- package/package.json +1 -1
- package/template/claude-task-manager/api-prompts.js +11 -2
- package/template/claude-task-manager/db.js +94 -75
- package/template/claude-task-manager/docs/session-tooltip-freshness-design.md +224 -0
- package/template/claude-task-manager/docs/session-ux-issue-review-2026-05-01.md +369 -0
- package/template/claude-task-manager/fuzzy-utils.js +10 -2
- package/template/claude-task-manager/git-utils.js +29 -7
- package/template/claude-task-manager/lib/agent-capabilities.js +1 -1
- package/template/claude-task-manager/lib/agent-presets.js +38 -5
- package/template/claude-task-manager/lib/codex-terminal-final.js +53 -0
- package/template/claude-task-manager/lib/ctm-session-context-api.js +222 -0
- package/template/claude-task-manager/lib/session-diagnostics.js +56 -0
- package/template/claude-task-manager/lib/session-history.js +165 -0
- package/template/claude-task-manager/lib/session-stream.js +253 -20
- package/template/claude-task-manager/lib/walle-ctm-history.js +49 -6
- package/template/claude-task-manager/lib/walle-mcp-auto-config.js +6 -2
- package/template/claude-task-manager/lib/walle-supervisor.js +3 -0
- package/template/claude-task-manager/lib/walle-transcript.js +1 -3
- package/template/claude-task-manager/package.json +1 -0
- package/template/claude-task-manager/public/css/walle.css +66 -0
- package/template/claude-task-manager/public/index.html +869 -223
- package/template/claude-task-manager/public/js/message-renderer.js +314 -35
- package/template/claude-task-manager/public/js/session-search-utils.js +15 -3
- package/template/claude-task-manager/public/js/session-status-precedence.js +125 -0
- package/template/claude-task-manager/public/js/stream-view.js +341 -49
- package/template/claude-task-manager/public/js/terminal-restore-state.js +57 -0
- package/template/claude-task-manager/public/js/walle-session.js +162 -11
- package/template/claude-task-manager/public/js/walle.js +109 -0
- package/template/claude-task-manager/server.js +600 -234
- package/template/claude-task-manager/session-integrity.js +19 -13
- package/template/claude-task-manager/workers/scrollback-worker.js +5 -6
- package/template/package.json +1 -1
- package/template/wall-e/agent-runners/claude-code.js +2 -0
- package/template/wall-e/agent.js +27 -1
- package/template/wall-e/api-walle.js +272 -46
- package/template/wall-e/brain.js +291 -42
- package/template/wall-e/chat.js +172 -15
- package/template/wall-e/coding/compaction-service.js +19 -5
- package/template/wall-e/coding/workspace-replay.js +1 -4
- package/template/wall-e/coding-orchestrator.js +224 -74
- package/template/wall-e/compat.js +0 -28
- package/template/wall-e/context/context-builder.js +3 -1
- package/template/wall-e/embeddings.js +2 -7
- package/template/wall-e/eval/agent-runner.js +14 -5
- package/template/wall-e/eval/benchmarks/chat-eval.json +66 -6
- package/template/wall-e/eval/cc-replay.js +1 -0
- package/template/wall-e/eval/debug-agent003.js +1 -0
- package/template/wall-e/eval/run-model-comparison.js +1 -0
- package/template/wall-e/eval/swebench-adapter.js +1 -0
- package/template/wall-e/evaluation/quorum-evaluator.js +0 -1
- package/template/wall-e/extraction/knowledge-extractor.js +1 -2
- package/template/wall-e/lib/mcp-integration.js +131 -15
- package/template/wall-e/loops/initiative.js +87 -2
- package/template/wall-e/mcp-server.js +621 -30
- package/template/wall-e/memory/ctm-context-client.js +230 -0
- package/template/wall-e/memory/ctm-session-context.js +490 -24
- package/template/wall-e/prompts/coding/memory-protocol.md +6 -0
- package/template/wall-e/server.js +5 -1
- package/template/wall-e/skills/_bundled/memory-search/SKILL.md +8 -0
- package/template/wall-e/skills/_bundled/slack-mentions/run.js +471 -188
- package/template/wall-e/skills/skill-planner.js +35 -2
- package/template/wall-e/slack/socket-mode-listener.js +276 -0
- package/template/wall-e/telemetry.js +70 -2
- package/template/website/index.html +2 -2
package/README.md
CHANGED
|
@@ -20,9 +20,9 @@ A web dashboard for running and managing AI coding sessions across multiple prov
|
|
|
20
20
|
|
|
21
21
|
An always-on AI agent that learns from your Slack, email, calendar, and coding sessions.
|
|
22
22
|
|
|
23
|
-
- **Second Brain** — Automatically ingests your digital life into a searchable memory store with full-text search, knowledge extraction, and pattern detection
|
|
23
|
+
- **Second Brain** — Automatically ingests your digital life and coding sessions into a searchable memory store with full-text search, knowledge extraction, and pattern detection
|
|
24
24
|
- **Proactive Intelligence** — Surfaces time-sensitive items, suggests actions, and delivers morning briefings and weekly reflections without being asked
|
|
25
|
-
- **Chat with Tools** — Talk to Wall-E in the browser — it can search
|
|
25
|
+
- **Chat with Tools** — Talk to Wall-E in the browser — it can search memories, recall prior coding sessions, look up people, run skills, and call external tools via MCP
|
|
26
26
|
- **20 Bundled Skills** — Morning briefing, weekly reflection, proactive alerts, Slack monitoring, email sync, calendar integration, coding agent, memory search, model training, model pricing sync, and more
|
|
27
27
|
- **Multi-Model** — Works with Claude, GPT, Gemini, DeepSeek, and local models via Ollama, LM Studio, or MLX with smart routing
|
|
28
28
|
- **Skill Management GUI** — Search, filter, create, edit, and monitor skills from the browser with rich cards, config forms, execution history, export/import, and pre-flight validation
|
package/bin/create-walle.js
CHANGED
|
@@ -17,6 +17,8 @@ const { injectMcpConfigs } = require('./mcp-inject');
|
|
|
17
17
|
const TEMPLATE_DIR = path.join(__dirname, '..', 'template');
|
|
18
18
|
const LABEL = 'com.walle.server';
|
|
19
19
|
const INSTALL_PATH_FILE = path.join(process.env.HOME, '.walle', 'install-path');
|
|
20
|
+
const TELEMETRY_DATA_DIR = process.env.WALL_E_DATA_DIR || path.join(process.env.HOME || '/tmp', '.walle', 'data');
|
|
21
|
+
const CLI_LIFECYCLE_FILE = path.join(TELEMETRY_DATA_DIR, '.cli-lifecycle.jsonl');
|
|
20
22
|
const MANAGED_PACKAGE_DIRS = ['claude-task-manager', 'wall-e'];
|
|
21
23
|
const NATIVE_DEPENDENCIES = new Set([
|
|
22
24
|
'better-sqlite3',
|
|
@@ -28,6 +30,24 @@ const NATIVE_DEPENDENCIES = new Set([
|
|
|
28
30
|
// Files to preserve during update (user config, not code)
|
|
29
31
|
const PRESERVE_ON_UPDATE = ['.env', 'wall-e/wall-e-config.json'];
|
|
30
32
|
|
|
33
|
+
function writeCliLifecycleEvent(event, meta = {}) {
|
|
34
|
+
if (process.env.WALLE_TELEMETRY === '0' || process.env.WALLE_TELEMETRY === 'false') return;
|
|
35
|
+
try {
|
|
36
|
+
fs.mkdirSync(TELEMETRY_DATA_DIR, { recursive: true });
|
|
37
|
+
const pkg = require('../package.json');
|
|
38
|
+
const entry = {
|
|
39
|
+
event,
|
|
40
|
+
meta: {
|
|
41
|
+
command: meta.command || '',
|
|
42
|
+
package_version: pkg.version || 'unknown',
|
|
43
|
+
elapsed_ms: Number.isFinite(meta.elapsed_ms) ? meta.elapsed_ms : undefined,
|
|
44
|
+
},
|
|
45
|
+
t: Date.now(),
|
|
46
|
+
};
|
|
47
|
+
fs.appendFileSync(CLI_LIFECYCLE_FILE, JSON.stringify(entry) + '\n', { mode: 0o600 });
|
|
48
|
+
} catch {}
|
|
49
|
+
}
|
|
50
|
+
|
|
31
51
|
// ── CLI Router ──
|
|
32
52
|
|
|
33
53
|
if (require.main === module) {
|
|
@@ -97,9 +117,11 @@ function printMcpResults(wallePort) {
|
|
|
97
117
|
} else if (r.action === 'already_configured') {
|
|
98
118
|
console.log(` ${DIM}= ${r.tool} -- already configured${RESET}`);
|
|
99
119
|
} else if (r.action === 'updated') {
|
|
100
|
-
|
|
120
|
+
const label = r.kind === 'agent_instructions' ? 'updated memory routing in' : 'updated port in';
|
|
121
|
+
console.log(` ${GREEN}~ ${r.tool}${RESET} -- ${label} ${DIM}${r.configPath}${RESET}`);
|
|
101
122
|
} else {
|
|
102
|
-
|
|
123
|
+
const label = r.kind === 'agent_instructions' ? 'added memory routing to' : 'added Wall-E to';
|
|
124
|
+
console.log(` ${GREEN}+ ${r.tool}${RESET} -- ${label} ${DIM}${r.configPath}${RESET}`);
|
|
103
125
|
}
|
|
104
126
|
}
|
|
105
127
|
console.log(`\n ${DIM}Your AI coding tools can now access Wall-E's memory and knowledge.`);
|
|
@@ -140,6 +162,8 @@ function install(targetDir) {
|
|
|
140
162
|
console.error(' Template not found. Try: npm cache clean --force && npx create-walle@latest');
|
|
141
163
|
process.exit(1);
|
|
142
164
|
}
|
|
165
|
+
const installStartedAt = Date.now();
|
|
166
|
+
writeCliLifecycleEvent('cli_install_started', { command: 'install' });
|
|
143
167
|
|
|
144
168
|
const ownerName = detectName().replace(/[\r\n=]/g, '').trim().slice(0, 200);
|
|
145
169
|
const timezone = detectTimezone();
|
|
@@ -216,12 +240,18 @@ function install(targetDir) {
|
|
|
216
240
|
npx create-walle logs ${DIM}View logs${RESET}
|
|
217
241
|
`);
|
|
218
242
|
printMcpResults(parseInt(wallePort));
|
|
243
|
+
writeCliLifecycleEvent('cli_install_completed', {
|
|
244
|
+
command: 'install',
|
|
245
|
+
elapsed_ms: Date.now() - installStartedAt,
|
|
246
|
+
});
|
|
219
247
|
}
|
|
220
248
|
|
|
221
249
|
function update() {
|
|
222
250
|
const dir = findWalleDir();
|
|
223
251
|
const port = readPort(dir);
|
|
224
252
|
const pkg = require('../package.json');
|
|
253
|
+
const updateStartedAt = Date.now();
|
|
254
|
+
writeCliLifecycleEvent('cli_update_started', { command: 'update' });
|
|
225
255
|
|
|
226
256
|
console.log(`${BOLD}${CYAN} Wall-E${RESET} — Updating to v${pkg.version}...\n`);
|
|
227
257
|
console.log(` ${DIM}Directory: ${dir}${RESET}`);
|
|
@@ -274,6 +304,10 @@ function update() {
|
|
|
274
304
|
${DIM}Your .env and config were preserved.${RESET}
|
|
275
305
|
`);
|
|
276
306
|
printMcpResults(parseInt(readWallePort(dir)));
|
|
307
|
+
writeCliLifecycleEvent('cli_update_completed', {
|
|
308
|
+
command: 'update',
|
|
309
|
+
elapsed_ms: Date.now() - updateStartedAt,
|
|
310
|
+
});
|
|
277
311
|
}
|
|
278
312
|
|
|
279
313
|
function start() {
|
|
@@ -732,4 +766,5 @@ module.exports = {
|
|
|
732
766
|
npmCliCandidates,
|
|
733
767
|
repairNativeDependencies,
|
|
734
768
|
resolveNpmRunner,
|
|
769
|
+
writeCliLifecycleEvent,
|
|
735
770
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-walle",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.15",
|
|
4
4
|
"description": "CTM + Wall-E \u2014 AI coding dashboard and personal digital twin agent. Multi-agent terminal for Claude Code, Codex, Gemini, Aider, OpenCode, and more, plus prompt editor, task queue, and an agent that learns from Slack, email & calendar.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"create-walle": "bin/create-walle.js"
|
|
@@ -10,6 +10,12 @@ const walleClient = require('./lib/walle-client');
|
|
|
10
10
|
const claudeDesktopSessions = require('./lib/claude-desktop-sessions');
|
|
11
11
|
// AI search uses direct HTTP calls to Claude API (supports Portkey proxy)
|
|
12
12
|
|
|
13
|
+
let dbMaintenanceRunner = null;
|
|
14
|
+
|
|
15
|
+
function setDbMaintenanceRunner(fn) {
|
|
16
|
+
dbMaintenanceRunner = typeof fn === 'function' ? fn : null;
|
|
17
|
+
}
|
|
18
|
+
|
|
13
19
|
// Embed a prompt (async, fire-and-forget)
|
|
14
20
|
async function _embedPrompt(promptId, title, content) {
|
|
15
21
|
try {
|
|
@@ -1467,7 +1473,10 @@ async function handleRestoreBackup(req, res) {
|
|
|
1467
1473
|
try {
|
|
1468
1474
|
const data = await readBody(req);
|
|
1469
1475
|
if (!data.name) return jsonResponse(res, 400, { error: 'Missing backup name' });
|
|
1470
|
-
const
|
|
1476
|
+
const restore = () => db.restoreBackup(data.name);
|
|
1477
|
+
const result = dbMaintenanceRunner
|
|
1478
|
+
? await dbMaintenanceRunner({ kind: 'restore-backup', backupName: data.name }, restore)
|
|
1479
|
+
: restore();
|
|
1471
1480
|
jsonResponse(res, 200, result);
|
|
1472
1481
|
} catch (e) { jsonResponse(res, 500, { error: e.message }); }
|
|
1473
1482
|
}
|
|
@@ -2814,4 +2823,4 @@ function safeParse(json, fallback) {
|
|
|
2814
2823
|
try { return JSON.parse(json); } catch { return fallback; }
|
|
2815
2824
|
}
|
|
2816
2825
|
|
|
2817
|
-
module.exports = { handlePromptApi, queueEngine, importPermissionsToDb, runIncrementalConversationImport, importSessionFile, setUiPrefsBroadcaster };
|
|
2826
|
+
module.exports = { handlePromptApi, queueEngine, importPermissionsToDb, runIncrementalConversationImport, importSessionFile, setUiPrefsBroadcaster, setDbMaintenanceRunner };
|
|
@@ -2141,7 +2141,7 @@ function listSessionConversations({ search, limit, offset, hostname, allDevices
|
|
|
2141
2141
|
}
|
|
2142
2142
|
if (search) {
|
|
2143
2143
|
sql += ' AND (title LIKE ? OR first_message LIKE ? OR project_path LIKE ? OR messages LIKE ?)';
|
|
2144
|
-
const q = `%${search}%`;
|
|
2144
|
+
const q = `%${normalizeSessionSearchValue(search) || search}%`;
|
|
2145
2145
|
params.push(q, q, q, q);
|
|
2146
2146
|
}
|
|
2147
2147
|
sql += ' ORDER BY imported_at DESC';
|
|
@@ -2154,6 +2154,14 @@ function getSessionConversation(sessionId) {
|
|
|
2154
2154
|
return getDb().prepare('SELECT * FROM session_conversations WHERE ctm_session_id = ?').get(sessionId);
|
|
2155
2155
|
}
|
|
2156
2156
|
|
|
2157
|
+
function normalizeSessionSearchValue(value) {
|
|
2158
|
+
return String(value || '')
|
|
2159
|
+
.trim()
|
|
2160
|
+
.toLowerCase()
|
|
2161
|
+
.replace(/(^|[\s([{])[$/]+(?=[a-z0-9_-])/g, '$1')
|
|
2162
|
+
.replace(/\s+/g, ' ');
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2157
2165
|
function updateSessionModel(sessionId, modelProvider, modelId) {
|
|
2158
2166
|
getDb().prepare(
|
|
2159
2167
|
'UPDATE session_conversations SET model_provider = ?, model_id = ? WHERE ctm_session_id = ?'
|
|
@@ -2215,10 +2223,25 @@ function checkpointWal(mode) {
|
|
|
2215
2223
|
const m = (mode || 'PASSIVE').toUpperCase();
|
|
2216
2224
|
if (!_VALID_CHECKPOINT_MODES.has(m)) return;
|
|
2217
2225
|
try {
|
|
2218
|
-
db.pragma(`wal_checkpoint(${m})`);
|
|
2226
|
+
return db.pragma(`wal_checkpoint(${m})`);
|
|
2219
2227
|
} catch {}
|
|
2220
2228
|
}
|
|
2221
2229
|
|
|
2230
|
+
function checkpointWalOrThrow(mode) {
|
|
2231
|
+
if (!db) throw new Error('Database not initialized');
|
|
2232
|
+
const m = (mode || 'PASSIVE').toUpperCase();
|
|
2233
|
+
if (!_VALID_CHECKPOINT_MODES.has(m)) throw new Error(`Invalid WAL checkpoint mode: ${mode}`);
|
|
2234
|
+
const rows = db.pragma(`wal_checkpoint(${m})`);
|
|
2235
|
+
const row = Array.isArray(rows) ? rows[0] : rows;
|
|
2236
|
+
const busy = Number(row?.busy ?? row?.[0] ?? 0);
|
|
2237
|
+
if (busy > 0) {
|
|
2238
|
+
const log = row?.log ?? row?.[1] ?? 'unknown';
|
|
2239
|
+
const checkpointed = row?.checkpointed ?? row?.[2] ?? 'unknown';
|
|
2240
|
+
throw new Error(`WAL checkpoint ${m} could not complete: busy=${busy}, log=${log}, checkpointed=${checkpointed}`);
|
|
2241
|
+
}
|
|
2242
|
+
return rows;
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2222
2245
|
// Gzip a .db file to .db.gz and remove the original.
|
|
2223
2246
|
// Returns the actual output path (destPath if gzip succeeded, srcPath if it didn't).
|
|
2224
2247
|
function _gzipBackup(srcPath, destPath) {
|
|
@@ -2234,41 +2257,12 @@ function _gzipBackup(srcPath, destPath) {
|
|
|
2234
2257
|
}
|
|
2235
2258
|
|
|
2236
2259
|
function createBackup(label) {
|
|
2237
|
-
|
|
2238
|
-
checkpointWal('TRUNCATE');
|
|
2239
|
-
|
|
2240
|
-
const now = new Date();
|
|
2241
|
-
const ts = now.toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
2242
|
-
const tag = label ? `-${label.replace(/[^a-zA-Z0-9_-]/g, '')}` : '';
|
|
2243
|
-
const tmpPath = path.join(BACKUP_DIR, `task-manager-${ts}${tag}.db`);
|
|
2244
|
-
const backupName = `task-manager-${ts}${tag}.db.gz`;
|
|
2245
|
-
const backupPath = path.join(BACKUP_DIR, backupName);
|
|
2246
|
-
|
|
2247
|
-
// Use SQLite backup API, then gzip
|
|
2248
|
-
db.backup(tmpPath).then(() => {
|
|
2249
|
-
_gzipBackup(tmpPath, backupPath);
|
|
2250
|
-
}).catch(() => {
|
|
2251
|
-
fs.copyFileSync(currentDbPath, tmpPath);
|
|
2252
|
-
_gzipBackup(tmpPath, backupPath);
|
|
2253
|
-
});
|
|
2254
|
-
|
|
2255
|
-
// Also copy images dir as a tarball if it has content
|
|
2256
|
-
const imagesBackup = path.join(BACKUP_DIR, `images-${ts}${tag}.tar.gz`);
|
|
2257
|
-
try {
|
|
2258
|
-
|
|
2259
|
-
const imageFiles = fs.readdirSync(DEFAULT_IMAGES_DIR);
|
|
2260
|
-
if (imageFiles.length > 0) {
|
|
2261
|
-
require('child_process').spawnSync('tar', ['-czf', imagesBackup, '-C', path.dirname(DEFAULT_IMAGES_DIR), path.basename(DEFAULT_IMAGES_DIR)], { timeout: 30000 });
|
|
2262
|
-
}
|
|
2263
|
-
} catch {}
|
|
2264
|
-
|
|
2265
|
-
cleanOldBackups();
|
|
2266
|
-
return { backupName, backupPath, timestamp: now.toISOString() };
|
|
2260
|
+
return createBackupSync(label);
|
|
2267
2261
|
}
|
|
2268
2262
|
|
|
2269
2263
|
function createBackupSync(label) {
|
|
2270
2264
|
if (!db || !currentDbPath) throw new Error('Database not initialized');
|
|
2271
|
-
|
|
2265
|
+
checkpointWalOrThrow('TRUNCATE');
|
|
2272
2266
|
|
|
2273
2267
|
const now = new Date();
|
|
2274
2268
|
const ts = now.toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
@@ -2328,7 +2322,7 @@ function restoreBackup(backupName) {
|
|
|
2328
2322
|
createBackupSync('pre-restore');
|
|
2329
2323
|
|
|
2330
2324
|
// Close current DB
|
|
2331
|
-
|
|
2325
|
+
checkpointWalOrThrow('TRUNCATE');
|
|
2332
2326
|
if (db) { db.close(); db = null; }
|
|
2333
2327
|
|
|
2334
2328
|
// Decompress if needed, then copy over current DB
|
|
@@ -2874,7 +2868,7 @@ function updateStartupTaskBranch(sessionId, branch, worktreePath) {
|
|
|
2874
2868
|
|
|
2875
2869
|
function updateStartupTaskCwd(sessionId, cwd) {
|
|
2876
2870
|
getDb().prepare('UPDATE startup_tasks SET cwd = ?, worktree_path = ? WHERE ctm_session_id = ?')
|
|
2877
|
-
.run(cwd || '', cwd &&
|
|
2871
|
+
.run(cwd || '', cwd && /\/\.(?:claude|walle)\/worktrees\//.test(cwd) ? cwd : null, sessionId);
|
|
2878
2872
|
flushWal();
|
|
2879
2873
|
}
|
|
2880
2874
|
|
|
@@ -3593,24 +3587,15 @@ function upsertSession(id, data, opts) {
|
|
|
3593
3587
|
}
|
|
3594
3588
|
|
|
3595
3589
|
const agentId = data.agentSessionId;
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
const err = new Error(
|
|
3606
|
-
`agent_session_id ${agentId} is already claimed by ctm_session ${existing.ctm_session_id} (refusing cross-tab claim for ${id})`
|
|
3607
|
-
);
|
|
3608
|
-
err.code = 'E_CROSS_TAB_CLAIM';
|
|
3609
|
-
err.existingCtmSessionId = existing.ctm_session_id;
|
|
3610
|
-
err.attemptedCtmSessionId = id;
|
|
3611
|
-
err.agentSessionId = agentId;
|
|
3612
|
-
throw err;
|
|
3613
|
-
}
|
|
3590
|
+
function throwCrossTabClaim(existingCtmSessionId) {
|
|
3591
|
+
const err = new Error(
|
|
3592
|
+
`agent_session_id ${agentId} is already claimed by ctm_session ${existingCtmSessionId} (refusing cross-tab claim for ${id})`
|
|
3593
|
+
);
|
|
3594
|
+
err.code = 'E_CROSS_TAB_CLAIM';
|
|
3595
|
+
err.existingCtmSessionId = existingCtmSessionId;
|
|
3596
|
+
err.attemptedCtmSessionId = id;
|
|
3597
|
+
err.agentSessionId = agentId;
|
|
3598
|
+
throw err;
|
|
3614
3599
|
}
|
|
3615
3600
|
const ctmParams = {
|
|
3616
3601
|
id,
|
|
@@ -3636,6 +3621,7 @@ function upsertSession(id, data, opts) {
|
|
|
3636
3621
|
git_branch: data.gitBranch || '',
|
|
3637
3622
|
user_msg_count: data.userMsgCount || 0,
|
|
3638
3623
|
slug: data.slug || '',
|
|
3624
|
+
allow_reclaim: allowReclaim ? 1 : 0,
|
|
3639
3625
|
} : null;
|
|
3640
3626
|
|
|
3641
3627
|
// Wrap both upserts in a transaction to keep ctm_sessions + agent_sessions atomic
|
|
@@ -3656,13 +3642,21 @@ function upsertSession(id, data, opts) {
|
|
|
3656
3642
|
|
|
3657
3643
|
// If agent session data provided, upsert agent_sessions
|
|
3658
3644
|
if (agentParams) {
|
|
3659
|
-
|
|
3645
|
+
if (!allowReclaim) {
|
|
3646
|
+
const existing = d.prepare(
|
|
3647
|
+
'SELECT ctm_session_id FROM agent_sessions WHERE agent_session_id = ?'
|
|
3648
|
+
).get(agentId);
|
|
3649
|
+
if (existing && existing.ctm_session_id && existing.ctm_session_id !== id) {
|
|
3650
|
+
throwCrossTabClaim(existing.ctm_session_id);
|
|
3651
|
+
}
|
|
3652
|
+
}
|
|
3653
|
+
const result = d.prepare(`
|
|
3660
3654
|
INSERT INTO agent_sessions (agent_session_id, ctm_session_id, provider, project_path, jsonl_path,
|
|
3661
3655
|
first_message, file_size, modified_at, hostname, model, git_branch, user_msg_count, slug)
|
|
3662
3656
|
VALUES (@agent_session_id, @ctm_session_id, @provider, @project_path, @jsonl_path,
|
|
3663
3657
|
@first_message, @file_size, @modified_at, @hostname, @model, @git_branch, @user_msg_count, @slug)
|
|
3664
3658
|
ON CONFLICT(agent_session_id) DO UPDATE SET
|
|
3665
|
-
ctm_session_id =
|
|
3659
|
+
ctm_session_id = excluded.ctm_session_id,
|
|
3666
3660
|
provider = COALESCE(NULLIF(excluded.provider, ''), agent_sessions.provider),
|
|
3667
3661
|
project_path = COALESCE(NULLIF(excluded.project_path, ''), agent_sessions.project_path),
|
|
3668
3662
|
jsonl_path = COALESCE(NULLIF(excluded.jsonl_path, ''), agent_sessions.jsonl_path),
|
|
@@ -3675,10 +3669,20 @@ function upsertSession(id, data, opts) {
|
|
|
3675
3669
|
user_msg_count = CASE WHEN excluded.user_msg_count > 0 THEN excluded.user_msg_count ELSE agent_sessions.user_msg_count END,
|
|
3676
3670
|
slug = COALESCE(NULLIF(excluded.slug, ''), agent_sessions.slug),
|
|
3677
3671
|
updated_at = datetime('now')
|
|
3672
|
+
WHERE @allow_reclaim = 1
|
|
3673
|
+
OR agent_sessions.ctm_session_id IS NULL
|
|
3674
|
+
OR agent_sessions.ctm_session_id = excluded.ctm_session_id
|
|
3678
3675
|
`).run(agentParams);
|
|
3676
|
+
if (!allowReclaim && result.changes === 0) {
|
|
3677
|
+
const existing = d.prepare(
|
|
3678
|
+
'SELECT ctm_session_id FROM agent_sessions WHERE agent_session_id = ?'
|
|
3679
|
+
).get(agentId);
|
|
3680
|
+
throwCrossTabClaim(existing?.ctm_session_id || 'unknown');
|
|
3681
|
+
}
|
|
3679
3682
|
}
|
|
3680
3683
|
});
|
|
3681
|
-
txn();
|
|
3684
|
+
if (typeof txn.immediate === 'function') txn.immediate();
|
|
3685
|
+
else txn();
|
|
3682
3686
|
}
|
|
3683
3687
|
|
|
3684
3688
|
function setSessionStar(id, starred) {
|
|
@@ -3757,13 +3761,17 @@ function getSessionTitleNew(id) {
|
|
|
3757
3761
|
function getAllSessionsData() {
|
|
3758
3762
|
return getDb().prepare(`
|
|
3759
3763
|
SELECT c.*, a.agent_session_id, a.jsonl_path, a.first_message, a.file_size,
|
|
3760
|
-
a.modified_at, a.hostname, a.model, a.git_branch,
|
|
3761
|
-
a.
|
|
3764
|
+
a.modified_at, a.hostname, a.model, a.git_branch,
|
|
3765
|
+
MAX(COALESCE(a.user_msg_count, 0), COALESCE(sc.user_msg_count, 0)) as user_msg_count,
|
|
3766
|
+
COALESCE(NULLIF(a.last_user_content, ''), sc.last_user_content) as last_user_content,
|
|
3767
|
+
COALESCE(NULLIF(a.first_assistant_text, ''), sc.first_assistant_text) as first_assistant_text,
|
|
3768
|
+
COALESCE(NULLIF(a.rename_name, ''), sc.rename_name) as rename_name
|
|
3762
3769
|
FROM ctm_sessions c
|
|
3763
3770
|
LEFT JOIN (
|
|
3764
3771
|
SELECT *, ROW_NUMBER() OVER (PARTITION BY ctm_session_id ORDER BY modified_at DESC, created_at DESC) as rn
|
|
3765
3772
|
FROM agent_sessions
|
|
3766
3773
|
) a ON a.ctm_session_id = c.id AND a.rn = 1
|
|
3774
|
+
LEFT JOIN session_conversations sc ON sc.ctm_session_id = a.agent_session_id
|
|
3767
3775
|
`).all();
|
|
3768
3776
|
}
|
|
3769
3777
|
|
|
@@ -3789,22 +3797,33 @@ function getAgentSession(agentSessionId) {
|
|
|
3789
3797
|
*/
|
|
3790
3798
|
function deleteCtmSession(ctmSessionId) {
|
|
3791
3799
|
const d = getDb();
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3800
|
+
const cleanupTables = [
|
|
3801
|
+
['startup_tasks', 'ctm_session_id'],
|
|
3802
|
+
['scrollback_log', 'ctm_session_id'],
|
|
3803
|
+
['session_conversations', 'ctm_session_id'],
|
|
3804
|
+
['session_messages', 'ctm_session_id'],
|
|
3805
|
+
['session_analyses', 'ctm_session_id'],
|
|
3806
|
+
['prompt_queues', 'ctm_session_id'],
|
|
3807
|
+
];
|
|
3808
|
+
const txn = d.transaction(() => {
|
|
3809
|
+
// Collect JSONL paths before delete (for disk cleanup)
|
|
3810
|
+
const agentRows = d.prepare('SELECT jsonl_path FROM agent_sessions WHERE ctm_session_id = ?').all(ctmSessionId);
|
|
3811
|
+
const jsonlPaths = agentRows.map(r => r.jsonl_path).filter(Boolean);
|
|
3812
|
+
|
|
3813
|
+
// Also clean up child tables without FK constraints before the parent row is removed.
|
|
3814
|
+
for (const [table, idColumn] of cleanupTables) {
|
|
3815
|
+
const exists = d.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name = ?").get(table);
|
|
3816
|
+
if (!exists) continue;
|
|
3817
|
+
const cols = d.prepare(`PRAGMA table_info(${table})`).all().map(c => c.name);
|
|
3818
|
+
const column = cols.includes(idColumn) ? idColumn : (cols.includes('session_id') ? 'session_id' : null);
|
|
3819
|
+
if (column) d.prepare(`DELETE FROM ${table} WHERE ${column} = ?`).run(ctmSessionId);
|
|
3820
|
+
}
|
|
3821
|
+
|
|
3822
|
+
// CASCADE delete: deleting from ctm_sessions cascades to agent_sessions.
|
|
3823
|
+
d.prepare('DELETE FROM ctm_sessions WHERE id = ?').run(ctmSessionId);
|
|
3824
|
+
return jsonlPaths;
|
|
3825
|
+
});
|
|
3826
|
+
return txn();
|
|
3808
3827
|
}
|
|
3809
3828
|
|
|
3810
3829
|
// Legacy compatibility: upsertSessionIndex is now a no-op (session_index dropped)
|
|
@@ -3833,7 +3852,7 @@ module.exports = {
|
|
|
3833
3852
|
getSessionTitle, setSessionTitle, isSessionUserRenamed, getAllSessionTitles,
|
|
3834
3853
|
createTemplate, listTemplates, getTemplate, deleteTemplate,
|
|
3835
3854
|
trackPromptUsage, getPromptUsageStats,
|
|
3836
|
-
checkpointWal, createBackup, createBackupSync, listBackups, restoreBackup, deleteBackup, startDailyBackup,
|
|
3855
|
+
checkpointWal, checkpointWalOrThrow, createBackup, createBackupSync, listBackups, restoreBackup, deleteBackup, startDailyBackup,
|
|
3837
3856
|
saveQueue, loadQueue, loadAllQueues, deleteQueueDb,
|
|
3838
3857
|
listPermRules, addPermRule, removePermRule, bulkSetPermRules, getPermRulesByProject,
|
|
3839
3858
|
listAutoApprovals, upsertAutoApproval, toggleAutoApproval, deleteAutoApproval, getEnabledAutoApprovals,
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# Session Tooltip Freshness Design
|
|
2
|
+
|
|
3
|
+
## Problem
|
|
4
|
+
|
|
5
|
+
The active-session tooltip can show an AI summary that is technically cached
|
|
6
|
+
correctly but no longer represents the current task. The most visible failure is
|
|
7
|
+
a running session whose tooltip headline still describes an older task while the
|
|
8
|
+
latest prompt and progress have already moved on.
|
|
9
|
+
|
|
10
|
+
This is worse than an empty tooltip because it gives false confidence. The
|
|
11
|
+
operator uses the tooltip to decide which session needs attention, so stale
|
|
12
|
+
intent text can send the user to the wrong session or hide a current blocker.
|
|
13
|
+
|
|
14
|
+
## Existing Behavior
|
|
15
|
+
|
|
16
|
+
Current flow:
|
|
17
|
+
|
|
18
|
+
1. `stream-view.js` opens the tooltip after hover and fetches
|
|
19
|
+
`/api/sessions/:id/summary?turns=3`.
|
|
20
|
+
2. `SessionStream` stores cleaned user prompts in `userPromptCache`.
|
|
21
|
+
3. A new user prompt debounces AI summary generation by 2 seconds.
|
|
22
|
+
4. The configured provider generates a 10-15 word summary over the cached
|
|
23
|
+
prompt list.
|
|
24
|
+
5. `getSummary()` returns `summary`, `intent`, `displayPrompt`, `lastPrompt`,
|
|
25
|
+
and `progress`.
|
|
26
|
+
|
|
27
|
+
Current failure modes:
|
|
28
|
+
|
|
29
|
+
- Cached `intent.source = ai-summary` is treated as authoritative even when its
|
|
30
|
+
timestamp is older than the latest prompt.
|
|
31
|
+
- The tooltip does not refetch or rerender while it is already open for the same
|
|
32
|
+
session.
|
|
33
|
+
- The AI summary input is a flat list of recent prompts, so older work can
|
|
34
|
+
dominate the summary after a task switch.
|
|
35
|
+
- The UI label `Intent` does not explain freshness or distinguish current
|
|
36
|
+
prompt evidence from slower AI synthesis.
|
|
37
|
+
|
|
38
|
+
## Design Principles
|
|
39
|
+
|
|
40
|
+
- Latest user intent is the primary truth. AI is a compression layer, not a
|
|
41
|
+
source of freshness.
|
|
42
|
+
- Stale AI should be visible as context, never promoted as the current task.
|
|
43
|
+
- Refresh should be scoped to visible UI. Do not add broad polling across every
|
|
44
|
+
session.
|
|
45
|
+
- The tooltip should be copyable, stable, dense, and operational.
|
|
46
|
+
- Existing API fields must stay backward compatible for Session Overview and
|
|
47
|
+
older clients.
|
|
48
|
+
|
|
49
|
+
## API Contract
|
|
50
|
+
|
|
51
|
+
`GET /api/sessions/:id/summary` keeps existing fields and adds:
|
|
52
|
+
|
|
53
|
+
```js
|
|
54
|
+
{
|
|
55
|
+
currentTask: {
|
|
56
|
+
text: string | null,
|
|
57
|
+
source: 'latest-prompt' | 'ai-summary' | 'prompt-fallback' | 'title-fallback' | 'missing',
|
|
58
|
+
freshness: 'fresh' | 'updating' | 'stale' | 'missing',
|
|
59
|
+
updatedAt: number,
|
|
60
|
+
promptTimestamp: number,
|
|
61
|
+
staleReason?: string
|
|
62
|
+
},
|
|
63
|
+
latestPrompt: {
|
|
64
|
+
text: string | null,
|
|
65
|
+
timestamp: number
|
|
66
|
+
},
|
|
67
|
+
aiSummary: {
|
|
68
|
+
text: string | null,
|
|
69
|
+
source: string | null,
|
|
70
|
+
status: 'fresh' | 'updating' | 'stale' | 'fallback' | 'unavailable',
|
|
71
|
+
updatedAt: number,
|
|
72
|
+
promptTimestamp: number,
|
|
73
|
+
promptCount: number,
|
|
74
|
+
staleReason?: string
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Compatibility mapping:
|
|
80
|
+
|
|
81
|
+
- `intent` remains present.
|
|
82
|
+
- `summary` remains present.
|
|
83
|
+
- When AI is stale, `intent.text` should follow `currentTask.text` so existing
|
|
84
|
+
clients do not keep rendering stale AI as the primary task.
|
|
85
|
+
- `aiSummary.text` retains the older AI text for secondary display.
|
|
86
|
+
|
|
87
|
+
Freshness rules:
|
|
88
|
+
|
|
89
|
+
- If usable AI summary exists and `aiSummary.updatedAt >= latestPrompt.timestamp`,
|
|
90
|
+
`currentTask.source = ai-summary` and `freshness = fresh`.
|
|
91
|
+
- If usable AI summary exists but is older than the latest prompt,
|
|
92
|
+
`currentTask.source = latest-prompt`, `freshness = updating`, and
|
|
93
|
+
`aiSummary.status = stale` or `updating`.
|
|
94
|
+
- If no AI summary exists, use the most recent content-rich prompt and mark the
|
|
95
|
+
AI summary as `unavailable` or `fallback`.
|
|
96
|
+
- If a fallback summary is raw prompt text, keep current task on prompt evidence
|
|
97
|
+
and mark `aiSummary.status = fallback`.
|
|
98
|
+
|
|
99
|
+
## Summary Generation
|
|
100
|
+
|
|
101
|
+
Change the AI prompt from a flat prompt list to latest-prompt-weighted input.
|
|
102
|
+
|
|
103
|
+
Recommended input:
|
|
104
|
+
|
|
105
|
+
```text
|
|
106
|
+
Latest prompt:
|
|
107
|
+
<most recent cleaned prompt>
|
|
108
|
+
|
|
109
|
+
Recent context:
|
|
110
|
+
1. <older prompt>
|
|
111
|
+
2. <older prompt>
|
|
112
|
+
3. <older prompt>
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Recommended system prompt:
|
|
116
|
+
|
|
117
|
+
```text
|
|
118
|
+
Summarize the user's current task. Prioritize the latest prompt. Use older
|
|
119
|
+
prompts only as context. Return 8-14 words. Return only the summary, no quotes
|
|
120
|
+
or prefix.
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
The cached summary should record the prompt timestamp and prompt count used to
|
|
124
|
+
generate it. This makes freshness testable without depending on provider speed.
|
|
125
|
+
|
|
126
|
+
## Tooltip UX
|
|
127
|
+
|
|
128
|
+
Replace the main `Intent` section with `Current Task`.
|
|
129
|
+
|
|
130
|
+
Fresh state:
|
|
131
|
+
|
|
132
|
+
```text
|
|
133
|
+
CURRENT TASK
|
|
134
|
+
[AI SUMMARY] [FRESH] [now]
|
|
135
|
+
Fixing Wall-E session history restore and tooltip freshness.
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Updating state:
|
|
139
|
+
|
|
140
|
+
```text
|
|
141
|
+
CURRENT TASK
|
|
142
|
+
[LATEST PROMPT] [UPDATING] [now]
|
|
143
|
+
Does WallE coding agent have auto compact logic like OpenCode or Claude?
|
|
144
|
+
|
|
145
|
+
AI SUMMARY
|
|
146
|
+
[STALE] [2m ago]
|
|
147
|
+
Previous: Fixing session tab drag behavior and search issues.
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Unavailable state:
|
|
151
|
+
|
|
152
|
+
```text
|
|
153
|
+
CURRENT TASK
|
|
154
|
+
[LATEST PROMPT] [FALLBACK] [now]
|
|
155
|
+
Fix the session tooltip freshness behavior.
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Progress remains separate and should keep using assistant-event evidence.
|
|
159
|
+
|
|
160
|
+
Interaction behavior:
|
|
161
|
+
|
|
162
|
+
- Opening a tooltip fetches summary immediately.
|
|
163
|
+
- If the tooltip is already open for that session, new stream events rerender it.
|
|
164
|
+
- While visible, perform a lightweight refresh every 10 seconds.
|
|
165
|
+
- Stop the refresh timer when the tooltip is hidden.
|
|
166
|
+
- Clicking inside the tooltip continues to preserve it for copy/select.
|
|
167
|
+
- Activating a different session tooltip replaces the current tooltip.
|
|
168
|
+
|
|
169
|
+
## Implementation Plan
|
|
170
|
+
|
|
171
|
+
Phase 1: documentation
|
|
172
|
+
|
|
173
|
+
- Add this design note.
|
|
174
|
+
- Commit documentation by itself after TL review.
|
|
175
|
+
|
|
176
|
+
Phase 2: backend freshness
|
|
177
|
+
|
|
178
|
+
- Extend `SessionStream` cached summary metadata with prompt timestamp/count.
|
|
179
|
+
- Add `latestPrompt`, `aiSummary`, and `currentTask` to `getSummary()`.
|
|
180
|
+
- Ensure `intent` maps to `currentTask`, not stale AI.
|
|
181
|
+
- Update summary generation prompt to prioritize latest prompt.
|
|
182
|
+
- Add unit tests for fresh, stale, fallback, and provider-lag cases.
|
|
183
|
+
|
|
184
|
+
Phase 3: tooltip UI
|
|
185
|
+
|
|
186
|
+
- Render `Current Task` from `currentTask`.
|
|
187
|
+
- Render stale AI only as secondary context.
|
|
188
|
+
- Add freshness/status pills.
|
|
189
|
+
- Add visible-tooltip refresh and stream-event rerender.
|
|
190
|
+
- Add browser/render coverage for a tooltip that updates while open.
|
|
191
|
+
|
|
192
|
+
Phase 4: dev validation
|
|
193
|
+
|
|
194
|
+
- Start isolated CTM via `ctm-dev`.
|
|
195
|
+
- Verify service health on the dev port pair.
|
|
196
|
+
- Exercise stale summary behavior through a real browser.
|
|
197
|
+
- Confirm no browser console errors for the tooltip flow.
|
|
198
|
+
- Commit implementation after TL review.
|
|
199
|
+
|
|
200
|
+
## Test Matrix
|
|
201
|
+
|
|
202
|
+
Backend:
|
|
203
|
+
|
|
204
|
+
- AI summary generated after latest prompt: primary task is AI summary.
|
|
205
|
+
- AI summary timestamp older than latest prompt: primary task is latest prompt.
|
|
206
|
+
- AI provider slow or failed: primary task remains latest prompt.
|
|
207
|
+
- Fallback summary raw prompt: primary task remains prompt evidence.
|
|
208
|
+
- Summary generation input puts latest prompt before recent context.
|
|
209
|
+
|
|
210
|
+
Frontend:
|
|
211
|
+
|
|
212
|
+
- Tooltip opens and labels `Current Task`.
|
|
213
|
+
- Stale AI is demoted into `AI Summary`.
|
|
214
|
+
- New user stream event updates an already-open tooltip.
|
|
215
|
+
- Fresh summary event promotes AI summary.
|
|
216
|
+
- Tooltip remains copyable and dismisses only on outside click or another
|
|
217
|
+
tooltip activation.
|
|
218
|
+
|
|
219
|
+
Dev validation:
|
|
220
|
+
|
|
221
|
+
- CTM dev server starts on a random non-primary port.
|
|
222
|
+
- `/api/services/status` succeeds on the dev port.
|
|
223
|
+
- Browser test uses the dev port, never `3456` or `3457`.
|
|
224
|
+
- Screenshot or DOM assertions prove the freshness state.
|