pi-mega-compact 0.9.2 → 0.11.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.
- package/README.md +3 -2
- package/dist/extensions/dashboard-server/api-contracts/endpoints.js +95 -47
- package/dist/extensions/dashboard-server/api-contracts/index.js +1 -1
- package/dist/extensions/dashboard-server/api-contracts/turns.js +10 -0
- package/dist/extensions/dashboard-server/api-contracts.test.js +3 -2
- package/dist/extensions/dashboard-server/routes-topics.js +87 -0
- package/dist/extensions/dashboard-server/routes-turns.js +264 -0
- package/dist/extensions/dashboard-server/routes.js +3 -1
- package/dist/extensions/dashboard-server/server.js +6 -2
- package/dist/extensions/mega-compact.js +10 -6
- package/dist/extensions/mega-config.js +4 -0
- package/dist/extensions/mega-events/agent-handlers.js +62 -49
- package/dist/extensions/mega-events/context-handler.js +48 -2
- package/dist/extensions/mega-events/session-handlers.js +25 -1
- package/dist/extensions/mega-metrics-cmds.js +90 -0
- package/dist/extensions/mega-pipeline/recall.js +47 -17
- package/dist/extensions/mega-topics-cmds.js +111 -0
- package/dist/extensions/mega-turn-store.js +110 -0
- package/dist/extensions/mega-turn-store.test.js +68 -0
- package/dist/src/config/turns.js +48 -0
- package/dist/src/fork.js +33 -0
- package/dist/src/fork.test.js +81 -0
- package/dist/src/intent.js +51 -0
- package/dist/src/intent.test.js +78 -0
- package/dist/src/metrics/index.js +4 -0
- package/dist/src/metrics/turns.js +118 -0
- package/dist/src/metrics/turns.test.js +152 -0
- package/dist/src/store/sqlite/raw-transcript.js +5 -3
- package/dist/src/store/sqlite.dbmirror.test.js +9 -0
- package/dist/src/store/turns/connection.js +123 -0
- package/dist/src/store/turns/contract-compliance.test.js +428 -0
- package/dist/src/store/turns/index.js +31 -0
- package/dist/src/store/turns/memory-store.js +357 -0
- package/dist/src/store/turns/memory-store.test.js +9 -0
- package/dist/src/store/turns/migrations.js +178 -0
- package/dist/src/store/turns/migrations.test.js +138 -0
- package/dist/src/store/turns/schema.js +115 -0
- package/dist/src/store/turns/sqlite-store.js +455 -0
- package/dist/src/store/turns/sqlite-store.test.js +144 -0
- package/dist/src/store/turns/types.js +16 -0
- package/dist/src/topics/cluster.js +141 -0
- package/dist/src/topics/cluster.test.js +150 -0
- package/dist/src/topics/index.js +6 -0
- package/dist/src/topics/kselection.js +142 -0
- package/dist/src/topics/labels.js +56 -0
- package/dist/src/topics/store.js +119 -0
- package/dist/src/topics/store.test.js +117 -0
- package/dist/src/topics/types.js +8 -0
- package/dist/src/wiki.js +133 -0
- package/dist/src/wiki.test.js +171 -0
- package/extensions/dashboard-client/dist/assets/{AchievementTiles-QPo46Pwg.js → AchievementTiles-DZp_xQOW.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{AchievementTiles-QPo46Pwg.js.map → AchievementTiles-DZp_xQOW.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{AchievementsTab-BDJxRXRY.js → AchievementsTab-BzFHpAHM.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{AchievementsTab-BDJxRXRY.js.map → AchievementsTab-BzFHpAHM.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{CacheTab-CUMqXQjd.js → CacheTab-DEQbTq2T.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{CacheTab-CUMqXQjd.js.map → CacheTab-DEQbTq2T.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{ConfigTab-j1dRzurG.js → ConfigTab-DQ4FG33J.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{ConfigTab-j1dRzurG.js.map → ConfigTab-DQ4FG33J.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{EventsTab-CMglXK4p.js → EventsTab-D5HoEVYo.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{EventsTab-CMglXK4p.js.map → EventsTab-D5HoEVYo.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{GameTab-fpaDoyxi.js → GameTab-Jzsc4yeY.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{GameTab-fpaDoyxi.js.map → GameTab-Jzsc4yeY.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MetricsTab-CWG2crG_.js → MetricsTab-D_XnsEYX.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MetricsTab-CWG2crG_.js.map → MetricsTab-D_XnsEYX.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{OverviewTab-BaV2D2nf.js → OverviewTab-BMREvAth.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{OverviewTab-BaV2D2nf.js.map → OverviewTab-BMREvAth.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{ReposTab-C-0kuZ_0.js → ReposTab-DuyhOktG.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{ReposTab-C-0kuZ_0.js.map → ReposTab-DuyhOktG.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{SessionsTab-CSYhddCA.js → SessionsTab-mz_vz8i2.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SessionsTab-CSYhddCA.js.map → SessionsTab-mz_vz8i2.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-Dy4KRjSM.js → TimeSavedCard-B8AIjNTY.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-Dy4KRjSM.js.map → TimeSavedCard-B8AIjNTY.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/TopicsTab-DKuct4oD.js +2 -0
- package/extensions/dashboard-client/dist/assets/TopicsTab-DKuct4oD.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/TurnsTab-11MGLxY8.js +2 -0
- package/extensions/dashboard-client/dist/assets/TurnsTab-11MGLxY8.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{index-D_WtU2TV.js → index-CqLN7STl.js} +10 -10
- package/extensions/dashboard-client/dist/assets/index-CqLN7STl.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{index-og3ghxyY.css → index-gn66lYD2.css} +1 -1
- package/extensions/dashboard-client/dist/assets/{useSSE-C9yVJlGP.js → useSSE-CXrQSbVY.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{useSSE-C9yVJlGP.js.map → useSSE-CXrQSbVY.js.map} +1 -1
- package/extensions/dashboard-client/dist/index.html +2 -2
- package/extensions/dashboard-client/src/App.tsx +10 -2
- package/extensions/dashboard-client/src/api/client.ts +90 -1
- package/extensions/dashboard-client/src/main.tsx +1 -0
- package/extensions/dashboard-client/src/styles/turns.css +142 -0
- package/extensions/dashboard-client/src/tabs/TopicsTab.tsx +186 -0
- package/extensions/dashboard-client/src/tabs/TurnsTab.tsx +336 -0
- package/extensions/dashboard-server/api-contracts/endpoints.ts +394 -291
- package/extensions/dashboard-server/api-contracts/game-types.ts +141 -88
- package/extensions/dashboard-server/api-contracts/index.ts +153 -133
- package/extensions/dashboard-server/api-contracts/turns.ts +114 -0
- package/extensions/dashboard-server/api-contracts.test.ts +920 -919
- package/extensions/dashboard-server/routes-topics.ts +119 -0
- package/extensions/dashboard-server/routes-turns.ts +305 -0
- package/extensions/dashboard-server/routes.ts +8 -1
- package/extensions/dashboard-server/server.ts +6 -2
- package/extensions/mega-compact.ts +75 -59
- package/extensions/mega-config.ts +232 -220
- package/extensions/mega-events/agent-handlers.ts +194 -151
- package/extensions/mega-events/context-handler.ts +50 -1
- package/extensions/mega-events/session-handlers.ts +26 -5
- package/extensions/mega-metrics-cmds.ts +114 -0
- package/extensions/mega-pipeline/recall.ts +215 -152
- package/extensions/mega-topics-cmds.ts +151 -0
- package/extensions/mega-turn-store.test.ts +108 -0
- package/extensions/mega-turn-store.ts +169 -0
- package/package.json +3 -1
- package/src/config/turns.ts +69 -0
- package/src/fork.test.ts +94 -0
- package/src/fork.ts +78 -0
- package/src/intent.test.ts +87 -0
- package/src/intent.ts +115 -0
- package/src/metrics/index.ts +9 -0
- package/src/metrics/turns.test.ts +165 -0
- package/src/metrics/turns.ts +201 -0
- package/src/store/sqlite/raw-transcript.ts +9 -3
- package/src/store/sqlite.dbmirror.test.ts +10 -0
- package/src/store/turns/connection.ts +137 -0
- package/src/store/turns/contract-compliance.test.ts +562 -0
- package/src/store/turns/index.ts +65 -0
- package/src/store/turns/memory-store.test.ts +15 -0
- package/src/store/turns/memory-store.ts +469 -0
- package/src/store/turns/migrations.test.ts +161 -0
- package/src/store/turns/migrations.ts +203 -0
- package/src/store/turns/schema.ts +158 -0
- package/src/store/turns/sqlite-store.test.ts +161 -0
- package/src/store/turns/sqlite-store.ts +656 -0
- package/src/store/turns/types.ts +181 -0
- package/src/topics/cluster.test.ts +176 -0
- package/src/topics/cluster.ts +191 -0
- package/src/topics/index.ts +18 -0
- package/src/topics/kselection.ts +182 -0
- package/src/topics/labels.ts +67 -0
- package/src/topics/store.test.ts +132 -0
- package/src/topics/store.ts +208 -0
- package/src/topics/types.ts +64 -0
- package/src/wiki.test.ts +217 -0
- package/src/wiki.ts +198 -0
- package/dist/extensions/openclaw-mega-compact.js +0 -291
- package/dist/src/minilm.js +0 -92
- package/dist/src/wordpiece.js +0 -129
- package/extensions/dashboard-client/dist/assets/index-D_WtU2TV.js.map +0 -1
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* routes-topics.ts — S51 auto-categorizing wiki topics route handler.
|
|
3
|
+
*
|
|
4
|
+
* GET /api/topics — Returns topic list with memory counts + TF-IDF labels.
|
|
5
|
+
* Read-only (PREVENT-PI-001/002); parameterized queries (PREVENT-002);
|
|
6
|
+
* pure local node:sqlite (PREVENT-PI-004).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
10
|
+
import type { RouteContext } from "./routes-core.js";
|
|
11
|
+
import { openTurnStore } from "../../src/store/turns/connection.js";
|
|
12
|
+
import { createTopicStore } from "../../src/topics/store.js";
|
|
13
|
+
import type { TopicsResponse, TopicRow } from "./api-contracts/game-types.js";
|
|
14
|
+
import type { TopicMemoriesResponse } from "./api-contracts/turns.js";
|
|
15
|
+
|
|
16
|
+
export function handleTopics(
|
|
17
|
+
req: IncomingMessage,
|
|
18
|
+
res: ServerResponse,
|
|
19
|
+
ctx: RouteContext,
|
|
20
|
+
): boolean {
|
|
21
|
+
const url = req.url ?? "";
|
|
22
|
+
// ── GET /api/topics/:topicId/memories — wiki topic drill-down (S52) ──
|
|
23
|
+
const drillMatch =
|
|
24
|
+
req.method === "GET"
|
|
25
|
+
? url.match(/^\/api\/topics\/([^/?]+)\/memories$/)
|
|
26
|
+
: null;
|
|
27
|
+
if (drillMatch) {
|
|
28
|
+
try {
|
|
29
|
+
const topicId = decodeURIComponent(drillMatch[1]);
|
|
30
|
+
const store = createTopicStore(ctx.stateDir);
|
|
31
|
+
const topics = store.getTopics();
|
|
32
|
+
const topic = topics.find((t) => t.id === topicId);
|
|
33
|
+
const assignments = store.getMemoriesForTopic(topicId, 200);
|
|
34
|
+
const body: TopicMemoriesResponse = {
|
|
35
|
+
topicId,
|
|
36
|
+
label: topic?.label ?? topicId,
|
|
37
|
+
assignments: assignments.map((a) => ({
|
|
38
|
+
memoryId: a.memoryId,
|
|
39
|
+
confidence: a.confidence ?? null,
|
|
40
|
+
assignedAt: a.assignedAt ?? null,
|
|
41
|
+
})),
|
|
42
|
+
};
|
|
43
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
44
|
+
res.end(JSON.stringify(body));
|
|
45
|
+
} catch (e) {
|
|
46
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
47
|
+
res.end(JSON.stringify({ error: String(e) }));
|
|
48
|
+
}
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (url !== "/api/topics") return false;
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const tdb = openTurnStore(ctx.stateDir);
|
|
56
|
+
const topics: TopicRow[] = (
|
|
57
|
+
tdb
|
|
58
|
+
.prepare(
|
|
59
|
+
`SELECT id, label, term_scores, memory_count, last_updated
|
|
60
|
+
FROM topics ORDER BY memory_count DESC, id ASC`,
|
|
61
|
+
)
|
|
62
|
+
.all() as Array<{
|
|
63
|
+
id: string;
|
|
64
|
+
label: string;
|
|
65
|
+
term_scores: string | null;
|
|
66
|
+
memory_count: number;
|
|
67
|
+
last_updated: number | null;
|
|
68
|
+
}>
|
|
69
|
+
).map((r) => ({
|
|
70
|
+
id: r.id,
|
|
71
|
+
label: r.label,
|
|
72
|
+
memoryCount: r.memory_count,
|
|
73
|
+
lastUpdated: r.last_updated ?? 0,
|
|
74
|
+
termScore: safeParse(r.term_scores),
|
|
75
|
+
}));
|
|
76
|
+
|
|
77
|
+
const totalAssigned = (
|
|
78
|
+
tdb.prepare("SELECT COUNT(*) AS c FROM memory_topics").get() as {
|
|
79
|
+
c: number;
|
|
80
|
+
}
|
|
81
|
+
).c;
|
|
82
|
+
|
|
83
|
+
const lastRebuildAt = (
|
|
84
|
+
tdb
|
|
85
|
+
.prepare("SELECT MAX(cluster_model_built_at) AS m FROM topics")
|
|
86
|
+
.get() as { m: number | null }
|
|
87
|
+
).m;
|
|
88
|
+
|
|
89
|
+
const body: TopicsResponse = {
|
|
90
|
+
updatedAt: new Date().toISOString(),
|
|
91
|
+
totalTopics: topics.length,
|
|
92
|
+
totalAssigned,
|
|
93
|
+
lastRebuildAt,
|
|
94
|
+
topics,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
98
|
+
res.end(JSON.stringify(body));
|
|
99
|
+
} catch (e) {
|
|
100
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
101
|
+
res.end(JSON.stringify({ error: String(e) }));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function safeParse(
|
|
108
|
+
json: string | null,
|
|
109
|
+
): Array<{ term: string; score: number }> {
|
|
110
|
+
if (!json) return [];
|
|
111
|
+
try {
|
|
112
|
+
const v = JSON.parse(json) as unknown;
|
|
113
|
+
return Array.isArray(v)
|
|
114
|
+
? (v as Array<{ term: string; score: number }>)
|
|
115
|
+
: [];
|
|
116
|
+
} catch {
|
|
117
|
+
return [];
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* routes-turns.ts — S52 turn-by-turn memory tracking + recall + rewind routes.
|
|
3
|
+
*
|
|
4
|
+
* GET /api/turns — conversation list (turns tab payload)
|
|
5
|
+
* GET /api/turns/conversation/:convId — per-turn detail + recall provenance
|
|
6
|
+
* GET /api/turns/intents — pending rewind intents (S52A)
|
|
7
|
+
* POST /api/turns/intent — post a rewind intent
|
|
8
|
+
* POST /api/fork — fork a conversation at a turn
|
|
9
|
+
* POST /api/turns/prune — admin prune (capability-gated)
|
|
10
|
+
* POST /api/turns/vacuum — admin vacuum
|
|
11
|
+
*
|
|
12
|
+
* Capability-gated per the S49 contract: display via `asReader()`, prune/vacuum
|
|
13
|
+
* via `asAdmin()`, fork via `asWriter()`. Read-mostly. The store never calls
|
|
14
|
+
* back into the host (ledger protocol). Loopback-only (PREVENT-PI-004).
|
|
15
|
+
* Parameterized (PREVENT-002).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
19
|
+
import type { RouteContext } from "./routes-core.js";
|
|
20
|
+
import { createTurnStore } from "../../src/store/turns/index.js";
|
|
21
|
+
import { openTurnStore } from "../../src/store/turns/connection.js";
|
|
22
|
+
import { openIntentQueue } from "../../src/intent.js";
|
|
23
|
+
import { forkFromConversation } from "../../src/fork.js";
|
|
24
|
+
import type {
|
|
25
|
+
TurnsResponse,
|
|
26
|
+
ConversationTurnsResponse,
|
|
27
|
+
ConversationSummary,
|
|
28
|
+
TurnRow,
|
|
29
|
+
RecallHit,
|
|
30
|
+
RewindIntentsResponse,
|
|
31
|
+
ForkResponse,
|
|
32
|
+
PostIntentRequest,
|
|
33
|
+
PruneTurnsResponse,
|
|
34
|
+
} from "./api-contracts/turns.js";
|
|
35
|
+
|
|
36
|
+
function sendJson(res: ServerResponse, status: number, body: unknown): void {
|
|
37
|
+
res.writeHead(status, { "Content-Type": "application/json" }); // guardrails-allow PREVENT-PI-004: loopback dashboard response (local)
|
|
38
|
+
res.end(JSON.stringify(body));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Read a capped JSON body; returns { ok, value } or { error }. */
|
|
42
|
+
function readJsonBody(
|
|
43
|
+
req: IncomingMessage,
|
|
44
|
+
cb: (
|
|
45
|
+
result:
|
|
46
|
+
| { ok: true; value: Record<string, unknown> }
|
|
47
|
+
| { ok: false; error: string },
|
|
48
|
+
) => void,
|
|
49
|
+
): void {
|
|
50
|
+
let body = "";
|
|
51
|
+
let tooBig = false;
|
|
52
|
+
req.on("data", (chunk: Buffer) => {
|
|
53
|
+
if (body.length > 65536) {
|
|
54
|
+
tooBig = true;
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
body += chunk.toString();
|
|
58
|
+
});
|
|
59
|
+
req.on("end", () => {
|
|
60
|
+
if (tooBig) return cb({ ok: false, error: "body_too_large" });
|
|
61
|
+
try {
|
|
62
|
+
const v = body ? JSON.parse(body) : {};
|
|
63
|
+
if (typeof v !== "object" || v === null || Array.isArray(v)) {
|
|
64
|
+
return cb({ ok: false, error: "invalid_object" });
|
|
65
|
+
}
|
|
66
|
+
cb({ ok: true, value: v as Record<string, unknown> });
|
|
67
|
+
} catch {
|
|
68
|
+
cb({ ok: false, error: "invalid_json" });
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function handleTurns(
|
|
74
|
+
req: IncomingMessage,
|
|
75
|
+
res: ServerResponse,
|
|
76
|
+
ctx: RouteContext,
|
|
77
|
+
): boolean {
|
|
78
|
+
const url = req.url ?? "";
|
|
79
|
+
if (!url.startsWith("/api/turns") && url !== "/api/fork") return false;
|
|
80
|
+
|
|
81
|
+
// ── GET /api/turns — conversation list ─────────────────────────────
|
|
82
|
+
if (req.method === "GET" && url === "/api/turns") {
|
|
83
|
+
try {
|
|
84
|
+
const store = createTurnStore({ stateDir: ctx.stateDir });
|
|
85
|
+
const reader = store.asReader();
|
|
86
|
+
// Enumerate conversations via session_conversations + a distinct scan.
|
|
87
|
+
const tdb = openTurnStore(ctx.stateDir);
|
|
88
|
+
const convRows = tdb
|
|
89
|
+
.prepare(
|
|
90
|
+
`SELECT DISTINCT conversation_id FROM turns ORDER BY conversation_id ASC`,
|
|
91
|
+
)
|
|
92
|
+
.all() as Array<{ conversation_id: string }>;
|
|
93
|
+
const conversations: ConversationSummary[] = convRows.map((r) => {
|
|
94
|
+
const stats = reader.conversationStats(r.conversation_id);
|
|
95
|
+
const epochCount = (
|
|
96
|
+
tdb
|
|
97
|
+
.prepare(
|
|
98
|
+
"SELECT COUNT(DISTINCT epoch_id) AS c FROM turns WHERE conversation_id = ? AND epoch_id IS NOT NULL",
|
|
99
|
+
)
|
|
100
|
+
.get(r.conversation_id) as { c: number }
|
|
101
|
+
).c;
|
|
102
|
+
const totalRecall = (
|
|
103
|
+
tdb
|
|
104
|
+
.prepare(
|
|
105
|
+
`SELECT COUNT(*) AS c FROM turn_recall tr JOIN turns t ON tr.turn_id = t.id
|
|
106
|
+
WHERE t.conversation_id = ?`,
|
|
107
|
+
)
|
|
108
|
+
.get(r.conversation_id) as { c: number }
|
|
109
|
+
).c;
|
|
110
|
+
return {
|
|
111
|
+
conversationId: r.conversation_id,
|
|
112
|
+
turnCount: stats.turnCount,
|
|
113
|
+
firstTurnAt: stats.firstTurnAt,
|
|
114
|
+
lastTurnAt: stats.lastTurnAt,
|
|
115
|
+
avgCtxPercent: stats.avgCtxPercent,
|
|
116
|
+
epochCount,
|
|
117
|
+
totalRecall,
|
|
118
|
+
};
|
|
119
|
+
});
|
|
120
|
+
// Active conversation = the one with the most recent turn across all.
|
|
121
|
+
let activeConversationId: string | null = null;
|
|
122
|
+
let lastSeen = 0;
|
|
123
|
+
for (const c of conversations) {
|
|
124
|
+
if (c.lastTurnAt > lastSeen) {
|
|
125
|
+
lastSeen = c.lastTurnAt;
|
|
126
|
+
activeConversationId = c.conversationId;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
sendJson(res, 200, {
|
|
130
|
+
conversations,
|
|
131
|
+
activeConversationId,
|
|
132
|
+
} satisfies TurnsResponse);
|
|
133
|
+
return true;
|
|
134
|
+
} catch (e) {
|
|
135
|
+
sendJson(res, 500, { error: String(e) });
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ── GET /api/turns/conversation/:convId — per-turn detail + recall ─
|
|
141
|
+
const convMatch =
|
|
142
|
+
req.method === "GET"
|
|
143
|
+
? url.match(/^\/api\/turns\/conversation\/([^/?]+)$/)
|
|
144
|
+
: null;
|
|
145
|
+
if (convMatch) {
|
|
146
|
+
try {
|
|
147
|
+
const convId = decodeURIComponent(convMatch[1]);
|
|
148
|
+
const store = createTurnStore({ stateDir: ctx.stateDir });
|
|
149
|
+
const reader = store.asReader();
|
|
150
|
+
const entries = reader.query({ conversationId: convId, limit: 10000 });
|
|
151
|
+
const turns: TurnRow[] = entries.map((e) => ({
|
|
152
|
+
turnIndex: e.turnIndex,
|
|
153
|
+
conversationId: e.conversationId,
|
|
154
|
+
sessionId: e.sessionId,
|
|
155
|
+
role: e.role,
|
|
156
|
+
endedAt: e.endedAt,
|
|
157
|
+
ctxTokens: e.ctxTokens ?? null,
|
|
158
|
+
ctxPercent: e.ctxPercent ?? null,
|
|
159
|
+
pressureBand: e.pressureBand ?? null,
|
|
160
|
+
model: e.model ?? null,
|
|
161
|
+
epochId: e.epochId ?? null,
|
|
162
|
+
recall: reader.listRecallByIndex(convId, e.turnIndex).map(
|
|
163
|
+
(r): RecallHit => ({
|
|
164
|
+
checkpointId: r.checkpointId,
|
|
165
|
+
score: r.score,
|
|
166
|
+
source: r.source,
|
|
167
|
+
raptorLevel: r.raptorLevel ?? null,
|
|
168
|
+
}),
|
|
169
|
+
),
|
|
170
|
+
}));
|
|
171
|
+
sendJson(res, 200, {
|
|
172
|
+
conversationId: convId,
|
|
173
|
+
turns,
|
|
174
|
+
} satisfies ConversationTurnsResponse);
|
|
175
|
+
return true;
|
|
176
|
+
} catch (e) {
|
|
177
|
+
sendJson(res, 500, { error: String(e) });
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ── GET /api/turns/intents — pending rewind intents ───────────────
|
|
183
|
+
if (req.method === "GET" && url === "/api/turns/intents") {
|
|
184
|
+
try {
|
|
185
|
+
const tdb = openTurnStore(ctx.stateDir);
|
|
186
|
+
const q = openIntentQueue(tdb);
|
|
187
|
+
const intents = q.pendingIntents().map((i) => ({
|
|
188
|
+
id: i.id,
|
|
189
|
+
conversationId: i.conversationId,
|
|
190
|
+
targetTurnIndex: i.targetTurnIndex,
|
|
191
|
+
createdAt: i.createdAt,
|
|
192
|
+
status: i.status,
|
|
193
|
+
}));
|
|
194
|
+
sendJson(res, 200, { intents } satisfies RewindIntentsResponse);
|
|
195
|
+
return true;
|
|
196
|
+
} catch (e) {
|
|
197
|
+
sendJson(res, 500, { error: String(e) });
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ── POST /api/turns/intent — post a rewind intent ─────────────────
|
|
203
|
+
if (req.method === "POST" && url === "/api/turns/intent") {
|
|
204
|
+
readJsonBody(req, (result) => {
|
|
205
|
+
if (!result.ok) return sendJson(res, 400, { error: result.error });
|
|
206
|
+
const v = result.value;
|
|
207
|
+
const conversationId =
|
|
208
|
+
typeof v.conversationId === "string" ? v.conversationId : "";
|
|
209
|
+
const targetTurnIndex =
|
|
210
|
+
typeof v.targetTurnIndex === "number" ? v.targetTurnIndex : -1;
|
|
211
|
+
if (!conversationId || targetTurnIndex < 0) {
|
|
212
|
+
return sendJson(res, 400, {
|
|
213
|
+
error: "missing_conversationId_or_targetTurnIndex",
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
try {
|
|
217
|
+
const tdb = openTurnStore(ctx.stateDir);
|
|
218
|
+
const q = openIntentQueue(tdb);
|
|
219
|
+
const intent = q.postIntent({
|
|
220
|
+
conversationId,
|
|
221
|
+
targetTurnIndex,
|
|
222
|
+
} satisfies PostIntentRequest);
|
|
223
|
+
sendJson(res, 201, intent);
|
|
224
|
+
} catch (e) {
|
|
225
|
+
sendJson(res, 500, { error: String(e) });
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ── POST /api/fork — fork a conversation at a turn ─────────────────
|
|
232
|
+
if (req.method === "POST" && url === "/api/fork") {
|
|
233
|
+
readJsonBody(req, (result) => {
|
|
234
|
+
if (!result.ok) return sendJson(res, 400, { error: result.error });
|
|
235
|
+
const v = result.value;
|
|
236
|
+
const conversationId =
|
|
237
|
+
typeof v.conversationId === "string" ? v.conversationId : "";
|
|
238
|
+
const turnIndex = typeof v.turnIndex === "number" ? v.turnIndex : -1;
|
|
239
|
+
if (!conversationId || turnIndex < 0) {
|
|
240
|
+
return sendJson(res, 400, {
|
|
241
|
+
error: "missing_conversationId_or_turnIndex",
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
try {
|
|
245
|
+
const store = createTurnStore({ stateDir: ctx.stateDir });
|
|
246
|
+
const out = forkFromConversation(store, conversationId, turnIndex);
|
|
247
|
+
sendJson(res, 201, {
|
|
248
|
+
childConversationId: out.childConversationId,
|
|
249
|
+
recalledCount: out.recalled.length,
|
|
250
|
+
checkpointIds: out.checkpointIds,
|
|
251
|
+
} satisfies ForkResponse);
|
|
252
|
+
} catch (e) {
|
|
253
|
+
sendJson(res, 400, { error: String(e) });
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ── POST /api/turns/prune — admin prune ───────────────────────────
|
|
260
|
+
if (req.method === "POST" && url === "/api/turns/prune") {
|
|
261
|
+
readJsonBody(req, (result) => {
|
|
262
|
+
if (!result.ok) return sendJson(res, 400, { error: result.error });
|
|
263
|
+
const v = result.value;
|
|
264
|
+
const maxTurnAgeMs =
|
|
265
|
+
typeof v.maxTurnAgeMs === "number" ? v.maxTurnAgeMs : 0;
|
|
266
|
+
const keepMinPerConversation =
|
|
267
|
+
typeof v.keepMinPerConversation === "number"
|
|
268
|
+
? v.keepMinPerConversation
|
|
269
|
+
: 50;
|
|
270
|
+
if (maxTurnAgeMs <= 0) {
|
|
271
|
+
return sendJson(res, 400, { error: "invalid_maxTurnAgeMs" });
|
|
272
|
+
}
|
|
273
|
+
try {
|
|
274
|
+
const store = createTurnStore({ stateDir: ctx.stateDir });
|
|
275
|
+
const report = store.asAdmin().prune({
|
|
276
|
+
maxTurnAgeMs,
|
|
277
|
+
keepMinPerConversation,
|
|
278
|
+
vacuumAfterPrune: true,
|
|
279
|
+
});
|
|
280
|
+
sendJson(res, 200, {
|
|
281
|
+
turnsRemoved: report.turnsRemoved,
|
|
282
|
+
recallRemoved: report.recallRemoved,
|
|
283
|
+
branchesPreserved: report.branchesPreserved,
|
|
284
|
+
freedBytes: report.freedBytes,
|
|
285
|
+
} satisfies PruneTurnsResponse);
|
|
286
|
+
} catch (e) {
|
|
287
|
+
sendJson(res, 500, { error: String(e) });
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
return true;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// ── POST /api/turns/vacuum — admin vacuum ────────────────────────
|
|
294
|
+
if (req.method === "POST" && url === "/api/turns/vacuum") {
|
|
295
|
+
try {
|
|
296
|
+
createTurnStore({ stateDir: ctx.stateDir }).asAdmin().vacuum();
|
|
297
|
+
sendJson(res, 200, { ok: true });
|
|
298
|
+
} catch (e) {
|
|
299
|
+
sendJson(res, 500, { error: String(e) });
|
|
300
|
+
}
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
@@ -9,5 +9,12 @@ export { buildRouteContext } from "./routes-core.js";
|
|
|
9
9
|
export type { RouteContext } from "./routes-core.js";
|
|
10
10
|
|
|
11
11
|
export { handleIndex, handleRepoIndex, handleStatic } from "./routes-repo.js";
|
|
12
|
-
export {
|
|
12
|
+
export {
|
|
13
|
+
handleGameState,
|
|
14
|
+
handleGameScores,
|
|
15
|
+
handlePerf,
|
|
16
|
+
handleAchievements,
|
|
17
|
+
} from "./routes-game.js";
|
|
13
18
|
export { handleEvents, handleSessions } from "./routes-sessions.js";
|
|
19
|
+
export { handleTopics } from "./routes-topics.js";
|
|
20
|
+
export { handleTurns } from "./routes-turns.js";
|
|
@@ -34,6 +34,8 @@ import {
|
|
|
34
34
|
handlePerf,
|
|
35
35
|
handleAchievements,
|
|
36
36
|
handleSessions,
|
|
37
|
+
handleTopics,
|
|
38
|
+
handleTurns,
|
|
37
39
|
handleStatic,
|
|
38
40
|
} from "./routes.js";
|
|
39
41
|
|
|
@@ -201,7 +203,7 @@ export async function launchDashboardServer(
|
|
|
201
203
|
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
202
204
|
res.setHeader("Vary", "Origin");
|
|
203
205
|
}
|
|
204
|
-
res.setHeader("Access-Control-Allow-Methods", "GET, PUT, OPTIONS");
|
|
206
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, OPTIONS");
|
|
205
207
|
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
206
208
|
|
|
207
209
|
if (req.method === "OPTIONS") {
|
|
@@ -219,6 +221,8 @@ export async function launchDashboardServer(
|
|
|
219
221
|
if (handlePerf(req, res, ctx)) return;
|
|
220
222
|
if (handleAchievements(req, res, ctx)) return;
|
|
221
223
|
if (handleSessions(req, res, ctx)) return;
|
|
224
|
+
if (handleTopics(req, res, ctx)) return;
|
|
225
|
+
if (handleTurns(req, res, ctx)) return;
|
|
222
226
|
handleStatic(req, res, ctx);
|
|
223
227
|
});
|
|
224
228
|
|
|
@@ -320,4 +324,4 @@ if (process.argv[1] && process.argv[1].includes("dashboard-server")) {
|
|
|
320
324
|
console.error("[mega-compact] dashboard server failed:", err);
|
|
321
325
|
process.exit(1);
|
|
322
326
|
});
|
|
323
|
-
}
|
|
327
|
+
}
|
|
@@ -37,65 +37,81 @@ import { registerDashboardCommands } from "./mega-dashboard-cmds.js";
|
|
|
37
37
|
import { registerConflictCommands } from "./mega-conflict-cmds.js";
|
|
38
38
|
import { registerDbCommands } from "./mega-db-cmds.js";
|
|
39
39
|
import { registerGameCommands } from "./mega-game-cmds.js";
|
|
40
|
+
import { registerMetricsCommands } from "./mega-metrics-cmds.js";
|
|
41
|
+
import { registerTopicsCommands } from "./mega-topics-cmds.js";
|
|
40
42
|
|
|
41
43
|
export default function (pi: ExtensionAPI) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
44
|
+
const config = loadConfig();
|
|
45
|
+
// S38.9: preflight env validation — check for obviously invalid values at startup.
|
|
46
|
+
// Non-fatal: log warnings and fall back to defaults.
|
|
47
|
+
if (config.autoRetryTransientMax < 0) {
|
|
48
|
+
console.warn(
|
|
49
|
+
"[mega-compact] MEGACOMPACT_AUTO_RETRY_TRANSIENT_MAX must be >= 0; using default 5",
|
|
50
|
+
);
|
|
51
|
+
config.autoRetryTransientMax = 5;
|
|
52
|
+
}
|
|
53
|
+
if (config.autoRetryPermanentMax < 0) {
|
|
54
|
+
console.warn(
|
|
55
|
+
"[mega-compact] MEGACOMPACT_AUTO_RETRY_PERMANENT_MAX must be >= 0; using default 1",
|
|
56
|
+
);
|
|
57
|
+
config.autoRetryPermanentMax = 1;
|
|
58
|
+
}
|
|
59
|
+
if (config.maxConsecutiveErrors < 1) {
|
|
60
|
+
console.warn(
|
|
61
|
+
"[mega-compact] MEGACOMPACT_MAX_CONSECUTIVE_ERRORS must be >= 1; using default 10",
|
|
62
|
+
);
|
|
63
|
+
config.maxConsecutiveErrors = 10;
|
|
64
|
+
}
|
|
65
|
+
// R2: session-global cap validation — 0 disables (valid), negative is invalid.
|
|
66
|
+
if (config.errorRetrySessionMax < 0) {
|
|
67
|
+
console.warn(
|
|
68
|
+
"[mega-compact] MEGACOMPACT_ERROR_RETRY_SESSION_MAX must be >= 0; using default 3",
|
|
69
|
+
);
|
|
70
|
+
config.errorRetrySessionMax = 3;
|
|
71
|
+
}
|
|
72
|
+
// R1: backoff base must be >= 0 (0 means no gating, useful for tests).
|
|
73
|
+
if (config.errorRetryBackoffMs < 0) {
|
|
74
|
+
console.warn(
|
|
75
|
+
"[mega-compact] MEGACOMPACT_ERROR_RETRY_BACKOFF_MS must be >= 0; using default 5000",
|
|
76
|
+
);
|
|
77
|
+
config.errorRetryBackoffMs = 5000;
|
|
78
|
+
}
|
|
79
|
+
// R3: repeat threshold must be >= 1.
|
|
80
|
+
if (config.poisonedContextRepeatThreshold < 1) {
|
|
81
|
+
console.warn(
|
|
82
|
+
"[mega-compact] MEGACOMPACT_POISONED_REPEAT_THRESHOLD must be >= 1; using default 3",
|
|
83
|
+
);
|
|
84
|
+
config.poisonedContextRepeatThreshold = 3;
|
|
85
|
+
}
|
|
86
|
+
const runtime = new MegaRuntime(config);
|
|
87
|
+
registerEventHandlers(pi, runtime, config);
|
|
88
|
+
registerCommands(pi, runtime, config);
|
|
89
|
+
registerDashboardCommands(pi, runtime);
|
|
90
|
+
registerConflictCommands(pi, runtime);
|
|
91
|
+
registerDbCommands(pi, runtime);
|
|
92
|
+
registerMetricsCommands(pi, runtime, config);
|
|
93
|
+
registerGameCommands(pi, runtime);
|
|
94
|
+
registerTopicsCommands(pi, runtime, config);
|
|
95
|
+
// v0.8.5 (audit P3): release the fs.watch game-state watcher handle on
|
|
96
|
+
// session teardown so it doesn't linger across reloads. pi exposes no
|
|
97
|
+
// extension-unload event (the factory return value is ignored and there is no
|
|
98
|
+
// "shutdown" event on the ExtensionAPI), so dispose() is wired to the
|
|
99
|
+
// session_shutdown lifecycle event — the closest valid teardown signal.
|
|
100
|
+
// dispose() is idempotent, and the next snapshot() re-opens the watcher
|
|
101
|
+
// lazily via bindRepo() → ensureGameStateWatcher(), so there is no permanent
|
|
102
|
+
// leak and no per-session fd accumulation.
|
|
103
|
+
// The PGlite indexes (vectorIndex / memoryIndex) are lazily opened module
|
|
104
|
+
// singletons. closeVectorIndex()/closeMemoryIndex() existed but had no
|
|
105
|
+
// non-test callers, so a session left both open: PGlite is WASM Postgres and
|
|
106
|
+
// its handles keep node's event loop alive, so `pi -p` produced its answer
|
|
107
|
+
// and then hung until killed rather than exiting. dispose() only released the
|
|
108
|
+
// fs.watch handle and the perf interval, neither of which was the culprit
|
|
109
|
+
// (the interval is unref'd).
|
|
110
|
+
//
|
|
111
|
+
// Both closes are idempotent and safe when the index was never opened, and
|
|
112
|
+
// the next initVectorIndex()/initMemoryIndex() re-opens lazily.
|
|
113
|
+
pi.on("session_shutdown", async () => {
|
|
114
|
+
runtime.dispose();
|
|
115
|
+
await Promise.all([closeVectorIndex(), closeMemoryIndex()]);
|
|
116
|
+
});
|
|
101
117
|
}
|