@gamaze/hicortex 0.22.1 → 0.22.3
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/assets/dashboard.html +209 -108
- package/dist/calibration.d.ts +45 -0
- package/dist/calibration.js +52 -2
- package/dist/classify-domains.js +6 -1
- package/dist/consolidate.js +73 -14
- package/dist/dashboard.d.ts +11 -0
- package/dist/dashboard.js +9 -0
- package/dist/db.js +74 -0
- package/dist/mcp-server.js +35 -8
- package/dist/nightly.js +14 -0
- package/dist/recall-index.d.ts +46 -3
- package/dist/recall-index.js +83 -26
- package/dist/recall-precision.d.ts +212 -0
- package/dist/recall-precision.js +381 -0
- package/dist/types.d.ts +7 -0
- package/package.json +1 -1
- package/server.json +3 -3
package/dist/calibration.js
CHANGED
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
* the removal is never silent.
|
|
25
25
|
*/
|
|
26
26
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
|
-
exports.
|
|
28
|
-
exports.DIAGNOSTIC_ENV_TIER = void 0;
|
|
27
|
+
exports.STAGE_TRUTH_STRENGTH = exports.STAGE_BELIEF_STRENGTH = exports.STAGE_FADING_STRENGTH = exports.STAGE_FADING_DAYS = exports.WEAK_PRIMARY_FLOOR = exports.CORRECTION_REWRITE_MIN_CONFIDENCE = exports.CORRECTION_MIN_SIMILARITY = exports.SUPERSESSION_MIN_SIMILARITY = exports.DEDUP_AUTO_MERGE_THRESHOLD = exports.IMPORTANCE_SETTLE_WINDOW_DAYS = exports.IMPORTANCE_CEILING = exports.BM25_WEIGHT_DOMAIN = exports.BM25_WEIGHT_PROJECT = exports.BM25_WEIGHT_BODY = exports.BOTH_CHANNEL_BOOST = exports.RRF_VECTOR_WEIGHT = exports.RRF_FTS_WEIGHT = exports.RRF_COMPOSITE_WEIGHT = exports.RRF_K = exports.SCOPE_AFFINITY_WEIGHT = exports.SUPERSEDED_DEMOTION = exports.RECENCY_HEAD_DAYS = exports.RECENCY_HEAD_WEIGHT = exports.RECENCY_HOURLY_DECAY = exports.SCORE_RECENCY_WEIGHT = exports.CONNECTIONS_SATURATION_DEGREE = exports.SCORE_CONNECTIONS_WEIGHT = exports.SCORE_STRENGTH_WEIGHT = exports.SCORE_SIMILARITY_WEIGHT = exports.MEMORY_PRECISION_PROMPT_EXCERPT_CHARS = exports.MEMORY_PRECISION_DIVERGENCE_MIN_SHOWN = exports.MEMORY_PRECISION_REDUNDANT_ABOVE = exports.MEMORY_PRECISION_WINDOW_DAYS = exports.RECALL_USES_AXIS_MAX = exports.RECALL_USES_NORMAL_MAX = exports.RECALL_USES_LOW_MAX = exports.RECALL_RESHOW_TURNS = exports.NOVELTY_FLOOR_SLOTS = exports.RECALL_TITLE_CHARS = exports.RECALL_MIN_PROMPT_CHARS = exports.RECALL_MAX_ITEMS = exports.RECALL_MIN_SIMILARITY = exports.SESSION_INTENT_WEIGHT = exports.COLD_EXPOSURE_SLOTS = exports.RECENT_WINDOW_DAYS = exports.RECENT_LIMIT = exports.SEARCH_LIMIT = exports.PROMOTION_STRENGTH_FLOOR = exports.PROMOTION_RATE = exports.DECAY_HALF_LIFE_DAYS = void 0;
|
|
28
|
+
exports.DIAGNOSTIC_ENV_TIER = exports.OLLAMA_FLUSH_WAIT_MS = exports.OLLAMA_FLUSH_EVERY = exports.NUM_CTX = exports.DEFAULT_FIRST_RUN_LOOKBACK_DAYS = exports.ENRICH_STRENGTH_DELTA = void 0;
|
|
29
29
|
exports.resolveNumCtx = resolveNumCtx;
|
|
30
30
|
exports.resolveOllamaFlushEvery = resolveOllamaFlushEvery;
|
|
31
31
|
exports.resolveOllamaFlushWaitMs = resolveOllamaFlushWaitMs;
|
|
@@ -127,6 +127,44 @@ exports.RECALL_USES_NORMAL_MAX = 0.25;
|
|
|
127
127
|
* PROVISIONAL (owner anchor 2026-09-13, #426) — chosen so Overfetching
|
|
128
128
|
* keeps a visible span, not a measured bound. */
|
|
129
129
|
exports.RECALL_USES_AXIS_MAX = 0.30;
|
|
130
|
+
// Memory-precision family (#476) — the console card's deterministic proxies.
|
|
131
|
+
// All four are release-managed per this module's evolution contract: no config
|
|
132
|
+
// keys, nothing hardcoded elsewhere. Values are stored THRESHOLD-FREE (raw
|
|
133
|
+
// cosines in recall_events); the thresholds below are applied at RENDER time
|
|
134
|
+
// only, so recalibrating them never rewrites history.
|
|
135
|
+
/**
|
|
136
|
+
* Retention horizon for the recall_pushes/recall_events sidecar tables AND the
|
|
137
|
+
* longest window the Memory Precision card shows — ONE constant feeds both
|
|
138
|
+
* (the CAPTURE_HEALTH_WINDOW_DAYS single-constant law: the card can never
|
|
139
|
+
* claim a window the store no longer has rows for). 90 = the longest #452
|
|
140
|
+
* range option that the retention can honestly serve; 180d and all clamp to
|
|
141
|
+
* it server-side and the payload echoes the effective window_days.
|
|
142
|
+
*/
|
|
143
|
+
exports.MEMORY_PRECISION_WINDOW_DAYS = 90;
|
|
144
|
+
/**
|
|
145
|
+
* Cosine at/above which a pushed line reads "redundant" (restating the
|
|
146
|
+
* session's standing context — the top lessons + identity every session-start
|
|
147
|
+
* hook injects). Direction-only display guidance, never a gate. 0.80 mirrors
|
|
148
|
+
* SUPERSESSION_MIN_SIMILARITY: at/above it a pair reads as the same
|
|
149
|
+
* statement, below it as topical overlap — a reasonable first anchor until
|
|
150
|
+
* the judged-calibration follow-up measures the real distribution (the
|
|
151
|
+
* #426 provisional-anchor posture).
|
|
152
|
+
*/
|
|
153
|
+
exports.MEMORY_PRECISION_REDUNDANT_ABOVE = 0.8;
|
|
154
|
+
/**
|
|
155
|
+
* Window showings a live memory needs (with zero window fetches) to count as
|
|
156
|
+
* DIVERGING — the index keeps pushing it, nothing ever reads it. 5 is the
|
|
157
|
+
* spec's anchor (#476): enough showings that the silence is a pattern, not
|
|
158
|
+
* one prompt's turn-suppression.
|
|
159
|
+
*/
|
|
160
|
+
exports.MEMORY_PRECISION_DIVERGENCE_MIN_SHOWN = 5;
|
|
161
|
+
/**
|
|
162
|
+
* Cap on the prompt excerpt persisted per recall_pushes row (Option A,
|
|
163
|
+
* owner-blessed 2026-09-19: the first-ever server-side persistence of raw
|
|
164
|
+
* prompt text, bounded to 256 chars and pruned at the retention horizon —
|
|
165
|
+
* the same trust boundary as /distill's denoised conversation text).
|
|
166
|
+
*/
|
|
167
|
+
exports.MEMORY_PRECISION_PROMPT_EXCERPT_CHARS = 256;
|
|
130
168
|
// ---------------------------------------------------------------------------
|
|
131
169
|
// Composite ranking weights (was: score*Weight, supersededDemotion,
|
|
132
170
|
// *AffinityWeight, rrf*; the pre-#430 freshness bonus constants merged into
|
|
@@ -260,6 +298,18 @@ exports.BM25_WEIGHT_DOMAIN = 2.0;
|
|
|
260
298
|
* base-1.0 rows decay again.
|
|
261
299
|
*/
|
|
262
300
|
exports.IMPORTANCE_CEILING = 0.95;
|
|
301
|
+
/**
|
|
302
|
+
* How many days a memory stays in the nightly importance-settle pool after
|
|
303
|
+
* ingest (#478): a row's importance re-settles nightly while young — keyed
|
|
304
|
+
* on PER-ROW age (ingested_at), never the global lastConsolidated
|
|
305
|
+
* watermark, which sticks whenever a run defers and used to drag a growing
|
|
306
|
+
* cohort back through re-scoring (each re-settle overwrites base_strength,
|
|
307
|
+
* erasing promotion gains). Rows leave the pool after the window; promoted
|
|
308
|
+
* or enriched rows leave earlier via the paid-gain guard. Release-managed
|
|
309
|
+
* (#408 discipline): moving it is a release decision with soak evidence,
|
|
310
|
+
* not a config knob.
|
|
311
|
+
*/
|
|
312
|
+
exports.IMPORTANCE_SETTLE_WINDOW_DAYS = 3;
|
|
263
313
|
/** Deterministic merge ceiling of the unified resolution pass (#392): pairs
|
|
264
314
|
* at/above this cosine merge LLM-free; [CORRECTION_MIN_SIMILARITY, this)
|
|
265
315
|
* get the one verdict call. 0.92 — measured on the #191 mechanical audit
|
package/dist/classify-domains.js
CHANGED
|
@@ -175,10 +175,15 @@ async function runClassifyDomains(options = {}) {
|
|
|
175
175
|
// everything from the final tag sets.
|
|
176
176
|
const { prototypes } = await (0, schema_prototypes_js_1.computeDomainPrototypes)(db, domains, getEmbedFn);
|
|
177
177
|
// Scope filter: default = NULL / not-in-set / no tags yet; --all = everything.
|
|
178
|
+
// #477: absorbed dedup losers are excluded from the default scope (dead
|
|
179
|
+
// evidence — the merge already cleared their tags and nulled domain);
|
|
180
|
+
// conjoined OUTSIDE the parenthesized OR group, same as the nightly
|
|
181
|
+
// stageContentDomains twin. --all stays a wholesale operator re-judge.
|
|
178
182
|
const placeholders = domains.map(() => "?").join(", ");
|
|
179
183
|
const scopeSql = all
|
|
180
184
|
? "rowid > ?"
|
|
181
|
-
: `rowid > ? AND (
|
|
185
|
+
: `rowid > ? AND COALESCE(status, '') != 'absorbed' ` +
|
|
186
|
+
`AND (domain IS NULL OR domain NOT IN (${placeholders}) ` +
|
|
182
187
|
`OR id NOT IN (SELECT DISTINCT memory_id FROM memory_tags))`;
|
|
183
188
|
const batchStmt = db.prepare(`SELECT rowid AS __rowid, id, content, project, domain FROM memories
|
|
184
189
|
WHERE ${scopeSql} ORDER BY rowid ASC LIMIT ?`);
|
package/dist/consolidate.js
CHANGED
|
@@ -467,8 +467,13 @@ async function scoreMemoriesImportance(db, memories, llm, opts = {}) {
|
|
|
467
467
|
}
|
|
468
468
|
return { scored, failed, skipped_budget: skippedBudget };
|
|
469
469
|
}
|
|
470
|
-
async function stageImportance(db, memories, llm, budget, dryRun, deadline
|
|
471
|
-
|
|
470
|
+
async function stageImportance(db, memories, llm, budget, dryRun, deadline,
|
|
471
|
+
/** #478: pool candidates the paid-gain guard dropped at the stage
|
|
472
|
+
* boundary — reported, not scored, so the run's evidence shows the
|
|
473
|
+
* promotion/enrichment gains that survived the nightly. */
|
|
474
|
+
guardSkipped = 0) {
|
|
475
|
+
const r = await scoreMemoriesImportance(db, memories, llm, { budget, deadline, dryRun });
|
|
476
|
+
return { ...r, guard_skipped: guardSkipped };
|
|
472
477
|
}
|
|
473
478
|
// ---------------------------------------------------------------------------
|
|
474
479
|
// Stage 2.5: Reflection
|
|
@@ -639,12 +644,18 @@ async function stageContentDomains(db, domains, llm, budget, embedFn, dryRun, st
|
|
|
639
644
|
// - domain NOT IN the current vocabulary (a rename/removal re-files), OR
|
|
640
645
|
// - no memory_tags rows yet (single-domain memories from feat/content-domains
|
|
641
646
|
// that have a primary but no tag set — backfill them to multi-tag).
|
|
647
|
+
// #477: absorbed dedup losers are excluded — the merge clears their tags
|
|
648
|
+
// and nulls domain before absorbing, so without the predicate every dead
|
|
649
|
+
// loser re-enters this scope nightly (one classify call + a halving on
|
|
650
|
+
// evidence no recall can ever see). Conjoined OUTSIDE the parenthesized OR
|
|
651
|
+
// group so SQL precedence cannot let an OR arm absorb it.
|
|
642
652
|
const placeholders = domains.map(() => "?").join(", ");
|
|
643
653
|
const rows = db
|
|
644
654
|
.prepare(`SELECT id, content, project FROM memories
|
|
645
|
-
WHERE
|
|
646
|
-
|
|
647
|
-
|
|
655
|
+
WHERE COALESCE(status, '') != 'absorbed'
|
|
656
|
+
AND (domain IS NULL
|
|
657
|
+
OR domain NOT IN (${placeholders})
|
|
658
|
+
OR id NOT IN (SELECT DISTINCT memory_id FROM memory_tags))`)
|
|
648
659
|
.all(...domains.map((d) => d.name));
|
|
649
660
|
if (dryRun) {
|
|
650
661
|
return { curated: false, domains: domains.length, classified: 0, reason: `dry_run (${rows.length} would classify)` };
|
|
@@ -1664,12 +1675,29 @@ deadline) {
|
|
|
1664
1675
|
};
|
|
1665
1676
|
// Stage 1: Pre-check
|
|
1666
1677
|
const precheck = stagePrecheck(db, stateDir);
|
|
1667
|
-
//
|
|
1678
|
+
// #478: the importance pool is ROW-AGE bound — young (ingested within
|
|
1679
|
+
// IMPORTANCE_SETTLE_WINDOW_DAYS) ∪ never-scored. The lastConsolidated
|
|
1680
|
+
// watermark no longer defines any part of it: a deferred run's stuck
|
|
1681
|
+
// watermark used to re-settle a growing cohort nightly, and every
|
|
1682
|
+
// re-settle overwrites base_strength outright (erasing gains
|
|
1683
|
+
// stagePromotion had already paid — 5/5 observed erasures). The watermark
|
|
1684
|
+
// cohort (precheck.newMemories) STILL feeds reflection + links below —
|
|
1685
|
+
// their work is batch retry by design; only importance's per-row settling
|
|
1686
|
+
// was mis-bound to the batch marker.
|
|
1687
|
+
const settleCutoff = new Date(Date.now() - CALIBRATION.IMPORTANCE_SETTLE_WINDOW_DAYS * 86_400_000).toISOString();
|
|
1688
|
+
// getMemoriesSince keys on ingested_at only — absorbed rows (invisible to
|
|
1689
|
+
// recall) are filtered here in the same vocabulary getUnscoredMemories
|
|
1690
|
+
// uses in SQL: no LLM call on dead evidence.
|
|
1691
|
+
const young = storage
|
|
1692
|
+
.getMemoriesSince(db, settleCutoff)
|
|
1693
|
+
.filter((m) => m.status !== "absorbed");
|
|
1694
|
+
const youngIds = new Set(young.map((m) => m.id));
|
|
1695
|
+
// Also check for unscored memories (#425 watermark pool) — first settle is
|
|
1696
|
+
// age-independent, so a row used before its first score is never stranded.
|
|
1668
1697
|
const unscored = storage.getUnscoredMemories(db);
|
|
1669
|
-
const newIds = new Set(precheck.newMemories.map((m) => m.id));
|
|
1670
1698
|
const scoreMemories = [
|
|
1671
|
-
...
|
|
1672
|
-
...unscored.filter((m) => !
|
|
1699
|
+
...young,
|
|
1700
|
+
...unscored.filter((m) => !youngIds.has(m.id)),
|
|
1673
1701
|
];
|
|
1674
1702
|
// #194 no-fit scope: untagged rows (domain IS NULL) stay in the
|
|
1675
1703
|
// re-evaluation scope — the decay/re-attempt contract ("re-halves once per
|
|
@@ -1678,15 +1706,26 @@ deadline) {
|
|
|
1678
1706
|
// never caught because stagePrecheck used to read the AMBIENT (always
|
|
1679
1707
|
// empty in the suite) state instead of the run's own watermark — threading
|
|
1680
1708
|
// stateDir (#357) exposed the divergence between test and production.
|
|
1681
|
-
|
|
1682
|
-
|
|
1709
|
+
// #477: absorbed dedup losers (tags cleared, domain NULLed by the merge)
|
|
1710
|
+
// are dead evidence — they must not keep the no-fit scope (and the run)
|
|
1711
|
+
// alive every night. Same predicate spelling as getUnscoredMemories.
|
|
1712
|
+
const nofitInScope = db
|
|
1713
|
+
.prepare("SELECT COUNT(*) AS n FROM memories WHERE domain IS NULL AND COALESCE(status, '') != 'absorbed'")
|
|
1714
|
+
.get().n;
|
|
1715
|
+
// #478: the quiet-night gate is the UNION — watermark cohort OR young OR
|
|
1716
|
+
// unscored OR no-fit scope. A stuck watermark alone must never silence
|
|
1717
|
+
// reflection/links (their pool is the watermark cohort, and their retry
|
|
1718
|
+
// semantics are the reason a deferred run holds the watermark at all).
|
|
1719
|
+
const skip = scoreMemories.length === 0 &&
|
|
1720
|
+
precheck.newMemories.length === 0 &&
|
|
1721
|
+
nofitInScope === 0;
|
|
1683
1722
|
report.stages.precheck = {
|
|
1684
1723
|
skip,
|
|
1685
1724
|
reason: skip
|
|
1686
1725
|
? precheck.reason
|
|
1687
|
-
: `${precheck.newMemories.length} new + ${scoreMemories.length -
|
|
1726
|
+
: `${precheck.newMemories.length} new (watermark) + ${young.length} young + ${scoreMemories.length - young.length} unscored memories`,
|
|
1688
1727
|
new_memory_count: precheck.newMemories.length,
|
|
1689
|
-
unscored_count:
|
|
1728
|
+
unscored_count: unscored.length,
|
|
1690
1729
|
};
|
|
1691
1730
|
// Strength promotion (#448) — runs in the pre-skip deterministic zone, in
|
|
1692
1731
|
// the same placement discipline as memory_cap BELOW it: accesses happen on
|
|
@@ -1724,7 +1763,27 @@ deadline) {
|
|
|
1724
1763
|
// deferred. Deferred stages drain next run (cursors hold below them).
|
|
1725
1764
|
// Stage 2: Importance Scoring
|
|
1726
1765
|
if (!deadline?.hit("importance")) {
|
|
1727
|
-
|
|
1766
|
+
// #478 paid-gain guard — evaluated HERE, at the stage boundary, not at
|
|
1767
|
+
// pool-build time: the pool above was built before this run's
|
|
1768
|
+
// stagePromotion payment, so a row whose FIRST use lands this run
|
|
1769
|
+
// passes a pool-build check and its just-paid gain is overwritten at
|
|
1770
|
+
// the first re-settle. This read sits after promotion by construction
|
|
1771
|
+
// and sees the advanced baseline. Already-scored rows carrying a paid
|
|
1772
|
+
// gain (promotion baseline advanced OR owner corroboration) keep it —
|
|
1773
|
+
// re-settling would stomp base_strength; `hicortex rescore-importance`
|
|
1774
|
+
// remains the wholesale operator re-judge. Never-scored rows
|
|
1775
|
+
// (importance_scored_at IS NULL) are NEVER skipped: first settle
|
|
1776
|
+
// always happens, whatever their use history.
|
|
1777
|
+
const paidGainIds = scoreMemories.length
|
|
1778
|
+
? new Set(db.prepare(`SELECT id FROM memories
|
|
1779
|
+
WHERE importance_scored_at IS NOT NULL
|
|
1780
|
+
AND (COALESCE(promotion_last_count, 0) > 0
|
|
1781
|
+
OR COALESCE(corroboration_count, 0) > 0)
|
|
1782
|
+
AND id IN (${scoreMemories.map(() => "?").join(", ")})`).all(...scoreMemories.map((m) => m.id))
|
|
1783
|
+
.map((r) => r.id))
|
|
1784
|
+
: new Set();
|
|
1785
|
+
const settleCandidates = scoreMemories.filter((m) => !paidGainIds.has(m.id));
|
|
1786
|
+
report.stages.importance = await stageImportance(db, settleCandidates, llm, budget, dryRun, deadline, scoreMemories.length - settleCandidates.length);
|
|
1728
1787
|
}
|
|
1729
1788
|
// Stage 2.5: Reflection
|
|
1730
1789
|
if (deadline?.hit("reflection")) {
|
package/dist/dashboard.d.ts
CHANGED
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
import type express from "express";
|
|
24
24
|
import type Database from "better-sqlite3";
|
|
25
25
|
import { type CaptureHealthRow } from "./capture-health.js";
|
|
26
|
+
import { type MemoryPrecision } from "./recall-precision.js";
|
|
26
27
|
import { type Stage } from "./stages.js";
|
|
27
28
|
/** Corpus-shape snapshot. `adoption` is null in backfilled rows (point-in-time,
|
|
28
29
|
* can't be reconstructed from created_at). */
|
|
@@ -257,6 +258,16 @@ export interface DashboardData {
|
|
|
257
258
|
last_outcome: string;
|
|
258
259
|
}>;
|
|
259
260
|
};
|
|
261
|
+
/**
|
|
262
|
+
* #476 — the Memory Precision card: Level 1 (pushed-index precision
|
|
263
|
+
* proxies over the recall_pushes/recall_events window) beside Level 2
|
|
264
|
+
* (recall depth = the uses-per-showing ratio over the SAME window, from
|
|
265
|
+
* snapshot deltas) + the divergence list. ALWAYS present; edges echoed
|
|
266
|
+
* like every threshold (the page never hardcodes), and the block measures
|
|
267
|
+
* ≤ 2 KB against the live 30-day payload. The page degrades to the
|
|
268
|
+
* pre-#476 recall-card rendering when the block is absent.
|
|
269
|
+
*/
|
|
270
|
+
memory_precision: MemoryPrecision;
|
|
260
271
|
digest: {
|
|
261
272
|
date: string | null;
|
|
262
273
|
run_at: string | null;
|
package/dist/dashboard.js
CHANGED
|
@@ -45,6 +45,7 @@ const recall_index_js_1 = require("./recall-index.js");
|
|
|
45
45
|
const config_read_js_1 = require("./config-read.js");
|
|
46
46
|
const consolidate_js_1 = require("./consolidate.js");
|
|
47
47
|
const capture_health_js_1 = require("./capture-health.js");
|
|
48
|
+
const recall_precision_js_1 = require("./recall-precision.js");
|
|
48
49
|
const capture_pause_js_1 = require("./capture-pause.js");
|
|
49
50
|
const state_js_1 = require("./state.js");
|
|
50
51
|
const retrieval_js_1 = require("./retrieval.js");
|
|
@@ -576,6 +577,14 @@ function handleDashboardData(db, query, config) {
|
|
|
576
577
|
by_source_machine: live.by_source_machine,
|
|
577
578
|
},
|
|
578
579
|
capture_health: { ...(0, capture_health_js_1.readCaptureHealth)(db), ...(0, capture_health_js_1.readCaptureHealthWindow)(db) },
|
|
580
|
+
// #476 Memory Precision: the window aggregation over the precision
|
|
581
|
+
// event tables + snapshot deltas. Level 2's "latest" is the SAME live
|
|
582
|
+
// adoption this handler already computed (one definition of corpus
|
|
583
|
+
// shape — computeDashboardMetrics); readMemoryPrecision owns the rest.
|
|
584
|
+
memory_precision: (0, recall_precision_js_1.readMemoryPrecision)(db, rangeParam, {
|
|
585
|
+
shown_sum: live.adoption?.shown_sum ?? 0,
|
|
586
|
+
used_sum: live.adoption?.used_sum ?? 0,
|
|
587
|
+
}),
|
|
579
588
|
// #423 phase 3: pauses + presence in one block — one source of truth
|
|
580
589
|
// for the rail's dots, toggles and the capture card's PAUSED badges.
|
|
581
590
|
fleet: {
|
package/dist/db.js
CHANGED
|
@@ -668,6 +668,80 @@ const MIGRATIONS = [
|
|
|
668
668
|
db.exec("UPDATE memories SET promotion_last_count = access_count WHERE promotion_last_count IS NULL");
|
|
669
669
|
},
|
|
670
670
|
},
|
|
671
|
+
{
|
|
672
|
+
version: 21,
|
|
673
|
+
name: "memory_layout",
|
|
674
|
+
up: (db) => {
|
|
675
|
+
// #464 — the console field's server-computed placement. One row per
|
|
676
|
+
// placed memory at one epoch; the CURRENT epoch is MAX(epoch) and the
|
|
677
|
+
// payload (dashboard.ts handleDashboardField) LEFT JOINs on it. A full
|
|
678
|
+
// re-projection (`hicortex layout --reproject`) writes a NEW epoch and
|
|
679
|
+
// drops older ones in the SAME transaction (atomic swap — a mid-write
|
|
680
|
+
// failure leaves the previous epoch fully intact). Epochs exist so the
|
|
681
|
+
// whole store moves at once (one announced visual shift), never as a
|
|
682
|
+
// mix of projections. Idempotent: IF NOT EXISTS (the v18 pattern —
|
|
683
|
+
// plain CREATE, no ALTER).
|
|
684
|
+
// Kept after the #464/#475 field revert (#474): nothing reads or writes
|
|
685
|
+
// this table now — a dormant orphan. Rc databases already applied v21
|
|
686
|
+
// (user_version 21), so removing it would fork schema versions; it
|
|
687
|
+
// stays per the append-only migration discipline.
|
|
688
|
+
db.exec(`
|
|
689
|
+
CREATE TABLE IF NOT EXISTS memory_layout (
|
|
690
|
+
memory_id TEXT PRIMARY KEY,
|
|
691
|
+
x REAL NOT NULL,
|
|
692
|
+
y REAL NOT NULL,
|
|
693
|
+
epoch INTEGER NOT NULL
|
|
694
|
+
)
|
|
695
|
+
`);
|
|
696
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_memory_layout_epoch ON memory_layout(epoch)");
|
|
697
|
+
},
|
|
698
|
+
},
|
|
699
|
+
{
|
|
700
|
+
version: 22,
|
|
701
|
+
name: "recall_precision_events",
|
|
702
|
+
up: (db) => {
|
|
703
|
+
// #476 — the Memory Precision card's event store (Option A, owner
|
|
704
|
+
// decision 2026-09-19: per-push event rows incl. a ≤256-char prompt
|
|
705
|
+
// excerpt — the exact-window Level-1 measures + the judge follow-up's
|
|
706
|
+
// sample frame). Two sidecar tables, the v16 distill_activity pattern:
|
|
707
|
+
// OPERATIONS telemetry, no memories FK — recall_events.memory_id is
|
|
708
|
+
// DATA (absorbed memories' history stays queryable; rows outliving
|
|
709
|
+
// their memory are valid). recall_pushes: one row per NON-skipped
|
|
710
|
+
// /recall-index call (short-prompt skips and resets record nothing; a
|
|
711
|
+
// silent turn records its push row with zero events — the silence rate
|
|
712
|
+
// is computable). recall_events: one kind='shown' row per pushed line
|
|
713
|
+
// (similarity = cosine vs the PURE prompt embedding, redundancy = max
|
|
714
|
+
// cosine vs the standing-context basis — both RAW, verdicts computed at
|
|
715
|
+
// render so recalibration never rewrites history) + one kind='fetch'
|
|
716
|
+
// row per handleMemoryGet (push_id NULL). Pruned nightly at the
|
|
717
|
+
// retention horizon (= the longest window, the capture-health
|
|
718
|
+
// single-constant law). Idempotent: IF NOT EXISTS everywhere.
|
|
719
|
+
db.exec(`
|
|
720
|
+
CREATE TABLE IF NOT EXISTS recall_pushes (
|
|
721
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
722
|
+
ts TEXT NOT NULL,
|
|
723
|
+
day TEXT NOT NULL,
|
|
724
|
+
session_id TEXT,
|
|
725
|
+
prompt_excerpt TEXT
|
|
726
|
+
)
|
|
727
|
+
`);
|
|
728
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_recall_pushes_day ON recall_pushes(day)");
|
|
729
|
+
db.exec(`
|
|
730
|
+
CREATE TABLE IF NOT EXISTS recall_events (
|
|
731
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
732
|
+
ts TEXT NOT NULL,
|
|
733
|
+
day TEXT NOT NULL,
|
|
734
|
+
push_id INTEGER,
|
|
735
|
+
memory_id TEXT NOT NULL,
|
|
736
|
+
similarity REAL,
|
|
737
|
+
redundancy REAL,
|
|
738
|
+
kind TEXT NOT NULL
|
|
739
|
+
)
|
|
740
|
+
`);
|
|
741
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_recall_events_day ON recall_events(day)");
|
|
742
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_recall_events_memory_kind ON recall_events(memory_id, kind)");
|
|
743
|
+
},
|
|
744
|
+
},
|
|
671
745
|
];
|
|
672
746
|
/**
|
|
673
747
|
* Run all pending migrations against the database.
|
package/dist/mcp-server.js
CHANGED
|
@@ -90,6 +90,7 @@ const dedup_js_1 = require("./dedup.js");
|
|
|
90
90
|
const reconsolidation_js_1 = require("./reconsolidation.js");
|
|
91
91
|
const redact_js_1 = require("./redact.js");
|
|
92
92
|
const capture_health_js_1 = require("./capture-health.js");
|
|
93
|
+
const recall_precision_js_1 = require("./recall-precision.js");
|
|
93
94
|
const capture_pause_js_1 = require("./capture-pause.js");
|
|
94
95
|
const init_js_1 = require("./init.js");
|
|
95
96
|
// ---------------------------------------------------------------------------
|
|
@@ -207,8 +208,10 @@ function createMcpServer() {
|
|
|
207
208
|
// (incl. the #204 FETCHED marker) is built in ONE place shared with the
|
|
208
209
|
// REST GET /memory path. CC reaches Hicortex through THIS MCP tool;
|
|
209
210
|
// before #207's fix it got a marker-less citation built inline here.
|
|
211
|
+
// #476: the fetch recorder forwards through the same funnel — exactly
|
|
212
|
+
// one precision event per fetch, whichever path served it.
|
|
210
213
|
try {
|
|
211
|
-
const r = (0, recall_index_js_1.formatMemoryGetText)(db, { id });
|
|
214
|
+
const r = (0, recall_index_js_1.formatMemoryGetText)(db, { id }, { recordFetch: recall_precision_js_1.recordRecallFetch });
|
|
212
215
|
return { content: [{ type: "text", text: r.text }], isError: r.status !== 200 };
|
|
213
216
|
}
|
|
214
217
|
catch (err) {
|
|
@@ -1053,6 +1056,22 @@ async function startServer(options = {}) {
|
|
|
1053
1056
|
// last_accessed, NOT access_count (that stays reserved for hicortex_get /
|
|
1054
1057
|
// GET /memory — real use). {reset: true} clears the session's dedup state
|
|
1055
1058
|
// (SessionStart/compaction).
|
|
1059
|
+
//
|
|
1060
|
+
// #476 Memory Precision: the route also wires the precision seams — the
|
|
1061
|
+
// factory's exposed embedPrompt (the per-request memo: the recorder
|
|
1062
|
+
// measures per-line similarity with ZERO extra embeds), the 24h-TTL
|
|
1063
|
+
// standing-context basis (lessons in /learnings order + identity only when
|
|
1064
|
+
// every known client is served — the redundancy reference), and the
|
|
1065
|
+
// fail-soft recorder. handleRecallIndex owns the fail-soft law: a
|
|
1066
|
+
// recording failure never touches the response.
|
|
1067
|
+
const standingContextBasis = (0, recall_precision_js_1.createStandingContextBasis)({
|
|
1068
|
+
// Lazy getters: identityClients is resolved at boot and stateDir is
|
|
1069
|
+
// assigned before the routes serve, but both land AFTER this provider is
|
|
1070
|
+
// created — read per cache rebuild (a config restart applies at the next
|
|
1071
|
+
// TTL), never captured once.
|
|
1072
|
+
clients: () => identityClients,
|
|
1073
|
+
identityDir: () => (0, node_path_1.join)(stateDir, "identity"),
|
|
1074
|
+
});
|
|
1056
1075
|
app.post("/recall-index", async (req, res) => {
|
|
1057
1076
|
if (!db) {
|
|
1058
1077
|
res.status(503).json({ error: "Server not initialized" });
|
|
@@ -1067,22 +1086,30 @@ async function startServer(options = {}) {
|
|
|
1067
1086
|
// that searches unblended and touches no centroid state. Extracted so the
|
|
1068
1087
|
// exact behavior is unit-testable without HTTP (blendQueryVector
|
|
1069
1088
|
// precedent); this adapter stays thin.
|
|
1089
|
+
const factory = (0, recall_index_js_1.createRecallRetrieveFn)({
|
|
1090
|
+
db,
|
|
1091
|
+
registry: recallRegistry,
|
|
1092
|
+
embedFn: embedder_js_1.embed,
|
|
1093
|
+
});
|
|
1070
1094
|
const r = await (0, recall_index_js_1.handleRecallIndex)({
|
|
1071
1095
|
db,
|
|
1072
1096
|
registry: recallRegistry,
|
|
1073
|
-
retrieveFn:
|
|
1074
|
-
db,
|
|
1075
|
-
registry: recallRegistry,
|
|
1076
|
-
embedFn: embedder_js_1.embed,
|
|
1077
|
-
}),
|
|
1097
|
+
retrieveFn: factory.retrieveFn,
|
|
1078
1098
|
options: recallIndexOptions,
|
|
1099
|
+
precision: {
|
|
1100
|
+
promptEmbed: factory.embedPrompt,
|
|
1101
|
+
basis: standingContextBasis,
|
|
1102
|
+
recorder: recall_precision_js_1.recordRecallPush,
|
|
1103
|
+
},
|
|
1079
1104
|
}, req.body);
|
|
1080
1105
|
res.status(r.status).json(r.body);
|
|
1081
1106
|
});
|
|
1082
1107
|
// REST /memory?id= — fetch one memory's full content (lazy-load counterpart
|
|
1083
1108
|
// of /recall-index for REST clients: Hermes/OC plugins). Marks it as used.
|
|
1084
1109
|
// Prefix ids resolve. 0.16.x: the `privacy` query param is accepted but
|
|
1085
|
-
// ignored (column is vestigial, never filtered). Logic in handleMemoryGet
|
|
1110
|
+
// ignored (column is vestigial, never filtered). Logic in handleMemoryGet;
|
|
1111
|
+
// the #476 fetch recorder rides the same funnel (exactly one event per
|
|
1112
|
+
// fetch, shared with the MCP hicortex_get path).
|
|
1086
1113
|
app.get("/memory", (req, res) => {
|
|
1087
1114
|
if (!db) {
|
|
1088
1115
|
res.status(503).json({ error: "Server not initialized" });
|
|
@@ -1090,7 +1117,7 @@ async function startServer(options = {}) {
|
|
|
1090
1117
|
}
|
|
1091
1118
|
warnDeprecatedPrivacyParamIfPresent(req.query, "memory");
|
|
1092
1119
|
try {
|
|
1093
|
-
const r = (0, recall_index_js_1.handleMemoryGet)(db, { id: req.query.id });
|
|
1120
|
+
const r = (0, recall_index_js_1.handleMemoryGet)(db, { id: req.query.id }, { recordFetch: recall_precision_js_1.recordRecallFetch });
|
|
1094
1121
|
res.status(r.status).json(r.body);
|
|
1095
1122
|
}
|
|
1096
1123
|
catch (err) {
|
package/dist/nightly.js
CHANGED
|
@@ -79,6 +79,7 @@ const capture_cursors_js_1 = require("./capture-cursors.js");
|
|
|
79
79
|
const capture_js_1 = require("./capture.js");
|
|
80
80
|
const run_deadline_js_1 = require("./run-deadline.js");
|
|
81
81
|
const dashboard_js_1 = require("./dashboard.js");
|
|
82
|
+
const recall_precision_js_1 = require("./recall-precision.js");
|
|
82
83
|
const telemetry_js_1 = require("./telemetry.js");
|
|
83
84
|
const init_js_1 = require("./init.js");
|
|
84
85
|
const backup_js_1 = require("./backup.js");
|
|
@@ -1071,6 +1072,19 @@ async function runNightly(options = {}) {
|
|
|
1071
1072
|
console.warn(`[hicortex] Dashboard snapshot write failed: ` +
|
|
1072
1073
|
`${snapErr instanceof Error ? snapErr.message : String(snapErr)}`);
|
|
1073
1074
|
}
|
|
1075
|
+
// #476 — recall-precision retention prune (zero-LLM, full nightly only):
|
|
1076
|
+
// both event tables drop rows outside the rolling window whose length IS
|
|
1077
|
+
// the retention constant (the capture-health single-constant law — the
|
|
1078
|
+
// Memory Precision card can never claim a window the store no longer
|
|
1079
|
+
// holds rows for). Own try/catch like the snapshot writer: telemetry
|
|
1080
|
+
// housekeeping must never fail the run.
|
|
1081
|
+
try {
|
|
1082
|
+
(0, recall_precision_js_1.pruneRecallPrecision)(db);
|
|
1083
|
+
}
|
|
1084
|
+
catch (pruneErr) {
|
|
1085
|
+
console.warn(`[hicortex] Recall-precision retention prune failed: ` +
|
|
1086
|
+
`${pruneErr instanceof Error ? pruneErr.message : String(pruneErr)}`);
|
|
1087
|
+
}
|
|
1074
1088
|
}
|
|
1075
1089
|
// Anonymous telemetry (fire-and-forget, full nightly only).
|
|
1076
1090
|
// Capture-only runs are excluded to avoid inflating install pings.
|
package/dist/recall-index.d.ts
CHANGED
|
@@ -51,6 +51,7 @@ import type Database from "better-sqlite3";
|
|
|
51
51
|
import type { MemorySearchResult } from "./types.js";
|
|
52
52
|
import * as storage from "./storage.js";
|
|
53
53
|
import { SessionRecallRegistry } from "./recall-registry.js";
|
|
54
|
+
import type { RecallPushEntry, RecallFetchRecorder } from "./recall-precision.js";
|
|
54
55
|
export interface RecallIndexOptions {
|
|
55
56
|
/** Minimum measured cosine for vector-only candidates (release-managed
|
|
56
57
|
* since #408 — calibration.ts RECALL_MIN_SIMILARITY; this field is the
|
|
@@ -161,6 +162,16 @@ export interface RecallFilters {
|
|
|
161
162
|
* RecallIndexDeps.retrieveFn). Named so the production factory
|
|
162
163
|
* (createRecallRetrieveFn) and test doubles share one type. */
|
|
163
164
|
export type RecallRetrieveFn = (query: string, limit: number, filters: RecallFilters | undefined, sessionId: string, purePrompt?: boolean) => Promise<MemorySearchResult[]>;
|
|
165
|
+
/** What createRecallRetrieveFn returns (#476): the search closure PLUS the
|
|
166
|
+
* per-request prompt-embed memo it already maintained — exposed so the
|
|
167
|
+
* precision recorder reuses the SAME embedding (zero extra embeds) instead
|
|
168
|
+
* of re-embedding the prompt to measure per-line similarity. */
|
|
169
|
+
export interface RecallRetrieveFactory {
|
|
170
|
+
retrieveFn: RecallRetrieveFn;
|
|
171
|
+
/** The single-entry embed memo (keyed on the query text; the factory is
|
|
172
|
+
* built per request, so the memo never outlives it). */
|
|
173
|
+
embedPrompt: (query: string) => Promise<Float32Array>;
|
|
174
|
+
}
|
|
164
175
|
export interface RecallIndexDeps {
|
|
165
176
|
db: Database.Database;
|
|
166
177
|
registry: SessionRecallRegistry;
|
|
@@ -176,6 +187,30 @@ export interface RecallIndexDeps {
|
|
|
176
187
|
* no breakage. */
|
|
177
188
|
retrieveFn: RecallRetrieveFn;
|
|
178
189
|
options?: RecallIndexOptions;
|
|
190
|
+
/** #476 Memory Precision seams — OPTIONAL so existing callers (tests,
|
|
191
|
+
* library use) keep their exact no-recording behavior. Absent → no event
|
|
192
|
+
* rows, the plain exposure write runs as before. mcp-server wires the
|
|
193
|
+
* production set (the real recorder, the request-memoized prompt embed,
|
|
194
|
+
* the 24h-TTL standing-context basis). Recording is FAIL-SOFT: any error
|
|
195
|
+
* is caught here and the recall response (200 + block) is never affected;
|
|
196
|
+
* on failure the exposure touch re-runs standalone so shown_count never
|
|
197
|
+
* depends on telemetry. */
|
|
198
|
+
precision?: RecallPrecisionDeps;
|
|
199
|
+
}
|
|
200
|
+
/** The #476 precision-recording seams (see RecallIndexDeps.precision). All
|
|
201
|
+
* three are injectable; tests force failures through them. */
|
|
202
|
+
export interface RecallPrecisionDeps {
|
|
203
|
+
/** The request's memoized pure-prompt embed (createRecallRetrieveFn's
|
|
204
|
+
* embedPrompt) — the recorder computes per-line cosines with ZERO extra
|
|
205
|
+
* embeds (the perf law). */
|
|
206
|
+
promptEmbed: (prompt: string) => Promise<Float32Array>;
|
|
207
|
+
/** Standing-context basis vectors (recall-precision.ts's 24h-TTL cached
|
|
208
|
+
* provider; empty array → redundancy NULL, unmeasured). */
|
|
209
|
+
basis: (db: Database.Database) => Promise<Float32Array[]>;
|
|
210
|
+
/** The recorder (recall-precision.ts recordRecallPush). Throws on failure —
|
|
211
|
+
* this handler catches (fail-soft) and falls back to the plain exposure
|
|
212
|
+
* write. */
|
|
213
|
+
recorder: (db: Database.Database, entry: RecallPushEntry, basis?: Float32Array[]) => void;
|
|
179
214
|
}
|
|
180
215
|
/**
|
|
181
216
|
* The PRODUCTION /recall-index retrieveFn (what mcp-server wires into
|
|
@@ -209,7 +244,7 @@ export declare function createRecallRetrieveFn(deps: {
|
|
|
209
244
|
embedFn: (text: string) => Promise<Float32Array>;
|
|
210
245
|
/** FTS resolution override (tests). Defaults to storage.searchFts. */
|
|
211
246
|
ftsFn?: typeof storage.searchFts;
|
|
212
|
-
}):
|
|
247
|
+
}): RecallRetrieveFactory;
|
|
213
248
|
/** Normalize a request-supplied string-list param: array of strings or a CSV
|
|
214
249
|
* string → string[] | undefined. Anything else (or an empty result) means
|
|
215
250
|
* "absent" — never a partial guess. Used by `mission_domains` (#203) so it
|
|
@@ -220,6 +255,14 @@ export declare function parseStringListParam(v: unknown): string[] | undefined;
|
|
|
220
255
|
* all behavior lives here so tests exercise it directly.
|
|
221
256
|
*/
|
|
222
257
|
export declare function handleRecallIndex(deps: RecallIndexDeps, body: unknown): Promise<RecallIndexResult>;
|
|
258
|
+
/** Optional #476 deps for handleMemoryGet: the fetch-event recorder. Absent
|
|
259
|
+
* (old callers, tests) → no recording, byte-identical behavior. mcp-server
|
|
260
|
+
* wires recordRecallFetch so BOTH fetch paths (REST GET /memory, MCP
|
|
261
|
+
* hicortex_get via formatMemoryGetText) record exactly once — this is the
|
|
262
|
+
* ONE funnel. Fail-soft: a recorder error is caught here, never surfaced. */
|
|
263
|
+
export interface MemoryGetDeps {
|
|
264
|
+
recordFetch: RecallFetchRecorder;
|
|
265
|
+
}
|
|
223
266
|
/**
|
|
224
267
|
* Handle a GET /memory request (lazy-load counterpart of the recall index for
|
|
225
268
|
* REST clients). Thin Express adapter in mcp-server.ts; behavior lives here so
|
|
@@ -235,7 +278,7 @@ export declare function handleRecallIndex(deps: RecallIndexDeps, body: unknown):
|
|
|
235
278
|
*/
|
|
236
279
|
export declare function handleMemoryGet(db: Database.Database, query: {
|
|
237
280
|
id?: unknown;
|
|
238
|
-
}): RecallIndexResult;
|
|
281
|
+
}, deps?: MemoryGetDeps): RecallIndexResult;
|
|
239
282
|
/**
|
|
240
283
|
* MCP `hicortex_get` presentation: handleMemoryGet's result framed as the
|
|
241
284
|
* text block the MCP tool returns (provenance header + the SHARED citation +
|
|
@@ -249,7 +292,7 @@ export declare function handleMemoryGet(db: Database.Database, query: {
|
|
|
249
292
|
*/
|
|
250
293
|
export declare function formatMemoryGetText(db: Database.Database, query: {
|
|
251
294
|
id?: unknown;
|
|
252
|
-
}): {
|
|
295
|
+
}, deps?: MemoryGetDeps): {
|
|
253
296
|
status: number;
|
|
254
297
|
text: string;
|
|
255
298
|
};
|