create-walle 0.9.13 → 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.
Files changed (98) hide show
  1. package/README.md +8 -3
  2. package/bin/create-walle.js +232 -32
  3. package/bin/mcp-inject.js +18 -53
  4. package/package.json +3 -1
  5. package/template/claude-task-manager/api-prompts.js +11 -2
  6. package/template/claude-task-manager/approval-agent.js +7 -0
  7. package/template/claude-task-manager/db.js +94 -75
  8. package/template/claude-task-manager/docs/session-standup-command-center-design.md +242 -0
  9. package/template/claude-task-manager/docs/session-tooltip-freshness-design.md +224 -0
  10. package/template/claude-task-manager/docs/session-ux-issue-review-2026-05-01.md +369 -0
  11. package/template/claude-task-manager/fuzzy-utils.js +10 -2
  12. package/template/claude-task-manager/git-utils.js +140 -10
  13. package/template/claude-task-manager/lib/agent-capabilities.js +1 -1
  14. package/template/claude-task-manager/lib/agent-presets.js +38 -5
  15. package/template/claude-task-manager/lib/codex-terminal-final.js +53 -0
  16. package/template/claude-task-manager/lib/ctm-session-context-api.js +222 -0
  17. package/template/claude-task-manager/lib/session-diagnostics.js +56 -0
  18. package/template/claude-task-manager/lib/session-history.js +309 -16
  19. package/template/claude-task-manager/lib/session-standup.js +409 -0
  20. package/template/claude-task-manager/lib/session-stream.js +253 -20
  21. package/template/claude-task-manager/lib/standup-attention.js +200 -0
  22. package/template/claude-task-manager/lib/status-hooks.js +8 -2
  23. package/template/claude-task-manager/lib/update-telemetry.js +114 -0
  24. package/template/claude-task-manager/lib/walle-ctm-history.js +49 -6
  25. package/template/claude-task-manager/lib/walle-default-model.js +55 -0
  26. package/template/claude-task-manager/lib/walle-mcp-auto-config.js +66 -0
  27. package/template/claude-task-manager/lib/walle-supervisor.js +86 -19
  28. package/template/claude-task-manager/lib/walle-transcript.js +1 -3
  29. package/template/claude-task-manager/lib/worktree-cwd.js +82 -0
  30. package/template/claude-task-manager/package.json +1 -0
  31. package/template/claude-task-manager/providers/codex-mcp.js +104 -0
  32. package/template/claude-task-manager/providers/index.js +2 -0
  33. package/template/claude-task-manager/public/css/setup.css +2 -1
  34. package/template/claude-task-manager/public/css/walle.css +71 -0
  35. package/template/claude-task-manager/public/index.html +2388 -429
  36. package/template/claude-task-manager/public/js/message-renderer.js +314 -35
  37. package/template/claude-task-manager/public/js/session-search-utils.js +185 -3
  38. package/template/claude-task-manager/public/js/session-status-precedence.js +125 -0
  39. package/template/claude-task-manager/public/js/setup.js +62 -19
  40. package/template/claude-task-manager/public/js/stream-view.js +396 -55
  41. package/template/claude-task-manager/public/js/terminal-restore-state.js +57 -0
  42. package/template/claude-task-manager/public/js/walle-session.js +234 -26
  43. package/template/claude-task-manager/public/js/walle.js +143 -2
  44. package/template/claude-task-manager/server.js +1402 -433
  45. package/template/claude-task-manager/session-integrity.js +77 -28
  46. package/template/claude-task-manager/workers/approval-widget-validator.js +15 -5
  47. package/template/claude-task-manager/workers/scrollback-worker.js +5 -6
  48. package/template/claude-task-manager/workers/state-detectors/codex.js +6 -0
  49. package/template/package.json +1 -1
  50. package/template/wall-e/agent-runners/claude-code.js +2 -0
  51. package/template/wall-e/agent.js +63 -8
  52. package/template/wall-e/api-walle.js +330 -52
  53. package/template/wall-e/brain.js +291 -42
  54. package/template/wall-e/chat.js +172 -15
  55. package/template/wall-e/coding/compaction-service.js +19 -5
  56. package/template/wall-e/coding/stream-processor.js +22 -2
  57. package/template/wall-e/coding/workspace-replay.js +1 -4
  58. package/template/wall-e/coding-orchestrator.js +250 -80
  59. package/template/wall-e/compat.js +0 -28
  60. package/template/wall-e/context/context-builder.js +3 -1
  61. package/template/wall-e/embeddings.js +2 -7
  62. package/template/wall-e/eval/agent-runner.js +30 -9
  63. package/template/wall-e/eval/benchmark-generator.js +21 -1
  64. package/template/wall-e/eval/benchmarks/chat-eval.json +66 -6
  65. package/template/wall-e/eval/benchmarks/coding-agent.json +0 -596
  66. package/template/wall-e/eval/cc-replay.js +1 -0
  67. package/template/wall-e/eval/codex-cli-baseline.js +633 -0
  68. package/template/wall-e/eval/debug-agent003.js +1 -0
  69. package/template/wall-e/eval/eval-orchestrator.js +3 -3
  70. package/template/wall-e/eval/run-agent-benchmarks.js +11 -3
  71. package/template/wall-e/eval/run-codex-cli-baseline.js +177 -0
  72. package/template/wall-e/eval/run-model-comparison.js +1 -0
  73. package/template/wall-e/eval/swebench-adapter.js +1 -0
  74. package/template/wall-e/evaluation/quorum-evaluator.js +0 -1
  75. package/template/wall-e/extraction/knowledge-extractor.js +1 -2
  76. package/template/wall-e/lib/mcp-integration.js +336 -0
  77. package/template/wall-e/llm/ollama.js +47 -8
  78. package/template/wall-e/llm/ollama.plugin.json +1 -1
  79. package/template/wall-e/llm/tool-adapter.js +1 -0
  80. package/template/wall-e/loops/ingest.js +42 -8
  81. package/template/wall-e/loops/initiative.js +87 -2
  82. package/template/wall-e/mcp-server.js +872 -19
  83. package/template/wall-e/memory/ctm-context-client.js +230 -0
  84. package/template/wall-e/memory/ctm-session-context.js +1376 -0
  85. package/template/wall-e/prompts/coding/memory-protocol.md +6 -0
  86. package/template/wall-e/server.js +30 -1
  87. package/template/wall-e/skills/_bundled/memory-search/SKILL.md +8 -0
  88. package/template/wall-e/skills/_bundled/scan-ctm-sessions/SKILL.md +20 -0
  89. package/template/wall-e/skills/_bundled/scan-ctm-sessions/run.js +43 -0
  90. package/template/wall-e/skills/_bundled/slack-mentions/run.js +471 -188
  91. package/template/wall-e/skills/skill-planner.js +86 -4
  92. package/template/wall-e/slack/socket-mode-listener.js +276 -0
  93. package/template/wall-e/telemetry.js +70 -2
  94. package/template/wall-e/tools/builtin-middleware.js +55 -2
  95. package/template/wall-e/tools/shell-policy.js +1 -1
  96. package/template/wall-e/tools/slack-owner.js +104 -0
  97. package/template/website/index.html +4 -4
  98. package/template/builder-journal.md +0 -17
@@ -431,6 +431,7 @@ function _detectUpgrade(previousSchemaVersion) {
431
431
  migrations_applied: Math.max(0, SCHEMA_VERSION - previousSchemaVersion),
432
432
  first_tracked: true,
433
433
  });
434
+ telemetry.trackFunnelStep?.('upgrade_completed');
434
435
  setKv('last_upgrade_at', new Date().toISOString());
435
436
  } else {
436
437
  setKv('first_installed_at', new Date().toISOString());
@@ -444,6 +445,7 @@ function _detectUpgrade(previousSchemaVersion) {
444
445
  schema_to: SCHEMA_VERSION,
445
446
  migrations_applied: SCHEMA_VERSION - previousSchemaVersion,
446
447
  });
448
+ telemetry.trackFunnelStep?.('upgrade_completed');
447
449
  setKv('installed_version', pkgVersion);
448
450
  setKv('last_upgrade_at', new Date().toISOString());
449
451
  }
@@ -495,6 +497,27 @@ function getDbPath() {
495
497
  return currentDbPath || DEFAULT_DB_PATH;
496
498
  }
497
499
 
500
+ const _VALID_CHECKPOINT_MODES = new Set(['PASSIVE', 'FULL', 'RESTART', 'TRUNCATE']);
501
+ function checkpointWalOrThrow(mode) {
502
+ const d = getDb();
503
+ const m = (mode || 'PASSIVE').toUpperCase();
504
+ if (!_VALID_CHECKPOINT_MODES.has(m)) throw new Error(`Invalid WAL checkpoint mode: ${mode}`);
505
+ const rows = d.pragma(`wal_checkpoint(${m})`);
506
+ const row = Array.isArray(rows) ? rows[0] : rows;
507
+ const busy = Number(row?.busy ?? row?.[0] ?? 0);
508
+ if (busy > 0) {
509
+ const log = row?.log ?? row?.[1] ?? 'unknown';
510
+ const checkpointed = row?.checkpointed ?? row?.[2] ?? 'unknown';
511
+ throw new Error(`WAL checkpoint ${m} could not complete: busy=${busy}, log=${log}, checkpointed=${checkpointed}`);
512
+ }
513
+ return rows;
514
+ }
515
+
516
+ function runImmediateTransaction(d, fn, ...args) {
517
+ const txn = d.transaction(fn);
518
+ return typeof txn.immediate === 'function' ? txn.immediate(...args) : txn(...args);
519
+ }
520
+
498
521
  function initDb(dbPath) {
499
522
  dbPath = dbPath || DEFAULT_DB_PATH;
500
523
  currentDbPath = dbPath;
@@ -936,6 +959,21 @@ function createTables() {
936
959
  );
937
960
  CREATE INDEX IF NOT EXISTS idx_slack_threads_active ON slack_threads(last_activity);
938
961
 
962
+ CREATE TABLE IF NOT EXISTS slack_inbound_events (
963
+ id TEXT PRIMARY KEY,
964
+ channel_id TEXT NOT NULL,
965
+ message_ts TEXT NOT NULL,
966
+ thread_ts TEXT,
967
+ source TEXT,
968
+ status TEXT DEFAULT 'claimed',
969
+ payload_hash TEXT,
970
+ error TEXT,
971
+ created_at TEXT DEFAULT (datetime('now')),
972
+ updated_at TEXT DEFAULT (datetime('now'))
973
+ );
974
+ CREATE INDEX IF NOT EXISTS idx_slack_inbound_events_updated ON slack_inbound_events(updated_at);
975
+ CREATE INDEX IF NOT EXISTS idx_slack_inbound_events_thread ON slack_inbound_events(channel_id, thread_ts);
976
+
939
977
  -- Model management tables
940
978
  CREATE TABLE IF NOT EXISTS model_providers (
941
979
  id TEXT PRIMARY KEY,
@@ -1397,11 +1435,27 @@ function listMemories({ source, since, extractionStatus, limit } = {}) {
1397
1435
  return getDb().prepare(sql).all(...params);
1398
1436
  }
1399
1437
 
1400
- function searchMemories({ query, limit } = {}) {
1438
+ function searchMemories({ query, limit, source, memory_type, since, until } = {}) {
1401
1439
  if (!query || !query.trim()) return [];
1402
1440
  const terms = query.trim().split(/\s+/).filter(Boolean);
1403
1441
  const conditions = terms.map(() => "content LIKE ? ESCAPE '\\'");
1404
1442
  const params = terms.map(t => '%' + t.replace(/[%_\\]/g, '\\$&') + '%');
1443
+ if (source) {
1444
+ conditions.push('source = ?');
1445
+ params.push(source);
1446
+ }
1447
+ if (memory_type) {
1448
+ conditions.push('memory_type = ?');
1449
+ params.push(memory_type);
1450
+ }
1451
+ if (since) {
1452
+ conditions.push('timestamp >= ?');
1453
+ params.push(since);
1454
+ }
1455
+ if (until) {
1456
+ conditions.push('timestamp <= ?');
1457
+ params.push(until);
1458
+ }
1405
1459
  const lim = Math.min(Math.max(limit || 50, 1), 200);
1406
1460
  params.push(lim);
1407
1461
  return getDb().prepare(
@@ -1633,7 +1687,7 @@ let backupIntervalId = null;
1633
1687
 
1634
1688
  function createBackup(label) {
1635
1689
  getDb(); // throws if not initialized
1636
- db.pragma('wal_checkpoint(TRUNCATE)');
1690
+ checkpointWalOrThrow('TRUNCATE');
1637
1691
 
1638
1692
  // Use backup dir relative to the current DB, not the module-level constant.
1639
1693
  // This prevents test runs from writing backups to the production backup dir.
@@ -1842,18 +1896,20 @@ function _editDistance(a, b) {
1842
1896
 
1843
1897
  function mergeEntities(keepId, removeId) {
1844
1898
  const db = getDb();
1845
- const keep = getEntity(keepId);
1846
- const remove = getEntity(removeId);
1847
- if (!keep || !remove) throw new Error('Both entities must exist');
1848
- let keepAliases = [], removeAliases = [];
1849
- try { keepAliases = JSON.parse(keep.aliases || '[]'); } catch {}
1850
- try { removeAliases = JSON.parse(remove.aliases || '[]'); } catch {}
1851
- const mergedAliases = [...new Set([...keepAliases, ...removeAliases, remove.canonical_name])];
1852
- db.prepare("UPDATE entities SET aliases = ?, updated_at = datetime('now') WHERE id = ?").run(JSON.stringify(mergedAliases), keepId);
1853
- db.prepare('UPDATE knowledge SET subject_entity_id = ? WHERE subject_entity_id = ?').run(keepId, removeId);
1854
- db.prepare('UPDATE knowledge SET object_entity_id = ? WHERE object_entity_id = ?').run(keepId, removeId);
1855
- db.prepare('DELETE FROM entities WHERE id = ?').run(removeId);
1856
- return { merged: true, kept: keepId, removed: removeId, aliases: mergedAliases };
1899
+ return runImmediateTransaction(db, (targetKeepId, targetRemoveId) => {
1900
+ const keep = db.prepare('SELECT * FROM entities WHERE id = ?').get(targetKeepId);
1901
+ const remove = db.prepare('SELECT * FROM entities WHERE id = ?').get(targetRemoveId);
1902
+ if (!keep || !remove) throw new Error('Both entities must exist');
1903
+ let keepAliases = [], removeAliases = [];
1904
+ try { keepAliases = JSON.parse(keep.aliases || '[]'); } catch {}
1905
+ try { removeAliases = JSON.parse(remove.aliases || '[]'); } catch {}
1906
+ const mergedAliases = [...new Set([...keepAliases, ...removeAliases, remove.canonical_name])];
1907
+ db.prepare("UPDATE entities SET aliases = ?, updated_at = datetime('now') WHERE id = ?").run(JSON.stringify(mergedAliases), targetKeepId);
1908
+ db.prepare('UPDATE knowledge SET subject_entity_id = ? WHERE subject_entity_id = ?').run(targetKeepId, targetRemoveId);
1909
+ db.prepare('UPDATE knowledge SET object_entity_id = ? WHERE object_entity_id = ?').run(targetKeepId, targetRemoveId);
1910
+ db.prepare('DELETE FROM entities WHERE id = ?').run(targetRemoveId);
1911
+ return { merged: true, kept: targetKeepId, removed: targetRemoveId, aliases: mergedAliases };
1912
+ }, keepId, removeId);
1857
1913
  }
1858
1914
 
1859
1915
  function listEntities({ entity_type, limit } = {}) {
@@ -2247,14 +2303,16 @@ function searchChatMessages({ query, limit } = {}) {
2247
2303
 
2248
2304
  function deleteChatMessages({ session_id, user_content, assistant_content }) {
2249
2305
  const db = getDb();
2250
- if (user_content) {
2251
- db.prepare('DELETE FROM chat_messages WHERE session_id = ? AND role = ? AND content = ?')
2252
- .run(session_id || 'default', 'user', user_content);
2253
- }
2254
- if (assistant_content) {
2255
- db.prepare('DELETE FROM chat_messages WHERE session_id = ? AND role = ? AND content = ?')
2256
- .run(session_id || 'default', 'assistant', assistant_content);
2257
- }
2306
+ return runImmediateTransaction(db, (targetSessionId, userContent, assistantContent) => {
2307
+ if (userContent) {
2308
+ db.prepare('DELETE FROM chat_messages WHERE session_id = ? AND role = ? AND content = ?')
2309
+ .run(targetSessionId || 'default', 'user', userContent);
2310
+ }
2311
+ if (assistantContent) {
2312
+ db.prepare('DELETE FROM chat_messages WHERE session_id = ? AND role = ? AND content = ?')
2313
+ .run(targetSessionId || 'default', 'assistant', assistantContent);
2314
+ }
2315
+ }, session_id, user_content, assistant_content);
2258
2316
  }
2259
2317
 
2260
2318
  function clearChatSession(session_id) {
@@ -2379,11 +2437,14 @@ function updateSkillStats(skillId, success) {
2379
2437
  }
2380
2438
 
2381
2439
  function deleteSkill(id) {
2382
- const skill = getDb().prepare('SELECT * FROM skills WHERE id = ?').get(id);
2383
- if (!skill) throw new Error(`Skill not found: ${id}`);
2384
- getDb().prepare('DELETE FROM skill_executions WHERE skill_id = ?').run(id);
2385
- getDb().prepare('DELETE FROM skills WHERE id = ?').run(id);
2386
- return skill;
2440
+ const db = getDb();
2441
+ return runImmediateTransaction(db, (skillId) => {
2442
+ const skill = db.prepare('SELECT * FROM skills WHERE id = ?').get(skillId);
2443
+ if (!skill) throw new Error(`Skill not found: ${skillId}`);
2444
+ db.prepare('DELETE FROM skill_executions WHERE skill_id = ?').run(skillId);
2445
+ db.prepare('DELETE FROM skills WHERE id = ?').run(skillId);
2446
+ return skill;
2447
+ }, id);
2387
2448
  }
2388
2449
 
2389
2450
  // ── Skill dispatch decision log (kv-backed) ──
@@ -2496,21 +2557,25 @@ function deleteAuthProfile(id) {
2496
2557
  // ── Tasks ──
2497
2558
 
2498
2559
  function insertTask({ title, description, priority, type, execution, script, schedule, due_at, next_run_at, skill, skill_config, source, source_ref }) {
2499
- // Dedup: skip if a task with the same title+source+source_ref already exists and isn't archived
2500
- if (title && source) {
2501
- const existing = source_ref
2502
- ? getDb().prepare(`SELECT id, status FROM tasks WHERE title = ? AND source = ? AND source_ref = ? AND status != 'archived' LIMIT 1`).get(title, source, source_ref)
2503
- : getDb().prepare(`SELECT id, status FROM tasks WHERE title = ? AND source = ? AND status NOT IN ('archived','completed') LIMIT 1`).get(title, source);
2504
- if (existing) return { id: existing.id, deduplicated: true };
2505
- }
2506
- const id = uuidv4();
2507
- getDb().prepare(`
2508
- INSERT INTO tasks (id, title, description, priority, type, execution, script, schedule, due_at, next_run_at, skill, skill_config, source, source_ref)
2509
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2510
- `).run(id, title, description || null, priority || 'normal', type || 'once',
2511
- execution || 'chat', script || null, schedule || null, due_at || null, next_run_at || due_at || null,
2512
- skill || null, skill_config || null, source || 'system', source_ref || null);
2513
- return { id };
2560
+ const db = getDb();
2561
+ return runImmediateTransaction(db, (task) => {
2562
+ const taskSource = task.source || 'system';
2563
+ // Dedup: skip if a task with the same title+source+source_ref already exists and isn't archived.
2564
+ if (task.title && taskSource) {
2565
+ const existing = task.source_ref
2566
+ ? db.prepare(`SELECT id, status FROM tasks WHERE title = ? AND source = ? AND source_ref = ? AND status != 'archived' LIMIT 1`).get(task.title, taskSource, task.source_ref)
2567
+ : db.prepare(`SELECT id, status FROM tasks WHERE title = ? AND source = ? AND status NOT IN ('archived','completed') LIMIT 1`).get(task.title, taskSource);
2568
+ if (existing) return { id: existing.id, deduplicated: true };
2569
+ }
2570
+ const id = uuidv4();
2571
+ db.prepare(`
2572
+ INSERT INTO tasks (id, title, description, priority, type, execution, script, schedule, due_at, next_run_at, skill, skill_config, source, source_ref)
2573
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2574
+ `).run(id, task.title, task.description || null, task.priority || 'normal', task.type || 'once',
2575
+ task.execution || 'chat', task.script || null, task.schedule || null, task.due_at || null, task.next_run_at || task.due_at || null,
2576
+ task.skill || null, task.skill_config || null, taskSource, task.source_ref || null);
2577
+ return { id };
2578
+ }, { title, description, priority, type, execution, script, schedule, due_at, next_run_at, skill, skill_config, source, source_ref });
2514
2579
  }
2515
2580
 
2516
2581
  function getTask(id) {
@@ -2710,6 +2775,48 @@ function deleteExpiredSlackThreads(watchDurationMs) {
2710
2775
  return getDb().prepare('DELETE FROM slack_threads WHERE last_activity < ?').run(cutoff).changes;
2711
2776
  }
2712
2777
 
2778
+ // -- Slack Inbound Event Ledger --
2779
+
2780
+ function slackInboundEventId(channelId, messageTs) {
2781
+ return `${channelId}:${messageTs}`;
2782
+ }
2783
+
2784
+ function claimSlackInboundEvent({ channelId, messageTs, threadTs, source, payloadHash } = {}) {
2785
+ if (!channelId || !messageTs) throw new Error('Slack inbound event requires channelId and messageTs');
2786
+ const id = slackInboundEventId(channelId, messageTs);
2787
+ const now = new Date().toISOString();
2788
+ const result = getDb().prepare(`
2789
+ INSERT OR IGNORE INTO slack_inbound_events
2790
+ (id, channel_id, message_ts, thread_ts, source, status, payload_hash, updated_at)
2791
+ VALUES (?, ?, ?, ?, ?, 'claimed', ?, ?)
2792
+ `).run(id, channelId, messageTs, threadTs || null, source || null, payloadHash || null, now);
2793
+
2794
+ if (result.changes > 0) return { id, claimed: true };
2795
+ const event = getDb().prepare('SELECT * FROM slack_inbound_events WHERE id = ?').get(id);
2796
+ return { id, claimed: false, event };
2797
+ }
2798
+
2799
+ function completeSlackInboundEvent(id, { status = 'completed', error = null } = {}) {
2800
+ if (!id) return 0;
2801
+ const now = new Date().toISOString();
2802
+ const result = getDb().prepare(`
2803
+ UPDATE slack_inbound_events
2804
+ SET status = ?, error = ?, updated_at = ?
2805
+ WHERE id = ?
2806
+ `).run(status, error || null, now, id);
2807
+ return result.changes;
2808
+ }
2809
+
2810
+ function releaseSlackInboundEvent(id) {
2811
+ if (!id) return 0;
2812
+ return getDb().prepare('DELETE FROM slack_inbound_events WHERE id = ?').run(id).changes;
2813
+ }
2814
+
2815
+ function pruneSlackInboundEvents(ttlMs = 48 * 60 * 60 * 1000) {
2816
+ const cutoff = new Date(Date.now() - ttlMs).toISOString();
2817
+ return getDb().prepare('DELETE FROM slack_inbound_events WHERE updated_at < ?').run(cutoff).changes;
2818
+ }
2819
+
2713
2820
  // -- Model Provider CRUD --
2714
2821
 
2715
2822
  function upsertModelProvider({ id, name, type, baseUrl, apiKeyEncrypted, customHeaders, enabled }) {
@@ -2776,6 +2883,121 @@ function getProviderAuthMethod(type) {
2776
2883
  return row?.auth_method || null;
2777
2884
  }
2778
2885
 
2886
+ function saveSetupProvider({
2887
+ type,
2888
+ name,
2889
+ id,
2890
+ baseUrl,
2891
+ apiKeyEncrypted,
2892
+ customHeaders,
2893
+ enabled = true,
2894
+ model,
2895
+ setDefault = false,
2896
+ authMethod,
2897
+ preserveExistingKey = true,
2898
+ registerModel = true,
2899
+ }) {
2900
+ if (!type) throw new Error('Provider type required');
2901
+ if (authMethod && !VALID_AUTH_METHODS.has(authMethod)) {
2902
+ throw new Error(`Invalid auth_method: ${authMethod}. Must be one of: ${[...VALID_AUTH_METHODS].join(', ')}`);
2903
+ }
2904
+ const providerId = id || `${type}-default`;
2905
+ const displayName = name || type;
2906
+ const db = getDb();
2907
+ return runImmediateTransaction(db, () => {
2908
+ const currentDefaultProvider = getKv('walle_provider') || process.env.WALLE_PROVIDER || 'anthropic';
2909
+ const syncDefaultModel = !!(model && currentDefaultProvider === type && !setDefault);
2910
+ let storedKey = apiKeyEncrypted || null;
2911
+ if (!storedKey && preserveExistingKey) {
2912
+ const existingProvider = getModelProviderWithKey(providerId);
2913
+ storedKey = existingProvider?.api_key_encrypted || null;
2914
+ if (!storedKey) {
2915
+ const row = db.prepare(
2916
+ 'SELECT api_key_encrypted FROM model_providers WHERE type = ? AND enabled = 1 AND api_key_encrypted IS NOT NULL ORDER BY updated_at DESC LIMIT 1'
2917
+ ).get(type);
2918
+ storedKey = row?.api_key_encrypted || null;
2919
+ }
2920
+ }
2921
+
2922
+ upsertModelProvider({
2923
+ id: providerId,
2924
+ name: displayName,
2925
+ type,
2926
+ baseUrl: baseUrl || null,
2927
+ apiKeyEncrypted: storedKey,
2928
+ customHeaders: customHeaders || null,
2929
+ enabled,
2930
+ });
2931
+ if (authMethod) setProviderAuthMethod(type, authMethod);
2932
+ if (model) {
2933
+ setKv(`walle_model_${type}`, model);
2934
+ if (syncDefaultModel) setKv('walle_model', model);
2935
+ if (registerModel) {
2936
+ upsertModelRegistryEntry({
2937
+ id: `${providerId}:${model}`,
2938
+ providerId,
2939
+ modelId: model,
2940
+ displayName: model,
2941
+ enabled: true,
2942
+ });
2943
+ }
2944
+ }
2945
+ if (setDefault) {
2946
+ setKv('walle_provider', type);
2947
+ if (model) setKv('walle_model', model);
2948
+ }
2949
+
2950
+ return {
2951
+ provider_id: providerId,
2952
+ type,
2953
+ default_provider: getKv('walle_provider') || null,
2954
+ default_model: getKv('walle_model') || null,
2955
+ synced_default_model: syncDefaultModel,
2956
+ registry_id: model && registerModel ? `${providerId}:${model}` : null,
2957
+ };
2958
+ });
2959
+ }
2960
+
2961
+ function setDefaultProviderSelection({ type, requestedModel, targetModel }) {
2962
+ if (!type) throw new Error('Provider type required');
2963
+ const db = getDb();
2964
+ return runImmediateTransaction(db, () => {
2965
+ setKv('walle_provider', type);
2966
+ if (requestedModel) setKv(`walle_model_${type}`, requestedModel);
2967
+ setKv('walle_model', targetModel || '');
2968
+ return {
2969
+ type,
2970
+ default_provider: getKv('walle_provider') || null,
2971
+ default_model: getKv('walle_model') || '',
2972
+ };
2973
+ });
2974
+ }
2975
+
2976
+ function disableModelProviderByType(type) {
2977
+ if (!type) throw new Error('Provider type required');
2978
+ const db = getDb();
2979
+ return runImmediateTransaction(db, () => {
2980
+ const changes = db.prepare('UPDATE model_providers SET enabled = 0, updated_at = datetime(\'now\') WHERE type = ?').run(type).changes;
2981
+ let fallback = null;
2982
+ if (getKv('walle_provider') === type) {
2983
+ const enabled = listEnabledProviders();
2984
+ if (enabled.length > 0) {
2985
+ fallback = enabled[0];
2986
+ setKv('walle_provider', fallback.type);
2987
+ const fallbackModel = getKv(`walle_model_${fallback.type}`);
2988
+ if (fallbackModel) setKv('walle_model', fallbackModel);
2989
+ }
2990
+ }
2991
+ return {
2992
+ type,
2993
+ disabled: changes,
2994
+ fallback_provider: fallback?.type || null,
2995
+ default_provider: getKv('walle_provider') || null,
2996
+ default_model: getKv('walle_model') || null,
2997
+ };
2998
+ });
2999
+ }
3000
+
2779
3001
  // -- Model Registry CRUD --
2780
3002
 
2781
3003
  function upsertModelRegistryEntry({ id, providerId, modelId, displayName, capabilities, costPer1mInput, costPer1mOutput, maxContextTokens, maxOutputTokens, speedTier, enabled, isFineTuned }) {
@@ -2955,6 +3177,24 @@ function setModelDefault(taskType, modelRegistryId, strategy, quorumSize) {
2955
3177
  `).run(taskType, modelRegistryId || null, strategy || 'single', quorumSize || 1);
2956
3178
  }
2957
3179
 
3180
+ function setTaskModelDefault({ taskType, modelRegistryId, strategy, quorumSize }) {
3181
+ if (!taskType) throw new Error('Default requires taskType');
3182
+ const db = getDb();
3183
+ return runImmediateTransaction(db, () => {
3184
+ if (modelRegistryId) {
3185
+ const exists = db.prepare('SELECT 1 FROM model_registry WHERE id = ?').get(modelRegistryId);
3186
+ if (!exists) throw new Error('model_registry_id does not match any registered model');
3187
+ }
3188
+ setModelDefault(taskType, modelRegistryId, strategy, quorumSize);
3189
+ return {
3190
+ task_type: taskType,
3191
+ model_registry_id: modelRegistryId || null,
3192
+ strategy: strategy || 'single',
3193
+ quorum_size: quorumSize || 1,
3194
+ };
3195
+ });
3196
+ }
3197
+
2958
3198
  function getModelDefault(taskType) {
2959
3199
  return getDb().prepare('SELECT * FROM model_defaults WHERE task_type = ?').get(taskType) || null;
2960
3200
  }
@@ -3810,6 +4050,7 @@ module.exports = {
3810
4050
  saveChatBranches,
3811
4051
  loadChatBranches,
3812
4052
  // Backup
4053
+ checkpointWalOrThrow,
3813
4054
  createBackup,
3814
4055
  listBackups,
3815
4056
  deleteBackup,
@@ -3861,6 +4102,10 @@ module.exports = {
3861
4102
  listActiveSlackThreads,
3862
4103
  updateSlackThread,
3863
4104
  deleteExpiredSlackThreads,
4105
+ claimSlackInboundEvent,
4106
+ completeSlackInboundEvent,
4107
+ releaseSlackInboundEvent,
4108
+ pruneSlackInboundEvents,
3864
4109
  // Model Providers
3865
4110
  upsertModelProvider,
3866
4111
  getModelProvider,
@@ -3870,6 +4115,9 @@ module.exports = {
3870
4115
  getProviderByType,
3871
4116
  setProviderAuthMethod,
3872
4117
  getProviderAuthMethod,
4118
+ saveSetupProvider,
4119
+ setDefaultProviderSelection,
4120
+ disableModelProviderByType,
3873
4121
  VALID_AUTH_METHODS,
3874
4122
  SCHEMA_VERSION,
3875
4123
  deleteModelProvider,
@@ -3888,6 +4136,7 @@ module.exports = {
3888
4136
  getAgentRunnerScorecard,
3889
4137
  // Model Defaults
3890
4138
  setModelDefault,
4139
+ setTaskModelDefault,
3891
4140
  getModelDefault,
3892
4141
  listModelDefaults,
3893
4142
  // Benchmark Evaluations