@gamaze/hicortex 0.20.7 → 0.20.10

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.
Files changed (86) hide show
  1. package/README.md +18 -41
  2. package/assets/dashboard.html +3989 -836
  3. package/dist/calibration.d.ts +293 -0
  4. package/dist/calibration.js +379 -0
  5. package/dist/capture-health.d.ts +87 -0
  6. package/dist/capture-health.js +106 -0
  7. package/dist/capture-pause.d.ts +86 -0
  8. package/dist/capture-pause.js +127 -0
  9. package/dist/capture.d.ts +24 -3
  10. package/dist/capture.js +11 -1
  11. package/dist/classify-domains.d.ts +6 -0
  12. package/dist/classify-domains.js +7 -1
  13. package/dist/cli.js +38 -3
  14. package/dist/config-read.d.ts +1 -1
  15. package/dist/config-read.js +96 -9
  16. package/dist/consolidate.d.ts +114 -68
  17. package/dist/consolidate.js +302 -182
  18. package/dist/dashboard.d.ts +326 -6
  19. package/dist/dashboard.js +592 -7
  20. package/dist/db.js +105 -0
  21. package/dist/dedup.d.ts +34 -26
  22. package/dist/dedup.js +91 -57
  23. package/dist/distiller.js +1 -1
  24. package/dist/domain-classify.d.ts +7 -6
  25. package/dist/domain-classify.js +12 -10
  26. package/dist/eval/decay-eval.d.ts +3 -3
  27. package/dist/eval/decay-eval.js +4 -4
  28. package/dist/eval/importance-eval.d.ts +85 -0
  29. package/dist/eval/importance-eval.js +286 -0
  30. package/dist/eval/planted-eval.d.ts +26 -0
  31. package/dist/eval/planted-eval.js +97 -0
  32. package/dist/eval/planted-fixtures.d.ts +107 -0
  33. package/dist/eval/planted-fixtures.js +283 -0
  34. package/dist/eval/planted-harness.d.ts +176 -0
  35. package/dist/eval/planted-harness.js +649 -0
  36. package/dist/eval/ranking-battery.d.ts +78 -0
  37. package/dist/eval/ranking-battery.js +181 -0
  38. package/dist/eval/ranking-eval.d.ts +41 -0
  39. package/dist/eval/ranking-eval.js +391 -0
  40. package/dist/eval/ranking-fixtures.d.ts +77 -0
  41. package/dist/eval/ranking-fixtures.js +226 -0
  42. package/dist/identity-store.d.ts +21 -0
  43. package/dist/identity-store.js +49 -0
  44. package/dist/index.js +4 -3
  45. package/dist/init.d.ts +23 -3
  46. package/dist/init.js +84 -9
  47. package/dist/llm.d.ts +43 -58
  48. package/dist/llm.js +87 -101
  49. package/dist/mcp-server.d.ts +12 -0
  50. package/dist/mcp-server.js +213 -32
  51. package/dist/nightly.d.ts +9 -1
  52. package/dist/nightly.js +164 -110
  53. package/dist/nofit.d.ts +4 -11
  54. package/dist/nofit.js +6 -23
  55. package/dist/prompts.d.ts +10 -0
  56. package/dist/prompts.js +28 -5
  57. package/dist/recall-index.d.ts +30 -28
  58. package/dist/recall-index.js +21 -18
  59. package/dist/recall-registry.d.ts +2 -1
  60. package/dist/recall-registry.js +35 -1
  61. package/dist/reconsolidation.d.ts +168 -87
  62. package/dist/reconsolidation.js +818 -377
  63. package/dist/relink.js +3 -4
  64. package/dist/rescore-importance.d.ts +80 -0
  65. package/dist/rescore-importance.js +236 -0
  66. package/dist/retrieval.d.ts +80 -35
  67. package/dist/retrieval.js +322 -105
  68. package/dist/run-deadline.d.ts +62 -0
  69. package/dist/run-deadline.js +73 -0
  70. package/dist/schema-prototypes.d.ts +3 -3
  71. package/dist/schema-prototypes.js +3 -3
  72. package/dist/stages.d.ts +37 -0
  73. package/dist/stages.js +51 -0
  74. package/dist/state.d.ts +34 -9
  75. package/dist/storage.d.ts +50 -18
  76. package/dist/storage.js +125 -30
  77. package/dist/telemetry.d.ts +8 -7
  78. package/dist/token-budget.js +3 -4
  79. package/dist/type-classify.js +4 -4
  80. package/dist/types.d.ts +143 -155
  81. package/domains.example.json +4 -5
  82. package/hermes-plugin/hicortex/README.md +2 -2
  83. package/openclaw.plugin.json +1 -1
  84. package/package.json +4 -1
  85. package/pi-extension/hicortex/README.md +1 -1
  86. package/server.json +3 -3
@@ -0,0 +1,379 @@
1
+ "use strict";
2
+ /**
3
+ * Release-managed calibration constants (#408) — the single home of every
4
+ * tuning value the product ships. One value, one definition, one provenance
5
+ * comment. Nothing in here is read from ~/.hicortex/config.json anymore: the
6
+ * ~35 tuning keys that 0.15–0.20 exposed as config are now CONSTANTS that
7
+ * move only in releases. The user config surface shrinks to the keys that
8
+ * describe an install (mode, model, schedules, identity, budgets), not the
9
+ * ones that tune the brain.
10
+ *
11
+ * EVOLUTION CONTRACT: these values change ONLY in releases, with the
12
+ * eval/band-stats evidence linked in the changelog line that moves them
13
+ * (eval harness = `npm run eval` + the resolution band stats in the nightly
14
+ * report). Never in a patch to quiet one corpus, never behind a new config
15
+ * key. The seams for EXPERIMENTS are the configure*() functions in
16
+ * retrieval.ts / storage.ts (and the stage Options fields) — the eval and
17
+ * the tests sweep values through them; production never passes anything, so
18
+ * every process scores with exactly these constants.
19
+ *
20
+ * Every value below equals the default the code shipped the day this module
21
+ * was introduced (verified by tests/calibration.test.ts) — an install that
22
+ * never set the old config keys sees byte-identical behavior. The old keys
23
+ * are warned as RELEASE-MANAGED at the config boundary (config-read.ts) so
24
+ * the removal is never silent.
25
+ */
26
+ Object.defineProperty(exports, "__esModule", { value: true });
27
+ exports.DIAGNOSTIC_ENV_TIER = exports.OLLAMA_FLUSH_WAIT_MS = exports.OLLAMA_FLUSH_EVERY = exports.NUM_CTX = exports.DEFAULT_FIRST_RUN_LOOKBACK_DAYS = exports.ENRICH_STRENGTH_DELTA = 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.DOMAIN_AFFINITY_WEIGHT = exports.PROJECT_AFFINITY_WEIGHT = exports.SUPERSEDED_DEMOTION = exports.FRESHNESS_BOOST_WEIGHT = exports.FRESHNESS_BOOST_DAYS = exports.SCORE_RECENCY_WEIGHT = 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.DECAY_HALF_LIFE_DAYS = void 0;
28
+ exports.resolveNumCtx = resolveNumCtx;
29
+ exports.resolveOllamaFlushEvery = resolveOllamaFlushEvery;
30
+ exports.resolveOllamaFlushWaitMs = resolveOllamaFlushWaitMs;
31
+ // ---------------------------------------------------------------------------
32
+ // Recall / decay family (was: decayHalfLifeDays, searchLimit, recentLimit,
33
+ // recentWindowDays, coldExposureSlots, sessionIntentWeight, recall*,
34
+ // noveltyFloorSlots, recallReshowTurns)
35
+ // ---------------------------------------------------------------------------
36
+ /**
37
+ * Memory-decay half-life (days) at the reference importance 0.5. #192 recall/
38
+ * decay alignment: was ~115 days — aggressive enough to bury the long tail in
39
+ * ranking. Long-term remembering is the product; time preference stays mild.
40
+ */
41
+ exports.DECAY_HALF_LIFE_DAYS = 365;
42
+ /** Default k for retrieve() (/search without an explicit limit). #192. */
43
+ exports.SEARCH_LIMIT = 8;
44
+ /** Default k for searchRecent() (/recent without an explicit limit). #192. */
45
+ exports.RECENT_LIMIT = 12;
46
+ /** searchRecent() candidate window, days. #192. */
47
+ exports.RECENT_WINDOW_DAYS = 180;
48
+ /** Top-k slots reservable for never-accessed memories (cold exposure). #192:
49
+ * recall was too passive (88% of memories never accessed) — the long tail
50
+ * gets guaranteed slots instead of waiting for the strength clock. */
51
+ exports.COLD_EXPOSURE_SLOTS = 2;
52
+ /** Blend weight of the session-intent centroid in the recall search vector
53
+ * (#192, 0.15.3): query = (1-w)·prompt + w·centroid. The kill-switch is the
54
+ * configureSessionIntent(0) seam (eval-only); production always ships 0.33. */
55
+ exports.SESSION_INTENT_WEIGHT = 0.33;
56
+ /** Relevance-gate floor for vector-only /recall-index candidates. 0.62
57
+ * (raised from 0.55 on 2026-08-03 per a 0.01-step floor sweep on the
58
+ * rewritten corpus): steady ~3:1 noise:signal removal with no knee; sits
59
+ * below the 0.63 local pessimum. FTS-matched candidates pass regardless. */
60
+ exports.RECALL_MIN_SIMILARITY = 0.62;
61
+ /** Max lines in the pushed recall index. 5 (lowered from 6 on 2026-08-03):
62
+ * per-slot decomposition at floor 0.62 showed slot 6 gives NO prompt its
63
+ * first relevant memory. The K-sweep is monotone toward 4, but the 4-vs-5
64
+ * distinction rests on 5 of 98 prompts — 5 hedges with coverage. */
65
+ exports.RECALL_MAX_ITEMS = 5;
66
+ /** Prompts shorter than this skip the recall index (continuations, "yes"). */
67
+ exports.RECALL_MIN_PROMPT_CHARS = 20;
68
+ /** Chars of a memory's first line shown in an index entry. 100 (reverted
69
+ * from 150 on 2026-08-03): the full-corpus relevance eval found 100 vs 150
70
+ * statistically identical (full CI overlap at N=40); 100 saves ~13% tokens. */
71
+ exports.RECALL_TITLE_CHARS = 100;
72
+ /** Slots of RECALL_MAX_ITEMS guaranteed to the pure-prompt (unblended)
73
+ * search's top passing hit(s) — the #324 novelty floor. 2 mirrors
74
+ * COLD_EXPOSURE_SLOTS sizing: a floor, never a takeover. */
75
+ exports.NOVELTY_FLOOR_SLOTS = 2;
76
+ /** Turns an already-shown memory stays suppressed in the same session before
77
+ * it may reappear in the pushed index (#192 turn-based dedup). */
78
+ exports.RECALL_RESHOW_TURNS = 30;
79
+ // Recall-uses band (console) — PROVISIONAL zone edges. Owner anchor
80
+ // 2026-09-13 (#426 final ruling): the console renders uses-per-showing on a
81
+ // red→green→red band with three zones (Low / Normal / Overfetching; owner
82
+ // confirmed "3 bands is fine"). The owner's calibration state is explicit —
83
+ // "we have no clue on how to calibrate yet" — so these edges are provisional
84
+ // anchors from #426's owner-anchor record, NOT measured boundaries; fleet
85
+ // telemetry is expected to move them (the console marks the band
86
+ // "provisional" and renders no edge numerics). They position zone boundaries
87
+ // on the console's gradient and classify the marker's zone word; they gate
88
+ // nothing server-side.
89
+ /** uses_per_showing below this reads Low on the console band.
90
+ * PROVISIONAL (owner anchor 2026-09-13, #426). */
91
+ exports.RECALL_USES_LOW_MAX = 0.05;
92
+ /** uses_per_showing below this (and ≥ RECALL_USES_LOW_MAX) reads Normal.
93
+ * PROVISIONAL (owner anchor 2026-09-13, #426). */
94
+ exports.RECALL_USES_NORMAL_MAX = 0.25;
95
+ /** Display-axis maximum for the console band (the marker clamps here).
96
+ * PROVISIONAL (owner anchor 2026-09-13, #426) — chosen so Overfetching
97
+ * keeps a visible span, not a measured bound. */
98
+ exports.RECALL_USES_AXIS_MAX = 0.30;
99
+ // ---------------------------------------------------------------------------
100
+ // Composite ranking weights (was: score*Weight, freshnessBoost*,
101
+ // supersededDemotion, *AffinityWeight, rrf*)
102
+ // ---------------------------------------------------------------------------
103
+ /** Semantic-similarity share of the composite score. 0.50 (raised from 0.40
104
+ * in the 0.15.2 rebalance, #191 Phase B): on the production corpus effective
105
+ * strength (0.30) outweighed what similarity could recover — hardened old
106
+ * memories beat exact matches for their own topic. Similarity now leads;
107
+ * strength breaks ties and rewards real use. */
108
+ exports.SCORE_SIMILARITY_WEIGHT = 0.50;
109
+ /** Effective-strength share of the composite score (was 0.30; see above). */
110
+ exports.SCORE_STRENGTH_WEIGHT = 0.20;
111
+ /** Graph-centrality share of the composite score (was 0.20; see above). */
112
+ exports.SCORE_CONNECTIONS_WEIGHT = 0.15;
113
+ /** Slow recency curve share of the composite score (was 0.10; see above). */
114
+ exports.SCORE_RECENCY_WEIGHT = 0.15;
115
+ /** Fresh-memory window: the additive bonus fades linearly to 0 over this
116
+ * many days. 7 — nightly capture means 1 day is the floor of "fresh"
117
+ * (#191 Phase B). */
118
+ exports.FRESHNESS_BOOST_DAYS = 7;
119
+ /** Fresh-memory bonus size at age 0 (#191 Phase B; 0 = disabled via seam). */
120
+ exports.FRESHNESS_BOOST_WEIGHT = 0.15;
121
+ /** Score multiplier for a memory a later decision superseded (0.15.2; the
122
+ * belief walk (#393 D) is the primary mechanism — this is the safety net
123
+ * for rows the walk does not reach). */
124
+ exports.SUPERSEDED_DEMOTION = 0.50;
125
+ /** #203 soft boost on exact project match. ADDITIVE, zero-boost neutral,
126
+ * never a penalty — a foreign memory ranks equal, not lower. */
127
+ exports.PROJECT_AFFINITY_WEIGHT = 0.15;
128
+ /** #203 soft boost multiplier on max overlapping domain-tag weight. */
129
+ exports.DOMAIN_AFFINITY_WEIGHT = 0.15;
130
+ /** #205 RRF k parameter (1/(k+rank+1)) — matches the pre-#205 hardcoded 60
131
+ * so the no-config path was byte-identical to 0.15.3. */
132
+ exports.RRF_K = 60;
133
+ /** #205 composite-score share of the final blend (RRF gets the remainder);
134
+ * pre-#205 hardcoded value carried forward. */
135
+ exports.RRF_COMPOSITE_WEIGHT = 0.8;
136
+ /** #205 per-list RRF weight for the FTS list. 0.5 is the bisection point
137
+ * where BM25F + composite-affinity flip the token-exact marine body match
138
+ * below the same-scope hardware field (Q4 contamination 0.20 → 0.00) while
139
+ * pure-keyword queries keep recall@5 = 1.0. 0.7 was measured too timid. */
140
+ exports.RRF_FTS_WEIGHT = 0.5;
141
+ /** #205 per-list RRF weight for the vector list (vec stays at 1.0 — the
142
+ * conservative nudge is on the FTS side only). */
143
+ exports.RRF_VECTOR_WEIGHT = 1.0;
144
+ /**
145
+ * Additive boost for candidates the TWO retrieval channels AGREE on
146
+ * (vector KNN AND BM25 FTS both matched — computeScore option
147
+ * `bothChannel`). #425 (owner decision D3, 2026-09-13): the two-channel
148
+ * signal is the genuine-match signature and is immune to FTS-only token
149
+ * collisions, which is why the boost lives in the composite score, not in
150
+ * the RRF FTS weight (raising that re-opens the #205 cross-scope collision
151
+ * it fixed). D3's dominance property: a both-channel match outranks a
152
+ * single-channel rival unless the rival is >0.10 more similar — at the
153
+ * shipped simWeight 0.50, 0.10 gives that property 2x headroom (a rival
154
+ * needs >0.20 more similarity).
155
+ *
156
+ * SIZING (the #425 sweep, 2026-09-13, production snapshot copy: case-1
157
+ * planted fixture + a 30-query real battery vs the boost-0 baseline): every
158
+ * value in {0.05, 0.10, 0.15} passed the hard gates (exact-match rank-1,
159
+ * no-match byte-stability, zero both-channel exact-match losses, the
160
+ * Sirnäs flip) with IDENTICAL battery stability (96.7% top-1 ex-promotions
161
+ * — every raw change was a both-channel exact match displacing a
162
+ * single-channel row, the property firing); they differed only in case-1
163
+ * margin (0.021 / 0.061 / 0.101) and per-id top-3 churn. 0.10 chosen:
164
+ * 0.05's margin is within one embedder revision of flipping, 0.15 churns
165
+ * more for no gate benefit. Weight rebalances measured WORSE (sim .55/str
166
+ * .15 → 93.3%; sim .60/str .10 → 86.7%, fails the 90% gate) — the
167
+ * SCORE_*_WEIGHT values stay as shipped. ZERO-boost neutral (never a
168
+ * penalty): graph-only and single-channel candidates add nothing.
169
+ */
170
+ exports.BOTH_CHANNEL_BOOST = 0.10;
171
+ // ---------------------------------------------------------------------------
172
+ // BM25F field weights (was: bm25WeightBody/Project/Domain) — #205. Body is
173
+ // down-weighted relative to the scope fields so a project/domain token match
174
+ // outranks a token-exact body collision from a foreign scope.
175
+ // ---------------------------------------------------------------------------
176
+ exports.BM25_WEIGHT_BODY = 1.0;
177
+ exports.BM25_WEIGHT_PROJECT = 2.0;
178
+ exports.BM25_WEIGHT_DOMAIN = 2.0;
179
+ // ---------------------------------------------------------------------------
180
+ // Resolution / dedup family (was: dedupAutoMergeThreshold [legacy
181
+ // dedupMergeThreshold], supersessionMinSimilarity, correctionMinSimilarity,
182
+ // correctionRewriteMinConfidence, weakPrimaryFloor)
183
+ // ---------------------------------------------------------------------------
184
+ /**
185
+ * Write cap on base_strength / importance (#425, owner decision D2
186
+ * 2026-09-13: cap 0.95). The decay model's rate is `1 − BASE_DECAY·(1 −
187
+ * importance)` — at importance exactly 1.0 the rate is exactly 1.0 and the
188
+ * row NEVER decays (measured on the production snapshot: ~14% of live rows
189
+ * pegged at ≥0.999, 1838/17399 at exactly 1.0). The cap kills the
190
+ * immortality cliff at every write site (stageImportance, enrich, hub
191
+ * boost) while keeping a multi-year half-life for the top band (0.95 →
192
+ * ~13 years). effectiveStrength ALSO clamps the read side, so legacy
193
+ * base-1.0 rows decay again.
194
+ */
195
+ exports.IMPORTANCE_CEILING = 0.95;
196
+ /** Deterministic merge ceiling of the unified resolution pass (#392): pairs
197
+ * at/above this cosine merge LLM-free; [CORRECTION_MIN_SIMILARITY, this)
198
+ * get the one verdict call. 0.92 — measured on the #191 mechanical audit
199
+ * corpus (89 clusters / 110 excess rows; data/audit-20260729). */
200
+ exports.DEDUP_AUTO_MERGE_THRESHOLD = 0.92;
201
+ /** Minimum cosine for a nightly supersession candidate pair (#100 stage,
202
+ * 0.15.0): one classify-tier call per pair above the bar. */
203
+ exports.SUPERSESSION_MIN_SIMILARITY = 0.80;
204
+ /** Minimum cosine for a reconsolidation correction pair (#384). Deliberately
205
+ * wider than supersession's 0.80: a retraction often rides inside an
206
+ * otherwise unrelated memory; the verdict + confidence gate carry the
207
+ * precision. */
208
+ exports.CORRECTION_MIN_SIMILARITY = 0.75;
209
+ /** Minimum verdict confidence for the REWRITE (and #392 merge-apply) fork
210
+ * (#384): below it a `corrects` degrades to mark-only — a weak mark is
211
+ * recoverable, a weak rewrite is corruption. */
212
+ exports.CORRECTION_REWRITE_MIN_CONFIDENCE = 0.80;
213
+ /** Minimum cosine(memory embedding, best domain prototype) for a no-fit
214
+ * memory to earn a WEAK primary instead of decaying (owner amendment
215
+ * 07.07). Starting point for bge-small-en-v1.5. */
216
+ exports.WEAK_PRIMARY_FLOOR = 0.45;
217
+ // ---------------------------------------------------------------------------
218
+ // Console presentation family (#409/#421) — stage thresholds for the
219
+ // /dashboard console. The STAGE BANDS are calibrated per release against the
220
+ // MEASURED distribution (2026-09-13 against the inflated store; this release
221
+ // re-derived against the post-#425 backfilled store — see the block JSDoc
222
+ // immediately below). The recall-grade zones that used to
223
+ // live here are REMOVED per the owner semantics ruling 2026-09-13 (#426):
224
+ // recall depends only on the conversation — "if the index is good enough for
225
+ // the context needed, it does not have to be fetched" — so a graded
226
+ // Low/Medium/Good/Excellent scale is the wrong instrument (higher is not a
227
+ // target that exists). The console shows the raw uses-per-showing ratio + a
228
+ // trend; the reference expectations for reading that number live on #426,
229
+ // not in shipped bands. The evolution contract still applies to the stage
230
+ // bands: they move only in releases, with the eval / band-stats evidence
231
+ // linked in the changelog line that moves them.
232
+ // ---------------------------------------------------------------------------
233
+ /**
234
+ * Console stage bands (#409/#421) — TWO #408-contract calibrations:
235
+ *
236
+ * (1) 2026-09-13 (PR #424 fix round) against the INFLATED distribution:
237
+ * effectiveStrength p25 = 0.599, p50 = 0.788, p86 = 0.900, ~14%
238
+ * saturated at exactly 1.000 → edges 0.40/0.60/0.90 targeting a rendered
239
+ * split ≈ 10/15/61/14 (fading/forming/belief/truth).
240
+ *
241
+ * (2) This release (#425) against the POST-FIX distribution: the importance
242
+ * recalibration (re-anchored prompt + 0.95 cap) plus the
243
+ * rescore-importance backfill re-spread the same 17,399-row store to
244
+ * base_strength median 0.40 / p90 0.60 / max 0.90 (zero rows ≥ 0.999);
245
+ * measured effectiveStrength on the backfilled copy: p25 ≈ 0.296,
246
+ * p50 ≈ 0.397, p75 ≈ 0.497, p86 ≈ 0.585, max ≈ 0.900. The stale 2026-09-13
247
+ * edges rendered that store as 58.5/37.2/4.2/0 — no Truth left. These
248
+ * edges (0.20/0.30/0.58) re-derive the same ≈ 10/15/61/14 split intent
249
+ * against the honest distribution and render 11.8/25.6/47.7/14.9 (the
250
+ * scorer emits one-decimal scores, so the mass clusters at exact atoms —
251
+ * 0.20/0.30/0.60 — and the 0.60 atom alone holds ~12% of the store; the
252
+ * truth edge therefore sits at 0.58, just under the atom, rather than on
253
+ * the round number that would strand it). Evidence: the #425
254
+ * post-backfill band-stats photo (backfilled copy, deriveStage math via
255
+ * the production effectiveStrength).
256
+ */
257
+ /**
258
+ * Days without access after which a memory's derived stage is Fading
259
+ * (regardless of strength — the recency gate runs FIRST in deriveStage).
260
+ * 120 ≈ a season: long enough that a working memory is never mislabeled,
261
+ * short enough that the rim of the field turns over within a year.
262
+ */
263
+ exports.STAGE_FADING_DAYS = 120;
264
+ /**
265
+ * Effective-strength ceiling of the living bands: below this the memory is
266
+ * Fading (the measured weak cluster + the recency-gated rim, together ~12% of
267
+ * the post-#425 store) even when recently touched.
268
+ */
269
+ exports.STAGE_FADING_STRENGTH = 0.20;
270
+ /** The Forming/Belief edge: [FADING, this) → forming, ≥ this → belief. */
271
+ exports.STAGE_BELIEF_STRENGTH = 0.30;
272
+ /** The Belief/Truth edge: ≥ this → truth — the hardened core (~15% post-#425;
273
+ * just under the 0.60 score atom, see the block JSDoc above). */
274
+ exports.STAGE_TRUTH_STRENGTH = 0.58;
275
+ /**
276
+ * One owner corroboration's bump to base_strength (#423 phase 3, POST
277
+ * /enrich — evidence about importance). Post-#425 the forming band
278
+ * [0.20,0.30) is 0.10 wide; +0.10 = a full band — one enrich visibly crosses
279
+ * an edge for a mid-band Forming memory. Repeated enriches cap at the
280
+ * IMPORTANCE_CEILING (0.95, the SQL MIN); for a young memory effStr ≈ base so
281
+ * the bump lands ~1:1 (the phase-3 verify "enrich a Forming memory → stage
282
+ * change" holds for a mid-band memory ≥0.25).
283
+ */
284
+ exports.ENRICH_STRENGTH_DELTA = 0.10;
285
+ // ---------------------------------------------------------------------------
286
+ // Capture / first-run cost family (#436)
287
+ // ---------------------------------------------------------------------------
288
+ /**
289
+ * Days of session history a FIRST nightly run discovers (the first-run
290
+ * watermark is now − this many days, not the epoch).
291
+ *
292
+ * Owner ruling (2026-09-14, #436): full-history first-run ingestion is not
293
+ * feasible — "if everybody signing up ingests their full history, we will
294
+ * pay millions in tokens during the trial period before customers pay us
295
+ * anything." Cloud needs a very short window; the local/installable version
296
+ * uses the SAME default.
297
+ *
298
+ * Unlike every other constant in this module, this one KEEPS a config
299
+ * override (`firstRunLookbackDays`, read at the capture call sites): it
300
+ * describes an install's cost posture — the same family as
301
+ * `llmTokensPerMonth` / `captureCooldownHours`, which stayed config through
302
+ * #408 — not brain tuning. More history is a deliberate act:
303
+ * `hicortex nightly --recapture-window <days>`.
304
+ */
305
+ exports.DEFAULT_FIRST_RUN_LOOKBACK_DAYS = 7;
306
+ // ---------------------------------------------------------------------------
307
+ // Diagnostic tier (env-overridable — #408). The ollama-operational family is
308
+ // NOT user tuning: it exists so an operator of a constrained box can pin the
309
+ // three values into a service unit's environment without a config-file
310
+ // round-trip. Precedence: env > the constant below. An invalid env value
311
+ // warns and falls back to the constant (the resolveMemorySoftCap boundary
312
+ // posture, applied to the env half).
313
+ // ---------------------------------------------------------------------------
314
+ /**
315
+ * Context window for ollama (one value, all phases; #220/#228). 8192 is
316
+ * where context stops being the binding constraint for a sub-8B model on
317
+ * ollama. Also drives `detectChunkSize` (chunkChars ≤ numCtx × 0.6 × 4
318
+ * chars) so the chunker and the request agree by construction.
319
+ */
320
+ exports.NUM_CTX = 8192;
321
+ /** Flush ollama's accumulated memory every N LLM calls (0 = off; #220).
322
+ * Opt-in operational workaround for ollama runner RSS growth — never a
323
+ * default-on behavior. */
324
+ exports.OLLAMA_FLUSH_EVERY = 0;
325
+ /** Ms to wait after an ollama flush (`keep_alive:0`) for the runner to exit
326
+ * + release memory. The runner takes >90 s to exit; 3 min allows margin. */
327
+ exports.OLLAMA_FLUSH_WAIT_MS = 180000;
328
+ /** The env-tier table (release surface: names + defaults are frozen —
329
+ * adding a knob here is a release decision, not a runtime one). */
330
+ exports.DIAGNOSTIC_ENV_TIER = Object.freeze({
331
+ numCtx: Object.freeze({ env: "HICORTEX_NUM_CTX", default: exports.NUM_CTX }),
332
+ ollamaFlushEvery: Object.freeze({
333
+ env: "HICORTEX_OLLAMA_FLUSH_EVERY",
334
+ default: exports.OLLAMA_FLUSH_EVERY,
335
+ }),
336
+ ollamaFlushWaitMs: Object.freeze({
337
+ env: "HICORTEX_OLLAMA_FLUSH_WAIT_MS",
338
+ default: exports.OLLAMA_FLUSH_WAIT_MS,
339
+ }),
340
+ });
341
+ /** Resolve the effective ollama context window: a positive finite
342
+ * HICORTEX_NUM_CTX wins; anything else (absent/blank/invalid) keeps NUM_CTX
343
+ * with a warn on the invalid case. */
344
+ function resolveNumCtx() {
345
+ const raw = process.env[exports.DIAGNOSTIC_ENV_TIER.numCtx.env];
346
+ if (raw === undefined || raw === "")
347
+ return exports.NUM_CTX;
348
+ const v = Number(raw);
349
+ if (Number.isFinite(v) && v > 0)
350
+ return v;
351
+ console.warn(`[hicortex] env HICORTEX_NUM_CTX=${JSON.stringify(raw)} is not a positive finite number — using default ${exports.NUM_CTX}.`);
352
+ return exports.NUM_CTX;
353
+ }
354
+ /** Resolve the flush cadence: a non-negative finite
355
+ * HICORTEX_OLLAMA_FLUSH_EVERY wins (0 = the valid off value); invalid warns
356
+ * and keeps OLLAMA_FLUSH_EVERY. */
357
+ function resolveOllamaFlushEvery() {
358
+ const raw = process.env[exports.DIAGNOSTIC_ENV_TIER.ollamaFlushEvery.env];
359
+ if (raw === undefined || raw === "")
360
+ return exports.OLLAMA_FLUSH_EVERY;
361
+ const v = Number(raw);
362
+ if (Number.isFinite(v) && v >= 0)
363
+ return Math.floor(v);
364
+ console.warn(`[hicortex] env HICORTEX_OLLAMA_FLUSH_EVERY=${JSON.stringify(raw)} is not a non-negative finite number — using default ${exports.OLLAMA_FLUSH_EVERY}.`);
365
+ return exports.OLLAMA_FLUSH_EVERY;
366
+ }
367
+ /** Resolve the post-flush wait: a positive finite
368
+ * HICORTEX_OLLAMA_FLUSH_WAIT_MS wins; invalid warns and keeps
369
+ * OLLAMA_FLUSH_WAIT_MS. */
370
+ function resolveOllamaFlushWaitMs() {
371
+ const raw = process.env[exports.DIAGNOSTIC_ENV_TIER.ollamaFlushWaitMs.env];
372
+ if (raw === undefined || raw === "")
373
+ return exports.OLLAMA_FLUSH_WAIT_MS;
374
+ const v = Number(raw);
375
+ if (Number.isFinite(v) && v > 0)
376
+ return v;
377
+ console.warn(`[hicortex] env HICORTEX_OLLAMA_FLUSH_WAIT_MS=${JSON.stringify(raw)} is not a positive finite number — using default ${exports.OLLAMA_FLUSH_WAIT_MS}.`);
378
+ return exports.OLLAMA_FLUSH_WAIT_MS;
379
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * /distill capture-health accounting (#422 Phase 2).
3
+ *
4
+ * BOTH halves of the console's capture-health card live here (the same
5
+ * layering as recall-index.ts / identity-store.ts: pure logic, unit-testable
6
+ * without booting express — mcp-server.ts only wires the record calls into
7
+ * the /distill exits):
8
+ *
9
+ * - recordDistillActivity(db, entry) — one row per /distill POST, whatever
10
+ * the outcome ('ok' | 'skipped' | 'held'). Held rows are the point: they
11
+ * are the failed posts the client will retry next run (its cursor is
12
+ * held), so the card can show "2 held" instead of a silently missing
13
+ * night. `retried` is computed at INSERT (an earlier row with the same
14
+ * session_id + segment_id = this POST is the retry) and never updated.
15
+ *
16
+ * - readCaptureHealth(db) — the aggregation /dashboard/data exposes as
17
+ * `capture_health`: the most recent day with rows, grouped by machine ×
18
+ * agent (posts / sessions / bytes / held / retried), bytes DESC.
19
+ *
20
+ * Retention: 7 days, pruned inside recordDistillActivity at most ONCE per
21
+ * process per UTC day (in-module memo) — the insert path must not pay a
22
+ * DELETE on every POST. Old rows exist to explain recent nights, nothing
23
+ * else; the durable record of WHAT was captured is the memories themselves.
24
+ */
25
+ import type Database from "better-sqlite3";
26
+ /** A /distill POST outcome. 'held' = the client will retry (cursor held):
27
+ * no-LLM, dead-endpoint probe, budget 429, distill failure. 'skipped' = a
28
+ * duplicate the dedup prechecks rejected (nothing owed). 'ok' = stored.
29
+ * 'paused' (#423 phase 3, D3) = deliberately skipped by the operator's
30
+ * capture pause — the session is NOT captured and will not be backfilled
31
+ * (the 200 advances the client's cursor by design). */
32
+ export type DistillOutcome = "ok" | "skipped" | "held" | "paused";
33
+ /** One /distill POST to record. The wire fields arrive as-is from the request
34
+ * body (machine/agent/sessionId/segmentId may be absent or mistyped) —
35
+ * normalization happens HERE so every handler call site is a one-liner. */
36
+ export interface DistillActivityEntry {
37
+ /** ISO timestamp of the POST; defaults to now. */
38
+ ts?: string;
39
+ /** Raw source_machine wire value — sanitized via storage.sanitizeSourceMachine, '' when absent. */
40
+ machine: unknown;
41
+ /** Raw source_agent wire value — 'unknown' when absent/blank. */
42
+ agent: unknown;
43
+ /** Raw session_id wire value — stored when a non-empty string, else NULL. */
44
+ sessionId: unknown;
45
+ /** Raw segment_id wire value — stored when a non-empty string, else NULL. */
46
+ segmentId: unknown;
47
+ /** Resolved conversationText length (post-redaction), 0 when unresolved. */
48
+ bytes: number;
49
+ outcome: DistillOutcome;
50
+ }
51
+ /** One aggregated machine × agent row of readCaptureHealth(). */
52
+ export interface CaptureHealthRow {
53
+ machine: string;
54
+ agent: string;
55
+ posts: number;
56
+ sessions: number;
57
+ bytes: number;
58
+ held: number;
59
+ retried: number;
60
+ }
61
+ /** The /dashboard/data capture_health block. day=null + rows=[] when nothing
62
+ * is recorded (fresh install, or every row pruned). */
63
+ export interface CaptureHealth {
64
+ day: string | null;
65
+ rows: CaptureHealthRow[];
66
+ }
67
+ /** Test-only: re-arm the once-per-day prune memo. The memo is deliberately
68
+ * process-global (production must not DELETE per insert); the test suite
69
+ * needs it fresh per case. Exported for testability, same precedent as
70
+ * quarantineMalformedConfig. */
71
+ export declare function resetDistillPruneMemoForTests(): void;
72
+ /**
73
+ * Record one /distill POST outcome. Computes `retried` at insert: 1 when an
74
+ * EARLIER row exists with the same session_id AND same segment_id (matched on
75
+ * COALESCE(segment_id, '') so legacy whole-session POSTs — no segment_id —
76
+ * retry-match on the empty key). A NULL session_id never matches anything:
77
+ * without a session id the POST has no identity to be a retry OF. Also prunes
78
+ * rows older than 7 days, at most once per process per UTC day.
79
+ */
80
+ export declare function recordDistillActivity(db: Database.Database, entry: DistillActivityEntry): void;
81
+ /**
82
+ * Aggregate the most recent day with rows into per machine × agent bundles.
83
+ * day = today when today has rows, else the most recent day with rows, else
84
+ * null (empty shape). sessions = COUNT(DISTINCT session_id) — NULL session
85
+ * ids don't count (no identity). Sorted bytes DESC (the card's bar scale).
86
+ */
87
+ export declare function readCaptureHealth(db: Database.Database): CaptureHealth;
@@ -0,0 +1,106 @@
1
+ "use strict";
2
+ /**
3
+ * /distill capture-health accounting (#422 Phase 2).
4
+ *
5
+ * BOTH halves of the console's capture-health card live here (the same
6
+ * layering as recall-index.ts / identity-store.ts: pure logic, unit-testable
7
+ * without booting express — mcp-server.ts only wires the record calls into
8
+ * the /distill exits):
9
+ *
10
+ * - recordDistillActivity(db, entry) — one row per /distill POST, whatever
11
+ * the outcome ('ok' | 'skipped' | 'held'). Held rows are the point: they
12
+ * are the failed posts the client will retry next run (its cursor is
13
+ * held), so the card can show "2 held" instead of a silently missing
14
+ * night. `retried` is computed at INSERT (an earlier row with the same
15
+ * session_id + segment_id = this POST is the retry) and never updated.
16
+ *
17
+ * - readCaptureHealth(db) — the aggregation /dashboard/data exposes as
18
+ * `capture_health`: the most recent day with rows, grouped by machine ×
19
+ * agent (posts / sessions / bytes / held / retried), bytes DESC.
20
+ *
21
+ * Retention: 7 days, pruned inside recordDistillActivity at most ONCE per
22
+ * process per UTC day (in-module memo) — the insert path must not pay a
23
+ * DELETE on every POST. Old rows exist to explain recent nights, nothing
24
+ * else; the durable record of WHAT was captured is the memories themselves.
25
+ */
26
+ Object.defineProperty(exports, "__esModule", { value: true });
27
+ exports.resetDistillPruneMemoForTests = resetDistillPruneMemoForTests;
28
+ exports.recordDistillActivity = recordDistillActivity;
29
+ exports.readCaptureHealth = readCaptureHealth;
30
+ const storage_js_1 = require("./storage.js");
31
+ /** Once-per-process-per-UTC-day prune memo (see module doc). The UTC day of
32
+ * the last insert that ran the DELETE. Tests reset it because vitest runs
33
+ * every suite in ONE process — see resetDistillPruneMemoForTests. */
34
+ let lastPruneDay = null;
35
+ /** Test-only: re-arm the once-per-day prune memo. The memo is deliberately
36
+ * process-global (production must not DELETE per insert); the test suite
37
+ * needs it fresh per case. Exported for testability, same precedent as
38
+ * quarantineMalformedConfig. */
39
+ function resetDistillPruneMemoForTests() {
40
+ lastPruneDay = null;
41
+ }
42
+ /** Normalize an optional wire string: non-empty trimmed string, else null. */
43
+ function optString(v) {
44
+ if (typeof v !== "string")
45
+ return null;
46
+ const t = v.trim();
47
+ return t.length > 0 ? t : null;
48
+ }
49
+ /**
50
+ * Record one /distill POST outcome. Computes `retried` at insert: 1 when an
51
+ * EARLIER row exists with the same session_id AND same segment_id (matched on
52
+ * COALESCE(segment_id, '') so legacy whole-session POSTs — no segment_id —
53
+ * retry-match on the empty key). A NULL session_id never matches anything:
54
+ * without a session id the POST has no identity to be a retry OF. Also prunes
55
+ * rows older than 7 days, at most once per process per UTC day.
56
+ */
57
+ function recordDistillActivity(db, entry) {
58
+ const ts = entry.ts ?? new Date().toISOString();
59
+ // UTC YYYY-MM-DD of ts — day buckets are UTC everywhere (dashboard
60
+ // conventions; SQLite date('now') is UTC too).
61
+ const day = ts.slice(0, 10);
62
+ const sessionId = optString(entry.sessionId);
63
+ const segmentId = optString(entry.segmentId);
64
+ // Retry detection BEFORE the insert (the new row must not match itself).
65
+ // `IS ?` binds NULL correctly for the session half.
66
+ const retried = sessionId !== null &&
67
+ (db
68
+ .prepare("SELECT 1 FROM distill_activity WHERE session_id IS ? AND COALESCE(segment_id, '') = ? LIMIT 1")
69
+ .get(sessionId, segmentId ?? "") !== undefined)
70
+ ? 1
71
+ : 0;
72
+ db.prepare(`INSERT INTO distill_activity (ts, day, machine, agent, session_id, segment_id, bytes, outcome, retried)
73
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(ts, day, (0, storage_js_1.sanitizeSourceMachine)(entry.machine) ?? "", optString(entry.agent) ?? "unknown", sessionId, segmentId, entry.bytes, entry.outcome, retried);
74
+ // 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.
76
+ if (lastPruneDay !== day) {
77
+ db.prepare("DELETE FROM distill_activity WHERE day < date('now', '-7 days')").run();
78
+ lastPruneDay = day;
79
+ }
80
+ }
81
+ /**
82
+ * Aggregate the most recent day with rows into per machine × agent bundles.
83
+ * day = today when today has rows, else the most recent day with rows, else
84
+ * null (empty shape). sessions = COUNT(DISTINCT session_id) — NULL session
85
+ * ids don't count (no identity). Sorted bytes DESC (the card's bar scale).
86
+ */
87
+ function readCaptureHealth(db) {
88
+ const dayRow = db
89
+ .prepare("SELECT MAX(day) AS day FROM distill_activity")
90
+ .get();
91
+ if (!dayRow.day)
92
+ return { day: null, rows: [] };
93
+ const rows = db
94
+ .prepare(`SELECT machine, agent,
95
+ COUNT(*) AS posts,
96
+ COUNT(DISTINCT session_id) AS sessions,
97
+ COALESCE(SUM(bytes), 0) AS bytes,
98
+ COALESCE(SUM(outcome = 'held'), 0) AS held,
99
+ COALESCE(SUM(retried), 0) AS retried
100
+ FROM distill_activity
101
+ WHERE day = ?
102
+ GROUP BY machine, agent
103
+ ORDER BY bytes DESC`)
104
+ .all(dayRow.day);
105
+ return { day: dayRow.day, rows };
106
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Operator capture pause (#423 phase 3, D3) — the server-side 200-skip.
3
+ *
4
+ * A pause is a row in `capture_pauses` (migration v18): machine × harness →
5
+ * paused_at. A row EXISTS = paused for that bundle; the /distill handler
6
+ * reads the table per POST, so a pause takes effect on the very next post —
7
+ * no restart — and answers 200 {skipped: true, paused: true}. The 200 is the
8
+ * whole point: capture.ts treats every 200 as confirmed and advances its
9
+ * cursor, so sessions that arrive while paused are deliberately NOT captured
10
+ * and are never re-sent or backfilled. Zero client changes.
11
+ *
12
+ * THE BUNDLE KEY. The pause's (machine, harness) must derive EXACTLY like
13
+ * the traffic it gates: machine via storage.sanitizeSourceMachine ('' when
14
+ * absent) and harness via harnessOfAgent below — the same normalization
15
+ * recordDistillActivity applies when it writes distill_activity, and the same
16
+ * "harness/profile" → "harness" split the console groups its bundles on. A
17
+ * key derived any other way would never match and the pause would silently
18
+ * not fire.
19
+ *
20
+ * LAST-SEEN (presence dots) derives ONLY from /distill activity — the one
21
+ * per-agent-identified traffic the server sees. Recall traffic (/search,
22
+ * /recall-index, /memory) carries no agent/machine identity on the wire, so
23
+ * attributing it would need new client fields — the heartbeats the spec
24
+ * forbids. Thresholds (green ≤36h — a nightly poster reads online through
25
+ * the following day; amber ≤7d — the distill_activity retention window; none
26
+ * beyond or with no rows) are page-side presentation; this module just
27
+ * reports the newest ts per bundle.
28
+ *
29
+ * Pure, unit-testable without express (the capture-health.ts layering):
30
+ * mcp-server.ts and dashboard.ts wire these functions to the live db.
31
+ */
32
+ import type Database from "better-sqlite3";
33
+ /** The bundle half of a pause key: "harness/profile" → "harness". */
34
+ export interface CapturePauseKey {
35
+ /** Sanitized machine ('' when the post carried none) — matches distill_activity. */
36
+ machine: string;
37
+ /** The harness name (pre-slash part, 128-char cap; 'unknown' when absent/blank). */
38
+ harness: string;
39
+ }
40
+ /** One paused bundle as the dashboard fleet block serves it. */
41
+ export interface CapturePause {
42
+ machine: string;
43
+ harness: string;
44
+ paused_at: string;
45
+ }
46
+ /** One bundle's newest /distill activity row (the presence signal). */
47
+ export interface FleetLastSeen {
48
+ machine: string;
49
+ harness: string;
50
+ last_seen: string;
51
+ /** The outcome of that newest row ('ok' | 'skipped' | 'held' | 'paused'). */
52
+ last_outcome: string;
53
+ }
54
+ /**
55
+ * Derive the harness from a source_agent wire value — the bundle split the
56
+ * console already uses ("claude-code/main" → "claude-code"): the part before
57
+ * the first '/' when a slash is present at index > 0, else the whole trimmed
58
+ * string, capped at 128. Non-string/blank → "unknown" (mirrors how
59
+ * recordDistillActivity stores the agent when absent).
60
+ */
61
+ export declare function harnessOfAgent(sourceAgent: unknown): string;
62
+ /**
63
+ * Normalize the raw /distill wire fields into the pause key. MUST match the
64
+ * recordDistillActivity normalization (machine '' when absent, agent
65
+ * 'unknown' when absent) and the console's bundle grouping
66
+ * ((machine||'')+'|'+harness) — see the module doc.
67
+ */
68
+ export declare function capturePauseKey(machine: unknown, sourceAgent: unknown): CapturePauseKey;
69
+ /** True when a pause row exists for the (machine, harness) bundle. */
70
+ export declare function isCapturePaused(db: Database.Database, machine: string, harness: string): boolean;
71
+ /**
72
+ * Pause (upsert the row, timestamp now) or resume (delete it). Returns the
73
+ * persisted paused_at when pausing, null when resuming. No pruning, ever —
74
+ * see migration v18's provenance comment.
75
+ */
76
+ export declare function setCapturePause(db: Database.Database, machine: string, harness: string, paused: boolean): string | null;
77
+ /** Every paused bundle (newest first) — the dashboard fleet.pauses block. */
78
+ export declare function listCapturePauses(db: Database.Database): CapturePause[];
79
+ /**
80
+ * The newest /distill activity per bundle: for each (machine, agent) take
81
+ * MAX(ts) with that latest row's outcome, derive the harness per agent, then
82
+ * merge same-bundle agents keeping the newest ts (one dot per bundle, not
83
+ * per profile). Reads only distill_activity, which the recorder prunes to
84
+ * 7 days — older-than-window bundles simply have no rows and no dot.
85
+ */
86
+ export declare function readFleetLastSeen(db: Database.Database): FleetLastSeen[];