@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.
- package/README.md +18 -41
- package/assets/dashboard.html +3989 -836
- package/dist/calibration.d.ts +293 -0
- package/dist/calibration.js +379 -0
- package/dist/capture-health.d.ts +87 -0
- package/dist/capture-health.js +106 -0
- package/dist/capture-pause.d.ts +86 -0
- package/dist/capture-pause.js +127 -0
- package/dist/capture.d.ts +24 -3
- package/dist/capture.js +11 -1
- package/dist/classify-domains.d.ts +6 -0
- package/dist/classify-domains.js +7 -1
- package/dist/cli.js +38 -3
- package/dist/config-read.d.ts +1 -1
- package/dist/config-read.js +96 -9
- package/dist/consolidate.d.ts +114 -68
- package/dist/consolidate.js +302 -182
- package/dist/dashboard.d.ts +326 -6
- package/dist/dashboard.js +592 -7
- package/dist/db.js +105 -0
- package/dist/dedup.d.ts +34 -26
- package/dist/dedup.js +91 -57
- package/dist/distiller.js +1 -1
- package/dist/domain-classify.d.ts +7 -6
- package/dist/domain-classify.js +12 -10
- package/dist/eval/decay-eval.d.ts +3 -3
- package/dist/eval/decay-eval.js +4 -4
- package/dist/eval/importance-eval.d.ts +85 -0
- package/dist/eval/importance-eval.js +286 -0
- package/dist/eval/planted-eval.d.ts +26 -0
- package/dist/eval/planted-eval.js +97 -0
- package/dist/eval/planted-fixtures.d.ts +107 -0
- package/dist/eval/planted-fixtures.js +283 -0
- package/dist/eval/planted-harness.d.ts +176 -0
- package/dist/eval/planted-harness.js +649 -0
- package/dist/eval/ranking-battery.d.ts +78 -0
- package/dist/eval/ranking-battery.js +181 -0
- package/dist/eval/ranking-eval.d.ts +41 -0
- package/dist/eval/ranking-eval.js +391 -0
- package/dist/eval/ranking-fixtures.d.ts +77 -0
- package/dist/eval/ranking-fixtures.js +226 -0
- package/dist/identity-store.d.ts +21 -0
- package/dist/identity-store.js +49 -0
- package/dist/index.js +4 -3
- package/dist/init.d.ts +23 -3
- package/dist/init.js +84 -9
- package/dist/llm.d.ts +43 -58
- package/dist/llm.js +87 -101
- package/dist/mcp-server.d.ts +12 -0
- package/dist/mcp-server.js +213 -32
- package/dist/nightly.d.ts +9 -1
- package/dist/nightly.js +164 -110
- package/dist/nofit.d.ts +4 -11
- package/dist/nofit.js +6 -23
- package/dist/prompts.d.ts +10 -0
- package/dist/prompts.js +28 -5
- package/dist/recall-index.d.ts +30 -28
- package/dist/recall-index.js +21 -18
- package/dist/recall-registry.d.ts +2 -1
- package/dist/recall-registry.js +35 -1
- package/dist/reconsolidation.d.ts +168 -87
- package/dist/reconsolidation.js +818 -377
- package/dist/relink.js +3 -4
- package/dist/rescore-importance.d.ts +80 -0
- package/dist/rescore-importance.js +236 -0
- package/dist/retrieval.d.ts +80 -35
- package/dist/retrieval.js +322 -105
- package/dist/run-deadline.d.ts +62 -0
- package/dist/run-deadline.js +73 -0
- package/dist/schema-prototypes.d.ts +3 -3
- package/dist/schema-prototypes.js +3 -3
- package/dist/stages.d.ts +37 -0
- package/dist/stages.js +51 -0
- package/dist/state.d.ts +34 -9
- package/dist/storage.d.ts +50 -18
- package/dist/storage.js +125 -30
- package/dist/telemetry.d.ts +8 -7
- package/dist/token-budget.js +3 -4
- package/dist/type-classify.js +4 -4
- package/dist/types.d.ts +143 -155
- package/domains.example.json +4 -5
- package/hermes-plugin/hicortex/README.md +2 -2
- package/openclaw.plugin.json +1 -1
- package/package.json +4 -1
- package/pi-extension/hicortex/README.md +1 -1
- package/server.json +3 -3
package/dist/retrieval.js
CHANGED
|
@@ -3,14 +3,15 @@
|
|
|
3
3
|
* Retrieval layer with composite scoring, RRF fusion, and graph traversal.
|
|
4
4
|
* Ported from hicortex/retrieval.py — same scoring model and weights.
|
|
5
5
|
*
|
|
6
|
-
* Scoring model (weights are
|
|
6
|
+
* Scoring model (weights are RELEASE-MANAGED since #408 — calibration.ts;
|
|
7
|
+
* configureScoring is the eval/test seam only):
|
|
7
8
|
* score = similarity * 0.50 + effective_strength * 0.20
|
|
8
9
|
* + connection_score * 0.15 + recency * 0.15
|
|
9
10
|
* + fresh-memory bonus (≤ 0.15, linear over the first 7 days)
|
|
10
11
|
* then × 0.50 if the memory was superseded by a later decision
|
|
11
12
|
*
|
|
12
13
|
* Decay model (B+E+D):
|
|
13
|
-
* base_decay = derived from
|
|
14
|
+
* base_decay = derived from the calibration half-life (365 → ~1-year
|
|
14
15
|
* half-life at importance 0.5, importance-scaled either way)
|
|
15
16
|
* decay_rate = 1 - base_decay * (1 - importance)
|
|
16
17
|
* decay_rate = 1 - (1 - decay_rate) * 0.7^access_count
|
|
@@ -52,7 +53,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
52
53
|
};
|
|
53
54
|
})();
|
|
54
55
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
55
|
-
exports.SESSION_INTENT_ALPHA = exports.DEFAULT_DECAY_HALF_LIFE_DAYS = void 0;
|
|
56
|
+
exports.BELIEF_WALK_MAX_HOPS = exports.SESSION_INTENT_ALPHA = exports.DEFAULT_DECAY_HALF_LIFE_DAYS = void 0;
|
|
56
57
|
exports.decayConstantForHalfLife = decayConstantForHalfLife;
|
|
57
58
|
exports.configureDecay = configureDecay;
|
|
58
59
|
exports.configureRecall = configureRecall;
|
|
@@ -64,7 +65,9 @@ exports.blendQueryVector = blendQueryVector;
|
|
|
64
65
|
exports.recallQueryVector = recallQueryVector;
|
|
65
66
|
exports.findSupersededIds = findSupersededIds;
|
|
66
67
|
exports.findDemotedIds = findDemotedIds;
|
|
68
|
+
exports.beliefWalkTerminal = beliefWalkTerminal;
|
|
67
69
|
exports.l2ToCosine = l2ToCosine;
|
|
70
|
+
exports.cosineBetweenVectors = cosineBetweenVectors;
|
|
68
71
|
exports.effectiveStrength = effectiveStrength;
|
|
69
72
|
exports.computeScore = computeScore;
|
|
70
73
|
exports.retrieve = retrieve;
|
|
@@ -72,11 +75,10 @@ exports.searchRecent = searchRecent;
|
|
|
72
75
|
const storage = __importStar(require("./storage.js"));
|
|
73
76
|
const schema_prototypes_js_1 = require("./schema-prototypes.js");
|
|
74
77
|
const type_labels_js_1 = require("./type-labels.js");
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
*
|
|
78
|
-
|
|
79
|
-
exports.DEFAULT_DECAY_HALF_LIFE_DAYS = 365;
|
|
78
|
+
const CALIBRATION = __importStar(require("./calibration.js"));
|
|
79
|
+
/** Default decay half-life (days) at importance 0.5 — release-managed
|
|
80
|
+
* (#408): the constant lives in calibration.ts with its provenance. */
|
|
81
|
+
exports.DEFAULT_DECAY_HALF_LIFE_DAYS = CALIBRATION.DECAY_HALF_LIFE_DAYS;
|
|
80
82
|
/**
|
|
81
83
|
* Derive the per-hour base decay constant from a half-life target: for the
|
|
82
84
|
* decayable portion, retention^hours = 0.5 at `days`, evaluated at the
|
|
@@ -89,110 +91,103 @@ function decayConstantForHalfLife(days) {
|
|
|
89
91
|
}
|
|
90
92
|
let BASE_DECAY = decayConstantForHalfLife(exports.DEFAULT_DECAY_HALF_LIFE_DAYS);
|
|
91
93
|
/**
|
|
92
|
-
* Configure the decay speed
|
|
93
|
-
*
|
|
94
|
-
*
|
|
94
|
+
* Configure the decay speed for THIS process (the eval/test seam — #408).
|
|
95
|
+
* Production NEVER passes an argument: every process scores with the
|
|
96
|
+
* calibration half-life (calibration.ts DECAY_HALF_LIFE_DAYS). An
|
|
97
|
+
* invalid/absent value keeps the default. Exported value for tests.
|
|
95
98
|
*/
|
|
96
|
-
function configureDecay(
|
|
97
|
-
const days = Number(
|
|
99
|
+
function configureDecay(halfLifeDays) {
|
|
100
|
+
const days = Number(halfLifeDays);
|
|
98
101
|
BASE_DECAY = decayConstantForHalfLife(Number.isFinite(days) && days > 0 ? days : exports.DEFAULT_DECAY_HALF_LIFE_DAYS);
|
|
99
102
|
return BASE_DECAY;
|
|
100
103
|
}
|
|
101
104
|
const RECALL_DEFAULTS = {
|
|
102
|
-
searchLimit:
|
|
103
|
-
recentLimit:
|
|
104
|
-
recentWindowDays:
|
|
105
|
-
coldExposureSlots:
|
|
105
|
+
searchLimit: CALIBRATION.SEARCH_LIMIT,
|
|
106
|
+
recentLimit: CALIBRATION.RECENT_LIMIT,
|
|
107
|
+
recentWindowDays: CALIBRATION.RECENT_WINDOW_DAYS,
|
|
108
|
+
coldExposureSlots: CALIBRATION.COLD_EXPOSURE_SLOTS,
|
|
106
109
|
};
|
|
107
110
|
let recallDefaults = { ...RECALL_DEFAULTS };
|
|
108
111
|
/**
|
|
109
|
-
* Configure recall breadth from
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
+
* Configure recall breadth from RESOLVED overrides (the eval/test seam —
|
|
113
|
+
* #408). Production calls this with no argument: the calibration defaults
|
|
114
|
+
* (calibration.ts) apply. Invalid/absent values keep the shipped default per
|
|
115
|
+
* key. Returns the resolved values (for logging + tests).
|
|
112
116
|
*/
|
|
113
|
-
function configureRecall(
|
|
114
|
-
const pick = (
|
|
115
|
-
const
|
|
116
|
-
return Number.isFinite(
|
|
117
|
+
function configureRecall(overrides) {
|
|
118
|
+
const pick = (v, dflt) => {
|
|
119
|
+
const n = Number(v);
|
|
120
|
+
return Number.isFinite(n) && n >= 0 ? Math.floor(n) : dflt;
|
|
117
121
|
};
|
|
118
122
|
recallDefaults = {
|
|
119
|
-
searchLimit: Math.max(1, pick(
|
|
120
|
-
recentLimit: Math.max(1, pick(
|
|
121
|
-
recentWindowDays: Math.max(1, pick(
|
|
122
|
-
coldExposureSlots: pick(
|
|
123
|
+
searchLimit: Math.max(1, pick(overrides?.searchLimit, RECALL_DEFAULTS.searchLimit)),
|
|
124
|
+
recentLimit: Math.max(1, pick(overrides?.recentLimit, RECALL_DEFAULTS.recentLimit)),
|
|
125
|
+
recentWindowDays: Math.max(1, pick(overrides?.recentWindowDays, RECALL_DEFAULTS.recentWindowDays)),
|
|
126
|
+
coldExposureSlots: pick(overrides?.coldExposureSlots, RECALL_DEFAULTS.coldExposureSlots),
|
|
123
127
|
};
|
|
124
128
|
return { ...recallDefaults };
|
|
125
129
|
}
|
|
126
130
|
const SCORING_DEFAULTS = {
|
|
127
|
-
similarity:
|
|
128
|
-
strength:
|
|
129
|
-
connections:
|
|
130
|
-
recency:
|
|
131
|
-
freshnessBoostDays:
|
|
132
|
-
freshnessBoostWeight:
|
|
133
|
-
supersededDemotion:
|
|
134
|
-
projectAffinity:
|
|
135
|
-
domainAffinity:
|
|
136
|
-
// #205
|
|
137
|
-
// values (60 and 0.8)
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
//
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
//
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
rrfVectorWeight: 1.0,
|
|
131
|
+
similarity: CALIBRATION.SCORE_SIMILARITY_WEIGHT,
|
|
132
|
+
strength: CALIBRATION.SCORE_STRENGTH_WEIGHT,
|
|
133
|
+
connections: CALIBRATION.SCORE_CONNECTIONS_WEIGHT,
|
|
134
|
+
recency: CALIBRATION.SCORE_RECENCY_WEIGHT,
|
|
135
|
+
freshnessBoostDays: CALIBRATION.FRESHNESS_BOOST_DAYS,
|
|
136
|
+
freshnessBoostWeight: CALIBRATION.FRESHNESS_BOOST_WEIGHT,
|
|
137
|
+
supersededDemotion: CALIBRATION.SUPERSEDED_DEMOTION,
|
|
138
|
+
projectAffinity: CALIBRATION.PROJECT_AFFINITY_WEIGHT,
|
|
139
|
+
domainAffinity: CALIBRATION.DOMAIN_AFFINITY_WEIGHT,
|
|
140
|
+
// #205 (calibration.ts): rrfK + rrfCompositeWeight match the pre-#205
|
|
141
|
+
// hardcoded values (60 and 0.8); the FTS per-list weight (1.0 → 0.5) is the
|
|
142
|
+
// one deliberate nudge toward vector — the bisection point where BM25F +
|
|
143
|
+
// composite-affinity flip the token-exact marine body match below the
|
|
144
|
+
// same-scope hardware field while pure-keyword queries keep recall@5 = 1.0.
|
|
145
|
+
rrfK: CALIBRATION.RRF_K,
|
|
146
|
+
rrfCompositeWeight: CALIBRATION.RRF_COMPOSITE_WEIGHT,
|
|
147
|
+
rrfFtsWeight: CALIBRATION.RRF_FTS_WEIGHT,
|
|
148
|
+
rrfVectorWeight: CALIBRATION.RRF_VECTOR_WEIGHT,
|
|
149
|
+
// #425 (calibration.ts): the both-channel genuine-match boost. 0.10 is
|
|
150
|
+
// the sweep-chosen size — D3's dominance margin (>= 0.10 x the similarity
|
|
151
|
+
// weight) with the battery stability gates intact.
|
|
152
|
+
bothChannelBoost: CALIBRATION.BOTH_CHANNEL_BOOST,
|
|
150
153
|
};
|
|
151
154
|
let scoringWeights = { ...SCORING_DEFAULTS };
|
|
152
155
|
/**
|
|
153
|
-
* Configure scoring weights + ranking knobs from
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
* SAME config object for one-place tuning.
|
|
156
|
+
* Configure scoring weights + ranking knobs from RESOLVED overrides (the
|
|
157
|
+
* eval/test seam — #408). Production calls this with no argument: the
|
|
158
|
+
* calibration defaults (calibration.ts) apply, identically in the daemon and
|
|
159
|
+
* the nightly. Invalid/absent values keep the shipped default per key.
|
|
160
|
+
* Returns the resolved set for logging/tests. (The #205 BM25F field weights
|
|
161
|
+
* are NOT touched here — they live in storage.ts and resolve from the same
|
|
162
|
+
* calibration module via storage.configureBm25Fts.)
|
|
161
163
|
*/
|
|
162
|
-
function configureScoring(
|
|
163
|
-
const num = (
|
|
164
|
-
const
|
|
165
|
-
return Number.isFinite(
|
|
164
|
+
function configureScoring(overrides) {
|
|
165
|
+
const num = (v, dflt, min, max) => {
|
|
166
|
+
const n = Number(v);
|
|
167
|
+
return Number.isFinite(n) && n >= min && n <= max ? n : dflt;
|
|
166
168
|
};
|
|
167
|
-
// #205 BM25F weights use a [0, ∞) range (no upper bound
|
|
168
|
-
//
|
|
169
|
-
const numW = (
|
|
170
|
-
const
|
|
171
|
-
return Number.isFinite(
|
|
169
|
+
// #205 BM25F-style weights use a [0, ∞) range (no upper bound; 0 drops the
|
|
170
|
+
// field/list entirely). Invalid ⇒ default.
|
|
171
|
+
const numW = (v, dflt) => {
|
|
172
|
+
const n = Number(v);
|
|
173
|
+
return Number.isFinite(n) && n >= 0 ? n : dflt;
|
|
172
174
|
};
|
|
173
175
|
scoringWeights = {
|
|
174
|
-
similarity: num(
|
|
175
|
-
strength: num(
|
|
176
|
-
connections: num(
|
|
177
|
-
recency: num(
|
|
178
|
-
freshnessBoostDays: num(
|
|
179
|
-
freshnessBoostWeight: num(
|
|
180
|
-
supersededDemotion: num(
|
|
181
|
-
projectAffinity: num(
|
|
182
|
-
domainAffinity: num(
|
|
183
|
-
rrfK: numW(
|
|
184
|
-
rrfCompositeWeight: num(
|
|
185
|
-
rrfFtsWeight: numW(
|
|
186
|
-
rrfVectorWeight: numW(
|
|
176
|
+
similarity: num(overrides?.similarity, SCORING_DEFAULTS.similarity, 0, 1),
|
|
177
|
+
strength: num(overrides?.strength, SCORING_DEFAULTS.strength, 0, 1),
|
|
178
|
+
connections: num(overrides?.connections, SCORING_DEFAULTS.connections, 0, 1),
|
|
179
|
+
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),
|
|
182
|
+
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),
|
|
185
|
+
rrfK: numW(overrides?.rrfK, SCORING_DEFAULTS.rrfK),
|
|
186
|
+
rrfCompositeWeight: num(overrides?.rrfCompositeWeight, SCORING_DEFAULTS.rrfCompositeWeight, 0, 1),
|
|
187
|
+
rrfFtsWeight: numW(overrides?.rrfFtsWeight, SCORING_DEFAULTS.rrfFtsWeight),
|
|
188
|
+
rrfVectorWeight: numW(overrides?.rrfVectorWeight, SCORING_DEFAULTS.rrfVectorWeight),
|
|
189
|
+
bothChannelBoost: num(overrides?.bothChannelBoost, SCORING_DEFAULTS.bothChannelBoost, 0, 1),
|
|
187
190
|
};
|
|
188
|
-
// #205: push BM25F field weights into storage so searchFts uses them. Same
|
|
189
|
-
// config object, one tuning surface; storage owns the module-level mirror
|
|
190
|
-
// next to the FTS column declaration (the positional order matters there).
|
|
191
|
-
storage.configureBm25Fts({
|
|
192
|
-
bm25WeightBody: numW("bm25WeightBody", 1.0),
|
|
193
|
-
bm25WeightProject: numW("bm25WeightProject", 2.0),
|
|
194
|
-
bm25WeightDomain: numW("bm25WeightDomain", 2.0),
|
|
195
|
-
});
|
|
196
191
|
return { ...scoringWeights };
|
|
197
192
|
}
|
|
198
193
|
/** Current resolved weights (tests + status output). */
|
|
@@ -200,11 +195,11 @@ function getScoringWeights() {
|
|
|
200
195
|
return { ...scoringWeights };
|
|
201
196
|
}
|
|
202
197
|
// ---------------------------------------------------------------------------
|
|
203
|
-
// Session-intent keying (#192, 0.15.3). ONE
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
//
|
|
198
|
+
// Session-intent keying (#192, 0.15.3). ONE calibration constant (#408):
|
|
199
|
+
// SESSION_INTENT_WEIGHT 0.33 blend weight of the rolling centroid in the
|
|
200
|
+
// search vector: query = (1-w)·prompt + w·centroid.
|
|
201
|
+
// configureSessionIntent(0) is the eval-only
|
|
202
|
+
// kill-switch (pure prompt). Range [0, 1].
|
|
208
203
|
//
|
|
209
204
|
// The EMA rate α is a shipped constant (SESSION_INTENT_ALPHA, 0.4), not a
|
|
210
205
|
// second knob — owner directive 0.15.3: one knob is enough to tune/disable;
|
|
@@ -218,18 +213,18 @@ function getScoringWeights() {
|
|
|
218
213
|
// ---------------------------------------------------------------------------
|
|
219
214
|
/** EMA rate for the session-intent centroid: centroid_new = (1-α)·old + α·prompt. */
|
|
220
215
|
exports.SESSION_INTENT_ALPHA = 0.4;
|
|
221
|
-
const SESSION_INTENT_DEFAULT_WEIGHT =
|
|
216
|
+
const SESSION_INTENT_DEFAULT_WEIGHT = CALIBRATION.SESSION_INTENT_WEIGHT;
|
|
222
217
|
let sessionIntentWeight = SESSION_INTENT_DEFAULT_WEIGHT;
|
|
223
218
|
/**
|
|
224
|
-
* Configure session-intent keying
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
* one call.
|
|
219
|
+
* Configure session-intent keying for THIS process (the eval/test seam —
|
|
220
|
+
* #408). Production calls this with no argument: the calibration weight
|
|
221
|
+
* (calibration.ts SESSION_INTENT_WEIGHT) applies. `weight` is [0,1] (0 =
|
|
222
|
+
* disabled — the eval kill-switch); invalid/out-of-range values keep the
|
|
223
|
+
* shipped default. Returns `{ weight, alpha }` — alpha is the fixed constant,
|
|
224
|
+
* surfaced so the recall closure passes it to the registry in one call.
|
|
230
225
|
*/
|
|
231
|
-
function configureSessionIntent(
|
|
232
|
-
const v = Number(
|
|
226
|
+
function configureSessionIntent(weight) {
|
|
227
|
+
const v = Number(weight);
|
|
233
228
|
sessionIntentWeight =
|
|
234
229
|
Number.isFinite(v) && v >= 0 && v <= 1 ? v : SESSION_INTENT_DEFAULT_WEIGHT;
|
|
235
230
|
return { weight: sessionIntentWeight, alpha: exports.SESSION_INTENT_ALPHA };
|
|
@@ -305,6 +300,123 @@ function findDemotedIds(db, candidateIds) {
|
|
|
305
300
|
.all(...candidateIds, ...candidateIds);
|
|
306
301
|
return new Set(rows.map((r) => r.id));
|
|
307
302
|
}
|
|
303
|
+
// ---------------------------------------------------------------------------
|
|
304
|
+
// Belief walk (#393 increment D)
|
|
305
|
+
// ---------------------------------------------------------------------------
|
|
306
|
+
/**
|
|
307
|
+
* Hop cap for the belief walk (#393 D). Supersession edges advance
|
|
308
|
+
* created_at monotonically (the stage only links old → new), so chains are
|
|
309
|
+
* acyclic by construction and 10 hops is far beyond any real revision depth;
|
|
310
|
+
* the cap is cheap insurance (see beliefWalkTerminal for why it is needed
|
|
311
|
+
* anyway).
|
|
312
|
+
*/
|
|
313
|
+
exports.BELIEF_WALK_MAX_HOPS = 10;
|
|
314
|
+
/**
|
|
315
|
+
* The one outgoing supersession edge to follow from `id` (#393 D): when a
|
|
316
|
+
* memory carries several `superseded_by` edges the NEWEST target by
|
|
317
|
+
* created_at wins (deterministic target_id tie-break), null when there is
|
|
318
|
+
* none. Indexed by idx_links_source; one row read.
|
|
319
|
+
*/
|
|
320
|
+
function nextSupersedingId(db, id) {
|
|
321
|
+
const row = db
|
|
322
|
+
.prepare(`SELECT ml.target_id AS target_id
|
|
323
|
+
FROM memory_links ml
|
|
324
|
+
JOIN memories m ON m.id = ml.target_id
|
|
325
|
+
WHERE ml.source_id = ? AND ml.relationship = 'superseded_by'
|
|
326
|
+
ORDER BY m.created_at DESC, ml.target_id ASC
|
|
327
|
+
LIMIT 1`)
|
|
328
|
+
.get(id);
|
|
329
|
+
return row ? row.target_id : null;
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Terminal of the supersession chain starting at `id` (#393 D): follow
|
|
333
|
+
* superseded_by edges transitively until a memory with no outgoing edge and
|
|
334
|
+
* return it — `id` itself when there is nothing to walk, or whichever node
|
|
335
|
+
* the walk stopped on when it aborts. Shared by retrieval (the belief-walk
|
|
336
|
+
* splice in retrieve()/searchRecent()) and the eval harness (the
|
|
337
|
+
* planted-pairs version_chain class probe), so both agree on what "the
|
|
338
|
+
* chain's current truth" is.
|
|
339
|
+
*
|
|
340
|
+
* Edges advance created_at monotonically (acyclic by construction), BUT
|
|
341
|
+
* applyExplicitMark does no age check and created_at is backdatable from
|
|
342
|
+
* session_date — so a cycle or an absurdly long chain is not impossible.
|
|
343
|
+
* The visited set (seeded with `id`) and the BELIEF_WALK_MAX_HOPS cap are
|
|
344
|
+
* cheap insurance against exactly that; the supersededDemotion multiplier
|
|
345
|
+
* in computeScore remains the safety net for rows the walk does not fully
|
|
346
|
+
* resolve (no edge, cycle, cap abort, absorbed terminal).
|
|
347
|
+
*/
|
|
348
|
+
function beliefWalkTerminal(db, id, maxHops = exports.BELIEF_WALK_MAX_HOPS) {
|
|
349
|
+
const visited = new Set([id]);
|
|
350
|
+
let current = id;
|
|
351
|
+
for (let hop = 0; hop < maxHops; hop++) {
|
|
352
|
+
const next = nextSupersedingId(db, current);
|
|
353
|
+
if (next === null || visited.has(next))
|
|
354
|
+
return current;
|
|
355
|
+
visited.add(next);
|
|
356
|
+
current = next;
|
|
357
|
+
}
|
|
358
|
+
return current;
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* The belief-walk splice (#393 D) — the retrieval-side half of "newest wins
|
|
362
|
+
* by construction". Applied to the FINAL top-k of retrieve()/searchRecent(),
|
|
363
|
+
* after the sort and after the cold-exposure splice: a superseded candidate
|
|
364
|
+
* is replaced IN ITS SLOT by its chain's terminal (beliefWalkTerminal), so a
|
|
365
|
+
* strong stale record can never outrank — or appear alongside — the truth
|
|
366
|
+
* that replaced it. The ancestor stays fetchable by id (evidence) but never
|
|
367
|
+
* surfaces as a competing truth in recall.
|
|
368
|
+
*
|
|
369
|
+
* Per entry of `top`, in order:
|
|
370
|
+
* - not superseded → kept untouched (fast path, zero queries)
|
|
371
|
+
* - terminal === entry → kept (no outgoing edge — the entry is its
|
|
372
|
+
* own terminal; a self-loop aborts here too.
|
|
373
|
+
* A hop-cap or non-self-cycle abort stops the
|
|
374
|
+
* walk on a DIFFERENT node, so the entry
|
|
375
|
+
* falls through to the bullets below — e.g.
|
|
376
|
+
* a 2-cycle with both members in top drops
|
|
377
|
+
* both, conservative but nothing stale
|
|
378
|
+
* surfaces (corrupt-data corner only; edges
|
|
379
|
+
* advance created_at, acyclic by
|
|
380
|
+
* construction). supersededDemotion in
|
|
381
|
+
* computeScore stays the safety net for rows
|
|
382
|
+
* the walk does not resolve)
|
|
383
|
+
* - terminal already surfaced → the entry is DROPPED (its truth is present)
|
|
384
|
+
* - otherwise → buildReplacement(terminal); null (terminal
|
|
385
|
+
* unfetchable or absorbed — absorbed rows
|
|
386
|
+
* are invisible to recall by contract) keeps
|
|
387
|
+
* the ancestor in-slot, fail-soft
|
|
388
|
+
*
|
|
389
|
+
* The replacement takes the ancestor's SLOT (position), not its score — no
|
|
390
|
+
* re-sort after the splice; ranking remains the sort's verdict. Idempotent
|
|
391
|
+
* by construction: walk-stable and unsuperseded entries are returned as-is.
|
|
392
|
+
*/
|
|
393
|
+
function applyBeliefWalk(db, top, supersededIds, buildReplacement) {
|
|
394
|
+
const present = new Set(top.map((t) => t.mem.id));
|
|
395
|
+
const out = [];
|
|
396
|
+
for (const entry of top) {
|
|
397
|
+
if (!supersededIds.has(entry.mem.id)) {
|
|
398
|
+
out.push(entry);
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
const terminal = beliefWalkTerminal(db, entry.mem.id);
|
|
402
|
+
if (terminal === entry.mem.id) {
|
|
403
|
+
out.push(entry);
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
if (present.has(terminal)) {
|
|
407
|
+
// The chain's truth is already surfaced — drop the ancestor.
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
const replacement = buildReplacement(terminal);
|
|
411
|
+
if (replacement === null) {
|
|
412
|
+
out.push(entry);
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
out.push(replacement);
|
|
416
|
+
present.add(terminal);
|
|
417
|
+
}
|
|
418
|
+
return out;
|
|
419
|
+
}
|
|
308
420
|
/**
|
|
309
421
|
* Placeholder L2 distance for candidates that have no measured vector
|
|
310
422
|
* distance (FTS-only hits and graph-discovered neighbors). Chosen so that
|
|
@@ -332,6 +444,28 @@ const DEFAULT_RRF_K = 60;
|
|
|
332
444
|
function l2ToCosine(distance) {
|
|
333
445
|
return 1 - (distance * distance) / 2;
|
|
334
446
|
}
|
|
447
|
+
/**
|
|
448
|
+
* Cosine similarity between two stored embeddings (#393 increment B). The
|
|
449
|
+
* similarity source measures cosines transitively via vec0 L2 distances; the
|
|
450
|
+
* scout source finds its candidates through FTS (no vec0 query), so it
|
|
451
|
+
* measures the pair cosine directly from the stored vectors instead —
|
|
452
|
+
* valid because every embedding we store is L2-normalized (embedder.ts).
|
|
453
|
+
* Used as link strength / a ranker, never as a gate (the scout has no
|
|
454
|
+
* similarity floor — that is the point of the increment).
|
|
455
|
+
*/
|
|
456
|
+
function cosineBetweenVectors(a, b) {
|
|
457
|
+
let dot = 0;
|
|
458
|
+
let na = 0;
|
|
459
|
+
let nb = 0;
|
|
460
|
+
for (let i = 0; i < a.length; i++) {
|
|
461
|
+
dot += a[i] * b[i];
|
|
462
|
+
na += a[i] * a[i];
|
|
463
|
+
nb += b[i] * b[i];
|
|
464
|
+
}
|
|
465
|
+
if (na === 0 || nb === 0)
|
|
466
|
+
return 0;
|
|
467
|
+
return dot / Math.sqrt(na * nb);
|
|
468
|
+
}
|
|
335
469
|
// ---------------------------------------------------------------------------
|
|
336
470
|
// Timestamp parsing
|
|
337
471
|
// ---------------------------------------------------------------------------
|
|
@@ -354,9 +488,16 @@ function parseTimestamp(ts) {
|
|
|
354
488
|
/**
|
|
355
489
|
* Compute decayed strength with adaptive decay (B+E+D model).
|
|
356
490
|
* Exported for use by consolidation decay/prune stage.
|
|
491
|
+
*
|
|
492
|
+
* #425 read-side law: the decay-relevant importance is CLAMPED at the
|
|
493
|
+
* release-managed ceiling (calibration.ts IMPORTANCE_CEILING) — at importance
|
|
494
|
+
* exactly 1.0 the decay rate is exactly 1.0 and the row never decays, so
|
|
495
|
+
* legacy base-1.0 rows (and any write site that predates the cap) decay
|
|
496
|
+
* again. The clamp applies to explicit importance passes too.
|
|
357
497
|
*/
|
|
358
498
|
function effectiveStrength(baseStrength, lastAccessed, now, options) {
|
|
359
|
-
const
|
|
499
|
+
const rawImportance = options?.importance ?? baseStrength;
|
|
500
|
+
const importance = Math.min(rawImportance, CALIBRATION.IMPORTANCE_CEILING);
|
|
360
501
|
const accessCount = options?.accessCount ?? 0;
|
|
361
502
|
const linkCount = options?.linkCount ?? 0;
|
|
362
503
|
const hours = Math.max((now.getTime() - parseTimestamp(lastAccessed).getTime()) / 3_600_000, 0);
|
|
@@ -383,6 +524,11 @@ function computeScore(memory, distance, connectionCount, maxConnections, now, op
|
|
|
383
524
|
// is a data-driven follow-up if the eval shows it is needed.
|
|
384
525
|
const similarity = Math.max(0, l2ToCosine(distance));
|
|
385
526
|
const effStrength = effectiveStrength(memory.base_strength ?? 0.5, memory.last_accessed, now, {
|
|
527
|
+
// #425: importance passed EXPLICITLY (the same default value
|
|
528
|
+
// effectiveStrength would apply — base strength IS importance at read
|
|
529
|
+
// time — now stated at the call site so the triple-win coupling
|
|
530
|
+
// (score share, decay rate, floor) is visible and single-sourced).
|
|
531
|
+
importance: memory.base_strength ?? 0.5,
|
|
386
532
|
accessCount: memory.access_count ?? 0,
|
|
387
533
|
linkCount: connectionCount,
|
|
388
534
|
});
|
|
@@ -438,6 +584,15 @@ function computeScore(memory, distance, connectionCount, maxConnections, now, op
|
|
|
438
584
|
score += maxWeight * scoringWeights.domainAffinity;
|
|
439
585
|
}
|
|
440
586
|
}
|
|
587
|
+
// #425 both-channel boost: vector KNN and BM25 FTS AGREEING on a candidate
|
|
588
|
+
// is the genuine-match signature (a distinctive proper noun the user knows
|
|
589
|
+
// exists — the field failure this fixes: 0.90/0.95-strength domain-adjacent
|
|
590
|
+
// memories outranked the best-similarity exact-token match). ADDITIVE,
|
|
591
|
+
// zero-boost neutral, never a penalty; rides the composite side only (like
|
|
592
|
+
// projectAffinity — the RRF side is #205 territory); applied BEFORE the
|
|
593
|
+
// superseded multiplier so a superseded both-channel row still demotes.
|
|
594
|
+
if (options?.bothChannel)
|
|
595
|
+
score += scoringWeights.bothChannelBoost;
|
|
441
596
|
// Superseded demotion (#191 Phase B): a memory whose decision was reversed by
|
|
442
597
|
// a later one keeps its content and strength but must not outrank the
|
|
443
598
|
// decision that replaced it. Applied as an explicit multiplier here rather
|
|
@@ -673,6 +828,9 @@ async function retrieve(db, embedFn, query, options) {
|
|
|
673
828
|
superseded: supersededIds.has(mid),
|
|
674
829
|
scope,
|
|
675
830
|
tagWeights: tagWeightsByMemory?.get(mid),
|
|
831
|
+
// #425: the two retrieval channels agreeing is the genuine-match
|
|
832
|
+
// signature — graph-only and single-channel candidates add nothing.
|
|
833
|
+
bothChannel: source === "both",
|
|
676
834
|
});
|
|
677
835
|
const effStr = effectiveStrength(mem.base_strength ?? 0.5, mem.last_accessed, now, {
|
|
678
836
|
accessCount: mem.access_count ?? 0,
|
|
@@ -715,6 +873,38 @@ async function retrieve(db, embedFn, query, options) {
|
|
|
715
873
|
}
|
|
716
874
|
}
|
|
717
875
|
}
|
|
876
|
+
// 6b. Belief walk (#393 D) — a superseded top-k member is replaced in-slot
|
|
877
|
+
// by its chain's terminal. A terminal already in `scored` reuses its honest
|
|
878
|
+
// entry (similarity/RRF/score); one outside the candidate set is computed
|
|
879
|
+
// fresh with source "graph" and RRF share 0 (it entered by LINK, not by any
|
|
880
|
+
// retrieval channel — finalScore = composite × rrfCompositeWeight only).
|
|
881
|
+
// The strengthen() below then fires on the SURFACED set: the terminal
|
|
882
|
+
// accrues the use signal, the dropped ancestor does not. supersededDemotion
|
|
883
|
+
// in computeScore stays as the safety net for rows the walk does not reach.
|
|
884
|
+
const scoredById = new Map(scored.map((s) => [s.mem.id, s]));
|
|
885
|
+
top = applyBeliefWalk(db, top, supersededIds, (terminalId) => {
|
|
886
|
+
const existing = scoredById.get(terminalId);
|
|
887
|
+
if (existing)
|
|
888
|
+
return existing;
|
|
889
|
+
const mem = storage.getMemory(db, terminalId);
|
|
890
|
+
if (!mem || mem.status === "absorbed")
|
|
891
|
+
return null; // fail-soft
|
|
892
|
+
// Hard-filter invariant (#393 D): sourceAgent is the one hard filter left
|
|
893
|
+
// in retrieve(), and the walk must not bypass it — a terminal authored by
|
|
894
|
+
// a different agent never slips into recall through its link entry
|
|
895
|
+
// (mirrors the graph pull-in guard above; fail-soft: the ancestor keeps
|
|
896
|
+
// its slot, demoted).
|
|
897
|
+
if (sourceAgent && mem.source_agent !== sourceAgent)
|
|
898
|
+
return null;
|
|
899
|
+
const connCount = storage.getLinks(db, terminalId, "both").length;
|
|
900
|
+
const composite = computeScore(mem, DEFAULT_GRAPH_DISTANCE, connCount, maxConnections, now, { superseded: supersededIds.has(terminalId) });
|
|
901
|
+
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
|
+
});
|
|
906
|
+
return { mem, finalScore, effStr, connCount, similarity: null, source: "graph" };
|
|
907
|
+
});
|
|
718
908
|
const results = top.map((t) => formatResult(t.mem, t.finalScore, t.effStr, t.connCount, {
|
|
719
909
|
similarity: t.similarity,
|
|
720
910
|
source: t.source,
|
|
@@ -765,7 +955,34 @@ function searchRecent(db, options) {
|
|
|
765
955
|
scored.push({ mem, score, effStr, connCount });
|
|
766
956
|
}
|
|
767
957
|
scored.sort((a, b) => b.score - a.score);
|
|
768
|
-
|
|
958
|
+
let top = scored.slice(0, limit);
|
|
959
|
+
// Belief walk (#393 D) — same contract as retrieve(): a superseded recent
|
|
960
|
+
// memory is replaced in-slot by its chain terminal (this path has no
|
|
961
|
+
// similarity/RRF channels, so a fresh replacement carries the composite
|
|
962
|
+
// score only). The trailing strengthen() runs over the WALKED list — the
|
|
963
|
+
// terminal accrues the use signal, the dropped ancestor does not.
|
|
964
|
+
// supersededDemotion in computeScore stays as the safety net for rows the
|
|
965
|
+
// walk does not reach (no edge, absorbed terminal, cycles/caps).
|
|
966
|
+
top = applyBeliefWalk(db, top, supersededRecent, (terminalId) => {
|
|
967
|
+
const mem = storage.getMemory(db, terminalId);
|
|
968
|
+
if (!mem || mem.status === "absorbed")
|
|
969
|
+
return null; // fail-soft
|
|
970
|
+
// Hard-filter invariant (#393 D, review fix): project is the one hard
|
|
971
|
+
// filter in searchRecent(), and the walk must not bypass it — supersession
|
|
972
|
+
// edges are discovered corpus-wide (no project predicate), so a terminal
|
|
973
|
+
// in a different project never slips into project-scoped recall through
|
|
974
|
+
// its link entry (mirrors retrieve()'s sourceAgent guard; fail-soft: the
|
|
975
|
+
// ancestor keeps its slot, demoted).
|
|
976
|
+
if (project && mem.project !== project)
|
|
977
|
+
return null;
|
|
978
|
+
const connCount = storage.getLinks(db, terminalId, "both").length;
|
|
979
|
+
const score = computeScore(mem, DEFAULT_GRAPH_DISTANCE, connCount, maxConnections, now, { superseded: supersededRecent.has(terminalId) });
|
|
980
|
+
const effStr = effectiveStrength(mem.base_strength ?? 0.5, mem.last_accessed, now, {
|
|
981
|
+
accessCount: mem.access_count ?? 0,
|
|
982
|
+
linkCount: connCount,
|
|
983
|
+
});
|
|
984
|
+
return { mem, score, effStr, connCount };
|
|
985
|
+
});
|
|
769
986
|
const results = top.map((t) => formatResult(t.mem, t.score, t.effStr, t.connCount));
|
|
770
987
|
strengthen(db, top.map((t) => t.mem), now);
|
|
771
988
|
return results;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ONE cooperative wall-clock deadline for a nightly run (#405).
|
|
3
|
+
*
|
|
4
|
+
* Generalizes the #401/#402/#404 reconsolidation pattern (a deadline checked
|
|
5
|
+
* at safe boundaries + resumable cursors) from one stage to the whole
|
|
6
|
+
* pipeline: capture segments, every consolidation stage boundary, the
|
|
7
|
+
* item loops, and the deterministic merge zone all check the SAME deadline,
|
|
8
|
+
* created once at nightly start. On expiry each check site stops cleanly at
|
|
9
|
+
* its last safe boundary — cursors (capture per-session, supersession,
|
|
10
|
+
* reconsolidation) hold below unconfirmed work, so the next run resumes
|
|
11
|
+
* without redoing confirmed work or losing deferred work.
|
|
12
|
+
*
|
|
13
|
+
* Deferral is a REPORTED outcome, not an error: a run whose deadline fired
|
|
14
|
+
* reports consolidation status "deferred" and does NOT advance
|
|
15
|
+
* `lastConsolidated` (the same gate as endpoint_down — otherwise the
|
|
16
|
+
* pending-set queries would silently lose the deferred work).
|
|
17
|
+
*
|
|
18
|
+
* One structured log line per deferred stage (`event=deadline_deferred`,
|
|
19
|
+
* the same key=value style as `event=budget_exhausted` /
|
|
20
|
+
* `event=circuit_open`) so `grep event=deadline_deferred` is one of the two
|
|
21
|
+
* nightly health greps.
|
|
22
|
+
*/
|
|
23
|
+
/** Default wall-clock budget for a full nightly run, in minutes (#405).
|
|
24
|
+
* 240 sits between the old stage-local reconsolidation clock (120) and the
|
|
25
|
+
* old systemd backstop (360) — the deadline is now the operating bound and
|
|
26
|
+
* the unit backstop (budget + 60 min slack) only catches a true hang. */
|
|
27
|
+
export declare const DEFAULT_NIGHTLY_TIME_BUDGET_MINUTES = 240;
|
|
28
|
+
/**
|
|
29
|
+
* Cooperative deadline handle. Injectable clock (`now`) so tests can pin or
|
|
30
|
+
* advance time deterministically (the reconsolidation.test.ts 0.000001-minute
|
|
31
|
+
* pattern). All methods are safe to call after expiry — they keep answering.
|
|
32
|
+
*/
|
|
33
|
+
export interface RunDeadline {
|
|
34
|
+
/** Absolute epoch-ms timestamp the run must finish by. */
|
|
35
|
+
readonly deadlineAt: number;
|
|
36
|
+
/** Milliseconds left until the deadline (never negative). */
|
|
37
|
+
remainingMs(): number;
|
|
38
|
+
/** True once the clock has passed the deadline. */
|
|
39
|
+
expired(): boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Check + record in one call: returns true when the deadline has fired,
|
|
42
|
+
* logging `event=deadline_deferred stage=<name>` ONCE per stage name (a
|
|
43
|
+
* boundary check and in-loop checks for the same stage log once). Use the
|
|
44
|
+
* same stage name at a stage's boundary and inside its loops.
|
|
45
|
+
*/
|
|
46
|
+
hit(stage: string): boolean;
|
|
47
|
+
/** Stage names that deferred so far this run (insertion order). */
|
|
48
|
+
deferredStages(): readonly string[];
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Create a run deadline of `minutes` (already-resolved value; see
|
|
52
|
+
* resolveNightlyTimeBudgetMinutes for the config boundary).
|
|
53
|
+
*/
|
|
54
|
+
export declare function createRunDeadline(minutes: number, now?: () => number): RunDeadline;
|
|
55
|
+
/**
|
|
56
|
+
* Resolve `nightlyTimeBudgetMinutes` from the saved config (#405): positive
|
|
57
|
+
* finite number wins; absent/0/invalid → the 240 default. Unlike the old
|
|
58
|
+
* `reconsolidationMaxMinutes`, 0 does NOT disable — there is ALWAYS a
|
|
59
|
+
* deadline (readPositiveConfig rejects 0 with a warn, which is the loud
|
|
60
|
+
* migration signal for an operator who used 0 as "off").
|
|
61
|
+
*/
|
|
62
|
+
export declare function resolveNightlyTimeBudgetMinutes(config: Record<string, unknown> | null | undefined): number;
|