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,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* index.ts — Barrel for src/store/turns/ (S49 reconciled).
|
|
3
|
+
*
|
|
4
|
+
* Re-exports the contract-first factory + all contract types (master's design)
|
|
5
|
+
* and the two backend constructors (SqliteTurnStore / InMemoryTurnStore), plus
|
|
6
|
+
* this branch's reconciled connection layer + migration helper. Hosts import
|
|
7
|
+
* only this file + types.ts.
|
|
8
|
+
*
|
|
9
|
+
* PREVENT-PI-004: no network. Reuse-clean: no pi imports.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export type {
|
|
13
|
+
TurnId,
|
|
14
|
+
ConversationId,
|
|
15
|
+
SessionId,
|
|
16
|
+
TurnEntry,
|
|
17
|
+
TurnRecallEntry,
|
|
18
|
+
ConversationFork,
|
|
19
|
+
TurnFilter,
|
|
20
|
+
PruneReport,
|
|
21
|
+
RetentionPolicy,
|
|
22
|
+
StoreSnapshot,
|
|
23
|
+
ConversationStats,
|
|
24
|
+
TurnReader,
|
|
25
|
+
TurnWriter,
|
|
26
|
+
TurnAdmin,
|
|
27
|
+
TurnStore,
|
|
28
|
+
TurnStoreOptions,
|
|
29
|
+
TurnStoreFactory,
|
|
30
|
+
} from "./types.js";
|
|
31
|
+
|
|
32
|
+
export { SqliteTurnStore } from "./sqlite-store.js";
|
|
33
|
+
export { InMemoryTurnStore } from "./memory-store.js";
|
|
34
|
+
|
|
35
|
+
// Reconciled connection layer (master shape + this branch's migration +
|
|
36
|
+
// closed-handle eviction + env override + openTurnStore back-compat alias).
|
|
37
|
+
export {
|
|
38
|
+
openTurnDb,
|
|
39
|
+
openTurnStore,
|
|
40
|
+
closeTurnDb,
|
|
41
|
+
closeTurnStore,
|
|
42
|
+
closeAllTurnDbs,
|
|
43
|
+
turnDbPath,
|
|
44
|
+
TURNS_DB_FILE,
|
|
45
|
+
withTx,
|
|
46
|
+
} from "./connection.js";
|
|
47
|
+
export { initTurnSchema } from "./schema.js";
|
|
48
|
+
export { migrateTurnTablesIfNeeded } from "./migrations.js";
|
|
49
|
+
|
|
50
|
+
import { SqliteTurnStore } from "./sqlite-store.js";
|
|
51
|
+
import { InMemoryTurnStore } from "./memory-store.js";
|
|
52
|
+
import type { TurnStore, TurnStoreOptions } from "./types.js";
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Factory: create a TurnStore from options.
|
|
56
|
+
*
|
|
57
|
+
* - Default: SqliteTurnStore backed by turns.db in stateDir
|
|
58
|
+
* - inMemory: InMemoryTurnStore (no file I/O)
|
|
59
|
+
*/
|
|
60
|
+
export function createTurnStore(options: TurnStoreOptions): TurnStore {
|
|
61
|
+
if (options.inMemory) {
|
|
62
|
+
return new InMemoryTurnStore(options);
|
|
63
|
+
}
|
|
64
|
+
return new SqliteTurnStore(options);
|
|
65
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* memory-store.test.ts — InMemoryTurnStore compliance.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { InMemoryTurnStore } from "./memory-store.js";
|
|
6
|
+
import { runComplianceSuite } from "./contract-compliance.test.js";
|
|
7
|
+
|
|
8
|
+
// Run the shared compliance suite against the in-memory backend
|
|
9
|
+
runComplianceSuite(
|
|
10
|
+
"InMemoryTurnStore",
|
|
11
|
+
(options) => {
|
|
12
|
+
return new InMemoryTurnStore(options);
|
|
13
|
+
},
|
|
14
|
+
{ stateDir: "/tmp/turns-compliance-memory", inMemory: true },
|
|
15
|
+
);
|
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* memory-store.ts — InMemoryTurnStore: test harness + embeddable backend.
|
|
3
|
+
*
|
|
4
|
+
* Same contract as SqliteTurnStore, backed by Maps. No file I/O.
|
|
5
|
+
* Used for tests and for hosts that don't want SQLite.
|
|
6
|
+
*
|
|
7
|
+
* PREVENT-PI-004: no network. PREVENT-002: no SQL here.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { randomBytes } from "node:crypto";
|
|
11
|
+
import type {
|
|
12
|
+
TurnStore,
|
|
13
|
+
TurnReader,
|
|
14
|
+
TurnWriter,
|
|
15
|
+
TurnAdmin,
|
|
16
|
+
TurnId,
|
|
17
|
+
ConversationId,
|
|
18
|
+
SessionId,
|
|
19
|
+
TurnEntry,
|
|
20
|
+
TurnRecallEntry,
|
|
21
|
+
ConversationFork,
|
|
22
|
+
TurnFilter,
|
|
23
|
+
PruneReport,
|
|
24
|
+
RetentionPolicy,
|
|
25
|
+
StoreSnapshot,
|
|
26
|
+
ConversationStats,
|
|
27
|
+
TurnStoreOptions,
|
|
28
|
+
} from "./types.js";
|
|
29
|
+
|
|
30
|
+
function newConversationId(): ConversationId {
|
|
31
|
+
return `conv_${randomBytes(8).toString("hex")}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function normalizeSessionId(sid: SessionId): SessionId {
|
|
35
|
+
return sid.replace(/-.*$/, "");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Auto-increment counter
|
|
39
|
+
let nextId = 1;
|
|
40
|
+
function allocId(): number {
|
|
41
|
+
return nextId++;
|
|
42
|
+
}
|
|
43
|
+
function resetIdCounter(): void {
|
|
44
|
+
nextId = 1;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Internal row with numeric id
|
|
48
|
+
interface TurnRow {
|
|
49
|
+
id: number;
|
|
50
|
+
entry: TurnEntry;
|
|
51
|
+
epochId?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface RecallRow {
|
|
55
|
+
turnId: number;
|
|
56
|
+
entry: TurnRecallEntry;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export class InMemoryTurnStore implements TurnStore {
|
|
60
|
+
private turns: Map<number, TurnRow> = new Map();
|
|
61
|
+
private recall: RecallRow[] = [];
|
|
62
|
+
private forks: ConversationFork[] = [];
|
|
63
|
+
// conversation_id → turn ids (for fast lookup)
|
|
64
|
+
private convIndex: Map<ConversationId, number[]> = new Map();
|
|
65
|
+
// session_id → conversation_id
|
|
66
|
+
private sessionConv: Map<SessionId, ConversationId> = new Map();
|
|
67
|
+
|
|
68
|
+
constructor(_options?: TurnStoreOptions) {
|
|
69
|
+
resetIdCounter();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ── TurnReader ──────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
query(filter: TurnFilter): TurnEntry[] {
|
|
75
|
+
let results = [...this.turns.values()].map((r) => r.entry);
|
|
76
|
+
|
|
77
|
+
if (filter.conversationId !== undefined) {
|
|
78
|
+
results = results.filter(
|
|
79
|
+
(t) => t.conversationId === filter.conversationId,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
if (filter.sessionId !== undefined) {
|
|
83
|
+
results = results.filter(
|
|
84
|
+
(t) =>
|
|
85
|
+
normalizeSessionId(t.sessionId) ===
|
|
86
|
+
normalizeSessionId(filter.sessionId!),
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
if (filter.sinceMs !== undefined) {
|
|
90
|
+
results = results.filter((t) => t.endedAt >= filter.sinceMs!);
|
|
91
|
+
}
|
|
92
|
+
if (filter.untilMs !== undefined) {
|
|
93
|
+
results = results.filter((t) => t.endedAt <= filter.untilMs!);
|
|
94
|
+
}
|
|
95
|
+
if (filter.pressureBand !== undefined) {
|
|
96
|
+
results = results.filter((t) => t.pressureBand === filter.pressureBand);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
results.sort((a, b) => a.endedAt - b.endedAt);
|
|
100
|
+
|
|
101
|
+
const offset = filter.offset ?? 0;
|
|
102
|
+
const limit = filter.limit ?? 1000;
|
|
103
|
+
return results.slice(offset, offset + limit);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
getTurn(turnId: TurnId): TurnEntry | undefined {
|
|
107
|
+
const row = this.turns.get(Number(turnId));
|
|
108
|
+
return row ? { ...row.entry, epochId: row.epochId } : undefined;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
getTurnByIndex(
|
|
112
|
+
conversationId: ConversationId,
|
|
113
|
+
turnIndex: number,
|
|
114
|
+
): TurnEntry | undefined {
|
|
115
|
+
const ids = this.convIndex.get(conversationId) ?? [];
|
|
116
|
+
for (const id of ids) {
|
|
117
|
+
const row = this.turns.get(id);
|
|
118
|
+
if (row && row.entry.turnIndex === turnIndex) {
|
|
119
|
+
return { ...row.entry, epochId: row.epochId };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
listRecall(turnId: TurnId): TurnRecallEntry[] {
|
|
126
|
+
return this.recall
|
|
127
|
+
.filter((r) => r.turnId === Number(turnId))
|
|
128
|
+
.map((r) => r.entry)
|
|
129
|
+
.sort((a, b) => b.score - a.score);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
listRecallByIndex(
|
|
133
|
+
conversationId: ConversationId,
|
|
134
|
+
turnIndex: number,
|
|
135
|
+
): TurnRecallEntry[] {
|
|
136
|
+
const turn = this.getTurnByIndex(conversationId, turnIndex);
|
|
137
|
+
if (!turn) return [];
|
|
138
|
+
// Resolve the numeric row id for this turn via the convIndex.
|
|
139
|
+
const ids = this.convIndex.get(conversationId) ?? [];
|
|
140
|
+
for (const id of ids) {
|
|
141
|
+
const row = this.turns.get(id);
|
|
142
|
+
if (row && row.entry.turnIndex === turnIndex) {
|
|
143
|
+
return this.listRecall(String(id));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return [];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
listForks(conversationId: ConversationId): ConversationFork[] {
|
|
150
|
+
return this.forks
|
|
151
|
+
.filter((f) => f.parentConversationId === conversationId)
|
|
152
|
+
.sort((a, b) => a.createdAt - b.createdAt);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
countTurns(conversationId: ConversationId): number {
|
|
156
|
+
const ids = this.convIndex.get(conversationId);
|
|
157
|
+
return ids?.length ?? 0;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
conversationStats(conversationId: ConversationId): ConversationStats {
|
|
161
|
+
const ids = this.convIndex.get(conversationId) ?? [];
|
|
162
|
+
const entries = ids
|
|
163
|
+
.map((id) => this.turns.get(id)?.entry)
|
|
164
|
+
.filter((e): e is TurnEntry => e !== undefined);
|
|
165
|
+
|
|
166
|
+
if (entries.length === 0) {
|
|
167
|
+
return {
|
|
168
|
+
turnCount: 0,
|
|
169
|
+
firstTurnAt: 0,
|
|
170
|
+
lastTurnAt: 0,
|
|
171
|
+
avgCtxPercent: 0,
|
|
172
|
+
pressureBands: {},
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
let ctxSum = 0;
|
|
177
|
+
let ctxCount = 0;
|
|
178
|
+
const bands: Record<string, number> = {};
|
|
179
|
+
|
|
180
|
+
for (const e of entries) {
|
|
181
|
+
if (e.ctxPercent !== undefined) {
|
|
182
|
+
ctxSum += e.ctxPercent;
|
|
183
|
+
ctxCount++;
|
|
184
|
+
}
|
|
185
|
+
if (e.pressureBand)
|
|
186
|
+
bands[e.pressureBand] = (bands[e.pressureBand] ?? 0) + 1;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
turnCount: entries.length,
|
|
191
|
+
firstTurnAt: entries[0].endedAt,
|
|
192
|
+
lastTurnAt: entries[entries.length - 1].endedAt,
|
|
193
|
+
avgCtxPercent: ctxCount > 0 ? ctxSum / ctxCount : 0,
|
|
194
|
+
pressureBands: bands,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ── TurnWriter ──────────────────────────────────────────────
|
|
199
|
+
|
|
200
|
+
appendTurn(entry: TurnEntry): TurnId {
|
|
201
|
+
const id = allocId();
|
|
202
|
+
const sid = normalizeSessionId(entry.sessionId);
|
|
203
|
+
const normalized = { ...entry, sessionId: sid };
|
|
204
|
+
this.turns.set(id, { id, entry: normalized });
|
|
205
|
+
|
|
206
|
+
// Update indices
|
|
207
|
+
const convIds = this.convIndex.get(entry.conversationId) ?? [];
|
|
208
|
+
convIds.push(id);
|
|
209
|
+
this.convIndex.set(entry.conversationId, convIds);
|
|
210
|
+
|
|
211
|
+
this.sessionConv.set(sid, entry.conversationId);
|
|
212
|
+
|
|
213
|
+
return String(id);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
appendRecall(entry: TurnRecallEntry): void {
|
|
217
|
+
// De-dup by (turn_id, checkpoint_id)
|
|
218
|
+
const exists = this.recall.some(
|
|
219
|
+
(r) =>
|
|
220
|
+
r.turnId === Number(entry.turnId) &&
|
|
221
|
+
r.entry.checkpointId === entry.checkpointId,
|
|
222
|
+
);
|
|
223
|
+
if (!exists) {
|
|
224
|
+
this.recall.push({ turnId: Number(entry.turnId), entry });
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
ensureConversationId(sessionId: SessionId): ConversationId {
|
|
229
|
+
const sid = normalizeSessionId(sessionId);
|
|
230
|
+
const existing = this.sessionConv.get(sid);
|
|
231
|
+
if (existing) return existing;
|
|
232
|
+
// Generate and store a new conversation ID for this session
|
|
233
|
+
const convId = newConversationId();
|
|
234
|
+
this.sessionConv.set(sid, convId);
|
|
235
|
+
return convId;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
forkConversation(
|
|
239
|
+
parentId: ConversationId,
|
|
240
|
+
forkTurnIndex: number,
|
|
241
|
+
): ConversationId {
|
|
242
|
+
const childId = newConversationId();
|
|
243
|
+
const now = Date.now();
|
|
244
|
+
|
|
245
|
+
// 1. Record the fork lineage
|
|
246
|
+
this.forks.push({
|
|
247
|
+
parentConversationId: parentId,
|
|
248
|
+
childConversationId: childId,
|
|
249
|
+
forkTurnIndex,
|
|
250
|
+
createdAt: now,
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// 2. Find the parent's turn at forkTurnIndex
|
|
254
|
+
const parentTurnIds = this.convIndex.get(parentId) ?? [];
|
|
255
|
+
const parentTurnRow = parentTurnIds
|
|
256
|
+
.map((id) => this.turns.get(id))
|
|
257
|
+
.find((r) => r?.entry.turnIndex === forkTurnIndex);
|
|
258
|
+
|
|
259
|
+
if (!parentTurnRow) return childId; // no parent turn — nothing to seed
|
|
260
|
+
|
|
261
|
+
// 3. Create a seed turn in the child conversation
|
|
262
|
+
const seedId = allocId();
|
|
263
|
+
const seedEntry: TurnEntry = {
|
|
264
|
+
conversationId: childId,
|
|
265
|
+
sessionId: `fork_${childId}`,
|
|
266
|
+
turnIndex: 0,
|
|
267
|
+
role: "system",
|
|
268
|
+
endedAt: now,
|
|
269
|
+
};
|
|
270
|
+
this.turns.set(seedId, { id: seedId, entry: seedEntry });
|
|
271
|
+
this.convIndex.set(childId, [seedId]);
|
|
272
|
+
|
|
273
|
+
// 4. Copy the parent's recall entries into the child's seed turn
|
|
274
|
+
const parentRecall = this.recall.filter(
|
|
275
|
+
(r) => r.turnId === parentTurnRow.id,
|
|
276
|
+
);
|
|
277
|
+
for (const r of parentRecall) {
|
|
278
|
+
this.recall.push({
|
|
279
|
+
turnId: seedId,
|
|
280
|
+
entry: {
|
|
281
|
+
turnId: String(seedId),
|
|
282
|
+
checkpointId: r.entry.checkpointId,
|
|
283
|
+
score: r.entry.score,
|
|
284
|
+
source: r.entry.source,
|
|
285
|
+
raptorLevel: r.entry.raptorLevel,
|
|
286
|
+
},
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return childId;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// ── TurnAdmin ───────────────────────────────────────────────
|
|
294
|
+
|
|
295
|
+
prune(policy: RetentionPolicy): PruneReport {
|
|
296
|
+
const cutoff = Date.now() - policy.maxTurnAgeMs;
|
|
297
|
+
let turnsRemoved = 0;
|
|
298
|
+
let recallRemoved = 0;
|
|
299
|
+
|
|
300
|
+
for (const [convId, turnIds] of this.convIndex.entries()) {
|
|
301
|
+
const total = turnIds.length;
|
|
302
|
+
const canDelete = Math.max(0, total - policy.keepMinPerConversation);
|
|
303
|
+
if (canDelete === 0) continue;
|
|
304
|
+
|
|
305
|
+
// Find old turns to delete
|
|
306
|
+
const oldIds = turnIds
|
|
307
|
+
.filter((id) => {
|
|
308
|
+
const row = this.turns.get(id);
|
|
309
|
+
return row && row.entry.endedAt < cutoff;
|
|
310
|
+
})
|
|
311
|
+
.slice(0, canDelete);
|
|
312
|
+
|
|
313
|
+
for (const id of oldIds) {
|
|
314
|
+
// Remove recall
|
|
315
|
+
const before = this.recall.length;
|
|
316
|
+
this.recall = this.recall.filter((r) => r.turnId !== id);
|
|
317
|
+
recallRemoved += before - this.recall.length;
|
|
318
|
+
|
|
319
|
+
this.turns.delete(id);
|
|
320
|
+
turnsRemoved++;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Update convIndex
|
|
324
|
+
this.convIndex.set(
|
|
325
|
+
convId,
|
|
326
|
+
turnIds.filter((id) => this.turns.has(id)),
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const branchesPreserved = this.forks.length;
|
|
331
|
+
|
|
332
|
+
return {
|
|
333
|
+
turnsRemoved,
|
|
334
|
+
recallRemoved,
|
|
335
|
+
branchesPreserved,
|
|
336
|
+
freedBytes: 0, // in-memory — no file size to measure
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
vacuum(): void {
|
|
341
|
+
// No-op for in-memory store
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
checkpoint(): StoreSnapshot {
|
|
345
|
+
// Emit turns in a stable order (by internal ID / insertion order)
|
|
346
|
+
const sorted = [...this.turns.values()].sort((a, b) => a.id - b.id);
|
|
347
|
+
const turns = sorted.map((r) => r.entry);
|
|
348
|
+
|
|
349
|
+
// Build mapping: internal ID → 1-based position
|
|
350
|
+
const idToPos = new Map<number, number>();
|
|
351
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
352
|
+
idToPos.set(sorted[i].id, i + 1);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Replace recall's turnId with the stable 1-based position
|
|
356
|
+
const recall = this.recall.map((r) => ({
|
|
357
|
+
turnId: String(idToPos.get(r.turnId) ?? r.turnId),
|
|
358
|
+
checkpointId: r.entry.checkpointId,
|
|
359
|
+
score: r.entry.score,
|
|
360
|
+
source: r.entry.source,
|
|
361
|
+
raptorLevel: r.entry.raptorLevel,
|
|
362
|
+
}));
|
|
363
|
+
|
|
364
|
+
return {
|
|
365
|
+
version: 1,
|
|
366
|
+
exportedAt: Date.now(),
|
|
367
|
+
turns,
|
|
368
|
+
recall,
|
|
369
|
+
forks: [...this.forks],
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
restore(from: StoreSnapshot): void {
|
|
374
|
+
this.clear();
|
|
375
|
+
|
|
376
|
+
// Map: 1-based position → new internal ID
|
|
377
|
+
const posToNewId = new Map<number, number>();
|
|
378
|
+
|
|
379
|
+
for (let i = 0; i < from.turns.length; i++) {
|
|
380
|
+
const t = from.turns[i];
|
|
381
|
+
const newId = allocId();
|
|
382
|
+
const sid = normalizeSessionId(t.sessionId);
|
|
383
|
+
const normalized = { ...t, sessionId: sid };
|
|
384
|
+
this.turns.set(newId, { id: newId, entry: normalized });
|
|
385
|
+
|
|
386
|
+
// Update indices
|
|
387
|
+
const convIds = this.convIndex.get(t.conversationId) ?? [];
|
|
388
|
+
convIds.push(newId);
|
|
389
|
+
this.convIndex.set(t.conversationId, convIds);
|
|
390
|
+
this.sessionConv.set(sid, t.conversationId);
|
|
391
|
+
|
|
392
|
+
posToNewId.set(i + 1, newId);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Re-attach recall with position-based TurnId → new internal ID mapping
|
|
396
|
+
for (const r of from.recall) {
|
|
397
|
+
const targetInternalId = posToNewId.get(Number(r.turnId));
|
|
398
|
+
if (targetInternalId !== undefined) {
|
|
399
|
+
this.recall.push({
|
|
400
|
+
turnId: targetInternalId,
|
|
401
|
+
entry: r,
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
this.forks = [...from.forks];
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
clear(): void {
|
|
410
|
+
this.turns.clear();
|
|
411
|
+
this.recall = [];
|
|
412
|
+
this.forks = [];
|
|
413
|
+
this.convIndex.clear();
|
|
414
|
+
this.sessionConv.clear();
|
|
415
|
+
resetIdCounter();
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
stampTurnsEpoch(sessionId: SessionId, epochId: string): number {
|
|
419
|
+
const sid = normalizeSessionId(sessionId);
|
|
420
|
+
let stamped = 0;
|
|
421
|
+
for (const row of this.turns.values()) {
|
|
422
|
+
if (row.epochId === undefined && row.entry.sessionId === sid) {
|
|
423
|
+
row.epochId = epochId;
|
|
424
|
+
stamped++;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return stamped;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// ── Capability gating ───────────────────────────────────────
|
|
431
|
+
|
|
432
|
+
asReader(): TurnReader {
|
|
433
|
+
return {
|
|
434
|
+
query: (f) => this.query(f),
|
|
435
|
+
getTurn: (id) => this.getTurn(id),
|
|
436
|
+
getTurnByIndex: (c, t) => this.getTurnByIndex(c, t),
|
|
437
|
+
listRecall: (id) => this.listRecall(id),
|
|
438
|
+
listRecallByIndex: (c, t) => this.listRecallByIndex(c, t),
|
|
439
|
+
listForks: (id) => this.listForks(id),
|
|
440
|
+
countTurns: (id) => this.countTurns(id),
|
|
441
|
+
conversationStats: (id) => this.conversationStats(id),
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
asWriter(): TurnWriter {
|
|
446
|
+
return {
|
|
447
|
+
appendTurn: (e) => this.appendTurn(e),
|
|
448
|
+
appendRecall: (e) => this.appendRecall(e),
|
|
449
|
+
ensureConversationId: (s) => this.ensureConversationId(s),
|
|
450
|
+
forkConversation: (p, t) => this.forkConversation(p, t),
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
asAdmin(): TurnAdmin {
|
|
455
|
+
return {
|
|
456
|
+
prune: (p) => this.prune(p),
|
|
457
|
+
vacuum: () => this.vacuum(),
|
|
458
|
+
checkpoint: () => this.checkpoint(),
|
|
459
|
+
restore: (s) => this.restore(s),
|
|
460
|
+
clear: () => this.clear(),
|
|
461
|
+
stampTurnsEpoch: (s, e) => this.stampTurnsEpoch(s, e),
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
close(): void {
|
|
466
|
+
// No-op for in-memory store
|
|
467
|
+
this.clear();
|
|
468
|
+
}
|
|
469
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* migrations.test.ts — S49B main-db → turns.db migration tests.
|
|
3
|
+
*
|
|
4
|
+
* Seeds a legacy main sqlite.db (with S48-era turn tables + session_state
|
|
5
|
+
* conversation pointers), then opens the turns store and asserts rows moved,
|
|
6
|
+
* legacy dropped, and the move is idempotent + flag-gated. No network.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { test, beforeEach, afterEach } from "node:test";
|
|
10
|
+
import assert from "node:assert/strict";
|
|
11
|
+
import { mkdtempSync, rmSync, mkdirSync } from "node:fs";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { DatabaseSync } from "node:sqlite";
|
|
15
|
+
import { openTurnStore, closeTurnStore } from "./connection.js";
|
|
16
|
+
import { TurnsConfig } from "../../config/turns.js";
|
|
17
|
+
|
|
18
|
+
let tmpDir: string;
|
|
19
|
+
let counter = 0;
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
tmpDir = mkdtempSync(join(tmpdir(), "mc-migrate-"));
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
function stateDir(): string {
|
|
30
|
+
return join(tmpDir, `run-${counter++}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Seed a legacy main db with S48-era turn tables + a conversation pointer. */
|
|
34
|
+
function seedLegacyMainDb(dir: string): void {
|
|
35
|
+
mkdirSync(dir, { recursive: true });
|
|
36
|
+
const db = new DatabaseSync(join(dir, "sqlite.db"));
|
|
37
|
+
db.exec(`
|
|
38
|
+
CREATE TABLE turns (
|
|
39
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT, conversation_id TEXT NOT NULL,
|
|
40
|
+
session_id TEXT NOT NULL, turn_index INTEGER NOT NULL, role TEXT,
|
|
41
|
+
started_at INTEGER NOT NULL, ended_at INTEGER, ctx_tokens INTEGER,
|
|
42
|
+
ctx_percent REAL, pressure_band TEXT, model_id TEXT, epoch_id TEXT,
|
|
43
|
+
UNIQUE(session_id, turn_index)
|
|
44
|
+
);
|
|
45
|
+
CREATE TABLE turn_recall (
|
|
46
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT, turn_id INTEGER NOT NULL,
|
|
47
|
+
checkpoint_id TEXT NOT NULL, score REAL NOT NULL, source TEXT NOT NULL,
|
|
48
|
+
raptor_level INTEGER, UNIQUE(turn_id, checkpoint_id)
|
|
49
|
+
);
|
|
50
|
+
CREATE TABLE conversation_branches (
|
|
51
|
+
conversation_id TEXT PRIMARY KEY, parent_conversation_id TEXT NOT NULL,
|
|
52
|
+
fork_turn_id INTEGER NOT NULL, created_at INTEGER NOT NULL
|
|
53
|
+
);
|
|
54
|
+
CREATE TABLE session_state (
|
|
55
|
+
session_id TEXT PRIMARY KEY, injected_checkpoint_ids TEXT,
|
|
56
|
+
stored_region_hashes TEXT, conversation_id TEXT, last_turn_id INTEGER
|
|
57
|
+
);
|
|
58
|
+
INSERT INTO turns (conversation_id, session_id, turn_index, started_at, ended_at, ctx_tokens)
|
|
59
|
+
VALUES ('conv_leg', 'sess_leg', 0, 100, 200, 500);
|
|
60
|
+
INSERT INTO turn_recall (turn_id, checkpoint_id, score, source)
|
|
61
|
+
VALUES (1, 'cp_leg', 0.6, 'flat');
|
|
62
|
+
INSERT INTO conversation_branches (conversation_id, parent_conversation_id, fork_turn_id, created_at)
|
|
63
|
+
VALUES ('conv_child', 'conv_leg', 1, 300);
|
|
64
|
+
INSERT INTO session_state (session_id, conversation_id) VALUES ('sess_leg', 'conv_leg');
|
|
65
|
+
`);
|
|
66
|
+
db.close();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function tableExists(db: DatabaseSync, name: string): boolean {
|
|
70
|
+
return (
|
|
71
|
+
db
|
|
72
|
+
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name=?")
|
|
73
|
+
.get(name) !== undefined
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
test("fresh dir (no main db) → no-op, marks migrated", () => {
|
|
78
|
+
const dir = stateDir();
|
|
79
|
+
const db = openTurnStore(dir);
|
|
80
|
+
// openTurnStore ran the migration hook; with no sqlite.db it should just mark.
|
|
81
|
+
const row = db
|
|
82
|
+
.prepare("SELECT value FROM turns_meta WHERE key='migrated_from_main'")
|
|
83
|
+
.get() as { value: string } | undefined;
|
|
84
|
+
assert.equal(row?.value, "1");
|
|
85
|
+
closeTurnStore(dir);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("legacy rows moved, legacy tables dropped, conversation pointer migrated", () => {
|
|
89
|
+
const dir = stateDir();
|
|
90
|
+
seedLegacyMainDb(dir);
|
|
91
|
+
const db = openTurnStore(dir); // triggers migration
|
|
92
|
+
|
|
93
|
+
// Rows present in turns.db.
|
|
94
|
+
const turns = db.prepare("SELECT * FROM turns").all() as Array<
|
|
95
|
+
Record<string, unknown>
|
|
96
|
+
>;
|
|
97
|
+
assert.equal(turns.length, 1);
|
|
98
|
+
assert.equal(turns[0].conversation_id, "conv_leg");
|
|
99
|
+
const recall = db.prepare("SELECT * FROM turn_recall").all() as Array<
|
|
100
|
+
Record<string, unknown>
|
|
101
|
+
>;
|
|
102
|
+
assert.equal(recall.length, 1);
|
|
103
|
+
assert.equal(recall[0].checkpoint_id, "cp_leg");
|
|
104
|
+
const branches = db
|
|
105
|
+
.prepare("SELECT * FROM conversation_forks")
|
|
106
|
+
.all() as Array<Record<string, unknown>>;
|
|
107
|
+
assert.equal(branches.length, 1);
|
|
108
|
+
assert.equal(branches[0].parent_conversation_id, "conv_leg");
|
|
109
|
+
assert.equal(branches[0].child_conversation_id, "conv_child");
|
|
110
|
+
assert.equal(branches[0].fork_turn_index, 1);
|
|
111
|
+
// Conversation pointer migrated to session_conversations (session_id → conversation_id).
|
|
112
|
+
const ptr = db
|
|
113
|
+
.prepare(
|
|
114
|
+
"SELECT conversation_id FROM session_conversations WHERE session_id='sess_leg'",
|
|
115
|
+
)
|
|
116
|
+
.get() as { conversation_id: string } | undefined;
|
|
117
|
+
assert.equal(ptr?.conversation_id, "conv_leg");
|
|
118
|
+
|
|
119
|
+
closeTurnStore(dir);
|
|
120
|
+
|
|
121
|
+
// Legacy tables dropped from the main db.
|
|
122
|
+
const main = new DatabaseSync(join(dir, "sqlite.db"));
|
|
123
|
+
assert.ok(!tableExists(main, "turns"));
|
|
124
|
+
assert.ok(!tableExists(main, "turn_recall"));
|
|
125
|
+
assert.ok(!tableExists(main, "conversation_branches"));
|
|
126
|
+
assert.ok(tableExists(main, "session_state")); // left intact
|
|
127
|
+
main.close();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("idempotent: re-open does not re-copy or re-drop", () => {
|
|
131
|
+
const dir = stateDir();
|
|
132
|
+
seedLegacyMainDb(dir);
|
|
133
|
+
openTurnStore(dir);
|
|
134
|
+
closeTurnStore(dir);
|
|
135
|
+
// Second open — marker set, should be a no-op even though main db now lacks turn tables.
|
|
136
|
+
const db = openTurnStore(dir);
|
|
137
|
+
const turns = db.prepare("SELECT * FROM turns").all() as Array<
|
|
138
|
+
Record<string, unknown>
|
|
139
|
+
>;
|
|
140
|
+
assert.equal(turns.length, 1); // still exactly the migrated row, not duplicated
|
|
141
|
+
closeTurnStore(dir);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("flag OFF → legacy tables untouched, no marker write from hook", () => {
|
|
145
|
+
const dir = stateDir();
|
|
146
|
+
seedLegacyMainDb(dir);
|
|
147
|
+
const original = TurnsConfig.TURNS_DB_ENABLED;
|
|
148
|
+
TurnsConfig.TURNS_DB_ENABLED = false;
|
|
149
|
+
try {
|
|
150
|
+
const db = openTurnStore(dir);
|
|
151
|
+
closeTurnStore(dir);
|
|
152
|
+
void db;
|
|
153
|
+
} finally {
|
|
154
|
+
TurnsConfig.TURNS_DB_ENABLED = original;
|
|
155
|
+
}
|
|
156
|
+
// Main db turn tables must be intact (migration hook was gated off).
|
|
157
|
+
const main = new DatabaseSync(join(dir, "sqlite.db"));
|
|
158
|
+
assert.ok(tableExists(main, "turns"));
|
|
159
|
+
assert.ok(tableExists(main, "conversation_branches"));
|
|
160
|
+
main.close();
|
|
161
|
+
});
|