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.
Files changed (143) hide show
  1. package/README.md +3 -2
  2. package/dist/extensions/dashboard-server/api-contracts/endpoints.js +95 -47
  3. package/dist/extensions/dashboard-server/api-contracts/index.js +1 -1
  4. package/dist/extensions/dashboard-server/api-contracts/turns.js +10 -0
  5. package/dist/extensions/dashboard-server/api-contracts.test.js +3 -2
  6. package/dist/extensions/dashboard-server/routes-topics.js +87 -0
  7. package/dist/extensions/dashboard-server/routes-turns.js +264 -0
  8. package/dist/extensions/dashboard-server/routes.js +3 -1
  9. package/dist/extensions/dashboard-server/server.js +6 -2
  10. package/dist/extensions/mega-compact.js +10 -6
  11. package/dist/extensions/mega-config.js +4 -0
  12. package/dist/extensions/mega-events/agent-handlers.js +62 -49
  13. package/dist/extensions/mega-events/context-handler.js +48 -2
  14. package/dist/extensions/mega-events/session-handlers.js +25 -1
  15. package/dist/extensions/mega-metrics-cmds.js +90 -0
  16. package/dist/extensions/mega-pipeline/recall.js +47 -17
  17. package/dist/extensions/mega-topics-cmds.js +111 -0
  18. package/dist/extensions/mega-turn-store.js +110 -0
  19. package/dist/extensions/mega-turn-store.test.js +68 -0
  20. package/dist/src/config/turns.js +48 -0
  21. package/dist/src/fork.js +33 -0
  22. package/dist/src/fork.test.js +81 -0
  23. package/dist/src/intent.js +51 -0
  24. package/dist/src/intent.test.js +78 -0
  25. package/dist/src/metrics/index.js +4 -0
  26. package/dist/src/metrics/turns.js +118 -0
  27. package/dist/src/metrics/turns.test.js +152 -0
  28. package/dist/src/store/sqlite/raw-transcript.js +5 -3
  29. package/dist/src/store/sqlite.dbmirror.test.js +9 -0
  30. package/dist/src/store/turns/connection.js +123 -0
  31. package/dist/src/store/turns/contract-compliance.test.js +428 -0
  32. package/dist/src/store/turns/index.js +31 -0
  33. package/dist/src/store/turns/memory-store.js +357 -0
  34. package/dist/src/store/turns/memory-store.test.js +9 -0
  35. package/dist/src/store/turns/migrations.js +178 -0
  36. package/dist/src/store/turns/migrations.test.js +138 -0
  37. package/dist/src/store/turns/schema.js +115 -0
  38. package/dist/src/store/turns/sqlite-store.js +455 -0
  39. package/dist/src/store/turns/sqlite-store.test.js +144 -0
  40. package/dist/src/store/turns/types.js +16 -0
  41. package/dist/src/topics/cluster.js +141 -0
  42. package/dist/src/topics/cluster.test.js +150 -0
  43. package/dist/src/topics/index.js +6 -0
  44. package/dist/src/topics/kselection.js +142 -0
  45. package/dist/src/topics/labels.js +56 -0
  46. package/dist/src/topics/store.js +119 -0
  47. package/dist/src/topics/store.test.js +117 -0
  48. package/dist/src/topics/types.js +8 -0
  49. package/dist/src/wiki.js +133 -0
  50. package/dist/src/wiki.test.js +171 -0
  51. package/extensions/dashboard-client/dist/assets/{AchievementTiles-QPo46Pwg.js → AchievementTiles-DZp_xQOW.js} +2 -2
  52. package/extensions/dashboard-client/dist/assets/{AchievementTiles-QPo46Pwg.js.map → AchievementTiles-DZp_xQOW.js.map} +1 -1
  53. package/extensions/dashboard-client/dist/assets/{AchievementsTab-BDJxRXRY.js → AchievementsTab-BzFHpAHM.js} +2 -2
  54. package/extensions/dashboard-client/dist/assets/{AchievementsTab-BDJxRXRY.js.map → AchievementsTab-BzFHpAHM.js.map} +1 -1
  55. package/extensions/dashboard-client/dist/assets/{CacheTab-CUMqXQjd.js → CacheTab-DEQbTq2T.js} +2 -2
  56. package/extensions/dashboard-client/dist/assets/{CacheTab-CUMqXQjd.js.map → CacheTab-DEQbTq2T.js.map} +1 -1
  57. package/extensions/dashboard-client/dist/assets/{ConfigTab-j1dRzurG.js → ConfigTab-DQ4FG33J.js} +2 -2
  58. package/extensions/dashboard-client/dist/assets/{ConfigTab-j1dRzurG.js.map → ConfigTab-DQ4FG33J.js.map} +1 -1
  59. package/extensions/dashboard-client/dist/assets/{EventsTab-CMglXK4p.js → EventsTab-D5HoEVYo.js} +2 -2
  60. package/extensions/dashboard-client/dist/assets/{EventsTab-CMglXK4p.js.map → EventsTab-D5HoEVYo.js.map} +1 -1
  61. package/extensions/dashboard-client/dist/assets/{GameTab-fpaDoyxi.js → GameTab-Jzsc4yeY.js} +2 -2
  62. package/extensions/dashboard-client/dist/assets/{GameTab-fpaDoyxi.js.map → GameTab-Jzsc4yeY.js.map} +1 -1
  63. package/extensions/dashboard-client/dist/assets/{MetricsTab-CWG2crG_.js → MetricsTab-D_XnsEYX.js} +2 -2
  64. package/extensions/dashboard-client/dist/assets/{MetricsTab-CWG2crG_.js.map → MetricsTab-D_XnsEYX.js.map} +1 -1
  65. package/extensions/dashboard-client/dist/assets/{OverviewTab-BaV2D2nf.js → OverviewTab-BMREvAth.js} +2 -2
  66. package/extensions/dashboard-client/dist/assets/{OverviewTab-BaV2D2nf.js.map → OverviewTab-BMREvAth.js.map} +1 -1
  67. package/extensions/dashboard-client/dist/assets/{ReposTab-C-0kuZ_0.js → ReposTab-DuyhOktG.js} +2 -2
  68. package/extensions/dashboard-client/dist/assets/{ReposTab-C-0kuZ_0.js.map → ReposTab-DuyhOktG.js.map} +1 -1
  69. package/extensions/dashboard-client/dist/assets/{SessionsTab-CSYhddCA.js → SessionsTab-mz_vz8i2.js} +2 -2
  70. package/extensions/dashboard-client/dist/assets/{SessionsTab-CSYhddCA.js.map → SessionsTab-mz_vz8i2.js.map} +1 -1
  71. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-Dy4KRjSM.js → TimeSavedCard-B8AIjNTY.js} +2 -2
  72. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-Dy4KRjSM.js.map → TimeSavedCard-B8AIjNTY.js.map} +1 -1
  73. package/extensions/dashboard-client/dist/assets/TopicsTab-DKuct4oD.js +2 -0
  74. package/extensions/dashboard-client/dist/assets/TopicsTab-DKuct4oD.js.map +1 -0
  75. package/extensions/dashboard-client/dist/assets/TurnsTab-11MGLxY8.js +2 -0
  76. package/extensions/dashboard-client/dist/assets/TurnsTab-11MGLxY8.js.map +1 -0
  77. package/extensions/dashboard-client/dist/assets/{index-D_WtU2TV.js → index-CqLN7STl.js} +10 -10
  78. package/extensions/dashboard-client/dist/assets/index-CqLN7STl.js.map +1 -0
  79. package/extensions/dashboard-client/dist/assets/{index-og3ghxyY.css → index-gn66lYD2.css} +1 -1
  80. package/extensions/dashboard-client/dist/assets/{useSSE-C9yVJlGP.js → useSSE-CXrQSbVY.js} +2 -2
  81. package/extensions/dashboard-client/dist/assets/{useSSE-C9yVJlGP.js.map → useSSE-CXrQSbVY.js.map} +1 -1
  82. package/extensions/dashboard-client/dist/index.html +2 -2
  83. package/extensions/dashboard-client/src/App.tsx +10 -2
  84. package/extensions/dashboard-client/src/api/client.ts +90 -1
  85. package/extensions/dashboard-client/src/main.tsx +1 -0
  86. package/extensions/dashboard-client/src/styles/turns.css +142 -0
  87. package/extensions/dashboard-client/src/tabs/TopicsTab.tsx +186 -0
  88. package/extensions/dashboard-client/src/tabs/TurnsTab.tsx +336 -0
  89. package/extensions/dashboard-server/api-contracts/endpoints.ts +394 -291
  90. package/extensions/dashboard-server/api-contracts/game-types.ts +141 -88
  91. package/extensions/dashboard-server/api-contracts/index.ts +153 -133
  92. package/extensions/dashboard-server/api-contracts/turns.ts +114 -0
  93. package/extensions/dashboard-server/api-contracts.test.ts +920 -919
  94. package/extensions/dashboard-server/routes-topics.ts +119 -0
  95. package/extensions/dashboard-server/routes-turns.ts +305 -0
  96. package/extensions/dashboard-server/routes.ts +8 -1
  97. package/extensions/dashboard-server/server.ts +6 -2
  98. package/extensions/mega-compact.ts +75 -59
  99. package/extensions/mega-config.ts +232 -220
  100. package/extensions/mega-events/agent-handlers.ts +194 -151
  101. package/extensions/mega-events/context-handler.ts +50 -1
  102. package/extensions/mega-events/session-handlers.ts +26 -5
  103. package/extensions/mega-metrics-cmds.ts +114 -0
  104. package/extensions/mega-pipeline/recall.ts +215 -152
  105. package/extensions/mega-topics-cmds.ts +151 -0
  106. package/extensions/mega-turn-store.test.ts +108 -0
  107. package/extensions/mega-turn-store.ts +169 -0
  108. package/package.json +3 -1
  109. package/src/config/turns.ts +69 -0
  110. package/src/fork.test.ts +94 -0
  111. package/src/fork.ts +78 -0
  112. package/src/intent.test.ts +87 -0
  113. package/src/intent.ts +115 -0
  114. package/src/metrics/index.ts +9 -0
  115. package/src/metrics/turns.test.ts +165 -0
  116. package/src/metrics/turns.ts +201 -0
  117. package/src/store/sqlite/raw-transcript.ts +9 -3
  118. package/src/store/sqlite.dbmirror.test.ts +10 -0
  119. package/src/store/turns/connection.ts +137 -0
  120. package/src/store/turns/contract-compliance.test.ts +562 -0
  121. package/src/store/turns/index.ts +65 -0
  122. package/src/store/turns/memory-store.test.ts +15 -0
  123. package/src/store/turns/memory-store.ts +469 -0
  124. package/src/store/turns/migrations.test.ts +161 -0
  125. package/src/store/turns/migrations.ts +203 -0
  126. package/src/store/turns/schema.ts +158 -0
  127. package/src/store/turns/sqlite-store.test.ts +161 -0
  128. package/src/store/turns/sqlite-store.ts +656 -0
  129. package/src/store/turns/types.ts +181 -0
  130. package/src/topics/cluster.test.ts +176 -0
  131. package/src/topics/cluster.ts +191 -0
  132. package/src/topics/index.ts +18 -0
  133. package/src/topics/kselection.ts +182 -0
  134. package/src/topics/labels.ts +67 -0
  135. package/src/topics/store.test.ts +132 -0
  136. package/src/topics/store.ts +208 -0
  137. package/src/topics/types.ts +64 -0
  138. package/src/wiki.test.ts +217 -0
  139. package/src/wiki.ts +198 -0
  140. package/dist/extensions/openclaw-mega-compact.js +0 -291
  141. package/dist/src/minilm.js +0 -92
  142. package/dist/src/wordpiece.js +0 -129
  143. 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 { handleGameState, handleGameScores, handlePerf, handleAchievements } from "./routes-game.js";
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
- const config = loadConfig();
43
- // S38.9: preflight env validation — check for obviously invalid values at startup.
44
- // Non-fatal: log warnings and fall back to defaults.
45
- if (config.autoRetryTransientMax < 0) {
46
- console.warn('[mega-compact] MEGACOMPACT_AUTO_RETRY_TRANSIENT_MAX must be >= 0; using default 5');
47
- config.autoRetryTransientMax = 5;
48
- }
49
- if (config.autoRetryPermanentMax < 0) {
50
- console.warn('[mega-compact] MEGACOMPACT_AUTO_RETRY_PERMANENT_MAX must be >= 0; using default 1');
51
- config.autoRetryPermanentMax = 1;
52
- }
53
- if (config.maxConsecutiveErrors < 1) {
54
- console.warn('[mega-compact] MEGACOMPACT_MAX_CONSECUTIVE_ERRORS must be >= 1; using default 10');
55
- config.maxConsecutiveErrors = 10;
56
- }
57
- // R2: session-global cap validation — 0 disables (valid), negative is invalid.
58
- if (config.errorRetrySessionMax < 0) {
59
- console.warn('[mega-compact] MEGACOMPACT_ERROR_RETRY_SESSION_MAX must be >= 0; using default 3');
60
- config.errorRetrySessionMax = 3;
61
- }
62
- // R1: backoff base must be >= 0 (0 means no gating, useful for tests).
63
- if (config.errorRetryBackoffMs < 0) {
64
- console.warn('[mega-compact] MEGACOMPACT_ERROR_RETRY_BACKOFF_MS must be >= 0; using default 5000');
65
- config.errorRetryBackoffMs = 5000;
66
- }
67
- // R3: repeat threshold must be >= 1.
68
- if (config.poisonedContextRepeatThreshold < 1) {
69
- console.warn('[mega-compact] MEGACOMPACT_POISONED_REPEAT_THRESHOLD must be >= 1; using default 3');
70
- config.poisonedContextRepeatThreshold = 3;
71
- }
72
- const runtime = new MegaRuntime(config);
73
- registerEventHandlers(pi, runtime, config);
74
- registerCommands(pi, runtime, config);
75
- registerDashboardCommands(pi, runtime);
76
- registerConflictCommands(pi, runtime);
77
- registerDbCommands(pi, runtime);
78
- registerGameCommands(pi, runtime);
79
- // v0.8.5 (audit P3): release the fs.watch game-state watcher handle on
80
- // session teardown so it doesn't linger across reloads. pi exposes no
81
- // extension-unload event (the factory return value is ignored and there is no
82
- // "shutdown" event on the ExtensionAPI), so dispose() is wired to the
83
- // session_shutdown lifecycle event — the closest valid teardown signal.
84
- // dispose() is idempotent, and the next snapshot() re-opens the watcher
85
- // lazily via bindRepo() ensureGameStateWatcher(), so there is no permanent
86
- // leak and no per-session fd accumulation.
87
- // The PGlite indexes (vectorIndex / memoryIndex) are lazily opened module
88
- // singletons. closeVectorIndex()/closeMemoryIndex() existed but had no
89
- // non-test callers, so a session left both open: PGlite is WASM Postgres and
90
- // its handles keep node's event loop alive, so `pi -p` produced its answer
91
- // and then hung until killed rather than exiting. dispose() only released the
92
- // fs.watch handle and the perf interval, neither of which was the culprit
93
- // (the interval is unref'd).
94
- //
95
- // Both closes are idempotent and safe when the index was never opened, and
96
- // the next initVectorIndex()/initMemoryIndex() re-opens lazily.
97
- pi.on("session_shutdown", async () => {
98
- runtime.dispose();
99
- await Promise.all([closeVectorIndex(), closeMemoryIndex()]);
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
  }