@gamaze/hicortex 0.22.3 → 0.23.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/dist/distiller.js CHANGED
@@ -10,10 +10,12 @@ exports.extractConversationText = extractConversationText;
10
10
  exports.distillSession = distillSession;
11
11
  exports.isNoExtractResponse = isNoExtractResponse;
12
12
  exports.hasMinimalSubstance = hasMinimalSubstance;
13
+ exports.isVolatileStatusEntry = isVolatileStatusEntry;
13
14
  exports.typeFromTag = typeFromTag;
14
15
  exports.parseDistilledEntries = parseDistilledEntries;
15
16
  const prompts_js_1 = require("./prompts.js");
16
17
  const redact_js_1 = require("./redact.js");
18
+ const calibration_js_1 = require("./calibration.js");
17
19
  const MAX_TRANSCRIPT_CHARS = 80_000;
18
20
  const MIN_CONVERSATION_CHARS = 200;
19
21
  // #339 (2026-08-24 postmortem): NO_EXTRACT over-firing visibility threshold.
@@ -403,21 +405,36 @@ async function distillChunk(llm, transcript, projectName, date, onUsage) {
403
405
  console.log(`[hicortex] topic-first check: ${offTopic}/${parsed.length} entries look actor/bracket-led (prompt may be ignored)`);
404
406
  }
405
407
  const entries = [];
406
- const dropped = [];
408
+ const substanceDropped = [];
409
+ const volatileDropped = [];
407
410
  for (const entry of parsed) {
408
- if (hasMinimalSubstance(entry.content)) {
409
- entries.push(entry);
411
+ if (!hasMinimalSubstance(entry.content)) {
412
+ substanceDropped.push(entry.content);
413
+ continue;
410
414
  }
411
- else {
412
- dropped.push(entry.content);
415
+ // #489 volatility gate (owner decision 2: entry-level, deterministic,
416
+ // beside the substance gate — cleanMessageContent untouched): GH-ticket /
417
+ // version-bump / commit-state status shapes drop into the SAME #156
418
+ // trail (owner decision 1: mark-and-skip, never silent). Kill-switch =
419
+ // the release-managed calibration constant (owner decision 5).
420
+ if (calibration_js_1.VOLATILE_STATUS_FILTER && isVolatileStatusEntry(entry.content)) {
421
+ volatileDropped.push(entry.content);
422
+ continue;
413
423
  }
424
+ entries.push(entry);
414
425
  }
415
- if (dropped.length > 0) {
416
- for (const d of dropped) {
426
+ const dropped = [...substanceDropped, ...volatileDropped];
427
+ for (const [label, list] of [
428
+ ["Substance gate", substanceDropped],
429
+ ["Volatility gate", volatileDropped],
430
+ ]) {
431
+ if (list.length === 0)
432
+ continue;
433
+ for (const d of list) {
417
434
  const preview = d.length > 120 ? `${d.slice(0, 120)}…` : d;
418
- console.log(`[hicortex] Substance gate: dropped "${preview}"`);
435
+ console.log(`[hicortex] ${label}: dropped "${preview}"`);
419
436
  }
420
- console.log(`[hicortex] Substance gate: dropped ${dropped.length}/${parsed.length} content-free fragment(s)`);
437
+ console.log(`[hicortex] ${label}: dropped ${list.length}/${parsed.length} entr(ies)`);
421
438
  }
422
439
  // Parsed-zero bypass (#339 CR finding 2): a non-empty response with no
423
440
  // NO_EXTRACT token that still parses to zero bullets is the silent twin of
@@ -508,6 +525,74 @@ function hasMinimalSubstance(entry) {
508
525
  return false;
509
526
  return true;
510
527
  }
528
+ // ---------------------------------------------------------------------------
529
+ // Volatility gate (#489) — deterministic, entry-level, beside the substance
530
+ // gate. Owner directive (gold-set A adjudication): GitHub ticket/workflow
531
+ // states and version numbers churn faster than nightly consolidation retires
532
+ // them — they arrive as supersession noise and inflate the store with
533
+ // same-subject rows. The ephemera PROMPT already names these categories and
534
+ // demonstrably under-fires (a month of it running, gold A still full of
535
+ // GH-status/version rows) — so the filter is deterministic, catching entries
536
+ // synthesized from ANY source prose.
537
+ // ---------------------------------------------------------------------------
538
+ /**
539
+ * Durability escapes — decision/policy predicates that OVERRIDE every
540
+ * volatility trigger (#489 spec): "switched from X to Y" is a durable model
541
+ * choice even though it names two versions; a version boundary is a policy
542
+ * even though it cites one. Whole-word, case-insensitive. Version numbers
543
+ * never trigger alone — version + STATUS-verb triggers; version +
544
+ * DECISION-verb escapes.
545
+ */
546
+ const VOLATILE_ESCAPES = /\b(?:switch(?:ed)?\s+from|adopt(?:ed)?|standardi[sz]ed|deprecated|polic(?:y|ies)|rule|boundary|convention|must|never|always|only\s+when)\b/i;
547
+ /** T1 — GH ticket/workflow status: a #number ticket + a status predicate, or
548
+ * a workflow-run shape (checks/CI outcome, test-count status). */
549
+ const TICKET_REF = /(?:^|[\s(#])(?:pr|issue|epic)?\s*#\d+/i;
550
+ const TICKET_STATUS = /\b(?:merged?|closed?|reopened?|opened?|approved?|blocked?|failing|pass(?:ed|ing)?|green|red|ready|draft)\b/i;
551
+ const WORKFLOW_STATUS = /\b(?:checks?\s+(?:passed|failed|green|red)|ci\s+(?:green|red)|\d+\s+tests?\s+(?:pass(?:ed|ing)?|fail(?:ed|ing)?))\b/i;
552
+ /** T2 — version-bump status: a semver-ish number + a release verb, or a
553
+ * version→version transition. The `\b` boundary keeps "Qwen3.6" (no
554
+ * boundary between n and 3) from matching — model names are not versions. */
555
+ const SEMVERISH = /\bv?\d+\.\d+(?:\.\d+)?\b/;
556
+ const RELEASE_VERB = /\b(?:released?|deployed?|promoted?|published?|tagged?|bumped?|cut|shipped?|rolled?\s+out|dist-tags?)\b/i;
557
+ const VERSION_TRANSITION = /\bv?\d+\.\d+(?:\.\d+)?\b[^.\d]{0,20}(?:→|->|to)\s*v?\d+\.\d+(?:\.\d+)?\b/i;
558
+ /** T3 — branch/commit state: a short sha (7-40 hex, word-bounded) + a
559
+ * vcs-motion verb. The SHA is the discriminator, so the verb list is bare
560
+ * ("rebased feature branch onto 4f9c1ab" must fire); a durable narrative
561
+ * carrying a sha + verb rides the escape list or the length cap. */
562
+ const SHORT_SHA = /\b[0-9a-f]{7,40}\b/;
563
+ const VCS_STATE = /\b(?:pushed?|merged?|rebased?|force-?pushed?|updated?)\b/i;
564
+ /**
565
+ * True when the entry is a VOLATILE STATUS SHAPE — GH-ticket/workflow status,
566
+ * version-bump status, or branch/commit state — and carries no durability
567
+ * escape (#489, owner decisions 1+2). Runs in distillChunk's gate zone beside
568
+ * hasMinimalSubstance; drops ride the SAME #156 dropped[] trail. Also reused
569
+ * verbatim by `hicortex sweep-volatile` over the stored corpus — ONE gate,
570
+ * one meaning.
571
+ *
572
+ * PRECISION OVER RECALL (the substance gate's law, inherited): escapes are
573
+ * checked FIRST and override every trigger; entries longer than
574
+ * VOLATILE_GATE_MAX_CHARS are treated as mixed prose whose status clause is
575
+ * not the DOMINANT content (kept). Accepted false-positive mode, stated
576
+ * plainly: an entry pairing a status clause with a distinct durable clause
577
+ * and no escape word drops, losing the durable half — unless the distiller
578
+ * emitted that half as its own entry, which is exactly what the ephemera
579
+ * prompt tells it to do. Every drop is auditable via the trail.
580
+ */
581
+ function isVolatileStatusEntry(entry) {
582
+ const raw = entry.trim();
583
+ if (!raw || raw.length > calibration_js_1.VOLATILE_GATE_MAX_CHARS)
584
+ return false;
585
+ if (VOLATILE_ESCAPES.test(raw))
586
+ return false;
587
+ const ticketStatus = TICKET_REF.test(raw) && TICKET_STATUS.test(raw);
588
+ const workflowStatus = WORKFLOW_STATUS.test(raw);
589
+ // A version number + release verb, OR a bare version→version transition
590
+ // (the transition fires without a verb — "0.22.2 → 0.22.3 on rc" is a
591
+ // version-bump status; the escape list carries the durable flips).
592
+ const versionBump = (SEMVERISH.test(raw) && RELEASE_VERB.test(raw)) || VERSION_TRANSITION.test(raw);
593
+ const commitState = SHORT_SHA.test(raw) && VCS_STATE.test(raw);
594
+ return ticketStatus || workflowStatus || versionBump || commitState;
595
+ }
511
596
  /**
512
597
  * Map a single-letter type tag to the stored memory_type. Unknown/absent →
513
598
  * experience (the pre-#216 default). `[L]` is explicitly rejected →
@@ -644,7 +644,7 @@ function renderPlantedReport(args) {
644
644
  `- conflicts (guard-C): flagged ${s.conflict_flagged}, skipped ${s.conflict_skipped} ` +
645
645
  `(zone clusters + judged merges refused on a conflicts link)\n` +
646
646
  `- zone: clusters_found ${s.merges.clusters_found}, merged ${s.merges.merged_clusters}, ` +
647
- `losers_merged ${s.merges.losers_merged}, metadata_skipped ${s.merges.skipped_metadata_mismatch}, ` +
647
+ `losers_merged ${s.merges.losers_merged}, project_skipped ${s.merges.skipped_project_mismatch}, ` +
648
648
  `conflict_skipped ${s.merges.skipped_conflict}\n` +
649
649
  `- skipped: infra ${s.skipped_infra}, idempotent ${s.skipped_idempotent}, above_ceiling ${s.skipped_above_ceiling}\n`);
650
650
  return L.join("\n");
@@ -66,11 +66,9 @@ function renderDups(d) {
66
66
  `${d.pairAttribution.recoveryReingest} recovery/re-ingest-suspect (${pct(d.pairAttribution.totalPairs > 0 ? d.pairAttribution.recoveryReingest / d.pairAttribution.totalPairs : 0)}), ${d.pairAttribution.organic} organic (${pct(d.pairAttribution.totalPairs > 0 ? d.pairAttribution.organic / d.pairAttribution.totalPairs : 0)}).\n`);
67
67
  lines.push(`### Top ${d.topClusters.length} clusters (0.90 threshold, largest first)\n`);
68
68
  d.topClusters.forEach((cluster, i) => {
69
+ // #206 decision 2: project is the only merge-safety rail (agent rail removed).
69
70
  const mismatch = cluster.metadataMismatch;
70
- const mismatchFlags = [
71
- mismatch.projectMismatch ? "project" : null,
72
- mismatch.sourceAgentMismatch ? "source_agent" : null,
73
- ].filter(Boolean);
71
+ const mismatchFlags = [mismatch.projectMismatch ? "project" : null].filter(Boolean);
74
72
  lines.push(`**Cluster ${i + 1}** — size ${cluster.size}, attribution: ${cluster.attribution.recoveryReingest} recovery-pair(s) / ${cluster.attribution.organic} organic-pair(s)` +
75
73
  (mismatchFlags.length > 0 ? `, metadata mismatch: ${mismatchFlags.join(", ")}` : ", metadata consistent"));
76
74
  for (const m of cluster.members) {
package/dist/nightly.js CHANGED
@@ -782,13 +782,11 @@ async function runNightly(options = {}) {
782
782
  // #408: weakPrimaryFloor is a release-managed calibration
783
783
  // constant now — no config threading; the Options field stays
784
784
  // as the eval/test seam.
785
- }, {
786
- // #405: no supersessionMaxCalls — the ONE run budget is the
787
- // only call cap. #408: minSimilarity defaults to the
788
- // calibration constant (seam only).
789
785
  },
790
786
  // #405: the ONE per-run LLM-call ceiling (default 5000;
791
787
  // consolidateMaxLlmCalls honored as a deprecated alias).
788
+ // (#206-B: the retired supersession stage's options slot sat
789
+ // here — removed with the stage; true-update detection is 3.8's.)
792
790
  (0, consolidate_js_1.resolveNightlyLlmCallBudget)(savedConfig),
793
791
  // #245: soft cap on the corpus (default 10000; 0 disables eviction).
794
792
  memorySoftCapResolved, {
@@ -17,6 +17,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
17
17
  exports.buildHookRequest = buildHookRequest;
18
18
  exports.runRecallHook = runRecallHook;
19
19
  const learnings_identity_js_1 = require("./learnings-identity.js");
20
+ const wrapper_prompt_js_1 = require("./wrapper-prompt.js");
20
21
  const node_path_1 = require("node:path");
21
22
  const FETCH_TIMEOUT_MS = 1000;
22
23
  /** Read all of stdin (CC pipes the hook payload JSON). */
@@ -44,6 +45,15 @@ function buildHookRequest(payload, cwd = process.cwd()) {
44
45
  const prompt = typeof payload.prompt === "string" ? payload.prompt : "";
45
46
  if (!prompt)
46
47
  return null;
48
+ // #489: a pure plumbing payload (task-notification without a report result,
49
+ // command envelope, caveat, stdout echo, bare system-reminder) produces NO
50
+ // request at all — no HTTP call, so the 1000 ms hook budget is spent on
51
+ // real prompts only. A wrapper CARRYING prose (agent-message hand-back,
52
+ // report <result>, user text around the wrapper) recalls on the full text
53
+ // (owner decision 4) — the shared classifier, same function the server
54
+ // guards with (wrapper-prompt.ts).
55
+ if ((0, wrapper_prompt_js_1.isPureWrapperPrompt)(prompt))
56
+ return null;
47
57
  // #203 scope: derive project from the session cwd so retrieval can apply a
48
58
  // soft project-affinity boost. basename(cwd) matches capture's
49
59
  // decodeProjectDirName for non-hyphenated dirs (the common case); a hyphen
@@ -93,6 +93,7 @@ exports.handleRecallIndex = handleRecallIndex;
93
93
  exports.handleMemoryGet = handleMemoryGet;
94
94
  exports.formatMemoryGetText = formatMemoryGetText;
95
95
  const storage = __importStar(require("./storage.js"));
96
+ const wrapper_prompt_js_1 = require("./wrapper-prompt.js");
96
97
  const type_labels_js_1 = require("./type-labels.js");
97
98
  const retrieval_js_1 = require("./retrieval.js");
98
99
  const CALIBRATION = __importStar(require("./calibration.js"));
@@ -321,6 +322,17 @@ async function handleRecallIndex(deps, body) {
321
322
  return { status: 200, body: { ok: true, reset: true } };
322
323
  }
323
324
  const prompt = typeof req.prompt === "string" ? req.prompt.trim() : "";
325
+ // #489 wrapper guard — the short-prompt gate's slot, BEFORE beginTurn and
326
+ // before any precision recording, so a pure plumbing payload (CC delivers
327
+ // task-notifications/command envelopes as user-role messages; any client
328
+ // could POST one) gets {block: null} with NO recall_pushes row, NO turn
329
+ // burn, NO shown_count bump, NO last_accessed refresh. The SAME shared
330
+ // classifier the hook client guards with (wrapper-prompt.ts) — defense in
331
+ // depth. A wrapper carrying real payload prose falls through and recalls on
332
+ // the full text, exactly today's behavior (owner decision 4).
333
+ if ((0, wrapper_prompt_js_1.isPureWrapperPrompt)(prompt)) {
334
+ return { status: 200, body: { block: null, skipped: "wrapper-prompt" } };
335
+ }
324
336
  const minPromptLength = deps.options?.minPromptLength ?? DEFAULT_MIN_PROMPT_LENGTH;
325
337
  if (prompt.length < minPromptLength) {
326
338
  return { status: 200, body: { block: null, skipped: "short-prompt" } };
@@ -2,7 +2,7 @@
2
2
  * Reconsolidation (#384, #392) — the store resolves its own corrections, and
3
3
  * THE unified resolution stage.
4
4
  *
5
- * Nightly consolidation stage (runs as Stage 3.8, after supersession, before
5
+ * Nightly consolidation stage (runs as Stage 3.8, after links, before
6
6
  * decay/prune) that detects memories which correct, retract, supersede, or
7
7
  * DUPLICATE older ones; REWRITES corrected facts in place (absorbing
8
8
  * transition-only trigger memories), MERGES confirmed duplicates via the
@@ -44,8 +44,9 @@
44
44
  * planDedup and the judged mergeMemoryIds) refuse to blend a conflicts-linked
45
45
  * pair, counted as conflict_skipped. The zone therefore runs AFTER the scan —
46
46
  * with the zone first, a >=0.92 conflict pair was blended
47
- * before the judge ever saw it (the planted-eval harm: canonical=older, the
48
- * newer truth erased); running it last means verdicts/marks/binds land first
47
+ * before the judge ever saw it (the planted-eval harm: an unjudged blend —
48
+ * whichever row lost the canonical pick, its wording was simply erased);
49
+ * running it last means verdicts/marks/binds land first
49
50
  * and the zone merges only what no verdict claimed — a conflicts bind set by
50
51
  * this run's scan guards the SAME run's zone.
51
52
  *
@@ -94,10 +95,14 @@ export declare const RECONSOLIDATION_STAGE_LABEL = "reconsolidation";
94
95
  /**
95
96
  * Default minimum COSINE similarity for a correction candidate pair —
96
97
  * RELEASE-MANAGED since #408 (calibration.ts CORRECTION_MIN_SIMILARITY;
97
- * provenance there). Lower than the supersession stage's 0.80 on purpose: a
98
- * retraction often rides inside an otherwise unrelated memory (the field
99
- * failure that opened this issue), so the neighborhood gate must be a touch
100
- * wider while the LLM verdict + confidence gate carry the precision load.
98
+ * provenance there). Deliberately wide: a retraction often rides inside an
99
+ * otherwise unrelated memory (the field failure that opened this issue), so
100
+ * the neighborhood gate must be a touch wider while the LLM verdict +
101
+ * confidence gate carry the precision load. Since #206-B (owner decision 6)
102
+ * this floor also subsumes the retired Stage 3.7 supersession scan's 0.80:
103
+ * 3.8 is the ONLY true-update detector, scanning every new memory — no
104
+ * shape gate, wider floor — with `corrects`/`supersedes` partitioning what
105
+ * 3.7's binary prompt called a supersession.
101
106
  */
102
107
  export declare const DEFAULT_CORRECTION_MIN_SIMILARITY = 0.75;
103
108
  /**
@@ -193,7 +198,7 @@ export interface ScoutShape {
193
198
  }
194
199
  /**
195
200
  * Build the constrained correction-shape prompt (classify-tier cost profile:
196
- * 1500-char truncation, supersession/verdict precedent). The wording asks for
201
+ * 1500-char truncation, verdict-prompt precedent). The wording asks for
197
202
  * the OLD claim's distinctive terms — the field-failure mechanism is that a
198
203
  * correction CONTAINS the words of what it corrects, even when the surrounding
199
204
  * topics (and therefore the embedding cosine) are unrelated. Guard-C extends
@@ -204,13 +209,14 @@ export declare function buildScoutShapePrompt(content: string): string;
204
209
  /**
205
210
  * Parse the scout shape reply. Null on unparseable JSON, a missing/non-boolean
206
211
  * `correction`, or a missing/out-of-range `confidence` — the caller counts
207
- * skipped_infra and moves on (parseSupersessionReply discipline: never
212
+ * skipped_infra and moves on (the retired supersession stage's parse
213
+ * discipline, kept: never
208
214
  * mis-detect on ambiguity). `references` is lenient (missing/non-string → "")
209
215
  * because an empty string simply yields no FTS hits — a harmless miss, not a
210
216
  * mis-judgment.
211
217
  */
212
218
  export declare function parseScoutShape(reply: string): ScoutShape | null;
213
- /** Build the constrained pair-verdict prompt (1500-char truncation, supersession precedent). */
219
+ /** Build the constrained pair-verdict prompt (1500-char truncation). */
214
220
  export declare function buildCorrectionVerdictPrompt(oldContent: string, newContent: string): string;
215
221
  export interface CorrectionVerdict {
216
222
  action: ResolutionAction;
@@ -219,7 +225,7 @@ export interface CorrectionVerdict {
219
225
  /**
220
226
  * Parse the pair verdict. Null on anything unparseable, unknown action, or an
221
227
  * out-of-range/missing confidence — the caller counts skipped_infra and moves
222
- * on (same discipline as parseSupersessionReply: never mis-judge on ambiguity).
228
+ * on (the same never-mis-judge-on-ambiguity discipline).
223
229
  */
224
230
  export declare function parseCorrectionVerdict(reply: string): CorrectionVerdict | null;
225
231
  export interface RewriteTriggerDisposition {
@@ -3,7 +3,7 @@
3
3
  * Reconsolidation (#384, #392) — the store resolves its own corrections, and
4
4
  * THE unified resolution stage.
5
5
  *
6
- * Nightly consolidation stage (runs as Stage 3.8, after supersession, before
6
+ * Nightly consolidation stage (runs as Stage 3.8, after links, before
7
7
  * decay/prune) that detects memories which correct, retract, supersede, or
8
8
  * DUPLICATE older ones; REWRITES corrected facts in place (absorbing
9
9
  * transition-only trigger memories), MERGES confirmed duplicates via the
@@ -45,8 +45,9 @@
45
45
  * planDedup and the judged mergeMemoryIds) refuse to blend a conflicts-linked
46
46
  * pair, counted as conflict_skipped. The zone therefore runs AFTER the scan —
47
47
  * with the zone first, a >=0.92 conflict pair was blended
48
- * before the judge ever saw it (the planted-eval harm: canonical=older, the
49
- * newer truth erased); running it last means verdicts/marks/binds land first
48
+ * before the judge ever saw it (the planted-eval harm: an unjudged blend —
49
+ * whichever row lost the canonical pick, its wording was simply erased);
50
+ * running it last means verdicts/marks/binds land first
50
51
  * and the zone merges only what no verdict claimed — a conflicts bind set by
51
52
  * this run's scan guards the SAME run's zone.
52
53
  *
@@ -154,10 +155,14 @@ exports.RECONSOLIDATION_STAGE_LABEL = "reconsolidation";
154
155
  /**
155
156
  * Default minimum COSINE similarity for a correction candidate pair —
156
157
  * RELEASE-MANAGED since #408 (calibration.ts CORRECTION_MIN_SIMILARITY;
157
- * provenance there). Lower than the supersession stage's 0.80 on purpose: a
158
- * retraction often rides inside an otherwise unrelated memory (the field
159
- * failure that opened this issue), so the neighborhood gate must be a touch
160
- * wider while the LLM verdict + confidence gate carry the precision load.
158
+ * provenance there). Deliberately wide: a retraction often rides inside an
159
+ * otherwise unrelated memory (the field failure that opened this issue), so
160
+ * the neighborhood gate must be a touch wider while the LLM verdict +
161
+ * confidence gate carry the precision load. Since #206-B (owner decision 6)
162
+ * this floor also subsumes the retired Stage 3.7 supersession scan's 0.80:
163
+ * 3.8 is the ONLY true-update detector, scanning every new memory — no
164
+ * shape gate, wider floor — with `corrects`/`supersedes` partitioning what
165
+ * 3.7's binary prompt called a supersession.
161
166
  */
162
167
  exports.DEFAULT_CORRECTION_MIN_SIMILARITY = CALIBRATION.CORRECTION_MIN_SIMILARITY;
163
168
  /**
@@ -167,11 +172,12 @@ exports.DEFAULT_CORRECTION_MIN_SIMILARITY = CALIBRATION.CORRECTION_MIN_SIMILARIT
167
172
  * weak rewrite is corruption.
168
173
  */
169
174
  exports.DEFAULT_CORRECTION_REWRITE_MIN_CONFIDENCE = CALIBRATION.CORRECTION_REWRITE_MIN_CONFIDENCE;
170
- /** Neighbor pool size before older/similarity filtering narrows to top 5 (supersession mirror). */
175
+ /** Neighbor pool size before older/similarity filtering narrows to top 5
176
+ * (formerly the supersession stage's constants — 3.8 inherited the shape). */
171
177
  const CORRECTION_NEIGHBOR_POOL = 15;
172
- /** Older-neighbor pairs kept per candidate after filtering (supersession mirror). */
178
+ /** Older-neighbor pairs kept per candidate after filtering. */
173
179
  const CORRECTION_NEIGHBOR_TOP_K = 5;
174
- /** Content truncation for prompts (classify-tier cost profile; supersession precedent). */
180
+ /** Content truncation for prompts (classify-tier cost profile). */
175
181
  const PROMPT_TRUNCATE_CHARS = 1500;
176
182
  /** Head of the old content quoted in the provenance footer. */
177
183
  exports.FOOTER_HEAD_MAX_CHARS = 160;
@@ -214,7 +220,7 @@ function nowIso() {
214
220
  }
215
221
  /**
216
222
  * Build the constrained correction-shape prompt (classify-tier cost profile:
217
- * 1500-char truncation, supersession/verdict precedent). The wording asks for
223
+ * 1500-char truncation, verdict-prompt precedent). The wording asks for
218
224
  * the OLD claim's distinctive terms — the field-failure mechanism is that a
219
225
  * correction CONTAINS the words of what it corrects, even when the surrounding
220
226
  * topics (and therefore the embedding cosine) are unrelated. Guard-C extends
@@ -237,7 +243,8 @@ function buildScoutShapePrompt(content) {
237
243
  /**
238
244
  * Parse the scout shape reply. Null on unparseable JSON, a missing/non-boolean
239
245
  * `correction`, or a missing/out-of-range `confidence` — the caller counts
240
- * skipped_infra and moves on (parseSupersessionReply discipline: never
246
+ * skipped_infra and moves on (the retired supersession stage's parse
247
+ * discipline, kept: never
241
248
  * mis-detect on ambiguity). `references` is lenient (missing/non-string → "")
242
249
  * because an empty string simply yields no FTS hits — a harmless miss, not a
243
250
  * mis-judgment.
@@ -264,7 +271,7 @@ function parseScoutShape(reply) {
264
271
  const references = typeof obj.references === "string" ? obj.references : "";
265
272
  return { correction: obj.correction, references: references.trim(), confidence };
266
273
  }
267
- /** Build the constrained pair-verdict prompt (1500-char truncation, supersession precedent). */
274
+ /** Build the constrained pair-verdict prompt (1500-char truncation). */
268
275
  function buildCorrectionVerdictPrompt(oldContent, newContent) {
269
276
  const trunc = (s) => (s.length > PROMPT_TRUNCATE_CHARS ? `${s.slice(0, PROMPT_TRUNCATE_CHARS)}…` : s);
270
277
  return (`You are checking how a NEWER memory relates to an OLDER one in an AI agent's long-term memory.\n\n` +
@@ -287,7 +294,7 @@ function buildCorrectionVerdictPrompt(oldContent, newContent) {
287
294
  /**
288
295
  * Parse the pair verdict. Null on anything unparseable, unknown action, or an
289
296
  * out-of-range/missing confidence — the caller counts skipped_infra and moves
290
- * on (same discipline as parseSupersessionReply: never mis-judge on ambiguity).
297
+ * on (the same never-mis-judge-on-ambiguity discipline).
291
298
  */
292
299
  function parseCorrectionVerdict(reply) {
293
300
  if (!reply)
@@ -558,8 +565,8 @@ function accumulateBandStat(cumulative, run) {
558
565
  cumulative.none += run.none;
559
566
  cumulative.merge_below_gate += run.merge_below_gate;
560
567
  cumulative.conf_sum += run.conf_sum;
561
- if (run.metadata_skipped !== undefined) {
562
- cumulative.metadata_skipped = (cumulative.metadata_skipped ?? 0) + run.metadata_skipped;
568
+ if (run.project_skipped !== undefined) {
569
+ cumulative.project_skipped = (cumulative.project_skipped ?? 0) + run.project_skipped;
563
570
  }
564
571
  }
565
572
  /**
@@ -715,7 +722,9 @@ async function stageReconsolidation(db, llm, budget, embedFn, dryRun, stateDir,
715
722
  // re-detect). Once the backlog drains, pairs_reevaluated reads 0.
716
723
  const prevScannedRowid = (0, state_js_1.loadState)(stateDir).reconsolidationScannedRowid ?? startCursor;
717
724
  let scannedRowidHighwater = startCursor;
718
- // NO shape filter (AC2) — unlike stageSupersession. Absorbed rows are
725
+ // NO shape filter (AC2) — also why 3.8 subsumes the retired Stage 3.7
726
+ // supersession scan (its shape gate would miss exactly these pairs).
727
+ // Absorbed rows are
719
728
  // excluded: they are invisible to recall and must not re-enter judgment.
720
729
  const rows = db
721
730
  .prepare(`SELECT rowid AS __rowid, * FROM memories
@@ -739,7 +748,7 @@ async function stageReconsolidation(db, llm, budget, embedFn, dryRun, stateDir,
739
748
  let explicitDivergent = 0;
740
749
  let mergeBelowGate = 0;
741
750
  let skippedAboveCeiling = 0;
742
- let skippedMetadataMismatch = 0;
751
+ let skippedProjectMismatch = 0;
743
752
  let mergePairsApplied = 0;
744
753
  // #393 guard-C: conflicts verdicts rendered (link written, both live) and
745
754
  // judged-path merge refusals on a conflicts-linked pair.
@@ -1228,10 +1237,10 @@ async function stageReconsolidation(db, llm, budget, embedFn, dryRun, stateDir,
1228
1237
  console.log(`[hicortex] Reconsolidation: merged ${pair.oldId.slice(0, 8)} + ${pair.newId.slice(0, 8)} ` +
1229
1238
  `into canonical ${result.canonicalId.slice(0, 8)} (${result.linksRepointed} link(s) re-pointed)`);
1230
1239
  }
1231
- else if (result.reason === "metadata_mismatch") {
1232
- skippedMetadataMismatch++;
1240
+ else if (result.reason === "project_mismatch") {
1241
+ skippedProjectMismatch++;
1233
1242
  console.log(`[hicortex] Reconsolidation: merge of ${pair.oldId.slice(0, 8)} + ${pair.newId.slice(0, 8)} ` +
1234
- `skipped (metadata mismatch) — both kept`);
1243
+ `skipped (project mismatch) — both kept`);
1235
1244
  }
1236
1245
  else if (result.reason === "conflict_linked") {
1237
1246
  // #393 guard-C: the pair is conflicts-linked (operator-planted
@@ -1446,10 +1455,10 @@ async function stageReconsolidation(db, llm, budget, embedFn, dryRun, stateDir,
1446
1455
  console.log(`[hicortex] Reconsolidation: merged ${pair.oldId.slice(0, 8)} + ${pair.newId.slice(0, 8)} ` +
1447
1456
  `into canonical ${result.canonicalId.slice(0, 8)} (${result.linksRepointed} link(s) re-pointed)`);
1448
1457
  }
1449
- else if (result.reason === "metadata_mismatch") {
1450
- skippedMetadataMismatch++;
1458
+ else if (result.reason === "project_mismatch") {
1459
+ skippedProjectMismatch++;
1451
1460
  console.log(`[hicortex] Reconsolidation: merge of ${pair.oldId.slice(0, 8)} + ${pair.newId.slice(0, 8)} ` +
1452
- `skipped (metadata mismatch) — both kept`);
1461
+ `skipped (project mismatch) — both kept`);
1453
1462
  }
1454
1463
  else if (result.reason === "conflict_linked") {
1455
1464
  conflictSkippedJudged++;
@@ -1473,7 +1482,8 @@ async function stageReconsolidation(db, llm, budget, embedFn, dryRun, stateDir,
1473
1482
  // deterministic sweep: verdicts, marks, and binds land first, and the zone
1474
1483
  // merges only what no verdict claimed. With the zone first, a >=0.92
1475
1484
  // genuine-conflict pair was blended before the judge ever saw it
1476
- // (canonical = oldest, the newer truth erased — the planted-eval harm);
1485
+ // (an unjudged blend — the canonical-pick loser's wording erased —
1486
+ // the planted-eval harm);
1477
1487
  // running it last means a `conflicts` bind set by THIS run's scan guards
1478
1488
  // the SAME run's zone. LLM-free and budget-free — an LLM-less night still
1479
1489
  // drains duplicates (a deadline-deferred cluster re-detects next run at
@@ -1499,8 +1509,8 @@ async function stageReconsolidation(db, llm, budget, embedFn, dryRun, stateDir,
1499
1509
  det.pairs = merges.losers_merged;
1500
1510
  det.merge = merges.losers_merged;
1501
1511
  det.conf_sum = merges.losers_merged;
1502
- if (merges.skipped_metadata_mismatch > 0) {
1503
- det.metadata_skipped = merges.skipped_metadata_mismatch;
1512
+ if (merges.skipped_project_mismatch > 0) {
1513
+ det.project_skipped = merges.skipped_project_mismatch;
1504
1514
  }
1505
1515
  bandStats[`>=${autoMergeThreshold}`] = det;
1506
1516
  }
@@ -1569,7 +1579,7 @@ async function stageReconsolidation(db, llm, budget, embedFn, dryRun, stateDir,
1569
1579
  merge_pairs_applied: mergePairsApplied,
1570
1580
  merge_below_gate: mergeBelowGate,
1571
1581
  skipped_above_ceiling: skippedAboveCeiling,
1572
- skipped_metadata_mismatch: skippedMetadataMismatch,
1582
+ skipped_project_mismatch: skippedProjectMismatch,
1573
1583
  conflict_flagged: conflictFlagged,
1574
1584
  conflict_skipped: conflictSkippedJudged + merges.skipped_conflict,
1575
1585
  scout_scanned: scoutScanned,
package/dist/relink.d.ts CHANGED
@@ -86,8 +86,8 @@ export interface RelinkReport {
86
86
  /**
87
87
  * Read the stored embedding for a memory from memory_vectors.
88
88
  * Returns null when the row is missing (caller falls back to re-embedding).
89
- * @deprecated moved to storage.ts (shared with consolidate.ts's supersession
90
- * stage); re-exported here so existing importers of relink.ts keep working.
89
+ * @deprecated moved to storage.ts (shared with the resolution stages);
90
+ * re-exported here so existing importers of relink.ts keep working.
91
91
  */
92
92
  export { getStoredEmbedding } from "./storage.js";
93
93
  /**
package/dist/relink.js CHANGED
@@ -107,8 +107,8 @@ function loadExistingPairs(db) {
107
107
  /**
108
108
  * Read the stored embedding for a memory from memory_vectors.
109
109
  * Returns null when the row is missing (caller falls back to re-embedding).
110
- * @deprecated moved to storage.ts (shared with consolidate.ts's supersession
111
- * stage); re-exported here so existing importers of relink.ts keep working.
110
+ * @deprecated moved to storage.ts (shared with the resolution stages);
111
+ * re-exported here so existing importers of relink.ts keep working.
112
112
  */
113
113
  var storage_js_1 = require("./storage.js");
114
114
  Object.defineProperty(exports, "getStoredEmbedding", { enumerable: true, get: function () { return storage_js_1.getStoredEmbedding; } });
@@ -155,14 +155,15 @@ export declare function recallQueryVector(registry: CentroidStore, sessionId: st
155
155
  }): Float32Array;
156
156
  /**
157
157
  * Ids among `candidateIds` that have been superseded by a later memory — i.e.
158
- * they are the SOURCE of a `superseded_by` link (stageSupersession links
159
- * old → new). One query, not per-candidate.
158
+ * they are the SOURCE of a `superseded_by` link (the resolution pass links
159
+ * old → new; formerly also the retired Stage 3.7 supersession scan). One
160
+ * query, not per-candidate.
160
161
  */
161
162
  export declare function findSupersededIds(db: Database.Database, candidateIds: string[]): Set<string>;
162
163
  /**
163
164
  * The full ranking-demotion set among `candidateIds` (#384): the UNION of
164
- * (a) sources of a `superseded_by` link (legacy + stageSupersession — link
165
- * driven, works on pre-v14 rows with NULL status) and (b) rows whose
165
+ * (a) sources of a `superseded_by` link (legacy + retired-3.7 + resolution
166
+ * verdicts — link driven, works on pre-v14 rows with NULL status) and (b) rows whose
166
167
  * `memories.status` is 'superseded' or 'retracted' (reconsolidation marks +
167
168
  * explicit ingest marks). `corrected` is deliberately NOT demoting — a
168
169
  * rewritten memory carries the CORRECTION, and demoting it would bury the
package/dist/retrieval.js CHANGED
@@ -275,8 +275,9 @@ function recallQueryVector(registry, sessionId, promptEmb, opts) {
275
275
  }
276
276
  /**
277
277
  * Ids among `candidateIds` that have been superseded by a later memory — i.e.
278
- * they are the SOURCE of a `superseded_by` link (stageSupersession links
279
- * old → new). One query, not per-candidate.
278
+ * they are the SOURCE of a `superseded_by` link (the resolution pass links
279
+ * old → new; formerly also the retired Stage 3.7 supersession scan). One
280
+ * query, not per-candidate.
280
281
  */
281
282
  function findSupersededIds(db, candidateIds) {
282
283
  if (candidateIds.length === 0)
@@ -290,8 +291,8 @@ function findSupersededIds(db, candidateIds) {
290
291
  }
291
292
  /**
292
293
  * The full ranking-demotion set among `candidateIds` (#384): the UNION of
293
- * (a) sources of a `superseded_by` link (legacy + stageSupersession — link
294
- * driven, works on pre-v14 rows with NULL status) and (b) rows whose
294
+ * (a) sources of a `superseded_by` link (legacy + retired-3.7 + resolution
295
+ * verdicts — link driven, works on pre-v14 rows with NULL status) and (b) rows whose
295
296
  * `memories.status` is 'superseded' or 'retracted' (reconsolidation marks +
296
297
  * explicit ingest marks). `corrected` is deliberately NOT demoting — a
297
298
  * rewritten memory carries the CORRECTION, and demoting it would bury the
package/dist/state.d.ts CHANGED
@@ -51,19 +51,19 @@ export interface HicortexState {
51
51
  */
52
52
  domainCursor?: number;
53
53
  /**
54
- * Resume cursor for the nightly's supersession-detection stage (#191 Phase
55
- * B) — highest memories.rowid whose decision/correction candidates have
56
- * been evaluated (or infra-skipped) this run. Absent/0 = never run. Unlike
57
- * relinkCursor/domainCursor (separate resumable CLI commands), this cursor
58
- * advances within the shared nightly LLM call budget as part of the regular
59
- * nightly — the corpus is back-processed gradually over many nights.
54
+ * #206-B: the retired supersession stage's cursor key
55
+ * (`supersessionCursor`) is deliberately NOT modeled here anymore. The
56
+ * stage (3.7, #191 Phase B) is retired into the reconsolidation pass, the
57
+ * whole-corpus backfill was complete before retirement, and nothing reads
58
+ * the key — pre-#206-B installs keep their stale value on disk, left to
59
+ * rot unread (no migration, no deletion). Do not repurpose the name.
60
60
  */
61
- supersessionCursor?: number;
62
61
  /**
63
62
  * Resume cursor for the nightly's reconsolidation stage (#384) — highest
64
63
  * memories.rowid whose candidates have been evaluated (or infra-skipped)
65
64
  * with all their CONFIRMED work applied. Absent/0 = never run. Same
66
- * advance-past-considered-candidates discipline as supersessionCursor,
65
+ * advance-past-considered-candidates discipline (formerly the retired
66
+ * supersessionCursor's),
67
67
  * with one addition (#439): confirmed merges and rewrite groups apply at
68
68
  * the candidate boundary — the END of the iteration that confirmed them —
69
69
  * and the cursor advances past a candidate only when that apply landed.
package/dist/storage.d.ts CHANGED
@@ -149,8 +149,8 @@ export declare function getMemoryTagsWeightedBatched(db: Database.Database, memo
149
149
  * Read the stored embedding for a memory from memory_vectors.
150
150
  * Returns null when the row is missing (caller falls back to re-embedding).
151
151
  *
152
- * Shared by `hicortex relink` and the nightly's supersession stage
153
- * (consolidate.ts) — lives here (not in relink.ts) so consolidate.ts can use
152
+ * Shared by `hicortex relink` and the nightly's resolution stages
153
+ * (reconsolidation.ts, consolidate.ts) — lives here (not in relink.ts) so they can use
154
154
  * it without importing from relink.ts, which itself imports from
155
155
  * consolidate.ts (BudgetTracker, discoverLinkCandidates).
156
156
  */
package/dist/storage.js CHANGED
@@ -408,8 +408,8 @@ function getMemoryTagsWeightedBatched(db, memoryIds) {
408
408
  * Read the stored embedding for a memory from memory_vectors.
409
409
  * Returns null when the row is missing (caller falls back to re-embedding).
410
410
  *
411
- * Shared by `hicortex relink` and the nightly's supersession stage
412
- * (consolidate.ts) — lives here (not in relink.ts) so consolidate.ts can use
411
+ * Shared by `hicortex relink` and the nightly's resolution stages
412
+ * (reconsolidation.ts, consolidate.ts) — lives here (not in relink.ts) so they can use
413
413
  * it without importing from relink.ts, which itself imports from
414
414
  * consolidate.ts (BudgetTracker, discoverLinkCandidates).
415
415
  */