@ijfw/memory-server 1.4.4 → 1.5.1
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/bin/ijfw-memorize +14 -7
- package/fixtures/team/book.json +6 -6
- package/fixtures/team/business.json +146 -20
- package/fixtures/team/content.json +6 -6
- package/fixtures/team/design.json +148 -20
- package/fixtures/team/mixed.json +206 -27
- package/fixtures/team/research.json +146 -20
- package/fixtures/team/software.json +148 -20
- package/fixtures/truncation-corpus/_generate-corpus.js +367 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-01/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-01/intent-journal.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-01/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-01/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-02/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-02/intent-journal.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-02/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-02/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-03/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-03/intent-journal.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-03/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-03/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-04/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-04/intent-journal.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-04/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-04/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-05/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-05/intent-journal.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-05/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-05/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/snapshots/v-midO-1-advance.json +11 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/snapshots/v-midO-2-advance.json +11 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/snapshots/v-midO-3-advance.json +11 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/snapshots/v-midO-4-advance.json +11 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/snapshots/v-midO-5-advance.json +11 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-01/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-01/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-01/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-01/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-02/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-02/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-02/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-02/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-03/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-03/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-03/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-03/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-04/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-04/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-04/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-04/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-05/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-05/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-05/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-05/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-01/events.jsonl +0 -0
- package/fixtures/truncation-corpus/fx-04-no-events-01/intent-journal.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-01/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-04-no-events-01/snapshots/v-noEv-1-set-phase.json +11 -0
- package/fixtures/truncation-corpus/fx-04-no-events-01/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-02/events.jsonl +0 -0
- package/fixtures/truncation-corpus/fx-04-no-events-02/intent-journal.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-02/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-04-no-events-02/snapshots/v-noEv-2-set-phase.json +11 -0
- package/fixtures/truncation-corpus/fx-04-no-events-02/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-03/events.jsonl +0 -0
- package/fixtures/truncation-corpus/fx-04-no-events-03/intent-journal.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-03/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-04-no-events-03/snapshots/v-noEv-3-set-phase.json +11 -0
- package/fixtures/truncation-corpus/fx-04-no-events-03/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-04/events.jsonl +0 -0
- package/fixtures/truncation-corpus/fx-04-no-events-04/intent-journal.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-04/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-04-no-events-04/snapshots/v-noEv-4-set-phase.json +11 -0
- package/fixtures/truncation-corpus/fx-04-no-events-04/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-05/events.jsonl +0 -0
- package/fixtures/truncation-corpus/fx-04-no-events-05/intent-journal.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-05/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-04-no-events-05/snapshots/v-noEv-5-set-phase.json +11 -0
- package/fixtures/truncation-corpus/fx-04-no-events-05/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-01/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-01/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-01/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-01/snapshots/v-errT-1-partial.json +11 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-01/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-02/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-02/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-02/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-02/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-03/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-03/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-03/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-03/snapshots/v-errT-3-partial.json +11 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-03/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-04/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-04/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-04/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-04/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-05/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-05/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-05/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-05/snapshots/v-errT-5-partial.json +11 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-05/target/.ijfw/state/workflow.json +1 -0
- package/package.json +6 -3
- package/src/active-extension-writer.js +144 -64
- package/src/api-client.js +43 -5
- package/src/audit-roster.js +80 -5
- package/src/blackboard.js +298 -6
- package/src/cli-run.js +33 -5
- package/src/codex-agents.js +96 -5
- package/src/cost/aggregator.js +39 -9
- package/src/cost/pricing.js +57 -0
- package/src/cost/readers/gemini.js +1 -1
- package/src/cross-audit-chunker.js +189 -0
- package/src/cross-dispatcher.js +124 -21
- package/src/cross-orchestrator-cli.js +754 -159
- package/src/cross-orchestrator.js +1065 -17
- package/src/cross-project-search.js +195 -9
- package/src/dashboard-client-waves.html +304 -0
- package/src/dashboard-client.html +5 -1
- package/src/dashboard-server.js +73 -0
- package/src/deploy-alerts.js +150 -0
- package/src/design/iframe-bridge.js +242 -0
- package/src/design-companion.js +144 -0
- package/src/dispatch/checkpoint-cli.js +97 -0
- package/src/dispatch/colon-syntax.js +81 -1
- package/src/dispatch/extension.js +26 -2
- package/src/dispatch/registry-cli.js +4 -1
- package/src/dispatch/wave-cli.js +201 -6
- package/src/dispatch/worktree-cli.js +40 -0
- package/src/dispatch-planner.js +97 -2
- package/src/dream/runner.mjs +47 -11
- package/src/dream/stage-runner.js +40 -0
- package/src/dream/state-file.js +102 -0
- package/src/extension-installer.js +70 -24
- package/src/extension-quota-tracker.js +4 -2
- package/src/extension-registry.js +289 -35
- package/src/feedback-detector.js +26 -0
- package/src/fs-lock.js +259 -7
- package/src/gate-result.js +95 -1
- package/src/hardware-signer.js +4 -2
- package/src/hero-line.js +86 -5
- package/src/intent-router.js +35 -0
- package/src/lib/a11y-contract.js +117 -0
- package/src/lib/atomic-io.js +29 -8
- package/src/lib/cache-keepalive.js +150 -0
- package/src/lib/jsonl-rotation.js +104 -0
- package/src/lib/lighthouse-pillar.js +121 -0
- package/src/lib/llm-call.js +121 -0
- package/src/lib/playwright-baseline.js +205 -0
- package/src/lib/rekor-bridge.js +221 -0
- package/src/lib/repo-map.js +392 -0
- package/src/lib/shasum-verify.js +164 -0
- package/src/lib/sketches-gc.js +132 -0
- package/src/lib/tmp-suffix.js +62 -0
- package/src/lib/ui-review-runner.js +595 -0
- package/src/lib/uispec-drift.js +301 -0
- package/src/lib/uispec-intake.js +381 -0
- package/src/lib/worktree-guards.js +118 -0
- package/src/lib/worktree-recovery.js +100 -0
- package/src/memory/auto-linker.js +267 -0
- package/src/memory/benchmark.js +498 -0
- package/src/memory/dedup.js +126 -0
- package/src/memory/embedding-cache.js +136 -0
- package/src/memory/fact-extractor.js +168 -0
- package/src/memory/fts5.js +65 -1
- package/src/memory/migration-runner.js +6 -1
- package/src/memory/migrations/004-bitemporal.js +91 -0
- package/src/memory/migrations/005-vector-cache.js +61 -0
- package/src/memory/migrations/006-obsidian-graph.js +46 -0
- package/src/memory/migrations/007-skill-telemetry.js +24 -0
- package/src/memory/migrations/008-write-provenance.js +41 -0
- package/src/memory/migrations/009-obsidian-backfill.js +50 -0
- package/src/memory/obsidian-parser.js +152 -0
- package/src/memory/query-dataview.js +86 -0
- package/src/memory/search.js +46 -15
- package/src/memory/temporal.js +529 -0
- package/src/memory/tokenize.js +10 -0
- package/src/memory-facts-handler.js +37 -0
- package/src/memory-feedback.js +260 -2
- package/src/model-refresh.js +292 -0
- package/src/observability/cost-anomaly.js +166 -0
- package/src/observability/evaluator-checkpoint-contract.js +117 -0
- package/src/observability/trace-id.js +163 -0
- package/src/orchestrator/agents-md-blackboard.js +152 -0
- package/src/orchestrator/checkpoint-contract.md +140 -0
- package/src/orchestrator/debug-trident-trigger.js +374 -0
- package/src/orchestrator/debug-trident.js +570 -0
- package/src/orchestrator/merge-block-aware.js +350 -0
- package/src/orchestrator/plan-checker.js +475 -0
- package/src/orchestrator/post-done-runner.js +277 -0
- package/src/orchestrator/review.js +38 -3
- package/src/orchestrator/skill-telemetry-sink.js +29 -0
- package/src/orchestrator/skill-telemetry.js +37 -0
- package/src/orchestrator/state-events.js +459 -0
- package/src/orchestrator/state-sdk.js +1932 -0
- package/src/orchestrator/status-protocol.js +84 -17
- package/src/orchestrator/subagent-telemetry.js +471 -0
- package/src/orchestrator/termination.js +160 -0
- package/src/orchestrator/verification-gate.js +200 -16
- package/src/orchestrator/wave-state.js +332 -23
- package/src/orchestrator/worktree-provision.js +77 -0
- package/src/override-resolver.js +5 -3
- package/src/override-use-registry.js +111 -5
- package/src/receipts.js +36 -4
- package/src/recovery/checkpoint.js +56 -3
- package/src/recovery/code-fixer.js +961 -0
- package/src/recovery/truncation.js +317 -0
- package/src/redactor.js +75 -6
- package/src/runtime-mediator.js +15 -1
- package/src/sanitizer.js +10 -0
- package/src/search-hybrid.js +139 -0
- package/src/server.js +795 -112
- package/src/swarm/worktree.js +27 -4
- package/src/swarm-config.js +102 -17
- package/src/team/domain-templates/book.json +51 -0
- package/src/team/domain-templates/business.json +44 -0
- package/src/team/domain-templates/content.json +50 -0
- package/src/team/domain-templates/design.json +44 -0
- package/src/team/domain-templates/research.json +44 -0
- package/src/team/domain-templates/software.json +40 -0
- package/src/team/generator.js +440 -3
- package/src/team/modify.js +203 -0
- package/src/team/schemas.js +48 -0
- package/src/update-apply.js +19 -3
- package/src/dashboard-charts.js +0 -239
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// v1.5.0 audit MED #7 (memory-engine.md F-PRF-4): persistent embedding
|
|
2
|
+
// cache. Thin read/write surface over the memory_entry_vectors table
|
|
3
|
+
// (migration 005). All functions are sync to match the rest of the
|
|
4
|
+
// memory tier (better-sqlite3 is synchronous) and tolerate a db that
|
|
5
|
+
// hasn't reached schema v5 -- callers see "miss" rather than a throw,
|
|
6
|
+
// so hybrid rerank degrades to live re-embed without flooding stderr.
|
|
7
|
+
//
|
|
8
|
+
// v1.5.0 wire-W1.C: PK is content-hash (sha256(text)) so the cache
|
|
9
|
+
// works for ANY repeated snippet, not just rows that carry a stable
|
|
10
|
+
// memory_entry id. The high-level helpers `cacheKeyFor(text)`,
|
|
11
|
+
// `getCachedEmbedding(db, key, modelId)`, and `setCachedEmbedding(...)`
|
|
12
|
+
// are the production wire surface for search-hybrid's
|
|
13
|
+
// maybeRerankWithVectors path.
|
|
14
|
+
//
|
|
15
|
+
// Format: Float32Array <-> Buffer at the SQLite boundary. Each float
|
|
16
|
+
// is 4 bytes little-endian. Decode is endian-agnostic via DataView.
|
|
17
|
+
|
|
18
|
+
import { createHash } from 'node:crypto';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Stable content-hash key for the cache. Lowercase hex sha256 over the
|
|
22
|
+
* literal snippet bytes. Whitespace and case are preserved -- the caller
|
|
23
|
+
* is expected to pass the same exact text on every lookup.
|
|
24
|
+
*
|
|
25
|
+
* @param {string} text
|
|
26
|
+
* @returns {string|null} null when text is not a non-empty string
|
|
27
|
+
*/
|
|
28
|
+
export function cacheKeyFor(text) {
|
|
29
|
+
if (typeof text !== 'string' || text.length === 0) return null;
|
|
30
|
+
return createHash('sha256').update(text, 'utf8').digest('hex');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* encodeVector(vec) -- Float32Array | number[] -> Buffer
|
|
35
|
+
* Returns null on bad input.
|
|
36
|
+
*/
|
|
37
|
+
export function encodeVector(vec) {
|
|
38
|
+
if (!vec || typeof vec.length !== 'number' || vec.length === 0) return null;
|
|
39
|
+
const buf = Buffer.alloc(vec.length * 4);
|
|
40
|
+
for (let i = 0; i < vec.length; i++) {
|
|
41
|
+
buf.writeFloatLE(Number(vec[i]) || 0, i * 4);
|
|
42
|
+
}
|
|
43
|
+
return buf;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* decodeVector(buf) -- Buffer | Uint8Array -> number[]
|
|
48
|
+
* Returns null when the input length is not a multiple of 4.
|
|
49
|
+
*/
|
|
50
|
+
export function decodeVector(buf) {
|
|
51
|
+
if (!buf || typeof buf.length !== 'number' || buf.length === 0) return null;
|
|
52
|
+
if (buf.length % 4 !== 0) return null;
|
|
53
|
+
const n = buf.length / 4;
|
|
54
|
+
const out = Array.from({ length: n });
|
|
55
|
+
// Work via DataView so we accept Uint8Array (better-sqlite3 BLOB return)
|
|
56
|
+
// and a plain Buffer interchangeably.
|
|
57
|
+
const view = ArrayBuffer.isView(buf)
|
|
58
|
+
? new DataView(buf.buffer, buf.byteOffset, buf.byteLength)
|
|
59
|
+
: new DataView(buf);
|
|
60
|
+
for (let i = 0; i < n; i++) out[i] = view.getFloat32(i * 4, /*LE*/ true);
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** True iff the memory db has migration 005 applied (table exists). */
|
|
65
|
+
export function hasVectorCache(db) {
|
|
66
|
+
if (!db || typeof db.prepare !== 'function') return false;
|
|
67
|
+
try {
|
|
68
|
+
const row = db.prepare(
|
|
69
|
+
"SELECT 1 FROM sqlite_master WHERE type='table' AND name='memory_entry_vectors'"
|
|
70
|
+
).get();
|
|
71
|
+
return !!row;
|
|
72
|
+
} catch {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* getCachedEmbedding(db, cacheKey, modelId) -> number[] | null
|
|
79
|
+
* Returns the stored embedding for the (cache_key, model_id) pair, or
|
|
80
|
+
* null on miss / when the table is absent / when cacheKey is invalid.
|
|
81
|
+
*/
|
|
82
|
+
export function getCachedEmbedding(db, cacheKey, modelId) {
|
|
83
|
+
if (!hasVectorCache(db)) return null;
|
|
84
|
+
if (typeof cacheKey !== 'string' || cacheKey.length === 0) return null;
|
|
85
|
+
if (typeof modelId !== 'string' || modelId.length === 0) return null;
|
|
86
|
+
try {
|
|
87
|
+
const row = db.prepare(
|
|
88
|
+
'SELECT embedding FROM memory_entry_vectors WHERE cache_key = ? AND model_id = ?'
|
|
89
|
+
).get(cacheKey, modelId);
|
|
90
|
+
if (!row || !row.embedding) return null;
|
|
91
|
+
return decodeVector(row.embedding);
|
|
92
|
+
} catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* setCachedEmbedding(db, cacheKey, modelId, vec) -> boolean (success).
|
|
99
|
+
* INSERT OR REPLACE so re-embed after a model change overwrites the
|
|
100
|
+
* stale row. No-op if the table is missing or the vector encoding fails.
|
|
101
|
+
*/
|
|
102
|
+
export function setCachedEmbedding(db, cacheKey, modelId, vec) {
|
|
103
|
+
if (!hasVectorCache(db)) return false;
|
|
104
|
+
if (typeof cacheKey !== 'string' || cacheKey.length === 0) return false;
|
|
105
|
+
if (typeof modelId !== 'string' || modelId.length === 0) return false;
|
|
106
|
+
const blob = encodeVector(vec);
|
|
107
|
+
if (!blob) return false;
|
|
108
|
+
try {
|
|
109
|
+
db.prepare(
|
|
110
|
+
'INSERT OR REPLACE INTO memory_entry_vectors(cache_key, model_id, embedding) VALUES (?, ?, ?)'
|
|
111
|
+
).run(cacheKey, modelId, blob);
|
|
112
|
+
return true;
|
|
113
|
+
} catch {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* countCachedVectors(db, modelId?) -> number
|
|
120
|
+
* Debug helper; tests assert that re-running search doesn't re-write rows.
|
|
121
|
+
*/
|
|
122
|
+
export function countCachedVectors(db, modelId) {
|
|
123
|
+
if (!hasVectorCache(db)) return 0;
|
|
124
|
+
try {
|
|
125
|
+
if (modelId) {
|
|
126
|
+
const row = db.prepare(
|
|
127
|
+
'SELECT COUNT(*) AS n FROM memory_entry_vectors WHERE model_id = ?'
|
|
128
|
+
).get(modelId);
|
|
129
|
+
return row ? Number(row.n) : 0;
|
|
130
|
+
}
|
|
131
|
+
const row = db.prepare('SELECT COUNT(*) AS n FROM memory_entry_vectors').get();
|
|
132
|
+
return row ? Number(row.n) : 0;
|
|
133
|
+
} catch {
|
|
134
|
+
return 0;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* H5.5 — Heuristic fact extraction at ingest time.
|
|
3
|
+
*
|
|
4
|
+
* Competitors (mem0, Zep, Letta) extract structured facts from raw memory at
|
|
5
|
+
* ingest so retrieval can match on subject/predicate/object as well as on raw
|
|
6
|
+
* content. IJFW historically stored raw markdown only -- this module closes
|
|
7
|
+
* that gap without an LLM round-trip.
|
|
8
|
+
*
|
|
9
|
+
* Output shape: array of { subject, predicate, object, confidence } where
|
|
10
|
+
* confidence ∈ [0,1] reflects pattern specificity (anchored patterns score
|
|
11
|
+
* higher than catch-alls). Empty input or no matches → [].
|
|
12
|
+
*
|
|
13
|
+
* Design constraints:
|
|
14
|
+
* - Zero LLM calls (deterministic, runs inline in handleStore).
|
|
15
|
+
* - Zero deps -- pure JS regexes over already-sanitized text.
|
|
16
|
+
* - Conservative: prefer "miss" over false-positive; we'd rather record
|
|
17
|
+
* nothing than poison the facts.jsonl stream with garbage.
|
|
18
|
+
* - Output is structural, not semantic -- consumers can post-process.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
// Cap per-call output so a 100KB pasted memory doesn't blow up facts.jsonl.
|
|
22
|
+
const MAX_FACTS_PER_CALL = 64;
|
|
23
|
+
// Cap on each field length so a runaway match doesn't write a 10KB JSON line.
|
|
24
|
+
const MAX_FIELD_LEN = 240;
|
|
25
|
+
|
|
26
|
+
function clean(s) {
|
|
27
|
+
if (typeof s !== 'string') return '';
|
|
28
|
+
// Single-line, trimmed, length-capped. Already sanitized upstream but we
|
|
29
|
+
// still strip newlines so each fact is a single JSONL line at write time.
|
|
30
|
+
return s.replace(/\s+/g, ' ').trim().slice(0, MAX_FIELD_LEN);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function add(facts, subject, predicate, object, confidence) {
|
|
34
|
+
const s = clean(subject);
|
|
35
|
+
const p = clean(predicate);
|
|
36
|
+
const o = clean(object);
|
|
37
|
+
if (!s || !p || !o) return;
|
|
38
|
+
// Cheap intra-call dedup so the same fact extracted by overlapping patterns
|
|
39
|
+
// doesn't write twice. Keyed on lowercase triple.
|
|
40
|
+
const key = `${s.toLowerCase()}|${p.toLowerCase()}|${o.toLowerCase()}`;
|
|
41
|
+
if (facts._seen.has(key)) return;
|
|
42
|
+
facts._seen.add(key);
|
|
43
|
+
facts.push({ subject: s, predicate: p, object: o, confidence });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* extractFacts(content)
|
|
48
|
+
*
|
|
49
|
+
* @param {string} content - already-sanitized markdown text
|
|
50
|
+
* @returns {Array<{subject:string, predicate:string, object:string, confidence:number}>}
|
|
51
|
+
*/
|
|
52
|
+
export function extractFacts(content) {
|
|
53
|
+
const facts = [];
|
|
54
|
+
facts._seen = new Set();
|
|
55
|
+
if (typeof content !== 'string' || !content.trim()) {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Process line-by-line. Many of our patterns are anchored to clause shape,
|
|
60
|
+
// and pasted memories usually have one fact per line. Also split on the
|
|
61
|
+
// " | " separator that sanitizer.js inserts when collapsing newlines --
|
|
62
|
+
// store callers send multi-line markdown, sanitizer flattens it before we
|
|
63
|
+
// see it, so the original line boundaries are encoded as " | ".
|
|
64
|
+
const lines = content.split(/\r?\n|\s\|\s/);
|
|
65
|
+
for (const rawLine of lines) {
|
|
66
|
+
if (facts.length >= MAX_FACTS_PER_CALL) break;
|
|
67
|
+
const line = rawLine.trim();
|
|
68
|
+
if (!line) continue;
|
|
69
|
+
|
|
70
|
+
// ---- Pattern: "ship date: <date>" / "release date: <date>" / "due: <date>"
|
|
71
|
+
// Confidence 0.95 (very specific keyword + colon).
|
|
72
|
+
{
|
|
73
|
+
const m = line.match(/^\s*(ship\s+date|release\s+date|launch\s+date|due\s+date|due|deadline)\s*:\s*(.+)$/i);
|
|
74
|
+
if (m) {
|
|
75
|
+
add(facts, 'project', m[1].toLowerCase().replace(/\s+/g, '_'), m[2], 0.95);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ---- Pattern: "owner: <name>" / "assignee: <name>" / "lead: <name>"
|
|
81
|
+
// Confidence 0.95.
|
|
82
|
+
{
|
|
83
|
+
const m = line.match(/^\s*(owner|assignee|lead|maintainer|author)\s*:\s*(.+)$/i);
|
|
84
|
+
if (m) {
|
|
85
|
+
add(facts, 'project', m[1].toLowerCase(), m[2], 0.95);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ---- Pattern: "<key>: <value>" — generic key/value declarations.
|
|
91
|
+
// Lower confidence (0.6). Only fires for short, single-token-ish keys
|
|
92
|
+
// so prose colons ("for example:") don't trip it.
|
|
93
|
+
// Skips known-noise keys (http, https, file paths).
|
|
94
|
+
{
|
|
95
|
+
const m = line.match(/^\s*([A-Z][A-Za-z0-9_-]{1,30})\s*:\s*(\S.{0,200})$/);
|
|
96
|
+
if (m && !/^https?$/i.test(m[1]) && m[2].length >= 2) {
|
|
97
|
+
add(facts, 'project', m[1].toLowerCase(), m[2], 0.60);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ---- Pattern: "decided to <verb> [obj]" / "decision: <text>"
|
|
103
|
+
// Confidence 0.85.
|
|
104
|
+
{
|
|
105
|
+
// eslint-disable-next-line security/detect-unsafe-regex -- bounded {0,12} quantifier on internal markdown line, not network input
|
|
106
|
+
const m = line.match(/\bdecided\s+to\s+(\w+(?:\s+\S+){0,12})/i);
|
|
107
|
+
if (m) {
|
|
108
|
+
add(facts, 'team', 'decided_to', m[1], 0.85);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const m2 = line.match(/^\s*decision\s*:\s*(.+)$/i);
|
|
112
|
+
if (m2) {
|
|
113
|
+
add(facts, 'team', 'decision', m2[1], 0.90);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ---- Pattern: "<Name> uses <Tool>" — capitalised subject + uses + capitalised obj.
|
|
119
|
+
// Confidence 0.80. Both ends Title-Case to avoid matching prose like
|
|
120
|
+
// "the team uses redis" (which would be too noisy).
|
|
121
|
+
{
|
|
122
|
+
// eslint-disable-next-line security/detect-unsafe-regex -- bounded {1,40} quantifiers on internal markdown line, not network input
|
|
123
|
+
const m = line.match(/\b([A-Z][A-Za-z0-9_-]{1,40}(?:\s+[A-Z][A-Za-z0-9_-]{1,40})?)\s+uses\s+([A-Z][A-Za-z0-9_.+-]{1,40}(?:\s+[A-Za-z0-9_.+-]{1,40})?)/);
|
|
124
|
+
if (m) {
|
|
125
|
+
add(facts, m[1], 'uses', m[2], 0.80);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ---- Pattern: "<X> is <Y>" — copular sentences, capitalised subject.
|
|
131
|
+
// Confidence 0.70. Anchor on capital S so we don't extract
|
|
132
|
+
// "it is fine" / "this is broken" noise.
|
|
133
|
+
// Cap object phrase at 6 words to keep facts terse.
|
|
134
|
+
{
|
|
135
|
+
// eslint-disable-next-line security/detect-unsafe-regex -- bounded {1,40} and {0,5} quantifiers on internal markdown line, not network input
|
|
136
|
+
const m = line.match(/^\s*([A-Z][A-Za-z0-9_.-]{1,40}(?:\s+[A-Za-z0-9_.-]{1,40})?)\s+is\s+((?:[\w-]+(?:\s+[\w-]+){0,5}))[.!?]?\s*$/);
|
|
137
|
+
if (m) {
|
|
138
|
+
// Skip pronouns and bare articles disguised as subjects.
|
|
139
|
+
const subj = m[1];
|
|
140
|
+
if (!/^(It|This|That|There|Here|He|She|They|We|I)$/i.test(subj)) {
|
|
141
|
+
add(facts, subj, 'is', m[2], 0.70);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Strip the internal _seen marker before returning so JSON.stringify is clean.
|
|
149
|
+
delete facts._seen;
|
|
150
|
+
return facts;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* factToJsonl(fact, meta)
|
|
155
|
+
*
|
|
156
|
+
* Serialize one fact as a JSONL line, adding source metadata (timestamp,
|
|
157
|
+
* memory_id) so the facts stream is self-contained and joinable back to the
|
|
158
|
+
* journal entry it came from.
|
|
159
|
+
*/
|
|
160
|
+
export function factToJsonl(fact, meta = {}) {
|
|
161
|
+
const line = {
|
|
162
|
+
ts: meta.ts || new Date().toISOString(),
|
|
163
|
+
memory_id: meta.memory_id || null,
|
|
164
|
+
source: meta.source || 'memory_store',
|
|
165
|
+
...fact,
|
|
166
|
+
};
|
|
167
|
+
return JSON.stringify(line);
|
|
168
|
+
}
|
package/src/memory/fts5.js
CHANGED
|
@@ -36,6 +36,8 @@ import {
|
|
|
36
36
|
} from './migration-runner.js';
|
|
37
37
|
import { autoIndexGraphFromMemoryBody } from '../compute/graph-auto-index.js';
|
|
38
38
|
import { redactSecrets } from '../redactor.js';
|
|
39
|
+
import { indexObsidianRelations } from './obsidian-parser.js';
|
|
40
|
+
import { autoLink } from './auto-linker.js';
|
|
39
41
|
|
|
40
42
|
// D-PILLAR-SPEC section 12 ingest scrub gate. Default-on; the only escape hatch
|
|
41
43
|
// is the IJFW_INGEST_SCRUB=0 env var, used for local debugging only and
|
|
@@ -232,6 +234,43 @@ export function indexEntry(db, entry) {
|
|
|
232
234
|
});
|
|
233
235
|
tx();
|
|
234
236
|
|
|
237
|
+
// v1.5.0 memory-moat M1 (INT.1): write Obsidian-grade indexing rows
|
|
238
|
+
// ([[wikilinks]], #nested/tags, [key:: value] inline metadata) into
|
|
239
|
+
// memory_links / memory_tags / memory_meta. Best-effort: a missing
|
|
240
|
+
// migration-006 schema or a parse failure must never block ingest.
|
|
241
|
+
try {
|
|
242
|
+
if (inserted && inserted.id != null) {
|
|
243
|
+
indexObsidianRelations(db, String(inserted.id), row.body);
|
|
244
|
+
}
|
|
245
|
+
} catch (e) {
|
|
246
|
+
try { console.error('[fts5] indexObsidianRelations failed:', e?.message || e); } catch { /* never throw */ }
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// v1.5.0 memory-moat M2 (INT.2): fire A-Mem-style auto-linker. Asks the
|
|
250
|
+
// LLM (Claude Haiku 4.5 by default, env-gated) for (classification,
|
|
251
|
+
// link-proposals, neighbor-edits) over top-k lexical neighbors, then
|
|
252
|
+
// applies the proposal to memory_links + memory_tags atomically.
|
|
253
|
+
//
|
|
254
|
+
// Fire-and-forget — the indexEntry contract stays synchronous, and any
|
|
255
|
+
// failure (parse, budget, network, no key) returns skipped from autoLink
|
|
256
|
+
// without writes. IJFW_AUTOLINK_OFF=1 is the kill switch.
|
|
257
|
+
if (inserted && inserted.id != null) {
|
|
258
|
+
try {
|
|
259
|
+
// Don't await — let the LLM round-trip happen in the background.
|
|
260
|
+
// Capture for diagnostic visibility only.
|
|
261
|
+
const p = autoLink(db, {
|
|
262
|
+
id: inserted.id,
|
|
263
|
+
body: row.body,
|
|
264
|
+
}).catch((e) => {
|
|
265
|
+
try { console.error('[fts5] autoLink failed:', e?.message || e); } catch { /* never throw */ }
|
|
266
|
+
});
|
|
267
|
+
// expose for tests that want to await deterministic completion
|
|
268
|
+
indexEntry.__lastAutoLinkPromise = p;
|
|
269
|
+
} catch (e) {
|
|
270
|
+
try { console.error('[fts5] autoLink dispatch failed:', e?.message || e); } catch { /* never throw */ }
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
235
274
|
// GA-B3: fire D2 graph auto-population on memory ingest. The graph
|
|
236
275
|
// lives in the compute db at the same projectRoot, so we open compute
|
|
237
276
|
// on demand. Async-but-fire-and-forget: the indexEntry contract stays
|
|
@@ -243,8 +282,33 @@ export function indexEntry(db, entry) {
|
|
|
243
282
|
const ts = row.created_at;
|
|
244
283
|
const sessionId = row.session_id;
|
|
245
284
|
const body = row.body;
|
|
285
|
+
// v1.5.0 audit-LOW-memory-#14: dead-letter receipt for auto-index failures.
|
|
286
|
+
// Fire-and-forget was already swallowed silently; now we append an
|
|
287
|
+
// append-only JSONL receipt so silent indexer breakage is detectable in
|
|
288
|
+
// post-mortems. Best-effort: file write failures themselves are swallowed
|
|
289
|
+
// because indexEntry must never throw on diagnostic-write failure.
|
|
246
290
|
const p = autoIndexGraphFromMemoryBody({ memoryDb: db, body, sessionId, ts })
|
|
247
|
-
.catch(() =>
|
|
291
|
+
.catch((err) => {
|
|
292
|
+
try {
|
|
293
|
+
// Lazy import; node:fs/promises is always available.
|
|
294
|
+
import('node:fs/promises').then(({ appendFile, mkdir }) => {
|
|
295
|
+
try {
|
|
296
|
+
const indexDir = '.ijfw/index';
|
|
297
|
+
return mkdir(indexDir, { recursive: true })
|
|
298
|
+
.then(() => appendFile(
|
|
299
|
+
`${indexDir}/graph-errors.jsonl`,
|
|
300
|
+
JSON.stringify({
|
|
301
|
+
ts: new Date().toISOString(),
|
|
302
|
+
session_id: sessionId || null,
|
|
303
|
+
err: err && err.message ? String(err.message) : String(err),
|
|
304
|
+
}) + '\n',
|
|
305
|
+
))
|
|
306
|
+
.catch(() => {});
|
|
307
|
+
} catch { /* swallow */ }
|
|
308
|
+
}).catch(() => {});
|
|
309
|
+
} catch { /* swallow */ }
|
|
310
|
+
return null;
|
|
311
|
+
});
|
|
248
312
|
__lastAutoIndexPromise = p;
|
|
249
313
|
} catch {
|
|
250
314
|
// never throw out of indexEntry due to auto-index
|
|
@@ -29,7 +29,12 @@ export class SchemaVersionError extends Error {
|
|
|
29
29
|
// Discover and load every migration module under ./migrations/, sorted by
|
|
30
30
|
// numeric prefix ascending. Each module must export VERSION (integer),
|
|
31
31
|
// DESCRIPTION (string), and up(db) (function).
|
|
32
|
-
|
|
32
|
+
//
|
|
33
|
+
// Exported (v1.5.1 W3.B) so search.js (and any other consumer that needs
|
|
34
|
+
// the sync migration pipeline) can reuse the SAME discovery path instead
|
|
35
|
+
// of maintaining a parallel hardcoded list. Single source of truth: drop
|
|
36
|
+
// a new NNN-name.js into ./migrations/ and every consumer sees it.
|
|
37
|
+
export async function loadMigrations() {
|
|
33
38
|
let files;
|
|
34
39
|
try {
|
|
35
40
|
files = readdirSync(MIGRATIONS_DIR);
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// IJFW v1.5.0 -- memory migration 004: Graphiti-style bi-temporal validity edges.
|
|
2
|
+
//
|
|
3
|
+
// Source authority: v1.5.0 audit finding H5.4. Closes the spec-vs-impl gap where
|
|
4
|
+
// the fact stream APPENDED forever -- contradictory facts about the same
|
|
5
|
+
// (subject, predicate) accumulated instead of invalidating priors. After this
|
|
6
|
+
// migration, storing a NEW fact with the same (subject, predicate) but a
|
|
7
|
+
// DIFFERENT object will close the prior fact's valid_to and only the newest
|
|
8
|
+
// becomes "currently valid".
|
|
9
|
+
//
|
|
10
|
+
// Spec called for the migration filename to be "<NNN>-bitemporal.sql". The
|
|
11
|
+
// memory migration runner (./migration-runner.js) only discovers `.js`
|
|
12
|
+
// modules matching /^\d+-.+\.js$/, so the canonical pattern is .js with the
|
|
13
|
+
// SQL embedded. Same effect as a .sql file: the migration runner wraps up()
|
|
14
|
+
// in BEGIN IMMEDIATE, so DDL + INSERT OR IGNORE inside still apply atomically
|
|
15
|
+
// and roll back to user_version 3 on any failure.
|
|
16
|
+
//
|
|
17
|
+
// Adds the `facts` table fresh (no pre-existing facts table to ALTER):
|
|
18
|
+
// id INTEGER PRIMARY KEY AUTOINCREMENT
|
|
19
|
+
// subject TEXT NOT NULL
|
|
20
|
+
// predicate TEXT NOT NULL
|
|
21
|
+
// object TEXT NOT NULL
|
|
22
|
+
// confidence REAL DEFAULT 1.0
|
|
23
|
+
// memory_id TEXT -- join key back to journal entry
|
|
24
|
+
// source TEXT -- "memory_store:<type>" or test caller
|
|
25
|
+
// valid_from TEXT NOT NULL -- ISO-8601 timestamp; default current ts
|
|
26
|
+
// valid_to TEXT -- NULL = currently valid; else when invalidated
|
|
27
|
+
// created_at INTEGER NOT NULL -- unix ms wall-clock for ordering
|
|
28
|
+
//
|
|
29
|
+
// Indexes:
|
|
30
|
+
// facts_current_idx -- (subject, predicate, valid_to) supports the canonical
|
|
31
|
+
// "is this (subject, predicate) currently valid?" query
|
|
32
|
+
// used by invalidateOlderFacts and getValidAt.
|
|
33
|
+
// facts_subject_predicate_idx -- (subject, predicate, valid_from) supports
|
|
34
|
+
// getHistory's ORDER BY scan.
|
|
35
|
+
//
|
|
36
|
+
// Backwards compat:
|
|
37
|
+
// - Existing facts.jsonl sidecar continues to be appended-to in handleStore.
|
|
38
|
+
// Migration 004 is ADDITIVE -- it brings up a SQL twin of the JSONL stream
|
|
39
|
+
// so getValidAt() / getHistory() can answer point-in-time questions the
|
|
40
|
+
// JSONL stream cannot answer cheaply.
|
|
41
|
+
// - For any installation that has facts.jsonl rows from prior versions, the
|
|
42
|
+
// SQL table starts empty. New stores fill it. Backfill from JSONL is a
|
|
43
|
+
// separate ops task (out of scope for this migration; spec says "Existing
|
|
44
|
+
// rows get valid_from=NOW(), valid_to=NULL on migrate" -- since the SQL
|
|
45
|
+
// table is born empty here, that invariant holds by construction).
|
|
46
|
+
//
|
|
47
|
+
// Crash safety: migration runner wraps up() in BEGIN IMMEDIATE. If any DDL
|
|
48
|
+
// fails the txn rolls back and user_version stays at 3.
|
|
49
|
+
|
|
50
|
+
export const VERSION = 4;
|
|
51
|
+
export const DESCRIPTION = 'memory v1.5.0 -- bi-temporal facts table (Graphiti-style valid_from/valid_to)';
|
|
52
|
+
|
|
53
|
+
// SQL DDL stored as a constant so the diff stays readable and the migration
|
|
54
|
+
// runner's BEGIN IMMEDIATE wrapper applies it atomically via the SQLite
|
|
55
|
+
// driver's multi-statement runner. Note: db.exec here is the sqlite driver's
|
|
56
|
+
// SQL exec, not a shell exec; pre-commit hook may flag the verb name.
|
|
57
|
+
const SQL_CREATE_TABLE = (
|
|
58
|
+
'CREATE TABLE IF NOT EXISTS facts (' +
|
|
59
|
+
'id INTEGER PRIMARY KEY AUTOINCREMENT,' +
|
|
60
|
+
'subject TEXT NOT NULL,' +
|
|
61
|
+
'predicate TEXT NOT NULL,' +
|
|
62
|
+
'object TEXT NOT NULL,' +
|
|
63
|
+
'confidence REAL DEFAULT 1.0,' +
|
|
64
|
+
'memory_id TEXT,' +
|
|
65
|
+
'source TEXT,' +
|
|
66
|
+
"valid_from TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))," +
|
|
67
|
+
'valid_to TEXT,' +
|
|
68
|
+
"created_at INTEGER NOT NULL DEFAULT (CAST(strftime('%s','now') AS INTEGER) * 1000)" +
|
|
69
|
+
')'
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const SQL_CURRENT_IDX = (
|
|
73
|
+
'CREATE INDEX IF NOT EXISTS facts_current_idx ' +
|
|
74
|
+
'ON facts(subject, predicate, valid_to)'
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const SQL_HISTORY_IDX = (
|
|
78
|
+
'CREATE INDEX IF NOT EXISTS facts_subject_predicate_idx ' +
|
|
79
|
+
'ON facts(subject, predicate, valid_from)'
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
export function up(db) {
|
|
83
|
+
// SQLite's .exec runs multi-statement DDL inside the migration runner's
|
|
84
|
+
// BEGIN IMMEDIATE transaction. CREATE TABLE IF NOT EXISTS keeps the
|
|
85
|
+
// migration idempotent for re-application scenarios.
|
|
86
|
+
db.exec(SQL_CREATE_TABLE);
|
|
87
|
+
db.exec(SQL_CURRENT_IDX);
|
|
88
|
+
db.exec(SQL_HISTORY_IDX);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export default { version: VERSION, description: DESCRIPTION, up };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// IJFW v1.5.0 -- memory migration 005: persistent embedding cache.
|
|
2
|
+
//
|
|
3
|
+
// Source authority: v1.5.0 audit MED #7 (memory-engine.md F-PRF-4). The
|
|
4
|
+
// cold-tier hybrid rerank in src/search-hybrid.js was re-embedding every
|
|
5
|
+
// candidate snippet on every query. For repeated queries over a stable
|
|
6
|
+
// corpus this is wasted work: the same (snippet_text, model_id) pair always
|
|
7
|
+
// produces the same vector. This migration adds a `memory_entry_vectors`
|
|
8
|
+
// table so embedding becomes a write-once-per-(content, model) operation.
|
|
9
|
+
//
|
|
10
|
+
// v1.5.0 wire-W1.C key change: PK is content-hash (sha256(snippet)) rather
|
|
11
|
+
// than memory_id. The hybrid rerank's input candidates do not carry a stable
|
|
12
|
+
// memory_entry id (the corpus is built per-query from heterogenous markdown
|
|
13
|
+
// + memory sources), so a content-hash PK is the only key that makes the
|
|
14
|
+
// cache actually wire into production. Migration 005 was new in v1.5.0 and
|
|
15
|
+
// never shipped under the memory_id schema, so the schema change is non-
|
|
16
|
+
// breaking -- no rows to migrate.
|
|
17
|
+
//
|
|
18
|
+
// Schema:
|
|
19
|
+
// cache_key TEXT NOT NULL -- sha256(snippet) in hex, lowercase
|
|
20
|
+
// model_id TEXT NOT NULL -- e.g. 'Xenova/all-MiniLM-L6-v2'
|
|
21
|
+
// embedding BLOB NOT NULL -- Float32Array little-endian, 4B/float
|
|
22
|
+
// created_at TEXT NOT NULL -- ISO 8601 UTC; defaults to "now"
|
|
23
|
+
// PRIMARY KEY (cache_key, model_id)
|
|
24
|
+
//
|
|
25
|
+
// Indexes:
|
|
26
|
+
// memory_entry_vectors_model_idx -- supports lookup by model_id during
|
|
27
|
+
// model migrations (rare; ALTER-equiv).
|
|
28
|
+
//
|
|
29
|
+
// Crash safety: migration runner wraps up() in a transaction; a partial
|
|
30
|
+
// CREATE rolls back to user_version=4.
|
|
31
|
+
|
|
32
|
+
export const VERSION = 5;
|
|
33
|
+
export const DESCRIPTION = 'memory v1.5.0 -- persistent embedding cache (memory_entry_vectors, content-keyed)';
|
|
34
|
+
|
|
35
|
+
const SQL_CREATE = (
|
|
36
|
+
'CREATE TABLE IF NOT EXISTS memory_entry_vectors (' +
|
|
37
|
+
'cache_key TEXT NOT NULL,' +
|
|
38
|
+
'model_id TEXT NOT NULL,' +
|
|
39
|
+
'embedding BLOB NOT NULL,' +
|
|
40
|
+
"created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))," +
|
|
41
|
+
'PRIMARY KEY (cache_key, model_id)' +
|
|
42
|
+
')'
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const SQL_MODEL_IDX = (
|
|
46
|
+
'CREATE INDEX IF NOT EXISTS memory_entry_vectors_model_idx ' +
|
|
47
|
+
'ON memory_entry_vectors(model_id)'
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
export function up(db) {
|
|
51
|
+
// r20-MED fix: drop the table first to handle the case where a developer
|
|
52
|
+
// machine applied an earlier draft of migration 005 with the old
|
|
53
|
+
// memory_id-keyed schema. Since 005 was net-new in v1.5.0 (never shipped
|
|
54
|
+
// to npm), there are no production rows to preserve. The DROP is a no-op
|
|
55
|
+
// on a fresh db.
|
|
56
|
+
db.exec('DROP TABLE IF EXISTS memory_entry_vectors');
|
|
57
|
+
db.exec(SQL_CREATE);
|
|
58
|
+
db.exec(SQL_MODEL_IDX);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export default { version: VERSION, description: DESCRIPTION, up };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// IJFW v1.5.0 -- memory migration 006: Obsidian-grade graph indexing.
|
|
2
|
+
//
|
|
3
|
+
// Adds three tables that make IJFW's hot markdown semantically deeper than
|
|
4
|
+
// "Obsidian-readable": backlinks, hierarchical tags, and inline metadata.
|
|
5
|
+
//
|
|
6
|
+
// memory_links -- directed edges parsed from [[wikilink]] syntax.
|
|
7
|
+
// to_target is the link target as written, normalised
|
|
8
|
+
// to lowercase + dash-collapsed. Resolution happens at
|
|
9
|
+
// query time so unresolved targets do not break ingest.
|
|
10
|
+
// memory_tags -- nested tag mentions. #project/r17/audit splits into
|
|
11
|
+
// path + depth; queries can match prefix.
|
|
12
|
+
// memory_meta -- Dataview-style inline metadata: [key:: value].
|
|
13
|
+
|
|
14
|
+
export const VERSION = 6;
|
|
15
|
+
export const DESCRIPTION = 'obsidian-graph — memory_links, memory_tags, memory_meta tables';
|
|
16
|
+
|
|
17
|
+
export function up(db) {
|
|
18
|
+
db.exec(`
|
|
19
|
+
CREATE TABLE IF NOT EXISTS memory_links (
|
|
20
|
+
from_id TEXT NOT NULL,
|
|
21
|
+
to_target TEXT NOT NULL,
|
|
22
|
+
line INTEGER,
|
|
23
|
+
created_at INTEGER NOT NULL DEFAULT (CAST(strftime('%s','now') AS INTEGER)),
|
|
24
|
+
PRIMARY KEY (from_id, to_target, line)
|
|
25
|
+
);
|
|
26
|
+
CREATE INDEX IF NOT EXISTS memory_links_to_idx ON memory_links(to_target);
|
|
27
|
+
|
|
28
|
+
CREATE TABLE IF NOT EXISTS memory_tags (
|
|
29
|
+
memory_id TEXT NOT NULL,
|
|
30
|
+
tag_path TEXT NOT NULL,
|
|
31
|
+
depth INTEGER NOT NULL,
|
|
32
|
+
created_at INTEGER NOT NULL DEFAULT (CAST(strftime('%s','now') AS INTEGER)),
|
|
33
|
+
PRIMARY KEY (memory_id, tag_path)
|
|
34
|
+
);
|
|
35
|
+
CREATE INDEX IF NOT EXISTS memory_tags_path_idx ON memory_tags(tag_path);
|
|
36
|
+
|
|
37
|
+
CREATE TABLE IF NOT EXISTS memory_meta (
|
|
38
|
+
memory_id TEXT NOT NULL,
|
|
39
|
+
key TEXT NOT NULL,
|
|
40
|
+
value TEXT NOT NULL,
|
|
41
|
+
created_at INTEGER NOT NULL DEFAULT (CAST(strftime('%s','now') AS INTEGER)),
|
|
42
|
+
PRIMARY KEY (memory_id, key, value)
|
|
43
|
+
);
|
|
44
|
+
CREATE INDEX IF NOT EXISTS memory_meta_key_idx ON memory_meta(key);
|
|
45
|
+
`);
|
|
46
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// IJFW v1.5.0 -- memory migration 007: skill telemetry table.
|
|
2
|
+
//
|
|
3
|
+
// Records per-execution skill outcomes so the procedural-memory tier can
|
|
4
|
+
// reorder the skill surface at session start. Wayland pattern.
|
|
5
|
+
|
|
6
|
+
export const VERSION = 7;
|
|
7
|
+
export const DESCRIPTION = 'memory v1.5.0 -- skill_telemetry table for skills-feedback loop';
|
|
8
|
+
|
|
9
|
+
export function up(db) {
|
|
10
|
+
db.exec(`
|
|
11
|
+
CREATE TABLE IF NOT EXISTS skill_telemetry (
|
|
12
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
13
|
+
skill_id TEXT NOT NULL,
|
|
14
|
+
session_id TEXT,
|
|
15
|
+
outcome TEXT NOT NULL CHECK (outcome IN ('success','failure','aborted')),
|
|
16
|
+
latency_ms INTEGER,
|
|
17
|
+
created_at INTEGER NOT NULL
|
|
18
|
+
);
|
|
19
|
+
CREATE INDEX IF NOT EXISTS skill_telemetry_aggr_idx
|
|
20
|
+
ON skill_telemetry(skill_id, outcome, created_at DESC);
|
|
21
|
+
`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default { version: VERSION, description: DESCRIPTION, up };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// IJFW v1.5.0 -- memory migration 008: write-origin provenance.
|
|
2
|
+
//
|
|
3
|
+
// Adds `origin` column to memory_entries (Hermes BACKGROUND_REVIEW pattern,
|
|
4
|
+
// see tools/skill_provenance.py in Hermes — the operator's sibling project).
|
|
5
|
+
// Default 'foreground' so existing rows are treated as user-owned.
|
|
6
|
+
//
|
|
7
|
+
// Origin values established by v1.5.0 memory-moat:
|
|
8
|
+
// 'foreground' user-originated handleStore call (default)
|
|
9
|
+
// 'auto-linker' A-Mem auto-linker write-time evolution
|
|
10
|
+
// 'dream-cycle' background consolidation / promotion writes
|
|
11
|
+
//
|
|
12
|
+
// Future curators (v1.6+) should only auto-modify rows whose origin is
|
|
13
|
+
// in {'auto-linker', 'dream-cycle'} to preserve the operator-owned
|
|
14
|
+
// 'foreground' tier.
|
|
15
|
+
|
|
16
|
+
export const VERSION = 8;
|
|
17
|
+
export const DESCRIPTION =
|
|
18
|
+
'memory v1.5.0 -- write-origin provenance column on memory_entries';
|
|
19
|
+
|
|
20
|
+
export function up(db) {
|
|
21
|
+
// ALTER TABLE ADD COLUMN is idempotent-by-guard since SQLite has no
|
|
22
|
+
// "IF NOT EXISTS" clause for ALTER. Check PRAGMA table_info first.
|
|
23
|
+
const cols = db
|
|
24
|
+
.prepare('PRAGMA table_info(memory_entries)')
|
|
25
|
+
.all()
|
|
26
|
+
.map((c) => c.name);
|
|
27
|
+
if (!cols.includes('origin')) {
|
|
28
|
+
const ALTER_SQL = (
|
|
29
|
+
"ALTER TABLE memory_entries " +
|
|
30
|
+
"ADD COLUMN origin TEXT DEFAULT 'foreground'"
|
|
31
|
+
);
|
|
32
|
+
db.prepare(ALTER_SQL).run();
|
|
33
|
+
}
|
|
34
|
+
const IDX_SQL = (
|
|
35
|
+
'CREATE INDEX IF NOT EXISTS memory_entries_origin_idx ' +
|
|
36
|
+
'ON memory_entries(origin)'
|
|
37
|
+
);
|
|
38
|
+
db.prepare(IDX_SQL).run();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export default { version: VERSION, description: DESCRIPTION, up };
|