create-walle 0.9.10 → 0.9.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/package.json +2 -2
- package/template/bin/dev.sh +7 -1
- package/template/bin/setup.js +53 -9
- package/template/bin/sync-images.js +53 -0
- package/template/builder-journal.md +17 -0
- package/template/claude-task-manager/api-prompts.js +98 -13
- package/template/claude-task-manager/api-reviews.js +82 -5
- package/template/claude-task-manager/db.js +32 -5
- package/template/claude-task-manager/docs/session-capture-foundation-design.md +1273 -0
- package/template/claude-task-manager/lib/claude-desktop-sessions.js +696 -0
- package/template/claude-task-manager/lib/coding-agent-models.js +49 -1
- package/template/claude-task-manager/lib/session-capture.js +421 -0
- package/template/claude-task-manager/lib/session-history.js +135 -15
- package/template/claude-task-manager/lib/session-jobs.js +10 -5
- package/template/claude-task-manager/lib/session-stream.js +87 -19
- package/template/claude-task-manager/lib/setup-provider-config.js +115 -0
- package/template/claude-task-manager/lib/walle-ctm-history.js +72 -0
- package/template/claude-task-manager/lib/walle-session-context.js +61 -0
- package/template/claude-task-manager/lib/walle-transcript.js +176 -0
- package/template/claude-task-manager/public/css/setup.css +35 -8
- package/template/claude-task-manager/public/css/walle-session.css +56 -0
- package/template/claude-task-manager/public/css/walle.css +120 -0
- package/template/claude-task-manager/public/index.html +814 -181
- package/template/claude-task-manager/public/js/message-renderer.js +148 -19
- package/template/claude-task-manager/public/js/reviews.js +120 -62
- package/template/claude-task-manager/public/js/setup.js +75 -31
- package/template/claude-task-manager/public/js/stream-view.js +115 -55
- package/template/claude-task-manager/public/js/walle-session.js +84 -2
- package/template/claude-task-manager/public/js/walle.js +308 -54
- package/template/claude-task-manager/server.js +1092 -146
- package/template/claude-task-manager/session-integrity.js +181 -54
- package/template/claude-task-manager/session-utils.js +123 -41
- package/template/claude-task-manager/workers/state-detectors/codex.js +5 -2
- package/template/package.json +1 -1
- package/template/wall-e/adapters/ctm.js +39 -18
- package/template/wall-e/agent-runners/contract.js +17 -0
- package/template/wall-e/agent-runners/index.js +22 -0
- package/template/wall-e/agent-runtime/harness.js +212 -0
- package/template/wall-e/agent-runtime/index.js +8 -0
- package/template/wall-e/agent-runtime/registry.js +67 -0
- package/template/wall-e/agent-runtime/session-store.js +179 -0
- package/template/wall-e/agent-runtime/spawn.js +208 -0
- package/template/wall-e/api-walle.js +174 -7
- package/template/wall-e/brain.js +266 -28
- package/template/wall-e/channels/policy.js +88 -0
- package/template/wall-e/channels/registry.js +15 -1
- package/template/wall-e/channels/reply-dispatcher.js +70 -0
- package/template/wall-e/channels/session-bindings.js +51 -0
- package/template/wall-e/chat/code-review-context.js +29 -0
- package/template/wall-e/chat.js +188 -42
- package/template/wall-e/coding/acp-adapter.js +188 -0
- package/template/wall-e/coding/agent-catalog.js +129 -0
- package/template/wall-e/coding/compaction-service.js +247 -0
- package/template/wall-e/coding/execution-trace.js +3 -0
- package/template/wall-e/coding/instruction-service.js +224 -0
- package/template/wall-e/coding/model-message.js +67 -0
- package/template/wall-e/coding/permission-rules-store.js +111 -0
- package/template/wall-e/coding/permission-service.js +266 -0
- package/template/wall-e/coding/prompt-bundle.js +67 -0
- package/template/wall-e/coding/prompt-runtime.js +243 -0
- package/template/wall-e/coding/provider-transform.js +188 -0
- package/template/wall-e/coding/runtime-mode.js +132 -0
- package/template/wall-e/coding/snapshot-service.js +155 -0
- package/template/wall-e/coding/stream-processor.js +268 -0
- package/template/wall-e/coding/task-tool.js +255 -0
- package/template/wall-e/coding/tool-registry.js +361 -0
- package/template/wall-e/coding/transcript-writer.js +143 -0
- package/template/wall-e/coding/workspace-replay.js +324 -0
- package/template/wall-e/coding-context.js +4 -22
- package/template/wall-e/coding-orchestrator.js +307 -18
- package/template/wall-e/coding-prompts.js +44 -3
- package/template/wall-e/context/context-builder.js +43 -1
- package/template/wall-e/context/topic-matcher.js +1 -1
- package/template/wall-e/eval/agent-runner.js +59 -13
- package/template/wall-e/eval/benchmarks/memory-retrieval.json +155 -57
- package/template/wall-e/eval/benchmarks.js +100 -16
- package/template/wall-e/eval/eval-orchestrator.js +218 -8
- package/template/wall-e/eval/harvester.js +49 -5
- package/template/wall-e/eval/head-to-head.js +23 -2
- package/template/wall-e/eval/humaneval-adapter.js +30 -5
- package/template/wall-e/eval/livecodebench-adapter.js +29 -5
- package/template/wall-e/eval/manifest.js +186 -0
- package/template/wall-e/eval/run-agent-benchmarks.js +66 -2
- package/template/wall-e/eval/session-retrieval-benchmark.js +150 -0
- package/template/wall-e/eval/session-transcripts.js +57 -4
- package/template/wall-e/eval/swebench-adapter.js +109 -3
- package/template/wall-e/evaluation/agent-router.js +53 -1
- package/template/wall-e/evaluation/coding-quorum.js +48 -1
- package/template/wall-e/evaluation/router.js +4 -2
- package/template/wall-e/evaluation/tier-selector.js +11 -1
- package/template/wall-e/extraction/contradiction.js +2 -2
- package/template/wall-e/extraction/indexer.js +2 -1
- package/template/wall-e/extraction/knowledge-extractor.js +2 -2
- package/template/wall-e/fly.toml +1 -0
- package/template/wall-e/hooks/cli.js +92 -0
- package/template/wall-e/hooks/discovery.js +119 -0
- package/template/wall-e/hooks/index.js +7 -0
- package/template/wall-e/hooks/manifest.js +55 -0
- package/template/wall-e/hooks/runtime.js +84 -0
- package/template/wall-e/hooks/session-memory.js +225 -0
- package/template/wall-e/http/auth.js +7 -2
- package/template/wall-e/http/chat-api.js +54 -8
- package/template/wall-e/integrations/claude-plugin/hooks/hooks.json +27 -0
- package/template/wall-e/integrations/claude-plugin/hooks/walle-precompact-hook.sh +5 -0
- package/template/wall-e/integrations/claude-plugin/hooks/walle-stop-hook.sh +5 -0
- package/template/wall-e/integrations/codex-plugin/hooks/walle-hook.sh +7 -0
- package/template/wall-e/integrations/codex-plugin/hooks.json +37 -0
- package/template/wall-e/listening/calendar.js +3 -1
- package/template/wall-e/llm/client.js +64 -10
- package/template/wall-e/llm/google.js +39 -5
- package/template/wall-e/llm/ollama.js +1 -1
- package/template/wall-e/llm/ollama.plugin.json +1 -1
- package/template/wall-e/llm/provider-availability.js +10 -0
- package/template/wall-e/llm/provider-error.js +269 -0
- package/template/wall-e/llm/tool-adapter.js +48 -12
- package/template/wall-e/loops/boot.js +2 -1
- package/template/wall-e/loops/initiative.js +2 -2
- package/template/wall-e/loops/tasks.js +8 -47
- package/template/wall-e/loops/workspace-prompts.js +20 -0
- package/template/wall-e/mcp-server.js +442 -1
- package/template/wall-e/memory/session-ingest-service.js +159 -0
- package/template/wall-e/memory/source-indexer.js +289 -0
- package/template/wall-e/plugins/discovery.js +83 -0
- package/template/wall-e/plugins/manifest-loader.js +50 -10
- package/template/wall-e/plugins/manifest-schema.js +69 -0
- package/template/wall-e/plugins/model-catalog.js +55 -0
- package/template/wall-e/prompts/coding/base.txt +2 -0
- package/template/wall-e/prompts/coding/deepseek.txt +1 -0
- package/template/wall-e/prompts/coding/memory-protocol.md +9 -0
- package/template/wall-e/prompts/coding/plan.txt +1 -0
- package/template/wall-e/runtime/execution-trace.js +220 -0
- package/template/wall-e/security/audit.js +266 -0
- package/template/wall-e/security/ssrf.js +236 -0
- package/template/wall-e/session-files.js +303 -0
- package/template/wall-e/skills/_bundled/slack-backfill/SKILL.md +3 -0
- package/template/wall-e/skills/_bundled/slack-sync/SKILL.md +3 -0
- package/template/wall-e/skills/internal-skill-registry.js +2 -2
- package/template/wall-e/skills/script-skill-runner.js +143 -0
- package/template/wall-e/skills/skill-executor.js +5 -6
- package/template/wall-e/skills/skill-fallback.js +3 -1
- package/template/wall-e/skills/skill-harness-registry.js +7 -8
- package/template/wall-e/skills/skill-planner.js +52 -4
- package/template/wall-e/skills/slack-ingest.js +11 -3
- package/template/wall-e/sources/base.js +90 -0
- package/template/wall-e/sources/builtin.js +33 -0
- package/template/wall-e/sources/claude-code-jsonl.js +78 -0
- package/template/wall-e/sources/codex-jsonl.js +125 -0
- package/template/wall-e/sources/coding-session-utils.js +117 -0
- package/template/wall-e/sources/contract-suite.js +59 -0
- package/template/wall-e/sources/gemini-jsonl.js +85 -0
- package/template/wall-e/sources/index.js +9 -0
- package/template/wall-e/sources/jsonl-utils.js +181 -0
- package/template/wall-e/sources/record-types.js +252 -0
- package/template/wall-e/sources/registry.js +92 -0
- package/template/wall-e/sources/transforms.js +100 -0
- package/template/wall-e/sources/walle-jsonl.js +108 -0
- package/template/wall-e/tools/coding-middleware.js +31 -1
- package/template/wall-e/tools/file-tracker.js +25 -1
- package/template/wall-e/tools/local-tools.js +75 -47
- package/template/wall-e/tools/session-sharing.js +68 -1
- package/template/wall-e/tools/shell-analyzer.js +1 -1
- package/template/wall-e/tools/shell-policy.js +47 -0
- package/template/wall-e/tools/snapshot.js +42 -0
- package/template/wall-e/training/harvester.js +49 -5
- package/template/wall-e/utils/repair.js +253 -1
- package/template/website/index.html +3 -3
- package/template/wall-e/skills/_bundled/slack-mentions/.watched-threads.json +0 -18
package/README.md
CHANGED
|
@@ -8,12 +8,12 @@ Set up **CTM + Wall-E** in one command — a browser-based dashboard for AI codi
|
|
|
8
8
|
|
|
9
9
|
A web dashboard for running and managing AI coding sessions across multiple providers.
|
|
10
10
|
|
|
11
|
-
- **Terminal Multiplexer** — Run Claude Code, Codex, Gemini CLI, and
|
|
11
|
+
- **Terminal Multiplexer** — Run Claude Code, Codex, Gemini CLI, Aider, OpenCode, Cursor Agent, and more side by side with live status, persistent scrollback, model switching, and AI-generated titles
|
|
12
12
|
- **Prompt Editor** — Save, version, and organize prompts with folders, tags, chains, templates, and AI search
|
|
13
13
|
- **Task Queue** — Queue prompts for sequential execution with auto-advance when the agent finishes, or step through manually
|
|
14
14
|
- **Approval Workflows** — Auto-approve tool-use requests based on learned rules; uncertain cases escalate to you
|
|
15
15
|
- **Code Review** — View git diffs from any project, staged or unstaged, with line-level detail
|
|
16
|
-
- **Model Registry** — Manage providers (Anthropic, OpenAI, Google, Ollama), compare pricing, switch models per session
|
|
16
|
+
- **Model Registry** — Manage providers (Anthropic, OpenAI, Google, DeepSeek, Ollama, LM Studio, MLX, and CLI subscription providers), compare pricing, switch models per session
|
|
17
17
|
- **Session Insights** — Analyze patterns across sessions to optimize prompts and workflows
|
|
18
18
|
|
|
19
19
|
### Wall-E (Personal Digital Twin)
|
|
@@ -24,7 +24,7 @@ An always-on AI agent that learns from your Slack, email, calendar, and coding s
|
|
|
24
24
|
- **Proactive Intelligence** — Surfaces time-sensitive items, suggests actions, and delivers morning briefings and weekly reflections without being asked
|
|
25
25
|
- **Chat with Tools** — Talk to Wall-E in the browser — it can search your memories, look up people, run skills, and call external tools via MCP (Slack, Glean, etc.)
|
|
26
26
|
- **19 Bundled Skills** — Morning briefing, weekly reflection, proactive alerts, Slack monitoring, email sync, calendar integration, coding agent, model training, model pricing sync, and more
|
|
27
|
-
- **Multi-Model** — Works with Claude, GPT, Gemini, and local models via Ollama with smart routing
|
|
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
|
|
29
29
|
- **Multi-Device** — Share your brain across machines via Dropbox or iCloud
|
|
30
30
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-walle",
|
|
3
|
-
"version": "0.9.
|
|
4
|
-
"description": "CTM + Wall-E \u2014 AI coding dashboard and personal digital twin agent. Multi-agent terminal for Claude Code, Codex, Gemini
|
|
3
|
+
"version": "0.9.12",
|
|
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"
|
|
7
7
|
},
|
package/template/bin/dev.sh
CHANGED
|
@@ -53,6 +53,10 @@ elif [[ "$1" == "--refresh" ]]; then
|
|
|
53
53
|
echo "[dev] Snapshotting production databases to $DEV_DIR ..."
|
|
54
54
|
node "$ROOT/bin/sqlite-snapshot.js" "$PROD_CTM_DIR/task-manager.db" "$DEV_DIR/task-manager.db" "CTM"
|
|
55
55
|
node "$ROOT/bin/sqlite-snapshot.js" "$PROD_WALLE_DIR/wall-e-brain.db" "$DEV_DIR/wall-e-brain.db" "Brain"
|
|
56
|
+
if [[ -d "$PROD_CTM_DIR/images" ]]; then
|
|
57
|
+
echo " Images: syncing $PROD_CTM_DIR/images -> $DEV_DIR/images"
|
|
58
|
+
node "$ROOT/bin/sync-images.js" "$PROD_CTM_DIR/images" "$DEV_DIR/images"
|
|
59
|
+
fi
|
|
56
60
|
# Ensure the dev instance owns its WAL files from first open.
|
|
57
61
|
rm -f "$DEV_DIR"/*.db-wal "$DEV_DIR"/*.db-shm
|
|
58
62
|
fi
|
|
@@ -72,6 +76,8 @@ export CTM_PORT="$DEV_CTM_PORT"
|
|
|
72
76
|
export WALL_E_PORT="$DEV_WALLE_PORT"
|
|
73
77
|
export CTM_DATA_DIR="$DEV_DIR"
|
|
74
78
|
export WALL_E_DATA_DIR="$DEV_DIR"
|
|
79
|
+
export WALLE_SESSIONS_DIR="$DEV_DIR/sessions"
|
|
80
|
+
export WALL_E_SESSIONS_DIR="$DEV_DIR/sessions"
|
|
75
81
|
export CTM_HOST="127.0.0.1"
|
|
76
82
|
export CTM_INSTANCE_TAG="dev-$DEV_CTM_PORT"
|
|
77
83
|
export OAUTH_PROXY_PORT="$DEV_OAUTH_PROXY_PORT"
|
|
@@ -79,7 +85,7 @@ export OAUTH_PROXY_PORT="$DEV_OAUTH_PROXY_PORT"
|
|
|
79
85
|
# Source the rest of .env (API keys, owner name, etc.)
|
|
80
86
|
if [[ -f "$ROOT/.env" ]]; then
|
|
81
87
|
set -a
|
|
82
|
-
source <(grep -v '^#' "$ROOT/.env" | grep -vE '^(CTM_PORT|WALL_E_PORT|CTM_DATA_DIR|WALL_E_DATA_DIR|CTM_HOST|CTM_INSTANCE_TAG|OAUTH_PROXY_PORT)=' | grep '=')
|
|
88
|
+
source <(grep -v '^#' "$ROOT/.env" | grep -vE '^(CTM_PORT|WALL_E_PORT|CTM_DATA_DIR|WALL_E_DATA_DIR|WALLE_SESSIONS_DIR|WALL_E_SESSIONS_DIR|CTM_HOST|CTM_INSTANCE_TAG|OAUTH_PROXY_PORT)=' | grep '=')
|
|
83
89
|
set +a
|
|
84
90
|
fi
|
|
85
91
|
|
package/template/bin/setup.js
CHANGED
|
@@ -8,6 +8,9 @@ const { execFileSync } = require('child_process');
|
|
|
8
8
|
const fs = require('fs');
|
|
9
9
|
const path = require('path');
|
|
10
10
|
const { atomicWriteFileSync } = require('../claude-task-manager/atomic-write');
|
|
11
|
+
const {
|
|
12
|
+
setupProviderHasRuntimeAccess,
|
|
13
|
+
} = require('../claude-task-manager/lib/setup-provider-config');
|
|
11
14
|
|
|
12
15
|
const ROOT = path.resolve(__dirname, '..');
|
|
13
16
|
const ENV_PATH = path.join(ROOT, '.env');
|
|
@@ -74,19 +77,60 @@ function runIfNeeded() {
|
|
|
74
77
|
|
|
75
78
|
/** Returns true if setup is needed (no API key configured anywhere). Cached after first check. */
|
|
76
79
|
let _needsSetupCache = undefined;
|
|
80
|
+
|
|
81
|
+
function parseEnvFile(content) {
|
|
82
|
+
const out = {};
|
|
83
|
+
for (const line of String(content || '').split('\n')) {
|
|
84
|
+
const m = line.match(/^\s*([A-Z0-9_]+)\s*=\s*(.*)\s*$/);
|
|
85
|
+
if (!m) continue;
|
|
86
|
+
out[m[1]] = m[2].replace(/^['"]|['"]$/g, '');
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function readEnvFile() {
|
|
92
|
+
if (!fs.existsSync(ENV_PATH)) return {};
|
|
93
|
+
try { return parseEnvFile(fs.readFileSync(ENV_PATH, 'utf8')); }
|
|
94
|
+
catch { return {}; }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function readBrainProviderState(env) {
|
|
98
|
+
try {
|
|
99
|
+
const brain = require('../wall-e/brain');
|
|
100
|
+
if (typeof brain.getDb === 'function') brain.getDb();
|
|
101
|
+
const provider = brain.getKv?.('walle_provider') || env.WALLE_PROVIDER || 'anthropic';
|
|
102
|
+
const row = brain.getDb().prepare(
|
|
103
|
+
'SELECT api_key_encrypted, auth_method FROM model_providers WHERE type = ? AND enabled = 1 ORDER BY updated_at DESC LIMIT 1'
|
|
104
|
+
).get(provider);
|
|
105
|
+
return {
|
|
106
|
+
provider,
|
|
107
|
+
authMethod: row?.auth_method || brain.getProviderAuthMethod?.(provider) || env.WALLE_AUTH_METHOD || '',
|
|
108
|
+
hasStoredKey: !!row?.api_key_encrypted,
|
|
109
|
+
};
|
|
110
|
+
} catch {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
77
115
|
function needsSetup() {
|
|
78
116
|
if (_needsSetupCache !== undefined) return _needsSetupCache;
|
|
79
|
-
// Check process.env first (may be set via Portkey, environment, etc.)
|
|
80
|
-
if (process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_BASE_URL) {
|
|
81
|
-
_needsSetupCache = false;
|
|
82
|
-
return false;
|
|
83
|
-
}
|
|
84
117
|
if (!fs.existsSync(ENV_PATH)) {
|
|
85
|
-
|
|
86
|
-
|
|
118
|
+
const provider = process.env.WALLE_PROVIDER || 'anthropic';
|
|
119
|
+
_needsSetupCache = !setupProviderHasRuntimeAccess({
|
|
120
|
+
type: provider,
|
|
121
|
+
env: process.env,
|
|
122
|
+
authMethod: process.env.WALLE_AUTH_METHOD || '',
|
|
123
|
+
hasStoredKey: false,
|
|
124
|
+
});
|
|
125
|
+
return _needsSetupCache;
|
|
87
126
|
}
|
|
88
|
-
const
|
|
89
|
-
|
|
127
|
+
const fileEnv = readEnvFile();
|
|
128
|
+
const env = { ...fileEnv, ...process.env };
|
|
129
|
+
const dbState = readBrainProviderState(env);
|
|
130
|
+
const provider = dbState?.provider || env.WALLE_PROVIDER || 'anthropic';
|
|
131
|
+
const authMethod = dbState?.authMethod || env.WALLE_AUTH_METHOD || '';
|
|
132
|
+
const hasStoredKey = !!dbState?.hasStoredKey;
|
|
133
|
+
_needsSetupCache = !setupProviderHasRuntimeAccess({ type: provider, env, authMethod, hasStoredKey });
|
|
90
134
|
return _needsSetupCache;
|
|
91
135
|
}
|
|
92
136
|
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { pipeline } = require('stream/promises');
|
|
7
|
+
|
|
8
|
+
async function copyFileStream(src, dest) {
|
|
9
|
+
await fs.promises.mkdir(path.dirname(dest), { recursive: true });
|
|
10
|
+
await pipeline(fs.createReadStream(src), fs.createWriteStream(dest));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function main() {
|
|
14
|
+
const [srcDir, destDir] = process.argv.slice(2);
|
|
15
|
+
if (!srcDir || !destDir) {
|
|
16
|
+
console.error('usage: sync-images.js <srcDir> <destDir>');
|
|
17
|
+
process.exit(2);
|
|
18
|
+
}
|
|
19
|
+
if (!fs.existsSync(srcDir)) {
|
|
20
|
+
console.log(` Images: source missing (${srcDir}), skipped`);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
await fs.promises.rm(destDir, { recursive: true, force: true });
|
|
25
|
+
await fs.promises.mkdir(destDir, { recursive: true });
|
|
26
|
+
|
|
27
|
+
const entries = await fs.promises.readdir(srcDir, { withFileTypes: true });
|
|
28
|
+
let copied = 0;
|
|
29
|
+
let skipped = 0;
|
|
30
|
+
for (const entry of entries) {
|
|
31
|
+
if (!entry.isFile()) continue;
|
|
32
|
+
const src = path.join(srcDir, entry.name);
|
|
33
|
+
const dest = path.join(destDir, entry.name);
|
|
34
|
+
try {
|
|
35
|
+
await copyFileStream(src, dest);
|
|
36
|
+
const stat = await fs.promises.stat(dest);
|
|
37
|
+
if (stat.size === 0) throw new Error('copied zero-byte file');
|
|
38
|
+
copied += 1;
|
|
39
|
+
} catch (err) {
|
|
40
|
+
skipped += 1;
|
|
41
|
+
try { await fs.promises.unlink(dest); } catch {}
|
|
42
|
+
if (skipped <= 5) {
|
|
43
|
+
console.warn(` Images: skipped ${entry.name}: ${err.message}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const suffix = skipped > 5 ? ` (${skipped - 5} more skipped)` : '';
|
|
48
|
+
console.log(` Images: synced ${copied}, skipped ${skipped}${suffix}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
main().catch((err) => {
|
|
52
|
+
console.error(` Images: warning - image sync failed: ${err.message}`);
|
|
53
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Builder Journal
|
|
2
|
+
|
|
3
|
+
## 2026-04-29 — Surface Wall-E AI provider failures
|
|
4
|
+
|
|
5
|
+
- Added structured `AI_PROVIDER_ERROR` classification for provider failures, including rate limit, quota, auth, model unavailable, timeout, network, context-window, and unavailable-provider cases.
|
|
6
|
+
- Threaded provider error metadata through Wall-E chat, `/api/wall-e/chat` SSE/JSON responses, CTM Wall-E session WebSocket errors, JSONL error parts, and service alerts.
|
|
7
|
+
- Updated Wall-E chat UX with a persistent provider-failure banner, inline diagnostic message, setup action, dismiss control, and collapsible raw provider details.
|
|
8
|
+
- Updated CTM Wall-E session error notices to render the same provider diagnostic shape instead of collapsing it into a generic string.
|
|
9
|
+
- Added focused node tests for provider classification and chat alerting, plus a Playwright rendering scenario for mocked provider 429 errors.
|
|
10
|
+
|
|
11
|
+
## 2026-04-29 — Make default-provider switching feel instant
|
|
12
|
+
|
|
13
|
+
- Found the slow path: CTM wrote the new default, then restarted Wall-E so its cached default client would reload, while the UI waited for the whole request before changing the star.
|
|
14
|
+
- Changed `/api/setup/set-default` to call Wall-E's live setup config endpoint and only schedule a Wall-E restart if that live update fails.
|
|
15
|
+
- Made setup default stars optimistic with rollback on failure, while keeping the selected star visually selected during the in-flight save.
|
|
16
|
+
- Raised contrast on provider card borders, default stars, and toggle tracks so the inline styles no longer wash out the controls.
|
|
17
|
+
- Added a Playwright setup-card regression for optimistic default switching and computed control colors.
|
|
@@ -7,6 +7,7 @@ const queueEngine = require('./queue-engine');
|
|
|
7
7
|
const harvest = require('./prompt-harvest');
|
|
8
8
|
const permissionSync = require('./lib/permission-sync');
|
|
9
9
|
const walleClient = require('./lib/walle-client');
|
|
10
|
+
const claudeDesktopSessions = require('./lib/claude-desktop-sessions');
|
|
10
11
|
// AI search uses direct HTTP calls to Claude API (supports Portkey proxy)
|
|
11
12
|
|
|
12
13
|
// Embed a prompt (async, fire-and-forget)
|
|
@@ -593,26 +594,80 @@ function handleGetImage(req, res, url) {
|
|
|
593
594
|
}
|
|
594
595
|
|
|
595
596
|
function handleServeImage(req, res, url) {
|
|
596
|
-
|
|
597
|
+
let rawFilename = url.pathname.replace('/api/images/file/', '');
|
|
598
|
+
try { rawFilename = decodeURIComponent(rawFilename); } catch {}
|
|
597
599
|
const safeFilename = path.basename(rawFilename);
|
|
600
|
+
const isUsableImageFile = (p) => {
|
|
601
|
+
try { return fs.existsSync(p) && fs.statSync(p).isFile() && fs.statSync(p).size > 0; }
|
|
602
|
+
catch { return false; }
|
|
603
|
+
};
|
|
604
|
+
const canReadImageFile = (p) => {
|
|
605
|
+
let fd = null;
|
|
606
|
+
try {
|
|
607
|
+
fd = fs.openSync(p, 'r');
|
|
608
|
+
const probe = Buffer.alloc(1);
|
|
609
|
+
return fs.readSync(fd, probe, 0, 1, 0) > 0;
|
|
610
|
+
} catch {
|
|
611
|
+
return false;
|
|
612
|
+
} finally {
|
|
613
|
+
if (fd !== null) { try { fs.closeSync(fd); } catch {} }
|
|
614
|
+
}
|
|
615
|
+
};
|
|
616
|
+
const serveImagePlaceholder = () => {
|
|
617
|
+
const svg = Buffer.from(
|
|
618
|
+
'<svg xmlns="http://www.w3.org/2000/svg" width="320" height="180" viewBox="0 0 320 180">' +
|
|
619
|
+
'<rect width="320" height="180" rx="8" fill="#1f2335"/>' +
|
|
620
|
+
'<text x="160" y="92" text-anchor="middle" font-family="sans-serif" font-size="14" fill="#7aa2f7">Image unavailable</text>' +
|
|
621
|
+
'</svg>'
|
|
622
|
+
);
|
|
623
|
+
res.writeHead(200, {
|
|
624
|
+
'Content-Type': 'image/svg+xml',
|
|
625
|
+
'Content-Length': svg.length,
|
|
626
|
+
'Cache-Control': 'no-store',
|
|
627
|
+
});
|
|
628
|
+
res.end(svg);
|
|
629
|
+
};
|
|
630
|
+
const mimeMap = { '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.webp': 'image/webp', '.svg': 'image/svg+xml' };
|
|
631
|
+
const requestedExt = path.extname(safeFilename).toLowerCase();
|
|
632
|
+
if (!mimeMap[requestedExt]) {
|
|
633
|
+
serveImagePlaceholder(); return;
|
|
634
|
+
}
|
|
598
635
|
let filePath = path.join(db.DEFAULT_IMAGES_DIR, safeFilename);
|
|
599
|
-
if (!
|
|
600
|
-
// Filename
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
636
|
+
if (!isUsableImageFile(filePath)) {
|
|
637
|
+
// Filename may be either the original upload name or the stored hash
|
|
638
|
+
// basename embedded in markdown/session history. Resolve only paths that
|
|
639
|
+
// came from the images table and whose basename matches the request.
|
|
640
|
+
const imgs = db.getDb().prepare(`
|
|
641
|
+
SELECT file_path FROM images
|
|
642
|
+
WHERE filename = ? OR file_path LIKE ? OR file_path LIKE ?
|
|
643
|
+
LIMIT 5
|
|
644
|
+
`).all(safeFilename, '%/' + safeFilename, '%\\' + safeFilename);
|
|
645
|
+
for (const img of imgs) {
|
|
646
|
+
const resolvedImgPath = img && img.file_path ? path.resolve(img.file_path) : null;
|
|
647
|
+
if (!resolvedImgPath || path.basename(resolvedImgPath) !== safeFilename) continue;
|
|
648
|
+
if (isUsableImageFile(resolvedImgPath)) {
|
|
649
|
+
filePath = resolvedImgPath;
|
|
650
|
+
break;
|
|
651
|
+
}
|
|
607
652
|
}
|
|
608
653
|
}
|
|
654
|
+
if (!isUsableImageFile(filePath)) {
|
|
655
|
+
serveImagePlaceholder(); return;
|
|
656
|
+
}
|
|
657
|
+
if (!canReadImageFile(filePath)) {
|
|
658
|
+
serveImagePlaceholder(); return;
|
|
659
|
+
}
|
|
609
660
|
const ext = path.extname(filePath).toLowerCase();
|
|
610
|
-
const mimeMap = { '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.webp': 'image/webp', '.svg': 'image/svg+xml' };
|
|
611
661
|
res.writeHead(200, {
|
|
612
|
-
'Content-Type': mimeMap[ext]
|
|
662
|
+
'Content-Type': mimeMap[ext],
|
|
613
663
|
'Cache-Control': 'public, max-age=86400'
|
|
614
664
|
});
|
|
615
|
-
fs.createReadStream(filePath)
|
|
665
|
+
const stream = fs.createReadStream(filePath);
|
|
666
|
+
stream.on('error', () => {
|
|
667
|
+
if (!res.headersSent) serveImagePlaceholder();
|
|
668
|
+
else res.destroy();
|
|
669
|
+
});
|
|
670
|
+
stream.pipe(res);
|
|
616
671
|
}
|
|
617
672
|
|
|
618
673
|
async function handleUpdateAnnotations(req, res, url) {
|
|
@@ -977,6 +1032,36 @@ async function _importCompactPair(parsed, jsonlPath, bakPath, jsonlSize, bakSize
|
|
|
977
1032
|
|
|
978
1033
|
async function importSessionFile(filePath, projectPath, projectEntry) {
|
|
979
1034
|
const parsed = parseSessionFile(filePath, projectPath, projectEntry);
|
|
1035
|
+
if (parsed.agent === claudeDesktopSessions.DESKTOP_AGENT) {
|
|
1036
|
+
const messages = claudeDesktopSessions.getMessages(parsed.sessionId) || [];
|
|
1037
|
+
if (messages.length === 0) return false;
|
|
1038
|
+
const userMessages = messages.filter(m => m.role === 'user');
|
|
1039
|
+
const assistantMessages = messages.filter(m => m.role === 'assistant');
|
|
1040
|
+
const firstUser = userMessages[0]?.text || parsed.firstMessage || '';
|
|
1041
|
+
const lastUser = userMessages[userMessages.length - 1]?.text || parsed.lastUserContent || firstUser;
|
|
1042
|
+
const firstAssistant = assistantMessages[0]?.text || parsed.firstAssistantText || '';
|
|
1043
|
+
const existing = db.getSessionConversation(parsed.sessionId);
|
|
1044
|
+
if (existing && existing.file_size === parsed.fileSize && existing.model_provider) return false;
|
|
1045
|
+
db.importSessionConversation({
|
|
1046
|
+
session_id: parsed.sessionId,
|
|
1047
|
+
project_path: parsed.project,
|
|
1048
|
+
messages,
|
|
1049
|
+
user_msg_count: userMessages.length,
|
|
1050
|
+
assistant_msg_count: assistantMessages.length,
|
|
1051
|
+
title: parsed.title || (existing && existing.title) || '',
|
|
1052
|
+
first_message: firstUser,
|
|
1053
|
+
last_user_content: lastUser,
|
|
1054
|
+
first_assistant_text: firstAssistant,
|
|
1055
|
+
rename_name: '',
|
|
1056
|
+
git_branch: '',
|
|
1057
|
+
file_size: parsed.fileSize,
|
|
1058
|
+
session_created_at: parsed.timestamp,
|
|
1059
|
+
hostname: parsed.hostname,
|
|
1060
|
+
model_provider: parsed.modelProvider || (existing && existing.model_provider) || '',
|
|
1061
|
+
model_id: parsed.modelId || (existing && existing.model_id) || '',
|
|
1062
|
+
});
|
|
1063
|
+
return true;
|
|
1064
|
+
}
|
|
980
1065
|
if (parsed.isEmpty) return false;
|
|
981
1066
|
|
|
982
1067
|
// Compact-pair detection: Claude Code's /compact rotates <id>.jsonl →
|
|
@@ -1108,7 +1193,7 @@ async function runIncrementalConversationImport() {
|
|
|
1108
1193
|
// several imports in the same event-loop turn.
|
|
1109
1194
|
for (const { filePath, projectPath, projectEntry } of allFiles) {
|
|
1110
1195
|
try {
|
|
1111
|
-
const stat = await fsp.stat(filePath);
|
|
1196
|
+
const stat = await fsp.stat(claudeDesktopSessions.sourcePathForStat(filePath));
|
|
1112
1197
|
if (stat.mtimeMs <= lastScanAt) continue;
|
|
1113
1198
|
scanned++;
|
|
1114
1199
|
if (await importSessionFile(filePath, projectPath, projectEntry)) imported++;
|
|
@@ -4,6 +4,10 @@ const fs = require('fs');
|
|
|
4
4
|
const db = require('./db');
|
|
5
5
|
const gitUtils = require('./git-utils');
|
|
6
6
|
|
|
7
|
+
const REVIEW_SEVERITIES = new Set(['comment', 'suggestion', 'issue', 'nit', 'question']);
|
|
8
|
+
const REVIEW_STATUSES = new Set(['open', 'resolved']);
|
|
9
|
+
const REVIEW_SIDES = new Set(['old', 'new']);
|
|
10
|
+
|
|
7
11
|
// --- Helpers ---
|
|
8
12
|
function isValidProjectPath(p) {
|
|
9
13
|
if (!p || typeof p !== 'string') return false;
|
|
@@ -36,6 +40,69 @@ function readBody(req, limit = 1024 * 1024) {
|
|
|
36
40
|
});
|
|
37
41
|
}
|
|
38
42
|
|
|
43
|
+
function normalizeReviewSeverity(value) {
|
|
44
|
+
value = String(value || 'comment').trim().toLowerCase();
|
|
45
|
+
return REVIEW_SEVERITIES.has(value) ? value : 'comment';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function normalizeReviewStatus(value) {
|
|
49
|
+
value = String(value || 'open').trim().toLowerCase();
|
|
50
|
+
if (!REVIEW_STATUSES.has(value)) throw new Error('Invalid review comment status');
|
|
51
|
+
return value;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function normalizeReviewSide(value) {
|
|
55
|
+
value = String(value || 'new').trim().toLowerCase();
|
|
56
|
+
return REVIEW_SIDES.has(value) ? value : 'new';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function positiveLineNumber(value, fieldName) {
|
|
60
|
+
const n = Number(value);
|
|
61
|
+
if (!Number.isInteger(n) || n < 1) throw new Error(`${fieldName} must be a positive integer`);
|
|
62
|
+
return n;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function sanitizeReviewCommentInput(reviewId, body) {
|
|
66
|
+
body = body || {};
|
|
67
|
+
const filePath = String(body.file_path || '').trim();
|
|
68
|
+
const text = String(body.body || '').trim();
|
|
69
|
+
if (!filePath) throw new Error('file_path required');
|
|
70
|
+
if (!text) throw new Error('body required');
|
|
71
|
+
if (filePath.length > 4096) throw new Error('file_path too long');
|
|
72
|
+
if (text.length > 20000) throw new Error('body too long');
|
|
73
|
+
const lineStart = positiveLineNumber(body.line_start, 'line_start');
|
|
74
|
+
let lineEnd = body.line_end == null || body.line_end === '' ? lineStart : positiveLineNumber(body.line_end, 'line_end');
|
|
75
|
+
if (lineEnd < lineStart) lineEnd = lineStart;
|
|
76
|
+
return {
|
|
77
|
+
review_id: reviewId,
|
|
78
|
+
file_path: filePath,
|
|
79
|
+
line_start: lineStart,
|
|
80
|
+
line_end: lineEnd,
|
|
81
|
+
side: normalizeReviewSide(body.side),
|
|
82
|
+
body: text,
|
|
83
|
+
severity: normalizeReviewSeverity(body.severity),
|
|
84
|
+
ai_generated: !!body.ai_generated,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function sanitizeReviewCommentUpdate(body) {
|
|
89
|
+
body = body || {};
|
|
90
|
+
const update = {};
|
|
91
|
+
if (Object.prototype.hasOwnProperty.call(body, 'body')) {
|
|
92
|
+
const text = String(body.body || '').trim();
|
|
93
|
+
if (!text) throw new Error('body required');
|
|
94
|
+
if (text.length > 20000) throw new Error('body too long');
|
|
95
|
+
update.body = text;
|
|
96
|
+
}
|
|
97
|
+
if (Object.prototype.hasOwnProperty.call(body, 'severity')) {
|
|
98
|
+
update.severity = normalizeReviewSeverity(body.severity);
|
|
99
|
+
}
|
|
100
|
+
if (Object.prototype.hasOwnProperty.call(body, 'status')) {
|
|
101
|
+
update.status = normalizeReviewStatus(body.status);
|
|
102
|
+
}
|
|
103
|
+
return update;
|
|
104
|
+
}
|
|
105
|
+
|
|
39
106
|
// --- Change Detection for Badge Polling ---
|
|
40
107
|
// Track last known diff stats per project to detect changes
|
|
41
108
|
const changeTracker = new Map(); // projectPath -> { files: [...], hash: string }
|
|
@@ -218,8 +285,9 @@ function handleReviewApi(req, res, url) {
|
|
|
218
285
|
const commentsMatch = p.match(/^\/api\/reviews\/(\d+)\/comments$/);
|
|
219
286
|
if (commentsMatch && m === 'POST') {
|
|
220
287
|
readBody(req).then(body => {
|
|
221
|
-
|
|
222
|
-
|
|
288
|
+
const reviewId = parseInt(commentsMatch[1], 10);
|
|
289
|
+
if (!db.getReview(reviewId)) return jsonResponse(res, 404, { error: 'Review not found' });
|
|
290
|
+
const id = db.addReviewComment(sanitizeReviewCommentInput(reviewId, body));
|
|
223
291
|
jsonResponse(res, 201, { id });
|
|
224
292
|
}).catch(e => jsonResponse(res, 400, { error: e.message }));
|
|
225
293
|
return true;
|
|
@@ -229,7 +297,7 @@ function handleReviewApi(req, res, url) {
|
|
|
229
297
|
const commentMatch = p.match(/^\/api\/review-comments\/(\d+)$/);
|
|
230
298
|
if (commentMatch && m === 'PUT') {
|
|
231
299
|
readBody(req).then(body => {
|
|
232
|
-
db.updateReviewComment(parseInt(commentMatch[1]), body);
|
|
300
|
+
db.updateReviewComment(parseInt(commentMatch[1], 10), sanitizeReviewCommentUpdate(body));
|
|
233
301
|
jsonResponse(res, 200, { ok: true });
|
|
234
302
|
}).catch(e => jsonResponse(res, 400, { error: e.message }));
|
|
235
303
|
return true;
|
|
@@ -272,7 +340,8 @@ function composeReviewPrompt(review) {
|
|
|
272
340
|
const lineRef = c.line_end && c.line_end !== c.line_start
|
|
273
341
|
? `Lines ${c.line_start}-${c.line_end}`
|
|
274
342
|
: `Line ${c.line_start}`;
|
|
275
|
-
const
|
|
343
|
+
const normalizedSeverity = normalizeReviewSeverity(c.severity);
|
|
344
|
+
const severity = normalizedSeverity !== 'comment' ? ` [${normalizedSeverity}]` : '';
|
|
276
345
|
lines.push(`**${lineRef}**${severity}: ${c.body}\n`);
|
|
277
346
|
}
|
|
278
347
|
}
|
|
@@ -284,4 +353,12 @@ function composeReviewPrompt(review) {
|
|
|
284
353
|
return lines.join('\n');
|
|
285
354
|
}
|
|
286
355
|
|
|
287
|
-
module.exports = {
|
|
356
|
+
module.exports = {
|
|
357
|
+
handleReviewApi,
|
|
358
|
+
checkForChanges,
|
|
359
|
+
composeReviewPrompt,
|
|
360
|
+
_normalizeReviewSeverity: normalizeReviewSeverity,
|
|
361
|
+
_normalizeReviewStatus: normalizeReviewStatus,
|
|
362
|
+
_sanitizeReviewCommentInput: sanitizeReviewCommentInput,
|
|
363
|
+
_sanitizeReviewCommentUpdate: sanitizeReviewCommentUpdate,
|
|
364
|
+
};
|
|
@@ -2593,11 +2593,30 @@ function deleteReview(id) {
|
|
|
2593
2593
|
getDb().prepare('DELETE FROM code_reviews WHERE id = ?').run(id);
|
|
2594
2594
|
}
|
|
2595
2595
|
|
|
2596
|
+
const REVIEW_COMMENT_SEVERITIES = new Set(['comment', 'suggestion', 'issue', 'nit', 'question']);
|
|
2597
|
+
const REVIEW_COMMENT_STATUSES = new Set(['open', 'resolved']);
|
|
2598
|
+
const REVIEW_COMMENT_SIDES = new Set(['old', 'new']);
|
|
2599
|
+
|
|
2600
|
+
function normalizeReviewCommentSeverity(severity) {
|
|
2601
|
+
severity = String(severity || 'comment').trim().toLowerCase();
|
|
2602
|
+
return REVIEW_COMMENT_SEVERITIES.has(severity) ? severity : 'comment';
|
|
2603
|
+
}
|
|
2604
|
+
|
|
2605
|
+
function normalizeReviewCommentStatus(status) {
|
|
2606
|
+
status = String(status || 'open').trim().toLowerCase();
|
|
2607
|
+
return REVIEW_COMMENT_STATUSES.has(status) ? status : 'open';
|
|
2608
|
+
}
|
|
2609
|
+
|
|
2610
|
+
function normalizeReviewCommentSide(side) {
|
|
2611
|
+
side = String(side || 'new').trim().toLowerCase();
|
|
2612
|
+
return REVIEW_COMMENT_SIDES.has(side) ? side : 'new';
|
|
2613
|
+
}
|
|
2614
|
+
|
|
2596
2615
|
function addReviewComment({ review_id, file_path, line_start, line_end, side, body, severity, ai_generated }) {
|
|
2597
2616
|
const result = getDb().prepare(
|
|
2598
2617
|
`INSERT INTO review_comments (review_id, file_path, line_start, line_end, side, body, severity, ai_generated)
|
|
2599
2618
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
|
2600
|
-
).run(review_id, file_path, line_start, line_end || line_start, side
|
|
2619
|
+
).run(review_id, file_path, line_start, line_end || line_start, normalizeReviewCommentSide(side), body, normalizeReviewCommentSeverity(severity), ai_generated ? 1 : 0);
|
|
2601
2620
|
// Update comment count
|
|
2602
2621
|
const count = getDb().prepare('SELECT COUNT(*) as n FROM review_comments WHERE review_id = ?').get(review_id);
|
|
2603
2622
|
getDb().prepare('UPDATE code_reviews SET comment_count = ? WHERE id = ?').run(count.n, review_id);
|
|
@@ -2608,8 +2627,8 @@ function updateReviewComment(id, { body, severity, status }) {
|
|
|
2608
2627
|
const sets = [];
|
|
2609
2628
|
const params = [];
|
|
2610
2629
|
if (body !== undefined) { sets.push('body = ?'); params.push(body); }
|
|
2611
|
-
if (severity !== undefined) { sets.push('severity = ?'); params.push(severity); }
|
|
2612
|
-
if (status !== undefined) { sets.push('status = ?'); params.push(status); }
|
|
2630
|
+
if (severity !== undefined) { sets.push('severity = ?'); params.push(normalizeReviewCommentSeverity(severity)); }
|
|
2631
|
+
if (status !== undefined) { sets.push('status = ?'); params.push(normalizeReviewCommentStatus(status)); }
|
|
2613
2632
|
if (sets.length === 0) return;
|
|
2614
2633
|
params.push(id);
|
|
2615
2634
|
getDb().prepare(`UPDATE review_comments SET ${sets.join(', ')} WHERE id = ?`).run(...params);
|
|
@@ -3513,6 +3532,13 @@ function resolveSession(id) {
|
|
|
3513
3532
|
return null;
|
|
3514
3533
|
}
|
|
3515
3534
|
|
|
3535
|
+
function normalizeDbCreatedAt(value) {
|
|
3536
|
+
if (!value) return '';
|
|
3537
|
+
const ms = new Date(value).getTime();
|
|
3538
|
+
if (!Number.isFinite(ms)) return '';
|
|
3539
|
+
return new Date(ms).toISOString().replace('T', ' ').replace(/\.\d+Z$/, '');
|
|
3540
|
+
}
|
|
3541
|
+
|
|
3516
3542
|
/**
|
|
3517
3543
|
* Merge a ctm_sessions row + agent_sessions row into a unified object
|
|
3518
3544
|
* that matches the old sessions table shape (for backward compatibility).
|
|
@@ -3594,6 +3620,7 @@ function upsertSession(id, data, opts) {
|
|
|
3594
3620
|
title: data.title || '',
|
|
3595
3621
|
user_renamed: data.userRenamed ? 1 : 0,
|
|
3596
3622
|
starred: data.starred ? 1 : 0,
|
|
3623
|
+
created_at: normalizeDbCreatedAt(data.createdAt),
|
|
3597
3624
|
};
|
|
3598
3625
|
const agentParams = (agentId && agentId !== '__CLEAR__') ? {
|
|
3599
3626
|
agent_session_id: agentId,
|
|
@@ -3615,8 +3642,8 @@ function upsertSession(id, data, opts) {
|
|
|
3615
3642
|
const txn = d.transaction(() => {
|
|
3616
3643
|
// Upsert ctm_sessions
|
|
3617
3644
|
d.prepare(`
|
|
3618
|
-
INSERT INTO ctm_sessions (id, provider, project_path, cwd, title, user_renamed, starred)
|
|
3619
|
-
VALUES (@id, @provider, @project_path, @cwd, @title, @user_renamed, @starred)
|
|
3645
|
+
INSERT INTO ctm_sessions (id, provider, project_path, cwd, title, user_renamed, starred, created_at)
|
|
3646
|
+
VALUES (@id, @provider, @project_path, @cwd, @title, @user_renamed, @starred, COALESCE(NULLIF(@created_at, ''), datetime('now')))
|
|
3620
3647
|
ON CONFLICT(id) DO UPDATE SET
|
|
3621
3648
|
provider = COALESCE(NULLIF(excluded.provider, ''), ctm_sessions.provider),
|
|
3622
3649
|
project_path = COALESCE(NULLIF(excluded.project_path, ''), ctm_sessions.project_path),
|