@gamaze/hicortex 0.20.9 → 0.21.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/README.md +8 -0
- package/assets/dashboard.html +4174 -835
- package/dist/calibration.d.ts +119 -0
- package/dist/calibration.js +149 -1
- 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 +9 -0
- package/dist/capture.js +2 -1
- package/dist/cli.js +36 -0
- package/dist/consolidate.d.ts +35 -0
- package/dist/consolidate.js +85 -9
- package/dist/dashboard.d.ts +322 -3
- package/dist/dashboard.js +592 -7
- package/dist/db.js +105 -0
- package/dist/eval/importance-eval.d.ts +85 -0
- package/dist/eval/importance-eval.js +286 -0
- package/dist/eval/planted-fixtures.d.ts +1 -1
- 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/init.d.ts +14 -0
- package/dist/init.js +32 -0
- package/dist/mcp-server.d.ts +12 -0
- package/dist/mcp-server.js +184 -3
- package/dist/nightly.d.ts +9 -1
- package/dist/nightly.js +59 -7
- package/dist/prompts.d.ts +10 -0
- package/dist/prompts.js +28 -5
- package/dist/reconsolidation.d.ts +59 -30
- package/dist/reconsolidation.js +526 -296
- package/dist/rescore-importance.d.ts +80 -0
- package/dist/rescore-importance.js +236 -0
- package/dist/retrieval.d.ts +12 -0
- package/dist/retrieval.js +30 -1
- package/dist/stages.d.ts +37 -0
- package/dist/stages.js +51 -0
- package/dist/state.d.ts +32 -6
- package/dist/storage.d.ts +34 -2
- package/dist/storage.js +63 -6
- package/dist/types.d.ts +48 -0
- package/package.json +3 -1
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Operator capture pause (#423 phase 3, D3) — the server-side 200-skip.
|
|
4
|
+
*
|
|
5
|
+
* A pause is a row in `capture_pauses` (migration v18): machine × harness →
|
|
6
|
+
* paused_at. A row EXISTS = paused for that bundle; the /distill handler
|
|
7
|
+
* reads the table per POST, so a pause takes effect on the very next post —
|
|
8
|
+
* no restart — and answers 200 {skipped: true, paused: true}. The 200 is the
|
|
9
|
+
* whole point: capture.ts treats every 200 as confirmed and advances its
|
|
10
|
+
* cursor, so sessions that arrive while paused are deliberately NOT captured
|
|
11
|
+
* and are never re-sent or backfilled. Zero client changes.
|
|
12
|
+
*
|
|
13
|
+
* THE BUNDLE KEY. The pause's (machine, harness) must derive EXACTLY like
|
|
14
|
+
* the traffic it gates: machine via storage.sanitizeSourceMachine ('' when
|
|
15
|
+
* absent) and harness via harnessOfAgent below — the same normalization
|
|
16
|
+
* recordDistillActivity applies when it writes distill_activity, and the same
|
|
17
|
+
* "harness/profile" → "harness" split the console groups its bundles on. A
|
|
18
|
+
* key derived any other way would never match and the pause would silently
|
|
19
|
+
* not fire.
|
|
20
|
+
*
|
|
21
|
+
* LAST-SEEN (presence dots) derives ONLY from /distill activity — the one
|
|
22
|
+
* per-agent-identified traffic the server sees. Recall traffic (/search,
|
|
23
|
+
* /recall-index, /memory) carries no agent/machine identity on the wire, so
|
|
24
|
+
* attributing it would need new client fields — the heartbeats the spec
|
|
25
|
+
* forbids. Thresholds (green ≤36h — a nightly poster reads online through
|
|
26
|
+
* the following day; amber ≤7d — the distill_activity retention window; none
|
|
27
|
+
* beyond or with no rows) are page-side presentation; this module just
|
|
28
|
+
* reports the newest ts per bundle.
|
|
29
|
+
*
|
|
30
|
+
* Pure, unit-testable without express (the capture-health.ts layering):
|
|
31
|
+
* mcp-server.ts and dashboard.ts wire these functions to the live db.
|
|
32
|
+
*/
|
|
33
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
34
|
+
exports.harnessOfAgent = harnessOfAgent;
|
|
35
|
+
exports.capturePauseKey = capturePauseKey;
|
|
36
|
+
exports.isCapturePaused = isCapturePaused;
|
|
37
|
+
exports.setCapturePause = setCapturePause;
|
|
38
|
+
exports.listCapturePauses = listCapturePauses;
|
|
39
|
+
exports.readFleetLastSeen = readFleetLastSeen;
|
|
40
|
+
const storage_js_1 = require("./storage.js");
|
|
41
|
+
/**
|
|
42
|
+
* Derive the harness from a source_agent wire value — the bundle split the
|
|
43
|
+
* console already uses ("claude-code/main" → "claude-code"): the part before
|
|
44
|
+
* the first '/' when a slash is present at index > 0, else the whole trimmed
|
|
45
|
+
* string, capped at 128. Non-string/blank → "unknown" (mirrors how
|
|
46
|
+
* recordDistillActivity stores the agent when absent).
|
|
47
|
+
*/
|
|
48
|
+
function harnessOfAgent(sourceAgent) {
|
|
49
|
+
if (typeof sourceAgent !== "string")
|
|
50
|
+
return "unknown";
|
|
51
|
+
const t = sourceAgent.trim();
|
|
52
|
+
if (t.length === 0)
|
|
53
|
+
return "unknown";
|
|
54
|
+
const slash = t.indexOf("/");
|
|
55
|
+
return (slash > 0 ? t.slice(0, slash) : t).slice(0, 128);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Normalize the raw /distill wire fields into the pause key. MUST match the
|
|
59
|
+
* recordDistillActivity normalization (machine '' when absent, agent
|
|
60
|
+
* 'unknown' when absent) and the console's bundle grouping
|
|
61
|
+
* ((machine||'')+'|'+harness) — see the module doc.
|
|
62
|
+
*/
|
|
63
|
+
function capturePauseKey(machine, sourceAgent) {
|
|
64
|
+
return {
|
|
65
|
+
machine: (0, storage_js_1.sanitizeSourceMachine)(machine) ?? "",
|
|
66
|
+
harness: harnessOfAgent(sourceAgent),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/** True when a pause row exists for the (machine, harness) bundle. */
|
|
70
|
+
function isCapturePaused(db, machine, harness) {
|
|
71
|
+
return (db
|
|
72
|
+
.prepare("SELECT 1 FROM capture_pauses WHERE machine = ? AND harness = ?")
|
|
73
|
+
.get(machine, harness) !== undefined);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Pause (upsert the row, timestamp now) or resume (delete it). Returns the
|
|
77
|
+
* persisted paused_at when pausing, null when resuming. No pruning, ever —
|
|
78
|
+
* see migration v18's provenance comment.
|
|
79
|
+
*/
|
|
80
|
+
function setCapturePause(db, machine, harness, paused) {
|
|
81
|
+
if (!paused) {
|
|
82
|
+
db.prepare("DELETE FROM capture_pauses WHERE machine = ? AND harness = ?").run(machine, harness);
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
const pausedAt = new Date().toISOString();
|
|
86
|
+
db.prepare("INSERT OR REPLACE INTO capture_pauses (machine, harness, paused_at) VALUES (?, ?, ?)").run(machine, harness, pausedAt);
|
|
87
|
+
return pausedAt;
|
|
88
|
+
}
|
|
89
|
+
/** Every paused bundle (newest first) — the dashboard fleet.pauses block. */
|
|
90
|
+
function listCapturePauses(db) {
|
|
91
|
+
return db
|
|
92
|
+
.prepare("SELECT machine, harness, paused_at FROM capture_pauses ORDER BY paused_at DESC")
|
|
93
|
+
.all();
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* The newest /distill activity per bundle: for each (machine, agent) take
|
|
97
|
+
* MAX(ts) with that latest row's outcome, derive the harness per agent, then
|
|
98
|
+
* merge same-bundle agents keeping the newest ts (one dot per bundle, not
|
|
99
|
+
* per profile). Reads only distill_activity, which the recorder prunes to
|
|
100
|
+
* 7 days — older-than-window bundles simply have no rows and no dot.
|
|
101
|
+
*/
|
|
102
|
+
function readFleetLastSeen(db) {
|
|
103
|
+
const rows = db
|
|
104
|
+
.prepare(`SELECT a.machine, a.agent, a.ts, a.outcome
|
|
105
|
+
FROM distill_activity a
|
|
106
|
+
JOIN (
|
|
107
|
+
SELECT machine, agent, MAX(ts) AS max_ts
|
|
108
|
+
FROM distill_activity
|
|
109
|
+
GROUP BY machine, agent
|
|
110
|
+
) latest
|
|
111
|
+
ON a.machine = latest.machine AND a.agent = latest.agent AND a.ts = latest.max_ts`)
|
|
112
|
+
.all();
|
|
113
|
+
const byBundle = new Map();
|
|
114
|
+
for (const r of rows) {
|
|
115
|
+
const key = `${r.machine}|${harnessOfAgent(r.agent)}`;
|
|
116
|
+
const prev = byBundle.get(key);
|
|
117
|
+
if (!prev || r.ts > prev.last_seen) {
|
|
118
|
+
byBundle.set(key, {
|
|
119
|
+
machine: r.machine,
|
|
120
|
+
harness: harnessOfAgent(r.agent),
|
|
121
|
+
last_seen: r.ts,
|
|
122
|
+
last_outcome: r.outcome,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return [...byBundle.values()].sort((a, b) => (a.last_seen < b.last_seen ? 1 : -1));
|
|
127
|
+
}
|
package/dist/capture.d.ts
CHANGED
|
@@ -57,6 +57,9 @@ export interface DistillBody {
|
|
|
57
57
|
source_agent_id?: string | null;
|
|
58
58
|
/** Client-declared topic/domain of the capturing agent. Provenance only. */
|
|
59
59
|
source_domain?: string | null;
|
|
60
|
+
/** Machine this capture ran on (#421 machine × harness): config
|
|
61
|
+
* `machineName` ?? os.hostname(), stamped by the nightly. */
|
|
62
|
+
source_machine?: string | null;
|
|
60
63
|
project: string;
|
|
61
64
|
session_id: string;
|
|
62
65
|
segment_id: string;
|
|
@@ -106,6 +109,12 @@ export interface CaptureOptions {
|
|
|
106
109
|
* `source_domain` provenance. Null when undeclared.
|
|
107
110
|
*/
|
|
108
111
|
sourceDomain?: string | null;
|
|
112
|
+
/**
|
|
113
|
+
* Machine stamp on every segment (#421 machine × harness): config
|
|
114
|
+
* `machineName` when set, else os.hostname() — resolved by the nightly
|
|
115
|
+
* caller. Null disables stamping.
|
|
116
|
+
*/
|
|
117
|
+
sourceMachine?: string | null;
|
|
109
118
|
/**
|
|
110
119
|
* The run-wide pipeline deadline (#405), checked BETWEEN segment POSTs —
|
|
111
120
|
* a boundary the per-session cursor discipline already guarantees is safe
|
package/dist/capture.js
CHANGED
|
@@ -204,7 +204,7 @@ async function postWithRateRetry(post, body) {
|
|
|
204
204
|
* re-paying the Retry-After ladder (#327).
|
|
205
205
|
*/
|
|
206
206
|
async function captureBatches(batches, opts) {
|
|
207
|
-
const { post, cursorStore, dryRun = false, segmentMaxChars = exports.SEGMENT_MAX_CHARS, sourceAgentId, sourceDomain, deadline } = opts;
|
|
207
|
+
const { post, cursorStore, dryRun = false, segmentMaxChars = exports.SEGMENT_MAX_CHARS, sourceAgentId, sourceDomain, sourceMachine, deadline } = opts;
|
|
208
208
|
let memoriesIngested = 0;
|
|
209
209
|
let sessionsSent = 0;
|
|
210
210
|
let hadTransientFailure = false;
|
|
@@ -276,6 +276,7 @@ async function captureBatches(batches, opts) {
|
|
|
276
276
|
source_agent: batch.sourceAgent ?? `claude-code/${batch.projectName}`,
|
|
277
277
|
source_agent_id: sourceAgentId ?? null,
|
|
278
278
|
source_domain: sourceDomain ?? null,
|
|
279
|
+
source_machine: sourceMachine ?? null,
|
|
279
280
|
project: batch.projectName,
|
|
280
281
|
session_id: batch.sessionId,
|
|
281
282
|
segment_id: `${genPrefix}${seg.segStart}-${seg.segEnd}${seg.idSuffix}`,
|
package/dist/cli.js
CHANGED
|
@@ -189,6 +189,36 @@ switch (command) {
|
|
|
189
189
|
});
|
|
190
190
|
break;
|
|
191
191
|
}
|
|
192
|
+
case "rescore-importance": {
|
|
193
|
+
// #425 — one-shot LLM backfill: re-judge the corpus under the
|
|
194
|
+
// re-anchored importance rubric. classify-domains shape (resumable
|
|
195
|
+
// cursor, --batch, --reset) + dedup discipline (dry-run default,
|
|
196
|
+
// --apply, DB backup before any write).
|
|
197
|
+
const args = process.argv.slice(3);
|
|
198
|
+
const intFlag = (name) => {
|
|
199
|
+
const idx = args.indexOf(name);
|
|
200
|
+
if (idx === -1)
|
|
201
|
+
return undefined;
|
|
202
|
+
const val = parseInt(args[idx + 1], 10);
|
|
203
|
+
if (isNaN(val)) {
|
|
204
|
+
console.error(`[hicortex] rescore-importance: ${name} requires an integer value`);
|
|
205
|
+
process.exit(1);
|
|
206
|
+
}
|
|
207
|
+
return val;
|
|
208
|
+
};
|
|
209
|
+
const rescoreOptions = {
|
|
210
|
+
apply: args.includes("--apply"),
|
|
211
|
+
reset: args.includes("--reset"),
|
|
212
|
+
batchSize: intFlag("--batch"),
|
|
213
|
+
};
|
|
214
|
+
import("./rescore-importance.js").then(({ runRescoreImportance }) => {
|
|
215
|
+
runRescoreImportance(rescoreOptions).catch((err) => {
|
|
216
|
+
console.error(err instanceof Error ? err.message : `[hicortex] rescore-importance failed: ${err}`);
|
|
217
|
+
process.exit(1);
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
192
222
|
case "classify-types": {
|
|
193
223
|
const args = process.argv.slice(3);
|
|
194
224
|
const intFlag = (name) => {
|
|
@@ -421,6 +451,8 @@ Commands:
|
|
|
421
451
|
backup Snapshot the DB + identity + state to a tar.gz (online, WAL-safe)
|
|
422
452
|
classify-domains Backfill content-based domain tags over the corpus (server mode, needs config.domains)
|
|
423
453
|
classify-types Backfill episode→fact/decision type tags over the corpus (server mode)
|
|
454
|
+
rescore-importance Re-judge all memories' importance under the current rubric
|
|
455
|
+
(server mode; dry run by default — --apply executes; resumable)
|
|
424
456
|
learnings-identity Fetch identity + lessons and print Markdown to stdout (CC SessionStart hook)
|
|
425
457
|
(alias: lessons-context — the pre-#264 name, kept for backcompat)
|
|
426
458
|
recall-hook Pushed recall index for the current prompt (CC UserPromptSubmit/SessionStart hook)
|
|
@@ -458,6 +490,10 @@ Options:
|
|
|
458
490
|
classify-types --all Reclassify every memory (default: only episodes)
|
|
459
491
|
classify-types --batch <n> Memories per batch (default: 200)
|
|
460
492
|
classify-types --reset Restart from the beginning (ignore saved cursor)
|
|
493
|
+
rescore-importance --apply Execute the importance backfill (default: dry run, report only)
|
|
494
|
+
Takes a DB backup first; resumable via a state.json cursor
|
|
495
|
+
rescore-importance --batch <n> Rows per invocation (default: 500; LLM calls are 10 rows each)
|
|
496
|
+
rescore-importance --reset Restart from the beginning (ignore saved cursor)
|
|
461
497
|
identity show [name] Print all identity sections, or just <name> (raw, pipeable)
|
|
462
498
|
identity edit <name> Edit a section in $EDITOR; PUT only if changed
|
|
463
499
|
identity … --agent <id> Target a per-agent scope instead of the global set
|
package/dist/consolidate.d.ts
CHANGED
|
@@ -120,6 +120,18 @@ export declare class BudgetTracker {
|
|
|
120
120
|
} | undefined): void;
|
|
121
121
|
summary(): NonNullable<ConsolidationReport["budget"]>;
|
|
122
122
|
}
|
|
123
|
+
/**
|
|
124
|
+
* #427 observability: warn when a consolidation run made LLM CALLS but
|
|
125
|
+
* metered ZERO tokens — the endpoint returned no usage objects on its
|
|
126
|
+
* completions (recordUsage skips undefined by design, never fabricates a
|
|
127
|
+
* zero). Such a run still spends budget calls but its snapshot carries token
|
|
128
|
+
* nulls, which read as a mystery on the dashboard. The warn is a structured
|
|
129
|
+
* event in the same journald-greppable style as `event=budget_exhausted`
|
|
130
|
+
* (grep `event=tokens_unmetered`), so the blind spot is visible instead of
|
|
131
|
+
* silent. Returns true when it warned (for tests); no fabrication either
|
|
132
|
+
* way — the numbers stay exactly what the endpoint reported.
|
|
133
|
+
*/
|
|
134
|
+
export declare function warnUnmeteredTokensRun(budget: NonNullable<ConsolidationReport["budget"]>): boolean;
|
|
123
135
|
/**
|
|
124
136
|
* True when a token-period start stamp is ABSENT or sits in a previous UTC
|
|
125
137
|
* calendar month than `now` — the monthly-reset staleness check. #405: ONE
|
|
@@ -155,6 +167,29 @@ export declare function shouldThrottleTokens(cap: number, period: {
|
|
|
155
167
|
* Parse JSON from LLM output, tolerating markdown fences and indexed formats.
|
|
156
168
|
*/
|
|
157
169
|
export declare function parseJsonLenient<T>(text: string, fallback: T): T;
|
|
170
|
+
/**
|
|
171
|
+
* The shared importance-scoring loop (#425 extraction): one LLM call per
|
|
172
|
+
* 10-memory batch through the production `importanceScoring` prompt, each
|
|
173
|
+
* written score clamped at IMPORTANCE_CEILING and stamped with the
|
|
174
|
+
* importance_scored_at watermark. Used by the nightly's stageImportance AND
|
|
175
|
+
* `hicortex rescore-importance` — there is exactly one scoring code path
|
|
176
|
+
* (no forked backfill logic; cap + watermark write identically everywhere).
|
|
177
|
+
*
|
|
178
|
+
* Failure semantics: a batch whose LLM call THROWS writes nothing (counted
|
|
179
|
+
* in `failed` — retried naturally later); a batch whose reply parses to a
|
|
180
|
+
* non-array falls back to 0.5 per memory (written + watermarked — the
|
|
181
|
+
* endpoint answered, the answer was unusable).
|
|
182
|
+
*/
|
|
183
|
+
export declare function scoreMemoriesImportance(db: Database.Database, memories: Memory[], llm: LlmClient, opts?: {
|
|
184
|
+
budget?: BudgetTracker;
|
|
185
|
+
deadline?: RunDeadline;
|
|
186
|
+
dryRun?: boolean;
|
|
187
|
+
onBatch?: (written: number, failed: number) => void;
|
|
188
|
+
}): Promise<{
|
|
189
|
+
scored: number;
|
|
190
|
+
failed: number;
|
|
191
|
+
skipped_budget: number;
|
|
192
|
+
}>;
|
|
158
193
|
/**
|
|
159
194
|
* Rebuild moduleIndex from the configured domain set + live DB counts, and
|
|
160
195
|
* persist it. Shared by the nightly stage and `hicortex classify-domains`.
|
package/dist/consolidate.js
CHANGED
|
@@ -41,9 +41,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
41
41
|
exports.DEFAULT_MEMORY_SOFT_CAP = exports.DEFAULT_SUPERSESSION_MIN_SIMILARITY = exports.BudgetTracker = exports.REFLECTION_CONTRADICTION_MIN_COSINE = exports.l2ToCosine = exports.CROSS_PROJECT_LINK_THRESHOLD = exports.CONSOLIDATE_LINK_TOP_K = exports.CONSOLIDATE_LINK_THRESHOLD = exports.DEFAULT_NIGHTLY_LLM_CALL_BUDGET = void 0;
|
|
42
42
|
exports.resolveNightlyLlmCallBudget = resolveNightlyLlmCallBudget;
|
|
43
43
|
exports.isContradictionCandidate = isContradictionCandidate;
|
|
44
|
+
exports.warnUnmeteredTokensRun = warnUnmeteredTokensRun;
|
|
44
45
|
exports.isStaleTokenPeriod = isStaleTokenPeriod;
|
|
45
46
|
exports.shouldThrottleTokens = shouldThrottleTokens;
|
|
46
47
|
exports.parseJsonLenient = parseJsonLenient;
|
|
48
|
+
exports.scoreMemoriesImportance = scoreMemoriesImportance;
|
|
47
49
|
exports.rebuildContentModuleIndex = rebuildContentModuleIndex;
|
|
48
50
|
exports.discoverLinkCandidates = discoverLinkCandidates;
|
|
49
51
|
exports.classifyLinkCandidates = classifyLinkCandidates;
|
|
@@ -240,6 +242,27 @@ class BudgetTracker {
|
|
|
240
242
|
}
|
|
241
243
|
}
|
|
242
244
|
exports.BudgetTracker = BudgetTracker;
|
|
245
|
+
/**
|
|
246
|
+
* #427 observability: warn when a consolidation run made LLM CALLS but
|
|
247
|
+
* metered ZERO tokens — the endpoint returned no usage objects on its
|
|
248
|
+
* completions (recordUsage skips undefined by design, never fabricates a
|
|
249
|
+
* zero). Such a run still spends budget calls but its snapshot carries token
|
|
250
|
+
* nulls, which read as a mystery on the dashboard. The warn is a structured
|
|
251
|
+
* event in the same journald-greppable style as `event=budget_exhausted`
|
|
252
|
+
* (grep `event=tokens_unmetered`), so the blind spot is visible instead of
|
|
253
|
+
* silent. Returns true when it warned (for tests); no fabrication either
|
|
254
|
+
* way — the numbers stay exactly what the endpoint reported.
|
|
255
|
+
*/
|
|
256
|
+
function warnUnmeteredTokensRun(budget) {
|
|
257
|
+
const calls = budget.calls_used ?? 0;
|
|
258
|
+
const tokens = budget.tokens_total?.total ?? 0;
|
|
259
|
+
if (calls <= 0 || tokens > 0)
|
|
260
|
+
return false;
|
|
261
|
+
console.warn(`[hicortex] event=tokens_unmetered calls_used=${calls} — the LLM endpoint ` +
|
|
262
|
+
`returned no usage objects on its completions; this run's snapshot carries ` +
|
|
263
|
+
`no token metering (budget calls were still counted).`);
|
|
264
|
+
return true;
|
|
265
|
+
}
|
|
243
266
|
// ---------------------------------------------------------------------------
|
|
244
267
|
// Token fair-use throttle decision (#246)
|
|
245
268
|
// ---------------------------------------------------------------------------
|
|
@@ -351,13 +374,29 @@ function stagePrecheck(db, stateDir) {
|
|
|
351
374
|
// ---------------------------------------------------------------------------
|
|
352
375
|
// Stage 2: Importance Scoring
|
|
353
376
|
// ---------------------------------------------------------------------------
|
|
354
|
-
|
|
377
|
+
/**
|
|
378
|
+
* The shared importance-scoring loop (#425 extraction): one LLM call per
|
|
379
|
+
* 10-memory batch through the production `importanceScoring` prompt, each
|
|
380
|
+
* written score clamped at IMPORTANCE_CEILING and stamped with the
|
|
381
|
+
* importance_scored_at watermark. Used by the nightly's stageImportance AND
|
|
382
|
+
* `hicortex rescore-importance` — there is exactly one scoring code path
|
|
383
|
+
* (no forked backfill logic; cap + watermark write identically everywhere).
|
|
384
|
+
*
|
|
385
|
+
* Failure semantics: a batch whose LLM call THROWS writes nothing (counted
|
|
386
|
+
* in `failed` — retried naturally later); a batch whose reply parses to a
|
|
387
|
+
* non-array falls back to 0.5 per memory (written + watermarked — the
|
|
388
|
+
* endpoint answered, the answer was unusable).
|
|
389
|
+
*/
|
|
390
|
+
async function scoreMemoriesImportance(db, memories, llm, opts = {}) {
|
|
355
391
|
const batchSize = 10;
|
|
392
|
+
const budget = opts.budget;
|
|
393
|
+
const deadline = opts.deadline;
|
|
394
|
+
const dryRun = opts.dryRun ?? false;
|
|
356
395
|
let scored = 0;
|
|
357
396
|
let failed = 0;
|
|
358
397
|
let skippedBudget = 0;
|
|
359
398
|
for (let i = 0; i < memories.length; i += batchSize) {
|
|
360
|
-
if (budget
|
|
399
|
+
if (budget?.exhausted) {
|
|
361
400
|
skippedBudget += memories.length - i;
|
|
362
401
|
break;
|
|
363
402
|
}
|
|
@@ -372,13 +411,13 @@ async function stageImportance(db, memories, llm, budget, dryRun, deadline) {
|
|
|
372
411
|
const prompt = (0, prompts_js_1.importanceScoring)(memoriesBlock);
|
|
373
412
|
if (dryRun)
|
|
374
413
|
continue;
|
|
375
|
-
if (!budget.use("importance")) {
|
|
414
|
+
if (budget && !budget.use("importance")) {
|
|
376
415
|
skippedBudget += memories.length - i;
|
|
377
416
|
break;
|
|
378
417
|
}
|
|
379
418
|
try {
|
|
380
419
|
const r = await llm.complete(prompt);
|
|
381
|
-
budget
|
|
420
|
+
budget?.recordUsage("importance", r.usage);
|
|
382
421
|
let scores = parseJsonLenient(r.text, null);
|
|
383
422
|
if (!Array.isArray(scores)) {
|
|
384
423
|
scores = new Array(batch.length).fill(0.5);
|
|
@@ -386,6 +425,8 @@ async function stageImportance(db, memories, llm, budget, dryRun, deadline) {
|
|
|
386
425
|
while (scores.length < batch.length)
|
|
387
426
|
scores.push(0.5);
|
|
388
427
|
scores = scores.slice(0, batch.length);
|
|
428
|
+
let batchWritten = 0;
|
|
429
|
+
let batchFailed = 0;
|
|
389
430
|
for (let j = 0; j < batch.length; j++) {
|
|
390
431
|
let scoreVal = 0.5;
|
|
391
432
|
try {
|
|
@@ -396,21 +437,37 @@ async function stageImportance(db, memories, llm, budget, dryRun, deadline) {
|
|
|
396
437
|
catch {
|
|
397
438
|
scoreVal = 0.5;
|
|
398
439
|
}
|
|
440
|
+
// #425: the write cap — importance exactly 1.0 has decay rate exactly
|
|
441
|
+
// 1.0 and never decays, so no row is ever born immortal. The scored-at
|
|
442
|
+
// watermark lands in the SAME update, taking the row out of the
|
|
443
|
+
// nightly's unscored pool however it scored (the pre-#425 0.5-sentinel
|
|
444
|
+
// re-rolled genuinely-0.5 rows every night).
|
|
445
|
+
scoreVal = Math.min(scoreVal, CALIBRATION.IMPORTANCE_CEILING);
|
|
399
446
|
try {
|
|
400
|
-
storage.updateMemory(db, batch[j].id, {
|
|
447
|
+
storage.updateMemory(db, batch[j].id, {
|
|
448
|
+
base_strength: scoreVal,
|
|
449
|
+
importance_scored_at: new Date().toISOString(),
|
|
450
|
+
});
|
|
401
451
|
scored++;
|
|
452
|
+
batchWritten++;
|
|
402
453
|
}
|
|
403
454
|
catch {
|
|
404
455
|
failed++;
|
|
456
|
+
batchFailed++;
|
|
405
457
|
}
|
|
406
458
|
}
|
|
459
|
+
opts.onBatch?.(batchWritten, batchFailed);
|
|
407
460
|
}
|
|
408
461
|
catch {
|
|
409
462
|
failed += batch.length;
|
|
463
|
+
opts.onBatch?.(0, batch.length);
|
|
410
464
|
}
|
|
411
465
|
}
|
|
412
466
|
return { scored, failed, skipped_budget: skippedBudget };
|
|
413
467
|
}
|
|
468
|
+
async function stageImportance(db, memories, llm, budget, dryRun, deadline) {
|
|
469
|
+
return scoreMemoriesImportance(db, memories, llm, { budget, deadline, dryRun });
|
|
470
|
+
}
|
|
414
471
|
// ---------------------------------------------------------------------------
|
|
415
472
|
// Stage 2.5: Reflection
|
|
416
473
|
// ---------------------------------------------------------------------------
|
|
@@ -964,7 +1021,9 @@ function classifyRelationship(source, target, similarity) {
|
|
|
964
1021
|
// Stage 3.5: Hub Detection & Strength Boost
|
|
965
1022
|
// ---------------------------------------------------------------------------
|
|
966
1023
|
const HUB_BOOST = 0.1;
|
|
967
|
-
|
|
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;
|
|
968
1027
|
function stageHubBoost(db, dryRun) {
|
|
969
1028
|
const hubs = (0, graph_js_1.detectHubs)(db);
|
|
970
1029
|
if (hubs.length === 0)
|
|
@@ -1362,7 +1421,15 @@ function stageMemoryCapEviction(db, dryRun, cap) {
|
|
|
1362
1421
|
// rejects negatives at the boundary, but this stage is callable directly).
|
|
1363
1422
|
if (cap <= 0)
|
|
1364
1423
|
return { cap, evicted: 0 };
|
|
1365
|
-
|
|
1424
|
+
// #422 (#317 discipline): the cap keys off LIVE (non-absorbed) rows on BOTH
|
|
1425
|
+
// the count and the victim SELECT — absorbed rows are invisible evidence
|
|
1426
|
+
// (no vector, no FTS, recall never serves them); they must neither consume
|
|
1427
|
+
// cap headroom nor be picked as eviction victims. The DISPLAYED headroom
|
|
1428
|
+
// (dashboard.ts headline live_memories vs memory_soft_cap) reads the same
|
|
1429
|
+
// predicate, so the enforced and displayed caps cannot disagree.
|
|
1430
|
+
const count = db
|
|
1431
|
+
.prepare("SELECT COUNT(*) AS c FROM memories WHERE COALESCE(status, '') != 'absorbed'")
|
|
1432
|
+
.get().c;
|
|
1366
1433
|
if (count <= cap)
|
|
1367
1434
|
return { cap, evicted: 0 };
|
|
1368
1435
|
const surplus = count - cap;
|
|
@@ -1370,10 +1437,12 @@ function stageMemoryCapEviction(db, dryRun, cap) {
|
|
|
1370
1437
|
// NOT NULL after scoring; the `?? 0.5` mirrors stageDecayPrune's defensive
|
|
1371
1438
|
// default for unscored rows (inserts at 0.5). last_accessed is NULL until
|
|
1372
1439
|
// first /recall-index exposure — COALESCE to created_at for the tiebreak so
|
|
1373
|
-
// never-shown memories sort by when they entered the corpus.
|
|
1440
|
+
// never-shown memories sort by when they entered the corpus. Same
|
|
1441
|
+
// non-absorbed predicate as the count above.
|
|
1374
1442
|
const rows = db
|
|
1375
1443
|
.prepare(`SELECT id, base_strength, last_accessed, access_count, created_at
|
|
1376
|
-
FROM memories
|
|
1444
|
+
FROM memories
|
|
1445
|
+
WHERE COALESCE(status, '') != 'absorbed'`)
|
|
1377
1446
|
.all();
|
|
1378
1447
|
const linkCounts = storage.getAllLinkCounts(db);
|
|
1379
1448
|
const now = new Date();
|
|
@@ -1469,6 +1538,13 @@ async function skippedRunResolutionReport(db, dryRun, stateDir, options = {}) {
|
|
|
1469
1538
|
explicit_verified: 0,
|
|
1470
1539
|
explicit_divergent: 0,
|
|
1471
1540
|
cursor: (0, state_js_1.loadState)(stateDir).reconsolidationCursor ?? 0,
|
|
1541
|
+
// #439 fields: zeros on a quiet night (no scan ran — nothing re-judged,
|
|
1542
|
+
// new, skipped, or deferred; the type carries them so the report surface
|
|
1543
|
+
// stays uniform).
|
|
1544
|
+
pairs_reevaluated: 0,
|
|
1545
|
+
pairs_new: 0,
|
|
1546
|
+
skipped_absorbed: 0,
|
|
1547
|
+
merge_pairs_deferred: 0,
|
|
1472
1548
|
merges,
|
|
1473
1549
|
merge_pairs_applied: 0,
|
|
1474
1550
|
merge_below_gate: 0,
|