@kinqs/brainrouter-mcp-server 0.3.4
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/.env.example +144 -0
- package/README.md +56 -0
- package/agents/README.md +120 -0
- package/agents/code-reviewer.md +97 -0
- package/agents/security-auditor.md +101 -0
- package/agents/test-engineer.md +95 -0
- package/dist/__tests__/agent_mode.test.d.ts +1 -0
- package/dist/__tests__/api-routes.test.d.ts +1 -0
- package/dist/__tests__/api-routes.test.js +170 -0
- package/dist/__tests__/crypto.test.d.ts +1 -0
- package/dist/__tests__/crypto.test.js +28 -0
- package/dist/__tests__/host-integrations.test.d.ts +1 -0
- package/dist/__tests__/host-integrations.test.js +82 -0
- package/dist/__tests__/integration.test.d.ts +1 -0
- package/dist/__tests__/integration.test.js +50 -0
- package/dist/__tests__/loader.test.d.ts +1 -0
- package/dist/__tests__/loader.test.js +89 -0
- package/dist/__tests__/neural-spark.test.d.ts +1 -0
- package/dist/__tests__/neural-spark.test.js +112 -0
- package/dist/__tests__/pagination.test.d.ts +1 -0
- package/dist/__tests__/pagination.test.js +23 -0
- package/dist/__tests__/redaction.test.d.ts +1 -0
- package/dist/__tests__/redaction.test.js +17 -0
- package/dist/__tests__/registry.test.d.ts +1 -0
- package/dist/__tests__/registry.test.js +56 -0
- package/dist/__tests__/retry.test.d.ts +1 -0
- package/dist/__tests__/retry.test.js +30 -0
- package/dist/__tests__/skill-activation.test.d.ts +1 -0
- package/dist/__tests__/skill-activation.test.js +112 -0
- package/dist/__tests__/working-memory.test.d.ts +1 -0
- package/dist/__tests__/working-memory.test.js +200 -0
- package/dist/__tests__/workspace-paths.test.d.ts +1 -0
- package/dist/__tests__/workspace-paths.test.js +56 -0
- package/dist/__tests__/writer.test.d.ts +1 -0
- package/dist/__tests__/writer.test.js +94 -0
- package/dist/api/auth/crypto.d.ts +4 -0
- package/dist/api/auth/crypto.js +54 -0
- package/dist/api/middleware/auth.d.ts +12 -0
- package/dist/api/middleware/auth.js +90 -0
- package/dist/api/pagination.d.ts +18 -0
- package/dist/api/pagination.js +32 -0
- package/dist/api/routes/auth.d.ts +1 -0
- package/dist/api/routes/auth.js +130 -0
- package/dist/api/routes/chat-completions.d.ts +7 -0
- package/dist/api/routes/chat-completions.js +474 -0
- package/dist/api/routes/contradictions.d.ts +1 -0
- package/dist/api/routes/contradictions.js +28 -0
- package/dist/api/routes/evidence.d.ts +1 -0
- package/dist/api/routes/evidence.js +59 -0
- package/dist/api/routes/governance.d.ts +1 -0
- package/dist/api/routes/governance.js +95 -0
- package/dist/api/routes/graph.d.ts +1 -0
- package/dist/api/routes/graph.js +25 -0
- package/dist/api/routes/hooks.d.ts +1 -0
- package/dist/api/routes/hooks.js +88 -0
- package/dist/api/routes/memories.d.ts +1 -0
- package/dist/api/routes/memories.js +92 -0
- package/dist/api/routes/persona.d.ts +1 -0
- package/dist/api/routes/persona.js +9 -0
- package/dist/api/routes/scenes.d.ts +1 -0
- package/dist/api/routes/scenes.js +35 -0
- package/dist/api/routes/skills.d.ts +1 -0
- package/dist/api/routes/skills.js +14 -0
- package/dist/api/routes/stats.d.ts +1 -0
- package/dist/api/routes/stats.js +8 -0
- package/dist/api/routes/users.d.ts +1 -0
- package/dist/api/routes/users.js +82 -0
- package/dist/api/routes/working.d.ts +1 -0
- package/dist/api/routes/working.js +88 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +492 -0
- package/dist/integrations/claude-code.d.ts +12 -0
- package/dist/integrations/claude-code.js +35 -0
- package/dist/integrations/codex.d.ts +12 -0
- package/dist/integrations/codex.js +34 -0
- package/dist/integrations/generic-mcp.d.ts +52 -0
- package/dist/integrations/generic-mcp.js +118 -0
- package/dist/loader.d.ts +29 -0
- package/dist/loader.js +200 -0
- package/dist/memory/capture.d.ts +35 -0
- package/dist/memory/capture.js +230 -0
- package/dist/memory/config.d.ts +2 -0
- package/dist/memory/config.js +3 -0
- package/dist/memory/engine.d.ts +203 -0
- package/dist/memory/engine.js +626 -0
- package/dist/memory/llm-semaphore.d.ts +41 -0
- package/dist/memory/llm-semaphore.js +81 -0
- package/dist/memory/memory-type-config.d.ts +11 -0
- package/dist/memory/memory-type-config.js +65 -0
- package/dist/memory/pipeline/cognitive-contradiction.d.ts +7 -0
- package/dist/memory/pipeline/cognitive-contradiction.js +59 -0
- package/dist/memory/pipeline/cognitive-dedup.d.ts +23 -0
- package/dist/memory/pipeline/cognitive-dedup.js +38 -0
- package/dist/memory/pipeline/cognitive-extractor.d.ts +21 -0
- package/dist/memory/pipeline/cognitive-extractor.js +183 -0
- package/dist/memory/pipeline/contextual-focus-builder.d.ts +13 -0
- package/dist/memory/pipeline/contextual-focus-builder.js +135 -0
- package/dist/memory/pipeline/focus-direction-shift.d.ts +10 -0
- package/dist/memory/pipeline/focus-direction-shift.js +27 -0
- package/dist/memory/pipeline/graph-builder.d.ts +11 -0
- package/dist/memory/pipeline/graph-builder.js +88 -0
- package/dist/memory/pipeline/graph-recall.d.ts +13 -0
- package/dist/memory/pipeline/graph-recall.js +55 -0
- package/dist/memory/pipeline/identity-distiller.d.ts +15 -0
- package/dist/memory/pipeline/identity-distiller.js +40 -0
- package/dist/memory/pipeline/l1-contradiction.d.ts +7 -0
- package/dist/memory/pipeline/l1-contradiction.js +66 -0
- package/dist/memory/pipeline/l1-dedup.d.ts +23 -0
- package/dist/memory/pipeline/l1-dedup.js +39 -0
- package/dist/memory/pipeline/l1-extractor.d.ts +21 -0
- package/dist/memory/pipeline/l1-extractor.js +180 -0
- package/dist/memory/pipeline/l2-direction-shift.d.ts +10 -0
- package/dist/memory/pipeline/l2-direction-shift.js +27 -0
- package/dist/memory/pipeline/l2-scene.d.ts +15 -0
- package/dist/memory/pipeline/l2-scene.js +140 -0
- package/dist/memory/pipeline/l3-distiller.d.ts +15 -0
- package/dist/memory/pipeline/l3-distiller.js +40 -0
- package/dist/memory/pipeline/neural-spark.d.ts +27 -0
- package/dist/memory/pipeline/neural-spark.js +78 -0
- package/dist/memory/pipeline/skill-prewarm.d.ts +63 -0
- package/dist/memory/pipeline/skill-prewarm.js +127 -0
- package/dist/memory/pipeline/task-queue.d.ts +54 -0
- package/dist/memory/pipeline/task-queue.js +117 -0
- package/dist/memory/prompts/cognitive-contradiction.d.ts +1 -0
- package/dist/memory/prompts/cognitive-contradiction.js +25 -0
- package/dist/memory/prompts/cognitive-extraction.d.ts +10 -0
- package/dist/memory/prompts/cognitive-extraction.js +114 -0
- package/dist/memory/prompts/core-identity.d.ts +6 -0
- package/dist/memory/prompts/core-identity.js +60 -0
- package/dist/memory/prompts/focus-direction-shift.d.ts +5 -0
- package/dist/memory/prompts/focus-direction-shift.js +32 -0
- package/dist/memory/prompts/focus-scene-cluster.d.ts +2 -0
- package/dist/memory/prompts/focus-scene-cluster.js +33 -0
- package/dist/memory/prompts/focus-scene.d.ts +7 -0
- package/dist/memory/prompts/focus-scene.js +40 -0
- package/dist/memory/prompts/graph-extraction-batch.d.ts +14 -0
- package/dist/memory/prompts/graph-extraction-batch.js +54 -0
- package/dist/memory/prompts/graph-extraction.d.ts +2 -0
- package/dist/memory/prompts/graph-extraction.js +53 -0
- package/dist/memory/prompts/l1-contradiction-batch.d.ts +16 -0
- package/dist/memory/prompts/l1-contradiction-batch.js +47 -0
- package/dist/memory/prompts/l1-contradiction.d.ts +1 -0
- package/dist/memory/prompts/l1-contradiction.js +25 -0
- package/dist/memory/prompts/l1-extraction.d.ts +10 -0
- package/dist/memory/prompts/l1-extraction.js +114 -0
- package/dist/memory/prompts/l2-direction-shift.d.ts +5 -0
- package/dist/memory/prompts/l2-direction-shift.js +32 -0
- package/dist/memory/prompts/l2-scene-cluster.d.ts +2 -0
- package/dist/memory/prompts/l2-scene-cluster.js +33 -0
- package/dist/memory/prompts/l2-scene.d.ts +7 -0
- package/dist/memory/prompts/l2-scene.js +40 -0
- package/dist/memory/prompts/l3-persona.d.ts +6 -0
- package/dist/memory/prompts/l3-persona.js +60 -0
- package/dist/memory/recall.d.ts +47 -0
- package/dist/memory/recall.js +427 -0
- package/dist/memory/redaction.d.ts +1 -0
- package/dist/memory/redaction.js +24 -0
- package/dist/memory/retry.d.ts +13 -0
- package/dist/memory/retry.js +53 -0
- package/dist/memory/scheduler.d.ts +9 -0
- package/dist/memory/scheduler.js +16 -0
- package/dist/memory/skill-hints-loader.d.ts +30 -0
- package/dist/memory/skill-hints-loader.js +100 -0
- package/dist/memory/store/embedding.d.ts +16 -0
- package/dist/memory/store/embedding.js +68 -0
- package/dist/memory/store/reranker.d.ts +24 -0
- package/dist/memory/store/reranker.js +83 -0
- package/dist/memory/store/sqlite.d.ts +167 -0
- package/dist/memory/store/sqlite.js +1816 -0
- package/dist/memory/store/types.d.ts +101 -0
- package/dist/memory/store/types.js +1 -0
- package/dist/memory/types.d.ts +207 -0
- package/dist/memory/types.js +7 -0
- package/dist/memory/validation.d.ts +441 -0
- package/dist/memory/validation.js +129 -0
- package/dist/memory/working/canvas.d.ts +5 -0
- package/dist/memory/working/canvas.js +43 -0
- package/dist/memory/working/offload.d.ts +71 -0
- package/dist/memory/working/offload.js +211 -0
- package/dist/memory/working/step-log.d.ts +16 -0
- package/dist/memory/working/step-log.js +35 -0
- package/dist/registry.d.ts +34 -0
- package/dist/registry.js +305 -0
- package/dist/resolver.d.ts +17 -0
- package/dist/resolver.js +126 -0
- package/dist/scripts/validate-foreign-workspace-path.d.ts +1 -0
- package/dist/scripts/validate-foreign-workspace-path.js +39 -0
- package/dist/tools/agent_memory_tools.d.ts +485 -0
- package/dist/tools/agent_memory_tools.js +793 -0
- package/dist/tools/create_skill.d.ts +46 -0
- package/dist/tools/create_skill.js +46 -0
- package/dist/tools/get_doc.d.ts +21 -0
- package/dist/tools/get_doc.js +24 -0
- package/dist/tools/get_persona.d.ts +15 -0
- package/dist/tools/get_persona.js +20 -0
- package/dist/tools/get_reference.d.ts +15 -0
- package/dist/tools/get_reference.js +20 -0
- package/dist/tools/get_skill.d.ts +34 -0
- package/dist/tools/get_skill.js +65 -0
- package/dist/tools/get_template_doc.d.ts +21 -0
- package/dist/tools/get_template_doc.js +24 -0
- package/dist/tools/list_docs.d.ts +15 -0
- package/dist/tools/list_docs.js +16 -0
- package/dist/tools/list_skills.d.ts +18 -0
- package/dist/tools/list_skills.js +17 -0
- package/dist/tools/list_template_docs.d.ts +15 -0
- package/dist/tools/list_template_docs.js +16 -0
- package/dist/tools/memory-engineering.d.ts +225 -0
- package/dist/tools/memory-engineering.js +284 -0
- package/dist/tools/memory-explain.d.ts +34 -0
- package/dist/tools/memory-explain.js +109 -0
- package/dist/tools/memory-governance.d.ts +171 -0
- package/dist/tools/memory-governance.js +224 -0
- package/dist/tools/memory-hooks.d.ts +67 -0
- package/dist/tools/memory-hooks.js +102 -0
- package/dist/tools/memory-working.d.ts +98 -0
- package/dist/tools/memory-working.js +101 -0
- package/dist/tools/memory_capture_turn.d.ts +66 -0
- package/dist/tools/memory_capture_turn.js +85 -0
- package/dist/tools/memory_consolidate.d.ts +55 -0
- package/dist/tools/memory_consolidate.js +176 -0
- package/dist/tools/memory_contradictions.d.ts +53 -0
- package/dist/tools/memory_contradictions.js +52 -0
- package/dist/tools/memory_graph_query.d.ts +51 -0
- package/dist/tools/memory_graph_query.js +35 -0
- package/dist/tools/memory_mark_cited.d.ts +43 -0
- package/dist/tools/memory_mark_cited.js +63 -0
- package/dist/tools/memory_recall.d.ts +77 -0
- package/dist/tools/memory_recall.js +81 -0
- package/dist/tools/memory_register_skill_hints.d.ts +49 -0
- package/dist/tools/memory_register_skill_hints.js +55 -0
- package/dist/tools/memory_resolve_session.d.ts +24 -0
- package/dist/tools/memory_resolve_session.js +133 -0
- package/dist/tools/memory_search.d.ts +146 -0
- package/dist/tools/memory_search.js +84 -0
- package/dist/tools/search_skills.d.ts +18 -0
- package/dist/tools/search_skills.js +17 -0
- package/dist/tools/update_doc.d.ts +24 -0
- package/dist/tools/update_doc.js +35 -0
- package/dist/tools/update_skill.d.ts +30 -0
- package/dist/tools/update_skill.js +80 -0
- package/dist/types.d.ts +81 -0
- package/dist/types.js +4 -0
- package/dist/writer.d.ts +30 -0
- package/dist/writer.js +220 -0
- package/docs/TEMPLATE ONLY +1 -0
- package/docs/api/API.md +64 -0
- package/docs/api/security/SECURITY.md +58 -0
- package/docs/deployment/DockerDeployment.md +30 -0
- package/docs/design/Design.md +59 -0
- package/docs/design/themes/apple.md +101 -0
- package/docs/design/themes/dieter-grid.md +100 -0
- package/docs/design/themes/gallery-white.md +100 -0
- package/docs/design/themes/pinterest.md +101 -0
- package/docs/design/themes/realty-open-house.md +101 -0
- package/docs/design/themes/vodafone.md +101 -0
- package/docs/hooks/Hooks.md +30 -0
- package/docs/schema/Schema.md +35 -0
- package/docs/strategy/ScalingStrategy.md +19 -0
- package/package.json +88 -0
- package/references/accessibility-checklist.md +160 -0
- package/references/orchestration-patterns.md +370 -0
- package/references/performance-checklist.md +153 -0
- package/references/security-checklist.md +134 -0
- package/references/testing-patterns.md +236 -0
- package/skills/agent/adr-skill/SKILL.md +299 -0
- package/skills/agent/agentic-engineering-workflow/SKILL.md +95 -0
- package/skills/agent/bootstrap-skill/SKILL.md +103 -0
- package/skills/agent/context-engineering/SKILL.md +307 -0
- package/skills/agent/debugging-and-error-recovery/SKILL.md +308 -0
- package/skills/agent/developer-growth-analysis/SKILL.md +328 -0
- package/skills/agent/doubt-driven-skill/SKILL.md +249 -0
- package/skills/agent/handover-skill/SKILL.md +112 -0
- package/skills/agent/idea-refine-skill/SKILL.md +185 -0
- package/skills/agent/idea-refine-skill/examples.md +238 -0
- package/skills/agent/idea-refine-skill/frameworks.md +99 -0
- package/skills/agent/idea-refine-skill/refinement-criteria.md +113 -0
- package/skills/agent/interview-skill/SKILL.md +226 -0
- package/skills/agent/planning-skill/SKILL.md +270 -0
- package/skills/agent/skill-authoring/SKILL.md +189 -0
- package/skills/agent/source-driven-skill/SKILL.md +197 -0
- package/skills/agent/spec-driven-skill/SKILL.md +221 -0
- package/skills/agent/sync-skill/SKILL.md +92 -0
- package/skills/agent/using-agent-skills/SKILL.md +189 -0
- package/skills/api/a11y-skill/SKILL.md +88 -0
- package/skills/api/api-skill/SKILL.md +123 -0
- package/skills/api/auth-skill/SKILL.md +80 -0
- package/skills/api/debug-skill/SKILL.md +535 -0
- package/skills/api/performance-skill/SKILL.md +100 -0
- package/skills/api/testing-skill/SKILL.md +100 -0
- package/skills/codebase/code-review-and-quality/SKILL.md +228 -0
- package/skills/codebase/code-simplification/SKILL.md +352 -0
- package/skills/codebase/code-structure-cleanup/SKILL.md +142 -0
- package/skills/codebase/concerns-skill/SKILL.md +89 -0
- package/skills/codebase/conventions-skill/SKILL.md +95 -0
- package/skills/codebase/doc-management-skill/SKILL.md +47 -0
- package/skills/codebase/git-workflow-skill/SKILL.md +312 -0
- package/skills/communication/1-3-1-rule/SKILL.md +120 -0
- package/skills/design/brutalist-skill/SKILL.md +131 -0
- package/skills/design/concept-diagrams/SKILL.md +387 -0
- package/skills/design/concept-diagrams/examples/apartment-floor-plan-conversion.md +244 -0
- package/skills/design/concept-diagrams/examples/automated-password-reset-flow.md +276 -0
- package/skills/design/concept-diagrams/examples/autonomous-llm-research-agent-flow.md +240 -0
- package/skills/design/concept-diagrams/examples/banana-journey-tree-to-smoothie.md +161 -0
- package/skills/design/concept-diagrams/examples/commercial-aircraft-structure.md +209 -0
- package/skills/design/concept-diagrams/examples/cpu-ooo-microarchitecture.md +236 -0
- package/skills/design/concept-diagrams/examples/electricity-grid-flow.md +182 -0
- package/skills/design/concept-diagrams/examples/feature-film-production-pipeline.md +172 -0
- package/skills/design/concept-diagrams/examples/hospital-emergency-department-flow.md +165 -0
- package/skills/design/concept-diagrams/examples/ml-benchmark-grouped-bar-chart.md +114 -0
- package/skills/design/concept-diagrams/examples/place-order-uml-sequence.md +325 -0
- package/skills/design/concept-diagrams/examples/smart-city-infrastructure.md +173 -0
- package/skills/design/concept-diagrams/examples/smartphone-layer-anatomy.md +154 -0
- package/skills/design/concept-diagrams/examples/sn2-reaction-mechanism.md +247 -0
- package/skills/design/concept-diagrams/examples/wind-turbine-structure.md +338 -0
- package/skills/design/concept-diagrams/references/dashboard-patterns.md +43 -0
- package/skills/design/concept-diagrams/references/infrastructure-patterns.md +144 -0
- package/skills/design/concept-diagrams/references/physical-shape-cookbook.md +42 -0
- package/skills/design/concept-diagrams/templates/template.html +174 -0
- package/skills/design/gpt-tasteskill/SKILL.md +114 -0
- package/skills/design/minimalist-skill/SKILL.md +116 -0
- package/skills/design/output-skill/SKILL.md +87 -0
- package/skills/design/redesign-skill/SKILL.md +213 -0
- package/skills/design/soft-skill/SKILL.md +132 -0
- package/skills/design/stitch-skill/EXAMPLE.md +121 -0
- package/skills/design/stitch-skill/SKILL.md +222 -0
- package/skills/design/taste-skill/SKILL.md +269 -0
- package/skills/devops/ci-cd-skill/SKILL.md +402 -0
- package/skills/devops/docker-skill/SKILL.md +297 -0
- package/skills/devops/domain-skill/SKILL.md +234 -0
- package/skills/lifecycle/changelog-generator/SKILL.md +135 -0
- package/skills/lifecycle/incremental-skill/SKILL.md +257 -0
- package/skills/lifecycle/migration-skill/SKILL.md +218 -0
- package/skills/lifecycle/shipping-skill/SKILL.md +321 -0
- package/skills/memory/agent-memory/SKILL.md +122 -0
- package/skills/qa/browser-testing-skill/SKILL.md +314 -0
- package/skills/ux/adversarial-ux-skill/SKILL.md +168 -0
|
@@ -0,0 +1,1816 @@
|
|
|
1
|
+
import { DatabaseSync } from "node:sqlite";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import * as sqliteVec from "sqlite-vec";
|
|
4
|
+
// Ensure Node version has node:sqlite (v22+)
|
|
5
|
+
const DB_VERSION_ERROR = "Memory Engine requires Node.js v22+ with node:sqlite built-in.";
|
|
6
|
+
// A minimal BM25 search ranking helper (for simple text split)
|
|
7
|
+
function bm25RankToScore(rank) {
|
|
8
|
+
if (!Number.isFinite(rank))
|
|
9
|
+
return 1 / (1 + 999);
|
|
10
|
+
if (rank < 0) {
|
|
11
|
+
const relevance = -rank;
|
|
12
|
+
return relevance / (1 + relevance);
|
|
13
|
+
}
|
|
14
|
+
return 1 / (1 + rank);
|
|
15
|
+
}
|
|
16
|
+
function buildFtsQuery(raw) {
|
|
17
|
+
// Simple Unicode regex split for English + general tokens
|
|
18
|
+
const tokens = raw
|
|
19
|
+
.match(/[\p{L}\p{N}_]+/gu)
|
|
20
|
+
?.map((t) => t.trim())
|
|
21
|
+
.filter(Boolean) ?? [];
|
|
22
|
+
if (tokens.length === 0)
|
|
23
|
+
return null;
|
|
24
|
+
const quoted = tokens.map((t) => `"${t.replaceAll('"', "")}"`);
|
|
25
|
+
return quoted.join(" OR ");
|
|
26
|
+
}
|
|
27
|
+
function parseJsonObject(raw) {
|
|
28
|
+
if (!raw)
|
|
29
|
+
return {};
|
|
30
|
+
try {
|
|
31
|
+
const parsed = JSON.parse(raw);
|
|
32
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return {};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function parseJsonArray(raw) {
|
|
39
|
+
if (!raw)
|
|
40
|
+
return [];
|
|
41
|
+
try {
|
|
42
|
+
const parsed = JSON.parse(raw);
|
|
43
|
+
return Array.isArray(parsed) ? parsed.map(String) : [];
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function cognitiveRowToRecord(row) {
|
|
50
|
+
return {
|
|
51
|
+
id: row.record_id,
|
|
52
|
+
userId: row.user_id,
|
|
53
|
+
sessionKey: row.session_key ?? "",
|
|
54
|
+
sessionId: row.session_id ?? "",
|
|
55
|
+
content: row.content,
|
|
56
|
+
type: row.type || "episodic",
|
|
57
|
+
priority: row.priority ?? 50,
|
|
58
|
+
sceneName: row.scene_name ?? "",
|
|
59
|
+
skillTag: row.skill_tag ?? "",
|
|
60
|
+
halfLifeDays: row.half_life_days ?? null,
|
|
61
|
+
supersededBy: row.superseded_by ?? null,
|
|
62
|
+
invalidAt: row.invalid_at ?? null,
|
|
63
|
+
timestampStr: row.timestamp_str ?? "",
|
|
64
|
+
timestampStart: row.timestamp_start ?? "",
|
|
65
|
+
timestampEnd: row.timestamp_end ?? "",
|
|
66
|
+
createdTime: row.created_time ?? "",
|
|
67
|
+
updatedTime: row.updated_time ?? "",
|
|
68
|
+
metadata: parseJsonObject(row.metadata_json),
|
|
69
|
+
confidence: typeof row.confidence === "number" ? row.confidence : 0.65,
|
|
70
|
+
status: row.status ?? (row.archived ? "archived" : "active"),
|
|
71
|
+
sourceKind: row.source_kind ?? "",
|
|
72
|
+
verificationStatus: row.verification_status ?? "",
|
|
73
|
+
repoPaths: parseJsonArray(row.repo_paths_json),
|
|
74
|
+
filePaths: parseJsonArray(row.file_paths_json),
|
|
75
|
+
commands: parseJsonArray(row.commands_json),
|
|
76
|
+
citationCount: row.citation_count ?? 0,
|
|
77
|
+
lastCitedAt: row.last_cited_at ?? null,
|
|
78
|
+
neverCitedCount: row.never_cited_count ?? 0,
|
|
79
|
+
archived: Boolean(row.archived),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function evidenceRowToRecord(row) {
|
|
83
|
+
return {
|
|
84
|
+
id: row.id,
|
|
85
|
+
userId: row.user_id,
|
|
86
|
+
recordId: row.record_id,
|
|
87
|
+
kind: row.kind,
|
|
88
|
+
ref: row.ref,
|
|
89
|
+
excerpt: row.excerpt ?? "",
|
|
90
|
+
observedAt: row.observed_at ?? "",
|
|
91
|
+
metadata: parseJsonObject(row.metadata_json),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function operationRowToRecord(row) {
|
|
95
|
+
return {
|
|
96
|
+
id: row.id,
|
|
97
|
+
userId: row.user_id,
|
|
98
|
+
recordId: row.record_id ?? null,
|
|
99
|
+
operation: row.operation,
|
|
100
|
+
actor: row.actor ?? "",
|
|
101
|
+
sessionKey: row.session_key ?? "",
|
|
102
|
+
reason: row.reason ?? "",
|
|
103
|
+
createdAt: row.created_at ?? "",
|
|
104
|
+
metadata: parseJsonObject(row.metadata_json),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
export class SqliteMemoryStore {
|
|
108
|
+
db;
|
|
109
|
+
// Sensory statements
|
|
110
|
+
stmtSensoryUpsertMeta;
|
|
111
|
+
stmtSensoryQueryAfter;
|
|
112
|
+
// Cognitive statements
|
|
113
|
+
stmtCognitiveUpsertMeta;
|
|
114
|
+
stmtCognitiveGetMeta;
|
|
115
|
+
// FTS statements
|
|
116
|
+
stmtCognitiveFtsInsert;
|
|
117
|
+
stmtCognitiveFtsSearch;
|
|
118
|
+
// Vector statements
|
|
119
|
+
stmtCognitiveVecInsert;
|
|
120
|
+
stmtCognitiveVecDelete;
|
|
121
|
+
vecLoaded = false;
|
|
122
|
+
vecDimensions = 0;
|
|
123
|
+
constructor(dbPath) {
|
|
124
|
+
try {
|
|
125
|
+
this.db = new DatabaseSync(dbPath, { allowExtension: true });
|
|
126
|
+
}
|
|
127
|
+
catch (e) {
|
|
128
|
+
throw new Error(`${DB_VERSION_ERROR}\n${e}`);
|
|
129
|
+
}
|
|
130
|
+
this.db.exec("PRAGMA busy_timeout = 5000");
|
|
131
|
+
this.db.exec("PRAGMA journal_mode = WAL");
|
|
132
|
+
}
|
|
133
|
+
init() {
|
|
134
|
+
this.initSchema();
|
|
135
|
+
}
|
|
136
|
+
initVec(dimensions) {
|
|
137
|
+
if (dimensions <= 0)
|
|
138
|
+
return;
|
|
139
|
+
try {
|
|
140
|
+
sqliteVec.load(this.db);
|
|
141
|
+
this.vecLoaded = true;
|
|
142
|
+
}
|
|
143
|
+
catch (e) {
|
|
144
|
+
console.error("[BrainRouter] Failed to load sqlite-vec. Vector search disabled.", e);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
this.vecDimensions = dimensions;
|
|
148
|
+
this.db.exec(`
|
|
149
|
+
CREATE TABLE IF NOT EXISTS embedding_meta (
|
|
150
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
151
|
+
dimensions INTEGER NOT NULL,
|
|
152
|
+
created_at TEXT NOT NULL
|
|
153
|
+
)
|
|
154
|
+
`);
|
|
155
|
+
// Robust dimension check: extract actual dimension from the virtual table schema
|
|
156
|
+
const tableInfo = this.db.prepare("SELECT sql FROM sqlite_master WHERE name = 'cognitive_vec'").get();
|
|
157
|
+
let actualDimensions = -1;
|
|
158
|
+
if (tableInfo && tableInfo.sql) {
|
|
159
|
+
const match = tableInfo.sql.match(/float\[(\d+)\]/i);
|
|
160
|
+
if (match)
|
|
161
|
+
actualDimensions = parseInt(match[1], 10);
|
|
162
|
+
}
|
|
163
|
+
if (actualDimensions !== -1 && actualDimensions !== dimensions) {
|
|
164
|
+
console.error(`[BrainRouter] Embedding dimensions changed (${actualDimensions} -> ${dimensions}). Recreating vector tables.`);
|
|
165
|
+
try {
|
|
166
|
+
this.db.exec("DROP TABLE IF EXISTS cognitive_vec");
|
|
167
|
+
}
|
|
168
|
+
catch (e) {
|
|
169
|
+
console.warn("[BrainRouter] Error dropping cognitive_vec:", e);
|
|
170
|
+
}
|
|
171
|
+
this.db.prepare("UPDATE embedding_meta SET dimensions = ?, created_at = ? WHERE id = 1")
|
|
172
|
+
.run(dimensions, new Date().toISOString());
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
const metaRow = this.db.prepare("SELECT dimensions FROM embedding_meta WHERE id = 1").get();
|
|
176
|
+
if (!metaRow) {
|
|
177
|
+
this.db.prepare("INSERT INTO embedding_meta (id, dimensions, created_at) VALUES (1, ?, ?)")
|
|
178
|
+
.run(dimensions, new Date().toISOString());
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
this.db.exec(`
|
|
182
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS cognitive_vec USING vec0(
|
|
183
|
+
record_id TEXT PRIMARY KEY,
|
|
184
|
+
embedding float[${dimensions}] distance_metric=cosine
|
|
185
|
+
)
|
|
186
|
+
`);
|
|
187
|
+
this.stmtCognitiveVecInsert = this.db.prepare("INSERT INTO cognitive_vec (record_id, embedding) VALUES (?, ?)");
|
|
188
|
+
this.stmtCognitiveVecDelete = this.db.prepare("DELETE FROM cognitive_vec WHERE record_id = ?");
|
|
189
|
+
}
|
|
190
|
+
isVecAvailable() {
|
|
191
|
+
return this.vecLoaded && this.vecDimensions > 0;
|
|
192
|
+
}
|
|
193
|
+
async reembedStaleRecords(embedder) {
|
|
194
|
+
if (!this.vecLoaded)
|
|
195
|
+
return 0;
|
|
196
|
+
const rows = this.db.prepare(`
|
|
197
|
+
SELECT r.record_id, r.content
|
|
198
|
+
FROM cognitive_records r
|
|
199
|
+
LEFT JOIN cognitive_vec v ON r.record_id = v.record_id
|
|
200
|
+
WHERE r.invalid_at IS NULL
|
|
201
|
+
AND r.archived = 0
|
|
202
|
+
AND v.record_id IS NULL
|
|
203
|
+
ORDER BY r.created_time ASC, r.record_id ASC
|
|
204
|
+
`).all();
|
|
205
|
+
let successCount = 0;
|
|
206
|
+
for (const row of rows) {
|
|
207
|
+
try {
|
|
208
|
+
const embedding = await embedder(row.content);
|
|
209
|
+
this.upsertCognitiveVec(row.record_id, embedding);
|
|
210
|
+
successCount += 1;
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
console.error(`[BrainRouter] Failed to re-embed record ${row.record_id}:`, error instanceof Error ? error.message : error);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return successCount;
|
|
217
|
+
}
|
|
218
|
+
getSqliteVersion() {
|
|
219
|
+
try {
|
|
220
|
+
const row = this.db.prepare("SELECT sqlite_version() AS version").get();
|
|
221
|
+
return row?.version ?? "unknown";
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
return "unknown";
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
initSchema() {
|
|
228
|
+
// ── Sensory Schema ──
|
|
229
|
+
this.db.exec(`
|
|
230
|
+
CREATE TABLE IF NOT EXISTS sensory_stream (
|
|
231
|
+
record_id TEXT PRIMARY KEY,
|
|
232
|
+
user_id TEXT NOT NULL,
|
|
233
|
+
session_key TEXT NOT NULL,
|
|
234
|
+
session_id TEXT DEFAULT '',
|
|
235
|
+
role TEXT NOT NULL DEFAULT '',
|
|
236
|
+
message_text TEXT NOT NULL,
|
|
237
|
+
recorded_at TEXT DEFAULT '',
|
|
238
|
+
timestamp INTEGER DEFAULT 0,
|
|
239
|
+
skill_tag TEXT DEFAULT '',
|
|
240
|
+
extracted_at TEXT DEFAULT NULL
|
|
241
|
+
)
|
|
242
|
+
`);
|
|
243
|
+
this.db.exec("CREATE INDEX IF NOT EXISTS idx_sensory_user_session ON sensory_stream(user_id, session_key)");
|
|
244
|
+
this.db.exec("CREATE INDEX IF NOT EXISTS idx_sensory_recorded ON sensory_stream(recorded_at)");
|
|
245
|
+
this.db.exec("CREATE INDEX IF NOT EXISTS idx_sensory_extracted ON sensory_stream(user_id, session_key, extracted_at)");
|
|
246
|
+
this.db.exec(`
|
|
247
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
248
|
+
user_id TEXT PRIMARY KEY,
|
|
249
|
+
api_key TEXT NOT NULL UNIQUE,
|
|
250
|
+
password_hash TEXT DEFAULT NULL,
|
|
251
|
+
display_name TEXT DEFAULT '',
|
|
252
|
+
email TEXT DEFAULT '',
|
|
253
|
+
is_admin INTEGER DEFAULT 0,
|
|
254
|
+
status TEXT DEFAULT 'active',
|
|
255
|
+
created_at TEXT NOT NULL
|
|
256
|
+
)
|
|
257
|
+
`);
|
|
258
|
+
this.stmtSensoryUpsertMeta = this.db.prepare(`
|
|
259
|
+
INSERT INTO sensory_stream (
|
|
260
|
+
record_id, user_id, session_key, session_id, role, message_text, recorded_at, timestamp, skill_tag
|
|
261
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
262
|
+
ON CONFLICT(record_id) DO UPDATE SET
|
|
263
|
+
message_text=excluded.message_text,
|
|
264
|
+
recorded_at=excluded.recorded_at,
|
|
265
|
+
timestamp=excluded.timestamp
|
|
266
|
+
`);
|
|
267
|
+
this.stmtSensoryQueryAfter = this.db.prepare(`
|
|
268
|
+
SELECT record_id as id, user_id as userId, session_key as sessionKey, session_id as sessionId,
|
|
269
|
+
role, message_text as messageText, recorded_at as recordedAt, timestamp, skill_tag as skillTag
|
|
270
|
+
FROM sensory_stream
|
|
271
|
+
WHERE user_id = ? AND session_key = ? AND recorded_at > ? AND extracted_at IS NULL
|
|
272
|
+
ORDER BY recorded_at DESC
|
|
273
|
+
LIMIT ?
|
|
274
|
+
`);
|
|
275
|
+
// ── Cognitive Schema ──
|
|
276
|
+
this.db.exec(`
|
|
277
|
+
CREATE TABLE IF NOT EXISTS cognitive_records (
|
|
278
|
+
record_id TEXT PRIMARY KEY,
|
|
279
|
+
user_id TEXT NOT NULL,
|
|
280
|
+
session_key TEXT DEFAULT '',
|
|
281
|
+
session_id TEXT DEFAULT '',
|
|
282
|
+
content TEXT NOT NULL,
|
|
283
|
+
type TEXT DEFAULT '',
|
|
284
|
+
priority INTEGER DEFAULT 50,
|
|
285
|
+
scene_name TEXT DEFAULT '',
|
|
286
|
+
skill_tag TEXT DEFAULT '',
|
|
287
|
+
half_life_days INTEGER,
|
|
288
|
+
superseded_by TEXT,
|
|
289
|
+
invalid_at TEXT DEFAULT NULL,
|
|
290
|
+
timestamp_str TEXT DEFAULT '',
|
|
291
|
+
timestamp_start TEXT DEFAULT '',
|
|
292
|
+
timestamp_end TEXT DEFAULT '',
|
|
293
|
+
created_time TEXT DEFAULT '',
|
|
294
|
+
updated_time TEXT DEFAULT '',
|
|
295
|
+
metadata_json TEXT DEFAULT '{}',
|
|
296
|
+
citation_count INTEGER DEFAULT 0,
|
|
297
|
+
last_cited_at TEXT,
|
|
298
|
+
never_cited_count INTEGER DEFAULT 0,
|
|
299
|
+
archived INTEGER DEFAULT 0,
|
|
300
|
+
confidence REAL DEFAULT 0.65,
|
|
301
|
+
status TEXT DEFAULT 'active',
|
|
302
|
+
source_kind TEXT DEFAULT '',
|
|
303
|
+
verification_status TEXT DEFAULT '',
|
|
304
|
+
repo_paths_json TEXT DEFAULT '[]',
|
|
305
|
+
file_paths_json TEXT DEFAULT '[]',
|
|
306
|
+
commands_json TEXT DEFAULT '[]'
|
|
307
|
+
)
|
|
308
|
+
`);
|
|
309
|
+
this.db.exec(`
|
|
310
|
+
CREATE TABLE IF NOT EXISTS memory_evidence (
|
|
311
|
+
id TEXT PRIMARY KEY,
|
|
312
|
+
user_id TEXT NOT NULL,
|
|
313
|
+
record_id TEXT NOT NULL,
|
|
314
|
+
kind TEXT NOT NULL,
|
|
315
|
+
ref TEXT NOT NULL,
|
|
316
|
+
excerpt TEXT DEFAULT '',
|
|
317
|
+
observed_at TEXT NOT NULL,
|
|
318
|
+
metadata_json TEXT DEFAULT '{}'
|
|
319
|
+
)
|
|
320
|
+
`);
|
|
321
|
+
this.db.exec("CREATE INDEX IF NOT EXISTS idx_memory_evidence_record ON memory_evidence(user_id, record_id)");
|
|
322
|
+
this.db.exec(`
|
|
323
|
+
CREATE TABLE IF NOT EXISTS memory_operations (
|
|
324
|
+
id TEXT PRIMARY KEY,
|
|
325
|
+
user_id TEXT NOT NULL,
|
|
326
|
+
record_id TEXT DEFAULT NULL,
|
|
327
|
+
operation TEXT NOT NULL,
|
|
328
|
+
actor TEXT DEFAULT '',
|
|
329
|
+
session_key TEXT DEFAULT '',
|
|
330
|
+
reason TEXT DEFAULT '',
|
|
331
|
+
created_at TEXT NOT NULL,
|
|
332
|
+
metadata_json TEXT DEFAULT '{}'
|
|
333
|
+
)
|
|
334
|
+
`);
|
|
335
|
+
this.db.exec("CREATE INDEX IF NOT EXISTS idx_memory_operations_user_time ON memory_operations(user_id, created_at DESC, id ASC)");
|
|
336
|
+
this.db.exec(`
|
|
337
|
+
CREATE TABLE IF NOT EXISTS memory_file_index (
|
|
338
|
+
id TEXT PRIMARY KEY,
|
|
339
|
+
user_id TEXT NOT NULL,
|
|
340
|
+
record_id TEXT NOT NULL,
|
|
341
|
+
file_path TEXT NOT NULL,
|
|
342
|
+
symbol TEXT DEFAULT '',
|
|
343
|
+
created_time TEXT NOT NULL
|
|
344
|
+
)
|
|
345
|
+
`);
|
|
346
|
+
this.db.exec("CREATE INDEX IF NOT EXISTS idx_memory_file_index_path ON memory_file_index(user_id, file_path, created_time DESC)");
|
|
347
|
+
this.db.exec("CREATE INDEX IF NOT EXISTS idx_memory_file_index_record ON memory_file_index(user_id, record_id)");
|
|
348
|
+
this.db.exec("CREATE INDEX IF NOT EXISTS idx_cognitive_user_type ON cognitive_records(user_id, type)");
|
|
349
|
+
this.db.exec("CREATE INDEX IF NOT EXISTS idx_cognitive_user_session ON cognitive_records(user_id, session_key)");
|
|
350
|
+
this.stmtCognitiveUpsertMeta = this.db.prepare(`
|
|
351
|
+
INSERT INTO cognitive_records (
|
|
352
|
+
record_id, user_id, session_key, session_id, content, type, priority, scene_name, skill_tag,
|
|
353
|
+
half_life_days, superseded_by, invalid_at, timestamp_str, timestamp_start, timestamp_end,
|
|
354
|
+
created_time, updated_time, metadata_json, confidence, status, source_kind, verification_status,
|
|
355
|
+
repo_paths_json, file_paths_json, commands_json
|
|
356
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
357
|
+
ON CONFLICT(record_id) DO UPDATE SET
|
|
358
|
+
content=excluded.content,
|
|
359
|
+
type=excluded.type,
|
|
360
|
+
priority=excluded.priority,
|
|
361
|
+
scene_name=excluded.scene_name,
|
|
362
|
+
skill_tag=excluded.skill_tag,
|
|
363
|
+
half_life_days=excluded.half_life_days,
|
|
364
|
+
superseded_by=excluded.superseded_by,
|
|
365
|
+
invalid_at=excluded.invalid_at,
|
|
366
|
+
timestamp_str=excluded.timestamp_str,
|
|
367
|
+
timestamp_start=excluded.timestamp_start,
|
|
368
|
+
timestamp_end=excluded.timestamp_end,
|
|
369
|
+
updated_time=excluded.updated_time,
|
|
370
|
+
metadata_json=excluded.metadata_json,
|
|
371
|
+
confidence=excluded.confidence,
|
|
372
|
+
status=excluded.status,
|
|
373
|
+
source_kind=excluded.source_kind,
|
|
374
|
+
verification_status=excluded.verification_status,
|
|
375
|
+
repo_paths_json=excluded.repo_paths_json,
|
|
376
|
+
file_paths_json=excluded.file_paths_json,
|
|
377
|
+
commands_json=excluded.commands_json
|
|
378
|
+
`);
|
|
379
|
+
this.stmtCognitiveGetMeta = this.db.prepare(`
|
|
380
|
+
SELECT * FROM cognitive_records WHERE record_id = ? AND user_id = ?
|
|
381
|
+
`);
|
|
382
|
+
// ── Cognitive FTS5 Schema ──
|
|
383
|
+
this.db.exec(`
|
|
384
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS cognitive_fts USING fts5(
|
|
385
|
+
content,
|
|
386
|
+
content_original UNINDEXED,
|
|
387
|
+
record_id UNINDEXED,
|
|
388
|
+
user_id UNINDEXED,
|
|
389
|
+
type UNINDEXED,
|
|
390
|
+
priority UNINDEXED,
|
|
391
|
+
scene_name UNINDEXED,
|
|
392
|
+
skill_tag UNINDEXED,
|
|
393
|
+
session_key UNINDEXED,
|
|
394
|
+
timestamp_str UNINDEXED,
|
|
395
|
+
created_time UNINDEXED
|
|
396
|
+
)
|
|
397
|
+
`);
|
|
398
|
+
this.stmtCognitiveFtsInsert = this.db.prepare(`
|
|
399
|
+
INSERT INTO cognitive_fts (
|
|
400
|
+
content, content_original, record_id, user_id, type, priority, scene_name,
|
|
401
|
+
skill_tag, session_key, timestamp_str, created_time
|
|
402
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
403
|
+
`);
|
|
404
|
+
this.stmtCognitiveFtsSearch = this.db.prepare(`
|
|
405
|
+
SELECT
|
|
406
|
+
f.record_id, f.user_id, f.content_original as content, f.type, f.priority, f.scene_name,
|
|
407
|
+
f.skill_tag, f.session_key, f.timestamp_str, f.created_time,
|
|
408
|
+
f.rank, r.citation_count
|
|
409
|
+
FROM cognitive_fts f
|
|
410
|
+
JOIN cognitive_records r ON f.record_id = r.record_id
|
|
411
|
+
WHERE f.user_id = ? AND cognitive_fts MATCH ? AND r.invalid_at IS NULL AND r.archived = 0
|
|
412
|
+
ORDER BY rank
|
|
413
|
+
LIMIT ?
|
|
414
|
+
`);
|
|
415
|
+
this.db.exec(`
|
|
416
|
+
CREATE TABLE IF NOT EXISTS contradictions (
|
|
417
|
+
id TEXT PRIMARY KEY,
|
|
418
|
+
user_id TEXT NOT NULL,
|
|
419
|
+
record_id_a TEXT NOT NULL,
|
|
420
|
+
record_id_b TEXT NOT NULL,
|
|
421
|
+
reason TEXT,
|
|
422
|
+
confidence REAL,
|
|
423
|
+
status TEXT DEFAULT 'pending', -- pending, resolved, dismissed
|
|
424
|
+
created_time TEXT DEFAULT ''
|
|
425
|
+
)
|
|
426
|
+
`);
|
|
427
|
+
this.db.exec("CREATE INDEX IF NOT EXISTS idx_contradictions_user ON contradictions(user_id, status)");
|
|
428
|
+
// ── Skill Extraction Hints Schema ──
|
|
429
|
+
this.db.exec(`
|
|
430
|
+
CREATE TABLE IF NOT EXISTS skill_extraction_hints (
|
|
431
|
+
skill_name TEXT PRIMARY KEY,
|
|
432
|
+
hints TEXT NOT NULL,
|
|
433
|
+
source_file TEXT DEFAULT '',
|
|
434
|
+
registered_at TEXT DEFAULT ''
|
|
435
|
+
)
|
|
436
|
+
`);
|
|
437
|
+
this.db.exec(`
|
|
438
|
+
CREATE TABLE IF NOT EXISTS skill_activations (
|
|
439
|
+
user_id TEXT NOT NULL,
|
|
440
|
+
skill_name TEXT NOT NULL,
|
|
441
|
+
potential REAL DEFAULT 0.0,
|
|
442
|
+
last_decay_time TEXT NOT NULL,
|
|
443
|
+
PRIMARY KEY (user_id, skill_name)
|
|
444
|
+
)
|
|
445
|
+
`);
|
|
446
|
+
// ── Contextual Focus Scenes ──
|
|
447
|
+
this.db.exec(`
|
|
448
|
+
CREATE TABLE IF NOT EXISTS contextual_focus (
|
|
449
|
+
id TEXT PRIMARY KEY,
|
|
450
|
+
user_id TEXT NOT NULL,
|
|
451
|
+
scene_name TEXT NOT NULL,
|
|
452
|
+
summary_md TEXT NOT NULL,
|
|
453
|
+
heat_score REAL DEFAULT 100.0,
|
|
454
|
+
last_active_time TEXT DEFAULT '',
|
|
455
|
+
created_time TEXT DEFAULT '',
|
|
456
|
+
updated_time TEXT DEFAULT '',
|
|
457
|
+
UNIQUE(user_id, scene_name)
|
|
458
|
+
)
|
|
459
|
+
`);
|
|
460
|
+
this.db.exec("CREATE INDEX IF NOT EXISTS idx_focus_user_heat ON contextual_focus(user_id, heat_score DESC)");
|
|
461
|
+
// ── Core Identity ──
|
|
462
|
+
this.db.exec(`
|
|
463
|
+
CREATE TABLE IF NOT EXISTS core_identity (
|
|
464
|
+
user_id TEXT PRIMARY KEY,
|
|
465
|
+
persona_md TEXT NOT NULL,
|
|
466
|
+
cognitive_count_at_generation INTEGER DEFAULT 0,
|
|
467
|
+
created_time TEXT DEFAULT '',
|
|
468
|
+
updated_time TEXT DEFAULT ''
|
|
469
|
+
)
|
|
470
|
+
`);
|
|
471
|
+
// ── Scheduler State ──
|
|
472
|
+
this.db.exec(`
|
|
473
|
+
CREATE TABLE IF NOT EXISTS scheduler_state (
|
|
474
|
+
user_id TEXT PRIMARY KEY,
|
|
475
|
+
cognitive_count_since_last_focus INTEGER DEFAULT 0,
|
|
476
|
+
cognitive_count_since_last_identity INTEGER DEFAULT 0,
|
|
477
|
+
total_cognitive_count INTEGER DEFAULT 0,
|
|
478
|
+
extraction_errors INTEGER DEFAULT 0,
|
|
479
|
+
last_error_message TEXT DEFAULT NULL,
|
|
480
|
+
last_error_at TEXT DEFAULT NULL
|
|
481
|
+
)
|
|
482
|
+
`);
|
|
483
|
+
// ── GraphRAG Nodes ──
|
|
484
|
+
this.db.exec(`
|
|
485
|
+
CREATE TABLE IF NOT EXISTS graph_nodes (
|
|
486
|
+
id TEXT PRIMARY KEY,
|
|
487
|
+
user_id TEXT NOT NULL,
|
|
488
|
+
entity TEXT NOT NULL,
|
|
489
|
+
entity_type TEXT NOT NULL,
|
|
490
|
+
skill_tag TEXT DEFAULT '',
|
|
491
|
+
confidence REAL DEFAULT 1.0,
|
|
492
|
+
source_record_id TEXT,
|
|
493
|
+
created_time TEXT DEFAULT '',
|
|
494
|
+
UNIQUE(user_id, entity)
|
|
495
|
+
)
|
|
496
|
+
`);
|
|
497
|
+
this.db.exec("CREATE INDEX IF NOT EXISTS idx_graph_nodes_user ON graph_nodes(user_id)");
|
|
498
|
+
// ── GraphRAG Edges ──
|
|
499
|
+
this.db.exec(`
|
|
500
|
+
CREATE TABLE IF NOT EXISTS graph_edges (
|
|
501
|
+
id TEXT PRIMARY KEY,
|
|
502
|
+
user_id TEXT NOT NULL,
|
|
503
|
+
from_node_id TEXT NOT NULL,
|
|
504
|
+
to_node_id TEXT NOT NULL,
|
|
505
|
+
relation TEXT NOT NULL,
|
|
506
|
+
skill_tag TEXT DEFAULT '',
|
|
507
|
+
confidence REAL DEFAULT 1.0,
|
|
508
|
+
source_record_id TEXT,
|
|
509
|
+
created_time TEXT DEFAULT '',
|
|
510
|
+
UNIQUE(user_id, from_node_id, to_node_id, relation)
|
|
511
|
+
)
|
|
512
|
+
`);
|
|
513
|
+
this.db.exec("CREATE INDEX IF NOT EXISTS idx_graph_edges_from ON graph_edges(from_node_id)");
|
|
514
|
+
this.db.exec("CREATE INDEX IF NOT EXISTS idx_graph_edges_to ON graph_edges(to_node_id)");
|
|
515
|
+
// ── Dendritic Spines (Cognitive Connections) ──
|
|
516
|
+
this.db.exec(`
|
|
517
|
+
CREATE TABLE IF NOT EXISTS cognitive_connections (
|
|
518
|
+
user_id TEXT NOT NULL,
|
|
519
|
+
source_id TEXT NOT NULL,
|
|
520
|
+
target_id TEXT NOT NULL,
|
|
521
|
+
weight REAL DEFAULT 0.5,
|
|
522
|
+
last_activated_at TEXT DEFAULT '',
|
|
523
|
+
PRIMARY KEY (user_id, source_id, target_id),
|
|
524
|
+
FOREIGN KEY (source_id) REFERENCES cognitive_records(record_id) ON DELETE CASCADE,
|
|
525
|
+
FOREIGN KEY (target_id) REFERENCES cognitive_records(record_id) ON DELETE CASCADE
|
|
526
|
+
)
|
|
527
|
+
`);
|
|
528
|
+
this.db.exec("CREATE INDEX IF NOT EXISTS idx_cognitive_conn_user_src ON cognitive_connections(user_id, source_id)");
|
|
529
|
+
this.db.exec("CREATE INDEX IF NOT EXISTS idx_cognitive_conn_user_tgt ON cognitive_connections(user_id, target_id)");
|
|
530
|
+
}
|
|
531
|
+
// ============================
|
|
532
|
+
// Sensory Stream Methods
|
|
533
|
+
// ============================
|
|
534
|
+
upsertSensory(record) {
|
|
535
|
+
this.db.exec("BEGIN");
|
|
536
|
+
try {
|
|
537
|
+
this.stmtSensoryUpsertMeta.run(record.id, record.userId, record.sessionKey, record.sessionId, record.role, record.messageText, record.recordedAt, record.timestamp, record.skillTag);
|
|
538
|
+
this.db.exec("COMMIT");
|
|
539
|
+
}
|
|
540
|
+
catch (e) {
|
|
541
|
+
this.db.exec("ROLLBACK");
|
|
542
|
+
throw e;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
getRecentSensoryMessages(userId, sessionKey, limit, afterIsoTime = "") {
|
|
546
|
+
const rows = this.stmtSensoryQueryAfter.all(userId, sessionKey, afterIsoTime, limit);
|
|
547
|
+
// Reverse so chronologically they are oldest first
|
|
548
|
+
return rows.reverse().map(r => ({
|
|
549
|
+
id: r.id,
|
|
550
|
+
userId: r.userId,
|
|
551
|
+
sessionKey: r.sessionKey,
|
|
552
|
+
sessionId: r.sessionId,
|
|
553
|
+
role: r.role,
|
|
554
|
+
messageText: r.messageText,
|
|
555
|
+
recordedAt: r.recordedAt,
|
|
556
|
+
timestamp: r.timestamp,
|
|
557
|
+
skillTag: r.skillTag
|
|
558
|
+
}));
|
|
559
|
+
}
|
|
560
|
+
getUnextractedSensoryCount(userId, sessionKey) {
|
|
561
|
+
const stmtCount = this.db.prepare("SELECT COUNT(*) as count FROM sensory_stream WHERE user_id = ? AND session_key = ? AND extracted_at IS NULL");
|
|
562
|
+
const row = stmtCount.get(userId, sessionKey);
|
|
563
|
+
return row?.count || 0;
|
|
564
|
+
}
|
|
565
|
+
markSensoryExtracted(userId, sessionKey, recordIds, extractedAt = new Date().toISOString()) {
|
|
566
|
+
if (recordIds.length === 0)
|
|
567
|
+
return;
|
|
568
|
+
this.db.exec("BEGIN");
|
|
569
|
+
try {
|
|
570
|
+
const stmt = this.db.prepare("UPDATE sensory_stream SET extracted_at = ? WHERE user_id = ? AND session_key = ? AND record_id = ?");
|
|
571
|
+
for (const recordId of recordIds) {
|
|
572
|
+
stmt.run(extractedAt, userId, sessionKey, recordId);
|
|
573
|
+
}
|
|
574
|
+
this.db.exec("COMMIT");
|
|
575
|
+
}
|
|
576
|
+
catch (e) {
|
|
577
|
+
this.db.exec("ROLLBACK");
|
|
578
|
+
throw e;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
// ============================
|
|
582
|
+
// Cognitive Methods
|
|
583
|
+
// ============================
|
|
584
|
+
upsertCognitiveBatch(entries, options) {
|
|
585
|
+
this.db.exec("BEGIN");
|
|
586
|
+
try {
|
|
587
|
+
const deleteFts = this.db.prepare("DELETE FROM cognitive_fts WHERE record_id = ? AND user_id = ?");
|
|
588
|
+
for (const entry of entries) {
|
|
589
|
+
const record = entry.record;
|
|
590
|
+
this.stmtCognitiveUpsertMeta.run(record.id, record.userId, record.sessionKey, record.sessionId, record.content, record.type, record.priority, record.sceneName, record.skillTag, record.halfLifeDays, record.supersededBy, record.invalidAt || null, record.timestampStr, record.timestampStart, record.timestampEnd, record.createdTime, record.updatedTime, JSON.stringify(record.metadata), record.confidence ?? 0.65, record.status ?? "active", record.sourceKind ?? "", record.verificationStatus ?? "", JSON.stringify(record.repoPaths ?? []), JSON.stringify(record.filePaths ?? []), JSON.stringify(record.commands ?? []));
|
|
591
|
+
// FTS5 Insert
|
|
592
|
+
deleteFts.run(record.id, record.userId);
|
|
593
|
+
this.stmtCognitiveFtsInsert.run(record.content, record.content, record.id, record.userId, record.type, record.priority, record.sceneName, record.skillTag, record.sessionKey, record.timestampStr, record.createdTime);
|
|
594
|
+
// Vector Insert
|
|
595
|
+
if (entry.embedding && this.vecLoaded && this.stmtCognitiveVecInsert && this.stmtCognitiveVecDelete) {
|
|
596
|
+
this.stmtCognitiveVecDelete.run(record.id);
|
|
597
|
+
this.stmtCognitiveVecInsert.run(record.id, entry.embedding);
|
|
598
|
+
}
|
|
599
|
+
this.replaceFileIndex(record);
|
|
600
|
+
if (!options?.skipAudit) {
|
|
601
|
+
this.insertOperation({
|
|
602
|
+
id: randomUUID(),
|
|
603
|
+
userId: record.userId,
|
|
604
|
+
recordId: record.id,
|
|
605
|
+
operation: "cognitive_upsert",
|
|
606
|
+
actor: "system",
|
|
607
|
+
sessionKey: record.sessionKey,
|
|
608
|
+
reason: "",
|
|
609
|
+
createdAt: new Date().toISOString(),
|
|
610
|
+
metadata: { batch: true, type: record.type },
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
this.db.exec("COMMIT");
|
|
615
|
+
}
|
|
616
|
+
catch (e) {
|
|
617
|
+
this.db.exec("ROLLBACK");
|
|
618
|
+
throw e;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
upsertCognitive(record, options) {
|
|
622
|
+
this.db.exec("BEGIN");
|
|
623
|
+
try {
|
|
624
|
+
this.stmtCognitiveUpsertMeta.run(record.id, record.userId, record.sessionKey, record.sessionId, record.content, record.type, record.priority, record.sceneName, record.skillTag, record.halfLifeDays, record.supersededBy, record.invalidAt || null, record.timestampStr, record.timestampStart, record.timestampEnd, record.createdTime, record.updatedTime, JSON.stringify(record.metadata), record.confidence ?? 0.65, record.status ?? "active", record.sourceKind ?? "", record.verificationStatus ?? "", JSON.stringify(record.repoPaths ?? []), JSON.stringify(record.filePaths ?? []), JSON.stringify(record.commands ?? []));
|
|
625
|
+
// FTS5 Insert (delete old first if it exists to emulate UPSERT)
|
|
626
|
+
const deleteFts = this.db.prepare("DELETE FROM cognitive_fts WHERE record_id = ? AND user_id = ?");
|
|
627
|
+
deleteFts.run(record.id, record.userId);
|
|
628
|
+
this.stmtCognitiveFtsInsert.run(record.content, record.content, record.id, record.userId, record.type, record.priority, record.sceneName, record.skillTag, record.sessionKey, record.timestampStr, record.createdTime);
|
|
629
|
+
this.replaceFileIndex(record);
|
|
630
|
+
if (!options?.skipAudit) {
|
|
631
|
+
this.insertOperation({
|
|
632
|
+
id: randomUUID(),
|
|
633
|
+
userId: record.userId,
|
|
634
|
+
recordId: record.id,
|
|
635
|
+
operation: "cognitive_upsert",
|
|
636
|
+
actor: "system",
|
|
637
|
+
sessionKey: record.sessionKey,
|
|
638
|
+
reason: "",
|
|
639
|
+
createdAt: new Date().toISOString(),
|
|
640
|
+
metadata: { type: record.type },
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
this.db.exec("COMMIT");
|
|
644
|
+
}
|
|
645
|
+
catch (e) {
|
|
646
|
+
this.db.exec("ROLLBACK");
|
|
647
|
+
throw e;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
invalidateCognitiveRecord(userId, recordId, supersededById) {
|
|
651
|
+
const stmt = this.db.prepare("UPDATE cognitive_records SET invalid_at = ?, superseded_by = ?, status = 'superseded' WHERE user_id = ? AND record_id = ?");
|
|
652
|
+
const now = new Date().toISOString();
|
|
653
|
+
stmt.run(now, supersededById, userId, recordId);
|
|
654
|
+
this.insertOperation({
|
|
655
|
+
id: randomUUID(),
|
|
656
|
+
userId,
|
|
657
|
+
recordId,
|
|
658
|
+
operation: "cognitive_supersede",
|
|
659
|
+
actor: "system",
|
|
660
|
+
sessionKey: "",
|
|
661
|
+
reason: `Superseded by ${supersededById}`,
|
|
662
|
+
createdAt: now,
|
|
663
|
+
metadata: { supersededById },
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
getMemoryById(userId, recordId) {
|
|
667
|
+
const row = this.stmtCognitiveGetMeta.get(recordId, userId);
|
|
668
|
+
return row ? cognitiveRowToRecord(row) : null;
|
|
669
|
+
}
|
|
670
|
+
getMemoriesByFilePath(userId, filePath, limit) {
|
|
671
|
+
const rows = this.db.prepare(`
|
|
672
|
+
SELECT r.*
|
|
673
|
+
FROM memory_file_index i
|
|
674
|
+
JOIN cognitive_records r ON r.user_id = i.user_id AND r.record_id = i.record_id
|
|
675
|
+
WHERE i.user_id = ?
|
|
676
|
+
AND (i.file_path = ? OR i.file_path LIKE ?)
|
|
677
|
+
AND r.invalid_at IS NULL
|
|
678
|
+
AND r.archived = 0
|
|
679
|
+
ORDER BY i.created_time DESC, r.priority DESC
|
|
680
|
+
LIMIT ?
|
|
681
|
+
`).all(userId, filePath, `%${filePath}%`, limit);
|
|
682
|
+
return rows.map(cognitiveRowToRecord);
|
|
683
|
+
}
|
|
684
|
+
updateCognitiveConfidence(userId, recordId, confidence, status) {
|
|
685
|
+
const now = new Date().toISOString();
|
|
686
|
+
this.db.prepare("UPDATE cognitive_records SET confidence = ?, status = ?, archived = CASE WHEN ? = 'archived' THEN 1 ELSE archived END, updated_time = ? WHERE user_id = ? AND record_id = ?").run(confidence, status, status, now, userId, recordId);
|
|
687
|
+
this.insertOperation({
|
|
688
|
+
id: randomUUID(),
|
|
689
|
+
userId,
|
|
690
|
+
recordId,
|
|
691
|
+
operation: "cognitive_status_update",
|
|
692
|
+
actor: "system",
|
|
693
|
+
sessionKey: "",
|
|
694
|
+
reason: "",
|
|
695
|
+
createdAt: now,
|
|
696
|
+
metadata: { confidence, status },
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
insertEvidence(ev) {
|
|
700
|
+
this.db.prepare(`
|
|
701
|
+
INSERT INTO memory_evidence (id, user_id, record_id, kind, ref, excerpt, observed_at, metadata_json)
|
|
702
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
703
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
704
|
+
kind=excluded.kind,
|
|
705
|
+
ref=excluded.ref,
|
|
706
|
+
excerpt=excluded.excerpt,
|
|
707
|
+
observed_at=excluded.observed_at,
|
|
708
|
+
metadata_json=excluded.metadata_json
|
|
709
|
+
`).run(ev.id, ev.userId, ev.recordId, ev.kind, ev.ref, ev.excerpt, ev.observedAt, JSON.stringify(ev.metadata ?? {}));
|
|
710
|
+
this.insertOperation({
|
|
711
|
+
id: randomUUID(),
|
|
712
|
+
userId: ev.userId,
|
|
713
|
+
recordId: ev.recordId,
|
|
714
|
+
operation: "evidence_add",
|
|
715
|
+
actor: "system",
|
|
716
|
+
sessionKey: "",
|
|
717
|
+
reason: "",
|
|
718
|
+
createdAt: new Date().toISOString(),
|
|
719
|
+
metadata: { evidenceId: ev.id, kind: ev.kind, ref: ev.ref },
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
getEvidenceByRecord(userId, recordId) {
|
|
723
|
+
const rows = this.db.prepare(`
|
|
724
|
+
SELECT id, user_id, record_id, kind, ref, excerpt, observed_at, metadata_json
|
|
725
|
+
FROM memory_evidence
|
|
726
|
+
WHERE user_id = ? AND record_id = ?
|
|
727
|
+
ORDER BY observed_at DESC, id ASC
|
|
728
|
+
`).all(userId, recordId);
|
|
729
|
+
return rows.map(evidenceRowToRecord);
|
|
730
|
+
}
|
|
731
|
+
listEvidence(userId, filters, pagination) {
|
|
732
|
+
const where = ["user_id = ?"];
|
|
733
|
+
const args = [userId];
|
|
734
|
+
if (filters?.recordId) {
|
|
735
|
+
where.push("record_id = ?");
|
|
736
|
+
args.push(filters.recordId);
|
|
737
|
+
}
|
|
738
|
+
if (filters?.kind) {
|
|
739
|
+
where.push("kind = ?");
|
|
740
|
+
args.push(filters.kind);
|
|
741
|
+
}
|
|
742
|
+
if (pagination?.cursor) {
|
|
743
|
+
where.push("(observed_at < ? OR (observed_at = ? AND id > ?))");
|
|
744
|
+
args.push(pagination.cursor.observedAt, pagination.cursor.observedAt, pagination.cursor.id);
|
|
745
|
+
}
|
|
746
|
+
args.push(pagination?.limit ?? 100);
|
|
747
|
+
const rows = this.db.prepare(`
|
|
748
|
+
SELECT id, user_id, record_id, kind, ref, excerpt, observed_at, metadata_json
|
|
749
|
+
FROM memory_evidence
|
|
750
|
+
WHERE ${where.join(" AND ")}
|
|
751
|
+
ORDER BY observed_at DESC, id ASC
|
|
752
|
+
LIMIT ?
|
|
753
|
+
`).all(...args);
|
|
754
|
+
return rows.map(evidenceRowToRecord);
|
|
755
|
+
}
|
|
756
|
+
insertOperation(op) {
|
|
757
|
+
this.db.prepare(`
|
|
758
|
+
INSERT INTO memory_operations (id, user_id, record_id, operation, actor, session_key, reason, created_at, metadata_json)
|
|
759
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
760
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
761
|
+
operation=excluded.operation,
|
|
762
|
+
actor=excluded.actor,
|
|
763
|
+
session_key=excluded.session_key,
|
|
764
|
+
reason=excluded.reason,
|
|
765
|
+
metadata_json=excluded.metadata_json
|
|
766
|
+
`).run(op.id, op.userId, op.recordId, op.operation, op.actor, op.sessionKey, op.reason, op.createdAt, JSON.stringify(op.metadata ?? {}));
|
|
767
|
+
}
|
|
768
|
+
getOperationLog(userId, options, filters) {
|
|
769
|
+
const where = ["user_id = ?"];
|
|
770
|
+
const args = [userId];
|
|
771
|
+
if (filters?.operation) {
|
|
772
|
+
where.push("operation = ?");
|
|
773
|
+
args.push(filters.operation);
|
|
774
|
+
}
|
|
775
|
+
if (filters?.sessionKey) {
|
|
776
|
+
where.push("session_key = ?");
|
|
777
|
+
args.push(filters.sessionKey);
|
|
778
|
+
}
|
|
779
|
+
if (filters?.createdAfter) {
|
|
780
|
+
where.push("created_at >= ?");
|
|
781
|
+
args.push(filters.createdAfter);
|
|
782
|
+
}
|
|
783
|
+
if (filters?.createdBefore) {
|
|
784
|
+
where.push("created_at <= ?");
|
|
785
|
+
args.push(filters.createdBefore);
|
|
786
|
+
}
|
|
787
|
+
if (options?.cursor) {
|
|
788
|
+
where.push("(created_at < ? OR (created_at = ? AND id > ?))");
|
|
789
|
+
args.push(options.cursor.createdAt, options.cursor.createdAt, options.cursor.id);
|
|
790
|
+
}
|
|
791
|
+
args.push(options?.limit ?? 100);
|
|
792
|
+
const rows = this.db.prepare(`
|
|
793
|
+
SELECT id, user_id, record_id, operation, actor, session_key, reason, created_at, metadata_json
|
|
794
|
+
FROM memory_operations
|
|
795
|
+
WHERE ${where.join(" AND ")}
|
|
796
|
+
ORDER BY created_at DESC, id ASC
|
|
797
|
+
LIMIT ?
|
|
798
|
+
`).all(...args);
|
|
799
|
+
return rows.map(operationRowToRecord);
|
|
800
|
+
}
|
|
801
|
+
exportMemories(userId) {
|
|
802
|
+
const memoryRows = this.db.prepare("SELECT * FROM cognitive_records WHERE user_id = ? ORDER BY created_time ASC, record_id ASC").all(userId);
|
|
803
|
+
const evidenceRows = this.db.prepare("SELECT * FROM memory_evidence WHERE user_id = ? ORDER BY observed_at ASC, id ASC").all(userId);
|
|
804
|
+
const operationRows = this.db.prepare("SELECT * FROM memory_operations WHERE user_id = ? ORDER BY created_at ASC, id ASC").all(userId);
|
|
805
|
+
return {
|
|
806
|
+
version: 1,
|
|
807
|
+
exportedAt: new Date().toISOString(),
|
|
808
|
+
userId,
|
|
809
|
+
memories: memoryRows.map(cognitiveRowToRecord),
|
|
810
|
+
evidence: evidenceRows.map(evidenceRowToRecord),
|
|
811
|
+
operations: operationRows.map(operationRowToRecord),
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
importMemories(userId, data) {
|
|
815
|
+
let importedMemories = 0;
|
|
816
|
+
let importedEvidence = 0;
|
|
817
|
+
let importedOperations = 0;
|
|
818
|
+
this.db.exec("BEGIN");
|
|
819
|
+
try {
|
|
820
|
+
for (const record of data.memories ?? []) {
|
|
821
|
+
this.stmtCognitiveUpsertMeta.run(record.id, userId, record.sessionKey, record.sessionId, record.content, record.type, record.priority, record.sceneName, record.skillTag, record.halfLifeDays, record.supersededBy, record.invalidAt || null, record.timestampStr, record.timestampStart, record.timestampEnd, record.createdTime, record.updatedTime, JSON.stringify(record.metadata ?? {}), record.confidence ?? 0.65, record.status ?? "active", record.sourceKind ?? "", record.verificationStatus ?? "", JSON.stringify(record.repoPaths ?? []), JSON.stringify(record.filePaths ?? []), JSON.stringify(record.commands ?? []));
|
|
822
|
+
this.db.prepare("DELETE FROM cognitive_fts WHERE record_id = ? AND user_id = ?").run(record.id, userId);
|
|
823
|
+
this.stmtCognitiveFtsInsert.run(record.content, record.content, record.id, userId, record.type, record.priority, record.sceneName, record.skillTag, record.sessionKey, record.timestampStr, record.createdTime);
|
|
824
|
+
this.replaceFileIndex({ ...record, userId });
|
|
825
|
+
importedMemories++;
|
|
826
|
+
}
|
|
827
|
+
for (const ev of data.evidence ?? []) {
|
|
828
|
+
this.db.prepare(`
|
|
829
|
+
INSERT INTO memory_evidence (id, user_id, record_id, kind, ref, excerpt, observed_at, metadata_json)
|
|
830
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
831
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
832
|
+
kind=excluded.kind,
|
|
833
|
+
ref=excluded.ref,
|
|
834
|
+
excerpt=excluded.excerpt,
|
|
835
|
+
observed_at=excluded.observed_at,
|
|
836
|
+
metadata_json=excluded.metadata_json
|
|
837
|
+
`).run(ev.id, userId, ev.recordId, ev.kind, ev.ref, ev.excerpt, ev.observedAt, JSON.stringify(ev.metadata ?? {}));
|
|
838
|
+
importedEvidence++;
|
|
839
|
+
}
|
|
840
|
+
for (const op of data.operations ?? []) {
|
|
841
|
+
this.db.prepare(`
|
|
842
|
+
INSERT INTO memory_operations (id, user_id, record_id, operation, actor, session_key, reason, created_at, metadata_json)
|
|
843
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
844
|
+
ON CONFLICT(id) DO NOTHING
|
|
845
|
+
`).run(op.id, userId, op.recordId, op.operation, op.actor, op.sessionKey, op.reason, op.createdAt, JSON.stringify(op.metadata ?? {}));
|
|
846
|
+
importedOperations++;
|
|
847
|
+
}
|
|
848
|
+
const now = new Date().toISOString();
|
|
849
|
+
this.db.prepare(`
|
|
850
|
+
INSERT INTO memory_operations (id, user_id, record_id, operation, actor, session_key, reason, created_at, metadata_json)
|
|
851
|
+
VALUES (?, ?, NULL, 'import', 'system', '', '', ?, ?)
|
|
852
|
+
`).run(randomUUID(), userId, now, JSON.stringify({ importedMemories, importedEvidence, importedOperations }));
|
|
853
|
+
this.db.exec("COMMIT");
|
|
854
|
+
}
|
|
855
|
+
catch (e) {
|
|
856
|
+
this.db.exec("ROLLBACK");
|
|
857
|
+
throw e;
|
|
858
|
+
}
|
|
859
|
+
return { importedMemories, importedEvidence, importedOperations };
|
|
860
|
+
}
|
|
861
|
+
hardDeleteMemory(userId, recordId, reason) {
|
|
862
|
+
const now = new Date().toISOString();
|
|
863
|
+
this.db.exec("BEGIN");
|
|
864
|
+
try {
|
|
865
|
+
this.db.prepare("DELETE FROM memory_evidence WHERE user_id = ? AND record_id = ?").run(userId, recordId);
|
|
866
|
+
this.db.prepare("DELETE FROM memory_file_index WHERE user_id = ? AND record_id = ?").run(userId, recordId);
|
|
867
|
+
this.db.prepare("DELETE FROM cognitive_fts WHERE user_id = ? AND record_id = ?").run(userId, recordId);
|
|
868
|
+
if (this.stmtCognitiveVecDelete) {
|
|
869
|
+
this.stmtCognitiveVecDelete.run(recordId);
|
|
870
|
+
}
|
|
871
|
+
this.db.prepare("DELETE FROM cognitive_records WHERE user_id = ? AND record_id = ?").run(userId, recordId);
|
|
872
|
+
this.db.prepare(`
|
|
873
|
+
INSERT INTO memory_operations (id, user_id, record_id, operation, actor, session_key, reason, created_at, metadata_json)
|
|
874
|
+
VALUES (?, ?, ?, 'governance_delete', 'system', '', ?, ?, '{}')
|
|
875
|
+
`).run(randomUUID(), userId, recordId, reason, now);
|
|
876
|
+
this.db.exec("COMMIT");
|
|
877
|
+
}
|
|
878
|
+
catch (e) {
|
|
879
|
+
this.db.exec("ROLLBACK");
|
|
880
|
+
throw e;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
searchCognitiveFts(userId, query, limit) {
|
|
884
|
+
const ftsQuery = buildFtsQuery(query);
|
|
885
|
+
if (!ftsQuery)
|
|
886
|
+
return [];
|
|
887
|
+
const rows = this.stmtCognitiveFtsSearch.all(userId, ftsQuery, limit);
|
|
888
|
+
return rows.map(r => ({
|
|
889
|
+
record_id: r.record_id,
|
|
890
|
+
user_id: r.user_id,
|
|
891
|
+
content: r.content,
|
|
892
|
+
type: r.type,
|
|
893
|
+
priority: r.priority,
|
|
894
|
+
scene_name: r.scene_name,
|
|
895
|
+
skill_tag: r.skill_tag,
|
|
896
|
+
score: bm25RankToScore(r.rank),
|
|
897
|
+
timestamp_str: r.timestamp_str,
|
|
898
|
+
timestamp_start: "",
|
|
899
|
+
timestamp_end: "",
|
|
900
|
+
session_key: r.session_key,
|
|
901
|
+
session_id: "",
|
|
902
|
+
metadata_json: "{}",
|
|
903
|
+
created_time: r.created_time,
|
|
904
|
+
citation_count: r.citation_count ?? 0
|
|
905
|
+
}));
|
|
906
|
+
}
|
|
907
|
+
searchCognitiveFtsAsOf(userId, query, limit, asOf) {
|
|
908
|
+
const ftsQuery = buildFtsQuery(query);
|
|
909
|
+
if (!ftsQuery)
|
|
910
|
+
return [];
|
|
911
|
+
const stmt = this.db.prepare(`
|
|
912
|
+
SELECT
|
|
913
|
+
f.record_id, f.user_id, f.content_original as content, f.type, f.priority, f.scene_name,
|
|
914
|
+
f.skill_tag, f.session_key, f.timestamp_str, f.created_time,
|
|
915
|
+
f.rank
|
|
916
|
+
FROM cognitive_fts f
|
|
917
|
+
JOIN cognitive_records r ON f.record_id = r.record_id
|
|
918
|
+
WHERE f.user_id = ?
|
|
919
|
+
AND cognitive_fts MATCH ?
|
|
920
|
+
AND r.created_time <= ? -- memory must have existed at asOf
|
|
921
|
+
AND (r.invalid_at IS NULL OR r.invalid_at > ?) -- must have been valid at asOf
|
|
922
|
+
AND r.archived = 0
|
|
923
|
+
ORDER BY rank
|
|
924
|
+
LIMIT ?
|
|
925
|
+
`);
|
|
926
|
+
const rows = stmt.all(userId, ftsQuery, asOf, asOf, limit);
|
|
927
|
+
return rows.map(r => ({
|
|
928
|
+
record_id: r.record_id,
|
|
929
|
+
user_id: r.user_id,
|
|
930
|
+
content: r.content,
|
|
931
|
+
type: r.type,
|
|
932
|
+
priority: r.priority,
|
|
933
|
+
scene_name: r.scene_name,
|
|
934
|
+
skill_tag: r.skill_tag,
|
|
935
|
+
score: bm25RankToScore(r.rank),
|
|
936
|
+
timestamp_str: r.timestamp_str,
|
|
937
|
+
timestamp_start: "",
|
|
938
|
+
timestamp_end: "",
|
|
939
|
+
session_key: r.session_key,
|
|
940
|
+
session_id: "",
|
|
941
|
+
metadata_json: "{}",
|
|
942
|
+
created_time: r.created_time
|
|
943
|
+
}));
|
|
944
|
+
}
|
|
945
|
+
upsertCognitiveVec(recordId, embedding) {
|
|
946
|
+
if (!this.vecLoaded)
|
|
947
|
+
return;
|
|
948
|
+
if (this.vecDimensions !== embedding.length) {
|
|
949
|
+
this.initVec(embedding.length);
|
|
950
|
+
}
|
|
951
|
+
if (!this.stmtCognitiveVecInsert || !this.stmtCognitiveVecDelete)
|
|
952
|
+
return;
|
|
953
|
+
this.db.exec("BEGIN");
|
|
954
|
+
try {
|
|
955
|
+
this.stmtCognitiveVecDelete.run(recordId);
|
|
956
|
+
this.stmtCognitiveVecInsert.run(recordId, embedding);
|
|
957
|
+
this.db.exec("COMMIT");
|
|
958
|
+
}
|
|
959
|
+
catch (e) {
|
|
960
|
+
this.db.exec("ROLLBACK");
|
|
961
|
+
throw e;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
searchCognitiveVec(userId, queryEmbedding, limit) {
|
|
965
|
+
if (!this.vecLoaded)
|
|
966
|
+
return [];
|
|
967
|
+
if (this.vecDimensions !== queryEmbedding.length) {
|
|
968
|
+
this.initVec(queryEmbedding.length);
|
|
969
|
+
}
|
|
970
|
+
if (!this.vecDimensions)
|
|
971
|
+
return [];
|
|
972
|
+
const stmt = this.db.prepare(`
|
|
973
|
+
SELECT
|
|
974
|
+
v.record_id, v.distance,
|
|
975
|
+
r.user_id, r.content, r.type, r.priority, r.scene_name, r.skill_tag,
|
|
976
|
+
r.session_key, r.timestamp_str, r.created_time
|
|
977
|
+
FROM cognitive_vec v
|
|
978
|
+
JOIN cognitive_records r ON v.record_id = r.record_id
|
|
979
|
+
WHERE v.embedding MATCH ? AND k = ? AND r.user_id = ? AND r.invalid_at IS NULL AND r.archived = 0
|
|
980
|
+
ORDER BY distance
|
|
981
|
+
`);
|
|
982
|
+
try {
|
|
983
|
+
const rows = stmt.all(queryEmbedding, limit, userId);
|
|
984
|
+
return rows.map(r => ({
|
|
985
|
+
record_id: r.record_id,
|
|
986
|
+
user_id: r.user_id,
|
|
987
|
+
content: r.content,
|
|
988
|
+
type: r.type,
|
|
989
|
+
priority: r.priority,
|
|
990
|
+
scene_name: r.scene_name,
|
|
991
|
+
skill_tag: r.skill_tag,
|
|
992
|
+
score: 1 - r.distance,
|
|
993
|
+
timestamp_str: r.timestamp_str,
|
|
994
|
+
timestamp_start: "",
|
|
995
|
+
timestamp_end: "",
|
|
996
|
+
session_key: r.session_key,
|
|
997
|
+
session_id: "",
|
|
998
|
+
metadata_json: "{}",
|
|
999
|
+
created_time: r.created_time
|
|
1000
|
+
}));
|
|
1001
|
+
}
|
|
1002
|
+
catch (e) {
|
|
1003
|
+
console.error("[BrainRouter] Vector search failed:", e);
|
|
1004
|
+
return [];
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
upsertContradiction(data) {
|
|
1008
|
+
const stmt = this.db.prepare(`
|
|
1009
|
+
INSERT INTO contradictions (id, user_id, record_id_a, record_id_b, reason, confidence, created_time)
|
|
1010
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
1011
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
1012
|
+
reason=excluded.reason,
|
|
1013
|
+
confidence=excluded.confidence
|
|
1014
|
+
`);
|
|
1015
|
+
stmt.run(data.id, data.userId, data.recordIdA, data.recordIdB, data.reason, data.confidence, new Date().toISOString());
|
|
1016
|
+
}
|
|
1017
|
+
getPendingContradictions(userId, pagination) {
|
|
1018
|
+
const where = ["c.user_id = ?", "c.status = 'pending'"];
|
|
1019
|
+
const args = [userId];
|
|
1020
|
+
if (pagination?.cursor) {
|
|
1021
|
+
where.push("(c.confidence < ? OR (c.confidence = ? AND c.id > ?))");
|
|
1022
|
+
args.push(pagination.cursor.confidence, pagination.cursor.confidence, pagination.cursor.id);
|
|
1023
|
+
}
|
|
1024
|
+
args.push(pagination?.limit ?? 20);
|
|
1025
|
+
const stmt = this.db.prepare(`
|
|
1026
|
+
SELECT c.*, r1.content as content_a, r2.content as content_b
|
|
1027
|
+
FROM contradictions c
|
|
1028
|
+
JOIN cognitive_records r1 ON c.record_id_a = r1.record_id
|
|
1029
|
+
JOIN cognitive_records r2 ON c.record_id_b = r2.record_id
|
|
1030
|
+
WHERE ${where.join(" AND ")}
|
|
1031
|
+
ORDER BY c.confidence DESC, c.id ASC
|
|
1032
|
+
LIMIT ?
|
|
1033
|
+
`);
|
|
1034
|
+
return stmt.all(...args);
|
|
1035
|
+
}
|
|
1036
|
+
resolveContradiction(id, userId, status) {
|
|
1037
|
+
const stmt = this.db.prepare("UPDATE contradictions SET status = ? WHERE id = ? AND user_id = ?");
|
|
1038
|
+
stmt.run(status, id, userId);
|
|
1039
|
+
this.insertOperation({
|
|
1040
|
+
id: randomUUID(),
|
|
1041
|
+
userId,
|
|
1042
|
+
recordId: id,
|
|
1043
|
+
operation: "contradiction_resolve",
|
|
1044
|
+
actor: "system",
|
|
1045
|
+
sessionKey: "",
|
|
1046
|
+
reason: status,
|
|
1047
|
+
createdAt: new Date().toISOString(),
|
|
1048
|
+
metadata: { contradictionId: id, status },
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
// ============================
|
|
1052
|
+
// Skill Hints Methods
|
|
1053
|
+
// ============================
|
|
1054
|
+
upsertSkillHints(skillName, hints, sourceFile = "") {
|
|
1055
|
+
const stmt = this.db.prepare(`
|
|
1056
|
+
INSERT INTO skill_extraction_hints (skill_name, hints, source_file, registered_at)
|
|
1057
|
+
VALUES (?, ?, ?, ?)
|
|
1058
|
+
ON CONFLICT(skill_name) DO UPDATE SET
|
|
1059
|
+
hints=excluded.hints,
|
|
1060
|
+
source_file=excluded.source_file,
|
|
1061
|
+
registered_at=excluded.registered_at
|
|
1062
|
+
`);
|
|
1063
|
+
stmt.run(skillName, hints, sourceFile, new Date().toISOString());
|
|
1064
|
+
}
|
|
1065
|
+
listSkillHints() {
|
|
1066
|
+
const stmt = this.db.prepare("SELECT skill_name, hints, source_file, registered_at FROM skill_extraction_hints ORDER BY registered_at DESC");
|
|
1067
|
+
const rows = stmt.all();
|
|
1068
|
+
return rows.map(r => ({
|
|
1069
|
+
skillName: r.skill_name,
|
|
1070
|
+
hints: r.hints,
|
|
1071
|
+
sourceFile: r.source_file,
|
|
1072
|
+
registeredAt: r.registered_at
|
|
1073
|
+
}));
|
|
1074
|
+
}
|
|
1075
|
+
// ============================
|
|
1076
|
+
// Contextual Focus Methods
|
|
1077
|
+
// ============================
|
|
1078
|
+
upsertContextualFocus(record) {
|
|
1079
|
+
const stmt = this.db.prepare(`
|
|
1080
|
+
INSERT INTO contextual_focus (id, user_id, scene_name, summary_md, heat_score, last_active_time, created_time, updated_time)
|
|
1081
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
1082
|
+
ON CONFLICT(user_id, scene_name) DO UPDATE SET
|
|
1083
|
+
summary_md=excluded.summary_md,
|
|
1084
|
+
heat_score=excluded.heat_score,
|
|
1085
|
+
last_active_time=excluded.last_active_time,
|
|
1086
|
+
updated_time=excluded.updated_time
|
|
1087
|
+
`);
|
|
1088
|
+
stmt.run(record.id, record.userId, record.sceneName, record.summaryMd, record.heatScore, record.lastActiveTime, record.createdTime, record.updatedTime);
|
|
1089
|
+
}
|
|
1090
|
+
getTopContextualFocus(userId, limit = 3, cursor) {
|
|
1091
|
+
const where = ["user_id = ?"];
|
|
1092
|
+
const args = [userId];
|
|
1093
|
+
if (cursor) {
|
|
1094
|
+
where.push("(heat_score < ? OR (heat_score = ? AND id > ?))");
|
|
1095
|
+
args.push(cursor.heatScore, cursor.heatScore, cursor.id);
|
|
1096
|
+
}
|
|
1097
|
+
args.push(limit);
|
|
1098
|
+
const stmt = this.db.prepare(`SELECT id, user_id, scene_name, summary_md, heat_score, last_active_time, created_time, updated_time
|
|
1099
|
+
FROM contextual_focus
|
|
1100
|
+
WHERE ${where.join(" AND ")}
|
|
1101
|
+
ORDER BY heat_score DESC, id ASC
|
|
1102
|
+
LIMIT ?`);
|
|
1103
|
+
const rows = stmt.all(...args);
|
|
1104
|
+
return rows.map(r => ({
|
|
1105
|
+
id: r.id, userId: r.user_id, sceneName: r.scene_name,
|
|
1106
|
+
summaryMd: r.summary_md, heatScore: r.heat_score,
|
|
1107
|
+
lastActiveTime: r.last_active_time, createdTime: r.created_time, updatedTime: r.updated_time
|
|
1108
|
+
}));
|
|
1109
|
+
}
|
|
1110
|
+
decayContextualFocusHeatScores(userId, decayFactor = 0.95) {
|
|
1111
|
+
const stmt = this.db.prepare("UPDATE contextual_focus SET heat_score = heat_score * ? WHERE user_id = ?");
|
|
1112
|
+
stmt.run(decayFactor, userId);
|
|
1113
|
+
}
|
|
1114
|
+
boostContextualFocusHeatScore(userId, sceneName, boost = 20) {
|
|
1115
|
+
const stmt = this.db.prepare("UPDATE contextual_focus SET heat_score = MIN(100.0, heat_score + ?), last_active_time = ? WHERE user_id = ? AND scene_name = ?");
|
|
1116
|
+
stmt.run(boost, new Date().toISOString(), userId, sceneName);
|
|
1117
|
+
}
|
|
1118
|
+
getCognitivesByFocus(userId, sceneName, limit = 30) {
|
|
1119
|
+
const stmt = this.db.prepare("SELECT record_id, content, type, priority, skill_tag, created_time FROM cognitive_records WHERE user_id = ? AND scene_name = ? AND invalid_at IS NULL ORDER BY priority DESC LIMIT ?");
|
|
1120
|
+
return stmt.all(userId, sceneName, limit);
|
|
1121
|
+
}
|
|
1122
|
+
getContextualFocusCount(userId) {
|
|
1123
|
+
const stmt = this.db.prepare("SELECT COUNT(*) as count FROM contextual_focus WHERE user_id = ?");
|
|
1124
|
+
const row = stmt.get(userId);
|
|
1125
|
+
return row?.count || 0;
|
|
1126
|
+
}
|
|
1127
|
+
getColdContextualFocus(userId, limit) {
|
|
1128
|
+
const stmt = this.db.prepare("SELECT id, user_id, scene_name, summary_md, heat_score, last_active_time, created_time, updated_time FROM contextual_focus WHERE user_id = ? ORDER BY heat_score ASC LIMIT ?");
|
|
1129
|
+
const rows = stmt.all(userId, limit);
|
|
1130
|
+
return rows.map(r => ({
|
|
1131
|
+
id: r.id, userId: r.user_id, sceneName: r.scene_name,
|
|
1132
|
+
summaryMd: r.summary_md, heatScore: r.heat_score,
|
|
1133
|
+
lastActiveTime: r.last_active_time, createdTime: r.created_time, updatedTime: r.updated_time
|
|
1134
|
+
}));
|
|
1135
|
+
}
|
|
1136
|
+
deleteContextualFocus(userId, sceneIds) {
|
|
1137
|
+
if (sceneIds.length === 0)
|
|
1138
|
+
return;
|
|
1139
|
+
const placeholders = sceneIds.map(() => "?").join(",");
|
|
1140
|
+
const stmt = this.db.prepare(`DELETE FROM contextual_focus WHERE user_id = ? AND id IN (${placeholders})`);
|
|
1141
|
+
stmt.run(userId, ...sceneIds);
|
|
1142
|
+
}
|
|
1143
|
+
getContextualFocusByName(userId, sceneName) {
|
|
1144
|
+
const stmt = this.db.prepare("SELECT id, user_id, scene_name, summary_md, heat_score, last_active_time, created_time, updated_time FROM contextual_focus WHERE user_id = ? AND scene_name = ?");
|
|
1145
|
+
const row = stmt.get(userId, sceneName);
|
|
1146
|
+
if (!row)
|
|
1147
|
+
return null;
|
|
1148
|
+
return {
|
|
1149
|
+
id: row.id, userId: row.user_id, sceneName: row.scene_name,
|
|
1150
|
+
summaryMd: row.summary_md, heatScore: row.heat_score,
|
|
1151
|
+
lastActiveTime: row.last_active_time, createdTime: row.created_time, updatedTime: row.updated_time
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1154
|
+
getDistinctSceneNames(userId) {
|
|
1155
|
+
const stmt = this.db.prepare("SELECT DISTINCT scene_name FROM cognitive_records WHERE user_id = ? AND scene_name != ''");
|
|
1156
|
+
const rows = stmt.all(userId);
|
|
1157
|
+
return rows.map(r => r.scene_name);
|
|
1158
|
+
}
|
|
1159
|
+
renameFocusInCognitiveRecords(userId, oldName, canonicalName) {
|
|
1160
|
+
this.db.exec("BEGIN");
|
|
1161
|
+
try {
|
|
1162
|
+
const stmtUpdate = this.db.prepare("UPDATE cognitive_records SET scene_name = ?, updated_time = ? WHERE user_id = ? AND scene_name = ?");
|
|
1163
|
+
stmtUpdate.run(canonicalName, new Date().toISOString(), userId, oldName);
|
|
1164
|
+
const stmtDelete = this.db.prepare("DELETE FROM contextual_focus WHERE user_id = ? AND scene_name = ?");
|
|
1165
|
+
stmtDelete.run(userId, oldName);
|
|
1166
|
+
this.db.exec("COMMIT");
|
|
1167
|
+
}
|
|
1168
|
+
catch (e) {
|
|
1169
|
+
this.db.exec("ROLLBACK");
|
|
1170
|
+
throw e;
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
// ============================
|
|
1174
|
+
// Core Identity Methods
|
|
1175
|
+
// ============================
|
|
1176
|
+
upsertCoreIdentity(record) {
|
|
1177
|
+
const stmt = this.db.prepare(`
|
|
1178
|
+
INSERT INTO core_identity (user_id, persona_md, cognitive_count_at_generation, created_time, updated_time)
|
|
1179
|
+
VALUES (?, ?, ?, ?, ?)
|
|
1180
|
+
ON CONFLICT(user_id) DO UPDATE SET
|
|
1181
|
+
persona_md=excluded.persona_md,
|
|
1182
|
+
cognitive_count_at_generation=excluded.cognitive_count_at_generation,
|
|
1183
|
+
updated_time=excluded.updated_time
|
|
1184
|
+
`);
|
|
1185
|
+
stmt.run(record.userId, record.personaMd, record.cognitiveCountAtGeneration, record.createdTime, record.updatedTime);
|
|
1186
|
+
}
|
|
1187
|
+
getCoreIdentity(userId) {
|
|
1188
|
+
const stmt = this.db.prepare("SELECT user_id, persona_md, cognitive_count_at_generation, created_time, updated_time FROM core_identity WHERE user_id = ?");
|
|
1189
|
+
const row = stmt.get(userId);
|
|
1190
|
+
if (!row)
|
|
1191
|
+
return null;
|
|
1192
|
+
return {
|
|
1193
|
+
userId: row.user_id, personaMd: row.persona_md,
|
|
1194
|
+
cognitiveCountAtGeneration: row.cognitive_count_at_generation,
|
|
1195
|
+
createdTime: row.created_time, updatedTime: row.updated_time
|
|
1196
|
+
};
|
|
1197
|
+
}
|
|
1198
|
+
getIdentityAndInstructionCognitives(userId, limit = 100) {
|
|
1199
|
+
const stmt = this.db.prepare("SELECT record_id, content, type, priority, skill_tag, created_time FROM cognitive_records WHERE user_id = ? AND type IN ('persona','instruction') AND invalid_at IS NULL ORDER BY priority DESC, created_time DESC LIMIT ?");
|
|
1200
|
+
return stmt.all(userId, limit);
|
|
1201
|
+
}
|
|
1202
|
+
// ============================
|
|
1203
|
+
// Scheduler State Methods
|
|
1204
|
+
// ============================
|
|
1205
|
+
getSchedulerState(userId) {
|
|
1206
|
+
const stmt = this.db.prepare("SELECT cognitive_count_since_last_focus, cognitive_count_since_last_identity, total_cognitive_count, extraction_errors, last_error_message, last_error_at FROM scheduler_state WHERE user_id = ?");
|
|
1207
|
+
const row = stmt.get(userId);
|
|
1208
|
+
if (!row) {
|
|
1209
|
+
return {
|
|
1210
|
+
cognitiveCountSinceLastFocus: 0,
|
|
1211
|
+
cognitiveCountSinceLastIdentity: 0,
|
|
1212
|
+
totalCognitiveCount: 0,
|
|
1213
|
+
extractionErrors: 0,
|
|
1214
|
+
lastErrorMessage: null,
|
|
1215
|
+
lastErrorAt: null,
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
1218
|
+
return {
|
|
1219
|
+
cognitiveCountSinceLastFocus: row.cognitive_count_since_last_focus,
|
|
1220
|
+
cognitiveCountSinceLastIdentity: row.cognitive_count_since_last_identity,
|
|
1221
|
+
totalCognitiveCount: row.total_cognitive_count,
|
|
1222
|
+
extractionErrors: row.extraction_errors ?? 0,
|
|
1223
|
+
lastErrorMessage: row.last_error_message ?? null,
|
|
1224
|
+
lastErrorAt: row.last_error_at ?? null,
|
|
1225
|
+
};
|
|
1226
|
+
}
|
|
1227
|
+
incrementSchedulerCognitiveCount(userId, count) {
|
|
1228
|
+
const stmt = this.db.prepare(`
|
|
1229
|
+
INSERT INTO scheduler_state (user_id, cognitive_count_since_last_focus, cognitive_count_since_last_identity, total_cognitive_count)
|
|
1230
|
+
VALUES (?, ?, ?, ?)
|
|
1231
|
+
ON CONFLICT(user_id) DO UPDATE SET
|
|
1232
|
+
cognitive_count_since_last_focus = cognitive_count_since_last_focus + excluded.cognitive_count_since_last_focus,
|
|
1233
|
+
cognitive_count_since_last_identity = cognitive_count_since_last_identity + excluded.cognitive_count_since_last_identity,
|
|
1234
|
+
total_cognitive_count = total_cognitive_count + excluded.total_cognitive_count
|
|
1235
|
+
`);
|
|
1236
|
+
stmt.run(userId, count, count, count);
|
|
1237
|
+
}
|
|
1238
|
+
resetSchedulerFocusCount(userId) {
|
|
1239
|
+
const stmt = this.db.prepare("UPDATE scheduler_state SET cognitive_count_since_last_focus = 0 WHERE user_id = ?");
|
|
1240
|
+
stmt.run(userId);
|
|
1241
|
+
}
|
|
1242
|
+
resetSchedulerIdentityCount(userId) {
|
|
1243
|
+
const stmt = this.db.prepare("UPDATE scheduler_state SET cognitive_count_since_last_identity = 0 WHERE user_id = ?");
|
|
1244
|
+
stmt.run(userId);
|
|
1245
|
+
}
|
|
1246
|
+
recordExtractionFailure(userId, message) {
|
|
1247
|
+
const now = new Date().toISOString();
|
|
1248
|
+
const stmt = this.db.prepare(`
|
|
1249
|
+
INSERT INTO scheduler_state (user_id, extraction_errors, last_error_message, last_error_at)
|
|
1250
|
+
VALUES (?, 1, ?, ?)
|
|
1251
|
+
ON CONFLICT(user_id) DO UPDATE SET
|
|
1252
|
+
extraction_errors = COALESCE(extraction_errors, 0) + 1,
|
|
1253
|
+
last_error_message = excluded.last_error_message,
|
|
1254
|
+
last_error_at = excluded.last_error_at
|
|
1255
|
+
`);
|
|
1256
|
+
stmt.run(userId, message.slice(0, 1000), now);
|
|
1257
|
+
}
|
|
1258
|
+
resetExtractionFailures(userId) {
|
|
1259
|
+
const stmt = this.db.prepare(`
|
|
1260
|
+
INSERT INTO scheduler_state (user_id, extraction_errors, last_error_message, last_error_at)
|
|
1261
|
+
VALUES (?, 0, NULL, NULL)
|
|
1262
|
+
ON CONFLICT(user_id) DO UPDATE SET
|
|
1263
|
+
extraction_errors = 0,
|
|
1264
|
+
last_error_message = NULL,
|
|
1265
|
+
last_error_at = NULL
|
|
1266
|
+
`);
|
|
1267
|
+
stmt.run(userId);
|
|
1268
|
+
}
|
|
1269
|
+
getExtractionStatus(userId) {
|
|
1270
|
+
const state = this.getSchedulerState(userId);
|
|
1271
|
+
return {
|
|
1272
|
+
extractionErrors: state.extractionErrors,
|
|
1273
|
+
lastErrorMessage: state.lastErrorMessage,
|
|
1274
|
+
lastErrorAt: state.lastErrorAt,
|
|
1275
|
+
syncPaused: state.extractionErrors >= 5,
|
|
1276
|
+
};
|
|
1277
|
+
}
|
|
1278
|
+
sweepUnextractedBacklog(options) {
|
|
1279
|
+
const cutoff = new Date(Date.now() - options.olderThanMs).toISOString();
|
|
1280
|
+
const minUnextracted = options.minUnextracted ?? 1;
|
|
1281
|
+
const maxFailures = options.maxFailures ?? 5;
|
|
1282
|
+
const limit = options.limit ?? 20;
|
|
1283
|
+
const rows = this.db.prepare(`
|
|
1284
|
+
SELECT
|
|
1285
|
+
l0.user_id,
|
|
1286
|
+
l0.session_key,
|
|
1287
|
+
COALESCE(MAX(l0.session_id), '') AS session_id,
|
|
1288
|
+
COUNT(*) AS unextracted_count,
|
|
1289
|
+
MAX(l0.recorded_at) AS latest_recorded_at,
|
|
1290
|
+
COALESCE(ss.extraction_errors, 0) AS extraction_errors,
|
|
1291
|
+
ss.last_error_message
|
|
1292
|
+
FROM sensory_stream l0
|
|
1293
|
+
LEFT JOIN scheduler_state ss ON ss.user_id = l0.user_id
|
|
1294
|
+
WHERE l0.extracted_at IS NULL
|
|
1295
|
+
GROUP BY l0.user_id, l0.session_key
|
|
1296
|
+
HAVING
|
|
1297
|
+
COUNT(*) >= ?
|
|
1298
|
+
AND MAX(l0.recorded_at) <= ?
|
|
1299
|
+
AND COALESCE(ss.extraction_errors, 0) < ?
|
|
1300
|
+
ORDER BY MAX(l0.recorded_at) ASC
|
|
1301
|
+
LIMIT ?
|
|
1302
|
+
`).all(minUnextracted, cutoff, maxFailures, limit);
|
|
1303
|
+
return rows.map((row) => ({
|
|
1304
|
+
userId: row.user_id,
|
|
1305
|
+
sessionKey: row.session_key,
|
|
1306
|
+
sessionId: row.session_id ?? "",
|
|
1307
|
+
unextractedCount: row.unextracted_count ?? 0,
|
|
1308
|
+
latestRecordedAt: row.latest_recorded_at ?? "",
|
|
1309
|
+
extractionErrors: row.extraction_errors ?? 0,
|
|
1310
|
+
lastErrorMessage: row.last_error_message ?? null,
|
|
1311
|
+
}));
|
|
1312
|
+
}
|
|
1313
|
+
// ============================
|
|
1314
|
+
// GraphRAG Methods
|
|
1315
|
+
// ============================
|
|
1316
|
+
getAllGraphNodes(userId) {
|
|
1317
|
+
const stmt = this.db.prepare("SELECT id, user_id, entity, entity_type, skill_tag, confidence, source_record_id, created_time FROM graph_nodes WHERE user_id = ?");
|
|
1318
|
+
const rows = stmt.all(userId);
|
|
1319
|
+
return rows.map(r => ({
|
|
1320
|
+
id: r.id, userId: r.user_id, entity: r.entity,
|
|
1321
|
+
entityType: r.entity_type, skillTag: r.skill_tag,
|
|
1322
|
+
confidence: r.confidence, sourceRecordId: r.source_record_id,
|
|
1323
|
+
createdTime: r.created_time
|
|
1324
|
+
}));
|
|
1325
|
+
}
|
|
1326
|
+
upsertGraphNode(node) {
|
|
1327
|
+
const stmt = this.db.prepare(`
|
|
1328
|
+
INSERT INTO graph_nodes (id, user_id, entity, entity_type, skill_tag, confidence, source_record_id, created_time)
|
|
1329
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
1330
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
1331
|
+
entity_type=excluded.entity_type,
|
|
1332
|
+
skill_tag=excluded.skill_tag,
|
|
1333
|
+
confidence=excluded.confidence,
|
|
1334
|
+
source_record_id=excluded.source_record_id
|
|
1335
|
+
`);
|
|
1336
|
+
stmt.run(node.id, node.userId, node.entity, node.entityType, node.skillTag || "", node.confidence, node.sourceRecordId, node.createdTime);
|
|
1337
|
+
}
|
|
1338
|
+
upsertGraphEdge(edge) {
|
|
1339
|
+
const stmt = this.db.prepare(`
|
|
1340
|
+
INSERT INTO graph_edges (id, user_id, from_node_id, to_node_id, relation, skill_tag, confidence, source_record_id, created_time)
|
|
1341
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1342
|
+
ON CONFLICT(user_id, from_node_id, to_node_id, relation) DO UPDATE SET
|
|
1343
|
+
skill_tag=excluded.skill_tag,
|
|
1344
|
+
confidence=excluded.confidence,
|
|
1345
|
+
source_record_id=excluded.source_record_id,
|
|
1346
|
+
created_time=excluded.created_time
|
|
1347
|
+
`);
|
|
1348
|
+
stmt.run(edge.id, edge.userId, edge.fromNodeId, edge.toNodeId, edge.relation, edge.skillTag || "", edge.confidence, edge.sourceRecordId, edge.createdTime);
|
|
1349
|
+
}
|
|
1350
|
+
getGraphNodeByEntity(userId, entity) {
|
|
1351
|
+
const stmt = this.db.prepare("SELECT id, user_id, entity, entity_type, skill_tag, confidence, source_record_id, created_time FROM graph_nodes WHERE user_id = ? AND LOWER(entity) = LOWER(?)");
|
|
1352
|
+
const row = stmt.get(userId, entity);
|
|
1353
|
+
if (!row)
|
|
1354
|
+
return null;
|
|
1355
|
+
return {
|
|
1356
|
+
id: row.id,
|
|
1357
|
+
userId: row.user_id,
|
|
1358
|
+
entity: row.entity,
|
|
1359
|
+
entityType: row.entity_type,
|
|
1360
|
+
skillTag: row.skill_tag,
|
|
1361
|
+
confidence: row.confidence,
|
|
1362
|
+
sourceRecordId: row.source_record_id,
|
|
1363
|
+
createdTime: row.created_time
|
|
1364
|
+
};
|
|
1365
|
+
}
|
|
1366
|
+
getGraphNeighbors(userId, entityId, skillTag, maxHops = 2) {
|
|
1367
|
+
const visitedNodes = new Map();
|
|
1368
|
+
const visitedEdges = new Map();
|
|
1369
|
+
const stmtNodeById = this.db.prepare("SELECT id, user_id, entity, entity_type, skill_tag, confidence, source_record_id, created_time FROM graph_nodes WHERE user_id = ? AND id = ?");
|
|
1370
|
+
const startRow = stmtNodeById.get(userId, entityId);
|
|
1371
|
+
if (!startRow)
|
|
1372
|
+
return { nodes: [], edges: [] };
|
|
1373
|
+
const startNode = {
|
|
1374
|
+
id: startRow.id,
|
|
1375
|
+
userId: startRow.user_id,
|
|
1376
|
+
entity: startRow.entity,
|
|
1377
|
+
entityType: startRow.entity_type,
|
|
1378
|
+
skillTag: startRow.skill_tag,
|
|
1379
|
+
confidence: startRow.confidence,
|
|
1380
|
+
sourceRecordId: startRow.source_record_id,
|
|
1381
|
+
createdTime: startRow.created_time
|
|
1382
|
+
};
|
|
1383
|
+
visitedNodes.set(startNode.id, startNode);
|
|
1384
|
+
let queue = [startNode.id];
|
|
1385
|
+
let currentHop = 0;
|
|
1386
|
+
while (queue.length > 0 && currentHop < maxHops) {
|
|
1387
|
+
const nextQueue = [];
|
|
1388
|
+
for (const nodeId of queue) {
|
|
1389
|
+
const queryParams = [userId, nodeId, nodeId];
|
|
1390
|
+
let edgeSql = `
|
|
1391
|
+
SELECT id, user_id, from_node_id, to_node_id, relation, skill_tag, confidence, source_record_id, created_time
|
|
1392
|
+
FROM graph_edges
|
|
1393
|
+
WHERE user_id = ? AND (from_node_id = ? OR to_node_id = ?)
|
|
1394
|
+
`;
|
|
1395
|
+
if (skillTag) {
|
|
1396
|
+
edgeSql += " AND (skill_tag = ? OR skill_tag = '')";
|
|
1397
|
+
queryParams.push(skillTag);
|
|
1398
|
+
}
|
|
1399
|
+
const stmtEdges = this.db.prepare(edgeSql);
|
|
1400
|
+
const edgeRows = stmtEdges.all(...queryParams);
|
|
1401
|
+
for (const row of edgeRows) {
|
|
1402
|
+
const edge = {
|
|
1403
|
+
id: row.id,
|
|
1404
|
+
userId: row.user_id,
|
|
1405
|
+
fromNodeId: row.from_node_id,
|
|
1406
|
+
toNodeId: row.to_node_id,
|
|
1407
|
+
relation: row.relation,
|
|
1408
|
+
skillTag: row.skill_tag,
|
|
1409
|
+
confidence: row.confidence,
|
|
1410
|
+
sourceRecordId: row.source_record_id,
|
|
1411
|
+
createdTime: row.created_time
|
|
1412
|
+
};
|
|
1413
|
+
visitedEdges.set(edge.id, edge);
|
|
1414
|
+
const neighborId = edge.fromNodeId === nodeId ? edge.toNodeId : edge.fromNodeId;
|
|
1415
|
+
if (!visitedNodes.has(neighborId)) {
|
|
1416
|
+
const neighborRow = stmtNodeById.get(userId, neighborId);
|
|
1417
|
+
if (neighborRow) {
|
|
1418
|
+
const neighborNode = {
|
|
1419
|
+
id: neighborRow.id,
|
|
1420
|
+
userId: neighborRow.user_id,
|
|
1421
|
+
entity: neighborRow.entity,
|
|
1422
|
+
entityType: neighborRow.entity_type,
|
|
1423
|
+
skillTag: neighborRow.skill_tag,
|
|
1424
|
+
confidence: neighborRow.confidence,
|
|
1425
|
+
sourceRecordId: neighborRow.source_record_id,
|
|
1426
|
+
createdTime: neighborRow.created_time
|
|
1427
|
+
};
|
|
1428
|
+
visitedNodes.set(neighborId, neighborNode);
|
|
1429
|
+
nextQueue.push(neighborId);
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
queue = nextQueue;
|
|
1435
|
+
currentHop++;
|
|
1436
|
+
}
|
|
1437
|
+
return {
|
|
1438
|
+
nodes: Array.from(visitedNodes.values()),
|
|
1439
|
+
edges: Array.from(visitedEdges.values())
|
|
1440
|
+
};
|
|
1441
|
+
}
|
|
1442
|
+
// ============================
|
|
1443
|
+
// ACE Feedback Loop Methods
|
|
1444
|
+
// ============================
|
|
1445
|
+
markCited(userId, recordIds) {
|
|
1446
|
+
if (recordIds.length === 0)
|
|
1447
|
+
return;
|
|
1448
|
+
const now = new Date().toISOString();
|
|
1449
|
+
const placeholders = recordIds.map(() => "?").join(",");
|
|
1450
|
+
const stmt = this.db.prepare(`
|
|
1451
|
+
UPDATE cognitive_records
|
|
1452
|
+
SET citation_count = citation_count + 1,
|
|
1453
|
+
last_cited_at = ?,
|
|
1454
|
+
never_cited_count = 0,
|
|
1455
|
+
updated_time = ?
|
|
1456
|
+
WHERE user_id = ? AND record_id IN (${placeholders})
|
|
1457
|
+
`);
|
|
1458
|
+
stmt.run(now, now, userId, ...recordIds);
|
|
1459
|
+
}
|
|
1460
|
+
incrementNeverCited(userId, recordIds) {
|
|
1461
|
+
if (recordIds.length === 0)
|
|
1462
|
+
return [];
|
|
1463
|
+
const now = new Date().toISOString();
|
|
1464
|
+
const placeholders = recordIds.map(() => "?").join(",");
|
|
1465
|
+
this.db.prepare(`
|
|
1466
|
+
UPDATE cognitive_records
|
|
1467
|
+
SET never_cited_count = never_cited_count + 1, updated_time = ?
|
|
1468
|
+
WHERE user_id = ? AND record_id IN (${placeholders})
|
|
1469
|
+
`).run(now, userId, ...recordIds);
|
|
1470
|
+
const rows = this.db.prepare(`
|
|
1471
|
+
SELECT record_id, never_cited_count FROM cognitive_records
|
|
1472
|
+
WHERE user_id = ? AND record_id IN (${placeholders})
|
|
1473
|
+
`).all(userId, ...recordIds);
|
|
1474
|
+
return rows.map(r => ({ recordId: r.record_id, neverCitedCount: r.never_cited_count }));
|
|
1475
|
+
}
|
|
1476
|
+
archiveCognitiveRecord(userId, recordId) {
|
|
1477
|
+
const now = new Date().toISOString();
|
|
1478
|
+
this.db.prepare("UPDATE cognitive_records SET archived = 1, status = 'archived', updated_time = ? WHERE user_id = ? AND record_id = ?").run(now, userId, recordId);
|
|
1479
|
+
this.insertOperation({
|
|
1480
|
+
id: randomUUID(),
|
|
1481
|
+
userId,
|
|
1482
|
+
recordId,
|
|
1483
|
+
operation: "archive",
|
|
1484
|
+
actor: "system",
|
|
1485
|
+
sessionKey: "",
|
|
1486
|
+
reason: "",
|
|
1487
|
+
createdAt: now,
|
|
1488
|
+
metadata: {},
|
|
1489
|
+
});
|
|
1490
|
+
}
|
|
1491
|
+
// ============================
|
|
1492
|
+
// Skill Pre-warming Helpers
|
|
1493
|
+
// ============================
|
|
1494
|
+
getRecentSkillContextCognitives(userId, limit) {
|
|
1495
|
+
const rows = this.db.prepare(`
|
|
1496
|
+
SELECT skill_tag, created_time FROM cognitive_records
|
|
1497
|
+
WHERE user_id = ? AND type = 'skill_context' AND skill_tag != '' AND invalid_at IS NULL AND archived = 0
|
|
1498
|
+
ORDER BY created_time DESC
|
|
1499
|
+
LIMIT ?
|
|
1500
|
+
`).all(userId, limit);
|
|
1501
|
+
return rows.map(r => ({ skillTag: r.skill_tag, createdTime: r.created_time }));
|
|
1502
|
+
}
|
|
1503
|
+
getSkillHints(skillName) {
|
|
1504
|
+
const row = this.db.prepare("SELECT hints FROM skill_extraction_hints WHERE skill_name = ?").get(skillName);
|
|
1505
|
+
return row?.hints ?? null;
|
|
1506
|
+
}
|
|
1507
|
+
getSkillActivations(userId) {
|
|
1508
|
+
const rows = this.db.prepare(`
|
|
1509
|
+
SELECT skill_name, potential, last_decay_time
|
|
1510
|
+
FROM skill_activations
|
|
1511
|
+
WHERE user_id = ?
|
|
1512
|
+
ORDER BY potential DESC, skill_name ASC
|
|
1513
|
+
`).all(userId);
|
|
1514
|
+
return rows.map((row) => ({
|
|
1515
|
+
skillName: row.skill_name,
|
|
1516
|
+
potential: row.potential,
|
|
1517
|
+
lastDecayTime: row.last_decay_time,
|
|
1518
|
+
}));
|
|
1519
|
+
}
|
|
1520
|
+
upsertSkillActivations(userId, activations) {
|
|
1521
|
+
if (activations.length === 0)
|
|
1522
|
+
return;
|
|
1523
|
+
const stmt = this.db.prepare(`
|
|
1524
|
+
INSERT INTO skill_activations (user_id, skill_name, potential, last_decay_time)
|
|
1525
|
+
VALUES (?, ?, ?, ?)
|
|
1526
|
+
ON CONFLICT(user_id, skill_name) DO UPDATE SET
|
|
1527
|
+
potential=excluded.potential,
|
|
1528
|
+
last_decay_time=excluded.last_decay_time
|
|
1529
|
+
`);
|
|
1530
|
+
this.db.exec("BEGIN");
|
|
1531
|
+
try {
|
|
1532
|
+
for (const record of activations) {
|
|
1533
|
+
stmt.run(userId, record.skillName, record.potential, record.lastDecayTime);
|
|
1534
|
+
}
|
|
1535
|
+
this.db.exec("COMMIT");
|
|
1536
|
+
}
|
|
1537
|
+
catch (error) {
|
|
1538
|
+
this.db.exec("ROLLBACK");
|
|
1539
|
+
throw error;
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
createUser(userId, apiKey, displayName = "", isAdmin = false) {
|
|
1543
|
+
const createdAt = new Date().toISOString();
|
|
1544
|
+
this.db.prepare(`
|
|
1545
|
+
INSERT INTO users (user_id, api_key, password_hash, display_name, email, is_admin, status, created_at)
|
|
1546
|
+
VALUES (?, ?, NULL, ?, '', ?, 'active', ?)
|
|
1547
|
+
`).run(userId, apiKey, displayName, isAdmin ? 1 : 0, createdAt);
|
|
1548
|
+
return {
|
|
1549
|
+
userId,
|
|
1550
|
+
apiKey,
|
|
1551
|
+
passwordHash: null,
|
|
1552
|
+
displayName,
|
|
1553
|
+
email: "",
|
|
1554
|
+
isAdmin,
|
|
1555
|
+
status: "active",
|
|
1556
|
+
createdAt,
|
|
1557
|
+
};
|
|
1558
|
+
}
|
|
1559
|
+
getUserByApiKey(apiKey) {
|
|
1560
|
+
const row = this.db.prepare("SELECT user_id, api_key, password_hash, display_name, email, is_admin, status, created_at FROM users WHERE api_key = ?").get(apiKey);
|
|
1561
|
+
if (!row)
|
|
1562
|
+
return null;
|
|
1563
|
+
return {
|
|
1564
|
+
userId: row.user_id,
|
|
1565
|
+
apiKey: row.api_key,
|
|
1566
|
+
passwordHash: row.password_hash ?? null,
|
|
1567
|
+
displayName: row.display_name ?? "",
|
|
1568
|
+
email: row.email ?? "",
|
|
1569
|
+
isAdmin: Boolean(row.is_admin),
|
|
1570
|
+
status: row.status === "disabled" ? "disabled" : "active",
|
|
1571
|
+
createdAt: row.created_at,
|
|
1572
|
+
};
|
|
1573
|
+
}
|
|
1574
|
+
getUserByEmail(email) {
|
|
1575
|
+
const row = this.db.prepare("SELECT user_id, api_key, password_hash, display_name, email, is_admin, status, created_at FROM users WHERE lower(email) = lower(?)").get(email);
|
|
1576
|
+
if (!row)
|
|
1577
|
+
return null;
|
|
1578
|
+
return {
|
|
1579
|
+
userId: row.user_id,
|
|
1580
|
+
apiKey: row.api_key,
|
|
1581
|
+
passwordHash: row.password_hash ?? null,
|
|
1582
|
+
displayName: row.display_name ?? "",
|
|
1583
|
+
email: row.email ?? "",
|
|
1584
|
+
isAdmin: Boolean(row.is_admin),
|
|
1585
|
+
status: row.status === "disabled" ? "disabled" : "active",
|
|
1586
|
+
createdAt: row.created_at,
|
|
1587
|
+
};
|
|
1588
|
+
}
|
|
1589
|
+
getUserById(userId) {
|
|
1590
|
+
const row = this.db.prepare("SELECT user_id, api_key, password_hash, display_name, email, is_admin, status, created_at FROM users WHERE user_id = ?").get(userId);
|
|
1591
|
+
if (!row)
|
|
1592
|
+
return null;
|
|
1593
|
+
return {
|
|
1594
|
+
userId: row.user_id,
|
|
1595
|
+
apiKey: row.api_key,
|
|
1596
|
+
passwordHash: row.password_hash ?? null,
|
|
1597
|
+
displayName: row.display_name ?? "",
|
|
1598
|
+
email: row.email ?? "",
|
|
1599
|
+
isAdmin: Boolean(row.is_admin),
|
|
1600
|
+
status: row.status === "disabled" ? "disabled" : "active",
|
|
1601
|
+
createdAt: row.created_at,
|
|
1602
|
+
};
|
|
1603
|
+
}
|
|
1604
|
+
updateUserPassword(userId, passwordHash) {
|
|
1605
|
+
this.db.prepare("UPDATE users SET password_hash = ? WHERE user_id = ?").run(passwordHash, userId);
|
|
1606
|
+
}
|
|
1607
|
+
updateUserEmail(userId, email) {
|
|
1608
|
+
this.db.prepare("UPDATE users SET email = ? WHERE user_id = ?").run(email, userId);
|
|
1609
|
+
}
|
|
1610
|
+
updateUserDisplayName(userId, displayName) {
|
|
1611
|
+
this.db.prepare("UPDATE users SET display_name = ? WHERE user_id = ?").run(displayName, userId);
|
|
1612
|
+
}
|
|
1613
|
+
updateUserStatus(userId, status) {
|
|
1614
|
+
this.db.prepare("UPDATE users SET status = ? WHERE user_id = ?").run(status, userId);
|
|
1615
|
+
}
|
|
1616
|
+
updateUserApiKey(userId, apiKey) {
|
|
1617
|
+
this.db.prepare("UPDATE users SET api_key = ? WHERE user_id = ?").run(apiKey, userId);
|
|
1618
|
+
}
|
|
1619
|
+
listUsers(pagination) {
|
|
1620
|
+
const where = [];
|
|
1621
|
+
const args = [];
|
|
1622
|
+
if (pagination?.cursor) {
|
|
1623
|
+
where.push("(created_at < ? OR (created_at = ? AND user_id > ?))");
|
|
1624
|
+
args.push(pagination.cursor.createdAt, pagination.cursor.createdAt, pagination.cursor.userId);
|
|
1625
|
+
}
|
|
1626
|
+
args.push(pagination?.limit ?? 500);
|
|
1627
|
+
const rows = this.db.prepare(`SELECT user_id, api_key, password_hash, display_name, email, is_admin, status, created_at
|
|
1628
|
+
FROM users
|
|
1629
|
+
${where.length ? `WHERE ${where.join(" AND ")}` : ""}
|
|
1630
|
+
ORDER BY created_at DESC, user_id ASC
|
|
1631
|
+
LIMIT ?`).all(...args);
|
|
1632
|
+
return rows.map((row) => ({
|
|
1633
|
+
userId: row.user_id,
|
|
1634
|
+
apiKey: row.api_key,
|
|
1635
|
+
passwordHash: row.password_hash ?? null,
|
|
1636
|
+
displayName: row.display_name ?? "",
|
|
1637
|
+
email: row.email ?? "",
|
|
1638
|
+
isAdmin: Boolean(row.is_admin),
|
|
1639
|
+
status: row.status === "disabled" ? "disabled" : "active",
|
|
1640
|
+
createdAt: row.created_at,
|
|
1641
|
+
}));
|
|
1642
|
+
}
|
|
1643
|
+
deleteUser(userId) {
|
|
1644
|
+
this.db.exec("BEGIN");
|
|
1645
|
+
try {
|
|
1646
|
+
this.db.prepare("DELETE FROM users WHERE user_id = ?").run(userId);
|
|
1647
|
+
this.db.prepare("DELETE FROM sensory_stream WHERE user_id = ?").run(userId);
|
|
1648
|
+
this.db.prepare("DELETE FROM cognitive_fts WHERE user_id = ?").run(userId);
|
|
1649
|
+
this.db.prepare("DELETE FROM cognitive_records WHERE user_id = ?").run(userId);
|
|
1650
|
+
this.db.prepare("DELETE FROM contradictions WHERE user_id = ?").run(userId);
|
|
1651
|
+
this.db.prepare("DELETE FROM contextual_focus WHERE user_id = ?").run(userId);
|
|
1652
|
+
this.db.prepare("DELETE FROM core_identity WHERE user_id = ?").run(userId);
|
|
1653
|
+
this.db.prepare("DELETE FROM scheduler_state WHERE user_id = ?").run(userId);
|
|
1654
|
+
this.db.prepare("DELETE FROM graph_nodes WHERE user_id = ?").run(userId);
|
|
1655
|
+
this.db.prepare("DELETE FROM graph_edges WHERE user_id = ?").run(userId);
|
|
1656
|
+
this.db.prepare("DELETE FROM cognitive_connections WHERE user_id = ?").run(userId);
|
|
1657
|
+
this.db.prepare("DELETE FROM memory_evidence WHERE user_id = ?").run(userId);
|
|
1658
|
+
this.db.prepare("DELETE FROM memory_operations WHERE user_id = ?").run(userId);
|
|
1659
|
+
this.db.prepare("DELETE FROM memory_file_index WHERE user_id = ?").run(userId);
|
|
1660
|
+
this.db.exec("COMMIT");
|
|
1661
|
+
}
|
|
1662
|
+
catch (e) {
|
|
1663
|
+
this.db.exec("ROLLBACK");
|
|
1664
|
+
throw e;
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
listMemories(userId, filters, pagination) {
|
|
1668
|
+
const where = ["user_id = ?"];
|
|
1669
|
+
const args = [userId];
|
|
1670
|
+
if (filters?.query) {
|
|
1671
|
+
where.push("content LIKE ?");
|
|
1672
|
+
args.push(`%${filters.query}%`);
|
|
1673
|
+
}
|
|
1674
|
+
if (filters?.type) {
|
|
1675
|
+
where.push("type = ?");
|
|
1676
|
+
args.push(filters.type);
|
|
1677
|
+
}
|
|
1678
|
+
if (filters?.scene) {
|
|
1679
|
+
where.push("scene_name = ?");
|
|
1680
|
+
args.push(filters.scene);
|
|
1681
|
+
}
|
|
1682
|
+
if (filters?.skill) {
|
|
1683
|
+
where.push("skill_tag = ?");
|
|
1684
|
+
args.push(filters.skill);
|
|
1685
|
+
}
|
|
1686
|
+
if (typeof filters?.archived === "boolean") {
|
|
1687
|
+
where.push("archived = ?");
|
|
1688
|
+
args.push(filters.archived ? 1 : 0);
|
|
1689
|
+
}
|
|
1690
|
+
if (pagination?.cursor) {
|
|
1691
|
+
where.push("(created_time < ? OR (created_time = ? AND record_id > ?))");
|
|
1692
|
+
args.push(pagination.cursor.createdTime, pagination.cursor.createdTime, pagination.cursor.recordId);
|
|
1693
|
+
}
|
|
1694
|
+
args.push(pagination?.limit ?? 500);
|
|
1695
|
+
const rows = this.db.prepare(`
|
|
1696
|
+
SELECT record_id, content, type, priority, scene_name, skill_tag, created_time, citation_count, never_cited_count, archived
|
|
1697
|
+
FROM cognitive_records
|
|
1698
|
+
WHERE ${where.join(" AND ")}
|
|
1699
|
+
ORDER BY created_time DESC, record_id ASC
|
|
1700
|
+
LIMIT ?
|
|
1701
|
+
`).all(...args);
|
|
1702
|
+
return rows.map((row) => ({
|
|
1703
|
+
recordId: row.record_id,
|
|
1704
|
+
content: row.content,
|
|
1705
|
+
type: row.type,
|
|
1706
|
+
priority: row.priority,
|
|
1707
|
+
sceneName: row.scene_name ?? "",
|
|
1708
|
+
skillTag: row.skill_tag ?? "",
|
|
1709
|
+
createdTime: row.created_time,
|
|
1710
|
+
citationCount: row.citation_count ?? 0,
|
|
1711
|
+
neverCitedCount: row.never_cited_count ?? 0,
|
|
1712
|
+
archived: Boolean(row.archived),
|
|
1713
|
+
}));
|
|
1714
|
+
}
|
|
1715
|
+
getMemoryStats(userId) {
|
|
1716
|
+
const totalRow = this.db.prepare("SELECT COUNT(*) as c FROM cognitive_records WHERE user_id = ?").get(userId);
|
|
1717
|
+
const archivedRow = this.db.prepare("SELECT COUNT(*) as c FROM cognitive_records WHERE user_id = ? AND archived = 1").get(userId);
|
|
1718
|
+
const typeRows = this.db.prepare("SELECT type, COUNT(*) as c FROM cognitive_records WHERE user_id = ? GROUP BY type").all(userId);
|
|
1719
|
+
const citationRows = this.db.prepare("SELECT SUM(citation_count) as cited, COUNT(*) as total FROM cognitive_records WHERE user_id = ?").get(userId);
|
|
1720
|
+
const lastRecall = this.db.prepare("SELECT MAX(recorded_at) as last_at FROM sensory_stream WHERE user_id = ?").get(userId);
|
|
1721
|
+
const byType = {};
|
|
1722
|
+
for (const row of typeRows)
|
|
1723
|
+
byType[row.type] = row.c;
|
|
1724
|
+
const totalRecords = totalRow?.c ?? 0;
|
|
1725
|
+
const cited = citationRows?.cited ?? 0;
|
|
1726
|
+
return {
|
|
1727
|
+
total: totalRecords,
|
|
1728
|
+
archived: archivedRow?.c ?? 0,
|
|
1729
|
+
byType,
|
|
1730
|
+
citationRate: totalRecords > 0 ? cited / totalRecords : 0,
|
|
1731
|
+
lastRecallAt: lastRecall?.last_at ?? null,
|
|
1732
|
+
extraction: this.getExtractionStatus(userId),
|
|
1733
|
+
};
|
|
1734
|
+
}
|
|
1735
|
+
replaceFileIndex(record) {
|
|
1736
|
+
this.db.prepare("DELETE FROM memory_file_index WHERE user_id = ? AND record_id = ?").run(record.userId, record.id);
|
|
1737
|
+
const filePaths = [...new Set((record.filePaths ?? []).map((filePath) => filePath.trim()).filter(Boolean))];
|
|
1738
|
+
if (filePaths.length === 0)
|
|
1739
|
+
return;
|
|
1740
|
+
const stmt = this.db.prepare(`
|
|
1741
|
+
INSERT INTO memory_file_index (id, user_id, record_id, file_path, symbol, created_time)
|
|
1742
|
+
VALUES (?, ?, ?, ?, '', ?)
|
|
1743
|
+
`);
|
|
1744
|
+
for (const filePath of filePaths) {
|
|
1745
|
+
stmt.run(randomUUID(), record.userId, record.id, filePath, record.createdTime);
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
upsertConnection(userId, sourceId, targetId, weight) {
|
|
1749
|
+
const stmt = this.db.prepare(`
|
|
1750
|
+
INSERT INTO cognitive_connections (user_id, source_id, target_id, weight, last_activated_at)
|
|
1751
|
+
VALUES (?, ?, ?, ?, datetime('now'))
|
|
1752
|
+
ON CONFLICT(user_id, source_id, target_id) DO UPDATE SET
|
|
1753
|
+
weight = excluded.weight,
|
|
1754
|
+
last_activated_at = datetime('now')
|
|
1755
|
+
`);
|
|
1756
|
+
stmt.run(userId, sourceId, targetId, weight);
|
|
1757
|
+
}
|
|
1758
|
+
getConnectionsForSource(userId, sourceId) {
|
|
1759
|
+
const rows = this.db.prepare(`
|
|
1760
|
+
SELECT target_id, weight FROM cognitive_connections
|
|
1761
|
+
WHERE user_id = ? AND source_id = ? AND weight >= 0.1
|
|
1762
|
+
`).all(userId, sourceId);
|
|
1763
|
+
return rows.map(r => ({ targetId: r.target_id, weight: r.weight }));
|
|
1764
|
+
}
|
|
1765
|
+
strengthenConnectionsBatch(userId, pairs, delta) {
|
|
1766
|
+
if (pairs.length === 0)
|
|
1767
|
+
return;
|
|
1768
|
+
this.db.exec("BEGIN");
|
|
1769
|
+
try {
|
|
1770
|
+
const stmt = this.db.prepare(`
|
|
1771
|
+
INSERT INTO cognitive_connections (user_id, source_id, target_id, weight, last_activated_at)
|
|
1772
|
+
VALUES (?, ?, ?, ?, datetime('now'))
|
|
1773
|
+
ON CONFLICT(user_id, source_id, target_id) DO UPDATE SET
|
|
1774
|
+
weight = MIN(1.0, weight + ?),
|
|
1775
|
+
last_activated_at = datetime('now')
|
|
1776
|
+
`);
|
|
1777
|
+
for (const pair of pairs) {
|
|
1778
|
+
stmt.run(userId, pair.source, pair.target, delta, delta);
|
|
1779
|
+
stmt.run(userId, pair.target, pair.source, delta, delta);
|
|
1780
|
+
}
|
|
1781
|
+
this.db.exec("COMMIT");
|
|
1782
|
+
}
|
|
1783
|
+
catch (e) {
|
|
1784
|
+
this.db.exec("ROLLBACK");
|
|
1785
|
+
throw e;
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
decayConnections(userId, decayFactor) {
|
|
1789
|
+
const stmt = this.db.prepare(`
|
|
1790
|
+
UPDATE cognitive_connections
|
|
1791
|
+
SET weight = MAX(0.0, weight * ?)
|
|
1792
|
+
WHERE user_id = ?
|
|
1793
|
+
`);
|
|
1794
|
+
stmt.run(decayFactor, userId);
|
|
1795
|
+
}
|
|
1796
|
+
pruneConnections(userId, threshold) {
|
|
1797
|
+
const stmt = this.db.prepare(`
|
|
1798
|
+
DELETE FROM cognitive_connections
|
|
1799
|
+
WHERE user_id = ? AND weight < ?
|
|
1800
|
+
`);
|
|
1801
|
+
stmt.run(userId, threshold);
|
|
1802
|
+
}
|
|
1803
|
+
getAllConnections(userId) {
|
|
1804
|
+
const rows = this.db.prepare(`
|
|
1805
|
+
SELECT source_id, target_id, weight, last_activated_at
|
|
1806
|
+
FROM cognitive_connections
|
|
1807
|
+
WHERE user_id = ?
|
|
1808
|
+
`).all(userId);
|
|
1809
|
+
return rows.map(r => ({
|
|
1810
|
+
sourceId: r.source_id,
|
|
1811
|
+
targetId: r.target_id,
|
|
1812
|
+
weight: r.weight,
|
|
1813
|
+
lastActivatedAt: r.last_activated_at ?? "",
|
|
1814
|
+
}));
|
|
1815
|
+
}
|
|
1816
|
+
}
|