@gamaze/hicortex 0.20.7 → 0.20.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -41
- package/assets/dashboard.html +3989 -836
- package/dist/calibration.d.ts +293 -0
- package/dist/calibration.js +379 -0
- package/dist/capture-health.d.ts +87 -0
- package/dist/capture-health.js +106 -0
- package/dist/capture-pause.d.ts +86 -0
- package/dist/capture-pause.js +127 -0
- package/dist/capture.d.ts +24 -3
- package/dist/capture.js +11 -1
- package/dist/classify-domains.d.ts +6 -0
- package/dist/classify-domains.js +7 -1
- package/dist/cli.js +38 -3
- package/dist/config-read.d.ts +1 -1
- package/dist/config-read.js +96 -9
- package/dist/consolidate.d.ts +114 -68
- package/dist/consolidate.js +302 -182
- package/dist/dashboard.d.ts +326 -6
- package/dist/dashboard.js +592 -7
- package/dist/db.js +105 -0
- package/dist/dedup.d.ts +34 -26
- package/dist/dedup.js +91 -57
- package/dist/distiller.js +1 -1
- package/dist/domain-classify.d.ts +7 -6
- package/dist/domain-classify.js +12 -10
- package/dist/eval/decay-eval.d.ts +3 -3
- package/dist/eval/decay-eval.js +4 -4
- package/dist/eval/importance-eval.d.ts +85 -0
- package/dist/eval/importance-eval.js +286 -0
- package/dist/eval/planted-eval.d.ts +26 -0
- package/dist/eval/planted-eval.js +97 -0
- package/dist/eval/planted-fixtures.d.ts +107 -0
- package/dist/eval/planted-fixtures.js +283 -0
- package/dist/eval/planted-harness.d.ts +176 -0
- package/dist/eval/planted-harness.js +649 -0
- package/dist/eval/ranking-battery.d.ts +78 -0
- package/dist/eval/ranking-battery.js +181 -0
- package/dist/eval/ranking-eval.d.ts +41 -0
- package/dist/eval/ranking-eval.js +391 -0
- package/dist/eval/ranking-fixtures.d.ts +77 -0
- package/dist/eval/ranking-fixtures.js +226 -0
- package/dist/identity-store.d.ts +21 -0
- package/dist/identity-store.js +49 -0
- package/dist/index.js +4 -3
- package/dist/init.d.ts +23 -3
- package/dist/init.js +84 -9
- package/dist/llm.d.ts +43 -58
- package/dist/llm.js +87 -101
- package/dist/mcp-server.d.ts +12 -0
- package/dist/mcp-server.js +213 -32
- package/dist/nightly.d.ts +9 -1
- package/dist/nightly.js +164 -110
- package/dist/nofit.d.ts +4 -11
- package/dist/nofit.js +6 -23
- package/dist/prompts.d.ts +10 -0
- package/dist/prompts.js +28 -5
- package/dist/recall-index.d.ts +30 -28
- package/dist/recall-index.js +21 -18
- package/dist/recall-registry.d.ts +2 -1
- package/dist/recall-registry.js +35 -1
- package/dist/reconsolidation.d.ts +168 -87
- package/dist/reconsolidation.js +818 -377
- package/dist/relink.js +3 -4
- package/dist/rescore-importance.d.ts +80 -0
- package/dist/rescore-importance.js +236 -0
- package/dist/retrieval.d.ts +80 -35
- package/dist/retrieval.js +322 -105
- package/dist/run-deadline.d.ts +62 -0
- package/dist/run-deadline.js +73 -0
- package/dist/schema-prototypes.d.ts +3 -3
- package/dist/schema-prototypes.js +3 -3
- package/dist/stages.d.ts +37 -0
- package/dist/stages.js +51 -0
- package/dist/state.d.ts +34 -9
- package/dist/storage.d.ts +50 -18
- package/dist/storage.js +125 -30
- package/dist/telemetry.d.ts +8 -7
- package/dist/token-budget.js +3 -4
- package/dist/type-classify.js +4 -4
- package/dist/types.d.ts +143 -155
- package/domains.example.json +4 -5
- package/hermes-plugin/hicortex/README.md +2 -2
- package/openclaw.plugin.json +1 -1
- package/package.json +4 -1
- package/pi-extension/hicortex/README.md +1 -1
- package/server.json +3 -3
package/dist/db.js
CHANGED
|
@@ -547,6 +547,111 @@ const MIGRATIONS = [
|
|
|
547
547
|
db.exec("CREATE INDEX IF NOT EXISTS idx_memory_history_memory ON memory_history(memory_id)");
|
|
548
548
|
},
|
|
549
549
|
},
|
|
550
|
+
{
|
|
551
|
+
version: 15,
|
|
552
|
+
name: "source_machine",
|
|
553
|
+
up: (db) => {
|
|
554
|
+
// #421 owner direction (machine × harness identity): nullable, NO
|
|
555
|
+
// backfill — honesty over guesses. Rows written before this column
|
|
556
|
+
// stay NULL and group under "earlier captures" in the console; that
|
|
557
|
+
// bucket shrinks as nights accumulate stamped captures. Stamped by the
|
|
558
|
+
// nightly capture path (config `machineName` ?? os.hostname()) and
|
|
559
|
+
// accepted optionally by /distill + /ingest. Idempotent via hasColumn.
|
|
560
|
+
if (!hasColumn(db, "memories", "source_machine")) {
|
|
561
|
+
db.exec("ALTER TABLE memories ADD COLUMN source_machine TEXT");
|
|
562
|
+
}
|
|
563
|
+
},
|
|
564
|
+
},
|
|
565
|
+
{
|
|
566
|
+
version: 16,
|
|
567
|
+
name: "distill_activity",
|
|
568
|
+
up: (db) => {
|
|
569
|
+
// #422 Phase 2 — /distill capture-health accounting. One row per POST
|
|
570
|
+
// (every outcome incl. held/skipped), written by
|
|
571
|
+
// capture-health.ts:recordDistillActivity from the /distill handler's
|
|
572
|
+
// exits. This is OPERATIONS telemetry, not memory data: rows prune
|
|
573
|
+
// after 7 days (in-module, once per process per UTC day), so the table
|
|
574
|
+
// stays bounded while giving the console's capture-health card a real
|
|
575
|
+
// posts/sessions/bytes/held picture per machine × agent. `retried` is
|
|
576
|
+
// computed at insert (an earlier row with the same session_id +
|
|
577
|
+
// segment_id means this POST is a client retry of a cursor-held
|
|
578
|
+
// segment) — never updated afterwards. Sidecar table (no memories FK):
|
|
579
|
+
// the memory rows of a failed POST may never exist; the activity row
|
|
580
|
+
// must record the attempt anyway. Idempotent: IF NOT EXISTS everywhere.
|
|
581
|
+
db.exec(`
|
|
582
|
+
CREATE TABLE IF NOT EXISTS distill_activity (
|
|
583
|
+
ts TEXT NOT NULL,
|
|
584
|
+
day TEXT NOT NULL,
|
|
585
|
+
machine TEXT NOT NULL DEFAULT '',
|
|
586
|
+
agent TEXT NOT NULL DEFAULT '',
|
|
587
|
+
session_id TEXT,
|
|
588
|
+
segment_id TEXT,
|
|
589
|
+
bytes INTEGER NOT NULL DEFAULT 0,
|
|
590
|
+
outcome TEXT NOT NULL,
|
|
591
|
+
retried INTEGER NOT NULL DEFAULT 0
|
|
592
|
+
)
|
|
593
|
+
`);
|
|
594
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_distill_activity_day ON distill_activity(day)");
|
|
595
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_distill_activity_session_segment ON distill_activity(session_id, segment_id)");
|
|
596
|
+
},
|
|
597
|
+
},
|
|
598
|
+
{
|
|
599
|
+
version: 17,
|
|
600
|
+
name: "memory_corroboration",
|
|
601
|
+
up: (db) => {
|
|
602
|
+
// #423 phase 3 — explicit owner corroboration trail (POST /enrich).
|
|
603
|
+
// DEFAULT 0 with NO backfill: a pre-v17 row was never enriched and 0 is
|
|
604
|
+
// the honest count. /memory rides it via SELECT * (the console detail's
|
|
605
|
+
// "corroborated × N"); the enrich itself writes base_strength — it
|
|
606
|
+
// never fakes access/shown counts (those are the recall-adoption
|
|
607
|
+
// signal). Idempotent via hasColumn (the v15 pattern).
|
|
608
|
+
if (!hasColumn(db, "memories", "corroboration_count")) {
|
|
609
|
+
db.exec("ALTER TABLE memories ADD COLUMN corroboration_count INTEGER NOT NULL DEFAULT 0");
|
|
610
|
+
}
|
|
611
|
+
},
|
|
612
|
+
},
|
|
613
|
+
{
|
|
614
|
+
version: 18,
|
|
615
|
+
name: "capture_pauses",
|
|
616
|
+
up: (db) => {
|
|
617
|
+
// #423 phase 3, D3 — server-side 200-skip for a paused machine ×
|
|
618
|
+
// harness bundle. A row EXISTS = paused; absence = capturing (no
|
|
619
|
+
// "paused" flag to keep honest). Deliberately NO retention/pruning: the
|
|
620
|
+
// rows are few and operator-owned, and pruning one would silently
|
|
621
|
+
// resume capture the operator meant to hold. Idempotent: IF NOT EXISTS.
|
|
622
|
+
db.exec(`
|
|
623
|
+
CREATE TABLE IF NOT EXISTS capture_pauses (
|
|
624
|
+
machine TEXT NOT NULL DEFAULT '',
|
|
625
|
+
harness TEXT NOT NULL,
|
|
626
|
+
paused_at TEXT NOT NULL,
|
|
627
|
+
PRIMARY KEY(machine, harness)
|
|
628
|
+
)
|
|
629
|
+
`);
|
|
630
|
+
},
|
|
631
|
+
},
|
|
632
|
+
{
|
|
633
|
+
version: 19,
|
|
634
|
+
name: "add_importance_scored_at",
|
|
635
|
+
up: (db) => {
|
|
636
|
+
// #425 — scored-at watermark for importance scoring. getUnscoredMemories
|
|
637
|
+
// keys on importance_scored_at IS NULL (v19+), replacing the old
|
|
638
|
+
// base_strength = 0.5 sentinel, which re-rolled every row the model
|
|
639
|
+
// genuinely scored 0.5 every night. Backfill: existing rows that are NOT
|
|
640
|
+
// at the 0.5 sentinel are stamped "settled" (COALESCE(updated_at,
|
|
641
|
+
// ingested_at, created_at)) — they carry a real historical score and
|
|
642
|
+
// leave the nightly pool. Rows AT the sentinel stay NULL so the next
|
|
643
|
+
// nightly scores them ONCE under the new rubric (bounded — the watermark
|
|
644
|
+
// write in stageImportance then takes them out of the pool). The
|
|
645
|
+
// rescore-importance backfill CLI re-judges settled rows wholesale under
|
|
646
|
+
// its own cursor; this migration only makes the NIGHTLY pool honest.
|
|
647
|
+
// Idempotent via hasColumn (the v8 pattern).
|
|
648
|
+
if (!hasColumn(db, "memories", "importance_scored_at")) {
|
|
649
|
+
db.exec("ALTER TABLE memories ADD COLUMN importance_scored_at TEXT");
|
|
650
|
+
}
|
|
651
|
+
db.exec(`UPDATE memories SET importance_scored_at = COALESCE(updated_at, ingested_at, created_at)
|
|
652
|
+
WHERE importance_scored_at IS NULL AND base_strength != 0.5`);
|
|
653
|
+
},
|
|
654
|
+
},
|
|
550
655
|
];
|
|
551
656
|
/**
|
|
552
657
|
* Run all pending migrations against the database.
|
package/dist/dedup.d.ts
CHANGED
|
@@ -8,12 +8,14 @@
|
|
|
8
8
|
* ONE core:
|
|
9
9
|
*
|
|
10
10
|
* - `runDedup` — the manual CLI. Default is a DRY RUN: report only, zero
|
|
11
|
-
* writes. `--apply` executes the merge. Threshold resolution:
|
|
12
|
-
* `--threshold` >
|
|
13
|
-
*
|
|
11
|
+
* writes. `--apply` executes the merge. Threshold resolution (#408):
|
|
12
|
+
* `--threshold` > the release-managed calibration ceiling (0.92,
|
|
13
|
+
* calibration.ts DEDUP_AUTO_MERGE_THRESHOLD — the config keys are gone).
|
|
14
14
|
* - `runDeterministicMergeZone` (#392) — the nightly's LLM-free merge zone:
|
|
15
|
-
* pairs at/above the ceiling merge deterministically, ZERO LLM calls,
|
|
16
|
-
*
|
|
15
|
+
* pairs at/above the ceiling merge deterministically, ZERO LLM calls,
|
|
16
|
+
* bounded by the run-wide pipeline deadline's stop-check (#405 — the
|
|
17
|
+
* dedupNightlyMaxMerges pacing cap is gone; merges are local transactions,
|
|
18
|
+
* so the deadline bounds their wall-clock). Called from the
|
|
17
19
|
* reconsolidation stage (and from the quiet-night skip path in
|
|
18
20
|
* consolidate.ts) so one stage report covers all resolution work.
|
|
19
21
|
*
|
|
@@ -62,20 +64,15 @@ import type Database from "better-sqlite3";
|
|
|
62
64
|
import { type ClusterMetadataMismatch } from "./cluster.js";
|
|
63
65
|
import { acquireCaptureLock } from "./capture.js";
|
|
64
66
|
import type { DeterministicMergeZoneReport } from "./types.js";
|
|
67
|
+
import type { RunDeadline } from "./run-deadline.js";
|
|
65
68
|
/**
|
|
66
|
-
* Default merge threshold
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* boundary of the unified
|
|
69
|
+
* Default merge threshold — RELEASE-MANAGED since #408: the constant (with
|
|
70
|
+
* its provenance: measured on the #191 mechanical audit corpus, 89 clusters /
|
|
71
|
+
* 110 excess rows at 0.92, data/audit-20260729/eval-report.md) lives in
|
|
72
|
+
* calibration.ts. #392: also the deterministic/LLM boundary of the unified
|
|
73
|
+
* resolution pass.
|
|
70
74
|
*/
|
|
71
75
|
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
76
|
/**
|
|
80
77
|
* Row shape read from `memories` for merge decisions — a superset of the
|
|
81
78
|
* fields the Memory type declares (shown_count isn't on that interface yet).
|
|
@@ -124,6 +121,11 @@ export interface DedupMismatchCluster {
|
|
|
124
121
|
memberIds: string[];
|
|
125
122
|
mismatch: ClusterMetadataMismatch;
|
|
126
123
|
}
|
|
124
|
+
/** #393 guard-C: a cluster set aside because a member pair is conflicts-linked. */
|
|
125
|
+
export interface DedupConflictCluster {
|
|
126
|
+
size: number;
|
|
127
|
+
memberIds: string[];
|
|
128
|
+
}
|
|
127
129
|
export interface DedupReport {
|
|
128
130
|
dryRun: boolean;
|
|
129
131
|
threshold: number;
|
|
@@ -131,6 +133,8 @@ export interface DedupReport {
|
|
|
131
133
|
clusterCount: number;
|
|
132
134
|
mergeable: DedupClusterPlan[];
|
|
133
135
|
mismatchSkipped: DedupMismatchCluster[];
|
|
136
|
+
/** #393 guard-C: clusters skipped because a member pair is conflicts-linked. */
|
|
137
|
+
conflictSkipped: DedupConflictCluster[];
|
|
134
138
|
/** Rows that would disappear if every mergeable cluster merged (loser count). */
|
|
135
139
|
plannedMerges: number;
|
|
136
140
|
/**
|
|
@@ -197,13 +201,15 @@ export interface PlanDedupResult {
|
|
|
197
201
|
/** Clusters that passed the metadata rails, in discovery order. */
|
|
198
202
|
mergePlans: DedupMergePlan[];
|
|
199
203
|
mismatchSkipped: DedupMismatchCluster[];
|
|
204
|
+
/** #393 guard-C: clusters set aside on a conflicts-linked member pair. */
|
|
205
|
+
conflictSkipped: DedupConflictCluster[];
|
|
200
206
|
}
|
|
201
207
|
/**
|
|
202
208
|
* Discovery + merge planning at a cosine threshold (read-only — no writes).
|
|
203
209
|
* The ONE clustering core shared by the manual CLI (`runDedup`) and the
|
|
204
210
|
* nightly deterministic merge zone (`runDeterministicMergeZone`): KNN edges
|
|
205
|
-
* (k=10) → union-find clusters → member load → metadata-rail
|
|
206
|
-
* canonical pick. Never forked.
|
|
211
|
+
* (k=10) → union-find clusters → member load → conflicts/metadata-rail
|
|
212
|
+
* classification → canonical pick. Never forked.
|
|
207
213
|
*/
|
|
208
214
|
export declare function planDedup(db: Database.Database, threshold: number): PlanDedupResult;
|
|
209
215
|
export type MergeMemoryIdsResult = {
|
|
@@ -213,7 +219,7 @@ export type MergeMemoryIdsResult = {
|
|
|
213
219
|
linksRepointed: number;
|
|
214
220
|
} | {
|
|
215
221
|
ok: false;
|
|
216
|
-
reason: "metadata_mismatch" | "no_members";
|
|
222
|
+
reason: "metadata_mismatch" | "conflict_linked" | "no_members";
|
|
217
223
|
};
|
|
218
224
|
/**
|
|
219
225
|
* Merge an explicit set of memories (the judged-pair phase of #392: the
|
|
@@ -221,7 +227,8 @@ export type MergeMemoryIdsResult = {
|
|
|
221
227
|
* through THIS function so the merge math stays single-definition). Loads the
|
|
222
228
|
* LIVE rows at apply time — members that vanished or were absorbed between
|
|
223
229
|
* verdict and apply are dropped defensively; a metadata disagreement refuses
|
|
224
|
-
* the merge (both memories stay live)
|
|
230
|
+
* the merge (both memories stay live); a conflicts-linked pair refuses it
|
|
231
|
+
* exactly the same way (#393 guard-C). One transaction for the whole set.
|
|
225
232
|
*/
|
|
226
233
|
export declare function mergeMemoryIds(db: Database.Database, ids: string[]): MergeMemoryIdsResult;
|
|
227
234
|
/**
|
|
@@ -237,18 +244,19 @@ export interface DeterministicMergeZoneOptions {
|
|
|
237
244
|
stateDir?: string;
|
|
238
245
|
/** Cosine ceiling; validated (0,1] → DEFAULT_DEDUP_MERGE_THRESHOLD. */
|
|
239
246
|
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
247
|
/** Discovery + bounded preview only — zero writes, no lock, no backup. */
|
|
247
248
|
dryRun?: boolean;
|
|
248
249
|
/** Config override (backupRetention) — defaults to reading stateDir/config.json. */
|
|
249
250
|
config?: Record<string, unknown> | null;
|
|
250
251
|
/** Capture-lock acquirer override (tests). Defaults to the real capture.ts lock. */
|
|
251
252
|
acquireLock?: typeof acquireCaptureLock;
|
|
253
|
+
/**
|
|
254
|
+
* The run-wide pipeline deadline (#405) — checked BETWEEN cluster merges
|
|
255
|
+
* (each merge is a local transaction, so the boundary is safe). On expiry
|
|
256
|
+
* the un-attempted clusters count as deadline_deferred and drain next run
|
|
257
|
+
* (re-discovery is structural: the pairs stay above the threshold).
|
|
258
|
+
*/
|
|
259
|
+
deadline?: RunDeadline;
|
|
252
260
|
}
|
|
253
261
|
/**
|
|
254
262
|
* The >= dedupAutoMergeThreshold band of the unified resolution pass (#392):
|
package/dist/dedup.js
CHANGED
|
@@ -9,12 +9,14 @@
|
|
|
9
9
|
* ONE core:
|
|
10
10
|
*
|
|
11
11
|
* - `runDedup` — the manual CLI. Default is a DRY RUN: report only, zero
|
|
12
|
-
* writes. `--apply` executes the merge. Threshold resolution:
|
|
13
|
-
* `--threshold` >
|
|
14
|
-
*
|
|
12
|
+
* writes. `--apply` executes the merge. Threshold resolution (#408):
|
|
13
|
+
* `--threshold` > the release-managed calibration ceiling (0.92,
|
|
14
|
+
* calibration.ts DEDUP_AUTO_MERGE_THRESHOLD — the config keys are gone).
|
|
15
15
|
* - `runDeterministicMergeZone` (#392) — the nightly's LLM-free merge zone:
|
|
16
|
-
* pairs at/above the ceiling merge deterministically, ZERO LLM calls,
|
|
17
|
-
*
|
|
16
|
+
* pairs at/above the ceiling merge deterministically, ZERO LLM calls,
|
|
17
|
+
* bounded by the run-wide pipeline deadline's stop-check (#405 — the
|
|
18
|
+
* dedupNightlyMaxMerges pacing cap is gone; merges are local transactions,
|
|
19
|
+
* so the deadline bounds their wall-clock). Called from the
|
|
18
20
|
* reconsolidation stage (and from the quiet-night skip path in
|
|
19
21
|
* consolidate.ts) so one stage report covers all resolution work.
|
|
20
22
|
*
|
|
@@ -93,7 +95,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
93
95
|
};
|
|
94
96
|
})();
|
|
95
97
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
96
|
-
exports.
|
|
98
|
+
exports.DEFAULT_DEDUP_MERGE_THRESHOLD = void 0;
|
|
97
99
|
exports.planDedup = planDedup;
|
|
98
100
|
exports.mergeMemoryIds = mergeMemoryIds;
|
|
99
101
|
exports.takePreDedupBackup = takePreDedupBackup;
|
|
@@ -111,22 +113,17 @@ const cluster_js_1 = require("./cluster.js");
|
|
|
111
113
|
const capture_js_1 = require("./capture.js");
|
|
112
114
|
const state_js_1 = require("./state.js");
|
|
113
115
|
const config_read_js_1 = require("./config-read.js");
|
|
116
|
+
const CALIBRATION = __importStar(require("./calibration.js"));
|
|
114
117
|
const backup_js_1 = require("./backup.js");
|
|
115
118
|
const HICORTEX_HOME = (0, paths_js_1.hicortexHome)();
|
|
116
119
|
/**
|
|
117
|
-
* Default merge threshold
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
* boundary of the unified
|
|
120
|
+
* Default merge threshold — RELEASE-MANAGED since #408: the constant (with
|
|
121
|
+
* its provenance: measured on the #191 mechanical audit corpus, 89 clusters /
|
|
122
|
+
* 110 excess rows at 0.92, data/audit-20260729/eval-report.md) lives in
|
|
123
|
+
* calibration.ts. #392: also the deterministic/LLM boundary of the unified
|
|
124
|
+
* resolution pass.
|
|
121
125
|
*/
|
|
122
|
-
exports.DEFAULT_DEDUP_MERGE_THRESHOLD =
|
|
123
|
-
/**
|
|
124
|
-
* Default pacing cap on merge OPERATIONS per nightly run (#392): the zone's
|
|
125
|
-
* clusters and the stage's judged pair merges count against ONE cap. Bounds a
|
|
126
|
-
* misbehaving-distiller burst; a large backlog drains over a few nights.
|
|
127
|
-
* `0` disables the merge machinery entirely.
|
|
128
|
-
*/
|
|
129
|
-
exports.DEFAULT_DEDUP_NIGHTLY_MAX_MERGES = 250;
|
|
126
|
+
exports.DEFAULT_DEDUP_MERGE_THRESHOLD = CALIBRATION.DEDUP_AUTO_MERGE_THRESHOLD;
|
|
130
127
|
/** KNN neighbors considered per memory — same as the #191 audit (cluster.ts default). */
|
|
131
128
|
const DEDUP_KNN_K = 10;
|
|
132
129
|
/** Pre-merge backup filename pattern (takePreDedupBackup) — scoped retention. */
|
|
@@ -140,25 +137,19 @@ function readConfig(stateDir) {
|
|
|
140
137
|
}
|
|
141
138
|
}
|
|
142
139
|
/**
|
|
143
|
-
* Threshold resolution for the manual CLI (#
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
140
|
+
* Threshold resolution for the manual CLI (#408): explicit `--threshold` >
|
|
141
|
+
* the release-managed calibration constant. An invalid explicit value throws
|
|
142
|
+
* (existing error style). The config keys (`dedupAutoMergeThreshold` /
|
|
143
|
+
* legacy `dedupMergeThreshold`) are gone from the surface — a config carrying
|
|
144
|
+
* them changes nothing (warned at the config boundary, config-read.ts).
|
|
148
145
|
*/
|
|
149
|
-
function resolveThreshold(explicit
|
|
146
|
+
function resolveThreshold(explicit) {
|
|
150
147
|
if (explicit !== undefined) {
|
|
151
148
|
if (!Number.isFinite(explicit) || explicit <= 0 || explicit > 1) {
|
|
152
149
|
throw new Error(`[hicortex] dedup: invalid --threshold value: ${explicit} (must be in (0, 1])`);
|
|
153
150
|
}
|
|
154
151
|
return explicit;
|
|
155
152
|
}
|
|
156
|
-
for (const key of ["dedupAutoMergeThreshold", "dedupMergeThreshold"]) {
|
|
157
|
-
const fromConfig = Number(config?.[key]);
|
|
158
|
-
if (Number.isFinite(fromConfig) && fromConfig > 0 && fromConfig <= 1) {
|
|
159
|
-
return fromConfig;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
153
|
return exports.DEFAULT_DEDUP_MERGE_THRESHOLD;
|
|
163
154
|
}
|
|
164
155
|
function loadMembers(db, ids) {
|
|
@@ -246,22 +237,49 @@ function planLinkRepoints(db, canonical, losers) {
|
|
|
246
237
|
}
|
|
247
238
|
return { toAdd, skippedSelfLink, skippedExisting };
|
|
248
239
|
}
|
|
240
|
+
/**
|
|
241
|
+
* True when ANY member pair of the set holds a `conflicts` link. The member-set
|
|
242
|
+
* IN(...) on both endpoints makes the check symmetric by construction —
|
|
243
|
+
* whichever direction the edge was written in, both ids are in the set. #393
|
|
244
|
+
* guard-C: a conflicts link is the judge's word that two records cannot both
|
|
245
|
+
* be true, so no merge path may ever blend them.
|
|
246
|
+
*/
|
|
247
|
+
function clusterHasConflictLink(db, memberIds) {
|
|
248
|
+
if (memberIds.length < 2)
|
|
249
|
+
return false;
|
|
250
|
+
const placeholders = memberIds.map(() => "?").join(", ");
|
|
251
|
+
const row = db
|
|
252
|
+
.prepare(`SELECT 1 FROM memory_links WHERE relationship = 'conflicts'
|
|
253
|
+
AND source_id IN (${placeholders}) AND target_id IN (${placeholders}) LIMIT 1`)
|
|
254
|
+
.get(...memberIds, ...memberIds);
|
|
255
|
+
return !!row;
|
|
256
|
+
}
|
|
249
257
|
/**
|
|
250
258
|
* Discovery + merge planning at a cosine threshold (read-only — no writes).
|
|
251
259
|
* The ONE clustering core shared by the manual CLI (`runDedup`) and the
|
|
252
260
|
* nightly deterministic merge zone (`runDeterministicMergeZone`): KNN edges
|
|
253
|
-
* (k=10) → union-find clusters → member load → metadata-rail
|
|
254
|
-
* canonical pick. Never forked.
|
|
261
|
+
* (k=10) → union-find clusters → member load → conflicts/metadata-rail
|
|
262
|
+
* classification → canonical pick. Never forked.
|
|
255
263
|
*/
|
|
256
264
|
function planDedup(db, threshold) {
|
|
257
265
|
const edges = (0, cluster_js_1.buildKnnEdges)(db, { k: DEDUP_KNN_K, minCosine: threshold });
|
|
258
266
|
const clusters = (0, cluster_js_1.clusterEdges)(edges, threshold);
|
|
259
267
|
const mergePlans = [];
|
|
260
268
|
const mismatchSkipped = [];
|
|
269
|
+
const conflictSkipped = [];
|
|
261
270
|
for (const memberIds of clusters) {
|
|
262
271
|
const members = loadMembers(db, memberIds);
|
|
263
272
|
if (members.length < 2)
|
|
264
273
|
continue; // defensive — a member vanished between KNN and load
|
|
274
|
+
// #393 guard-C: the conflicts check runs BEFORE the metadata rails — a
|
|
275
|
+
// conflicts link is the judge's semantic verdict ("never blend"), which
|
|
276
|
+
// outranks the metadata classification; a cluster that is both
|
|
277
|
+
// conflict-linked and metadata-mismatched reports as conflict-skipped
|
|
278
|
+
// (the stronger, semantic reason).
|
|
279
|
+
if (clusterHasConflictLink(db, members.map((m) => m.id))) {
|
|
280
|
+
conflictSkipped.push({ size: members.length, memberIds: members.map((m) => m.id) });
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
265
283
|
const mismatch = (0, cluster_js_1.clusterMetadataMismatch)(members);
|
|
266
284
|
if (mismatch.projectMismatch || mismatch.sourceAgentMismatch) {
|
|
267
285
|
mismatchSkipped.push({ size: members.length, memberIds: members.map((m) => m.id), mismatch });
|
|
@@ -274,7 +292,7 @@ function planDedup(db, threshold) {
|
|
|
274
292
|
membersOldestFirst: [...members].sort((a, b) => a.created_at.localeCompare(b.created_at)),
|
|
275
293
|
});
|
|
276
294
|
}
|
|
277
|
-
return { clusterCount: clusters.length, mergePlans, mismatchSkipped };
|
|
295
|
+
return { clusterCount: clusters.length, mergePlans, mismatchSkipped, conflictSkipped };
|
|
278
296
|
}
|
|
279
297
|
/**
|
|
280
298
|
* Apply one cluster's merge. Pure DB writes against the passed connection —
|
|
@@ -363,13 +381,20 @@ function mergeCluster(db, canonical, losers, injectFailure) {
|
|
|
363
381
|
* through THIS function so the merge math stays single-definition). Loads the
|
|
364
382
|
* LIVE rows at apply time — members that vanished or were absorbed between
|
|
365
383
|
* verdict and apply are dropped defensively; a metadata disagreement refuses
|
|
366
|
-
* the merge (both memories stay live)
|
|
384
|
+
* the merge (both memories stay live); a conflicts-linked pair refuses it
|
|
385
|
+
* exactly the same way (#393 guard-C). One transaction for the whole set.
|
|
367
386
|
*/
|
|
368
387
|
function mergeMemoryIds(db, ids) {
|
|
369
388
|
const unique = [...new Set(ids)];
|
|
370
389
|
const members = loadMembers(db, unique).filter((m) => m.status !== "absorbed");
|
|
371
390
|
if (members.length < 2)
|
|
372
391
|
return { ok: false, reason: "no_members" };
|
|
392
|
+
// #393 guard-C: a conflicts-linked pair is never blended — the mirror of the
|
|
393
|
+
// metadata rails (both memories stay live; the caller's verdict was still
|
|
394
|
+
// rendered, so its cursor advances).
|
|
395
|
+
if (clusterHasConflictLink(db, members.map((m) => m.id))) {
|
|
396
|
+
return { ok: false, reason: "conflict_linked" };
|
|
397
|
+
}
|
|
373
398
|
const mismatch = (0, cluster_js_1.clusterMetadataMismatch)(members);
|
|
374
399
|
if (mismatch.projectMismatch || mismatch.sourceAgentMismatch) {
|
|
375
400
|
return { ok: false, reason: "metadata_mismatch" };
|
|
@@ -423,28 +448,19 @@ async function runDeterministicMergeZone(db, opts = {}) {
|
|
|
423
448
|
return Number.isFinite(n) && ok(n) ? n : fallback;
|
|
424
449
|
};
|
|
425
450
|
const threshold = validNumber(opts.threshold, exports.DEFAULT_DEDUP_MERGE_THRESHOLD, (n) => n > 0 && n <= 1);
|
|
426
|
-
const maxMerges = validNumber(opts.maxMerges, exports.DEFAULT_DEDUP_NIGHTLY_MAX_MERGES, (n) => n >= 0);
|
|
427
451
|
const stateDir = opts.stateDir ?? HICORTEX_HOME;
|
|
428
452
|
const dryRun = opts.dryRun ?? false;
|
|
429
|
-
// 0 = the merge machinery is disabled — skip discovery entirely.
|
|
430
|
-
if (maxMerges === 0) {
|
|
431
|
-
return {
|
|
432
|
-
threshold, max_merges: 0, clusters_found: 0, mergeable_clusters: 0,
|
|
433
|
-
merged_clusters: 0, losers_merged: 0, links_repointed: 0,
|
|
434
|
-
skipped_metadata_mismatch: 0, capped: 0, failed: 0,
|
|
435
|
-
};
|
|
436
|
-
}
|
|
437
453
|
try {
|
|
438
454
|
const plan = planDedup(db, threshold);
|
|
439
455
|
const report = {
|
|
440
456
|
threshold,
|
|
441
|
-
max_merges: maxMerges,
|
|
442
457
|
clusters_found: plan.clusterCount,
|
|
443
458
|
mergeable_clusters: plan.mergePlans.length,
|
|
444
459
|
merged_clusters: 0,
|
|
445
460
|
losers_merged: 0,
|
|
446
461
|
links_repointed: 0,
|
|
447
462
|
skipped_metadata_mismatch: plan.mismatchSkipped.length,
|
|
463
|
+
skipped_conflict: plan.conflictSkipped.length,
|
|
448
464
|
capped: 0,
|
|
449
465
|
failed: 0,
|
|
450
466
|
};
|
|
@@ -467,7 +483,7 @@ async function runDeterministicMergeZone(db, opts = {}) {
|
|
|
467
483
|
const label = `>=${threshold}`;
|
|
468
484
|
const bands = s.resolutionBandStats ?? {};
|
|
469
485
|
const b = bands[label] ?? {
|
|
470
|
-
pairs: 0, merge: 0, corrects: 0, supersedes: 0, none: 0,
|
|
486
|
+
pairs: 0, merge: 0, corrects: 0, supersedes: 0, conflicts: 0, none: 0,
|
|
471
487
|
merge_below_gate: 0, conf_sum: 0,
|
|
472
488
|
};
|
|
473
489
|
bands[label] = {
|
|
@@ -514,13 +530,23 @@ async function runDeterministicMergeZone(db, opts = {}) {
|
|
|
514
530
|
return report;
|
|
515
531
|
}
|
|
516
532
|
report.backup_path = backupPath;
|
|
517
|
-
// Discovery order
|
|
518
|
-
//
|
|
519
|
-
//
|
|
520
|
-
//
|
|
521
|
-
const toAttempt = plan.mergePlans
|
|
522
|
-
|
|
523
|
-
|
|
533
|
+
// Discovery order. #405: no pacing slice — the deadline stop-check
|
|
534
|
+
// below is the only bound (the deferred clusters count as
|
|
535
|
+
// deadline_deferred/capped and drain next run; re-discovery is
|
|
536
|
+
// content-based, so they re-appear).
|
|
537
|
+
const toAttempt = plan.mergePlans;
|
|
538
|
+
for (let pi = 0; pi < toAttempt.length; pi++) {
|
|
539
|
+
const p = toAttempt[pi];
|
|
540
|
+
// #405: stop-check between cluster merges — each merge is a local
|
|
541
|
+
// transaction, so this is a safe boundary. The un-attempted clusters
|
|
542
|
+
// drain next run (discovery is content-based, so they re-appear).
|
|
543
|
+
if (opts.deadline?.hit("dedup_merge_zone")) {
|
|
544
|
+
report.deadline_deferred = toAttempt.length - pi;
|
|
545
|
+
report.capped += toAttempt.length - pi;
|
|
546
|
+
console.warn(`[hicortex] deterministic-merge zone: run deadline reached — ` +
|
|
547
|
+
`${report.deadline_deferred} cluster merge(s) deferred to the next run`);
|
|
548
|
+
break;
|
|
549
|
+
}
|
|
524
550
|
try {
|
|
525
551
|
const tx = db.transaction(() => mergeCluster(db, p.canonical, p.losers));
|
|
526
552
|
const appliedPlan = tx();
|
|
@@ -537,8 +563,9 @@ async function runDeterministicMergeZone(db, opts = {}) {
|
|
|
537
563
|
}
|
|
538
564
|
console.log(`[hicortex] deterministic-merge zone (>= ${threshold}): ${report.merged_clusters}/${plan.mergePlans.length} ` +
|
|
539
565
|
`cluster(s) merged, ${report.losers_merged} loser(s) absorbed, ` +
|
|
540
|
-
`${report.skipped_metadata_mismatch} skipped (metadata mismatch)` +
|
|
541
|
-
|
|
566
|
+
`${report.skipped_metadata_mismatch} skipped (metadata mismatch), ` +
|
|
567
|
+
`${report.skipped_conflict} skipped (conflict-flagged)` +
|
|
568
|
+
(report.capped > 0 ? `, ${report.capped} deferred (run deadline)` : "") +
|
|
542
569
|
(report.failed > 0 ? `, ${report.failed} FAILED` : ""));
|
|
543
570
|
persistBand();
|
|
544
571
|
return report;
|
|
@@ -553,9 +580,9 @@ async function runDeterministicMergeZone(db, opts = {}) {
|
|
|
553
580
|
console.error(`[hicortex] deterministic-merge zone failed: ${err instanceof Error ? err.message : String(err)} ` +
|
|
554
581
|
`(no merges attempted; retried next run).`);
|
|
555
582
|
return {
|
|
556
|
-
threshold,
|
|
583
|
+
threshold, clusters_found: 0, mergeable_clusters: 0,
|
|
557
584
|
merged_clusters: 0, losers_merged: 0, links_repointed: 0,
|
|
558
|
-
skipped_metadata_mismatch: 0, capped: 0, failed: 0,
|
|
585
|
+
skipped_metadata_mismatch: 0, skipped_conflict: 0, capped: 0, failed: 0,
|
|
559
586
|
};
|
|
560
587
|
}
|
|
561
588
|
}
|
|
@@ -577,7 +604,7 @@ async function runDedup(options = {}) {
|
|
|
577
604
|
throw new Error("[hicortex] dedup is server-mode only (it needs the local DB). " +
|
|
578
605
|
`This machine is a client of ${config.serverUrl ?? "a remote server"} — run dedup on the server.`);
|
|
579
606
|
}
|
|
580
|
-
const threshold = resolveThreshold(options.threshold
|
|
607
|
+
const threshold = resolveThreshold(options.threshold);
|
|
581
608
|
const apply = options.apply ?? false;
|
|
582
609
|
const dbPath = (0, db_js_1.resolveDbPath)(options.dbPath);
|
|
583
610
|
const db = (0, db_js_1.initDb)(dbPath);
|
|
@@ -614,11 +641,13 @@ async function runDedup(options = {}) {
|
|
|
614
641
|
clusterCount: plan.clusterCount,
|
|
615
642
|
mergeable,
|
|
616
643
|
mismatchSkipped: plan.mismatchSkipped,
|
|
644
|
+
conflictSkipped: plan.conflictSkipped,
|
|
617
645
|
plannedMerges,
|
|
618
646
|
linksSkippedExisting: linksSkippedExistingPreview,
|
|
619
647
|
};
|
|
620
648
|
console.log(`[hicortex] dedup: ${plan.clusterCount} cluster(s) found, ${mergeable.length} mergeable ` +
|
|
621
649
|
`(${plannedMerges} row(s) would be absorbed), ${plan.mismatchSkipped.length} skipped (metadata mismatch), ` +
|
|
650
|
+
`${plan.conflictSkipped.length} skipped (conflict-flagged), ` +
|
|
622
651
|
`${linksSkippedExistingPreview} link(s) would be skipped (existing edge on the canonical)`);
|
|
623
652
|
if (!apply) {
|
|
624
653
|
for (const c of mergeable) {
|
|
@@ -634,6 +663,11 @@ async function runDedup(options = {}) {
|
|
|
634
663
|
.join(", ");
|
|
635
664
|
console.log(`[hicortex] SKIPPED (${reasons}): ${c.memberIds.map((id) => id.slice(0, 8)).join(", ")}`);
|
|
636
665
|
}
|
|
666
|
+
// #393 guard-C: listed for review like the mismatch clusters — a
|
|
667
|
+
// conflicts-linked near-duplicate pair is deliberate, not an error.
|
|
668
|
+
for (const c of plan.conflictSkipped) {
|
|
669
|
+
console.log(`[hicortex] SKIPPED (conflict-flagged): ${c.memberIds.map((id) => id.slice(0, 8)).join(", ")}`);
|
|
670
|
+
}
|
|
637
671
|
return report;
|
|
638
672
|
}
|
|
639
673
|
// --apply: acquire the single-flight capture lock so a concurrent
|
package/dist/distiller.js
CHANGED
|
@@ -381,7 +381,7 @@ async function distillChunk(llm, transcript, projectName, date, onUsage) {
|
|
|
381
381
|
// failures, 4xx/5xx, model-not-found, timeouts) propagate up to the caller
|
|
382
382
|
// so the nightly pipeline can treat them as "retry later" instead of
|
|
383
383
|
// "processed successfully with zero extractions".
|
|
384
|
-
const { text: result, usage } = await llm.
|
|
384
|
+
const { text: result, usage } = await llm.complete(prompt);
|
|
385
385
|
// #5: report this chunk's token usage to the caller's budget meter. Optional
|
|
386
386
|
// (absent for callers that don't meter); a missing/undefined usage (claude-cli)
|
|
387
387
|
// is a no-op — consistent with the existing design that such tenants never
|
|
@@ -25,16 +25,17 @@
|
|
|
25
25
|
* there is NO fallback category in the vocabulary and no configured domain is
|
|
26
26
|
* ever auto-assigned on a no-fit. A genuine no-fit is the distinct result
|
|
27
27
|
* `{tags: []}`; the CALLER then derives a weak primary from prototype cosines
|
|
28
|
-
* (>= the
|
|
29
|
-
* (see nofit.ts). If a user still configures
|
|
30
|
-
* just a normal domain with no special
|
|
28
|
+
* (>= the weak-primary floor, release-managed since #408) or, below the
|
|
29
|
+
* floor, applies accelerated decay (see nofit.ts). If a user still configures
|
|
30
|
+
* a domain named "Unsorted", it is just a normal domain with no special
|
|
31
|
+
* semantics.
|
|
31
32
|
*
|
|
32
33
|
* The `project` name is passed to the classifier as a HINT (content wins;
|
|
33
34
|
* project only breaks ties). This rescues terse technical memories from
|
|
34
35
|
* projects whose content alone reads as ambiguous.
|
|
35
36
|
*
|
|
36
37
|
* The classifier makes ONE constrained LLM call per memory (via the one model
|
|
37
|
-
* #231 —
|
|
38
|
+
* #231/#405 — the one complete() surface,
|
|
38
39
|
* validates every returned name against the configured vocabulary
|
|
39
40
|
* (case-insensitive), and retries once on an invalid/unparseable reply.
|
|
40
41
|
*
|
|
@@ -141,8 +142,8 @@ export declare function parseTagReply(reply: string, domains: DomainDef[]): TagR
|
|
|
141
142
|
/**
|
|
142
143
|
* Multi-tag classify one memory's content against the configured vocabulary.
|
|
143
144
|
*
|
|
144
|
-
* Uses the one model (
|
|
145
|
-
*
|
|
145
|
+
* Uses the one model (the single complete() surface, #231/#405).
|
|
146
|
+
* Per-memory classification failures return null (issue
|
|
146
147
|
* #150): the caller leaves the memory unclassified and the cursor advances,
|
|
147
148
|
* so a re-run retries it.
|
|
148
149
|
*
|
package/dist/domain-classify.js
CHANGED
|
@@ -26,16 +26,17 @@
|
|
|
26
26
|
* there is NO fallback category in the vocabulary and no configured domain is
|
|
27
27
|
* ever auto-assigned on a no-fit. A genuine no-fit is the distinct result
|
|
28
28
|
* `{tags: []}`; the CALLER then derives a weak primary from prototype cosines
|
|
29
|
-
* (>= the
|
|
30
|
-
* (see nofit.ts). If a user still configures
|
|
31
|
-
* just a normal domain with no special
|
|
29
|
+
* (>= the weak-primary floor, release-managed since #408) or, below the
|
|
30
|
+
* floor, applies accelerated decay (see nofit.ts). If a user still configures
|
|
31
|
+
* a domain named "Unsorted", it is just a normal domain with no special
|
|
32
|
+
* semantics.
|
|
32
33
|
*
|
|
33
34
|
* The `project` name is passed to the classifier as a HINT (content wins;
|
|
34
35
|
* project only breaks ties). This rescues terse technical memories from
|
|
35
36
|
* projects whose content alone reads as ambiguous.
|
|
36
37
|
*
|
|
37
38
|
* The classifier makes ONE constrained LLM call per memory (via the one model
|
|
38
|
-
* #231 —
|
|
39
|
+
* #231/#405 — the one complete() surface,
|
|
39
40
|
* validates every returned name against the configured vocabulary
|
|
40
41
|
* (case-insensitive), and retries once on an invalid/unparseable reply.
|
|
41
42
|
*
|
|
@@ -238,8 +239,8 @@ function parseTagReply(reply, domains) {
|
|
|
238
239
|
/**
|
|
239
240
|
* Multi-tag classify one memory's content against the configured vocabulary.
|
|
240
241
|
*
|
|
241
|
-
* Uses the one model (
|
|
242
|
-
*
|
|
242
|
+
* Uses the one model (the single complete() surface, #231/#405).
|
|
243
|
+
* Per-memory classification failures return null (issue
|
|
243
244
|
* #150): the caller leaves the memory unclassified and the cursor advances,
|
|
244
245
|
* so a re-run retries it.
|
|
245
246
|
*
|
|
@@ -273,10 +274,11 @@ async function classifyMemoryTags(content, project, domains, llm, onUsage) {
|
|
|
273
274
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
274
275
|
let raw;
|
|
275
276
|
try {
|
|
276
|
-
// No per-call cap (#391): the
|
|
277
|
-
//
|
|
278
|
-
// starved reasoning models whose
|
|
279
|
-
|
|
277
|
+
// No per-call cap (#391/#405): maxTokens — the ONE ceiling — resolves
|
|
278
|
+
// inside complete(); the old hardcoded 64 (and later the separate
|
|
279
|
+
// classifyMaxTokens tier ceiling) starved reasoning models whose
|
|
280
|
+
// thinking ate the whole output budget.
|
|
281
|
+
const r = await llm.complete(prompt);
|
|
280
282
|
raw = r.text;
|
|
281
283
|
threw = false;
|
|
282
284
|
// Surface the usage ONLY when this attempt's reply parses (below). Hold
|
|
@@ -40,9 +40,9 @@ export interface PruneDryRunReport {
|
|
|
40
40
|
/**
|
|
41
41
|
* Run the actual `stageDecayPrune` (imported from consolidate.ts, `dryRun:
|
|
42
42
|
* true`) against the snapshot. Configures the decay clock to the given
|
|
43
|
-
* half-life first (
|
|
44
|
-
* caller should pass the shipped default
|
|
45
|
-
* production score with the same clock.
|
|
43
|
+
* half-life first via the eval seam (#408: the half-life is a calibration
|
|
44
|
+
* constant — the caller should pass the shipped default, see run-eval.ts) so
|
|
45
|
+
* the eval and production score with the same clock.
|
|
46
46
|
*/
|
|
47
47
|
export declare function runPruneDryRun(db: Database.Database, decayHalfLifeDays?: number): PruneDryRunReport;
|
|
48
48
|
/**
|