@cortexkit/pi-magic-context 0.46.1 → 0.48.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/dist/auto-search-worker.js +112 -0
- package/dist/embedding-activity-ew6dshfd.js +10 -0
- package/dist/embedding-backfill-bnnytehy.js +20 -0
- package/dist/embedding-cache-7rp4gqzr.js +16 -0
- package/dist/embedding-worker.js +11 -8
- package/dist/harness-9872e0ym.js +10 -0
- package/dist/historian-calibration-extension.js +3 -2
- package/dist/index-0am8a7k1.js +152 -0
- package/dist/index-0h2mkab2.js +17 -0
- package/dist/index-135e8hvg.js +43 -0
- package/dist/{index-wv28nczk.js → index-3cgbv47m.js} +354 -38
- package/dist/index-419bndtm.js +2309 -0
- package/dist/index-544c0e0x.js +3026 -0
- package/dist/index-5hd7h4rr.js +3098 -0
- package/dist/index-5jp1h0bz.js +750 -0
- package/dist/index-603mt6nm.js +171 -0
- package/dist/index-674f6qs2.js +16 -0
- package/dist/{index-r01b1b78.js → index-6pgq5ns2.js} +157 -2454
- package/dist/index-72bdc1bd.js +3591 -0
- package/dist/index-8976mezv.js +209 -0
- package/dist/{index-td885aah.js → index-8y5knskr.js} +98 -231
- package/dist/{index-ykb8kkm3.js → index-98z1c1r0.js} +14599 -22287
- package/dist/index-9p40dgkm.js +22 -0
- package/dist/index-9x1f96r8.js +11 -0
- package/dist/{index-266qrgss.js → index-9xb0axhc.js} +301 -27
- package/dist/{index-phh7e055.js → index-bgmv2kaf.js} +28 -28
- package/dist/index-dkx0b1kt.js +514 -0
- package/dist/{index-7n8gb6j3.js → index-f956htf3.js} +28 -31
- package/dist/index-fapenpch.js +13 -0
- package/dist/index-fm3ksh5e.js +1060 -0
- package/dist/{index-2p1e8706.js → index-fp37he9a.js} +3 -3
- package/dist/index-hrf13mn3.js +2388 -0
- package/dist/index-pvm4pd5d.js +3069 -0
- package/dist/index-r7xg4hvw.js +3480 -0
- package/dist/index-rt5jgzp1.js +1127 -0
- package/dist/{index-n3g5mwa8.js → index-wrm4nbj7.js} +3 -3
- package/dist/index-xjqheg7a.js +931 -0
- package/dist/index-yhzhdp3m.js +143 -0
- package/dist/{index-w76x3af7.js → index-zrr5t5a7.js} +1 -3
- package/dist/index.js +6345 -2585
- package/dist/logger-v3dsxdnm.js +19 -0
- package/dist/message-index-bounded-t7vvty76.js +120 -0
- package/dist/{message-time-backfill-4688z1k2.js → message-time-backfill-4ndf7hrq.js} +7 -6
- package/dist/migration-worker.js +24 -16
- package/dist/project-embedding-registry-jy2hmq28.js +85 -0
- package/dist/raw-ordinal-warmup-worker.js +60 -0
- package/dist/{read-session-chunk-tc6yn2pe.js → read-session-chunk-47pjn7fz.js} +12 -6
- package/dist/read-session-raw-yn8nawav.js +55 -0
- package/dist/{safe-notification-target-6cn3z99t.js → safe-notification-target-3c1ns931.js} +3 -2
- package/dist/search-42szfhwp.js +40 -0
- package/dist/sqlite-3yye76ht.js +44 -0
- package/dist/sqlite-context-pragmas-gcgch6qz.js +6 -0
- package/dist/storage-memory-1z20y55x.js +71 -0
- package/dist/storage-tags-jym5z6yk.js +127 -0
- package/dist/subagent-entry.js +36 -16
- package/dist/transformers-node-wasm.js +18415 -18415
- package/package.json +3 -3
- package/dist/index-4vfrjwf6.js +0 -7737
- package/dist/index-xvw8zptx.js +0 -5123
- /package/dist/{index-901ekbga.js → index-5prbx026.js} +0 -0
|
@@ -0,0 +1,3591 @@
|
|
|
1
|
+
import {
|
|
2
|
+
V2_MEMORY_CATEGORIES
|
|
3
|
+
} from "./index-135e8hvg.js";
|
|
4
|
+
import {
|
|
5
|
+
log2
|
|
6
|
+
} from "./index-f956htf3.js";
|
|
7
|
+
import {
|
|
8
|
+
ensureColumn,
|
|
9
|
+
healAllNullColumns,
|
|
10
|
+
installV95PerfSchema
|
|
11
|
+
} from "./index-8y5knskr.js";
|
|
12
|
+
import {
|
|
13
|
+
resolveOpenCodeDbPath,
|
|
14
|
+
openCodeDbPathExists,
|
|
15
|
+
getOpenCodeDbProbeDescriptions
|
|
16
|
+
} from "./index-8976mezv.js";
|
|
17
|
+
import {
|
|
18
|
+
Database2
|
|
19
|
+
} from "./index-3cgbv47m.js";
|
|
20
|
+
import {
|
|
21
|
+
normalizeStoredProjectPath,
|
|
22
|
+
storedPathBelongsToIdentity
|
|
23
|
+
} from "./index-dkx0b1kt.js";
|
|
24
|
+
import {
|
|
25
|
+
logSlowWriteTransaction
|
|
26
|
+
} from "./index-fp37he9a.js";
|
|
27
|
+
|
|
28
|
+
// ../plugin/src/features/magic-context/migrations.ts
|
|
29
|
+
import { isMainThread } from "node:worker_threads";
|
|
30
|
+
|
|
31
|
+
// ../plugin/src/shared/xml-unescape.ts
|
|
32
|
+
var XML_ENTITY_REGEX = /&(amp|apos|quot|lt|gt);/g;
|
|
33
|
+
var XML_ENTITIES = {
|
|
34
|
+
amp: "&",
|
|
35
|
+
apos: "'",
|
|
36
|
+
quot: '"',
|
|
37
|
+
lt: "<",
|
|
38
|
+
gt: ">"
|
|
39
|
+
};
|
|
40
|
+
function unescapeXml(s) {
|
|
41
|
+
return s.replace(XML_ENTITY_REGEX, (entity, name) => XML_ENTITIES[name] ?? entity);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ../plugin/src/hooks/magic-context/compartment-parser.ts
|
|
45
|
+
var COMPARTMENT_OPEN_REGEX = /<compartment\s+((?:[^>"]|"[^"\n]*")*?)\s*>/g;
|
|
46
|
+
var COMPARTMENT_CLOSE_TAG = "</compartment>";
|
|
47
|
+
var COMPARTMENT_CLOSE_FOLLOWER_REGEX = /^\s*(?:$|<(?:compartment\s|\/compartments\s*>|\/output\s*>|(?:facts|events|meta|user_observations|primer_candidates|unprocessed_from)\s*>))/;
|
|
48
|
+
var ATTR_START_REGEX = /\bstart="(\d+)"/;
|
|
49
|
+
var ATTR_END_REGEX = /\bend="(\d+)"/;
|
|
50
|
+
var ATTR_TITLE_REGEX = /\btitle="([^"]*)"/;
|
|
51
|
+
var ATTR_EPISODE_REGEX = /\bepisode_type="([^"]*)"/;
|
|
52
|
+
var ATTR_IMPORTANCE_REGEX = /\bimportance="(\d+)"/;
|
|
53
|
+
var TIER_OPEN_REGEX = /<p(\d)\s*(\/?)>/g;
|
|
54
|
+
var TIER_CLOSE_REGEX = /<\/p(\d)\s*>/g;
|
|
55
|
+
var TIER_CLOSE_FOLLOWER_REGEX = /^\s*(?:$|<\/?p\d\s*\/?>)/;
|
|
56
|
+
var TIER_CLOSE_ANY_REGEX = /<\/p\d/;
|
|
57
|
+
var TIER_OPEN_ANY_REGEX = /<p\d/;
|
|
58
|
+
var CATEGORY_BLOCK_REGEX = /<([A-Za-z_][A-Za-z0-9_-]*)>(.*?)<\/\1>/gs;
|
|
59
|
+
var HISTORIAN_CATEGORIES = new Set(V2_MEMORY_CATEGORIES);
|
|
60
|
+
var FACT_ITEM_REGEX = /^\s*\*\s*(.+)$/gm;
|
|
61
|
+
var UNPROCESSED_REGEX = /<unprocessed_from>(\d+)<\/unprocessed_from>/;
|
|
62
|
+
var USER_OBSERVATIONS_REGEX = /<user_observations>(.*?)<\/user_observations>/s;
|
|
63
|
+
var USER_OBS_ITEM_REGEX = /^\s*\*\s*(.+)$/gm;
|
|
64
|
+
var PRIMER_CANDIDATES_REGEX = /<primer_candidates>(.*?)<\/primer_candidates>/s;
|
|
65
|
+
var PRIMER_ELEMENT_REGEX = /<primer\s+at_compartment="(\d+)"\s*>(.*?)<\/primer>/gs;
|
|
66
|
+
var PRIMER_ITEM_REGEX = /^\s*(?:\*|-|\d+\.)\s*(.+)$/gm;
|
|
67
|
+
var FACTS_BLOCK_REGEX = /<facts>(.*?)<\/facts>/s;
|
|
68
|
+
var EVENTS_BLOCK_REGEX = /<events>(.*?)<\/events>/s;
|
|
69
|
+
var EVENT_ELEMENT_REGEX = /<([a-z_]+)\s+at_compartment="(\d+)"\s*>(.*?)<\/\1>/gs;
|
|
70
|
+
var EVENT_FIELD_REGEX = /<([a-z_]+)\s*>(.*?)<\/\1>/gs;
|
|
71
|
+
function searchFrom(s, regex, from) {
|
|
72
|
+
const at = s.slice(from).search(regex);
|
|
73
|
+
return at === -1 ? -1 : from + at;
|
|
74
|
+
}
|
|
75
|
+
function hasTierOpenerFrom(inner, digit, from) {
|
|
76
|
+
return new RegExp(`<p${digit}\\s*/?>`).test(inner.slice(from));
|
|
77
|
+
}
|
|
78
|
+
function findMatchingTierClose(inner, bodyStart, digit, found) {
|
|
79
|
+
const closeRegex = new RegExp(TIER_CLOSE_REGEX.source, "g");
|
|
80
|
+
closeRegex.lastIndex = bodyStart;
|
|
81
|
+
for (let close = closeRegex.exec(inner);close; close = closeRegex.exec(inner)) {
|
|
82
|
+
const end = close.index + close[0].length;
|
|
83
|
+
if (Number(close[1]) !== digit)
|
|
84
|
+
continue;
|
|
85
|
+
if (!TIER_CLOSE_FOLLOWER_REGEX.test(inner.slice(end)))
|
|
86
|
+
continue;
|
|
87
|
+
const body = inner.slice(bodyStart, close.index);
|
|
88
|
+
for (const open of body.matchAll(new RegExp(TIER_OPEN_REGEX.source, "g"))) {
|
|
89
|
+
const other = Number(open[1]);
|
|
90
|
+
const otherwiseMissing = other >= 1 && other <= 4 && other !== digit && !found.has(other) && !hasTierOpenerFrom(inner, other, end);
|
|
91
|
+
if (otherwiseMissing)
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
return { start: close.index, end };
|
|
95
|
+
}
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
function extractTiers(inner) {
|
|
99
|
+
const tiers = new Map;
|
|
100
|
+
const openRegex = new RegExp(TIER_OPEN_REGEX.source, "g");
|
|
101
|
+
let pos = 0;
|
|
102
|
+
for (;; ) {
|
|
103
|
+
openRegex.lastIndex = pos;
|
|
104
|
+
const open = openRegex.exec(inner);
|
|
105
|
+
if (!open)
|
|
106
|
+
break;
|
|
107
|
+
const digit = Number(open[1]);
|
|
108
|
+
const bodyStart = open.index + open[0].length;
|
|
109
|
+
let bodyEnd = bodyStart;
|
|
110
|
+
pos = bodyStart;
|
|
111
|
+
if (open[2] !== "/") {
|
|
112
|
+
const close = findMatchingTierClose(inner, bodyStart, digit, tiers);
|
|
113
|
+
if (close) {
|
|
114
|
+
bodyEnd = close.start;
|
|
115
|
+
pos = close.end;
|
|
116
|
+
} else {
|
|
117
|
+
const closeAt = searchFrom(inner, TIER_CLOSE_ANY_REGEX, bodyStart);
|
|
118
|
+
bodyEnd = closeAt === -1 ? inner.length : closeAt;
|
|
119
|
+
pos = closeAt === -1 ? inner.length : closeAt + 1;
|
|
120
|
+
const openInside = searchFrom(inner.slice(0, bodyEnd), TIER_OPEN_ANY_REGEX, bodyStart);
|
|
121
|
+
if (openInside !== -1) {
|
|
122
|
+
bodyEnd = openInside;
|
|
123
|
+
pos = openInside;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (!tiers.has(digit)) {
|
|
128
|
+
tiers.set(digit, unescapeXml(inner.slice(bodyStart, bodyEnd).trim()));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return tiers;
|
|
132
|
+
}
|
|
133
|
+
function extractTiersFromInner(inner) {
|
|
134
|
+
const tiers = extractTiers(inner);
|
|
135
|
+
return { p1: tiers.get(1), p2: tiers.get(2), p3: tiers.get(3), p4: tiers.get(4) };
|
|
136
|
+
}
|
|
137
|
+
function findCompartmentElements(text) {
|
|
138
|
+
const elements = [];
|
|
139
|
+
const openRegex = new RegExp(COMPARTMENT_OPEN_REGEX.source, "g");
|
|
140
|
+
let cursor = 0;
|
|
141
|
+
for (;; ) {
|
|
142
|
+
openRegex.lastIndex = cursor;
|
|
143
|
+
const open = openRegex.exec(text);
|
|
144
|
+
if (!open)
|
|
145
|
+
break;
|
|
146
|
+
const bodyStart = open.index + open[0].length;
|
|
147
|
+
const nextOpen = openRegex.exec(text);
|
|
148
|
+
const windowEnd = nextOpen ? nextOpen.index : text.length;
|
|
149
|
+
const firstClose = text.indexOf(COMPARTMENT_CLOSE_TAG, bodyStart);
|
|
150
|
+
if (firstClose === -1)
|
|
151
|
+
break;
|
|
152
|
+
let closeAt = firstClose;
|
|
153
|
+
for (let at = firstClose;at !== -1 && at < windowEnd; at = text.indexOf(COMPARTMENT_CLOSE_TAG, at + 1)) {
|
|
154
|
+
if (COMPARTMENT_CLOSE_FOLLOWER_REGEX.test(text.slice(at + COMPARTMENT_CLOSE_TAG.length))) {
|
|
155
|
+
closeAt = at;
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const end = closeAt + COMPARTMENT_CLOSE_TAG.length;
|
|
160
|
+
elements.push({
|
|
161
|
+
start: open.index,
|
|
162
|
+
end,
|
|
163
|
+
attrs: open[1],
|
|
164
|
+
inner: text.slice(bodyStart, closeAt)
|
|
165
|
+
});
|
|
166
|
+
cursor = end;
|
|
167
|
+
}
|
|
168
|
+
return elements;
|
|
169
|
+
}
|
|
170
|
+
function textOutsideCompartments(text, elements) {
|
|
171
|
+
let outside = "";
|
|
172
|
+
let last = 0;
|
|
173
|
+
for (const element of elements) {
|
|
174
|
+
outside += text.slice(last, element.start);
|
|
175
|
+
last = element.end;
|
|
176
|
+
}
|
|
177
|
+
return outside + text.slice(last);
|
|
178
|
+
}
|
|
179
|
+
function parseCompartmentOutput(text) {
|
|
180
|
+
const compartments = [];
|
|
181
|
+
const facts = [];
|
|
182
|
+
let droppedFactBlocks = 0;
|
|
183
|
+
let droppedFacts = 0;
|
|
184
|
+
const elements = findCompartmentElements(text);
|
|
185
|
+
for (const { attrs, inner } of elements) {
|
|
186
|
+
const startMatch = attrs.match(ATTR_START_REGEX);
|
|
187
|
+
const endMatch = attrs.match(ATTR_END_REGEX);
|
|
188
|
+
const titleMatch = attrs.match(ATTR_TITLE_REGEX);
|
|
189
|
+
if (!startMatch || !endMatch || !titleMatch)
|
|
190
|
+
continue;
|
|
191
|
+
const startMessage = parseInt(startMatch[1], 10);
|
|
192
|
+
const endMessage = parseInt(endMatch[1], 10);
|
|
193
|
+
const title = unescapeXml(titleMatch[1]);
|
|
194
|
+
if (Number.isNaN(startMessage) || Number.isNaN(endMessage) || !title)
|
|
195
|
+
continue;
|
|
196
|
+
const episodeMatch = attrs.match(ATTR_EPISODE_REGEX);
|
|
197
|
+
const importanceMatch = attrs.match(ATTR_IMPORTANCE_REGEX);
|
|
198
|
+
const episodeType = episodeMatch ? unescapeXml(episodeMatch[1]) : undefined;
|
|
199
|
+
const importance = importanceMatch ? parseInt(importanceMatch[1], 10) : undefined;
|
|
200
|
+
const { p1, p2, p3, p4 } = extractTiersFromInner(inner);
|
|
201
|
+
if (typeof p1 === "string" && p1.length > 0) {
|
|
202
|
+
compartments.push({
|
|
203
|
+
startMessage,
|
|
204
|
+
endMessage,
|
|
205
|
+
title,
|
|
206
|
+
content: p1,
|
|
207
|
+
p1,
|
|
208
|
+
p2: typeof p2 === "string" ? p2 : p1,
|
|
209
|
+
p3: typeof p3 === "string" ? p3 : typeof p2 === "string" ? p2 : p1,
|
|
210
|
+
p4: typeof p4 === "string" ? p4 : "",
|
|
211
|
+
importance,
|
|
212
|
+
episodeType
|
|
213
|
+
});
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
const content = unescapeXml(inner.trim());
|
|
217
|
+
if (content) {
|
|
218
|
+
compartments.push({
|
|
219
|
+
startMessage,
|
|
220
|
+
endMessage,
|
|
221
|
+
title,
|
|
222
|
+
content,
|
|
223
|
+
importance,
|
|
224
|
+
episodeType
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
const outside = textOutsideCompartments(text, elements);
|
|
229
|
+
const factsBlockMatch = outside.match(FACTS_BLOCK_REGEX);
|
|
230
|
+
const factsScope = factsBlockMatch ? factsBlockMatch[1] : outside.replace(EVENTS_BLOCK_REGEX, "").replace(/<(meta|user_observations|primer_candidates)>.*?<\/\1>/gs, "").replace(/<\/?(?:output|compartments)>/g, "");
|
|
231
|
+
for (const categoryMatch of factsScope.matchAll(CATEGORY_BLOCK_REGEX)) {
|
|
232
|
+
const category = categoryMatch[1];
|
|
233
|
+
const blockContent = categoryMatch[2];
|
|
234
|
+
const items = [...blockContent.matchAll(FACT_ITEM_REGEX)].map((match) => unescapeXml(match[1].trim())).filter(Boolean);
|
|
235
|
+
if (!HISTORIAN_CATEGORIES.has(category)) {
|
|
236
|
+
if (items.length === 0)
|
|
237
|
+
continue;
|
|
238
|
+
droppedFactBlocks++;
|
|
239
|
+
droppedFacts += items.length;
|
|
240
|
+
log2(`[historian] Dropped <facts> category ${category} (${items.length} facts)`);
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
for (const content of items)
|
|
244
|
+
facts.push({ category, content });
|
|
245
|
+
}
|
|
246
|
+
const unprocessedMatch = outside.match(UNPROCESSED_REGEX);
|
|
247
|
+
const unprocessedFrom = unprocessedMatch ? parseInt(unprocessedMatch[1], 10) : null;
|
|
248
|
+
const userObservations = [];
|
|
249
|
+
const userObsMatch = outside.match(USER_OBSERVATIONS_REGEX);
|
|
250
|
+
if (userObsMatch) {
|
|
251
|
+
for (const itemMatch of userObsMatch[1].matchAll(USER_OBS_ITEM_REGEX)) {
|
|
252
|
+
const obs = unescapeXml(itemMatch[1].trim());
|
|
253
|
+
if (obs)
|
|
254
|
+
userObservations.push(obs);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
const primerCandidates = [];
|
|
258
|
+
const primerMatch = outside.match(PRIMER_CANDIDATES_REGEX);
|
|
259
|
+
if (primerMatch) {
|
|
260
|
+
const block = primerMatch[1];
|
|
261
|
+
let sawElement = false;
|
|
262
|
+
for (const el of block.matchAll(PRIMER_ELEMENT_REGEX)) {
|
|
263
|
+
sawElement = true;
|
|
264
|
+
const question = unescapeXml(el[2].trim());
|
|
265
|
+
if (question) {
|
|
266
|
+
primerCandidates.push({
|
|
267
|
+
question,
|
|
268
|
+
originCompartmentIndex: Number.parseInt(el[1], 10)
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
if (!sawElement) {
|
|
273
|
+
for (const itemMatch of block.matchAll(PRIMER_ITEM_REGEX)) {
|
|
274
|
+
const question = unescapeXml(itemMatch[1].trim());
|
|
275
|
+
if (question)
|
|
276
|
+
primerCandidates.push({ question });
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
const events = parseEvents(outside);
|
|
281
|
+
const sortedByStart = compartments.map((compartment, emitted) => ({ compartment, emitted })).sort((a, b) => a.compartment.startMessage - b.compartment.startMessage);
|
|
282
|
+
const sortedPosition = [];
|
|
283
|
+
sortedByStart.forEach(({ emitted }, sorted) => {
|
|
284
|
+
sortedPosition[emitted] = sorted + 1;
|
|
285
|
+
});
|
|
286
|
+
const remapAnchor = (anchor) => anchor >= 1 && anchor <= sortedPosition.length ? sortedPosition[anchor - 1] : anchor;
|
|
287
|
+
return {
|
|
288
|
+
compartments: sortedByStart.map(({ compartment }) => compartment),
|
|
289
|
+
facts,
|
|
290
|
+
droppedFactBlocks,
|
|
291
|
+
droppedFacts,
|
|
292
|
+
events: events.map((event) => event.atCompartment === null ? event : { ...event, atCompartment: remapAnchor(event.atCompartment) }),
|
|
293
|
+
unprocessedFrom,
|
|
294
|
+
userObservations,
|
|
295
|
+
primerCandidates: primerCandidates.map((candidate) => candidate.originCompartmentIndex === undefined ? candidate : {
|
|
296
|
+
...candidate,
|
|
297
|
+
originCompartmentIndex: remapAnchor(candidate.originCompartmentIndex)
|
|
298
|
+
})
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
function parseEvents(text) {
|
|
302
|
+
const blockMatch = text.match(EVENTS_BLOCK_REGEX);
|
|
303
|
+
if (!blockMatch)
|
|
304
|
+
return [];
|
|
305
|
+
const block = blockMatch[1];
|
|
306
|
+
const events = [];
|
|
307
|
+
for (const elMatch of block.matchAll(EVENT_ELEMENT_REGEX)) {
|
|
308
|
+
const kind = elMatch[1];
|
|
309
|
+
const atRaw = parseInt(elMatch[2], 10);
|
|
310
|
+
const atCompartment = Number.isNaN(atRaw) ? null : atRaw;
|
|
311
|
+
const fields = {};
|
|
312
|
+
for (const fieldMatch of elMatch[3].matchAll(EVENT_FIELD_REGEX)) {
|
|
313
|
+
const name = fieldMatch[1];
|
|
314
|
+
const value = unescapeXml(fieldMatch[2].trim());
|
|
315
|
+
if (value)
|
|
316
|
+
fields[name] = value;
|
|
317
|
+
}
|
|
318
|
+
events.push({ kind, atCompartment, fields });
|
|
319
|
+
}
|
|
320
|
+
return events;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// ../plugin/src/features/magic-context/lkg-prefix-chunks.ts
|
|
324
|
+
import { createHash } from "node:crypto";
|
|
325
|
+
var LKG_PREFIX_CHUNK_CHARS = 64 * 1024;
|
|
326
|
+
function splitLkgPrefix(jsonPrefix) {
|
|
327
|
+
const chunks = [];
|
|
328
|
+
let start = 0;
|
|
329
|
+
while (start < jsonPrefix.length) {
|
|
330
|
+
let end = Math.min(start + LKG_PREFIX_CHUNK_CHARS, jsonPrefix.length);
|
|
331
|
+
if (end < jsonPrefix.length) {
|
|
332
|
+
const last = jsonPrefix.charCodeAt(end - 1);
|
|
333
|
+
if (last >= 55296 && last <= 56319)
|
|
334
|
+
end -= 1;
|
|
335
|
+
}
|
|
336
|
+
chunks.push(jsonPrefix.slice(start, end));
|
|
337
|
+
start = end;
|
|
338
|
+
}
|
|
339
|
+
return chunks;
|
|
340
|
+
}
|
|
341
|
+
function hashLkgChunk(body) {
|
|
342
|
+
return createHash("sha256").update(body).digest("hex");
|
|
343
|
+
}
|
|
344
|
+
function hashLkgChunkList(chunkHashes) {
|
|
345
|
+
return createHash("sha256").update(chunkHashes.join(`
|
|
346
|
+
`)).digest("hex");
|
|
347
|
+
}
|
|
348
|
+
function layoutLkgPrefix(jsonPrefix) {
|
|
349
|
+
const chunks = splitLkgPrefix(jsonPrefix);
|
|
350
|
+
const chunkHashes = chunks.map(hashLkgChunk);
|
|
351
|
+
return { chunks, chunkHashes, chars: jsonPrefix.length, hash: hashLkgChunkList(chunkHashes) };
|
|
352
|
+
}
|
|
353
|
+
function assembleLkgPrefix(expected, rows) {
|
|
354
|
+
const { chars, chunks, hash } = expected;
|
|
355
|
+
if (typeof chars !== "number" || !Number.isSafeInteger(chars) || typeof chunks !== "number" || !Number.isSafeInteger(chunks) || typeof hash !== "string" || rows.length !== chunks) {
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
const bodies = [];
|
|
359
|
+
let length = 0;
|
|
360
|
+
for (let index = 0;index < rows.length; index += 1) {
|
|
361
|
+
const row = rows[index];
|
|
362
|
+
if (row?.chunk !== index || typeof row.body !== "string")
|
|
363
|
+
return;
|
|
364
|
+
bodies.push(row.body);
|
|
365
|
+
length += row.body.length;
|
|
366
|
+
}
|
|
367
|
+
if (length !== chars)
|
|
368
|
+
return;
|
|
369
|
+
const chunkHashes = bodies.map(hashLkgChunk);
|
|
370
|
+
if (chunkHashes.some((chunkHash, index) => rows[index]?.hash !== chunkHash))
|
|
371
|
+
return;
|
|
372
|
+
if (hashLkgChunkList(chunkHashes) !== hash)
|
|
373
|
+
return;
|
|
374
|
+
return { jsonPrefix: bodies.join(""), chunkHashes, hash };
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// ../plugin/src/features/magic-context/migration-v94-write-split.ts
|
|
378
|
+
var LKG_SLOT_CHUNKS_DDL = `
|
|
379
|
+
CREATE TABLE IF NOT EXISTS lkg_slot_chunks (
|
|
380
|
+
session_id TEXT NOT NULL,
|
|
381
|
+
chunk INTEGER NOT NULL,
|
|
382
|
+
hash TEXT NOT NULL,
|
|
383
|
+
body TEXT NOT NULL,
|
|
384
|
+
PRIMARY KEY (session_id, chunk)
|
|
385
|
+
);
|
|
386
|
+
`;
|
|
387
|
+
var SESSION_REPLAY_DECISIONS_DDL = `
|
|
388
|
+
CREATE TABLE IF NOT EXISTS session_replay_decisions (
|
|
389
|
+
session_id TEXT NOT NULL,
|
|
390
|
+
message_id TEXT NOT NULL,
|
|
391
|
+
decision TEXT NOT NULL,
|
|
392
|
+
PRIMARY KEY (session_id, message_id)
|
|
393
|
+
) WITHOUT ROWID;
|
|
394
|
+
`;
|
|
395
|
+
function lkgSlotsDdl(table) {
|
|
396
|
+
return `
|
|
397
|
+
CREATE TABLE IF NOT EXISTS ${table} (
|
|
398
|
+
session_id TEXT PRIMARY KEY,
|
|
399
|
+
json_prefix_chars INTEGER NOT NULL,
|
|
400
|
+
json_prefix_chunks INTEGER NOT NULL,
|
|
401
|
+
json_prefix_hash TEXT NOT NULL,
|
|
402
|
+
input_id_seq TEXT NOT NULL,
|
|
403
|
+
input_content_digests TEXT NOT NULL,
|
|
404
|
+
input_content_signatures TEXT,
|
|
405
|
+
last_input_message_id TEXT NOT NULL,
|
|
406
|
+
model_key TEXT,
|
|
407
|
+
provider_key TEXT,
|
|
408
|
+
captured_at INTEGER NOT NULL,
|
|
409
|
+
row_version INTEGER,
|
|
410
|
+
capture_sequence INTEGER
|
|
411
|
+
);
|
|
412
|
+
`;
|
|
413
|
+
}
|
|
414
|
+
var LKG_SLOTS_DDL = lkgSlotsDdl("lkg_slots");
|
|
415
|
+
var LKG_MOVE_WINDOW_MS = 24 * 60 * 60 * 1000;
|
|
416
|
+
function tableExists(db, name) {
|
|
417
|
+
return Boolean(db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name = ?").get(name));
|
|
418
|
+
}
|
|
419
|
+
function columnExists(db, table, column) {
|
|
420
|
+
const rows = db.prepare(`PRAGMA table_info(${table})`).all();
|
|
421
|
+
return rows.some((row) => row.name === column);
|
|
422
|
+
}
|
|
423
|
+
function splitLkgSlotPrefixes(db, now = Date.now()) {
|
|
424
|
+
db.exec(LKG_SLOT_CHUNKS_DDL);
|
|
425
|
+
if (!tableExists(db, "lkg_slots")) {
|
|
426
|
+
db.exec(LKG_SLOTS_DDL);
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
if (!columnExists(db, "lkg_slots", "json_prefix"))
|
|
430
|
+
return;
|
|
431
|
+
db.prepare("DELETE FROM lkg_slots WHERE NOT (captured_at >= ?)").run(now - LKG_MOVE_WINDOW_MS);
|
|
432
|
+
db.exec("DROP TABLE IF EXISTS lkg_slots_v94");
|
|
433
|
+
db.exec(lkgSlotsDdl("lkg_slots_v94"));
|
|
434
|
+
const sessionIds = db.prepare("SELECT session_id FROM lkg_slots").all().map((row) => row.session_id);
|
|
435
|
+
const readPrefix = db.prepare("SELECT json_prefix FROM lkg_slots WHERE session_id IS ?");
|
|
436
|
+
const insertSlot = db.prepare(`INSERT OR IGNORE INTO lkg_slots_v94 (
|
|
437
|
+
session_id, json_prefix_chars, json_prefix_chunks, json_prefix_hash,
|
|
438
|
+
input_id_seq, input_content_digests, input_content_signatures,
|
|
439
|
+
last_input_message_id, model_key, provider_key,
|
|
440
|
+
captured_at, row_version, capture_sequence
|
|
441
|
+
)
|
|
442
|
+
SELECT session_id, ?, ?, ?,
|
|
443
|
+
input_id_seq, input_content_digests, input_content_signatures,
|
|
444
|
+
last_input_message_id, model_key, provider_key,
|
|
445
|
+
captured_at, row_version, capture_sequence
|
|
446
|
+
FROM lkg_slots WHERE session_id IS ?`);
|
|
447
|
+
const insertChunk = db.prepare("INSERT INTO lkg_slot_chunks (session_id, chunk, hash, body) VALUES (?, ?, ?, ?)");
|
|
448
|
+
db.prepare("DELETE FROM lkg_slot_chunks").run();
|
|
449
|
+
for (const sessionId of sessionIds) {
|
|
450
|
+
if (typeof sessionId !== "string")
|
|
451
|
+
continue;
|
|
452
|
+
const row = readPrefix.get(sessionId);
|
|
453
|
+
if (typeof row?.json_prefix !== "string")
|
|
454
|
+
continue;
|
|
455
|
+
const layout = layoutLkgPrefix(row.json_prefix);
|
|
456
|
+
const inserted = insertSlot.run(layout.chars, layout.chunks.length, layout.hash, sessionId);
|
|
457
|
+
if (Number(inserted.changes) !== 1)
|
|
458
|
+
continue;
|
|
459
|
+
for (let index = 0;index < layout.chunks.length; index += 1) {
|
|
460
|
+
insertChunk.run(sessionId, index, layout.chunkHashes[index], layout.chunks[index]);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
db.exec("DROP TABLE lkg_slots");
|
|
464
|
+
db.exec("ALTER TABLE lkg_slots_v94 RENAME TO lkg_slots");
|
|
465
|
+
}
|
|
466
|
+
function validDecisionSql(type, value) {
|
|
467
|
+
return `(${type} = 'text' AND (
|
|
468
|
+
${value} IN ('keep', 'strip') OR (
|
|
469
|
+
${value} GLOB 'keep:[1-9]*'
|
|
470
|
+
AND substr(${value}, 6) NOT GLOB '*[^0-9]*'
|
|
471
|
+
AND length(${value}) <= 10
|
|
472
|
+
AND CAST(substr(${value}, 6) AS INTEGER) BETWEEN 2 AND 10000
|
|
473
|
+
)
|
|
474
|
+
))`;
|
|
475
|
+
}
|
|
476
|
+
function splitReplayDecisions(db) {
|
|
477
|
+
db.exec(SESSION_REPLAY_DECISIONS_DDL);
|
|
478
|
+
if (!tableExists(db, "session_meta"))
|
|
479
|
+
return;
|
|
480
|
+
if (!columnExists(db, "session_meta", "trailing_blank_decisions"))
|
|
481
|
+
return;
|
|
482
|
+
const doc = "sm.trailing_blank_decisions";
|
|
483
|
+
const hasVersionKey = `json_type(${doc}, '$.version') IS NOT NULL`;
|
|
484
|
+
const versionIsDecision = validDecisionSql(`json_type(${doc}, '$.version')`, `json_extract(${doc}, '$.version')`);
|
|
485
|
+
const isV2 = `(${hasVersionKey} AND NOT ${versionIsDecision})`;
|
|
486
|
+
const v2Valid = `(
|
|
487
|
+
json_type(${doc}, '$.version') IN ('integer', 'real')
|
|
488
|
+
AND json_extract(${doc}, '$.version') = 2
|
|
489
|
+
AND json_type(${doc}, '$.trailingBlank') = 'object'
|
|
490
|
+
AND NOT EXISTS (
|
|
491
|
+
SELECT 1 FROM json_each(${doc}, '$.trailingBlank') AS entry
|
|
492
|
+
WHERE entry.key = '' OR NOT ${validDecisionSql("entry.type", "entry.value")}
|
|
493
|
+
)
|
|
494
|
+
)`;
|
|
495
|
+
const v1Valid = `NOT EXISTS (
|
|
496
|
+
SELECT 1 FROM json_each(${doc}) AS entry
|
|
497
|
+
WHERE entry.key = '' OR NOT ${validDecisionSql("entry.type", "entry.value")}
|
|
498
|
+
)`;
|
|
499
|
+
db.exec("DROP TABLE IF EXISTS temp.v94_replay_documents");
|
|
500
|
+
db.exec(`
|
|
501
|
+
CREATE TEMP TABLE v94_replay_documents AS
|
|
502
|
+
SELECT session_id, version FROM (
|
|
503
|
+
SELECT sm.session_id AS session_id, CASE WHEN ${isV2} THEN 2 ELSE 1 END AS version,
|
|
504
|
+
CASE WHEN ${isV2} THEN ${v2Valid} ELSE ${v1Valid} END AS strictly_valid
|
|
505
|
+
FROM session_meta AS sm
|
|
506
|
+
WHERE typeof(${doc}) = 'text' AND ${doc} <> ''
|
|
507
|
+
AND json_valid(${doc}) AND json_type(${doc}) = 'object'
|
|
508
|
+
) WHERE strictly_valid;
|
|
509
|
+
`);
|
|
510
|
+
db.exec(`
|
|
511
|
+
INSERT INTO session_replay_decisions (session_id, message_id, decision)
|
|
512
|
+
SELECT sm.session_id, entry.key, entry.value
|
|
513
|
+
FROM v94_replay_documents AS moved
|
|
514
|
+
JOIN session_meta AS sm ON sm.session_id = moved.session_id
|
|
515
|
+
JOIN json_each(
|
|
516
|
+
${doc}, CASE moved.version WHEN 2 THEN '$.trailingBlank' ELSE '$' END
|
|
517
|
+
) AS entry
|
|
518
|
+
WHERE true
|
|
519
|
+
ON CONFLICT(session_id, message_id) DO UPDATE SET decision = excluded.decision;
|
|
520
|
+
`);
|
|
521
|
+
db.exec(`
|
|
522
|
+
UPDATE session_meta SET trailing_blank_decisions = (
|
|
523
|
+
SELECT CASE moved.version
|
|
524
|
+
WHEN 2 THEN json_set(session_meta.trailing_blank_decisions, '$.trailingBlank', json('{}'))
|
|
525
|
+
ELSE ''
|
|
526
|
+
END
|
|
527
|
+
FROM v94_replay_documents AS moved WHERE moved.session_id = session_meta.session_id
|
|
528
|
+
)
|
|
529
|
+
WHERE session_id IN (SELECT session_id FROM v94_replay_documents);
|
|
530
|
+
`);
|
|
531
|
+
db.exec("DROP TABLE temp.v94_replay_documents");
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// ../plugin/src/features/magic-context/opencode2-relabel.ts
|
|
535
|
+
var OPENCODE2_RELABEL_STATE_KEY = "v87_opencode2_relabel";
|
|
536
|
+
var V87_HARNESS_TWIN_RULES = [
|
|
537
|
+
{
|
|
538
|
+
table: "session_projects",
|
|
539
|
+
keyColumns: ["session_id"],
|
|
540
|
+
incomingIsNewer: "incoming.updated_at > existing.updated_at"
|
|
541
|
+
},
|
|
542
|
+
{
|
|
543
|
+
table: "primer_candidates",
|
|
544
|
+
keyColumns: [
|
|
545
|
+
"project_path",
|
|
546
|
+
"session_id",
|
|
547
|
+
"source_start_message_id",
|
|
548
|
+
"source_end_message_id"
|
|
549
|
+
],
|
|
550
|
+
incomingIsNewer: "incoming.created_at > existing.created_at"
|
|
551
|
+
},
|
|
552
|
+
{
|
|
553
|
+
table: "transform_decisions",
|
|
554
|
+
keyColumns: ["session_id", "message_id"],
|
|
555
|
+
incomingIsNewer: "incoming.ts_ms > existing.ts_ms"
|
|
556
|
+
}
|
|
557
|
+
];
|
|
558
|
+
function tableExists2(db, name) {
|
|
559
|
+
return Boolean(db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name = ?").get(name));
|
|
560
|
+
}
|
|
561
|
+
function columnNames(db, table) {
|
|
562
|
+
return new Set(db.prepare(`PRAGMA table_info(${table})`).all().map((column) => column.name));
|
|
563
|
+
}
|
|
564
|
+
function listSessionScopedHarnessTables(db, tables) {
|
|
565
|
+
const shapes = [];
|
|
566
|
+
for (const table of tables) {
|
|
567
|
+
if (!tableExists2(db, table))
|
|
568
|
+
continue;
|
|
569
|
+
const columns = columnNames(db, table);
|
|
570
|
+
if (!columns.has("harness"))
|
|
571
|
+
continue;
|
|
572
|
+
shapes.push({ table, hasSessionId: columns.has("session_id") });
|
|
573
|
+
}
|
|
574
|
+
return shapes;
|
|
575
|
+
}
|
|
576
|
+
function collectCandidateSessions(db, shapes) {
|
|
577
|
+
const sessions = new Set;
|
|
578
|
+
for (const shape of shapes) {
|
|
579
|
+
if (!shape.hasSessionId)
|
|
580
|
+
continue;
|
|
581
|
+
const rows = db.prepare(`SELECT DISTINCT session_id AS id FROM ${shape.table}
|
|
582
|
+
WHERE session_id IS NOT NULL AND harness IN ('opencode', 'opencode2')`).all();
|
|
583
|
+
for (const row of rows)
|
|
584
|
+
sessions.add(row.id);
|
|
585
|
+
}
|
|
586
|
+
return [...sessions].sort();
|
|
587
|
+
}
|
|
588
|
+
function openEvidenceStore(path) {
|
|
589
|
+
const store = new Database2(path, { readonly: true, fileMustExist: true });
|
|
590
|
+
try {
|
|
591
|
+
const hasV1Messages = tableExists2(store, "message");
|
|
592
|
+
const hasV1Session = tableExists2(store, "session");
|
|
593
|
+
const hasV2 = tableExists2(store, "session_v2");
|
|
594
|
+
const hasV2Messages = hasV2 && tableExists2(store, "session_message");
|
|
595
|
+
if (!hasV1Messages && !hasV1Session && !hasV2) {
|
|
596
|
+
store.close();
|
|
597
|
+
return null;
|
|
598
|
+
}
|
|
599
|
+
const v1Message = hasV1Messages ? store.prepare("SELECT MAX(time_updated) AS newest FROM message WHERE session_id = ?") : null;
|
|
600
|
+
const v1Session = hasV1Session ? store.prepare("SELECT time_updated AS newest FROM session WHERE id = ?") : null;
|
|
601
|
+
const v2Message = hasV2Messages ? store.prepare("SELECT MAX(time_updated) AS newest FROM session_message WHERE session_id = ?") : null;
|
|
602
|
+
const v2Session = hasV2 ? store.prepare("SELECT time_updated AS newest FROM session_v2 WHERE id = ?") : null;
|
|
603
|
+
const newest = (statements, sessionId) => {
|
|
604
|
+
let best = null;
|
|
605
|
+
for (const statement of statements) {
|
|
606
|
+
if (!statement)
|
|
607
|
+
continue;
|
|
608
|
+
const row = statement.get(sessionId);
|
|
609
|
+
const value = row?.newest;
|
|
610
|
+
if (typeof value !== "number")
|
|
611
|
+
continue;
|
|
612
|
+
if (best === null || value > best)
|
|
613
|
+
best = value;
|
|
614
|
+
}
|
|
615
|
+
return best;
|
|
616
|
+
};
|
|
617
|
+
return {
|
|
618
|
+
newestV1Activity: (sessionId) => newest([v1Message, v1Session], sessionId),
|
|
619
|
+
newestV2Activity: (sessionId) => newest([v2Message, v2Session], sessionId),
|
|
620
|
+
close: () => store.close()
|
|
621
|
+
};
|
|
622
|
+
} catch (error) {
|
|
623
|
+
store.close();
|
|
624
|
+
throw error;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
function resolveHarnessFromEvidence(evidence, sessionId) {
|
|
628
|
+
const v1 = evidence.newestV1Activity(sessionId);
|
|
629
|
+
const v2 = evidence.newestV2Activity(sessionId);
|
|
630
|
+
if (v1 === null && v2 === null)
|
|
631
|
+
return null;
|
|
632
|
+
if (v2 !== null && (v1 === null || v2 > v1))
|
|
633
|
+
return "opencode2";
|
|
634
|
+
if (v1 !== null && (v2 === null || v1 > v2))
|
|
635
|
+
return "opencode";
|
|
636
|
+
return null;
|
|
637
|
+
}
|
|
638
|
+
function prepareTwinResolver(db, table, keyColumns, incomingIsNewer) {
|
|
639
|
+
const joinOn = keyColumns.map((column) => `incoming.${column} = existing.${column}`).join(" AND ");
|
|
640
|
+
const dropIncoming = db.prepare(`DELETE FROM ${table} WHERE rowid IN (
|
|
641
|
+
SELECT incoming.rowid FROM ${table} AS incoming
|
|
642
|
+
JOIN ${table} AS existing ON ${joinOn} AND existing.harness = ?
|
|
643
|
+
WHERE incoming.session_id = ? AND incoming.harness = ?
|
|
644
|
+
AND NOT (${incomingIsNewer})
|
|
645
|
+
)`);
|
|
646
|
+
const dropExisting = db.prepare(`DELETE FROM ${table} WHERE rowid IN (
|
|
647
|
+
SELECT existing.rowid FROM ${table} AS existing
|
|
648
|
+
JOIN ${table} AS incoming ON ${joinOn} AND incoming.harness = ?
|
|
649
|
+
WHERE existing.session_id = ? AND existing.harness = ?
|
|
650
|
+
AND (${incomingIsNewer})
|
|
651
|
+
)`);
|
|
652
|
+
return {
|
|
653
|
+
run(sessionId, target, source) {
|
|
654
|
+
dropIncoming.run(target, sessionId, source);
|
|
655
|
+
dropExisting.run(source, sessionId, target);
|
|
656
|
+
}
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
function writeUnresolvedState(db, reason, resolution, sessionIds) {
|
|
660
|
+
if (!tableExists2(db, "schema_migrations_meta"))
|
|
661
|
+
return;
|
|
662
|
+
const value = {
|
|
663
|
+
reason,
|
|
664
|
+
lookedFor: getOpenCodeDbProbeDescriptions(resolution).join(", "),
|
|
665
|
+
sessionIds: [...sessionIds],
|
|
666
|
+
recordedAt: Date.now()
|
|
667
|
+
};
|
|
668
|
+
db.prepare(`INSERT INTO schema_migrations_meta (key, value) VALUES (?, ?)
|
|
669
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value`).run(OPENCODE2_RELABEL_STATE_KEY, JSON.stringify(value));
|
|
670
|
+
}
|
|
671
|
+
function clearUnresolvedState(db) {
|
|
672
|
+
if (!tableExists2(db, "schema_migrations_meta"))
|
|
673
|
+
return;
|
|
674
|
+
db.prepare("DELETE FROM schema_migrations_meta WHERE key = ?").run(OPENCODE2_RELABEL_STATE_KEY);
|
|
675
|
+
}
|
|
676
|
+
function repairOpenCode2HarnessLabels(db, options) {
|
|
677
|
+
const shapes = listSessionScopedHarnessTables(db, options.tables);
|
|
678
|
+
const candidateSessionIds = collectCandidateSessions(db, shapes);
|
|
679
|
+
if (candidateSessionIds.length === 0) {
|
|
680
|
+
clearUnresolvedState(db);
|
|
681
|
+
return {
|
|
682
|
+
status: "no_candidates",
|
|
683
|
+
storePath: null,
|
|
684
|
+
candidateSessionIds,
|
|
685
|
+
relabelledSessions: []
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
const resolution = resolveOpenCodeDbPath("v1", { env: options.env });
|
|
689
|
+
let evidence = null;
|
|
690
|
+
let reason = null;
|
|
691
|
+
if (!openCodeDbPathExists(resolution)) {
|
|
692
|
+
reason = "store_not_found";
|
|
693
|
+
} else {
|
|
694
|
+
try {
|
|
695
|
+
evidence = openEvidenceStore(resolution.path);
|
|
696
|
+
if (!evidence)
|
|
697
|
+
reason = "store_schema_unknown";
|
|
698
|
+
} catch (error) {
|
|
699
|
+
reason = "store_unreadable";
|
|
700
|
+
log2(`[migration] OpenCode session database at ${resolution.path} could not be read: ${error instanceof Error ? error.message : String(error)}`);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
if (!evidence || reason !== null) {
|
|
704
|
+
evidence?.close();
|
|
705
|
+
writeUnresolvedState(db, reason ?? "store_not_found", resolution, candidateSessionIds);
|
|
706
|
+
log2(`[migration] OpenCode harness labels left unverified for ${candidateSessionIds.length} session(s): ` + `no readable OpenCode session database (${reason ?? "store_not_found"}) at ${resolution.path}. ` + "Nothing was relabelled; `magic-context doctor` reports the affected sessions.");
|
|
707
|
+
return {
|
|
708
|
+
status: "unresolved",
|
|
709
|
+
reason: reason ?? "store_not_found",
|
|
710
|
+
storePath: null,
|
|
711
|
+
candidateSessionIds,
|
|
712
|
+
relabelledSessions: []
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
const relabelledSessions = [];
|
|
716
|
+
try {
|
|
717
|
+
const updates = shapes.filter((shape) => shape.hasSessionId).map((shape) => db.prepare(`UPDATE ${shape.table} SET harness = ? WHERE session_id = ? AND harness = ?`));
|
|
718
|
+
const presentTables = new Set(shapes.map((shape) => shape.table));
|
|
719
|
+
const twinResolvers = V87_HARNESS_TWIN_RULES.filter((rule) => presentTables.has(rule.table)).map((rule) => prepareTwinResolver(db, rule.table, rule.keyColumns, rule.incomingIsNewer));
|
|
720
|
+
for (const sessionId of candidateSessionIds) {
|
|
721
|
+
const target = resolveHarnessFromEvidence(evidence, sessionId);
|
|
722
|
+
if (target === null)
|
|
723
|
+
continue;
|
|
724
|
+
const source = target === "opencode" ? "opencode2" : "opencode";
|
|
725
|
+
for (const resolver of twinResolvers)
|
|
726
|
+
resolver.run(sessionId, target, source);
|
|
727
|
+
let moved = 0;
|
|
728
|
+
for (const update of updates) {
|
|
729
|
+
moved += Number(update.run(target, sessionId, source).changes ?? 0);
|
|
730
|
+
}
|
|
731
|
+
if (moved > 0)
|
|
732
|
+
relabelledSessions.push({ sessionId, harness: target });
|
|
733
|
+
}
|
|
734
|
+
} finally {
|
|
735
|
+
evidence.close();
|
|
736
|
+
}
|
|
737
|
+
clearUnresolvedState(db);
|
|
738
|
+
if (relabelledSessions.length > 0) {
|
|
739
|
+
const toOpenCode2 = relabelledSessions.filter((entry) => entry.harness === "opencode2").length;
|
|
740
|
+
log2(`[migration] OpenCode harness labels repaired from ${resolution.path}: ` + `${toOpenCode2} session(s) restored to opencode2, ` + `${relabelledSessions.length - toOpenCode2} session(s) set to opencode.`);
|
|
741
|
+
}
|
|
742
|
+
return {
|
|
743
|
+
status: "resolved",
|
|
744
|
+
storePath: resolution.path,
|
|
745
|
+
candidateSessionIds,
|
|
746
|
+
relabelledSessions
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// ../plugin/src/features/magic-context/storage-compartment-history-version.ts
|
|
751
|
+
function definitions(db) {
|
|
752
|
+
const hasPrivilegeTable = Boolean(db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='context_privilege_state'").get());
|
|
753
|
+
const externalUpdate = hasPrivilegeTable ? "CASE WHEN COALESCE((SELECT enabled FROM context_privilege_state WHERE id=1),0)=0 THEN 1 ELSE 0 END" : "1";
|
|
754
|
+
return [
|
|
755
|
+
{
|
|
756
|
+
type: "table",
|
|
757
|
+
name: "compartment_history_versions",
|
|
758
|
+
sql: `CREATE TABLE compartment_history_versions (
|
|
759
|
+
session_id TEXT PRIMARY KEY NOT NULL,
|
|
760
|
+
generation TEXT NOT NULL DEFAULT (lower(hex(randomblob(16)))),
|
|
761
|
+
version INTEGER NOT NULL DEFAULT 0 CHECK (version >= 0),
|
|
762
|
+
rewrite_version INTEGER NOT NULL DEFAULT 0 CHECK (rewrite_version >= 0),
|
|
763
|
+
seeded INTEGER NOT NULL DEFAULT 0 CHECK (seeded IN (0, 1))
|
|
764
|
+
)`
|
|
765
|
+
},
|
|
766
|
+
{
|
|
767
|
+
type: "trigger",
|
|
768
|
+
name: "compartment_history_ai",
|
|
769
|
+
sql: `CREATE TRIGGER compartment_history_ai AFTER INSERT ON compartments BEGIN
|
|
770
|
+
INSERT INTO compartment_history_versions(session_id, version) VALUES (NEW.session_id, 1)
|
|
771
|
+
ON CONFLICT(session_id) DO UPDATE SET version = version + 1;
|
|
772
|
+
END`
|
|
773
|
+
},
|
|
774
|
+
{
|
|
775
|
+
type: "trigger",
|
|
776
|
+
name: "compartment_history_ad",
|
|
777
|
+
sql: `CREATE TRIGGER compartment_history_ad AFTER DELETE ON compartments BEGIN
|
|
778
|
+
INSERT INTO compartment_history_versions(session_id, version) VALUES (OLD.session_id, 1)
|
|
779
|
+
ON CONFLICT(session_id) DO UPDATE SET version = version + 1;
|
|
780
|
+
END`
|
|
781
|
+
},
|
|
782
|
+
{
|
|
783
|
+
type: "trigger",
|
|
784
|
+
name: "compartment_history_au",
|
|
785
|
+
sql: `CREATE TRIGGER compartment_history_au AFTER UPDATE ON compartments BEGIN
|
|
786
|
+
INSERT INTO compartment_history_versions(session_id, version, rewrite_version) VALUES (OLD.session_id, 1, ${externalUpdate})
|
|
787
|
+
ON CONFLICT(session_id) DO UPDATE SET version = version + 1, rewrite_version = rewrite_version + ${externalUpdate};
|
|
788
|
+
INSERT INTO compartment_history_versions(session_id, version, rewrite_version)
|
|
789
|
+
SELECT NEW.session_id, 1, ${externalUpdate} WHERE NEW.session_id != OLD.session_id
|
|
790
|
+
ON CONFLICT(session_id) DO UPDATE SET version = version + 1, rewrite_version = rewrite_version + ${externalUpdate};
|
|
791
|
+
END`
|
|
792
|
+
}
|
|
793
|
+
];
|
|
794
|
+
}
|
|
795
|
+
function installedDefinitions(db) {
|
|
796
|
+
const rows = db.prepare(`SELECT type, name, sql FROM sqlite_master
|
|
797
|
+
WHERE name IN ('compartment_history_versions', 'compartment_history_ai', 'compartment_history_ad', 'compartment_history_au')`).all();
|
|
798
|
+
return new Map(rows.map((row) => [`${row.type}:${row.name}`, row.sql]));
|
|
799
|
+
}
|
|
800
|
+
function matches(expected, installed) {
|
|
801
|
+
return expected.every((definition) => installed.get(`${definition.type}:${definition.name}`) === definition.sql);
|
|
802
|
+
}
|
|
803
|
+
function installCompartmentHistoryVersions(db) {
|
|
804
|
+
if (!db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='compartments'").get())
|
|
805
|
+
return;
|
|
806
|
+
if (matches(definitions(db), installedDefinitions(db)))
|
|
807
|
+
return;
|
|
808
|
+
db.transaction(() => {
|
|
809
|
+
const expected = definitions(db);
|
|
810
|
+
const installed = installedDefinitions(db);
|
|
811
|
+
if (matches(expected, installed))
|
|
812
|
+
return;
|
|
813
|
+
const table = expected[0];
|
|
814
|
+
const existingTable = installed.get(`table:${table.name}`);
|
|
815
|
+
if (existingTable !== undefined && existingTable !== table.sql) {
|
|
816
|
+
throw new Error("compartment_history_versions schema differs from v93; refusing to replace revision data");
|
|
817
|
+
}
|
|
818
|
+
if (existingTable === undefined)
|
|
819
|
+
db.exec(table.sql);
|
|
820
|
+
db.exec(`INSERT OR IGNORE INTO compartment_history_versions(session_id, seeded)
|
|
821
|
+
SELECT DISTINCT session_id, 1 FROM compartments`);
|
|
822
|
+
for (const trigger of expected.slice(1)) {
|
|
823
|
+
if (installed.get(`trigger:${trigger.name}`) === trigger.sql)
|
|
824
|
+
continue;
|
|
825
|
+
db.exec(`DROP TRIGGER IF EXISTS ${trigger.name}`);
|
|
826
|
+
db.exec(trigger.sql);
|
|
827
|
+
}
|
|
828
|
+
}).immediate();
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// ../plugin/src/features/magic-context/workspaces.ts
|
|
832
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
833
|
+
var VALID_SHARE_CATEGORIES = new Set(V2_MEMORY_CATEGORIES);
|
|
834
|
+
var DEFAULT_WORKSPACE_SHARE_CATEGORIES = ["CONSTRAINTS"];
|
|
835
|
+
function tableExists3(db, tableName) {
|
|
836
|
+
const row = db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name = ? LIMIT 1").get(tableName);
|
|
837
|
+
return Boolean(row);
|
|
838
|
+
}
|
|
839
|
+
function columnExists2(db, tableName, columnName) {
|
|
840
|
+
const rows = db.prepare(`PRAGMA table_info(${tableName})`).all();
|
|
841
|
+
return rows.some((row) => row.name === columnName);
|
|
842
|
+
}
|
|
843
|
+
function uniqueSorted(values) {
|
|
844
|
+
return [...new Set(values)].sort((left, right) => left.localeCompare(right));
|
|
845
|
+
}
|
|
846
|
+
function placeholders(values) {
|
|
847
|
+
return values.map(() => "?").join(", ");
|
|
848
|
+
}
|
|
849
|
+
function defaultWorkspaceShareCategories() {
|
|
850
|
+
return [...DEFAULT_WORKSPACE_SHARE_CATEGORIES];
|
|
851
|
+
}
|
|
852
|
+
function warnInvalidShareCategories(reason, raw) {
|
|
853
|
+
log2("[magic-context] WARN: invalid workspace share_categories; sharing no foreign memory categories", {
|
|
854
|
+
reason,
|
|
855
|
+
raw
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
function normalizeShareCategories(raw) {
|
|
859
|
+
if (raw === null || raw === undefined) {
|
|
860
|
+
return defaultWorkspaceShareCategories();
|
|
861
|
+
}
|
|
862
|
+
if (typeof raw !== "string") {
|
|
863
|
+
warnInvalidShareCategories("not a string", raw);
|
|
864
|
+
return [];
|
|
865
|
+
}
|
|
866
|
+
let parsed;
|
|
867
|
+
try {
|
|
868
|
+
parsed = JSON.parse(raw);
|
|
869
|
+
} catch {
|
|
870
|
+
warnInvalidShareCategories("malformed JSON", raw);
|
|
871
|
+
return [];
|
|
872
|
+
}
|
|
873
|
+
if (!Array.isArray(parsed)) {
|
|
874
|
+
warnInvalidShareCategories("not a JSON array", raw);
|
|
875
|
+
return [];
|
|
876
|
+
}
|
|
877
|
+
const categories = [];
|
|
878
|
+
for (const value of parsed) {
|
|
879
|
+
if (typeof value !== "string" || !VALID_SHARE_CATEGORIES.has(value)) {
|
|
880
|
+
warnInvalidShareCategories("unknown category", raw);
|
|
881
|
+
return [];
|
|
882
|
+
}
|
|
883
|
+
if (!categories.includes(value))
|
|
884
|
+
categories.push(value);
|
|
885
|
+
}
|
|
886
|
+
return categories.sort((left, right) => left.localeCompare(right));
|
|
887
|
+
}
|
|
888
|
+
function selectWorkspaceShareCategories(db, identities) {
|
|
889
|
+
const candidates = uniqueSorted(identities.filter((identity) => identity.length > 0));
|
|
890
|
+
if (candidates.length === 0 || !tableExists3(db, "workspace_members")) {
|
|
891
|
+
return null;
|
|
892
|
+
}
|
|
893
|
+
const hasMembership = Boolean(db.prepare(`SELECT 1
|
|
894
|
+
FROM workspace_members
|
|
895
|
+
WHERE project_path IN (${placeholders(candidates)})
|
|
896
|
+
LIMIT 1`).get(...candidates));
|
|
897
|
+
if (!hasMembership)
|
|
898
|
+
return null;
|
|
899
|
+
if (!tableExists3(db, "workspaces")) {
|
|
900
|
+
log2("[magic-context] WARN: workspace member has no workspaces table; sharing no foreign memory categories");
|
|
901
|
+
return [];
|
|
902
|
+
}
|
|
903
|
+
if (!columnExists2(db, "workspaces", "share_categories")) {
|
|
904
|
+
return defaultWorkspaceShareCategories();
|
|
905
|
+
}
|
|
906
|
+
const row = db.prepare(`SELECT workspace.share_categories AS shareCategories
|
|
907
|
+
FROM workspace_members AS member
|
|
908
|
+
JOIN workspaces AS workspace ON workspace.id = member.workspace_id
|
|
909
|
+
WHERE member.project_path IN (${placeholders(candidates)})
|
|
910
|
+
ORDER BY workspace.id ASC
|
|
911
|
+
LIMIT 1`).get(...candidates);
|
|
912
|
+
if (!row) {
|
|
913
|
+
log2("[magic-context] WARN: workspace member has no workspace share_categories row; sharing no foreign memory categories");
|
|
914
|
+
return [];
|
|
915
|
+
}
|
|
916
|
+
return normalizeShareCategories(row.shareCategories);
|
|
917
|
+
}
|
|
918
|
+
function resolveWorkspaceShareCategories(db, projectIdentity) {
|
|
919
|
+
return selectWorkspaceShareCategories(db, [projectIdentity]);
|
|
920
|
+
}
|
|
921
|
+
function resolveWorkspaceIdentitySet(db, projectIdentity) {
|
|
922
|
+
if (!tableExists3(db, "workspace_members")) {
|
|
923
|
+
return { identities: [projectIdentity], namesByIdentity: new Map };
|
|
924
|
+
}
|
|
925
|
+
const rows = db.prepare(`SELECT member.project_path AS identity, member.display_name AS displayName
|
|
926
|
+
FROM workspace_members AS anchor
|
|
927
|
+
JOIN workspace_members AS member ON member.workspace_id = anchor.workspace_id
|
|
928
|
+
WHERE anchor.project_path = ?
|
|
929
|
+
ORDER BY member.display_name ASC, member.project_path ASC`).all(projectIdentity);
|
|
930
|
+
if (rows.length === 0) {
|
|
931
|
+
return { identities: [projectIdentity], namesByIdentity: new Map };
|
|
932
|
+
}
|
|
933
|
+
const namesByIdentity = new Map;
|
|
934
|
+
const identities = [];
|
|
935
|
+
for (const row of rows) {
|
|
936
|
+
if (typeof row.identity !== "string" || row.identity.length === 0)
|
|
937
|
+
continue;
|
|
938
|
+
if (identities.includes(row.identity))
|
|
939
|
+
continue;
|
|
940
|
+
identities.push(row.identity);
|
|
941
|
+
if (typeof row.displayName === "string" && row.displayName.length > 0) {
|
|
942
|
+
namesByIdentity.set(row.identity, row.displayName);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
return identities.length > 0 ? { identities, namesByIdentity } : { identities: [projectIdentity], namesByIdentity: new Map };
|
|
946
|
+
}
|
|
947
|
+
function expandWorkspaceIdentitySetWithAliases(db, identities) {
|
|
948
|
+
const canonical = uniqueSorted(identities.filter((identity) => identity.length > 0));
|
|
949
|
+
const expanded = new Set(canonical);
|
|
950
|
+
const canonicalIdentityByStoredPath = new Map;
|
|
951
|
+
for (const identity of canonical) {
|
|
952
|
+
canonicalIdentityByStoredPath.set(identity, identity);
|
|
953
|
+
}
|
|
954
|
+
if (canonical.length === 0 || !tableExists3(db, "v22_identity_rekey_map")) {
|
|
955
|
+
return { expandedIdentities: [...expanded], canonicalIdentityByStoredPath };
|
|
956
|
+
}
|
|
957
|
+
const rows = db.prepare(`SELECT old_project_path AS oldProjectPath, new_project_path AS newProjectPath
|
|
958
|
+
FROM v22_identity_rekey_map
|
|
959
|
+
WHERE new_project_path IN (${placeholders(canonical)})
|
|
960
|
+
ORDER BY old_project_path ASC`).all(...canonical);
|
|
961
|
+
for (const row of rows) {
|
|
962
|
+
if (typeof row.oldProjectPath !== "string" || typeof row.newProjectPath !== "string") {
|
|
963
|
+
continue;
|
|
964
|
+
}
|
|
965
|
+
if (!canonicalIdentityByStoredPath.has(row.newProjectPath))
|
|
966
|
+
continue;
|
|
967
|
+
expanded.add(row.oldProjectPath);
|
|
968
|
+
canonicalIdentityByStoredPath.set(row.oldProjectPath, row.newProjectPath);
|
|
969
|
+
}
|
|
970
|
+
return { expandedIdentities: [...expanded], canonicalIdentityByStoredPath };
|
|
971
|
+
}
|
|
972
|
+
function resolveStoredPathWorkspaceIdentity(storedProjectPath, memberIdentities, canonicalIdentityByStoredPath) {
|
|
973
|
+
const direct = canonicalIdentityByStoredPath.get(storedProjectPath);
|
|
974
|
+
if (direct)
|
|
975
|
+
return direct;
|
|
976
|
+
const normalized = normalizeStoredProjectPath(storedProjectPath);
|
|
977
|
+
const normalizedDirect = canonicalIdentityByStoredPath.get(normalized);
|
|
978
|
+
if (normalizedDirect)
|
|
979
|
+
return normalizedDirect;
|
|
980
|
+
if (memberIdentities.includes(normalized))
|
|
981
|
+
return normalized;
|
|
982
|
+
for (const identity of memberIdentities) {
|
|
983
|
+
if (storedPathBelongsToIdentity(storedProjectPath, identity)) {
|
|
984
|
+
return identity;
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
return null;
|
|
988
|
+
}
|
|
989
|
+
function storedPathBelongsToWorkspace(storedProjectPath, memberIdentities, expandedIdentities, canonicalIdentityByStoredPath) {
|
|
990
|
+
if (expandedIdentities.includes(storedProjectPath))
|
|
991
|
+
return true;
|
|
992
|
+
return resolveStoredPathWorkspaceIdentity(storedProjectPath, memberIdentities, canonicalIdentityByStoredPath) !== null;
|
|
993
|
+
}
|
|
994
|
+
function sourceNameForMemory(storedProjectPath, ownIdentity, memberIdentities, namesByIdentity, canonicalIdentityByStoredPath) {
|
|
995
|
+
const canonicalIdentity = resolveStoredPathWorkspaceIdentity(storedProjectPath, memberIdentities, canonicalIdentityByStoredPath);
|
|
996
|
+
if (!canonicalIdentity || canonicalIdentity === ownIdentity)
|
|
997
|
+
return;
|
|
998
|
+
return namesByIdentity.get(canonicalIdentity);
|
|
999
|
+
}
|
|
1000
|
+
function getEpochMap(db, identities) {
|
|
1001
|
+
if (identities.length === 0)
|
|
1002
|
+
return new Map;
|
|
1003
|
+
const rows = db.prepare(`SELECT project_path AS projectPath, project_memory_epoch AS epoch
|
|
1004
|
+
FROM project_state
|
|
1005
|
+
WHERE project_path IN (${placeholders(identities)})`).all(...identities);
|
|
1006
|
+
const epochs = new Map;
|
|
1007
|
+
for (const row of rows) {
|
|
1008
|
+
if (typeof row.projectPath !== "string" || typeof row.epoch !== "number")
|
|
1009
|
+
continue;
|
|
1010
|
+
epochs.set(row.projectPath, row.epoch);
|
|
1011
|
+
}
|
|
1012
|
+
return epochs;
|
|
1013
|
+
}
|
|
1014
|
+
function computeWorkspaceEpochFingerprint(db, identities) {
|
|
1015
|
+
const canonical = uniqueSorted(identities.filter((identity) => identity.length > 0));
|
|
1016
|
+
const epochs = getEpochMap(db, canonical);
|
|
1017
|
+
const shareCategories = selectWorkspaceShareCategories(db, canonical);
|
|
1018
|
+
const hash = createHash2("sha256");
|
|
1019
|
+
hash.update("share_categories", "utf8");
|
|
1020
|
+
hash.update("\x00");
|
|
1021
|
+
hash.update(shareCategories === null ? "NO_WORKSPACE" : JSON.stringify(shareCategories), "utf8");
|
|
1022
|
+
hash.update(`
|
|
1023
|
+
`);
|
|
1024
|
+
for (const identity of canonical) {
|
|
1025
|
+
hash.update(identity, "utf8");
|
|
1026
|
+
hash.update("\x00");
|
|
1027
|
+
hash.update(String(epochs.get(identity) ?? 0), "utf8");
|
|
1028
|
+
hash.update(`
|
|
1029
|
+
`);
|
|
1030
|
+
}
|
|
1031
|
+
return hash.digest("hex");
|
|
1032
|
+
}
|
|
1033
|
+
function isInTransaction(db) {
|
|
1034
|
+
const candidate = db;
|
|
1035
|
+
return candidate.inTransaction === true || candidate.isTransaction === true;
|
|
1036
|
+
}
|
|
1037
|
+
function bumpEpochRows(db, identities, now) {
|
|
1038
|
+
const stmt = db.prepare(`INSERT INTO project_state
|
|
1039
|
+
(project_path, project_memory_epoch, project_user_profile_version, updated_at)
|
|
1040
|
+
VALUES (?, 1, 0, ?)
|
|
1041
|
+
ON CONFLICT(project_path) DO UPDATE SET
|
|
1042
|
+
project_memory_epoch = project_memory_epoch + 1,
|
|
1043
|
+
updated_at = excluded.updated_at`);
|
|
1044
|
+
for (const identity of uniqueSorted(identities)) {
|
|
1045
|
+
stmt.run(identity, now);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
function bumpEpochsForWorkspaceMemberSet(db, identities, now = Date.now()) {
|
|
1049
|
+
const run = () => bumpEpochRows(db, identities, now);
|
|
1050
|
+
if (isInTransaction(db)) {
|
|
1051
|
+
run();
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1054
|
+
db.exec("BEGIN IMMEDIATE");
|
|
1055
|
+
const transactionStartedAt = performance.now();
|
|
1056
|
+
try {
|
|
1057
|
+
run();
|
|
1058
|
+
db.exec("COMMIT");
|
|
1059
|
+
logSlowWriteTransaction("workspace_epoch_bump", transactionStartedAt);
|
|
1060
|
+
} catch (error) {
|
|
1061
|
+
try {
|
|
1062
|
+
db.exec("ROLLBACK");
|
|
1063
|
+
} catch {}
|
|
1064
|
+
throw error;
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
// ../plugin/src/features/magic-context/migrations.ts
|
|
1069
|
+
var FORK_MIGRATION_VERSION_FLOOR = 1e4;
|
|
1070
|
+
var MIGRATION_LOCK_RETRY_DELAYS_MS = [1000, 2000, 4000, 8000, 15000];
|
|
1071
|
+
|
|
1072
|
+
class MigrationLockBusyError extends Error {
|
|
1073
|
+
constructor(message) {
|
|
1074
|
+
super(message);
|
|
1075
|
+
this.name = "MigrationLockBusyError";
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
function isSqliteLockError(error) {
|
|
1079
|
+
if (!error || typeof error !== "object")
|
|
1080
|
+
return false;
|
|
1081
|
+
const candidate = error;
|
|
1082
|
+
if (candidate.code === "SQLITE_BUSY" || candidate.code === "SQLITE_LOCKED")
|
|
1083
|
+
return true;
|
|
1084
|
+
return typeof candidate.message === "string" && /database is locked|sqlite_(busy|locked)/i.test(candidate.message);
|
|
1085
|
+
}
|
|
1086
|
+
function tableExists4(db, name) {
|
|
1087
|
+
return Boolean(db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name = ?").get(name));
|
|
1088
|
+
}
|
|
1089
|
+
var V85_OPENCODE2_RELABEL_TABLES = [
|
|
1090
|
+
"tags",
|
|
1091
|
+
"pending_ops",
|
|
1092
|
+
"source_contents",
|
|
1093
|
+
"compartments",
|
|
1094
|
+
"compartment_chunk_embeddings",
|
|
1095
|
+
"session_projects",
|
|
1096
|
+
"compartment_events",
|
|
1097
|
+
"compression_depth",
|
|
1098
|
+
"session_facts",
|
|
1099
|
+
"primer_candidates",
|
|
1100
|
+
"notes",
|
|
1101
|
+
"message_history_index",
|
|
1102
|
+
"message_history_source",
|
|
1103
|
+
"pending_session_cleanup",
|
|
1104
|
+
"message_history_orphan_sweep",
|
|
1105
|
+
"session_meta",
|
|
1106
|
+
"subagent_invocations",
|
|
1107
|
+
"historian_runs",
|
|
1108
|
+
"transform_decisions",
|
|
1109
|
+
"recomp_compartments",
|
|
1110
|
+
"recomp_facts"
|
|
1111
|
+
];
|
|
1112
|
+
var V85_OPTIONAL_OPENCODE2_RELABEL_TABLES = ["session_project_backfill_state"];
|
|
1113
|
+
function healMismatchedTierClose(db, table, hasLegacy) {
|
|
1114
|
+
if (!tableExists4(db, table))
|
|
1115
|
+
return;
|
|
1116
|
+
const columns = new Set(db.prepare(`PRAGMA table_info(${table})`).all().map((row) => row.name));
|
|
1117
|
+
for (const required of ["content", "p1", "p2", "p3", "p4"]) {
|
|
1118
|
+
if (!columns.has(required))
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1121
|
+
if (hasLegacy && !columns.has("legacy"))
|
|
1122
|
+
return;
|
|
1123
|
+
const predicate = hasLegacy ? "legacy = 1 AND p1 IS NULL AND content LIKE '%<p1%'" : "p1 IS NULL AND content LIKE '%<p1%'";
|
|
1124
|
+
const rows = db.prepare(`SELECT id, content FROM ${table} WHERE ${predicate}`).all();
|
|
1125
|
+
const update = db.prepare(`UPDATE ${table} SET p1 = ?, p2 = ?, p3 = ?, p4 = ?${hasLegacy ? ", legacy = 0" : ""} WHERE id = ?`);
|
|
1126
|
+
for (const row of rows) {
|
|
1127
|
+
const tiers = extractTiersFromInner(row.content);
|
|
1128
|
+
if (typeof tiers.p1 !== "string" || tiers.p1.length === 0)
|
|
1129
|
+
continue;
|
|
1130
|
+
const p1 = tiers.p1;
|
|
1131
|
+
const p2 = typeof tiers.p2 === "string" ? tiers.p2 : p1;
|
|
1132
|
+
const p3 = typeof tiers.p3 === "string" ? tiers.p3 : p2;
|
|
1133
|
+
const p4 = typeof tiers.p4 === "string" ? tiers.p4 : "";
|
|
1134
|
+
update.run(p1, p2, p3, p4, row.id);
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
function assertForeignKeyIntegrity(db, table) {
|
|
1138
|
+
const rows = (table ? db.prepare(`PRAGMA foreign_key_check(${table})`) : db.prepare("PRAGMA foreign_key_check")).all();
|
|
1139
|
+
if (rows.length > 0) {
|
|
1140
|
+
throw new Error(`foreign_key_check failed after embedding table rebuild${table ? ` (${table})` : ""} (${rows.length} violation(s))`);
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
function authorityPrivilegeCheck() {
|
|
1144
|
+
return "COALESCE((SELECT enabled FROM context_privilege_state WHERE id = 1), 0) = 0";
|
|
1145
|
+
}
|
|
1146
|
+
function managedAuthorityNoteRow(row) {
|
|
1147
|
+
return `(
|
|
1148
|
+
EXISTS (SELECT 1 FROM authority_managed WHERE project_path = ${row}.project_path)
|
|
1149
|
+
OR EXISTS (SELECT 1 FROM authority_repair_pending WHERE project_path = ${row}.project_path)
|
|
1150
|
+
OR EXISTS (
|
|
1151
|
+
SELECT 1 FROM session_projects sp
|
|
1152
|
+
JOIN authority_managed am ON am.project_path = sp.project_path
|
|
1153
|
+
WHERE sp.session_id = ${row}.session_id
|
|
1154
|
+
)
|
|
1155
|
+
OR EXISTS (
|
|
1156
|
+
SELECT 1 FROM session_projects sp
|
|
1157
|
+
JOIN authority_repair_pending arp ON arp.project_path = sp.project_path
|
|
1158
|
+
WHERE sp.session_id = ${row}.session_id
|
|
1159
|
+
)
|
|
1160
|
+
)`;
|
|
1161
|
+
}
|
|
1162
|
+
function installLatestAuthorityTriggers(db) {
|
|
1163
|
+
const privilegeCheck = authorityPrivilegeCheck();
|
|
1164
|
+
if (tableExists4(db, "memories")) {
|
|
1165
|
+
db.exec(`
|
|
1166
|
+
DROP TRIGGER IF EXISTS memories_authority_guard_insert;
|
|
1167
|
+
DROP TRIGGER IF EXISTS memories_authority_guard_update;
|
|
1168
|
+
DROP TRIGGER IF EXISTS memories_authority_guard_delete;
|
|
1169
|
+
CREATE TRIGGER memories_authority_guard_insert
|
|
1170
|
+
BEFORE INSERT ON memories
|
|
1171
|
+
WHEN (EXISTS (SELECT 1 FROM authority_managed WHERE project_path = NEW.project_path)
|
|
1172
|
+
OR EXISTS (SELECT 1 FROM authority_repair_pending WHERE project_path = NEW.project_path))
|
|
1173
|
+
AND ${privilegeCheck}
|
|
1174
|
+
BEGIN SELECT RAISE(ABORT, 'context.db memory writes are managed by the Rust module'); END;
|
|
1175
|
+
CREATE TRIGGER memories_authority_guard_update
|
|
1176
|
+
BEFORE UPDATE ON memories
|
|
1177
|
+
WHEN (EXISTS (SELECT 1 FROM authority_managed WHERE project_path = OLD.project_path)
|
|
1178
|
+
OR EXISTS (SELECT 1 FROM authority_managed WHERE project_path = NEW.project_path)
|
|
1179
|
+
OR EXISTS (SELECT 1 FROM authority_repair_pending WHERE project_path = OLD.project_path)
|
|
1180
|
+
OR EXISTS (SELECT 1 FROM authority_repair_pending WHERE project_path = NEW.project_path))
|
|
1181
|
+
AND ${privilegeCheck}
|
|
1182
|
+
BEGIN SELECT RAISE(ABORT, 'context.db memory writes are managed by the Rust module'); END;
|
|
1183
|
+
CREATE TRIGGER memories_authority_guard_delete
|
|
1184
|
+
BEFORE DELETE ON memories
|
|
1185
|
+
WHEN (EXISTS (SELECT 1 FROM authority_managed WHERE project_path = OLD.project_path)
|
|
1186
|
+
OR EXISTS (SELECT 1 FROM authority_repair_pending WHERE project_path = OLD.project_path))
|
|
1187
|
+
AND ${privilegeCheck}
|
|
1188
|
+
BEGIN SELECT RAISE(ABORT, 'context.db memory writes are managed by the Rust module'); END;
|
|
1189
|
+
`);
|
|
1190
|
+
}
|
|
1191
|
+
if (tableExists4(db, "notes")) {
|
|
1192
|
+
const managedOld = managedAuthorityNoteRow("OLD");
|
|
1193
|
+
const managedNew = managedAuthorityNoteRow("NEW");
|
|
1194
|
+
db.exec(`
|
|
1195
|
+
DROP TRIGGER IF EXISTS notes_authority_guard_insert;
|
|
1196
|
+
DROP TRIGGER IF EXISTS notes_authority_guard_update;
|
|
1197
|
+
DROP TRIGGER IF EXISTS notes_authority_guard_delete;
|
|
1198
|
+
CREATE TRIGGER notes_authority_guard_insert
|
|
1199
|
+
BEFORE INSERT ON notes
|
|
1200
|
+
WHEN ${managedNew} AND ${privilegeCheck}
|
|
1201
|
+
BEGIN SELECT RAISE(ABORT, 'context.db note writes are managed by the Rust module'); END;
|
|
1202
|
+
CREATE TRIGGER notes_authority_guard_update
|
|
1203
|
+
BEFORE UPDATE ON notes
|
|
1204
|
+
WHEN (${managedOld} OR ${managedNew}) AND ${privilegeCheck}
|
|
1205
|
+
BEGIN SELECT RAISE(ABORT, 'context.db note writes are managed by the Rust module'); END;
|
|
1206
|
+
CREATE TRIGGER notes_authority_guard_delete
|
|
1207
|
+
BEFORE DELETE ON notes
|
|
1208
|
+
WHEN ${managedOld} AND ${privilegeCheck}
|
|
1209
|
+
BEGIN SELECT RAISE(ABORT, 'context.db note writes are managed by the Rust module'); END;
|
|
1210
|
+
`);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
var MIGRATIONS = [
|
|
1214
|
+
{
|
|
1215
|
+
version: 1,
|
|
1216
|
+
description: "Merge session_notes + smart_notes into unified notes table",
|
|
1217
|
+
up: (db) => {
|
|
1218
|
+
db.exec(`
|
|
1219
|
+
CREATE TABLE IF NOT EXISTS notes (
|
|
1220
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1221
|
+
type TEXT NOT NULL DEFAULT 'session',
|
|
1222
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
1223
|
+
content TEXT NOT NULL,
|
|
1224
|
+
session_id TEXT,
|
|
1225
|
+
project_path TEXT,
|
|
1226
|
+
surface_condition TEXT,
|
|
1227
|
+
created_at INTEGER NOT NULL,
|
|
1228
|
+
updated_at INTEGER NOT NULL,
|
|
1229
|
+
last_checked_at INTEGER,
|
|
1230
|
+
ready_at INTEGER,
|
|
1231
|
+
ready_reason TEXT,
|
|
1232
|
+
compiled_provider TEXT,
|
|
1233
|
+
compiled_config TEXT,
|
|
1234
|
+
compiled_at INTEGER,
|
|
1235
|
+
compile_status TEXT CHECK(compile_status IN ('compiled', 'plain', 'refused'))
|
|
1236
|
+
);
|
|
1237
|
+
CREATE INDEX IF NOT EXISTS idx_notes_session_status ON notes(session_id, status);
|
|
1238
|
+
CREATE INDEX IF NOT EXISTS idx_notes_project_status ON notes(project_path, status);
|
|
1239
|
+
CREATE INDEX IF NOT EXISTS idx_notes_type_status ON notes(type, status);
|
|
1240
|
+
`);
|
|
1241
|
+
const hasSessionNotes = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='session_notes'").get();
|
|
1242
|
+
if (hasSessionNotes) {
|
|
1243
|
+
db.exec(`
|
|
1244
|
+
INSERT INTO notes (type, status, content, session_id, created_at, updated_at)
|
|
1245
|
+
SELECT 'session', 'active', content, session_id, created_at, created_at
|
|
1246
|
+
FROM session_notes
|
|
1247
|
+
`);
|
|
1248
|
+
}
|
|
1249
|
+
const hasSmartNotes = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='smart_notes'").get();
|
|
1250
|
+
if (hasSmartNotes) {
|
|
1251
|
+
db.exec(`
|
|
1252
|
+
INSERT INTO notes (type, status, content, session_id, project_path, surface_condition,
|
|
1253
|
+
created_at, updated_at, last_checked_at, ready_at, ready_reason)
|
|
1254
|
+
SELECT 'smart', status, content, created_session_id, project_path, surface_condition,
|
|
1255
|
+
created_at, updated_at, last_checked_at, ready_at, ready_reason
|
|
1256
|
+
FROM smart_notes
|
|
1257
|
+
`);
|
|
1258
|
+
}
|
|
1259
|
+
if (hasSessionNotes) {
|
|
1260
|
+
const sourceCount = db.prepare("SELECT COUNT(*) as c FROM session_notes").get().c;
|
|
1261
|
+
const migratedCount = db.prepare("SELECT COUNT(*) as c FROM notes WHERE type = 'session'").get().c;
|
|
1262
|
+
if (migratedCount >= sourceCount) {
|
|
1263
|
+
db.exec("DROP TABLE session_notes");
|
|
1264
|
+
} else {
|
|
1265
|
+
throw new Error(`session_notes migration verification failed: expected ${sourceCount} rows, got ${migratedCount}`);
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
if (hasSmartNotes) {
|
|
1269
|
+
const sourceCount = db.prepare("SELECT COUNT(*) as c FROM smart_notes").get().c;
|
|
1270
|
+
const migratedCount = db.prepare("SELECT COUNT(*) as c FROM notes WHERE type = 'smart'").get().c;
|
|
1271
|
+
if (migratedCount >= sourceCount) {
|
|
1272
|
+
db.exec("DROP TABLE smart_notes");
|
|
1273
|
+
} else {
|
|
1274
|
+
throw new Error(`smart_notes migration verification failed: expected ${sourceCount} rows, got ${migratedCount}`);
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
},
|
|
1279
|
+
{
|
|
1280
|
+
version: 2,
|
|
1281
|
+
description: "Add plugin_messages table for TUI ↔ server communication",
|
|
1282
|
+
up: (db) => {
|
|
1283
|
+
db.exec(`
|
|
1284
|
+
CREATE TABLE IF NOT EXISTS plugin_messages (
|
|
1285
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1286
|
+
direction TEXT NOT NULL,
|
|
1287
|
+
type TEXT NOT NULL,
|
|
1288
|
+
payload TEXT NOT NULL DEFAULT '{}',
|
|
1289
|
+
session_id TEXT,
|
|
1290
|
+
created_at INTEGER NOT NULL,
|
|
1291
|
+
consumed_at INTEGER
|
|
1292
|
+
);
|
|
1293
|
+
CREATE INDEX IF NOT EXISTS idx_plugin_messages_direction_consumed
|
|
1294
|
+
ON plugin_messages(direction, consumed_at);
|
|
1295
|
+
CREATE INDEX IF NOT EXISTS idx_plugin_messages_created
|
|
1296
|
+
ON plugin_messages(created_at);
|
|
1297
|
+
`);
|
|
1298
|
+
}
|
|
1299
|
+
},
|
|
1300
|
+
{
|
|
1301
|
+
version: 3,
|
|
1302
|
+
description: "Add user_memory_candidates and user_memories tables",
|
|
1303
|
+
up: (db) => {
|
|
1304
|
+
db.exec(`
|
|
1305
|
+
CREATE TABLE IF NOT EXISTS user_memory_candidates (
|
|
1306
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1307
|
+
content TEXT NOT NULL,
|
|
1308
|
+
session_id TEXT NOT NULL,
|
|
1309
|
+
source_compartment_start INTEGER,
|
|
1310
|
+
source_compartment_end INTEGER,
|
|
1311
|
+
created_at INTEGER NOT NULL
|
|
1312
|
+
);
|
|
1313
|
+
CREATE INDEX IF NOT EXISTS idx_umc_created ON user_memory_candidates(created_at);
|
|
1314
|
+
|
|
1315
|
+
CREATE TABLE IF NOT EXISTS user_memories (
|
|
1316
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1317
|
+
content TEXT NOT NULL,
|
|
1318
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
1319
|
+
promoted_at INTEGER NOT NULL,
|
|
1320
|
+
source_candidate_ids TEXT DEFAULT '[]',
|
|
1321
|
+
source_candidate_provenance TEXT,
|
|
1322
|
+
created_at INTEGER NOT NULL,
|
|
1323
|
+
updated_at INTEGER NOT NULL
|
|
1324
|
+
);
|
|
1325
|
+
CREATE INDEX IF NOT EXISTS idx_um_status ON user_memories(status);
|
|
1326
|
+
`);
|
|
1327
|
+
}
|
|
1328
|
+
},
|
|
1329
|
+
{
|
|
1330
|
+
version: 4,
|
|
1331
|
+
description: "Add git_commits + git_commit_embeddings + git_commits_fts tables",
|
|
1332
|
+
up: (db) => {
|
|
1333
|
+
db.exec(`
|
|
1334
|
+
CREATE TABLE IF NOT EXISTS git_commits (
|
|
1335
|
+
sha TEXT PRIMARY KEY,
|
|
1336
|
+
project_path TEXT NOT NULL,
|
|
1337
|
+
short_sha TEXT NOT NULL,
|
|
1338
|
+
message TEXT NOT NULL,
|
|
1339
|
+
author TEXT,
|
|
1340
|
+
committed_at INTEGER NOT NULL,
|
|
1341
|
+
indexed_at INTEGER NOT NULL
|
|
1342
|
+
);
|
|
1343
|
+
CREATE INDEX IF NOT EXISTS idx_git_commits_project_time
|
|
1344
|
+
ON git_commits(project_path, committed_at DESC);
|
|
1345
|
+
|
|
1346
|
+
CREATE TABLE IF NOT EXISTS git_commit_embeddings (
|
|
1347
|
+
sha TEXT PRIMARY KEY,
|
|
1348
|
+
embedding BLOB NOT NULL,
|
|
1349
|
+
model_id TEXT NOT NULL,
|
|
1350
|
+
created_at INTEGER NOT NULL,
|
|
1351
|
+
-- FK-cascade audit (v12): git_commit_embeddings.sha -> git_commits.sha
|
|
1352
|
+
-- uses ON DELETE CASCADE, so SQLite PRAGMA foreign_keys must be ON on
|
|
1353
|
+
-- every connection and v12 cleans historical orphan rows.
|
|
1354
|
+
FOREIGN KEY(sha) REFERENCES git_commits(sha) ON DELETE CASCADE
|
|
1355
|
+
);
|
|
1356
|
+
|
|
1357
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS git_commits_fts USING fts5(
|
|
1358
|
+
sha UNINDEXED,
|
|
1359
|
+
project_path UNINDEXED,
|
|
1360
|
+
message,
|
|
1361
|
+
tokenize = 'porter unicode61'
|
|
1362
|
+
);
|
|
1363
|
+
|
|
1364
|
+
-- Mirror writes into FTS. We intentionally rebuild FTS rows on
|
|
1365
|
+
-- every INSERT OR REPLACE so amended commits or re-indexed
|
|
1366
|
+
-- messages update cleanly.
|
|
1367
|
+
CREATE TRIGGER IF NOT EXISTS git_commits_fts_insert
|
|
1368
|
+
AFTER INSERT ON git_commits BEGIN
|
|
1369
|
+
DELETE FROM git_commits_fts WHERE sha = NEW.sha;
|
|
1370
|
+
INSERT INTO git_commits_fts(sha, project_path, message)
|
|
1371
|
+
VALUES (NEW.sha, NEW.project_path, NEW.message);
|
|
1372
|
+
END;
|
|
1373
|
+
|
|
1374
|
+
CREATE TRIGGER IF NOT EXISTS git_commits_fts_delete
|
|
1375
|
+
AFTER DELETE ON git_commits BEGIN
|
|
1376
|
+
DELETE FROM git_commits_fts WHERE sha = OLD.sha;
|
|
1377
|
+
END;
|
|
1378
|
+
|
|
1379
|
+
CREATE TRIGGER IF NOT EXISTS git_commits_fts_update
|
|
1380
|
+
AFTER UPDATE OF message, project_path ON git_commits BEGIN
|
|
1381
|
+
DELETE FROM git_commits_fts WHERE sha = OLD.sha;
|
|
1382
|
+
INSERT INTO git_commits_fts(sha, project_path, message)
|
|
1383
|
+
VALUES (NEW.sha, NEW.project_path, NEW.message);
|
|
1384
|
+
END;
|
|
1385
|
+
`);
|
|
1386
|
+
}
|
|
1387
|
+
},
|
|
1388
|
+
{
|
|
1389
|
+
version: 5,
|
|
1390
|
+
description: "One-shot heal of NULL session_meta columns",
|
|
1391
|
+
up: (db) => {
|
|
1392
|
+
healAllNullColumns(db);
|
|
1393
|
+
}
|
|
1394
|
+
},
|
|
1395
|
+
{
|
|
1396
|
+
version: 6,
|
|
1397
|
+
description: "Heal session_meta.counter drift below MAX(tag_number)",
|
|
1398
|
+
up: (db) => {
|
|
1399
|
+
db.prepare(`UPDATE session_meta
|
|
1400
|
+
SET counter = (
|
|
1401
|
+
SELECT MAX(tag_number)
|
|
1402
|
+
FROM tags
|
|
1403
|
+
WHERE tags.session_id = session_meta.session_id
|
|
1404
|
+
)
|
|
1405
|
+
WHERE EXISTS (
|
|
1406
|
+
SELECT 1
|
|
1407
|
+
FROM tags
|
|
1408
|
+
WHERE tags.session_id = session_meta.session_id
|
|
1409
|
+
AND tags.tag_number > session_meta.counter
|
|
1410
|
+
)`).run();
|
|
1411
|
+
}
|
|
1412
|
+
},
|
|
1413
|
+
{
|
|
1414
|
+
version: 7,
|
|
1415
|
+
description: "Add harness column to notes table for cross-harness sharing",
|
|
1416
|
+
up: (db) => {
|
|
1417
|
+
const cols = db.prepare("PRAGMA table_info(notes)").all();
|
|
1418
|
+
if (!cols.some((c) => c.name === "harness")) {
|
|
1419
|
+
db.exec("ALTER TABLE notes ADD COLUMN harness TEXT NOT NULL DEFAULT 'opencode'");
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
},
|
|
1423
|
+
{
|
|
1424
|
+
version: 8,
|
|
1425
|
+
description: "Add partial indexes on tags(session_id, tag_number) for active and dropped",
|
|
1426
|
+
up: (db) => {
|
|
1427
|
+
db.exec(`
|
|
1428
|
+
CREATE INDEX IF NOT EXISTS idx_tags_active_session_tag_number
|
|
1429
|
+
ON tags(session_id, tag_number)
|
|
1430
|
+
WHERE status = 'active';
|
|
1431
|
+
|
|
1432
|
+
CREATE INDEX IF NOT EXISTS idx_tags_dropped_session_tag_number
|
|
1433
|
+
ON tags(session_id, tag_number)
|
|
1434
|
+
WHERE status = 'dropped';
|
|
1435
|
+
`);
|
|
1436
|
+
db.exec("ANALYZE tags;");
|
|
1437
|
+
}
|
|
1438
|
+
},
|
|
1439
|
+
{
|
|
1440
|
+
version: 9,
|
|
1441
|
+
description: "Persist tool_definition_measurements across plugin restarts",
|
|
1442
|
+
up: (db) => {
|
|
1443
|
+
db.exec(`
|
|
1444
|
+
CREATE TABLE IF NOT EXISTS tool_definition_measurements (
|
|
1445
|
+
provider_id TEXT NOT NULL,
|
|
1446
|
+
model_id TEXT NOT NULL,
|
|
1447
|
+
agent_name TEXT NOT NULL,
|
|
1448
|
+
tool_id TEXT NOT NULL,
|
|
1449
|
+
token_count INTEGER NOT NULL,
|
|
1450
|
+
recorded_at INTEGER NOT NULL,
|
|
1451
|
+
PRIMARY KEY (provider_id, model_id, agent_name, tool_id)
|
|
1452
|
+
);
|
|
1453
|
+
`);
|
|
1454
|
+
}
|
|
1455
|
+
},
|
|
1456
|
+
{
|
|
1457
|
+
version: 10,
|
|
1458
|
+
description: "Add tool_owner_message_id column to tags + composite identity indexes",
|
|
1459
|
+
up: (db) => {
|
|
1460
|
+
const cols = db.prepare("PRAGMA table_info(tags)").all();
|
|
1461
|
+
if (!cols.some((c) => c.name === "tool_owner_message_id")) {
|
|
1462
|
+
db.exec("ALTER TABLE tags ADD COLUMN tool_owner_message_id TEXT DEFAULT NULL");
|
|
1463
|
+
}
|
|
1464
|
+
db.exec(`
|
|
1465
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_tags_tool_composite
|
|
1466
|
+
ON tags(session_id, message_id, tool_owner_message_id)
|
|
1467
|
+
WHERE type = 'tool' AND tool_owner_message_id IS NOT NULL;
|
|
1468
|
+
|
|
1469
|
+
CREATE INDEX IF NOT EXISTS idx_tags_tool_null_owner
|
|
1470
|
+
ON tags(session_id, message_id)
|
|
1471
|
+
WHERE type = 'tool' AND tool_owner_message_id IS NULL;
|
|
1472
|
+
`);
|
|
1473
|
+
}
|
|
1474
|
+
},
|
|
1475
|
+
{
|
|
1476
|
+
version: 11,
|
|
1477
|
+
description: "Add todo state synthesis columns to session_meta",
|
|
1478
|
+
up: (db) => {
|
|
1479
|
+
const cols = db.prepare("PRAGMA table_info(session_meta)").all();
|
|
1480
|
+
if (!cols.some((c) => c.name === "last_todo_state")) {
|
|
1481
|
+
db.exec("ALTER TABLE session_meta ADD COLUMN last_todo_state TEXT DEFAULT ''");
|
|
1482
|
+
}
|
|
1483
|
+
if (!cols.some((c) => c.name === "todo_synthetic_call_id")) {
|
|
1484
|
+
db.exec("ALTER TABLE session_meta ADD COLUMN todo_synthetic_call_id TEXT DEFAULT ''");
|
|
1485
|
+
}
|
|
1486
|
+
if (!cols.some((c) => c.name === "todo_synthetic_anchor_message_id")) {
|
|
1487
|
+
db.exec("ALTER TABLE session_meta ADD COLUMN todo_synthetic_anchor_message_id TEXT DEFAULT ''");
|
|
1488
|
+
}
|
|
1489
|
+
if (!cols.some((c) => c.name === "todo_synthetic_state_json")) {
|
|
1490
|
+
db.exec("ALTER TABLE session_meta ADD COLUMN todo_synthetic_state_json TEXT DEFAULT ''");
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
},
|
|
1494
|
+
{
|
|
1495
|
+
version: 12,
|
|
1496
|
+
description: "Clean orphan rows from FK-cascade embedding tables",
|
|
1497
|
+
up: (db) => {
|
|
1498
|
+
const hasTable = (name) => Boolean(db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name = ?").get(name));
|
|
1499
|
+
const memoryEmbeddings = hasTable("memory_embeddings") ? db.prepare(`DELETE FROM memory_embeddings
|
|
1500
|
+
WHERE memory_id NOT IN (SELECT id FROM memories)`).run().changes : 0;
|
|
1501
|
+
log2(`[migrations] v12 cleaned ${memoryEmbeddings} orphan memory_embeddings row(s)`);
|
|
1502
|
+
const gitCommitEmbeddings = hasTable("git_commit_embeddings") ? db.prepare(`DELETE FROM git_commit_embeddings
|
|
1503
|
+
WHERE sha NOT IN (SELECT sha FROM git_commits)`).run().changes : 0;
|
|
1504
|
+
log2(`[migrations] v12 cleaned ${gitCommitEmbeddings} orphan git_commit_embeddings row(s)`);
|
|
1505
|
+
}
|
|
1506
|
+
},
|
|
1507
|
+
{
|
|
1508
|
+
version: 13,
|
|
1509
|
+
description: "Add pending_compaction_marker_state column for deferred marker drain",
|
|
1510
|
+
up: (db) => {
|
|
1511
|
+
const cols = db.prepare("PRAGMA table_info(session_meta)").all();
|
|
1512
|
+
if (!cols.some((c) => c.name === "pending_compaction_marker_state")) {
|
|
1513
|
+
db.exec("ALTER TABLE session_meta ADD COLUMN pending_compaction_marker_state TEXT");
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
},
|
|
1517
|
+
{
|
|
1518
|
+
version: 14,
|
|
1519
|
+
description: "Add project-scoped key files and version counter",
|
|
1520
|
+
up: (db) => {
|
|
1521
|
+
db.exec(`
|
|
1522
|
+
CREATE TABLE IF NOT EXISTS project_key_files (
|
|
1523
|
+
project_path TEXT NOT NULL,
|
|
1524
|
+
path TEXT NOT NULL,
|
|
1525
|
+
content TEXT NOT NULL,
|
|
1526
|
+
content_hash TEXT NOT NULL,
|
|
1527
|
+
local_token_estimate INTEGER NOT NULL,
|
|
1528
|
+
generated_at INTEGER NOT NULL,
|
|
1529
|
+
generated_by_model TEXT,
|
|
1530
|
+
generation_config_hash TEXT NOT NULL,
|
|
1531
|
+
stale_reason TEXT,
|
|
1532
|
+
PRIMARY KEY (project_path, path)
|
|
1533
|
+
);
|
|
1534
|
+
|
|
1535
|
+
CREATE INDEX IF NOT EXISTS idx_project_key_files_project
|
|
1536
|
+
ON project_key_files(project_path);
|
|
1537
|
+
CREATE INDEX IF NOT EXISTS idx_project_key_files_generated_at
|
|
1538
|
+
ON project_key_files(project_path, generated_at);
|
|
1539
|
+
|
|
1540
|
+
CREATE TABLE IF NOT EXISTS project_key_files_version (
|
|
1541
|
+
project_path TEXT PRIMARY KEY,
|
|
1542
|
+
version INTEGER NOT NULL DEFAULT 0
|
|
1543
|
+
);
|
|
1544
|
+
`);
|
|
1545
|
+
}
|
|
1546
|
+
},
|
|
1547
|
+
{
|
|
1548
|
+
version: 15,
|
|
1549
|
+
description: "Add the now-retired deferred_execute_state column",
|
|
1550
|
+
up: (db) => {
|
|
1551
|
+
const cols = db.prepare("PRAGMA table_info(session_meta)").all();
|
|
1552
|
+
if (!cols.some((c) => c.name === "deferred_execute_state")) {
|
|
1553
|
+
db.exec("ALTER TABLE session_meta ADD COLUMN deferred_execute_state TEXT");
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
},
|
|
1557
|
+
{
|
|
1558
|
+
version: 16,
|
|
1559
|
+
description: "Add context-limit cache regression sentinels",
|
|
1560
|
+
up: (db) => {
|
|
1561
|
+
const cols = db.prepare("PRAGMA table_info(session_meta)").all();
|
|
1562
|
+
if (!cols.some((c) => c.name === "observed_safe_input_tokens")) {
|
|
1563
|
+
db.exec("ALTER TABLE session_meta ADD COLUMN observed_safe_input_tokens INTEGER NOT NULL DEFAULT 0");
|
|
1564
|
+
}
|
|
1565
|
+
if (!cols.some((c) => c.name === "cache_alert_sent")) {
|
|
1566
|
+
db.exec("ALTER TABLE session_meta ADD COLUMN cache_alert_sent INTEGER NOT NULL DEFAULT 0");
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
},
|
|
1570
|
+
{
|
|
1571
|
+
version: 17,
|
|
1572
|
+
description: "Multi-anchor JSON storage for note-nudge and auto-search-hint persistence",
|
|
1573
|
+
up: (db) => {
|
|
1574
|
+
const cols = db.prepare("PRAGMA table_info(session_meta)").all();
|
|
1575
|
+
if (!cols.some((c) => c.name === "note_nudge_anchors")) {
|
|
1576
|
+
db.exec("ALTER TABLE session_meta ADD COLUMN note_nudge_anchors TEXT NOT NULL DEFAULT '[]'");
|
|
1577
|
+
}
|
|
1578
|
+
if (!cols.some((c) => c.name === "auto_search_hint_decisions")) {
|
|
1579
|
+
db.exec("ALTER TABLE session_meta ADD COLUMN auto_search_hint_decisions TEXT NOT NULL DEFAULT '[]'");
|
|
1580
|
+
}
|
|
1581
|
+
db.exec(`
|
|
1582
|
+
UPDATE session_meta
|
|
1583
|
+
SET note_nudge_anchors = json_array(
|
|
1584
|
+
json_object(
|
|
1585
|
+
'messageId', note_nudge_sticky_message_id,
|
|
1586
|
+
'text', note_nudge_sticky_text
|
|
1587
|
+
)
|
|
1588
|
+
)
|
|
1589
|
+
WHERE COALESCE(note_nudge_sticky_text, '') != ''
|
|
1590
|
+
AND COALESCE(note_nudge_sticky_message_id, '') != ''
|
|
1591
|
+
AND (note_nudge_anchors IS NULL OR note_nudge_anchors = '[]')
|
|
1592
|
+
`);
|
|
1593
|
+
db.exec(`
|
|
1594
|
+
UPDATE session_meta SET note_nudge_anchors = '[]'
|
|
1595
|
+
WHERE note_nudge_anchors IS NULL
|
|
1596
|
+
`);
|
|
1597
|
+
db.exec(`
|
|
1598
|
+
UPDATE session_meta SET auto_search_hint_decisions = '[]'
|
|
1599
|
+
WHERE auto_search_hint_decisions IS NULL
|
|
1600
|
+
`);
|
|
1601
|
+
}
|
|
1602
|
+
},
|
|
1603
|
+
{
|
|
1604
|
+
version: 18,
|
|
1605
|
+
description: "Add pending_pi_compaction_marker_state column for Pi deferred marker drain",
|
|
1606
|
+
up: (db) => {
|
|
1607
|
+
const cols = db.prepare("PRAGMA table_info(session_meta)").all();
|
|
1608
|
+
if (!cols.some((c) => c.name === "pending_pi_compaction_marker_state")) {
|
|
1609
|
+
db.exec("ALTER TABLE session_meta ADD COLUMN pending_pi_compaction_marker_state TEXT");
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
},
|
|
1613
|
+
{
|
|
1614
|
+
version: 19,
|
|
1615
|
+
description: "Add compartment state lease table",
|
|
1616
|
+
up: (db) => {
|
|
1617
|
+
db.exec(`
|
|
1618
|
+
CREATE TABLE IF NOT EXISTS compartment_state_lease (
|
|
1619
|
+
session_id TEXT PRIMARY KEY NOT NULL,
|
|
1620
|
+
holder_id TEXT NOT NULL,
|
|
1621
|
+
acquired_at INTEGER NOT NULL,
|
|
1622
|
+
expires_at INTEGER NOT NULL
|
|
1623
|
+
);
|
|
1624
|
+
CREATE INDEX IF NOT EXISTS idx_compartment_state_lease_expires
|
|
1625
|
+
ON compartment_state_lease(expires_at);
|
|
1626
|
+
`);
|
|
1627
|
+
}
|
|
1628
|
+
},
|
|
1629
|
+
{
|
|
1630
|
+
version: 20,
|
|
1631
|
+
description: "Add subagent invocation token accounting",
|
|
1632
|
+
up: (db) => {
|
|
1633
|
+
db.exec(`
|
|
1634
|
+
CREATE TABLE IF NOT EXISTS subagent_invocations (
|
|
1635
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1636
|
+
session_id TEXT NOT NULL,
|
|
1637
|
+
harness TEXT NOT NULL,
|
|
1638
|
+
subagent TEXT NOT NULL,
|
|
1639
|
+
task TEXT,
|
|
1640
|
+
provider_id TEXT,
|
|
1641
|
+
model_id TEXT,
|
|
1642
|
+
started_at INTEGER NOT NULL,
|
|
1643
|
+
ended_at INTEGER,
|
|
1644
|
+
status TEXT NOT NULL,
|
|
1645
|
+
input_tokens INTEGER NOT NULL DEFAULT 0,
|
|
1646
|
+
output_tokens INTEGER NOT NULL DEFAULT 0,
|
|
1647
|
+
cache_read_tokens INTEGER NOT NULL DEFAULT 0,
|
|
1648
|
+
cache_write_tokens INTEGER NOT NULL DEFAULT 0,
|
|
1649
|
+
error TEXT,
|
|
1650
|
+
parent_invocation_id INTEGER
|
|
1651
|
+
);
|
|
1652
|
+
CREATE INDEX IF NOT EXISTS idx_sai_session_started
|
|
1653
|
+
ON subagent_invocations(session_id, started_at DESC);
|
|
1654
|
+
CREATE INDEX IF NOT EXISTS idx_sai_subagent
|
|
1655
|
+
ON subagent_invocations(subagent, started_at DESC);
|
|
1656
|
+
`);
|
|
1657
|
+
}
|
|
1658
|
+
},
|
|
1659
|
+
{
|
|
1660
|
+
version: 21,
|
|
1661
|
+
description: "Add session lifetime work metrics",
|
|
1662
|
+
up: (db) => {
|
|
1663
|
+
const cols = db.prepare("PRAGMA table_info(session_meta)").all();
|
|
1664
|
+
if (!cols.some((c) => c.name === "new_work_tokens")) {
|
|
1665
|
+
db.exec("ALTER TABLE session_meta ADD COLUMN new_work_tokens INTEGER NOT NULL DEFAULT 0");
|
|
1666
|
+
}
|
|
1667
|
+
if (!cols.some((c) => c.name === "total_input_tokens")) {
|
|
1668
|
+
db.exec("ALTER TABLE session_meta ADD COLUMN total_input_tokens INTEGER NOT NULL DEFAULT 0");
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
},
|
|
1672
|
+
{
|
|
1673
|
+
version: 22,
|
|
1674
|
+
description: "v2.0 cache architecture schema foundation",
|
|
1675
|
+
up: (db) => {
|
|
1676
|
+
const hasSessionMetaTable = tableExists4(db, "session_meta");
|
|
1677
|
+
const hasCompartmentsTable = tableExists4(db, "compartments");
|
|
1678
|
+
const hasMemoriesTable = tableExists4(db, "memories");
|
|
1679
|
+
if (hasSessionMetaTable) {
|
|
1680
|
+
ensureColumn(db, "session_meta", "cached_m0_bytes", "BLOB");
|
|
1681
|
+
ensureColumn(db, "session_meta", "cached_m0_project_memory_epoch", "INTEGER");
|
|
1682
|
+
ensureColumn(db, "session_meta", "cached_m0_project_user_profile_version", "INTEGER");
|
|
1683
|
+
ensureColumn(db, "session_meta", "cached_m0_max_compartment_seq", "INTEGER");
|
|
1684
|
+
ensureColumn(db, "session_meta", "cached_m0_max_memory_id", "INTEGER");
|
|
1685
|
+
ensureColumn(db, "session_meta", "cached_m0_max_mutation_id", "INTEGER");
|
|
1686
|
+
ensureColumn(db, "session_meta", "cached_m0_project_docs_hash", "TEXT");
|
|
1687
|
+
ensureColumn(db, "session_meta", "cached_m0_materialized_at", "INTEGER");
|
|
1688
|
+
ensureColumn(db, "session_meta", "cached_m0_session_facts_version", "INTEGER");
|
|
1689
|
+
ensureColumn(db, "session_meta", "cached_m0_upgrade_state", "TEXT");
|
|
1690
|
+
ensureColumn(db, "session_meta", "upgrade_reminded_at", "INTEGER");
|
|
1691
|
+
}
|
|
1692
|
+
if (hasCompartmentsTable) {
|
|
1693
|
+
ensureColumn(db, "compartments", "p1", "TEXT");
|
|
1694
|
+
ensureColumn(db, "compartments", "p2", "TEXT");
|
|
1695
|
+
ensureColumn(db, "compartments", "p3", "TEXT");
|
|
1696
|
+
ensureColumn(db, "compartments", "p4", "TEXT");
|
|
1697
|
+
ensureColumn(db, "compartments", "importance", "INTEGER NOT NULL DEFAULT 50");
|
|
1698
|
+
ensureColumn(db, "compartments", "episode_type", "TEXT");
|
|
1699
|
+
ensureColumn(db, "compartments", "p1_embedding", "BLOB");
|
|
1700
|
+
ensureColumn(db, "compartments", "p1_embedding_model_id", "TEXT");
|
|
1701
|
+
ensureColumn(db, "compartments", "legacy", "INTEGER NOT NULL DEFAULT 0");
|
|
1702
|
+
}
|
|
1703
|
+
const hasRecompCompartmentsTable = tableExists4(db, "recomp_compartments");
|
|
1704
|
+
if (hasRecompCompartmentsTable) {
|
|
1705
|
+
ensureColumn(db, "recomp_compartments", "p1", "TEXT");
|
|
1706
|
+
ensureColumn(db, "recomp_compartments", "p2", "TEXT");
|
|
1707
|
+
ensureColumn(db, "recomp_compartments", "p3", "TEXT");
|
|
1708
|
+
ensureColumn(db, "recomp_compartments", "p4", "TEXT");
|
|
1709
|
+
ensureColumn(db, "recomp_compartments", "importance", "INTEGER NOT NULL DEFAULT 50");
|
|
1710
|
+
ensureColumn(db, "recomp_compartments", "episode_type", "TEXT");
|
|
1711
|
+
}
|
|
1712
|
+
if (hasMemoriesTable) {
|
|
1713
|
+
ensureColumn(db, "memories", "importance", "INTEGER");
|
|
1714
|
+
}
|
|
1715
|
+
db.exec(`
|
|
1716
|
+
CREATE TABLE IF NOT EXISTS schema_migrations_meta (
|
|
1717
|
+
key TEXT PRIMARY KEY,
|
|
1718
|
+
value TEXT NOT NULL
|
|
1719
|
+
);
|
|
1720
|
+
|
|
1721
|
+
CREATE TABLE IF NOT EXISTS project_state (
|
|
1722
|
+
project_path TEXT PRIMARY KEY,
|
|
1723
|
+
project_memory_epoch INTEGER NOT NULL DEFAULT 0,
|
|
1724
|
+
project_user_profile_version INTEGER NOT NULL DEFAULT 0,
|
|
1725
|
+
updated_at INTEGER NOT NULL DEFAULT 0
|
|
1726
|
+
);
|
|
1727
|
+
|
|
1728
|
+
CREATE TABLE IF NOT EXISTS m0_mutation_log (
|
|
1729
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1730
|
+
session_id TEXT NOT NULL,
|
|
1731
|
+
mutation_type TEXT NOT NULL CHECK (mutation_type IN (
|
|
1732
|
+
'compartment_delete',
|
|
1733
|
+
'compartment_merge',
|
|
1734
|
+
'recomp_boundary_change',
|
|
1735
|
+
'compartment_upgrade'
|
|
1736
|
+
)),
|
|
1737
|
+
target_id INTEGER,
|
|
1738
|
+
queued_at INTEGER NOT NULL
|
|
1739
|
+
);
|
|
1740
|
+
CREATE INDEX IF NOT EXISTS idx_m0_mutation_log_session
|
|
1741
|
+
ON m0_mutation_log(session_id);
|
|
1742
|
+
|
|
1743
|
+
CREATE TABLE IF NOT EXISTS v22_identity_rekey_map (
|
|
1744
|
+
old_project_path TEXT PRIMARY KEY,
|
|
1745
|
+
new_project_path TEXT NOT NULL,
|
|
1746
|
+
rekeyed_at INTEGER NOT NULL
|
|
1747
|
+
);
|
|
1748
|
+
|
|
1749
|
+
CREATE TABLE IF NOT EXISTS v22_backfill_failures (
|
|
1750
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1751
|
+
table_name TEXT NOT NULL,
|
|
1752
|
+
row_id INTEGER NOT NULL,
|
|
1753
|
+
raw_project_path TEXT NOT NULL,
|
|
1754
|
+
error_class TEXT NOT NULL CHECK (error_class IN (
|
|
1755
|
+
'not_git_repo',
|
|
1756
|
+
'git_missing',
|
|
1757
|
+
'git_timeout',
|
|
1758
|
+
'permission_denied',
|
|
1759
|
+
'unknown'
|
|
1760
|
+
)),
|
|
1761
|
+
error_message TEXT,
|
|
1762
|
+
failed_at INTEGER NOT NULL,
|
|
1763
|
+
UNIQUE(table_name, row_id)
|
|
1764
|
+
);
|
|
1765
|
+
`);
|
|
1766
|
+
if (hasCompartmentsTable) {
|
|
1767
|
+
db.exec(`
|
|
1768
|
+
INSERT OR IGNORE INTO schema_migrations_meta (key, value)
|
|
1769
|
+
SELECT 'v22_legacy_compartment_boundary', CAST(COALESCE(MAX(id), 0) AS TEXT)
|
|
1770
|
+
FROM compartments
|
|
1771
|
+
`);
|
|
1772
|
+
const boundaryRow = db.prepare("SELECT value FROM schema_migrations_meta WHERE key = 'v22_legacy_compartment_boundary'").get();
|
|
1773
|
+
const compartmentBoundary = Number.parseInt(boundaryRow?.value ?? "0", 10);
|
|
1774
|
+
db.prepare("UPDATE compartments SET legacy = 1 WHERE legacy = 0 AND id <= ?").run(Number.isFinite(compartmentBoundary) ? compartmentBoundary : 0);
|
|
1775
|
+
} else {
|
|
1776
|
+
db.prepare("INSERT OR IGNORE INTO schema_migrations_meta (key, value) VALUES ('v22_legacy_compartment_boundary', '0')").run();
|
|
1777
|
+
}
|
|
1778
|
+
db.prepare("INSERT OR IGNORE INTO schema_migrations_meta (key, value) VALUES ('v22_legacy_memory_backfill', 'pending')").run();
|
|
1779
|
+
}
|
|
1780
|
+
},
|
|
1781
|
+
{
|
|
1782
|
+
version: 23,
|
|
1783
|
+
description: "v2 compartment events storage (causal_incident / trajectory_correction)",
|
|
1784
|
+
up: (db) => {
|
|
1785
|
+
db.exec(`
|
|
1786
|
+
CREATE TABLE IF NOT EXISTS compartment_events (
|
|
1787
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1788
|
+
session_id TEXT NOT NULL,
|
|
1789
|
+
compartment_id INTEGER,
|
|
1790
|
+
kind TEXT NOT NULL,
|
|
1791
|
+
at_compartment INTEGER,
|
|
1792
|
+
fields_json TEXT NOT NULL DEFAULT '{}',
|
|
1793
|
+
created_at INTEGER NOT NULL,
|
|
1794
|
+
harness TEXT NOT NULL DEFAULT 'opencode'
|
|
1795
|
+
);
|
|
1796
|
+
CREATE INDEX IF NOT EXISTS idx_compartment_events_session
|
|
1797
|
+
ON compartment_events(session_id);
|
|
1798
|
+
`);
|
|
1799
|
+
}
|
|
1800
|
+
},
|
|
1801
|
+
{
|
|
1802
|
+
version: 24,
|
|
1803
|
+
description: "historian_runs metrics (per-run quality/cost telemetry)",
|
|
1804
|
+
up: (db) => {
|
|
1805
|
+
db.exec(`
|
|
1806
|
+
CREATE TABLE IF NOT EXISTS historian_runs (
|
|
1807
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1808
|
+
session_id TEXT NOT NULL,
|
|
1809
|
+
harness TEXT NOT NULL DEFAULT 'opencode',
|
|
1810
|
+
subagent_invocation_id INTEGER,
|
|
1811
|
+
run_kind TEXT NOT NULL,
|
|
1812
|
+
status TEXT NOT NULL,
|
|
1813
|
+
failure_reason TEXT,
|
|
1814
|
+
chunk_start_ordinal INTEGER,
|
|
1815
|
+
chunk_end_ordinal INTEGER,
|
|
1816
|
+
unprocessed_from INTEGER,
|
|
1817
|
+
compartments_produced INTEGER NOT NULL DEFAULT 0,
|
|
1818
|
+
compartment_id_min INTEGER,
|
|
1819
|
+
compartment_id_max INTEGER,
|
|
1820
|
+
facts_emitted INTEGER NOT NULL DEFAULT 0,
|
|
1821
|
+
facts_by_category_json TEXT,
|
|
1822
|
+
events_emitted INTEGER NOT NULL DEFAULT 0,
|
|
1823
|
+
importance_min INTEGER,
|
|
1824
|
+
importance_max INTEGER,
|
|
1825
|
+
importance_avg REAL,
|
|
1826
|
+
discarded_last INTEGER NOT NULL DEFAULT 0,
|
|
1827
|
+
legacy INTEGER NOT NULL DEFAULT 0,
|
|
1828
|
+
created_at INTEGER NOT NULL
|
|
1829
|
+
);
|
|
1830
|
+
CREATE INDEX IF NOT EXISTS idx_historian_runs_session
|
|
1831
|
+
ON historian_runs(session_id, created_at DESC);
|
|
1832
|
+
CREATE INDEX IF NOT EXISTS idx_historian_runs_status
|
|
1833
|
+
ON historian_runs(status, created_at DESC);
|
|
1834
|
+
`);
|
|
1835
|
+
}
|
|
1836
|
+
},
|
|
1837
|
+
{
|
|
1838
|
+
version: 25,
|
|
1839
|
+
description: "pi_stable_id_scheme session_meta column (Pi message-id cutover gate)",
|
|
1840
|
+
up: (db) => {
|
|
1841
|
+
const rows = db.prepare("PRAGMA table_info(session_meta)").all();
|
|
1842
|
+
if (!rows.some((row) => row.name === "pi_stable_id_scheme")) {
|
|
1843
|
+
db.exec("ALTER TABLE session_meta ADD COLUMN pi_stable_id_scheme INTEGER");
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
},
|
|
1847
|
+
{
|
|
1848
|
+
version: 26,
|
|
1849
|
+
description: "memory mutation log and atomic m[1] cache columns",
|
|
1850
|
+
up: (db) => {
|
|
1851
|
+
db.exec(`
|
|
1852
|
+
CREATE TABLE IF NOT EXISTS memory_mutation_log (
|
|
1853
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1854
|
+
project_path TEXT NOT NULL,
|
|
1855
|
+
mutation_type TEXT NOT NULL CHECK (mutation_type IN (
|
|
1856
|
+
'archive',
|
|
1857
|
+
'delete',
|
|
1858
|
+
'update',
|
|
1859
|
+
'superseded'
|
|
1860
|
+
)),
|
|
1861
|
+
target_memory_id INTEGER NOT NULL,
|
|
1862
|
+
superseded_by_id INTEGER,
|
|
1863
|
+
category TEXT,
|
|
1864
|
+
new_content TEXT,
|
|
1865
|
+
queued_at INTEGER NOT NULL
|
|
1866
|
+
);
|
|
1867
|
+
CREATE INDEX IF NOT EXISTS idx_memory_mutation_log_project
|
|
1868
|
+
ON memory_mutation_log(project_path, id);
|
|
1869
|
+
`);
|
|
1870
|
+
ensureColumn(db, "session_meta", "cached_m0_bytes", "BLOB");
|
|
1871
|
+
ensureColumn(db, "session_meta", "cached_m0_project_memory_epoch", "INTEGER");
|
|
1872
|
+
ensureColumn(db, "session_meta", "cached_m0_project_user_profile_version", "INTEGER");
|
|
1873
|
+
ensureColumn(db, "session_meta", "cached_m0_max_compartment_seq", "INTEGER");
|
|
1874
|
+
ensureColumn(db, "session_meta", "cached_m0_max_memory_id", "INTEGER");
|
|
1875
|
+
ensureColumn(db, "session_meta", "cached_m0_max_mutation_id", "INTEGER");
|
|
1876
|
+
ensureColumn(db, "session_meta", "cached_m0_max_memory_mutation_id", "INTEGER");
|
|
1877
|
+
ensureColumn(db, "session_meta", "cached_m0_project_docs_hash", "TEXT");
|
|
1878
|
+
ensureColumn(db, "session_meta", "cached_m0_materialized_at", "INTEGER");
|
|
1879
|
+
ensureColumn(db, "session_meta", "cached_m0_session_facts_version", "INTEGER");
|
|
1880
|
+
ensureColumn(db, "session_meta", "cached_m0_upgrade_state", "TEXT");
|
|
1881
|
+
ensureColumn(db, "session_meta", "cached_m1_bytes", "BLOB");
|
|
1882
|
+
ensureColumn(db, "session_meta", "last_observed_model_key", "TEXT");
|
|
1883
|
+
ensureColumn(db, "session_meta", "memory_block_cache", "TEXT DEFAULT ''");
|
|
1884
|
+
ensureColumn(db, "session_meta", "memory_block_count", "INTEGER DEFAULT 0");
|
|
1885
|
+
ensureColumn(db, "session_meta", "memory_block_ids", "TEXT DEFAULT ''");
|
|
1886
|
+
db.prepare(`UPDATE session_meta SET
|
|
1887
|
+
cached_m0_bytes = NULL,
|
|
1888
|
+
cached_m1_bytes = NULL,
|
|
1889
|
+
cached_m0_project_memory_epoch = NULL,
|
|
1890
|
+
cached_m0_project_user_profile_version = NULL,
|
|
1891
|
+
cached_m0_max_compartment_seq = NULL,
|
|
1892
|
+
cached_m0_max_memory_id = NULL,
|
|
1893
|
+
cached_m0_max_mutation_id = NULL,
|
|
1894
|
+
cached_m0_max_memory_mutation_id = NULL,
|
|
1895
|
+
cached_m0_project_docs_hash = NULL,
|
|
1896
|
+
cached_m0_materialized_at = NULL,
|
|
1897
|
+
cached_m0_session_facts_version = NULL,
|
|
1898
|
+
cached_m0_upgrade_state = NULL,
|
|
1899
|
+
memory_block_cache = '',
|
|
1900
|
+
memory_block_count = 0,
|
|
1901
|
+
memory_block_ids = ''`).run();
|
|
1902
|
+
}
|
|
1903
|
+
},
|
|
1904
|
+
{
|
|
1905
|
+
version: 27,
|
|
1906
|
+
description: "tags.entry_fingerprint for Pi fallback-tag adoption",
|
|
1907
|
+
up: (db) => {
|
|
1908
|
+
const hasTags = db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='tags' LIMIT 1").get();
|
|
1909
|
+
if (!hasTags)
|
|
1910
|
+
return;
|
|
1911
|
+
ensureColumn(db, "tags", "entry_fingerprint", "TEXT");
|
|
1912
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_tags_pi_adopt
|
|
1913
|
+
ON tags(session_id, entry_fingerprint)
|
|
1914
|
+
WHERE type='message' AND entry_fingerprint IS NOT NULL`);
|
|
1915
|
+
}
|
|
1916
|
+
},
|
|
1917
|
+
{
|
|
1918
|
+
version: 28,
|
|
1919
|
+
description: "Add git commit sweep coordinator lease/cooldown table",
|
|
1920
|
+
up: (db) => {
|
|
1921
|
+
db.exec(`
|
|
1922
|
+
CREATE TABLE IF NOT EXISTS git_sweep_coordinator (
|
|
1923
|
+
project_path TEXT PRIMARY KEY,
|
|
1924
|
+
lease_holder TEXT,
|
|
1925
|
+
lease_expires_at INTEGER,
|
|
1926
|
+
last_swept_at INTEGER
|
|
1927
|
+
);
|
|
1928
|
+
CREATE INDEX IF NOT EXISTS idx_git_sweep_coordinator_lease_expires
|
|
1929
|
+
ON git_sweep_coordinator(lease_expires_at);
|
|
1930
|
+
CREATE INDEX IF NOT EXISTS idx_git_sweep_coordinator_last_swept
|
|
1931
|
+
ON git_sweep_coordinator(last_swept_at);
|
|
1932
|
+
`);
|
|
1933
|
+
}
|
|
1934
|
+
},
|
|
1935
|
+
{
|
|
1936
|
+
version: 29,
|
|
1937
|
+
description: "Add anchor_ordinal to notes (traceback to the conversation tail)",
|
|
1938
|
+
up: (db) => {
|
|
1939
|
+
const notesExists = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='notes'").get();
|
|
1940
|
+
if (!notesExists) {
|
|
1941
|
+
return;
|
|
1942
|
+
}
|
|
1943
|
+
const columns = db.prepare("PRAGMA table_info(notes)").all();
|
|
1944
|
+
if (!columns.some((column) => column.name === "anchor_ordinal")) {
|
|
1945
|
+
db.exec("ALTER TABLE notes ADD COLUMN anchor_ordinal INTEGER");
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
},
|
|
1949
|
+
{
|
|
1950
|
+
version: 30,
|
|
1951
|
+
description: "HARD-bust m[0] markers: cached system/tool-set/model identity",
|
|
1952
|
+
up: (db) => {
|
|
1953
|
+
const hasSessionMeta = db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='session_meta' LIMIT 1").get();
|
|
1954
|
+
if (!hasSessionMeta)
|
|
1955
|
+
return;
|
|
1956
|
+
ensureColumn(db, "session_meta", "cached_m0_system_hash", "TEXT");
|
|
1957
|
+
ensureColumn(db, "session_meta", "cached_m0_tool_set_hash", "TEXT");
|
|
1958
|
+
ensureColumn(db, "session_meta", "cached_m0_model_key", "TEXT");
|
|
1959
|
+
const columns = new Set(db.prepare("PRAGMA table_info(session_meta)").all().map((column) => column.name));
|
|
1960
|
+
if (columns.has("cached_m0_bytes")) {
|
|
1961
|
+
db.prepare(`UPDATE session_meta SET
|
|
1962
|
+
cached_m0_bytes = NULL,
|
|
1963
|
+
cached_m1_bytes = NULL,
|
|
1964
|
+
cached_m0_materialized_at = NULL,
|
|
1965
|
+
cached_m0_system_hash = NULL,
|
|
1966
|
+
cached_m0_tool_set_hash = NULL,
|
|
1967
|
+
cached_m0_model_key = NULL`).run();
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
},
|
|
1971
|
+
{
|
|
1972
|
+
version: 31,
|
|
1973
|
+
description: "Nudge redesign: Channel 1 cadence (last_nudge_undropped) + Channel 2 ceiling lease " + "(channel2_nudge_state); zero legacy ctx_reduce-nudge sticky/anchor state (startup heal)",
|
|
1974
|
+
up: (db) => {
|
|
1975
|
+
const hasSessionMeta = db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='session_meta' LIMIT 1").get();
|
|
1976
|
+
if (!hasSessionMeta)
|
|
1977
|
+
return;
|
|
1978
|
+
ensureColumn(db, "session_meta", "last_nudge_undropped", "INTEGER DEFAULT 0");
|
|
1979
|
+
ensureColumn(db, "session_meta", "channel2_nudge_state", "TEXT DEFAULT ''");
|
|
1980
|
+
const columns = new Set(db.prepare("PRAGMA table_info(session_meta)").all().map((column) => column.name));
|
|
1981
|
+
if (columns.has("sticky_turn_reminder_text")) {
|
|
1982
|
+
db.prepare(`UPDATE session_meta SET
|
|
1983
|
+
sticky_turn_reminder_text = '',
|
|
1984
|
+
sticky_turn_reminder_message_id = '',
|
|
1985
|
+
nudge_anchor_message_id = '',
|
|
1986
|
+
nudge_anchor_text = ''`).run();
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
},
|
|
1990
|
+
{
|
|
1991
|
+
version: 32,
|
|
1992
|
+
description: "Protected tail boundary state, usage resolver fields, recovery escape, and drain quota",
|
|
1993
|
+
up: (db) => {
|
|
1994
|
+
const hasSessionMeta = db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='session_meta' LIMIT 1").get();
|
|
1995
|
+
if (!hasSessionMeta)
|
|
1996
|
+
return;
|
|
1997
|
+
ensureColumn(db, "session_meta", "prior_boundary_ordinal", "INTEGER NOT NULL DEFAULT 1");
|
|
1998
|
+
ensureColumn(db, "session_meta", "protected_tail_policy_version", "INTEGER NOT NULL DEFAULT 0");
|
|
1999
|
+
ensureColumn(db, "session_meta", "protected_tail_drain_window_started_at", "INTEGER NOT NULL DEFAULT 0");
|
|
2000
|
+
ensureColumn(db, "session_meta", "protected_tail_drain_tokens", "INTEGER NOT NULL DEFAULT 0");
|
|
2001
|
+
ensureColumn(db, "session_meta", "recovery_no_eligible_head_count", "INTEGER NOT NULL DEFAULT 0");
|
|
2002
|
+
ensureColumn(db, "session_meta", "force_emergency_bypass_window_start", "INTEGER NOT NULL DEFAULT 0");
|
|
2003
|
+
ensureColumn(db, "session_meta", "force_emergency_bypass_used", "INTEGER NOT NULL DEFAULT 0");
|
|
2004
|
+
ensureColumn(db, "session_meta", "last_usage_context_limit", "INTEGER NOT NULL DEFAULT 0");
|
|
2005
|
+
db.prepare("UPDATE session_meta SET prior_boundary_ordinal = 1 WHERE prior_boundary_ordinal IS NULL OR prior_boundary_ordinal < 1").run();
|
|
2006
|
+
db.prepare("UPDATE session_meta SET protected_tail_policy_version = 0 WHERE protected_tail_policy_version IS NULL").run();
|
|
2007
|
+
db.prepare("UPDATE session_meta SET protected_tail_drain_window_started_at = 0 WHERE protected_tail_drain_window_started_at IS NULL").run();
|
|
2008
|
+
db.prepare("UPDATE session_meta SET protected_tail_drain_tokens = 0 WHERE protected_tail_drain_tokens IS NULL").run();
|
|
2009
|
+
db.prepare("UPDATE session_meta SET recovery_no_eligible_head_count = 0 WHERE recovery_no_eligible_head_count IS NULL").run();
|
|
2010
|
+
db.prepare("UPDATE session_meta SET force_emergency_bypass_window_start = 0 WHERE force_emergency_bypass_window_start IS NULL").run();
|
|
2011
|
+
db.prepare("UPDATE session_meta SET force_emergency_bypass_used = 0 WHERE force_emergency_bypass_used IS NULL").run();
|
|
2012
|
+
db.prepare("UPDATE session_meta SET last_usage_context_limit = 0 WHERE last_usage_context_limit IS NULL").run();
|
|
2013
|
+
}
|
|
2014
|
+
},
|
|
2015
|
+
{
|
|
2016
|
+
version: 33,
|
|
2017
|
+
description: "Compartment chunk embeddings for semantic message-history search",
|
|
2018
|
+
up: (db) => {
|
|
2019
|
+
db.exec(`
|
|
2020
|
+
CREATE TABLE IF NOT EXISTS compartment_chunk_embeddings (
|
|
2021
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
2022
|
+
compartment_id INTEGER NOT NULL REFERENCES compartments(id) ON DELETE CASCADE,
|
|
2023
|
+
session_id TEXT NOT NULL,
|
|
2024
|
+
project_path TEXT NOT NULL,
|
|
2025
|
+
harness TEXT NOT NULL DEFAULT 'opencode',
|
|
2026
|
+
window_index INTEGER NOT NULL DEFAULT 0,
|
|
2027
|
+
start_ordinal INTEGER NOT NULL,
|
|
2028
|
+
end_ordinal INTEGER NOT NULL,
|
|
2029
|
+
chunk_hash TEXT NOT NULL,
|
|
2030
|
+
model_id TEXT NOT NULL,
|
|
2031
|
+
dims INTEGER NOT NULL,
|
|
2032
|
+
vector BLOB NOT NULL,
|
|
2033
|
+
created_at INTEGER NOT NULL,
|
|
2034
|
+
UNIQUE(compartment_id, window_index)
|
|
2035
|
+
);
|
|
2036
|
+
CREATE INDEX IF NOT EXISTS idx_cce_session
|
|
2037
|
+
ON compartment_chunk_embeddings(session_id);
|
|
2038
|
+
CREATE INDEX IF NOT EXISTS idx_cce_project_model
|
|
2039
|
+
ON compartment_chunk_embeddings(project_path, model_id);
|
|
2040
|
+
`);
|
|
2041
|
+
}
|
|
2042
|
+
},
|
|
2043
|
+
{
|
|
2044
|
+
version: 34,
|
|
2045
|
+
description: "workspace tables and m[0] workspace fingerprint cache reset",
|
|
2046
|
+
up: (db) => {
|
|
2047
|
+
db.exec(`
|
|
2048
|
+
CREATE TABLE IF NOT EXISTS workspaces (
|
|
2049
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
2050
|
+
name TEXT NOT NULL UNIQUE,
|
|
2051
|
+
created_at INTEGER NOT NULL,
|
|
2052
|
+
updated_at INTEGER NOT NULL
|
|
2053
|
+
);
|
|
2054
|
+
CREATE TABLE IF NOT EXISTS workspace_members (
|
|
2055
|
+
workspace_id INTEGER NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
|
|
2056
|
+
project_path TEXT NOT NULL,
|
|
2057
|
+
display_name TEXT NOT NULL,
|
|
2058
|
+
display_path TEXT NOT NULL,
|
|
2059
|
+
added_at INTEGER NOT NULL,
|
|
2060
|
+
PRIMARY KEY (workspace_id, project_path)
|
|
2061
|
+
);
|
|
2062
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_workspace_member_unique
|
|
2063
|
+
ON workspace_members(project_path);
|
|
2064
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_workspace_member_name
|
|
2065
|
+
ON workspace_members(workspace_id, display_name);
|
|
2066
|
+
`);
|
|
2067
|
+
const hasSessionMeta = db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='session_meta' LIMIT 1").get();
|
|
2068
|
+
if (!hasSessionMeta)
|
|
2069
|
+
return;
|
|
2070
|
+
ensureColumn(db, "session_meta", "cached_m0_workspace_fingerprint", "TEXT");
|
|
2071
|
+
const columns = new Set(db.prepare("PRAGMA table_info(session_meta)").all().map((column) => column.name));
|
|
2072
|
+
const clears = [
|
|
2073
|
+
["cached_m0_bytes", null],
|
|
2074
|
+
["cached_m1_bytes", null],
|
|
2075
|
+
["cached_m0_project_memory_epoch", null],
|
|
2076
|
+
["cached_m0_workspace_fingerprint", null],
|
|
2077
|
+
["cached_m0_project_user_profile_version", null],
|
|
2078
|
+
["cached_m0_max_compartment_seq", null],
|
|
2079
|
+
["cached_m0_max_memory_id", null],
|
|
2080
|
+
["cached_m0_max_mutation_id", null],
|
|
2081
|
+
["cached_m0_max_memory_mutation_id", null],
|
|
2082
|
+
["cached_m0_project_docs_hash", null],
|
|
2083
|
+
["cached_m0_materialized_at", null],
|
|
2084
|
+
["cached_m0_session_facts_version", null],
|
|
2085
|
+
["cached_m0_upgrade_state", null],
|
|
2086
|
+
["cached_m0_system_hash", null],
|
|
2087
|
+
["cached_m0_tool_set_hash", null],
|
|
2088
|
+
["cached_m0_model_key", null],
|
|
2089
|
+
["cached_m0_last_baseline_end_message_id", null],
|
|
2090
|
+
["memory_block_cache", ""],
|
|
2091
|
+
["memory_block_ids", ""],
|
|
2092
|
+
["memory_block_count", 0]
|
|
2093
|
+
];
|
|
2094
|
+
const setClauses = [];
|
|
2095
|
+
const values = [];
|
|
2096
|
+
for (const [column, value] of clears) {
|
|
2097
|
+
if (!columns.has(column))
|
|
2098
|
+
continue;
|
|
2099
|
+
setClauses.push(`${column} = ?`);
|
|
2100
|
+
values.push(value);
|
|
2101
|
+
}
|
|
2102
|
+
if (setClauses.length > 0) {
|
|
2103
|
+
db.prepare(`UPDATE session_meta SET ${setClauses.join(", ")}`).run(...values);
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
},
|
|
2107
|
+
{
|
|
2108
|
+
version: 35,
|
|
2109
|
+
description: "workspace per-category share defaults and epoch refresh",
|
|
2110
|
+
up: (db) => {
|
|
2111
|
+
db.exec(`
|
|
2112
|
+
CREATE TABLE IF NOT EXISTS workspaces (
|
|
2113
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
2114
|
+
name TEXT NOT NULL UNIQUE,
|
|
2115
|
+
created_at INTEGER NOT NULL,
|
|
2116
|
+
updated_at INTEGER NOT NULL,
|
|
2117
|
+
share_categories TEXT NOT NULL DEFAULT '["CONSTRAINTS"]'
|
|
2118
|
+
);
|
|
2119
|
+
`);
|
|
2120
|
+
ensureColumn(db, "workspaces", "share_categories", `TEXT NOT NULL DEFAULT '["CONSTRAINTS"]'`);
|
|
2121
|
+
db.prepare(`UPDATE workspaces
|
|
2122
|
+
SET share_categories = '["CONSTRAINTS"]'
|
|
2123
|
+
WHERE share_categories IS NULL OR share_categories = ''`).run();
|
|
2124
|
+
if (!tableExists4(db, "workspace_members"))
|
|
2125
|
+
return;
|
|
2126
|
+
const rows = db.prepare(`SELECT DISTINCT project_path AS identity
|
|
2127
|
+
FROM workspace_members
|
|
2128
|
+
WHERE project_path IS NOT NULL AND project_path <> ''
|
|
2129
|
+
ORDER BY project_path ASC`).all();
|
|
2130
|
+
const identities = rows.map((row) => typeof row.identity === "string" ? row.identity : "").filter((identity) => identity.length > 0);
|
|
2131
|
+
if (identities.length > 0) {
|
|
2132
|
+
bumpEpochsForWorkspaceMemberSet(db, identities, Date.now());
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
},
|
|
2136
|
+
{
|
|
2137
|
+
version: 36,
|
|
2138
|
+
description: "session project ownership map for compartment chunk backfill scoping",
|
|
2139
|
+
up: (db) => {
|
|
2140
|
+
db.exec(`
|
|
2141
|
+
CREATE TABLE IF NOT EXISTS session_projects (
|
|
2142
|
+
session_id TEXT NOT NULL,
|
|
2143
|
+
harness TEXT NOT NULL DEFAULT 'opencode',
|
|
2144
|
+
project_path TEXT NOT NULL,
|
|
2145
|
+
updated_at INTEGER NOT NULL,
|
|
2146
|
+
PRIMARY KEY(session_id, harness)
|
|
2147
|
+
);
|
|
2148
|
+
CREATE INDEX IF NOT EXISTS idx_session_projects_project
|
|
2149
|
+
ON session_projects(project_path);
|
|
2150
|
+
`);
|
|
2151
|
+
const hasChunkTable = db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='compartment_chunk_embeddings'").get();
|
|
2152
|
+
if (hasChunkTable) {
|
|
2153
|
+
db.exec(`
|
|
2154
|
+
INSERT OR IGNORE INTO session_projects (session_id, harness, project_path, updated_at)
|
|
2155
|
+
SELECT session_id, harness, MIN(project_path), 0
|
|
2156
|
+
FROM compartment_chunk_embeddings
|
|
2157
|
+
GROUP BY session_id, harness
|
|
2158
|
+
HAVING COUNT(DISTINCT project_path) = 1;
|
|
2159
|
+
`);
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
},
|
|
2163
|
+
{
|
|
2164
|
+
version: 37,
|
|
2165
|
+
description: "emergency drain catch-up latch + historian drain failure backoff",
|
|
2166
|
+
up: (db) => {
|
|
2167
|
+
const hasSessionMeta = db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='session_meta'").get();
|
|
2168
|
+
if (!hasSessionMeta)
|
|
2169
|
+
return;
|
|
2170
|
+
ensureColumn(db, "session_meta", "emergency_drain_active", "INTEGER NOT NULL DEFAULT 0");
|
|
2171
|
+
ensureColumn(db, "session_meta", "historian_drain_failure_at", "INTEGER NOT NULL DEFAULT 0");
|
|
2172
|
+
}
|
|
2173
|
+
},
|
|
2174
|
+
{
|
|
2175
|
+
version: 38,
|
|
2176
|
+
description: "durable transform decisions for cache-event cause attribution",
|
|
2177
|
+
up: (db) => {
|
|
2178
|
+
db.exec(`
|
|
2179
|
+
CREATE TABLE IF NOT EXISTS transform_decisions (
|
|
2180
|
+
session_id TEXT NOT NULL,
|
|
2181
|
+
harness TEXT NOT NULL DEFAULT 'opencode',
|
|
2182
|
+
message_id TEXT NOT NULL,
|
|
2183
|
+
ts_ms INTEGER NOT NULL,
|
|
2184
|
+
decision TEXT NOT NULL,
|
|
2185
|
+
materialized INTEGER NOT NULL DEFAULT 0,
|
|
2186
|
+
materialize_reason TEXT,
|
|
2187
|
+
emergency INTEGER NOT NULL DEFAULT 0,
|
|
2188
|
+
dropped_tokens INTEGER NOT NULL DEFAULT 0,
|
|
2189
|
+
dropped_count INTEGER NOT NULL DEFAULT 0,
|
|
2190
|
+
input_tokens INTEGER NOT NULL DEFAULT 0,
|
|
2191
|
+
PRIMARY KEY (session_id, harness, message_id)
|
|
2192
|
+
);
|
|
2193
|
+
CREATE INDEX IF NOT EXISTS idx_transform_decisions_session_harness
|
|
2194
|
+
ON transform_decisions(session_id, harness);
|
|
2195
|
+
`);
|
|
2196
|
+
}
|
|
2197
|
+
},
|
|
2198
|
+
{
|
|
2199
|
+
version: 39,
|
|
2200
|
+
description: "persist compaction marker target end message id",
|
|
2201
|
+
up: (db) => {
|
|
2202
|
+
const hasSessionMeta = db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='session_meta'").get();
|
|
2203
|
+
if (!hasSessionMeta)
|
|
2204
|
+
return;
|
|
2205
|
+
ensureColumn(db, "session_meta", "compaction_marker_state", "TEXT DEFAULT ''");
|
|
2206
|
+
ensureColumn(db, "session_meta", "compaction_marker_target_end_message_id", "TEXT");
|
|
2207
|
+
db.exec(`
|
|
2208
|
+
UPDATE session_meta
|
|
2209
|
+
SET compaction_marker_target_end_message_id = json_extract(compaction_marker_state, '$.targetEndMessageId')
|
|
2210
|
+
WHERE compaction_marker_target_end_message_id IS NULL
|
|
2211
|
+
AND COALESCE(compaction_marker_state, '') != ''
|
|
2212
|
+
AND json_valid(compaction_marker_state)
|
|
2213
|
+
AND typeof(json_extract(compaction_marker_state, '$.targetEndMessageId')) = 'text'
|
|
2214
|
+
`);
|
|
2215
|
+
}
|
|
2216
|
+
},
|
|
2217
|
+
{
|
|
2218
|
+
version: 40,
|
|
2219
|
+
description: "index Pi fallback tool owners for stable-id cutover",
|
|
2220
|
+
up: (db) => {
|
|
2221
|
+
if (!tableExists4(db, "tags"))
|
|
2222
|
+
return;
|
|
2223
|
+
db.exec(`
|
|
2224
|
+
CREATE INDEX IF NOT EXISTS idx_tags_pi_fallback_tool_owner
|
|
2225
|
+
ON tags(session_id, tool_owner_message_id)
|
|
2226
|
+
WHERE type='tool';
|
|
2227
|
+
`);
|
|
2228
|
+
}
|
|
2229
|
+
},
|
|
2230
|
+
{
|
|
2231
|
+
version: 41,
|
|
2232
|
+
description: "key detected context limits by model",
|
|
2233
|
+
up: (db) => {
|
|
2234
|
+
if (!tableExists4(db, "session_meta"))
|
|
2235
|
+
return;
|
|
2236
|
+
ensureColumn(db, "session_meta", "detected_context_limit_model_key", "TEXT");
|
|
2237
|
+
}
|
|
2238
|
+
},
|
|
2239
|
+
{
|
|
2240
|
+
version: 42,
|
|
2241
|
+
description: "per-task dreamer scheduling state (Dreamer v2 A+B)",
|
|
2242
|
+
up: (db) => {
|
|
2243
|
+
db.exec(`
|
|
2244
|
+
CREATE TABLE IF NOT EXISTS task_schedule_state (
|
|
2245
|
+
project_path TEXT NOT NULL,
|
|
2246
|
+
task TEXT NOT NULL,
|
|
2247
|
+
last_run_at INTEGER,
|
|
2248
|
+
next_due_at INTEGER,
|
|
2249
|
+
schedule TEXT,
|
|
2250
|
+
last_status TEXT,
|
|
2251
|
+
last_error TEXT,
|
|
2252
|
+
retry_count INTEGER NOT NULL DEFAULT 0,
|
|
2253
|
+
PRIMARY KEY (project_path, task)
|
|
2254
|
+
);
|
|
2255
|
+
CREATE INDEX IF NOT EXISTS idx_task_schedule_due
|
|
2256
|
+
ON task_schedule_state(next_due_at);
|
|
2257
|
+
`);
|
|
2258
|
+
}
|
|
2259
|
+
},
|
|
2260
|
+
{
|
|
2261
|
+
version: 43,
|
|
2262
|
+
description: "memory verification side table and verify watermarks",
|
|
2263
|
+
up: (db) => {
|
|
2264
|
+
db.exec(`
|
|
2265
|
+
CREATE TABLE IF NOT EXISTS memory_verifications (
|
|
2266
|
+
memory_id INTEGER NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
|
2267
|
+
file_path TEXT NOT NULL,
|
|
2268
|
+
verified_at INTEGER NOT NULL,
|
|
2269
|
+
PRIMARY KEY (memory_id, file_path)
|
|
2270
|
+
);
|
|
2271
|
+
CREATE INDEX IF NOT EXISTS idx_memory_verifications_memory
|
|
2272
|
+
ON memory_verifications(memory_id);
|
|
2273
|
+
`);
|
|
2274
|
+
if (tableExists4(db, "task_schedule_state")) {
|
|
2275
|
+
ensureColumn(db, "task_schedule_state", "last_checked_commit", "TEXT");
|
|
2276
|
+
ensureColumn(db, "task_schedule_state", "last_broad_run_at", "INTEGER");
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
},
|
|
2280
|
+
{
|
|
2281
|
+
version: 44,
|
|
2282
|
+
description: "memory classification scope and shareability columns",
|
|
2283
|
+
up: (db) => {
|
|
2284
|
+
if (!tableExists4(db, "memories"))
|
|
2285
|
+
return;
|
|
2286
|
+
ensureColumn(db, "memories", "scope", "TEXT NOT NULL DEFAULT 'project'");
|
|
2287
|
+
ensureColumn(db, "memories", "shareable", "INTEGER NOT NULL DEFAULT 0");
|
|
2288
|
+
}
|
|
2289
|
+
},
|
|
2290
|
+
{
|
|
2291
|
+
version: 45,
|
|
2292
|
+
description: "retrospective content watermark and processed-window idempotence",
|
|
2293
|
+
up: (db) => {
|
|
2294
|
+
if (tableExists4(db, "task_schedule_state")) {
|
|
2295
|
+
ensureColumn(db, "task_schedule_state", "retrospective_watermark_ms", "INTEGER");
|
|
2296
|
+
}
|
|
2297
|
+
db.exec(`
|
|
2298
|
+
CREATE TABLE IF NOT EXISTS retrospective_processed_windows (
|
|
2299
|
+
project_path TEXT NOT NULL,
|
|
2300
|
+
window_key TEXT NOT NULL,
|
|
2301
|
+
processed_at INTEGER NOT NULL,
|
|
2302
|
+
PRIMARY KEY (project_path, window_key)
|
|
2303
|
+
);
|
|
2304
|
+
`);
|
|
2305
|
+
}
|
|
2306
|
+
},
|
|
2307
|
+
{
|
|
2308
|
+
version: 46,
|
|
2309
|
+
description: "Primers v1 candidate and promoted primer storage",
|
|
2310
|
+
up: (db) => {
|
|
2311
|
+
db.exec(`
|
|
2312
|
+
CREATE TABLE IF NOT EXISTS primer_candidates (
|
|
2313
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
2314
|
+
project_path TEXT NOT NULL,
|
|
2315
|
+
harness TEXT NOT NULL DEFAULT 'opencode',
|
|
2316
|
+
session_id TEXT NOT NULL,
|
|
2317
|
+
question TEXT NOT NULL,
|
|
2318
|
+
normalized_question TEXT NOT NULL,
|
|
2319
|
+
source_compartment_start INTEGER,
|
|
2320
|
+
source_compartment_end INTEGER,
|
|
2321
|
+
source_start_message_id TEXT NOT NULL DEFAULT '',
|
|
2322
|
+
source_end_message_id TEXT NOT NULL DEFAULT '',
|
|
2323
|
+
source_message_time INTEGER NOT NULL,
|
|
2324
|
+
question_embedding BLOB,
|
|
2325
|
+
question_embedding_model_id TEXT,
|
|
2326
|
+
created_at INTEGER NOT NULL,
|
|
2327
|
+
UNIQUE(project_path, harness, session_id, source_start_message_id, source_end_message_id)
|
|
2328
|
+
);
|
|
2329
|
+
CREATE INDEX IF NOT EXISTS idx_primer_candidates_project_time
|
|
2330
|
+
ON primer_candidates(project_path, source_message_time);
|
|
2331
|
+
CREATE INDEX IF NOT EXISTS idx_primer_candidates_session
|
|
2332
|
+
ON primer_candidates(session_id, harness);
|
|
2333
|
+
CREATE INDEX IF NOT EXISTS idx_primer_candidates_embedding_model
|
|
2334
|
+
ON primer_candidates(project_path, question_embedding_model_id);
|
|
2335
|
+
|
|
2336
|
+
CREATE TABLE IF NOT EXISTS primers (
|
|
2337
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
2338
|
+
project_path TEXT NOT NULL,
|
|
2339
|
+
question TEXT NOT NULL,
|
|
2340
|
+
question_embedding BLOB,
|
|
2341
|
+
question_embedding_model_id TEXT,
|
|
2342
|
+
answer TEXT NOT NULL DEFAULT '',
|
|
2343
|
+
status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'archived')),
|
|
2344
|
+
total_support INTEGER NOT NULL DEFAULT 0,
|
|
2345
|
+
last_observed_at INTEGER,
|
|
2346
|
+
answer_refreshed_at INTEGER,
|
|
2347
|
+
source_candidate_ids TEXT NOT NULL DEFAULT '[]',
|
|
2348
|
+
source_candidate_provenance TEXT,
|
|
2349
|
+
created_at INTEGER NOT NULL,
|
|
2350
|
+
updated_at INTEGER NOT NULL
|
|
2351
|
+
);
|
|
2352
|
+
CREATE INDEX IF NOT EXISTS idx_primers_project_status_observed
|
|
2353
|
+
ON primers(project_path, status, last_observed_at DESC);
|
|
2354
|
+
CREATE INDEX IF NOT EXISTS idx_primers_embedding_model
|
|
2355
|
+
ON primers(project_path, question_embedding_model_id);
|
|
2356
|
+
|
|
2357
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS primers_fts USING fts5(
|
|
2358
|
+
question,
|
|
2359
|
+
answer,
|
|
2360
|
+
project_path UNINDEXED,
|
|
2361
|
+
content='primers',
|
|
2362
|
+
content_rowid='id',
|
|
2363
|
+
tokenize='porter unicode61'
|
|
2364
|
+
);
|
|
2365
|
+
CREATE TRIGGER IF NOT EXISTS primers_ai AFTER INSERT ON primers BEGIN
|
|
2366
|
+
INSERT INTO primers_fts(rowid, question, answer, project_path)
|
|
2367
|
+
VALUES (new.id, new.question, new.answer, new.project_path);
|
|
2368
|
+
END;
|
|
2369
|
+
CREATE TRIGGER IF NOT EXISTS primers_ad AFTER DELETE ON primers BEGIN
|
|
2370
|
+
INSERT INTO primers_fts(primers_fts, rowid, question, answer, project_path)
|
|
2371
|
+
VALUES ('delete', old.id, old.question, old.answer, old.project_path);
|
|
2372
|
+
END;
|
|
2373
|
+
CREATE TRIGGER IF NOT EXISTS primers_au AFTER UPDATE ON primers BEGIN
|
|
2374
|
+
INSERT INTO primers_fts(primers_fts, rowid, question, answer, project_path)
|
|
2375
|
+
VALUES ('delete', old.id, old.question, old.answer, old.project_path);
|
|
2376
|
+
INSERT INTO primers_fts(rowid, question, answer, project_path)
|
|
2377
|
+
VALUES (new.id, new.question, new.answer, new.project_path);
|
|
2378
|
+
END;
|
|
2379
|
+
`);
|
|
2380
|
+
}
|
|
2381
|
+
},
|
|
2382
|
+
{
|
|
2383
|
+
version: 47,
|
|
2384
|
+
description: "compiled smart-note checks and runtime policy state",
|
|
2385
|
+
up: (db) => {
|
|
2386
|
+
if (!tableExists4(db, "notes"))
|
|
2387
|
+
return;
|
|
2388
|
+
ensureColumn(db, "notes", "compiled_check", "TEXT");
|
|
2389
|
+
ensureColumn(db, "notes", "manifest_json", "TEXT");
|
|
2390
|
+
ensureColumn(db, "notes", "check_hash", "TEXT");
|
|
2391
|
+
ensureColumn(db, "notes", "check_cron", "TEXT");
|
|
2392
|
+
ensureColumn(db, "notes", "check_version", "INTEGER NOT NULL DEFAULT 0");
|
|
2393
|
+
ensureColumn(db, "notes", "check_status", "TEXT NOT NULL DEFAULT 'uncompiled'");
|
|
2394
|
+
ensureColumn(db, "notes", "check_failure_count", "INTEGER NOT NULL DEFAULT 0");
|
|
2395
|
+
ensureColumn(db, "notes", "check_network_failure_count", "INTEGER NOT NULL DEFAULT 0");
|
|
2396
|
+
ensureColumn(db, "notes", "check_quarantined_until", "INTEGER");
|
|
2397
|
+
ensureColumn(db, "notes", "check_next_due_at", "INTEGER");
|
|
2398
|
+
ensureColumn(db, "notes", "check_compiled_at", "INTEGER");
|
|
2399
|
+
ensureColumn(db, "notes", "check_false_since_at", "INTEGER");
|
|
2400
|
+
ensureColumn(db, "notes", "check_last_liveness_at", "INTEGER");
|
|
2401
|
+
ensureColumn(db, "notes", "policy_version", "INTEGER NOT NULL DEFAULT 1");
|
|
2402
|
+
db.exec(`
|
|
2403
|
+
CREATE INDEX IF NOT EXISTS idx_notes_smart_checks_due
|
|
2404
|
+
ON notes(project_path, check_status, check_next_due_at)
|
|
2405
|
+
WHERE type = 'smart' AND status = 'pending';
|
|
2406
|
+
CREATE INDEX IF NOT EXISTS idx_notes_smart_checks_liveness
|
|
2407
|
+
ON notes(project_path, check_false_since_at, check_last_liveness_at)
|
|
2408
|
+
WHERE type = 'smart' AND status = 'pending';
|
|
2409
|
+
`);
|
|
2410
|
+
}
|
|
2411
|
+
},
|
|
2412
|
+
{
|
|
2413
|
+
version: 48,
|
|
2414
|
+
description: "DreamerV2 rework: memory→file mapping vs verification split, classify marker",
|
|
2415
|
+
up: (db) => {
|
|
2416
|
+
if (tableExists4(db, "memory_verifications")) {
|
|
2417
|
+
ensureColumn(db, "memory_verifications", "mapped_at", "INTEGER NOT NULL DEFAULT 0");
|
|
2418
|
+
}
|
|
2419
|
+
if (tableExists4(db, "memories")) {
|
|
2420
|
+
ensureColumn(db, "memories", "classified_at", "INTEGER");
|
|
2421
|
+
}
|
|
2422
|
+
}
|
|
2423
|
+
},
|
|
2424
|
+
{
|
|
2425
|
+
version: 49,
|
|
2426
|
+
description: "per-model embedding coexistence and active identity tracking",
|
|
2427
|
+
up: (db) => {
|
|
2428
|
+
if (tableExists4(db, "memory_embeddings")) {
|
|
2429
|
+
db.exec(`
|
|
2430
|
+
UPDATE memory_embeddings
|
|
2431
|
+
SET model_id = 'legacy:unknown'
|
|
2432
|
+
WHERE model_id IS NULL;
|
|
2433
|
+
`);
|
|
2434
|
+
if (tableExists4(db, "memories")) {
|
|
2435
|
+
db.exec(`
|
|
2436
|
+
DELETE FROM memory_embeddings
|
|
2437
|
+
WHERE memory_id NOT IN (SELECT id FROM memories);
|
|
2438
|
+
`);
|
|
2439
|
+
}
|
|
2440
|
+
db.exec(`
|
|
2441
|
+
DROP TABLE IF EXISTS memory_embeddings_v49_new;
|
|
2442
|
+
CREATE TABLE memory_embeddings_v49_new (
|
|
2443
|
+
memory_id INTEGER NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
|
2444
|
+
embedding BLOB NOT NULL,
|
|
2445
|
+
model_id TEXT NOT NULL,
|
|
2446
|
+
PRIMARY KEY(memory_id, model_id)
|
|
2447
|
+
);
|
|
2448
|
+
INSERT INTO memory_embeddings_v49_new (memory_id, embedding, model_id)
|
|
2449
|
+
SELECT memory_id, embedding, model_id
|
|
2450
|
+
FROM memory_embeddings;
|
|
2451
|
+
DROP TABLE memory_embeddings;
|
|
2452
|
+
ALTER TABLE memory_embeddings_v49_new RENAME TO memory_embeddings;
|
|
2453
|
+
`);
|
|
2454
|
+
assertForeignKeyIntegrity(db, "memory_embeddings");
|
|
2455
|
+
}
|
|
2456
|
+
if (tableExists4(db, "git_commit_embeddings")) {
|
|
2457
|
+
if (tableExists4(db, "git_commits")) {
|
|
2458
|
+
db.exec(`
|
|
2459
|
+
DELETE FROM git_commit_embeddings
|
|
2460
|
+
WHERE sha NOT IN (SELECT sha FROM git_commits);
|
|
2461
|
+
`);
|
|
2462
|
+
}
|
|
2463
|
+
db.exec(`
|
|
2464
|
+
DROP TABLE IF EXISTS git_commit_embeddings_v49_new;
|
|
2465
|
+
CREATE TABLE git_commit_embeddings_v49_new (
|
|
2466
|
+
sha TEXT NOT NULL,
|
|
2467
|
+
embedding BLOB NOT NULL,
|
|
2468
|
+
model_id TEXT NOT NULL,
|
|
2469
|
+
created_at INTEGER NOT NULL,
|
|
2470
|
+
PRIMARY KEY(sha, model_id),
|
|
2471
|
+
FOREIGN KEY(sha) REFERENCES git_commits(sha) ON DELETE CASCADE
|
|
2472
|
+
);
|
|
2473
|
+
INSERT INTO git_commit_embeddings_v49_new (sha, embedding, model_id, created_at)
|
|
2474
|
+
SELECT sha, embedding, model_id, created_at
|
|
2475
|
+
FROM git_commit_embeddings;
|
|
2476
|
+
DROP TABLE git_commit_embeddings;
|
|
2477
|
+
ALTER TABLE git_commit_embeddings_v49_new RENAME TO git_commit_embeddings;
|
|
2478
|
+
`);
|
|
2479
|
+
assertForeignKeyIntegrity(db, "git_commit_embeddings");
|
|
2480
|
+
}
|
|
2481
|
+
if (tableExists4(db, "compartment_chunk_embeddings")) {
|
|
2482
|
+
if (tableExists4(db, "compartments")) {
|
|
2483
|
+
db.exec(`
|
|
2484
|
+
DELETE FROM compartment_chunk_embeddings
|
|
2485
|
+
WHERE compartment_id NOT IN (SELECT id FROM compartments);
|
|
2486
|
+
`);
|
|
2487
|
+
}
|
|
2488
|
+
db.exec(`
|
|
2489
|
+
DROP INDEX IF EXISTS idx_cce_session;
|
|
2490
|
+
DROP INDEX IF EXISTS idx_cce_project_model;
|
|
2491
|
+
DROP TABLE IF EXISTS compartment_chunk_embeddings_v49_new;
|
|
2492
|
+
CREATE TABLE compartment_chunk_embeddings_v49_new (
|
|
2493
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
2494
|
+
compartment_id INTEGER NOT NULL REFERENCES compartments(id) ON DELETE CASCADE,
|
|
2495
|
+
session_id TEXT NOT NULL,
|
|
2496
|
+
project_path TEXT NOT NULL,
|
|
2497
|
+
harness TEXT NOT NULL DEFAULT 'opencode',
|
|
2498
|
+
window_index INTEGER NOT NULL DEFAULT 0,
|
|
2499
|
+
start_ordinal INTEGER NOT NULL,
|
|
2500
|
+
end_ordinal INTEGER NOT NULL,
|
|
2501
|
+
chunk_hash TEXT NOT NULL,
|
|
2502
|
+
model_id TEXT NOT NULL,
|
|
2503
|
+
dims INTEGER NOT NULL,
|
|
2504
|
+
vector BLOB NOT NULL,
|
|
2505
|
+
created_at INTEGER NOT NULL,
|
|
2506
|
+
UNIQUE(compartment_id, model_id, window_index)
|
|
2507
|
+
);
|
|
2508
|
+
INSERT INTO compartment_chunk_embeddings_v49_new (
|
|
2509
|
+
id, compartment_id, session_id, project_path, harness, window_index,
|
|
2510
|
+
start_ordinal, end_ordinal, chunk_hash, model_id, dims, vector, created_at
|
|
2511
|
+
)
|
|
2512
|
+
SELECT id, compartment_id, session_id, project_path, harness, window_index,
|
|
2513
|
+
start_ordinal, end_ordinal, chunk_hash, model_id, dims, vector, created_at
|
|
2514
|
+
FROM compartment_chunk_embeddings;
|
|
2515
|
+
DROP TABLE compartment_chunk_embeddings;
|
|
2516
|
+
ALTER TABLE compartment_chunk_embeddings_v49_new RENAME TO compartment_chunk_embeddings;
|
|
2517
|
+
CREATE INDEX IF NOT EXISTS idx_cce_session ON compartment_chunk_embeddings(session_id);
|
|
2518
|
+
CREATE INDEX IF NOT EXISTS idx_cce_project_model ON compartment_chunk_embeddings(project_path, model_id);
|
|
2519
|
+
`);
|
|
2520
|
+
assertForeignKeyIntegrity(db, "compartment_chunk_embeddings");
|
|
2521
|
+
}
|
|
2522
|
+
db.exec(`
|
|
2523
|
+
CREATE TABLE IF NOT EXISTS embedding_identity_active (
|
|
2524
|
+
project_path TEXT NOT NULL,
|
|
2525
|
+
scope TEXT NOT NULL CHECK(scope IN ('memory', 'commit', 'chunk')),
|
|
2526
|
+
model_id TEXT NOT NULL,
|
|
2527
|
+
last_active_at INTEGER NOT NULL,
|
|
2528
|
+
PRIMARY KEY(project_path, scope, model_id)
|
|
2529
|
+
);
|
|
2530
|
+
`);
|
|
2531
|
+
}
|
|
2532
|
+
},
|
|
2533
|
+
{
|
|
2534
|
+
version: 50,
|
|
2535
|
+
description: "add durable ctx-wrapup session marker",
|
|
2536
|
+
up(db) {
|
|
2537
|
+
if (tableExists4(db, "session_meta")) {
|
|
2538
|
+
ensureColumn(db, "session_meta", "wrapup_in_progress_state", "TEXT");
|
|
2539
|
+
}
|
|
2540
|
+
}
|
|
2541
|
+
},
|
|
2542
|
+
{
|
|
2543
|
+
version: 51,
|
|
2544
|
+
description: "version tool-owner backfill state and repair legacy NULL session metadata",
|
|
2545
|
+
up(db) {
|
|
2546
|
+
db.exec(`
|
|
2547
|
+
CREATE TABLE IF NOT EXISTS tool_owner_backfill_state (
|
|
2548
|
+
session_id TEXT PRIMARY KEY,
|
|
2549
|
+
status TEXT NOT NULL CHECK (status IN ('pending', 'running', 'completed', 'skipped')),
|
|
2550
|
+
started_at INTEGER,
|
|
2551
|
+
lease_expires_at INTEGER,
|
|
2552
|
+
completed_at INTEGER,
|
|
2553
|
+
last_error TEXT
|
|
2554
|
+
);
|
|
2555
|
+
CREATE INDEX IF NOT EXISTS idx_tool_owner_backfill_state_status
|
|
2556
|
+
ON tool_owner_backfill_state(status);
|
|
2557
|
+
`);
|
|
2558
|
+
healAllNullColumns(db);
|
|
2559
|
+
}
|
|
2560
|
+
},
|
|
2561
|
+
{
|
|
2562
|
+
version: 52,
|
|
2563
|
+
description: "persist emergency recovery origin",
|
|
2564
|
+
up(db) {
|
|
2565
|
+
if (tableExists4(db, "session_meta")) {
|
|
2566
|
+
ensureColumn(db, "session_meta", "emergency_recovery_origin", "TEXT DEFAULT ''");
|
|
2567
|
+
}
|
|
2568
|
+
}
|
|
2569
|
+
},
|
|
2570
|
+
{
|
|
2571
|
+
version: 53,
|
|
2572
|
+
description: "add Synapse batch, shadow, and measurement storage",
|
|
2573
|
+
up(db) {
|
|
2574
|
+
db.exec(`
|
|
2575
|
+
CREATE TABLE IF NOT EXISTS embedding_registrations (
|
|
2576
|
+
project_path TEXT PRIMARY KEY,
|
|
2577
|
+
provider_identity TEXT NOT NULL DEFAULT '',
|
|
2578
|
+
model_id TEXT NOT NULL DEFAULT '',
|
|
2579
|
+
chunk_model_id TEXT NOT NULL DEFAULT '',
|
|
2580
|
+
fingerprint TEXT NOT NULL DEFAULT '',
|
|
2581
|
+
table_epoch INTEGER NOT NULL DEFAULT 0,
|
|
2582
|
+
dims INTEGER NOT NULL DEFAULT 0,
|
|
2583
|
+
provenance_json TEXT NOT NULL DEFAULT '{}',
|
|
2584
|
+
generation INTEGER NOT NULL DEFAULT 0,
|
|
2585
|
+
updated_at INTEGER NOT NULL DEFAULT 0
|
|
2586
|
+
);
|
|
2587
|
+
CREATE TABLE IF NOT EXISTS synapse_batch_ledger (
|
|
2588
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
2589
|
+
session_id TEXT NOT NULL,
|
|
2590
|
+
project_path TEXT NOT NULL DEFAULT '',
|
|
2591
|
+
scope TEXT NOT NULL DEFAULT '',
|
|
2592
|
+
manifest_json TEXT NOT NULL DEFAULT '{}',
|
|
2593
|
+
request_key TEXT NOT NULL DEFAULT '',
|
|
2594
|
+
job_id TEXT,
|
|
2595
|
+
cursor TEXT,
|
|
2596
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
2597
|
+
created_at INTEGER NOT NULL DEFAULT 0,
|
|
2598
|
+
updated_at INTEGER NOT NULL DEFAULT 0,
|
|
2599
|
+
UNIQUE(session_id, request_key)
|
|
2600
|
+
);
|
|
2601
|
+
CREATE INDEX IF NOT EXISTS idx_synapse_batch_ledger_session
|
|
2602
|
+
ON synapse_batch_ledger(session_id, updated_at);
|
|
2603
|
+
CREATE TABLE IF NOT EXISTS shadow_embedding_registrations (
|
|
2604
|
+
project_path TEXT NOT NULL,
|
|
2605
|
+
scope TEXT NOT NULL CHECK(scope IN ('memory', 'commit', 'chunk')),
|
|
2606
|
+
model_id TEXT NOT NULL,
|
|
2607
|
+
generation INTEGER NOT NULL DEFAULT 0,
|
|
2608
|
+
fingerprint TEXT NOT NULL DEFAULT '',
|
|
2609
|
+
table_epoch INTEGER NOT NULL DEFAULT 0,
|
|
2610
|
+
dims INTEGER NOT NULL DEFAULT 0,
|
|
2611
|
+
provenance_json TEXT NOT NULL DEFAULT '{}',
|
|
2612
|
+
updated_at INTEGER NOT NULL DEFAULT 0,
|
|
2613
|
+
PRIMARY KEY(project_path, scope, model_id)
|
|
2614
|
+
);
|
|
2615
|
+
CREATE TABLE IF NOT EXISTS embedding_measurement_corpus (
|
|
2616
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
2617
|
+
session_id TEXT NOT NULL,
|
|
2618
|
+
project_path TEXT NOT NULL DEFAULT '',
|
|
2619
|
+
dedup_key TEXT NOT NULL DEFAULT '',
|
|
2620
|
+
cohort_key TEXT NOT NULL DEFAULT '',
|
|
2621
|
+
query_text_hash TEXT NOT NULL DEFAULT '',
|
|
2622
|
+
primary_result_ids_json TEXT NOT NULL DEFAULT '[]',
|
|
2623
|
+
shadow_result_ids_json TEXT NOT NULL DEFAULT '[]',
|
|
2624
|
+
primary_latency_ms INTEGER,
|
|
2625
|
+
shadow_latency_ms INTEGER,
|
|
2626
|
+
primary_failed INTEGER NOT NULL DEFAULT 0,
|
|
2627
|
+
shadow_failed INTEGER NOT NULL DEFAULT 0,
|
|
2628
|
+
primary_model_id TEXT NOT NULL DEFAULT '',
|
|
2629
|
+
shadow_model_id TEXT NOT NULL DEFAULT '',
|
|
2630
|
+
primary_fingerprint TEXT NOT NULL DEFAULT '',
|
|
2631
|
+
shadow_fingerprint TEXT NOT NULL DEFAULT '',
|
|
2632
|
+
primary_epoch INTEGER NOT NULL DEFAULT 0,
|
|
2633
|
+
shadow_epoch INTEGER NOT NULL DEFAULT 0,
|
|
2634
|
+
corpus_hash TEXT NOT NULL DEFAULT '',
|
|
2635
|
+
coverage_json TEXT NOT NULL DEFAULT '{}',
|
|
2636
|
+
created_at INTEGER NOT NULL DEFAULT 0,
|
|
2637
|
+
UNIQUE(dedup_key, cohort_key)
|
|
2638
|
+
);
|
|
2639
|
+
CREATE INDEX IF NOT EXISTS idx_embedding_measurement_session
|
|
2640
|
+
ON embedding_measurement_corpus(session_id, created_at);
|
|
2641
|
+
`);
|
|
2642
|
+
}
|
|
2643
|
+
},
|
|
2644
|
+
{
|
|
2645
|
+
version: 54,
|
|
2646
|
+
description: "add authority identity, managed-write guards, and mirror cursors",
|
|
2647
|
+
up(db) {
|
|
2648
|
+
const memoriesPresent = tableExists4(db, "memories");
|
|
2649
|
+
const notesPresent = tableExists4(db, "notes");
|
|
2650
|
+
db.exec(`
|
|
2651
|
+
CREATE TABLE IF NOT EXISTS context_store_meta (
|
|
2652
|
+
key TEXT PRIMARY KEY,
|
|
2653
|
+
value TEXT NOT NULL
|
|
2654
|
+
);
|
|
2655
|
+
CREATE TABLE IF NOT EXISTS authority_managed (
|
|
2656
|
+
project_path TEXT PRIMARY KEY,
|
|
2657
|
+
context_store_uuid TEXT NOT NULL,
|
|
2658
|
+
marked_at INTEGER NOT NULL
|
|
2659
|
+
);
|
|
2660
|
+
CREATE TABLE IF NOT EXISTS authority_repair_pending (
|
|
2661
|
+
project_path TEXT PRIMARY KEY,
|
|
2662
|
+
started_at INTEGER NOT NULL
|
|
2663
|
+
);
|
|
2664
|
+
CREATE TABLE IF NOT EXISTS mirror_identity (
|
|
2665
|
+
domain TEXT NOT NULL CHECK(domain IN ('memories', 'notes')),
|
|
2666
|
+
module_project TEXT NOT NULL,
|
|
2667
|
+
module_row_id INTEGER NOT NULL,
|
|
2668
|
+
context_row_id INTEGER NOT NULL,
|
|
2669
|
+
PRIMARY KEY(domain, module_project, module_row_id),
|
|
2670
|
+
UNIQUE(domain, context_row_id)
|
|
2671
|
+
);
|
|
2672
|
+
CREATE TABLE IF NOT EXISTS mirror_cursors (
|
|
2673
|
+
domain TEXT PRIMARY KEY CHECK(domain IN ('memories', 'notes')),
|
|
2674
|
+
cursor INTEGER NOT NULL DEFAULT 0,
|
|
2675
|
+
updated_at INTEGER NOT NULL DEFAULT 0
|
|
2676
|
+
);
|
|
2677
|
+
CREATE TABLE IF NOT EXISTS context_privilege_state (
|
|
2678
|
+
id INTEGER PRIMARY KEY CHECK(id = 1),
|
|
2679
|
+
enabled INTEGER NOT NULL DEFAULT 0 CHECK(enabled IN (0, 1))
|
|
2680
|
+
);
|
|
2681
|
+
`);
|
|
2682
|
+
if (memoriesPresent) {
|
|
2683
|
+
db.exec(`
|
|
2684
|
+
DROP TRIGGER IF EXISTS memories_authority_guard_insert;
|
|
2685
|
+
DROP TRIGGER IF EXISTS memories_authority_guard_update;
|
|
2686
|
+
DROP TRIGGER IF EXISTS memories_authority_guard_delete;
|
|
2687
|
+
CREATE TRIGGER memories_authority_guard_insert
|
|
2688
|
+
BEFORE INSERT ON memories
|
|
2689
|
+
WHEN (
|
|
2690
|
+
EXISTS (SELECT 1 FROM authority_managed WHERE project_path = NEW.project_path)
|
|
2691
|
+
OR EXISTS (SELECT 1 FROM authority_repair_pending WHERE project_path = NEW.project_path)
|
|
2692
|
+
) AND COALESCE((SELECT enabled FROM context_privilege_state WHERE id = 1), 0) = 0
|
|
2693
|
+
BEGIN
|
|
2694
|
+
SELECT RAISE(ABORT, 'context.db memory writes are managed by the Rust module');
|
|
2695
|
+
END;
|
|
2696
|
+
CREATE TRIGGER memories_authority_guard_update
|
|
2697
|
+
BEFORE UPDATE ON memories
|
|
2698
|
+
WHEN (
|
|
2699
|
+
EXISTS (SELECT 1 FROM authority_managed WHERE project_path = OLD.project_path)
|
|
2700
|
+
OR EXISTS (SELECT 1 FROM authority_managed WHERE project_path = NEW.project_path)
|
|
2701
|
+
OR EXISTS (SELECT 1 FROM authority_repair_pending WHERE project_path = OLD.project_path)
|
|
2702
|
+
OR EXISTS (SELECT 1 FROM authority_repair_pending WHERE project_path = NEW.project_path)
|
|
2703
|
+
) AND COALESCE((SELECT enabled FROM context_privilege_state WHERE id = 1), 0) = 0
|
|
2704
|
+
BEGIN
|
|
2705
|
+
SELECT RAISE(ABORT, 'context.db memory writes are managed by the Rust module');
|
|
2706
|
+
END;
|
|
2707
|
+
CREATE TRIGGER memories_authority_guard_delete
|
|
2708
|
+
BEFORE DELETE ON memories
|
|
2709
|
+
WHEN (
|
|
2710
|
+
EXISTS (SELECT 1 FROM authority_managed WHERE project_path = OLD.project_path)
|
|
2711
|
+
OR EXISTS (SELECT 1 FROM authority_repair_pending WHERE project_path = OLD.project_path)
|
|
2712
|
+
) AND COALESCE((SELECT enabled FROM context_privilege_state WHERE id = 1), 0) = 0
|
|
2713
|
+
BEGIN
|
|
2714
|
+
SELECT RAISE(ABORT, 'context.db memory writes are managed by the Rust module');
|
|
2715
|
+
END;
|
|
2716
|
+
`);
|
|
2717
|
+
}
|
|
2718
|
+
if (notesPresent) {
|
|
2719
|
+
db.exec(`
|
|
2720
|
+
DROP TRIGGER IF EXISTS notes_authority_guard_insert;
|
|
2721
|
+
DROP TRIGGER IF EXISTS notes_authority_guard_update;
|
|
2722
|
+
DROP TRIGGER IF EXISTS notes_authority_guard_delete;
|
|
2723
|
+
CREATE TRIGGER notes_authority_guard_insert
|
|
2724
|
+
BEFORE INSERT ON notes
|
|
2725
|
+
WHEN NEW.type = 'smart' AND NEW.project_path IS NOT NULL
|
|
2726
|
+
AND (
|
|
2727
|
+
EXISTS (SELECT 1 FROM authority_managed WHERE project_path = NEW.project_path)
|
|
2728
|
+
OR EXISTS (SELECT 1 FROM authority_repair_pending WHERE project_path = NEW.project_path)
|
|
2729
|
+
) AND COALESCE((SELECT enabled FROM context_privilege_state WHERE id = 1), 0) = 0
|
|
2730
|
+
BEGIN
|
|
2731
|
+
SELECT RAISE(ABORT, 'context.db smart-note writes are managed by the Rust module');
|
|
2732
|
+
END;
|
|
2733
|
+
CREATE TRIGGER notes_authority_guard_update
|
|
2734
|
+
BEFORE UPDATE ON notes
|
|
2735
|
+
WHEN (
|
|
2736
|
+
(OLD.type = 'smart' AND OLD.project_path IS NOT NULL
|
|
2737
|
+
AND (EXISTS (SELECT 1 FROM authority_managed WHERE project_path = OLD.project_path)
|
|
2738
|
+
OR EXISTS (SELECT 1 FROM authority_repair_pending WHERE project_path = OLD.project_path)))
|
|
2739
|
+
OR
|
|
2740
|
+
(NEW.type = 'smart' AND NEW.project_path IS NOT NULL
|
|
2741
|
+
AND (EXISTS (SELECT 1 FROM authority_managed WHERE project_path = NEW.project_path)
|
|
2742
|
+
OR EXISTS (SELECT 1 FROM authority_repair_pending WHERE project_path = NEW.project_path)))
|
|
2743
|
+
) AND COALESCE((SELECT enabled FROM context_privilege_state WHERE id = 1), 0) = 0
|
|
2744
|
+
BEGIN
|
|
2745
|
+
SELECT RAISE(ABORT, 'context.db smart-note writes are managed by the Rust module');
|
|
2746
|
+
END;
|
|
2747
|
+
CREATE TRIGGER notes_authority_guard_delete
|
|
2748
|
+
BEFORE DELETE ON notes
|
|
2749
|
+
WHEN OLD.type = 'smart' AND OLD.project_path IS NOT NULL
|
|
2750
|
+
AND (
|
|
2751
|
+
EXISTS (SELECT 1 FROM authority_managed WHERE project_path = OLD.project_path)
|
|
2752
|
+
OR EXISTS (SELECT 1 FROM authority_repair_pending WHERE project_path = OLD.project_path)
|
|
2753
|
+
) AND COALESCE((SELECT enabled FROM context_privilege_state WHERE id = 1), 0) = 0
|
|
2754
|
+
BEGIN
|
|
2755
|
+
SELECT RAISE(ABORT, 'context.db smart-note writes are managed by the Rust module');
|
|
2756
|
+
END;
|
|
2757
|
+
`);
|
|
2758
|
+
}
|
|
2759
|
+
}
|
|
2760
|
+
},
|
|
2761
|
+
{
|
|
2762
|
+
version: 55,
|
|
2763
|
+
description: "make managed-write privilege connection-local",
|
|
2764
|
+
up(db) {
|
|
2765
|
+
const memoriesPresent = tableExists4(db, "memories");
|
|
2766
|
+
const notesPresent = tableExists4(db, "notes");
|
|
2767
|
+
const native = db;
|
|
2768
|
+
const privilegeCheck = typeof native.function === "function" || typeof native.createFunction === "function" ? "mc_privileged_writer() = 0" : "COALESCE((SELECT enabled FROM context_privilege_state WHERE id = 1), 0) = 0";
|
|
2769
|
+
if (memoriesPresent) {
|
|
2770
|
+
db.exec(`
|
|
2771
|
+
DROP TRIGGER IF EXISTS memories_authority_guard_insert;
|
|
2772
|
+
DROP TRIGGER IF EXISTS memories_authority_guard_update;
|
|
2773
|
+
DROP TRIGGER IF EXISTS memories_authority_guard_delete;
|
|
2774
|
+
CREATE TRIGGER memories_authority_guard_insert
|
|
2775
|
+
BEFORE INSERT ON memories
|
|
2776
|
+
WHEN (EXISTS (SELECT 1 FROM authority_managed WHERE project_path = NEW.project_path)
|
|
2777
|
+
OR EXISTS (SELECT 1 FROM authority_repair_pending WHERE project_path = NEW.project_path))
|
|
2778
|
+
AND ${privilegeCheck}
|
|
2779
|
+
BEGIN SELECT RAISE(ABORT, 'context.db memory writes are managed by the Rust module'); END;
|
|
2780
|
+
CREATE TRIGGER memories_authority_guard_update
|
|
2781
|
+
BEFORE UPDATE ON memories
|
|
2782
|
+
WHEN (EXISTS (SELECT 1 FROM authority_managed WHERE project_path = OLD.project_path)
|
|
2783
|
+
OR EXISTS (SELECT 1 FROM authority_managed WHERE project_path = NEW.project_path)
|
|
2784
|
+
OR EXISTS (SELECT 1 FROM authority_repair_pending WHERE project_path = OLD.project_path)
|
|
2785
|
+
OR EXISTS (SELECT 1 FROM authority_repair_pending WHERE project_path = NEW.project_path))
|
|
2786
|
+
AND ${privilegeCheck}
|
|
2787
|
+
BEGIN SELECT RAISE(ABORT, 'context.db memory writes are managed by the Rust module'); END;
|
|
2788
|
+
CREATE TRIGGER memories_authority_guard_delete
|
|
2789
|
+
BEFORE DELETE ON memories
|
|
2790
|
+
WHEN (EXISTS (SELECT 1 FROM authority_managed WHERE project_path = OLD.project_path)
|
|
2791
|
+
OR EXISTS (SELECT 1 FROM authority_repair_pending WHERE project_path = OLD.project_path))
|
|
2792
|
+
AND ${privilegeCheck}
|
|
2793
|
+
BEGIN SELECT RAISE(ABORT, 'context.db memory writes are managed by the Rust module'); END;
|
|
2794
|
+
`);
|
|
2795
|
+
}
|
|
2796
|
+
if (notesPresent) {
|
|
2797
|
+
db.exec(`
|
|
2798
|
+
DROP TRIGGER IF EXISTS notes_authority_guard_insert;
|
|
2799
|
+
DROP TRIGGER IF EXISTS notes_authority_guard_update;
|
|
2800
|
+
DROP TRIGGER IF EXISTS notes_authority_guard_delete;
|
|
2801
|
+
CREATE TRIGGER notes_authority_guard_insert
|
|
2802
|
+
BEFORE INSERT ON notes
|
|
2803
|
+
WHEN NEW.type = 'smart' AND NEW.project_path IS NOT NULL
|
|
2804
|
+
AND (EXISTS (SELECT 1 FROM authority_managed WHERE project_path = NEW.project_path)
|
|
2805
|
+
OR EXISTS (SELECT 1 FROM authority_repair_pending WHERE project_path = NEW.project_path))
|
|
2806
|
+
AND ${privilegeCheck}
|
|
2807
|
+
BEGIN SELECT RAISE(ABORT, 'context.db smart-note writes are managed by the Rust module'); END;
|
|
2808
|
+
CREATE TRIGGER notes_authority_guard_update
|
|
2809
|
+
BEFORE UPDATE ON notes
|
|
2810
|
+
WHEN ((OLD.type = 'smart' AND OLD.project_path IS NOT NULL
|
|
2811
|
+
AND (EXISTS (SELECT 1 FROM authority_managed WHERE project_path = OLD.project_path)
|
|
2812
|
+
OR EXISTS (SELECT 1 FROM authority_repair_pending WHERE project_path = OLD.project_path)))
|
|
2813
|
+
OR (NEW.type = 'smart' AND NEW.project_path IS NOT NULL
|
|
2814
|
+
AND (EXISTS (SELECT 1 FROM authority_managed WHERE project_path = NEW.project_path)
|
|
2815
|
+
OR EXISTS (SELECT 1 FROM authority_repair_pending WHERE project_path = NEW.project_path))))
|
|
2816
|
+
AND ${privilegeCheck}
|
|
2817
|
+
BEGIN SELECT RAISE(ABORT, 'context.db smart-note writes are managed by the Rust module'); END;
|
|
2818
|
+
CREATE TRIGGER notes_authority_guard_delete
|
|
2819
|
+
BEFORE DELETE ON notes
|
|
2820
|
+
WHEN OLD.type = 'smart' AND OLD.project_path IS NOT NULL
|
|
2821
|
+
AND (EXISTS (SELECT 1 FROM authority_managed WHERE project_path = OLD.project_path)
|
|
2822
|
+
OR EXISTS (SELECT 1 FROM authority_repair_pending WHERE project_path = OLD.project_path))
|
|
2823
|
+
AND ${privilegeCheck}
|
|
2824
|
+
BEGIN SELECT RAISE(ABORT, 'context.db smart-note writes are managed by the Rust module'); END;
|
|
2825
|
+
`);
|
|
2826
|
+
}
|
|
2827
|
+
}
|
|
2828
|
+
},
|
|
2829
|
+
{
|
|
2830
|
+
version: 56,
|
|
2831
|
+
description: "record authority capture bounds and pending mirror references",
|
|
2832
|
+
up(db) {
|
|
2833
|
+
db.exec(`
|
|
2834
|
+
CREATE TABLE IF NOT EXISTS authority_capture_bounds (
|
|
2835
|
+
project_path TEXT NOT NULL,
|
|
2836
|
+
domain TEXT NOT NULL CHECK(domain IN ('memories', 'notes')),
|
|
2837
|
+
max_rowid INTEGER NOT NULL,
|
|
2838
|
+
data_version INTEGER NOT NULL,
|
|
2839
|
+
captured_at INTEGER NOT NULL,
|
|
2840
|
+
PRIMARY KEY(project_path, domain)
|
|
2841
|
+
);
|
|
2842
|
+
CREATE TABLE IF NOT EXISTS mirror_pending_references (
|
|
2843
|
+
domain TEXT NOT NULL CHECK(domain = 'memories'),
|
|
2844
|
+
module_project TEXT NOT NULL,
|
|
2845
|
+
module_row_id INTEGER NOT NULL,
|
|
2846
|
+
target_module_row_id INTEGER NOT NULL,
|
|
2847
|
+
PRIMARY KEY(domain, module_project, module_row_id)
|
|
2848
|
+
);
|
|
2849
|
+
CREATE INDEX IF NOT EXISTS idx_mirror_pending_reference_target
|
|
2850
|
+
ON mirror_pending_references(domain, module_project, target_module_row_id);
|
|
2851
|
+
CREATE TABLE IF NOT EXISTS mirror_note_revisions (
|
|
2852
|
+
module_project TEXT NOT NULL,
|
|
2853
|
+
module_row_id INTEGER NOT NULL,
|
|
2854
|
+
context_row_id INTEGER NOT NULL,
|
|
2855
|
+
status_version INTEGER NOT NULL DEFAULT 0,
|
|
2856
|
+
PRIMARY KEY(module_project, module_row_id),
|
|
2857
|
+
UNIQUE(context_row_id)
|
|
2858
|
+
);
|
|
2859
|
+
`);
|
|
2860
|
+
installLatestAuthorityTriggers(db);
|
|
2861
|
+
}
|
|
2862
|
+
},
|
|
2863
|
+
{
|
|
2864
|
+
version: 57,
|
|
2865
|
+
description: "domain mutation epoch for authority capture bounds",
|
|
2866
|
+
up(db) {
|
|
2867
|
+
db.exec(`
|
|
2868
|
+
CREATE TABLE IF NOT EXISTS domain_mutation_epoch (
|
|
2869
|
+
project_path TEXT NOT NULL,
|
|
2870
|
+
domain TEXT NOT NULL CHECK(domain IN ('memories', 'notes')),
|
|
2871
|
+
epoch INTEGER NOT NULL DEFAULT 0,
|
|
2872
|
+
PRIMARY KEY(project_path, domain)
|
|
2873
|
+
);
|
|
2874
|
+
`);
|
|
2875
|
+
if (tableExists4(db, "authority_capture_bounds")) {
|
|
2876
|
+
ensureColumn(db, "authority_capture_bounds", "mutation_epoch", "INTEGER NOT NULL DEFAULT 0");
|
|
2877
|
+
}
|
|
2878
|
+
}
|
|
2879
|
+
},
|
|
2880
|
+
{
|
|
2881
|
+
version: 58,
|
|
2882
|
+
description: "track live module memory identities during mirror replay",
|
|
2883
|
+
up(db) {
|
|
2884
|
+
db.exec(`
|
|
2885
|
+
CREATE TABLE IF NOT EXISTS mirror_live_memory_rows (
|
|
2886
|
+
module_project TEXT NOT NULL,
|
|
2887
|
+
module_row_id INTEGER NOT NULL,
|
|
2888
|
+
category TEXT NOT NULL,
|
|
2889
|
+
normalized_hash TEXT NOT NULL,
|
|
2890
|
+
PRIMARY KEY(module_project, module_row_id)
|
|
2891
|
+
);
|
|
2892
|
+
CREATE INDEX IF NOT EXISTS idx_mirror_live_memory_content
|
|
2893
|
+
ON mirror_live_memory_rows(module_project, category, normalized_hash);
|
|
2894
|
+
CREATE TABLE IF NOT EXISTS mirror_resnapshot_state (
|
|
2895
|
+
domain TEXT PRIMARY KEY CHECK(domain = 'memories'),
|
|
2896
|
+
status TEXT NOT NULL CHECK(status IN ('pending_check', 'resnapshotting', 'complete')),
|
|
2897
|
+
updated_at INTEGER NOT NULL
|
|
2898
|
+
);
|
|
2899
|
+
INSERT OR IGNORE INTO mirror_resnapshot_state(domain, status, updated_at)
|
|
2900
|
+
VALUES ('memories', 'pending_check', 0);
|
|
2901
|
+
`);
|
|
2902
|
+
}
|
|
2903
|
+
},
|
|
2904
|
+
{
|
|
2905
|
+
version: 59,
|
|
2906
|
+
description: "stage paged live memory resnapshots before atomic replacement",
|
|
2907
|
+
up(db) {
|
|
2908
|
+
db.exec(`
|
|
2909
|
+
CREATE TABLE IF NOT EXISTS mirror_live_staging (
|
|
2910
|
+
generation TEXT NOT NULL,
|
|
2911
|
+
module_project TEXT NOT NULL,
|
|
2912
|
+
module_row_id INTEGER NOT NULL,
|
|
2913
|
+
category TEXT NOT NULL,
|
|
2914
|
+
normalized_hash TEXT NOT NULL,
|
|
2915
|
+
PRIMARY KEY(generation, module_project, module_row_id)
|
|
2916
|
+
);
|
|
2917
|
+
CREATE INDEX IF NOT EXISTS idx_mirror_live_staging_generation
|
|
2918
|
+
ON mirror_live_staging(generation);
|
|
2919
|
+
`);
|
|
2920
|
+
}
|
|
2921
|
+
},
|
|
2922
|
+
{
|
|
2923
|
+
version: 60,
|
|
2924
|
+
description: "persist the owning live memory resnapshot generation",
|
|
2925
|
+
up(db) {
|
|
2926
|
+
db.exec(`
|
|
2927
|
+
CREATE TABLE IF NOT EXISTS mirror_resnapshot_state (
|
|
2928
|
+
domain TEXT PRIMARY KEY CHECK(domain = 'memories'),
|
|
2929
|
+
status TEXT NOT NULL CHECK(status IN ('pending_check', 'resnapshotting', 'complete')),
|
|
2930
|
+
updated_at INTEGER NOT NULL
|
|
2931
|
+
);
|
|
2932
|
+
`);
|
|
2933
|
+
db.exec("INSERT OR IGNORE INTO mirror_resnapshot_state(domain, status, updated_at) VALUES ('memories', 'pending_check', 0)");
|
|
2934
|
+
ensureColumn(db, "mirror_resnapshot_state", "generation", "TEXT");
|
|
2935
|
+
}
|
|
2936
|
+
},
|
|
2937
|
+
{
|
|
2938
|
+
version: 61,
|
|
2939
|
+
description: "retain complete memory snapshots for mirror healing",
|
|
2940
|
+
up(db) {
|
|
2941
|
+
ensureColumn(db, "mirror_live_memory_rows", "full_row_snapshot", "TEXT");
|
|
2942
|
+
ensureColumn(db, "mirror_live_staging", "full_row_snapshot", "TEXT");
|
|
2943
|
+
db.prepare(`UPDATE mirror_resnapshot_state
|
|
2944
|
+
SET status = 'pending_check', generation = NULL, updated_at = ?
|
|
2945
|
+
WHERE domain = 'memories'
|
|
2946
|
+
AND status = 'complete'
|
|
2947
|
+
AND NOT EXISTS (
|
|
2948
|
+
SELECT 1 FROM schema_migrations WHERE version = 61
|
|
2949
|
+
)`).run(Date.now());
|
|
2950
|
+
}
|
|
2951
|
+
},
|
|
2952
|
+
{
|
|
2953
|
+
version: 62,
|
|
2954
|
+
description: "durable row-level project identity merge audit log",
|
|
2955
|
+
up(db) {
|
|
2956
|
+
db.exec(`
|
|
2957
|
+
CREATE TABLE IF NOT EXISTS identity_merge_log (
|
|
2958
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
2959
|
+
from_identity TEXT NOT NULL,
|
|
2960
|
+
to_identity TEXT NOT NULL,
|
|
2961
|
+
table_name TEXT NOT NULL,
|
|
2962
|
+
row_id TEXT NOT NULL,
|
|
2963
|
+
action TEXT NOT NULL,
|
|
2964
|
+
target_row_id TEXT,
|
|
2965
|
+
merged_at INTEGER NOT NULL
|
|
2966
|
+
);
|
|
2967
|
+
CREATE INDEX IF NOT EXISTS idx_identity_merge_log_identities
|
|
2968
|
+
ON identity_merge_log(from_identity, to_identity, merged_at);
|
|
2969
|
+
CREATE INDEX IF NOT EXISTS idx_identity_merge_log_table_row
|
|
2970
|
+
ON identity_merge_log(table_name, row_id);
|
|
2971
|
+
`);
|
|
2972
|
+
}
|
|
2973
|
+
},
|
|
2974
|
+
{
|
|
2975
|
+
version: 63,
|
|
2976
|
+
description: "Add anchor_block_id to notes (module note mirror writes it)",
|
|
2977
|
+
up(db) {
|
|
2978
|
+
if (!tableExists4(db, "notes"))
|
|
2979
|
+
return;
|
|
2980
|
+
const columns = db.prepare("PRAGMA table_info(notes)").all();
|
|
2981
|
+
if (!columns.some((column) => column.name === "anchor_block_id")) {
|
|
2982
|
+
db.exec("ALTER TABLE notes ADD COLUMN anchor_block_id TEXT");
|
|
2983
|
+
}
|
|
2984
|
+
}
|
|
2985
|
+
},
|
|
2986
|
+
{
|
|
2987
|
+
version: 64,
|
|
2988
|
+
description: "store project-scoped rendered memory mural",
|
|
2989
|
+
up(db) {
|
|
2990
|
+
db.exec(`
|
|
2991
|
+
CREATE TABLE IF NOT EXISTS mural_manifest (
|
|
2992
|
+
project_path TEXT PRIMARY KEY,
|
|
2993
|
+
image BLOB NOT NULL,
|
|
2994
|
+
content_hash TEXT NOT NULL,
|
|
2995
|
+
rendered_at INTEGER NOT NULL,
|
|
2996
|
+
model TEXT,
|
|
2997
|
+
memory_ids_json TEXT NOT NULL DEFAULT '[]',
|
|
2998
|
+
width INTEGER NOT NULL DEFAULT 1092,
|
|
2999
|
+
height INTEGER NOT NULL DEFAULT 1092
|
|
3000
|
+
);
|
|
3001
|
+
`);
|
|
3002
|
+
ensureColumn(db, "mural_manifest", "model", "TEXT");
|
|
3003
|
+
ensureColumn(db, "mural_manifest", "memory_ids_json", "TEXT NOT NULL DEFAULT '[]'");
|
|
3004
|
+
ensureColumn(db, "mural_manifest", "width", "INTEGER NOT NULL DEFAULT 1092");
|
|
3005
|
+
ensureColumn(db, "mural_manifest", "height", "INTEGER NOT NULL DEFAULT 1092");
|
|
3006
|
+
}
|
|
3007
|
+
},
|
|
3008
|
+
{
|
|
3009
|
+
version: 65,
|
|
3010
|
+
description: "Add per-memory mural cue columns for the deterministic cue-compression cutover",
|
|
3011
|
+
up(db) {
|
|
3012
|
+
if (!tableExists4(db, "memories"))
|
|
3013
|
+
return;
|
|
3014
|
+
ensureColumn(db, "memories", "mural_cue", "TEXT");
|
|
3015
|
+
ensureColumn(db, "memories", "mural_cue_hash", "TEXT");
|
|
3016
|
+
ensureColumn(db, "memories", "mural_cue_at", "INTEGER");
|
|
3017
|
+
}
|
|
3018
|
+
},
|
|
3019
|
+
{
|
|
3020
|
+
version: 66,
|
|
3021
|
+
description: "bound per-session historian upgrade reminders",
|
|
3022
|
+
up(db) {
|
|
3023
|
+
if (!tableExists4(db, "session_meta"))
|
|
3024
|
+
return;
|
|
3025
|
+
ensureColumn(db, "session_meta", "upgrade_reminder_last_sent_at", "INTEGER");
|
|
3026
|
+
ensureColumn(db, "session_meta", "upgrade_reminder_count", "INTEGER NOT NULL DEFAULT 0");
|
|
3027
|
+
}
|
|
3028
|
+
},
|
|
3029
|
+
{
|
|
3030
|
+
version: 67,
|
|
3031
|
+
description: "persist the frozen mural payload with each cached m0 baseline",
|
|
3032
|
+
up(db) {
|
|
3033
|
+
if (!tableExists4(db, "session_meta"))
|
|
3034
|
+
return;
|
|
3035
|
+
ensureColumn(db, "session_meta", "cached_m0_mural_data_url", "TEXT");
|
|
3036
|
+
ensureColumn(db, "session_meta", "cached_m0_mural_hash", "TEXT");
|
|
3037
|
+
}
|
|
3038
|
+
},
|
|
3039
|
+
{
|
|
3040
|
+
version: 68,
|
|
3041
|
+
description: "converge message FTS deletions and same-ID source revisions",
|
|
3042
|
+
up(db) {
|
|
3043
|
+
db.exec(`
|
|
3044
|
+
CREATE TABLE IF NOT EXISTS message_history_source (
|
|
3045
|
+
session_id TEXT NOT NULL,
|
|
3046
|
+
message_id TEXT NOT NULL,
|
|
3047
|
+
message_ordinal INTEGER NOT NULL,
|
|
3048
|
+
source_version TEXT NOT NULL,
|
|
3049
|
+
normalized_content_hash TEXT NOT NULL,
|
|
3050
|
+
role TEXT NOT NULL,
|
|
3051
|
+
harness TEXT NOT NULL DEFAULT 'opencode',
|
|
3052
|
+
updated_at INTEGER NOT NULL,
|
|
3053
|
+
PRIMARY KEY(session_id, message_id)
|
|
3054
|
+
);
|
|
3055
|
+
CREATE INDEX IF NOT EXISTS idx_message_history_source_session_ordinal
|
|
3056
|
+
ON message_history_source(session_id, message_ordinal);
|
|
3057
|
+
|
|
3058
|
+
CREATE TABLE IF NOT EXISTS pending_session_cleanup (
|
|
3059
|
+
session_id TEXT PRIMARY KEY,
|
|
3060
|
+
harness TEXT NOT NULL DEFAULT 'opencode',
|
|
3061
|
+
requested_at INTEGER NOT NULL,
|
|
3062
|
+
last_attempt_at INTEGER
|
|
3063
|
+
);
|
|
3064
|
+
|
|
3065
|
+
CREATE TABLE IF NOT EXISTS message_history_orphan_sweep (
|
|
3066
|
+
harness TEXT PRIMARY KEY,
|
|
3067
|
+
cursor_session_id TEXT NOT NULL DEFAULT '',
|
|
3068
|
+
last_swept_at INTEGER
|
|
3069
|
+
);
|
|
3070
|
+
`);
|
|
3071
|
+
if (tableExists4(db, "message_history_index")) {
|
|
3072
|
+
const columns = new Set(db.prepare("PRAGMA table_info(message_history_index)").all().map((column) => column.name));
|
|
3073
|
+
if (columns.has("session_id") && columns.has("harness") && columns.has("updated_at")) {
|
|
3074
|
+
db.exec(`
|
|
3075
|
+
CREATE INDEX IF NOT EXISTS idx_message_history_index_orphan_sweep
|
|
3076
|
+
ON message_history_index(harness, session_id, updated_at);
|
|
3077
|
+
`);
|
|
3078
|
+
}
|
|
3079
|
+
}
|
|
3080
|
+
}
|
|
3081
|
+
},
|
|
3082
|
+
{
|
|
3083
|
+
version: 69,
|
|
3084
|
+
description: "index visibility mutation discovery and target loading",
|
|
3085
|
+
up(db) {
|
|
3086
|
+
if (!tableExists4(db, "memory_mutation_log"))
|
|
3087
|
+
return;
|
|
3088
|
+
db.exec(`
|
|
3089
|
+
CREATE INDEX IF NOT EXISTS idx_memory_mutation_log_visibility
|
|
3090
|
+
ON memory_mutation_log(project_path, category, id, target_memory_id);
|
|
3091
|
+
CREATE INDEX IF NOT EXISTS idx_memory_mutation_log_target
|
|
3092
|
+
ON memory_mutation_log(project_path, target_memory_id, id);
|
|
3093
|
+
`);
|
|
3094
|
+
}
|
|
3095
|
+
},
|
|
3096
|
+
{
|
|
3097
|
+
version: 70,
|
|
3098
|
+
description: "heal legacy compartments stranded by mismatched tier closing tags (issue #246)",
|
|
3099
|
+
up(db) {
|
|
3100
|
+
healMismatchedTierClose(db, "compartments", true);
|
|
3101
|
+
healMismatchedTierClose(db, "recomp_compartments", false);
|
|
3102
|
+
}
|
|
3103
|
+
},
|
|
3104
|
+
{
|
|
3105
|
+
version: 71,
|
|
3106
|
+
description: "rebuild authority guard triggers to the durable state-table form (issue #253)",
|
|
3107
|
+
up(db) {
|
|
3108
|
+
installLatestAuthorityTriggers(db);
|
|
3109
|
+
}
|
|
3110
|
+
},
|
|
3111
|
+
{
|
|
3112
|
+
version: 72,
|
|
3113
|
+
description: "add per-session compaction mode record column (issue #266)",
|
|
3114
|
+
up(db) {
|
|
3115
|
+
if (tableExists4(db, "session_meta")) {
|
|
3116
|
+
ensureColumn(db, "session_meta", "compaction_mode_record", "TEXT");
|
|
3117
|
+
}
|
|
3118
|
+
}
|
|
3119
|
+
},
|
|
3120
|
+
{
|
|
3121
|
+
version: 73,
|
|
3122
|
+
description: "persist the last successful todowrite permission verdict",
|
|
3123
|
+
up(db) {
|
|
3124
|
+
if (tableExists4(db, "session_meta")) {
|
|
3125
|
+
ensureColumn(db, "session_meta", "todo_permission_denied", "INTEGER NOT NULL DEFAULT 2");
|
|
3126
|
+
}
|
|
3127
|
+
}
|
|
3128
|
+
},
|
|
3129
|
+
{
|
|
3130
|
+
version: 74,
|
|
3131
|
+
description: "persist detected context-limit provenance",
|
|
3132
|
+
up(db) {
|
|
3133
|
+
if (tableExists4(db, "session_meta")) {
|
|
3134
|
+
ensureColumn(db, "session_meta", "detected_context_limit_provenance", "TEXT NOT NULL DEFAULT 'unknown'");
|
|
3135
|
+
}
|
|
3136
|
+
}
|
|
3137
|
+
},
|
|
3138
|
+
{
|
|
3139
|
+
version: 75,
|
|
3140
|
+
description: "persist mural cue validation rejection latches",
|
|
3141
|
+
up(db) {
|
|
3142
|
+
if (!tableExists4(db, "memories"))
|
|
3143
|
+
return;
|
|
3144
|
+
ensureColumn(db, "memories", "mural_cue_rejection_count", "INTEGER NOT NULL DEFAULT 0");
|
|
3145
|
+
}
|
|
3146
|
+
},
|
|
3147
|
+
{
|
|
3148
|
+
version: 76,
|
|
3149
|
+
description: "persist retina provider compilation for smart-note conditions",
|
|
3150
|
+
up(db) {
|
|
3151
|
+
if (!tableExists4(db, "notes"))
|
|
3152
|
+
return;
|
|
3153
|
+
ensureColumn(db, "notes", "compiled_provider", "TEXT");
|
|
3154
|
+
ensureColumn(db, "notes", "compiled_config", "TEXT");
|
|
3155
|
+
ensureColumn(db, "notes", "compiled_at", "INTEGER");
|
|
3156
|
+
ensureColumn(db, "notes", "compile_status", "TEXT CHECK(compile_status IN ('compiled', 'plain', 'refused'))");
|
|
3157
|
+
}
|
|
3158
|
+
},
|
|
3159
|
+
{
|
|
3160
|
+
version: 77,
|
|
3161
|
+
description: "persist scoped provenance for promoted user memories and primers",
|
|
3162
|
+
up(db) {
|
|
3163
|
+
if (tableExists4(db, "user_memories")) {
|
|
3164
|
+
ensureColumn(db, "user_memories", "source_candidate_provenance", "TEXT");
|
|
3165
|
+
}
|
|
3166
|
+
if (tableExists4(db, "primers")) {
|
|
3167
|
+
ensureColumn(db, "primers", "source_candidate_provenance", "TEXT");
|
|
3168
|
+
}
|
|
3169
|
+
}
|
|
3170
|
+
},
|
|
3171
|
+
{
|
|
3172
|
+
version: 78,
|
|
3173
|
+
description: "add migration_pending journal for crash-safe cross-harness session migration",
|
|
3174
|
+
up(db) {
|
|
3175
|
+
db.exec(`
|
|
3176
|
+
CREATE TABLE IF NOT EXISTS migration_pending (
|
|
3177
|
+
migration_key TEXT PRIMARY KEY,
|
|
3178
|
+
source_session_id TEXT NOT NULL,
|
|
3179
|
+
target_harness TEXT NOT NULL,
|
|
3180
|
+
pi_session_id TEXT NOT NULL,
|
|
3181
|
+
final_path TEXT NOT NULL,
|
|
3182
|
+
stage_path TEXT NOT NULL,
|
|
3183
|
+
content_sha256 TEXT NOT NULL,
|
|
3184
|
+
phase TEXT NOT NULL CHECK (phase IN ('staged', 'db_committed')),
|
|
3185
|
+
created_at INTEGER NOT NULL
|
|
3186
|
+
);
|
|
3187
|
+
`);
|
|
3188
|
+
}
|
|
3189
|
+
},
|
|
3190
|
+
{
|
|
3191
|
+
version: 79,
|
|
3192
|
+
description: "record m[0] system-hash and model-key comparison telemetry",
|
|
3193
|
+
up(db) {
|
|
3194
|
+
if (!tableExists4(db, "transform_decisions"))
|
|
3195
|
+
return;
|
|
3196
|
+
ensureColumn(db, "transform_decisions", "system_hash_prev", "TEXT");
|
|
3197
|
+
ensureColumn(db, "transform_decisions", "system_hash_new", "TEXT");
|
|
3198
|
+
ensureColumn(db, "transform_decisions", "m0_model_key_prev", "TEXT");
|
|
3199
|
+
ensureColumn(db, "transform_decisions", "m0_model_key_new", "TEXT");
|
|
3200
|
+
}
|
|
3201
|
+
},
|
|
3202
|
+
{
|
|
3203
|
+
version: 80,
|
|
3204
|
+
description: "record observed m[0] tool-set hash comparisons",
|
|
3205
|
+
up(db) {
|
|
3206
|
+
if (!tableExists4(db, "transform_decisions"))
|
|
3207
|
+
return;
|
|
3208
|
+
ensureColumn(db, "transform_decisions", "m0_tool_set_hash_prev", "TEXT");
|
|
3209
|
+
ensureColumn(db, "transform_decisions", "m0_tool_set_hash_new", "TEXT");
|
|
3210
|
+
}
|
|
3211
|
+
},
|
|
3212
|
+
{
|
|
3213
|
+
version: 81,
|
|
3214
|
+
description: "persist last-known-good transform snapshots across restarts",
|
|
3215
|
+
up(db) {
|
|
3216
|
+
db.exec(`
|
|
3217
|
+
CREATE TABLE IF NOT EXISTS lkg_slots (
|
|
3218
|
+
session_id TEXT PRIMARY KEY,
|
|
3219
|
+
json_prefix TEXT NOT NULL,
|
|
3220
|
+
input_id_seq TEXT NOT NULL,
|
|
3221
|
+
input_content_digests TEXT NOT NULL,
|
|
3222
|
+
input_content_signatures TEXT,
|
|
3223
|
+
last_input_message_id TEXT NOT NULL,
|
|
3224
|
+
model_key TEXT,
|
|
3225
|
+
provider_key TEXT,
|
|
3226
|
+
captured_at INTEGER NOT NULL,
|
|
3227
|
+
row_version INTEGER,
|
|
3228
|
+
capture_sequence INTEGER
|
|
3229
|
+
);
|
|
3230
|
+
`);
|
|
3231
|
+
}
|
|
3232
|
+
},
|
|
3233
|
+
{
|
|
3234
|
+
version: 82,
|
|
3235
|
+
description: "record the origin of memory file-independent mappings",
|
|
3236
|
+
up(db) {
|
|
3237
|
+
if (!tableExists4(db, "memory_verifications"))
|
|
3238
|
+
return;
|
|
3239
|
+
ensureColumn(db, "memory_verifications", "mapping_origin", "TEXT NOT NULL DEFAULT 'mapper'");
|
|
3240
|
+
}
|
|
3241
|
+
},
|
|
3242
|
+
{
|
|
3243
|
+
version: 83,
|
|
3244
|
+
description: "add indexed rowid access for message FTS content",
|
|
3245
|
+
up(db) {
|
|
3246
|
+
db.exec(`
|
|
3247
|
+
CREATE TABLE IF NOT EXISTS message_fts_rowid_map (
|
|
3248
|
+
session_id TEXT NOT NULL,
|
|
3249
|
+
message_ordinal INTEGER NOT NULL,
|
|
3250
|
+
fts_rowid INTEGER NOT NULL,
|
|
3251
|
+
PRIMARY KEY(session_id, message_ordinal)
|
|
3252
|
+
);
|
|
3253
|
+
|
|
3254
|
+
CREATE TABLE IF NOT EXISTS message_fts_rowid_map_backfill_state (
|
|
3255
|
+
id INTEGER PRIMARY KEY CHECK(id = 1),
|
|
3256
|
+
watermark_rowid INTEGER NOT NULL DEFAULT 0,
|
|
3257
|
+
completed INTEGER NOT NULL DEFAULT 0 CHECK(completed IN (0, 1)),
|
|
3258
|
+
updated_at INTEGER NOT NULL DEFAULT 0
|
|
3259
|
+
);
|
|
3260
|
+
INSERT OR IGNORE INTO message_fts_rowid_map_backfill_state
|
|
3261
|
+
(id, watermark_rowid, completed, updated_at)
|
|
3262
|
+
VALUES (1, 0, 0, 0);
|
|
3263
|
+
`);
|
|
3264
|
+
}
|
|
3265
|
+
},
|
|
3266
|
+
{
|
|
3267
|
+
version: 84,
|
|
3268
|
+
description: "persist protected-token floor state per session",
|
|
3269
|
+
up(db) {
|
|
3270
|
+
if (!tableExists4(db, "session_meta"))
|
|
3271
|
+
return;
|
|
3272
|
+
ensureColumn(db, "session_meta", "protected_tokens_effective", "INTEGER");
|
|
3273
|
+
ensureColumn(db, "session_meta", "protected_tokens_pre_snapshot", "TEXT");
|
|
3274
|
+
}
|
|
3275
|
+
},
|
|
3276
|
+
{
|
|
3277
|
+
version: 85,
|
|
3278
|
+
description: "relabel OpenCode 1.x mis-tagged opencode2 session rows (inert; superseded by v87)",
|
|
3279
|
+
up() {}
|
|
3280
|
+
},
|
|
3281
|
+
{
|
|
3282
|
+
version: 86,
|
|
3283
|
+
description: "track tag identity changes per session",
|
|
3284
|
+
up(db) {
|
|
3285
|
+
if (!tableExists4(db, "session_meta") || !tableExists4(db, "tags"))
|
|
3286
|
+
return;
|
|
3287
|
+
ensureColumn(db, "session_meta", "tags_version", "INTEGER NOT NULL DEFAULT 0");
|
|
3288
|
+
db.exec(`
|
|
3289
|
+
CREATE TRIGGER IF NOT EXISTS tags_version_ai AFTER INSERT ON tags BEGIN
|
|
3290
|
+
INSERT INTO session_meta(
|
|
3291
|
+
session_id, harness, last_response_time, cache_ttl, counter, tags_version,
|
|
3292
|
+
last_nudge_tokens, last_nudge_band, last_transform_error, is_subagent,
|
|
3293
|
+
last_context_percentage, last_input_tokens, observed_safe_input_tokens,
|
|
3294
|
+
cache_alert_sent, times_execute_threshold_reached, compartment_in_progress,
|
|
3295
|
+
system_prompt_hash, cleared_reasoning_through_tag
|
|
3296
|
+
) VALUES(NEW.session_id, NEW.harness, 0, '5m', 0, 1, 0, '', '', 0, 0, 0, 0, 0, 0, 0, '', 0)
|
|
3297
|
+
ON CONFLICT(session_id) DO UPDATE SET tags_version = tags_version + 1;
|
|
3298
|
+
END;
|
|
3299
|
+
CREATE TRIGGER IF NOT EXISTS tags_version_ad AFTER DELETE ON tags BEGIN
|
|
3300
|
+
INSERT INTO session_meta(
|
|
3301
|
+
session_id, harness, last_response_time, cache_ttl, counter, tags_version,
|
|
3302
|
+
last_nudge_tokens, last_nudge_band, last_transform_error, is_subagent,
|
|
3303
|
+
last_context_percentage, last_input_tokens, observed_safe_input_tokens,
|
|
3304
|
+
cache_alert_sent, times_execute_threshold_reached, compartment_in_progress,
|
|
3305
|
+
system_prompt_hash, cleared_reasoning_through_tag
|
|
3306
|
+
) VALUES(OLD.session_id, OLD.harness, 0, '5m', 0, 1, 0, '', '', 0, 0, 0, 0, 0, 0, 0, '', 0)
|
|
3307
|
+
ON CONFLICT(session_id) DO UPDATE SET tags_version = tags_version + 1;
|
|
3308
|
+
END;
|
|
3309
|
+
CREATE TRIGGER IF NOT EXISTS tags_version_au
|
|
3310
|
+
AFTER UPDATE OF session_id, message_id, tag_number, type, tool_owner_message_id, status
|
|
3311
|
+
ON tags BEGIN
|
|
3312
|
+
INSERT INTO session_meta(
|
|
3313
|
+
session_id, harness, last_response_time, cache_ttl, counter, tags_version,
|
|
3314
|
+
last_nudge_tokens, last_nudge_band, last_transform_error, is_subagent,
|
|
3315
|
+
last_context_percentage, last_input_tokens, observed_safe_input_tokens,
|
|
3316
|
+
cache_alert_sent, times_execute_threshold_reached, compartment_in_progress,
|
|
3317
|
+
system_prompt_hash, cleared_reasoning_through_tag
|
|
3318
|
+
) VALUES(OLD.session_id, OLD.harness, 0, '5m', 0, 1, 0, '', '', 0, 0, 0, 0, 0, 0, 0, '', 0)
|
|
3319
|
+
ON CONFLICT(session_id) DO UPDATE SET tags_version = tags_version + 1;
|
|
3320
|
+
INSERT INTO session_meta(
|
|
3321
|
+
session_id, harness, last_response_time, cache_ttl, counter, tags_version,
|
|
3322
|
+
last_nudge_tokens, last_nudge_band, last_transform_error, is_subagent,
|
|
3323
|
+
last_context_percentage, last_input_tokens, observed_safe_input_tokens,
|
|
3324
|
+
cache_alert_sent, times_execute_threshold_reached, compartment_in_progress,
|
|
3325
|
+
system_prompt_hash, cleared_reasoning_through_tag
|
|
3326
|
+
)
|
|
3327
|
+
SELECT NEW.session_id, NEW.harness, 0, '5m', 0, 1, 0, '', '', 0, 0, 0, 0, 0, 0, 0, '', 0
|
|
3328
|
+
WHERE NEW.session_id != OLD.session_id
|
|
3329
|
+
ON CONFLICT(session_id) DO UPDATE SET tags_version = tags_version + 1;
|
|
3330
|
+
END;
|
|
3331
|
+
`);
|
|
3332
|
+
}
|
|
3333
|
+
},
|
|
3334
|
+
{
|
|
3335
|
+
version: 87,
|
|
3336
|
+
description: "repair OpenCode harness labels from host-store evidence",
|
|
3337
|
+
up(db) {
|
|
3338
|
+
repairOpenCode2HarnessLabels(db, {
|
|
3339
|
+
tables: [...V85_OPENCODE2_RELABEL_TABLES, ...V85_OPTIONAL_OPENCODE2_RELABEL_TABLES]
|
|
3340
|
+
});
|
|
3341
|
+
}
|
|
3342
|
+
},
|
|
3343
|
+
{
|
|
3344
|
+
version: 88,
|
|
3345
|
+
description: "record the store projection each session's coordinates were derived against",
|
|
3346
|
+
up(db) {
|
|
3347
|
+
if (tableExists4(db, "session_meta")) {
|
|
3348
|
+
ensureColumn(db, "session_meta", "coordinate_generation", "TEXT");
|
|
3349
|
+
ensureColumn(db, "session_meta", "coordinate_rebase_notice", "TEXT");
|
|
3350
|
+
}
|
|
3351
|
+
if (tableExists4(db, "compartments")) {
|
|
3352
|
+
ensureColumn(db, "compartments", "rebase_status", "TEXT NOT NULL DEFAULT 'ok'");
|
|
3353
|
+
}
|
|
3354
|
+
if (tableExists4(db, "recomp_compartments")) {
|
|
3355
|
+
ensureColumn(db, "recomp_compartments", "rebase_status", "TEXT NOT NULL DEFAULT 'ok'");
|
|
3356
|
+
}
|
|
3357
|
+
}
|
|
3358
|
+
},
|
|
3359
|
+
{
|
|
3360
|
+
version: 89,
|
|
3361
|
+
description: "persist indexed message creation times for date-bounded search",
|
|
3362
|
+
up(db) {
|
|
3363
|
+
if (!tableExists4(db, "message_fts_rowid_map"))
|
|
3364
|
+
return;
|
|
3365
|
+
ensureColumn(db, "message_fts_rowid_map", "message_time_ms", "INTEGER");
|
|
3366
|
+
db.exec(`
|
|
3367
|
+
CREATE INDEX IF NOT EXISTS idx_message_fts_rowid_map_session_time
|
|
3368
|
+
ON message_fts_rowid_map(session_id, message_time_ms);
|
|
3369
|
+
CREATE TABLE IF NOT EXISTS message_time_backfill_state (
|
|
3370
|
+
id INTEGER PRIMARY KEY CHECK(id = 1),
|
|
3371
|
+
cursor_session_id TEXT NOT NULL DEFAULT '',
|
|
3372
|
+
cursor_ordinal INTEGER NOT NULL DEFAULT 0,
|
|
3373
|
+
completed INTEGER NOT NULL DEFAULT 0 CHECK(completed IN (0, 1)),
|
|
3374
|
+
updated_at INTEGER NOT NULL DEFAULT 0
|
|
3375
|
+
);
|
|
3376
|
+
INSERT OR IGNORE INTO message_time_backfill_state
|
|
3377
|
+
(id, cursor_session_id, cursor_ordinal, completed, updated_at)
|
|
3378
|
+
VALUES (1, '', 0, 0, 0);
|
|
3379
|
+
`);
|
|
3380
|
+
}
|
|
3381
|
+
},
|
|
3382
|
+
{
|
|
3383
|
+
version: 90,
|
|
3384
|
+
description: "record compartment lease owner pids",
|
|
3385
|
+
up(db) {
|
|
3386
|
+
if (!tableExists4(db, "compartment_state_lease"))
|
|
3387
|
+
return;
|
|
3388
|
+
ensureColumn(db, "compartment_state_lease", "owner_pid", "INTEGER");
|
|
3389
|
+
}
|
|
3390
|
+
},
|
|
3391
|
+
{
|
|
3392
|
+
version: 91,
|
|
3393
|
+
description: "per-project embedding high-water mark for memories written outside this host",
|
|
3394
|
+
up(db) {
|
|
3395
|
+
db.exec(`
|
|
3396
|
+
CREATE TABLE IF NOT EXISTS memory_embedding_watermarks (
|
|
3397
|
+
project_path TEXT PRIMARY KEY,
|
|
3398
|
+
written_memory_id INTEGER NOT NULL DEFAULT 0,
|
|
3399
|
+
embedded_memory_id INTEGER NOT NULL DEFAULT 0,
|
|
3400
|
+
updated_at INTEGER NOT NULL DEFAULT 0
|
|
3401
|
+
);
|
|
3402
|
+
`);
|
|
3403
|
+
}
|
|
3404
|
+
},
|
|
3405
|
+
{
|
|
3406
|
+
version: 92,
|
|
3407
|
+
description: "store-level offline single-store state and canonical compartment boundaries",
|
|
3408
|
+
up(db) {
|
|
3409
|
+
for (const table of ["compartments", "recomp_compartments"]) {
|
|
3410
|
+
if (!tableExists4(db, table))
|
|
3411
|
+
continue;
|
|
3412
|
+
ensureColumn(db, table, "start_block_index", "INTEGER");
|
|
3413
|
+
ensureColumn(db, table, "end_block_index", "INTEGER");
|
|
3414
|
+
}
|
|
3415
|
+
db.exec(`
|
|
3416
|
+
CREATE TABLE IF NOT EXISTS single_store_state (
|
|
3417
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
3418
|
+
state TEXT NOT NULL CHECK (state IN ('required', 'migrated')),
|
|
3419
|
+
migrated_at INTEGER,
|
|
3420
|
+
migrated_by TEXT,
|
|
3421
|
+
backup_dir TEXT,
|
|
3422
|
+
report_json TEXT
|
|
3423
|
+
);
|
|
3424
|
+
INSERT OR IGNORE INTO single_store_state(id, state) VALUES (1, 'required');
|
|
3425
|
+
`);
|
|
3426
|
+
}
|
|
3427
|
+
},
|
|
3428
|
+
{
|
|
3429
|
+
version: 93,
|
|
3430
|
+
description: "per-session compartment history revision for shared readers",
|
|
3431
|
+
up(db) {
|
|
3432
|
+
installCompartmentHistoryVersions(db);
|
|
3433
|
+
}
|
|
3434
|
+
},
|
|
3435
|
+
{
|
|
3436
|
+
version: 94,
|
|
3437
|
+
description: "store LKG prefixes as slices and replay decisions as rows instead of growing records",
|
|
3438
|
+
up(db) {
|
|
3439
|
+
splitLkgSlotPrefixes(db);
|
|
3440
|
+
splitReplayDecisions(db);
|
|
3441
|
+
}
|
|
3442
|
+
},
|
|
3443
|
+
{
|
|
3444
|
+
version: 95,
|
|
3445
|
+
description: "cover cleanup and retention reads, index git FTS rowids and store temporal replay decisions",
|
|
3446
|
+
up(db) {
|
|
3447
|
+
installV95PerfSchema(db, true);
|
|
3448
|
+
}
|
|
3449
|
+
}
|
|
3450
|
+
];
|
|
3451
|
+
var LATEST_MIGRATION_VERSION = MIGRATIONS.reduce((max, m) => Math.max(max, m.version), 0);
|
|
3452
|
+
function ensureMigrationsTable(db) {
|
|
3453
|
+
db.exec(`
|
|
3454
|
+
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
3455
|
+
version INTEGER PRIMARY KEY,
|
|
3456
|
+
description TEXT NOT NULL,
|
|
3457
|
+
applied_at INTEGER NOT NULL
|
|
3458
|
+
)
|
|
3459
|
+
`);
|
|
3460
|
+
}
|
|
3461
|
+
function getCurrentVersion(db) {
|
|
3462
|
+
const row = db.prepare("SELECT COALESCE(MAX(version), 0) AS version FROM schema_migrations WHERE version < ?").get(FORK_MIGRATION_VERSION_FLOOR);
|
|
3463
|
+
return row?.version ?? 0;
|
|
3464
|
+
}
|
|
3465
|
+
function isMigrationApplied(db, version) {
|
|
3466
|
+
return db.prepare("SELECT 1 FROM schema_migrations WHERE version = ?").get(version) != null;
|
|
3467
|
+
}
|
|
3468
|
+
function hasPendingMigrations(db) {
|
|
3469
|
+
const bookkeeping = db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'schema_migrations'").get();
|
|
3470
|
+
if (bookkeeping == null)
|
|
3471
|
+
return true;
|
|
3472
|
+
const currentVersion = getCurrentVersion(db);
|
|
3473
|
+
return MIGRATIONS.some((candidate) => candidate.version > currentVersion && !isMigrationApplied(db, candidate.version));
|
|
3474
|
+
}
|
|
3475
|
+
var mainThreadMigrationBodies = 0;
|
|
3476
|
+
function getMainThreadMigrationBodyCount() {
|
|
3477
|
+
return mainThreadMigrationBodies;
|
|
3478
|
+
}
|
|
3479
|
+
function isSiblingMigrationConflict(db, error, version) {
|
|
3480
|
+
if (!(error instanceof Error))
|
|
3481
|
+
return false;
|
|
3482
|
+
const msg = error.message;
|
|
3483
|
+
if (!msg.includes("schema_migrations"))
|
|
3484
|
+
return false;
|
|
3485
|
+
if (!msg.toLowerCase().includes("version"))
|
|
3486
|
+
return false;
|
|
3487
|
+
const confirmed = db.prepare("SELECT 1 FROM schema_migrations WHERE version = ?").get(version);
|
|
3488
|
+
return confirmed != null;
|
|
3489
|
+
}
|
|
3490
|
+
function runMigrations(db) {
|
|
3491
|
+
try {
|
|
3492
|
+
ensureMigrationsTable(db);
|
|
3493
|
+
} catch (error) {
|
|
3494
|
+
if (isSqliteLockError(error)) {
|
|
3495
|
+
throw new MigrationLockBusyError(`failed to prepare migration lock: ${error instanceof Error ? error.message : String(error)}`);
|
|
3496
|
+
}
|
|
3497
|
+
throw error;
|
|
3498
|
+
}
|
|
3499
|
+
let loggedPlan = false;
|
|
3500
|
+
let touchedLegacyAuthorityBatch = false;
|
|
3501
|
+
while (true) {
|
|
3502
|
+
let migration;
|
|
3503
|
+
const migrationState = {};
|
|
3504
|
+
let currentVersion = 0;
|
|
3505
|
+
try {
|
|
3506
|
+
currentVersion = getCurrentVersion(db);
|
|
3507
|
+
const pendingMigration = MIGRATIONS.find((candidate) => candidate.version > currentVersion && !isMigrationApplied(db, candidate.version));
|
|
3508
|
+
if (!pendingMigration)
|
|
3509
|
+
break;
|
|
3510
|
+
migration = undefined;
|
|
3511
|
+
let transactionStartedAt = 0;
|
|
3512
|
+
const applied = db.transaction(() => {
|
|
3513
|
+
transactionStartedAt = performance.now();
|
|
3514
|
+
currentVersion = getCurrentVersion(db);
|
|
3515
|
+
migration = MIGRATIONS.find((candidate) => candidate.version > currentVersion && !isMigrationApplied(db, candidate.version));
|
|
3516
|
+
migrationState.value = migration;
|
|
3517
|
+
if (!migration)
|
|
3518
|
+
return false;
|
|
3519
|
+
if (!loggedPlan) {
|
|
3520
|
+
const pendingCount = MIGRATIONS.filter((candidate) => candidate.version > currentVersion && !isMigrationApplied(db, candidate.version)).length;
|
|
3521
|
+
log2(`[migrations] current upstream migration lane: ${currentVersion}, applying ${pendingCount} migration(s)`);
|
|
3522
|
+
loggedPlan = true;
|
|
3523
|
+
}
|
|
3524
|
+
if (isMainThread)
|
|
3525
|
+
mainThreadMigrationBodies += 1;
|
|
3526
|
+
migration.up(db);
|
|
3527
|
+
db.prepare("INSERT INTO schema_migrations (version, description, applied_at) VALUES (?, ?, ?)").run(migration.version, migration.description, Date.now());
|
|
3528
|
+
return true;
|
|
3529
|
+
}).immediate();
|
|
3530
|
+
logSlowWriteTransaction("migration-runner", transactionStartedAt);
|
|
3531
|
+
migration = migrationState.value;
|
|
3532
|
+
if (!applied || !migration)
|
|
3533
|
+
break;
|
|
3534
|
+
if (migration.version <= 61)
|
|
3535
|
+
touchedLegacyAuthorityBatch = true;
|
|
3536
|
+
log2(`[migrations] applied v${migration.version}: ${migration.description}`);
|
|
3537
|
+
} catch (error) {
|
|
3538
|
+
if (!migration && isSqliteLockError(error)) {
|
|
3539
|
+
throw new MigrationLockBusyError(`failed to acquire migration write lock: ${error instanceof Error ? error.message : String(error)}`);
|
|
3540
|
+
}
|
|
3541
|
+
if (migration && isSiblingMigrationConflict(db, error, migration.version)) {
|
|
3542
|
+
log2(`[migrations] v${migration.version} already applied by sibling instance — resuming with re-read version`);
|
|
3543
|
+
const reReadVersion = getCurrentVersion(db);
|
|
3544
|
+
if (reReadVersion > currentVersion)
|
|
3545
|
+
continue;
|
|
3546
|
+
throw new Error(`Migration v${migration.version} failed: sibling conflict reported but version did not advance. Database may need manual repair.`);
|
|
3547
|
+
}
|
|
3548
|
+
const version = migration?.version ?? currentVersion + 1;
|
|
3549
|
+
const description = migration?.description ?? "acquire migration write lock";
|
|
3550
|
+
log2(`[migrations] FAILED v${version}: ${description} — ${error instanceof Error ? error.message : String(error)}`);
|
|
3551
|
+
throw new Error(`Migration v${version} failed: ${error instanceof Error ? error.message : String(error)}. Database may need manual repair.`);
|
|
3552
|
+
}
|
|
3553
|
+
}
|
|
3554
|
+
if (touchedLegacyAuthorityBatch) {
|
|
3555
|
+
try {
|
|
3556
|
+
let transactionStartedAt = 0;
|
|
3557
|
+
db.transaction(() => {
|
|
3558
|
+
transactionStartedAt = performance.now();
|
|
3559
|
+
installLatestAuthorityTriggers(db);
|
|
3560
|
+
}).immediate();
|
|
3561
|
+
logSlowWriteTransaction("migration-runner", transactionStartedAt);
|
|
3562
|
+
} catch (error) {
|
|
3563
|
+
throw new Error(`Migration authority-trigger postcondition failed: ${error instanceof Error ? error.message : String(error)}. Database may need manual repair.`);
|
|
3564
|
+
}
|
|
3565
|
+
}
|
|
3566
|
+
if (loggedPlan) {
|
|
3567
|
+
log2(`[migrations] upstream migration lane now: ${MIGRATIONS[MIGRATIONS.length - 1].version}`);
|
|
3568
|
+
}
|
|
3569
|
+
}
|
|
3570
|
+
async function runMigrationsWithRetry(db, options = {}) {
|
|
3571
|
+
const retryDelaysMs = options.retryDelaysMs ?? MIGRATION_LOCK_RETRY_DELAYS_MS;
|
|
3572
|
+
const sleep = options.sleep ?? ((delayMs) => new Promise((resolve) => setTimeout(resolve, delayMs)));
|
|
3573
|
+
const totalAttempts = retryDelaysMs.length + 1;
|
|
3574
|
+
for (let attempt = 1;attempt <= totalAttempts; attempt += 1) {
|
|
3575
|
+
log2(`[migrations] migration lock check attempt ${attempt}/${totalAttempts}`);
|
|
3576
|
+
try {
|
|
3577
|
+
runMigrations(db);
|
|
3578
|
+
return;
|
|
3579
|
+
} catch (error) {
|
|
3580
|
+
if (!(error instanceof MigrationLockBusyError))
|
|
3581
|
+
throw error;
|
|
3582
|
+
const delayMs = retryDelaysMs[attempt - 1];
|
|
3583
|
+
if (delayMs === undefined)
|
|
3584
|
+
throw error;
|
|
3585
|
+
log2(`[migrations] migration write lock is busy; retrying attempt ${attempt + 1}/${totalAttempts} in ${delayMs}ms`);
|
|
3586
|
+
await sleep(delayMs);
|
|
3587
|
+
}
|
|
3588
|
+
}
|
|
3589
|
+
}
|
|
3590
|
+
|
|
3591
|
+
export { unescapeXml, parseCompartmentOutput, layoutLkgPrefix, assembleLkgPrefix, LKG_SLOT_CHUNKS_DDL, SESSION_REPLAY_DECISIONS_DDL, LKG_SLOTS_DDL, installCompartmentHistoryVersions, resolveWorkspaceShareCategories, resolveWorkspaceIdentitySet, expandWorkspaceIdentitySetWithAliases, resolveStoredPathWorkspaceIdentity, storedPathBelongsToWorkspace, sourceNameForMemory, computeWorkspaceEpochFingerprint, FORK_MIGRATION_VERSION_FLOOR, managedAuthorityNoteRow, hasPendingMigrations, getMainThreadMigrationBodyCount, runMigrations, runMigrationsWithRetry };
|