@gamaze/hicortex 0.22.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.
@@ -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
- freshnessBoostDays: number;
60
- freshnessBoostWeight: number;
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
- /** #203 soft affinity boost on exact project match. */
63
- projectAffinity: number;
64
- /** #203 soft affinity boost multiplier on max overlapping domain-tag weight. */
65
- domainAffinity: number;
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). */
@@ -232,12 +244,13 @@ export declare function effectiveStrength(baseStrength: number, lastAccessed: st
232
244
  * Return a composite relevance score in [0, 1] for a candidate memory.
233
245
  * Exported for exact-value tests of the similarity component (#145).
234
246
  *
235
- * #203 soft affinity (options.scope + options.tagWeights): two additive,
236
- * graded, zero-boost-neutral terms — project affinity (exact project match)
237
- * and domain affinity (max overlapping memory_tags.weight × scope). Both are
238
- * 0 when the scope is absent (byte-identical to pre-#203) and NEVER negative
239
- * (a foreign memory adds 0, never a penalty — penalties re-introduce
240
- * soft-exclusion). See `AffinityScope`.
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`.
241
254
  */
242
255
  export interface AffinityScope {
243
256
  /** Exact-match project from the client (CC/OC cwd-derived; /search project). */
@@ -246,12 +259,13 @@ export interface AffinityScope {
246
259
  * vocabulary as memory_tags (config `domains`). */
247
260
  missionDomains?: string[];
248
261
  }
249
- export declare function computeScore(memory: Memory, distance: number, connectionCount: number, maxConnections: number, now: Date, options?: {
262
+ export declare function computeScore(memory: Memory, distance: number, connectionCount: number, now: Date, options?: {
250
263
  superseded?: boolean;
251
- /** #203: when present, project/domain affinity boosts are applied. */
264
+ /** #203/#430: when present, the scope-affinity boost is applied. */
252
265
  scope?: AffinityScope;
253
266
  /** Candidate's graded domain tags (memory_tags rows). Loaded batched for
254
- * the whole candidate set in retrieve(); used for domain affinity. */
267
+ * the whole candidate set in retrieve(); used for the scope-affinity
268
+ * term's domain signal. */
255
269
  tagWeights?: Array<{
256
270
  tag: string;
257
271
  weight: number | null;
@@ -308,6 +322,11 @@ export declare function retrieve(db: Database.Database, embedFn: EmbedFn, query:
308
322
  ftsCandidates?: (fetchLimit: number, sourceAgent?: string) => Array<Memory & {
309
323
  rank: number;
310
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;
311
330
  }): Promise<MemorySearchResult[]>;
312
331
  /**
313
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
- freshnessBoostDays: CALIBRATION.FRESHNESS_BOOST_DAYS,
136
- freshnessBoostWeight: CALIBRATION.FRESHNESS_BOOST_WEIGHT,
138
+ recencyHead: CALIBRATION.RECENCY_HEAD_WEIGHT,
139
+ recencyHeadDays: CALIBRATION.RECENCY_HEAD_DAYS,
137
140
  supersededDemotion: CALIBRATION.SUPERSEDED_DEMOTION,
138
- projectAffinity: CALIBRATION.PROJECT_AFFINITY_WEIGHT,
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
- freshnessBoostDays: num(overrides?.freshnessBoostDays, SCORING_DEFAULTS.freshnessBoostDays, 0, 365),
181
- freshnessBoostWeight: num(overrides?.freshnessBoostWeight, SCORING_DEFAULTS.freshnessBoostWeight, 0, 1),
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
- projectAffinity: num(overrides?.projectAffinity, SCORING_DEFAULTS.projectAffinity, 0, 1),
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). */
@@ -513,7 +529,54 @@ function effectiveStrength(baseStrength, lastAccessed, now, options) {
513
529
  const floor = baseStrength * importance * 0.1;
514
530
  return floor + (baseStrength - floor) * Math.pow(decayRate, hours);
515
531
  }
516
- function computeScore(memory, distance, connectionCount, maxConnections, now, options) {
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) {
517
580
  // TRUE cosine similarity (#145). The old `1 − distance` compressed real
518
581
  // cosines (cos 0.8 scored 0.37) and the 0-clamp at that scale flattened
519
582
  // everything below cos 0.5 to exactly 0, killing mid-relevance
@@ -531,65 +594,65 @@ function computeScore(memory, distance, connectionCount, maxConnections, now, op
531
594
  // (score share, decay rate, floor) is visible and single-sourced).
532
595
  importance: memory.base_strength ?? 0.5,
533
596
  });
534
- const connScore = maxConnections > 0 ? connectionCount / maxConnections : 0;
535
597
  const hoursSinceCreated = Math.max((now.getTime() - parseTimestamp(memory.created_at).getTime()) / 3_600_000, 0);
536
- const recency = Math.pow(0.9995, hoursSinceCreated);
537
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));
538
616
  let score = similarity * w.similarity +
539
617
  effStrength * w.strength +
540
618
  connScore * w.connections +
541
- recency * w.recency;
542
- // Fresh-memory window (#191 Phase B): a memory is born highly available and
543
- // settles into the normal ranking over `freshnessBoostDays`. Age is measured
544
- // from created_at, which the nightly sets from the session's own date — so a
545
- // session captured last night ranks as ~1 day old (not 0), and backfilled
546
- // older content correctly gets no boost. The slow
547
- // `recency` term above (≈58-day half-life at weight 0.15) could never lift a
548
- // day-old memory past an old high-strength one — measured case: an
549
- // exact-match 1-day-old memory (strength 0.50) lost to an unrelated memory
550
- // at strength 0.80. This is an ADDITIVE bonus that decays linearly to zero
551
- // at the window edge, so it cannot distort ranking among memories that are
552
- // all old.
553
- const ageDays = hoursSinceCreated / 24;
554
- if (memory.created_at && ageDays < scoringWeights.freshnessBoostDays) {
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
- if (scope.project && memory.project === scope.project) {
569
- score += scoringWeights.projectAffinity;
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 > maxWeight)
580
- maxWeight = w;
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
- // projectAffinity — the RRF side is #205 territory); applied BEFORE the
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
- const count = links.length;
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
- const maxConnections = Math.max(...([...connectionCounts.values()].length > 0
809
- ? [...connectionCounts.values()]
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, maxConnections, now, {
903
+ const composite = computeScore(mem, distance, connCount, now, {
828
904
  superseded: supersededIds.has(mid),
829
905
  scope,
830
906
  tagWeights: tagWeightsByMemory?.get(mid),
@@ -895,7 +971,7 @@ async function retrieve(db, embedFn, query, options) {
895
971
  if (sourceAgent && mem.source_agent !== sourceAgent)
896
972
  return null;
897
973
  const connCount = storage.getLinks(db, terminalId, "both").length;
898
- const composite = computeScore(mem, DEFAULT_GRAPH_DISTANCE, connCount, maxConnections, now, { superseded: supersededIds.has(terminalId) });
974
+ const composite = computeScore(mem, DEFAULT_GRAPH_DISTANCE, connCount, now, { superseded: supersededIds.has(terminalId) });
899
975
  const finalScore = composite * scoringWeights.rrfCompositeWeight;
900
976
  const effStr = effectiveStrength(mem.base_strength ?? 0.5, mem.last_accessed, now);
901
977
  return { mem, finalScore, effStr, connCount, similarity: null, source: "graph" };
@@ -931,16 +1007,13 @@ function searchRecent(db, options) {
931
1007
  return [];
932
1008
  const allIds = candidates.map((c) => c.id);
933
1009
  const connectionCounts = collectLinks(db, allIds, 1);
934
- const maxConnections = Math.max(...([...connectionCounts.values()].length > 0
935
- ? [...connectionCounts.values()]
936
- : [0]));
937
1010
  const scored = [];
938
1011
  // #384: same full demotion set as retrieve() — link-driven supersessions
939
1012
  // UNION status-marked superseded/retracted rows (findDemotedIds).
940
1013
  const supersededRecent = findDemotedIds(db, candidates.map((c) => c.id));
941
1014
  for (const mem of candidates) {
942
1015
  const connCount = connectionCounts.get(mem.id) ?? 0;
943
- const score = computeScore(mem, DEFAULT_GRAPH_DISTANCE, connCount, maxConnections, now, {
1016
+ const score = computeScore(mem, DEFAULT_GRAPH_DISTANCE, connCount, now, {
944
1017
  superseded: supersededRecent.has(mem.id),
945
1018
  });
946
1019
  const effStr = effectiveStrength(mem.base_strength ?? 0.5, mem.last_accessed, now);
@@ -968,7 +1041,7 @@ function searchRecent(db, options) {
968
1041
  if (project && mem.project !== project)
969
1042
  return null;
970
1043
  const connCount = storage.getLinks(db, terminalId, "both").length;
971
- const score = computeScore(mem, DEFAULT_GRAPH_DISTANCE, connCount, maxConnections, now, { superseded: supersededRecent.has(terminalId) });
1044
+ const score = computeScore(mem, DEFAULT_GRAPH_DISTANCE, connCount, now, { superseded: supersededRecent.has(terminalId) });
972
1045
  const effStr = effectiveStrength(mem.base_strength ?? 0.5, mem.last_accessed, now);
973
1046
  return { mem, score, effStr, connCount };
974
1047
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gamaze/hicortex",
3
- "version": "0.22.0",
3
+ "version": "0.22.1",
4
4
  "description": "Persistent agent identity for AI agents \u2014 a hand-edited identity layer, nightly-distilled experience, and lessons injected every session, shared across your whole fleet. Works with Hermes, OpenClaw, Claude Code, Pi, and opencode.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
package/server.json CHANGED
@@ -3,12 +3,12 @@
3
3
  "name": "io.github.gamaze-labs/hicortex",
4
4
  "title": "Hicortex \u2014 AI Fleet Memory",
5
5
  "description": "Shared fleet memory for AI agents: nightly self-correction, recall every prompt (supported agents).",
6
- "version": "0.22.0",
6
+ "version": "0.22.1",
7
7
  "packages": [
8
8
  {
9
9
  "registryType": "npm",
10
10
  "identifier": "@gamaze/hicortex",
11
- "version": "0.22.0",
11
+ "version": "0.22.1",
12
12
  "transport": {
13
13
  "type": "stdio",
14
14
  "command": "npx",