@gamaze/hicortex 0.21.0 → 0.22.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/assets/dashboard.html +282 -27
- package/dist/calibration.d.ts +77 -12
- package/dist/calibration.js +82 -15
- package/dist/capture-health.d.ts +42 -5
- package/dist/capture-health.js +57 -7
- package/dist/capture-pause.d.ts +7 -4
- package/dist/capture-pause.js +7 -4
- package/dist/consolidate.d.ts +19 -0
- package/dist/consolidate.js +125 -51
- package/dist/dashboard.d.ts +41 -16
- package/dist/dashboard.js +110 -41
- package/dist/db.js +16 -0
- package/dist/dedup.js +4 -1
- package/dist/eval/decay-eval.d.ts +6 -5
- package/dist/eval/decay-eval.js +10 -48
- package/dist/eval/eval-clock.d.ts +32 -0
- package/dist/eval/eval-clock.js +47 -0
- package/dist/eval/graph-eval.d.ts +15 -2
- package/dist/eval/graph-eval.js +51 -5
- package/dist/eval/planted-eval.d.ts +4 -0
- package/dist/eval/planted-eval.js +27 -2
- package/dist/eval/planted-harness.d.ts +7 -0
- package/dist/eval/planted-harness.js +2 -0
- package/dist/eval/ranking-battery.d.ts +49 -2
- package/dist/eval/ranking-battery.js +110 -2
- package/dist/eval/ranking-eval.d.ts +26 -6
- package/dist/eval/ranking-eval.js +197 -34
- package/dist/eval/ranking-fixtures.d.ts +46 -6
- package/dist/eval/ranking-fixtures.js +268 -9
- package/dist/eval/recall-sweep.d.ts +7 -2
- package/dist/eval/recall-sweep.js +42 -13
- package/dist/eval/relevance-eval.d.ts +115 -1
- package/dist/eval/relevance-eval.js +318 -32
- package/dist/eval/run-eval.d.ts +7 -4
- package/dist/eval/run-eval.js +37 -10
- package/dist/graph.d.ts +1 -2
- package/dist/graph.js +3 -4
- package/dist/mcp-server.js +3 -2
- package/dist/nofit.d.ts +2 -1
- package/dist/nofit.js +2 -1
- package/dist/recall-index.d.ts +3 -2
- package/dist/recall-index.js +3 -2
- package/dist/retrieval.d.ts +43 -18
- package/dist/retrieval.js +148 -86
- package/dist/status.js +18 -0
- package/dist/storage.d.ts +2 -1
- package/dist/storage.js +6 -1
- package/dist/types.d.ts +25 -4
- package/package.json +1 -1
- package/server.json +4 -4
package/dist/calibration.js
CHANGED
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
* the removal is never silent.
|
|
25
25
|
*/
|
|
26
26
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
|
-
exports.
|
|
27
|
+
exports.OLLAMA_FLUSH_WAIT_MS = exports.OLLAMA_FLUSH_EVERY = exports.NUM_CTX = exports.DEFAULT_FIRST_RUN_LOOKBACK_DAYS = exports.ENRICH_STRENGTH_DELTA = 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_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.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 = void 0;
|
|
28
29
|
exports.resolveNumCtx = resolveNumCtx;
|
|
29
30
|
exports.resolveOllamaFlushEvery = resolveOllamaFlushEvery;
|
|
30
31
|
exports.resolveOllamaFlushWaitMs = resolveOllamaFlushWaitMs;
|
|
@@ -39,6 +40,36 @@ exports.resolveOllamaFlushWaitMs = resolveOllamaFlushWaitMs;
|
|
|
39
40
|
* ranking. Long-term remembering is the product; time preference stays mild.
|
|
40
41
|
*/
|
|
41
42
|
exports.DECAY_HALF_LIFE_DAYS = 365;
|
|
43
|
+
/**
|
|
44
|
+
* Per-use strength promotion rate (#448, the strength model's upward path):
|
|
45
|
+
* one application of `promotion = PROMOTION_RATE × (1 − S / IMPORTANCE_CEILING)
|
|
46
|
+
* × S^(−0.3)` per new use (S = stored base_strength), iterated once per
|
|
47
|
+
* access-count delta by the nightly stagePromotion stage and clamped at the
|
|
48
|
+
* ceiling. Calibrated so the first access on a median-0.35 memory gives ~+0.03
|
|
49
|
+
* (0.35 → 0.3786). Shape grounded in LTP saturation curves (Cao & Harris
|
|
50
|
+
* 2014), the power law of practice (α ≈ 0.3, Newell & Rosenbloom 1981), and
|
|
51
|
+
* spaced-repetition stability multipliers (S^−0.53, SuperMemo SM-17,
|
|
52
|
+
* n > 60,000 real repetitions): bigger boosts for weaker memories,
|
|
53
|
+
* saturating near the ceiling, never fully stopping (asymptotic). The decay
|
|
54
|
+
* side is untouched — a memory's fade RATE no longer depends on its use or
|
|
55
|
+
* link history (the #448 hardening removal); use raises the STORED score
|
|
56
|
+
* instead, and abandonment fades it on the same 365-day clock as everything
|
|
57
|
+
* else. RELEASE-MANAGED per this module's evolution contract — no config key.
|
|
58
|
+
*/
|
|
59
|
+
exports.PROMOTION_RATE = 0.033;
|
|
60
|
+
/**
|
|
61
|
+
* Lower bound on the promotion formula's S input (#453 review Minor finding,
|
|
62
|
+
* owner decision 2026-09-16). base_strength 0.0 is a writable value (legacy
|
|
63
|
+
* rows, operator edits), and S^(−0.3) has a zero-singularity: unfloored, one
|
|
64
|
+
* access on a 0.0 row computed delta = 0.033 × (1 − 0/0.95) × 0^−0.3 =
|
|
65
|
+
* Infinity, and the ceiling clamp turned that into a leap straight to 0.95.
|
|
66
|
+
* Floored at 0.01, the same access lands at 0.033 × (1 − 0.01/0.95) ×
|
|
67
|
+
* 0.01^(−0.3) ≈ +0.13 (0.0 → ≈0.14) — the biggest first boost the formula
|
|
68
|
+
* can give, bounded and far from the ceiling. The floor clamps the FORMULA
|
|
69
|
+
* INPUT only: storage keeps a 0.0 row at 0.0 until it is used, and the
|
|
70
|
+
* promoted result is nonzero from then on. RELEASE-MANAGED like PROMOTION_RATE.
|
|
71
|
+
*/
|
|
72
|
+
exports.PROMOTION_STRENGTH_FLOOR = 0.01;
|
|
42
73
|
/** Default k for retrieve() (/search without an explicit limit). #192. */
|
|
43
74
|
exports.SEARCH_LIMIT = 8;
|
|
44
75
|
/** Default k for searchRecent() (/recent without an explicit limit). #192. */
|
|
@@ -97,8 +128,9 @@ exports.RECALL_USES_NORMAL_MAX = 0.25;
|
|
|
97
128
|
* keeps a visible span, not a measured bound. */
|
|
98
129
|
exports.RECALL_USES_AXIS_MAX = 0.30;
|
|
99
130
|
// ---------------------------------------------------------------------------
|
|
100
|
-
// Composite ranking weights (was: score*Weight,
|
|
101
|
-
//
|
|
131
|
+
// Composite ranking weights (was: score*Weight, supersededDemotion,
|
|
132
|
+
// *AffinityWeight, rrf*; the pre-#430 freshness bonus constants merged into
|
|
133
|
+
// the RECENCY family below)
|
|
102
134
|
// ---------------------------------------------------------------------------
|
|
103
135
|
/** Semantic-similarity share of the composite score. 0.50 (raised from 0.40
|
|
104
136
|
* in the 0.15.2 rebalance, #191 Phase B): on the production corpus effective
|
|
@@ -110,23 +142,58 @@ exports.SCORE_SIMILARITY_WEIGHT = 0.50;
|
|
|
110
142
|
exports.SCORE_STRENGTH_WEIGHT = 0.20;
|
|
111
143
|
/** Graph-centrality share of the composite score (was 0.20; see above). */
|
|
112
144
|
exports.SCORE_CONNECTIONS_WEIGHT = 0.15;
|
|
113
|
-
/**
|
|
145
|
+
/**
|
|
146
|
+
* Log-saturation degree K for the connections term (#449 PR E, items 1+3):
|
|
147
|
+
* the credit is min(1, log1p(k) / log1p(K)) × SCORE_CONNECTIONS_WEIGHT on
|
|
148
|
+
* the ABSOLUTE undirected-degree scale k — full at k = K, exactly +0 at
|
|
149
|
+
* k = 0 (unlinked rows score bit-identically to the linear term they
|
|
150
|
+
* replace). Provenance: p99 of the REAL undirected degree distribution
|
|
151
|
+
* (read-only audit 2026-09-17 of the canonical wave snapshot, 18,798
|
|
152
|
+
* memories / 14,600 linked: p50 = 4, p90 = 8, p95 = 9, p99 = 15, p99.9 =
|
|
153
|
+
* 28.4, max = 40; 135 memories ≥ 16 links = 0.92% of linked) — so the top
|
|
154
|
+
* ~1% of hubs tie at full credit and everything below differentiates
|
|
155
|
+
* modestly. Shape grounded in the #449 research base: ACT-R fan saturation
|
|
156
|
+
* (Anderson & Reder 1999 — activation falls with the LOG of fan), SAM's
|
|
157
|
+
* saturating returns, cue overload (Watkins & Watkins 1975). Replaces the
|
|
158
|
+
* candidate-set-relative normalization #449 deleted (k / the per-query max
|
|
159
|
+
* degree) — a row's score no longer depends on which other rows happened
|
|
160
|
+
* to match. Alternatives K = 8 (p90 — collapses all p90+ rows to
|
|
161
|
+
* full credit) and K = 32 (≈p99.9 — leaves the p99 hub at 0.12 of the
|
|
162
|
+
* term) were tabled in the #449 spec. RELEASE-MANAGED per this module's
|
|
163
|
+
* evolution contract — overridable only through the configureScoring seam.
|
|
164
|
+
*/
|
|
165
|
+
exports.CONNECTIONS_SATURATION_DEGREE = 16;
|
|
166
|
+
/** Time-curve share of the composite score — the merged curve's blend weight
|
|
167
|
+
* AND slow-region amplitude (was 0.10; see above). The four blend weights
|
|
168
|
+
* still sum to 1.0; the head amplitude below is a documented overshoot, NOT
|
|
169
|
+
* a fifth blend weight (#430). Zero (via the seam) disables the whole term. */
|
|
114
170
|
exports.SCORE_RECENCY_WEIGHT = 0.15;
|
|
115
|
-
/**
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
171
|
+
/** Hourly decay of the merged curve's slow region (#430) — promoted from the
|
|
172
|
+
* inline literal computeScore carried; half-life ≈ 57.7 days. At and beyond
|
|
173
|
+
* RECENCY_HEAD_DAYS the curve is bit-identical to the pre-#430 slow term
|
|
174
|
+
* (SCORE_RECENCY_WEIGHT × RECENCY_HOURLY_DECAY^hours). */
|
|
175
|
+
exports.RECENCY_HOURLY_DECAY = 0.9995;
|
|
176
|
+
/** Merged time-curve head amplitude at age 0 (#430) — the pre-#430 slow
|
|
177
|
+
* weight 0.15 + fresh bonus 0.15: the freshness job is now the early steep
|
|
178
|
+
* part of ONE curve. The head joins value-continuously onto the slow region
|
|
179
|
+
* at RECENCY_HEAD_DAYS; its rate is DERIVED from that continuity, never a
|
|
180
|
+
* free constant. */
|
|
181
|
+
exports.RECENCY_HEAD_WEIGHT = 0.30;
|
|
182
|
+
/** Join age in days of the merged time curve (#430) — the head window edge,
|
|
183
|
+
* carried over from the pre-#430 freshness window. Nightly capture means
|
|
184
|
+
* 1 day is the floor of "fresh" (#191 Phase B). */
|
|
185
|
+
exports.RECENCY_HEAD_DAYS = 7;
|
|
121
186
|
/** Score multiplier for a memory a later decision superseded (0.15.2; the
|
|
122
187
|
* belief walk (#393 D) is the primary mechanism — this is the safety net
|
|
123
188
|
* for rows the walk does not reach). */
|
|
124
189
|
exports.SUPERSEDED_DEMOTION = 0.50;
|
|
125
|
-
/** #
|
|
126
|
-
*
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
190
|
+
/** #430 merged scope affinity — ONE term replacing #203's two boosts (the
|
|
191
|
+
* project-affinity and domain-affinity constants) at their shared value.
|
|
192
|
+
* Boost = max(project-match indicator (1 on exact match), max overlapping
|
|
193
|
+
* domain-tag weight) × this weight: the strongest single scope signal counts
|
|
194
|
+
* once, never stacked. ADDITIVE, zero-boost neutral, never a penalty — an
|
|
195
|
+
* absent scope adds exactly 0; a foreign memory ranks equal, not lower. */
|
|
196
|
+
exports.SCOPE_AFFINITY_WEIGHT = 0.15;
|
|
130
197
|
/** #205 RRF k parameter (1/(k+rank+1)) — matches the pre-#205 hardcoded 60
|
|
131
198
|
* so the no-config path was byte-identical to 0.15.3. */
|
|
132
199
|
exports.RRF_K = 60;
|
package/dist/capture-health.d.ts
CHANGED
|
@@ -17,10 +17,19 @@
|
|
|
17
17
|
* `capture_health`: the most recent day with rows, grouped by machine ×
|
|
18
18
|
* agent (posts / sessions / bytes / held / retried), bytes DESC.
|
|
19
19
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
20
|
+
* - readCaptureHealthWindow(db) — the same grouping over the ROLLING
|
|
21
|
+
* capture window (#409 fix round 7, owner ruling 2026-09-14: the card
|
|
22
|
+
* shows the normal 30 days like the band's other cards). The newest
|
|
23
|
+
* night's rows stay on the payload too (secondary info: the card's
|
|
24
|
+
* "tonight" suffix); the window sums are the card's face.
|
|
25
|
+
*
|
|
26
|
+
* Retention: CAPTURE_HEALTH_WINDOW_DAYS (30), pruned inside
|
|
27
|
+
* recordDistillActivity at most ONCE per process per UTC day (in-module
|
|
28
|
+
* memo) — the insert path must not pay a DELETE on every POST. Retention and
|
|
29
|
+
* the window aggregate share the ONE constant, so the card can never claim a
|
|
30
|
+
* window the store no longer has rows for. Old rows exist to feed the card's
|
|
31
|
+
* rolling window, nothing else; the durable record of WHAT was captured is
|
|
32
|
+
* the memories themselves.
|
|
24
33
|
*/
|
|
25
34
|
import type Database from "better-sqlite3";
|
|
26
35
|
/** A /distill POST outcome. 'held' = the client will retry (cursor held):
|
|
@@ -64,6 +73,13 @@ export interface CaptureHealth {
|
|
|
64
73
|
day: string | null;
|
|
65
74
|
rows: CaptureHealthRow[];
|
|
66
75
|
}
|
|
76
|
+
/** The rolling capture window the console's card shows, in days — ALSO the
|
|
77
|
+
* retention horizon for distill_activity (the recorder prunes past it).
|
|
78
|
+
* Owner ruling 2026-09-14: the capture card shows the normal 30 day like
|
|
79
|
+
* the band's other cards, so the store must retain enough nights to sum
|
|
80
|
+
* one. ONE constant feeds both the window aggregate and the prune: the card
|
|
81
|
+
* can never claim a window the store no longer has rows for. */
|
|
82
|
+
export declare const CAPTURE_HEALTH_WINDOW_DAYS = 30;
|
|
67
83
|
/** Test-only: re-arm the once-per-day prune memo. The memo is deliberately
|
|
68
84
|
* process-global (production must not DELETE per insert); the test suite
|
|
69
85
|
* needs it fresh per case. Exported for testability, same precedent as
|
|
@@ -75,7 +91,8 @@ export declare function resetDistillPruneMemoForTests(): void;
|
|
|
75
91
|
* COALESCE(segment_id, '') so legacy whole-session POSTs — no segment_id —
|
|
76
92
|
* retry-match on the empty key). A NULL session_id never matches anything:
|
|
77
93
|
* without a session id the POST has no identity to be a retry OF. Also prunes
|
|
78
|
-
* rows
|
|
94
|
+
* rows outside the rolling capture window (CAPTURE_HEALTH_WINDOW_DAYS), at
|
|
95
|
+
* most once per process per UTC day.
|
|
79
96
|
*/
|
|
80
97
|
export declare function recordDistillActivity(db: Database.Database, entry: DistillActivityEntry): void;
|
|
81
98
|
/**
|
|
@@ -85,3 +102,23 @@ export declare function recordDistillActivity(db: Database.Database, entry: Dist
|
|
|
85
102
|
* ids don't count (no identity). Sorted bytes DESC (the card's bar scale).
|
|
86
103
|
*/
|
|
87
104
|
export declare function readCaptureHealth(db: Database.Database): CaptureHealth;
|
|
105
|
+
/** The /dashboard/data capture_health window block: the echoed window length
|
|
106
|
+
* + the per machine × agent rows over it. Field names are the WIRE keys —
|
|
107
|
+
* dashboard.ts spreads this over the newest-night shape, so the keys must
|
|
108
|
+
* not collide with {day, rows}. window_rows=[] when the window is empty
|
|
109
|
+
* (fresh install, static snapshot without activity rows — the page keeps its
|
|
110
|
+
* counts fallback). */
|
|
111
|
+
export interface CaptureHealthWindow {
|
|
112
|
+
window_days: number;
|
|
113
|
+
window_rows: CaptureHealthRow[];
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Aggregate the ROLLING capture window (CAPTURE_HEALTH_WINDOW_DAYS calendar
|
|
117
|
+
* days, today inclusive) into per machine × agent bundles — the card's face
|
|
118
|
+
* (#409 fix round 7: the band's other cards are all 30-day; the newest night
|
|
119
|
+
* moves to secondary info). Same shape as readCaptureHealth's rows, but
|
|
120
|
+
* summed across nights: sessions stays COUNT(DISTINCT session_id), so a
|
|
121
|
+
* session retried on a later night counts once (identity, not occurrences).
|
|
122
|
+
* Sorted bytes DESC (the card's bar scale).
|
|
123
|
+
*/
|
|
124
|
+
export declare function readCaptureHealthWindow(db: Database.Database): CaptureHealthWindow;
|
package/dist/capture-health.js
CHANGED
|
@@ -18,20 +18,43 @@
|
|
|
18
18
|
* `capture_health`: the most recent day with rows, grouped by machine ×
|
|
19
19
|
* agent (posts / sessions / bytes / held / retried), bytes DESC.
|
|
20
20
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
21
|
+
* - readCaptureHealthWindow(db) — the same grouping over the ROLLING
|
|
22
|
+
* capture window (#409 fix round 7, owner ruling 2026-09-14: the card
|
|
23
|
+
* shows the normal 30 days like the band's other cards). The newest
|
|
24
|
+
* night's rows stay on the payload too (secondary info: the card's
|
|
25
|
+
* "tonight" suffix); the window sums are the card's face.
|
|
26
|
+
*
|
|
27
|
+
* Retention: CAPTURE_HEALTH_WINDOW_DAYS (30), pruned inside
|
|
28
|
+
* recordDistillActivity at most ONCE per process per UTC day (in-module
|
|
29
|
+
* memo) — the insert path must not pay a DELETE on every POST. Retention and
|
|
30
|
+
* the window aggregate share the ONE constant, so the card can never claim a
|
|
31
|
+
* window the store no longer has rows for. Old rows exist to feed the card's
|
|
32
|
+
* rolling window, nothing else; the durable record of WHAT was captured is
|
|
33
|
+
* the memories themselves.
|
|
25
34
|
*/
|
|
26
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.CAPTURE_HEALTH_WINDOW_DAYS = void 0;
|
|
27
37
|
exports.resetDistillPruneMemoForTests = resetDistillPruneMemoForTests;
|
|
28
38
|
exports.recordDistillActivity = recordDistillActivity;
|
|
29
39
|
exports.readCaptureHealth = readCaptureHealth;
|
|
40
|
+
exports.readCaptureHealthWindow = readCaptureHealthWindow;
|
|
30
41
|
const storage_js_1 = require("./storage.js");
|
|
31
42
|
/** Once-per-process-per-UTC-day prune memo (see module doc). The UTC day of
|
|
32
43
|
* the last insert that ran the DELETE. Tests reset it because vitest runs
|
|
33
44
|
* every suite in ONE process — see resetDistillPruneMemoForTests. */
|
|
34
45
|
let lastPruneDay = null;
|
|
46
|
+
/** The rolling capture window the console's card shows, in days — ALSO the
|
|
47
|
+
* retention horizon for distill_activity (the recorder prunes past it).
|
|
48
|
+
* Owner ruling 2026-09-14: the capture card shows the normal 30 day like
|
|
49
|
+
* the band's other cards, so the store must retain enough nights to sum
|
|
50
|
+
* one. ONE constant feeds both the window aggregate and the prune: the card
|
|
51
|
+
* can never claim a window the store no longer has rows for. */
|
|
52
|
+
exports.CAPTURE_HEALTH_WINDOW_DAYS = 30;
|
|
53
|
+
/** Inclusive window cutoff as SQL: day >= today−(N−1) — the last N calendar
|
|
54
|
+
* days with today counted (the same inclusive convention as
|
|
55
|
+
* /dashboard/events' days param). The prune keeps exactly this predicate's
|
|
56
|
+
* complement (day < cutoff is deleted), so retained rows == window rows. */
|
|
57
|
+
const WINDOW_CUTOFF_SQL = `date('now', '-${exports.CAPTURE_HEALTH_WINDOW_DAYS - 1} days')`;
|
|
35
58
|
/** Test-only: re-arm the once-per-day prune memo. The memo is deliberately
|
|
36
59
|
* process-global (production must not DELETE per insert); the test suite
|
|
37
60
|
* needs it fresh per case. Exported for testability, same precedent as
|
|
@@ -52,7 +75,8 @@ function optString(v) {
|
|
|
52
75
|
* COALESCE(segment_id, '') so legacy whole-session POSTs — no segment_id —
|
|
53
76
|
* retry-match on the empty key). A NULL session_id never matches anything:
|
|
54
77
|
* without a session id the POST has no identity to be a retry OF. Also prunes
|
|
55
|
-
* rows
|
|
78
|
+
* rows outside the rolling capture window (CAPTURE_HEALTH_WINDOW_DAYS), at
|
|
79
|
+
* most once per process per UTC day.
|
|
56
80
|
*/
|
|
57
81
|
function recordDistillActivity(db, entry) {
|
|
58
82
|
const ts = entry.ts ?? new Date().toISOString();
|
|
@@ -72,9 +96,11 @@ function recordDistillActivity(db, entry) {
|
|
|
72
96
|
db.prepare(`INSERT INTO distill_activity (ts, day, machine, agent, session_id, segment_id, bytes, outcome, retried)
|
|
73
97
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(ts, day, (0, storage_js_1.sanitizeSourceMachine)(entry.machine) ?? "", optString(entry.agent) ?? "unknown", sessionId, segmentId, entry.bytes, entry.outcome, retried);
|
|
74
98
|
// Prune, at most once per process per UTC day. Runs AFTER the insert so the
|
|
75
|
-
// triggering row is subject to the same window as everything else.
|
|
99
|
+
// triggering row is subject to the same window as everything else. The
|
|
100
|
+
// cutoff is the capture window's own edge — retention keeps exactly the
|
|
101
|
+
// nights the card's rolling view sums (see CAPTURE_HEALTH_WINDOW_DAYS).
|
|
76
102
|
if (lastPruneDay !== day) {
|
|
77
|
-
db.prepare(
|
|
103
|
+
db.prepare(`DELETE FROM distill_activity WHERE day < ${WINDOW_CUTOFF_SQL}`).run();
|
|
78
104
|
lastPruneDay = day;
|
|
79
105
|
}
|
|
80
106
|
}
|
|
@@ -104,3 +130,27 @@ function readCaptureHealth(db) {
|
|
|
104
130
|
.all(dayRow.day);
|
|
105
131
|
return { day: dayRow.day, rows };
|
|
106
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* Aggregate the ROLLING capture window (CAPTURE_HEALTH_WINDOW_DAYS calendar
|
|
135
|
+
* days, today inclusive) into per machine × agent bundles — the card's face
|
|
136
|
+
* (#409 fix round 7: the band's other cards are all 30-day; the newest night
|
|
137
|
+
* moves to secondary info). Same shape as readCaptureHealth's rows, but
|
|
138
|
+
* summed across nights: sessions stays COUNT(DISTINCT session_id), so a
|
|
139
|
+
* session retried on a later night counts once (identity, not occurrences).
|
|
140
|
+
* Sorted bytes DESC (the card's bar scale).
|
|
141
|
+
*/
|
|
142
|
+
function readCaptureHealthWindow(db) {
|
|
143
|
+
const rows = db
|
|
144
|
+
.prepare(`SELECT machine, agent,
|
|
145
|
+
COUNT(*) AS posts,
|
|
146
|
+
COUNT(DISTINCT session_id) AS sessions,
|
|
147
|
+
COALESCE(SUM(bytes), 0) AS bytes,
|
|
148
|
+
COALESCE(SUM(outcome = 'held'), 0) AS held,
|
|
149
|
+
COALESCE(SUM(retried), 0) AS retried
|
|
150
|
+
FROM distill_activity
|
|
151
|
+
WHERE day >= ${WINDOW_CUTOFF_SQL}
|
|
152
|
+
GROUP BY machine, agent
|
|
153
|
+
ORDER BY bytes DESC`)
|
|
154
|
+
.all();
|
|
155
|
+
return { window_days: exports.CAPTURE_HEALTH_WINDOW_DAYS, window_rows: rows };
|
|
156
|
+
}
|
package/dist/capture-pause.d.ts
CHANGED
|
@@ -22,9 +22,11 @@
|
|
|
22
22
|
* /recall-index, /memory) carries no agent/machine identity on the wire, so
|
|
23
23
|
* attributing it would need new client fields — the heartbeats the spec
|
|
24
24
|
* forbids. Thresholds (green ≤36h — a nightly poster reads online through
|
|
25
|
-
* the following day; amber ≤7d
|
|
26
|
-
*
|
|
27
|
-
*
|
|
25
|
+
* the following day; amber ≤7d; none beyond or with no rows) are page-side
|
|
26
|
+
* presentation; this module just reports the newest ts per bundle. (Amber's
|
|
27
|
+
* 7d predates and outlives the old 7-day retention — activity rows are now
|
|
28
|
+
* kept for the capture card's 30-day window, CAPTURE_HEALTH_WINDOW_DAYS;
|
|
29
|
+
* the dot's thresholds are a design choice, not a retention artifact.)
|
|
28
30
|
*
|
|
29
31
|
* Pure, unit-testable without express (the capture-health.ts layering):
|
|
30
32
|
* mcp-server.ts and dashboard.ts wire these functions to the live db.
|
|
@@ -81,6 +83,7 @@ export declare function listCapturePauses(db: Database.Database): CapturePause[]
|
|
|
81
83
|
* MAX(ts) with that latest row's outcome, derive the harness per agent, then
|
|
82
84
|
* merge same-bundle agents keeping the newest ts (one dot per bundle, not
|
|
83
85
|
* per profile). Reads only distill_activity, which the recorder prunes to
|
|
84
|
-
*
|
|
86
|
+
* the capture card's rolling window (CAPTURE_HEALTH_WINDOW_DAYS) —
|
|
87
|
+
* older-than-window bundles simply have no rows and no dot.
|
|
85
88
|
*/
|
|
86
89
|
export declare function readFleetLastSeen(db: Database.Database): FleetLastSeen[];
|
package/dist/capture-pause.js
CHANGED
|
@@ -23,9 +23,11 @@
|
|
|
23
23
|
* /recall-index, /memory) carries no agent/machine identity on the wire, so
|
|
24
24
|
* attributing it would need new client fields — the heartbeats the spec
|
|
25
25
|
* forbids. Thresholds (green ≤36h — a nightly poster reads online through
|
|
26
|
-
* the following day; amber ≤7d
|
|
27
|
-
*
|
|
28
|
-
*
|
|
26
|
+
* the following day; amber ≤7d; none beyond or with no rows) are page-side
|
|
27
|
+
* presentation; this module just reports the newest ts per bundle. (Amber's
|
|
28
|
+
* 7d predates and outlives the old 7-day retention — activity rows are now
|
|
29
|
+
* kept for the capture card's 30-day window, CAPTURE_HEALTH_WINDOW_DAYS;
|
|
30
|
+
* the dot's thresholds are a design choice, not a retention artifact.)
|
|
29
31
|
*
|
|
30
32
|
* Pure, unit-testable without express (the capture-health.ts layering):
|
|
31
33
|
* mcp-server.ts and dashboard.ts wire these functions to the live db.
|
|
@@ -97,7 +99,8 @@ function listCapturePauses(db) {
|
|
|
97
99
|
* MAX(ts) with that latest row's outcome, derive the harness per agent, then
|
|
98
100
|
* merge same-bundle agents keeping the newest ts (one dot per bundle, not
|
|
99
101
|
* per profile). Reads only distill_activity, which the recorder prunes to
|
|
100
|
-
*
|
|
102
|
+
* the capture card's rolling window (CAPTURE_HEALTH_WINDOW_DAYS) —
|
|
103
|
+
* older-than-window bundles simply have no rows and no dot.
|
|
101
104
|
*/
|
|
102
105
|
function readFleetLastSeen(db) {
|
|
103
106
|
const rows = db
|
package/dist/consolidate.d.ts
CHANGED
|
@@ -328,6 +328,25 @@ export declare function stageDecayPrune(db: Database.Database, dryRun: boolean):
|
|
|
328
328
|
pruned: number;
|
|
329
329
|
failed: number;
|
|
330
330
|
};
|
|
331
|
+
/**
|
|
332
|
+
* ONE application of the #448 promotion formula:
|
|
333
|
+
* `S + PROMOTION_RATE × (1 − S / IMPORTANCE_CEILING) × S^(−0.3)`, clamped at
|
|
334
|
+
* the ceiling. Pure + exported for exact-value tests; the stage applies it
|
|
335
|
+
* iterated once per new use — iteration is what makes the ceiling asymptotic
|
|
336
|
+
* (a count-sized single shot would overshoot it).
|
|
337
|
+
*
|
|
338
|
+
* The formula's S input is floored at PROMOTION_STRENGTH_FLOOR (#453 review,
|
|
339
|
+
* owner decision 2026-09-16): S^(−0.3) has a zero-singularity, and a writable
|
|
340
|
+
* base_strength of 0.0 would otherwise leap to the ceiling via Infinity→clamp
|
|
341
|
+
* on a single access. The floor bounds the first boost at ≈+0.13; it clamps
|
|
342
|
+
* the INPUT only — the stored 0.0 row is not rewritten, and the promoted
|
|
343
|
+
* result is nonzero from then on.
|
|
344
|
+
*/
|
|
345
|
+
export declare function applyStrengthPromotion(baseStrength: number): number;
|
|
346
|
+
export declare function stagePromotion(db: Database.Database, dryRun: boolean): {
|
|
347
|
+
promoted: number;
|
|
348
|
+
demoted_skipped: number;
|
|
349
|
+
};
|
|
331
350
|
/**
|
|
332
351
|
* Default soft cap on the memory corpus (#245). Above this the lowest-value
|
|
333
352
|
* memories are evicted each nightly. 10000 balances headroom for a busy
|
package/dist/consolidate.js
CHANGED
|
@@ -54,6 +54,8 @@ exports.buildSupersessionPrompt = buildSupersessionPrompt;
|
|
|
54
54
|
exports.parseSupersessionReply = parseSupersessionReply;
|
|
55
55
|
exports.stageSupersession = stageSupersession;
|
|
56
56
|
exports.stageDecayPrune = stageDecayPrune;
|
|
57
|
+
exports.applyStrengthPromotion = applyStrengthPromotion;
|
|
58
|
+
exports.stagePromotion = stagePromotion;
|
|
57
59
|
exports.resolveMemorySoftCap = resolveMemorySoftCap;
|
|
58
60
|
exports.stageMemoryCapEviction = stageMemoryCapEviction;
|
|
59
61
|
exports.runConsolidation = runConsolidation;
|
|
@@ -1018,37 +1020,6 @@ function classifyRelationship(source, target, similarity) {
|
|
|
1018
1020
|
return "relates_to";
|
|
1019
1021
|
}
|
|
1020
1022
|
// ---------------------------------------------------------------------------
|
|
1021
|
-
// Stage 3.5: Hub Detection & Strength Boost
|
|
1022
|
-
// ---------------------------------------------------------------------------
|
|
1023
|
-
const HUB_BOOST = 0.1;
|
|
1024
|
-
// #425: the release-managed importance ceiling (calibration.ts) — hub boosts
|
|
1025
|
-
// can no longer push a row to base 1.0 (decay rate exactly 1.0 = immortal).
|
|
1026
|
-
const HUB_STRENGTH_CAP = CALIBRATION.IMPORTANCE_CEILING;
|
|
1027
|
-
function stageHubBoost(db, dryRun) {
|
|
1028
|
-
const hubs = (0, graph_js_1.detectHubs)(db);
|
|
1029
|
-
if (hubs.length === 0)
|
|
1030
|
-
return { hubs_found: 0, boosted: 0 };
|
|
1031
|
-
let boosted = 0;
|
|
1032
|
-
if (!dryRun) {
|
|
1033
|
-
const stmt = db.prepare("UPDATE memories SET base_strength = MIN(?, base_strength + ?) WHERE id = ? AND base_strength < ?");
|
|
1034
|
-
const tx = db.transaction(() => {
|
|
1035
|
-
for (const hub of hubs) {
|
|
1036
|
-
const result = stmt.run(HUB_STRENGTH_CAP, HUB_BOOST, hub.id, HUB_STRENGTH_CAP);
|
|
1037
|
-
if (result.changes > 0)
|
|
1038
|
-
boosted++;
|
|
1039
|
-
}
|
|
1040
|
-
});
|
|
1041
|
-
tx();
|
|
1042
|
-
}
|
|
1043
|
-
else {
|
|
1044
|
-
boosted = hubs.length;
|
|
1045
|
-
}
|
|
1046
|
-
if (hubs.length > 0) {
|
|
1047
|
-
console.log(`[hicortex] Hub detection: ${hubs.length} hubs found, ${boosted} boosted (+${HUB_BOOST})`);
|
|
1048
|
-
}
|
|
1049
|
-
return { hubs_found: hubs.length, boosted };
|
|
1050
|
-
}
|
|
1051
|
-
// ---------------------------------------------------------------------------
|
|
1052
1023
|
// Stage 3.7: Supersession Detection (#191 Phase B)
|
|
1053
1024
|
// ---------------------------------------------------------------------------
|
|
1054
1025
|
//
|
|
@@ -1327,16 +1298,11 @@ function stageDecayPrune(db, dryRun) {
|
|
|
1327
1298
|
const now = new Date();
|
|
1328
1299
|
const cutoff = new Date(now.getTime() - CONSOLIDATE_PRUNE_MIN_AGE_DAYS * 24 * 60 * 60 * 1000);
|
|
1329
1300
|
const oldUnaccessed = storage.getPruneCandidates(db, cutoff.toISOString());
|
|
1330
|
-
const linkCounts = storage.getAllLinkCounts(db);
|
|
1331
1301
|
let candidates = 0;
|
|
1332
1302
|
let pruned = 0;
|
|
1333
1303
|
let failed = 0;
|
|
1334
1304
|
for (const mem of oldUnaccessed) {
|
|
1335
|
-
const
|
|
1336
|
-
const eff = (0, retrieval_js_1.effectiveStrength)(mem.base_strength ?? 0.5, mem.last_accessed, now, {
|
|
1337
|
-
accessCount: 0,
|
|
1338
|
-
linkCount: memLinkCount,
|
|
1339
|
-
});
|
|
1305
|
+
const eff = (0, retrieval_js_1.effectiveStrength)(mem.base_strength ?? 0.5, mem.last_accessed, now);
|
|
1340
1306
|
if (eff >= 0.01)
|
|
1341
1307
|
continue;
|
|
1342
1308
|
candidates++;
|
|
@@ -1353,6 +1319,112 @@ function stageDecayPrune(db, dryRun) {
|
|
|
1353
1319
|
return { candidates, pruned, failed };
|
|
1354
1320
|
}
|
|
1355
1321
|
// ---------------------------------------------------------------------------
|
|
1322
|
+
// Stage: Strength promotion (#448) — the strength model's upward path
|
|
1323
|
+
// ---------------------------------------------------------------------------
|
|
1324
|
+
//
|
|
1325
|
+
// Real use of a memory (a surfaced /search or /recent hit, a hicortex_get)
|
|
1326
|
+
// bumps access_count (storage.strengthenMemory). This stage converts that use
|
|
1327
|
+
// signal into STRENGTH: every run, each memory whose access_count exceeds its
|
|
1328
|
+
// stored baseline (promotion_last_count, migration v20 — backfilled to
|
|
1329
|
+
// access_count at upgrade so lifetime counts are never replayed) is promoted
|
|
1330
|
+
// once per new use by the calibration formula, capped at the importance
|
|
1331
|
+
// ceiling. Decay stays untouched and uniform (#448 removed the hardening
|
|
1332
|
+
// terms) — promotion is the only way use raises a score.
|
|
1333
|
+
//
|
|
1334
|
+
// Deterministic (zero LLM) and idempotent: the baseline advances to
|
|
1335
|
+
// access_count in the same write, so a delta-less night is a no-op and a
|
|
1336
|
+
// crashed run replays exactly the unconsumed delta. The demotion set
|
|
1337
|
+
// (findDemotedIds: superseded/retracted rows + superseded_by-link sources)
|
|
1338
|
+
// plus status 'absorbed' rows (named explicitly — findDemotedIds excludes
|
|
1339
|
+
// them by design; they are filtered at candidacy elsewhere, but this stage
|
|
1340
|
+
// reads the memories table directly) get NO bump — hidden evidence must not
|
|
1341
|
+
// rise — but their baseline still advances, so a later un-mark never replays
|
|
1342
|
+
// a stale delta.
|
|
1343
|
+
/**
|
|
1344
|
+
* ONE application of the #448 promotion formula:
|
|
1345
|
+
* `S + PROMOTION_RATE × (1 − S / IMPORTANCE_CEILING) × S^(−0.3)`, clamped at
|
|
1346
|
+
* the ceiling. Pure + exported for exact-value tests; the stage applies it
|
|
1347
|
+
* iterated once per new use — iteration is what makes the ceiling asymptotic
|
|
1348
|
+
* (a count-sized single shot would overshoot it).
|
|
1349
|
+
*
|
|
1350
|
+
* The formula's S input is floored at PROMOTION_STRENGTH_FLOOR (#453 review,
|
|
1351
|
+
* owner decision 2026-09-16): S^(−0.3) has a zero-singularity, and a writable
|
|
1352
|
+
* base_strength of 0.0 would otherwise leap to the ceiling via Infinity→clamp
|
|
1353
|
+
* on a single access. The floor bounds the first boost at ≈+0.13; it clamps
|
|
1354
|
+
* the INPUT only — the stored 0.0 row is not rewritten, and the promoted
|
|
1355
|
+
* result is nonzero from then on.
|
|
1356
|
+
*/
|
|
1357
|
+
function applyStrengthPromotion(baseStrength) {
|
|
1358
|
+
const s = Math.max(baseStrength, CALIBRATION.PROMOTION_STRENGTH_FLOOR);
|
|
1359
|
+
return Math.min(s +
|
|
1360
|
+
CALIBRATION.PROMOTION_RATE *
|
|
1361
|
+
(1 - s / CALIBRATION.IMPORTANCE_CEILING) *
|
|
1362
|
+
Math.pow(s, -0.3), CALIBRATION.IMPORTANCE_CEILING);
|
|
1363
|
+
}
|
|
1364
|
+
function stagePromotion(db, dryRun) {
|
|
1365
|
+
// Candidates: rows with at least one use since their stored baseline.
|
|
1366
|
+
// COALESCE treats a NULL baseline as 0 — dup-over-loss: an unknown baseline
|
|
1367
|
+
// promotes once, a held one never forgets a use.
|
|
1368
|
+
const rows = db
|
|
1369
|
+
.prepare(`SELECT id, base_strength, access_count, status,
|
|
1370
|
+
access_count - COALESCE(promotion_last_count, 0) AS delta
|
|
1371
|
+
FROM memories
|
|
1372
|
+
WHERE access_count > COALESCE(promotion_last_count, 0)`)
|
|
1373
|
+
.all();
|
|
1374
|
+
if (rows.length === 0)
|
|
1375
|
+
return { promoted: 0, demoted_skipped: 0 };
|
|
1376
|
+
const demoted = (0, retrieval_js_1.findDemotedIds)(db, rows.map((r) => r.id));
|
|
1377
|
+
let promoted = 0;
|
|
1378
|
+
let demotedSkipped = 0;
|
|
1379
|
+
let totalGain = 0;
|
|
1380
|
+
const writes = [];
|
|
1381
|
+
for (const row of rows) {
|
|
1382
|
+
const accessCount = row.access_count ?? 0;
|
|
1383
|
+
// Absorbed rows are invisible to recall but sit in the memories table —
|
|
1384
|
+
// they are demotion-set members this query CAN see (findDemotedIds
|
|
1385
|
+
// deliberately excludes them), so name them here.
|
|
1386
|
+
if (demoted.has(row.id) || row.status === "absorbed") {
|
|
1387
|
+
demotedSkipped++;
|
|
1388
|
+
writes.push({ id: row.id, fields: { promotion_last_count: accessCount } });
|
|
1389
|
+
continue;
|
|
1390
|
+
}
|
|
1391
|
+
// base_strength is NOT NULL after scoring; the `?? 0.5` mirrors
|
|
1392
|
+
// stageDecayPrune's defensive default for unscored rows (inserts at 0.5).
|
|
1393
|
+
const startStrength = row.base_strength ?? 0.5;
|
|
1394
|
+
let strength = startStrength;
|
|
1395
|
+
for (let i = 0; i < row.delta; i++)
|
|
1396
|
+
strength = applyStrengthPromotion(strength);
|
|
1397
|
+
totalGain += strength - startStrength;
|
|
1398
|
+
promoted++;
|
|
1399
|
+
writes.push({
|
|
1400
|
+
id: row.id,
|
|
1401
|
+
fields: { base_strength: strength, promotion_last_count: accessCount },
|
|
1402
|
+
});
|
|
1403
|
+
}
|
|
1404
|
+
// #459: the stage's one-line summary in the shared stage idiom (rows
|
|
1405
|
+
// examined / promoted / total gain, like the supersession summary) — the
|
|
1406
|
+
// stage writes its report in-memory only, so the log line is the soak-time
|
|
1407
|
+
// health signal. Zero examined rows stay silent (the supersession gate).
|
|
1408
|
+
if (dryRun) {
|
|
1409
|
+
console.log(`[hicortex] Strength promotion (dry-run): ${rows.length} examined, would promote ` +
|
|
1410
|
+
`${promoted} (+${totalGain.toFixed(3)} total strength, ` +
|
|
1411
|
+
`${demotedSkipped} demotion-set rows advance their baseline only).`);
|
|
1412
|
+
return { promoted, demoted_skipped: demotedSkipped };
|
|
1413
|
+
}
|
|
1414
|
+
// One transaction for the whole stage (the stageMemoryCapEviction pattern):
|
|
1415
|
+
// strength + baseline move together or not at all — a baseline that
|
|
1416
|
+
// advanced without its bump (or vice versa) would lose or replay a use.
|
|
1417
|
+
const tx = db.transaction(() => {
|
|
1418
|
+
for (const w of writes)
|
|
1419
|
+
storage.updateMemory(db, w.id, w.fields);
|
|
1420
|
+
});
|
|
1421
|
+
tx();
|
|
1422
|
+
console.log(`[hicortex] Strength promotion: ${rows.length} examined, promoted ${promoted} ` +
|
|
1423
|
+
`(+${totalGain.toFixed(3)} total strength, ` +
|
|
1424
|
+
`${demotedSkipped} demotion-set rows advance their baseline only).`);
|
|
1425
|
+
return { promoted, demoted_skipped: demotedSkipped };
|
|
1426
|
+
}
|
|
1427
|
+
// ---------------------------------------------------------------------------
|
|
1356
1428
|
// Stage 4.5: Memory cap eviction (#245)
|
|
1357
1429
|
// ---------------------------------------------------------------------------
|
|
1358
1430
|
//
|
|
@@ -1365,10 +1437,13 @@ function stageDecayPrune(db, dryRun) {
|
|
|
1365
1437
|
//
|
|
1366
1438
|
// Eviction reuses the SAME effectiveStrength() the recall ranker uses — no
|
|
1367
1439
|
// formula duplication, so the eviction criterion cannot drift from what
|
|
1368
|
-
// surfaces in the top-k.
|
|
1369
|
-
//
|
|
1370
|
-
//
|
|
1371
|
-
//
|
|
1440
|
+
// surfaces in the top-k. #448: the ordering is base + recency ONLY (the
|
|
1441
|
+
// access/link hardening terms are gone), and a row used since the last run
|
|
1442
|
+
// was promoted by stagePromotion earlier in the SAME run — a promoted row
|
|
1443
|
+
// survives a cap it would otherwise have lost. The evicted tail is, by
|
|
1444
|
+
// construction, the tail that was not surfacing anyway (cold, decayed). Ties
|
|
1445
|
+
// are broken by oldest COALESCE(last_accessed, created_at) — i.e. the
|
|
1446
|
+
// memories that have gone longest without anyone looking at them.
|
|
1372
1447
|
//
|
|
1373
1448
|
// `cap = 0` disables the stage (indefinite growth — the pre-#245 default is
|
|
1374
1449
|
// preserved opt-out). The JS-side sort is O(n log n); at 10K memories the
|
|
@@ -1440,19 +1515,15 @@ function stageMemoryCapEviction(db, dryRun, cap) {
|
|
|
1440
1515
|
// never-shown memories sort by when they entered the corpus. Same
|
|
1441
1516
|
// non-absorbed predicate as the count above.
|
|
1442
1517
|
const rows = db
|
|
1443
|
-
.prepare(`SELECT id, base_strength, last_accessed,
|
|
1518
|
+
.prepare(`SELECT id, base_strength, last_accessed, created_at
|
|
1444
1519
|
FROM memories
|
|
1445
1520
|
WHERE COALESCE(status, '') != 'absorbed'`)
|
|
1446
1521
|
.all();
|
|
1447
|
-
const linkCounts = storage.getAllLinkCounts(db);
|
|
1448
1522
|
const now = new Date();
|
|
1449
1523
|
// Decorate + sort: lowest effectiveStrength first; ties broken by oldest
|
|
1450
1524
|
// COALESCE(last_accessed, created_at). The victims are the first `surplus`.
|
|
1451
1525
|
const decorated = rows.map((r) => {
|
|
1452
|
-
const eff = (0, retrieval_js_1.effectiveStrength)(r.base_strength ?? 0.5, r.last_accessed, now
|
|
1453
|
-
accessCount: r.access_count ?? 0,
|
|
1454
|
-
linkCount: linkCounts.get(r.id) ?? 0,
|
|
1455
|
-
});
|
|
1526
|
+
const eff = (0, retrieval_js_1.effectiveStrength)(r.base_strength ?? 0.5, r.last_accessed, now);
|
|
1456
1527
|
return {
|
|
1457
1528
|
id: r.id,
|
|
1458
1529
|
eff,
|
|
@@ -1617,6 +1688,13 @@ deadline) {
|
|
|
1617
1688
|
new_memory_count: precheck.newMemories.length,
|
|
1618
1689
|
unscored_count: scoreMemories.length - precheck.newMemories.length,
|
|
1619
1690
|
};
|
|
1691
|
+
// Strength promotion (#448) — runs in the pre-skip deterministic zone, in
|
|
1692
|
+
// the same placement discipline as memory_cap BELOW it: accesses happen on
|
|
1693
|
+
// quiet nights too (no new memories must still promote the day's uses),
|
|
1694
|
+
// and a used row must be promoted BEFORE eviction victims are chosen (a
|
|
1695
|
+
// promoted row survives a cap it would otherwise lose). Zero LLM — it sits
|
|
1696
|
+
// before the BudgetTracker exists, ungated by budget and deadline.
|
|
1697
|
+
report.stages.promotion = stagePromotion(db, dryRun);
|
|
1620
1698
|
// Memory cap eviction (#245) — runs BEFORE the precheck skip so the corpus
|
|
1621
1699
|
// is bounded even on quiet nights (no new memories → precheck would skip,
|
|
1622
1700
|
// but the cap stage is pure DB: cheap, idempotent when under cap).
|
|
@@ -1689,10 +1767,6 @@ deadline) {
|
|
|
1689
1767
|
if (!deadline?.hit("links")) {
|
|
1690
1768
|
report.stages.links = await stageLinks(db, precheck.newMemories, embedFn, dryRun, llm, budget);
|
|
1691
1769
|
}
|
|
1692
|
-
// Stage 3.5: Hub Detection — boost highly-connected memories
|
|
1693
|
-
if (!deadline?.hit("hub_boost")) {
|
|
1694
|
-
report.stages.hub_boost = stageHubBoost(db, dryRun);
|
|
1695
|
-
}
|
|
1696
1770
|
// Stage 3.7: Supersession Detection (#191 Phase B)
|
|
1697
1771
|
if (!deadline?.hit("supersession")) {
|
|
1698
1772
|
report.stages.supersession = await stageSupersession(db, llm, budget, embedFn, dryRun, stateDir, { ...supersessionOptions, deadline });
|