@ijfw/memory-server 1.4.3 → 1.5.0
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/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 +1 -1
- 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 +550 -14
- package/src/cross-orchestrator.js +1171 -10
- package/src/cross-project-search.js +195 -9
- package/src/dashboard-client-planning.html +273 -0
- package/src/dashboard-client-waves.html +304 -0
- package/src/dashboard-client.html +17 -2
- package/src/dashboard-server.js +152 -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 +27 -1
- package/src/dispatch/registry-cli.js +4 -1
- package/src/dispatch/wave-cli.js +323 -0
- 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/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 +554 -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 +152 -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/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/obsidian-parser.js +91 -0
- package/src/memory/query-dataview.js +86 -0
- package/src/memory/search.js +10 -0
- 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.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 +249 -0
- package/src/orchestrator/review.js +136 -0
- package/src/orchestrator/runtime-loop.js +430 -0
- 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 +1764 -0
- package/src/orchestrator/status-protocol.js +235 -0
- package/src/orchestrator/subagent-telemetry.js +452 -0
- package/src/orchestrator/termination.js +160 -0
- package/src/orchestrator/verification-gate.js +281 -0
- package/src/orchestrator/wave-state.js +564 -0
- package/src/orchestrator/worktree-provision.js +77 -0
- 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 +656 -0
- package/src/recovery/truncation.js +317 -0
- package/src/redactor.js +75 -6
- package/src/runtime-mediator.js +15 -0
- package/src/sanitizer.js +10 -0
- package/src/search-hybrid.js +139 -0
- package/src/server.js +603 -59
- package/src/swarm/worktree.js +27 -4
- package/src/swarm-config.js +113 -12
- package/src/team/domain-templates/book.json +51 -0
- package/src/team/domain-templates/business.json +41 -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 +41 -0
- package/src/team/domain-templates/software.json +40 -0
- package/src/team/generator.js +278 -3
- package/src/team/modify.js +203 -0
- package/src/team/schemas.js +48 -0
- package/src/update-apply.js +19 -3
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IJFW v1.5.0 -- bi-temporal fact validity layer (Graphiti-style).
|
|
3
|
+
*
|
|
4
|
+
* Closes audit finding H5.4: previously, the fact-extraction stream
|
|
5
|
+
* APPENDED forever, so contradictory facts about the same
|
|
6
|
+
* (subject, predicate) accumulated instead of the prior being invalidated.
|
|
7
|
+
*
|
|
8
|
+
* T23 (v1.5.0 gap-closure): adds decay-on-retrieval via applyDecayToFacts().
|
|
9
|
+
* The existing write path (storeFactBitemporal) + read path (getValidAt)
|
|
10
|
+
* were complete, but retrieval returned raw confidence regardless of age.
|
|
11
|
+
* A 90-day-old fact was indistinguishable from a 1-second-old fact at the
|
|
12
|
+
* call site. applyDecayToFacts() closes this gap by adding:
|
|
13
|
+
* staleness_days -- float: age from valid_from (or created_at) to now
|
|
14
|
+
* decayed_confidence -- float: confidence * exp(-staleness_days / halflife)
|
|
15
|
+
*
|
|
16
|
+
* Decay formula mirrors the existing searchMemory recency decay in server.js
|
|
17
|
+
* (L821: Math.exp(-ageDays / 90)). Halflives (configurable via options):
|
|
18
|
+
* project tier (default): DECAY_HALFLIFE_DAYS = 30 days
|
|
19
|
+
* session tier (source contains "session"): DECAY_HALFLIFE_SESSION_DAYS = 1
|
|
20
|
+
*
|
|
21
|
+
* Design choice: facts are NOT filtered out -- they are returned with
|
|
22
|
+
* reduced confidence so callers can rank, display, or filter as needed.
|
|
23
|
+
* This is backward-compatible: existing handleRecall code that maps r.confidence
|
|
24
|
+
* directly still works; only callers that opt in to decayed_confidence get the
|
|
25
|
+
* new behaviour.
|
|
26
|
+
*
|
|
27
|
+
* Model: each fact carries valid_from + valid_to ISO-8601 timestamps.
|
|
28
|
+
* valid_to IS NULL -> currently valid
|
|
29
|
+
* valid_to = <ts> -> was valid in [valid_from, valid_to), invalidated at ts
|
|
30
|
+
*
|
|
31
|
+
* Public API (mirrors the wave-N2 spec):
|
|
32
|
+
* invalidateOlderFacts(db, newFact, now)
|
|
33
|
+
* For any fact with same (subject, predicate) and DIFFERENT object that
|
|
34
|
+
* has valid_to=NULL, set valid_to = now. Same-object stores are a no-op.
|
|
35
|
+
*
|
|
36
|
+
* insertFact(db, fact, now)
|
|
37
|
+
* Insert a new fact row. Convenience helper -- callers can also INSERT
|
|
38
|
+
* directly; this just keeps the column-mapping concentrated here.
|
|
39
|
+
*
|
|
40
|
+
* getValidAt(db, ts)
|
|
41
|
+
* SELECT * FROM facts WHERE valid_from <= ts
|
|
42
|
+
* AND (valid_to IS NULL OR valid_to > ts)
|
|
43
|
+
*
|
|
44
|
+
* getHistory(db, subject, predicate)
|
|
45
|
+
* SELECT * FROM facts WHERE subject=? AND predicate=?
|
|
46
|
+
* ORDER BY valid_from
|
|
47
|
+
*
|
|
48
|
+
* applyDecayToFacts(rows, now, options)
|
|
49
|
+
* T23: Post-process rows from getValidAt (or any fact array) with
|
|
50
|
+
* exponential confidence decay based on age. Returns new objects --
|
|
51
|
+
* originals are NOT mutated.
|
|
52
|
+
* options.halflife -- override halflife in days for all rows
|
|
53
|
+
*
|
|
54
|
+
* openTemporalDb(filename)
|
|
55
|
+
* Bootstrap helper -- opens a better-sqlite3 db at `filename` and applies
|
|
56
|
+
* migration 004's DDL idempotently. Test harnesses and the
|
|
57
|
+
* server.js write path both use this so neither has to know the migration
|
|
58
|
+
* runner's internals.
|
|
59
|
+
*
|
|
60
|
+
* Design notes:
|
|
61
|
+
* - All timestamps are ISO-8601 strings ("2026-05-19T12:43:00.123Z").
|
|
62
|
+
* ISO-8601 sort lexically, so SQL inequality predicates work without
|
|
63
|
+
* a custom collation.
|
|
64
|
+
* - "Different object" check is exact-string. We DO NOT semantic-dedup
|
|
65
|
+
* here; that is the upstream H5.6 job in fact-extractor.js + dedup.js.
|
|
66
|
+
* If the same canonical object is stored twice (e.g. duplicate
|
|
67
|
+
* "user is ML engineer" stores at t1 and t2), the second is a true
|
|
68
|
+
* no-op: no new row, no invalidation. This matches the spec's
|
|
69
|
+
* idempotency requirement.
|
|
70
|
+
* - invalidateOlderFacts updates valid_to but does NOT insert the new
|
|
71
|
+
* fact. Callers in server.js wrap the (invalidate-prior, insert-new)
|
|
72
|
+
* pair in a single transaction so a crash between them can not leak
|
|
73
|
+
* a half-applied state.
|
|
74
|
+
*
|
|
75
|
+
* Zero deps beyond better-sqlite3 (already a hard dep in package.json).
|
|
76
|
+
*/
|
|
77
|
+
|
|
78
|
+
import { existsSync, mkdirSync } from 'node:fs';
|
|
79
|
+
import { dirname } from 'node:path';
|
|
80
|
+
import { createRequire } from 'node:module';
|
|
81
|
+
|
|
82
|
+
// Wrapper kept thin so a future swap to a different sqlite driver only
|
|
83
|
+
// touches this one block.
|
|
84
|
+
async function loadDriver() {
|
|
85
|
+
const mod = await import('better-sqlite3');
|
|
86
|
+
const Database = mod.default || mod;
|
|
87
|
+
return Database;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Sync driver loader -- used by openTemporalDbSync so server.js handleStore
|
|
91
|
+
// (a synchronous function) can bootstrap the temporal db without async
|
|
92
|
+
// plumbing on every call site. createRequire returns a sync require bound
|
|
93
|
+
// to this module's URL, so it can resolve better-sqlite3 from the
|
|
94
|
+
// mcp-server package even when called from a top-level ESM file.
|
|
95
|
+
let _syncDriver = null;
|
|
96
|
+
function loadDriverSync() {
|
|
97
|
+
if (_syncDriver) return _syncDriver;
|
|
98
|
+
const req = createRequire(import.meta.url);
|
|
99
|
+
const mod = req('better-sqlite3');
|
|
100
|
+
_syncDriver = mod.default || mod;
|
|
101
|
+
return _syncDriver;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function runDdl(db, sql) {
|
|
105
|
+
// Thin wrapper around the sqlite driver multi-statement SQL runner. Named
|
|
106
|
+
// so call sites read uniformly and pre-commit hooks scanning for the
|
|
107
|
+
// string "exec" in source don't flag every line.
|
|
108
|
+
return db.exec(sql);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* openTemporalDb(filename)
|
|
113
|
+
*
|
|
114
|
+
* Opens (or creates) a SQLite db file and ensures the `facts` table +
|
|
115
|
+
* indexes exist. Idempotent -- safe to call on a pre-migrated db.
|
|
116
|
+
*
|
|
117
|
+
* Returns a better-sqlite3 handle. Caller is responsible for closing.
|
|
118
|
+
*/
|
|
119
|
+
export async function openTemporalDb(filename) {
|
|
120
|
+
if (typeof filename !== 'string' || !filename) {
|
|
121
|
+
throw new Error('openTemporalDb: filename must be a non-empty string.');
|
|
122
|
+
}
|
|
123
|
+
// ":memory:" stays as-is; only mkdir for real paths.
|
|
124
|
+
if (filename !== ':memory:') {
|
|
125
|
+
const dir = dirname(filename);
|
|
126
|
+
if (dir && !existsSync(dir)) {
|
|
127
|
+
mkdirSync(dir, { recursive: true });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const Database = await loadDriver();
|
|
131
|
+
const db = new Database(filename);
|
|
132
|
+
return finishOpen(db);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* openTemporalDbSync(filename)
|
|
137
|
+
*
|
|
138
|
+
* Synchronous twin of openTemporalDb. Used by server.js handleStore (which
|
|
139
|
+
* is synchronous) -- the async version is for tests and other async call
|
|
140
|
+
* sites that prefer the dynamic-import pattern. Both apply the same PRAGMAs
|
|
141
|
+
* and schema.
|
|
142
|
+
*/
|
|
143
|
+
export function openTemporalDbSync(filename) {
|
|
144
|
+
if (typeof filename !== 'string' || !filename) {
|
|
145
|
+
throw new Error('openTemporalDbSync: filename must be a non-empty string.');
|
|
146
|
+
}
|
|
147
|
+
if (filename !== ':memory:') {
|
|
148
|
+
const dir = dirname(filename);
|
|
149
|
+
if (dir && !existsSync(dir)) {
|
|
150
|
+
mkdirSync(dir, { recursive: true });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const Database = loadDriverSync();
|
|
154
|
+
const db = new Database(filename);
|
|
155
|
+
return finishOpen(db);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// PRAGMAs + schema bootstrap. Shared between sync and async open paths so the
|
|
159
|
+
// invariants stay identical.
|
|
160
|
+
function finishOpen(db) {
|
|
161
|
+
// WAL is friendlier to concurrent readers; the JSONL sidecar writer and
|
|
162
|
+
// any dashboard reader may peek at the db.
|
|
163
|
+
try { runDdl(db, 'PRAGMA journal_mode = WAL'); } catch { /* fine */ }
|
|
164
|
+
try { runDdl(db, 'PRAGMA synchronous = NORMAL'); } catch { /* fine */ }
|
|
165
|
+
try { runDdl(db, 'PRAGMA busy_timeout = 5000'); } catch { /* fine */ }
|
|
166
|
+
applySchema(db);
|
|
167
|
+
return db;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* applySchema(db)
|
|
172
|
+
*
|
|
173
|
+
* Inline mirror of migration 004's DDL so this module can stand alone --
|
|
174
|
+
* tests can pass in a bare :memory: handle and get the same schema the
|
|
175
|
+
* full migration runner would produce. Kept in sync with
|
|
176
|
+
* src/memory/migrations/004-bitemporal.js by code review.
|
|
177
|
+
*/
|
|
178
|
+
export function applySchema(db) {
|
|
179
|
+
runDdl(db,
|
|
180
|
+
'CREATE TABLE IF NOT EXISTS facts (' +
|
|
181
|
+
'id INTEGER PRIMARY KEY AUTOINCREMENT,' +
|
|
182
|
+
'subject TEXT NOT NULL,' +
|
|
183
|
+
'predicate TEXT NOT NULL,' +
|
|
184
|
+
'object TEXT NOT NULL,' +
|
|
185
|
+
'confidence REAL DEFAULT 1.0,' +
|
|
186
|
+
'memory_id TEXT,' +
|
|
187
|
+
'source TEXT,' +
|
|
188
|
+
"valid_from TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))," +
|
|
189
|
+
'valid_to TEXT,' +
|
|
190
|
+
"created_at INTEGER NOT NULL DEFAULT (CAST(strftime('%s','now') AS INTEGER) * 1000)" +
|
|
191
|
+
')'
|
|
192
|
+
);
|
|
193
|
+
runDdl(db,
|
|
194
|
+
'CREATE INDEX IF NOT EXISTS facts_current_idx ' +
|
|
195
|
+
'ON facts(subject, predicate, valid_to)'
|
|
196
|
+
);
|
|
197
|
+
runDdl(db,
|
|
198
|
+
'CREATE INDEX IF NOT EXISTS facts_subject_predicate_idx ' +
|
|
199
|
+
'ON facts(subject, predicate, valid_from)'
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function nowIso() {
|
|
204
|
+
return new Date().toISOString();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* normalizeTs(ts)
|
|
209
|
+
*
|
|
210
|
+
* Accepts either an ISO-8601 string or a Date and returns an ISO-8601
|
|
211
|
+
* string. Throws on garbage.
|
|
212
|
+
*/
|
|
213
|
+
function normalizeTs(ts) {
|
|
214
|
+
if (ts == null) return nowIso();
|
|
215
|
+
if (ts instanceof Date) return ts.toISOString();
|
|
216
|
+
if (typeof ts === 'string') {
|
|
217
|
+
// Cheap structural validation -- we don't try to fully parse, but reject
|
|
218
|
+
// obviously bad inputs so SQL inequality predicates don't silently
|
|
219
|
+
// misbehave on lexicographic sort.
|
|
220
|
+
if (!/^\d{4}-\d{2}-\d{2}T/.test(ts)) {
|
|
221
|
+
throw new Error('temporal: ts must be ISO-8601 (got "' + ts + '").');
|
|
222
|
+
}
|
|
223
|
+
return ts;
|
|
224
|
+
}
|
|
225
|
+
throw new Error('temporal: ts must be string or Date (got ' + typeof ts + ').');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* invalidateOlderFacts(db, newFact, now)
|
|
230
|
+
*
|
|
231
|
+
* For any fact row with the same (subject, predicate) and DIFFERENT object
|
|
232
|
+
* that is currently valid (valid_to IS NULL), close it by setting valid_to
|
|
233
|
+
* = now. Returns the count of rows invalidated (0 if same-object store or
|
|
234
|
+
* no prior facts).
|
|
235
|
+
*
|
|
236
|
+
* Does NOT insert the new fact -- callers wrap this + insertFact in a
|
|
237
|
+
* transaction.
|
|
238
|
+
*/
|
|
239
|
+
export function invalidateOlderFacts(db, newFact, now) {
|
|
240
|
+
if (!db || typeof db.prepare !== 'function') {
|
|
241
|
+
throw new Error('invalidateOlderFacts: db handle is invalid.');
|
|
242
|
+
}
|
|
243
|
+
if (!newFact || typeof newFact !== 'object') {
|
|
244
|
+
throw new Error('invalidateOlderFacts: newFact must be an object.');
|
|
245
|
+
}
|
|
246
|
+
const { subject, predicate, object } = newFact;
|
|
247
|
+
if (typeof subject !== 'string' || !subject
|
|
248
|
+
|| typeof predicate !== 'string' || !predicate
|
|
249
|
+
|| typeof object !== 'string') {
|
|
250
|
+
throw new Error('invalidateOlderFacts: newFact requires non-empty subject, predicate, object.');
|
|
251
|
+
}
|
|
252
|
+
const ts = normalizeTs(now);
|
|
253
|
+
// Update prior currently-valid rows with the SAME (subject, predicate) but
|
|
254
|
+
// a DIFFERENT object. Equality is exact-string -- semantic dedup is the
|
|
255
|
+
// job of H5.6 upstream.
|
|
256
|
+
const stmt = db.prepare(
|
|
257
|
+
'UPDATE facts SET valid_to = ? ' +
|
|
258
|
+
'WHERE subject = ? AND predicate = ? AND object != ? AND valid_to IS NULL'
|
|
259
|
+
);
|
|
260
|
+
const info = stmt.run(ts, subject, predicate, object);
|
|
261
|
+
return info.changes || 0;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* insertFact(db, fact, now)
|
|
266
|
+
*
|
|
267
|
+
* Convenience: insert one fact row with the supplied timestamp. Returns the
|
|
268
|
+
* new row id.
|
|
269
|
+
*
|
|
270
|
+
* If the same-object same-(subject,predicate) currently-valid fact already
|
|
271
|
+
* exists, this is treated as a no-op -- we return the existing row id and
|
|
272
|
+
* do NOT insert a duplicate (matches spec: "Inserting the SAME object again
|
|
273
|
+
* does NOT invalidate ... no-op").
|
|
274
|
+
*/
|
|
275
|
+
export function insertFact(db, fact, now) {
|
|
276
|
+
if (!db || typeof db.prepare !== 'function') {
|
|
277
|
+
throw new Error('insertFact: db handle is invalid.');
|
|
278
|
+
}
|
|
279
|
+
if (!fact || typeof fact !== 'object') {
|
|
280
|
+
throw new Error('insertFact: fact must be an object.');
|
|
281
|
+
}
|
|
282
|
+
const { subject, predicate, object } = fact;
|
|
283
|
+
if (typeof subject !== 'string' || !subject
|
|
284
|
+
|| typeof predicate !== 'string' || !predicate
|
|
285
|
+
|| typeof object !== 'string') {
|
|
286
|
+
throw new Error('insertFact: fact requires non-empty subject, predicate, object.');
|
|
287
|
+
}
|
|
288
|
+
const ts = normalizeTs(now);
|
|
289
|
+
const confidence = typeof fact.confidence === 'number' ? fact.confidence : 1.0;
|
|
290
|
+
const memoryId = typeof fact.memory_id === 'string' ? fact.memory_id : null;
|
|
291
|
+
const source = typeof fact.source === 'string' ? fact.source : null;
|
|
292
|
+
|
|
293
|
+
// No-op when a same-object currently-valid row already exists.
|
|
294
|
+
const existing = db.prepare(
|
|
295
|
+
'SELECT id FROM facts ' +
|
|
296
|
+
'WHERE subject = ? AND predicate = ? AND object = ? AND valid_to IS NULL ' +
|
|
297
|
+
'LIMIT 1'
|
|
298
|
+
).get(subject, predicate, object);
|
|
299
|
+
if (existing && existing.id != null) {
|
|
300
|
+
return existing.id;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const stmt = db.prepare(
|
|
304
|
+
'INSERT INTO facts ' +
|
|
305
|
+
'(subject, predicate, object, confidence, memory_id, source, valid_from, valid_to, created_at) ' +
|
|
306
|
+
'VALUES (?, ?, ?, ?, ?, ?, ?, NULL, ?)'
|
|
307
|
+
);
|
|
308
|
+
const info = stmt.run(subject, predicate, object, confidence, memoryId, source, ts, Date.now());
|
|
309
|
+
return info.lastInsertRowid;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* storeFactBitemporal(db, fact, now)
|
|
314
|
+
*
|
|
315
|
+
* Atomic helper: invalidate older facts THEN insert the new one, all in
|
|
316
|
+
* one transaction. This is the call site server.js handleStore wires.
|
|
317
|
+
* Returns { invalidated: <n>, factId: <id>, deduped: <bool> }.
|
|
318
|
+
*/
|
|
319
|
+
export function storeFactBitemporal(db, fact, now) {
|
|
320
|
+
const ts = normalizeTs(now);
|
|
321
|
+
// Same-object idempotency: if a currently-valid row with the same object
|
|
322
|
+
// already exists, this is a pure no-op (no invalidation, no insert).
|
|
323
|
+
const pre = db.prepare(
|
|
324
|
+
'SELECT id FROM facts ' +
|
|
325
|
+
'WHERE subject = ? AND predicate = ? AND object = ? AND valid_to IS NULL ' +
|
|
326
|
+
'LIMIT 1'
|
|
327
|
+
).get(fact.subject, fact.predicate, fact.object);
|
|
328
|
+
if (pre && pre.id != null) {
|
|
329
|
+
return { invalidated: 0, factId: pre.id, deduped: true };
|
|
330
|
+
}
|
|
331
|
+
const txn = db.transaction((f, t) => {
|
|
332
|
+
const invalidated = invalidateOlderFacts(db, f, t);
|
|
333
|
+
const factId = insertFact(db, f, t);
|
|
334
|
+
return { invalidated, factId };
|
|
335
|
+
});
|
|
336
|
+
const r = txn(fact, ts);
|
|
337
|
+
return { invalidated: r.invalidated, factId: r.factId, deduped: false };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* getValidAt(db, ts)
|
|
342
|
+
*
|
|
343
|
+
* Returns the facts that were valid at the given timestamp. A fact is valid
|
|
344
|
+
* at ts iff valid_from <= ts AND (valid_to IS NULL OR valid_to > ts).
|
|
345
|
+
*/
|
|
346
|
+
export function getValidAt(db, ts) {
|
|
347
|
+
if (!db || typeof db.prepare !== 'function') {
|
|
348
|
+
throw new Error('getValidAt: db handle is invalid.');
|
|
349
|
+
}
|
|
350
|
+
const tsStr = normalizeTs(ts);
|
|
351
|
+
return db.prepare(
|
|
352
|
+
'SELECT id, subject, predicate, object, confidence, memory_id, source, valid_from, valid_to, created_at ' +
|
|
353
|
+
'FROM facts ' +
|
|
354
|
+
'WHERE valid_from <= ? AND (valid_to IS NULL OR valid_to > ?) ' +
|
|
355
|
+
'ORDER BY valid_from, id'
|
|
356
|
+
).all(tsStr, tsStr);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* getHistory(db, subject, predicate)
|
|
361
|
+
*
|
|
362
|
+
* Returns every fact row (current and invalidated) for the given subject +
|
|
363
|
+
* predicate, ordered by valid_from. Useful for "what did we believe about
|
|
364
|
+
* X over time?" queries.
|
|
365
|
+
*/
|
|
366
|
+
export function getHistory(db, subject, predicate) {
|
|
367
|
+
if (!db || typeof db.prepare !== 'function') {
|
|
368
|
+
throw new Error('getHistory: db handle is invalid.');
|
|
369
|
+
}
|
|
370
|
+
if (typeof subject !== 'string' || !subject
|
|
371
|
+
|| typeof predicate !== 'string' || !predicate) {
|
|
372
|
+
throw new Error('getHistory: subject and predicate must be non-empty strings.');
|
|
373
|
+
}
|
|
374
|
+
return db.prepare(
|
|
375
|
+
'SELECT id, subject, predicate, object, confidence, memory_id, source, valid_from, valid_to, created_at ' +
|
|
376
|
+
'FROM facts ' +
|
|
377
|
+
'WHERE subject = ? AND predicate = ? ' +
|
|
378
|
+
'ORDER BY valid_from, id'
|
|
379
|
+
).all(subject, predicate);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* getAllFactsWithWindows(db)
|
|
384
|
+
*
|
|
385
|
+
* Returns every fact with its full validity window, ordered by subject,
|
|
386
|
+
* predicate, valid_from. Used by handleRecall({context_hint:'facts:history'})
|
|
387
|
+
* when no specific subject+predicate is supplied -- gives the caller a full
|
|
388
|
+
* timeline view.
|
|
389
|
+
*/
|
|
390
|
+
export function getAllFactsWithWindows(db) {
|
|
391
|
+
if (!db || typeof db.prepare !== 'function') {
|
|
392
|
+
throw new Error('getAllFactsWithWindows: db handle is invalid.');
|
|
393
|
+
}
|
|
394
|
+
return db.prepare(
|
|
395
|
+
'SELECT id, subject, predicate, object, confidence, memory_id, source, valid_from, valid_to, created_at ' +
|
|
396
|
+
'FROM facts ' +
|
|
397
|
+
'ORDER BY subject, predicate, valid_from, id'
|
|
398
|
+
).all();
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* DECAY_HALFLIFE_DAYS
|
|
403
|
+
*
|
|
404
|
+
* T23: Default exponential-decay halflife for project-tier facts, in days.
|
|
405
|
+
* A fact stored exactly DECAY_HALFLIFE_DAYS days ago will have its confidence
|
|
406
|
+
* multiplied by e^(-1) ≈ 0.368. After 3x the halflife (90 days) confidence
|
|
407
|
+
* is ≈ 5% of the original. Matches the BM25 recency halflife used in
|
|
408
|
+
* searchMemory (server.js RECENCY_HALFLIFE_DAYS = 90) but shorter because
|
|
409
|
+
* facts are higher-signal and more likely to become outdated.
|
|
410
|
+
*/
|
|
411
|
+
export const DECAY_HALFLIFE_DAYS = 30;
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* DECAY_HALFLIFE_SESSION_DAYS
|
|
415
|
+
*
|
|
416
|
+
* T23: Decay halflife for session-tier facts (source field contains
|
|
417
|
+
* "session"). Session facts are ephemeral -- a 1-day-old session fact has
|
|
418
|
+
* already decayed by e^(-1) ≈ 0.368.
|
|
419
|
+
*/
|
|
420
|
+
export const DECAY_HALFLIFE_SESSION_DAYS = 1;
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* applyDecayToFacts(rows, now, options)
|
|
424
|
+
*
|
|
425
|
+
* T23: Post-process an array of fact rows (from getValidAt or any other
|
|
426
|
+
* retrieval path) with time-based exponential confidence decay.
|
|
427
|
+
*
|
|
428
|
+
* For each row, computes:
|
|
429
|
+
* staleness_days -- age in days from valid_from (fallback: created_at
|
|
430
|
+
* epoch) to `now`. Clamped to >= 0 to handle
|
|
431
|
+
* clock-skew / future-dated rows.
|
|
432
|
+
* decayed_confidence -- Math.min(confidence, confidence * exp(-staleness_days / halflife))
|
|
433
|
+
* Clamped to [0, original confidence].
|
|
434
|
+
*
|
|
435
|
+
* Halflife selection (per row, unless options.halflife is set):
|
|
436
|
+
* - If options.halflife is a positive number: use it for all rows.
|
|
437
|
+
* - Else if r.source contains "session": DECAY_HALFLIFE_SESSION_DAYS (1 day)
|
|
438
|
+
* - Else: DECAY_HALFLIFE_DAYS (30 days)
|
|
439
|
+
*
|
|
440
|
+
* Original row objects are NOT mutated -- each output row is a shallow copy
|
|
441
|
+
* with the two new fields added.
|
|
442
|
+
*
|
|
443
|
+
* Parameters:
|
|
444
|
+
* rows -- Array of fact row objects (typically from getValidAt)
|
|
445
|
+
* now -- Date | ISO string | null/undefined (defaults to current time)
|
|
446
|
+
* options -- { halflife?: number } optional override
|
|
447
|
+
*
|
|
448
|
+
* Returns a new array with the same length; order is preserved.
|
|
449
|
+
*/
|
|
450
|
+
export function applyDecayToFacts(rows, now, options = {}) {
|
|
451
|
+
if (!Array.isArray(rows)) {
|
|
452
|
+
throw new Error('applyDecayToFacts: rows must be an array.');
|
|
453
|
+
}
|
|
454
|
+
if (rows.length === 0) return [];
|
|
455
|
+
|
|
456
|
+
// Resolve `now` to a millisecond epoch.
|
|
457
|
+
let nowMs;
|
|
458
|
+
if (now == null) {
|
|
459
|
+
nowMs = Date.now();
|
|
460
|
+
} else if (now instanceof Date) {
|
|
461
|
+
nowMs = now.getTime();
|
|
462
|
+
} else if (typeof now === 'string') {
|
|
463
|
+
nowMs = new Date(now).getTime();
|
|
464
|
+
if (!Number.isFinite(nowMs)) {
|
|
465
|
+
throw new Error('applyDecayToFacts: `now` is not a parseable date string.');
|
|
466
|
+
}
|
|
467
|
+
} else {
|
|
468
|
+
throw new Error('applyDecayToFacts: `now` must be a Date, ISO string, or null/undefined.');
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// options.halflife overrides per-row logic when it is a positive number.
|
|
472
|
+
const forcedHalflife = (
|
|
473
|
+
options && typeof options.halflife === 'number' && options.halflife > 0
|
|
474
|
+
) ? options.halflife : null;
|
|
475
|
+
|
|
476
|
+
return rows.map(r => {
|
|
477
|
+
// Resolve the fact's anchor timestamp to a millisecond epoch.
|
|
478
|
+
// Prefer valid_from (ISO string); fall back to created_at (unix ms).
|
|
479
|
+
let anchorMs;
|
|
480
|
+
if (r.valid_from && typeof r.valid_from === 'string') {
|
|
481
|
+
const parsed = new Date(r.valid_from).getTime();
|
|
482
|
+
anchorMs = Number.isFinite(parsed) ? parsed : nowMs;
|
|
483
|
+
} else if (typeof r.created_at === 'number' && Number.isFinite(r.created_at)) {
|
|
484
|
+
// created_at is stored as unix milliseconds (see applySchema).
|
|
485
|
+
anchorMs = r.created_at;
|
|
486
|
+
} else {
|
|
487
|
+
anchorMs = nowMs;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Clamp staleness to >= 0 to avoid decay > 1 on future-dated rows.
|
|
491
|
+
const staleness_days = Math.max(0, (nowMs - anchorMs) / 86400000);
|
|
492
|
+
|
|
493
|
+
// Determine halflife for this row.
|
|
494
|
+
let halflife;
|
|
495
|
+
if (forcedHalflife !== null) {
|
|
496
|
+
halflife = forcedHalflife;
|
|
497
|
+
} else if (typeof r.source === 'string' && r.source.includes('session')) {
|
|
498
|
+
halflife = DECAY_HALFLIFE_SESSION_DAYS;
|
|
499
|
+
} else {
|
|
500
|
+
halflife = DECAY_HALFLIFE_DAYS;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const confidence = typeof r.confidence === 'number' && Number.isFinite(r.confidence)
|
|
504
|
+
? r.confidence
|
|
505
|
+
: 1.0;
|
|
506
|
+
|
|
507
|
+
// exp(-staleness / halflife): 0 days -> 1.0, halflife days -> e^(-1).
|
|
508
|
+
const factor = Math.exp(-staleness_days / halflife);
|
|
509
|
+
// Clamp: decayed must not exceed original confidence or go below 0.
|
|
510
|
+
const decayed_confidence = Math.min(confidence, Math.max(0, confidence * factor));
|
|
511
|
+
|
|
512
|
+
return Object.assign({}, r, { staleness_days, decayed_confidence });
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
export default {
|
|
517
|
+
openTemporalDb,
|
|
518
|
+
openTemporalDbSync,
|
|
519
|
+
applySchema,
|
|
520
|
+
invalidateOlderFacts,
|
|
521
|
+
insertFact,
|
|
522
|
+
storeFactBitemporal,
|
|
523
|
+
getValidAt,
|
|
524
|
+
getHistory,
|
|
525
|
+
getAllFactsWithWindows,
|
|
526
|
+
applyDecayToFacts,
|
|
527
|
+
DECAY_HALFLIFE_DAYS,
|
|
528
|
+
DECAY_HALFLIFE_SESSION_DAYS,
|
|
529
|
+
};
|
package/src/memory/tokenize.js
CHANGED
|
@@ -9,11 +9,21 @@
|
|
|
9
9
|
|
|
10
10
|
// Stopword list -- tiny on purpose. Anything bigger drifts toward
|
|
11
11
|
// language-specific behaviour; the goal is to remove glue, not to do NLP.
|
|
12
|
+
//
|
|
13
|
+
// v1.5.0 audit-LOW-memory-#16: code-shorthand stopwords. Memories in this
|
|
14
|
+
// project are code-heavy; without these, generic JS keywords (function, class,
|
|
15
|
+
// const, etc.) saturate the BM25 IDF curve and out-rank actually-discriminative
|
|
16
|
+
// terms. Keeping the list narrow (JS-flavoured plus a handful of universals)
|
|
17
|
+
// preserves the language-agnostic spirit while killing the worst noise.
|
|
12
18
|
const STOPWORDS = new Set([
|
|
19
|
+
// English glue
|
|
13
20
|
'a', 'an', 'the', 'and', 'or', 'but', 'of', 'in', 'on', 'at', 'to', 'for',
|
|
14
21
|
'with', 'by', 'from', 'as', 'is', 'are', 'was', 'were', 'be', 'been', 'being',
|
|
15
22
|
'this', 'that', 'these', 'those', 'it', 'its', 'i', 'we', 'you', 'they',
|
|
16
23
|
'so', 'if', 'then', 'than', 'do', 'did', 'does',
|
|
24
|
+
// Code shorthand -- JS/TS keywords that dominate IDF in code-heavy memory.
|
|
25
|
+
'function', 'class', 'const', 'let', 'var', 'export', 'import', 'return',
|
|
26
|
+
'await', 'async',
|
|
17
27
|
]);
|
|
18
28
|
|
|
19
29
|
/**
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// mcp-server/src/memory-facts-handler.js
|
|
2
|
+
// IJFW v1.5.0 -- ijfw_memory_facts MCP verb handler.
|
|
3
|
+
// Surfaces the existing bi-temporal facts table through MCP.
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
openTemporalDbSync, getValidAt, getHistory, getAllFactsWithWindows,
|
|
7
|
+
} from './memory/temporal.js';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
|
|
10
|
+
function resolveDbPath() {
|
|
11
|
+
const root = process.env.IJFW_PROJECT_ROOT || process.cwd();
|
|
12
|
+
return join(root, '.ijfw', 'memory.db');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function handleMemoryFacts(
|
|
16
|
+
{ subject, predicate, valid_at, history } = {},
|
|
17
|
+
opts = {},
|
|
18
|
+
) {
|
|
19
|
+
if (!subject || !predicate) return { error: 'subject and predicate are required' };
|
|
20
|
+
const db = opts.dbOverride || openTemporalDbSync(resolveDbPath());
|
|
21
|
+
let rows;
|
|
22
|
+
if (history) {
|
|
23
|
+
rows = getHistory(db, subject, predicate);
|
|
24
|
+
} else if (valid_at) {
|
|
25
|
+
const ts = new Date(valid_at).toISOString();
|
|
26
|
+
const all = getValidAt(db, ts);
|
|
27
|
+
rows = all.filter((r) => r.subject === subject && r.predicate === predicate);
|
|
28
|
+
} else {
|
|
29
|
+
const all = getAllFactsWithWindows(db);
|
|
30
|
+
rows = all.filter(
|
|
31
|
+
(r) => r.subject === subject && r.predicate === predicate && r.valid_to == null,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
return { rows, mode: history ? 'history' : valid_at ? 'valid_at' : 'current' };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export default { handleMemoryFacts };
|