@gamaze/hicortex 0.21.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/dashboard.html +274 -25
- package/dist/calibration.d.ts +30 -0
- package/dist/calibration.js +31 -1
- package/dist/capture-health.d.ts +42 -5
- package/dist/capture-health.js +57 -7
- package/dist/capture-pause.d.ts +7 -4
- package/dist/capture-pause.js +7 -4
- package/dist/consolidate.d.ts +19 -0
- package/dist/consolidate.js +116 -51
- package/dist/dashboard.d.ts +41 -16
- package/dist/dashboard.js +110 -41
- package/dist/db.js +16 -0
- package/dist/dedup.js +4 -1
- package/dist/eval/decay-eval.d.ts +2 -3
- package/dist/eval/decay-eval.js +6 -44
- package/dist/eval/ranking-eval.js +1 -1
- package/dist/eval/ranking-fixtures.d.ts +5 -5
- package/dist/eval/ranking-fixtures.js +7 -7
- package/dist/eval/run-eval.js +1 -1
- package/dist/graph.d.ts +1 -2
- package/dist/graph.js +3 -4
- package/dist/mcp-server.js +1 -1
- package/dist/nofit.d.ts +2 -1
- package/dist/nofit.js +2 -1
- package/dist/recall-index.d.ts +3 -2
- package/dist/recall-index.js +3 -2
- package/dist/retrieval.d.ts +9 -3
- package/dist/retrieval.js +21 -32
- package/dist/status.js +18 -0
- package/dist/storage.d.ts +2 -1
- package/dist/storage.js +6 -1
- package/dist/types.d.ts +25 -4
- package/package.json +1 -1
- package/server.json +4 -4
package/dist/capture-health.d.ts
CHANGED
|
@@ -17,10 +17,19 @@
|
|
|
17
17
|
* `capture_health`: the most recent day with rows, grouped by machine ×
|
|
18
18
|
* agent (posts / sessions / bytes / held / retried), bytes DESC.
|
|
19
19
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
20
|
+
* - readCaptureHealthWindow(db) — the same grouping over the ROLLING
|
|
21
|
+
* capture window (#409 fix round 7, owner ruling 2026-09-14: the card
|
|
22
|
+
* shows the normal 30 days like the band's other cards). The newest
|
|
23
|
+
* night's rows stay on the payload too (secondary info: the card's
|
|
24
|
+
* "tonight" suffix); the window sums are the card's face.
|
|
25
|
+
*
|
|
26
|
+
* Retention: CAPTURE_HEALTH_WINDOW_DAYS (30), pruned inside
|
|
27
|
+
* recordDistillActivity at most ONCE per process per UTC day (in-module
|
|
28
|
+
* memo) — the insert path must not pay a DELETE on every POST. Retention and
|
|
29
|
+
* the window aggregate share the ONE constant, so the card can never claim a
|
|
30
|
+
* window the store no longer has rows for. Old rows exist to feed the card's
|
|
31
|
+
* rolling window, nothing else; the durable record of WHAT was captured is
|
|
32
|
+
* the memories themselves.
|
|
24
33
|
*/
|
|
25
34
|
import type Database from "better-sqlite3";
|
|
26
35
|
/** A /distill POST outcome. 'held' = the client will retry (cursor held):
|
|
@@ -64,6 +73,13 @@ export interface CaptureHealth {
|
|
|
64
73
|
day: string | null;
|
|
65
74
|
rows: CaptureHealthRow[];
|
|
66
75
|
}
|
|
76
|
+
/** The rolling capture window the console's card shows, in days — ALSO the
|
|
77
|
+
* retention horizon for distill_activity (the recorder prunes past it).
|
|
78
|
+
* Owner ruling 2026-09-14: the capture card shows the normal 30 day like
|
|
79
|
+
* the band's other cards, so the store must retain enough nights to sum
|
|
80
|
+
* one. ONE constant feeds both the window aggregate and the prune: the card
|
|
81
|
+
* can never claim a window the store no longer has rows for. */
|
|
82
|
+
export declare const CAPTURE_HEALTH_WINDOW_DAYS = 30;
|
|
67
83
|
/** Test-only: re-arm the once-per-day prune memo. The memo is deliberately
|
|
68
84
|
* process-global (production must not DELETE per insert); the test suite
|
|
69
85
|
* needs it fresh per case. Exported for testability, same precedent as
|
|
@@ -75,7 +91,8 @@ export declare function resetDistillPruneMemoForTests(): void;
|
|
|
75
91
|
* COALESCE(segment_id, '') so legacy whole-session POSTs — no segment_id —
|
|
76
92
|
* retry-match on the empty key). A NULL session_id never matches anything:
|
|
77
93
|
* without a session id the POST has no identity to be a retry OF. Also prunes
|
|
78
|
-
* rows
|
|
94
|
+
* rows outside the rolling capture window (CAPTURE_HEALTH_WINDOW_DAYS), at
|
|
95
|
+
* most once per process per UTC day.
|
|
79
96
|
*/
|
|
80
97
|
export declare function recordDistillActivity(db: Database.Database, entry: DistillActivityEntry): void;
|
|
81
98
|
/**
|
|
@@ -85,3 +102,23 @@ export declare function recordDistillActivity(db: Database.Database, entry: Dist
|
|
|
85
102
|
* ids don't count (no identity). Sorted bytes DESC (the card's bar scale).
|
|
86
103
|
*/
|
|
87
104
|
export declare function readCaptureHealth(db: Database.Database): CaptureHealth;
|
|
105
|
+
/** The /dashboard/data capture_health window block: the echoed window length
|
|
106
|
+
* + the per machine × agent rows over it. Field names are the WIRE keys —
|
|
107
|
+
* dashboard.ts spreads this over the newest-night shape, so the keys must
|
|
108
|
+
* not collide with {day, rows}. window_rows=[] when the window is empty
|
|
109
|
+
* (fresh install, static snapshot without activity rows — the page keeps its
|
|
110
|
+
* counts fallback). */
|
|
111
|
+
export interface CaptureHealthWindow {
|
|
112
|
+
window_days: number;
|
|
113
|
+
window_rows: CaptureHealthRow[];
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Aggregate the ROLLING capture window (CAPTURE_HEALTH_WINDOW_DAYS calendar
|
|
117
|
+
* days, today inclusive) into per machine × agent bundles — the card's face
|
|
118
|
+
* (#409 fix round 7: the band's other cards are all 30-day; the newest night
|
|
119
|
+
* moves to secondary info). Same shape as readCaptureHealth's rows, but
|
|
120
|
+
* summed across nights: sessions stays COUNT(DISTINCT session_id), so a
|
|
121
|
+
* session retried on a later night counts once (identity, not occurrences).
|
|
122
|
+
* Sorted bytes DESC (the card's bar scale).
|
|
123
|
+
*/
|
|
124
|
+
export declare function readCaptureHealthWindow(db: Database.Database): CaptureHealthWindow;
|
package/dist/capture-health.js
CHANGED
|
@@ -18,20 +18,43 @@
|
|
|
18
18
|
* `capture_health`: the most recent day with rows, grouped by machine ×
|
|
19
19
|
* agent (posts / sessions / bytes / held / retried), bytes DESC.
|
|
20
20
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
21
|
+
* - readCaptureHealthWindow(db) — the same grouping over the ROLLING
|
|
22
|
+
* capture window (#409 fix round 7, owner ruling 2026-09-14: the card
|
|
23
|
+
* shows the normal 30 days like the band's other cards). The newest
|
|
24
|
+
* night's rows stay on the payload too (secondary info: the card's
|
|
25
|
+
* "tonight" suffix); the window sums are the card's face.
|
|
26
|
+
*
|
|
27
|
+
* Retention: CAPTURE_HEALTH_WINDOW_DAYS (30), pruned inside
|
|
28
|
+
* recordDistillActivity at most ONCE per process per UTC day (in-module
|
|
29
|
+
* memo) — the insert path must not pay a DELETE on every POST. Retention and
|
|
30
|
+
* the window aggregate share the ONE constant, so the card can never claim a
|
|
31
|
+
* window the store no longer has rows for. Old rows exist to feed the card's
|
|
32
|
+
* rolling window, nothing else; the durable record of WHAT was captured is
|
|
33
|
+
* the memories themselves.
|
|
25
34
|
*/
|
|
26
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.CAPTURE_HEALTH_WINDOW_DAYS = void 0;
|
|
27
37
|
exports.resetDistillPruneMemoForTests = resetDistillPruneMemoForTests;
|
|
28
38
|
exports.recordDistillActivity = recordDistillActivity;
|
|
29
39
|
exports.readCaptureHealth = readCaptureHealth;
|
|
40
|
+
exports.readCaptureHealthWindow = readCaptureHealthWindow;
|
|
30
41
|
const storage_js_1 = require("./storage.js");
|
|
31
42
|
/** Once-per-process-per-UTC-day prune memo (see module doc). The UTC day of
|
|
32
43
|
* the last insert that ran the DELETE. Tests reset it because vitest runs
|
|
33
44
|
* every suite in ONE process — see resetDistillPruneMemoForTests. */
|
|
34
45
|
let lastPruneDay = null;
|
|
46
|
+
/** The rolling capture window the console's card shows, in days — ALSO the
|
|
47
|
+
* retention horizon for distill_activity (the recorder prunes past it).
|
|
48
|
+
* Owner ruling 2026-09-14: the capture card shows the normal 30 day like
|
|
49
|
+
* the band's other cards, so the store must retain enough nights to sum
|
|
50
|
+
* one. ONE constant feeds both the window aggregate and the prune: the card
|
|
51
|
+
* can never claim a window the store no longer has rows for. */
|
|
52
|
+
exports.CAPTURE_HEALTH_WINDOW_DAYS = 30;
|
|
53
|
+
/** Inclusive window cutoff as SQL: day >= today−(N−1) — the last N calendar
|
|
54
|
+
* days with today counted (the same inclusive convention as
|
|
55
|
+
* /dashboard/events' days param). The prune keeps exactly this predicate's
|
|
56
|
+
* complement (day < cutoff is deleted), so retained rows == window rows. */
|
|
57
|
+
const WINDOW_CUTOFF_SQL = `date('now', '-${exports.CAPTURE_HEALTH_WINDOW_DAYS - 1} days')`;
|
|
35
58
|
/** Test-only: re-arm the once-per-day prune memo. The memo is deliberately
|
|
36
59
|
* process-global (production must not DELETE per insert); the test suite
|
|
37
60
|
* needs it fresh per case. Exported for testability, same precedent as
|
|
@@ -52,7 +75,8 @@ function optString(v) {
|
|
|
52
75
|
* COALESCE(segment_id, '') so legacy whole-session POSTs — no segment_id —
|
|
53
76
|
* retry-match on the empty key). A NULL session_id never matches anything:
|
|
54
77
|
* without a session id the POST has no identity to be a retry OF. Also prunes
|
|
55
|
-
* rows
|
|
78
|
+
* rows outside the rolling capture window (CAPTURE_HEALTH_WINDOW_DAYS), at
|
|
79
|
+
* most once per process per UTC day.
|
|
56
80
|
*/
|
|
57
81
|
function recordDistillActivity(db, entry) {
|
|
58
82
|
const ts = entry.ts ?? new Date().toISOString();
|
|
@@ -72,9 +96,11 @@ function recordDistillActivity(db, entry) {
|
|
|
72
96
|
db.prepare(`INSERT INTO distill_activity (ts, day, machine, agent, session_id, segment_id, bytes, outcome, retried)
|
|
73
97
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(ts, day, (0, storage_js_1.sanitizeSourceMachine)(entry.machine) ?? "", optString(entry.agent) ?? "unknown", sessionId, segmentId, entry.bytes, entry.outcome, retried);
|
|
74
98
|
// Prune, at most once per process per UTC day. Runs AFTER the insert so the
|
|
75
|
-
// triggering row is subject to the same window as everything else.
|
|
99
|
+
// triggering row is subject to the same window as everything else. The
|
|
100
|
+
// cutoff is the capture window's own edge — retention keeps exactly the
|
|
101
|
+
// nights the card's rolling view sums (see CAPTURE_HEALTH_WINDOW_DAYS).
|
|
76
102
|
if (lastPruneDay !== day) {
|
|
77
|
-
db.prepare(
|
|
103
|
+
db.prepare(`DELETE FROM distill_activity WHERE day < ${WINDOW_CUTOFF_SQL}`).run();
|
|
78
104
|
lastPruneDay = day;
|
|
79
105
|
}
|
|
80
106
|
}
|
|
@@ -104,3 +130,27 @@ function readCaptureHealth(db) {
|
|
|
104
130
|
.all(dayRow.day);
|
|
105
131
|
return { day: dayRow.day, rows };
|
|
106
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* Aggregate the ROLLING capture window (CAPTURE_HEALTH_WINDOW_DAYS calendar
|
|
135
|
+
* days, today inclusive) into per machine × agent bundles — the card's face
|
|
136
|
+
* (#409 fix round 7: the band's other cards are all 30-day; the newest night
|
|
137
|
+
* moves to secondary info). Same shape as readCaptureHealth's rows, but
|
|
138
|
+
* summed across nights: sessions stays COUNT(DISTINCT session_id), so a
|
|
139
|
+
* session retried on a later night counts once (identity, not occurrences).
|
|
140
|
+
* Sorted bytes DESC (the card's bar scale).
|
|
141
|
+
*/
|
|
142
|
+
function readCaptureHealthWindow(db) {
|
|
143
|
+
const rows = db
|
|
144
|
+
.prepare(`SELECT machine, agent,
|
|
145
|
+
COUNT(*) AS posts,
|
|
146
|
+
COUNT(DISTINCT session_id) AS sessions,
|
|
147
|
+
COALESCE(SUM(bytes), 0) AS bytes,
|
|
148
|
+
COALESCE(SUM(outcome = 'held'), 0) AS held,
|
|
149
|
+
COALESCE(SUM(retried), 0) AS retried
|
|
150
|
+
FROM distill_activity
|
|
151
|
+
WHERE day >= ${WINDOW_CUTOFF_SQL}
|
|
152
|
+
GROUP BY machine, agent
|
|
153
|
+
ORDER BY bytes DESC`)
|
|
154
|
+
.all();
|
|
155
|
+
return { window_days: exports.CAPTURE_HEALTH_WINDOW_DAYS, window_rows: rows };
|
|
156
|
+
}
|
package/dist/capture-pause.d.ts
CHANGED
|
@@ -22,9 +22,11 @@
|
|
|
22
22
|
* /recall-index, /memory) carries no agent/machine identity on the wire, so
|
|
23
23
|
* attributing it would need new client fields — the heartbeats the spec
|
|
24
24
|
* forbids. Thresholds (green ≤36h — a nightly poster reads online through
|
|
25
|
-
* the following day; amber ≤7d
|
|
26
|
-
*
|
|
27
|
-
*
|
|
25
|
+
* the following day; amber ≤7d; none beyond or with no rows) are page-side
|
|
26
|
+
* presentation; this module just reports the newest ts per bundle. (Amber's
|
|
27
|
+
* 7d predates and outlives the old 7-day retention — activity rows are now
|
|
28
|
+
* kept for the capture card's 30-day window, CAPTURE_HEALTH_WINDOW_DAYS;
|
|
29
|
+
* the dot's thresholds are a design choice, not a retention artifact.)
|
|
28
30
|
*
|
|
29
31
|
* Pure, unit-testable without express (the capture-health.ts layering):
|
|
30
32
|
* mcp-server.ts and dashboard.ts wire these functions to the live db.
|
|
@@ -81,6 +83,7 @@ export declare function listCapturePauses(db: Database.Database): CapturePause[]
|
|
|
81
83
|
* MAX(ts) with that latest row's outcome, derive the harness per agent, then
|
|
82
84
|
* merge same-bundle agents keeping the newest ts (one dot per bundle, not
|
|
83
85
|
* per profile). Reads only distill_activity, which the recorder prunes to
|
|
84
|
-
*
|
|
86
|
+
* the capture card's rolling window (CAPTURE_HEALTH_WINDOW_DAYS) —
|
|
87
|
+
* older-than-window bundles simply have no rows and no dot.
|
|
85
88
|
*/
|
|
86
89
|
export declare function readFleetLastSeen(db: Database.Database): FleetLastSeen[];
|
package/dist/capture-pause.js
CHANGED
|
@@ -23,9 +23,11 @@
|
|
|
23
23
|
* /recall-index, /memory) carries no agent/machine identity on the wire, so
|
|
24
24
|
* attributing it would need new client fields — the heartbeats the spec
|
|
25
25
|
* forbids. Thresholds (green ≤36h — a nightly poster reads online through
|
|
26
|
-
* the following day; amber ≤7d
|
|
27
|
-
*
|
|
28
|
-
*
|
|
26
|
+
* the following day; amber ≤7d; none beyond or with no rows) are page-side
|
|
27
|
+
* presentation; this module just reports the newest ts per bundle. (Amber's
|
|
28
|
+
* 7d predates and outlives the old 7-day retention — activity rows are now
|
|
29
|
+
* kept for the capture card's 30-day window, CAPTURE_HEALTH_WINDOW_DAYS;
|
|
30
|
+
* the dot's thresholds are a design choice, not a retention artifact.)
|
|
29
31
|
*
|
|
30
32
|
* Pure, unit-testable without express (the capture-health.ts layering):
|
|
31
33
|
* mcp-server.ts and dashboard.ts wire these functions to the live db.
|
|
@@ -97,7 +99,8 @@ function listCapturePauses(db) {
|
|
|
97
99
|
* MAX(ts) with that latest row's outcome, derive the harness per agent, then
|
|
98
100
|
* merge same-bundle agents keeping the newest ts (one dot per bundle, not
|
|
99
101
|
* per profile). Reads only distill_activity, which the recorder prunes to
|
|
100
|
-
*
|
|
102
|
+
* the capture card's rolling window (CAPTURE_HEALTH_WINDOW_DAYS) —
|
|
103
|
+
* older-than-window bundles simply have no rows and no dot.
|
|
101
104
|
*/
|
|
102
105
|
function readFleetLastSeen(db) {
|
|
103
106
|
const rows = db
|
package/dist/consolidate.d.ts
CHANGED
|
@@ -328,6 +328,25 @@ export declare function stageDecayPrune(db: Database.Database, dryRun: boolean):
|
|
|
328
328
|
pruned: number;
|
|
329
329
|
failed: number;
|
|
330
330
|
};
|
|
331
|
+
/**
|
|
332
|
+
* ONE application of the #448 promotion formula:
|
|
333
|
+
* `S + PROMOTION_RATE × (1 − S / IMPORTANCE_CEILING) × S^(−0.3)`, clamped at
|
|
334
|
+
* the ceiling. Pure + exported for exact-value tests; the stage applies it
|
|
335
|
+
* iterated once per new use — iteration is what makes the ceiling asymptotic
|
|
336
|
+
* (a count-sized single shot would overshoot it).
|
|
337
|
+
*
|
|
338
|
+
* The formula's S input is floored at PROMOTION_STRENGTH_FLOOR (#453 review,
|
|
339
|
+
* owner decision 2026-09-16): S^(−0.3) has a zero-singularity, and a writable
|
|
340
|
+
* base_strength of 0.0 would otherwise leap to the ceiling via Infinity→clamp
|
|
341
|
+
* on a single access. The floor bounds the first boost at ≈+0.13; it clamps
|
|
342
|
+
* the INPUT only — the stored 0.0 row is not rewritten, and the promoted
|
|
343
|
+
* result is nonzero from then on.
|
|
344
|
+
*/
|
|
345
|
+
export declare function applyStrengthPromotion(baseStrength: number): number;
|
|
346
|
+
export declare function stagePromotion(db: Database.Database, dryRun: boolean): {
|
|
347
|
+
promoted: number;
|
|
348
|
+
demoted_skipped: number;
|
|
349
|
+
};
|
|
331
350
|
/**
|
|
332
351
|
* Default soft cap on the memory corpus (#245). Above this the lowest-value
|
|
333
352
|
* memories are evicted each nightly. 10000 balances headroom for a busy
|
package/dist/consolidate.js
CHANGED
|
@@ -54,6 +54,8 @@ exports.buildSupersessionPrompt = buildSupersessionPrompt;
|
|
|
54
54
|
exports.parseSupersessionReply = parseSupersessionReply;
|
|
55
55
|
exports.stageSupersession = stageSupersession;
|
|
56
56
|
exports.stageDecayPrune = stageDecayPrune;
|
|
57
|
+
exports.applyStrengthPromotion = applyStrengthPromotion;
|
|
58
|
+
exports.stagePromotion = stagePromotion;
|
|
57
59
|
exports.resolveMemorySoftCap = resolveMemorySoftCap;
|
|
58
60
|
exports.stageMemoryCapEviction = stageMemoryCapEviction;
|
|
59
61
|
exports.runConsolidation = runConsolidation;
|
|
@@ -1018,37 +1020,6 @@ function classifyRelationship(source, target, similarity) {
|
|
|
1018
1020
|
return "relates_to";
|
|
1019
1021
|
}
|
|
1020
1022
|
// ---------------------------------------------------------------------------
|
|
1021
|
-
// Stage 3.5: Hub Detection & Strength Boost
|
|
1022
|
-
// ---------------------------------------------------------------------------
|
|
1023
|
-
const HUB_BOOST = 0.1;
|
|
1024
|
-
// #425: the release-managed importance ceiling (calibration.ts) — hub boosts
|
|
1025
|
-
// can no longer push a row to base 1.0 (decay rate exactly 1.0 = immortal).
|
|
1026
|
-
const HUB_STRENGTH_CAP = CALIBRATION.IMPORTANCE_CEILING;
|
|
1027
|
-
function stageHubBoost(db, dryRun) {
|
|
1028
|
-
const hubs = (0, graph_js_1.detectHubs)(db);
|
|
1029
|
-
if (hubs.length === 0)
|
|
1030
|
-
return { hubs_found: 0, boosted: 0 };
|
|
1031
|
-
let boosted = 0;
|
|
1032
|
-
if (!dryRun) {
|
|
1033
|
-
const stmt = db.prepare("UPDATE memories SET base_strength = MIN(?, base_strength + ?) WHERE id = ? AND base_strength < ?");
|
|
1034
|
-
const tx = db.transaction(() => {
|
|
1035
|
-
for (const hub of hubs) {
|
|
1036
|
-
const result = stmt.run(HUB_STRENGTH_CAP, HUB_BOOST, hub.id, HUB_STRENGTH_CAP);
|
|
1037
|
-
if (result.changes > 0)
|
|
1038
|
-
boosted++;
|
|
1039
|
-
}
|
|
1040
|
-
});
|
|
1041
|
-
tx();
|
|
1042
|
-
}
|
|
1043
|
-
else {
|
|
1044
|
-
boosted = hubs.length;
|
|
1045
|
-
}
|
|
1046
|
-
if (hubs.length > 0) {
|
|
1047
|
-
console.log(`[hicortex] Hub detection: ${hubs.length} hubs found, ${boosted} boosted (+${HUB_BOOST})`);
|
|
1048
|
-
}
|
|
1049
|
-
return { hubs_found: hubs.length, boosted };
|
|
1050
|
-
}
|
|
1051
|
-
// ---------------------------------------------------------------------------
|
|
1052
1023
|
// Stage 3.7: Supersession Detection (#191 Phase B)
|
|
1053
1024
|
// ---------------------------------------------------------------------------
|
|
1054
1025
|
//
|
|
@@ -1327,16 +1298,11 @@ function stageDecayPrune(db, dryRun) {
|
|
|
1327
1298
|
const now = new Date();
|
|
1328
1299
|
const cutoff = new Date(now.getTime() - CONSOLIDATE_PRUNE_MIN_AGE_DAYS * 24 * 60 * 60 * 1000);
|
|
1329
1300
|
const oldUnaccessed = storage.getPruneCandidates(db, cutoff.toISOString());
|
|
1330
|
-
const linkCounts = storage.getAllLinkCounts(db);
|
|
1331
1301
|
let candidates = 0;
|
|
1332
1302
|
let pruned = 0;
|
|
1333
1303
|
let failed = 0;
|
|
1334
1304
|
for (const mem of oldUnaccessed) {
|
|
1335
|
-
const
|
|
1336
|
-
const eff = (0, retrieval_js_1.effectiveStrength)(mem.base_strength ?? 0.5, mem.last_accessed, now, {
|
|
1337
|
-
accessCount: 0,
|
|
1338
|
-
linkCount: memLinkCount,
|
|
1339
|
-
});
|
|
1305
|
+
const eff = (0, retrieval_js_1.effectiveStrength)(mem.base_strength ?? 0.5, mem.last_accessed, now);
|
|
1340
1306
|
if (eff >= 0.01)
|
|
1341
1307
|
continue;
|
|
1342
1308
|
candidates++;
|
|
@@ -1353,6 +1319,103 @@ function stageDecayPrune(db, dryRun) {
|
|
|
1353
1319
|
return { candidates, pruned, failed };
|
|
1354
1320
|
}
|
|
1355
1321
|
// ---------------------------------------------------------------------------
|
|
1322
|
+
// Stage: Strength promotion (#448) — the strength model's upward path
|
|
1323
|
+
// ---------------------------------------------------------------------------
|
|
1324
|
+
//
|
|
1325
|
+
// Real use of a memory (a surfaced /search or /recent hit, a hicortex_get)
|
|
1326
|
+
// bumps access_count (storage.strengthenMemory). This stage converts that use
|
|
1327
|
+
// signal into STRENGTH: every run, each memory whose access_count exceeds its
|
|
1328
|
+
// stored baseline (promotion_last_count, migration v20 — backfilled to
|
|
1329
|
+
// access_count at upgrade so lifetime counts are never replayed) is promoted
|
|
1330
|
+
// once per new use by the calibration formula, capped at the importance
|
|
1331
|
+
// ceiling. Decay stays untouched and uniform (#448 removed the hardening
|
|
1332
|
+
// terms) — promotion is the only way use raises a score.
|
|
1333
|
+
//
|
|
1334
|
+
// Deterministic (zero LLM) and idempotent: the baseline advances to
|
|
1335
|
+
// access_count in the same write, so a delta-less night is a no-op and a
|
|
1336
|
+
// crashed run replays exactly the unconsumed delta. The demotion set
|
|
1337
|
+
// (findDemotedIds: superseded/retracted rows + superseded_by-link sources)
|
|
1338
|
+
// plus status 'absorbed' rows (named explicitly — findDemotedIds excludes
|
|
1339
|
+
// them by design; they are filtered at candidacy elsewhere, but this stage
|
|
1340
|
+
// reads the memories table directly) get NO bump — hidden evidence must not
|
|
1341
|
+
// rise — but their baseline still advances, so a later un-mark never replays
|
|
1342
|
+
// a stale delta.
|
|
1343
|
+
/**
|
|
1344
|
+
* ONE application of the #448 promotion formula:
|
|
1345
|
+
* `S + PROMOTION_RATE × (1 − S / IMPORTANCE_CEILING) × S^(−0.3)`, clamped at
|
|
1346
|
+
* the ceiling. Pure + exported for exact-value tests; the stage applies it
|
|
1347
|
+
* iterated once per new use — iteration is what makes the ceiling asymptotic
|
|
1348
|
+
* (a count-sized single shot would overshoot it).
|
|
1349
|
+
*
|
|
1350
|
+
* The formula's S input is floored at PROMOTION_STRENGTH_FLOOR (#453 review,
|
|
1351
|
+
* owner decision 2026-09-16): S^(−0.3) has a zero-singularity, and a writable
|
|
1352
|
+
* base_strength of 0.0 would otherwise leap to the ceiling via Infinity→clamp
|
|
1353
|
+
* on a single access. The floor bounds the first boost at ≈+0.13; it clamps
|
|
1354
|
+
* the INPUT only — the stored 0.0 row is not rewritten, and the promoted
|
|
1355
|
+
* result is nonzero from then on.
|
|
1356
|
+
*/
|
|
1357
|
+
function applyStrengthPromotion(baseStrength) {
|
|
1358
|
+
const s = Math.max(baseStrength, CALIBRATION.PROMOTION_STRENGTH_FLOOR);
|
|
1359
|
+
return Math.min(s +
|
|
1360
|
+
CALIBRATION.PROMOTION_RATE *
|
|
1361
|
+
(1 - s / CALIBRATION.IMPORTANCE_CEILING) *
|
|
1362
|
+
Math.pow(s, -0.3), CALIBRATION.IMPORTANCE_CEILING);
|
|
1363
|
+
}
|
|
1364
|
+
function stagePromotion(db, dryRun) {
|
|
1365
|
+
// Candidates: rows with at least one use since their stored baseline.
|
|
1366
|
+
// COALESCE treats a NULL baseline as 0 — dup-over-loss: an unknown baseline
|
|
1367
|
+
// promotes once, a held one never forgets a use.
|
|
1368
|
+
const rows = db
|
|
1369
|
+
.prepare(`SELECT id, base_strength, access_count, status,
|
|
1370
|
+
access_count - COALESCE(promotion_last_count, 0) AS delta
|
|
1371
|
+
FROM memories
|
|
1372
|
+
WHERE access_count > COALESCE(promotion_last_count, 0)`)
|
|
1373
|
+
.all();
|
|
1374
|
+
if (rows.length === 0)
|
|
1375
|
+
return { promoted: 0, demoted_skipped: 0 };
|
|
1376
|
+
const demoted = (0, retrieval_js_1.findDemotedIds)(db, rows.map((r) => r.id));
|
|
1377
|
+
let promoted = 0;
|
|
1378
|
+
let demotedSkipped = 0;
|
|
1379
|
+
const writes = [];
|
|
1380
|
+
for (const row of rows) {
|
|
1381
|
+
const accessCount = row.access_count ?? 0;
|
|
1382
|
+
// Absorbed rows are invisible to recall but sit in the memories table —
|
|
1383
|
+
// they are demotion-set members this query CAN see (findDemotedIds
|
|
1384
|
+
// deliberately excludes them), so name them here.
|
|
1385
|
+
if (demoted.has(row.id) || row.status === "absorbed") {
|
|
1386
|
+
demotedSkipped++;
|
|
1387
|
+
writes.push({ id: row.id, fields: { promotion_last_count: accessCount } });
|
|
1388
|
+
continue;
|
|
1389
|
+
}
|
|
1390
|
+
// base_strength is NOT NULL after scoring; the `?? 0.5` mirrors
|
|
1391
|
+
// stageDecayPrune's defensive default for unscored rows (inserts at 0.5).
|
|
1392
|
+
let strength = row.base_strength ?? 0.5;
|
|
1393
|
+
for (let i = 0; i < row.delta; i++)
|
|
1394
|
+
strength = applyStrengthPromotion(strength);
|
|
1395
|
+
promoted++;
|
|
1396
|
+
writes.push({
|
|
1397
|
+
id: row.id,
|
|
1398
|
+
fields: { base_strength: strength, promotion_last_count: accessCount },
|
|
1399
|
+
});
|
|
1400
|
+
}
|
|
1401
|
+
if (dryRun) {
|
|
1402
|
+
console.log(`[hicortex] Strength promotion (dry-run): would promote ${promoted} memories ` +
|
|
1403
|
+
`(${demotedSkipped} demotion-set rows advance their baseline only).`);
|
|
1404
|
+
return { promoted, demoted_skipped: demotedSkipped };
|
|
1405
|
+
}
|
|
1406
|
+
// One transaction for the whole stage (the stageMemoryCapEviction pattern):
|
|
1407
|
+
// strength + baseline move together or not at all — a baseline that
|
|
1408
|
+
// advanced without its bump (or vice versa) would lose or replay a use.
|
|
1409
|
+
const tx = db.transaction(() => {
|
|
1410
|
+
for (const w of writes)
|
|
1411
|
+
storage.updateMemory(db, w.id, w.fields);
|
|
1412
|
+
});
|
|
1413
|
+
tx();
|
|
1414
|
+
console.log(`[hicortex] Strength promotion: promoted ${promoted} memories ` +
|
|
1415
|
+
`(${demotedSkipped} demotion-set rows advance their baseline only).`);
|
|
1416
|
+
return { promoted, demoted_skipped: demotedSkipped };
|
|
1417
|
+
}
|
|
1418
|
+
// ---------------------------------------------------------------------------
|
|
1356
1419
|
// Stage 4.5: Memory cap eviction (#245)
|
|
1357
1420
|
// ---------------------------------------------------------------------------
|
|
1358
1421
|
//
|
|
@@ -1365,10 +1428,13 @@ function stageDecayPrune(db, dryRun) {
|
|
|
1365
1428
|
//
|
|
1366
1429
|
// Eviction reuses the SAME effectiveStrength() the recall ranker uses — no
|
|
1367
1430
|
// formula duplication, so the eviction criterion cannot drift from what
|
|
1368
|
-
// surfaces in the top-k.
|
|
1369
|
-
//
|
|
1370
|
-
//
|
|
1371
|
-
//
|
|
1431
|
+
// surfaces in the top-k. #448: the ordering is base + recency ONLY (the
|
|
1432
|
+
// access/link hardening terms are gone), and a row used since the last run
|
|
1433
|
+
// was promoted by stagePromotion earlier in the SAME run — a promoted row
|
|
1434
|
+
// survives a cap it would otherwise have lost. The evicted tail is, by
|
|
1435
|
+
// construction, the tail that was not surfacing anyway (cold, decayed). Ties
|
|
1436
|
+
// are broken by oldest COALESCE(last_accessed, created_at) — i.e. the
|
|
1437
|
+
// memories that have gone longest without anyone looking at them.
|
|
1372
1438
|
//
|
|
1373
1439
|
// `cap = 0` disables the stage (indefinite growth — the pre-#245 default is
|
|
1374
1440
|
// preserved opt-out). The JS-side sort is O(n log n); at 10K memories the
|
|
@@ -1440,19 +1506,15 @@ function stageMemoryCapEviction(db, dryRun, cap) {
|
|
|
1440
1506
|
// never-shown memories sort by when they entered the corpus. Same
|
|
1441
1507
|
// non-absorbed predicate as the count above.
|
|
1442
1508
|
const rows = db
|
|
1443
|
-
.prepare(`SELECT id, base_strength, last_accessed,
|
|
1509
|
+
.prepare(`SELECT id, base_strength, last_accessed, created_at
|
|
1444
1510
|
FROM memories
|
|
1445
1511
|
WHERE COALESCE(status, '') != 'absorbed'`)
|
|
1446
1512
|
.all();
|
|
1447
|
-
const linkCounts = storage.getAllLinkCounts(db);
|
|
1448
1513
|
const now = new Date();
|
|
1449
1514
|
// Decorate + sort: lowest effectiveStrength first; ties broken by oldest
|
|
1450
1515
|
// COALESCE(last_accessed, created_at). The victims are the first `surplus`.
|
|
1451
1516
|
const decorated = rows.map((r) => {
|
|
1452
|
-
const eff = (0, retrieval_js_1.effectiveStrength)(r.base_strength ?? 0.5, r.last_accessed, now
|
|
1453
|
-
accessCount: r.access_count ?? 0,
|
|
1454
|
-
linkCount: linkCounts.get(r.id) ?? 0,
|
|
1455
|
-
});
|
|
1517
|
+
const eff = (0, retrieval_js_1.effectiveStrength)(r.base_strength ?? 0.5, r.last_accessed, now);
|
|
1456
1518
|
return {
|
|
1457
1519
|
id: r.id,
|
|
1458
1520
|
eff,
|
|
@@ -1617,6 +1679,13 @@ deadline) {
|
|
|
1617
1679
|
new_memory_count: precheck.newMemories.length,
|
|
1618
1680
|
unscored_count: scoreMemories.length - precheck.newMemories.length,
|
|
1619
1681
|
};
|
|
1682
|
+
// Strength promotion (#448) — runs in the pre-skip deterministic zone, in
|
|
1683
|
+
// the same placement discipline as memory_cap BELOW it: accesses happen on
|
|
1684
|
+
// quiet nights too (no new memories must still promote the day's uses),
|
|
1685
|
+
// and a used row must be promoted BEFORE eviction victims are chosen (a
|
|
1686
|
+
// promoted row survives a cap it would otherwise lose). Zero LLM — it sits
|
|
1687
|
+
// before the BudgetTracker exists, ungated by budget and deadline.
|
|
1688
|
+
report.stages.promotion = stagePromotion(db, dryRun);
|
|
1620
1689
|
// Memory cap eviction (#245) — runs BEFORE the precheck skip so the corpus
|
|
1621
1690
|
// is bounded even on quiet nights (no new memories → precheck would skip,
|
|
1622
1691
|
// but the cap stage is pure DB: cheap, idempotent when under cap).
|
|
@@ -1689,10 +1758,6 @@ deadline) {
|
|
|
1689
1758
|
if (!deadline?.hit("links")) {
|
|
1690
1759
|
report.stages.links = await stageLinks(db, precheck.newMemories, embedFn, dryRun, llm, budget);
|
|
1691
1760
|
}
|
|
1692
|
-
// Stage 3.5: Hub Detection — boost highly-connected memories
|
|
1693
|
-
if (!deadline?.hit("hub_boost")) {
|
|
1694
|
-
report.stages.hub_boost = stageHubBoost(db, dryRun);
|
|
1695
|
-
}
|
|
1696
1761
|
// Stage 3.7: Supersession Detection (#191 Phase B)
|
|
1697
1762
|
if (!deadline?.hit("supersession")) {
|
|
1698
1763
|
report.stages.supersession = await stageSupersession(db, llm, budget, embedFn, dryRun, stateDir, { ...supersessionOptions, deadline });
|
package/dist/dashboard.d.ts
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
*/
|
|
23
23
|
import type express from "express";
|
|
24
24
|
import type Database from "better-sqlite3";
|
|
25
|
+
import { type CaptureHealthRow } from "./capture-health.js";
|
|
25
26
|
import { type Stage } from "./stages.js";
|
|
26
27
|
/** Corpus-shape snapshot. `adoption` is null in backfilled rows (point-in-time,
|
|
27
28
|
* can't be reconstructed from created_at). */
|
|
@@ -202,7 +203,7 @@ export interface DashboardData {
|
|
|
202
203
|
period_start: string | null;
|
|
203
204
|
};
|
|
204
205
|
};
|
|
205
|
-
range: "7d" | "30d" | "90d" | "all";
|
|
206
|
+
range: "7d" | "30d" | "90d" | "180d" | "all";
|
|
206
207
|
series: DashboardSnapshot[];
|
|
207
208
|
composition: {
|
|
208
209
|
by_type: Record<string, number>;
|
|
@@ -213,22 +214,28 @@ export interface DashboardData {
|
|
|
213
214
|
};
|
|
214
215
|
/**
|
|
215
216
|
* #422 Phase 2 — capture health: per machine × agent /distill outcome
|
|
216
|
-
* accounting
|
|
217
|
-
* held / retried). ALWAYS present; {day: null, rows: []} when nothing is
|
|
217
|
+
* accounting. ALWAYS present; {day: null, rows: []} when nothing is
|
|
218
218
|
* recorded (fresh install / all rows pruned — the page degrades to the
|
|
219
219
|
* phase-1 counts-only rows).
|
|
220
|
+
*
|
|
221
|
+
* #409 fix round 7 (owner ruling 2026-09-14: "the normal 30 day as the
|
|
222
|
+
* other cards"): `day`/`rows` keep the NEWEST night with rows (secondary
|
|
223
|
+
* info — the card's "tonight" suffix), while `window_days`/`window_rows`
|
|
224
|
+
* carry the ROLLING capture window's per machine × agent sums — the card's
|
|
225
|
+
* face. Both derive from the same distill_activity rows and the same
|
|
226
|
+
* CAPTURE_HEALTH_WINDOW_DAYS constant (retention == window). window_rows
|
|
227
|
+
* is [] exactly when rows is (same table) — the counts fallback then still
|
|
228
|
+
* applies. A pre-round-7 server sends neither window key; the page guards.
|
|
220
229
|
*/
|
|
221
230
|
capture_health: {
|
|
222
231
|
day: string | null;
|
|
223
|
-
rows:
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
retried: number;
|
|
231
|
-
}>;
|
|
232
|
+
rows: CaptureHealthRow[];
|
|
233
|
+
/** Echoed window length (the page renders "last N days" from it — never
|
|
234
|
+
* hardcoded client-side). */
|
|
235
|
+
window_days: number;
|
|
236
|
+
/** Per machine × agent sums over the rolling window (posts / sessions /
|
|
237
|
+
* bytes / held / retried), bytes DESC. */
|
|
238
|
+
window_rows: CaptureHealthRow[];
|
|
232
239
|
};
|
|
233
240
|
/**
|
|
234
241
|
* #423 phase 3 — fleet presence: the operator's capture pauses + per-bundle
|
|
@@ -565,7 +572,10 @@ export declare function handleDashboardField(db: Database.Database): {
|
|
|
565
572
|
export declare function dashboardFieldHandler(getDb: () => Database.Database): express.RequestHandler;
|
|
566
573
|
/** One night of the replay ledger. `by_agent` keys are source_agent strings
|
|
567
574
|
* ("(unknown)" when the memory row is gone); learned counts added, enriched
|
|
568
|
-
* counts distinct enriched memories, merged counts dedup losers
|
|
575
|
+
* counts distinct enriched memories, merged counts dedup losers, and the
|
|
576
|
+
* #452 truth-management keys count superseded/retracted sources and
|
|
577
|
+
* reconsolidation rewrites (agent = the OLD memory's source — the event
|
|
578
|
+
* belongs to the memory that was demoted). */
|
|
569
579
|
export interface DashboardEventsNight {
|
|
570
580
|
date: string;
|
|
571
581
|
added: string[];
|
|
@@ -578,10 +588,25 @@ export interface DashboardEventsNight {
|
|
|
578
588
|
a: string;
|
|
579
589
|
b: string;
|
|
580
590
|
}>;
|
|
591
|
+
/** #452 — superseded_by links born that night ({old, by} ids). */
|
|
592
|
+
superseded: Array<{
|
|
593
|
+
old: string;
|
|
594
|
+
by: string;
|
|
595
|
+
}>;
|
|
596
|
+
/** #452 — corrected_by links whose source is currently status='retracted'. */
|
|
597
|
+
retracted: Array<{
|
|
598
|
+
old: string;
|
|
599
|
+
by: string;
|
|
600
|
+
}>;
|
|
601
|
+
/** #452 — distinct memories reconsolidation rewrote that night. */
|
|
602
|
+
rewritten: string[];
|
|
581
603
|
by_agent: Record<string, {
|
|
582
604
|
learned: number;
|
|
583
605
|
enriched: number;
|
|
584
606
|
merged: number;
|
|
607
|
+
superseded: number;
|
|
608
|
+
retracted: number;
|
|
609
|
+
rewritten: number;
|
|
585
610
|
}>;
|
|
586
611
|
}
|
|
587
612
|
/** The /dashboard/events response. */
|
|
@@ -589,9 +614,9 @@ export interface DashboardEvents {
|
|
|
589
614
|
nights: DashboardEventsNight[];
|
|
590
615
|
}
|
|
591
616
|
/**
|
|
592
|
-
* The pure data handler for GET /dashboard/events?days=N. Buckets the
|
|
593
|
-
*
|
|
594
|
-
*
|
|
617
|
+
* The pure data handler for GET /dashboard/events?days=N. Buckets the event
|
|
618
|
+
* sources into UTC nights (only nights with events, ascending) within the
|
|
619
|
+
* last `days` calendar days (today inclusive).
|
|
595
620
|
*/
|
|
596
621
|
export declare function handleDashboardEvents(db: Database.Database, query: {
|
|
597
622
|
days?: unknown;
|