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
|
@@ -6,11 +6,11 @@
|
|
|
6
6
|
* the shared MegaRuntime (token accounting, ticker, dashboard events).
|
|
7
7
|
*/
|
|
8
8
|
import { sessionEntryToContextMessages } from "@earendil-works/pi-coding-agent";
|
|
9
|
-
import { recallAndInline, recallAndInlineAsync, formatRecallBlock } from "../../src/recall.js";
|
|
9
|
+
import { recallAndInline, recallAndInlineAsync, formatRecallBlock, } from "../../src/recall.js";
|
|
10
10
|
import { normalizeSessionId } from "../../src/store.js";
|
|
11
|
-
import { incRecallInjected, incCacheHitTokens, getIndexDir } from "../../src/store/sqlite.js";
|
|
12
|
-
import {
|
|
13
|
-
import { C
|
|
11
|
+
import { incRecallInjected, incCacheHitTokens, getIndexDir, } from "../../src/store/sqlite.js";
|
|
12
|
+
import { ensureConversationIdFor, recordTurnWrite, recordRecallWrite, } from "../mega-turn-store.js";
|
|
13
|
+
import { C } from "../mega-runtime.js";
|
|
14
14
|
/**
|
|
15
15
|
* Unified recall (Layer 5). The ONE path that injects. Returns the recall
|
|
16
16
|
* result; callers decide whether to stage it for before_agent_start (resume)
|
|
@@ -34,12 +34,22 @@ export function doRecall(runtime, config, ctx, query, source) {
|
|
|
34
34
|
liveWindow,
|
|
35
35
|
dedupSim: config.dedupSim,
|
|
36
36
|
}, runtime.store);
|
|
37
|
-
runtime.dashboard.event("recall", {
|
|
37
|
+
runtime.dashboard.event("recall", {
|
|
38
|
+
source,
|
|
39
|
+
query: query.slice(0, 120),
|
|
40
|
+
injected: result.toInject.length,
|
|
41
|
+
empty: result.empty,
|
|
42
|
+
});
|
|
38
43
|
if (!result.empty && result.toInject.length > 0) {
|
|
39
44
|
const top = result.toInject[0];
|
|
40
45
|
const scorePct = Math.round((top.score ?? 0) * 100);
|
|
41
46
|
const files = top.checkpoint.filesModified ?? [];
|
|
42
|
-
const label = files.length
|
|
47
|
+
const label = files.length
|
|
48
|
+
? files
|
|
49
|
+
.map((f) => f.split("/").pop() ?? f)
|
|
50
|
+
.slice(0, 2)
|
|
51
|
+
.join(", ")
|
|
52
|
+
: top.checkpoint.checkpointId;
|
|
43
53
|
runtime.pushTicker(`${C.amber}↩${C.reset} recalled ${top.checkpoint.checkpointId} · ${scorePct}% · ${label}`);
|
|
44
54
|
runtime.lastWhy = `why: recalled@${scorePct}% (${result.toInject.length} chkpt)`;
|
|
45
55
|
}
|
|
@@ -55,17 +65,22 @@ export function doRecall(runtime, config, ctx, query, source) {
|
|
|
55
65
|
// turn, their score + source path. Linked to the turn row written at
|
|
56
66
|
// turn_end via the conversation+turnIndex. Best-effort + non-fatal.
|
|
57
67
|
try {
|
|
58
|
-
const convId =
|
|
59
|
-
const turnId =
|
|
68
|
+
const convId = ensureConversationIdFor(config, sid, runtime.currentStateDir);
|
|
69
|
+
const turnId = recordTurnWrite(config, {
|
|
60
70
|
conversationId: convId,
|
|
61
71
|
sessionId: sid,
|
|
62
72
|
turnIndex: runtime.currentTurn,
|
|
73
|
+
role: "assistant",
|
|
63
74
|
startedAt: Date.now(),
|
|
64
75
|
}, runtime.currentStateDir);
|
|
65
|
-
|
|
76
|
+
recordRecallWrite(config, turnId, result.toInject.map((h) => ({
|
|
66
77
|
checkpointId: h.checkpoint.checkpointId,
|
|
67
78
|
score: h.score,
|
|
68
|
-
source:
|
|
79
|
+
source: h.raptorLevel !== undefined
|
|
80
|
+
? "raptor"
|
|
81
|
+
: h.repoId
|
|
82
|
+
? "cross-repo"
|
|
83
|
+
: "flat",
|
|
69
84
|
raptorLevel: h.raptorLevel,
|
|
70
85
|
})), runtime.currentStateDir);
|
|
71
86
|
}
|
|
@@ -92,9 +107,15 @@ export async function doRecallAsync(runtime, config, ctx, query, source, opts =
|
|
|
92
107
|
const liveWindow = config.windowDedupe ? extractLiveWindow(ctx) : undefined;
|
|
93
108
|
// Sync same-repo first (fast, never blocks).
|
|
94
109
|
const sameRepo = recallAndInline({
|
|
95
|
-
sessionId: sid,
|
|
96
|
-
|
|
97
|
-
|
|
110
|
+
sessionId: sid,
|
|
111
|
+
query,
|
|
112
|
+
limit: config.autoInlineK,
|
|
113
|
+
source,
|
|
114
|
+
skipInjected: true,
|
|
115
|
+
recallMaxTokens: config.recallMaxTokens,
|
|
116
|
+
windowDedupe: config.windowDedupe,
|
|
117
|
+
liveWindow,
|
|
118
|
+
dedupSim: config.dedupSim,
|
|
98
119
|
}, runtime.store);
|
|
99
120
|
if (!config.crossRepoEnabled || !opts.crossRepo)
|
|
100
121
|
return sameRepo;
|
|
@@ -103,9 +124,16 @@ export async function doRecallAsync(runtime, config, ctx, query, source, opts =
|
|
|
103
124
|
// Augment: cross-repo HNSW (async) with the stricter floor. Non-fatal.
|
|
104
125
|
try {
|
|
105
126
|
const x = await recallAndInlineAsync({
|
|
106
|
-
sessionId: sid,
|
|
107
|
-
|
|
108
|
-
|
|
127
|
+
sessionId: sid,
|
|
128
|
+
query,
|
|
129
|
+
limit: config.autoInlineK,
|
|
130
|
+
source,
|
|
131
|
+
skipInjected: true,
|
|
132
|
+
recallMaxTokens: config.recallMaxTokens,
|
|
133
|
+
windowDedupe: config.windowDedupe,
|
|
134
|
+
liveWindow,
|
|
135
|
+
dedupSim: config.crossRepoCosine,
|
|
136
|
+
crossRepo: true,
|
|
109
137
|
// F2: resolve the machine-wide index dir via the shared resolver so the
|
|
110
138
|
// cross-repo injected-set dedup works even when MEGACOMPACT_INDEX_DIR is
|
|
111
139
|
// unset. The env var still wins when set (getIndexDir checks it first);
|
|
@@ -116,7 +144,9 @@ export async function doRecallAsync(runtime, config, ctx, query, source, opts =
|
|
|
116
144
|
globalIndexDir: getIndexDir(),
|
|
117
145
|
}, runtime.store);
|
|
118
146
|
runtime.dashboard.event("recall-crossrepo", {
|
|
119
|
-
source,
|
|
147
|
+
source,
|
|
148
|
+
query: query.slice(0, 120),
|
|
149
|
+
injected: x.toInject.length,
|
|
120
150
|
sourceRepos: x.toInject.map((h) => h.repoId).filter(Boolean),
|
|
121
151
|
});
|
|
122
152
|
// Merge, dedup by checkpointId, respect the same token cap by reformatting.
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mega-topics-cmds.ts — S51C /mega-topics + /mega-topic commands.
|
|
3
|
+
*
|
|
4
|
+
* Lists auto-categorized wiki topics and renders wiki pages via ctx.ui.notify.
|
|
5
|
+
* Read-only (PREVENT-PI-001/002); local SQLite only (PREVENT-PI-004);
|
|
6
|
+
* parameterized queries (PREVENT-002). Requires turnsDbEnabled; no-ops with
|
|
7
|
+
* guidance when OFF or when AUTO_WIKI_ENABLED is false.
|
|
8
|
+
*/
|
|
9
|
+
import { createTopicStore } from "../src/topics/store.js";
|
|
10
|
+
import { buildTopicModel } from "../src/topics/cluster.js";
|
|
11
|
+
import { openStore } from "../src/store/sqlite/utils.js";
|
|
12
|
+
/** Register the /mega-topics + /mega-topic commands. */
|
|
13
|
+
export function registerTopicsCommands(pi, runtime, config) {
|
|
14
|
+
pi.registerCommand("mega-topics", {
|
|
15
|
+
description: "List auto-categorized wiki topics (k-means + TF-IDF over real embeddings). Usage: /mega-topics",
|
|
16
|
+
handler: async (_args, ctx) => {
|
|
17
|
+
try {
|
|
18
|
+
if (!config.turnsDbEnabled || !config.autoWikiEnabled) {
|
|
19
|
+
ctx.ui.notify("[mega-compact] /mega-topics requires turnsDbEnabled + autoWikiEnabled.");
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
runtime.bindRepo(ctx.cwd);
|
|
23
|
+
const stateDir = runtime.currentStateDir;
|
|
24
|
+
const topicStore = createTopicStore(stateDir);
|
|
25
|
+
const topics = topicStore.getTopics();
|
|
26
|
+
const stats = topicStore.getTopicStats();
|
|
27
|
+
if (topics.length === 0) {
|
|
28
|
+
ctx.ui.notify("[mega-compact] No topics yet. Topics are auto-generated after every 10th compaction from real memory embeddings (k-means + TF-IDF).");
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
ctx.ui.notify(`[mega-compact] ${stats.totalTopics} topic(s) · ${stats.totalAssigned} assigned memories` +
|
|
32
|
+
(stats.lastRebuildAt
|
|
33
|
+
? ` · last rebuild ${new Date(stats.lastRebuildAt).toLocaleString()}`
|
|
34
|
+
: ""));
|
|
35
|
+
for (const t of topics) {
|
|
36
|
+
const terms = t.termScores
|
|
37
|
+
.slice(0, 5)
|
|
38
|
+
.map((s) => s.term)
|
|
39
|
+
.join(", ");
|
|
40
|
+
ctx.ui.notify(` ${t.id}: ${t.label} (${t.memoryCount} memories) — terms: ${terms}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch (e) {
|
|
44
|
+
ctx.ui.notify(`[mega-compact] /mega-topics failed: ${String(e)}`);
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
pi.registerCommand("mega-topic", {
|
|
49
|
+
description: "Show a wiki page for one topic. Usage: /mega-topic <topicId>",
|
|
50
|
+
handler: async (args, ctx) => {
|
|
51
|
+
try {
|
|
52
|
+
if (!config.turnsDbEnabled || !config.autoWikiEnabled) {
|
|
53
|
+
ctx.ui.notify("[mega-compact] /mega-topic requires turnsDbEnabled + autoWikiEnabled.");
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const topicId = args.trim();
|
|
57
|
+
if (!topicId) {
|
|
58
|
+
ctx.ui.notify("[mega-compact] /mega-topic needs a topic id, e.g. /mega-topic topic_0");
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
runtime.bindRepo(ctx.cwd);
|
|
62
|
+
const stateDir = runtime.currentStateDir;
|
|
63
|
+
const topicStore = createTopicStore(stateDir);
|
|
64
|
+
const topics = topicStore.getTopics();
|
|
65
|
+
const topic = topics.find((t) => t.id === topicId);
|
|
66
|
+
if (!topic) {
|
|
67
|
+
ctx.ui.notify(`[mega-compact] topic ${topicId} not found. Use /mega-topics to list.`);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
// Show topic details.
|
|
71
|
+
const assignments = topicStore.getMemoriesForTopic(topicId);
|
|
72
|
+
ctx.ui.notify(`[mega-compact] ${topic.id}: ${topic.label} (${topic.memoryCount} memories)`);
|
|
73
|
+
const terms = topic.termScores
|
|
74
|
+
.slice(0, 8)
|
|
75
|
+
.map((s) => `${s.term} (${s.score.toFixed(2)})`)
|
|
76
|
+
.join(", ");
|
|
77
|
+
ctx.ui.notify(` terms: ${terms}`);
|
|
78
|
+
ctx.ui.notify(` assignments: ${assignments.length} memories`);
|
|
79
|
+
for (const a of assignments.slice(0, 10)) {
|
|
80
|
+
ctx.ui.notify(` ${a.memoryId} — confidence ${(a.confidence * 100).toFixed(1)}%`);
|
|
81
|
+
}
|
|
82
|
+
if (assignments.length > 10) {
|
|
83
|
+
ctx.ui.notify(` ... and ${assignments.length - 10} more`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch (e) {
|
|
87
|
+
ctx.ui.notify(`[mega-compact] /mega-topic failed: ${String(e)}`);
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
pi.registerCommand("mega-topics-rebuild", {
|
|
92
|
+
description: "Force-rebuild the topic model now (k-means + TF-IDF over real embeddings). Usage: /mega-topics-rebuild",
|
|
93
|
+
handler: async (_args, ctx) => {
|
|
94
|
+
try {
|
|
95
|
+
if (!config.turnsDbEnabled || !config.autoWikiEnabled) {
|
|
96
|
+
ctx.ui.notify("[mega-compact] /mega-topics-rebuild requires turnsDbEnabled + autoWikiEnabled.");
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
runtime.bindRepo(ctx.cwd);
|
|
100
|
+
const stateDir = runtime.currentStateDir;
|
|
101
|
+
const db = openStore(stateDir);
|
|
102
|
+
const model = buildTopicModel(db);
|
|
103
|
+
createTopicStore(stateDir).replaceTopicModel(model);
|
|
104
|
+
ctx.ui.notify(`[mega-compact] wiki rebuilt: ${model.k} topics from ${model.totalChunks} chunks (${model.criterion}, silhouette=${model.silhouetteScore?.toFixed(3) ?? "n/a"})`);
|
|
105
|
+
}
|
|
106
|
+
catch (e) {
|
|
107
|
+
ctx.ui.notify(`[mega-compact] /mega-topics-rebuild failed: ${String(e)}`);
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mega-turn-store.ts — S49 adapter: route per-turn writes to the isolated
|
|
3
|
+
* turns.db (flag ON) or the legacy main-db helpers (flag OFF).
|
|
4
|
+
*
|
|
5
|
+
* RECONCILIATION: maps the extension's per-turn writes onto the contract-first
|
|
6
|
+
* `TurnStore` (master's design): `appendTurn` / `appendRecall` /
|
|
7
|
+
* `ensureConversationId` / `asAdmin().stampTurnsEpoch` / `forkConversation`.
|
|
8
|
+
* src/store/turns/ stays pi-agnostic; this module is the ONLY pi-coupled seam for
|
|
9
|
+
* turn writes. Both paths are best-effort at the call site (try/catch in the
|
|
10
|
+
* handlers), so a failure never breaks the agent loop or the recall path.
|
|
11
|
+
*/
|
|
12
|
+
import { createTurnStore, } from "../src/store/turns/index.js";
|
|
13
|
+
import { recordTurn as legacyRecordTurn, recordTurnRecall as legacyRecordTurnRecall, ensureConversationId as legacyEnsureConversationId, } from "../src/store/sqlite/turns.js";
|
|
14
|
+
import { openStore } from "../src/store/sqlite/utils.js";
|
|
15
|
+
// Per-stateDir cache of turn stores so the extension doesn't reopen the file
|
|
16
|
+
// on every turn. Keyed by stateDir; the underlying connection is also cached
|
|
17
|
+
// in src/store/turns/connection.ts, so this is a thin factory memo.
|
|
18
|
+
const stores = new Map();
|
|
19
|
+
function storeFor(stateDir) {
|
|
20
|
+
let s = stores.get(stateDir);
|
|
21
|
+
if (!s) {
|
|
22
|
+
s = createTurnStore({ stateDir });
|
|
23
|
+
stores.set(stateDir, s);
|
|
24
|
+
}
|
|
25
|
+
return s;
|
|
26
|
+
}
|
|
27
|
+
/** Map a legacy RecallSource onto the contract `TurnRecallEntry.source` enum. */
|
|
28
|
+
function mapSource(s) {
|
|
29
|
+
switch (s) {
|
|
30
|
+
case "flat":
|
|
31
|
+
case "checkpoint":
|
|
32
|
+
return "checkpoint";
|
|
33
|
+
case "raptor":
|
|
34
|
+
case "cluster_summary":
|
|
35
|
+
return "cluster_summary";
|
|
36
|
+
default:
|
|
37
|
+
return "memory";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** Resolve (or generate) a session's conversation id on the active backend. */
|
|
41
|
+
export function ensureConversationIdFor(config, sessionId, stateDir) {
|
|
42
|
+
return config.turnsDbEnabled
|
|
43
|
+
? storeFor(stateDir).ensureConversationId(sessionId)
|
|
44
|
+
: legacyEnsureConversationId(sessionId, stateDir);
|
|
45
|
+
}
|
|
46
|
+
/** Record one turn row (turn_end). Returns the contract TurnId (string). */
|
|
47
|
+
export function recordTurnWrite(config, input, stateDir) {
|
|
48
|
+
if (!config.turnsDbEnabled) {
|
|
49
|
+
legacyRecordTurn(input, stateDir);
|
|
50
|
+
return "";
|
|
51
|
+
}
|
|
52
|
+
const entry = {
|
|
53
|
+
conversationId: input.conversationId,
|
|
54
|
+
sessionId: input.sessionId,
|
|
55
|
+
turnIndex: input.turnIndex,
|
|
56
|
+
role: input.role,
|
|
57
|
+
endedAt: input.endedAt ?? input.startedAt ?? Date.now(),
|
|
58
|
+
ctxTokens: input.ctxTokens,
|
|
59
|
+
ctxPercent: input.ctxPercent,
|
|
60
|
+
pressureBand: input.pressureBand,
|
|
61
|
+
model: input.modelId,
|
|
62
|
+
epochId: input.epochId,
|
|
63
|
+
};
|
|
64
|
+
return storeFor(stateDir).appendTurn(entry);
|
|
65
|
+
}
|
|
66
|
+
/** Record recall provenance for a turn (one hit at a time — contract is append-only). */
|
|
67
|
+
export function recordRecallWrite(config, turnId, hits, stateDir) {
|
|
68
|
+
if (!config.turnsDbEnabled) {
|
|
69
|
+
legacyRecordTurnRecall(Number(turnId), hits, stateDir);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const writer = storeFor(stateDir).asWriter();
|
|
73
|
+
for (const h of hits) {
|
|
74
|
+
const entry = {
|
|
75
|
+
turnId: String(turnId),
|
|
76
|
+
checkpointId: h.checkpointId,
|
|
77
|
+
score: h.score,
|
|
78
|
+
source: mapSource(h.source),
|
|
79
|
+
raptorLevel: h.raptorLevel,
|
|
80
|
+
};
|
|
81
|
+
writer.appendRecall(entry);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/** S50B: stamp `epoch_id` on a session's unstamped turns (compact-commit).
|
|
85
|
+
* Isolated-store only — the legacy main-db turn path is being retired, so it
|
|
86
|
+
* is a no-op when turnsDbEnabled is false. Returns the number stamped. */
|
|
87
|
+
export function stampTurnsEpochFor(config, sessionId, epochId, stateDir) {
|
|
88
|
+
return config.turnsDbEnabled
|
|
89
|
+
? storeFor(stateDir).asAdmin().stampTurnsEpoch(sessionId, epochId)
|
|
90
|
+
: 0;
|
|
91
|
+
}
|
|
92
|
+
/** S50C: resolve the active turn store for metrics/fork commands.
|
|
93
|
+
* Returns null when turnsDbEnabled is OFF (legacy main-db turn path — the
|
|
94
|
+
* per-turn metrics/fork commands are isolated-store only). */
|
|
95
|
+
export function turnStoreFor(config, stateDir) {
|
|
96
|
+
return config.turnsDbEnabled ? storeFor(stateDir) : null;
|
|
97
|
+
}
|
|
98
|
+
/** S50C: open (cached) the main db handle for metrics read-queries
|
|
99
|
+
* (raw_transcript + checkpoint_epochs). Callers must NOT close it. */
|
|
100
|
+
export function mainDbFor(stateDir) {
|
|
101
|
+
return openStore(stateDir);
|
|
102
|
+
}
|
|
103
|
+
/** Test/teardown: close all cached turn stores for a state dir. */
|
|
104
|
+
export function closeTurnStoreFor(stateDir) {
|
|
105
|
+
const s = stores.get(stateDir);
|
|
106
|
+
if (s) {
|
|
107
|
+
s.close();
|
|
108
|
+
stores.delete(stateDir);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mega-turn-store.test.ts — S49C adapter routing tests.
|
|
3
|
+
*
|
|
4
|
+
* Proves the adapter writes to the isolated turns.db when turnsDbEnabled is
|
|
5
|
+
* true and to the legacy main-db helpers when false. No network; temp dirs.
|
|
6
|
+
*/
|
|
7
|
+
import { test, beforeEach, afterEach } from "node:test";
|
|
8
|
+
import assert from "node:assert/strict";
|
|
9
|
+
import { mkdtempSync, rmSync, existsSync } from "node:fs";
|
|
10
|
+
import { tmpdir } from "node:os";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { DatabaseSync } from "node:sqlite";
|
|
13
|
+
import { ensureConversationIdFor, recordTurnWrite, recordRecallWrite, } from "./mega-turn-store.js";
|
|
14
|
+
import { closeTurnStore, turnDbPath } from "../src/store/turns/index.js";
|
|
15
|
+
let tmpDir;
|
|
16
|
+
let counter = 0;
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
tmpDir = mkdtempSync(join(tmpdir(), "mc-adapter-"));
|
|
19
|
+
});
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
22
|
+
});
|
|
23
|
+
function stateDir() {
|
|
24
|
+
return join(tmpDir, `run-${counter++}`);
|
|
25
|
+
}
|
|
26
|
+
/** Minimal MegaConfig stub — only the flag the adapter reads. */
|
|
27
|
+
function cfg(turnsDbEnabled) {
|
|
28
|
+
return { turnsDbEnabled };
|
|
29
|
+
}
|
|
30
|
+
test("flag ON → writes land in turns.db (not main sqlite.db)", () => {
|
|
31
|
+
const dir = stateDir();
|
|
32
|
+
const c = cfg(true);
|
|
33
|
+
const conv = ensureConversationIdFor(c, "sess_on", dir);
|
|
34
|
+
const turnId = recordTurnWrite(c, {
|
|
35
|
+
conversationId: conv,
|
|
36
|
+
sessionId: "sess_on",
|
|
37
|
+
turnIndex: 0,
|
|
38
|
+
role: "assistant",
|
|
39
|
+
endedAt: Date.now(),
|
|
40
|
+
}, dir);
|
|
41
|
+
recordRecallWrite(c, turnId, [{ checkpointId: "cp_on", score: 0.5, source: "flat" }], dir);
|
|
42
|
+
closeTurnStore(dir);
|
|
43
|
+
assert.ok(existsSync(turnDbPath(dir)), "turns.db should exist");
|
|
44
|
+
assert.ok(!existsSync(join(dir, "sqlite.db")), "main sqlite.db must NOT be created on the flag-ON path");
|
|
45
|
+
});
|
|
46
|
+
test("flag OFF → writes land in main sqlite.db (legacy S48 path)", () => {
|
|
47
|
+
const dir = stateDir();
|
|
48
|
+
const c = cfg(false);
|
|
49
|
+
const conv = ensureConversationIdFor(c, "sess_off", dir);
|
|
50
|
+
const turnId = recordTurnWrite(c, {
|
|
51
|
+
conversationId: conv,
|
|
52
|
+
sessionId: "sess_off",
|
|
53
|
+
turnIndex: 0,
|
|
54
|
+
role: "assistant",
|
|
55
|
+
endedAt: Date.now(),
|
|
56
|
+
}, dir);
|
|
57
|
+
recordRecallWrite(c, turnId, [{ checkpointId: "cp_off", score: 0.5, source: "flat" }], dir);
|
|
58
|
+
// Legacy helpers open the main sqlite.db.
|
|
59
|
+
assert.ok(existsSync(join(dir, "sqlite.db")), "main sqlite.db should exist on the flag-OFF path");
|
|
60
|
+
assert.ok(!existsSync(turnDbPath(dir)), "turns.db must NOT be created on the flag-OFF path");
|
|
61
|
+
// And the row is actually in the main db's turns table.
|
|
62
|
+
const main = new DatabaseSync(join(dir, "sqlite.db"));
|
|
63
|
+
const rows = main
|
|
64
|
+
.prepare("SELECT * FROM turns WHERE session_id = ?")
|
|
65
|
+
.all("sess_off");
|
|
66
|
+
assert.equal(rows.length, 1);
|
|
67
|
+
main.close();
|
|
68
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* config/turns.ts — SINGLE SOURCE OF TRUTH for the S49 turn-store flags.
|
|
3
|
+
*
|
|
4
|
+
* Feature-flag semantics (docs/specs/s49-turn-db-foundation.md):
|
|
5
|
+
* - TURNS_DB_ENABLED defaults ON (isolated turns.db). OFF = byte-identical S48
|
|
6
|
+
* behavior (turn helpers use the main sqlite.db). Opt-out, not opt-in.
|
|
7
|
+
* - All values read from MEGACOMPACT_* env at load, with the defaults below as
|
|
8
|
+
* fallback. Booleans/numbers only — no calibration.
|
|
9
|
+
*
|
|
10
|
+
* PREVENT-PI-004: pure config, no network.
|
|
11
|
+
*/
|
|
12
|
+
function envBool(name, def) {
|
|
13
|
+
const v = process.env[name];
|
|
14
|
+
if (v === undefined)
|
|
15
|
+
return def;
|
|
16
|
+
return v === "true" || v === "1";
|
|
17
|
+
}
|
|
18
|
+
function envNum(name, def) {
|
|
19
|
+
const v = process.env[name];
|
|
20
|
+
if (v === undefined)
|
|
21
|
+
return def;
|
|
22
|
+
const n = Number(v);
|
|
23
|
+
return Number.isFinite(n) ? n : def;
|
|
24
|
+
}
|
|
25
|
+
function envKRange(defMin, defMax) {
|
|
26
|
+
const v = process.env.MEGACOMPACT_WIKI_K_RANGE;
|
|
27
|
+
if (v) {
|
|
28
|
+
const parts = v.split(",").map((s) => Number(s.trim()));
|
|
29
|
+
if (parts.length === 2 && parts.every((n) => Number.isFinite(n) && n >= 1)) {
|
|
30
|
+
return [Math.floor(parts[0]), Math.floor(parts[1])];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return [defMin, defMax];
|
|
34
|
+
}
|
|
35
|
+
export function loadTurnsConfig() {
|
|
36
|
+
const [kMin, kMax] = envKRange(3, 15);
|
|
37
|
+
return {
|
|
38
|
+
TURNS_DB_ENABLED: envBool("MEGACOMPACT_TURNS_DB", true),
|
|
39
|
+
TURNS_RETENTION_DAYS: envNum("MEGACOMPACT_TURNS_RETENTION_DAYS", 30),
|
|
40
|
+
TURNS_KEEP_MIN_PER_CONVERSATION: envNum("MEGACOMPACT_TURNS_KEEP_MIN", 5),
|
|
41
|
+
AUTO_WIKI_ENABLED: envBool("MEGACOMPACT_AUTO_WIKI", true),
|
|
42
|
+
WIKI_K_MIN: kMin,
|
|
43
|
+
WIKI_K_MAX: kMax,
|
|
44
|
+
WIKI_LABEL_TOP_TERMS: envNum("MEGACOMPACT_WIKI_LABEL_TOP_TERMS", 5),
|
|
45
|
+
WIKI_REBUILD_EVERY_N_COMPACTS: envNum("MEGACOMPACT_WIKI_REBUILD_EVERY", 10),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export const TurnsConfig = loadTurnsConfig();
|
package/dist/src/fork.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** Typed error so hosts can distinguish "unknown turn" from storage failures. */
|
|
2
|
+
export class ForkError extends Error {
|
|
3
|
+
code;
|
|
4
|
+
constructor(code, message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "ForkError";
|
|
7
|
+
this.code = code;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Fork `parentConversationId` at `turnIndex`. Resolves the turn entry (by
|
|
12
|
+
* conversation + index via `getTurnByIndex`), creates the child conversation via
|
|
13
|
+
* `store.forkConversation` (returns the child ConversationId), and returns the
|
|
14
|
+
* parent turn's recorded recall set (`listRecallByIndex`) to rehydrate.
|
|
15
|
+
*
|
|
16
|
+
* @throws ForkError TURN_NOT_FOUND when the (conversation, turnIndex) row is absent.
|
|
17
|
+
* @throws ForkError NO_RECALL when the fork turn has no injected checkpoints to replay.
|
|
18
|
+
*/
|
|
19
|
+
export function forkFromConversation(store, parentConversationId, turnIndex) {
|
|
20
|
+
const turn = store.getTurnByIndex(parentConversationId, turnIndex);
|
|
21
|
+
if (!turn) {
|
|
22
|
+
throw new ForkError("TURN_NOT_FOUND", `no turn ${turnIndex} in conversation ${parentConversationId}`);
|
|
23
|
+
}
|
|
24
|
+
const childConversationId = store.forkConversation(parentConversationId, turnIndex);
|
|
25
|
+
const recalled = store.listRecallByIndex(parentConversationId, turnIndex);
|
|
26
|
+
const checkpointIds = [...new Set(recalled.map((r) => r.checkpointId))];
|
|
27
|
+
if (checkpointIds.length === 0) {
|
|
28
|
+
// The child conversation was still created (lineage is recorded), but a fork
|
|
29
|
+
// with nothing to rehydrate is almost always a caller mistake — surface it.
|
|
30
|
+
throw new ForkError("NO_RECALL", `turn ${turnIndex} in ${parentConversationId} has no injected checkpoints to fork from`);
|
|
31
|
+
}
|
|
32
|
+
return { childConversationId, forkTurn: turn, recalled, checkpointIds };
|
|
33
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fork.test.ts — S50C fork primitive tests. No network; temp dirs.
|
|
3
|
+
*
|
|
4
|
+
* Uses the contract-first TurnStore (createTurnStore({ stateDir })).
|
|
5
|
+
*/
|
|
6
|
+
import { test, beforeEach, afterEach } from "node:test";
|
|
7
|
+
import assert from "node:assert/strict";
|
|
8
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
9
|
+
import { tmpdir } from "node:os";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { createTurnStore, closeAllTurnDbs } from "./store/turns/index.js";
|
|
12
|
+
import { forkFromConversation, ForkError } from "./fork.js";
|
|
13
|
+
let tmpDir;
|
|
14
|
+
let counter = 0;
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
tmpDir = mkdtempSync(join(tmpdir(), "mc-fork-"));
|
|
17
|
+
});
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
closeAllTurnDbs();
|
|
20
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
21
|
+
});
|
|
22
|
+
function stateDir() {
|
|
23
|
+
return join(tmpDir, `run-${counter++}`);
|
|
24
|
+
}
|
|
25
|
+
test("fork resolves the turn + returns the recall set to rehydrate", () => {
|
|
26
|
+
const dir = stateDir();
|
|
27
|
+
const store = createTurnStore({ stateDir: dir });
|
|
28
|
+
const conv = store.ensureConversationId("sess_f");
|
|
29
|
+
const turnId = store.appendTurn({
|
|
30
|
+
conversationId: conv,
|
|
31
|
+
sessionId: "sess_f",
|
|
32
|
+
turnIndex: 2,
|
|
33
|
+
role: "assistant",
|
|
34
|
+
endedAt: Date.now(),
|
|
35
|
+
});
|
|
36
|
+
store.appendRecall({
|
|
37
|
+
turnId: turnId,
|
|
38
|
+
checkpointId: "cp_a",
|
|
39
|
+
score: 0.9,
|
|
40
|
+
source: "checkpoint",
|
|
41
|
+
});
|
|
42
|
+
store.appendRecall({
|
|
43
|
+
turnId: turnId,
|
|
44
|
+
checkpointId: "cp_b",
|
|
45
|
+
score: 0.7,
|
|
46
|
+
source: "cluster_summary",
|
|
47
|
+
raptorLevel: 1,
|
|
48
|
+
});
|
|
49
|
+
const out = forkFromConversation(store, conv, 2);
|
|
50
|
+
assert.ok(out.childConversationId.startsWith("conv_"));
|
|
51
|
+
assert.notEqual(out.childConversationId, conv);
|
|
52
|
+
assert.equal(out.forkTurn.turnIndex, 2);
|
|
53
|
+
assert.deepEqual(out.checkpointIds.sort(), ["cp_a", "cp_b"]);
|
|
54
|
+
assert.equal(out.recalled.length, 2);
|
|
55
|
+
store.close();
|
|
56
|
+
});
|
|
57
|
+
test("unknown turn → ForkError TURN_NOT_FOUND", () => {
|
|
58
|
+
const dir = stateDir();
|
|
59
|
+
const store = createTurnStore({ stateDir: dir });
|
|
60
|
+
const conv = store.ensureConversationId("sess_g");
|
|
61
|
+
assert.throws(() => forkFromConversation(store, conv, 99), (e) => {
|
|
62
|
+
return e instanceof ForkError && e.code === "TURN_NOT_FOUND";
|
|
63
|
+
});
|
|
64
|
+
store.close();
|
|
65
|
+
});
|
|
66
|
+
test("turn with no recall set → ForkError NO_RECALL", () => {
|
|
67
|
+
const dir = stateDir();
|
|
68
|
+
const store = createTurnStore({ stateDir: dir });
|
|
69
|
+
const conv = store.ensureConversationId("sess_h");
|
|
70
|
+
store.appendTurn({
|
|
71
|
+
conversationId: conv,
|
|
72
|
+
sessionId: "sess_h",
|
|
73
|
+
turnIndex: 0,
|
|
74
|
+
role: "assistant",
|
|
75
|
+
endedAt: Date.now(),
|
|
76
|
+
});
|
|
77
|
+
assert.throws(() => forkFromConversation(store, conv, 0), (e) => {
|
|
78
|
+
return e instanceof ForkError && e.code === "NO_RECALL";
|
|
79
|
+
});
|
|
80
|
+
store.close();
|
|
81
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/** Open an intent queue over a turns.db handle (from openTurnStore/openTurnDb). */
|
|
2
|
+
export function openIntentQueue(db) {
|
|
3
|
+
function rowToIntent(r) {
|
|
4
|
+
return {
|
|
5
|
+
id: String(r.id),
|
|
6
|
+
conversationId: String(r.target_conversation_id),
|
|
7
|
+
targetTurnIndex: Number(r.target_turn_id),
|
|
8
|
+
createdAt: Number(r.requested_at),
|
|
9
|
+
status: r.consumed_at == null ? "pending" : "consumed",
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
return {
|
|
13
|
+
postIntent(input) {
|
|
14
|
+
const createdAt = Date.now();
|
|
15
|
+
const res = db
|
|
16
|
+
.prepare(`INSERT INTO pending_fork
|
|
17
|
+
(target_conversation_id, target_turn_id, requested_at, consumed_at)
|
|
18
|
+
VALUES (?, ?, ?, NULL)`)
|
|
19
|
+
.run(input.conversationId, input.targetTurnIndex, createdAt);
|
|
20
|
+
const id = String(Number(res.lastInsertRowid));
|
|
21
|
+
return {
|
|
22
|
+
id,
|
|
23
|
+
conversationId: input.conversationId,
|
|
24
|
+
targetTurnIndex: input.targetTurnIndex,
|
|
25
|
+
createdAt,
|
|
26
|
+
status: "pending",
|
|
27
|
+
};
|
|
28
|
+
},
|
|
29
|
+
pendingIntents() {
|
|
30
|
+
const rows = db
|
|
31
|
+
.prepare(`SELECT id, target_conversation_id, target_turn_id, requested_at, consumed_at
|
|
32
|
+
FROM pending_fork WHERE consumed_at IS NULL
|
|
33
|
+
ORDER BY requested_at ASC`)
|
|
34
|
+
.all();
|
|
35
|
+
return rows.map(rowToIntent);
|
|
36
|
+
},
|
|
37
|
+
allIntents(limit = 100) {
|
|
38
|
+
const rows = db
|
|
39
|
+
.prepare(`SELECT id, target_conversation_id, target_turn_id, requested_at, consumed_at
|
|
40
|
+
FROM pending_fork ORDER BY requested_at DESC, id DESC LIMIT ?`)
|
|
41
|
+
.all(limit);
|
|
42
|
+
return rows.map(rowToIntent);
|
|
43
|
+
},
|
|
44
|
+
consumeIntent(id) {
|
|
45
|
+
db.prepare("UPDATE pending_fork SET consumed_at = ? WHERE id = ?").run(Date.now(), id);
|
|
46
|
+
},
|
|
47
|
+
abandonIntent(id) {
|
|
48
|
+
db.prepare("DELETE FROM pending_fork WHERE id = ?").run(id);
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|