create-walle 0.9.25 → 0.9.26
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 +8 -0
- package/bin/create-walle.js +815 -45
- package/package.json +2 -2
- package/template/bin/ctm-dev-cleanup.js +90 -4
- package/template/bin/ctm-launch.sh +49 -1
- package/template/bin/dev.sh +45 -1
- package/template/bin/ensure-stable-node.js +132 -0
- package/template/bin/install-service.sh +9 -0
- package/template/claude-task-manager/api-prompts.js +899 -119
- package/template/claude-task-manager/approval-agent.js +360 -40
- package/template/claude-task-manager/bin/ctm-disclaim.c +42 -0
- package/template/claude-task-manager/bin/ctm-hotkey.swift +67 -81
- package/template/claude-task-manager/bin/ctm-screen-auth.swift +37 -0
- package/template/claude-task-manager/bin/install-hotkey.sh +97 -49
- package/template/claude-task-manager/bin/restart-ctm.sh +14 -0
- package/template/claude-task-manager/db.js +399 -48
- package/template/claude-task-manager/docs/approval-hook-sandbox.md +84 -0
- package/template/claude-task-manager/docs/codex-app-server-approvals.md +72 -0
- package/template/claude-task-manager/docs/codex-native-sandbox.md +47 -0
- package/template/claude-task-manager/docs/prompt-editing-tree-design.md +18 -1
- package/template/claude-task-manager/lib/approval-hook.js +200 -0
- package/template/claude-task-manager/lib/approval-self-adapt.js +1 -0
- package/template/claude-task-manager/lib/auth-rules.js +11 -0
- package/template/claude-task-manager/lib/background-llm.js +32 -4
- package/template/claude-task-manager/lib/codesign-identity.js +140 -0
- package/template/claude-task-manager/lib/codex-app-server-client.js +119 -0
- package/template/claude-task-manager/lib/codex-approval-bridge.js +118 -0
- package/template/claude-task-manager/lib/codex-history-terminal-renderer.js +571 -0
- package/template/claude-task-manager/lib/codex-paths.js +73 -0
- package/template/claude-task-manager/lib/codex-rollout-snapshot.js +164 -0
- package/template/claude-task-manager/lib/codex-rollout-tail.js +72 -0
- package/template/claude-task-manager/lib/codex-sandbox-args.js +47 -0
- package/template/claude-task-manager/lib/coding-agent-models.js +118 -71
- package/template/claude-task-manager/lib/command-targets.js +163 -0
- package/template/claude-task-manager/lib/conversation-tail-merge.js +61 -19
- package/template/claude-task-manager/lib/db-owner-worker-client.js +29 -1
- package/template/claude-task-manager/lib/escalation-review.js +80 -3
- package/template/claude-task-manager/lib/flow-control.js +52 -0
- package/template/claude-task-manager/lib/fs-watcher.js +24 -15
- package/template/claude-task-manager/lib/ingest-cooldown.js +68 -0
- package/template/claude-task-manager/lib/jsonl-conversation-parser.js +8 -4
- package/template/claude-task-manager/lib/launchd-recovery.js +92 -0
- package/template/claude-task-manager/lib/microsoft-dev-tunnel-setup.js +207 -52
- package/template/claude-task-manager/lib/mobile-push-store.js +7 -0
- package/template/claude-task-manager/lib/model-overview-brain-fallback.js +102 -1
- package/template/claude-task-manager/lib/model-overview-cache.js +1 -0
- package/template/claude-task-manager/lib/oauth-proxy-supervisor.js +2 -1
- package/template/claude-task-manager/lib/perf-tracker.js +29 -2
- package/template/claude-task-manager/lib/permission-match.js +146 -16
- package/template/claude-task-manager/lib/project-slug.js +33 -0
- package/template/claude-task-manager/lib/prompt-intent.js +51 -4
- package/template/claude-task-manager/lib/read-pool-client.js +48 -3
- package/template/claude-task-manager/lib/real-node.js +73 -0
- package/template/claude-task-manager/lib/runtime-work-registry.js +131 -14
- package/template/claude-task-manager/lib/session-content-backfill.js +24 -5
- package/template/claude-task-manager/lib/session-diagnostics-batch.js +87 -0
- package/template/claude-task-manager/lib/session-history.js +5 -7
- package/template/claude-task-manager/lib/session-host-manager.js +19 -0
- package/template/claude-task-manager/lib/session-jobs.js +6 -0
- package/template/claude-task-manager/lib/session-message-response-cache.js +89 -0
- package/template/claude-task-manager/lib/session-messages-page.js +211 -0
- package/template/claude-task-manager/lib/session-messages-projection.js +170 -0
- package/template/claude-task-manager/lib/session-standup.js +8 -0
- package/template/claude-task-manager/lib/session-timeline-summary.js +16 -2
- package/template/claude-task-manager/lib/session-token-usage.js +30 -8
- package/template/claude-task-manager/lib/session-workspace-binding.js +29 -15
- package/template/claude-task-manager/lib/storage-migration.js +2 -1
- package/template/claude-task-manager/lib/transcript-store.js +179 -12
- package/template/claude-task-manager/lib/walle-ctm-history.js +298 -11
- package/template/claude-task-manager/lib/walle-permission-reply.js +49 -0
- package/template/claude-task-manager/lib/walle-session-cache.js +22 -1
- package/template/claude-task-manager/lib/walle-supervisor.js +42 -3
- package/template/claude-task-manager/package.json +5 -2
- package/template/claude-task-manager/prompt-harvest.js +31 -11
- package/template/claude-task-manager/providers/claude-code.js +29 -1
- package/template/claude-task-manager/providers/codex.js +13 -1
- package/template/claude-task-manager/public/css/setup.css +11 -0
- package/template/claude-task-manager/public/css/walle-session.css +132 -4
- package/template/claude-task-manager/public/css/walle.css +89 -0
- package/template/claude-task-manager/public/icon-16.png +0 -0
- package/template/claude-task-manager/public/icon-32.png +0 -0
- package/template/claude-task-manager/public/icon-512.png +0 -0
- package/template/claude-task-manager/public/index.html +2483 -165
- package/template/claude-task-manager/public/js/activation-render-check.js +55 -0
- package/template/claude-task-manager/public/js/flow-control-policy.js +52 -0
- package/template/claude-task-manager/public/js/message-renderer.js +60 -1
- package/template/claude-task-manager/public/js/prompts.js +13 -1
- package/template/claude-task-manager/public/js/session-status-precedence.js +9 -3
- package/template/claude-task-manager/public/js/setup.js +54 -10
- package/template/claude-task-manager/public/js/stream-resize-policy.js +80 -0
- package/template/claude-task-manager/public/js/stream-view.js +78 -0
- package/template/claude-task-manager/public/js/terminal-reconciler.js +52 -2
- package/template/claude-task-manager/public/js/tool-state.js +155 -0
- package/template/claude-task-manager/public/js/walle-session.js +887 -326
- package/template/claude-task-manager/public/js/walle.js +306 -195
- package/template/claude-task-manager/public/m/app.css +1 -0
- package/template/claude-task-manager/public/m/app.js +33 -3
- package/template/claude-task-manager/queue-engine.js +45 -1
- package/template/claude-task-manager/server.js +3367 -540
- package/template/claude-task-manager/workers/approval-blocklist.js +130 -17
- package/template/claude-task-manager/workers/db-owner-worker.js +31 -1
- package/template/claude-task-manager/workers/read-pool-worker.js +92 -5
- package/template/claude-task-manager/workers/session-host-process.js +10 -0
- package/template/claude-task-manager/workers/state-detectors/codex.js +58 -7
- package/template/package.json +2 -3
- package/template/shared/icons/AppIcon-ctm.icns +0 -0
- package/template/shared/icons/AppIcon-walle.icns +0 -0
- package/template/wall-e/agent.js +139 -18
- package/template/wall-e/api-walle.js +201 -22
- package/template/wall-e/bin/train-gemma-e4b-tooluse.js +1981 -0
- package/template/wall-e/brain.js +1049 -39
- package/template/wall-e/chat.js +427 -86
- package/template/wall-e/coding/acceptance-contract.js +26 -1
- package/template/wall-e/coding/action-memory-policy.js +353 -0
- package/template/wall-e/coding/action-memory-store.js +814 -0
- package/template/wall-e/coding/initial-messages.js +197 -0
- package/template/wall-e/coding/no-progress-guard.js +327 -0
- package/template/wall-e/coding/permission-service.js +88 -22
- package/template/wall-e/coding/session-workspaces.js +81 -0
- package/template/wall-e/coding/shell-sandbox.js +124 -0
- package/template/wall-e/coding/stream-processor.js +63 -2
- package/template/wall-e/coding/tool-execution-controller.js +14 -1
- package/template/wall-e/coding/tool-registry.js +1 -1
- package/template/wall-e/coding/transcript-writer.js +3 -0
- package/template/wall-e/coding-orchestrator.js +636 -35
- package/template/wall-e/coding-prompts.js +51 -2
- package/template/wall-e/docs/model-routing-policy.md +59 -0
- package/template/wall-e/docs/walle-shell-sandbox.md +61 -0
- package/template/wall-e/extraction/knowledge-extractor.js +76 -23
- package/template/wall-e/http/chat-api.js +30 -12
- package/template/wall-e/http/model-admin.js +93 -1
- package/template/wall-e/lib/background-lanes.js +133 -0
- package/template/wall-e/lib/boot-profile.js +11 -0
- package/template/wall-e/lib/brain-owner-worker-client.js +324 -0
- package/template/wall-e/lib/brain-read-pool-client.js +311 -0
- package/template/wall-e/lib/diagnostics-flags.js +87 -0
- package/template/wall-e/lib/event-loop-monitor.js +74 -3
- package/template/wall-e/lib/mcp-integration.js +7 -1
- package/template/wall-e/lib/real-node.js +98 -0
- package/template/wall-e/lib/runtime-health.js +206 -0
- package/template/wall-e/lib/runtime-worker-pool.js +101 -0
- package/template/wall-e/lib/scheduler-worker-jobs.js +231 -0
- package/template/wall-e/lib/scheduler.js +446 -17
- package/template/wall-e/lib/service-health.js +61 -2
- package/template/wall-e/lib/service-readiness.js +258 -0
- package/template/wall-e/lib/usage.js +152 -0
- package/template/wall-e/lib/worker-thread-pool.js +389 -0
- package/template/wall-e/llm/client.js +81 -4
- package/template/wall-e/llm/default-fallback.js +54 -8
- package/template/wall-e/llm/mlx.js +536 -73
- package/template/wall-e/llm/mlx.plugin.json +1 -1
- package/template/wall-e/llm/ollama.js +342 -43
- package/template/wall-e/llm/provider-error.js +18 -1
- package/template/wall-e/llm/provider-health-state.js +176 -0
- package/template/wall-e/llm/routing-policy.js +796 -0
- package/template/wall-e/llm/supported-models.js +5 -0
- package/template/wall-e/loops/tasks.js +60 -14
- package/template/wall-e/loops/think.js +89 -24
- package/template/wall-e/mcp-server.js +192 -28
- package/template/wall-e/server.js +32 -7
- package/template/wall-e/skills/script-skill-runner.js +8 -1
- package/template/wall-e/skills/skill-planner.js +64 -1
- package/template/wall-e/tools/builtin-middleware.js +67 -2
- package/template/wall-e/tools/local-tools.js +116 -26
- package/template/wall-e/tools/permission-checker.js +52 -4
- package/template/wall-e/tools/permission-rules.js +36 -0
- package/template/wall-e/tools/shell-analyzer.js +46 -1
- package/template/wall-e/training/gemma-e4b-qlora.js +314 -0
- package/template/wall-e/training/real-trajectory-miner.js +2617 -0
- package/template/wall-e/training/replay-eval-analysis.js +151 -0
- package/template/wall-e/training/run-shell-command-selector.js +277 -0
- package/template/wall-e/training/tool-sft-dataset.js +312 -0
- package/template/wall-e/training/tool-sft-renderers.js +144 -0
- package/template/wall-e/training/tool-trace-harvester.js +1440 -0
- package/template/wall-e/training/trajectory-action-selector.js +364 -0
- package/template/wall-e/weather-runtime.js +232 -0
- package/template/wall-e/workers/brain-owner-worker.js +162 -0
- package/template/wall-e/workers/brain-read-worker.js +148 -0
- package/template/wall-e/workers/runtime-worker.js +145 -0
package/template/wall-e/brain.js
CHANGED
|
@@ -30,6 +30,7 @@ const {
|
|
|
30
30
|
const {
|
|
31
31
|
createSqliteOwnerWriteQueue,
|
|
32
32
|
} = require('./shared/sqlite-owner-write-queue');
|
|
33
|
+
const runtimeHealth = require('./lib/runtime-health');
|
|
33
34
|
const _brainEvents = new EventEmitter();
|
|
34
35
|
_brainEvents.setMaxListeners(20);
|
|
35
36
|
let _stripNoise;
|
|
@@ -166,7 +167,7 @@ function _applyPersistedBackupDirSetting() {
|
|
|
166
167
|
}
|
|
167
168
|
|
|
168
169
|
// --- Schema versioning via PRAGMA user_version ---
|
|
169
|
-
const SCHEMA_VERSION =
|
|
170
|
+
const SCHEMA_VERSION = 25; // Bump on every migration addition
|
|
170
171
|
|
|
171
172
|
const MIGRATIONS = {
|
|
172
173
|
1: (d) => {
|
|
@@ -571,6 +572,150 @@ const MIGRATIONS = {
|
|
|
571
572
|
};
|
|
572
573
|
addCol('chat_branches', 'updated_at_ms', 'INTEGER NOT NULL DEFAULT 0');
|
|
573
574
|
},
|
|
575
|
+
19: (d) => {
|
|
576
|
+
d.exec(`
|
|
577
|
+
CREATE TABLE IF NOT EXISTS chat_branch_events (
|
|
578
|
+
id TEXT PRIMARY KEY,
|
|
579
|
+
session_id TEXT NOT NULL,
|
|
580
|
+
seq INTEGER NOT NULL,
|
|
581
|
+
type TEXT NOT NULL,
|
|
582
|
+
payload TEXT NOT NULL DEFAULT '{}',
|
|
583
|
+
created_at_ms INTEGER NOT NULL DEFAULT 0,
|
|
584
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
585
|
+
UNIQUE(session_id, seq)
|
|
586
|
+
);
|
|
587
|
+
CREATE INDEX IF NOT EXISTS idx_chat_branch_events_session
|
|
588
|
+
ON chat_branch_events(session_id, seq);
|
|
589
|
+
CREATE INDEX IF NOT EXISTS idx_chat_branch_events_type
|
|
590
|
+
ON chat_branch_events(session_id, type, seq);
|
|
591
|
+
|
|
592
|
+
CREATE TABLE IF NOT EXISTS chat_branch_state (
|
|
593
|
+
session_id TEXT PRIMARY KEY,
|
|
594
|
+
state TEXT NOT NULL DEFAULT '{}',
|
|
595
|
+
updated_at_ms INTEGER NOT NULL DEFAULT 0
|
|
596
|
+
);
|
|
597
|
+
`);
|
|
598
|
+
},
|
|
599
|
+
20: (d) => {
|
|
600
|
+
// Durable parentUuid tree for the default chat: each message records the id of
|
|
601
|
+
// its parent message so the conversation is a branchable tree (the same model the
|
|
602
|
+
// session view uses), replacing the index-keyed branch blob. NULL = a root prompt.
|
|
603
|
+
const addCol = (table, col, type) => {
|
|
604
|
+
try { d.prepare(`SELECT ${col} FROM ${table} LIMIT 0`).run(); } catch (_) {
|
|
605
|
+
d.prepare(`ALTER TABLE ${table} ADD COLUMN ${col} ${type}`).run();
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
addCol('chat_messages', 'parent_uuid', 'TEXT');
|
|
609
|
+
},
|
|
610
|
+
21: (d) => {
|
|
611
|
+
// Retire the index-blob branch persistence: the default chat is now a durable
|
|
612
|
+
// parentUuid tree (chat_messages.parent_uuid) and the only mutable branch state is
|
|
613
|
+
// the head-leaf KV walle_session_head:<sid>. Drop the unused blob/event tables.
|
|
614
|
+
// (Data is reconstructable from chat_messages; the head KV survives.)
|
|
615
|
+
d.exec(`
|
|
616
|
+
DROP TABLE IF EXISTS chat_branch_state;
|
|
617
|
+
DROP TABLE IF EXISTS chat_branch_events;
|
|
618
|
+
`);
|
|
619
|
+
},
|
|
620
|
+
22: (d) => {
|
|
621
|
+
// Backfill parent_uuid for EXISTING chat_messages so an upgrading user's history
|
|
622
|
+
// renders as a tree instead of a pile of roots. The pre-tree default chat persisted
|
|
623
|
+
// only the ACTIVE path to chat_messages (it cleared + reinserted on edit; alternate
|
|
624
|
+
// branches lived in the now-dropped blob), so chaining each session's rows in
|
|
625
|
+
// insertion order exactly reproduces the user's current linear thread. Idempotent:
|
|
626
|
+
// only fills NULLs, and the first row of each session legitimately stays NULL (root).
|
|
627
|
+
backfillChatMessageParents(d);
|
|
628
|
+
},
|
|
629
|
+
23: (d) => {
|
|
630
|
+
// Corrective re-derive: an early build backfilled parent_uuid in rowid (insertion)
|
|
631
|
+
// order, which disagrees with the chat's (created_at, id) display order for same-second
|
|
632
|
+
// imported rows and produced spurious branch pagers. Re-chain every row in display
|
|
633
|
+
// order. Safe: at the tree rollout boundary the chat is still purely linear (no genuine
|
|
634
|
+
// edit-resend siblings yet), and migrations run once so post-rollout branches are never
|
|
635
|
+
// touched. A no-op for a DB whose migration 22 already used the correct order.
|
|
636
|
+
backfillChatMessageParents(d, { overwrite: true });
|
|
637
|
+
},
|
|
638
|
+
24: (d) => {
|
|
639
|
+
// Durable structured parts for Wall-E chat turns. chat_messages remains the
|
|
640
|
+
// compatibility transcript; parts persist live context/tool results in a
|
|
641
|
+
// provider-neutral shape so UI/history/model-projection do not depend on
|
|
642
|
+
// prose parsing.
|
|
643
|
+
d.exec(`
|
|
644
|
+
CREATE TABLE IF NOT EXISTS chat_message_parts (
|
|
645
|
+
id TEXT PRIMARY KEY,
|
|
646
|
+
session_id TEXT,
|
|
647
|
+
message_id TEXT,
|
|
648
|
+
parent_message_id TEXT,
|
|
649
|
+
turn_id TEXT,
|
|
650
|
+
role TEXT,
|
|
651
|
+
part_type TEXT NOT NULL,
|
|
652
|
+
tool_name TEXT,
|
|
653
|
+
status TEXT,
|
|
654
|
+
ordinal INTEGER NOT NULL DEFAULT 0,
|
|
655
|
+
title TEXT,
|
|
656
|
+
summary TEXT,
|
|
657
|
+
content TEXT,
|
|
658
|
+
data_json TEXT NOT NULL DEFAULT '{}',
|
|
659
|
+
model_projection TEXT,
|
|
660
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
661
|
+
FOREIGN KEY(message_id) REFERENCES chat_messages(id) ON DELETE CASCADE
|
|
662
|
+
);
|
|
663
|
+
CREATE INDEX IF NOT EXISTS idx_chat_message_parts_session
|
|
664
|
+
ON chat_message_parts(session_id, created_at, ordinal);
|
|
665
|
+
CREATE INDEX IF NOT EXISTS idx_chat_message_parts_message
|
|
666
|
+
ON chat_message_parts(message_id, ordinal, created_at);
|
|
667
|
+
CREATE INDEX IF NOT EXISTS idx_chat_message_parts_type
|
|
668
|
+
ON chat_message_parts(part_type, tool_name, status);
|
|
669
|
+
`);
|
|
670
|
+
},
|
|
671
|
+
25: (d) => {
|
|
672
|
+
d.exec(`
|
|
673
|
+
CREATE TABLE IF NOT EXISTS model_routing_policies (
|
|
674
|
+
id TEXT PRIMARY KEY,
|
|
675
|
+
mode TEXT NOT NULL DEFAULT 'smart',
|
|
676
|
+
cli_tail_enabled INTEGER NOT NULL DEFAULT 1,
|
|
677
|
+
finalization_fallback_enabled INTEGER NOT NULL DEFAULT 1,
|
|
678
|
+
generated_refresh_interval_ms INTEGER,
|
|
679
|
+
last_generated_at TEXT,
|
|
680
|
+
last_effective_at TEXT,
|
|
681
|
+
last_error TEXT,
|
|
682
|
+
stale_reason TEXT,
|
|
683
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
684
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
685
|
+
);
|
|
686
|
+
|
|
687
|
+
CREATE TABLE IF NOT EXISTS model_routing_policy_routes (
|
|
688
|
+
policy_id TEXT NOT NULL REFERENCES model_routing_policies(id) ON DELETE CASCADE,
|
|
689
|
+
source TEXT NOT NULL,
|
|
690
|
+
route_order INTEGER NOT NULL,
|
|
691
|
+
provider_id TEXT,
|
|
692
|
+
provider_type TEXT NOT NULL,
|
|
693
|
+
runtime_type TEXT,
|
|
694
|
+
provider_name TEXT,
|
|
695
|
+
label TEXT,
|
|
696
|
+
model_registry_id TEXT,
|
|
697
|
+
model_id TEXT,
|
|
698
|
+
role TEXT,
|
|
699
|
+
candidate_source TEXT,
|
|
700
|
+
connection_kind TEXT,
|
|
701
|
+
route_policy TEXT,
|
|
702
|
+
runtime_access TEXT,
|
|
703
|
+
health_status TEXT,
|
|
704
|
+
health_json TEXT,
|
|
705
|
+
reason TEXT,
|
|
706
|
+
score REAL,
|
|
707
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
708
|
+
generated_at TEXT,
|
|
709
|
+
updated_at TEXT DEFAULT (datetime('now')),
|
|
710
|
+
metadata_json TEXT,
|
|
711
|
+
PRIMARY KEY(policy_id, source, route_order)
|
|
712
|
+
);
|
|
713
|
+
CREATE INDEX IF NOT EXISTS idx_model_routing_policy_routes_lookup
|
|
714
|
+
ON model_routing_policy_routes(policy_id, source, enabled, route_order);
|
|
715
|
+
CREATE INDEX IF NOT EXISTS idx_model_routing_policy_routes_provider
|
|
716
|
+
ON model_routing_policy_routes(provider_type, model_id);
|
|
717
|
+
`);
|
|
718
|
+
},
|
|
574
719
|
};
|
|
575
720
|
|
|
576
721
|
// Schema invariants — columns/tables that MUST exist after the named migration.
|
|
@@ -591,6 +736,11 @@ const SCHEMA_INVARIANTS = [
|
|
|
591
736
|
{ migration: 15, table: 'model_registry', column: 'source' },
|
|
592
737
|
{ migration: 17, table: 'model_usage_ledger', column: 'id' },
|
|
593
738
|
{ migration: 18, table: 'chat_branches', column: 'updated_at_ms' },
|
|
739
|
+
// (migration 19's chat_branch_events/chat_branch_state are dropped in migration 21)
|
|
740
|
+
{ migration: 20, table: 'chat_messages', column: 'parent_uuid' },
|
|
741
|
+
{ migration: 24, table: 'chat_message_parts', column: 'part_type' },
|
|
742
|
+
{ migration: 25, table: 'model_routing_policies', column: 'id' },
|
|
743
|
+
{ migration: 25, table: 'model_routing_policy_routes', column: 'policy_id' },
|
|
594
744
|
];
|
|
595
745
|
|
|
596
746
|
function _columnExists(d, table, column) {
|
|
@@ -1143,7 +1293,20 @@ function enqueueOwnerWrite(labelOrFn, fnOrOptions, maybeOptions) {
|
|
|
1143
1293
|
const fn = typeof labelOrFn === 'function' ? labelOrFn : fnOrOptions;
|
|
1144
1294
|
const options = typeof labelOrFn === 'function' ? (fnOrOptions || {}) : (maybeOptions || {});
|
|
1145
1295
|
if (typeof fn !== 'function') throw new TypeError('enqueueOwnerWrite requires a function');
|
|
1146
|
-
|
|
1296
|
+
const queuedAt = Date.now();
|
|
1297
|
+
return _getOwnerWriteQueue().enqueue(async () => {
|
|
1298
|
+
const op = runtimeHealth.beginOperation(`brain.write.${label}`, {
|
|
1299
|
+
queueWaitMs: Date.now() - queuedAt,
|
|
1300
|
+
});
|
|
1301
|
+
try {
|
|
1302
|
+
const value = await fn(getDb());
|
|
1303
|
+
op.end({ ok: true });
|
|
1304
|
+
return value;
|
|
1305
|
+
} catch (error) {
|
|
1306
|
+
op.end({ ok: false, error });
|
|
1307
|
+
throw error;
|
|
1308
|
+
}
|
|
1309
|
+
}, { ...options, label });
|
|
1147
1310
|
}
|
|
1148
1311
|
|
|
1149
1312
|
function getOwnerWriteQueueStatus() {
|
|
@@ -1429,6 +1592,23 @@ function createTables() {
|
|
|
1429
1592
|
created_at TEXT DEFAULT (datetime('now'))
|
|
1430
1593
|
);
|
|
1431
1594
|
|
|
1595
|
+
CREATE TABLE IF NOT EXISTS runtime_events (
|
|
1596
|
+
id TEXT PRIMARY KEY,
|
|
1597
|
+
source TEXT NOT NULL,
|
|
1598
|
+
event_type TEXT NOT NULL,
|
|
1599
|
+
session_id TEXT,
|
|
1600
|
+
channel TEXT,
|
|
1601
|
+
turn_id TEXT,
|
|
1602
|
+
message_id TEXT,
|
|
1603
|
+
tool_name TEXT,
|
|
1604
|
+
status TEXT,
|
|
1605
|
+
summary TEXT,
|
|
1606
|
+
duration_ms INTEGER,
|
|
1607
|
+
payload_json TEXT,
|
|
1608
|
+
occurred_at TEXT NOT NULL,
|
|
1609
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
1610
|
+
);
|
|
1611
|
+
|
|
1432
1612
|
-- Indexes
|
|
1433
1613
|
CREATE INDEX IF NOT EXISTS idx_memories_source ON memories(source, source_id);
|
|
1434
1614
|
CREATE INDEX IF NOT EXISTS idx_memories_type ON memories(memory_type, timestamp);
|
|
@@ -1461,6 +1641,9 @@ function createTables() {
|
|
|
1461
1641
|
|
|
1462
1642
|
CREATE INDEX IF NOT EXISTS idx_exchanges_person ON agent_exchanges(person_id);
|
|
1463
1643
|
CREATE INDEX IF NOT EXISTS idx_exchanges_remote ON agent_exchanges(remote_agent_id);
|
|
1644
|
+
CREATE INDEX IF NOT EXISTS idx_runtime_events_session ON runtime_events(session_id, occurred_at);
|
|
1645
|
+
CREATE INDEX IF NOT EXISTS idx_runtime_events_type ON runtime_events(event_type, occurred_at);
|
|
1646
|
+
CREATE INDEX IF NOT EXISTS idx_runtime_events_tool ON runtime_events(tool_name, occurred_at);
|
|
1464
1647
|
|
|
1465
1648
|
CREATE TABLE IF NOT EXISTS daily_summaries (
|
|
1466
1649
|
id TEXT PRIMARY KEY,
|
|
@@ -1487,6 +1670,32 @@ function createTables() {
|
|
|
1487
1670
|
CREATE INDEX IF NOT EXISTS idx_chat_messages_session ON chat_messages(session_id, created_at);
|
|
1488
1671
|
CREATE INDEX IF NOT EXISTS idx_chat_messages_time ON chat_messages(created_at);
|
|
1489
1672
|
|
|
1673
|
+
CREATE TABLE IF NOT EXISTS chat_message_parts (
|
|
1674
|
+
id TEXT PRIMARY KEY,
|
|
1675
|
+
session_id TEXT,
|
|
1676
|
+
message_id TEXT,
|
|
1677
|
+
parent_message_id TEXT,
|
|
1678
|
+
turn_id TEXT,
|
|
1679
|
+
role TEXT,
|
|
1680
|
+
part_type TEXT NOT NULL,
|
|
1681
|
+
tool_name TEXT,
|
|
1682
|
+
status TEXT,
|
|
1683
|
+
ordinal INTEGER NOT NULL DEFAULT 0,
|
|
1684
|
+
title TEXT,
|
|
1685
|
+
summary TEXT,
|
|
1686
|
+
content TEXT,
|
|
1687
|
+
data_json TEXT NOT NULL DEFAULT '{}',
|
|
1688
|
+
model_projection TEXT,
|
|
1689
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
1690
|
+
FOREIGN KEY(message_id) REFERENCES chat_messages(id) ON DELETE CASCADE
|
|
1691
|
+
);
|
|
1692
|
+
CREATE INDEX IF NOT EXISTS idx_chat_message_parts_session ON chat_message_parts(session_id, created_at, ordinal);
|
|
1693
|
+
CREATE INDEX IF NOT EXISTS idx_chat_message_parts_message ON chat_message_parts(message_id, ordinal, created_at);
|
|
1694
|
+
CREATE INDEX IF NOT EXISTS idx_chat_message_parts_type ON chat_message_parts(part_type, tool_name, status);
|
|
1695
|
+
|
|
1696
|
+
-- Legacy index-blob branch snapshot (pre-tree). Retained for old DBs; no longer
|
|
1697
|
+
-- written. The chat_branch_state/chat_branch_events blob+event tables are retired
|
|
1698
|
+
-- (tree-only branching via chat_messages.parent_uuid; head in KV) — migration 21.
|
|
1490
1699
|
CREATE TABLE IF NOT EXISTS chat_branches (
|
|
1491
1700
|
session_id TEXT PRIMARY KEY,
|
|
1492
1701
|
branches TEXT NOT NULL DEFAULT '{}',
|
|
@@ -1792,6 +2001,47 @@ function createTables() {
|
|
|
1792
2001
|
updated_at TEXT DEFAULT (datetime('now'))
|
|
1793
2002
|
);
|
|
1794
2003
|
|
|
2004
|
+
CREATE TABLE IF NOT EXISTS model_routing_policies (
|
|
2005
|
+
id TEXT PRIMARY KEY,
|
|
2006
|
+
mode TEXT NOT NULL DEFAULT 'smart',
|
|
2007
|
+
cli_tail_enabled INTEGER NOT NULL DEFAULT 1,
|
|
2008
|
+
finalization_fallback_enabled INTEGER NOT NULL DEFAULT 1,
|
|
2009
|
+
generated_refresh_interval_ms INTEGER,
|
|
2010
|
+
last_generated_at TEXT,
|
|
2011
|
+
last_effective_at TEXT,
|
|
2012
|
+
last_error TEXT,
|
|
2013
|
+
stale_reason TEXT,
|
|
2014
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
2015
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
2016
|
+
);
|
|
2017
|
+
|
|
2018
|
+
CREATE TABLE IF NOT EXISTS model_routing_policy_routes (
|
|
2019
|
+
policy_id TEXT NOT NULL REFERENCES model_routing_policies(id) ON DELETE CASCADE,
|
|
2020
|
+
source TEXT NOT NULL,
|
|
2021
|
+
route_order INTEGER NOT NULL,
|
|
2022
|
+
provider_id TEXT,
|
|
2023
|
+
provider_type TEXT NOT NULL,
|
|
2024
|
+
runtime_type TEXT,
|
|
2025
|
+
provider_name TEXT,
|
|
2026
|
+
label TEXT,
|
|
2027
|
+
model_registry_id TEXT,
|
|
2028
|
+
model_id TEXT,
|
|
2029
|
+
role TEXT,
|
|
2030
|
+
candidate_source TEXT,
|
|
2031
|
+
connection_kind TEXT,
|
|
2032
|
+
route_policy TEXT,
|
|
2033
|
+
runtime_access TEXT,
|
|
2034
|
+
health_status TEXT,
|
|
2035
|
+
health_json TEXT,
|
|
2036
|
+
reason TEXT,
|
|
2037
|
+
score REAL,
|
|
2038
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
2039
|
+
generated_at TEXT,
|
|
2040
|
+
updated_at TEXT DEFAULT (datetime('now')),
|
|
2041
|
+
metadata_json TEXT,
|
|
2042
|
+
PRIMARY KEY(policy_id, source, route_order)
|
|
2043
|
+
);
|
|
2044
|
+
|
|
1795
2045
|
CREATE INDEX IF NOT EXISTS idx_model_registry_provider ON model_registry(provider_id);
|
|
1796
2046
|
CREATE INDEX IF NOT EXISTS idx_model_evaluations_model ON model_evaluations(model_registry_id);
|
|
1797
2047
|
CREATE INDEX IF NOT EXISTS idx_model_evaluations_created ON model_evaluations(created_at);
|
|
@@ -1804,6 +2054,10 @@ function createTables() {
|
|
|
1804
2054
|
CREATE INDEX IF NOT EXISTS idx_model_usage_ledger_registry ON model_usage_ledger(model_registry_id, occurred_at);
|
|
1805
2055
|
CREATE INDEX IF NOT EXISTS idx_agent_runner_eval_runner ON agent_runner_evaluations(runner_id, task_type);
|
|
1806
2056
|
CREATE INDEX IF NOT EXISTS idx_agent_runner_eval_created ON agent_runner_evaluations(created_at);
|
|
2057
|
+
CREATE INDEX IF NOT EXISTS idx_model_routing_policy_routes_lookup
|
|
2058
|
+
ON model_routing_policy_routes(policy_id, source, enabled, route_order);
|
|
2059
|
+
CREATE INDEX IF NOT EXISTS idx_model_routing_policy_routes_provider
|
|
2060
|
+
ON model_routing_policy_routes(provider_type, model_id);
|
|
1807
2061
|
|
|
1808
2062
|
-- Benchmark evaluation runs
|
|
1809
2063
|
CREATE TABLE IF NOT EXISTS eval_benchmark_runs (
|
|
@@ -3266,24 +3520,288 @@ function listExchanges({ limit } = {}) {
|
|
|
3266
3520
|
return getDb().prepare(sql).all();
|
|
3267
3521
|
}
|
|
3268
3522
|
|
|
3523
|
+
// -- Runtime Event Ledger --
|
|
3524
|
+
|
|
3525
|
+
const RUNTIME_EVENT_TEXT_LIMIT = 240;
|
|
3526
|
+
const RUNTIME_EVENT_PAYLOAD_LIMIT = 2000;
|
|
3527
|
+
|
|
3528
|
+
function _safeRuntimeText(value, limit = RUNTIME_EVENT_TEXT_LIMIT) {
|
|
3529
|
+
if (value == null) return null;
|
|
3530
|
+
return String(value)
|
|
3531
|
+
.replace(/\/Users\/[^/\s]+/g, '/Users/<user>')
|
|
3532
|
+
.replace(/[A-Za-z]:\\Users\\[^\\\s]+/g, 'C:\\Users\\<user>')
|
|
3533
|
+
.replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, '<uuid>')
|
|
3534
|
+
.replace(/\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi, '<email>')
|
|
3535
|
+
.slice(0, limit);
|
|
3536
|
+
}
|
|
3537
|
+
|
|
3538
|
+
function _safeRuntimeDuration(value) {
|
|
3539
|
+
const n = Number(value);
|
|
3540
|
+
return Number.isFinite(n) ? Math.max(0, Math.trunc(n)) : null;
|
|
3541
|
+
}
|
|
3542
|
+
|
|
3543
|
+
function _runtimeEventPayload(event = {}) {
|
|
3544
|
+
const payload = {};
|
|
3545
|
+
const scalarKeys = [
|
|
3546
|
+
'provider',
|
|
3547
|
+
'model',
|
|
3548
|
+
'reason',
|
|
3549
|
+
'route',
|
|
3550
|
+
'attempt',
|
|
3551
|
+
'turn',
|
|
3552
|
+
'progressId',
|
|
3553
|
+
'approvalId',
|
|
3554
|
+
'runtimeProfile',
|
|
3555
|
+
];
|
|
3556
|
+
for (const key of scalarKeys) {
|
|
3557
|
+
const value = event[key];
|
|
3558
|
+
if (value == null) continue;
|
|
3559
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
3560
|
+
payload[key] = value;
|
|
3561
|
+
} else if (typeof value === 'string') {
|
|
3562
|
+
payload[key] = _safeRuntimeText(value, 160);
|
|
3563
|
+
}
|
|
3564
|
+
}
|
|
3565
|
+
if (event.detail && typeof event.detail === 'object') {
|
|
3566
|
+
const detail = {};
|
|
3567
|
+
for (const key of ['tool', 'status', 'kind', 'phase']) {
|
|
3568
|
+
if (event.detail[key] != null) detail[key] = _safeRuntimeText(event.detail[key], 120);
|
|
3569
|
+
}
|
|
3570
|
+
if (Object.keys(detail).length) payload.detail = detail;
|
|
3571
|
+
}
|
|
3572
|
+
let text = JSON.stringify(payload);
|
|
3573
|
+
if (text.length > RUNTIME_EVENT_PAYLOAD_LIMIT) {
|
|
3574
|
+
text = JSON.stringify({ truncated: true, size: text.length });
|
|
3575
|
+
}
|
|
3576
|
+
return text === '{}' ? null : text;
|
|
3577
|
+
}
|
|
3578
|
+
|
|
3579
|
+
function appendRuntimeEvent(event = {}, dbHandle = null) {
|
|
3580
|
+
const d = dbHandle || getDb();
|
|
3581
|
+
const id = event.id || uuidv4();
|
|
3582
|
+
const eventType = _safeRuntimeText(event.event_type || event.type || 'event', 80) || 'event';
|
|
3583
|
+
const source = _safeRuntimeText(event.source || 'wall-e', 80) || 'wall-e';
|
|
3584
|
+
const occurredAt = event.occurred_at || new Date().toISOString();
|
|
3585
|
+
const toolName = _safeRuntimeText(event.tool_name || event.tool || event.toolName || event.detail?.tool, 120);
|
|
3586
|
+
const summary = _safeRuntimeText(event.summary || event.message || event.title, RUNTIME_EVENT_TEXT_LIMIT);
|
|
3587
|
+
const status = _safeRuntimeText(event.status || event.state, 80);
|
|
3588
|
+
const durationMs = _safeRuntimeDuration(event.duration_ms ?? event.durationMs ?? event.elapsedMs);
|
|
3589
|
+
const payloadJson = _runtimeEventPayload(event);
|
|
3590
|
+
d.prepare(`
|
|
3591
|
+
INSERT INTO runtime_events (
|
|
3592
|
+
id, source, event_type, session_id, channel, turn_id, message_id,
|
|
3593
|
+
tool_name, status, summary, duration_ms, payload_json, occurred_at
|
|
3594
|
+
)
|
|
3595
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
3596
|
+
`).run(
|
|
3597
|
+
id,
|
|
3598
|
+
source,
|
|
3599
|
+
eventType,
|
|
3600
|
+
_safeRuntimeText(event.session_id || event.sessionId, 120),
|
|
3601
|
+
_safeRuntimeText(event.channel, 80),
|
|
3602
|
+
_safeRuntimeText(event.turn_id || event.turnId, 120),
|
|
3603
|
+
_safeRuntimeText(event.message_id || event.messageId, 120),
|
|
3604
|
+
toolName,
|
|
3605
|
+
status,
|
|
3606
|
+
summary,
|
|
3607
|
+
durationMs,
|
|
3608
|
+
payloadJson,
|
|
3609
|
+
occurredAt
|
|
3610
|
+
);
|
|
3611
|
+
return { id };
|
|
3612
|
+
}
|
|
3613
|
+
|
|
3614
|
+
function listRuntimeEvents({ session_id, channel, event_type, tool_name, limit } = {}) {
|
|
3615
|
+
const params = [];
|
|
3616
|
+
const conditions = [];
|
|
3617
|
+
if (session_id) { conditions.push('session_id = ?'); params.push(session_id); }
|
|
3618
|
+
if (channel) { conditions.push('channel = ?'); params.push(channel); }
|
|
3619
|
+
if (event_type) { conditions.push('event_type = ?'); params.push(event_type); }
|
|
3620
|
+
if (tool_name) { conditions.push('tool_name = ?'); params.push(tool_name); }
|
|
3621
|
+
const lim = Math.min(Math.max(Number(limit) || 100, 1), 1000);
|
|
3622
|
+
params.push(lim);
|
|
3623
|
+
const where = conditions.length ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
3624
|
+
return getDb().prepare(`
|
|
3625
|
+
SELECT * FROM runtime_events
|
|
3626
|
+
${where}
|
|
3627
|
+
ORDER BY occurred_at DESC, created_at DESC
|
|
3628
|
+
LIMIT ?
|
|
3629
|
+
`).all(...params);
|
|
3630
|
+
}
|
|
3631
|
+
|
|
3269
3632
|
// ── Chat Messages ──
|
|
3270
3633
|
|
|
3271
|
-
function insertChatMessage({ role, content, channel, session_id, id, attachments, model_id, model_provider }) {
|
|
3634
|
+
function insertChatMessage({ role, content, channel, session_id, id, parent_uuid, attachments, model_id, model_provider }) {
|
|
3272
3635
|
const messageId = id || uuidv4();
|
|
3273
3636
|
const attachmentsJson = attachments
|
|
3274
3637
|
? (typeof attachments === 'string' ? attachments : JSON.stringify(attachments))
|
|
3275
3638
|
: null;
|
|
3639
|
+
// parent_uuid threads the durable branch tree (the same model the session view uses):
|
|
3640
|
+
// each message points at the message it follows. When the caller doesn't specify one,
|
|
3641
|
+
// default to the session's current newest message (linear append). An explicit value
|
|
3642
|
+
// (incl. null for a root, or a prior message's id for an edit-resend sibling) wins.
|
|
3643
|
+
const parentUuid = parent_uuid !== undefined
|
|
3644
|
+
? (parent_uuid || null)
|
|
3645
|
+
: _latestChatMessageId(session_id);
|
|
3646
|
+
// INSERT OR REPLACE keeps a client-minted id idempotent (a re-sent same-id is a no-op
|
|
3647
|
+
// update, not a duplicate) — append-only with stable identity, no destructive re-sync.
|
|
3276
3648
|
// model_id / model_provider record which model produced an assistant turn so the
|
|
3277
|
-
// conversation log / token badge can attribute it.
|
|
3278
|
-
// but were never written; null-coalesced so user/system rows stay NULL.
|
|
3649
|
+
// conversation log / token badge can attribute it.
|
|
3279
3650
|
getDb().prepare(`
|
|
3280
|
-
INSERT INTO chat_messages (id, role, content, channel, session_id, attachments, model_id, model_provider, created_at)
|
|
3281
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))
|
|
3651
|
+
INSERT OR REPLACE INTO chat_messages (id, role, content, channel, session_id, parent_uuid, attachments, model_id, model_provider, created_at)
|
|
3652
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))
|
|
3282
3653
|
`).run(
|
|
3283
|
-
messageId, role, content, channel || 'ctm', session_id || null, attachmentsJson,
|
|
3654
|
+
messageId, role, content, channel || 'ctm', session_id || null, parentUuid, attachmentsJson,
|
|
3284
3655
|
model_id || null, model_provider || null,
|
|
3285
3656
|
);
|
|
3286
|
-
return { id: messageId };
|
|
3657
|
+
return { id: messageId, parent_uuid: parentUuid };
|
|
3658
|
+
}
|
|
3659
|
+
|
|
3660
|
+
// Chain each session's messages by their parent_uuid in the SAME order the chat displays
|
|
3661
|
+
// them — listChatMessages renders by (created_at ASC, id ASC), so the tree MUST chain in
|
|
3662
|
+
// that order too, or the edges contradict the visual order and buildMessageTree's bridge
|
|
3663
|
+
// invents a spurious fork (observed on real imported data whose rowid order ≠ created_at
|
|
3664
|
+
// order for same-second rows). Each row points at its display-predecessor; the first
|
|
3665
|
+
// displayed row of a session stays NULL = a root.
|
|
3666
|
+
//
|
|
3667
|
+
// Default (overwrite:false) only fills NULLs — the one-time forward backfill for upgraders
|
|
3668
|
+
// whose rows have no parent yet; idempotent and safe to re-run. overwrite:true re-derives
|
|
3669
|
+
// EVERY row's parent — a corrective pass for a DB that was backfilled in the wrong order
|
|
3670
|
+
// before this fix. Overwrite is only safe at the tree rollout boundary, where the chat is
|
|
3671
|
+
// still purely linear (no genuine edit-resend siblings exist yet); migrations run once, so
|
|
3672
|
+
// branches created AFTER the rollout are never touched. Pass the migrating db handle, or
|
|
3673
|
+
// omit to use the live db.
|
|
3674
|
+
function backfillChatMessageParents(db, { overwrite = false } = {}) {
|
|
3675
|
+
const d = db || getDb();
|
|
3676
|
+
// overwrite: re-derive EVERY row (the display-first row of each session is reset to NULL =
|
|
3677
|
+
// root). Default: only fill rows that are still NULL and have a display-predecessor (the
|
|
3678
|
+
// first row legitimately stays NULL, so it is left untouched).
|
|
3679
|
+
const where = overwrite
|
|
3680
|
+
? ''
|
|
3681
|
+
: 'WHERE parent_uuid IS NULL AND (SELECT prev_id FROM ordered o WHERE o.id = chat_messages.id) IS NOT NULL';
|
|
3682
|
+
d.exec(`
|
|
3683
|
+
WITH ordered AS (
|
|
3684
|
+
SELECT id, LAG(id) OVER (PARTITION BY session_id ORDER BY created_at ASC, id ASC) AS prev_id
|
|
3685
|
+
FROM chat_messages
|
|
3686
|
+
)
|
|
3687
|
+
UPDATE chat_messages
|
|
3688
|
+
SET parent_uuid = (SELECT prev_id FROM ordered o WHERE o.id = chat_messages.id)
|
|
3689
|
+
${where};
|
|
3690
|
+
`);
|
|
3691
|
+
}
|
|
3692
|
+
|
|
3693
|
+
// The newest message id for a session (its current head), used as the default parent
|
|
3694
|
+
// for a linearly-appended message. Orders by the implicit rowid (true insertion order):
|
|
3695
|
+
// a new send always has the max rowid AND the max created_at, so this returns the row the
|
|
3696
|
+
// user sees last — and unlike (created_at, id) it is immune to the same-second tiebreak
|
|
3697
|
+
// where a user's 'umsg-…' id sorts above the assistant's hex uuid (which would wrongly
|
|
3698
|
+
// make the user the head and fork the next turn). The one-time historical backfill, by
|
|
3699
|
+
// contrast, must follow the DISPLAY order — see backfillChatMessageParents.
|
|
3700
|
+
// Returns null when the session has no messages yet.
|
|
3701
|
+
function _latestChatMessageId(session_id) {
|
|
3702
|
+
if (!session_id) return null;
|
|
3703
|
+
const row = getDb().prepare(
|
|
3704
|
+
'SELECT id FROM chat_messages WHERE session_id = ? ORDER BY rowid DESC LIMIT 1'
|
|
3705
|
+
).get(session_id);
|
|
3706
|
+
return row ? row.id : null;
|
|
3707
|
+
}
|
|
3708
|
+
|
|
3709
|
+
function _jsonString(value) {
|
|
3710
|
+
if (typeof value === 'string') return value;
|
|
3711
|
+
try { return JSON.stringify(value == null ? {} : value); } catch (_) { return '{}'; }
|
|
3712
|
+
}
|
|
3713
|
+
|
|
3714
|
+
function _jsonValue(value, fallback) {
|
|
3715
|
+
if (value == null || value === '') return fallback;
|
|
3716
|
+
if (typeof value !== 'string') return value;
|
|
3717
|
+
try { return JSON.parse(value); } catch (_) { return fallback; }
|
|
3718
|
+
}
|
|
3719
|
+
|
|
3720
|
+
function _nextChatPartOrdinal({ message_id, session_id }) {
|
|
3721
|
+
const db = getDb();
|
|
3722
|
+
if (message_id) {
|
|
3723
|
+
const row = db.prepare(
|
|
3724
|
+
'SELECT COALESCE(MAX(ordinal) + 1, 0) AS next FROM chat_message_parts WHERE message_id = ?'
|
|
3725
|
+
).get(message_id);
|
|
3726
|
+
return row?.next || 0;
|
|
3727
|
+
}
|
|
3728
|
+
if (session_id) {
|
|
3729
|
+
const row = db.prepare(
|
|
3730
|
+
'SELECT COALESCE(MAX(ordinal) + 1, 0) AS next FROM chat_message_parts WHERE session_id = ? AND message_id IS NULL'
|
|
3731
|
+
).get(session_id);
|
|
3732
|
+
return row?.next || 0;
|
|
3733
|
+
}
|
|
3734
|
+
return 0;
|
|
3735
|
+
}
|
|
3736
|
+
|
|
3737
|
+
function _hydrateChatPart(row) {
|
|
3738
|
+
if (!row) return null;
|
|
3739
|
+
return {
|
|
3740
|
+
...row,
|
|
3741
|
+
data: _jsonValue(row.data_json, {}),
|
|
3742
|
+
};
|
|
3743
|
+
}
|
|
3744
|
+
|
|
3745
|
+
function insertChatMessagePart({
|
|
3746
|
+
id,
|
|
3747
|
+
session_id,
|
|
3748
|
+
message_id,
|
|
3749
|
+
parent_message_id,
|
|
3750
|
+
turn_id,
|
|
3751
|
+
role,
|
|
3752
|
+
part_type,
|
|
3753
|
+
tool_name,
|
|
3754
|
+
status,
|
|
3755
|
+
ordinal,
|
|
3756
|
+
title,
|
|
3757
|
+
summary,
|
|
3758
|
+
content,
|
|
3759
|
+
data,
|
|
3760
|
+
data_json,
|
|
3761
|
+
model_projection,
|
|
3762
|
+
}) {
|
|
3763
|
+
const partType = String(part_type || '').trim();
|
|
3764
|
+
if (!partType) throw new Error('Chat message part requires part_type');
|
|
3765
|
+
const partId = id || uuidv4();
|
|
3766
|
+
const ord = Number.isInteger(ordinal)
|
|
3767
|
+
? ordinal
|
|
3768
|
+
: _nextChatPartOrdinal({ message_id, session_id });
|
|
3769
|
+
getDb().prepare(`
|
|
3770
|
+
INSERT OR REPLACE INTO chat_message_parts (
|
|
3771
|
+
id, session_id, message_id, parent_message_id, turn_id, role, part_type,
|
|
3772
|
+
tool_name, status, ordinal, title, summary, content, data_json,
|
|
3773
|
+
model_projection, created_at
|
|
3774
|
+
)
|
|
3775
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))
|
|
3776
|
+
`).run(
|
|
3777
|
+
partId,
|
|
3778
|
+
session_id || null,
|
|
3779
|
+
message_id || null,
|
|
3780
|
+
parent_message_id || null,
|
|
3781
|
+
turn_id || null,
|
|
3782
|
+
role || null,
|
|
3783
|
+
partType,
|
|
3784
|
+
tool_name || null,
|
|
3785
|
+
status || null,
|
|
3786
|
+
ord,
|
|
3787
|
+
title || null,
|
|
3788
|
+
summary || null,
|
|
3789
|
+
content || null,
|
|
3790
|
+
data_json != null ? _jsonString(data_json) : _jsonString(data),
|
|
3791
|
+
model_projection || null,
|
|
3792
|
+
);
|
|
3793
|
+
return { id: partId, ordinal: ord };
|
|
3794
|
+
}
|
|
3795
|
+
|
|
3796
|
+
function listChatMessageParts({ session_id, message_id, limit } = {}) {
|
|
3797
|
+
const params = [];
|
|
3798
|
+
let where = ' WHERE 1=1';
|
|
3799
|
+
if (session_id) { where += ' AND session_id = ?'; params.push(session_id); }
|
|
3800
|
+
if (message_id) { where += ' AND message_id = ?'; params.push(message_id); }
|
|
3801
|
+
const lim = limit ? Math.min(Math.max(Number(limit) || 0, 1), 1000) : 0;
|
|
3802
|
+
const sql = `SELECT * FROM chat_message_parts${where} ORDER BY created_at ASC, ordinal ASC, id ASC${lim ? ' LIMIT ?' : ''}`;
|
|
3803
|
+
if (lim) params.push(lim);
|
|
3804
|
+
return getDb().prepare(sql).all(...params).map(_hydrateChatPart).filter(Boolean);
|
|
3287
3805
|
}
|
|
3288
3806
|
|
|
3289
3807
|
function listChatMessages({ session_id, channel, limit } = {}) {
|
|
@@ -3302,6 +3820,26 @@ function listChatMessages({ session_id, channel, limit } = {}) {
|
|
|
3302
3820
|
return getDb().prepare(`SELECT * FROM chat_messages${where} ORDER BY created_at ASC, id ASC`).all(...params);
|
|
3303
3821
|
}
|
|
3304
3822
|
|
|
3823
|
+
function listChatMessagesWithParts({ session_id, channel, limit } = {}) {
|
|
3824
|
+
const rows = listChatMessages({ session_id, channel, limit });
|
|
3825
|
+
if (rows.length === 0) return rows;
|
|
3826
|
+
const messageIds = rows.map((r) => r.id).filter(Boolean);
|
|
3827
|
+
if (messageIds.length === 0) return rows.map((r) => ({ ...r, parts: [] }));
|
|
3828
|
+
const placeholders = messageIds.map(() => '?').join(',');
|
|
3829
|
+
const parts = getDb().prepare(`
|
|
3830
|
+
SELECT * FROM chat_message_parts
|
|
3831
|
+
WHERE message_id IN (${placeholders})
|
|
3832
|
+
ORDER BY created_at ASC, ordinal ASC, id ASC
|
|
3833
|
+
`).all(...messageIds).map(_hydrateChatPart).filter(Boolean);
|
|
3834
|
+
const byMessage = new Map();
|
|
3835
|
+
for (const part of parts) {
|
|
3836
|
+
const key = part.message_id || '';
|
|
3837
|
+
if (!byMessage.has(key)) byMessage.set(key, []);
|
|
3838
|
+
byMessage.get(key).push(part);
|
|
3839
|
+
}
|
|
3840
|
+
return rows.map((row) => ({ ...row, parts: byMessage.get(row.id) || [] }));
|
|
3841
|
+
}
|
|
3842
|
+
|
|
3305
3843
|
function searchChatMessages({ query, limit } = {}) {
|
|
3306
3844
|
if (!query || !query.trim()) return [];
|
|
3307
3845
|
const terms = query.trim().split(/\s+/).filter(Boolean);
|
|
@@ -3329,35 +3867,16 @@ function deleteChatMessages({ session_id, user_content, assistant_content }) {
|
|
|
3329
3867
|
}
|
|
3330
3868
|
|
|
3331
3869
|
function clearChatSession(session_id) {
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
function saveChatBranches(session_id, branches, active, history, updated_at_ms) {
|
|
3338
|
-
const updatedAtMs = Number.isFinite(Number(updated_at_ms)) && Number(updated_at_ms) > 0
|
|
3339
|
-
? Math.floor(Number(updated_at_ms))
|
|
3340
|
-
: Date.now();
|
|
3341
|
-
getDb().prepare(`
|
|
3342
|
-
INSERT INTO chat_branches (session_id, branches, active, history, updated_at_ms)
|
|
3343
|
-
VALUES (?, ?, ?, ?, ?)
|
|
3344
|
-
ON CONFLICT(session_id) DO UPDATE SET branches = excluded.branches, active = excluded.active, history = excluded.history, updated_at_ms = excluded.updated_at_ms
|
|
3345
|
-
`).run(session_id, JSON.stringify(branches), JSON.stringify(active), JSON.stringify(history || []), updatedAtMs);
|
|
3870
|
+
const db = getDb();
|
|
3871
|
+
runImmediateTransaction(db, (targetSessionId) => {
|
|
3872
|
+
db.prepare('DELETE FROM chat_message_parts WHERE session_id = ?').run(targetSessionId);
|
|
3873
|
+
db.prepare('DELETE FROM chat_messages WHERE session_id = ?').run(targetSessionId);
|
|
3874
|
+
}, session_id);
|
|
3346
3875
|
}
|
|
3347
3876
|
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
try {
|
|
3352
|
-
return {
|
|
3353
|
-
branches: JSON.parse(row.branches),
|
|
3354
|
-
active: JSON.parse(row.active),
|
|
3355
|
-
history: JSON.parse(row.history || '[]'),
|
|
3356
|
-
updated_at_ms: Number(row.updated_at_ms || 0) || 0,
|
|
3357
|
-
exists: true,
|
|
3358
|
-
};
|
|
3359
|
-
} catch (e) { return { branches: {}, active: {}, history: [], exists: true }; }
|
|
3360
|
-
}
|
|
3877
|
+
// ── Chat Branches ── (retired: the conversation is a durable parentUuid tree built
|
|
3878
|
+
// from chat_messages.parent_uuid; the only persisted branch state is the head-leaf
|
|
3879
|
+
// KV walle_session_head:<sid>. The index-blob tables are dropped in migration 21.)
|
|
3361
3880
|
|
|
3362
3881
|
// -- Skills CRUD --
|
|
3363
3882
|
|
|
@@ -4499,6 +5018,10 @@ function _normalizeUsageTokens({ usage, inputTokens, outputTokens, cachedInputTo
|
|
|
4499
5018
|
?? raw.cached_input
|
|
4500
5019
|
?? raw.cachedInput
|
|
4501
5020
|
?? raw.cached_input_tokens
|
|
5021
|
+
?? raw.cache_read_input_tokens
|
|
5022
|
+
?? raw.cacheReadInputTokens
|
|
5023
|
+
?? raw.cache_read
|
|
5024
|
+
?? raw.cacheRead
|
|
4502
5025
|
?? raw.prompt_tokens_details?.cached_tokens
|
|
4503
5026
|
);
|
|
4504
5027
|
const reasoning = _nonNegativeInteger(
|
|
@@ -4648,7 +5171,7 @@ function recordModelUsage({
|
|
|
4648
5171
|
modelId,
|
|
4649
5172
|
modelRegistryId,
|
|
4650
5173
|
inputTokens: tokens.inputTokens,
|
|
4651
|
-
outputTokens: tokens.outputTokens,
|
|
5174
|
+
outputTokens: tokens.outputTokens + tokens.reasoningOutputTokens,
|
|
4652
5175
|
})
|
|
4653
5176
|
: {
|
|
4654
5177
|
costUsd: explicitCost,
|
|
@@ -4786,6 +5309,172 @@ function summarizeModelUsageLedger(filters = {}) {
|
|
|
4786
5309
|
return rows;
|
|
4787
5310
|
}
|
|
4788
5311
|
|
|
5312
|
+
function _compactUsageTokens(n) {
|
|
5313
|
+
const v = Math.max(0, Math.round(Number(n) || 0));
|
|
5314
|
+
if (v < 1000) return String(v);
|
|
5315
|
+
if (v < 1_000_000) {
|
|
5316
|
+
const k = v / 1000;
|
|
5317
|
+
return (k < 10 ? k.toFixed(1) : Math.round(k)) + 'k';
|
|
5318
|
+
}
|
|
5319
|
+
const m = v / 1_000_000;
|
|
5320
|
+
return (m < 10 ? m.toFixed(1) : Math.round(m)) + 'M';
|
|
5321
|
+
}
|
|
5322
|
+
|
|
5323
|
+
function _ledgerCountedTokens(row) {
|
|
5324
|
+
if (!row) return 0;
|
|
5325
|
+
const input = _nonNegativeInteger(row.input_tokens);
|
|
5326
|
+
const cached = _nonNegativeInteger(row.cached_input_tokens);
|
|
5327
|
+
const output = _nonNegativeInteger(row.output_tokens);
|
|
5328
|
+
const reasoning = _nonNegativeInteger(row.reasoning_output_tokens);
|
|
5329
|
+
return Math.max(0, input - cached) + output + reasoning;
|
|
5330
|
+
}
|
|
5331
|
+
|
|
5332
|
+
function _usageRouteLabel(row) {
|
|
5333
|
+
const provider = String(row?.provider_type || row?.provider_id || '').trim();
|
|
5334
|
+
const model = String(row?.model_id || row?.model_registry_id || '').trim();
|
|
5335
|
+
if (provider && model) return `${provider} / ${model}`;
|
|
5336
|
+
return model || provider || 'unknown route';
|
|
5337
|
+
}
|
|
5338
|
+
|
|
5339
|
+
function _modelContextWindowForUsageRow(row) {
|
|
5340
|
+
const registry = findModelRegistryForUsage({
|
|
5341
|
+
providerType: row?.provider_type,
|
|
5342
|
+
providerId: row?.provider_id,
|
|
5343
|
+
modelId: row?.model_id,
|
|
5344
|
+
modelRegistryId: row?.model_registry_id,
|
|
5345
|
+
});
|
|
5346
|
+
return _nonNegativeInteger(registry?.max_context_tokens || registry?.context_window || registry?.contextWindow);
|
|
5347
|
+
}
|
|
5348
|
+
|
|
5349
|
+
function summarizeSessionModelUsage({ sessionId } = {}) {
|
|
5350
|
+
const id = String(sessionId || '').trim();
|
|
5351
|
+
if (!id) return null;
|
|
5352
|
+
const db = getDb();
|
|
5353
|
+
const tokenWhere = `
|
|
5354
|
+
session_id = ?
|
|
5355
|
+
AND (
|
|
5356
|
+
input_tokens > 0
|
|
5357
|
+
OR output_tokens > 0
|
|
5358
|
+
OR total_tokens > 0
|
|
5359
|
+
OR cached_input_tokens > 0
|
|
5360
|
+
OR reasoning_output_tokens > 0
|
|
5361
|
+
)
|
|
5362
|
+
`;
|
|
5363
|
+
const grouped = db.prepare(`
|
|
5364
|
+
SELECT
|
|
5365
|
+
provider_type,
|
|
5366
|
+
provider_id,
|
|
5367
|
+
model_id,
|
|
5368
|
+
model_registry_id,
|
|
5369
|
+
gateway_type,
|
|
5370
|
+
route_label,
|
|
5371
|
+
COUNT(*) AS calls,
|
|
5372
|
+
SUM(input_tokens) AS input_tokens,
|
|
5373
|
+
SUM(output_tokens) AS output_tokens,
|
|
5374
|
+
SUM(total_tokens) AS raw_total_tokens,
|
|
5375
|
+
SUM(cached_input_tokens) AS cached_input_tokens,
|
|
5376
|
+
SUM(reasoning_output_tokens) AS reasoning_output_tokens,
|
|
5377
|
+
SUM(cost_usd) AS cost_usd,
|
|
5378
|
+
SUM(CASE WHEN cost_usd IS NULL THEN 1 ELSE 0 END) AS missing_cost_rows,
|
|
5379
|
+
MIN(occurred_at) AS first_seen_at,
|
|
5380
|
+
MAX(occurred_at) AS last_seen_at
|
|
5381
|
+
FROM model_usage_ledger
|
|
5382
|
+
WHERE ${tokenWhere}
|
|
5383
|
+
GROUP BY provider_type, provider_id, model_id, model_registry_id, gateway_type, route_label
|
|
5384
|
+
ORDER BY datetime(last_seen_at) DESC, provider_type, model_id
|
|
5385
|
+
`).all(id);
|
|
5386
|
+
if (!grouped.length) return null;
|
|
5387
|
+
|
|
5388
|
+
const latest = db.prepare(`
|
|
5389
|
+
SELECT *
|
|
5390
|
+
FROM model_usage_ledger
|
|
5391
|
+
WHERE ${tokenWhere}
|
|
5392
|
+
ORDER BY datetime(occurred_at) DESC, id DESC
|
|
5393
|
+
LIMIT 1
|
|
5394
|
+
`).get(id) || null;
|
|
5395
|
+
|
|
5396
|
+
let totalTokens = 0;
|
|
5397
|
+
let inputTokens = 0;
|
|
5398
|
+
let outputTokens = 0;
|
|
5399
|
+
let cachedInputTokens = 0;
|
|
5400
|
+
let reasoningOutputTokens = 0;
|
|
5401
|
+
let calls = 0;
|
|
5402
|
+
let costUsd = 0;
|
|
5403
|
+
let hasCost = false;
|
|
5404
|
+
let missingCostRows = 0;
|
|
5405
|
+
const routes = grouped.map((row) => {
|
|
5406
|
+
const routeTotal = _ledgerCountedTokens(row);
|
|
5407
|
+
const rowCost = _finiteNumberOrNull(row.cost_usd);
|
|
5408
|
+
const rowMissingCost = _nonNegativeInteger(row.missing_cost_rows);
|
|
5409
|
+
totalTokens += routeTotal;
|
|
5410
|
+
inputTokens += _nonNegativeInteger(row.input_tokens);
|
|
5411
|
+
outputTokens += _nonNegativeInteger(row.output_tokens);
|
|
5412
|
+
cachedInputTokens += _nonNegativeInteger(row.cached_input_tokens);
|
|
5413
|
+
reasoningOutputTokens += _nonNegativeInteger(row.reasoning_output_tokens);
|
|
5414
|
+
calls += _nonNegativeInteger(row.calls);
|
|
5415
|
+
missingCostRows += rowMissingCost;
|
|
5416
|
+
if (rowCost != null) {
|
|
5417
|
+
hasCost = true;
|
|
5418
|
+
costUsd += rowCost;
|
|
5419
|
+
}
|
|
5420
|
+
return {
|
|
5421
|
+
provider_type: row.provider_type || null,
|
|
5422
|
+
provider_id: row.provider_id || null,
|
|
5423
|
+
model_id: row.model_id || null,
|
|
5424
|
+
model_registry_id: row.model_registry_id || null,
|
|
5425
|
+
gateway_type: row.gateway_type || null,
|
|
5426
|
+
route_label: row.route_label || _usageRouteLabel(row),
|
|
5427
|
+
calls: _nonNegativeInteger(row.calls),
|
|
5428
|
+
total_tokens: routeTotal,
|
|
5429
|
+
raw_total_tokens: _nonNegativeInteger(row.raw_total_tokens),
|
|
5430
|
+
input_tokens: _nonNegativeInteger(row.input_tokens),
|
|
5431
|
+
output_tokens: _nonNegativeInteger(row.output_tokens),
|
|
5432
|
+
cached_input_tokens: _nonNegativeInteger(row.cached_input_tokens),
|
|
5433
|
+
reasoning_output_tokens: _nonNegativeInteger(row.reasoning_output_tokens),
|
|
5434
|
+
cost_usd: rowCost,
|
|
5435
|
+
missing_cost_rows: rowMissingCost,
|
|
5436
|
+
first_seen_at: row.first_seen_at || null,
|
|
5437
|
+
last_seen_at: row.last_seen_at || null,
|
|
5438
|
+
};
|
|
5439
|
+
});
|
|
5440
|
+
|
|
5441
|
+
const latestContextTokens = latest
|
|
5442
|
+
? Math.max(
|
|
5443
|
+
_nonNegativeInteger(latest.total_tokens),
|
|
5444
|
+
_nonNegativeInteger(latest.input_tokens) + _nonNegativeInteger(latest.output_tokens) + _nonNegativeInteger(latest.reasoning_output_tokens),
|
|
5445
|
+
)
|
|
5446
|
+
: 0;
|
|
5447
|
+
const contextWindow = latest ? _modelContextWindowForUsageRow(latest) : 0;
|
|
5448
|
+
const contextPct = contextWindow > 0
|
|
5449
|
+
? Math.round((Math.min(latestContextTokens, contextWindow) / contextWindow) * 100)
|
|
5450
|
+
: 0;
|
|
5451
|
+
|
|
5452
|
+
return {
|
|
5453
|
+
session_id: id,
|
|
5454
|
+
source: 'walle-ledger',
|
|
5455
|
+
exact: true,
|
|
5456
|
+
calls,
|
|
5457
|
+
total_tokens: totalTokens,
|
|
5458
|
+
label: _compactUsageTokens(totalTokens),
|
|
5459
|
+
context_tokens: latestContextTokens,
|
|
5460
|
+
latest_context_tokens: latestContextTokens,
|
|
5461
|
+
context_window: contextWindow,
|
|
5462
|
+
context_pct: contextPct,
|
|
5463
|
+
input_tokens: inputTokens,
|
|
5464
|
+
output_tokens: outputTokens,
|
|
5465
|
+
cached_input_tokens: cachedInputTokens,
|
|
5466
|
+
reasoning_output_tokens: reasoningOutputTokens,
|
|
5467
|
+
cost_usd: hasCost ? costUsd : null,
|
|
5468
|
+
missing_cost_rows: missingCostRows,
|
|
5469
|
+
latest_provider_type: latest?.provider_type || null,
|
|
5470
|
+
latest_provider_id: latest?.provider_id || null,
|
|
5471
|
+
latest_model_id: latest?.model_id || null,
|
|
5472
|
+
latest_model_registry_id: latest?.model_registry_id || null,
|
|
5473
|
+
latest_at: latest?.occurred_at || null,
|
|
5474
|
+
routes,
|
|
5475
|
+
};
|
|
5476
|
+
}
|
|
5477
|
+
|
|
4789
5478
|
// -- Model Evaluations --
|
|
4790
5479
|
|
|
4791
5480
|
function getEvaluationDaysCutoff(days) {
|
|
@@ -4823,6 +5512,43 @@ function getModelScorecard(modelRegistryId, days = 30) {
|
|
|
4823
5512
|
`).get(...params);
|
|
4824
5513
|
}
|
|
4825
5514
|
|
|
5515
|
+
// Batched scorecard: one GROUP BY for ALL models instead of N per-model queries
|
|
5516
|
+
// (the scorecard tab has thousands of models). Returns a Map keyed by
|
|
5517
|
+
// model_registry_id with the SAME column shape as getModelScorecard(); callers
|
|
5518
|
+
// fill missing models with emptyModelScorecardStats().
|
|
5519
|
+
function getModelScorecardsBatch(days = 30) {
|
|
5520
|
+
const cutoff = getEvaluationDaysCutoff(days);
|
|
5521
|
+
const where = cutoff ? 'WHERE created_at >= ?' : '';
|
|
5522
|
+
const params = cutoff ? [cutoff] : [];
|
|
5523
|
+
const rows = getDb().prepare(`
|
|
5524
|
+
SELECT
|
|
5525
|
+
model_registry_id AS id,
|
|
5526
|
+
COUNT(*) AS total_evals,
|
|
5527
|
+
AVG(quality_score) AS avg_quality,
|
|
5528
|
+
AVG(latency_ms) AS avg_latency_ms,
|
|
5529
|
+
SUM(cost_estimate) AS total_cost,
|
|
5530
|
+
SUM(CASE WHEN was_selected = 1 THEN 1 ELSE 0 END) AS wins
|
|
5531
|
+
FROM model_evaluations
|
|
5532
|
+
${where}
|
|
5533
|
+
GROUP BY model_registry_id
|
|
5534
|
+
`).all(...params);
|
|
5535
|
+
const map = new Map();
|
|
5536
|
+
for (const row of rows) {
|
|
5537
|
+
const id = row.id;
|
|
5538
|
+
delete row.id;
|
|
5539
|
+
map.set(id, row);
|
|
5540
|
+
}
|
|
5541
|
+
return map;
|
|
5542
|
+
}
|
|
5543
|
+
|
|
5544
|
+
// Zero row matching what getModelScorecard returns for a model with no evals
|
|
5545
|
+
// (SQLite aggregates over zero rows: COUNT=0, AVG/SUM=null).
|
|
5546
|
+
function emptyModelScorecardStats() {
|
|
5547
|
+
// Matches getModelScorecard() over zero rows exactly: COUNT=0, all SUM/AVG=null
|
|
5548
|
+
// (SQLite SUM over no rows is NULL, not 0).
|
|
5549
|
+
return { total_evals: 0, avg_quality: null, avg_latency_ms: null, total_cost: null, wins: null };
|
|
5550
|
+
}
|
|
5551
|
+
|
|
4826
5552
|
function getModelScorecardForTask(modelRegistryId, taskType, days = 30) {
|
|
4827
5553
|
const cutoff = getEvaluationDaysCutoff(days);
|
|
4828
5554
|
let where = 'model_registry_id = ? AND task_type = ?';
|
|
@@ -4959,6 +5685,276 @@ function listModelDefaults() {
|
|
|
4959
5685
|
`).all();
|
|
4960
5686
|
}
|
|
4961
5687
|
|
|
5688
|
+
// -- Model Routing Policy --
|
|
5689
|
+
|
|
5690
|
+
const DEFAULT_MODEL_ROUTING_POLICY_ID = 'default';
|
|
5691
|
+
const VALID_MODEL_ROUTING_ROUTE_SOURCES = new Set(['user', 'generated', 'effective']);
|
|
5692
|
+
|
|
5693
|
+
function _normalizeModelRoutingPolicyId(policyId) {
|
|
5694
|
+
const id = String(policyId || DEFAULT_MODEL_ROUTING_POLICY_ID).trim();
|
|
5695
|
+
return id || DEFAULT_MODEL_ROUTING_POLICY_ID;
|
|
5696
|
+
}
|
|
5697
|
+
|
|
5698
|
+
function _boolToInt(value, fallback = true) {
|
|
5699
|
+
if (value === undefined || value === null || value === '') return fallback ? 1 : 0;
|
|
5700
|
+
return value === false || value === 0 || value === '0' || String(value).toLowerCase() === 'false' ? 0 : 1;
|
|
5701
|
+
}
|
|
5702
|
+
|
|
5703
|
+
function _routingPolicySettingsFromKv() {
|
|
5704
|
+
const rawMode = String(getKv('model_routing_policy_mode') || 'smart').trim().toLowerCase();
|
|
5705
|
+
const mode = rawMode === 'strict' ? 'strict' : 'smart';
|
|
5706
|
+
return {
|
|
5707
|
+
mode,
|
|
5708
|
+
cliTailEnabled: _boolToInt(getKv('model_routing_policy_cli_tail'), true),
|
|
5709
|
+
finalizationFallbackEnabled: _boolToInt(getKv('model_routing_policy_finalization_fallback'), true),
|
|
5710
|
+
lastGeneratedAt: getKv('model_routing_policy_last_refresh_at') || null,
|
|
5711
|
+
};
|
|
5712
|
+
}
|
|
5713
|
+
|
|
5714
|
+
function _modelRoutingPolicyRowToPublic(row) {
|
|
5715
|
+
if (!row) return null;
|
|
5716
|
+
return {
|
|
5717
|
+
id: row.id,
|
|
5718
|
+
mode: row.mode || 'smart',
|
|
5719
|
+
fallback_enabled: (row.mode || 'smart') !== 'strict',
|
|
5720
|
+
cli_tail: row.cli_tail_enabled !== 0,
|
|
5721
|
+
finalization_fallback: row.finalization_fallback_enabled !== 0,
|
|
5722
|
+
generated_refresh_interval_ms: row.generated_refresh_interval_ms == null ? null : Number(row.generated_refresh_interval_ms),
|
|
5723
|
+
last_generated_at: row.last_generated_at || null,
|
|
5724
|
+
last_effective_at: row.last_effective_at || null,
|
|
5725
|
+
last_error: row.last_error || null,
|
|
5726
|
+
stale_reason: row.stale_reason || null,
|
|
5727
|
+
created_at: row.created_at || null,
|
|
5728
|
+
updated_at: row.updated_at || null,
|
|
5729
|
+
};
|
|
5730
|
+
}
|
|
5731
|
+
|
|
5732
|
+
function ensureModelRoutingPolicy(policyId = DEFAULT_MODEL_ROUTING_POLICY_ID) {
|
|
5733
|
+
const id = _normalizeModelRoutingPolicyId(policyId);
|
|
5734
|
+
const legacy = _routingPolicySettingsFromKv();
|
|
5735
|
+
getDb().prepare(`
|
|
5736
|
+
INSERT OR IGNORE INTO model_routing_policies
|
|
5737
|
+
(id, mode, cli_tail_enabled, finalization_fallback_enabled, last_generated_at, updated_at)
|
|
5738
|
+
VALUES (?, ?, ?, ?, ?, datetime('now'))
|
|
5739
|
+
`).run(
|
|
5740
|
+
id,
|
|
5741
|
+
legacy.mode,
|
|
5742
|
+
legacy.cliTailEnabled,
|
|
5743
|
+
legacy.finalizationFallbackEnabled,
|
|
5744
|
+
legacy.lastGeneratedAt
|
|
5745
|
+
);
|
|
5746
|
+
return _modelRoutingPolicyRowToPublic(getDb().prepare('SELECT * FROM model_routing_policies WHERE id = ?').get(id));
|
|
5747
|
+
}
|
|
5748
|
+
|
|
5749
|
+
function updateModelRoutingPolicy(policyId = DEFAULT_MODEL_ROUTING_POLICY_ID, patch = {}) {
|
|
5750
|
+
const id = _normalizeModelRoutingPolicyId(policyId);
|
|
5751
|
+
ensureModelRoutingPolicy(id);
|
|
5752
|
+
const current = getDb().prepare('SELECT * FROM model_routing_policies WHERE id = ?').get(id);
|
|
5753
|
+
const next = {
|
|
5754
|
+
mode: patch.mode === undefined ? current.mode : String(patch.mode || 'smart').trim().toLowerCase(),
|
|
5755
|
+
cliTailEnabled: patch.cli_tail === undefined && patch.cliTail === undefined
|
|
5756
|
+
? current.cli_tail_enabled
|
|
5757
|
+
: _boolToInt(patch.cli_tail === undefined ? patch.cliTail : patch.cli_tail, true),
|
|
5758
|
+
finalizationFallbackEnabled: patch.finalization_fallback === undefined && patch.finalizationFallback === undefined
|
|
5759
|
+
? current.finalization_fallback_enabled
|
|
5760
|
+
: _boolToInt(patch.finalization_fallback === undefined ? patch.finalizationFallback : patch.finalization_fallback, true),
|
|
5761
|
+
generatedRefreshIntervalMs: patch.generated_refresh_interval_ms === undefined
|
|
5762
|
+
? current.generated_refresh_interval_ms
|
|
5763
|
+
: patch.generated_refresh_interval_ms,
|
|
5764
|
+
lastGeneratedAt: patch.last_generated_at === undefined ? current.last_generated_at : patch.last_generated_at,
|
|
5765
|
+
lastEffectiveAt: patch.last_effective_at === undefined ? current.last_effective_at : patch.last_effective_at,
|
|
5766
|
+
lastError: patch.last_error === undefined ? current.last_error : patch.last_error,
|
|
5767
|
+
staleReason: patch.stale_reason === undefined ? current.stale_reason : patch.stale_reason,
|
|
5768
|
+
};
|
|
5769
|
+
if (!['smart', 'strict'].includes(next.mode)) next.mode = 'smart';
|
|
5770
|
+
getDb().prepare(`
|
|
5771
|
+
UPDATE model_routing_policies
|
|
5772
|
+
SET mode = ?,
|
|
5773
|
+
cli_tail_enabled = ?,
|
|
5774
|
+
finalization_fallback_enabled = ?,
|
|
5775
|
+
generated_refresh_interval_ms = ?,
|
|
5776
|
+
last_generated_at = ?,
|
|
5777
|
+
last_effective_at = ?,
|
|
5778
|
+
last_error = ?,
|
|
5779
|
+
stale_reason = ?,
|
|
5780
|
+
updated_at = datetime('now')
|
|
5781
|
+
WHERE id = ?
|
|
5782
|
+
`).run(
|
|
5783
|
+
next.mode,
|
|
5784
|
+
next.cliTailEnabled,
|
|
5785
|
+
next.finalizationFallbackEnabled,
|
|
5786
|
+
next.generatedRefreshIntervalMs == null ? null : Number(next.generatedRefreshIntervalMs),
|
|
5787
|
+
next.lastGeneratedAt || null,
|
|
5788
|
+
next.lastEffectiveAt || null,
|
|
5789
|
+
next.lastError || null,
|
|
5790
|
+
next.staleReason || null,
|
|
5791
|
+
id
|
|
5792
|
+
);
|
|
5793
|
+
// Mirror the legacy KV contract while client/chat code transitions to the
|
|
5794
|
+
// policy row. Existing scripts that read these keys keep working.
|
|
5795
|
+
setKv('model_routing_policy_mode', next.mode);
|
|
5796
|
+
setKv('model_routing_policy_cli_tail', next.cliTailEnabled ? '1' : '0');
|
|
5797
|
+
setKv('model_routing_policy_finalization_fallback', next.finalizationFallbackEnabled ? '1' : '0');
|
|
5798
|
+
if (next.lastGeneratedAt) setKv('model_routing_policy_last_refresh_at', next.lastGeneratedAt);
|
|
5799
|
+
return ensureModelRoutingPolicy(id);
|
|
5800
|
+
}
|
|
5801
|
+
|
|
5802
|
+
function _safeParseModelRoutingJson(raw, fallback) {
|
|
5803
|
+
if (!raw) return fallback;
|
|
5804
|
+
try {
|
|
5805
|
+
const parsed = JSON.parse(raw);
|
|
5806
|
+
return parsed == null ? fallback : parsed;
|
|
5807
|
+
} catch {
|
|
5808
|
+
return fallback;
|
|
5809
|
+
}
|
|
5810
|
+
}
|
|
5811
|
+
|
|
5812
|
+
function _modelRoutingRouteRowToPublic(row) {
|
|
5813
|
+
const health = _safeParseModelRoutingJson(row.health_json, null);
|
|
5814
|
+
const metadata = _safeParseModelRoutingJson(row.metadata_json, {});
|
|
5815
|
+
return {
|
|
5816
|
+
policy_id: row.policy_id,
|
|
5817
|
+
policy_source: row.source,
|
|
5818
|
+
order: row.route_order,
|
|
5819
|
+
provider_id: row.provider_id || null,
|
|
5820
|
+
provider_type: row.provider_type || null,
|
|
5821
|
+
runtime_type: row.runtime_type || row.provider_type || null,
|
|
5822
|
+
provider_name: row.provider_name || null,
|
|
5823
|
+
label: row.label || row.provider_name || row.provider_type || null,
|
|
5824
|
+
model_registry_id: row.model_registry_id || null,
|
|
5825
|
+
model: row.model_id || null,
|
|
5826
|
+
model_id: row.model_id || null,
|
|
5827
|
+
role: row.role || (row.route_order === 1 ? 'primary' : 'fallback'),
|
|
5828
|
+
source: row.candidate_source || null,
|
|
5829
|
+
connection_kind: row.connection_kind || null,
|
|
5830
|
+
route_policy: row.route_policy || null,
|
|
5831
|
+
runtime_access: row.runtime_access || null,
|
|
5832
|
+
health_status: row.health_status || (health && health.status) || null,
|
|
5833
|
+
health: health || (row.health_status ? { status: row.health_status } : null),
|
|
5834
|
+
reason: row.reason || metadata.reason || metadata.excluded_reason || '',
|
|
5835
|
+
score: row.score == null ? null : Number(row.score),
|
|
5836
|
+
enabled: row.enabled !== 0,
|
|
5837
|
+
generated_at: row.generated_at || null,
|
|
5838
|
+
updated_at: row.updated_at || null,
|
|
5839
|
+
metadata,
|
|
5840
|
+
};
|
|
5841
|
+
}
|
|
5842
|
+
|
|
5843
|
+
function listModelRoutingPolicyRoutes(policyId = DEFAULT_MODEL_ROUTING_POLICY_ID, source = null) {
|
|
5844
|
+
const id = _normalizeModelRoutingPolicyId(policyId);
|
|
5845
|
+
ensureModelRoutingPolicy(id);
|
|
5846
|
+
const normalizedSource = source ? String(source).trim().toLowerCase() : null;
|
|
5847
|
+
const rows = normalizedSource
|
|
5848
|
+
? getDb().prepare(`
|
|
5849
|
+
SELECT * FROM model_routing_policy_routes
|
|
5850
|
+
WHERE policy_id = ? AND source = ?
|
|
5851
|
+
ORDER BY route_order
|
|
5852
|
+
`).all(id, normalizedSource)
|
|
5853
|
+
: getDb().prepare(`
|
|
5854
|
+
SELECT * FROM model_routing_policy_routes
|
|
5855
|
+
WHERE policy_id = ?
|
|
5856
|
+
ORDER BY source, route_order
|
|
5857
|
+
`).all(id);
|
|
5858
|
+
return rows.map(_modelRoutingRouteRowToPublic);
|
|
5859
|
+
}
|
|
5860
|
+
|
|
5861
|
+
function hasModelRoutingPolicyRoutes(policyId = DEFAULT_MODEL_ROUTING_POLICY_ID, source = 'effective') {
|
|
5862
|
+
const id = _normalizeModelRoutingPolicyId(policyId);
|
|
5863
|
+
const normalizedSource = String(source || 'effective').trim().toLowerCase();
|
|
5864
|
+
const row = getDb().prepare(`
|
|
5865
|
+
SELECT 1
|
|
5866
|
+
FROM model_routing_policy_routes
|
|
5867
|
+
WHERE policy_id = ? AND source = ? AND enabled = 1
|
|
5868
|
+
LIMIT 1
|
|
5869
|
+
`).get(id, normalizedSource);
|
|
5870
|
+
return !!row;
|
|
5871
|
+
}
|
|
5872
|
+
|
|
5873
|
+
function _stringOrNull(value) {
|
|
5874
|
+
if (value === undefined || value === null) return null;
|
|
5875
|
+
const text = String(value).trim();
|
|
5876
|
+
return text ? text : null;
|
|
5877
|
+
}
|
|
5878
|
+
|
|
5879
|
+
function _jsonOrNull(value) {
|
|
5880
|
+
if (value === undefined || value === null) return null;
|
|
5881
|
+
if (typeof value === 'string') return value || null;
|
|
5882
|
+
try { return JSON.stringify(value); } catch { return null; }
|
|
5883
|
+
}
|
|
5884
|
+
|
|
5885
|
+
function replaceModelRoutingPolicyRoutes(policyId = DEFAULT_MODEL_ROUTING_POLICY_ID, source = 'generated', routes = []) {
|
|
5886
|
+
const id = _normalizeModelRoutingPolicyId(policyId);
|
|
5887
|
+
const normalizedSource = String(source || '').trim().toLowerCase();
|
|
5888
|
+
if (!VALID_MODEL_ROUTING_ROUTE_SOURCES.has(normalizedSource)) {
|
|
5889
|
+
throw new Error(`Invalid model routing route source: ${source}`);
|
|
5890
|
+
}
|
|
5891
|
+
ensureModelRoutingPolicy(id);
|
|
5892
|
+
const db = getDb();
|
|
5893
|
+
const now = new Date().toISOString();
|
|
5894
|
+
return runImmediateTransaction(db, () => {
|
|
5895
|
+
db.prepare('DELETE FROM model_routing_policy_routes WHERE policy_id = ? AND source = ?').run(id, normalizedSource);
|
|
5896
|
+
const insert = db.prepare(`
|
|
5897
|
+
INSERT INTO model_routing_policy_routes
|
|
5898
|
+
(policy_id, source, route_order, provider_id, provider_type, runtime_type,
|
|
5899
|
+
provider_name, label, model_registry_id, model_id, role, candidate_source,
|
|
5900
|
+
connection_kind, route_policy, runtime_access, health_status, health_json,
|
|
5901
|
+
reason, score, enabled, generated_at, updated_at, metadata_json)
|
|
5902
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
5903
|
+
`);
|
|
5904
|
+
let count = 0;
|
|
5905
|
+
(Array.isArray(routes) ? routes : []).forEach((route, index) => {
|
|
5906
|
+
if (!route) return;
|
|
5907
|
+
const providerType = _stringOrNull(route.provider_type || route.providerType || route.type);
|
|
5908
|
+
if (!providerType) return;
|
|
5909
|
+
const order = count + 1;
|
|
5910
|
+
const health = route.health || (route.health_status ? { status: route.health_status } : null);
|
|
5911
|
+
const metadata = {
|
|
5912
|
+
...(route.metadata && typeof route.metadata === 'object' ? route.metadata : {}),
|
|
5913
|
+
};
|
|
5914
|
+
if (route.excluded_reason && metadata.excluded_reason === undefined) metadata.excluded_reason = route.excluded_reason;
|
|
5915
|
+
if (route.reason && metadata.reason === undefined) metadata.reason = route.reason;
|
|
5916
|
+
insert.run(
|
|
5917
|
+
id,
|
|
5918
|
+
normalizedSource,
|
|
5919
|
+
Number.isFinite(order) ? Math.max(1, Math.trunc(order)) : index + 1,
|
|
5920
|
+
_stringOrNull(route.provider_id || route.providerId),
|
|
5921
|
+
providerType,
|
|
5922
|
+
_stringOrNull(route.runtime_type || route.runtimeType || providerType),
|
|
5923
|
+
_stringOrNull(route.provider_name || route.providerName),
|
|
5924
|
+
_stringOrNull(route.label || route.provider_name || route.providerName || providerType),
|
|
5925
|
+
_stringOrNull(route.model_registry_id || route.modelRegistryId),
|
|
5926
|
+
_stringOrNull(route.model_id || route.modelId || route.model),
|
|
5927
|
+
_stringOrNull(route.role || (order === 1 ? 'primary' : 'fallback')),
|
|
5928
|
+
_stringOrNull(route.candidate_source || route.candidateSource || route.source),
|
|
5929
|
+
_stringOrNull(route.connection_kind || route.connectionKind),
|
|
5930
|
+
_stringOrNull(route.route_policy || route.routePolicy),
|
|
5931
|
+
_stringOrNull(route.runtime_access || route.runtimeAccess),
|
|
5932
|
+
_stringOrNull(route.health_status || (health && health.status)),
|
|
5933
|
+
_jsonOrNull(health),
|
|
5934
|
+
_stringOrNull(route.reason || route.excluded_reason || route.excludedReason),
|
|
5935
|
+
route.score == null ? null : Number(route.score),
|
|
5936
|
+
route.enabled === false || route.enabled === 0 ? 0 : 1,
|
|
5937
|
+
route.generated_at || route.generatedAt || now,
|
|
5938
|
+
now,
|
|
5939
|
+
_jsonOrNull(Object.keys(metadata).length ? metadata : null)
|
|
5940
|
+
);
|
|
5941
|
+
count += 1;
|
|
5942
|
+
});
|
|
5943
|
+
return { policy_id: id, source: normalizedSource, count };
|
|
5944
|
+
});
|
|
5945
|
+
}
|
|
5946
|
+
|
|
5947
|
+
function getModelRoutingPolicy(policyId = DEFAULT_MODEL_ROUTING_POLICY_ID) {
|
|
5948
|
+
const id = _normalizeModelRoutingPolicyId(policyId);
|
|
5949
|
+
const policy = ensureModelRoutingPolicy(id);
|
|
5950
|
+
const routes = {
|
|
5951
|
+
user: listModelRoutingPolicyRoutes(id, 'user'),
|
|
5952
|
+
generated: listModelRoutingPolicyRoutes(id, 'generated'),
|
|
5953
|
+
effective: listModelRoutingPolicyRoutes(id, 'effective'),
|
|
5954
|
+
};
|
|
5955
|
+
return { policy, routes };
|
|
5956
|
+
}
|
|
5957
|
+
|
|
4962
5958
|
// -- Benchmark Evaluations --
|
|
4963
5959
|
|
|
4964
5960
|
function _benchmarkHash(value) {
|
|
@@ -5991,14 +6987,19 @@ module.exports = {
|
|
|
5991
6987
|
updateActionStatus,
|
|
5992
6988
|
// Agent Exchanges
|
|
5993
6989
|
listExchanges,
|
|
6990
|
+
// Runtime Event Ledger
|
|
6991
|
+
appendRuntimeEvent,
|
|
6992
|
+
listRuntimeEvents,
|
|
5994
6993
|
// Chat Messages
|
|
5995
6994
|
insertChatMessage,
|
|
6995
|
+
insertChatMessagePart,
|
|
5996
6996
|
listChatMessages,
|
|
6997
|
+
listChatMessagesWithParts,
|
|
6998
|
+
listChatMessageParts,
|
|
5997
6999
|
searchChatMessages,
|
|
5998
7000
|
deleteChatMessages,
|
|
5999
7001
|
clearChatSession,
|
|
6000
|
-
|
|
6001
|
-
loadChatBranches,
|
|
7002
|
+
backfillChatMessageParents,
|
|
6002
7003
|
// Backup
|
|
6003
7004
|
checkpointWalOrThrow,
|
|
6004
7005
|
createBackup,
|
|
@@ -6109,11 +7110,14 @@ module.exports = {
|
|
|
6109
7110
|
getModelUsageLedgerEntry,
|
|
6110
7111
|
listModelUsageLedger,
|
|
6111
7112
|
summarizeModelUsageLedger,
|
|
7113
|
+
summarizeSessionModelUsage,
|
|
6112
7114
|
estimateModelUsageCost,
|
|
6113
7115
|
findModelRegistryForUsage,
|
|
6114
7116
|
// Model Evaluations
|
|
6115
7117
|
insertModelEvaluation,
|
|
6116
7118
|
getModelScorecard,
|
|
7119
|
+
getModelScorecardsBatch,
|
|
7120
|
+
emptyModelScorecardStats,
|
|
6117
7121
|
getModelScorecardForTask,
|
|
6118
7122
|
insertAgentRunnerEvaluation,
|
|
6119
7123
|
getAgentRunnerScorecard,
|
|
@@ -6122,6 +7126,12 @@ module.exports = {
|
|
|
6122
7126
|
setTaskModelDefault,
|
|
6123
7127
|
getModelDefault,
|
|
6124
7128
|
listModelDefaults,
|
|
7129
|
+
ensureModelRoutingPolicy,
|
|
7130
|
+
updateModelRoutingPolicy,
|
|
7131
|
+
listModelRoutingPolicyRoutes,
|
|
7132
|
+
hasModelRoutingPolicyRoutes,
|
|
7133
|
+
replaceModelRoutingPolicyRoutes,
|
|
7134
|
+
getModelRoutingPolicy,
|
|
6125
7135
|
// Benchmark Evaluations
|
|
6126
7136
|
insertBenchmarkResult,
|
|
6127
7137
|
getBenchmarkResults,
|