@gamaze/hicortex 0.20.4 → 0.20.5
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 +4 -1
- package/dist/backup.d.ts +12 -8
- package/dist/backup.js +13 -9
- package/dist/claude-desktop.d.ts +138 -0
- package/dist/claude-desktop.js +251 -0
- package/dist/cli.d.ts +6 -2
- package/dist/cli.js +62 -3
- package/dist/consolidate.d.ts +8 -1
- package/dist/consolidate.js +82 -2
- package/dist/db.js +36 -0
- package/dist/dedup.d.ts +157 -25
- package/dist/dedup.js +376 -83
- package/dist/domain-classify.js +4 -2
- package/dist/index.js +7 -7
- package/dist/init.d.ts +4 -1
- package/dist/init.js +103 -1
- package/dist/llm.d.ts +19 -13
- package/dist/llm.js +25 -14
- package/dist/mcp-server.d.ts +6 -0
- package/dist/mcp-server.js +84 -13
- package/dist/mcp-stdio.js +6 -1
- package/dist/memory-instructions.d.ts +18 -0
- package/dist/memory-instructions.js +39 -2
- package/dist/nightly.js +14 -1
- package/dist/reconsolidation.d.ts +323 -0
- package/dist/reconsolidation.js +1226 -0
- package/dist/retrieval.d.ts +14 -0
- package/dist/retrieval.js +41 -3
- package/dist/state.d.ts +23 -1
- package/dist/storage.d.ts +25 -0
- package/dist/storage.js +49 -7
- package/dist/type-classify.js +4 -2
- package/dist/types.d.ts +188 -0
- package/hermes-plugin/hicortex/provider.py +29 -17
- package/opencode-plugin/hicortex/index.ts +7 -7
- package/package.json +1 -1
- package/pi-extension/hicortex/index.ts +7 -7
- package/server.json +2 -2
package/dist/consolidate.js
CHANGED
|
@@ -66,6 +66,8 @@ const state_js_1 = require("./state.js");
|
|
|
66
66
|
const domain_classify_js_1 = require("./domain-classify.js");
|
|
67
67
|
const schema_prototypes_js_1 = require("./schema-prototypes.js");
|
|
68
68
|
const nofit_js_1 = require("./nofit.js");
|
|
69
|
+
const reconsolidation_js_1 = require("./reconsolidation.js");
|
|
70
|
+
const dedup_js_1 = require("./dedup.js");
|
|
69
71
|
// Default config constants (matching Python config.py)
|
|
70
72
|
/**
|
|
71
73
|
* Default ceiling on total LLM calls across all classify-tier consolidation
|
|
@@ -1065,7 +1067,7 @@ function parseSupersessionReply(reply) {
|
|
|
1065
1067
|
*/
|
|
1066
1068
|
async function classifySupersession(llm, oldContent, newContent) {
|
|
1067
1069
|
try {
|
|
1068
|
-
const r = await llm.completeClassify(buildSupersessionPrompt(oldContent, newContent)
|
|
1070
|
+
const r = await llm.completeClassify(buildSupersessionPrompt(oldContent, newContent));
|
|
1069
1071
|
return { verdict: parseSupersessionReply(r.text), usage: r.usage };
|
|
1070
1072
|
}
|
|
1071
1073
|
catch {
|
|
@@ -1360,6 +1362,66 @@ function stageMemoryCapEviction(db, dryRun, cap) {
|
|
|
1360
1362
|
`memories (corpus was ${count}, cap ${cap}).`);
|
|
1361
1363
|
return { cap, evicted: victims.length };
|
|
1362
1364
|
}
|
|
1365
|
+
/**
|
|
1366
|
+
* Minimal resolution-stage report for a SKIPPED (quiet-night) consolidation
|
|
1367
|
+
* run (#392): every stage field zero except `merges` — the deterministic
|
|
1368
|
+
* zone's own report — and its derived deterministic band snapshot. Keeps the
|
|
1369
|
+
* one-report surface intact (the zone is the only resolution work a quiet
|
|
1370
|
+
* night does) while telemetry's "skipped = zero LLM work" stays true. Knob
|
|
1371
|
+
* validation mirrors the stage's own (invalid → defaults).
|
|
1372
|
+
*/
|
|
1373
|
+
async function skippedRunResolutionReport(db, dryRun, stateDir, options = {}) {
|
|
1374
|
+
const validNumber = (v, fallback, ok) => {
|
|
1375
|
+
const n = Number(v);
|
|
1376
|
+
return Number.isFinite(n) && ok(n) ? n : fallback;
|
|
1377
|
+
};
|
|
1378
|
+
const autoMergeThreshold = validNumber(options.autoMergeThreshold, dedup_js_1.DEFAULT_DEDUP_MERGE_THRESHOLD, (n) => n > 0 && n <= 1);
|
|
1379
|
+
const maxMerges = validNumber(options.maxMerges, dedup_js_1.DEFAULT_DEDUP_NIGHTLY_MAX_MERGES, (n) => n >= 0);
|
|
1380
|
+
const merges = await (0, dedup_js_1.runDeterministicMergeZone)(db, {
|
|
1381
|
+
stateDir,
|
|
1382
|
+
threshold: autoMergeThreshold,
|
|
1383
|
+
maxMerges,
|
|
1384
|
+
dryRun,
|
|
1385
|
+
acquireLock: options.acquireLock,
|
|
1386
|
+
});
|
|
1387
|
+
const bandStats = {};
|
|
1388
|
+
if (merges.max_merges > 0) {
|
|
1389
|
+
bandStats[`>=${autoMergeThreshold}`] = {
|
|
1390
|
+
pairs: merges.losers_merged,
|
|
1391
|
+
merge: merges.losers_merged,
|
|
1392
|
+
corrects: 0,
|
|
1393
|
+
supersedes: 0,
|
|
1394
|
+
none: 0,
|
|
1395
|
+
merge_below_gate: 0,
|
|
1396
|
+
conf_sum: merges.losers_merged,
|
|
1397
|
+
...(merges.skipped_metadata_mismatch > 0
|
|
1398
|
+
? { metadata_skipped: merges.skipped_metadata_mismatch }
|
|
1399
|
+
: {}),
|
|
1400
|
+
};
|
|
1401
|
+
}
|
|
1402
|
+
return {
|
|
1403
|
+
scanned: 0,
|
|
1404
|
+
pairs_evaluated: 0,
|
|
1405
|
+
rewritten: 0,
|
|
1406
|
+
absorbed: 0,
|
|
1407
|
+
kept_linked: 0,
|
|
1408
|
+
marked_superseded: 0,
|
|
1409
|
+
marked_retracted: 0,
|
|
1410
|
+
below_gate: 0,
|
|
1411
|
+
contract_failed: 0,
|
|
1412
|
+
skipped_infra: 0,
|
|
1413
|
+
skipped_idempotent: 0,
|
|
1414
|
+
explicit_verified: 0,
|
|
1415
|
+
explicit_divergent: 0,
|
|
1416
|
+
cursor: (0, state_js_1.loadState)(stateDir).reconsolidationCursor ?? 0,
|
|
1417
|
+
merges,
|
|
1418
|
+
merge_pairs_applied: 0,
|
|
1419
|
+
merge_below_gate: 0,
|
|
1420
|
+
skipped_above_ceiling: 0,
|
|
1421
|
+
skipped_metadata_mismatch: 0,
|
|
1422
|
+
band_stats: bandStats,
|
|
1423
|
+
};
|
|
1424
|
+
}
|
|
1363
1425
|
async function runConsolidation(db, llm, embedFn, dryRun = false, skipReflection = false, stateDir, domainOptions, supersessionOptions,
|
|
1364
1426
|
/** Total LLM-call ceiling across classify-tier stages (#241). The caller
|
|
1365
1427
|
* reads `consolidateMaxLlmCalls` from config and passes it; unset → the
|
|
@@ -1368,7 +1430,13 @@ budgetMaxCalls,
|
|
|
1368
1430
|
/** Soft cap on the corpus (#245). Nightly.ts reads `memorySoftCap` from
|
|
1369
1431
|
* config and passes it; unset → `DEFAULT_MEMORY_SOFT_CAP` (10000). `0`
|
|
1370
1432
|
* disables eviction (indefinite growth). */
|
|
1371
|
-
memorySoftCap
|
|
1433
|
+
memorySoftCap,
|
|
1434
|
+
/** Reconsolidation-stage knobs (#384), threaded from config by nightly.ts
|
|
1435
|
+
* (correctionMinSimilarity / correctionRewriteMinConfidence) exactly like
|
|
1436
|
+
* supersessionOptions above; unset fields → the stage's defaults.
|
|
1437
|
+
* Appended AFTER the pre-#384 params so every existing positional caller
|
|
1438
|
+
* (tests, hosted nightly) keeps its argument meaning. */
|
|
1439
|
+
reconsolidationOptions) {
|
|
1372
1440
|
const start = new Date();
|
|
1373
1441
|
const report = {
|
|
1374
1442
|
started_at: start.toISOString(),
|
|
@@ -1407,6 +1475,13 @@ memorySoftCap) {
|
|
|
1407
1475
|
// but the cap stage is pure DB: cheap, idempotent when under cap).
|
|
1408
1476
|
report.stages.memory_cap = stageMemoryCapEviction(db, dryRun, memorySoftCap ?? exports.DEFAULT_MEMORY_SOFT_CAP);
|
|
1409
1477
|
if (skip) {
|
|
1478
|
+
// #392: the deterministic merge zone is LLM-free, so a quiet night (zero
|
|
1479
|
+
// new memories → this skip) still drains a pre-existing duplicate
|
|
1480
|
+
// backlog — the memory_cap precedent. Results ride the ONE resolution
|
|
1481
|
+
// stage report (telemetry's "skipped = zero LLM work" stays true), and
|
|
1482
|
+
// the zone never runs twice: the main path runs it INSIDE the stage, this
|
|
1483
|
+
// skip path returns before that.
|
|
1484
|
+
report.stages.reconsolidation = await skippedRunResolutionReport(db, dryRun, stateDir, reconsolidationOptions);
|
|
1410
1485
|
report.status = "skipped";
|
|
1411
1486
|
report.completed_at = new Date().toISOString();
|
|
1412
1487
|
return report;
|
|
@@ -1455,6 +1530,11 @@ memorySoftCap) {
|
|
|
1455
1530
|
report.stages.hub_boost = stageHubBoost(db, dryRun);
|
|
1456
1531
|
// Stage 3.7: Supersession Detection (#191 Phase B)
|
|
1457
1532
|
report.stages.supersession = await stageSupersession(db, llm, budget, embedFn, dryRun, stateDir, supersessionOptions);
|
|
1533
|
+
// Stage 3.8: Reconsolidation (#384) — resolve corrections: rewrite
|
|
1534
|
+
// fact-shaped targets in place (absorbing transition-only triggers),
|
|
1535
|
+
// mark everything else. Rides the same shared budget under its own stage
|
|
1536
|
+
// label + cursor (supersession-stage pattern).
|
|
1537
|
+
report.stages.reconsolidation = await (0, reconsolidation_js_1.stageReconsolidation)(db, llm, budget, embedFn, dryRun, stateDir, reconsolidationOptions);
|
|
1458
1538
|
// Stage 4: Decay & Prune
|
|
1459
1539
|
report.stages.decay_prune = stageDecayPrune(db, dryRun);
|
|
1460
1540
|
// (Memory cap eviction moved before the precheck skip — see above.)
|
package/dist/db.js
CHANGED
|
@@ -511,6 +511,42 @@ const MIGRATIONS = [
|
|
|
511
511
|
db.exec("UPDATE memories SET memory_type = 'learnings' WHERE memory_type = 'lesson'");
|
|
512
512
|
},
|
|
513
513
|
},
|
|
514
|
+
{
|
|
515
|
+
version: 14,
|
|
516
|
+
name: "reconsolidation_status_history",
|
|
517
|
+
up: (db) => {
|
|
518
|
+
// #384 reconsolidation. `memories.status` is the code-defined state
|
|
519
|
+
// vocabulary (NULL/active default; 'superseded'/'retracted' demote in
|
|
520
|
+
// ranking; 'corrected' = rewritten, never demotes; 'absorbed' =
|
|
521
|
+
// invisible to recall — no vector, no FTS row). NULL default with NO
|
|
522
|
+
// backfill: legacy supersessions stay link-driven (findSupersededIds
|
|
523
|
+
// already demotes them); a status is only ever written by the
|
|
524
|
+
// reconsolidation stage, an explicit ingest mark, or a rollback.
|
|
525
|
+
// `memory_history` records CONTENT REWRITES ONLY (before/after content,
|
|
526
|
+
// prior status, trigger dispositions) — one structure serving audit AND
|
|
527
|
+
// `hicortex history --rollback`. Marks need no history row: they are
|
|
528
|
+
// auditable via links + status. Idempotent: hasColumn + IF NOT EXISTS.
|
|
529
|
+
if (!hasColumn(db, "memories", "status")) {
|
|
530
|
+
db.exec("ALTER TABLE memories ADD COLUMN status TEXT");
|
|
531
|
+
}
|
|
532
|
+
db.exec(`
|
|
533
|
+
CREATE TABLE IF NOT EXISTS memory_history (
|
|
534
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
535
|
+
memory_id TEXT NOT NULL,
|
|
536
|
+
old_content TEXT NOT NULL,
|
|
537
|
+
new_content TEXT NOT NULL,
|
|
538
|
+
prev_status TEXT,
|
|
539
|
+
new_status TEXT,
|
|
540
|
+
triggers_json TEXT,
|
|
541
|
+
evidence_id TEXT,
|
|
542
|
+
confidence REAL,
|
|
543
|
+
cause TEXT NOT NULL,
|
|
544
|
+
created_at TEXT NOT NULL
|
|
545
|
+
)
|
|
546
|
+
`);
|
|
547
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_memory_history_memory ON memory_history(memory_id)");
|
|
548
|
+
},
|
|
549
|
+
},
|
|
514
550
|
];
|
|
515
551
|
/**
|
|
516
552
|
* Run all pending migrations against the database.
|
package/dist/dedup.d.ts
CHANGED
|
@@ -1,52 +1,101 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `hicortex dedup`
|
|
2
|
+
* `hicortex dedup` + the nightly deterministic merge zone (issues #100, #392).
|
|
3
3
|
*
|
|
4
4
|
* Corpus-quality companion to `hicortex relink`/`classify-domains`: instead of
|
|
5
|
-
* discovering NEW structure, this
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* executes the merge.
|
|
5
|
+
* discovering NEW structure, this collapses memories that are near-identical
|
|
6
|
+
* (top-10 KNN cosine >= threshold, default 0.92, union-find clustered — same
|
|
7
|
+
* math as the #191 D1 duplicate-rate audit; see cluster.ts). Two surfaces,
|
|
8
|
+
* ONE core:
|
|
10
9
|
*
|
|
11
|
-
*
|
|
10
|
+
* - `runDedup` — the manual CLI. Default is a DRY RUN: report only, zero
|
|
11
|
+
* writes. `--apply` executes the merge. Threshold resolution:
|
|
12
|
+
* `--threshold` > config `dedupAutoMergeThreshold` > legacy config
|
|
13
|
+
* `dedupMergeThreshold` > 0.92.
|
|
14
|
+
* - `runDeterministicMergeZone` (#392) — the nightly's LLM-free merge zone:
|
|
15
|
+
* pairs at/above the ceiling merge deterministically, ZERO LLM calls, under
|
|
16
|
+
* its own pacing cap (`dedupNightlyMaxMerges`). Called from the
|
|
17
|
+
* reconsolidation stage (and from the quiet-night skip path in
|
|
18
|
+
* consolidate.ts) so one stage report covers all resolution work.
|
|
19
|
+
*
|
|
20
|
+
* Per cluster (shared `planDedup`/`mergeCluster` core — no forks):
|
|
12
21
|
* - Canonical = highest access_count (tie: oldest created_at, then
|
|
13
22
|
* lexicographically smallest id — fully deterministic for audit).
|
|
14
23
|
* - Losers' links are re-pointed onto the canonical (a link that would
|
|
15
24
|
* become a self-link, or one whose (canonical, target) ordered pair
|
|
16
25
|
* ALREADY holds an edge, is skipped rather than overwritten — see
|
|
17
|
-
* planLinkRepoints for why `relationship` cannot be part of that guard)
|
|
26
|
+
* planLinkRepoints for why `relationship` cannot be part of that guard);
|
|
27
|
+
* the losers' own link rows are then deleted (previously cascade-deleted
|
|
28
|
+
* with the row).
|
|
18
29
|
* - canonical.access_count/shown_count = summed across the cluster;
|
|
19
30
|
* last_accessed = max; base_strength = max.
|
|
20
31
|
* - Tags are UNIONED onto the canonical (weights NULL — the next nightly's
|
|
21
32
|
* reconsolidation pass recomputes weights and the derived primary from
|
|
22
|
-
* the merged tag set)
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
33
|
+
* the merged tag set); the losers' tag rows are cleared and their domain
|
|
34
|
+
* set NULL — a loser must not count in moduleIndex/tag recomputes.
|
|
35
|
+
* - A `dedup_log` row is written per loser — audit trail AND the safety net
|
|
36
|
+
* /distill consults (mcp-server.ts) so an absorbed loser's
|
|
37
|
+
* `source_session` marker still blocks a re-ingest.
|
|
38
|
+
* - Losers are ABSORBED (storage.absorbMemory), not deleted (#392): status
|
|
39
|
+
* 'absorbed', vector + FTS rows dropped, plain row retained — invisible
|
|
40
|
+
* to recall, fetchable by id as evidence. Same vocabulary as the
|
|
41
|
+
* reconsolidation rewrite path. Merges are NOT history-rollback-able —
|
|
42
|
+
* dedup_log (loser_id → canonical_id) + the retained loser row is the
|
|
43
|
+
* record.
|
|
28
44
|
*
|
|
29
|
-
* A cluster whose members disagree on project
|
|
30
|
-
*
|
|
45
|
+
* A cluster whose members disagree on project or source_agent is SKIPPED
|
|
46
|
+
* entirely and listed for manual review — no --force in this release.
|
|
31
47
|
*
|
|
32
|
-
* Safety rails
|
|
48
|
+
* Safety rails when applying (CLI and zone alike):
|
|
33
49
|
* - A full DB backup (SQLite backup API) is taken FIRST, to
|
|
34
|
-
*
|
|
35
|
-
*
|
|
50
|
+
* <state>/backups/pre-dedup-<ISO>.db, pruned to `backupRetention` newest
|
|
51
|
+
* (pattern-scoped: full `hicortex-*.tar.gz` artifacts keep their own
|
|
52
|
+
* count). The CLI aborts (no merges attempted) if the backup fails; the
|
|
53
|
+
* nightly zone is fail-soft (backup_failed flag, zero merges).
|
|
36
54
|
* - The existing single-flight capture lock (capture.ts) is held for the
|
|
37
55
|
* duration of the merge so a concurrent nightly/capture run can't race
|
|
38
|
-
* the dedup_log bookkeeping the merge relies on.
|
|
56
|
+
* the dedup_log bookkeeping the merge relies on. The CLI fails fast on a
|
|
57
|
+
* busy lock; the zone reports lock_busy and merges nothing.
|
|
39
58
|
*
|
|
40
59
|
* Server-mode only (needs the local DB), like relink/classify-domains.
|
|
41
60
|
*/
|
|
42
61
|
import type Database from "better-sqlite3";
|
|
43
62
|
import { type ClusterMetadataMismatch } from "./cluster.js";
|
|
44
63
|
import { acquireCaptureLock } from "./capture.js";
|
|
64
|
+
import type { DeterministicMergeZoneReport } from "./types.js";
|
|
45
65
|
/**
|
|
46
66
|
* Default merge threshold. Measured on the #191 mechanical audit corpus:
|
|
47
67
|
* 89 clusters / 110 excess rows at 0.92 (data/audit-20260729/eval-report.md).
|
|
68
|
+
* #392: also the default `dedupAutoMergeThreshold` — the deterministic/LLM
|
|
69
|
+
* boundary of the unified resolution pass.
|
|
48
70
|
*/
|
|
49
71
|
export declare const DEFAULT_DEDUP_MERGE_THRESHOLD = 0.92;
|
|
72
|
+
/**
|
|
73
|
+
* Default pacing cap on merge OPERATIONS per nightly run (#392): the zone's
|
|
74
|
+
* clusters and the stage's judged pair merges count against ONE cap. Bounds a
|
|
75
|
+
* misbehaving-distiller burst; a large backlog drains over a few nights.
|
|
76
|
+
* `0` disables the merge machinery entirely.
|
|
77
|
+
*/
|
|
78
|
+
export declare const DEFAULT_DEDUP_NIGHTLY_MAX_MERGES = 250;
|
|
79
|
+
/**
|
|
80
|
+
* Row shape read from `memories` for merge decisions — a superset of the
|
|
81
|
+
* fields the Memory type declares (shown_count isn't on that interface yet).
|
|
82
|
+
* `status` rides along so judged-pair merges can defensively drop rows that
|
|
83
|
+
* were absorbed between verdict and apply.
|
|
84
|
+
*/
|
|
85
|
+
interface DedupMemberRow {
|
|
86
|
+
id: string;
|
|
87
|
+
content: string;
|
|
88
|
+
access_count: number;
|
|
89
|
+
shown_count: number | null;
|
|
90
|
+
last_accessed: string | null;
|
|
91
|
+
base_strength: number;
|
|
92
|
+
created_at: string;
|
|
93
|
+
project: string | null;
|
|
94
|
+
privacy: string | null;
|
|
95
|
+
source_agent: string;
|
|
96
|
+
source_session: string | null;
|
|
97
|
+
status: string | null;
|
|
98
|
+
}
|
|
50
99
|
export interface DedupClusterPlan {
|
|
51
100
|
size: number;
|
|
52
101
|
canonicalId: string;
|
|
@@ -94,8 +143,8 @@ export interface DedupReport {
|
|
|
94
143
|
linksSkippedExisting: number;
|
|
95
144
|
/** --apply only: clusters actually merged. */
|
|
96
145
|
merged?: number;
|
|
97
|
-
/** --apply only: loser rows
|
|
98
|
-
|
|
146
|
+
/** --apply only: loser rows absorbed (hidden from recall, kept as evidence). */
|
|
147
|
+
losersAbsorbed?: number;
|
|
99
148
|
/** --apply only: clusters that errored mid-merge (rolled back; left for a re-run). */
|
|
100
149
|
failedClusters?: number;
|
|
101
150
|
/** --apply only: path to the pre-merge backup. */
|
|
@@ -104,7 +153,7 @@ export interface DedupReport {
|
|
|
104
153
|
export interface DedupOptions {
|
|
105
154
|
/** Execute the merge. Default false = dry run (report only, zero writes). */
|
|
106
155
|
apply?: boolean;
|
|
107
|
-
/** Override
|
|
156
|
+
/** Override the configured threshold for one run. */
|
|
108
157
|
threshold?: number;
|
|
109
158
|
/** DB path override (tests / manual snapshot verification). Defaults to resolveDbPath(). */
|
|
110
159
|
dbPath?: string;
|
|
@@ -116,7 +165,7 @@ export interface DedupOptions {
|
|
|
116
165
|
acquireLock?: typeof acquireCaptureLock;
|
|
117
166
|
/**
|
|
118
167
|
* Test-only failure injection: called once per cluster merge, after the
|
|
119
|
-
* link/tag/counter writes but before the audit-log +
|
|
168
|
+
* link/tag/counter writes but before the audit-log + absorb step. Throwing
|
|
120
169
|
* here proves a mid-merge error rolls the WHOLE cluster's writes back
|
|
121
170
|
* (better-sqlite3 transaction semantics) rather than leaving a half-merged
|
|
122
171
|
* cluster. Never set in production.
|
|
@@ -135,10 +184,93 @@ export interface LinkRepointPlan {
|
|
|
135
184
|
skippedSelfLink: number;
|
|
136
185
|
skippedExisting: number;
|
|
137
186
|
}
|
|
187
|
+
/** One cluster's execution plan from `planDedup` — canonical, losers, members. */
|
|
188
|
+
export interface DedupMergePlan {
|
|
189
|
+
canonical: DedupMemberRow;
|
|
190
|
+
losers: DedupMemberRow[];
|
|
191
|
+
/** All member rows, oldest first (CLI preview lines derive from this). */
|
|
192
|
+
membersOldestFirst: DedupMemberRow[];
|
|
193
|
+
}
|
|
194
|
+
export interface PlanDedupResult {
|
|
195
|
+
/** Every cluster found at the threshold (mergeable + mismatch-skipped). */
|
|
196
|
+
clusterCount: number;
|
|
197
|
+
/** Clusters that passed the metadata rails, in discovery order. */
|
|
198
|
+
mergePlans: DedupMergePlan[];
|
|
199
|
+
mismatchSkipped: DedupMismatchCluster[];
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Discovery + merge planning at a cosine threshold (read-only — no writes).
|
|
203
|
+
* The ONE clustering core shared by the manual CLI (`runDedup`) and the
|
|
204
|
+
* nightly deterministic merge zone (`runDeterministicMergeZone`): KNN edges
|
|
205
|
+
* (k=10) → union-find clusters → member load → metadata-rail classification →
|
|
206
|
+
* canonical pick. Never forked.
|
|
207
|
+
*/
|
|
208
|
+
export declare function planDedup(db: Database.Database, threshold: number): PlanDedupResult;
|
|
209
|
+
export type MergeMemoryIdsResult = {
|
|
210
|
+
ok: true;
|
|
211
|
+
canonicalId: string;
|
|
212
|
+
loserIds: string[];
|
|
213
|
+
linksRepointed: number;
|
|
214
|
+
} | {
|
|
215
|
+
ok: false;
|
|
216
|
+
reason: "metadata_mismatch" | "no_members";
|
|
217
|
+
};
|
|
218
|
+
/**
|
|
219
|
+
* Merge an explicit set of memories (the judged-pair phase of #392: the
|
|
220
|
+
* reconsolidation stage queues verdict-confirmed pairs and applies them
|
|
221
|
+
* through THIS function so the merge math stays single-definition). Loads the
|
|
222
|
+
* LIVE rows at apply time — members that vanished or were absorbed between
|
|
223
|
+
* verdict and apply are dropped defensively; a metadata disagreement refuses
|
|
224
|
+
* the merge (both memories stay live). One transaction for the whole set.
|
|
225
|
+
*/
|
|
226
|
+
export declare function mergeMemoryIds(db: Database.Database, ids: string[]): MergeMemoryIdsResult;
|
|
227
|
+
/**
|
|
228
|
+
* Take a pre-merge DB backup to <stateDir>/backups/pre-dedup-<ISO>.db and
|
|
229
|
+
* prune older pre-dedup backups to `backupRetention` (config, default 7;
|
|
230
|
+
* pattern-scoped so full `hicortex-*.tar.gz` artifacts keep their own,
|
|
231
|
+
* independent retention count). THROWS on failure — the callers own the
|
|
232
|
+
* policy: the CLI aborts, the nightly zone is fail-soft. Returns the path.
|
|
233
|
+
*/
|
|
234
|
+
export declare function takePreDedupBackup(db: Database.Database, stateDir: string, config?: Record<string, unknown> | null): Promise<string>;
|
|
235
|
+
export interface DeterministicMergeZoneOptions {
|
|
236
|
+
/** State dir (lock + backup + band-stats persistence). Defaults to ~/.hicortex. */
|
|
237
|
+
stateDir?: string;
|
|
238
|
+
/** Cosine ceiling; validated (0,1] → DEFAULT_DEDUP_MERGE_THRESHOLD. */
|
|
239
|
+
threshold?: number;
|
|
240
|
+
/**
|
|
241
|
+
* Pacing cap on merge operations this run; validated >= 0 →
|
|
242
|
+
* DEFAULT_DEDUP_NIGHTLY_MAX_MERGES. `0` disables the machinery entirely
|
|
243
|
+
* (discovery skipped, zeroed report).
|
|
244
|
+
*/
|
|
245
|
+
maxMerges?: number;
|
|
246
|
+
/** Discovery + bounded preview only — zero writes, no lock, no backup. */
|
|
247
|
+
dryRun?: boolean;
|
|
248
|
+
/** Config override (backupRetention) — defaults to reading stateDir/config.json. */
|
|
249
|
+
config?: Record<string, unknown> | null;
|
|
250
|
+
/** Capture-lock acquirer override (tests). Defaults to the real capture.ts lock. */
|
|
251
|
+
acquireLock?: typeof acquireCaptureLock;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* The >= dedupAutoMergeThreshold band of the unified resolution pass (#392):
|
|
255
|
+
* planDedup discovery + per-cluster mergeCluster — LLM-free, budget-free, so
|
|
256
|
+
* an LLM-less night still drains duplicates. Its own short capture-lock
|
|
257
|
+
* window and pre-merge backup; fail-soft on a busy lock (lock_busy) and on a
|
|
258
|
+
* backup failure (backup_failed) — zero merges either way, never a throw.
|
|
259
|
+
*
|
|
260
|
+
* Also persists the deterministic band's cumulative statistics to state.json
|
|
261
|
+
* `resolutionBandStats` (label `>=threshold`; losers count as merge verdicts
|
|
262
|
+
* at confidence 1.0, mismatch clusters as metadata_skipped) — skipped
|
|
263
|
+
* entirely on dry-run. Called from the reconsolidation stage (main path) and
|
|
264
|
+
* from runConsolidation's quiet-night skip path — exactly one of the two per
|
|
265
|
+
* run.
|
|
266
|
+
*/
|
|
267
|
+
export declare function runDeterministicMergeZone(db: Database.Database, opts?: DeterministicMergeZoneOptions): Promise<DeterministicMergeZoneReport>;
|
|
138
268
|
/**
|
|
139
269
|
* Run `hicortex dedup`. Dry run by default (options.apply falsy) — discovery
|
|
140
270
|
* + merge planning only, zero writes. `options.apply` executes: backup, then
|
|
141
|
-
* one transaction per cluster.
|
|
271
|
+
* one transaction per cluster. Fails fast (throws) on a busy capture lock or
|
|
272
|
+
* a failed backup — a deliberate manual command should be retried by the
|
|
273
|
+
* operator, not silently deferred (the nightly zone is the fail-soft twin).
|
|
142
274
|
*/
|
|
143
275
|
export declare function runDedup(options?: DedupOptions): Promise<DedupReport>;
|
|
144
276
|
/** Escape SQL LIKE wildcards — session ids (e.g. Hermes) can contain "_"/"%". */
|