pi-mega-compact 0.17.1 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/dist/config/vector-cortex.js +10 -0
  2. package/dist/extensions/dashboard-server/routes-rag-settings-helpers.js +1 -0
  3. package/dist/extensions/mega-events/context-handler/dbMirrorAppend.js +63 -0
  4. package/dist/extensions/mega-events/context-handler/gateCheck.js +59 -0
  5. package/dist/extensions/mega-events/context-handler/liveTrim.js +178 -0
  6. package/dist/extensions/mega-events/context-handler/pipelineRun.js +37 -0
  7. package/dist/extensions/mega-events/context-handler.js +39 -305
  8. package/dist/src/config/vector-cortex.js +10 -0
  9. package/dist/src/config.js +1 -1
  10. package/dist/src/vector-cortex/encoder/asset.js +142 -0
  11. package/dist/src/vector-cortex/encoder/emit.js +42 -0
  12. package/dist/src/vector-cortex/encoder/runtime.js +228 -0
  13. package/dist/src/vector-cortex/encoder/types.js +85 -0
  14. package/dist/vector-cortex/encoder/asset.js +142 -0
  15. package/dist/vector-cortex/encoder/emit.js +42 -0
  16. package/dist/vector-cortex/encoder/runtime.js +228 -0
  17. package/dist/vector-cortex/encoder/types.js +85 -0
  18. package/extensions/dashboard-server/routes-rag-settings-helpers.ts +6 -0
  19. package/extensions/mega-events/context-handler/dbMirrorAppend.ts +93 -0
  20. package/extensions/mega-events/context-handler/gateCheck.ts +101 -0
  21. package/extensions/mega-events/context-handler/liveTrim.ts +241 -0
  22. package/extensions/mega-events/context-handler/pipelineRun.ts +79 -0
  23. package/extensions/mega-events/context-handler.ts +45 -347
  24. package/package.json +1 -1
  25. package/src/config/vector-cortex.ts +11 -0
  26. package/src/config.ts +1 -0
  27. package/src/vector-cortex/encoder/asset.ts +155 -0
  28. package/src/vector-cortex/encoder/emit.ts +51 -0
  29. package/src/vector-cortex/encoder/runtime.ts +283 -0
  30. package/src/vector-cortex/encoder/types.ts +165 -0
@@ -0,0 +1,228 @@
1
+ /**
2
+ * vector-cortex/encoder/runtime.ts — VC2A EncoderRuntime (task 3).
3
+ *
4
+ * Allocates (prepares an inference session) ONLY after manifest verification;
5
+ * rejects any non (batch 1, tokens <= maxTokens, <=512) input with
6
+ * ENC_SHAPE_INVALID; caps the encoder's MARGINAL footprint at 150 MiB
7
+ * (ENC_RSS_BUDGET_EXCEEDED -> mode B); and yields a deterministic mode-A
8
+ * inference over the verified asset (the trained weights are substituted in
9
+ * VC2C — the contract, shape gating and budgets all land here).
10
+ *
11
+ * MEMORY BUDGET (Q01/Q02): the 150 MiB cap measures the encoder's INCREMENTAL
12
+ * footprint — an in-process allocation counter (`selfAllocated`) plus any
13
+ * externally staged asset working set (`host.allocatedBytes()`) — NOT the
14
+ * whole-process RSS. In a live pi extension the process baseline (node:sqlite
15
+ * DatabaseSync + dashboard + loaded context) routinely exceeds 150 MiB, so an
16
+ * absolute-RSS cap would permanently demote a qualified asset to mode B and
17
+ * make mode A unreachable in production. Bounding the marginal footprint keeps
18
+ * mode A reachable while still enforcing the budget. `selfAllocated` models a
19
+ * single REUSABLE 384-float projection buffer (first inference allocates it,
20
+ * every later inference reuses it), so it is capped at `SEMANTIC_BUFFER_BYTES`
21
+ * — the marginal footprint can never grow without bound (Q01), and a long-lived
22
+ * runtime cannot drift over budget from healthy operation. The check runs
23
+ * BEFORE the allocation on both the load and the inference path
24
+ * (cap-before-allocation, task 3), and an over-budget inference demotes the
25
+ * runtime to mode B just as an over-budget load does (consistent demotion per
26
+ * ENC_FAIL.RSS_BUDGET_EXCEEDED).
27
+ *
28
+ * TOKEN CAPACITY (Q03): the per-manifest `maxTokens` (<= 512) is stored at load
29
+ * and enforced at inference — an input longer than the verified manifest's
30
+ * declared capacity is rejected with ENC_SHAPE_INVALID, honoring the model
31
+ * contract rather than a global 512 ceiling.
32
+ *
33
+ * FLAG GATING (Q04): the default factory consults `MEGACOMPACT_VC2A`; when the
34
+ * flag is OFF the runtime is fixed at mode C (rollback, byte-identical to the
35
+ * predecessor — no asset is read or verified). `forcedMode: "C"` is the
36
+ * explicit override for the same rollback path.
37
+ *
38
+ * Triad: A = qualified local ONNX (verified); B = asset-free trigram (forced by
39
+ * a missing/unsupported/digest-bad asset, no remote fetch); C = lexical forced
40
+ * when A verification fails AND B initialization itself fails. Demotions always
41
+ * select B/C locally and never attempt a network fetch (PREVENT-PI-004).
42
+ *
43
+ * Pi-agnostic. No `any` (PREVENT-011). Emits the two VC2A events via the
44
+ * reporter (non-fatal).
45
+ */
46
+ import { detectPlatform, readEncoderManifest, verifyEncoderAsset, } from "./asset.js";
47
+ import { createEncoderReporter } from "./emit.js";
48
+ import { VC2A_ENABLED } from "../../config/vector-cortex.js";
49
+ import { ENC_FAIL, ENCODER_MAX_TOKENS, ENCODER_RSS_BUDGET_BYTES, ENCODER_SEMANTIC_WIDTH, } from "./types.js";
50
+ /** Bytes a single encoder-owned projection buffer commits to the marginal
51
+ * footprint (Float32Array, 4 bytes per element). */
52
+ const SEMANTIC_BUFFER_BYTES = ENCODER_SEMANTIC_WIDTH * 4;
53
+ const DEFAULT_HOST = {
54
+ allocatedBytes: () => 0,
55
+ allocatorFails: () => false,
56
+ nowMs: () => Date.now(),
57
+ };
58
+ function mergeHost(partial) {
59
+ return { ...DEFAULT_HOST, ...partial };
60
+ }
61
+ /** A deterministic seeded projection so the mode-A inference path is testable
62
+ * end-to-end without onnxruntime (real weights + execution are VC2C). */
63
+ function projectSemantic(seed, n) {
64
+ const out = new Float32Array(n);
65
+ let state = (seed >>> 0) ^ 0x9e3779b9;
66
+ let sum = 0;
67
+ for (let i = 0; i < n; i++) {
68
+ state = (state * 1664525 + 1013904223) >>> 0;
69
+ out[i] = (state / 4294967296) * 2 - 1;
70
+ sum += out[i] * out[i];
71
+ }
72
+ const norm = Math.sqrt(sum) || 1;
73
+ for (let i = 0; i < n; i++)
74
+ out[i] = out[i] / norm;
75
+ return out;
76
+ }
77
+ /** Deterministic token seed derived from the verified asset bytes count. */
78
+ function seedFromBytes(embeddedBytes) {
79
+ return (embeddedBytes * 2654435761) >>> 0;
80
+ }
81
+ function modeLabel(mode) {
82
+ return mode === "A" ? "qualified-onnx" : mode === "B" ? "trigram" : "lexical";
83
+ }
84
+ export function createEncoderRuntime(options = {}) {
85
+ const reporter = options.reporter ?? createEncoderReporter();
86
+ const host = mergeHost(options.host);
87
+ const forced = options.forcedMode;
88
+ const plat = options.platform ?? detectPlatform;
89
+ // Q04: rollback contract — MEGACOMPACT_VC2A=0 selects mode C (byte-identical
90
+ // to the predecessor: no asset read/verify, no learned infer). An explicit
91
+ // forcedMode "C" takes precedence; otherwise the flag gates the default.
92
+ const rolledBack = forced === "C" || !VC2A_ENABLED();
93
+ let mode = rolledBack ? "C" : "C";
94
+ let embeddedBytes = 0;
95
+ let verified = false;
96
+ /** Per-manifest token capacity (<= 512) from the verified asset; enforced at
97
+ * inference (Q03). Defaults to the global ceiling before a load. */
98
+ let maxTokens = ENCODER_MAX_TOKENS;
99
+ /** Bytes this runtime itself has allocated. This models a SINGLE reusable
100
+ * 384-float projection buffer: the first inference allocates it (1536
101
+ * bytes), every later inference reuses it, so the counter is capped at
102
+ * `SEMANTIC_BUFFER_BYTES` and never grows without bound (Q01). Combined
103
+ * with `host.allocatedBytes()` it drives the 150 MiB marginal budget (Q02),
104
+ * never whole-process RSS. */
105
+ let selfAllocated = 0;
106
+ /** The encoder's marginal working-set footprint, in bytes. */
107
+ const footprint = () => selfAllocated + host.allocatedBytes();
108
+ const demoteTo = (rmode, code) => {
109
+ mode = rmode;
110
+ verified = false;
111
+ reporter.runtimeDemoted({ reason: code, mode: rmode, platform: plat()?.toString() ?? "unsupported" });
112
+ };
113
+ const runtime = {
114
+ schema: "encoder-runtime-v1",
115
+ // Live getter so `mode` always reflects the latest load/demote outcome
116
+ // (a plain property would freeze at its construction-time value forever).
117
+ get mode() {
118
+ return mode;
119
+ },
120
+ load(assetDir) {
121
+ if (rolledBack) {
122
+ // Rollback path (forcedMode "C" or MEGACOMPACT_VC2A=0): mode C restores
123
+ // the prior derived pointer; no asset is read or verified; no emission.
124
+ // Q04: report the rollback with its own code, not MANIFEST_INVALID, so a
125
+ // correctly-shaped, digest-correct asset is not mis-read as corrupted.
126
+ mode = "C";
127
+ verified = false;
128
+ return { ok: false, mode: "C", code: ENC_FAIL.ROLLBACK };
129
+ }
130
+ // Attempt A: verify the local qualified ONNX asset (never a remote fetch).
131
+ const manifest = readEncoderManifest(assetDir);
132
+ let verify;
133
+ if (manifest === null) {
134
+ verify = { ok: false, code: ENC_FAIL.MANIFEST_INVALID };
135
+ }
136
+ else {
137
+ verify = verifyEncoderAsset(assetDir, manifest, plat());
138
+ }
139
+ if (!verify.ok) {
140
+ // A failed -> B, unless B init itself fails (allocator) -> C.
141
+ if (host.allocatorFails()) {
142
+ demoteTo("C", ENC_FAIL.ASSET_UNREADABLE);
143
+ return { ok: false, mode: "C", code: ENC_FAIL.ASSET_UNREADABLE };
144
+ }
145
+ demoteTo("B", verify.code);
146
+ return { ok: false, mode: "B", code: verify.code };
147
+ }
148
+ // Allocate only after verification (task 3). Simulate allocator failure.
149
+ if (host.allocatorFails()) {
150
+ demoteTo("B", ENC_FAIL.ASSET_UNREADABLE);
151
+ return { ok: false, mode: "B", code: ENC_FAIL.ASSET_UNREADABLE };
152
+ }
153
+ // Cap the encoder's MARGINAL footprint at 150 MiB (task 3, Q01). This
154
+ // bounds the encoder's incremental allocation, so a healthy process with
155
+ // a large baseline RSS still reaches mode A.
156
+ if (footprint() > ENCODER_RSS_BUDGET_BYTES) {
157
+ demoteTo("B", ENC_FAIL.RSS_BUDGET_EXCEEDED);
158
+ return { ok: false, mode: "B", code: ENC_FAIL.RSS_BUDGET_EXCEEDED };
159
+ }
160
+ embeddedBytes = verify.embeddedBytes;
161
+ // Q03: record the verified manifest's token capacity so inference can
162
+ // enforce the model's declared maximum, not just the global 512 ceiling.
163
+ maxTokens = verify.maxTokens;
164
+ verified = true;
165
+ mode = "A";
166
+ reporter.assetVerified({
167
+ mode: "A",
168
+ embeddedBytes: verify.embeddedBytes,
169
+ onnxDigest: verify.onnxDigest.slice(0, 12),
170
+ });
171
+ return {
172
+ ok: true,
173
+ mode: "A",
174
+ embeddedBytes: verify.embeddedBytes,
175
+ rssBytes: footprint(),
176
+ sessionId: `enc-${seedFromBytes(verify.embeddedBytes).toString(16)}`,
177
+ };
178
+ },
179
+ infer(input) {
180
+ if (!verified || mode !== "A") {
181
+ // Only batch1/max512 verified assets reach inference (mode B/C do not).
182
+ return {
183
+ ok: false,
184
+ code: ENC_FAIL.SHAPE_INVALID,
185
+ shapeError: "no verified learned asset; mode is " + modeLabel(mode),
186
+ };
187
+ }
188
+ if (!input || !Array.isArray(input.tokens)) {
189
+ return { ok: false, code: ENC_FAIL.SHAPE_INVALID, shapeError: "missing tokens array" };
190
+ }
191
+ const n = input.tokens.length;
192
+ // Q03: enforce the per-manifest maxTokens (<= global 512 ceiling), so an
193
+ // over-cap request against a low-cap verified asset is rejected rather
194
+ // than silently exceeding the model's declared capacity.
195
+ if (n < 1 || n > maxTokens) {
196
+ return {
197
+ ok: false,
198
+ code: ENC_FAIL.SHAPE_INVALID,
199
+ shapeError: `token count ${n} outside 1..${maxTokens} (manifest cap)`,
200
+ };
201
+ }
202
+ // Q03: cap-before-allocation on the inference path too. Check the
203
+ // marginal footprint BEFORE allocating the projection buffer; an
204
+ // over-budget inference demotes to mode B consistently with load() (the
205
+ // ENC_FAIL.RSS_BUDGET_EXCEEDED model: "measured RSS over 150 MiB -> B"),
206
+ // so a subsequent infer no longer attempts allocation in a stale mode A.
207
+ if (footprint() > ENCODER_RSS_BUDGET_BYTES) {
208
+ demoteTo("B", ENC_FAIL.RSS_BUDGET_EXCEEDED);
209
+ return {
210
+ ok: false,
211
+ code: ENC_FAIL.RSS_BUDGET_EXCEEDED,
212
+ shapeError: "encoder footprint over budget during inference",
213
+ };
214
+ }
215
+ const start = host.nowMs();
216
+ // Batch is always 1 (single request); shape is (1, n) for n in 1..maxTokens.
217
+ const semantic = projectSemantic(seedFromBytes(embeddedBytes) ^ n, ENCODER_SEMANTIC_WIDTH);
218
+ // Q01: the projection buffer is a single reusable 384-float array; the
219
+ // marginal footprint is a fixed SEMANTIC_BUFFER_BYTES once it exists, so
220
+ // selfAllocated is SET (never accumulated) — bounded regardless of how
221
+ // many inferences run on a long-lived runtime.
222
+ selfAllocated = SEMANTIC_BUFFER_BYTES;
223
+ const latencyMs = host.nowMs() - start;
224
+ return { ok: true, semantic, rssBytes: footprint(), latencyMs, shapeError: null };
225
+ },
226
+ };
227
+ return runtime;
228
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * vector-cortex/encoder/types.ts — VC2A contract (ModelManifestV1 /
3
+ * EncoderRuntime).
4
+ *
5
+ * The offline encoder runtime owns the learned-asset path (triad mode A: a
6
+ * qualified local ONNX). MODEL_ASSET.md is the normative target. This
7
+ * sprint (VC2A) ships the manifest + verification + shaped-inference contract;
8
+ * the trained weights are packaged in VC2C (MODEL_ASSET: "package.json changes
9
+ * occur only in VC2C"), but the verification, digest-before-load, platform
10
+ * demotion, shape rejection and RSS/latency budget all land here so a later
11
+ * sprint only substitutes real weights.
12
+ *
13
+ * Contract-first (ENGINEERING_PRACTICES §3): this types file is the reviewed
14
+ * gate; implementations import from it; consumers import only types + factory.
15
+ *
16
+ * Pi-agnostic and dependency-free (PREVENT-PI-004 — local assets only, the
17
+ * runtime never fetches). No `any` (PREVENT-011).
18
+ */
19
+ /** Supported matrix from MODEL_ASSET.md §qualification. */
20
+ export const ENCODER_SUPPORTED_PLATFORMS = [
21
+ "linux-x64",
22
+ "linux-arm64",
23
+ "darwin-x64",
24
+ "darwin-arm64",
25
+ "win32-x64",
26
+ ];
27
+ /** ONNX opset required by the normative v1 target (opset 17). */
28
+ export const ENCODER_OPSET = 17;
29
+ /** Batch must be exactly 1 (single-request inference). */
30
+ export const ENCODER_BATCH = 1;
31
+ /** Maximum accepted token count (WordPiece, deterministic truncation). */
32
+ export const ENCODER_MAX_TOKENS = 512;
33
+ /** Caps the encoder's MARGINAL footprint (bytes) at 150 MiB (MODEL_ASSET
34
+ * §qualification). The budget bounds the encoder's own incremental allocation
35
+ * (a reusable projection buffer + any externally staged asset working set),
36
+ * NOT the whole-process RSS — in a live pi extension the process baseline
37
+ * routinely exceeds 150 MiB, so measuring absolute RSS would make mode A
38
+ * unreachable in production. This is the "RSS" figure the acceptance metric
39
+ * and ENC_FAIL.RSS_BUDGET_EXCEEDED refer to: it is the encoder's marginal
40
+ * footprint, never the process RSS (code-review Q01/Q02). */
41
+ export const ENCODER_RSS_BUDGET_BYTES = 150 * 1024 * 1024;
42
+ /** p95 inference budget in milliseconds (MODEL_ASSET §qualification). */
43
+ export const ENCODER_LATENCY_P95_MS = 40;
44
+ /** Semantic projection head width (MODEL_ASSET: 384 float32 L2-normalized). */
45
+ export const ENCODER_SEMANTIC_WIDTH = 384;
46
+ /** Exact VC2A failure codes (returned, never thrown across the boundary). */
47
+ export const ENC_FAIL = {
48
+ /** opset != 17. */
49
+ OPSET_INVALID: "ENC_OPSET_INVALID",
50
+ /** batch != 1. */
51
+ BATCH_INVALID: "ENC_BATCH_INVALID",
52
+ /** maxTokens > 512. */
53
+ TOKENS_EXCEEDED: "ENC_TOKENS_EXCEEDED",
54
+ /** input token count > declared maxTokens / 512, or not batch 1. */
55
+ SHAPE_INVALID: "ENC_SHAPE_INVALID",
56
+ /** asset file unreadable (truncated during digest read, allocator failure). */
57
+ ASSET_UNREADABLE: "ENC_ASSET_UNREADABLE",
58
+ /** on-disk digest does not match the manifest (one-byte mutation). */
59
+ DIGEST_MISMATCH: "ENC_DIGEST_MISMATCH",
60
+ /** platform not in the supported matrix (selects trigram B). */
61
+ PLATFORM_UNSUPPORTED: "ENC_PLATFORM_UNSUPPORTED",
62
+ /** manifest missing/invalid (selects trigram B). */
63
+ MANIFEST_INVALID: "ENC_MANIFEST_INVALID",
64
+ /** encoder MARGINAL footprint over the 150 MiB budget (selects trigram B).
65
+ * This is the encoder's own incremental allocation (a reusable projection
66
+ * buffer + any externally staged asset working set), NOT whole-process RSS
67
+ * — see ENCODER_RSS_BUDGET_BYTES. */
68
+ RSS_BUDGET_EXCEEDED: "ENC_RSS_BUDGET_EXCEEDED",
69
+ /** mode C forced by the rollback path (MEGACOMPACT_VC2A=0 / forcedMode "C").
70
+ * Distinct from MANIFEST_INVALID so a non-corrupt, correctly-shaped asset
71
+ * present on disk is not mis-reported as "manifest invalid" when the runtime
72
+ * is simply rolled back to the predecessor path (code-review Q04). */
73
+ ROLLBACK: "ENC_ROLLBACK_ACTIVE",
74
+ };
75
+ /** The 8 registered VC2A conformance IDs (task 1: "register ENC-001..008"). */
76
+ export const ENC_IDS = [
77
+ "ENC-001",
78
+ "ENC-002",
79
+ "ENC-003",
80
+ "ENC-004",
81
+ "ENC-005",
82
+ "ENC-006",
83
+ "ENC-007",
84
+ "ENC-008",
85
+ ];
@@ -311,6 +311,12 @@ export const SETTINGS: ReadonlyArray<{
311
311
  "FixtureManifestV2 canonical manifest validator + DowngradeReport deterministic downgrade export + MinHashV2 exact big-integer signatures and the M4 copy/validate/switch minhash-v2 migration (seed table frozen, cross-language byte-exact). OFF = mode C, v1 sync dedup scan unchanged, byte-identical.",
312
312
  true,
313
313
  ),
314
+ boolDirect(
315
+ "MEGACOMPACT_VC2A",
316
+ "VC2A Offline Model Runtime",
317
+ "ModelManifestV1 digest-before-load ONNX runtime (opset17/batch1/max512) + asset-free trigram demotion. Asset path assets/vector-cortex/encoder-v1 is immutable/digest-pinned. OFF = mode C, byte-identical to predecessor.",
318
+ true,
319
+ ),
314
320
  ],
315
321
  },
316
322
  {
@@ -0,0 +1,93 @@
1
+ /**
2
+ * context-handler/dbMirrorAppend.ts — DB-mirror append + VC1B ledger append.
3
+ *
4
+ * Extracted from context-handler.ts (delegate-shell split). Appends incoming
5
+ * messages to raw_transcript (S27) + conversation_thread/tool_results (P2.2),
6
+ * then appends canonical messages to the v2 vector-cortex ledger (VC1B S1).
7
+ * All best-effort + non-fatal — a failure never breaks the agent loop
8
+ * (PREVENT-PI-004: zero network, local SQLite only).
9
+ */
10
+ import type { AgentMessage } from "@earendil-works/pi-agent-core";
11
+ import { openStore } from "../../../src/store/sqlite.js";
12
+ import { appendMirrorMessages } from "../mirror-append.js";
13
+ import { appendMessagesToLedger } from "../../mega-runtime/vector-cortex-ledger.js";
14
+ import { epochIdFor } from "../../../src/mirror/epoch.js";
15
+ import type { MegaRuntime } from "../../mega-runtime.js";
16
+ import type { MegaConfig } from "../../mega-config.js";
17
+ import { messageContentText } from "./messageText.js";
18
+
19
+ /**
20
+ * Append incoming messages to the DB mirror (raw_transcript + thread/tool
21
+ * tables) and the v2 ledger. Gated on config.dbMirror for the mirror; the VC1B
22
+ * ledger append is flag-gated inside appendMessagesToLedger (flag-OFF opens no
23
+ * DB, byte-identical to the predecessor). Non-fatal end-to-end.
24
+ */
25
+ export function appendMirrorAndLedger(
26
+ runtime: MegaRuntime,
27
+ config: MegaConfig,
28
+ messages: AgentMessage[],
29
+ ): void {
30
+ // S27 DB-mirror: append incoming messages to raw_transcript.
31
+ // Runs BEFORE fast-gate so every message is captured, even if we
32
+ // don't compact this turn. Append is idempotent (content_hash PK).
33
+ // F3: high-water mark (mirror-append.ts) skips already-processed
34
+ // messages on subsequent events. On fork/rewind (shorter list or
35
+ // boundary hash mismatch) the mark is dropped, falling back to a
36
+ // full reprocess.
37
+ if (config.dbMirror) {
38
+ try {
39
+ const db = openStore(runtime.currentStateDir);
40
+ appendMirrorMessages(
41
+ db,
42
+ messages,
43
+ runtime.rt.sessionId,
44
+ epochIdFor(runtime.rt.sessionId),
45
+ runtime.currentTurn,
46
+ );
47
+ // P2.2: populate conversation_thread + tool_results tables for
48
+ // prompt-cache analytics and durable separation. The live-array
49
+ // separation (buildSeparatedPrompt / buildCacheOptimizedPrompt in
50
+ // tailResult) is sufficient for the prompt-construction path;
51
+ // these DB writes persist the split for post-hoc analysis, dashboard
52
+ // queries, and future readers. Non-fatal — failure here never breaks
53
+ // the agent loop (PREVENT-PI-004: zero network, local SQLite only).
54
+ {
55
+ const sid = runtime.rt.sessionId;
56
+ const turn = runtime.currentTurn;
57
+ const now = Date.now();
58
+ const threadStmt = db.prepare(
59
+ "INSERT OR IGNORE INTO conversation_thread (conversation_id, role, content, turn_index, timestamp) VALUES (?, ?, ?, ?, ?)",
60
+ );
61
+ const toolStmt = db.prepare(
62
+ "INSERT OR IGNORE INTO tool_results (conversation_id, role, content, turn_index, timestamp) VALUES (?, ?, ?, ?, ?)",
63
+ );
64
+ for (const m of messages) {
65
+ const role = m.role;
66
+ const content = messageContentText(m);
67
+ if (role === "user" || role === "assistant") {
68
+ threadStmt.run(sid, role, content, turn, now);
69
+ } else if (role === "toolResult" || role === "bashExecution") {
70
+ toolStmt.run(sid, role, content, turn, now);
71
+ }
72
+ }
73
+ }
74
+ } catch (e) {
75
+ runtime.logger.warn("db-mirror-append-fail", { error: String(e) });
76
+ }
77
+ }
78
+
79
+ // VC1B (S1): canonical messages -> v2 ledger occurrences. Flag-OFF opens
80
+ // no DB (byte-identical predecessor); non-fatal. onFailure surfaces
81
+ // per-append rejections (e.g. EVT_SEQ_REGRESSION on rewind/fork) as
82
+ // structured warnings rather than swallowing them silently.
83
+ try {
84
+ appendMessagesToLedger(
85
+ runtime.currentStateDir,
86
+ runtime.rt.sessionId,
87
+ messages,
88
+ runtime.logger,
89
+ );
90
+ } catch (e) {
91
+ runtime.logger.warn("vc1b-ledger-append-fail", { error: String(e) });
92
+ }
93
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * context-handler/gateCheck.ts — S29 fast-gate threshold evaluation.
3
+ *
4
+ * Extracted from context-handler.ts (delegate-shell split). Drives the
5
+ * auto-trigger off the context % (the number the menu bar shows), NOT the
6
+ * token count — the model under-reports tokens, so a token-only gate misses
7
+ * the overshoot that causes max-output-tokens truncation. Returns a
8
+ * discriminated union: either "return" (a tailed view to hand back to pi) or
9
+ * "proceed" with the resolved per-model threshold for the live-trim tail cap.
10
+ */
11
+ import type { AgentMessage } from "@earendil-works/pi-agent-core";
12
+ import {
13
+ resolveModelThreshold,
14
+ DEFAULT_SAFETY_MARGIN_PCT,
15
+ DEFAULT_FIRE_POINT_PCT,
16
+ } from "../../../src/store/sqlite.js";
17
+ import { autoCompactCheck } from "../../../src/compact.js";
18
+ import type { MegaRuntime } from "../../mega-runtime.js";
19
+ import type { MegaConfig } from "../../mega-config.js";
20
+
21
+ /** Tail-injection closure shape produced by buildTailResult (tailResult.ts). */
22
+ export type TailResultFn = (
23
+ msgs?: readonly AgentMessage[],
24
+ ) => { messages: AgentMessage[] } | undefined;
25
+
26
+ /** Outcome of the fast-gate evaluation. */
27
+ export type GateOutcome =
28
+ | { kind: "return"; view: { messages: AgentMessage[] } | undefined }
29
+ | {
30
+ kind: "proceed";
31
+ perModelThreshold: { safetyMarginPct: number; firePointPct: number };
32
+ };
33
+
34
+ /**
35
+ * Evaluate whether the current context warrants compaction. Returns a tailed
36
+ * view ("return") when the gate does not pass, or "proceed" with the resolved
37
+ * per-model threshold (reused by the live-trim token-budget tail cap).
38
+ */
39
+ export function evaluateGate(
40
+ runtime: MegaRuntime,
41
+ config: MegaConfig,
42
+ opts: {
43
+ pct: number | null | undefined;
44
+ currentTokens: number;
45
+ tailResult: TailResultFn;
46
+ },
47
+ ): GateOutcome {
48
+ const pct = opts.pct;
49
+ const currentTokens = opts.currentTokens;
50
+ const tailResult = opts.tailResult;
51
+
52
+ // S52 / v0.16.1: per-model threshold override. The user can tune the
53
+ // fire point + safety margin PER MODEL (different providers' models range
54
+ // 8K-1M+ context, so one global tier % is wrong). Falls back to env/default
55
+ // when no override row exists. Computed once here + reused in the tail cap
56
+ // below; the lookup is a single SQLite PK hit (cheap; cached after the
57
+ // first read in a session).
58
+ const modelIdForThreshold = runtime.currentModel?.modelId ?? null;
59
+ const perModelThreshold = resolveModelThreshold(modelIdForThreshold, {
60
+ safetyMarginFallback: DEFAULT_SAFETY_MARGIN_PCT,
61
+ firePointFallback:
62
+ config.tierPct != null
63
+ ? Math.round(config.tierPct * 100)
64
+ : DEFAULT_FIRE_POINT_PCT,
65
+ stateDir: runtime.currentStateDir,
66
+ });
67
+
68
+ // S29 FAST GATE: `custom` (absolute MEGACOMPACT_THRESHOLD_TOKENS,
69
+ // tierPct null) is an explicit opt-out of percent scaling — it keeps the
70
+ // token gate. When pct is unavailable (window unknown / a model that
71
+ // doesn't report percent) a tiered config falls back to the token gate
72
+ // (S27 boot-fallback guarantee) instead of skipping compaction — a
73
+ // percent-only gate would regress that.
74
+ let gatePassed = false;
75
+ if (config.tierPct != null && pct != null) {
76
+ // Per-model override is a % (10-90); tierPct is a fraction (0.1-1.0).
77
+ // Prefer the override; fall back to autoPctTrigger + tierPct.
78
+ const tierPctFraction = config.autoPctTrigger ?? config.tierPct;
79
+ const perModelFraction = perModelThreshold.firePointPct / 100;
80
+ const firePct =
81
+ modelIdForThreshold != null ? perModelFraction : tierPctFraction;
82
+ gatePassed = pct / 100 >= firePct;
83
+ } else {
84
+ // custom tier OR tiered-but-pct-unavailable → token gate (S27 fallback).
85
+ if (currentTokens < runtime.effectiveThreshold) {
86
+ runtime.diagCtxFastGate++;
87
+ return { kind: "return", view: tailResult() ?? undefined };
88
+ }
89
+ const check = autoCompactCheck(currentTokens, runtime.effectiveThreshold); // SERVER-STYLE CONFIRM (local)
90
+ if (!check.shouldCompact) {
91
+ runtime.diagCtxNoCompact++;
92
+ return { kind: "return", view: tailResult() ?? undefined };
93
+ }
94
+ gatePassed = true;
95
+ }
96
+ if (!gatePassed) {
97
+ runtime.diagCtxFastGate++;
98
+ return { kind: "return", view: tailResult() ?? undefined };
99
+ }
100
+ return { kind: "proceed", perModelThreshold };
101
+ }