create-walle 0.9.21 → 0.9.23
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 +27 -5
- package/package.json +2 -2
- package/template/CLAUDE.md +2 -2
- package/template/LICENSE +1 -1
- package/template/bin/ctm-dev-cleanup.js +24 -3
- package/template/bin/ctm-launch.sh +13 -0
- package/template/bin/dev.sh +156 -18
- package/template/bin/node-bin.sh +84 -0
- package/template/bin/pin-node.sh +51 -0
- package/template/claude-task-manager/api-prompts.js +1203 -182
- package/template/claude-task-manager/api-reviews.js +109 -15
- package/template/claude-task-manager/approval-agent.js +1360 -280
- package/template/claude-task-manager/bin/restart-ctm.sh +64 -23
- package/template/claude-task-manager/bin/storage-migration-supervisor.js +338 -0
- package/template/claude-task-manager/db.js +4417 -295
- package/template/claude-task-manager/docs/app-update-refresh-protocol.md +69 -0
- package/template/claude-task-manager/docs/approval-ai-refinement.md +138 -0
- package/template/claude-task-manager/docs/approval-rescue-loop.md +74 -0
- package/template/claude-task-manager/docs/codex-operational-warning-health.md +107 -0
- package/template/claude-task-manager/docs/codex-resume-state-guard-design.md +17 -12
- package/template/claude-task-manager/docs/codex-terminal-render-controller-handoff.md +311 -0
- package/template/claude-task-manager/docs/coding-agent-hooks-architecture.md +418 -0
- package/template/claude-task-manager/docs/conversation-import-freshness.md +20 -0
- package/template/claude-task-manager/docs/google-workspace-auth-health.md +77 -0
- package/template/claude-task-manager/docs/image-paste-ux.md +13 -0
- package/template/claude-task-manager/docs/ipad-web-preview.md +88 -0
- package/template/claude-task-manager/docs/main-loop-offload-architecture.md +66 -0
- package/template/claude-task-manager/docs/microsoft-dev-tunnel-phone-access-design.md +274 -519
- package/template/claude-task-manager/docs/mobile-live-streaming.md +27 -5
- package/template/claude-task-manager/docs/mobile-remote-submission-lifecycle.md +69 -0
- package/template/claude-task-manager/docs/phone-access-design.md +53 -15
- package/template/claude-task-manager/docs/phone-passkey-identity.md +122 -0
- package/template/claude-task-manager/docs/phone-setup.md +3 -0
- package/template/claude-task-manager/docs/prompt-editing-tree-design.md +25 -1
- package/template/claude-task-manager/docs/remote-desktop-access-design.md +268 -0
- package/template/claude-task-manager/docs/restart-lifecycle-architecture.md +95 -0
- package/template/claude-task-manager/docs/runtime-work-control-plane.md +53 -0
- package/template/claude-task-manager/docs/session-interactive-wait-surfaces.md +38 -0
- package/template/claude-task-manager/docs/session-needs-you-dismissal.md +84 -0
- package/template/claude-task-manager/docs/session-render-state-management-design.md +91 -3
- package/template/claude-task-manager/docs/session-standup-command-center-design.md +25 -1
- package/template/claude-task-manager/docs/session-title-authority.md +32 -0
- package/template/claude-task-manager/docs/session-workspace-binding.md +33 -0
- package/template/claude-task-manager/docs/skill-intent-resolution-design.md +72 -0
- package/template/claude-task-manager/docs/walle-mcp-supervisor-health.md +86 -0
- package/template/claude-task-manager/docs/walle-relay-phone-access-design.md +24 -15
- package/template/claude-task-manager/docs/walle-session-history-hydration.md +114 -0
- package/template/claude-task-manager/docs/walle-session-input-queue.md +104 -0
- package/template/claude-task-manager/docs/walle-session-model-catalog.md +90 -0
- package/template/claude-task-manager/docs/walle-session-model-preferences.md +15 -6
- package/template/claude-task-manager/git-utils.js +897 -27
- package/template/claude-task-manager/lib/agent-capabilities.js +33 -0
- package/template/claude-task-manager/lib/agent-cli-cache.js +37 -7
- package/template/claude-task-manager/lib/agent-hooks-installer.js +26 -2
- package/template/claude-task-manager/lib/agent-presets.js +17 -1
- package/template/claude-task-manager/lib/all-sessions-query.js +108 -0
- package/template/claude-task-manager/lib/approval-ai-refinement.js +488 -0
- package/template/claude-task-manager/lib/approval-self-adapt.js +168 -0
- package/template/claude-task-manager/lib/async-semaphore.js +44 -0
- package/template/claude-task-manager/lib/auth-context.js +5 -0
- package/template/claude-task-manager/lib/auth-rate-limit.js +47 -4
- package/template/claude-task-manager/lib/auth-rules.js +29 -2
- package/template/claude-task-manager/lib/auto-approval-verifier.js +129 -16
- package/template/claude-task-manager/lib/background-llm.js +144 -17
- package/template/claude-task-manager/lib/branch-inventory.js +212 -0
- package/template/claude-task-manager/lib/claude-desktop-sessions.js +15 -3
- package/template/claude-task-manager/lib/coalesce-sync-frames.js +151 -0
- package/template/claude-task-manager/lib/codex-launch-health.js +762 -0
- package/template/claude-task-manager/lib/codex-transcript-pager.js +51 -0
- package/template/claude-task-manager/lib/codex-zst.js +124 -0
- package/template/claude-task-manager/lib/coding-agent-models.js +233 -30
- package/template/claude-task-manager/lib/connection-health.js +232 -0
- package/template/claude-task-manager/lib/conversation-blob-parser.js +42 -0
- package/template/claude-task-manager/lib/conversation-tail-merge.js +89 -26
- package/template/claude-task-manager/lib/ctm-session-context-api.js +39 -10
- package/template/claude-task-manager/lib/cursor-conversation-store.js +354 -0
- package/template/claude-task-manager/lib/db-owner-worker-client.js +315 -0
- package/template/claude-task-manager/lib/document-review.js +141 -6
- package/template/claude-task-manager/lib/escalation-review.js +152 -0
- package/template/claude-task-manager/lib/graceful-shutdown.js +159 -0
- package/template/claude-task-manager/lib/headless-term-service.js +678 -0
- package/template/claude-task-manager/lib/heavy-worker-fallback.js +38 -0
- package/template/claude-task-manager/lib/jsonl-conversation-parser.js +542 -0
- package/template/claude-task-manager/lib/jsonl-range-reader.js +112 -0
- package/template/claude-task-manager/lib/main-db-census.js +216 -0
- package/template/claude-task-manager/lib/message-pagination.js +106 -4
- package/template/claude-task-manager/lib/microsoft-dev-tunnel-setup.js +750 -26
- package/template/claude-task-manager/lib/mobile-auth-api.js +274 -7
- package/template/claude-task-manager/lib/mobile-auth-store.js +592 -10
- package/template/claude-task-manager/lib/mobile-notification-dispatcher.js +15 -0
- package/template/claude-task-manager/lib/model-overview-brain-fallback.js +311 -0
- package/template/claude-task-manager/lib/model-overview-cache.js +141 -0
- package/template/claude-task-manager/lib/models-health-routing-notice.js +126 -0
- package/template/claude-task-manager/lib/node-pin-guard.js +93 -0
- package/template/claude-task-manager/lib/perf-tracker.js +242 -6
- package/template/claude-task-manager/lib/permission-match.js +76 -0
- package/template/claude-task-manager/lib/permission-sync.js +133 -20
- package/template/claude-task-manager/lib/process-title.js +35 -0
- package/template/claude-task-manager/lib/prompt-executions-query.js +25 -0
- package/template/claude-task-manager/lib/prompt-index-disk-cache.js +44 -0
- package/template/claude-task-manager/lib/prompt-intent.js +132 -0
- package/template/claude-task-manager/lib/provider-user-context.js +34 -0
- package/template/claude-task-manager/lib/read-pool-client.js +313 -0
- package/template/claude-task-manager/lib/readpool-breaker.js +31 -0
- package/template/claude-task-manager/lib/recent-sessions-breaker.js +12 -0
- package/template/claude-task-manager/lib/remote-feedback-client.js +72 -0
- package/template/claude-task-manager/lib/remote-relay-protocol.js +37 -4
- package/template/claude-task-manager/lib/remote-relay-store.js +159 -0
- package/template/claude-task-manager/lib/remote-submission-observer.js +278 -0
- package/template/claude-task-manager/lib/restart-guard.js +109 -0
- package/template/claude-task-manager/lib/restore-interruption-detector.js +439 -0
- package/template/claude-task-manager/lib/restore-policy.js +13 -0
- package/template/claude-task-manager/lib/restore-resume-batch.js +74 -0
- package/template/claude-task-manager/lib/restore-runtime.js +68 -0
- package/template/claude-task-manager/lib/restore-storm.js +34 -0
- package/template/claude-task-manager/lib/resume-cwd.js +36 -0
- package/template/claude-task-manager/lib/resume-preflight.js +313 -0
- package/template/claude-task-manager/lib/runtime-work-registry.js +444 -0
- package/template/claude-task-manager/lib/sanitize-openai-auth.js +31 -0
- package/template/claude-task-manager/lib/scheduler.js +21 -1
- package/template/claude-task-manager/lib/scrollback-snapshot-store.js +159 -0
- package/template/claude-task-manager/lib/serial-task-queue.js +64 -0
- package/template/claude-task-manager/lib/server-listeners.js +239 -0
- package/template/claude-task-manager/lib/session-capture.js +42 -7
- package/template/claude-task-manager/lib/session-content-backfill.js +131 -0
- package/template/claude-task-manager/lib/session-history.js +388 -43
- package/template/claude-task-manager/lib/session-host-manager.js +287 -0
- package/template/claude-task-manager/lib/session-image-refs.js +209 -0
- package/template/claude-task-manager/lib/session-jobs.js +399 -59
- package/template/claude-task-manager/lib/session-prompt-index.js +137 -0
- package/template/claude-task-manager/lib/session-restore.js +53 -0
- package/template/claude-task-manager/lib/session-standup.js +123 -23
- package/template/claude-task-manager/lib/session-state-bus.js +14 -0
- package/template/claude-task-manager/lib/session-stream.js +64 -16
- package/template/claude-task-manager/lib/session-timeline-summary.js +260 -0
- package/template/claude-task-manager/lib/session-token-usage.js +494 -0
- package/template/claude-task-manager/lib/session-workspace-binding.js +356 -0
- package/template/claude-task-manager/lib/setup-network-config.js +9 -0
- package/template/claude-task-manager/lib/size-cap.js +45 -0
- package/template/claude-task-manager/lib/size-cap.test.js +62 -0
- package/template/claude-task-manager/lib/skill-autocomplete.js +180 -1
- package/template/claude-task-manager/lib/skill-intent-resolver.js +304 -0
- package/template/claude-task-manager/lib/sqlite-driver.js +19 -3
- package/template/claude-task-manager/lib/standup-attention.js +7 -3
- package/template/claude-task-manager/lib/status-authority.js +39 -0
- package/template/claude-task-manager/lib/status-hooks.js +4 -0
- package/template/claude-task-manager/lib/storage-migration.js +235 -0
- package/template/claude-task-manager/lib/structured-capture.js +298 -0
- package/template/claude-task-manager/lib/sync-io-census.js +163 -0
- package/template/claude-task-manager/lib/tailscale-setup.js +6 -0
- package/template/claude-task-manager/lib/terminal-activity-evidence.js +33 -0
- package/template/claude-task-manager/lib/terminal-choice.js +364 -0
- package/template/claude-task-manager/lib/terminal-control-sanitize.js +17 -0
- package/template/claude-task-manager/lib/terminal-fingerprint.js +48 -0
- package/template/claude-task-manager/lib/terminal-output-flush.js +84 -0
- package/template/claude-task-manager/lib/timeline-order.js +122 -0
- package/template/claude-task-manager/lib/transcript-store.js +348 -43
- package/template/claude-task-manager/lib/transport-security.js +84 -1
- package/template/claude-task-manager/lib/wait-state.js +184 -0
- package/template/claude-task-manager/lib/walle-client.js +47 -5
- package/template/claude-task-manager/lib/walle-ctm-history.js +564 -4
- package/template/claude-task-manager/lib/walle-external-actions.js +135 -16
- package/template/claude-task-manager/lib/walle-history-hydration.js +46 -0
- package/template/claude-task-manager/lib/walle-native-health.js +403 -0
- package/template/claude-task-manager/lib/walle-repair.js +701 -0
- package/template/claude-task-manager/lib/walle-session-cache.js +109 -0
- package/template/claude-task-manager/lib/walle-session-context.js +57 -21
- package/template/claude-task-manager/lib/walle-session-model-catalog.js +34 -0
- package/template/claude-task-manager/lib/walle-supervisor.js +539 -63
- package/template/claude-task-manager/lib/walle-transcript.js +52 -0
- package/template/claude-task-manager/lib/worktree-active-sync.js +11 -7
- package/template/claude-task-manager/lib/worktree-cwd.js +32 -1
- package/template/claude-task-manager/package.json +1 -1
- package/template/claude-task-manager/prompt-harvest.js +89 -66
- package/template/claude-task-manager/providers/claude-code.js +51 -3
- package/template/claude-task-manager/providers/cursor.js +140 -45
- package/template/claude-task-manager/public/css/reviews.css +551 -61
- package/template/claude-task-manager/public/css/setup.css +191 -0
- package/template/claude-task-manager/public/css/walle-session.css +865 -10
- package/template/claude-task-manager/public/css/walle.css +154 -0
- package/template/claude-task-manager/public/designs/ai-providers-consolidation-v2.html +830 -0
- package/template/claude-task-manager/public/index.html +18516 -2058
- package/template/claude-task-manager/public/ipad.html +363 -0
- package/template/claude-task-manager/public/js/document-review-links.js +301 -0
- package/template/claude-task-manager/public/js/image-normalize.js +69 -36
- package/template/claude-task-manager/public/js/message-renderer.js +1265 -77
- package/template/claude-task-manager/public/js/prompts.js +66 -29
- package/template/claude-task-manager/public/js/reviews.js +901 -133
- package/template/claude-task-manager/public/js/session-activity-utils.js +11 -1
- package/template/claude-task-manager/public/js/session-search-utils.js +94 -10
- package/template/claude-task-manager/public/js/session-status-precedence.js +23 -5
- package/template/claude-task-manager/public/js/setup.js +1273 -176
- package/template/claude-task-manager/public/js/stream-view.js +691 -73
- package/template/claude-task-manager/public/js/terminal-reconciler.js +210 -0
- package/template/claude-task-manager/public/js/walle-session.js +2455 -158
- package/template/claude-task-manager/public/js/walle.js +455 -28
- package/template/claude-task-manager/public/m/app.css +2909 -262
- package/template/claude-task-manager/public/m/app.js +6601 -398
- package/template/claude-task-manager/public/m/claim.html +224 -17
- package/template/claude-task-manager/public/m/index.html +117 -21
- package/template/claude-task-manager/public/m/sw.js +3 -1
- package/template/claude-task-manager/public/manifest.json +2 -2
- package/template/claude-task-manager/public/prompts.html +30 -14
- package/template/claude-task-manager/queue-engine.js +507 -28
- package/template/claude-task-manager/scripts/repair-claude-session-images.js +27 -8
- package/template/claude-task-manager/server.js +14341 -2197
- package/template/claude-task-manager/session-integrity.js +160 -18
- package/template/claude-task-manager/session-search-ranking.js +1 -0
- package/template/claude-task-manager/session-utils.js +25 -5
- package/template/claude-task-manager/workers/approval-blocklist.js +96 -6
- package/template/claude-task-manager/workers/approval-widget-validator.js +14 -8
- package/template/claude-task-manager/workers/conversation-import-worker.js +11 -50
- package/template/claude-task-manager/workers/db-owner-worker.js +386 -0
- package/template/claude-task-manager/workers/harvest-worker.js +9 -55
- package/template/claude-task-manager/workers/headless-term-worker.js +9 -530
- package/template/claude-task-manager/workers/read-pool-worker.js +387 -0
- package/template/claude-task-manager/workers/scrollback-worker.js +11 -72
- package/template/claude-task-manager/workers/session-host-process.js +146 -0
- package/template/claude-task-manager/workers/session-integrity-worker.js +10 -54
- package/template/claude-task-manager/workers/state-detectors/base.js +18 -1
- package/template/claude-task-manager/workers/state-detectors/claude-code.js +182 -9
- package/template/claude-task-manager/workers/state-detectors/codex.js +150 -2
- package/template/claude-task-manager/workers/state-detectors/cursor.js +127 -0
- package/template/claude-task-manager/workers/state-detectors/gemini.js +21 -0
- package/template/claude-task-manager/workers/state-detectors/index.js +29 -0
- package/template/claude-task-manager/workers/state-detectors/opencode.js +103 -0
- package/template/docs/design/markdown-review-pane.md +206 -0
- package/template/docs/designs/2026-05-17-portkey-gateway-provider-ux.md +129 -38
- package/template/docs/designs/2026-05-20-mobile-worktree-finish-command.md +27 -0
- package/template/docs/designs/2026-05-22-ai-configuration-consolidation.md +248 -0
- package/template/docs/designs/ai-configuration-consolidation-mock.html +812 -0
- package/template/docs/private-memory-and-pii-policy.md +69 -0
- package/template/package.json +2 -1
- package/template/scripts/check-private-data.js +201 -0
- package/template/shared/sqlite-owner-guard.js +30 -0
- package/template/shared/sqlite-owner-write-queue.js +225 -0
- package/template/shared/sqlite-storage-policy.js +111 -0
- package/template/shared/sqlite-write-lock.js +428 -0
- package/template/wall-e/agent-runners/claude-code.js +5 -0
- package/template/wall-e/agent.js +166 -22
- package/template/wall-e/api-walle.js +524 -70
- package/template/wall-e/auth/provider-flows.js +11 -1
- package/template/wall-e/bin/walle-mcp-stdio.js +341 -17
- package/template/wall-e/brain.js +1614 -141
- package/template/wall-e/chat/attachment-blocks.js +96 -0
- package/template/wall-e/chat/attachments.js +2 -1
- package/template/wall-e/chat/capability-resolver.js +7 -7
- package/template/wall-e/chat/context-messages.js +28 -0
- package/template/wall-e/chat/conversation-frame.js +630 -0
- package/template/wall-e/chat/provider-messages.js +125 -0
- package/template/wall-e/chat.js +1002 -233
- package/template/wall-e/coding/acceptance-contract.js +170 -0
- package/template/wall-e/coding/acp-adapter.js +1 -1
- package/template/wall-e/coding/agent-catalog.js +3 -0
- package/template/wall-e/coding/artifact-store.js +93 -0
- package/template/wall-e/coding/capability-router.js +120 -0
- package/template/wall-e/coding/coding-run-controller.js +423 -0
- package/template/wall-e/coding/compaction-service.js +157 -12
- package/template/wall-e/coding/frontend-verification.js +258 -0
- package/template/wall-e/coding/lifecycle-hooks.js +75 -0
- package/template/wall-e/coding/local-preview-contract.js +157 -0
- package/template/wall-e/coding/permission-service.js +57 -13
- package/template/wall-e/coding/prompt-bundle.js +19 -1
- package/template/wall-e/coding/prompt-section-registry.js +227 -0
- package/template/wall-e/coding/provider-compat.js +15 -0
- package/template/wall-e/coding/runtime-events.js +224 -0
- package/template/wall-e/coding/runtime-mode.js +3 -0
- package/template/wall-e/coding/side-git-snapshot.js +160 -4
- package/template/wall-e/coding/snapshot-service.js +143 -1
- package/template/wall-e/coding/stream-processor.js +388 -34
- package/template/wall-e/coding/task-tool.js +141 -4
- package/template/wall-e/coding/tool-execution-controller.js +365 -0
- package/template/wall-e/coding/tool-registry.js +43 -5
- package/template/wall-e/coding/user-hooks.js +217 -0
- package/template/wall-e/coding-orchestrator.js +1330 -221
- package/template/wall-e/coding-prompts.js +20 -4
- package/template/wall-e/context/context-builder.js +15 -2
- package/template/wall-e/decision/confidence.js +1 -1
- package/template/wall-e/docs/coding-acceptance-contract.md +41 -0
- package/template/wall-e/docs/external-action-controller.md +26 -6
- package/template/wall-e/docs/telemetry-lifecycle.md +8 -2
- package/template/wall-e/embeddings.js +591 -53
- package/template/wall-e/external-action-controller.js +12 -0
- package/template/wall-e/http/auth.js +1 -0
- package/template/wall-e/http/chat-api.js +46 -11
- package/template/wall-e/http/model-admin.js +836 -34
- package/template/wall-e/lib/boot-profile.js +88 -0
- package/template/wall-e/lib/event-loop-monitor.js +93 -0
- package/template/wall-e/lib/service-health.js +194 -0
- package/template/wall-e/llm/anthropic.js +130 -5
- package/template/wall-e/llm/client.js +266 -63
- package/template/wall-e/llm/default-fallback.js +382 -0
- package/template/wall-e/llm/health.js +19 -0
- package/template/wall-e/llm/message-guard.js +78 -0
- package/template/wall-e/llm/model-catalog.js +252 -1
- package/template/wall-e/llm/openai.js +26 -4
- package/template/wall-e/llm/portkey-sync.js +654 -0
- package/template/wall-e/llm/provider-error.js +30 -2
- package/template/wall-e/llm/registry.js +5 -1
- package/template/wall-e/llm/request-compat.js +67 -0
- package/template/wall-e/loops/backfill.js +79 -23
- package/template/wall-e/loops/brain-optimize.js +67 -0
- package/template/wall-e/loops/ingest.js +25 -10
- package/template/wall-e/loops/question-digest.js +160 -0
- package/template/wall-e/loops/reflect.js +6 -4
- package/template/wall-e/loops/think.js +39 -12
- package/template/wall-e/mcp-server.js +318 -36
- package/template/wall-e/memory/ctm-context-client.js +52 -14
- package/template/wall-e/memory/ctm-operational-context.js +237 -0
- package/template/wall-e/memory/ctm-prompt-executions-client.js +128 -0
- package/template/wall-e/memory/ctm-session-context.js +111 -63
- package/template/wall-e/prompts/coding/deepseek.txt +3 -0
- package/template/wall-e/prompts/coding/gemini.txt +6 -0
- package/template/wall-e/prompts/coding/gpt.txt +6 -0
- package/template/wall-e/prompts/coding/local.txt +7 -0
- package/template/wall-e/runtime/decision-hooks.js +115 -0
- package/template/wall-e/runtime/devbox-gateway.js +82 -8
- package/template/wall-e/runtime/prompt-manifest.js +86 -0
- package/template/wall-e/runtime/tool-executor.js +269 -0
- package/template/wall-e/runtime/tool-result-envelope.js +138 -0
- package/template/wall-e/runtime/transcript-projection.js +60 -0
- package/template/wall-e/runtime/walle-runtime.js +224 -0
- package/template/wall-e/scripts/db-optimize/migrate.js +162 -0
- package/template/wall-e/scripts/db-optimize/recall-eval.js +117 -0
- package/template/wall-e/server.js +15 -0
- package/template/wall-e/session-files.js +9 -0
- package/template/wall-e/skills/_bundled/google-calendar/run.js +1 -1
- package/template/wall-e/skills/_bundled/gws-workspace/run.js +1 -1
- package/template/wall-e/skills/_bundled/slack-mentions/run.js +76 -6
- package/template/wall-e/skills/claude-code-reader.js +7 -3
- package/template/wall-e/skills/script-skill-runner.js +10 -0
- package/template/wall-e/skills/skill-planner.js +38 -0
- package/template/wall-e/tools/builtin-middleware.js +19 -9
- package/template/wall-e/tools/local-tools.js +1428 -16
- package/template/wall-e/tools/permission-checker.js +73 -5
- package/template/wall-e/tools/question-manager.js +117 -7
- package/template/wall-e/training/harvester.js +12 -28
- package/template/wall-e/training/replay.js +25 -80
- package/template/website/index.html +10 -10
- package/template/wall-e/eval/ab-test.js +0 -203
- package/template/wall-e/eval/agent-runner.js +0 -772
- package/template/wall-e/eval/agent-scorer.js +0 -461
- package/template/wall-e/eval/aggregator.js +0 -414
- package/template/wall-e/eval/allowed-test-commands.js +0 -34
- package/template/wall-e/eval/benchmark-generator.js +0 -113
- package/template/wall-e/eval/benchmarks/chat-eval.json +0 -1662
- package/template/wall-e/eval/benchmarks/chat.json +0 -82
- package/template/wall-e/eval/benchmarks/coding-agent-real.json +0 -1
- package/template/wall-e/eval/benchmarks/coding-agent.json +0 -1581
- package/template/wall-e/eval/benchmarks/coding.json +0 -122
- package/template/wall-e/eval/benchmarks/memory-retrieval.json +0 -234
- package/template/wall-e/eval/benchmarks/reasoning.json +0 -82
- package/template/wall-e/eval/benchmarks/swebench-lite-30.json +0 -212
- package/template/wall-e/eval/benchmarks.js +0 -669
- package/template/wall-e/eval/cc-replay.js +0 -719
- package/template/wall-e/eval/chat-eval.js +0 -525
- package/template/wall-e/eval/check-keys.js +0 -15
- package/template/wall-e/eval/check-providers.js +0 -42
- package/template/wall-e/eval/codex-cli-baseline.js +0 -669
- package/template/wall-e/eval/coding-agent-real.js +0 -570
- package/template/wall-e/eval/context-compactor.js +0 -251
- package/template/wall-e/eval/debug-agent003.js +0 -68
- package/template/wall-e/eval/diagnostics.js +0 -216
- package/template/wall-e/eval/eval-orchestrator.js +0 -642
- package/template/wall-e/eval/evaluate.js +0 -202
- package/template/wall-e/eval/evaluator.js +0 -373
- package/template/wall-e/eval/exporter.js +0 -212
- package/template/wall-e/eval/fixtures/express-basic/package.json +0 -9
- package/template/wall-e/eval/fixtures/express-basic/server.js +0 -115
- package/template/wall-e/eval/fixtures/express-basic/test.js +0 -83
- package/template/wall-e/eval/fixtures/express-buggy/package.json +0 -9
- package/template/wall-e/eval/fixtures/express-buggy/server.js +0 -113
- package/template/wall-e/eval/fixtures/express-buggy/test.js +0 -83
- package/template/wall-e/eval/fixtures/express-buggy-items/package.json +0 -9
- package/template/wall-e/eval/fixtures/express-buggy-items/server.js +0 -112
- package/template/wall-e/eval/fixtures/express-buggy-items/test.js +0 -83
- package/template/wall-e/eval/fixtures/express-buggy-search/package.json +0 -9
- package/template/wall-e/eval/fixtures/express-buggy-search/server.js +0 -121
- package/template/wall-e/eval/fixtures/express-buggy-search/test.js +0 -83
- package/template/wall-e/eval/fixtures/express-rename-data/data.js +0 -34
- package/template/wall-e/eval/fixtures/express-rename-data/package.json +0 -9
- package/template/wall-e/eval/fixtures/express-rename-data/server.js +0 -97
- package/template/wall-e/eval/fixtures/express-rename-data/test.js +0 -88
- package/template/wall-e/eval/fixtures/express-xss/package.json +0 -12
- package/template/wall-e/eval/fixtures/express-xss/server.js +0 -90
- package/template/wall-e/eval/fixtures/express-xss/test.js +0 -67
- package/template/wall-e/eval/fixtures/express-xss/views/profile.ejs +0 -9
- package/template/wall-e/eval/fixtures/fullstack-app/config/default.js +0 -9
- package/template/wall-e/eval/fixtures/fullstack-app/config/test.js +0 -13
- package/template/wall-e/eval/fixtures/fullstack-app/package.json +0 -11
- package/template/wall-e/eval/fixtures/fullstack-app/public/css/style.css +0 -137
- package/template/wall-e/eval/fixtures/fullstack-app/public/index.html +0 -46
- package/template/wall-e/eval/fixtures/fullstack-app/public/js/app.js +0 -121
- package/template/wall-e/eval/fixtures/fullstack-app/public/js/auth.js +0 -71
- package/template/wall-e/eval/fixtures/fullstack-app/public/js/items.js +0 -80
- package/template/wall-e/eval/fixtures/fullstack-app/public/js/users.js +0 -46
- package/template/wall-e/eval/fixtures/fullstack-app/public/login.html +0 -45
- package/template/wall-e/eval/fixtures/fullstack-app/public/register.html +0 -38
- package/template/wall-e/eval/fixtures/fullstack-app/scripts/migrate.js +0 -23
- package/template/wall-e/eval/fixtures/fullstack-app/scripts/seed.js +0 -46
- package/template/wall-e/eval/fixtures/fullstack-app/server/db.js +0 -99
- package/template/wall-e/eval/fixtures/fullstack-app/server/index.js +0 -94
- package/template/wall-e/eval/fixtures/fullstack-app/server/middleware/auth.js +0 -19
- package/template/wall-e/eval/fixtures/fullstack-app/server/middleware/logger.js +0 -19
- package/template/wall-e/eval/fixtures/fullstack-app/server/router.js +0 -50
- package/template/wall-e/eval/fixtures/fullstack-app/server/routes/auth.js +0 -69
- package/template/wall-e/eval/fixtures/fullstack-app/server/routes/health.js +0 -23
- package/template/wall-e/eval/fixtures/fullstack-app/server/routes/items.js +0 -88
- package/template/wall-e/eval/fixtures/fullstack-app/server/routes/users.js +0 -75
- package/template/wall-e/eval/fixtures/fullstack-app/server/test.js +0 -198
- package/template/wall-e/eval/fixtures/fullstack-app/server/utils/response.js +0 -34
- package/template/wall-e/eval/fixtures/fullstack-app/server/utils/validate.js +0 -26
- package/template/wall-e/eval/fixtures/fullstack-app/server.js +0 -8
- package/template/wall-e/eval/fixtures/fullstack-app/test.js +0 -12
- package/template/wall-e/eval/fixtures/monorepo-basic/package.json +0 -8
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/data.js +0 -58
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/middleware.js +0 -46
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/package.json +0 -8
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/routes.js +0 -64
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/server.js +0 -56
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/test.js +0 -116
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/commands.js +0 -61
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/index.js +0 -62
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/output.js +0 -43
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/package.json +0 -11
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/test.js +0 -44
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/formatters.js +0 -43
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/index.js +0 -12
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/package.json +0 -5
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/test.js +0 -55
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/validators.js +0 -29
- package/template/wall-e/eval/fixtures/monorepo-basic/test.js +0 -46
- package/template/wall-e/eval/fixtures/node-cli/index.js +0 -78
- package/template/wall-e/eval/fixtures/node-cli/package.json +0 -10
- package/template/wall-e/eval/fixtures/node-cli/test.js +0 -57
- package/template/wall-e/eval/fixtures/node-typed/package.json +0 -8
- package/template/wall-e/eval/fixtures/node-typed/src/handlers.js +0 -31
- package/template/wall-e/eval/fixtures/node-typed/src/utils.js +0 -33
- package/template/wall-e/eval/fixtures/node-typed/test.js +0 -36
- package/template/wall-e/eval/fixtures/python-flask/app.py +0 -14
- package/template/wall-e/eval/fixtures/python-flask/requirements.txt +0 -2
- package/template/wall-e/eval/fixtures/python-flask/test_app.py +0 -25
- package/template/wall-e/eval/fixtures/wall-e-subset/brain.js +0 -105
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/aggregator.js +0 -101
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/benchmarks/chat.json +0 -20
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/benchmarks/coding.json +0 -32
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/benchmarks.js +0 -64
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/fixtures/simple-project/package.json +0 -6
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/fixtures/simple-project/server.js +0 -31
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/fixtures/simple-project/test.js +0 -18
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/fixtures/simple-project/utils.js +0 -34
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/runner.js +0 -104
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/scorer.js +0 -73
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/test.js +0 -134
- package/template/wall-e/eval/fixtures/wall-e-subset/llm/client.js +0 -99
- package/template/wall-e/eval/fixtures/wall-e-subset/llm/providers.js +0 -63
- package/template/wall-e/eval/fixtures/wall-e-subset/llm/test.js +0 -70
- package/template/wall-e/eval/fixtures/wall-e-subset/package.json +0 -10
- package/template/wall-e/eval/fixtures/wall-e-subset/test.js +0 -86
- package/template/wall-e/eval/harvester.js +0 -685
- package/template/wall-e/eval/head-to-head.js +0 -388
- package/template/wall-e/eval/humaneval-adapter.js +0 -321
- package/template/wall-e/eval/list-models.js +0 -31
- package/template/wall-e/eval/livecodebench-adapter.js +0 -291
- package/template/wall-e/eval/mail-integration.js +0 -443
- package/template/wall-e/eval/manifest.js +0 -186
- package/template/wall-e/eval/meta-harness/adapters/coding-agent.js +0 -57
- package/template/wall-e/eval/meta-harness/bootstrap-snapshot.js +0 -149
- package/template/wall-e/eval/meta-harness/candidate-store.js +0 -117
- package/template/wall-e/eval/meta-harness/cli.js +0 -86
- package/template/wall-e/eval/meta-harness/domain-spec.js +0 -154
- package/template/wall-e/eval/meta-harness/domains/coding-agent.domain.json +0 -84
- package/template/wall-e/eval/meta-harness/examples/env-bootstrap-candidate.js +0 -29
- package/template/wall-e/eval/meta-harness/experience-store.js +0 -174
- package/template/wall-e/eval/meta-harness/frontier.js +0 -96
- package/template/wall-e/eval/meta-harness/harness-interface.js +0 -90
- package/template/wall-e/eval/meta-harness/leakage-guard.js +0 -80
- package/template/wall-e/eval/meta-harness/optimizer.js +0 -207
- package/template/wall-e/eval/meta-harness/proposer-runner.js +0 -110
- package/template/wall-e/eval/meta-harness/reporting.js +0 -58
- package/template/wall-e/eval/meta-harness/telemetry.js +0 -27
- package/template/wall-e/eval/meta-harness/validation.js +0 -81
- package/template/wall-e/eval/promoter.js +0 -228
- package/template/wall-e/eval/provider-normalizer.js +0 -33
- package/template/wall-e/eval/replay.js +0 -395
- package/template/wall-e/eval/run-agent-benchmarks.js +0 -386
- package/template/wall-e/eval/run-codex-cli-baseline.js +0 -177
- package/template/wall-e/eval/run-coding-agent-real.js +0 -187
- package/template/wall-e/eval/run-eval.js +0 -435
- package/template/wall-e/eval/run-model-comparison.js +0 -142
- package/template/wall-e/eval/session-evaluator.js +0 -187
- package/template/wall-e/eval/session-miner.js +0 -207
- package/template/wall-e/eval/session-retrieval-benchmark.js +0 -150
- package/template/wall-e/eval/session-transcripts.js +0 -509
- package/template/wall-e/eval/shadow.js +0 -161
- package/template/wall-e/eval/swebench-adapter.js +0 -345
- package/template/wall-e/eval/swebench-docker.js +0 -192
- package/template/wall-e/eval/train.py +0 -320
- package/template/wall-e/eval/trainer.js +0 -232
- package/template/wall-e/eval/weekly-eval-loop.js +0 -241
package/template/wall-e/brain.js
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
// --- WALL-E Brain: SQLite Database Layer (WAL mode, better-sqlite3) ---
|
|
2
|
+
const {
|
|
3
|
+
assertNoForeignSqliteOwner,
|
|
4
|
+
} = require('../shared/sqlite-owner-guard');
|
|
5
|
+
|
|
6
|
+
assertNoForeignSqliteOwner({
|
|
7
|
+
blockedEnv: 'CTM_PROCESS_ROLE',
|
|
8
|
+
overrideEnv: 'WALL_E_ALLOW_EMBEDDED_BRAIN_IN_CTM',
|
|
9
|
+
databaseLabel: 'Wall-E brain DB',
|
|
10
|
+
ownerLabel: 'Wall-E',
|
|
11
|
+
});
|
|
12
|
+
|
|
2
13
|
const Database = require('better-sqlite3');
|
|
3
14
|
const path = require('path');
|
|
4
15
|
const fs = require('fs');
|
|
@@ -9,6 +20,16 @@ const { createWriteAuditLog } = require('./db/write-audit');
|
|
|
9
20
|
const { inferProviderFromModel, normalizeEvalProvider } = require('./eval/provider-normalizer');
|
|
10
21
|
const { capabilitiesForOllamaModel } = require('./llm/ollama');
|
|
11
22
|
const { findSupportedModel, supportedModelIdsForProvider } = require('./llm/supported-models');
|
|
23
|
+
const { isPortkeyProviderConfig } = require('./llm/portkey');
|
|
24
|
+
const {
|
|
25
|
+
enforceSqliteStoragePolicy,
|
|
26
|
+
} = require('../shared/sqlite-storage-policy');
|
|
27
|
+
const {
|
|
28
|
+
installSqliteWriteLock,
|
|
29
|
+
} = require('../shared/sqlite-write-lock');
|
|
30
|
+
const {
|
|
31
|
+
createSqliteOwnerWriteQueue,
|
|
32
|
+
} = require('../shared/sqlite-owner-write-queue');
|
|
12
33
|
const _brainEvents = new EventEmitter();
|
|
13
34
|
_brainEvents.setMaxListeners(20);
|
|
14
35
|
let _stripNoise;
|
|
@@ -28,16 +49,124 @@ configureDevboxGateway();
|
|
|
28
49
|
|
|
29
50
|
const DATA_DIR = process.env.WALL_E_DATA_DIR || path.join(process.env.HOME, '.walle', 'data');
|
|
30
51
|
const DEFAULT_DB_PATH = path.join(DATA_DIR, 'wall-e-brain.db');
|
|
31
|
-
const
|
|
52
|
+
const DEFAULT_BACKUP_DIR = _normalizeUserPath(process.env.WALL_E_BACKUP_DIR || path.join(DATA_DIR, 'backups'));
|
|
53
|
+
let BACKUP_DIR = DEFAULT_BACKUP_DIR;
|
|
54
|
+
let BACKUP_DIR_SOURCE = process.env.WALL_E_BACKUP_DIR ? 'env' : 'default';
|
|
32
55
|
const writeAuditLog = createWriteAuditLog({ dataDir: DATA_DIR });
|
|
33
56
|
const { logWrite } = writeAuditLog;
|
|
34
57
|
|
|
35
58
|
let db = null;
|
|
36
59
|
let currentDbPath = null;
|
|
60
|
+
let ownerWriteQueue = null;
|
|
37
61
|
let _daemonOwned = false; // When true, closeDb() is a no-op (daemon manages lifecycle)
|
|
62
|
+
let storageRisk = null;
|
|
63
|
+
|
|
64
|
+
function _normalizeUserPath(value) {
|
|
65
|
+
const raw = String(value || '').trim().replace(/[\r\n\0]/g, '');
|
|
66
|
+
if (!raw) return '';
|
|
67
|
+
const expanded = raw === '~'
|
|
68
|
+
? process.env.HOME
|
|
69
|
+
: raw.replace(/^~(?=\/|$)/, process.env.HOME || '~');
|
|
70
|
+
return path.resolve(expanded);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function _ensureBackupDir(dir) {
|
|
74
|
+
const target = _normalizeUserPath(dir || BACKUP_DIR || DEFAULT_BACKUP_DIR);
|
|
75
|
+
if (!target) throw new Error('Backup directory is empty');
|
|
76
|
+
fs.mkdirSync(target, { recursive: true });
|
|
77
|
+
const stat = fs.statSync(target);
|
|
78
|
+
if (!stat.isDirectory()) throw new Error('Backup path is not a directory');
|
|
79
|
+
return target;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function _moveFileAcrossDevices(src, dst) {
|
|
83
|
+
try {
|
|
84
|
+
fs.renameSync(src, dst);
|
|
85
|
+
return;
|
|
86
|
+
} catch (err) {
|
|
87
|
+
if (err && err.code !== 'EXDEV') throw err;
|
|
88
|
+
}
|
|
89
|
+
fs.copyFileSync(src, dst, fs.constants.COPYFILE_EXCL);
|
|
90
|
+
fs.unlinkSync(src);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function _wallEBackupFile(name) {
|
|
94
|
+
return /^wall-e-brain-.+\.db$/i.test(String(name || ''));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function moveBackupsToDir(targetDir, opts = {}) {
|
|
98
|
+
const previous = _backupDirForCurrentDb();
|
|
99
|
+
const target = _ensureBackupDir(targetDir || DEFAULT_BACKUP_DIR);
|
|
100
|
+
const moved = [];
|
|
101
|
+
const skipped = [];
|
|
102
|
+
if (path.resolve(previous) !== path.resolve(target) && fs.existsSync(previous)) {
|
|
103
|
+
for (const name of fs.readdirSync(previous)) {
|
|
104
|
+
if (!_wallEBackupFile(name)) continue;
|
|
105
|
+
const src = path.join(previous, name);
|
|
106
|
+
const dst = path.join(target, name);
|
|
107
|
+
try {
|
|
108
|
+
if (fs.existsSync(dst)) {
|
|
109
|
+
skipped.push({ name, reason: 'exists' });
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
_moveFileAcrossDevices(src, dst);
|
|
113
|
+
moved.push(name);
|
|
114
|
+
} catch (e) {
|
|
115
|
+
skipped.push({ name, reason: e.message });
|
|
116
|
+
if (opts.failFast !== false) throw e;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return { ok: true, previous_backup_dir: previous, backup_dir: target, moved, skipped };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function setBackupDir(dir, opts = {}) {
|
|
124
|
+
const requested = String(dir || '').trim();
|
|
125
|
+
const target = _ensureBackupDir(requested || DEFAULT_BACKUP_DIR);
|
|
126
|
+
const previous = BACKUP_DIR;
|
|
127
|
+
if (opts.moveExisting) moveBackupsToDir(target, { failFast: true });
|
|
128
|
+
BACKUP_DIR = target;
|
|
129
|
+
BACKUP_DIR_SOURCE = requested ? 'settings' : (process.env.WALL_E_BACKUP_DIR ? 'env' : 'default');
|
|
130
|
+
if (opts.persist && db) {
|
|
131
|
+
setKv('backup_dir', requested ? target : '');
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
ok: true,
|
|
135
|
+
backup_dir: BACKUP_DIR,
|
|
136
|
+
previous_backup_dir: previous,
|
|
137
|
+
default_backup_dir: DEFAULT_BACKUP_DIR,
|
|
138
|
+
source: BACKUP_DIR_SOURCE,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function getBackupDirInfo() {
|
|
143
|
+
let configured = '';
|
|
144
|
+
try {
|
|
145
|
+
if (db) configured = getKv('backup_dir') || '';
|
|
146
|
+
} catch {}
|
|
147
|
+
return {
|
|
148
|
+
backup_dir: _backupDirForCurrentDb(),
|
|
149
|
+
default_backup_dir: DEFAULT_BACKUP_DIR,
|
|
150
|
+
configured_backup_dir: configured,
|
|
151
|
+
source: configured ? 'settings' : BACKUP_DIR_SOURCE,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function _applyPersistedBackupDirSetting() {
|
|
156
|
+
try {
|
|
157
|
+
if (!db) return;
|
|
158
|
+
const configured = getKv('backup_dir') || '';
|
|
159
|
+
setBackupDir(configured, { persist: false });
|
|
160
|
+
} catch (e) {
|
|
161
|
+
console.error('[brain] Backup directory setting ignored:', e.message);
|
|
162
|
+
_ensureBackupDir(DEFAULT_BACKUP_DIR);
|
|
163
|
+
BACKUP_DIR = DEFAULT_BACKUP_DIR;
|
|
164
|
+
BACKUP_DIR_SOURCE = process.env.WALL_E_BACKUP_DIR ? 'env' : 'default';
|
|
165
|
+
}
|
|
166
|
+
}
|
|
38
167
|
|
|
39
168
|
// --- Schema versioning via PRAGMA user_version ---
|
|
40
|
-
const SCHEMA_VERSION =
|
|
169
|
+
const SCHEMA_VERSION = 18; // Bump on every migration addition
|
|
41
170
|
|
|
42
171
|
const MIGRATIONS = {
|
|
43
172
|
1: (d) => {
|
|
@@ -79,6 +208,7 @@ const MIGRATIONS = {
|
|
|
79
208
|
d.prepare("CREATE INDEX IF NOT EXISTS idx_memories_topic ON memories(topic)").run();
|
|
80
209
|
// History column on chat_branches
|
|
81
210
|
addCol('chat_branches', 'history', "TEXT NOT NULL DEFAULT '[]'");
|
|
211
|
+
addCol('chat_branches', 'updated_at_ms', 'INTEGER NOT NULL DEFAULT 0');
|
|
82
212
|
// Temporal validity on knowledge
|
|
83
213
|
addCol('knowledge', 'valid_from', 'TEXT');
|
|
84
214
|
addCol('knowledge', 'valid_to', 'TEXT');
|
|
@@ -365,6 +495,82 @@ const MIGRATIONS = {
|
|
|
365
495
|
ON channel_message_events(delivery_id);
|
|
366
496
|
`);
|
|
367
497
|
},
|
|
498
|
+
15: (d) => {
|
|
499
|
+
const addCol = (table, col, type) => {
|
|
500
|
+
try { d.prepare(`SELECT ${col} FROM ${table} LIMIT 0`).run(); } catch (_) {
|
|
501
|
+
d.prepare(`ALTER TABLE ${table} ADD COLUMN ${col} ${type}`).run();
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
addCol('model_registry', 'source', "TEXT DEFAULT 'catalog'");
|
|
505
|
+
addCol('model_registry', 'verification_status', "TEXT DEFAULT 'verified'");
|
|
506
|
+
addCol('model_registry', 'last_seen_at', 'TEXT');
|
|
507
|
+
addCol('model_registry', 'gateway_type', 'TEXT');
|
|
508
|
+
d.prepare("UPDATE model_registry SET source = 'catalog' WHERE source IS NULL OR source = ''").run();
|
|
509
|
+
d.prepare("UPDATE model_registry SET verification_status = 'verified' WHERE verification_status IS NULL OR verification_status = ''").run();
|
|
510
|
+
},
|
|
511
|
+
16: (d) => {
|
|
512
|
+
// Daily question digest: track when a question was delivered so it isn't re-asked every
|
|
513
|
+
// day (the digest is once/day and includes only not-yet-delivered pending questions).
|
|
514
|
+
const addCol = (table, col, type) => {
|
|
515
|
+
try { d.prepare(`SELECT ${col} FROM ${table} LIMIT 0`).run(); } catch (_) {
|
|
516
|
+
d.prepare(`ALTER TABLE ${table} ADD COLUMN ${col} ${type}`).run();
|
|
517
|
+
}
|
|
518
|
+
};
|
|
519
|
+
addCol('pending_questions', 'digest_delivered_at', 'TEXT');
|
|
520
|
+
d.prepare('CREATE INDEX IF NOT EXISTS idx_questions_digest ON pending_questions(status, digest_delivered_at)').run();
|
|
521
|
+
},
|
|
522
|
+
17: (d) => {
|
|
523
|
+
d.exec(`
|
|
524
|
+
CREATE TABLE IF NOT EXISTS model_usage_ledger (
|
|
525
|
+
id TEXT PRIMARY KEY,
|
|
526
|
+
occurred_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
527
|
+
source TEXT NOT NULL DEFAULT 'wall-e.chat',
|
|
528
|
+
feature TEXT,
|
|
529
|
+
session_id TEXT,
|
|
530
|
+
branch_id TEXT,
|
|
531
|
+
message_id TEXT,
|
|
532
|
+
request_id TEXT,
|
|
533
|
+
provider_type TEXT,
|
|
534
|
+
provider_id TEXT,
|
|
535
|
+
model_id TEXT,
|
|
536
|
+
model_registry_id TEXT,
|
|
537
|
+
gateway_type TEXT,
|
|
538
|
+
route_label TEXT,
|
|
539
|
+
input_tokens INTEGER DEFAULT 0,
|
|
540
|
+
output_tokens INTEGER DEFAULT 0,
|
|
541
|
+
total_tokens INTEGER DEFAULT 0,
|
|
542
|
+
cached_input_tokens INTEGER DEFAULT 0,
|
|
543
|
+
reasoning_output_tokens INTEGER DEFAULT 0,
|
|
544
|
+
latency_ms INTEGER,
|
|
545
|
+
stop_reason TEXT,
|
|
546
|
+
status TEXT DEFAULT 'success',
|
|
547
|
+
error_type TEXT,
|
|
548
|
+
cost_usd REAL,
|
|
549
|
+
cost_source TEXT,
|
|
550
|
+
metadata TEXT,
|
|
551
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
552
|
+
);
|
|
553
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_model_usage_ledger_request
|
|
554
|
+
ON model_usage_ledger(source, request_id)
|
|
555
|
+
WHERE request_id IS NOT NULL AND request_id != '';
|
|
556
|
+
CREATE INDEX IF NOT EXISTS idx_model_usage_ledger_occurred
|
|
557
|
+
ON model_usage_ledger(occurred_at);
|
|
558
|
+
CREATE INDEX IF NOT EXISTS idx_model_usage_ledger_session
|
|
559
|
+
ON model_usage_ledger(session_id, occurred_at);
|
|
560
|
+
CREATE INDEX IF NOT EXISTS idx_model_usage_ledger_provider_model
|
|
561
|
+
ON model_usage_ledger(provider_type, model_id, occurred_at);
|
|
562
|
+
CREATE INDEX IF NOT EXISTS idx_model_usage_ledger_registry
|
|
563
|
+
ON model_usage_ledger(model_registry_id, occurred_at);
|
|
564
|
+
`);
|
|
565
|
+
},
|
|
566
|
+
18: (d) => {
|
|
567
|
+
const addCol = (table, col, type) => {
|
|
568
|
+
try { d.prepare(`SELECT ${col} FROM ${table} LIMIT 0`).run(); } catch (_) {
|
|
569
|
+
d.prepare(`ALTER TABLE ${table} ADD COLUMN ${col} ${type}`).run();
|
|
570
|
+
}
|
|
571
|
+
};
|
|
572
|
+
addCol('chat_branches', 'updated_at_ms', 'INTEGER NOT NULL DEFAULT 0');
|
|
573
|
+
},
|
|
368
574
|
};
|
|
369
575
|
|
|
370
576
|
// Schema invariants — columns/tables that MUST exist after the named migration.
|
|
@@ -382,6 +588,9 @@ const SCHEMA_INVARIANTS = [
|
|
|
382
588
|
{ migration: 9, table: 'agent_runner_evaluations', column: 'runner_id' },
|
|
383
589
|
{ migration: 10, table: 'eval_benchmark_runs', column: 'dataset_version' },
|
|
384
590
|
{ migration: 14, table: 'channel_message_events', column: 'channel' },
|
|
591
|
+
{ migration: 15, table: 'model_registry', column: 'source' },
|
|
592
|
+
{ migration: 17, table: 'model_usage_ledger', column: 'id' },
|
|
593
|
+
{ migration: 18, table: 'chat_branches', column: 'updated_at_ms' },
|
|
385
594
|
];
|
|
386
595
|
|
|
387
596
|
function _columnExists(d, table, column) {
|
|
@@ -540,6 +749,428 @@ function getDbPath() {
|
|
|
540
749
|
return currentDbPath || DEFAULT_DB_PATH;
|
|
541
750
|
}
|
|
542
751
|
|
|
752
|
+
function getStorageRisk() {
|
|
753
|
+
return storageRisk;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
function _storagePolicyEnvForCurrentProcess() {
|
|
757
|
+
const isScriptSkillProcess = Boolean(process.env.WALLE_BUNDLED_SKILL_NAME || process.env.WALL_E_SKILL_DIR);
|
|
758
|
+
if (!isScriptSkillProcess) return process.env;
|
|
759
|
+
if (process.env.WALL_E_SQLITE_STORAGE_POLICY || process.env.SQLITE_STORAGE_POLICY) return process.env;
|
|
760
|
+
return {
|
|
761
|
+
...process.env,
|
|
762
|
+
WALL_E_SQLITE_STORAGE_POLICY: 'error',
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
function _checkStoragePolicy(dbPath) {
|
|
767
|
+
storageRisk = enforceSqliteStoragePolicy(dbPath, {
|
|
768
|
+
env: _storagePolicyEnvForCurrentProcess(),
|
|
769
|
+
prefix: 'WALL_E',
|
|
770
|
+
label: 'Wall-E wall-e-brain.db',
|
|
771
|
+
});
|
|
772
|
+
return storageRisk;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
function _loadSqliteVec(d) {
|
|
776
|
+
// Load sqlite-vec before schema/PRAGMA reads. Without it, DBs containing
|
|
777
|
+
// vec0 virtual tables can look corrupt even when the file is structurally OK.
|
|
778
|
+
try {
|
|
779
|
+
const sqliteVec = require('sqlite-vec');
|
|
780
|
+
sqliteVec.load(d);
|
|
781
|
+
} catch {
|
|
782
|
+
// sqlite-vec not installed — fine if DB has no vec0 tables.
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
function _quickCheckFile(filePath) {
|
|
787
|
+
let d = null;
|
|
788
|
+
try {
|
|
789
|
+
if (!fs.existsSync(filePath) || fs.statSync(filePath).size === 0) {
|
|
790
|
+
return { ok: true, skipped: true, reason: 'missing_or_empty' };
|
|
791
|
+
}
|
|
792
|
+
d = new Database(filePath, { readonly: true, fileMustExist: true });
|
|
793
|
+
_loadSqliteVec(d);
|
|
794
|
+
d.pragma('busy_timeout = 5000');
|
|
795
|
+
const rows = d.pragma('quick_check');
|
|
796
|
+
const messages = (Array.isArray(rows) ? rows : [rows])
|
|
797
|
+
.map(row => String(Object.values(row || {})[0] || '').trim())
|
|
798
|
+
.filter(Boolean);
|
|
799
|
+
if (messages.length === 1 && messages[0].toLowerCase() === 'ok') return { ok: true };
|
|
800
|
+
return { ok: false, error: messages.join('\n') || 'quick_check failed', classification: 'sqlite_structural_corruption' };
|
|
801
|
+
} catch (err) {
|
|
802
|
+
return _classifySqliteError(err);
|
|
803
|
+
} finally {
|
|
804
|
+
try { if (d) d.close(); } catch {}
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// Map a thrown SQLite error to {ok:false, error, code, classification}.
|
|
809
|
+
function _classifySqliteError(err) {
|
|
810
|
+
const code = String(err?.code || '');
|
|
811
|
+
const message = String(err?.message || err || '');
|
|
812
|
+
const text = `${code} ${message}`.toLowerCase();
|
|
813
|
+
let classification = 'unknown';
|
|
814
|
+
if (/sqlite_(corrupt|notadb)|database disk image is malformed|malformed database|database schema is corrupt|file is not a database/.test(text)) {
|
|
815
|
+
classification = 'sqlite_structural_corruption';
|
|
816
|
+
} else if (/sqlite_(busy|locked)|database is locked|database table is locked/.test(text)) {
|
|
817
|
+
classification = 'sqlite_locked';
|
|
818
|
+
} else if (/sqlite_cantopen|unable to open database file/.test(text)) {
|
|
819
|
+
classification = 'sqlite_unavailable';
|
|
820
|
+
}
|
|
821
|
+
return { ok: false, error: message, code: code || undefined, classification };
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// Cheap boot-time integrity probe: open readonly + read the header (user_version) and the
|
|
825
|
+
// schema. Catches GROSS corruption (truncated / not-a-db / malformed header or schema) in
|
|
826
|
+
// milliseconds, WITHOUT the full-file `quick_check` page scan — which costs 12-42s on a
|
|
827
|
+
// multi-GB brain on EVERY boot (measured via the boot profile). Deep page-level integrity
|
|
828
|
+
// is still verified off the boot path by the daily backup job (createBackup runs
|
|
829
|
+
// quick_check on the produced image and fails/alerts if it's bad).
|
|
830
|
+
function _cheapIntegrityCheck(filePath) {
|
|
831
|
+
let d = null;
|
|
832
|
+
try {
|
|
833
|
+
if (!fs.existsSync(filePath) || fs.statSync(filePath).size === 0) {
|
|
834
|
+
return { ok: true, skipped: true, reason: 'missing_or_empty' };
|
|
835
|
+
}
|
|
836
|
+
d = new Database(filePath, { readonly: true, fileMustExist: true });
|
|
837
|
+
d.pragma('busy_timeout = 5000');
|
|
838
|
+
d.pragma('user_version'); // reads the DB header
|
|
839
|
+
d.prepare('SELECT count(*) AS n FROM sqlite_master').get(); // reads the schema
|
|
840
|
+
return { ok: true };
|
|
841
|
+
} catch (err) {
|
|
842
|
+
return _classifySqliteError(err);
|
|
843
|
+
} finally {
|
|
844
|
+
try { if (d) d.close(); } catch {}
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
function _archiveDbFamily(dbPath, reason) {
|
|
849
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
850
|
+
const archived = [];
|
|
851
|
+
for (const ext of ['', '-wal', '-shm']) {
|
|
852
|
+
const src = dbPath + ext;
|
|
853
|
+
if (!fs.existsSync(src)) continue;
|
|
854
|
+
const dst = `${dbPath}.corrupt-${ts}${ext}`;
|
|
855
|
+
fs.renameSync(src, dst);
|
|
856
|
+
archived.push(dst);
|
|
857
|
+
}
|
|
858
|
+
console.error(`[brain] Archived corrupt DB family (${reason}): ${archived.map(f => path.basename(f)).join(', ')}`);
|
|
859
|
+
return archived;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
function _backupDirsForRestore(dbPath) {
|
|
863
|
+
const dirs = [
|
|
864
|
+
path.join(path.dirname(dbPath), 'backups'),
|
|
865
|
+
BACKUP_DIR,
|
|
866
|
+
];
|
|
867
|
+
return [...new Set(dirs)].filter(Boolean);
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
function _materializeBackupCandidate(backupPath, tmpPath) {
|
|
871
|
+
fs.rmSync(tmpPath, { force: true });
|
|
872
|
+
fs.copyFileSync(backupPath, tmpPath);
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
function _restoreLatestHealthyBackup(dbPath) {
|
|
876
|
+
const candidates = [];
|
|
877
|
+
for (const dir of _backupDirsForRestore(dbPath)) {
|
|
878
|
+
if (!fs.existsSync(dir)) continue;
|
|
879
|
+
for (const name of fs.readdirSync(dir)) {
|
|
880
|
+
if (!name.startsWith('wall-e-brain-') || !name.endsWith('.db')) continue;
|
|
881
|
+
const backupPath = path.join(dir, name);
|
|
882
|
+
try {
|
|
883
|
+
candidates.push({ path: backupPath, mtimeMs: fs.statSync(backupPath).mtimeMs });
|
|
884
|
+
} catch {}
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
candidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
888
|
+
|
|
889
|
+
const dir = path.dirname(dbPath);
|
|
890
|
+
for (const candidate of candidates) {
|
|
891
|
+
const tmp = path.join(dir, `wall-e-brain.db.restore-candidate-${process.pid}-${Date.now()}`);
|
|
892
|
+
try {
|
|
893
|
+
_materializeBackupCandidate(candidate.path, tmp);
|
|
894
|
+
const check = _quickCheckFile(tmp);
|
|
895
|
+
if (!check.ok) {
|
|
896
|
+
fs.rmSync(tmp, { force: true });
|
|
897
|
+
continue;
|
|
898
|
+
}
|
|
899
|
+
fs.renameSync(tmp, dbPath);
|
|
900
|
+
console.error(`[brain] Recovered from healthy backup ${path.basename(candidate.path)}`);
|
|
901
|
+
return candidate.path;
|
|
902
|
+
} catch (err) {
|
|
903
|
+
console.error(`[brain] Skipped backup candidate ${path.basename(candidate.path)}: ${err.message}`);
|
|
904
|
+
try { fs.rmSync(tmp, { force: true }); } catch {}
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
return null;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
function _recoverCorruptDbIfNeeded(dbPath) {
|
|
911
|
+
if (!fs.existsSync(dbPath) || fs.statSync(dbPath).size === 0) return null;
|
|
912
|
+
// Cheap header/schema probe (ms) instead of a full quick_check (12-42s/boot). Gross
|
|
913
|
+
// corruption that actually blocks operation is still caught here → archive + restore.
|
|
914
|
+
// Subtle page corruption is caught off-boot by the daily backup's quick_check.
|
|
915
|
+
const check = _cheapIntegrityCheck(dbPath);
|
|
916
|
+
if (check.ok) return null;
|
|
917
|
+
const classification = check.classification || 'unknown';
|
|
918
|
+
if (classification !== 'sqlite_structural_corruption') {
|
|
919
|
+
throw new Error(`Wall-E brain health check failed with ${classification}: ${check.error}`);
|
|
920
|
+
}
|
|
921
|
+
_archiveDbFamily(dbPath, `quick_check failed: ${String(check.error || '').split('\n')[0] || 'sqlite structural corruption'}`);
|
|
922
|
+
const restoredFrom = _restoreLatestHealthyBackup(dbPath);
|
|
923
|
+
if (!restoredFrom) {
|
|
924
|
+
throw new Error(`Wall-E brain quick_check failed and no healthy backup was found: ${check.error}`);
|
|
925
|
+
}
|
|
926
|
+
return { restoredFrom };
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
function _sqliteTimeoutMs(name, fallback) {
|
|
930
|
+
const n = Number(process.env[name]);
|
|
931
|
+
return Number.isFinite(n) ? Math.max(0, Math.trunc(n)) : fallback;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
function _sqlitePositiveInt(name, fallback) {
|
|
935
|
+
const n = Number(process.env[name]);
|
|
936
|
+
return Number.isFinite(n) ? Math.max(1, Math.trunc(n)) : fallback;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
function _isWriteLockBusyError(err) {
|
|
940
|
+
if (!err) return false;
|
|
941
|
+
if (err.code === 'SQLITE_BUSY' || err.code === 'SQLITE_WRITE_LOCK_BUSY') return true;
|
|
942
|
+
return /write lock busy|SQLITE_BUSY/i.test(String(err.message || ''));
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
// Boot-only bounded SYNC retry for cross-process write-lock contention while
|
|
946
|
+
// OPENING the brain (journal_mode=WAL, table create, migrations). Steady-state
|
|
947
|
+
// writes keep their 0ms main-thread fail-fast (set at installSqliteWriteLock);
|
|
948
|
+
// this runs only during initDb — before the daemon serves — so a transient busy
|
|
949
|
+
// lock at boot no longer throws FATAL and crash-loops with "MCP never became
|
|
950
|
+
// ready". The wrapped init writes are all idempotent, so re-running is safe.
|
|
951
|
+
function _retryBusySync(fn, deadlineMs, pollMs = 50) {
|
|
952
|
+
const start = Date.now();
|
|
953
|
+
for (;;) {
|
|
954
|
+
try {
|
|
955
|
+
return fn();
|
|
956
|
+
} catch (err) {
|
|
957
|
+
if (!_isWriteLockBusyError(err) || (Date.now() - start) >= deadlineMs) throw err;
|
|
958
|
+
try {
|
|
959
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, Math.max(1, pollMs));
|
|
960
|
+
} catch {
|
|
961
|
+
/* SharedArrayBuffer unavailable — fall through and retry immediately */
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// ---------------------------------------------------------------------------
|
|
968
|
+
// Write-lock probes (mirrors CTM's db.js instrumentation). The brain DB is
|
|
969
|
+
// contended CROSS-PROCESS (the daemon + each spawned skill subprocess opens its
|
|
970
|
+
// own connection to the same file lock), so unlike CTM the relevant identity is
|
|
971
|
+
// the OS pid, not a worker threadId. We aggregate per-label acquire/busy/hold/wait
|
|
972
|
+
// stats with hold/wait histograms (p50/p95) and, on slow/busy events, log the
|
|
973
|
+
// originating caller plus the lock HOLDER's pid — so log analysis can map
|
|
974
|
+
// "caller X in pid A blocked by holder pid B". Exposed via
|
|
975
|
+
// GET /api/wall-e/diagnostics/write-lock.
|
|
976
|
+
const WRITE_LOCK_SLOW_HOLD_MS = _sqlitePositiveInt('WALL_E_WRITE_LOCK_SLOW_HOLD_MS', 250);
|
|
977
|
+
const WRITE_LOCK_SLOW_WAIT_MS = _sqlitePositiveInt('WALL_E_WRITE_LOCK_SLOW_WAIT_MS', 250);
|
|
978
|
+
const WRITE_LOCK_HIST_BUCKETS_MS = [1, 5, 25, 100, 500, 2000];
|
|
979
|
+
function _newWriteLockHist() {
|
|
980
|
+
return new Array(WRITE_LOCK_HIST_BUCKETS_MS.length + 1).fill(0);
|
|
981
|
+
}
|
|
982
|
+
function _writeLockHistAdd(hist, ms) {
|
|
983
|
+
const value = Number(ms) || 0;
|
|
984
|
+
for (let i = 0; i < WRITE_LOCK_HIST_BUCKETS_MS.length; i += 1) {
|
|
985
|
+
if (value < WRITE_LOCK_HIST_BUCKETS_MS[i]) { hist[i] += 1; return; }
|
|
986
|
+
}
|
|
987
|
+
hist[WRITE_LOCK_HIST_BUCKETS_MS.length] += 1;
|
|
988
|
+
}
|
|
989
|
+
function _writeLockBucketLabel(i) {
|
|
990
|
+
if (i < WRITE_LOCK_HIST_BUCKETS_MS.length) return `<${WRITE_LOCK_HIST_BUCKETS_MS[i]}ms`;
|
|
991
|
+
return `>=${WRITE_LOCK_HIST_BUCKETS_MS[WRITE_LOCK_HIST_BUCKETS_MS.length - 1]}ms`;
|
|
992
|
+
}
|
|
993
|
+
function _writeLockPercentile(hist, pct) {
|
|
994
|
+
const total = hist.reduce((a, b) => a + b, 0);
|
|
995
|
+
if (!total) return null;
|
|
996
|
+
const target = Math.ceil(total * pct);
|
|
997
|
+
let cumulative = 0;
|
|
998
|
+
for (let i = 0; i < hist.length; i += 1) {
|
|
999
|
+
cumulative += hist[i];
|
|
1000
|
+
if (cumulative >= target) return _writeLockBucketLabel(i);
|
|
1001
|
+
}
|
|
1002
|
+
return _writeLockBucketLabel(hist.length - 1);
|
|
1003
|
+
}
|
|
1004
|
+
function _newWriteLockStats() {
|
|
1005
|
+
return {
|
|
1006
|
+
startedAtMs: Date.now(),
|
|
1007
|
+
acquires: 0, busy: 0, contended: 0,
|
|
1008
|
+
holdSum: 0, holdMax: 0, holdHist: _newWriteLockHist(),
|
|
1009
|
+
waitSum: 0, waitMax: 0, waitHist: _newWriteLockHist(),
|
|
1010
|
+
byLabel: new Map(),
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
let _writeLockStats = _newWriteLockStats();
|
|
1014
|
+
function _writeLockLabelStat(label) {
|
|
1015
|
+
let stat = _writeLockStats.byLabel.get(label);
|
|
1016
|
+
if (!stat) {
|
|
1017
|
+
stat = { acquires: 0, busy: 0, holdSum: 0, holdMax: 0, waitMax: 0 };
|
|
1018
|
+
_writeLockStats.byLabel.set(label, stat);
|
|
1019
|
+
}
|
|
1020
|
+
return stat;
|
|
1021
|
+
}
|
|
1022
|
+
// Identify the originating call site for slow/busy log lines. onEvent runs
|
|
1023
|
+
// synchronously inside withSqliteWriteLock (invoked synchronously via
|
|
1024
|
+
// stmt.run/exec/pragma/transaction), so the caller frame is still on the stack.
|
|
1025
|
+
// Only computed on a slow/busy event (rare), so the Error().stack cost is moot.
|
|
1026
|
+
function _writeLockCallerHint() {
|
|
1027
|
+
try {
|
|
1028
|
+
const lines = String(new Error().stack || '').split('\n').slice(2);
|
|
1029
|
+
for (const line of lines) {
|
|
1030
|
+
if (/sqlite-write-lock\.js|_onWriteLockEvent|_writeLockCallerHint|node:|node_modules/.test(line)) continue;
|
|
1031
|
+
const m = line.match(/at\s+(?:(\S+)\s+\()?(?:.*\/)?([^/\s):]+):(\d+):\d+\)?/);
|
|
1032
|
+
if (m) return `${m[1] ? m[1] + '@' : ''}${m[2]}:${m[3]}`;
|
|
1033
|
+
}
|
|
1034
|
+
} catch {}
|
|
1035
|
+
return '?';
|
|
1036
|
+
}
|
|
1037
|
+
// Pull the lock HOLDER's pid out of the busy error (the shared lock module formats
|
|
1038
|
+
// it as "... pid=<n> label=<l>: <path>.write-lock"). Cross-process attribution.
|
|
1039
|
+
function _writeLockHolderPid(err) {
|
|
1040
|
+
const m = /pid=(\d+)/.exec(err && err.message ? err.message : '');
|
|
1041
|
+
return m ? m[1] : '?';
|
|
1042
|
+
}
|
|
1043
|
+
function _onWriteLockEvent(ev) {
|
|
1044
|
+
try {
|
|
1045
|
+
const label = ev.label || '';
|
|
1046
|
+
const labelStat = _writeLockLabelStat(label);
|
|
1047
|
+
const waited = Number(ev.waitedMs) || 0;
|
|
1048
|
+
_writeLockStats.waitSum += waited;
|
|
1049
|
+
if (waited > _writeLockStats.waitMax) _writeLockStats.waitMax = waited;
|
|
1050
|
+
if (waited > labelStat.waitMax) labelStat.waitMax = waited;
|
|
1051
|
+
_writeLockHistAdd(_writeLockStats.waitHist, waited);
|
|
1052
|
+
|
|
1053
|
+
if (ev.acquired === false) {
|
|
1054
|
+
_writeLockStats.busy += 1;
|
|
1055
|
+
labelStat.busy += 1;
|
|
1056
|
+
const holder = _writeLockHolderPid(ev.error);
|
|
1057
|
+
console.warn(`[sqlite-write-lock] busy fail-fast label=${label} waitedMs=${waited} pid=${process.pid} holder=${holder} caller=${_writeLockCallerHint()}`);
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
const held = Number(ev.heldMs) || 0;
|
|
1062
|
+
_writeLockStats.acquires += 1;
|
|
1063
|
+
labelStat.acquires += 1;
|
|
1064
|
+
if (ev.contended) _writeLockStats.contended += 1;
|
|
1065
|
+
_writeLockStats.holdSum += held;
|
|
1066
|
+
if (held > _writeLockStats.holdMax) _writeLockStats.holdMax = held;
|
|
1067
|
+
labelStat.holdSum += held;
|
|
1068
|
+
if (held > labelStat.holdMax) labelStat.holdMax = held;
|
|
1069
|
+
_writeLockHistAdd(_writeLockStats.holdHist, held);
|
|
1070
|
+
|
|
1071
|
+
if (held >= WRITE_LOCK_SLOW_HOLD_MS || waited >= WRITE_LOCK_SLOW_WAIT_MS) {
|
|
1072
|
+
console.warn(`[sqlite-write-lock] slow label=${label} heldMs=${held} waitedMs=${waited} contended=${ev.contended} pid=${process.pid} caller=${_writeLockCallerHint()}`);
|
|
1073
|
+
}
|
|
1074
|
+
} catch {}
|
|
1075
|
+
}
|
|
1076
|
+
// Snapshot accumulated write-lock stats for this process. `byLabel` sorted by
|
|
1077
|
+
// total hold time (the most likely contention culprit first).
|
|
1078
|
+
function getWriteLockStats() {
|
|
1079
|
+
const s = _writeLockStats;
|
|
1080
|
+
const topLabelsByHold = [...s.byLabel.entries()]
|
|
1081
|
+
.map(([label, v]) => ({
|
|
1082
|
+
label,
|
|
1083
|
+
acquires: v.acquires,
|
|
1084
|
+
busy: v.busy,
|
|
1085
|
+
holdSumMs: v.holdSum,
|
|
1086
|
+
holdMaxMs: v.holdMax,
|
|
1087
|
+
holdAvgMs: v.acquires ? Math.round(v.holdSum / v.acquires) : 0,
|
|
1088
|
+
waitMaxMs: v.waitMax,
|
|
1089
|
+
}))
|
|
1090
|
+
.sort((a, b) => b.holdSumMs - a.holdSumMs)
|
|
1091
|
+
.slice(0, 12);
|
|
1092
|
+
return {
|
|
1093
|
+
pid: process.pid,
|
|
1094
|
+
uptimeMs: Date.now() - s.startedAtMs,
|
|
1095
|
+
acquires: s.acquires,
|
|
1096
|
+
busy: s.busy,
|
|
1097
|
+
contended: s.contended,
|
|
1098
|
+
hold: {
|
|
1099
|
+
sumMs: s.holdSum,
|
|
1100
|
+
maxMs: s.holdMax,
|
|
1101
|
+
avgMs: s.acquires ? Math.round(s.holdSum / s.acquires) : 0,
|
|
1102
|
+
p50: _writeLockPercentile(s.holdHist, 0.5),
|
|
1103
|
+
p95: _writeLockPercentile(s.holdHist, 0.95),
|
|
1104
|
+
},
|
|
1105
|
+
wait: { sumMs: s.waitSum, maxMs: s.waitMax, p95: _writeLockPercentile(s.waitHist, 0.95) },
|
|
1106
|
+
topLabelsByHold,
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
function resetWriteLockStats() {
|
|
1110
|
+
const previous = getWriteLockStats();
|
|
1111
|
+
_writeLockStats = _newWriteLockStats();
|
|
1112
|
+
return previous;
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
function _closeOwnerWriteQueueNoDrain() {
|
|
1116
|
+
const queue = ownerWriteQueue;
|
|
1117
|
+
ownerWriteQueue = null;
|
|
1118
|
+
if (!queue) return;
|
|
1119
|
+
try { queue.close({ drain: false }).catch(() => {}); } catch {}
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
function _resetOwnerWriteQueue(dbPath) {
|
|
1123
|
+
_closeOwnerWriteQueueNoDrain();
|
|
1124
|
+
ownerWriteQueue = createSqliteOwnerWriteQueue({
|
|
1125
|
+
name: `wall-e-brain:${path.basename(dbPath || DEFAULT_DB_PATH)}`,
|
|
1126
|
+
maxDepth: _sqlitePositiveInt('WALL_E_SQLITE_OWNER_WRITE_QUEUE_MAX_DEPTH', 1000),
|
|
1127
|
+
});
|
|
1128
|
+
return ownerWriteQueue;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
function _getOwnerWriteQueue() {
|
|
1132
|
+
if (!ownerWriteQueue) {
|
|
1133
|
+
ownerWriteQueue = createSqliteOwnerWriteQueue({
|
|
1134
|
+
name: `wall-e-brain:${path.basename(currentDbPath || DEFAULT_DB_PATH)}`,
|
|
1135
|
+
maxDepth: _sqlitePositiveInt('WALL_E_SQLITE_OWNER_WRITE_QUEUE_MAX_DEPTH', 1000),
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
1138
|
+
return ownerWriteQueue;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
function enqueueOwnerWrite(labelOrFn, fnOrOptions, maybeOptions) {
|
|
1142
|
+
const label = typeof labelOrFn === 'string' ? labelOrFn : 'write';
|
|
1143
|
+
const fn = typeof labelOrFn === 'function' ? labelOrFn : fnOrOptions;
|
|
1144
|
+
const options = typeof labelOrFn === 'function' ? (fnOrOptions || {}) : (maybeOptions || {});
|
|
1145
|
+
if (typeof fn !== 'function') throw new TypeError('enqueueOwnerWrite requires a function');
|
|
1146
|
+
return _getOwnerWriteQueue().enqueue(() => fn(getDb()), { ...options, label });
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
function getOwnerWriteQueueStatus() {
|
|
1150
|
+
return ownerWriteQueue
|
|
1151
|
+
? ownerWriteQueue.getStatus()
|
|
1152
|
+
: {
|
|
1153
|
+
name: `wall-e-brain:${path.basename(currentDbPath || DEFAULT_DB_PATH)}`,
|
|
1154
|
+
active: null,
|
|
1155
|
+
running: false,
|
|
1156
|
+
pending: 0,
|
|
1157
|
+
accepting: false,
|
|
1158
|
+
closed: true,
|
|
1159
|
+
maxDepth: _sqlitePositiveInt('WALL_E_SQLITE_OWNER_WRITE_QUEUE_MAX_DEPTH', 1000),
|
|
1160
|
+
enqueued: 0,
|
|
1161
|
+
completed: 0,
|
|
1162
|
+
failed: 0,
|
|
1163
|
+
idle: true,
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
function drainOwnerWrites(options = {}) {
|
|
1168
|
+
if (!ownerWriteQueue) {
|
|
1169
|
+
return Promise.resolve({ ok: true, timedOut: false, ...getOwnerWriteQueueStatus() });
|
|
1170
|
+
}
|
|
1171
|
+
return ownerWriteQueue.drain(options);
|
|
1172
|
+
}
|
|
1173
|
+
|
|
543
1174
|
const _VALID_CHECKPOINT_MODES = new Set(['PASSIVE', 'FULL', 'RESTART', 'TRUNCATE']);
|
|
544
1175
|
function checkpointWalOrThrow(mode) {
|
|
545
1176
|
const d = getDb();
|
|
@@ -563,40 +1194,52 @@ function runImmediateTransaction(d, fn, ...args) {
|
|
|
563
1194
|
|
|
564
1195
|
function initDb(dbPath) {
|
|
565
1196
|
dbPath = dbPath || DEFAULT_DB_PATH;
|
|
566
|
-
currentDbPath
|
|
1197
|
+
if (db && currentDbPath === dbPath) return db;
|
|
1198
|
+
if (db && currentDbPath !== dbPath) closeDb(true);
|
|
567
1199
|
const dir = path.dirname(dbPath);
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
const newDb = new Database(dbPath);
|
|
572
|
-
// Load sqlite-vec extension BEFORE any schema reads (including PRAGMAs).
|
|
573
|
-
// If the DB has vec0 virtual tables and the extension isn't loaded,
|
|
574
|
-
// SQLite returns SQLITE_CORRUPT: "malformed database schema".
|
|
575
|
-
try {
|
|
576
|
-
const sqliteVec = require('sqlite-vec');
|
|
577
|
-
sqliteVec.load(newDb);
|
|
578
|
-
} catch {
|
|
579
|
-
// sqlite-vec not installed — fine if DB has no vec0 tables
|
|
580
|
-
}
|
|
1200
|
+
let newDb = null;
|
|
1201
|
+
const boot = require('./lib/boot-profile'); // per-phase timing (surfaced at /api/wall-e/boot-profile)
|
|
581
1202
|
try {
|
|
582
|
-
|
|
583
|
-
|
|
1203
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
1204
|
+
_ensureBackupDir(BACKUP_DIR);
|
|
1205
|
+
boot.measure('initDb.storagePolicy', () => _checkStoragePolicy(dbPath));
|
|
1206
|
+
boot.measure('initDb.recoverCorruptCheck', () => _recoverCorruptDbIfNeeded(dbPath));
|
|
1207
|
+
|
|
1208
|
+
const busyTimeoutMs = _sqliteTimeoutMs('WALL_E_SQLITE_BUSY_TIMEOUT_MS', 1000);
|
|
1209
|
+
newDb = boot.measure('initDb.openDatabase', () => new Database(dbPath, { timeout: busyTimeoutMs }));
|
|
1210
|
+
installSqliteWriteLock(newDb, dbPath, {
|
|
1211
|
+
label: 'wall-e-brain',
|
|
1212
|
+
timeoutMs: _sqliteTimeoutMs('WALL_E_SQLITE_WRITE_LOCK_TIMEOUT_MS', 0),
|
|
1213
|
+
staleMs: Number(process.env.WALL_E_SQLITE_WRITE_LOCK_STALE_MS || 10 * 60 * 1000),
|
|
1214
|
+
onEvent: _onWriteLockEvent,
|
|
1215
|
+
});
|
|
1216
|
+
boot.measure('initDb.loadSqliteVec', () => _loadSqliteVec(newDb));
|
|
1217
|
+
// Boot writes tolerate transient cross-process lock contention (a few seconds)
|
|
1218
|
+
// instead of FATAL-ing the daemon before MCP is ready. journal_mode=WAL can also
|
|
1219
|
+
// trigger WAL recovery/checkpoint work on open — measured separately.
|
|
1220
|
+
const bootLockWaitMs = _sqliteTimeoutMs('WALL_E_BRAIN_BOOT_LOCK_WAIT_MS', 7000);
|
|
1221
|
+
boot.measure('initDb.pragma.walMode', () => _retryBusySync(() => newDb.pragma('journal_mode = WAL'), bootLockWaitMs));
|
|
1222
|
+
newDb.pragma(`busy_timeout = ${busyTimeoutMs}`);
|
|
584
1223
|
newDb.pragma('foreign_keys = ON');
|
|
585
1224
|
db = newDb;
|
|
586
|
-
|
|
1225
|
+
currentDbPath = dbPath;
|
|
1226
|
+
_resetOwnerWriteQueue(dbPath);
|
|
1227
|
+
boot.measure('initDb.createTables', () => _retryBusySync(() => createTables(), bootLockWaitMs));
|
|
1228
|
+
boot.measure('initDb.backupDirSetting', () => _retryBusySync(() => _applyPersistedBackupDirSetting(), bootLockWaitMs));
|
|
587
1229
|
|
|
588
1230
|
// --- Schema migrations via PRAGMA user_version ---
|
|
589
1231
|
// ensureSchema runs forward migrations and then verifies invariants;
|
|
590
1232
|
// it returns the pre-migration pragma value so telemetry can still
|
|
591
1233
|
// report what version we upgraded from.
|
|
592
|
-
const previousSchemaVersion = ensureSchema(newDb);
|
|
1234
|
+
const previousSchemaVersion = boot.measure('initDb.ensureSchema', () => _retryBusySync(() => ensureSchema(newDb), bootLockWaitMs));
|
|
593
1235
|
|
|
594
1236
|
// --- Upgrade detection (Phase 2) ---
|
|
595
|
-
_detectUpgrade(previousSchemaVersion);
|
|
1237
|
+
boot.measure('initDb.detectUpgrade', () => _detectUpgrade(previousSchemaVersion));
|
|
596
1238
|
} catch (err) {
|
|
597
|
-
newDb.close();
|
|
1239
|
+
if (newDb) newDb.close();
|
|
598
1240
|
db = null;
|
|
599
1241
|
currentDbPath = null;
|
|
1242
|
+
_closeOwnerWriteQueueNoDrain();
|
|
600
1243
|
throw err;
|
|
601
1244
|
}
|
|
602
1245
|
return db;
|
|
@@ -848,7 +1491,8 @@ function createTables() {
|
|
|
848
1491
|
session_id TEXT PRIMARY KEY,
|
|
849
1492
|
branches TEXT NOT NULL DEFAULT '{}',
|
|
850
1493
|
active TEXT NOT NULL DEFAULT '{}',
|
|
851
|
-
history TEXT NOT NULL DEFAULT '[]'
|
|
1494
|
+
history TEXT NOT NULL DEFAULT '[]',
|
|
1495
|
+
updated_at_ms INTEGER NOT NULL DEFAULT 0
|
|
852
1496
|
);
|
|
853
1497
|
|
|
854
1498
|
CREATE TABLE IF NOT EXISTS skills (
|
|
@@ -1071,6 +1715,10 @@ function createTables() {
|
|
|
1071
1715
|
speed_tier INTEGER DEFAULT 3,
|
|
1072
1716
|
enabled INTEGER DEFAULT 1,
|
|
1073
1717
|
is_fine_tuned INTEGER DEFAULT 0,
|
|
1718
|
+
source TEXT DEFAULT 'catalog',
|
|
1719
|
+
verification_status TEXT DEFAULT 'verified',
|
|
1720
|
+
last_seen_at TEXT,
|
|
1721
|
+
gateway_type TEXT,
|
|
1074
1722
|
UNIQUE(provider_id, model_id)
|
|
1075
1723
|
);
|
|
1076
1724
|
|
|
@@ -1088,6 +1736,36 @@ function createTables() {
|
|
|
1088
1736
|
created_at TEXT DEFAULT (datetime('now'))
|
|
1089
1737
|
);
|
|
1090
1738
|
|
|
1739
|
+
CREATE TABLE IF NOT EXISTS model_usage_ledger (
|
|
1740
|
+
id TEXT PRIMARY KEY,
|
|
1741
|
+
occurred_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1742
|
+
source TEXT NOT NULL DEFAULT 'wall-e.chat',
|
|
1743
|
+
feature TEXT,
|
|
1744
|
+
session_id TEXT,
|
|
1745
|
+
branch_id TEXT,
|
|
1746
|
+
message_id TEXT,
|
|
1747
|
+
request_id TEXT,
|
|
1748
|
+
provider_type TEXT,
|
|
1749
|
+
provider_id TEXT,
|
|
1750
|
+
model_id TEXT,
|
|
1751
|
+
model_registry_id TEXT,
|
|
1752
|
+
gateway_type TEXT,
|
|
1753
|
+
route_label TEXT,
|
|
1754
|
+
input_tokens INTEGER DEFAULT 0,
|
|
1755
|
+
output_tokens INTEGER DEFAULT 0,
|
|
1756
|
+
total_tokens INTEGER DEFAULT 0,
|
|
1757
|
+
cached_input_tokens INTEGER DEFAULT 0,
|
|
1758
|
+
reasoning_output_tokens INTEGER DEFAULT 0,
|
|
1759
|
+
latency_ms INTEGER,
|
|
1760
|
+
stop_reason TEXT,
|
|
1761
|
+
status TEXT DEFAULT 'success',
|
|
1762
|
+
error_type TEXT,
|
|
1763
|
+
cost_usd REAL,
|
|
1764
|
+
cost_source TEXT,
|
|
1765
|
+
metadata TEXT,
|
|
1766
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
1767
|
+
);
|
|
1768
|
+
|
|
1091
1769
|
CREATE TABLE IF NOT EXISTS agent_runner_evaluations (
|
|
1092
1770
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1093
1771
|
runner_id TEXT NOT NULL,
|
|
@@ -1117,6 +1795,13 @@ function createTables() {
|
|
|
1117
1795
|
CREATE INDEX IF NOT EXISTS idx_model_registry_provider ON model_registry(provider_id);
|
|
1118
1796
|
CREATE INDEX IF NOT EXISTS idx_model_evaluations_model ON model_evaluations(model_registry_id);
|
|
1119
1797
|
CREATE INDEX IF NOT EXISTS idx_model_evaluations_created ON model_evaluations(created_at);
|
|
1798
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_model_usage_ledger_request
|
|
1799
|
+
ON model_usage_ledger(source, request_id)
|
|
1800
|
+
WHERE request_id IS NOT NULL AND request_id != '';
|
|
1801
|
+
CREATE INDEX IF NOT EXISTS idx_model_usage_ledger_occurred ON model_usage_ledger(occurred_at);
|
|
1802
|
+
CREATE INDEX IF NOT EXISTS idx_model_usage_ledger_session ON model_usage_ledger(session_id, occurred_at);
|
|
1803
|
+
CREATE INDEX IF NOT EXISTS idx_model_usage_ledger_provider_model ON model_usage_ledger(provider_type, model_id, occurred_at);
|
|
1804
|
+
CREATE INDEX IF NOT EXISTS idx_model_usage_ledger_registry ON model_usage_ledger(model_registry_id, occurred_at);
|
|
1120
1805
|
CREATE INDEX IF NOT EXISTS idx_agent_runner_eval_runner ON agent_runner_evaluations(runner_id, task_type);
|
|
1121
1806
|
CREATE INDEX IF NOT EXISTS idx_agent_runner_eval_created ON agent_runner_evaluations(created_at);
|
|
1122
1807
|
|
|
@@ -1406,13 +2091,13 @@ function createTables() {
|
|
|
1406
2091
|
*/
|
|
1407
2092
|
function closeDb(force = false) {
|
|
1408
2093
|
if (_daemonOwned && !force) return; // Skill called closeDb() in-process — ignore
|
|
1409
|
-
|
|
1410
|
-
clearInterval(backupIntervalId);
|
|
1411
|
-
backupIntervalId = null;
|
|
1412
|
-
}
|
|
2094
|
+
_closeOwnerWriteQueueNoDrain();
|
|
1413
2095
|
writeAuditLog.close();
|
|
1414
2096
|
if (db) {
|
|
1415
|
-
|
|
2097
|
+
// Standalone bundled skills run in separate processes while the daemon and
|
|
2098
|
+
// other skills may still have this DB open. TRUNCATE can invalidate shared
|
|
2099
|
+
// WAL state for those peers; PASSIVE is the safe close-time checkpoint.
|
|
2100
|
+
try { db.pragma('wal_checkpoint(PASSIVE)'); } catch (e) { /* no-op if not in WAL mode */ }
|
|
1416
2101
|
db.close();
|
|
1417
2102
|
db = null;
|
|
1418
2103
|
currentDbPath = null;
|
|
@@ -1455,6 +2140,11 @@ function insertMemory(mem) {
|
|
|
1455
2140
|
|
|
1456
2141
|
// Strip noise from content at ingestion time (preserves raw in content_raw)
|
|
1457
2142
|
const cleanContent = _stripNoise ? _stripNoise(mem.content) : mem.content;
|
|
2143
|
+
// Only persist content_raw when it actually differs from the stored content; otherwise
|
|
2144
|
+
// NULL and readers fall back via `content_raw ?? content`. Avoids duplicating the text
|
|
2145
|
+
// for every memory whose raw == content (the common no-noise-stripped case).
|
|
2146
|
+
const _effectiveRaw = mem.content_raw || mem.content;
|
|
2147
|
+
const _rawToStore = _effectiveRaw === cleanContent ? null : _effectiveRaw;
|
|
1458
2148
|
|
|
1459
2149
|
const id = uuidv4();
|
|
1460
2150
|
getDb().prepare(`
|
|
@@ -1463,7 +2153,7 @@ function insertMemory(mem) {
|
|
|
1463
2153
|
`).run(
|
|
1464
2154
|
id, mem.source, mem.source_id || null, mem.source_channel || null,
|
|
1465
2155
|
mem.memory_type, mem.direction || null, mem.participants || null,
|
|
1466
|
-
mem.subject || null, cleanContent,
|
|
2156
|
+
mem.subject || null, cleanContent, _rawToStore,
|
|
1467
2157
|
mem.metadata || null, mem.importance ?? 0.5, mem.timestamp
|
|
1468
2158
|
);
|
|
1469
2159
|
logWrite('insert', 'memories', { id, source: mem.source, subject: mem.subject });
|
|
@@ -1472,7 +2162,10 @@ function insertMemory(mem) {
|
|
|
1472
2162
|
}
|
|
1473
2163
|
|
|
1474
2164
|
function getMemory(id) {
|
|
1475
|
-
|
|
2165
|
+
const row = getDb().prepare('SELECT * FROM memories WHERE id = ?').get(id) || null;
|
|
2166
|
+
// content_raw is NULL when identical to content (dedup); present it as the effective raw.
|
|
2167
|
+
if (row && row.content_raw == null) row.content_raw = row.content;
|
|
2168
|
+
return row;
|
|
1476
2169
|
}
|
|
1477
2170
|
|
|
1478
2171
|
function listMemories({ source, since, extractionStatus, limit } = {}) {
|
|
@@ -1505,34 +2198,129 @@ function listMemories({ source, since, extractionStatus, limit } = {}) {
|
|
|
1505
2198
|
return getDb().prepare(sql).all(...params);
|
|
1506
2199
|
}
|
|
1507
2200
|
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
const
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
}
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
2201
|
+
// COUNT variant of listMemories. Callers that only need `.length` were pulling every
|
|
2202
|
+
// matching row (with content blobs) just to count them — a full materialization on a
|
|
2203
|
+
// 350k-row brain. This counts in the DB instead.
|
|
2204
|
+
function countMemories({ source, since, extractionStatus } = {}) {
|
|
2205
|
+
const conditions = ['archived_at IS NULL'];
|
|
2206
|
+
const params = [];
|
|
2207
|
+
if (source) { conditions.push('source = ?'); params.push(source); }
|
|
2208
|
+
if (since) { conditions.push('timestamp >= ?'); params.push(since); }
|
|
2209
|
+
if (extractionStatus) { conditions.push('extraction_status = ?'); params.push(extractionStatus); }
|
|
2210
|
+
const sql = 'SELECT count(*) AS c FROM memories WHERE ' + conditions.join(' AND ');
|
|
2211
|
+
return getDb().prepare(sql).get(...params).c;
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
// Low-signal words dropped from free-text memory queries. The old search ANDed a
|
|
2215
|
+
// LIKE for EVERY whitespace token, so a natural-language request ("search history
|
|
2216
|
+
// about Eric Gu and write a note that reads like me") required one memory to
|
|
2217
|
+
// contain all ~14 words and matched nothing. Proper nouns/keywords carry the
|
|
2218
|
+
// signal; stopwords only ever weaken recall.
|
|
2219
|
+
const _MEMORY_STOPWORDS = new Set([
|
|
2220
|
+
'a','an','the','and','or','but','to','of','in','on','at','for','with','from','by','as',
|
|
2221
|
+
'is','are','was','were','be','been','being','it','this','that','these','those',
|
|
2222
|
+
'my','me','i','you','your','our','we','us','he','she','they','them','his','her','their',
|
|
2223
|
+
'can','could','would','should','will','shall','may','might','do','does','did','done','have','has','had',
|
|
2224
|
+
'please','help','write','writing','make','made','read','reads','reading','like','about',
|
|
2225
|
+
'search','find','history','note','notes','some','any','so','if','then','just','also',
|
|
2226
|
+
'get','got','want','wants','need','needs','tell','give','show','what','who','whom','when','where','why','how',
|
|
2227
|
+
'up','out','over','into','than','too','very','more','most','here','there','now','today',
|
|
2228
|
+
]);
|
|
2229
|
+
|
|
2230
|
+
// Tokenize a free-text query into the high-signal terms used for matching. Caller
|
|
2231
|
+
// may pass `extraTerms` (e.g. a person's aliases/emails/handles) to broaden recall.
|
|
2232
|
+
function _memoryQueryTerms(query, extraTerms) {
|
|
2233
|
+
const raw = String(query || '').toLowerCase().match(/[\p{L}\p{N}][\p{L}\p{N}.@_+-]*/gu) || [];
|
|
2234
|
+
if (Array.isArray(extraTerms)) {
|
|
2235
|
+
for (const t of extraTerms) {
|
|
2236
|
+
const v = String(t || '').toLowerCase().trim();
|
|
2237
|
+
if (v) raw.push(v);
|
|
2238
|
+
}
|
|
1528
2239
|
}
|
|
2240
|
+
const seen = new Set();
|
|
2241
|
+
const all = [];
|
|
2242
|
+
for (const t of raw) { if (!seen.has(t)) { seen.add(t); all.push(t); } }
|
|
2243
|
+
let signal = all.filter(t => t.length >= 2 && !_MEMORY_STOPWORDS.has(t));
|
|
2244
|
+
if (!signal.length) signal = all; // query was all stopwords — fall back to using them
|
|
2245
|
+
return signal.slice(0, 12);
|
|
2246
|
+
}
|
|
2247
|
+
|
|
2248
|
+
// Lexical memory search. Matches a memory if it contains ANY of the high-signal
|
|
2249
|
+
// terms (OR), then ranks by how many distinct terms it matched (so all-terms hits
|
|
2250
|
+
// rank above any-term hits) and recency. This keeps recall high for multi-word /
|
|
2251
|
+
// natural-language queries while preserving precision at the top of the list.
|
|
2252
|
+
function searchMemories({ query, limit, source, memory_type, since, until, terms } = {}) {
|
|
2253
|
+
const searchTerms = _memoryQueryTerms(query, terms);
|
|
2254
|
+
if (!searchTerms.length) return [];
|
|
2255
|
+
const likePatterns = searchTerms.map(t => '%' + t.replace(/[%_\\]/g, '\\$&') + '%');
|
|
2256
|
+
const scoreExpr = likePatterns.map(() => "(content LIKE ? ESCAPE '\\')").join(' + ');
|
|
2257
|
+
const orExpr = '(' + likePatterns.map(() => "content LIKE ? ESCAPE '\\'").join(' OR ') + ')';
|
|
2258
|
+
const params = [...likePatterns, ...likePatterns]; // score CASEs first, then WHERE OR
|
|
2259
|
+
const conditions = [orExpr];
|
|
2260
|
+
if (source) { conditions.push('source = ?'); params.push(source); }
|
|
2261
|
+
if (memory_type) { conditions.push('memory_type = ?'); params.push(memory_type); }
|
|
2262
|
+
if (since) { conditions.push('timestamp >= ?'); params.push(since); }
|
|
2263
|
+
if (until) { conditions.push('timestamp <= ?'); params.push(until); }
|
|
1529
2264
|
const lim = Math.min(Math.max(limit || 50, 1), 200);
|
|
1530
2265
|
params.push(lim);
|
|
1531
2266
|
return getDb().prepare(
|
|
1532
|
-
`SELECT
|
|
2267
|
+
`SELECT *, (${scoreExpr}) AS match_score FROM memories
|
|
2268
|
+
WHERE archived_at IS NULL AND ${conditions.join(' AND ')}
|
|
2269
|
+
ORDER BY match_score DESC, timestamp DESC LIMIT ?`
|
|
1533
2270
|
).all(...params);
|
|
1534
2271
|
}
|
|
1535
2272
|
|
|
2273
|
+
// Resolve a person's name to all of their searchable identities — canonical name,
|
|
2274
|
+
// aliases, emails, @handles — by consulting the people table and the entity graph.
|
|
2275
|
+
// Lets a memory search match messages that reference someone by email/handle even
|
|
2276
|
+
// when the brain has no memory containing their full display name.
|
|
2277
|
+
// Returns { found, canonical, terms: [...] }.
|
|
2278
|
+
function resolvePersonIdentities(name) {
|
|
2279
|
+
const clean = String(name || '').trim();
|
|
2280
|
+
const out = { found: false, canonical: clean, terms: [] };
|
|
2281
|
+
if (!clean) return out;
|
|
2282
|
+
const push = (v) => {
|
|
2283
|
+
const s = String(v == null ? '' : v).trim();
|
|
2284
|
+
if (s && !out.terms.some(t => t.toLowerCase() === s.toLowerCase())) out.terms.push(s);
|
|
2285
|
+
};
|
|
2286
|
+
push(clean);
|
|
2287
|
+
const lower = clean.toLowerCase();
|
|
2288
|
+
const db = getDb();
|
|
2289
|
+
let person = null;
|
|
2290
|
+
try {
|
|
2291
|
+
person = db.prepare('SELECT * FROM people WHERE LOWER(name) = ?').get(lower)
|
|
2292
|
+
|| db.prepare('SELECT * FROM people WHERE LOWER(name) LIKE ? LIMIT 1').get('%' + lower.replace(/[%_\\]/g, '\\$&') + '%');
|
|
2293
|
+
if (!person) {
|
|
2294
|
+
const withAliases = db.prepare('SELECT * FROM people WHERE aliases IS NOT NULL').all();
|
|
2295
|
+
person = withAliases.find(p => {
|
|
2296
|
+
try { return (JSON.parse(p.aliases) || []).some(a => String(a).toLowerCase().trim() === lower); }
|
|
2297
|
+
catch { return false; }
|
|
2298
|
+
}) || null;
|
|
2299
|
+
}
|
|
2300
|
+
} catch {}
|
|
2301
|
+
if (person) {
|
|
2302
|
+
out.found = true;
|
|
2303
|
+
out.canonical = person.name || clean;
|
|
2304
|
+
push(person.name);
|
|
2305
|
+
try { (JSON.parse(person.aliases || '[]') || []).forEach(push); } catch {}
|
|
2306
|
+
try {
|
|
2307
|
+
const ids = JSON.parse(person.identities || '[]');
|
|
2308
|
+
if (Array.isArray(ids)) ids.forEach(id => push(typeof id === 'string' ? id : (id && (id.value || id.email || id.handle || id.id))));
|
|
2309
|
+
else if (ids && typeof ids === 'object') Object.values(ids).forEach(push);
|
|
2310
|
+
} catch {}
|
|
2311
|
+
}
|
|
2312
|
+
try {
|
|
2313
|
+
const ent = findEntityFuzzy(clean);
|
|
2314
|
+
if (ent) {
|
|
2315
|
+
out.found = true;
|
|
2316
|
+
if (!person) out.canonical = ent.canonical_name || out.canonical;
|
|
2317
|
+
push(ent.canonical_name);
|
|
2318
|
+
try { (JSON.parse(ent.aliases || '[]') || []).forEach(push); } catch {}
|
|
2319
|
+
}
|
|
2320
|
+
} catch {}
|
|
2321
|
+
return out;
|
|
2322
|
+
}
|
|
2323
|
+
|
|
1536
2324
|
function updateMemoryExtraction(id, status) {
|
|
1537
2325
|
getDb().prepare('UPDATE memories SET extraction_status = ? WHERE id = ?').run(status, id);
|
|
1538
2326
|
logWrite('update_extraction', 'memories', { id, source: `status:${status}` });
|
|
@@ -1551,18 +2339,44 @@ function touchMemory(id) {
|
|
|
1551
2339
|
).run(id);
|
|
1552
2340
|
}
|
|
1553
2341
|
|
|
1554
|
-
|
|
2342
|
+
// Batched touch: one UPDATE (one write-lock acquisition) instead of N. Hot paths like
|
|
2343
|
+
// search_memories touch every result row; the per-row loop was an N+1 of write-locked
|
|
2344
|
+
// statements. Ids are parameterized.
|
|
2345
|
+
function touchMemories(ids) {
|
|
2346
|
+
const list = Array.isArray(ids) ? ids.filter(Boolean) : [];
|
|
2347
|
+
if (!list.length) return 0;
|
|
2348
|
+
const placeholders = list.map(() => '?').join(',');
|
|
2349
|
+
const res = getDb().prepare(
|
|
2350
|
+
`UPDATE memories SET last_accessed = datetime('now'), access_count = COALESCE(access_count, 0) + 1 WHERE id IN (${placeholders})`
|
|
2351
|
+
).run(...list);
|
|
2352
|
+
return res.changes;
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2355
|
+
function decayImportance({ windowSize = 5000 } = {}) {
|
|
1555
2356
|
const db = getDb();
|
|
1556
2357
|
const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString();
|
|
1557
|
-
|
|
2358
|
+
// Window by rowid instead of two single UPDATEs over the whole table. On a 350k-row
|
|
2359
|
+
// brain a global UPDATE holds the cross-process write lock for its entire duration,
|
|
2360
|
+
// which makes the OTHER process (CTM) busy-wait on its main thread (the
|
|
2361
|
+
// session-integrity starvation class). Disjoint rowid windows process each row exactly
|
|
2362
|
+
// once (same result) but release the write lock between statements. The decay and boost
|
|
2363
|
+
// predicates are mutually exclusive on last_accessed, so no row is touched twice.
|
|
2364
|
+
const decayStmt = db.prepare(`
|
|
1558
2365
|
UPDATE memories SET importance = MAX(0.1, importance * 0.95)
|
|
1559
|
-
WHERE (last_accessed IS NULL OR last_accessed < ?) AND importance > 0.1
|
|
1560
|
-
`)
|
|
1561
|
-
const
|
|
2366
|
+
WHERE rowid >= ? AND rowid < ? AND (last_accessed IS NULL OR last_accessed < ?) AND importance > 0.1
|
|
2367
|
+
`);
|
|
2368
|
+
const boostStmt = db.prepare(`
|
|
1562
2369
|
UPDATE memories SET importance = MIN(1.0, importance * 1.02)
|
|
1563
|
-
WHERE last_accessed IS NOT NULL AND last_accessed >= ? AND importance < 1.0
|
|
1564
|
-
`)
|
|
1565
|
-
|
|
2370
|
+
WHERE rowid >= ? AND rowid < ? AND last_accessed IS NOT NULL AND last_accessed >= ? AND importance < 1.0
|
|
2371
|
+
`);
|
|
2372
|
+
const maxRow = db.prepare('SELECT max(rowid) AS m FROM memories').get().m || 0;
|
|
2373
|
+
const step = Math.max(500, Number(windowSize) || 5000);
|
|
2374
|
+
let decayed = 0, boosted = 0;
|
|
2375
|
+
for (let lo = 0; lo <= maxRow; lo += step) {
|
|
2376
|
+
decayed += decayStmt.run(lo, lo + step, thirtyDaysAgo).changes;
|
|
2377
|
+
boosted += boostStmt.run(lo, lo + step, thirtyDaysAgo).changes;
|
|
2378
|
+
}
|
|
2379
|
+
return { decayed, boosted };
|
|
1566
2380
|
}
|
|
1567
2381
|
|
|
1568
2382
|
// -- Knowledge CRUD --
|
|
@@ -1805,54 +2619,80 @@ function deleteSchedulerJobState(job_name) {
|
|
|
1805
2619
|
|
|
1806
2620
|
// -- Backup --
|
|
1807
2621
|
|
|
1808
|
-
|
|
2622
|
+
function _backupDirForCurrentDb() {
|
|
2623
|
+
const dbDir = path.dirname(currentDbPath || DEFAULT_DB_PATH);
|
|
2624
|
+
if (BACKUP_DIR_SOURCE === 'default' && dbDir !== path.dirname(DEFAULT_DB_PATH)) {
|
|
2625
|
+
return path.join(dbDir, 'backups');
|
|
2626
|
+
}
|
|
2627
|
+
return BACKUP_DIR;
|
|
2628
|
+
}
|
|
1809
2629
|
|
|
1810
|
-
function
|
|
1811
|
-
|
|
1812
|
-
|
|
2630
|
+
function _quickCheckBackupFile(filePath) {
|
|
2631
|
+
return _quickCheckFile(filePath);
|
|
2632
|
+
}
|
|
1813
2633
|
|
|
1814
|
-
|
|
1815
|
-
//
|
|
1816
|
-
const
|
|
1817
|
-
const backupDir = dbDir === path.dirname(DEFAULT_DB_PATH) ? BACKUP_DIR : path.join(dbDir, 'backups');
|
|
2634
|
+
async function createBackup(label) {
|
|
2635
|
+
getDb(); // throws if not initialized
|
|
2636
|
+
const backupDir = _backupDirForCurrentDb();
|
|
1818
2637
|
|
|
1819
2638
|
const timestamp = new Date().toISOString().replace(/:/g, '-');
|
|
1820
|
-
const
|
|
2639
|
+
const safeLabel = String(label || 'manual').replace(/[^a-z0-9_.-]+/gi, '-').replace(/^-+|-+$/g, '') || 'manual';
|
|
2640
|
+
const backupName = `wall-e-brain-${timestamp}-${safeLabel}.db`;
|
|
1821
2641
|
const backupPath = path.join(backupDir, backupName);
|
|
2642
|
+
const tmpPath = path.join(backupDir, `.${backupName}.${process.pid}.tmp`);
|
|
1822
2643
|
|
|
1823
2644
|
if (!fs.existsSync(backupDir)) fs.mkdirSync(backupDir, { recursive: true });
|
|
1824
|
-
fs.
|
|
2645
|
+
fs.rmSync(tmpPath, { force: true });
|
|
2646
|
+
try {
|
|
2647
|
+
// Online incremental backup. better-sqlite3's db.backup() copies the database in
|
|
2648
|
+
// page batches and YIELDS to the event loop between them (default 100 pages/batch),
|
|
2649
|
+
// so a multi-GB brain never freezes the loop — unlike the old synchronous
|
|
2650
|
+
// `VACUUM INTO`, which blocked for 30-120s. Like VACUUM INTO it reads a consistent
|
|
2651
|
+
// snapshot via a read transaction and does not force a WAL checkpoint that would
|
|
2652
|
+
// perturb peer processes; SQLite transparently re-copies any pages a peer writes
|
|
2653
|
+
// mid-backup. The quick_check below validates the produced image before we keep it.
|
|
2654
|
+
await getDb().backup(tmpPath);
|
|
2655
|
+
const check = _quickCheckBackupFile(tmpPath);
|
|
2656
|
+
if (!check.ok) {
|
|
2657
|
+
throw new Error(`backup quick_check failed: ${check.error}`);
|
|
2658
|
+
}
|
|
2659
|
+
fs.renameSync(tmpPath, backupPath);
|
|
2660
|
+
} catch (err) {
|
|
2661
|
+
try { fs.rmSync(tmpPath, { force: true }); } catch {}
|
|
2662
|
+
throw err;
|
|
2663
|
+
}
|
|
1825
2664
|
|
|
1826
|
-
cleanOldBackups();
|
|
2665
|
+
cleanOldBackups(backupDir);
|
|
1827
2666
|
|
|
1828
2667
|
return { backupName, backupPath, timestamp };
|
|
1829
2668
|
}
|
|
1830
2669
|
|
|
1831
2670
|
function listBackups() {
|
|
1832
|
-
|
|
1833
|
-
|
|
2671
|
+
const backupDir = _backupDirForCurrentDb();
|
|
2672
|
+
if (!fs.existsSync(backupDir)) return [];
|
|
2673
|
+
return fs.readdirSync(backupDir)
|
|
1834
2674
|
.filter(f => f.startsWith('wall-e-brain-') && f.endsWith('.db'))
|
|
1835
2675
|
.sort().reverse()
|
|
1836
2676
|
.map(f => {
|
|
1837
|
-
const stat = fs.statSync(path.join(
|
|
1838
|
-
return { name: f, path: path.join(
|
|
2677
|
+
const stat = fs.statSync(path.join(backupDir, f));
|
|
2678
|
+
return { name: f, path: path.join(backupDir, f), size: stat.size, createdAt: stat.mtime.toISOString() };
|
|
1839
2679
|
});
|
|
1840
2680
|
}
|
|
1841
2681
|
|
|
1842
2682
|
function deleteBackup(backupName) {
|
|
1843
|
-
const p = path.join(
|
|
2683
|
+
const p = path.join(_backupDirForCurrentDb(), path.basename(backupName));
|
|
1844
2684
|
if (fs.existsSync(p)) fs.unlinkSync(p);
|
|
1845
2685
|
}
|
|
1846
2686
|
|
|
1847
|
-
function cleanOldBackups() {
|
|
1848
|
-
if (!fs.existsSync(
|
|
1849
|
-
const files = fs.readdirSync(
|
|
2687
|
+
function cleanOldBackups(backupDir = BACKUP_DIR) {
|
|
2688
|
+
if (!fs.existsSync(backupDir)) return;
|
|
2689
|
+
const files = fs.readdirSync(backupDir);
|
|
1850
2690
|
const cutoff = Date.now() - 7 * 24 * 60 * 60 * 1000;
|
|
1851
2691
|
|
|
1852
2692
|
// Always keep at least 3
|
|
1853
2693
|
const brainFiles = files.filter(f => f.startsWith('wall-e-brain-') && f.endsWith('.db')).sort().reverse();
|
|
1854
2694
|
for (const file of brainFiles.slice(3)) {
|
|
1855
|
-
const filePath = path.join(
|
|
2695
|
+
const filePath = path.join(backupDir, file);
|
|
1856
2696
|
const stat = fs.statSync(filePath);
|
|
1857
2697
|
if (stat.mtimeMs < cutoff) {
|
|
1858
2698
|
fs.unlinkSync(filePath);
|
|
@@ -1860,37 +2700,24 @@ function cleanOldBackups() {
|
|
|
1860
2700
|
}
|
|
1861
2701
|
}
|
|
1862
2702
|
|
|
1863
|
-
|
|
1864
|
-
|
|
2703
|
+
// Ensure a daily backup exists for today. Idempotent — skips if today's daily backup
|
|
2704
|
+
// is already present. This is registered as a SCHEDULER job (see agent.js), NOT coupled
|
|
2705
|
+
// to process boot or a hand-rolled hourly poll: the scheduler owns "run once a day +
|
|
2706
|
+
// replay if a day was missed during downtime" (persistJobState), and runs it well after
|
|
2707
|
+
// startup so the backup never blocks port bind / MCP readiness. Returns
|
|
2708
|
+
// { created: boolean, backupName?, backupPath?, timestamp? }.
|
|
2709
|
+
async function ensureDailyBackup() {
|
|
1865
2710
|
const today = new Date().toISOString().slice(0, 10);
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
const files = fs.readdirSync(BACKUP_DIR);
|
|
2711
|
+
const backupDir = _backupDirForCurrentDb();
|
|
2712
|
+
if (fs.existsSync(backupDir)) {
|
|
2713
|
+
const files = fs.readdirSync(backupDir);
|
|
1870
2714
|
const todayPrefix = `wall-e-brain-${today}`;
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
if (needsBackup) {
|
|
1875
|
-
try { createBackup('daily'); } catch (_) {}
|
|
1876
|
-
}
|
|
1877
|
-
|
|
1878
|
-
// Check hourly
|
|
1879
|
-
backupIntervalId = setInterval(() => {
|
|
1880
|
-
const todayNow = new Date().toISOString().slice(0, 10);
|
|
1881
|
-
let exists = false;
|
|
1882
|
-
if (fs.existsSync(BACKUP_DIR)) {
|
|
1883
|
-
const files = fs.readdirSync(BACKUP_DIR);
|
|
1884
|
-
const prefix = `wall-e-brain-${todayNow}`;
|
|
1885
|
-
exists = files.some(f => f.startsWith(prefix) && f.endsWith('-daily.db'));
|
|
1886
|
-
}
|
|
1887
|
-
if (!exists) {
|
|
1888
|
-
try { createBackup('daily'); } catch (_) {}
|
|
2715
|
+
if (files.some(f => f.startsWith(todayPrefix) && f.endsWith('-daily.db'))) {
|
|
2716
|
+
return { created: false };
|
|
1889
2717
|
}
|
|
1890
|
-
}
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
if (backupIntervalId.unref) backupIntervalId.unref();
|
|
2718
|
+
}
|
|
2719
|
+
const result = await createBackup('daily');
|
|
2720
|
+
return { created: true, ...result };
|
|
1894
2721
|
}
|
|
1895
2722
|
|
|
1896
2723
|
// -- People CRUD --
|
|
@@ -1973,29 +2800,37 @@ function findEntityFuzzy(name, maxDistance = 2) {
|
|
|
1973
2800
|
const lower = name.toLowerCase().trim();
|
|
1974
2801
|
const exact = findEntity(name);
|
|
1975
2802
|
if (exact) return exact;
|
|
1976
|
-
// Check aliases (exact match)
|
|
1977
|
-
|
|
1978
|
-
|
|
2803
|
+
// Check aliases (exact match). Only the alias column is needed for the scan; pull the
|
|
2804
|
+
// full row lazily once we've found the match.
|
|
2805
|
+
const aliasRows = getDb().prepare('SELECT id, aliases FROM entities WHERE aliases IS NOT NULL').all();
|
|
2806
|
+
for (const e of aliasRows) {
|
|
1979
2807
|
try {
|
|
1980
2808
|
const aliases = JSON.parse(e.aliases);
|
|
1981
|
-
if (Array.isArray(aliases) && aliases.some(a => a.toLowerCase().trim() === lower))
|
|
2809
|
+
if (Array.isArray(aliases) && aliases.some(a => a.toLowerCase().trim() === lower)) {
|
|
2810
|
+
return getDb().prepare('SELECT * FROM entities WHERE id = ?').get(e.id);
|
|
2811
|
+
}
|
|
1982
2812
|
} catch {}
|
|
1983
2813
|
}
|
|
1984
2814
|
// LIKE prefix match
|
|
1985
2815
|
const likeMatch = getDb().prepare('SELECT * FROM entities WHERE LOWER(canonical_name) LIKE ? LIMIT 1').get(`%${lower}%`);
|
|
1986
2816
|
if (likeMatch) return likeMatch;
|
|
1987
|
-
// Levenshtein fuzzy match (catches typos like "Jhon" → "John")
|
|
2817
|
+
// Levenshtein fuzzy match (catches typos like "Jhon" → "John"). Select only id +
|
|
2818
|
+
// canonical_name (not SELECT *), and skip any name whose length differs by more than
|
|
2819
|
+
// maxDistance — it can't possibly be within edit distance, so we avoid the O(n*m) DP
|
|
2820
|
+
// for the vast majority of rows.
|
|
1988
2821
|
if (lower.length >= 3) {
|
|
1989
|
-
const
|
|
1990
|
-
let
|
|
1991
|
-
for (const e of
|
|
1992
|
-
const
|
|
2822
|
+
const candidates = getDb().prepare('SELECT id, canonical_name FROM entities').all();
|
|
2823
|
+
let bestId = null, bestDist = maxDistance + 1;
|
|
2824
|
+
for (const e of candidates) {
|
|
2825
|
+
const cn = (e.canonical_name || '').toLowerCase().trim();
|
|
2826
|
+
if (Math.abs(cn.length - lower.length) > maxDistance) continue;
|
|
2827
|
+
const dist = _editDistance(lower, cn);
|
|
1993
2828
|
if (dist > 0 && dist <= maxDistance && dist < bestDist) {
|
|
1994
|
-
|
|
2829
|
+
bestId = e.id;
|
|
1995
2830
|
bestDist = dist;
|
|
1996
2831
|
}
|
|
1997
2832
|
}
|
|
1998
|
-
if (
|
|
2833
|
+
if (bestId) return getDb().prepare('SELECT * FROM entities WHERE id = ?').get(bestId);
|
|
1999
2834
|
}
|
|
2000
2835
|
return null;
|
|
2001
2836
|
}
|
|
@@ -2146,6 +2981,17 @@ function listQuestions({ status, question_type, limit } = {}) {
|
|
|
2146
2981
|
return getDb().prepare(sql).all(...params);
|
|
2147
2982
|
}
|
|
2148
2983
|
|
|
2984
|
+
// COUNT variant of listQuestions — avoids materializing every pending row just to count.
|
|
2985
|
+
function countQuestions({ status, question_type } = {}) {
|
|
2986
|
+
const conditions = [];
|
|
2987
|
+
const params = [];
|
|
2988
|
+
if (status) { conditions.push('status = ?'); params.push(status); }
|
|
2989
|
+
if (question_type) { conditions.push('question_type = ?'); params.push(question_type); }
|
|
2990
|
+
let sql = 'SELECT count(*) AS c FROM pending_questions';
|
|
2991
|
+
if (conditions.length) sql += ' WHERE ' + conditions.join(' AND ');
|
|
2992
|
+
return getDb().prepare(sql).get(...params).c;
|
|
2993
|
+
}
|
|
2994
|
+
|
|
2149
2995
|
function answerQuestion(id, { answer, resolution_type, resolution_evidence }) {
|
|
2150
2996
|
if (!answer) throw new Error('Answer is required');
|
|
2151
2997
|
if (!resolution_type) throw new Error('Resolution type is required');
|
|
@@ -2167,6 +3013,30 @@ function answerQuestion(id, { answer, resolution_type, resolution_evidence }) {
|
|
|
2167
3013
|
if (result.changes === 0) throw new Error(`Question not found: ${id}`);
|
|
2168
3014
|
}
|
|
2169
3015
|
|
|
3016
|
+
// Questions worth surfacing in the daily digest: still pending and not yet delivered,
|
|
3017
|
+
// highest priority first (high → normal → low), then oldest-first so nothing waits forever.
|
|
3018
|
+
function listUndeliveredDigestQuestions({ max = 10 } = {}) {
|
|
3019
|
+
const cap = Math.max(1, Math.min(50, Number(max) || 10));
|
|
3020
|
+
return getDb().prepare(`
|
|
3021
|
+
SELECT * FROM pending_questions
|
|
3022
|
+
WHERE status = 'pending' AND (digest_delivered_at IS NULL OR digest_delivered_at = '')
|
|
3023
|
+
ORDER BY CASE priority WHEN 'high' THEN 0 WHEN 'normal' THEN 1 ELSE 2 END, created_at ASC
|
|
3024
|
+
LIMIT ?
|
|
3025
|
+
`).all(cap);
|
|
3026
|
+
}
|
|
3027
|
+
|
|
3028
|
+
// Mark questions as delivered in a digest (so they aren't re-asked daily). Call only after
|
|
3029
|
+
// the digest was successfully sent.
|
|
3030
|
+
function markQuestionsDelivered(ids) {
|
|
3031
|
+
if (!Array.isArray(ids) || ids.length === 0) return 0;
|
|
3032
|
+
const now = new Date().toISOString();
|
|
3033
|
+
const stmt = getDb().prepare('UPDATE pending_questions SET digest_delivered_at = ? WHERE id = ?');
|
|
3034
|
+
let n = 0;
|
|
3035
|
+
const txn = getDb().transaction(() => { for (const id of ids) n += stmt.run(now, id).changes; });
|
|
3036
|
+
txn();
|
|
3037
|
+
return n;
|
|
3038
|
+
}
|
|
3039
|
+
|
|
2170
3040
|
// -- Knowledge updates --
|
|
2171
3041
|
|
|
2172
3042
|
function supersedeKnowledge(oldId, newId) {
|
|
@@ -2398,15 +3268,21 @@ function listExchanges({ limit } = {}) {
|
|
|
2398
3268
|
|
|
2399
3269
|
// ── Chat Messages ──
|
|
2400
3270
|
|
|
2401
|
-
function insertChatMessage({ role, content, channel, session_id, id, attachments }) {
|
|
3271
|
+
function insertChatMessage({ role, content, channel, session_id, id, attachments, model_id, model_provider }) {
|
|
2402
3272
|
const messageId = id || uuidv4();
|
|
2403
3273
|
const attachmentsJson = attachments
|
|
2404
3274
|
? (typeof attachments === 'string' ? attachments : JSON.stringify(attachments))
|
|
2405
3275
|
: null;
|
|
3276
|
+
// model_id / model_provider record which model produced an assistant turn so the
|
|
3277
|
+
// conversation log / token badge can attribute it. Columns exist (migrations 84-85)
|
|
3278
|
+
// but were never written; null-coalesced so user/system rows stay NULL.
|
|
2406
3279
|
getDb().prepare(`
|
|
2407
|
-
INSERT INTO chat_messages (id, role, content, channel, session_id, attachments, created_at)
|
|
2408
|
-
VALUES (?, ?, ?, ?, ?, ?, datetime('now'))
|
|
2409
|
-
`).run(
|
|
3280
|
+
INSERT INTO chat_messages (id, role, content, channel, session_id, attachments, model_id, model_provider, created_at)
|
|
3281
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))
|
|
3282
|
+
`).run(
|
|
3283
|
+
messageId, role, content, channel || 'ctm', session_id || null, attachmentsJson,
|
|
3284
|
+
model_id || null, model_provider || null,
|
|
3285
|
+
);
|
|
2410
3286
|
return { id: messageId };
|
|
2411
3287
|
}
|
|
2412
3288
|
|
|
@@ -2458,24 +3334,29 @@ function clearChatSession(session_id) {
|
|
|
2458
3334
|
|
|
2459
3335
|
// ── Chat Branches (ChatGPT-style edit/resend) ──
|
|
2460
3336
|
|
|
2461
|
-
function saveChatBranches(session_id, branches, active, history) {
|
|
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();
|
|
2462
3341
|
getDb().prepare(`
|
|
2463
|
-
INSERT INTO chat_branches (session_id, branches, active, history)
|
|
2464
|
-
VALUES (?, ?, ?, ?)
|
|
2465
|
-
ON CONFLICT(session_id) DO UPDATE SET branches = excluded.branches, active = excluded.active, history = excluded.history
|
|
2466
|
-
`).run(session_id, JSON.stringify(branches), JSON.stringify(active), JSON.stringify(history || []));
|
|
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);
|
|
2467
3346
|
}
|
|
2468
3347
|
|
|
2469
3348
|
function loadChatBranches(session_id) {
|
|
2470
|
-
const row = getDb().prepare('SELECT branches, active, history FROM chat_branches WHERE session_id = ?').get(session_id);
|
|
2471
|
-
if (!row) return { branches: {}, active: {}, history: [] };
|
|
3349
|
+
const row = getDb().prepare('SELECT branches, active, history, updated_at_ms FROM chat_branches WHERE session_id = ?').get(session_id);
|
|
3350
|
+
if (!row) return { branches: {}, active: {}, history: [], exists: false };
|
|
2472
3351
|
try {
|
|
2473
3352
|
return {
|
|
2474
3353
|
branches: JSON.parse(row.branches),
|
|
2475
3354
|
active: JSON.parse(row.active),
|
|
2476
3355
|
history: JSON.parse(row.history || '[]'),
|
|
3356
|
+
updated_at_ms: Number(row.updated_at_ms || 0) || 0,
|
|
3357
|
+
exists: true,
|
|
2477
3358
|
};
|
|
2478
|
-
} catch (e) { return { branches: {}, active: {}, history: [] }; }
|
|
3359
|
+
} catch (e) { return { branches: {}, active: {}, history: [], exists: true }; }
|
|
2479
3360
|
}
|
|
2480
3361
|
|
|
2481
3362
|
// -- Skills CRUD --
|
|
@@ -3055,6 +3936,86 @@ function pruneChannelMessageEvents(ttlMs = 7 * 24 * 60 * 60 * 1000) {
|
|
|
3055
3936
|
return getDb().prepare('DELETE FROM channel_message_events WHERE updated_at < ?').run(cutoff).changes;
|
|
3056
3937
|
}
|
|
3057
3938
|
|
|
3939
|
+
// -- Brain retention (write-volume audit, 2026-05-31) --
|
|
3940
|
+
//
|
|
3941
|
+
// Age-based pruning for tables that grew unbounded (see
|
|
3942
|
+
// docs/superpowers/specs/2026-05-31-question-digest-and-brain-retention-design.md).
|
|
3943
|
+
// DELETEs are BATCHED via a bounded `rowid IN (SELECT ... LIMIT ?)` sub-select so each
|
|
3944
|
+
// statement holds the cross-process write lock only briefly (a single unbounded DELETE
|
|
3945
|
+
// over a huge table can hold it for seconds — see the CTM approval_observations fix). The
|
|
3946
|
+
// table/column/where fragments are internal constants, never user input.
|
|
3947
|
+
const _RETENTION_BATCH = Math.max(50, Math.min(5000, Number(process.env.WALL_E_RETENTION_BATCH) || 500));
|
|
3948
|
+
function _pruneRowsByAge(table, cutoffIso, { batchSize = _RETENTION_BATCH, column = 'created_at', where = '' } = {}) {
|
|
3949
|
+
if (!db) return 0;
|
|
3950
|
+
const batch = Math.max(50, Math.min(5000, Number(batchSize) || _RETENTION_BATCH));
|
|
3951
|
+
const extra = where ? ` AND ${where}` : '';
|
|
3952
|
+
const stmt = getDb().prepare(
|
|
3953
|
+
`DELETE FROM ${table} WHERE rowid IN (
|
|
3954
|
+
SELECT rowid FROM ${table} WHERE ${column} < ?${extra} ORDER BY rowid LIMIT ?
|
|
3955
|
+
)`
|
|
3956
|
+
);
|
|
3957
|
+
let deleted = 0;
|
|
3958
|
+
for (let i = 0; i < 5000; i += 1) { // backstop against a runaway loop
|
|
3959
|
+
const res = stmt.run(cutoffIso, batch);
|
|
3960
|
+
deleted += res.changes || 0;
|
|
3961
|
+
if (!res.changes || res.changes < batch) break;
|
|
3962
|
+
}
|
|
3963
|
+
return deleted;
|
|
3964
|
+
}
|
|
3965
|
+
|
|
3966
|
+
function _ageCutoffIso(days, fallbackDays) {
|
|
3967
|
+
const d = Math.max(1, Number(days) || fallbackDays);
|
|
3968
|
+
return new Date(Date.now() - d * 24 * 60 * 60 * 1000).toISOString();
|
|
3969
|
+
}
|
|
3970
|
+
|
|
3971
|
+
// Stale unanswered questions. With the generator fixed (contradictions auto-supersede),
|
|
3972
|
+
// only genuine questions persist; a still-`pending` question older than the window has
|
|
3973
|
+
// gone stale (the daily digest gave it ~30 chances). Resolved rows (answered/dismissed/
|
|
3974
|
+
// inferred) are kept for audit — they're few. Deleting reclaims space from the historical
|
|
3975
|
+
// 125k-row contradiction backlog.
|
|
3976
|
+
function pruneStalePendingQuestions({ retentionDays = 30, batchSize } = {}) {
|
|
3977
|
+
return { deleted: _pruneRowsByAge('pending_questions', _ageCutoffIso(retentionDays, 30), { batchSize, where: "status = 'pending'" }) };
|
|
3978
|
+
}
|
|
3979
|
+
|
|
3980
|
+
function pruneActivityLog({ retentionDays = 30, batchSize } = {}) {
|
|
3981
|
+
return { deleted: _pruneRowsByAge('activity_log', _ageCutoffIso(retentionDays, 30), { batchSize }) };
|
|
3982
|
+
}
|
|
3983
|
+
|
|
3984
|
+
function pruneSkillExecutions({ retentionDays = 30, batchSize } = {}) {
|
|
3985
|
+
return { deleted: _pruneRowsByAge('skill_executions', _ageCutoffIso(retentionDays, 30), { batchSize }) };
|
|
3986
|
+
}
|
|
3987
|
+
|
|
3988
|
+
function pruneInitiativeLog({ retentionDays = 90, batchSize } = {}) {
|
|
3989
|
+
return { deleted: _pruneRowsByAge('initiative_log', _ageCutoffIso(retentionDays, 90), { batchSize }) };
|
|
3990
|
+
}
|
|
3991
|
+
|
|
3992
|
+
// Hard-delete a memory and everything keyed to it: its memory_index row(s) and its
|
|
3993
|
+
// embeddings (embedding_map + vec entries). Skills that re-sync external data
|
|
3994
|
+
// (gws-workspace, claude-code-reader, google-calendar) used to `DELETE FROM memories
|
|
3995
|
+
// WHERE id = ?` directly, which orphaned the embedding (the audited 47,649) and the
|
|
3996
|
+
// memory_index row. Route those deletes here so nothing leaks. memory_index has no FK to
|
|
3997
|
+
// memories so it must be cleaned explicitly.
|
|
3998
|
+
function deleteMemory(id) {
|
|
3999
|
+
if (!id) return 0;
|
|
4000
|
+
const d = getDb();
|
|
4001
|
+
const changes = d.prepare('DELETE FROM memories WHERE id = ?').run(id).changes;
|
|
4002
|
+
try { d.prepare('DELETE FROM memory_index WHERE memory_id = ?').run(id); } catch {}
|
|
4003
|
+
try { require('./embeddings').deleteEmbeddingsForEntity(id); } catch {}
|
|
4004
|
+
return changes;
|
|
4005
|
+
}
|
|
4006
|
+
|
|
4007
|
+
// One pass over all age-based retention. Returns per-table deleted counts. Env-tunable TTLs.
|
|
4008
|
+
function runBrainRetention(opts = {}) {
|
|
4009
|
+
const ttl = (name, dflt) => _sqlitePositiveInt(name, dflt);
|
|
4010
|
+
const out = {};
|
|
4011
|
+
try { out.pending_questions = pruneStalePendingQuestions({ retentionDays: ttl('WALL_E_RETAIN_QUESTIONS_DAYS', 30), ...opts }).deleted; } catch (e) { out.pending_questions = `err:${e.message}`; }
|
|
4012
|
+
try { out.activity_log = pruneActivityLog({ retentionDays: ttl('WALL_E_RETAIN_ACTIVITY_DAYS', 30), ...opts }).deleted; } catch (e) { out.activity_log = `err:${e.message}`; }
|
|
4013
|
+
try { out.skill_executions = pruneSkillExecutions({ retentionDays: ttl('WALL_E_RETAIN_SKILL_EXEC_DAYS', 30), ...opts }).deleted; } catch (e) { out.skill_executions = `err:${e.message}`; }
|
|
4014
|
+
try { out.initiative_log = pruneInitiativeLog({ retentionDays: ttl('WALL_E_RETAIN_INITIATIVE_DAYS', 90), ...opts }).deleted; } catch (e) { out.initiative_log = `err:${e.message}`; }
|
|
4015
|
+
try { out.orphan_embeddings = require('./embeddings').pruneOrphanEmbeddings({ batchSize: opts.batchSize }).deleted; } catch (e) { out.orphan_embeddings = `err:${e.message}`; }
|
|
4016
|
+
return out;
|
|
4017
|
+
}
|
|
4018
|
+
|
|
3058
4019
|
// -- Slack Inbound Event Ledger --
|
|
3059
4020
|
|
|
3060
4021
|
function slackInboundEventId(channelId, messageTs) {
|
|
@@ -3104,6 +4065,90 @@ function pruneSlackInboundEvents(ttlMs = 48 * 60 * 60 * 1000) {
|
|
|
3104
4065
|
|
|
3105
4066
|
// -- Model Provider CRUD --
|
|
3106
4067
|
|
|
4068
|
+
const VALID_PROVIDER_ROUTE_POLICIES = new Set(['auto', 'direct', 'portkey']);
|
|
4069
|
+
|
|
4070
|
+
function _modelProviderRoutePolicyKey(type) {
|
|
4071
|
+
return `model_provider_route_policy:${String(type || '').trim().toLowerCase()}`;
|
|
4072
|
+
}
|
|
4073
|
+
|
|
4074
|
+
function _modelProviderConnectionKind(row = {}) {
|
|
4075
|
+
if (row.auth_method && row.auth_method !== 'api_key') return row.auth_method;
|
|
4076
|
+
return isPortkeyProviderConfig({
|
|
4077
|
+
baseUrl: row.base_url || row.baseUrl,
|
|
4078
|
+
customHeaders: row.custom_headers || row.customHeaders,
|
|
4079
|
+
}) ? 'portkey' : 'direct';
|
|
4080
|
+
}
|
|
4081
|
+
|
|
4082
|
+
function _modelProviderHasCredential(row = {}) {
|
|
4083
|
+
const type = row.type || row.provider_type;
|
|
4084
|
+
if (!row) return false;
|
|
4085
|
+
if (type === 'ollama' || type === 'mlx') return true;
|
|
4086
|
+
if (row.auth_method && row.auth_method !== 'api_key') return true;
|
|
4087
|
+
if (row.api_key_encrypted) return true;
|
|
4088
|
+
if (type === 'anthropic' && process.env.ANTHROPIC_API_KEY) return true;
|
|
4089
|
+
if (type === 'openai' && process.env.OPENAI_API_KEY) return true;
|
|
4090
|
+
if (type === 'google' && (process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY)) return true;
|
|
4091
|
+
if (type === 'deepseek' && process.env.DEEPSEEK_API_KEY) return true;
|
|
4092
|
+
if (type === 'moonshot' && process.env.MOONSHOT_API_KEY) return true;
|
|
4093
|
+
return false;
|
|
4094
|
+
}
|
|
4095
|
+
|
|
4096
|
+
function _modelProviderRouteRank(row = {}, policy = 'auto') {
|
|
4097
|
+
const kind = _modelProviderConnectionKind(row);
|
|
4098
|
+
const isPortkey = kind === 'portkey';
|
|
4099
|
+
const hasCredential = _modelProviderHasCredential(row);
|
|
4100
|
+
let policyRank = 50;
|
|
4101
|
+
if (policy === 'portkey') policyRank = isPortkey ? 0 : 10;
|
|
4102
|
+
else if (policy === 'direct') policyRank = isPortkey ? 10 : 0;
|
|
4103
|
+
else policyRank = isPortkey ? 10 : 0;
|
|
4104
|
+
const credentialRank = hasCredential ? 0 : 20;
|
|
4105
|
+
const defaultRank = /-(default|auto)$/.test(String(row.id || '')) ? 0 : 2;
|
|
4106
|
+
return policyRank + credentialRank + defaultRank;
|
|
4107
|
+
}
|
|
4108
|
+
|
|
4109
|
+
function sortModelProvidersByRoutePolicy(rows = [], policy = 'auto') {
|
|
4110
|
+
const normalizedPolicy = VALID_PROVIDER_ROUTE_POLICIES.has(policy) ? policy : 'auto';
|
|
4111
|
+
return [...(rows || [])].sort((a, b) => {
|
|
4112
|
+
const ar = _modelProviderRouteRank(a, normalizedPolicy);
|
|
4113
|
+
const br = _modelProviderRouteRank(b, normalizedPolicy);
|
|
4114
|
+
if (ar !== br) return ar - br;
|
|
4115
|
+
const au = Date.parse(a.updated_at || '') || 0;
|
|
4116
|
+
const bu = Date.parse(b.updated_at || '') || 0;
|
|
4117
|
+
if (bu !== au) return bu - au;
|
|
4118
|
+
return String(a.name || a.id || '').localeCompare(String(b.name || b.id || ''));
|
|
4119
|
+
});
|
|
4120
|
+
}
|
|
4121
|
+
|
|
4122
|
+
function setProviderRoutePolicy({ type, policy }) {
|
|
4123
|
+
const providerType = String(type || '').trim().toLowerCase();
|
|
4124
|
+
const routePolicy = String(policy || 'auto').trim().toLowerCase();
|
|
4125
|
+
if (!providerType) throw new Error('Provider type required');
|
|
4126
|
+
if (!VALID_PROVIDER_ROUTE_POLICIES.has(routePolicy)) {
|
|
4127
|
+
throw new Error(`Invalid provider route policy: ${routePolicy}`);
|
|
4128
|
+
}
|
|
4129
|
+
setKv(_modelProviderRoutePolicyKey(providerType), routePolicy);
|
|
4130
|
+
return { type: providerType, policy: routePolicy };
|
|
4131
|
+
}
|
|
4132
|
+
|
|
4133
|
+
function getProviderRoutePolicy(type) {
|
|
4134
|
+
const providerType = String(type || '').trim().toLowerCase();
|
|
4135
|
+
if (!providerType) return 'auto';
|
|
4136
|
+
const policy = getKv(_modelProviderRoutePolicyKey(providerType));
|
|
4137
|
+
return VALID_PROVIDER_ROUTE_POLICIES.has(policy) ? policy : 'auto';
|
|
4138
|
+
}
|
|
4139
|
+
|
|
4140
|
+
function getPreferredModelProviderForType(type) {
|
|
4141
|
+
const providerType = String(type || '').trim().toLowerCase();
|
|
4142
|
+
if (!providerType) return null;
|
|
4143
|
+
const rows = getDb().prepare(`
|
|
4144
|
+
SELECT *
|
|
4145
|
+
FROM model_providers
|
|
4146
|
+
WHERE type = ? AND enabled = 1
|
|
4147
|
+
`).all(providerType);
|
|
4148
|
+
if (!rows.length) return null;
|
|
4149
|
+
return sortModelProvidersByRoutePolicy(rows, getProviderRoutePolicy(providerType))[0] || null;
|
|
4150
|
+
}
|
|
4151
|
+
|
|
3107
4152
|
function upsertModelProvider({ id, name, type, baseUrl, apiKeyEncrypted, customHeaders, enabled }) {
|
|
3108
4153
|
if (!id || !name || !type) throw new Error('Provider requires id, name, type');
|
|
3109
4154
|
getDb().prepare(`
|
|
@@ -3187,6 +4232,8 @@ function saveSetupProvider({
|
|
|
3187
4232
|
setDefault = false,
|
|
3188
4233
|
authMethod,
|
|
3189
4234
|
preserveExistingKey = true,
|
|
4235
|
+
preserveExistingBaseUrl = true,
|
|
4236
|
+
preserveExistingCustomHeaders = true,
|
|
3190
4237
|
registerModel = true,
|
|
3191
4238
|
}) {
|
|
3192
4239
|
if (!type) throw new Error('Provider type required');
|
|
@@ -3199,9 +4246,9 @@ function saveSetupProvider({
|
|
|
3199
4246
|
return runImmediateTransaction(db, () => {
|
|
3200
4247
|
const currentDefaultProvider = getKv('walle_provider') || process.env.WALLE_PROVIDER || 'anthropic';
|
|
3201
4248
|
const syncDefaultModel = !!(model && currentDefaultProvider === type && !setDefault);
|
|
4249
|
+
const existingProvider = getModelProviderWithKey(providerId);
|
|
3202
4250
|
let storedKey = apiKeyEncrypted || null;
|
|
3203
4251
|
if (!storedKey && preserveExistingKey) {
|
|
3204
|
-
const existingProvider = getModelProviderWithKey(providerId);
|
|
3205
4252
|
storedKey = existingProvider?.api_key_encrypted || null;
|
|
3206
4253
|
if (!storedKey) {
|
|
3207
4254
|
const row = db.prepare(
|
|
@@ -3210,14 +4257,34 @@ function saveSetupProvider({
|
|
|
3210
4257
|
storedKey = row?.api_key_encrypted || null;
|
|
3211
4258
|
}
|
|
3212
4259
|
}
|
|
4260
|
+
let storedBaseUrl = baseUrl;
|
|
4261
|
+
if (storedBaseUrl === undefined && preserveExistingBaseUrl) {
|
|
4262
|
+
storedBaseUrl = existingProvider?.base_url;
|
|
4263
|
+
if (!storedBaseUrl) {
|
|
4264
|
+
const row = db.prepare(
|
|
4265
|
+
'SELECT base_url FROM model_providers WHERE type = ? AND enabled = 1 AND base_url IS NOT NULL ORDER BY updated_at DESC LIMIT 1'
|
|
4266
|
+
).get(type);
|
|
4267
|
+
storedBaseUrl = row?.base_url;
|
|
4268
|
+
}
|
|
4269
|
+
}
|
|
4270
|
+
let storedCustomHeaders = customHeaders;
|
|
4271
|
+
if (storedCustomHeaders === undefined && preserveExistingCustomHeaders) {
|
|
4272
|
+
storedCustomHeaders = existingProvider?.custom_headers;
|
|
4273
|
+
if (!storedCustomHeaders) {
|
|
4274
|
+
const row = db.prepare(
|
|
4275
|
+
'SELECT custom_headers FROM model_providers WHERE type = ? AND enabled = 1 AND custom_headers IS NOT NULL ORDER BY updated_at DESC LIMIT 1'
|
|
4276
|
+
).get(type);
|
|
4277
|
+
storedCustomHeaders = row?.custom_headers;
|
|
4278
|
+
}
|
|
4279
|
+
}
|
|
3213
4280
|
|
|
3214
4281
|
upsertModelProvider({
|
|
3215
4282
|
id: providerId,
|
|
3216
4283
|
name: displayName,
|
|
3217
4284
|
type,
|
|
3218
|
-
baseUrl:
|
|
4285
|
+
baseUrl: storedBaseUrl || null,
|
|
3219
4286
|
apiKeyEncrypted: storedKey,
|
|
3220
|
-
customHeaders:
|
|
4287
|
+
customHeaders: storedCustomHeaders || null,
|
|
3221
4288
|
enabled,
|
|
3222
4289
|
});
|
|
3223
4290
|
if (authMethod) setProviderAuthMethod(type, authMethod);
|
|
@@ -3294,14 +4361,31 @@ function disableModelProviderByType(type) {
|
|
|
3294
4361
|
|
|
3295
4362
|
// -- Model Registry CRUD --
|
|
3296
4363
|
|
|
3297
|
-
function upsertModelRegistryEntry({
|
|
4364
|
+
function upsertModelRegistryEntry({
|
|
4365
|
+
id,
|
|
4366
|
+
providerId,
|
|
4367
|
+
modelId,
|
|
4368
|
+
displayName,
|
|
4369
|
+
capabilities,
|
|
4370
|
+
costPer1mInput,
|
|
4371
|
+
costPer1mOutput,
|
|
4372
|
+
maxContextTokens,
|
|
4373
|
+
maxOutputTokens,
|
|
4374
|
+
speedTier,
|
|
4375
|
+
enabled,
|
|
4376
|
+
isFineTuned,
|
|
4377
|
+
source,
|
|
4378
|
+
verificationStatus,
|
|
4379
|
+
lastSeenAt,
|
|
4380
|
+
gatewayType,
|
|
4381
|
+
}) {
|
|
3298
4382
|
if (!id || !providerId || !modelId || !displayName) throw new Error('Registry entry requires id, providerId, modelId, displayName');
|
|
3299
4383
|
const caps = (Array.isArray(capabilities) || (capabilities && typeof capabilities === 'object'))
|
|
3300
4384
|
? JSON.stringify(capabilities)
|
|
3301
4385
|
: (capabilities || '[]');
|
|
3302
4386
|
getDb().prepare(`
|
|
3303
|
-
INSERT INTO model_registry (id, provider_id, model_id, display_name, capabilities, cost_per_1m_input, cost_per_1m_output, max_context_tokens, max_output_tokens, speed_tier, enabled, is_fine_tuned)
|
|
3304
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
4387
|
+
INSERT INTO model_registry (id, provider_id, model_id, display_name, capabilities, cost_per_1m_input, cost_per_1m_output, max_context_tokens, max_output_tokens, speed_tier, enabled, is_fine_tuned, source, verification_status, last_seen_at, gateway_type)
|
|
4388
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
3305
4389
|
ON CONFLICT(id) DO UPDATE SET
|
|
3306
4390
|
provider_id = excluded.provider_id,
|
|
3307
4391
|
model_id = excluded.model_id,
|
|
@@ -3313,8 +4397,29 @@ function upsertModelRegistryEntry({ id, providerId, modelId, displayName, capabi
|
|
|
3313
4397
|
max_output_tokens = excluded.max_output_tokens,
|
|
3314
4398
|
speed_tier = excluded.speed_tier,
|
|
3315
4399
|
enabled = excluded.enabled,
|
|
3316
|
-
is_fine_tuned = excluded.is_fine_tuned
|
|
3317
|
-
|
|
4400
|
+
is_fine_tuned = excluded.is_fine_tuned,
|
|
4401
|
+
source = COALESCE(excluded.source, model_registry.source, 'catalog'),
|
|
4402
|
+
verification_status = COALESCE(excluded.verification_status, model_registry.verification_status, 'verified'),
|
|
4403
|
+
last_seen_at = COALESCE(excluded.last_seen_at, model_registry.last_seen_at),
|
|
4404
|
+
gateway_type = COALESCE(excluded.gateway_type, model_registry.gateway_type)
|
|
4405
|
+
`).run(
|
|
4406
|
+
id,
|
|
4407
|
+
providerId,
|
|
4408
|
+
modelId,
|
|
4409
|
+
displayName,
|
|
4410
|
+
caps,
|
|
4411
|
+
costPer1mInput || null,
|
|
4412
|
+
costPer1mOutput || null,
|
|
4413
|
+
maxContextTokens || null,
|
|
4414
|
+
maxOutputTokens || null,
|
|
4415
|
+
speedTier || 3,
|
|
4416
|
+
enabled !== undefined ? (enabled ? 1 : 0) : 1,
|
|
4417
|
+
isFineTuned ? 1 : 0,
|
|
4418
|
+
source || null,
|
|
4419
|
+
verificationStatus || null,
|
|
4420
|
+
lastSeenAt || null,
|
|
4421
|
+
gatewayType || null,
|
|
4422
|
+
);
|
|
3318
4423
|
}
|
|
3319
4424
|
|
|
3320
4425
|
function getModelRegistryEntry(id) {
|
|
@@ -3335,6 +4440,15 @@ function listModelsByProvider(providerId) {
|
|
|
3335
4440
|
return rows;
|
|
3336
4441
|
}
|
|
3337
4442
|
|
|
4443
|
+
function listModelCountsByProvider() {
|
|
4444
|
+
return getDb().prepare(`
|
|
4445
|
+
SELECT provider_id, COUNT(*) AS model_count
|
|
4446
|
+
FROM model_registry
|
|
4447
|
+
WHERE enabled = 1
|
|
4448
|
+
GROUP BY provider_id
|
|
4449
|
+
`).all();
|
|
4450
|
+
}
|
|
4451
|
+
|
|
3338
4452
|
function listAllModels() {
|
|
3339
4453
|
const rows = getDb().prepare(`
|
|
3340
4454
|
SELECT mr.*, mp.name AS provider_name, mp.type AS provider_type
|
|
@@ -3350,6 +4464,328 @@ function listAllModels() {
|
|
|
3350
4464
|
return rows;
|
|
3351
4465
|
}
|
|
3352
4466
|
|
|
4467
|
+
// -- Model Usage Ledger --
|
|
4468
|
+
|
|
4469
|
+
function _finiteNumberOrNull(value) {
|
|
4470
|
+
if (value == null || value === '') return null;
|
|
4471
|
+
const n = Number(value);
|
|
4472
|
+
return Number.isFinite(n) ? n : null;
|
|
4473
|
+
}
|
|
4474
|
+
|
|
4475
|
+
function _nonNegativeInteger(value) {
|
|
4476
|
+
const n = Number(value);
|
|
4477
|
+
if (!Number.isFinite(n) || n <= 0) return 0;
|
|
4478
|
+
return Math.max(0, Math.round(n));
|
|
4479
|
+
}
|
|
4480
|
+
|
|
4481
|
+
function _ledgerMetadata(value) {
|
|
4482
|
+
if (value == null) return null;
|
|
4483
|
+
if (typeof value === 'string') return value;
|
|
4484
|
+
try { return JSON.stringify(value); } catch { return String(value); }
|
|
4485
|
+
}
|
|
4486
|
+
|
|
4487
|
+
function _parseLedgerMetadata(row) {
|
|
4488
|
+
if (!row || !row.metadata) return row;
|
|
4489
|
+
try { row.metadata = JSON.parse(row.metadata); } catch {}
|
|
4490
|
+
return row;
|
|
4491
|
+
}
|
|
4492
|
+
|
|
4493
|
+
function _normalizeUsageTokens({ usage, inputTokens, outputTokens, cachedInputTokens, reasoningOutputTokens }) {
|
|
4494
|
+
const raw = usage && typeof usage === 'object' ? usage : {};
|
|
4495
|
+
const input = _nonNegativeInteger(inputTokens ?? raw.input ?? raw.input_tokens ?? raw.prompt_tokens);
|
|
4496
|
+
const output = _nonNegativeInteger(outputTokens ?? raw.output ?? raw.output_tokens ?? raw.completion_tokens);
|
|
4497
|
+
const cached = _nonNegativeInteger(
|
|
4498
|
+
cachedInputTokens
|
|
4499
|
+
?? raw.cached_input
|
|
4500
|
+
?? raw.cachedInput
|
|
4501
|
+
?? raw.cached_input_tokens
|
|
4502
|
+
?? raw.prompt_tokens_details?.cached_tokens
|
|
4503
|
+
);
|
|
4504
|
+
const reasoning = _nonNegativeInteger(
|
|
4505
|
+
reasoningOutputTokens
|
|
4506
|
+
?? raw.reasoning_output
|
|
4507
|
+
?? raw.reasoningOutput
|
|
4508
|
+
?? raw.reasoning_output_tokens
|
|
4509
|
+
?? raw.completion_tokens_details?.reasoning_tokens
|
|
4510
|
+
);
|
|
4511
|
+
return {
|
|
4512
|
+
inputTokens: input,
|
|
4513
|
+
outputTokens: output,
|
|
4514
|
+
totalTokens: _nonNegativeInteger(raw.total ?? raw.total_tokens) || input + output,
|
|
4515
|
+
cachedInputTokens: cached,
|
|
4516
|
+
reasoningOutputTokens: reasoning,
|
|
4517
|
+
};
|
|
4518
|
+
}
|
|
4519
|
+
|
|
4520
|
+
function findModelRegistryForUsage({ providerType, providerId, modelId, modelRegistryId } = {}) {
|
|
4521
|
+
const db = getDb();
|
|
4522
|
+
const registryKey = String(modelRegistryId || '').trim();
|
|
4523
|
+
if (registryKey) {
|
|
4524
|
+
const row = db.prepare(`
|
|
4525
|
+
SELECT mr.*, mp.name AS provider_name, mp.type AS provider_type
|
|
4526
|
+
FROM model_registry mr
|
|
4527
|
+
LEFT JOIN model_providers mp ON mr.provider_id = mp.id
|
|
4528
|
+
WHERE mr.id = ?
|
|
4529
|
+
LIMIT 1
|
|
4530
|
+
`).get(registryKey);
|
|
4531
|
+
if (row) return row;
|
|
4532
|
+
}
|
|
4533
|
+
|
|
4534
|
+
const model = String(modelId || '').trim();
|
|
4535
|
+
if (!model) return null;
|
|
4536
|
+
const modelLower = model.toLowerCase();
|
|
4537
|
+
const typeLower = String(providerType || '').trim().toLowerCase();
|
|
4538
|
+
const providerKey = String(providerId || '').trim();
|
|
4539
|
+
const where = ['(lower(mr.model_id) = ? OR lower(mr.id) = ?)'];
|
|
4540
|
+
const params = [modelLower, modelLower];
|
|
4541
|
+
if (providerKey) {
|
|
4542
|
+
where.push('mr.provider_id = ?');
|
|
4543
|
+
params.push(providerKey);
|
|
4544
|
+
} else if (typeLower) {
|
|
4545
|
+
where.push('lower(mp.type) = ?');
|
|
4546
|
+
params.push(typeLower);
|
|
4547
|
+
}
|
|
4548
|
+
const row = db.prepare(`
|
|
4549
|
+
SELECT mr.*, mp.name AS provider_name, mp.type AS provider_type
|
|
4550
|
+
FROM model_registry mr
|
|
4551
|
+
LEFT JOIN model_providers mp ON mr.provider_id = mp.id
|
|
4552
|
+
WHERE ${where.join(' AND ')}
|
|
4553
|
+
ORDER BY
|
|
4554
|
+
mr.enabled DESC,
|
|
4555
|
+
CASE WHEN mr.cost_per_1m_input IS NOT NULL AND mr.cost_per_1m_output IS NOT NULL THEN 0 ELSE 1 END,
|
|
4556
|
+
datetime(COALESCE(mr.last_seen_at, '1970-01-01')) DESC,
|
|
4557
|
+
mr.id ASC
|
|
4558
|
+
LIMIT 1
|
|
4559
|
+
`).get(...params);
|
|
4560
|
+
if (row) return row;
|
|
4561
|
+
|
|
4562
|
+
// Last resort for legacy rows that recorded only the executable model id.
|
|
4563
|
+
return db.prepare(`
|
|
4564
|
+
SELECT mr.*, mp.name AS provider_name, mp.type AS provider_type
|
|
4565
|
+
FROM model_registry mr
|
|
4566
|
+
LEFT JOIN model_providers mp ON mr.provider_id = mp.id
|
|
4567
|
+
WHERE lower(mr.model_id) = ? OR lower(mr.id) = ?
|
|
4568
|
+
ORDER BY
|
|
4569
|
+
CASE WHEN ? != '' AND lower(mp.type) = ? THEN 0 ELSE 1 END,
|
|
4570
|
+
mr.enabled DESC,
|
|
4571
|
+
CASE WHEN mr.cost_per_1m_input IS NOT NULL AND mr.cost_per_1m_output IS NOT NULL THEN 0 ELSE 1 END,
|
|
4572
|
+
datetime(COALESCE(mr.last_seen_at, '1970-01-01')) DESC,
|
|
4573
|
+
mr.id ASC
|
|
4574
|
+
LIMIT 1
|
|
4575
|
+
`).get(modelLower, modelLower, typeLower, typeLower) || null;
|
|
4576
|
+
}
|
|
4577
|
+
|
|
4578
|
+
function estimateModelUsageCost({ providerType, providerId, modelId, modelRegistryId, inputTokens, outputTokens } = {}) {
|
|
4579
|
+
const registry = findModelRegistryForUsage({ providerType, providerId, modelId, modelRegistryId });
|
|
4580
|
+
if (!registry) {
|
|
4581
|
+
return {
|
|
4582
|
+
costUsd: null,
|
|
4583
|
+
costSource: modelId || modelRegistryId ? 'missing_model_registry' : 'missing_model',
|
|
4584
|
+
modelRegistryId: modelRegistryId || null,
|
|
4585
|
+
providerId: providerId || null,
|
|
4586
|
+
providerType: providerType || null,
|
|
4587
|
+
gatewayType: null,
|
|
4588
|
+
};
|
|
4589
|
+
}
|
|
4590
|
+
const inputPrice = _finiteNumberOrNull(registry.cost_per_1m_input);
|
|
4591
|
+
const outputPrice = _finiteNumberOrNull(registry.cost_per_1m_output);
|
|
4592
|
+
const result = {
|
|
4593
|
+
costUsd: null,
|
|
4594
|
+
costSource: 'missing_price',
|
|
4595
|
+
modelRegistryId: registry.id,
|
|
4596
|
+
providerId: registry.provider_id || providerId || null,
|
|
4597
|
+
providerType: registry.provider_type || providerType || null,
|
|
4598
|
+
gatewayType: registry.gateway_type || null,
|
|
4599
|
+
};
|
|
4600
|
+
if (inputPrice == null || outputPrice == null) return result;
|
|
4601
|
+
result.costUsd = ((_nonNegativeInteger(inputTokens) * inputPrice) + (_nonNegativeInteger(outputTokens) * outputPrice)) / 1_000_000;
|
|
4602
|
+
result.costSource = 'model_registry';
|
|
4603
|
+
return result;
|
|
4604
|
+
}
|
|
4605
|
+
|
|
4606
|
+
function recordModelUsage({
|
|
4607
|
+
id,
|
|
4608
|
+
occurredAt,
|
|
4609
|
+
source = 'wall-e.chat',
|
|
4610
|
+
feature,
|
|
4611
|
+
sessionId,
|
|
4612
|
+
branchId,
|
|
4613
|
+
messageId,
|
|
4614
|
+
requestId,
|
|
4615
|
+
providerType,
|
|
4616
|
+
providerId,
|
|
4617
|
+
modelId,
|
|
4618
|
+
modelRegistryId,
|
|
4619
|
+
gatewayType,
|
|
4620
|
+
routeLabel,
|
|
4621
|
+
usage,
|
|
4622
|
+
inputTokens,
|
|
4623
|
+
outputTokens,
|
|
4624
|
+
cachedInputTokens,
|
|
4625
|
+
reasoningOutputTokens,
|
|
4626
|
+
latencyMs,
|
|
4627
|
+
stopReason,
|
|
4628
|
+
status,
|
|
4629
|
+
errorType,
|
|
4630
|
+
costUsd,
|
|
4631
|
+
costSource,
|
|
4632
|
+
metadata,
|
|
4633
|
+
} = {}) {
|
|
4634
|
+
const db = getDb();
|
|
4635
|
+
const normalizedSource = String(source || 'wall-e.chat').trim() || 'wall-e.chat';
|
|
4636
|
+
const normalizedRequestId = requestId == null ? null : String(requestId).trim();
|
|
4637
|
+
if (normalizedRequestId) {
|
|
4638
|
+
const existing = db.prepare('SELECT * FROM model_usage_ledger WHERE source = ? AND request_id = ?').get(normalizedSource, normalizedRequestId);
|
|
4639
|
+
if (existing) return _parseLedgerMetadata({ ...existing, duplicate: true });
|
|
4640
|
+
}
|
|
4641
|
+
|
|
4642
|
+
const tokens = _normalizeUsageTokens({ usage, inputTokens, outputTokens, cachedInputTokens, reasoningOutputTokens });
|
|
4643
|
+
const explicitCost = _finiteNumberOrNull(costUsd);
|
|
4644
|
+
const estimate = explicitCost == null
|
|
4645
|
+
? estimateModelUsageCost({
|
|
4646
|
+
providerType,
|
|
4647
|
+
providerId,
|
|
4648
|
+
modelId,
|
|
4649
|
+
modelRegistryId,
|
|
4650
|
+
inputTokens: tokens.inputTokens,
|
|
4651
|
+
outputTokens: tokens.outputTokens,
|
|
4652
|
+
})
|
|
4653
|
+
: {
|
|
4654
|
+
costUsd: explicitCost,
|
|
4655
|
+
costSource: costSource || 'explicit',
|
|
4656
|
+
modelRegistryId: modelRegistryId || null,
|
|
4657
|
+
providerId: providerId || null,
|
|
4658
|
+
providerType: providerType || null,
|
|
4659
|
+
gatewayType: gatewayType || null,
|
|
4660
|
+
};
|
|
4661
|
+
|
|
4662
|
+
const rowId = id || uuidv4();
|
|
4663
|
+
const insert = db.prepare(`
|
|
4664
|
+
INSERT INTO model_usage_ledger (
|
|
4665
|
+
id, occurred_at, source, feature, session_id, branch_id, message_id, request_id,
|
|
4666
|
+
provider_type, provider_id, model_id, model_registry_id, gateway_type, route_label,
|
|
4667
|
+
input_tokens, output_tokens, total_tokens, cached_input_tokens, reasoning_output_tokens,
|
|
4668
|
+
latency_ms, stop_reason, status, error_type, cost_usd, cost_source, metadata
|
|
4669
|
+
)
|
|
4670
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
4671
|
+
`);
|
|
4672
|
+
try {
|
|
4673
|
+
insert.run(
|
|
4674
|
+
rowId,
|
|
4675
|
+
occurredAt || new Date().toISOString(),
|
|
4676
|
+
normalizedSource,
|
|
4677
|
+
feature || null,
|
|
4678
|
+
sessionId || null,
|
|
4679
|
+
branchId || null,
|
|
4680
|
+
messageId || null,
|
|
4681
|
+
normalizedRequestId || null,
|
|
4682
|
+
providerType || estimate.providerType || null,
|
|
4683
|
+
providerId || estimate.providerId || null,
|
|
4684
|
+
modelId || null,
|
|
4685
|
+
modelRegistryId || estimate.modelRegistryId || null,
|
|
4686
|
+
gatewayType || estimate.gatewayType || null,
|
|
4687
|
+
routeLabel || null,
|
|
4688
|
+
tokens.inputTokens,
|
|
4689
|
+
tokens.outputTokens,
|
|
4690
|
+
tokens.totalTokens,
|
|
4691
|
+
tokens.cachedInputTokens,
|
|
4692
|
+
tokens.reasoningOutputTokens,
|
|
4693
|
+
latencyMs == null ? null : _nonNegativeInteger(latencyMs),
|
|
4694
|
+
stopReason || null,
|
|
4695
|
+
status || 'success',
|
|
4696
|
+
errorType || null,
|
|
4697
|
+
estimate.costUsd,
|
|
4698
|
+
costSource || estimate.costSource || null,
|
|
4699
|
+
_ledgerMetadata(metadata),
|
|
4700
|
+
);
|
|
4701
|
+
} catch (err) {
|
|
4702
|
+
if (normalizedRequestId && /UNIQUE constraint failed/.test(String(err.message || ''))) {
|
|
4703
|
+
const existing = db.prepare('SELECT * FROM model_usage_ledger WHERE source = ? AND request_id = ?').get(normalizedSource, normalizedRequestId);
|
|
4704
|
+
if (existing) return _parseLedgerMetadata({ ...existing, duplicate: true });
|
|
4705
|
+
}
|
|
4706
|
+
throw err;
|
|
4707
|
+
}
|
|
4708
|
+
return _parseLedgerMetadata(db.prepare('SELECT * FROM model_usage_ledger WHERE id = ?').get(rowId));
|
|
4709
|
+
}
|
|
4710
|
+
|
|
4711
|
+
function getModelUsageLedgerEntry(id) {
|
|
4712
|
+
const row = getDb().prepare('SELECT * FROM model_usage_ledger WHERE id = ?').get(id);
|
|
4713
|
+
return row ? _parseLedgerMetadata(row) : null;
|
|
4714
|
+
}
|
|
4715
|
+
|
|
4716
|
+
function _modelUsageWhere(filters = {}) {
|
|
4717
|
+
const clauses = [];
|
|
4718
|
+
const params = [];
|
|
4719
|
+
const addEq = (column, value, lower = false) => {
|
|
4720
|
+
const text = String(value || '').trim();
|
|
4721
|
+
if (!text) return;
|
|
4722
|
+
if (lower) {
|
|
4723
|
+
clauses.push(`lower(${column}) = ?`);
|
|
4724
|
+
params.push(text.toLowerCase());
|
|
4725
|
+
} else {
|
|
4726
|
+
clauses.push(`${column} = ?`);
|
|
4727
|
+
params.push(text);
|
|
4728
|
+
}
|
|
4729
|
+
};
|
|
4730
|
+
addEq('session_id', filters.sessionId || filters.session_id);
|
|
4731
|
+
addEq('provider_type', filters.providerType || filters.provider_type || filters.provider, true);
|
|
4732
|
+
addEq('provider_id', filters.providerId || filters.provider_id);
|
|
4733
|
+
addEq('model_id', filters.modelId || filters.model_id || filters.model, true);
|
|
4734
|
+
addEq('model_registry_id', filters.modelRegistryId || filters.model_registry_id);
|
|
4735
|
+
addEq('source', filters.source);
|
|
4736
|
+
if (filters.since) {
|
|
4737
|
+
clauses.push('occurred_at >= ?');
|
|
4738
|
+
params.push(String(filters.since));
|
|
4739
|
+
}
|
|
4740
|
+
if (filters.until) {
|
|
4741
|
+
clauses.push('occurred_at <= ?');
|
|
4742
|
+
params.push(String(filters.until));
|
|
4743
|
+
}
|
|
4744
|
+
return {
|
|
4745
|
+
where: clauses.length ? `WHERE ${clauses.join(' AND ')}` : '',
|
|
4746
|
+
params,
|
|
4747
|
+
};
|
|
4748
|
+
}
|
|
4749
|
+
|
|
4750
|
+
function listModelUsageLedger(filters = {}) {
|
|
4751
|
+
const { where, params } = _modelUsageWhere(filters);
|
|
4752
|
+
const limit = Math.max(1, Math.min(1000, _nonNegativeInteger(filters.limit) || 100));
|
|
4753
|
+
const rows = getDb().prepare(`
|
|
4754
|
+
SELECT *
|
|
4755
|
+
FROM model_usage_ledger
|
|
4756
|
+
${where}
|
|
4757
|
+
ORDER BY datetime(occurred_at) DESC, id DESC
|
|
4758
|
+
LIMIT ?
|
|
4759
|
+
`).all(...params, limit);
|
|
4760
|
+
return rows.map((row) => _parseLedgerMetadata(row));
|
|
4761
|
+
}
|
|
4762
|
+
|
|
4763
|
+
function summarizeModelUsageLedger(filters = {}) {
|
|
4764
|
+
const { where, params } = _modelUsageWhere(filters);
|
|
4765
|
+
const rows = getDb().prepare(`
|
|
4766
|
+
SELECT
|
|
4767
|
+
date(occurred_at) AS day,
|
|
4768
|
+
provider_type,
|
|
4769
|
+
model_id,
|
|
4770
|
+
model_registry_id,
|
|
4771
|
+
COUNT(*) AS calls,
|
|
4772
|
+
SUM(input_tokens) AS input_tokens,
|
|
4773
|
+
SUM(output_tokens) AS output_tokens,
|
|
4774
|
+
SUM(total_tokens) AS total_tokens,
|
|
4775
|
+
SUM(cached_input_tokens) AS cached_input_tokens,
|
|
4776
|
+
SUM(reasoning_output_tokens) AS reasoning_output_tokens,
|
|
4777
|
+
SUM(cost_usd) AS cost_usd,
|
|
4778
|
+
SUM(CASE WHEN cost_usd IS NULL THEN 1 ELSE 0 END) AS missing_cost_rows,
|
|
4779
|
+
MIN(occurred_at) AS first_seen_at,
|
|
4780
|
+
MAX(occurred_at) AS last_seen_at
|
|
4781
|
+
FROM model_usage_ledger
|
|
4782
|
+
${where}
|
|
4783
|
+
GROUP BY date(occurred_at), provider_type, model_id, model_registry_id
|
|
4784
|
+
ORDER BY datetime(last_seen_at) DESC, provider_type, model_id
|
|
4785
|
+
`).all(...params);
|
|
4786
|
+
return rows;
|
|
4787
|
+
}
|
|
4788
|
+
|
|
3353
4789
|
// -- Model Evaluations --
|
|
3354
4790
|
|
|
3355
4791
|
function getEvaluationDaysCutoff(days) {
|
|
@@ -3684,6 +5120,7 @@ function pruneUnsupportedCloudModelRegistryRowsForDb(d, { dryRun = false } = {})
|
|
|
3684
5120
|
JOIN model_providers mp ON mr.provider_id = mp.id
|
|
3685
5121
|
WHERE mp.type IN ('anthropic', 'openai', 'google', 'deepseek', 'moonshot')
|
|
3686
5122
|
AND COALESCE(mr.is_fine_tuned, 0) = 0
|
|
5123
|
+
AND COALESCE(mr.source, 'catalog') IN ('catalog', 'builtin')
|
|
3687
5124
|
`).all();
|
|
3688
5125
|
const stale = rows.filter((row) => {
|
|
3689
5126
|
const allowed = allowedByType.get(row.provider_type);
|
|
@@ -4480,10 +5917,18 @@ module.exports = {
|
|
|
4480
5917
|
getDb,
|
|
4481
5918
|
closeDb,
|
|
4482
5919
|
getDbPath,
|
|
5920
|
+
enqueueOwnerWrite,
|
|
5921
|
+
drainOwnerWrites,
|
|
5922
|
+
getOwnerWriteQueueStatus,
|
|
5923
|
+
getStorageRisk,
|
|
4483
5924
|
/** Mark DB as daemon-owned — closeDb() becomes no-op unless force=true */
|
|
4484
5925
|
setDaemonOwned() { _daemonOwned = true; },
|
|
4485
5926
|
DATA_DIR,
|
|
4486
|
-
BACKUP_DIR,
|
|
5927
|
+
get BACKUP_DIR() { return _backupDirForCurrentDb(); },
|
|
5928
|
+
DEFAULT_BACKUP_DIR,
|
|
5929
|
+
getBackupDirInfo,
|
|
5930
|
+
setBackupDir,
|
|
5931
|
+
moveBackupsToDir,
|
|
4487
5932
|
// Owner
|
|
4488
5933
|
setOwner,
|
|
4489
5934
|
getOwner,
|
|
@@ -4559,7 +6004,7 @@ module.exports = {
|
|
|
4559
6004
|
createBackup,
|
|
4560
6005
|
listBackups,
|
|
4561
6006
|
deleteBackup,
|
|
4562
|
-
|
|
6007
|
+
ensureDailyBackup,
|
|
4563
6008
|
// Skills
|
|
4564
6009
|
insertSkill,
|
|
4565
6010
|
getSkill,
|
|
@@ -4619,6 +6064,19 @@ module.exports = {
|
|
|
4619
6064
|
completeSlackInboundEvent,
|
|
4620
6065
|
releaseSlackInboundEvent,
|
|
4621
6066
|
pruneSlackInboundEvents,
|
|
6067
|
+
// Write-lock probes
|
|
6068
|
+
getWriteLockStats,
|
|
6069
|
+
resetWriteLockStats,
|
|
6070
|
+
// Question digest
|
|
6071
|
+
listUndeliveredDigestQuestions,
|
|
6072
|
+
markQuestionsDelivered,
|
|
6073
|
+
// Brain retention
|
|
6074
|
+
deleteMemory,
|
|
6075
|
+
pruneStalePendingQuestions,
|
|
6076
|
+
pruneActivityLog,
|
|
6077
|
+
pruneSkillExecutions,
|
|
6078
|
+
pruneInitiativeLog,
|
|
6079
|
+
runBrainRetention,
|
|
4622
6080
|
// Model Providers
|
|
4623
6081
|
upsertModelProvider,
|
|
4624
6082
|
getModelProvider,
|
|
@@ -4626,6 +6084,10 @@ module.exports = {
|
|
|
4626
6084
|
listModelProviders,
|
|
4627
6085
|
listEnabledProviders,
|
|
4628
6086
|
getProviderByType,
|
|
6087
|
+
setProviderRoutePolicy,
|
|
6088
|
+
getProviderRoutePolicy,
|
|
6089
|
+
getPreferredModelProviderForType,
|
|
6090
|
+
sortModelProvidersByRoutePolicy,
|
|
4629
6091
|
setProviderAuthMethod,
|
|
4630
6092
|
getProviderAuthMethod,
|
|
4631
6093
|
saveSetupProvider,
|
|
@@ -4640,7 +6102,15 @@ module.exports = {
|
|
|
4640
6102
|
upsertModelRegistryEntry,
|
|
4641
6103
|
getModelRegistryEntry,
|
|
4642
6104
|
listModelsByProvider,
|
|
6105
|
+
listModelCountsByProvider,
|
|
4643
6106
|
listAllModels,
|
|
6107
|
+
// Model Usage Ledger
|
|
6108
|
+
recordModelUsage,
|
|
6109
|
+
getModelUsageLedgerEntry,
|
|
6110
|
+
listModelUsageLedger,
|
|
6111
|
+
summarizeModelUsageLedger,
|
|
6112
|
+
estimateModelUsageCost,
|
|
6113
|
+
findModelRegistryForUsage,
|
|
4644
6114
|
// Model Evaluations
|
|
4645
6115
|
insertModelEvaluation,
|
|
4646
6116
|
getModelScorecard,
|
|
@@ -4695,11 +6165,14 @@ module.exports = {
|
|
|
4695
6165
|
getBestWorstExamples,
|
|
4696
6166
|
// MemPalace features
|
|
4697
6167
|
touchMemory,
|
|
6168
|
+
touchMemories,
|
|
6169
|
+
countMemories,
|
|
6170
|
+
countQuestions,
|
|
4698
6171
|
decayImportance,
|
|
4699
6172
|
backfillTemporalValidity,
|
|
4700
6173
|
updateKnowledgeEntityLinks,
|
|
4701
6174
|
// Entities
|
|
4702
|
-
insertEntity, getEntity, findEntity, findEntityFuzzy, mergeEntities, listEntities, getEntityGraph,
|
|
6175
|
+
insertEntity, getEntity, findEntity, findEntityFuzzy, resolvePersonIdentities, mergeEntities, listEntities, getEntityGraph,
|
|
4703
6176
|
// Memory Index
|
|
4704
6177
|
insertMemoryIndex, searchMemoryIndex, getMemoryIndex,
|
|
4705
6178
|
// Memory Lifecycle Hooks
|