pi-mega-compact 0.16.0 → 0.16.2
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/dashboard-server/api-contracts/endpoints/registry.js +7 -0
- package/dist/extensions/dashboard-server/api-contracts/model-thresholds.js +1 -0
- package/dist/extensions/dashboard-server/routes-model-thresholds.js +167 -0
- package/dist/extensions/dashboard-server/routes.js +3 -2
- package/dist/extensions/dashboard-server/server.js +6 -2
- package/dist/extensions/mega-events/context-handler.js +37 -11
- package/dist/src/store/sqlite/model-snapshots.js +21 -0
- package/dist/src/store/sqlite/model-thresholds.js +93 -0
- package/dist/src/store/sqlite/schema/core.js +10 -0
- package/dist/src/store/sqlite.js +1 -0
- package/dist/src/vector-search.js +1 -1
- package/extensions/dashboard-client/dist/assets/{AreaChart-CrmTcE7w.js → AreaChart-Y_X0Bbjd.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{AreaChart-CrmTcE7w.js.map → AreaChart-Y_X0Bbjd.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{BarChart-Cf8DTaXS.js → BarChart-B9XDb-Ob.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{BarChart-Cf8DTaXS.js.map → BarChart-B9XDb-Ob.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{CacheTab-Ce0MES6p.js → CacheTab-BUGVb1G1.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{CacheTab-Ce0MES6p.js.map → CacheTab-BUGVb1G1.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{EventsTab-DbaUuA58.js → EventsTab-dOqLYTsy.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{EventsTab-DbaUuA58.js.map → EventsTab-dOqLYTsy.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{HealthTab-BmEXkcfL.js → HealthTab-c2vezKg1.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{HealthTab-BmEXkcfL.js.map → HealthTab-c2vezKg1.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MaintenanceTab-n2JQrMy7.js → MaintenanceTab-bQYHIcdx.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MaintenanceTab-n2JQrMy7.js.map → MaintenanceTab-bQYHIcdx.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/MemoryMapTab-2L8pH6y2.js +2 -0
- package/extensions/dashboard-client/dist/assets/{MemoryMapTab-Dvu7vkOm.js.map → MemoryMapTab-2L8pH6y2.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MetricsTab-Q0g32AmP.js → MetricsTab-CzUx1jpo.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MetricsTab-Q0g32AmP.js.map → MetricsTab-CzUx1jpo.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{OverviewTab-iKiEcOLL.js → OverviewTab-eCNoaclv.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{OverviewTab-iKiEcOLL.js.map → OverviewTab-eCNoaclv.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{ReposTab-DWpnv77y.js → ReposTab-tb0IW3NF.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{ReposTab-DWpnv77y.js.map → ReposTab-tb0IW3NF.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{SessionsTab-G0YxR1vG.js → SessionsTab-C-IoLw5h.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SessionsTab-G0YxR1vG.js.map → SessionsTab-C-IoLw5h.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/SetupTab-BvmFi8gf.js +2 -0
- package/extensions/dashboard-client/dist/assets/SetupTab-BvmFi8gf.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-1_LCFiJp.js → TimeSavedCard-DBH-cOlA.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-1_LCFiJp.js.map → TimeSavedCard-DBH-cOlA.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TurnsTab-pqOkjE4L.js → TurnsTab-DBMgmvnX.js} +3 -3
- package/extensions/dashboard-client/dist/assets/{TurnsTab-pqOkjE4L.js.map → TurnsTab-DBMgmvnX.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/WikiTab-1LcQrXk3.js +2 -0
- package/extensions/dashboard-client/dist/assets/{WikiTab-99sKE4nB.js.map → WikiTab-1LcQrXk3.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/button-DtEsZTUE.js +2 -0
- package/extensions/dashboard-client/dist/assets/{button-YcV2VQEP.js.map → button-DtEsZTUE.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{card-Bwxzo50p.js → card-DcfTmw-4.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{card-Bwxzo50p.js.map → card-DcfTmw-4.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-C6hlfMYG.js → generateCategoricalChart-BsIxUDyw.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-C6hlfMYG.js.map → generateCategoricalChart-BsIxUDyw.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{index-DRa2d4TF.js → index-B93cewiX.js} +14 -14
- package/extensions/dashboard-client/dist/assets/index-B93cewiX.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/index-C_tCbOI0.css +1 -0
- package/extensions/dashboard-client/dist/assets/{switch-rU-lor0v.js → switch-Bowd7jCD.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{switch-rU-lor0v.js.map → switch-Bowd7jCD.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{toggle-h-9B4w9X.js → toggle-Bo7kJZ4u.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{toggle-h-9B4w9X.js.map → toggle-Bo7kJZ4u.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{useSSE-BxJORWx8.js → useSSE-zNhb0c3u.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{useSSE-BxJORWx8.js.map → useSSE-zNhb0c3u.js.map} +1 -1
- package/extensions/dashboard-client/dist/index.html +2 -2
- package/extensions/dashboard-client/src/api/client.ts +38 -13
- package/extensions/dashboard-client/src/tabs/SetupTab/ThresholdsPanel.tsx +334 -0
- package/extensions/dashboard-client/src/tabs/SetupTab.tsx +9 -1
- package/extensions/dashboard-server/api-contracts/endpoints/registry.ts +50 -17
- package/extensions/dashboard-server/api-contracts/index.ts +28 -3
- package/extensions/dashboard-server/api-contracts/model-thresholds.ts +76 -0
- package/extensions/dashboard-server/routes-model-thresholds.ts +222 -0
- package/extensions/dashboard-server/routes.ts +11 -2
- package/extensions/dashboard-server/server.ts +5 -1
- package/extensions/mega-events/context-handler.ts +106 -69
- package/package.json +1 -1
- package/src/store/sqlite/model-snapshots.ts +99 -62
- package/src/store/sqlite/model-thresholds.ts +168 -0
- package/src/store/sqlite/schema/core.ts +10 -0
- package/src/store/sqlite.ts +1 -0
- package/src/vector-search.ts +1 -1
- package/extensions/dashboard-client/dist/assets/MemoryMapTab-Dvu7vkOm.js +0 -2
- package/extensions/dashboard-client/dist/assets/SetupTab-C-y6H-F-.js +0 -2
- package/extensions/dashboard-client/dist/assets/SetupTab-C-y6H-F-.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/WikiTab-99sKE4nB.js +0 -2
- package/extensions/dashboard-client/dist/assets/button-YcV2VQEP.js +0 -2
- package/extensions/dashboard-client/dist/assets/index-DRa2d4TF.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/index-za_gmEmQ.css +0 -1
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* api-contracts/model-thresholds.ts — per-model compaction threshold API types.
|
|
3
|
+
*
|
|
4
|
+
* Types for the /api/model-thresholds endpoints used by the Setup panel
|
|
5
|
+
* "Thresholds" sub-tab to tune the safety margin + fire point per model.
|
|
6
|
+
*
|
|
7
|
+
* PREVENT-PI-004: type definitions only, no network code.
|
|
8
|
+
* PREVENT-011: no `any` type — all types are explicit.
|
|
9
|
+
*/
|
|
10
|
+
/** A per-model threshold (mirrors src/store/sqlite/model-thresholds.ts
|
|
11
|
+
* ModelThreshold — duplicated here so api-contracts has no cross-package
|
|
12
|
+
* import into src/, per the ast-grep:no-relative-cross-package-import rule). */
|
|
13
|
+
export interface ModelThresholdDTO {
|
|
14
|
+
readonly modelId: string;
|
|
15
|
+
readonly safetyMarginPct: number;
|
|
16
|
+
readonly firePointPct: number;
|
|
17
|
+
readonly updatedAt: number | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** A model known to this repo (most-recent model_snapshots row, deduped). */
|
|
21
|
+
export interface KnownModel {
|
|
22
|
+
readonly modelId: string;
|
|
23
|
+
readonly provider: string;
|
|
24
|
+
readonly modelName: string | null;
|
|
25
|
+
readonly contextWindow: number;
|
|
26
|
+
readonly maxTokens: number;
|
|
27
|
+
/** True when an override row exists in model_thresholds. */
|
|
28
|
+
readonly hasOverride: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Response for GET /api/model-thresholds — every known model with its
|
|
33
|
+
* threshold (override if present, otherwise defaults) for the UI table.
|
|
34
|
+
*/
|
|
35
|
+
export interface ModelThresholdsResponse {
|
|
36
|
+
readonly defaults: {
|
|
37
|
+
readonly safetyMarginPct: number;
|
|
38
|
+
readonly firePointPct: number;
|
|
39
|
+
readonly safetyMarginRange: readonly [number, number];
|
|
40
|
+
readonly firePointRange: readonly [number, number];
|
|
41
|
+
};
|
|
42
|
+
readonly models: ReadonlyArray<
|
|
43
|
+
KnownModel & {
|
|
44
|
+
readonly threshold: {
|
|
45
|
+
readonly safetyMarginPct: number;
|
|
46
|
+
readonly firePointPct: number;
|
|
47
|
+
readonly isOverride: boolean;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Request body for PUT /api/model-thresholds. */
|
|
54
|
+
export interface ModelThresholdPutRequest {
|
|
55
|
+
readonly modelId: string;
|
|
56
|
+
readonly safetyMarginPct: number;
|
|
57
|
+
readonly firePointPct: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Response for PUT /api/model-thresholds. */
|
|
61
|
+
export interface ModelThresholdPutResponse {
|
|
62
|
+
readonly threshold: ModelThresholdDTO;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Response for DELETE /api/model-thresholds/:modelId. */
|
|
66
|
+
export interface ModelThresholdDeleteResponse {
|
|
67
|
+
readonly deleted: boolean;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export type ModelThresholdsError =
|
|
71
|
+
| { readonly error: "method_not_allowed" }
|
|
72
|
+
| { readonly error: "invalid_json" }
|
|
73
|
+
| { readonly error: "body_too_large" }
|
|
74
|
+
| { readonly error: "missing_model_id" }
|
|
75
|
+
| { readonly error: "invalid_pct"; readonly detail: string }
|
|
76
|
+
| { readonly error: "internal" };
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dashboard-server/routes-model-thresholds.ts — per-model compaction
|
|
3
|
+
* thresholds (S52 / v0.16.1).
|
|
4
|
+
*
|
|
5
|
+
* Routes for the Setup panel "Thresholds" sub-tab.
|
|
6
|
+
*
|
|
7
|
+
* GET /api/model-thresholds — list every known model + its
|
|
8
|
+
* threshold (override or default),
|
|
9
|
+
* for the dashboard table.
|
|
10
|
+
* PUT /api/model-thresholds — upsert a per-model override.
|
|
11
|
+
* Body: ModelThresholdPutRequest.
|
|
12
|
+
* DELETE /api/model-thresholds/:modelId — delete the override (revert to
|
|
13
|
+
* env/defaults).
|
|
14
|
+
*
|
|
15
|
+
* Guardrails: PREVENT-PI-004 (loopback), PREVENT-001 (null-safe JSON),
|
|
16
|
+
* PREVENT-002 (parameterized via model-thresholds.ts),
|
|
17
|
+
* PREVENT-011 (no `any`).
|
|
18
|
+
*/
|
|
19
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
20
|
+
import type { RouteContext } from "./routes-core.js";
|
|
21
|
+
import type {
|
|
22
|
+
ModelThresholdsResponse,
|
|
23
|
+
ModelThresholdPutResponse,
|
|
24
|
+
ModelThresholdDeleteResponse,
|
|
25
|
+
ModelThresholdsError,
|
|
26
|
+
KnownModel,
|
|
27
|
+
} from "./api-contracts/model-thresholds.js";
|
|
28
|
+
import {
|
|
29
|
+
DEFAULT_SAFETY_MARGIN_PCT,
|
|
30
|
+
DEFAULT_FIRE_POINT_PCT,
|
|
31
|
+
MAX_SAFETY_MARGIN_PCT,
|
|
32
|
+
MIN_SAFETY_MARGIN_PCT,
|
|
33
|
+
MIN_FIRE_POINT_PCT,
|
|
34
|
+
MAX_FIRE_POINT_PCT,
|
|
35
|
+
getModelThreshold,
|
|
36
|
+
listModelThresholds,
|
|
37
|
+
putModelThreshold,
|
|
38
|
+
deleteModelThreshold,
|
|
39
|
+
} from "../../src/store/sqlite/model-thresholds.js";
|
|
40
|
+
import { listModelSnapshots } from "../../src/store/sqlite/model-snapshots.js";
|
|
41
|
+
|
|
42
|
+
function send(res: ServerResponse, status: number, body: unknown): void {
|
|
43
|
+
// guardrails-allow PREVENT-PI-004: loopback dashboard response (local)
|
|
44
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
45
|
+
res.end(JSON.stringify(body));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Read + JSON.parse a request body (被骗: spans POST/PUT/DELETE).
|
|
49
|
+
* Capped at 64KB; null-safe. */
|
|
50
|
+
function readBody(
|
|
51
|
+
req: IncomingMessage,
|
|
52
|
+
cb: (
|
|
53
|
+
result:
|
|
54
|
+
| { ok: true; value: Record<string, unknown> }
|
|
55
|
+
| { ok: false; error: ModelThresholdsError["error"] },
|
|
56
|
+
) => void,
|
|
57
|
+
): void {
|
|
58
|
+
let body = "";
|
|
59
|
+
let tooBig = false;
|
|
60
|
+
req.on("data", (chunk: Buffer) => {
|
|
61
|
+
if (body.length > 65536) {
|
|
62
|
+
tooBig = true;
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
body += chunk.toString();
|
|
66
|
+
});
|
|
67
|
+
req.on("end", () => {
|
|
68
|
+
if (tooBig) return cb({ ok: false, error: "body_too_large" });
|
|
69
|
+
try {
|
|
70
|
+
const v = body ? JSON.parse(body) : {}; // PREVENT-001: type-checked
|
|
71
|
+
if (typeof v !== "object" || v === null || Array.isArray(v)) {
|
|
72
|
+
return cb({ ok: false, error: "invalid_json" });
|
|
73
|
+
}
|
|
74
|
+
cb({ ok: true, value: v as Record<string, unknown> });
|
|
75
|
+
} catch {
|
|
76
|
+
cb({ ok: false, error: "invalid_json" });
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
req.on("error", () => cb({ ok: false, error: "invalid_json" }));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Read body that also handles 0 bytes. */
|
|
83
|
+
function requireNumber(v: unknown, label: string): number {
|
|
84
|
+
const n = Number(v);
|
|
85
|
+
if (!Number.isFinite(n)) {
|
|
86
|
+
throw new Error(`${label} must be a finite number, got ${String(v)}`);
|
|
87
|
+
}
|
|
88
|
+
return n;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** List known models from model_snapshots, deduped by model_id (latest wins). */
|
|
92
|
+
function listKnownModels(stateDir: string): KnownModel[] {
|
|
93
|
+
const snaps = listModelSnapshots(stateDir);
|
|
94
|
+
// Dedup by model_id, keep latest (highest capturedAt).
|
|
95
|
+
const byId = new Map<string, (typeof snaps)[number]>();
|
|
96
|
+
for (const s of snaps) {
|
|
97
|
+
const existing = byId.get(s.modelId);
|
|
98
|
+
if (!existing || s.capturedAt > existing.capturedAt) byId.set(s.modelId, s);
|
|
99
|
+
}
|
|
100
|
+
return Array.from(byId.values()).map((s) => ({
|
|
101
|
+
modelId: s.modelId,
|
|
102
|
+
provider: s.provider,
|
|
103
|
+
modelName: s.modelName,
|
|
104
|
+
contextWindow: s.contextWindow,
|
|
105
|
+
maxTokens: s.maxTokens,
|
|
106
|
+
hasOverride: getModelThreshold(s.modelId, stateDir) !== null,
|
|
107
|
+
}));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function handleModelThresholds(
|
|
111
|
+
req: IncomingMessage,
|
|
112
|
+
res: ServerResponse,
|
|
113
|
+
ctx: RouteContext,
|
|
114
|
+
): boolean {
|
|
115
|
+
if (!req.url?.startsWith("/api/model-thresholds")) return false;
|
|
116
|
+
const url = new URL(req.url, "http://x"); // guardrails-allow PREVENT-PI-004: localhost dashboard URL base (loopback-only)
|
|
117
|
+
|
|
118
|
+
// GET: list every known model + its threshold.
|
|
119
|
+
if (req.method === "GET" && url.pathname === "/api/model-thresholds") {
|
|
120
|
+
try {
|
|
121
|
+
const known = listKnownModels(ctx.stateDir);
|
|
122
|
+
const overrides = new Map(
|
|
123
|
+
listModelThresholds(ctx.stateDir).map((t) => [t.modelId, t]),
|
|
124
|
+
);
|
|
125
|
+
const body: ModelThresholdsResponse = {
|
|
126
|
+
defaults: {
|
|
127
|
+
safetyMarginPct: DEFAULT_SAFETY_MARGIN_PCT,
|
|
128
|
+
firePointPct: DEFAULT_FIRE_POINT_PCT,
|
|
129
|
+
safetyMarginRange: [MIN_SAFETY_MARGIN_PCT, MAX_SAFETY_MARGIN_PCT],
|
|
130
|
+
firePointRange: [MIN_FIRE_POINT_PCT, MAX_FIRE_POINT_PCT],
|
|
131
|
+
},
|
|
132
|
+
models: known.map((m) => {
|
|
133
|
+
const ov = overrides.get(m.modelId);
|
|
134
|
+
return {
|
|
135
|
+
...m,
|
|
136
|
+
threshold: ov
|
|
137
|
+
? {
|
|
138
|
+
safetyMarginPct: ov.safetyMarginPct,
|
|
139
|
+
firePointPct: ov.firePointPct,
|
|
140
|
+
isOverride: true,
|
|
141
|
+
}
|
|
142
|
+
: {
|
|
143
|
+
safetyMarginPct: DEFAULT_SAFETY_MARGIN_PCT,
|
|
144
|
+
firePointPct: DEFAULT_FIRE_POINT_PCT,
|
|
145
|
+
isOverride: false,
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
}),
|
|
149
|
+
};
|
|
150
|
+
send(res, 200, body);
|
|
151
|
+
} catch (e) {
|
|
152
|
+
send(res, 500, { error: "internal", detail: String(e) });
|
|
153
|
+
}
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// PUT: upsert a per-model override.
|
|
158
|
+
if (req.method === "PUT" && url.pathname === "/api/model-thresholds") {
|
|
159
|
+
readBody(req, (result) => {
|
|
160
|
+
if (!result.ok) return send(res, 400, { error: result.error });
|
|
161
|
+
try {
|
|
162
|
+
const v = result.value;
|
|
163
|
+
const modelId = v.modelId;
|
|
164
|
+
if (typeof modelId !== "string" || !modelId.trim()) {
|
|
165
|
+
return send(res, 400, { error: "missing_model_id" });
|
|
166
|
+
}
|
|
167
|
+
let safety: number;
|
|
168
|
+
let fire: number;
|
|
169
|
+
try {
|
|
170
|
+
safety = requireNumber(v.safetyMarginPct, "safetyMarginPct");
|
|
171
|
+
fire = requireNumber(v.firePointPct, "firePointPct");
|
|
172
|
+
} catch (e) {
|
|
173
|
+
return send(res, 400, {
|
|
174
|
+
error: "invalid_pct",
|
|
175
|
+
detail: e instanceof Error ? e.message : String(e),
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
const t = putModelThreshold(modelId, safety, fire, ctx.stateDir);
|
|
179
|
+
const body: ModelThresholdPutResponse = { threshold: t };
|
|
180
|
+
send(res, 200, body);
|
|
181
|
+
} catch (e) {
|
|
182
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
183
|
+
if (/must be in \[/.test(msg)) {
|
|
184
|
+
return send(res, 400, { error: "invalid_pct", detail: msg });
|
|
185
|
+
}
|
|
186
|
+
send(res, 500, { error: "internal", detail: msg });
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// DELETE: /api/model-thresholds/:modelId
|
|
193
|
+
if (
|
|
194
|
+
req.method === "DELETE" &&
|
|
195
|
+
url.pathname.startsWith("/api/model-thresholds/")
|
|
196
|
+
) {
|
|
197
|
+
const modelId = decodeURIComponent(
|
|
198
|
+
url.pathname.slice("/api/model-thresholds/".length),
|
|
199
|
+
);
|
|
200
|
+
if (!modelId) {
|
|
201
|
+
send(res, 400, { error: "missing_model_id" });
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
try {
|
|
205
|
+
const deleted = deleteModelThreshold(modelId, ctx.stateDir);
|
|
206
|
+
const body: ModelThresholdDeleteResponse = { deleted };
|
|
207
|
+
send(res, 200, body);
|
|
208
|
+
} catch (e) {
|
|
209
|
+
send(res, 500, { error: "internal", detail: String(e) });
|
|
210
|
+
}
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Catch-all: only GET/PUT/DELETE on /api/model-thresholds[*] are supported.
|
|
215
|
+
if (url.pathname === "/api/model-thresholds") {
|
|
216
|
+
send(res, 405, {
|
|
217
|
+
error: "method_not_allowed",
|
|
218
|
+
} satisfies ModelThresholdsError);
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
@@ -22,12 +22,21 @@ export { handleTurns } from "./routes-turns.js";
|
|
|
22
22
|
export { handleMaintenance } from "./routes-maintenance.js";
|
|
23
23
|
export { handleProviderCache } from "./routes-cache.js";
|
|
24
24
|
export { handleMemoryStatus } from "./routes-memory.js";
|
|
25
|
-
export {
|
|
25
|
+
export {
|
|
26
|
+
handleSetupStatus,
|
|
27
|
+
handleSetupDetect,
|
|
28
|
+
handleSetupConfigure,
|
|
29
|
+
} from "./routes-setup.js";
|
|
26
30
|
export { handleMemoryMap } from "./routes-memory-map.js";
|
|
27
31
|
export { handleRaptorTree, handleRaptorBuildHistory } from "./routes-raptor.js";
|
|
28
32
|
export { handleCacheStripes } from "./routes-cache.js";
|
|
29
|
-
export {
|
|
33
|
+
export {
|
|
34
|
+
handleContextHealth,
|
|
35
|
+
handleCachePoison,
|
|
36
|
+
handleHealthSettings,
|
|
37
|
+
} from "./routes-health.js";
|
|
30
38
|
export { handleEmbedderHealth } from "./routes-embedder-health.js";
|
|
31
39
|
export { handleRagSettings } from "./routes-rag-settings.js";
|
|
32
40
|
export { handleRagMetrics } from "./routes-rag-metrics.js";
|
|
41
|
+
export { handleModelThresholds } from "./routes-model-thresholds.js";
|
|
33
42
|
export { handleWiki } from "./routes-wiki.js";
|
|
@@ -54,6 +54,7 @@ import {
|
|
|
54
54
|
handleEmbedderHealth,
|
|
55
55
|
handleRagSettings,
|
|
56
56
|
handleRagMetrics,
|
|
57
|
+
handleModelThresholds,
|
|
57
58
|
handleWiki,
|
|
58
59
|
handleStatic,
|
|
59
60
|
} from "./routes.js";
|
|
@@ -160,7 +161,9 @@ export async function launchDashboardServer(
|
|
|
160
161
|
map: "application/json",
|
|
161
162
|
};
|
|
162
163
|
const payload: string | Uint8Array =
|
|
163
|
-
ext === "html"
|
|
164
|
+
ext === "html"
|
|
165
|
+
? injectUiFlag(readFileSync(file, "utf-8"))
|
|
166
|
+
: readFileSync(file);
|
|
164
167
|
res.writeHead(200, {
|
|
165
168
|
"Content-Type": types[ext] ?? "application/octet-stream",
|
|
166
169
|
});
|
|
@@ -270,6 +273,7 @@ export async function launchDashboardServer(
|
|
|
270
273
|
if (handleEmbedderHealth(req, res, ctx)) return;
|
|
271
274
|
if (handleRagSettings(req, res, ctx)) return;
|
|
272
275
|
if (handleRagMetrics(req, res, ctx)) return;
|
|
276
|
+
if (handleModelThresholds(req, res, ctx)) return;
|
|
273
277
|
if (handleWiki(req, res, ctx)) return;
|
|
274
278
|
handleStatic(req, res, ctx);
|
|
275
279
|
});
|
|
@@ -17,9 +17,16 @@ import type {
|
|
|
17
17
|
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
18
18
|
import {
|
|
19
19
|
openStore,
|
|
20
|
+
resolveModelThreshold,
|
|
21
|
+
DEFAULT_SAFETY_MARGIN_PCT,
|
|
22
|
+
DEFAULT_FIRE_POINT_PCT,
|
|
20
23
|
} from "../../src/store/sqlite.js";
|
|
21
24
|
import { autoCompactCheck } from "../../src/compact.js";
|
|
22
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
estimateSessionTokens,
|
|
27
|
+
estimateBlockTokens,
|
|
28
|
+
estimateMessageTokens,
|
|
29
|
+
} from "../../src/tokens.js";
|
|
23
30
|
import type { MegaRuntime } from "../mega-runtime.js";
|
|
24
31
|
import { runCompact, piCompactWouldNoop } from "../mega-pipeline.js";
|
|
25
32
|
import {
|
|
@@ -94,59 +101,75 @@ export function registerContextHandler(
|
|
|
94
101
|
const view = viewForFallback ?? runtime.engineView(messages);
|
|
95
102
|
|
|
96
103
|
// S27 DB-mirror: append incoming messages to raw_transcript.
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
104
|
+
// Runs BEFORE fast-gate so every message is captured, even if we
|
|
105
|
+
// don't compact this turn. Append is idempotent (content_hash PK).
|
|
106
|
+
// F3: high-water mark (mirror-append.ts) skips already-processed
|
|
107
|
+
// messages on subsequent events. On fork/rewind (shorter list or
|
|
108
|
+
// boundary hash mismatch) the mark is dropped, falling back to a
|
|
109
|
+
// full reprocess.
|
|
110
|
+
if (config.dbMirror) {
|
|
111
|
+
try {
|
|
112
|
+
const db = openStore(runtime.currentStateDir);
|
|
113
|
+
appendMirrorMessages(
|
|
114
|
+
db,
|
|
115
|
+
messages,
|
|
116
|
+
runtime.rt.sessionId,
|
|
117
|
+
epochIdFor(runtime.rt.sessionId),
|
|
118
|
+
runtime.currentTurn,
|
|
119
|
+
);
|
|
120
|
+
// P2.2: populate conversation_thread + tool_results tables for
|
|
121
|
+
// prompt-cache analytics and durable separation. The live-array
|
|
122
|
+
// separation (buildSeparatedPrompt / buildCacheOptimizedPrompt in
|
|
123
|
+
// tailResult above) is sufficient for the prompt-construction path;
|
|
124
|
+
// these DB writes persist the split for post-hoc analysis, dashboard
|
|
125
|
+
// queries, and future readers. Non-fatal — failure here never breaks
|
|
126
|
+
// the agent loop (PREVENT-PI-004: zero network, local SQLite only).
|
|
127
|
+
{
|
|
128
|
+
const sid = runtime.rt.sessionId;
|
|
129
|
+
const turn = runtime.currentTurn;
|
|
130
|
+
const now = Date.now();
|
|
131
|
+
const threadStmt = db.prepare(
|
|
132
|
+
"INSERT OR IGNORE INTO conversation_thread (conversation_id, role, content, turn_index, timestamp) VALUES (?, ?, ?, ?, ?)",
|
|
112
133
|
);
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const now = Date.now();
|
|
124
|
-
const threadStmt = db.prepare(
|
|
125
|
-
"INSERT OR IGNORE INTO conversation_thread (conversation_id, role, content, turn_index, timestamp) VALUES (?, ?, ?, ?, ?)",
|
|
126
|
-
);
|
|
127
|
-
const toolStmt = db.prepare(
|
|
128
|
-
"INSERT OR IGNORE INTO tool_results (conversation_id, role, content, turn_index, timestamp) VALUES (?, ?, ?, ?, ?)",
|
|
129
|
-
);
|
|
130
|
-
for (const m of messages) {
|
|
131
|
-
const role = m.role;
|
|
132
|
-
const content = messageContentText(m);
|
|
133
|
-
if (role === "user" || role === "assistant") {
|
|
134
|
-
threadStmt.run(sid, role, content, turn, now);
|
|
135
|
-
} else if (role === "toolResult" || role === "bashExecution") {
|
|
136
|
-
toolStmt.run(sid, role, content, turn, now);
|
|
137
|
-
}
|
|
134
|
+
const toolStmt = db.prepare(
|
|
135
|
+
"INSERT OR IGNORE INTO tool_results (conversation_id, role, content, turn_index, timestamp) VALUES (?, ?, ?, ?, ?)",
|
|
136
|
+
);
|
|
137
|
+
for (const m of messages) {
|
|
138
|
+
const role = m.role;
|
|
139
|
+
const content = messageContentText(m);
|
|
140
|
+
if (role === "user" || role === "assistant") {
|
|
141
|
+
threadStmt.run(sid, role, content, turn, now);
|
|
142
|
+
} else if (role === "toolResult" || role === "bashExecution") {
|
|
143
|
+
toolStmt.run(sid, role, content, turn, now);
|
|
138
144
|
}
|
|
139
145
|
}
|
|
140
|
-
} catch (e) {
|
|
141
|
-
runtime.logger.warn("db-mirror-append-fail", { error: String(e) });
|
|
142
146
|
}
|
|
147
|
+
} catch (e) {
|
|
148
|
+
runtime.logger.warn("db-mirror-append-fail", { error: String(e) });
|
|
143
149
|
}
|
|
150
|
+
}
|
|
144
151
|
|
|
152
|
+
// S52 / v0.16.1: per-model threshold override. The user can tune the
|
|
153
|
+
// fire point + safety margin PER MODEL (different providers' models range
|
|
154
|
+
// 8K-1M+ context, so one global tier % is wrong). Falls back to env/default
|
|
155
|
+
// when no override row exists. Computed once here + reused in the tail cap
|
|
156
|
+
// below; the lookup is a single SQLite PK hit (cheap; cached after the
|
|
157
|
+
// first read in a session).
|
|
158
|
+
const _modelIdForThreshold = runtime.currentModel?.modelId ?? null;
|
|
159
|
+
const _perModelThreshold = resolveModelThreshold(_modelIdForThreshold, {
|
|
160
|
+
safetyMarginFallback: DEFAULT_SAFETY_MARGIN_PCT,
|
|
161
|
+
firePointFallback:
|
|
162
|
+
config.tierPct != null
|
|
163
|
+
? Math.round(config.tierPct * 100)
|
|
164
|
+
: DEFAULT_FIRE_POINT_PCT,
|
|
165
|
+
stateDir: runtime.currentStateDir,
|
|
166
|
+
});
|
|
145
167
|
// S29 FAST GATE: drive the auto-trigger off the context % (the number the
|
|
146
168
|
// menu bar shows), NOT the token count — the model under-reports tokens,
|
|
147
169
|
// so a token-only gate misses the overshoot that causes max-output-tokens
|
|
148
|
-
// truncation. The fire point is the
|
|
149
|
-
//
|
|
170
|
+
// truncation. The fire point is the per-model override when present,
|
|
171
|
+
// otherwise the tier's percent threshold (tierPct) unless overridden by
|
|
172
|
+
// MEGACOMPACT_AUTO_PCT_TRIGGER. `custom` (absolute
|
|
150
173
|
// MEGACOMPACT_THRESHOLD_TOKENS, tierPct null) is an explicit opt-out of
|
|
151
174
|
// percent scaling — it keeps the token gate. When pct is unavailable
|
|
152
175
|
// (window unknown / a model that doesn't report percent) a tiered config
|
|
@@ -154,7 +177,12 @@ export function registerContextHandler(
|
|
|
154
177
|
// skipping compaction — a percent-only gate would regress that.
|
|
155
178
|
let gatePassed = false;
|
|
156
179
|
if (config.tierPct != null && pct != null) {
|
|
157
|
-
|
|
180
|
+
// Per-model override is a % (10-90); tierPct is a fraction (0.1-1.0).
|
|
181
|
+
// Prefer the override; fall back to autoPctTrigger + tierPct.
|
|
182
|
+
const tierPctFraction = config.autoPctTrigger ?? config.tierPct;
|
|
183
|
+
const perModelFraction = _perModelThreshold.firePointPct / 100;
|
|
184
|
+
const firePct =
|
|
185
|
+
_modelIdForThreshold != null ? perModelFraction : tierPctFraction;
|
|
158
186
|
gatePassed = pct / 100 >= firePct;
|
|
159
187
|
} else {
|
|
160
188
|
// custom tier OR tiered-but-pct-unavailable → token gate (S27 fallback).
|
|
@@ -204,7 +232,10 @@ export function registerContextHandler(
|
|
|
204
232
|
runtime.snapshot(ctx);
|
|
205
233
|
// v0.8.7: shallow-copy the cached summary so pi's transformContext can't
|
|
206
234
|
// mutate the shared reference across replays (audit P3).
|
|
207
|
-
const replayView = [
|
|
235
|
+
const replayView = [
|
|
236
|
+
{ ...runtime.trimCache.summaryAgentMsg },
|
|
237
|
+
...recent,
|
|
238
|
+
];
|
|
208
239
|
return tailResult(replayView) ?? { messages: replayView };
|
|
209
240
|
}
|
|
210
241
|
// else: context grew enough → fall through to re-compact (cache is stale)
|
|
@@ -324,23 +355,21 @@ export function registerContextHandler(
|
|
|
324
355
|
compactedFrom: ran.result.compactedFrom,
|
|
325
356
|
summary: ran.result.summary,
|
|
326
357
|
anchorUserMessages,
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
criticalOver:
|
|
342
|
-
(pct ?? 0) >= 90 ||
|
|
343
|
-
pressure >= 0.9,
|
|
358
|
+
// CRITICAL-OVER ESCAPE HATCH: when context is at/over ~90% of the
|
|
359
|
+
// window, relief takes priority over the anchor floor. Without this,
|
|
360
|
+
// computeLiveTrimCut bails to null (can't satisfy the floor) and the
|
|
361
|
+
// model is fed a raw overflow that errors every turn — the
|
|
362
|
+
// "Already compacted" + overflow death-spiral (2026-08-01 incident).
|
|
363
|
+
// A thin anchor is recoverable; an overflowed session is not.
|
|
364
|
+
//
|
|
365
|
+
// CRITICAL: pct is null for OpenAI-compatible providers that don't
|
|
366
|
+
// report usage.percent (e.g. neuralwatt). Without the token-pressure
|
|
367
|
+
// fallback the hatch never armed → cut=null → raw overflow → 400
|
|
368
|
+
// "conversation too long even after compaction" (2026-08-03 incident
|
|
369
|
+
// on glm-5.2-short, 200K window). Now also fires on pressure >= 0.9
|
|
370
|
+
// (token-basis) so the hatch arms regardless of whether the provider
|
|
371
|
+
// reports pct.
|
|
372
|
+
criticalOver: (pct ?? 0) >= 90 || pressure >= 0.9,
|
|
344
373
|
});
|
|
345
374
|
if (cut === null) {
|
|
346
375
|
runtime.diagCtxCutNull++;
|
|
@@ -386,6 +415,8 @@ export function registerContextHandler(
|
|
|
386
415
|
// turn so the agent can respond). This is a last-resort HARD cap — it
|
|
387
416
|
// only fires when the preserved tail alone is oversized, which is rare.
|
|
388
417
|
const ctxWindow = runtime.lastCtxWindow;
|
|
418
|
+
// Reuse the per-model threshold resolved at the gate (single lookup).
|
|
419
|
+
const modelThreshold = _perModelThreshold;
|
|
389
420
|
// Reserve room for output tokens. Use the model's reported max output
|
|
390
421
|
// when known; fall back to 10% of the window (scales with any model —
|
|
391
422
|
// 20K for a 200K window, 100K for a 1M window) so we never let the
|
|
@@ -397,10 +428,11 @@ export function registerContextHandler(
|
|
|
397
428
|
let recent = recentRaw;
|
|
398
429
|
if (ctxWindow > 0 && recentRaw.length > 1) {
|
|
399
430
|
const summaryTokens = estimateBlockTokens(summaryMsg.text);
|
|
400
|
-
// Reserve: summary + max output +
|
|
401
|
-
const safetyMargin = Math.ceil(
|
|
402
|
-
|
|
403
|
-
|
|
431
|
+
// Reserve: summary + max output + per-model safety margin (0-20%).
|
|
432
|
+
const safetyMargin = Math.ceil(
|
|
433
|
+
ctxWindow * (modelThreshold.safetyMarginPct / 100),
|
|
434
|
+
);
|
|
435
|
+
const budget = ctxWindow - maxOutput - safetyMargin - summaryTokens;
|
|
404
436
|
if (budget > 0) {
|
|
405
437
|
// Walk recent from the front, dropping oldest first until the
|
|
406
438
|
// remaining tail fits. Use the AgentMessage→engine-text estimate via
|
|
@@ -420,6 +452,7 @@ export function registerContextHandler(
|
|
|
420
452
|
sessionId: runtime.rt.sessionId,
|
|
421
453
|
dropped: startIdx,
|
|
422
454
|
tailTokens,
|
|
455
|
+
safetyMarginPct: modelThreshold.safetyMarginPct,
|
|
423
456
|
budget,
|
|
424
457
|
ctxWindow,
|
|
425
458
|
});
|
|
@@ -468,7 +501,11 @@ export function registerContextHandler(
|
|
|
468
501
|
ctxPct: pct,
|
|
469
502
|
ctxTokens: usage?.tokens ?? null,
|
|
470
503
|
});
|
|
471
|
-
return
|
|
504
|
+
return (
|
|
505
|
+
tailResult([summaryAgentMsg, ...recent]) ?? {
|
|
506
|
+
messages: [summaryAgentMsg, ...recent],
|
|
507
|
+
}
|
|
508
|
+
);
|
|
472
509
|
} catch {
|
|
473
510
|
runtime.diagCtxThrown++;
|
|
474
511
|
return tailResult() ?? undefined; // non-fatal: no trim this call; the next context event retries
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-mega-compact",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.2",
|
|
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",
|