pi-mega-compact 0.15.4 → 0.16.1
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 +102 -9
- 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/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-eCNoaclv.js.map +1 -0
- 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/components/SessionContextGauges.tsx +6 -1
- 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/resolve-snapshot.ts +1 -1
- 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 +159 -50
- 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/extensions/dashboard-client/dist/assets/MemoryMapTab-Dvu7vkOm.js +0 -2
- package/extensions/dashboard-client/dist/assets/OverviewTab-iKiEcOLL.js.map +0 -1
- 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" };
|
|
@@ -55,7 +55,7 @@ function mostRecentOtherRepoStateDir(launchStateDir: string): string | null {
|
|
|
55
55
|
const { DatabaseSync } =
|
|
56
56
|
require("node:sqlite") as typeof import("node:sqlite");
|
|
57
57
|
db = new DatabaseSync(indexPath, { readOnly: true });
|
|
58
|
-
|
|
58
|
+
const rows = db
|
|
59
59
|
.prepare(
|
|
60
60
|
`SELECT state_dir, last_seen FROM repo_registry
|
|
61
61
|
WHERE state_dir IS NOT NULL AND last_seen IS NOT NULL
|
|
@@ -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)
|
|
@@ -330,7 +361,15 @@ export function registerContextHandler(
|
|
|
330
361
|
// model is fed a raw overflow that errors every turn — the
|
|
331
362
|
// "Already compacted" + overflow death-spiral (2026-08-01 incident).
|
|
332
363
|
// A thin anchor is recoverable; an overflowed session is not.
|
|
333
|
-
|
|
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,
|
|
334
373
|
});
|
|
335
374
|
if (cut === null) {
|
|
336
375
|
runtime.diagCtxCutNull++;
|
|
@@ -357,7 +396,73 @@ export function registerContextHandler(
|
|
|
357
396
|
// on every replay within the same compaction epoch.
|
|
358
397
|
timestamp: runtime.rt.lastCompactAt ?? Date.now(),
|
|
359
398
|
} as unknown as AgentMessage;
|
|
360
|
-
const
|
|
399
|
+
const recentRaw = messages.slice(cut); // guardrails-allow PREVENT-PI-002: `cut` is the pre-sanitized `compactedFrom` produced by src/boundary.ts computeDropRange, so the preserved run begins on a toolPair-safe index.
|
|
400
|
+
|
|
401
|
+
// FIX 2 (2026-08-03 incident): TOKEN-BUDGET CAP on the live-trim view.
|
|
402
|
+
// Compaction fires at tier% of the window (140K for a 200K window),
|
|
403
|
+
// but a SINGLE turn can inject a huge tool output (file read, bash) that
|
|
404
|
+
// jumps context from 139K → 199K+ before the next gate fires. When that
|
|
405
|
+
// happens [summary + preserved tail] can STILL exceed the model window,
|
|
406
|
+
// and the provider rejects with 400 "conversation too long even after
|
|
407
|
+
// compaction". The anchor floor (PREVENT-PI-001) keeps ≥N user messages
|
|
408
|
+
// but has NO token cap, so a 2-message tail of two 80K bash outputs sails
|
|
409
|
+
// right past the window.
|
|
410
|
+
//
|
|
411
|
+
// Cap: when the model context window is known, reserve room for the
|
|
412
|
+
// summary + the model's max output tokens + a 10% safety margin, then
|
|
413
|
+
// drop oldest preserved messages from the front of `recentRaw` until the
|
|
414
|
+
// tail fits. Never drops below the FINAL message (always keep the latest
|
|
415
|
+
// turn so the agent can respond). This is a last-resort HARD cap — it
|
|
416
|
+
// only fires when the preserved tail alone is oversized, which is rare.
|
|
417
|
+
const ctxWindow = runtime.lastCtxWindow;
|
|
418
|
+
// Reuse the per-model threshold resolved at the gate (single lookup).
|
|
419
|
+
const modelThreshold = _perModelThreshold;
|
|
420
|
+
// Reserve room for output tokens. Use the model's reported max output
|
|
421
|
+
// when known; fall back to 10% of the window (scales with any model —
|
|
422
|
+
// 20K for a 200K window, 100K for a 1M window) so we never let the
|
|
423
|
+
// preserved tail eat the model's output budget when maxTokens is unknown.
|
|
424
|
+
const maxOutput =
|
|
425
|
+
runtime.currentModel?.maxTokens && runtime.currentModel.maxTokens > 0
|
|
426
|
+
? runtime.currentModel.maxTokens
|
|
427
|
+
: Math.ceil(ctxWindow * 0.1);
|
|
428
|
+
let recent = recentRaw;
|
|
429
|
+
if (ctxWindow > 0 && recentRaw.length > 1) {
|
|
430
|
+
const summaryTokens = estimateBlockTokens(summaryMsg.text);
|
|
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;
|
|
436
|
+
if (budget > 0) {
|
|
437
|
+
// Walk recent from the front, dropping oldest first until the
|
|
438
|
+
// remaining tail fits. Use the AgentMessage→engine-text estimate via
|
|
439
|
+
// messageContentText (already imported) + estimateMessageTokens.
|
|
440
|
+
let tailTokens = 0;
|
|
441
|
+
for (let i = recentRaw.length - 1; i >= 0; i--) {
|
|
442
|
+
const m = recentRaw[i];
|
|
443
|
+
tailTokens += estimateMessageTokens({
|
|
444
|
+
text: messageContentText(m),
|
|
445
|
+
});
|
|
446
|
+
if (tailTokens > budget) {
|
|
447
|
+
// Keep from i+1 onward; but never fewer than the final message.
|
|
448
|
+
const startIdx = Math.min(i + 1, recentRaw.length - 1);
|
|
449
|
+
if (startIdx > 0) {
|
|
450
|
+
recent = recentRaw.slice(startIdx);
|
|
451
|
+
runtime.logger.warn("live-trim-tail-cap", {
|
|
452
|
+
sessionId: runtime.rt.sessionId,
|
|
453
|
+
dropped: startIdx,
|
|
454
|
+
tailTokens,
|
|
455
|
+
safetyMarginPct: modelThreshold.safetyMarginPct,
|
|
456
|
+
budget,
|
|
457
|
+
ctxWindow,
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
break;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
361
466
|
// v0.8.6: cache the trim view so subsequent gated calls in this epoch
|
|
362
467
|
// replay it verbatim (stabilizing the KV-cache prefix) instead of
|
|
363
468
|
// regenerating a fresh summary + sentinel every fire.
|
|
@@ -396,7 +501,11 @@ export function registerContextHandler(
|
|
|
396
501
|
ctxPct: pct,
|
|
397
502
|
ctxTokens: usage?.tokens ?? null,
|
|
398
503
|
});
|
|
399
|
-
return
|
|
504
|
+
return (
|
|
505
|
+
tailResult([summaryAgentMsg, ...recent]) ?? {
|
|
506
|
+
messages: [summaryAgentMsg, ...recent],
|
|
507
|
+
}
|
|
508
|
+
);
|
|
400
509
|
} catch {
|
|
401
510
|
runtime.diagCtxThrown++;
|
|
402
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.
|
|
3
|
+
"version": "0.16.1",
|
|
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",
|