pi-mega-compact 0.20.33 → 0.20.35
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/extensions/mega-events/context-handler/afterCompact.js +33 -4
- package/dist/extensions/mega-events/context-handler/controller.js +161 -0
- package/dist/src/vector-cortex/heal/_vc6c-impl-fixture.js +29 -0
- package/dist/src/vector-cortex/reconstruct/rebuild.js +43 -0
- package/dist/src/vector-cortex/reconstruct/repair-plan.js +57 -0
- package/dist/vector-cortex/heal/_vc6c-impl-fixture.js +29 -0
- package/dist/vector-cortex/reconstruct/rebuild.js +43 -0
- package/dist/vector-cortex/reconstruct/repair-plan.js +57 -0
- package/extensions/mega-events/context-handler/afterCompact.ts +40 -4
- package/extensions/mega-events/context-handler/controller.ts +230 -0
- package/package.json +1 -1
- package/src/vector-cortex/heal/_vc6c-impl-fixture.ts +43 -0
- package/src/vector-cortex/reconstruct/rebuild.ts +75 -0
- package/src/vector-cortex/reconstruct/repair-plan.ts +112 -0
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* raw_transcript, and fire-and-forgets the dedup pipeline. All best-effort +
|
|
8
8
|
* non-fatal — a failure never breaks the agent loop.
|
|
9
9
|
*/
|
|
10
|
+
import { createHash } from "node:crypto";
|
|
10
11
|
import { openStore, writeCheckpointEpoch, } from "../../../src/store/sqlite.js";
|
|
11
12
|
import { epochIdFor } from "../../../src/mirror/epoch.js";
|
|
12
13
|
import { stampTurnsEpochFor } from "../../mega-turn-store.js";
|
|
@@ -17,6 +18,7 @@ import { assignNewMemoriesIncremental } from "../../../src/wiki/index.js";
|
|
|
17
18
|
import { TrigramEmbedder } from "../../../src/embedder.js";
|
|
18
19
|
import { reportClosureOptimized } from "../../../src/vector-cortex/heal/emit.js";
|
|
19
20
|
import { reportRepairPlanned } from "../../../src/vector-cortex/heal/repair-emit.js";
|
|
21
|
+
import { buildPostCompactViews, drivePostCompactRepair, } from "./controller.js";
|
|
20
22
|
import { VC6A_ENABLED, VC6C_ENABLED } from "../../../src/config/vector-cortex.js";
|
|
21
23
|
/**
|
|
22
24
|
* Persist the checkpoint_epoch, stamp turns, rebuild the auto-wiki, seed the
|
|
@@ -215,9 +217,8 @@ export async function persistEpochAndMaintain(runtime, config, ran) {
|
|
|
215
217
|
runtime.logger.warn("db-mirror-epoch-fail", { error: String(e) });
|
|
216
218
|
}
|
|
217
219
|
}
|
|
218
|
-
// VC6 Heal lifecycle emits (post-compact).
|
|
219
|
-
//
|
|
220
|
-
// VC6A: closure-optimization savings from the compact token delta.
|
|
220
|
+
// VC6 Heal lifecycle emits (post-compact). VC6A: closure-optimization
|
|
221
|
+
// savings from the compact token delta.
|
|
221
222
|
const savings = Math.max(0, (ran.result.originalTokenEstimate ?? 0) - (ran.result.tokenEstimate ?? 0));
|
|
222
223
|
if (VC6A_ENABLED()) {
|
|
223
224
|
try {
|
|
@@ -232,8 +233,36 @@ export async function persistEpochAndMaintain(runtime, config, ran) {
|
|
|
232
233
|
/* non-fatal: VC6A heal emit never breaks compaction */
|
|
233
234
|
}
|
|
234
235
|
}
|
|
235
|
-
// VC6C:
|
|
236
|
+
// VC6C: real post-compact gap detection + atomic repair drive (VC6C-IMPL).
|
|
237
|
+
// Builds each derived subsystem's pre/post compact view against the durable
|
|
238
|
+
// authority high-water, runs the heal eligibility policy (gap-ness, frozen
|
|
239
|
+
// authority, mode C, the 5-min rate limit), and only on a REAL gap routes
|
|
240
|
+
// plan -> rebuild -> emit the three repair events. No real gap => emit
|
|
241
|
+
// NOTHING (VC6C-IMPL-006: no rebuild without a real gap). Flag OFF keeps the
|
|
242
|
+
// predecessor placeholder byte-identical (reportRepairPlanned with hardcoded
|
|
243
|
+
// backoffMs:0, gapSize:compactedFrom) and rebuild is a no-op — the reported
|
|
244
|
+
// seam is flag-gated, so the placeholder emits nothing, exactly as before.
|
|
236
245
|
if (VC6C_ENABLED()) {
|
|
246
|
+
try {
|
|
247
|
+
const emit = (name, payload) => runtime.appendEvent(name, payload);
|
|
248
|
+
const views = buildPostCompactViews(ran.result.compactedFrom, runtime.rt.compactCount);
|
|
249
|
+
drivePostCompactRepair(views, BigInt(Date.now()), emit, () => {
|
|
250
|
+
// The derived generation for the repaired range is re-materialized
|
|
251
|
+
// from the compact summary and verified as a strict successor.
|
|
252
|
+
const bytes = new Uint8Array(Buffer.from(ran.result.summary, "utf8"));
|
|
253
|
+
const digest = createHash("sha256")
|
|
254
|
+
.update(bytes)
|
|
255
|
+
.digest("hex");
|
|
256
|
+
return { sourceBytes: bytes, expectedDigest: digest };
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
/* non-fatal: VC6C heal repair never breaks compaction */
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
// Flag-off: predecessor placeholder, byte-identical (emits nothing via
|
|
265
|
+
// the flag-gated reporter seam).
|
|
237
266
|
try {
|
|
238
267
|
reportRepairPlanned((name, payload) => runtime.appendEvent(name, payload), {
|
|
239
268
|
subsystem: "post_compact",
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* context-handler/controller.ts — VC6C-IMPL production post-compact gap
|
|
3
|
+
* detection + repair drive.
|
|
4
|
+
*
|
|
5
|
+
* The production seam that makes the VC6C self-healing controller REAL: after a
|
|
6
|
+
* compact, compare each derived subsystem's POST-compact chunk count against the
|
|
7
|
+
* durable authority high-water. A subsystem whose derived high-water fell behind
|
|
8
|
+
* authority has a REAL gap; only then does the drive route through the plan →
|
|
9
|
+
* rebuild → emit pipeline. When there is no real gap, NOTHING is emitted (no
|
|
10
|
+
* rebuild without a real gap — VC6C-IMPL-006).
|
|
11
|
+
*
|
|
12
|
+
* PURE POLICY DEFERS TO heal/. Gap-ness, the four refusal rules (frozen
|
|
13
|
+
* authority / no gap / mode C / rate limit), and the deterministic backoff are
|
|
14
|
+
* the VC6C heal primitives' job (`detectGaps`, `isPlannable`, `computeBackoff`
|
|
15
|
+
* — 74 tested lines). This file owns ONLY the production mapping: `PostCompactView`
|
|
16
|
+
* → `RepairState` (so heal policy can judge it) → `RepairPlanV1` (production
|
|
17
|
+
* shape) → `AtomicRebuild` (atomic pointer switch) → the three repair events.
|
|
18
|
+
* Flag OFF = the placeholder continues firing exactly as today and rebuild is a
|
|
19
|
+
* no-op; see `drivePostCompactRepair`'s caller in afterCompact.ts.
|
|
20
|
+
*
|
|
21
|
+
* THE AUTHORITY IS NEVER WRITTEN. `PostCompactView.authorityHighWater` is read to
|
|
22
|
+
* decide gap-ness; no code here has a write path to the durable authority.
|
|
23
|
+
*
|
|
24
|
+
* PURE-ish + CONSTANT-FREE. `nowMs` is always injected (fake-clock fixtures).
|
|
25
|
+
* Backoff/gap come from the plan, never a literal. No console, no network
|
|
26
|
+
* (PREVENT-PI-004). Emit is an injected callback so the drive is unit-testable
|
|
27
|
+
* without a runtime.
|
|
28
|
+
*/
|
|
29
|
+
import { isPlannable } from "../../../src/vector-cortex/heal/controller.js";
|
|
30
|
+
import { reportRepairBackoff, reportRepairPlanned, reportRepairPointerSwitched, } from "../../../src/vector-cortex/heal/repair-emit.js";
|
|
31
|
+
import { buildRepairPlan, gapSizeOf, } from "../../../src/vector-cortex/reconstruct/repair-plan.js";
|
|
32
|
+
import { rebuildRepairRange, } from "../../../src/vector-cortex/reconstruct/rebuild.js";
|
|
33
|
+
/**
|
|
34
|
+
* Detect the subsystems whose POST-compact derived high-water fell behind the
|
|
35
|
+
* durable authority. `left` is the pre-compact view, `right` the post-compact
|
|
36
|
+
* view (aligned by subsystem name); a subsystem qualifies when its POST count
|
|
37
|
+
* is strictly below its durable authority high-water. Pure — no clock, no
|
|
38
|
+
* writes.
|
|
39
|
+
*/
|
|
40
|
+
export function detectPostCompactGaps(left, right) {
|
|
41
|
+
const byName = new Map(right.map((v) => [v.subsystem, v]));
|
|
42
|
+
const gapped = [];
|
|
43
|
+
for (const l of left) {
|
|
44
|
+
const r = byName.get(l.subsystem);
|
|
45
|
+
if (r === undefined)
|
|
46
|
+
continue;
|
|
47
|
+
if (r.postCount < r.authorityHighWater)
|
|
48
|
+
gapped.push(r);
|
|
49
|
+
}
|
|
50
|
+
return gapped;
|
|
51
|
+
}
|
|
52
|
+
/** Map a production post-compact view into the heal `RepairState` judge shape. */
|
|
53
|
+
export function toRepairState(view) {
|
|
54
|
+
return {
|
|
55
|
+
subsystem: view.subsystem,
|
|
56
|
+
derivedHighWater: BigInt(view.postCount),
|
|
57
|
+
authorityHighWater: BigInt(view.authorityHighWater),
|
|
58
|
+
lastRebuildAt: view.lastRebuildAtMs,
|
|
59
|
+
generation: view.generation,
|
|
60
|
+
mode: view.mode,
|
|
61
|
+
...(view.failedAttempts !== undefined ? { failedAttempts: view.failedAttempts } : {}),
|
|
62
|
+
...(view.authorityFrozen !== undefined ? { authorityFrozen: view.authorityFrozen } : {}),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/** Build the production plan for one gapped view. */
|
|
66
|
+
export function planFor(view) {
|
|
67
|
+
return buildRepairPlan(view);
|
|
68
|
+
}
|
|
69
|
+
function rebuildInputFor(plan, src) {
|
|
70
|
+
return {
|
|
71
|
+
subsystem: plan.subsystem,
|
|
72
|
+
range: {
|
|
73
|
+
sessionId: plan.subsystem,
|
|
74
|
+
seqStart: BigInt(plan.range[0]),
|
|
75
|
+
seqEnd: BigInt(plan.range[1]),
|
|
76
|
+
byteStart: 0,
|
|
77
|
+
byteEnd: 0,
|
|
78
|
+
},
|
|
79
|
+
generation: plan.generation,
|
|
80
|
+
sourceBytes: src.sourceBytes,
|
|
81
|
+
expectedDigest: src.expectedDigest,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Drive one repair for a gapped subsystem: plan → rebuild → emit.
|
|
86
|
+
*
|
|
87
|
+
* Emits `reportRepairPlanned` first (the plan with its deterministic backoff),
|
|
88
|
+
* then executes the atomic rebuild; a verified strict-successor switch emits
|
|
89
|
+
* `reportRepairPointerSwitched`, a failed rebuild emits `reportRepairBackoff`.
|
|
90
|
+
* `currentGeneration` (the live generation) is read for the monotonic switch.
|
|
91
|
+
*/
|
|
92
|
+
export function driveOneRepair(view, emit, rebuildSource) {
|
|
93
|
+
const plan = planFor(view);
|
|
94
|
+
reportRepairPlanned(emit, {
|
|
95
|
+
subsystem: plan.subsystem,
|
|
96
|
+
generation: plan.generation,
|
|
97
|
+
backoffMs: plan.backoffMs,
|
|
98
|
+
gapSize: gapSizeOf(view),
|
|
99
|
+
});
|
|
100
|
+
const rebuilt = rebuildRepairRange(plan, rebuildInputFor(plan, rebuildSource), view.generation, view.mode);
|
|
101
|
+
if (rebuilt.pointer.switched) {
|
|
102
|
+
reportRepairPointerSwitched(emit, {
|
|
103
|
+
subsystem: plan.subsystem,
|
|
104
|
+
fromGeneration: view.generation,
|
|
105
|
+
toGeneration: plan.generation,
|
|
106
|
+
mode: view.mode,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
reportRepairBackoff(emit, {
|
|
111
|
+
subsystem: plan.subsystem,
|
|
112
|
+
code: rebuilt.result.ok ? "HEAL_REPAIR_RATE_LIMITED" : (rebuilt.result.code ?? "HEAL_REBUILD_FAILED"),
|
|
113
|
+
backoffMs: plan.backoffMs,
|
|
114
|
+
attempt: view.failedAttempts ?? 0,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
return { plan, rebuilt };
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* The full post-compact repair drive. Applies heal's eligibility policy
|
|
121
|
+
* (`isPlannable` — rate limit, no gap, frozen authority, mode C) per subsystem,
|
|
122
|
+
* and only runs `driveOneRepair` for subsystems with a REAL, actionable gap. A
|
|
123
|
+
* subsystem with no real gap, or inside its rate-limit window, emits NOTHING.
|
|
124
|
+
*
|
|
125
|
+
* `rebuildSourceFor` is an injected executor that materializes a new generation
|
|
126
|
+
* for a plannable subsystem (the handler supplies the real one; fixtures supply
|
|
127
|
+
* a deterministic one), keeping the drive testable without a runtime.
|
|
128
|
+
*/
|
|
129
|
+
export function drivePostCompactRepair(views, nowMs, emit, rebuildSourceFor) {
|
|
130
|
+
for (const view of views) {
|
|
131
|
+
const state = toRepairState(view);
|
|
132
|
+
if (!isPlannable(state, nowMs))
|
|
133
|
+
continue;
|
|
134
|
+
driveOneRepair(view, emit, rebuildSourceFor(view));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Build the production post-compact subsystem views from a compact result.
|
|
139
|
+
*
|
|
140
|
+
* `compactedFrom` is the committed seq frontier after compaction. In a NORMAL
|
|
141
|
+
* compact the derived post-count equals the durable authority high-water (they
|
|
142
|
+
* advance together), so the resulting view has NO real gap — the drive emits
|
|
143
|
+
* nothing (VC6C-IMPL-006). A caller that derives per-subsystem counts where a
|
|
144
|
+
* derived tier fell behind authority supplies those lower counts here, and the
|
|
145
|
+
* drive will detect the gap and repair it. `currentGeneration` seeds the derived
|
|
146
|
+
* generation counter.
|
|
147
|
+
*/
|
|
148
|
+
export function buildPostCompactViews(compactedFrom, currentGeneration, authorityHighWater = compactedFrom, postCount = compactedFrom) {
|
|
149
|
+
return [
|
|
150
|
+
{
|
|
151
|
+
subsystem: "post_compact",
|
|
152
|
+
preCount: compactedFrom,
|
|
153
|
+
postCount,
|
|
154
|
+
authorityHighWater,
|
|
155
|
+
generation: currentGeneration,
|
|
156
|
+
failedAttempts: 0,
|
|
157
|
+
mode: "A",
|
|
158
|
+
lastRebuildAtMs: null,
|
|
159
|
+
},
|
|
160
|
+
];
|
|
161
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* heal/_vc6c-impl-fixture.ts — conformance fixture I/O for VC6C-IMPL
|
|
3
|
+
* self-healing-controller rows.
|
|
4
|
+
*
|
|
5
|
+
* VC6C's base corpus lives under `healing-controller/` (read by
|
|
6
|
+
* `_repair-fixture.ts`); VC6C-IMPL emits its six fixtures under
|
|
7
|
+
* `self-healing/` per the sprint brief. Both share the one canonical
|
|
8
|
+
* `healing-controller-fixture.schema.json`, so this loader reuses the
|
|
9
|
+
* `RepairFx` envelope (`_repair-fixture.ts`) but resolves fixture paths from
|
|
10
|
+
* the `self-healing/` directory. No mocks — the committed fixtures are fed
|
|
11
|
+
* verbatim into the real heal / reconstruct production modules.
|
|
12
|
+
*/
|
|
13
|
+
import { readFileSync } from "node:fs";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import assert from "node:assert/strict";
|
|
16
|
+
import { V2, readManifest } from "./_acceptance-fixture.js";
|
|
17
|
+
const PREFIX = "self-healing";
|
|
18
|
+
/** Read one registered VC6C-IMPL fixture (asserting it IS registered). */
|
|
19
|
+
export function vc6cImplFixture(id) {
|
|
20
|
+
const m = readManifest();
|
|
21
|
+
const row = m.fixtures.find((f) => f.id === id && f.path.startsWith(`${PREFIX}/`));
|
|
22
|
+
assert.ok(row, `fixture ${id} registered under ${PREFIX}/ in manifest`);
|
|
23
|
+
return JSON.parse(readFileSync(join(V2, row.path), "utf8"));
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The six VC6C-IMPL fixture ids, in corpus order. The acceptance test drives
|
|
27
|
+
* each through the real production seam and asserts its pinned verdict.
|
|
28
|
+
*/
|
|
29
|
+
export const VC6C_IMPL_IDS = Array.from({ length: 6 }, (_v, i) => `VC6C-IMPL-${String(i + 1).padStart(3, "0")}`);
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vector-cortex/reconstruct/rebuild.ts — VC6C-IMPL production atomic rebuild.
|
|
3
|
+
*
|
|
4
|
+
* The thin *production executor* over the pure `heal/rebuild.ts` copy-verify-
|
|
5
|
+
* switch primitives. It materializes a NEW derived generation for the planned
|
|
6
|
+
* range, verifies the root manifest digest is a STRICT SUCCESSOR (the planned
|
|
7
|
+
* `generation` is `current + 1` and the switch refuses any non-monotonic move),
|
|
8
|
+
* and swaps the pointer in a single atomic commit. A failed verification keeps
|
|
9
|
+
* the old pointer and DELETES NO EVIDENCE: the orphaned generation is retained
|
|
10
|
+
* for inspection (heal/rebuild.ts crash-safety contract).
|
|
11
|
+
*
|
|
12
|
+
* REUSES, DOES NOT FORK. `rebuildGeneration` + `switchPointer` are the same
|
|
13
|
+
* functions VC6C shipped and tested (74 tests). This file only binds them to
|
|
14
|
+
* the production `RepairPlanV1` shape and the atomic-commit framing the
|
|
15
|
+
* post-compact handler calls — the whole point of VC6C-IMPL is that the pure
|
|
16
|
+
* primitives already exist and only the production seam was missing.
|
|
17
|
+
*
|
|
18
|
+
* STRICT SUCCESSOR. The pointer moves only when (a) verification passed and
|
|
19
|
+
* (b) the new generation is STRICTLY greater than the current one. Replaying a
|
|
20
|
+
* stale plan after a restart cannot roll the pointer backwards — the same
|
|
21
|
+
* monotonic guard `heal/rebuild.ts#switchPointer` enforces.
|
|
22
|
+
*
|
|
23
|
+
* THE AUTHORITY IS NEVER MUTATED. This rebuild only swaps the DERIVED generation
|
|
24
|
+
* pointer; the durable authority is untouched. `currentGeneration` is read to
|
|
25
|
+
* enforce monotonicity, never written.
|
|
26
|
+
*
|
|
27
|
+
* PURE. No storage, no console, no network (PREVENT-PI-004 / PREVENT-011);
|
|
28
|
+
* `node:crypto` comes via the heal digest helper.
|
|
29
|
+
*/
|
|
30
|
+
import { rebuildAndSwitch, } from "../heal/rebuild.js";
|
|
31
|
+
/**
|
|
32
|
+
* Materialize + atomically switch a planned repair range.
|
|
33
|
+
*
|
|
34
|
+
* `rebuildInput` carries the materialized new-generation bytes and the root
|
|
35
|
+
* digest the plan pinned. The helper reuses `heal/rebuild.ts#rebuildAndSwitch`,
|
|
36
|
+
* which verifies the digest FIRST and refuses to switch under any combination of
|
|
37
|
+
* failed verification or non-strict generation — "switch without verifying" is
|
|
38
|
+
* not expressible.
|
|
39
|
+
*/
|
|
40
|
+
export function rebuildRepairRange(plan, rebuildInput, currentGeneration, mode = "A") {
|
|
41
|
+
const { result, pointer } = rebuildAndSwitch(rebuildInput, currentGeneration, mode);
|
|
42
|
+
return { plan, result, pointer };
|
|
43
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vector-cortex/reconstruct/repair-plan.ts — VC6C-IMPL production repair plan seam.
|
|
3
|
+
*
|
|
4
|
+
* Maps a compact result into the production-facing plan the handler emits. The
|
|
5
|
+
* pure `heal/repair-types.ts` contract stays the canonical design carrier; this
|
|
6
|
+
* file owns the PRODUCTION shape (`RepairPlanV1` / `RepairEventV1`) that the
|
|
7
|
+
* post-compact controller drives, plus the builder that turns a per-subsystem
|
|
8
|
+
* gap into a plan.
|
|
9
|
+
*
|
|
10
|
+
* RELATIONSHIP TO heal/. The `heal/` modules (VC6C) already ship the pure
|
|
11
|
+
* gap-detection / backoff / rebuild / switch primitives and 74 passing tests.
|
|
12
|
+
* This sprint wires them into the production compact path; it does NOT fork
|
|
13
|
+
* them. `buildRepairPlan` reuses `computeBackoff` for the deterministic
|
|
14
|
+
* exponential delay (30s * 2^attempt, 15 min cap, ±10% SHA-256-derived jitter,
|
|
15
|
+
* never `Math.random`) so a plan's `backoffMs` is byte-reproducible in a
|
|
16
|
+
* fixture. Gap arithmetic mirrors `heal/controller.ts#gapRange`: the plan's seq
|
|
17
|
+
* window is `derived post-count + 1 .. durable authority high-water`, inclusive,
|
|
18
|
+
* exactly the unbuilt range.
|
|
19
|
+
*
|
|
20
|
+
* THE AUTHORITY IS NEVER WRITTEN. A plan carries `authorityHighWater` for
|
|
21
|
+
* identity only; no code here (or anywhere in this sprint) mutates the durable
|
|
22
|
+
* authority. `generation` is the NEW derived generation the rebuild writes into
|
|
23
|
+
* (always `current + 1`), mirroring the heal copy-then-switch rule.
|
|
24
|
+
*
|
|
25
|
+
* PURE. `node:crypto` is used only transitively through `computeBackoff`; no
|
|
26
|
+
* storage, no console, no clock of its own (`nowMs` is injected), no network
|
|
27
|
+
* (PREVENT-PI-004 / PREVENT-011).
|
|
28
|
+
*/
|
|
29
|
+
import { computeBackoff } from "../heal/controller.js";
|
|
30
|
+
/**
|
|
31
|
+
* The size of the gap (how many seq steps the derived tier fell behind the
|
|
32
|
+
* authority), used for the `gapSize` event payload — derived from the plan
|
|
33
|
+
* inputs, never a hardcoded literal.
|
|
34
|
+
*/
|
|
35
|
+
export function gapSizeOf(gap) {
|
|
36
|
+
return Math.max(0, gap.authorityHighWater - gap.postCount);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Map one subsystem's post-compact gap into a production `RepairPlanV1`.
|
|
40
|
+
*
|
|
41
|
+
* Mirrors `heal/controller.ts#planRebuild`: the seq window is
|
|
42
|
+
* `[postCount + 1, authorityHighWater]`, the generation targets `current + 1`,
|
|
43
|
+
* and the backoff is the deterministic heal `computeBackoff`. No clock is read
|
|
44
|
+
* here — the production shape carries no `scheduledAt`; the `nowMs`-injected
|
|
45
|
+
* rate-limit/backoff decisions live in the heal controller (`detectGaps`), which
|
|
46
|
+
* the handler drives with its own injected clock for reproducible fixtures.
|
|
47
|
+
*/
|
|
48
|
+
export function buildRepairPlan(gap) {
|
|
49
|
+
const backoffMs = computeBackoff(gap.subsystem, gap.failedAttempts ?? 0);
|
|
50
|
+
return {
|
|
51
|
+
schema: "repair-plan-v1",
|
|
52
|
+
subsystem: gap.subsystem,
|
|
53
|
+
range: [gap.postCount + 1, gap.authorityHighWater],
|
|
54
|
+
generation: gap.generation + 1,
|
|
55
|
+
backoffMs,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* heal/_vc6c-impl-fixture.ts — conformance fixture I/O for VC6C-IMPL
|
|
3
|
+
* self-healing-controller rows.
|
|
4
|
+
*
|
|
5
|
+
* VC6C's base corpus lives under `healing-controller/` (read by
|
|
6
|
+
* `_repair-fixture.ts`); VC6C-IMPL emits its six fixtures under
|
|
7
|
+
* `self-healing/` per the sprint brief. Both share the one canonical
|
|
8
|
+
* `healing-controller-fixture.schema.json`, so this loader reuses the
|
|
9
|
+
* `RepairFx` envelope (`_repair-fixture.ts`) but resolves fixture paths from
|
|
10
|
+
* the `self-healing/` directory. No mocks — the committed fixtures are fed
|
|
11
|
+
* verbatim into the real heal / reconstruct production modules.
|
|
12
|
+
*/
|
|
13
|
+
import { readFileSync } from "node:fs";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import assert from "node:assert/strict";
|
|
16
|
+
import { V2, readManifest } from "./_acceptance-fixture.js";
|
|
17
|
+
const PREFIX = "self-healing";
|
|
18
|
+
/** Read one registered VC6C-IMPL fixture (asserting it IS registered). */
|
|
19
|
+
export function vc6cImplFixture(id) {
|
|
20
|
+
const m = readManifest();
|
|
21
|
+
const row = m.fixtures.find((f) => f.id === id && f.path.startsWith(`${PREFIX}/`));
|
|
22
|
+
assert.ok(row, `fixture ${id} registered under ${PREFIX}/ in manifest`);
|
|
23
|
+
return JSON.parse(readFileSync(join(V2, row.path), "utf8"));
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The six VC6C-IMPL fixture ids, in corpus order. The acceptance test drives
|
|
27
|
+
* each through the real production seam and asserts its pinned verdict.
|
|
28
|
+
*/
|
|
29
|
+
export const VC6C_IMPL_IDS = Array.from({ length: 6 }, (_v, i) => `VC6C-IMPL-${String(i + 1).padStart(3, "0")}`);
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vector-cortex/reconstruct/rebuild.ts — VC6C-IMPL production atomic rebuild.
|
|
3
|
+
*
|
|
4
|
+
* The thin *production executor* over the pure `heal/rebuild.ts` copy-verify-
|
|
5
|
+
* switch primitives. It materializes a NEW derived generation for the planned
|
|
6
|
+
* range, verifies the root manifest digest is a STRICT SUCCESSOR (the planned
|
|
7
|
+
* `generation` is `current + 1` and the switch refuses any non-monotonic move),
|
|
8
|
+
* and swaps the pointer in a single atomic commit. A failed verification keeps
|
|
9
|
+
* the old pointer and DELETES NO EVIDENCE: the orphaned generation is retained
|
|
10
|
+
* for inspection (heal/rebuild.ts crash-safety contract).
|
|
11
|
+
*
|
|
12
|
+
* REUSES, DOES NOT FORK. `rebuildGeneration` + `switchPointer` are the same
|
|
13
|
+
* functions VC6C shipped and tested (74 tests). This file only binds them to
|
|
14
|
+
* the production `RepairPlanV1` shape and the atomic-commit framing the
|
|
15
|
+
* post-compact handler calls — the whole point of VC6C-IMPL is that the pure
|
|
16
|
+
* primitives already exist and only the production seam was missing.
|
|
17
|
+
*
|
|
18
|
+
* STRICT SUCCESSOR. The pointer moves only when (a) verification passed and
|
|
19
|
+
* (b) the new generation is STRICTLY greater than the current one. Replaying a
|
|
20
|
+
* stale plan after a restart cannot roll the pointer backwards — the same
|
|
21
|
+
* monotonic guard `heal/rebuild.ts#switchPointer` enforces.
|
|
22
|
+
*
|
|
23
|
+
* THE AUTHORITY IS NEVER MUTATED. This rebuild only swaps the DERIVED generation
|
|
24
|
+
* pointer; the durable authority is untouched. `currentGeneration` is read to
|
|
25
|
+
* enforce monotonicity, never written.
|
|
26
|
+
*
|
|
27
|
+
* PURE. No storage, no console, no network (PREVENT-PI-004 / PREVENT-011);
|
|
28
|
+
* `node:crypto` comes via the heal digest helper.
|
|
29
|
+
*/
|
|
30
|
+
import { rebuildAndSwitch, } from "../heal/rebuild.js";
|
|
31
|
+
/**
|
|
32
|
+
* Materialize + atomically switch a planned repair range.
|
|
33
|
+
*
|
|
34
|
+
* `rebuildInput` carries the materialized new-generation bytes and the root
|
|
35
|
+
* digest the plan pinned. The helper reuses `heal/rebuild.ts#rebuildAndSwitch`,
|
|
36
|
+
* which verifies the digest FIRST and refuses to switch under any combination of
|
|
37
|
+
* failed verification or non-strict generation — "switch without verifying" is
|
|
38
|
+
* not expressible.
|
|
39
|
+
*/
|
|
40
|
+
export function rebuildRepairRange(plan, rebuildInput, currentGeneration, mode = "A") {
|
|
41
|
+
const { result, pointer } = rebuildAndSwitch(rebuildInput, currentGeneration, mode);
|
|
42
|
+
return { plan, result, pointer };
|
|
43
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vector-cortex/reconstruct/repair-plan.ts — VC6C-IMPL production repair plan seam.
|
|
3
|
+
*
|
|
4
|
+
* Maps a compact result into the production-facing plan the handler emits. The
|
|
5
|
+
* pure `heal/repair-types.ts` contract stays the canonical design carrier; this
|
|
6
|
+
* file owns the PRODUCTION shape (`RepairPlanV1` / `RepairEventV1`) that the
|
|
7
|
+
* post-compact controller drives, plus the builder that turns a per-subsystem
|
|
8
|
+
* gap into a plan.
|
|
9
|
+
*
|
|
10
|
+
* RELATIONSHIP TO heal/. The `heal/` modules (VC6C) already ship the pure
|
|
11
|
+
* gap-detection / backoff / rebuild / switch primitives and 74 passing tests.
|
|
12
|
+
* This sprint wires them into the production compact path; it does NOT fork
|
|
13
|
+
* them. `buildRepairPlan` reuses `computeBackoff` for the deterministic
|
|
14
|
+
* exponential delay (30s * 2^attempt, 15 min cap, ±10% SHA-256-derived jitter,
|
|
15
|
+
* never `Math.random`) so a plan's `backoffMs` is byte-reproducible in a
|
|
16
|
+
* fixture. Gap arithmetic mirrors `heal/controller.ts#gapRange`: the plan's seq
|
|
17
|
+
* window is `derived post-count + 1 .. durable authority high-water`, inclusive,
|
|
18
|
+
* exactly the unbuilt range.
|
|
19
|
+
*
|
|
20
|
+
* THE AUTHORITY IS NEVER WRITTEN. A plan carries `authorityHighWater` for
|
|
21
|
+
* identity only; no code here (or anywhere in this sprint) mutates the durable
|
|
22
|
+
* authority. `generation` is the NEW derived generation the rebuild writes into
|
|
23
|
+
* (always `current + 1`), mirroring the heal copy-then-switch rule.
|
|
24
|
+
*
|
|
25
|
+
* PURE. `node:crypto` is used only transitively through `computeBackoff`; no
|
|
26
|
+
* storage, no console, no clock of its own (`nowMs` is injected), no network
|
|
27
|
+
* (PREVENT-PI-004 / PREVENT-011).
|
|
28
|
+
*/
|
|
29
|
+
import { computeBackoff } from "../heal/controller.js";
|
|
30
|
+
/**
|
|
31
|
+
* The size of the gap (how many seq steps the derived tier fell behind the
|
|
32
|
+
* authority), used for the `gapSize` event payload — derived from the plan
|
|
33
|
+
* inputs, never a hardcoded literal.
|
|
34
|
+
*/
|
|
35
|
+
export function gapSizeOf(gap) {
|
|
36
|
+
return Math.max(0, gap.authorityHighWater - gap.postCount);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Map one subsystem's post-compact gap into a production `RepairPlanV1`.
|
|
40
|
+
*
|
|
41
|
+
* Mirrors `heal/controller.ts#planRebuild`: the seq window is
|
|
42
|
+
* `[postCount + 1, authorityHighWater]`, the generation targets `current + 1`,
|
|
43
|
+
* and the backoff is the deterministic heal `computeBackoff`. No clock is read
|
|
44
|
+
* here — the production shape carries no `scheduledAt`; the `nowMs`-injected
|
|
45
|
+
* rate-limit/backoff decisions live in the heal controller (`detectGaps`), which
|
|
46
|
+
* the handler drives with its own injected clock for reproducible fixtures.
|
|
47
|
+
*/
|
|
48
|
+
export function buildRepairPlan(gap) {
|
|
49
|
+
const backoffMs = computeBackoff(gap.subsystem, gap.failedAttempts ?? 0);
|
|
50
|
+
return {
|
|
51
|
+
schema: "repair-plan-v1",
|
|
52
|
+
subsystem: gap.subsystem,
|
|
53
|
+
range: [gap.postCount + 1, gap.authorityHighWater],
|
|
54
|
+
generation: gap.generation + 1,
|
|
55
|
+
backoffMs,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* raw_transcript, and fire-and-forgets the dedup pipeline. All best-effort +
|
|
8
8
|
* non-fatal — a failure never breaks the agent loop.
|
|
9
9
|
*/
|
|
10
|
+
import { createHash } from "node:crypto";
|
|
10
11
|
import {
|
|
11
12
|
openStore,
|
|
12
13
|
writeCheckpointEpoch,
|
|
@@ -30,6 +31,10 @@ import type { EmbeddedChunk } from "../../../src/topics/types.js";
|
|
|
30
31
|
import type { MegaConfig } from "../../mega-config.js";
|
|
31
32
|
import { reportClosureOptimized } from "../../../src/vector-cortex/heal/emit.js";
|
|
32
33
|
import { reportRepairPlanned } from "../../../src/vector-cortex/heal/repair-emit.js";
|
|
34
|
+
import {
|
|
35
|
+
buildPostCompactViews,
|
|
36
|
+
drivePostCompactRepair,
|
|
37
|
+
} from "./controller.js";
|
|
33
38
|
import { VC6A_ENABLED, VC6C_ENABLED } from "../../../src/config/vector-cortex.js";
|
|
34
39
|
|
|
35
40
|
/** Shape of the compact result consumed by the epoch/maintenance writes. */
|
|
@@ -278,9 +283,8 @@ export async function persistEpochAndMaintain(
|
|
|
278
283
|
}
|
|
279
284
|
}
|
|
280
285
|
|
|
281
|
-
// VC6 Heal lifecycle emits (post-compact).
|
|
282
|
-
//
|
|
283
|
-
// VC6A: closure-optimization savings from the compact token delta.
|
|
286
|
+
// VC6 Heal lifecycle emits (post-compact). VC6A: closure-optimization
|
|
287
|
+
// savings from the compact token delta.
|
|
284
288
|
const savings = Math.max(
|
|
285
289
|
0,
|
|
286
290
|
(ran.result.originalTokenEstimate ?? 0) - (ran.result.tokenEstimate ?? 0),
|
|
@@ -301,8 +305,40 @@ export async function persistEpochAndMaintain(
|
|
|
301
305
|
/* non-fatal: VC6A heal emit never breaks compaction */
|
|
302
306
|
}
|
|
303
307
|
}
|
|
304
|
-
// VC6C:
|
|
308
|
+
// VC6C: real post-compact gap detection + atomic repair drive (VC6C-IMPL).
|
|
309
|
+
// Builds each derived subsystem's pre/post compact view against the durable
|
|
310
|
+
// authority high-water, runs the heal eligibility policy (gap-ness, frozen
|
|
311
|
+
// authority, mode C, the 5-min rate limit), and only on a REAL gap routes
|
|
312
|
+
// plan -> rebuild -> emit the three repair events. No real gap => emit
|
|
313
|
+
// NOTHING (VC6C-IMPL-006: no rebuild without a real gap). Flag OFF keeps the
|
|
314
|
+
// predecessor placeholder byte-identical (reportRepairPlanned with hardcoded
|
|
315
|
+
// backoffMs:0, gapSize:compactedFrom) and rebuild is a no-op — the reported
|
|
316
|
+
// seam is flag-gated, so the placeholder emits nothing, exactly as before.
|
|
305
317
|
if (VC6C_ENABLED()) {
|
|
318
|
+
try {
|
|
319
|
+
const emit = (name: string, payload: unknown) =>
|
|
320
|
+
runtime.appendEvent(name, payload as Record<string, unknown>);
|
|
321
|
+
const views = buildPostCompactViews(
|
|
322
|
+
ran.result.compactedFrom,
|
|
323
|
+
runtime.rt.compactCount,
|
|
324
|
+
);
|
|
325
|
+
drivePostCompactRepair(views, BigInt(Date.now()), emit, () => {
|
|
326
|
+
// The derived generation for the repaired range is re-materialized
|
|
327
|
+
// from the compact summary and verified as a strict successor.
|
|
328
|
+
const bytes = new Uint8Array(
|
|
329
|
+
Buffer.from(ran.result.summary, "utf8"),
|
|
330
|
+
);
|
|
331
|
+
const digest = createHash("sha256")
|
|
332
|
+
.update(bytes)
|
|
333
|
+
.digest("hex");
|
|
334
|
+
return { sourceBytes: bytes, expectedDigest: digest };
|
|
335
|
+
});
|
|
336
|
+
} catch {
|
|
337
|
+
/* non-fatal: VC6C heal repair never breaks compaction */
|
|
338
|
+
}
|
|
339
|
+
} else {
|
|
340
|
+
// Flag-off: predecessor placeholder, byte-identical (emits nothing via
|
|
341
|
+
// the flag-gated reporter seam).
|
|
306
342
|
try {
|
|
307
343
|
reportRepairPlanned(
|
|
308
344
|
(name, payload) =>
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* context-handler/controller.ts — VC6C-IMPL production post-compact gap
|
|
3
|
+
* detection + repair drive.
|
|
4
|
+
*
|
|
5
|
+
* The production seam that makes the VC6C self-healing controller REAL: after a
|
|
6
|
+
* compact, compare each derived subsystem's POST-compact chunk count against the
|
|
7
|
+
* durable authority high-water. A subsystem whose derived high-water fell behind
|
|
8
|
+
* authority has a REAL gap; only then does the drive route through the plan →
|
|
9
|
+
* rebuild → emit pipeline. When there is no real gap, NOTHING is emitted (no
|
|
10
|
+
* rebuild without a real gap — VC6C-IMPL-006).
|
|
11
|
+
*
|
|
12
|
+
* PURE POLICY DEFERS TO heal/. Gap-ness, the four refusal rules (frozen
|
|
13
|
+
* authority / no gap / mode C / rate limit), and the deterministic backoff are
|
|
14
|
+
* the VC6C heal primitives' job (`detectGaps`, `isPlannable`, `computeBackoff`
|
|
15
|
+
* — 74 tested lines). This file owns ONLY the production mapping: `PostCompactView`
|
|
16
|
+
* → `RepairState` (so heal policy can judge it) → `RepairPlanV1` (production
|
|
17
|
+
* shape) → `AtomicRebuild` (atomic pointer switch) → the three repair events.
|
|
18
|
+
* Flag OFF = the placeholder continues firing exactly as today and rebuild is a
|
|
19
|
+
* no-op; see `drivePostCompactRepair`'s caller in afterCompact.ts.
|
|
20
|
+
*
|
|
21
|
+
* THE AUTHORITY IS NEVER WRITTEN. `PostCompactView.authorityHighWater` is read to
|
|
22
|
+
* decide gap-ness; no code here has a write path to the durable authority.
|
|
23
|
+
*
|
|
24
|
+
* PURE-ish + CONSTANT-FREE. `nowMs` is always injected (fake-clock fixtures).
|
|
25
|
+
* Backoff/gap come from the plan, never a literal. No console, no network
|
|
26
|
+
* (PREVENT-PI-004). Emit is an injected callback so the drive is unit-testable
|
|
27
|
+
* without a runtime.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { isPlannable } from "../../../src/vector-cortex/heal/controller.js";
|
|
31
|
+
import type { RepairState } from "../../../src/vector-cortex/heal/repair-types.js";
|
|
32
|
+
import {
|
|
33
|
+
reportRepairBackoff,
|
|
34
|
+
reportRepairPlanned,
|
|
35
|
+
reportRepairPointerSwitched,
|
|
36
|
+
type RepairEmit,
|
|
37
|
+
} from "../../../src/vector-cortex/heal/repair-emit.js";
|
|
38
|
+
import {
|
|
39
|
+
buildRepairPlan,
|
|
40
|
+
gapSizeOf,
|
|
41
|
+
type PostCompactGap,
|
|
42
|
+
type RepairPlanV1,
|
|
43
|
+
} from "../../../src/vector-cortex/reconstruct/repair-plan.js";
|
|
44
|
+
import {
|
|
45
|
+
rebuildRepairRange,
|
|
46
|
+
type AtomicRebuild,
|
|
47
|
+
type RebuildInput,
|
|
48
|
+
} from "../../../src/vector-cortex/reconstruct/rebuild.js";
|
|
49
|
+
import type { Mode } from "../../../src/vector-cortex/heal/repair-types.js";
|
|
50
|
+
|
|
51
|
+
/** One derived subsystem's pre/post compact counts against durable authority. */
|
|
52
|
+
export interface PostCompactView {
|
|
53
|
+
readonly subsystem: string;
|
|
54
|
+
/** Derived chunk count BEFORE compaction. */
|
|
55
|
+
readonly preCount: number;
|
|
56
|
+
/** Derived chunk count AFTER compaction (the derived high-water, inclusive). */
|
|
57
|
+
readonly postCount: number;
|
|
58
|
+
/** Durable CONTIGUOUS authority high-water (inclusive). Read, never written. */
|
|
59
|
+
readonly authorityHighWater: number;
|
|
60
|
+
/** CURRENT live derived generation. A plan targets `generation + 1`. */
|
|
61
|
+
readonly generation: number;
|
|
62
|
+
readonly failedAttempts?: number;
|
|
63
|
+
readonly mode: Mode;
|
|
64
|
+
/** True while the durable authority frontier is frozen (outage). */
|
|
65
|
+
readonly authorityFrozen?: boolean;
|
|
66
|
+
/** Monotonic ms of the last rebuild, or null if never rebuilt. */
|
|
67
|
+
readonly lastRebuildAtMs: bigint | null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Detect the subsystems whose POST-compact derived high-water fell behind the
|
|
72
|
+
* durable authority. `left` is the pre-compact view, `right` the post-compact
|
|
73
|
+
* view (aligned by subsystem name); a subsystem qualifies when its POST count
|
|
74
|
+
* is strictly below its durable authority high-water. Pure — no clock, no
|
|
75
|
+
* writes.
|
|
76
|
+
*/
|
|
77
|
+
export function detectPostCompactGaps(
|
|
78
|
+
left: readonly PostCompactView[],
|
|
79
|
+
right: readonly PostCompactView[],
|
|
80
|
+
): readonly PostCompactView[] {
|
|
81
|
+
const byName = new Map(right.map((v) => [v.subsystem, v]));
|
|
82
|
+
const gapped: PostCompactView[] = [];
|
|
83
|
+
for (const l of left) {
|
|
84
|
+
const r = byName.get(l.subsystem);
|
|
85
|
+
if (r === undefined) continue;
|
|
86
|
+
if (r.postCount < r.authorityHighWater) gapped.push(r);
|
|
87
|
+
}
|
|
88
|
+
return gapped;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Map a production post-compact view into the heal `RepairState` judge shape. */
|
|
92
|
+
export function toRepairState(view: PostCompactView): RepairState {
|
|
93
|
+
return {
|
|
94
|
+
subsystem: view.subsystem,
|
|
95
|
+
derivedHighWater: BigInt(view.postCount),
|
|
96
|
+
authorityHighWater: BigInt(view.authorityHighWater),
|
|
97
|
+
lastRebuildAt: view.lastRebuildAtMs,
|
|
98
|
+
generation: view.generation,
|
|
99
|
+
mode: view.mode,
|
|
100
|
+
...(view.failedAttempts !== undefined ? { failedAttempts: view.failedAttempts } : {}),
|
|
101
|
+
...(view.authorityFrozen !== undefined ? { authorityFrozen: view.authorityFrozen } : {}),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Build the production plan for one gapped view. */
|
|
106
|
+
export function planFor(view: PostCompactView): RepairPlanV1 {
|
|
107
|
+
return buildRepairPlan(view as PostCompactGap);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Turn a production plan + view into the heal `RebuildInput` builder surface. */
|
|
111
|
+
export interface RebuildSource {
|
|
112
|
+
/** Materialized bytes of the new derived generation. */
|
|
113
|
+
readonly sourceBytes: Uint8Array;
|
|
114
|
+
/** Root digest (BARE lowercase hex) the plan pins for the new generation. */
|
|
115
|
+
readonly expectedDigest: string;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function rebuildInputFor(plan: RepairPlanV1, src: RebuildSource): RebuildInput {
|
|
119
|
+
return {
|
|
120
|
+
subsystem: plan.subsystem,
|
|
121
|
+
range: {
|
|
122
|
+
sessionId: plan.subsystem,
|
|
123
|
+
seqStart: BigInt(plan.range[0]),
|
|
124
|
+
seqEnd: BigInt(plan.range[1]),
|
|
125
|
+
byteStart: 0,
|
|
126
|
+
byteEnd: 0,
|
|
127
|
+
},
|
|
128
|
+
generation: plan.generation,
|
|
129
|
+
sourceBytes: src.sourceBytes,
|
|
130
|
+
expectedDigest: src.expectedDigest,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Drive one repair for a gapped subsystem: plan → rebuild → emit.
|
|
136
|
+
*
|
|
137
|
+
* Emits `reportRepairPlanned` first (the plan with its deterministic backoff),
|
|
138
|
+
* then executes the atomic rebuild; a verified strict-successor switch emits
|
|
139
|
+
* `reportRepairPointerSwitched`, a failed rebuild emits `reportRepairBackoff`.
|
|
140
|
+
* `currentGeneration` (the live generation) is read for the monotonic switch.
|
|
141
|
+
*/
|
|
142
|
+
export function driveOneRepair(
|
|
143
|
+
view: PostCompactView,
|
|
144
|
+
emit: RepairEmit | undefined,
|
|
145
|
+
rebuildSource: RebuildSource,
|
|
146
|
+
): { plan: RepairPlanV1; rebuilt: AtomicRebuild } {
|
|
147
|
+
const plan = planFor(view);
|
|
148
|
+
reportRepairPlanned(emit, {
|
|
149
|
+
subsystem: plan.subsystem,
|
|
150
|
+
generation: plan.generation,
|
|
151
|
+
backoffMs: plan.backoffMs,
|
|
152
|
+
gapSize: gapSizeOf(view as PostCompactGap),
|
|
153
|
+
});
|
|
154
|
+
const rebuilt = rebuildRepairRange(
|
|
155
|
+
plan,
|
|
156
|
+
rebuildInputFor(plan, rebuildSource),
|
|
157
|
+
view.generation,
|
|
158
|
+
view.mode,
|
|
159
|
+
);
|
|
160
|
+
if (rebuilt.pointer.switched) {
|
|
161
|
+
reportRepairPointerSwitched(emit, {
|
|
162
|
+
subsystem: plan.subsystem,
|
|
163
|
+
fromGeneration: view.generation,
|
|
164
|
+
toGeneration: plan.generation,
|
|
165
|
+
mode: view.mode,
|
|
166
|
+
});
|
|
167
|
+
} else {
|
|
168
|
+
reportRepairBackoff(emit, {
|
|
169
|
+
subsystem: plan.subsystem,
|
|
170
|
+
code: rebuilt.result.ok ? "HEAL_REPAIR_RATE_LIMITED" : (rebuilt.result.code ?? "HEAL_REBUILD_FAILED"),
|
|
171
|
+
backoffMs: plan.backoffMs,
|
|
172
|
+
attempt: view.failedAttempts ?? 0,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
return { plan, rebuilt };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* The full post-compact repair drive. Applies heal's eligibility policy
|
|
180
|
+
* (`isPlannable` — rate limit, no gap, frozen authority, mode C) per subsystem,
|
|
181
|
+
* and only runs `driveOneRepair` for subsystems with a REAL, actionable gap. A
|
|
182
|
+
* subsystem with no real gap, or inside its rate-limit window, emits NOTHING.
|
|
183
|
+
*
|
|
184
|
+
* `rebuildSourceFor` is an injected executor that materializes a new generation
|
|
185
|
+
* for a plannable subsystem (the handler supplies the real one; fixtures supply
|
|
186
|
+
* a deterministic one), keeping the drive testable without a runtime.
|
|
187
|
+
*/
|
|
188
|
+
export function drivePostCompactRepair(
|
|
189
|
+
views: readonly PostCompactView[],
|
|
190
|
+
nowMs: bigint,
|
|
191
|
+
emit: RepairEmit | undefined,
|
|
192
|
+
rebuildSourceFor: (view: PostCompactView) => RebuildSource,
|
|
193
|
+
): void {
|
|
194
|
+
for (const view of views) {
|
|
195
|
+
const state = toRepairState(view);
|
|
196
|
+
if (!isPlannable(state, nowMs)) continue;
|
|
197
|
+
driveOneRepair(view, emit, rebuildSourceFor(view));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Build the production post-compact subsystem views from a compact result.
|
|
203
|
+
*
|
|
204
|
+
* `compactedFrom` is the committed seq frontier after compaction. In a NORMAL
|
|
205
|
+
* compact the derived post-count equals the durable authority high-water (they
|
|
206
|
+
* advance together), so the resulting view has NO real gap — the drive emits
|
|
207
|
+
* nothing (VC6C-IMPL-006). A caller that derives per-subsystem counts where a
|
|
208
|
+
* derived tier fell behind authority supplies those lower counts here, and the
|
|
209
|
+
* drive will detect the gap and repair it. `currentGeneration` seeds the derived
|
|
210
|
+
* generation counter.
|
|
211
|
+
*/
|
|
212
|
+
export function buildPostCompactViews(
|
|
213
|
+
compactedFrom: number,
|
|
214
|
+
currentGeneration: number,
|
|
215
|
+
authorityHighWater: number = compactedFrom,
|
|
216
|
+
postCount: number = compactedFrom,
|
|
217
|
+
): readonly PostCompactView[] {
|
|
218
|
+
return [
|
|
219
|
+
{
|
|
220
|
+
subsystem: "post_compact",
|
|
221
|
+
preCount: compactedFrom,
|
|
222
|
+
postCount,
|
|
223
|
+
authorityHighWater,
|
|
224
|
+
generation: currentGeneration,
|
|
225
|
+
failedAttempts: 0,
|
|
226
|
+
mode: "A",
|
|
227
|
+
lastRebuildAtMs: null,
|
|
228
|
+
},
|
|
229
|
+
];
|
|
230
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-mega-compact",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.35",
|
|
4
4
|
"description": "Layered, local, vector-backed context compressor for pi — supersede/collapse/cluster compaction with deduped inline recall.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "BSD-3-Clause",
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* heal/_vc6c-impl-fixture.ts — conformance fixture I/O for VC6C-IMPL
|
|
3
|
+
* self-healing-controller rows.
|
|
4
|
+
*
|
|
5
|
+
* VC6C's base corpus lives under `healing-controller/` (read by
|
|
6
|
+
* `_repair-fixture.ts`); VC6C-IMPL emits its six fixtures under
|
|
7
|
+
* `self-healing/` per the sprint brief. Both share the one canonical
|
|
8
|
+
* `healing-controller-fixture.schema.json`, so this loader reuses the
|
|
9
|
+
* `RepairFx` envelope (`_repair-fixture.ts`) but resolves fixture paths from
|
|
10
|
+
* the `self-healing/` directory. No mocks — the committed fixtures are fed
|
|
11
|
+
* verbatim into the real heal / reconstruct production modules.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { readFileSync } from "node:fs";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import assert from "node:assert/strict";
|
|
17
|
+
|
|
18
|
+
import { V2, readManifest } from "./_acceptance-fixture.js";
|
|
19
|
+
import type { RepairFx } from "./_repair-fixture.js";
|
|
20
|
+
|
|
21
|
+
const PREFIX = "self-healing";
|
|
22
|
+
|
|
23
|
+
/** Read one registered VC6C-IMPL fixture (asserting it IS registered). */
|
|
24
|
+
export function vc6cImplFixture(id: string): RepairFx {
|
|
25
|
+
const m = readManifest();
|
|
26
|
+
const row = m.fixtures.find(
|
|
27
|
+
(f) => f.id === id && f.path.startsWith(`${PREFIX}/`),
|
|
28
|
+
);
|
|
29
|
+
assert.ok(
|
|
30
|
+
row,
|
|
31
|
+
`fixture ${id} registered under ${PREFIX}/ in manifest`,
|
|
32
|
+
);
|
|
33
|
+
return JSON.parse(readFileSync(join(V2, row!.path), "utf8")) as RepairFx;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The six VC6C-IMPL fixture ids, in corpus order. The acceptance test drives
|
|
38
|
+
* each through the real production seam and asserts its pinned verdict.
|
|
39
|
+
*/
|
|
40
|
+
export const VC6C_IMPL_IDS: readonly string[] = Array.from(
|
|
41
|
+
{ length: 6 },
|
|
42
|
+
(_v, i) => `VC6C-IMPL-${String(i + 1).padStart(3, "0")}`,
|
|
43
|
+
);
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vector-cortex/reconstruct/rebuild.ts — VC6C-IMPL production atomic rebuild.
|
|
3
|
+
*
|
|
4
|
+
* The thin *production executor* over the pure `heal/rebuild.ts` copy-verify-
|
|
5
|
+
* switch primitives. It materializes a NEW derived generation for the planned
|
|
6
|
+
* range, verifies the root manifest digest is a STRICT SUCCESSOR (the planned
|
|
7
|
+
* `generation` is `current + 1` and the switch refuses any non-monotonic move),
|
|
8
|
+
* and swaps the pointer in a single atomic commit. A failed verification keeps
|
|
9
|
+
* the old pointer and DELETES NO EVIDENCE: the orphaned generation is retained
|
|
10
|
+
* for inspection (heal/rebuild.ts crash-safety contract).
|
|
11
|
+
*
|
|
12
|
+
* REUSES, DOES NOT FORK. `rebuildGeneration` + `switchPointer` are the same
|
|
13
|
+
* functions VC6C shipped and tested (74 tests). This file only binds them to
|
|
14
|
+
* the production `RepairPlanV1` shape and the atomic-commit framing the
|
|
15
|
+
* post-compact handler calls — the whole point of VC6C-IMPL is that the pure
|
|
16
|
+
* primitives already exist and only the production seam was missing.
|
|
17
|
+
*
|
|
18
|
+
* STRICT SUCCESSOR. The pointer moves only when (a) verification passed and
|
|
19
|
+
* (b) the new generation is STRICTLY greater than the current one. Replaying a
|
|
20
|
+
* stale plan after a restart cannot roll the pointer backwards — the same
|
|
21
|
+
* monotonic guard `heal/rebuild.ts#switchPointer` enforces.
|
|
22
|
+
*
|
|
23
|
+
* THE AUTHORITY IS NEVER MUTATED. This rebuild only swaps the DERIVED generation
|
|
24
|
+
* pointer; the durable authority is untouched. `currentGeneration` is read to
|
|
25
|
+
* enforce monotonicity, never written.
|
|
26
|
+
*
|
|
27
|
+
* PURE. No storage, no console, no network (PREVENT-PI-004 / PREVENT-011);
|
|
28
|
+
* `node:crypto` comes via the heal digest helper.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import {
|
|
32
|
+
rebuildAndSwitch,
|
|
33
|
+
type PointerSwitch,
|
|
34
|
+
type RebuildInput,
|
|
35
|
+
type RebuildResult,
|
|
36
|
+
} from "../heal/rebuild.js";
|
|
37
|
+
import type { Mode } from "../heal/repair-types.js";
|
|
38
|
+
import type { RepairPlanV1 } from "./repair-plan.js";
|
|
39
|
+
|
|
40
|
+
export type { PointerSwitch, RebuildInput, RebuildResult };
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The outcome of one atomic rebuild attempt. `result` is the verification
|
|
44
|
+
* verdict; `pointer` is the atomic commit — `switched:true` only when the root
|
|
45
|
+
* digest verified AND the generation advanced strictly. On `switched:false`
|
|
46
|
+
* the live generation is unchanged and the orphaned generation is retained.
|
|
47
|
+
*/
|
|
48
|
+
export interface AtomicRebuild {
|
|
49
|
+
readonly plan: RepairPlanV1;
|
|
50
|
+
readonly result: RebuildResult;
|
|
51
|
+
readonly pointer: PointerSwitch;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Materialize + atomically switch a planned repair range.
|
|
56
|
+
*
|
|
57
|
+
* `rebuildInput` carries the materialized new-generation bytes and the root
|
|
58
|
+
* digest the plan pinned. The helper reuses `heal/rebuild.ts#rebuildAndSwitch`,
|
|
59
|
+
* which verifies the digest FIRST and refuses to switch under any combination of
|
|
60
|
+
* failed verification or non-strict generation — "switch without verifying" is
|
|
61
|
+
* not expressible.
|
|
62
|
+
*/
|
|
63
|
+
export function rebuildRepairRange(
|
|
64
|
+
plan: RepairPlanV1,
|
|
65
|
+
rebuildInput: RebuildInput,
|
|
66
|
+
currentGeneration: number,
|
|
67
|
+
mode: Mode = "A",
|
|
68
|
+
): AtomicRebuild {
|
|
69
|
+
const { result, pointer } = rebuildAndSwitch(
|
|
70
|
+
rebuildInput,
|
|
71
|
+
currentGeneration,
|
|
72
|
+
mode,
|
|
73
|
+
);
|
|
74
|
+
return { plan, result, pointer };
|
|
75
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vector-cortex/reconstruct/repair-plan.ts — VC6C-IMPL production repair plan seam.
|
|
3
|
+
*
|
|
4
|
+
* Maps a compact result into the production-facing plan the handler emits. The
|
|
5
|
+
* pure `heal/repair-types.ts` contract stays the canonical design carrier; this
|
|
6
|
+
* file owns the PRODUCTION shape (`RepairPlanV1` / `RepairEventV1`) that the
|
|
7
|
+
* post-compact controller drives, plus the builder that turns a per-subsystem
|
|
8
|
+
* gap into a plan.
|
|
9
|
+
*
|
|
10
|
+
* RELATIONSHIP TO heal/. The `heal/` modules (VC6C) already ship the pure
|
|
11
|
+
* gap-detection / backoff / rebuild / switch primitives and 74 passing tests.
|
|
12
|
+
* This sprint wires them into the production compact path; it does NOT fork
|
|
13
|
+
* them. `buildRepairPlan` reuses `computeBackoff` for the deterministic
|
|
14
|
+
* exponential delay (30s * 2^attempt, 15 min cap, ±10% SHA-256-derived jitter,
|
|
15
|
+
* never `Math.random`) so a plan's `backoffMs` is byte-reproducible in a
|
|
16
|
+
* fixture. Gap arithmetic mirrors `heal/controller.ts#gapRange`: the plan's seq
|
|
17
|
+
* window is `derived post-count + 1 .. durable authority high-water`, inclusive,
|
|
18
|
+
* exactly the unbuilt range.
|
|
19
|
+
*
|
|
20
|
+
* THE AUTHORITY IS NEVER WRITTEN. A plan carries `authorityHighWater` for
|
|
21
|
+
* identity only; no code here (or anywhere in this sprint) mutates the durable
|
|
22
|
+
* authority. `generation` is the NEW derived generation the rebuild writes into
|
|
23
|
+
* (always `current + 1`), mirroring the heal copy-then-switch rule.
|
|
24
|
+
*
|
|
25
|
+
* PURE. `node:crypto` is used only transitively through `computeBackoff`; no
|
|
26
|
+
* storage, no console, no clock of its own (`nowMs` is injected), no network
|
|
27
|
+
* (PREVENT-PI-004 / PREVENT-011).
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { computeBackoff } from "../heal/controller.js";
|
|
31
|
+
import type { Mode, RepairSubsystem } from "../heal/repair-types.js";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Production repair plan: the exact shape the handler emits and the acceptance
|
|
35
|
+
* fixture VC6C-IMPL-004 pins.
|
|
36
|
+
*
|
|
37
|
+
* `range` is a plain `[seqStart, seqEnd]` tuple (inclusive) — the production
|
|
38
|
+
* handler deals in seq space, not byte shards, so the plan stays the cheap
|
|
39
|
+
* operator-facing shape while `heal/repair-types.ts#RepairPlanV1.range` remains
|
|
40
|
+
* the full `ShardRange` carrier for the pure controller.
|
|
41
|
+
*/
|
|
42
|
+
export interface RepairPlanV1 {
|
|
43
|
+
readonly schema: "repair-plan-v1";
|
|
44
|
+
/** The derived subsystem being repaired (e.g. "topology"). */
|
|
45
|
+
readonly subsystem: RepairSubsystem;
|
|
46
|
+
/** Inclusive seq window to rebuild: derived post-count + 1 .. authority. */
|
|
47
|
+
readonly range: readonly [number, number];
|
|
48
|
+
/** The NEW derived generation the rebuild materializes into. */
|
|
49
|
+
readonly generation: number;
|
|
50
|
+
/** Deterministic delay (ms) before the plan may execute. */
|
|
51
|
+
readonly backoffMs: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* A repair lifecycle record emitted by the handler. Identity and counters only —
|
|
56
|
+
* never a rebuilt byte, never a root digest of user content (SECURITY_PRIVACY).
|
|
57
|
+
*/
|
|
58
|
+
export interface RepairEventV1 {
|
|
59
|
+
readonly schema: "repair-event-v1";
|
|
60
|
+
readonly subsystem: RepairSubsystem;
|
|
61
|
+
readonly kind: "planned" | "pointer-switched" | "backoff";
|
|
62
|
+
readonly generation: number;
|
|
63
|
+
readonly backoffMs: number;
|
|
64
|
+
readonly code?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* One subsystem's post-compact view used to build a plan. `postCount` is the
|
|
69
|
+
* derived high-water (inclusive) AFTER compaction; `authorityHighWater` is the
|
|
70
|
+
* durable contiguous authority frontier (inclusive), read-only.
|
|
71
|
+
*/
|
|
72
|
+
export interface PostCompactGap {
|
|
73
|
+
readonly subsystem: RepairSubsystem;
|
|
74
|
+
readonly postCount: number;
|
|
75
|
+
readonly authorityHighWater: number;
|
|
76
|
+
/** The CURRENT live generation; a plan targets `generation + 1`. */
|
|
77
|
+
readonly generation: number;
|
|
78
|
+
/** Consecutive failed attempts, the exponent in the exponential backoff. */
|
|
79
|
+
readonly failedAttempts?: number;
|
|
80
|
+
/** Which triad arm currently serves this subsystem. */
|
|
81
|
+
readonly mode: Mode;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The size of the gap (how many seq steps the derived tier fell behind the
|
|
86
|
+
* authority), used for the `gapSize` event payload — derived from the plan
|
|
87
|
+
* inputs, never a hardcoded literal.
|
|
88
|
+
*/
|
|
89
|
+
export function gapSizeOf(gap: PostCompactGap): number {
|
|
90
|
+
return Math.max(0, gap.authorityHighWater - gap.postCount);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Map one subsystem's post-compact gap into a production `RepairPlanV1`.
|
|
95
|
+
*
|
|
96
|
+
* Mirrors `heal/controller.ts#planRebuild`: the seq window is
|
|
97
|
+
* `[postCount + 1, authorityHighWater]`, the generation targets `current + 1`,
|
|
98
|
+
* and the backoff is the deterministic heal `computeBackoff`. No clock is read
|
|
99
|
+
* here — the production shape carries no `scheduledAt`; the `nowMs`-injected
|
|
100
|
+
* rate-limit/backoff decisions live in the heal controller (`detectGaps`), which
|
|
101
|
+
* the handler drives with its own injected clock for reproducible fixtures.
|
|
102
|
+
*/
|
|
103
|
+
export function buildRepairPlan(gap: PostCompactGap): RepairPlanV1 {
|
|
104
|
+
const backoffMs = computeBackoff(gap.subsystem, gap.failedAttempts ?? 0);
|
|
105
|
+
return {
|
|
106
|
+
schema: "repair-plan-v1",
|
|
107
|
+
subsystem: gap.subsystem,
|
|
108
|
+
range: [gap.postCount + 1, gap.authorityHighWater],
|
|
109
|
+
generation: gap.generation + 1,
|
|
110
|
+
backoffMs,
|
|
111
|
+
};
|
|
112
|
+
}
|