@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/mcp-server.js
CHANGED
|
@@ -787,7 +787,8 @@ async function startServer(options = {}) {
|
|
|
787
787
|
`/novelty=${CALIBRATION.NOVELTY_FLOOR_SLOTS}` +
|
|
788
788
|
` · ` +
|
|
789
789
|
`score sim=${scoringCfg.similarity}/str=${scoringCfg.strength}/conn=${scoringCfg.connections}` +
|
|
790
|
-
`/
|
|
790
|
+
`/K=${scoringCfg.connectionsSaturation}` +
|
|
791
|
+
`/rec=${scoringCfg.recency}, head=${scoringCfg.recencyHead}@${scoringCfg.recencyHeadDays}d, ` +
|
|
791
792
|
`superseded×${scoringCfg.supersededDemotion}` +
|
|
792
793
|
`, intent w=${sessionIntentCfg.weight}` +
|
|
793
794
|
(sessionIntentCfg.weight === 0 ? " (disabled)" : ""));
|
|
@@ -1548,7 +1549,7 @@ async function startServer(options = {}) {
|
|
|
1548
1549
|
//
|
|
1549
1550
|
// An enrich is EVIDENCE ABOUT IMPORTANCE: it bumps corroboration_count and
|
|
1550
1551
|
// base_strength (+the calibration delta, capped at 1.0) — the same anchor
|
|
1551
|
-
// the nightly's LLM scoring
|
|
1552
|
+
// the nightly's LLM scoring writes. It must NOT touch
|
|
1552
1553
|
// access_count (reserved for real recall use) or shown_count (index
|
|
1553
1554
|
// exposure) — faking either corrupts the uses-per-showing adoption metric.
|
|
1554
1555
|
// Never a stage write: stages are derived presentation (the E-reframe).
|
package/dist/nofit.d.ts
CHANGED
|
@@ -38,7 +38,8 @@
|
|
|
38
38
|
* memory from pruning. Halving does not touch last_accessed/access_count.
|
|
39
39
|
* - Re-classification: a later run whose LLM tags it, or whose evolved
|
|
40
40
|
* prototypes clear the floor, gives it a (weak) primary — halving stops.
|
|
41
|
-
* Its strength is
|
|
41
|
+
* Its strength is restored only by the nightly promotion stage (#448),
|
|
42
|
+
* which converts each new access_count delta into a base_strength bump.
|
|
42
43
|
*
|
|
43
44
|
* PRUNE INTERACTION (verified against stageDecayPrune + effectiveStrength):
|
|
44
45
|
* prune fires when effectiveStrength < 0.01 for a >90-day-old, never-accessed
|
package/dist/nofit.js
CHANGED
|
@@ -39,7 +39,8 @@
|
|
|
39
39
|
* memory from pruning. Halving does not touch last_accessed/access_count.
|
|
40
40
|
* - Re-classification: a later run whose LLM tags it, or whose evolved
|
|
41
41
|
* prototypes clear the floor, gives it a (weak) primary — halving stops.
|
|
42
|
-
* Its strength is
|
|
42
|
+
* Its strength is restored only by the nightly promotion stage (#448),
|
|
43
|
+
* which converts each new access_count delta into a base_strength bump.
|
|
43
44
|
*
|
|
44
45
|
* PRUNE INTERACTION (verified against stageDecayPrune + effectiveStrength):
|
|
45
46
|
* prune fires when effectiveStrength < 0.01 for a >90-day-old, never-accessed
|
package/dist/recall-index.d.ts
CHANGED
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
* Strengthening semantics (the recall/decay alignment):
|
|
11
11
|
* - Appearing in the index = exposure: shown_count + last_accessed refresh
|
|
12
12
|
* (mild, temporary strengthen — the decay clock resets) via
|
|
13
|
-
* storage.touchMemoriesShown. NO access_count bump:
|
|
14
|
-
* shield, and the adoption metric stay driven by real
|
|
13
|
+
* storage.touchMemoriesShown. NO access_count bump: the promotion signal
|
|
14
|
+
* (#448), the prune shield, and the adoption metric stay driven by real
|
|
15
|
+
* use.
|
|
15
16
|
* - hicortex_get = use: full strengthen (access_count + 1).
|
|
16
17
|
*
|
|
17
18
|
* Anti-bloat gates: relevance floor (measured cosine, or a real BM25 match),
|
package/dist/recall-index.js
CHANGED
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
* Strengthening semantics (the recall/decay alignment):
|
|
12
12
|
* - Appearing in the index = exposure: shown_count + last_accessed refresh
|
|
13
13
|
* (mild, temporary strengthen — the decay clock resets) via
|
|
14
|
-
* storage.touchMemoriesShown. NO access_count bump:
|
|
15
|
-
* shield, and the adoption metric stay driven by real
|
|
14
|
+
* storage.touchMemoriesShown. NO access_count bump: the promotion signal
|
|
15
|
+
* (#448), the prune shield, and the adoption metric stay driven by real
|
|
16
|
+
* use.
|
|
16
17
|
* - hicortex_get = use: full strengthen (access_count + 1).
|
|
17
18
|
*
|
|
18
19
|
* Anti-bloat gates: relevance floor (measured cosine, or a real BM25 match),
|
package/dist/retrieval.d.ts
CHANGED
|
@@ -55,14 +55,26 @@ export interface ScoringWeights {
|
|
|
55
55
|
similarity: number;
|
|
56
56
|
strength: number;
|
|
57
57
|
connections: number;
|
|
58
|
+
/**
|
|
59
|
+
* #449 log-saturation degree K for the connections term: the credit is
|
|
60
|
+
* min(1, log1p(k)/log1p(K)) — full at k = K, exactly +0 at k = 0. A count,
|
|
61
|
+
* not a weight: validated ≥ 1 (the rrfK non-weight seam precedent); no
|
|
62
|
+
* upper bound (the eval sweep widens K past the planted fixture degrees).
|
|
63
|
+
*/
|
|
64
|
+
connectionsSaturation: number;
|
|
58
65
|
recency: number;
|
|
59
|
-
|
|
60
|
-
|
|
66
|
+
/** #430 merged time curve: head amplitude at age 0 — the merged freshness
|
|
67
|
+
* job. A deliberate overshoot (max total 1.15 pre-clamp), NOT a fifth
|
|
68
|
+
* blend weight; the four blend weights still sum to 1.0. */
|
|
69
|
+
recencyHead: number;
|
|
70
|
+
/** #430 merged time curve: head window / join age in days. */
|
|
71
|
+
recencyHeadDays: number;
|
|
61
72
|
supersededDemotion: number;
|
|
62
|
-
/** #
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
73
|
+
/** #430 merged scope affinity: ONE term — max() of the project-match
|
|
74
|
+
* indicator (1 on exact match) and the max overlapping domain-tag weight,
|
|
75
|
+
* × this weight (born from #203's two additive boosts at their shared
|
|
76
|
+
* value). 0 (via the seam) disables the whole term. */
|
|
77
|
+
scopeAffinity: number;
|
|
66
78
|
/** #205 RRF k parameter (1/(k+rank+1)). Larger ⇒ shallower rank curve. */
|
|
67
79
|
rrfK: number;
|
|
68
80
|
/** #205 composite-score share of the final blend (RRF gets the remainder). */
|
|
@@ -208,7 +220,7 @@ export declare function l2ToCosine(distance: number): number;
|
|
|
208
220
|
*/
|
|
209
221
|
export declare function cosineBetweenVectors(a: Float32Array, b: Float32Array): number;
|
|
210
222
|
/**
|
|
211
|
-
* Compute decayed strength with adaptive decay (B+
|
|
223
|
+
* Compute decayed strength with adaptive decay (B+D model).
|
|
212
224
|
* Exported for use by consolidation decay/prune stage.
|
|
213
225
|
*
|
|
214
226
|
* #425 read-side law: the decay-relevant importance is CLAMPED at the
|
|
@@ -216,22 +228,29 @@ export declare function cosineBetweenVectors(a: Float32Array, b: Float32Array):
|
|
|
216
228
|
* exactly 1.0 the decay rate is exactly 1.0 and the row never decays, so
|
|
217
229
|
* legacy base-1.0 rows (and any write site that predates the cap) decay
|
|
218
230
|
* again. The clamp applies to explicit importance passes too.
|
|
231
|
+
*
|
|
232
|
+
* #448: the access/connectivity HARDENING terms are REMOVED — the fade rate
|
|
233
|
+
* no longer depends on access or link history (a briefly-used memory was
|
|
234
|
+
* near-immortal: 10 accesses cut the decay rate to 3% of normal). Real use
|
|
235
|
+
* now raises the STORED score via the nightly promotion stage
|
|
236
|
+
* (consolidate.ts stagePromotion) instead of slowing future decay, so a
|
|
237
|
+
* promoted-then-abandoned memory fades on the same 365-day clock as any
|
|
238
|
+
* other, from a higher anchor.
|
|
219
239
|
*/
|
|
220
240
|
export declare function effectiveStrength(baseStrength: number, lastAccessed: string | null, now: Date, options?: {
|
|
221
241
|
importance?: number;
|
|
222
|
-
accessCount?: number;
|
|
223
|
-
linkCount?: number;
|
|
224
242
|
}): number;
|
|
225
243
|
/**
|
|
226
244
|
* Return a composite relevance score in [0, 1] for a candidate memory.
|
|
227
245
|
* Exported for exact-value tests of the similarity component (#145).
|
|
228
246
|
*
|
|
229
|
-
*
|
|
230
|
-
* graded, zero-boost-neutral
|
|
231
|
-
* and
|
|
232
|
-
* 0 when the scope is absent
|
|
233
|
-
* (
|
|
234
|
-
* soft-exclusion). See
|
|
247
|
+
* Scope affinity (options.scope + options.tagWeights; #203, merged #430):
|
|
248
|
+
* ONE additive, graded, zero-boost-neutral term — max() of the project-match
|
|
249
|
+
* indicator (exact match ⇒ 1) and the max overlapping memory_tags.weight,
|
|
250
|
+
* × the scopeAffinity weight. It is 0 when the scope is absent
|
|
251
|
+
* (byte-identical to pre-#203) and NEVER negative (a foreign memory adds 0,
|
|
252
|
+
* never a penalty — penalties re-introduce soft-exclusion). See
|
|
253
|
+
* `AffinityScope`.
|
|
235
254
|
*/
|
|
236
255
|
export interface AffinityScope {
|
|
237
256
|
/** Exact-match project from the client (CC/OC cwd-derived; /search project). */
|
|
@@ -240,12 +259,13 @@ export interface AffinityScope {
|
|
|
240
259
|
* vocabulary as memory_tags (config `domains`). */
|
|
241
260
|
missionDomains?: string[];
|
|
242
261
|
}
|
|
243
|
-
export declare function computeScore(memory: Memory, distance: number, connectionCount: number,
|
|
262
|
+
export declare function computeScore(memory: Memory, distance: number, connectionCount: number, now: Date, options?: {
|
|
244
263
|
superseded?: boolean;
|
|
245
|
-
/** #203: when present,
|
|
264
|
+
/** #203/#430: when present, the scope-affinity boost is applied. */
|
|
246
265
|
scope?: AffinityScope;
|
|
247
266
|
/** Candidate's graded domain tags (memory_tags rows). Loaded batched for
|
|
248
|
-
* the whole candidate set in retrieve(); used for
|
|
267
|
+
* the whole candidate set in retrieve(); used for the scope-affinity
|
|
268
|
+
* term's domain signal. */
|
|
249
269
|
tagWeights?: Array<{
|
|
250
270
|
tag: string;
|
|
251
271
|
weight: number | null;
|
|
@@ -302,6 +322,11 @@ export declare function retrieve(db: Database.Database, embedFn: EmbedFn, query:
|
|
|
302
322
|
ftsCandidates?: (fetchLimit: number, sourceAgent?: string) => Array<Memory & {
|
|
303
323
|
rank: number;
|
|
304
324
|
}>;
|
|
325
|
+
/** #458 eval clock pin — the instant the decay/recency terms score
|
|
326
|
+
* against. Eval-only: no production caller passes it, and unset means
|
|
327
|
+
* the live clock (byte-identical to pre-#458). Mirrors the
|
|
328
|
+
* injectable-clock idiom of run-deadline.ts; see eval/eval-clock.ts. */
|
|
329
|
+
now?: Date;
|
|
305
330
|
}): Promise<MemorySearchResult[]>;
|
|
306
331
|
/**
|
|
307
332
|
* Get recent context, optionally filtered by project.
|
package/dist/retrieval.js
CHANGED
|
@@ -131,12 +131,14 @@ const SCORING_DEFAULTS = {
|
|
|
131
131
|
similarity: CALIBRATION.SCORE_SIMILARITY_WEIGHT,
|
|
132
132
|
strength: CALIBRATION.SCORE_STRENGTH_WEIGHT,
|
|
133
133
|
connections: CALIBRATION.SCORE_CONNECTIONS_WEIGHT,
|
|
134
|
+
// #449: the p99 of the real undirected degree distribution (see
|
|
135
|
+
// calibration.ts provenance) — saturates the top ~1% of linked memories.
|
|
136
|
+
connectionsSaturation: CALIBRATION.CONNECTIONS_SATURATION_DEGREE,
|
|
134
137
|
recency: CALIBRATION.SCORE_RECENCY_WEIGHT,
|
|
135
|
-
|
|
136
|
-
|
|
138
|
+
recencyHead: CALIBRATION.RECENCY_HEAD_WEIGHT,
|
|
139
|
+
recencyHeadDays: CALIBRATION.RECENCY_HEAD_DAYS,
|
|
137
140
|
supersededDemotion: CALIBRATION.SUPERSEDED_DEMOTION,
|
|
138
|
-
|
|
139
|
-
domainAffinity: CALIBRATION.DOMAIN_AFFINITY_WEIGHT,
|
|
141
|
+
scopeAffinity: CALIBRATION.SCOPE_AFFINITY_WEIGHT,
|
|
140
142
|
// #205 (calibration.ts): rrfK + rrfCompositeWeight match the pre-#205
|
|
141
143
|
// hardcoded values (60 and 0.8); the FTS per-list weight (1.0 → 0.5) is the
|
|
142
144
|
// one deliberate nudge toward vector — the bisection point where BM25F +
|
|
@@ -172,22 +174,36 @@ function configureScoring(overrides) {
|
|
|
172
174
|
const n = Number(v);
|
|
173
175
|
return Number.isFinite(n) && n >= 0 ? n : dflt;
|
|
174
176
|
};
|
|
177
|
+
// #449: the connections saturation degree is a COUNT ≥ 1 (log1p(K) must
|
|
178
|
+
// not divide by zero; K < 1 would saturate every k > 0 instantly). No
|
|
179
|
+
// upper bound — the rrfK non-weight precedent.
|
|
180
|
+
const numMin1 = (v, dflt) => {
|
|
181
|
+
const n = Number(v);
|
|
182
|
+
return Number.isFinite(n) && n >= 1 ? n : dflt;
|
|
183
|
+
};
|
|
175
184
|
scoringWeights = {
|
|
176
185
|
similarity: num(overrides?.similarity, SCORING_DEFAULTS.similarity, 0, 1),
|
|
177
186
|
strength: num(overrides?.strength, SCORING_DEFAULTS.strength, 0, 1),
|
|
178
187
|
connections: num(overrides?.connections, SCORING_DEFAULTS.connections, 0, 1),
|
|
188
|
+
connectionsSaturation: numMin1(overrides?.connectionsSaturation, SCORING_DEFAULTS.connectionsSaturation),
|
|
179
189
|
recency: num(overrides?.recency, SCORING_DEFAULTS.recency, 0, 1),
|
|
180
|
-
|
|
181
|
-
|
|
190
|
+
recencyHead: num(overrides?.recencyHead, SCORING_DEFAULTS.recencyHead, 0, 1),
|
|
191
|
+
recencyHeadDays: num(overrides?.recencyHeadDays, SCORING_DEFAULTS.recencyHeadDays, 0, 365),
|
|
182
192
|
supersededDemotion: num(overrides?.supersededDemotion, SCORING_DEFAULTS.supersededDemotion, 0, 1),
|
|
183
|
-
|
|
184
|
-
domainAffinity: num(overrides?.domainAffinity, SCORING_DEFAULTS.domainAffinity, 0, 1),
|
|
193
|
+
scopeAffinity: num(overrides?.scopeAffinity, SCORING_DEFAULTS.scopeAffinity, 0, 1),
|
|
185
194
|
rrfK: numW(overrides?.rrfK, SCORING_DEFAULTS.rrfK),
|
|
186
195
|
rrfCompositeWeight: num(overrides?.rrfCompositeWeight, SCORING_DEFAULTS.rrfCompositeWeight, 0, 1),
|
|
187
196
|
rrfFtsWeight: numW(overrides?.rrfFtsWeight, SCORING_DEFAULTS.rrfFtsWeight),
|
|
188
197
|
rrfVectorWeight: numW(overrides?.rrfVectorWeight, SCORING_DEFAULTS.rrfVectorWeight),
|
|
189
198
|
bothChannelBoost: num(overrides?.bothChannelBoost, SCORING_DEFAULTS.bothChannelBoost, 0, 1),
|
|
190
199
|
};
|
|
200
|
+
// #430: a head weaker than the slow weight it must join is nonsensical (the
|
|
201
|
+
// head curve would dive below the tail inside the window). Floor it at the
|
|
202
|
+
// resolved slow weight, defaulting to the shipped amplitude — the normal
|
|
203
|
+
// invalid case falls back to 0.30 rather than degrading the curve silently.
|
|
204
|
+
if (scoringWeights.recencyHead < scoringWeights.recency) {
|
|
205
|
+
scoringWeights.recencyHead = Math.max(SCORING_DEFAULTS.recencyHead, scoringWeights.recency);
|
|
206
|
+
}
|
|
191
207
|
return { ...scoringWeights };
|
|
192
208
|
}
|
|
193
209
|
/** Current resolved weights (tests + status output). */
|
|
@@ -486,7 +502,7 @@ function parseTimestamp(ts) {
|
|
|
486
502
|
// Scoring helpers
|
|
487
503
|
// ---------------------------------------------------------------------------
|
|
488
504
|
/**
|
|
489
|
-
* Compute decayed strength with adaptive decay (B+
|
|
505
|
+
* Compute decayed strength with adaptive decay (B+D model).
|
|
490
506
|
* Exported for use by consolidation decay/prune stage.
|
|
491
507
|
*
|
|
492
508
|
* #425 read-side law: the decay-relevant importance is CLAMPED at the
|
|
@@ -494,25 +510,73 @@ function parseTimestamp(ts) {
|
|
|
494
510
|
* exactly 1.0 the decay rate is exactly 1.0 and the row never decays, so
|
|
495
511
|
* legacy base-1.0 rows (and any write site that predates the cap) decay
|
|
496
512
|
* again. The clamp applies to explicit importance passes too.
|
|
513
|
+
*
|
|
514
|
+
* #448: the access/connectivity HARDENING terms are REMOVED — the fade rate
|
|
515
|
+
* no longer depends on access or link history (a briefly-used memory was
|
|
516
|
+
* near-immortal: 10 accesses cut the decay rate to 3% of normal). Real use
|
|
517
|
+
* now raises the STORED score via the nightly promotion stage
|
|
518
|
+
* (consolidate.ts stagePromotion) instead of slowing future decay, so a
|
|
519
|
+
* promoted-then-abandoned memory fades on the same 365-day clock as any
|
|
520
|
+
* other, from a higher anchor.
|
|
497
521
|
*/
|
|
498
522
|
function effectiveStrength(baseStrength, lastAccessed, now, options) {
|
|
499
523
|
const rawImportance = options?.importance ?? baseStrength;
|
|
500
524
|
const importance = Math.min(rawImportance, CALIBRATION.IMPORTANCE_CEILING);
|
|
501
|
-
const accessCount = options?.accessCount ?? 0;
|
|
502
|
-
const linkCount = options?.linkCount ?? 0;
|
|
503
525
|
const hours = Math.max((now.getTime() - parseTimestamp(lastAccessed).getTime()) / 3_600_000, 0);
|
|
504
526
|
// B: Importance slows decay
|
|
505
|
-
|
|
506
|
-
// E: Access hardening
|
|
507
|
-
const hardening = 0.7;
|
|
508
|
-
decayRate = 1.0 - (1.0 - decayRate) * Math.pow(hardening, accessCount);
|
|
509
|
-
// E: Connectivity hardening
|
|
510
|
-
decayRate = 1.0 - (1.0 - decayRate) * Math.pow(hardening, linkCount);
|
|
527
|
+
const decayRate = 1.0 - BASE_DECAY * (1.0 - importance);
|
|
511
528
|
// D: Asymptotic floor
|
|
512
529
|
const floor = baseStrength * importance * 0.1;
|
|
513
530
|
return floor + (baseStrength - floor) * Math.pow(decayRate, hours);
|
|
514
531
|
}
|
|
515
|
-
|
|
532
|
+
/**
|
|
533
|
+
* #430 merged time curve — ONE additive score term, two timescales. The
|
|
534
|
+
* pre-#430 pair (slow recency blend + linear fresh-memory bonus) is now a
|
|
535
|
+
* single piecewise exponential: a steep head with amplitude `recencyHead` at
|
|
536
|
+
* age 0, joined value-continuously onto the unchanged slow curve at
|
|
537
|
+
* `recencyHeadDays`. The head rate is DERIVED from value-continuity at the
|
|
538
|
+
* join (never a free constant), so the join cannot be mis-tuned and the eval
|
|
539
|
+
* seam retunes it automatically.
|
|
540
|
+
*
|
|
541
|
+
* A memory is born highly available and settles into the normal ranking over
|
|
542
|
+
* the head window. Age is measured from created_at, which the nightly sets
|
|
543
|
+
* from the session's own date — so a session captured last night ranks as
|
|
544
|
+
* ~1 day old (not 0), and backfilled older content correctly gets no head.
|
|
545
|
+
* The slow tail alone (≈58-day half-life at weight 0.15) could never lift a
|
|
546
|
+
* day-old memory past an old high-strength one — measured case: an
|
|
547
|
+
* exact-match 1-day-old memory (strength 0.50) lost to an unrelated memory
|
|
548
|
+
* at strength 0.80. That job is now the head's, so ranking among memories
|
|
549
|
+
* that are all old (at or beyond the join) is untouched.
|
|
550
|
+
*/
|
|
551
|
+
function timeScore(hoursSinceCreated, hasCreatedAt) {
|
|
552
|
+
const w = scoringWeights;
|
|
553
|
+
// Zero slow weight disables the WHOLE term (the term-isolation semantics
|
|
554
|
+
// the tests rely on). Also the NaN guard: with w.recency = 0 the derived
|
|
555
|
+
// head rate is ln(0)/join = −Inf and exp(−Inf · 0) = NaN at age 0.
|
|
556
|
+
if (w.recency === 0)
|
|
557
|
+
return 0;
|
|
558
|
+
// Absent created_at keeps the pre-#430 behavior exactly: the slow term at
|
|
559
|
+
// its age-0 value, no head. The guard is TRUTHINESS (the pre-#430 freshness
|
|
560
|
+
// guard was `memory.created_at && …`), so an empty string counts as absent
|
|
561
|
+
// too — and parseTimestamp("") falls back to now just like null (hours 0 →
|
|
562
|
+
// pow(·, 0) = 1 → exactly the slow weight).
|
|
563
|
+
if (!hasCreatedAt)
|
|
564
|
+
return w.recency;
|
|
565
|
+
const joinHours = w.recencyHeadDays * 24;
|
|
566
|
+
// The boundary belongs to the slow branch — at and beyond the join the
|
|
567
|
+
// expression is bit-identical to the pre-#430 slow term (IEEE754
|
|
568
|
+
// multiplication is commutative, so the operand order vs the old
|
|
569
|
+
// `recency * w.recency` is float-identical).
|
|
570
|
+
if (joinHours <= 0 || hoursSinceCreated >= joinHours) {
|
|
571
|
+
return w.recency * Math.pow(CALIBRATION.RECENCY_HOURLY_DECAY, hoursSinceCreated);
|
|
572
|
+
}
|
|
573
|
+
// Head branch (created_at present, 0 ≤ hours < joinHours): amplitude
|
|
574
|
+
// w.recencyHead decaying at the continuity-derived rate (≈ −0.004626/h —
|
|
575
|
+
// head half-life ≈ 6.24 d at the shipped constants).
|
|
576
|
+
const headRate = Math.log((w.recency * Math.pow(CALIBRATION.RECENCY_HOURLY_DECAY, joinHours)) / w.recencyHead) / joinHours;
|
|
577
|
+
return w.recencyHead * Math.exp(headRate * hoursSinceCreated);
|
|
578
|
+
}
|
|
579
|
+
function computeScore(memory, distance, connectionCount, now, options) {
|
|
516
580
|
// TRUE cosine similarity (#145). The old `1 − distance` compressed real
|
|
517
581
|
// cosines (cos 0.8 scored 0.37) and the 0-clamp at that scale flattened
|
|
518
582
|
// everything below cos 0.5 to exactly 0, killing mid-relevance
|
|
@@ -529,67 +593,66 @@ function computeScore(memory, distance, connectionCount, maxConnections, now, op
|
|
|
529
593
|
// time — now stated at the call site so the triple-win coupling
|
|
530
594
|
// (score share, decay rate, floor) is visible and single-sourced).
|
|
531
595
|
importance: memory.base_strength ?? 0.5,
|
|
532
|
-
accessCount: memory.access_count ?? 0,
|
|
533
|
-
linkCount: connectionCount,
|
|
534
596
|
});
|
|
535
|
-
const connScore = maxConnections > 0 ? connectionCount / maxConnections : 0;
|
|
536
597
|
const hoursSinceCreated = Math.max((now.getTime() - parseTimestamp(memory.created_at).getTime()) / 3_600_000, 0);
|
|
537
|
-
const recency = Math.pow(0.9995, hoursSinceCreated);
|
|
538
598
|
const w = scoringWeights;
|
|
599
|
+
// #449 (PR E, items 1+3): LOG-SATURATING connection credit on the
|
|
600
|
+
// ABSOLUTE degree scale — min(1, log1p(k)/log1p(K)), K = the resolved
|
|
601
|
+
// connectionsSaturation (default 16, the p99 of the real undirected
|
|
602
|
+
// degree distribution; calibration.ts carries the provenance). Shape per
|
|
603
|
+
// the #449 research base: ACT-R fan saturation (Anderson & Reder 1999 —
|
|
604
|
+
// activation falls with the LOG of fan, not linearly), SAM's saturating
|
|
605
|
+
// returns, cue overload (Watkins & Watkins 1975) — returns per
|
|
606
|
+
// additional link compress, and the top ~1% of hubs tie at full credit.
|
|
607
|
+
// This REPLACED the pre-#449 candidate-set normalization
|
|
608
|
+
// (connectionCount / maxConnections), which is deleted from this
|
|
609
|
+
// signature and every call site: a row's score no longer depends on
|
|
610
|
+
// which OTHER rows happened to match (set-relative scores were not
|
|
611
|
+
// reproducible, and the ~40-degree global hubs sat in nearly every
|
|
612
|
+
// 2-hop neighborhood — per-query max p50 = 36 — so the median linked
|
|
613
|
+
// candidate earned only k/36 of the term). k = 0 contributes exactly +0
|
|
614
|
+
// (log1p(0) = 0): unlinked rows score bit-identically to pre-#449.
|
|
615
|
+
const connScore = Math.min(1, Math.log1p(connectionCount) / Math.log1p(w.connectionsSaturation));
|
|
539
616
|
let score = similarity * w.similarity +
|
|
540
617
|
effStrength * w.strength +
|
|
541
618
|
connScore * w.connections +
|
|
542
|
-
|
|
543
|
-
//
|
|
544
|
-
//
|
|
545
|
-
//
|
|
546
|
-
//
|
|
547
|
-
//
|
|
548
|
-
//
|
|
549
|
-
//
|
|
550
|
-
//
|
|
551
|
-
//
|
|
552
|
-
//
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
const freshness = 1 - ageDays / scoringWeights.freshnessBoostDays;
|
|
556
|
-
score += freshness * scoringWeights.freshnessBoostWeight;
|
|
557
|
-
}
|
|
558
|
-
// #203 soft affinity (retrieval scoping). Two graded, additive terms — both
|
|
559
|
-
// ZERO when the scope is absent (byte-identical ranking) and ZERO for a
|
|
560
|
-
// non-matching memory (never a penalty). Project affinity is a flat boost on
|
|
561
|
-
// exact project match; domain affinity is max(overlapping tag weight) × the
|
|
562
|
-
// domain weight. NULL tag weights (not yet computed by the nightly
|
|
563
|
-
// reconsolidation) count as 0 — we never invent a boost from missing
|
|
564
|
-
// association strength. Affinity rides the 0.8 composite side only (the RRF
|
|
565
|
-
// 0.2 side is #205 territory and untouched here).
|
|
619
|
+
timeScore(hoursSinceCreated, Boolean(memory.created_at));
|
|
620
|
+
// #430 scope affinity (retrieval scoping; born from #203's two boosts).
|
|
621
|
+
// ONE graded, additive term — max() of the strongest single scope signal:
|
|
622
|
+
// the project-match indicator (exact project match ⇒ 1) and the max
|
|
623
|
+
// overlapping domain-tag weight. ZERO when the scope is absent
|
|
624
|
+
// (byte-identical ranking) and ZERO for a non-matching memory (never a
|
|
625
|
+
// penalty). The signals are never stacked — they are largely the same
|
|
626
|
+
// evidence and the domain one is the weaker, so the strongest counts once
|
|
627
|
+
// (single-signal scopes reproduce #203's exact floats). NULL tag weights
|
|
628
|
+
// (not yet computed by the nightly reconsolidation) count as 0 — we never
|
|
629
|
+
// invent a boost from missing association strength. Affinity rides the 0.8
|
|
630
|
+
// composite side only (the RRF 0.2 side is #205 territory and untouched
|
|
631
|
+
// here).
|
|
566
632
|
const scope = options?.scope;
|
|
567
633
|
if (scope) {
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
}
|
|
634
|
+
const projectMatch = scope.project && memory.project === scope.project;
|
|
635
|
+
let maxOverlapTagWeight = 0;
|
|
571
636
|
const domains = scope.missionDomains;
|
|
572
637
|
if (domains && domains.length > 0 && options.tagWeights && options.tagWeights.length > 0) {
|
|
573
638
|
const domainSet = domains.length === 1 ? null : new Set(domains);
|
|
574
|
-
let maxWeight = 0;
|
|
575
639
|
for (const tw of options.tagWeights) {
|
|
576
640
|
const overlaps = domainSet ? domainSet.has(tw.tag) : tw.tag === domains[0];
|
|
577
641
|
if (overlaps) {
|
|
578
642
|
const w = tw.weight ?? 0;
|
|
579
|
-
if (w >
|
|
580
|
-
|
|
643
|
+
if (w > maxOverlapTagWeight)
|
|
644
|
+
maxOverlapTagWeight = w;
|
|
581
645
|
}
|
|
582
646
|
}
|
|
583
|
-
if (maxWeight > 0)
|
|
584
|
-
score += maxWeight * scoringWeights.domainAffinity;
|
|
585
647
|
}
|
|
648
|
+
score += Math.max(projectMatch ? 1 : 0, maxOverlapTagWeight) * scoringWeights.scopeAffinity;
|
|
586
649
|
}
|
|
587
650
|
// #425 both-channel boost: vector KNN and BM25 FTS AGREEING on a candidate
|
|
588
651
|
// is the genuine-match signature (a distinctive proper noun the user knows
|
|
589
652
|
// exists — the field failure this fixes: 0.90/0.95-strength domain-adjacent
|
|
590
653
|
// memories outranked the best-similarity exact-token match). ADDITIVE,
|
|
591
654
|
// zero-boost neutral, never a penalty; rides the composite side only (like
|
|
592
|
-
//
|
|
655
|
+
// scopeAffinity — the RRF side is #205 territory); applied BEFORE the
|
|
593
656
|
// superseded multiplier so a superseded both-channel row still demotes.
|
|
594
657
|
if (options?.bothChannel)
|
|
595
658
|
score += scoringWeights.bothChannelBoost;
|
|
@@ -605,6 +668,18 @@ function computeScore(memory, distance, connectionCount, maxConnections, now, op
|
|
|
605
668
|
// ---------------------------------------------------------------------------
|
|
606
669
|
// Graph traversal
|
|
607
670
|
// ---------------------------------------------------------------------------
|
|
671
|
+
/**
|
|
672
|
+
* #466 — truth-management relationships that never pay connection credit.
|
|
673
|
+
* A superseded/corrected memory remains part of its knowledge neighborhood
|
|
674
|
+
* as EVIDENCE, but the administrative edge itself is not a semantic
|
|
675
|
+
* connection: "this memory was replaced" must not add ranking credit to the
|
|
676
|
+
* memory it replaced. Scoped to exactly the #463 audit's administrative set
|
|
677
|
+
* (`superseded_by`, `corrected_by`); every semantic edge — `extends`,
|
|
678
|
+
* `relates_to`, and the legacy vocabulary — still counts. Graph traversal
|
|
679
|
+
* (frontier expansion below, the belief walk, /graph queries) is untouched:
|
|
680
|
+
* those are discovery mechanisms, and only the credit stops.
|
|
681
|
+
*/
|
|
682
|
+
const ADMIN_RELATIONSHIPS = new Set(["superseded_by", "corrected_by"]);
|
|
608
683
|
function collectLinks(db, seedIds, maxHops = 2) {
|
|
609
684
|
const visited = new Set(seedIds);
|
|
610
685
|
const connectionCounts = new Map();
|
|
@@ -613,7 +688,9 @@ function collectLinks(db, seedIds, maxHops = 2) {
|
|
|
613
688
|
const nextFrontier = new Set();
|
|
614
689
|
for (const mid of frontier) {
|
|
615
690
|
const links = storage.getLinks(db, mid, "both");
|
|
616
|
-
|
|
691
|
+
// #466: the degree feeding computeScore's connections term counts
|
|
692
|
+
// semantic edges only — administrative edges do not pay credit.
|
|
693
|
+
const count = links.filter((l) => !ADMIN_RELATIONSHIPS.has(l.relationship)).length;
|
|
617
694
|
connectionCounts.set(mid, (connectionCounts.get(mid) ?? 0) + count);
|
|
618
695
|
for (const link of links) {
|
|
619
696
|
const linkedId = link.source_id === mid ? link.target_id : link.source_id;
|
|
@@ -631,7 +708,7 @@ function collectLinks(db, seedIds, maxHops = 2) {
|
|
|
631
708
|
for (const mid of visited) {
|
|
632
709
|
if (!connectionCounts.has(mid)) {
|
|
633
710
|
const links = storage.getLinks(db, mid, "both");
|
|
634
|
-
connectionCounts.set(mid, links.length);
|
|
711
|
+
connectionCounts.set(mid, links.filter((l) => !ADMIN_RELATIONSHIPS.has(l.relationship)).length);
|
|
635
712
|
}
|
|
636
713
|
}
|
|
637
714
|
return connectionCounts;
|
|
@@ -718,7 +795,7 @@ async function retrieve(db, embedFn, query, options) {
|
|
|
718
795
|
const project = options?.project;
|
|
719
796
|
const sourceAgent = options?.sourceAgent;
|
|
720
797
|
const missionDomains = options?.missionDomains;
|
|
721
|
-
const now = new Date();
|
|
798
|
+
const now = options?.now ?? new Date();
|
|
722
799
|
// #203 affinity scope — passed to computeScore for every candidate. Built
|
|
723
800
|
// once; absent fields yield no boost (zero-boost neutral).
|
|
724
801
|
const scope = project || (missionDomains && missionDomains.length > 0)
|
|
@@ -804,10 +881,9 @@ async function retrieve(db, embedFn, query, options) {
|
|
|
804
881
|
continue;
|
|
805
882
|
candidateMap.set(gid, { mem, distance: DEFAULT_GRAPH_DISTANCE, source: "graph" });
|
|
806
883
|
}
|
|
807
|
-
// 5. Compute composite scores
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
: [0]));
|
|
884
|
+
// 5. Compute composite scores (the connections term reads each row's own
|
|
885
|
+
// degree only — #449 deleted the candidate-set-max normalization: a
|
|
886
|
+
// row's score no longer depends on which other rows matched).
|
|
811
887
|
const scored = [];
|
|
812
888
|
const maxRrf = Math.max(...([...rrfScores.values()].length > 0 ? [...rrfScores.values()] : [1]));
|
|
813
889
|
// One query for the whole candidate set (#191 Phase B): superseded memories
|
|
@@ -824,7 +900,7 @@ async function retrieve(db, embedFn, query, options) {
|
|
|
824
900
|
: undefined;
|
|
825
901
|
for (const [mid, { mem, distance, source }] of candidateMap) {
|
|
826
902
|
const connCount = connectionCounts.get(mid) ?? 0;
|
|
827
|
-
const composite = computeScore(mem, distance, connCount,
|
|
903
|
+
const composite = computeScore(mem, distance, connCount, now, {
|
|
828
904
|
superseded: supersededIds.has(mid),
|
|
829
905
|
scope,
|
|
830
906
|
tagWeights: tagWeightsByMemory?.get(mid),
|
|
@@ -832,10 +908,7 @@ async function retrieve(db, embedFn, query, options) {
|
|
|
832
908
|
// signature — graph-only and single-channel candidates add nothing.
|
|
833
909
|
bothChannel: source === "both",
|
|
834
910
|
});
|
|
835
|
-
const effStr = effectiveStrength(mem.base_strength ?? 0.5, mem.last_accessed, now
|
|
836
|
-
accessCount: mem.access_count ?? 0,
|
|
837
|
-
linkCount: connectionCounts.get(mem.id) ?? 0,
|
|
838
|
-
});
|
|
911
|
+
const effStr = effectiveStrength(mem.base_strength ?? 0.5, mem.last_accessed, now);
|
|
839
912
|
const rrf = rrfScores.get(mid) ?? 0;
|
|
840
913
|
const normalizedRrf = maxRrf > 0 ? rrf / maxRrf : 0;
|
|
841
914
|
// #205: composite/RRF blend is now config-driven (rrfCompositeWeight, 0.8
|
|
@@ -851,7 +924,8 @@ async function retrieve(db, embedFn, query, options) {
|
|
|
851
924
|
scored.push({ mem, finalScore, effStr, connCount, similarity, source });
|
|
852
925
|
}
|
|
853
926
|
// 6. Sort and take top N — with cold-exposure slots (#192).
|
|
854
|
-
//
|
|
927
|
+
// Effective strength + the promotion stage (#448) make past winners
|
|
928
|
+
// self-reinforcing:
|
|
855
929
|
// 88% of the production corpus had never been returned by any query. Reserve
|
|
856
930
|
// up to 2 of k for the best-scoring never-accessed candidates so the long
|
|
857
931
|
// tail gets nonzero exposure whenever it is semantically in range. Slots are
|
|
@@ -897,12 +971,9 @@ async function retrieve(db, embedFn, query, options) {
|
|
|
897
971
|
if (sourceAgent && mem.source_agent !== sourceAgent)
|
|
898
972
|
return null;
|
|
899
973
|
const connCount = storage.getLinks(db, terminalId, "both").length;
|
|
900
|
-
const composite = computeScore(mem, DEFAULT_GRAPH_DISTANCE, connCount,
|
|
974
|
+
const composite = computeScore(mem, DEFAULT_GRAPH_DISTANCE, connCount, now, { superseded: supersededIds.has(terminalId) });
|
|
901
975
|
const finalScore = composite * scoringWeights.rrfCompositeWeight;
|
|
902
|
-
const effStr = effectiveStrength(mem.base_strength ?? 0.5, mem.last_accessed, now
|
|
903
|
-
accessCount: mem.access_count ?? 0,
|
|
904
|
-
linkCount: connCount,
|
|
905
|
-
});
|
|
976
|
+
const effStr = effectiveStrength(mem.base_strength ?? 0.5, mem.last_accessed, now);
|
|
906
977
|
return { mem, finalScore, effStr, connCount, similarity: null, source: "graph" };
|
|
907
978
|
});
|
|
908
979
|
const results = top.map((t) => formatResult(t.mem, t.finalScore, t.effStr, t.connCount, {
|
|
@@ -936,22 +1007,16 @@ function searchRecent(db, options) {
|
|
|
936
1007
|
return [];
|
|
937
1008
|
const allIds = candidates.map((c) => c.id);
|
|
938
1009
|
const connectionCounts = collectLinks(db, allIds, 1);
|
|
939
|
-
const maxConnections = Math.max(...([...connectionCounts.values()].length > 0
|
|
940
|
-
? [...connectionCounts.values()]
|
|
941
|
-
: [0]));
|
|
942
1010
|
const scored = [];
|
|
943
1011
|
// #384: same full demotion set as retrieve() — link-driven supersessions
|
|
944
1012
|
// UNION status-marked superseded/retracted rows (findDemotedIds).
|
|
945
1013
|
const supersededRecent = findDemotedIds(db, candidates.map((c) => c.id));
|
|
946
1014
|
for (const mem of candidates) {
|
|
947
1015
|
const connCount = connectionCounts.get(mem.id) ?? 0;
|
|
948
|
-
const score = computeScore(mem, DEFAULT_GRAPH_DISTANCE, connCount,
|
|
1016
|
+
const score = computeScore(mem, DEFAULT_GRAPH_DISTANCE, connCount, now, {
|
|
949
1017
|
superseded: supersededRecent.has(mem.id),
|
|
950
1018
|
});
|
|
951
|
-
const effStr = effectiveStrength(mem.base_strength ?? 0.5, mem.last_accessed, now
|
|
952
|
-
accessCount: mem.access_count ?? 0,
|
|
953
|
-
linkCount: connectionCounts.get(mem.id) ?? 0,
|
|
954
|
-
});
|
|
1019
|
+
const effStr = effectiveStrength(mem.base_strength ?? 0.5, mem.last_accessed, now);
|
|
955
1020
|
scored.push({ mem, score, effStr, connCount });
|
|
956
1021
|
}
|
|
957
1022
|
scored.sort((a, b) => b.score - a.score);
|
|
@@ -976,11 +1041,8 @@ function searchRecent(db, options) {
|
|
|
976
1041
|
if (project && mem.project !== project)
|
|
977
1042
|
return null;
|
|
978
1043
|
const connCount = storage.getLinks(db, terminalId, "both").length;
|
|
979
|
-
const score = computeScore(mem, DEFAULT_GRAPH_DISTANCE, connCount,
|
|
980
|
-
const effStr = effectiveStrength(mem.base_strength ?? 0.5, mem.last_accessed, now
|
|
981
|
-
accessCount: mem.access_count ?? 0,
|
|
982
|
-
linkCount: connCount,
|
|
983
|
-
});
|
|
1044
|
+
const score = computeScore(mem, DEFAULT_GRAPH_DISTANCE, connCount, now, { superseded: supersededRecent.has(terminalId) });
|
|
1045
|
+
const effStr = effectiveStrength(mem.base_strength ?? 0.5, mem.last_accessed, now);
|
|
984
1046
|
return { mem, score, effStr, connCount };
|
|
985
1047
|
});
|
|
986
1048
|
const results = top.map((t) => formatResult(t.mem, t.score, t.effStr, t.connCount));
|
package/dist/status.js
CHANGED
|
@@ -66,6 +66,24 @@ async function runStatus() {
|
|
|
66
66
|
console.log(`Memories: ${stats.memories} (${typeStr || "none"})`);
|
|
67
67
|
console.log(`Links: ${stats.links}`);
|
|
68
68
|
console.log(`DB size: ${(stats.db_size_bytes / 1024).toFixed(1)} KB`);
|
|
69
|
+
// 0.21 migration detection (#425): pre-0.21 stores have inflated importance
|
|
70
|
+
// scores (median ~0.80 vs the honest ~0.40). If the live median is high,
|
|
71
|
+
// recommend the one-shot rescore.
|
|
72
|
+
if (stats.memories > 0) {
|
|
73
|
+
try {
|
|
74
|
+
const live = db.prepare("SELECT base_strength FROM memories WHERE base_strength IS NOT NULL AND (status IS NULL OR status NOT IN ('absorbed','superseded','retracted')) ORDER BY base_strength").all();
|
|
75
|
+
if (live.length >= 10) {
|
|
76
|
+
const median = live[Math.floor(live.length / 2)].base_strength;
|
|
77
|
+
if (median >= 0.6) {
|
|
78
|
+
console.log(``);
|
|
79
|
+
console.log(`⚠ Pre-0.21 importance scores detected (median ${median.toFixed(2)}).`);
|
|
80
|
+
console.log(` Run: npx @gamaze/hicortex rescore-importance --apply`);
|
|
81
|
+
console.log(` to re-score your store under the honest rubric.`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch { /* non-fatal */ }
|
|
86
|
+
}
|
|
69
87
|
db.close();
|
|
70
88
|
}
|
|
71
89
|
catch (err) {
|
package/dist/storage.d.ts
CHANGED
|
@@ -68,7 +68,8 @@ export declare function enrichMemory(db: Database.Database, memoryId: string, no
|
|
|
68
68
|
* Record that memories appeared in a pushed recall index (#192): bump
|
|
69
69
|
* shown_count and refresh last_accessed (a mild strengthen — the decay clock
|
|
70
70
|
* resets so topically-live memories stop sinking) WITHOUT touching
|
|
71
|
-
* access_count, which stays reserved for real use (
|
|
71
|
+
* access_count, which stays reserved for real use (the promotion signal
|
|
72
|
+
* #448 + the prune shield).
|
|
72
73
|
*/
|
|
73
74
|
export declare function touchMemoriesShown(db: Database.Database, memoryIds: string[], nowIsoStr: string): void;
|
|
74
75
|
/**
|
package/dist/storage.js
CHANGED
|
@@ -190,6 +190,10 @@ const ALLOWED_UPDATE_FIELDS = new Set([
|
|
|
190
190
|
// stageImportance, the enrich path, and the rescore-importance backfill —
|
|
191
191
|
// the moment a row's base_strength is settled under some rubric.
|
|
192
192
|
"importance_scored_at",
|
|
193
|
+
// Promotion baseline (#448, migration v20): written by the nightly
|
|
194
|
+
// promotion stage and dedup merges (which sum a cluster's counters onto
|
|
195
|
+
// the canonical — the baseline must move with the summed access_count).
|
|
196
|
+
"promotion_last_count",
|
|
193
197
|
]);
|
|
194
198
|
/**
|
|
195
199
|
* Update specific fields on a memory.
|
|
@@ -256,7 +260,8 @@ function enrichMemory(db, memoryId, nowIsoStr) {
|
|
|
256
260
|
* Record that memories appeared in a pushed recall index (#192): bump
|
|
257
261
|
* shown_count and refresh last_accessed (a mild strengthen — the decay clock
|
|
258
262
|
* resets so topically-live memories stop sinking) WITHOUT touching
|
|
259
|
-
* access_count, which stays reserved for real use (
|
|
263
|
+
* access_count, which stays reserved for real use (the promotion signal
|
|
264
|
+
* #448 + the prune shield).
|
|
260
265
|
*/
|
|
261
266
|
function touchMemoriesShown(db, memoryIds, nowIsoStr) {
|
|
262
267
|
if (memoryIds.length === 0)
|