@sumeru/server 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/LICENSE +18 -0
  2. package/dist/.build-fingerprint +1 -0
  3. package/dist/config.d.ts +14 -0
  4. package/dist/config.d.ts.map +1 -0
  5. package/dist/config.js +142 -0
  6. package/dist/config.js.map +1 -0
  7. package/dist/envelope.d.ts +28 -0
  8. package/dist/envelope.d.ts.map +1 -0
  9. package/dist/envelope.js +43 -0
  10. package/dist/envelope.js.map +1 -0
  11. package/dist/export/bundle.d.ts +28 -0
  12. package/dist/export/bundle.d.ts.map +1 -0
  13. package/dist/export/bundle.js +78 -0
  14. package/dist/export/bundle.js.map +1 -0
  15. package/dist/export/handler.d.ts +24 -0
  16. package/dist/export/handler.d.ts.map +1 -0
  17. package/dist/export/handler.js +102 -0
  18. package/dist/export/handler.js.map +1 -0
  19. package/dist/export/index.d.ts +3 -0
  20. package/dist/export/index.d.ts.map +1 -0
  21. package/dist/export/index.js +3 -0
  22. package/dist/export/index.js.map +1 -0
  23. package/dist/handler.d.ts +24 -0
  24. package/dist/handler.d.ts.map +1 -0
  25. package/dist/handler.js +622 -0
  26. package/dist/handler.js.map +1 -0
  27. package/dist/index.d.ts +12 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +10 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/ocas/index.d.ts +3 -0
  32. package/dist/ocas/index.d.ts.map +1 -0
  33. package/dist/ocas/index.js +3 -0
  34. package/dist/ocas/index.js.map +1 -0
  35. package/dist/ocas/schemas.d.ts +41 -0
  36. package/dist/ocas/schemas.d.ts.map +1 -0
  37. package/dist/ocas/schemas.js +108 -0
  38. package/dist/ocas/schemas.js.map +1 -0
  39. package/dist/ocas/store.d.ts +58 -0
  40. package/dist/ocas/store.d.ts.map +1 -0
  41. package/dist/ocas/store.js +139 -0
  42. package/dist/ocas/store.js.map +1 -0
  43. package/dist/search/handler.d.ts +54 -0
  44. package/dist/search/handler.d.ts.map +1 -0
  45. package/dist/search/handler.js +178 -0
  46. package/dist/search/handler.js.map +1 -0
  47. package/dist/search/index.d.ts +4 -0
  48. package/dist/search/index.d.ts.map +1 -0
  49. package/dist/search/index.js +3 -0
  50. package/dist/search/index.js.map +1 -0
  51. package/dist/search/sqlite-index.d.ts +49 -0
  52. package/dist/search/sqlite-index.d.ts.map +1 -0
  53. package/dist/search/sqlite-index.js +508 -0
  54. package/dist/search/sqlite-index.js.map +1 -0
  55. package/dist/search/types.d.ts +143 -0
  56. package/dist/search/types.d.ts.map +1 -0
  57. package/dist/search/types.js +10 -0
  58. package/dist/search/types.js.map +1 -0
  59. package/dist/session/cwd.d.ts +31 -0
  60. package/dist/session/cwd.d.ts.map +1 -0
  61. package/dist/session/cwd.js +54 -0
  62. package/dist/session/cwd.js.map +1 -0
  63. package/dist/session/id.d.ts +12 -0
  64. package/dist/session/id.d.ts.map +1 -0
  65. package/dist/session/id.js +76 -0
  66. package/dist/session/id.js.map +1 -0
  67. package/dist/session/index.d.ts +5 -0
  68. package/dist/session/index.d.ts.map +1 -0
  69. package/dist/session/index.js +4 -0
  70. package/dist/session/index.js.map +1 -0
  71. package/dist/session/store.d.ts +89 -0
  72. package/dist/session/store.d.ts.map +1 -0
  73. package/dist/session/store.js +258 -0
  74. package/dist/session/store.js.map +1 -0
  75. package/dist/sse/buffer.d.ts +53 -0
  76. package/dist/sse/buffer.d.ts.map +1 -0
  77. package/dist/sse/buffer.js +119 -0
  78. package/dist/sse/buffer.js.map +1 -0
  79. package/dist/sse/index.d.ts +3 -0
  80. package/dist/sse/index.d.ts.map +1 -0
  81. package/dist/sse/index.js +3 -0
  82. package/dist/sse/index.js.map +1 -0
  83. package/dist/sse/messages.d.ts +30 -0
  84. package/dist/sse/messages.d.ts.map +1 -0
  85. package/dist/sse/messages.js +489 -0
  86. package/dist/sse/messages.js.map +1 -0
  87. package/dist/start.d.ts +22 -0
  88. package/dist/start.d.ts.map +1 -0
  89. package/dist/start.js +86 -0
  90. package/dist/start.js.map +1 -0
  91. package/dist/types.d.ts +252 -0
  92. package/dist/types.d.ts.map +1 -0
  93. package/dist/types.js +10 -0
  94. package/dist/types.js.map +1 -0
  95. package/package.json +31 -0
@@ -0,0 +1,508 @@
1
+ /**
2
+ * Phase 5 — FTS5 search index.
3
+ *
4
+ * This module owns the SQLite FTS5 schema that backs Sumeru's session search.
5
+ * The index lives in `<ocasDir>/_store.db` — the same file `@ocas/fs` opens
6
+ * for variables and tags. We open a second `DatabaseSync` handle on the same
7
+ * file (safe because `@ocas/fs` enables WAL).
8
+ *
9
+ * The schema:
10
+ * - `sumeru_turn_index` — one row per turn (PK = turn ocas hash)
11
+ * - `sumeru_turn_fts` — contentless FTS5 virtual table (mirrors content)
12
+ * - `sumeru_session_index` — one row per session (PK = session id)
13
+ *
14
+ * Triggers keep `sumeru_turn_fts` in lockstep with `sumeru_turn_index`.
15
+ *
16
+ * All write paths are idempotent on the turn hash / session id, so re-indexing
17
+ * the same node is a no-op.
18
+ */
19
+ import { DatabaseSync } from "node:sqlite";
20
+ const SCHEMA_DDL = `
21
+ CREATE TABLE IF NOT EXISTS sumeru_turn_index (
22
+ turn_hash TEXT PRIMARY KEY,
23
+ session_id TEXT NOT NULL,
24
+ gateway TEXT NOT NULL,
25
+ turn_index INTEGER NOT NULL,
26
+ role TEXT NOT NULL,
27
+ content TEXT NOT NULL,
28
+ created_at TEXT NOT NULL
29
+ );
30
+ CREATE INDEX IF NOT EXISTS idx_sumeru_turn_index_session
31
+ ON sumeru_turn_index(session_id);
32
+ CREATE INDEX IF NOT EXISTS idx_sumeru_turn_index_gateway
33
+ ON sumeru_turn_index(gateway);
34
+
35
+ CREATE TABLE IF NOT EXISTS sumeru_session_index (
36
+ session_id TEXT PRIMARY KEY,
37
+ gateway TEXT NOT NULL,
38
+ adapter TEXT NOT NULL,
39
+ status TEXT NOT NULL,
40
+ created_at TEXT NOT NULL,
41
+ last_active_at TEXT NOT NULL,
42
+ turn_count INTEGER NOT NULL DEFAULT 0,
43
+ meta_hash TEXT
44
+ );
45
+ CREATE INDEX IF NOT EXISTS idx_sumeru_session_index_gateway
46
+ ON sumeru_session_index(gateway);
47
+ CREATE INDEX IF NOT EXISTS idx_sumeru_session_index_last_active
48
+ ON sumeru_session_index(last_active_at DESC);
49
+
50
+ -- Phase 6 (Refs #399): durable, ordered, per-session turn-list pointer. This
51
+ -- is the canonical list the session store rehydrates on boot. It is NOT owned
52
+ -- by FTS and is never cleared by rebuild().
53
+ CREATE TABLE IF NOT EXISTS sumeru_session_turns (
54
+ session_id TEXT NOT NULL,
55
+ turn_index INTEGER NOT NULL,
56
+ turn_hash TEXT NOT NULL,
57
+ PRIMARY KEY (session_id, turn_index)
58
+ );
59
+ CREATE INDEX IF NOT EXISTS idx_sumeru_session_turns_session
60
+ ON sumeru_session_turns(session_id);
61
+
62
+ CREATE VIRTUAL TABLE IF NOT EXISTS sumeru_turn_fts USING fts5(
63
+ content,
64
+ content='sumeru_turn_index',
65
+ content_rowid='rowid',
66
+ tokenize='unicode61 remove_diacritics 2'
67
+ );
68
+
69
+ CREATE TRIGGER IF NOT EXISTS sumeru_turn_fts_ai AFTER INSERT ON sumeru_turn_index BEGIN
70
+ INSERT INTO sumeru_turn_fts(rowid, content) VALUES (new.rowid, new.content);
71
+ END;
72
+ CREATE TRIGGER IF NOT EXISTS sumeru_turn_fts_ad AFTER DELETE ON sumeru_turn_index BEGIN
73
+ INSERT INTO sumeru_turn_fts(sumeru_turn_fts, rowid, content) VALUES('delete', old.rowid, old.content);
74
+ END;
75
+ `;
76
+ /**
77
+ * Open a second SQLite handle on the same `_store.db` file `@ocas/fs` writes
78
+ * vars/tags into. Creates the FTS5 schema if not present. Retries up to 3×
79
+ * on `SQLITE_BUSY` with a 50 ms backoff.
80
+ *
81
+ * Throws `failed to create FTS5 index: <cause>` on persistent failure — the
82
+ * caller (`openSumeruOcas`) prepends `failed to open ocas store at <dir>: `
83
+ * so all boot failures share the same prefix.
84
+ */
85
+ export function createSearchIndex(dbPath) {
86
+ const db = openWithRetry(dbPath);
87
+ try {
88
+ db.exec("BEGIN");
89
+ db.exec(SCHEMA_DDL);
90
+ migrateMetaHashColumn(db);
91
+ db.exec("COMMIT");
92
+ }
93
+ catch (err) {
94
+ try {
95
+ db.exec("ROLLBACK");
96
+ }
97
+ catch {
98
+ // best-effort
99
+ }
100
+ const cause = err instanceof Error ? err.message : String(err);
101
+ throw new Error(`failed to create FTS5 index: ${cause}`);
102
+ }
103
+ const insertSession = db.prepare(`
104
+ INSERT INTO sumeru_session_index
105
+ (session_id, gateway, adapter, status, created_at, last_active_at, turn_count, meta_hash)
106
+ VALUES (?, ?, ?, 'idle', ?, ?, 0, ?)
107
+ ON CONFLICT(session_id) DO NOTHING
108
+ `);
109
+ const insertTurn = db.prepare(`
110
+ INSERT INTO sumeru_turn_index
111
+ (turn_hash, session_id, gateway, turn_index, role, content, created_at)
112
+ VALUES (?, ?, ?, ?, ?, ?, ?)
113
+ ON CONFLICT(turn_hash) DO NOTHING
114
+ `);
115
+ const bumpSession = db.prepare(`
116
+ UPDATE sumeru_session_index
117
+ SET last_active_at = ?,
118
+ turn_count = turn_count + 1
119
+ WHERE session_id = ?
120
+ `);
121
+ const closeSession = db.prepare(`
122
+ UPDATE sumeru_session_index
123
+ SET status = 'closed'
124
+ WHERE session_id = ?
125
+ `);
126
+ const countTurns = db.prepare(`SELECT COUNT(*) AS c FROM sumeru_turn_index`);
127
+ // Phase 6 (Refs #399): turn-list pointer statements.
128
+ const appendTurn = db.prepare(`
129
+ INSERT INTO sumeru_session_turns (session_id, turn_index, turn_hash)
130
+ VALUES (?, ?, ?)
131
+ ON CONFLICT(session_id, turn_index) DO NOTHING
132
+ `);
133
+ const selectTurnsForSession = db.prepare(`
134
+ SELECT turn_hash
135
+ FROM sumeru_session_turns
136
+ WHERE session_id = ?
137
+ ORDER BY turn_index ASC
138
+ `);
139
+ const selectAllTurns = db.prepare(`
140
+ SELECT session_id, turn_hash
141
+ FROM sumeru_session_turns
142
+ ORDER BY session_id, turn_index ASC
143
+ `);
144
+ const selectAllSessions = db.prepare(`
145
+ SELECT session_id, gateway, adapter, status,
146
+ created_at, last_active_at, turn_count, meta_hash
147
+ FROM sumeru_session_index
148
+ ORDER BY created_at ASC
149
+ `);
150
+ function indexSessionMeta(meta) {
151
+ insertSession.run(meta.sessionId, meta.gateway, meta.adapter, meta.createdAt, meta.createdAt, meta.metaHash);
152
+ }
153
+ function appendSessionTurn(sessionId, turnIndex, turnHash) {
154
+ appendTurn.run(sessionId, turnIndex, turnHash);
155
+ }
156
+ function listSessionTurns(sessionId) {
157
+ const rows = selectTurnsForSession.all(sessionId);
158
+ return rows.map((r) => r.turn_hash);
159
+ }
160
+ function loadSessionTurnsBulk() {
161
+ const rows = selectAllTurns.all();
162
+ const out = new Map();
163
+ for (const row of rows) {
164
+ let list = out.get(row.session_id);
165
+ if (list === undefined) {
166
+ list = [];
167
+ out.set(row.session_id, list);
168
+ }
169
+ list.push(row.turn_hash);
170
+ }
171
+ return out;
172
+ }
173
+ function loadSessionRows() {
174
+ const rows = selectAllSessions.all();
175
+ return rows.map((r) => ({
176
+ sessionId: r.session_id,
177
+ gateway: r.gateway,
178
+ adapter: r.adapter,
179
+ status: normalizeStatus(r.status),
180
+ createdAt: r.created_at,
181
+ lastActiveAt: r.last_active_at,
182
+ turnCount: Number(r.turn_count ?? 0),
183
+ metaHash: r.meta_hash === null ? null : r.meta_hash,
184
+ }));
185
+ }
186
+ function indexTurn(input) {
187
+ db.exec("BEGIN");
188
+ try {
189
+ const result = insertTurn.run(input.turnHash, input.sessionId, input.gateway, input.turnIndex, input.role, input.content, input.createdAt);
190
+ // On INSERT-vs-conflict: only bump turn_count when a new row was
191
+ // actually inserted. node:sqlite's run() returns { changes } where
192
+ // changes === 0 means the conflict path took DO NOTHING.
193
+ if (Number(result.changes ?? 0) > 0) {
194
+ bumpSession.run(input.createdAt, input.sessionId);
195
+ }
196
+ db.exec("COMMIT");
197
+ }
198
+ catch (err) {
199
+ try {
200
+ db.exec("ROLLBACK");
201
+ }
202
+ catch {
203
+ // best-effort
204
+ }
205
+ throw err;
206
+ }
207
+ }
208
+ function markSessionClosed(sessionId) {
209
+ try {
210
+ closeSession.run(sessionId);
211
+ }
212
+ catch (err) {
213
+ const cause = err instanceof Error ? err.message : String(err);
214
+ console.warn(`[sumeru] search index update failed: ${cause}`);
215
+ }
216
+ }
217
+ function search(opts) {
218
+ const trimmed = opts.query.trim();
219
+ if (trimmed.length === 0) {
220
+ return { query: "", results: [], total: 0 };
221
+ }
222
+ const matchExpr = quoteFtsPhrase(trimmed);
223
+ const limit = clamp(opts.limit, 1, 100);
224
+ const offset = Math.max(0, opts.offset);
225
+ // FTS5 auxiliary functions (bm25, snippet) only work in queries that
226
+ // reference the FTS5 virtual table at the top level; they don't work
227
+ // through CTEs. So we collect raw matches first, then aggregate in JS.
228
+ const matchStmt = db.prepare(`
229
+ SELECT t.session_id AS session_id,
230
+ snippet(sumeru_turn_fts, 0, '<<', '>>', '…', 24) AS snip,
231
+ bm25(sumeru_turn_fts) AS score
232
+ FROM sumeru_turn_fts
233
+ JOIN sumeru_turn_index t ON t.rowid = sumeru_turn_fts.rowid
234
+ WHERE sumeru_turn_fts MATCH ?
235
+ AND ( ?2 IS NULL OR t.gateway = ?2 )
236
+ ORDER BY score ASC
237
+ `);
238
+ let matchRows;
239
+ try {
240
+ matchRows = matchStmt.all(matchExpr, opts.gateway);
241
+ }
242
+ catch (err) {
243
+ const cause = err instanceof Error ? err.message : String(err);
244
+ console.warn(`[sumeru] search index query failed: ${cause}`);
245
+ return { query: trimmed, results: [], total: 0 };
246
+ }
247
+ // Aggregate to session granularity — keep best (lowest BM25) per session.
248
+ // `matchRows` is already sorted ASC by score, so the first occurrence per
249
+ // session is its best.
250
+ const bestBySession = new Map();
251
+ for (const row of matchRows) {
252
+ if (!bestBySession.has(row.session_id)) {
253
+ bestBySession.set(row.session_id, {
254
+ score: Number(row.score),
255
+ snip: String(row.snip),
256
+ });
257
+ }
258
+ }
259
+ const total = bestBySession.size;
260
+ if (total === 0) {
261
+ return { query: trimmed, results: [], total: 0 };
262
+ }
263
+ // Read session rows in one IN-clause query, then re-order in JS by best
264
+ // score ASC, last_active_at DESC for stable tie-break.
265
+ const ids = Array.from(bestBySession.keys());
266
+ const placeholders = ids.map(() => "?").join(", ");
267
+ const sessionStmt = db.prepare(`
268
+ SELECT session_id, gateway, status, last_active_at, turn_count
269
+ FROM sumeru_session_index
270
+ WHERE session_id IN (${placeholders})
271
+ `);
272
+ const sessionRows = sessionStmt.all(...ids);
273
+ const merged = [];
274
+ for (const sess of sessionRows) {
275
+ const best = bestBySession.get(sess.session_id);
276
+ if (best === undefined)
277
+ continue;
278
+ merged.push({ best, session: sess });
279
+ }
280
+ merged.sort((a, b) => {
281
+ if (a.best.score !== b.best.score)
282
+ return a.best.score - b.best.score;
283
+ // Tie-break by last_active_at DESC (newer first).
284
+ return a.session.last_active_at > b.session.last_active_at
285
+ ? -1
286
+ : a.session.last_active_at < b.session.last_active_at
287
+ ? 1
288
+ : 0;
289
+ });
290
+ const paged = merged.slice(offset, offset + limit);
291
+ const results = [];
292
+ for (const r of paged) {
293
+ const score = r.best.score;
294
+ // FTS5 BM25 is negative-weighted by default in SQLite (lower = better,
295
+ // negative numbers are common). Use abs for normalization so the
296
+ // 1/(1+|s|) formula keeps the (0, 1] mapping regardless of sign.
297
+ const relevance = 1 / (1 + Math.abs(score));
298
+ const rawSnip = r.best.snip;
299
+ const matchContext = opts.stripHighlights
300
+ ? rawSnip.replace(/<<|>>/g, "")
301
+ : rawSnip;
302
+ results.push({
303
+ id: r.session.session_id,
304
+ gateway: r.session.gateway,
305
+ status: r.session.status,
306
+ relevance,
307
+ matchContext,
308
+ turns: Number(r.session.turn_count ?? 0),
309
+ lastActiveAt: r.session.last_active_at,
310
+ });
311
+ }
312
+ return { query: trimmed, results, total };
313
+ }
314
+ function rebuild(ocas) {
315
+ // Step 1: Read sumeru_session_turns BEFORE any deletes to build
316
+ // turnHash → sessionId lookup (this table survives the rebuild).
317
+ const turnAssocRows = selectAllTurns.all();
318
+ const turnHashToSessionId = new Map();
319
+ for (const row of turnAssocRows) {
320
+ turnHashToSessionId.set(row.turn_hash, row.session_id);
321
+ }
322
+ // Step 2: DELETE from FTS index tables (sumeru_session_turns is NOT touched).
323
+ db.exec("BEGIN");
324
+ try {
325
+ db.exec("DELETE FROM sumeru_turn_index");
326
+ db.exec("DELETE FROM sumeru_session_index");
327
+ db.exec("COMMIT");
328
+ }
329
+ catch (err) {
330
+ try {
331
+ db.exec("ROLLBACK");
332
+ }
333
+ catch {
334
+ // best-effort
335
+ }
336
+ throw err;
337
+ }
338
+ // Step 3: Enumerate all session-meta nodes from ocas store.
339
+ const sessionMetaEntries = ocas.store.cas.listByType(ocas.sessionMetaSchemaHash);
340
+ // Step 4-6: Index each session-meta and build sessionId → gateway map.
341
+ const sessionIdToGateway = new Map();
342
+ for (const entry of sessionMetaEntries) {
343
+ const node = ocas.store.cas.get(entry.hash);
344
+ if (node === null)
345
+ continue;
346
+ const payload = node.payload;
347
+ indexSessionMeta({
348
+ sessionId: payload.id,
349
+ gateway: payload.gateway,
350
+ adapter: payload.adapter,
351
+ createdAt: payload.createdAt,
352
+ metaHash: entry.hash,
353
+ });
354
+ sessionIdToGateway.set(payload.id, payload.gateway);
355
+ }
356
+ // Step 7-8: Enumerate all turn nodes and index them.
357
+ const turnEntries = ocas.store.cas.listByType(ocas.turnSchemaHash);
358
+ for (const entry of turnEntries) {
359
+ const node = ocas.store.cas.get(entry.hash);
360
+ if (node === null)
361
+ continue;
362
+ const payload = node.payload;
363
+ // Look up sessionId from the turnHash → sessionId map.
364
+ const sessionId = turnHashToSessionId.get(entry.hash);
365
+ if (sessionId === undefined) {
366
+ console.warn(`[sumeru] rebuild: skipping orphaned turn ${entry.hash}`);
367
+ continue;
368
+ }
369
+ // Look up gateway from the sessionId → gateway map.
370
+ const gateway = sessionIdToGateway.get(sessionId);
371
+ if (gateway === undefined) {
372
+ console.warn(`[sumeru] rebuild: skipping orphaned turn ${entry.hash}`);
373
+ continue;
374
+ }
375
+ indexTurn({
376
+ turnHash: entry.hash,
377
+ sessionId,
378
+ gateway,
379
+ turnIndex: payload.index,
380
+ role: payload.role,
381
+ content: payload.content,
382
+ createdAt: payload.timestamp,
383
+ });
384
+ }
385
+ // Step 9: Corrective UPDATE to fix turn_count and last_active_at.
386
+ db.exec(`
387
+ UPDATE sumeru_session_index
388
+ SET turn_count = (
389
+ SELECT COUNT(*) FROM sumeru_turn_index
390
+ WHERE sumeru_turn_index.session_id = sumeru_session_index.session_id
391
+ ),
392
+ last_active_at = COALESCE(
393
+ (SELECT MAX(created_at) FROM sumeru_turn_index
394
+ WHERE sumeru_turn_index.session_id = sumeru_session_index.session_id),
395
+ sumeru_session_index.created_at
396
+ )
397
+ `);
398
+ }
399
+ function turnCount() {
400
+ const row = countTurns.get();
401
+ return Number(row?.c ?? 0);
402
+ }
403
+ function close() {
404
+ db.close();
405
+ }
406
+ return {
407
+ indexSessionMeta,
408
+ indexTurn,
409
+ markSessionClosed,
410
+ appendSessionTurn,
411
+ listSessionTurns,
412
+ loadSessionTurnsBulk,
413
+ loadSessionRows,
414
+ search,
415
+ rebuild,
416
+ turnCount,
417
+ close,
418
+ };
419
+ }
420
+ /**
421
+ * Walk every `@sumeru/session-meta` and `@sumeru/turn` node in the ocas store
422
+ * and rebuild the FTS5 index from scratch. The internal `rebuild` closure
423
+ * handles full enumeration via `listByType` and uses `sumeru_session_turns`
424
+ * for turn→session association.
425
+ *
426
+ * Callers no longer need to supply `roots` — the store is the source of truth.
427
+ */
428
+ export function rebuildSearchIndex(index, ocas) {
429
+ index.rebuild(ocas);
430
+ }
431
+ /** Wrap a query in `"..."` and double internal `"` to force FTS5 phrase mode. */
432
+ export function quoteFtsPhrase(raw) {
433
+ return `"${raw.replace(/"/g, '""')}"`;
434
+ }
435
+ /**
436
+ * Public wrapper that exposes the same `searchSessions` signature the spec
437
+ * documents. Equivalent to calling `index.search(opts)` with `stripHighlights`
438
+ * defaulted to `false`.
439
+ */
440
+ export function searchSessions(index, opts) {
441
+ return index.search({
442
+ query: opts.query,
443
+ gateway: opts.gateway,
444
+ limit: opts.limit,
445
+ offset: opts.offset,
446
+ stripHighlights: opts.stripHighlights ?? false,
447
+ });
448
+ }
449
+ function clamp(n, min, max) {
450
+ if (Number.isNaN(n))
451
+ return min;
452
+ if (n < min)
453
+ return min;
454
+ if (n > max)
455
+ return max;
456
+ return n;
457
+ }
458
+ /**
459
+ * Phase 6 (Refs #399): add the nullable `meta_hash` column to a pre-existing
460
+ * `sumeru_session_index` that predates it. SQLite has no
461
+ * `ADD COLUMN IF NOT EXISTS`, so we probe `PRAGMA table_info` first and only
462
+ * `ALTER` when the column is absent. Runs inside the open transaction; a no-op
463
+ * when the column already exists (fresh DBs already define it in the DDL).
464
+ */
465
+ function migrateMetaHashColumn(db) {
466
+ const cols = db
467
+ .prepare("PRAGMA table_info(sumeru_session_index)")
468
+ .all();
469
+ const hasMetaHash = cols.some((c) => c.name === "meta_hash");
470
+ if (!hasMetaHash) {
471
+ db.exec("ALTER TABLE sumeru_session_index ADD COLUMN meta_hash TEXT");
472
+ }
473
+ }
474
+ /**
475
+ * Normalize a persisted status column into a `SessionStatus` for rehydration.
476
+ * `closed` is preserved; everything else (idle, active, or any unexpected
477
+ * value) folds to `idle` — a process restart can never leave a send in flight,
478
+ * so `active` is a transient in-memory state that must not be restored.
479
+ */
480
+ function normalizeStatus(raw) {
481
+ return raw === "closed" ? "closed" : "idle";
482
+ }
483
+ /** Open a SQLite handle, retrying on SQLITE_BUSY up to 3× with 50 ms backoff. */
484
+ function openWithRetry(dbPath) {
485
+ let lastErr = null;
486
+ for (let attempt = 0; attempt < 3; attempt += 1) {
487
+ try {
488
+ const db = new DatabaseSync(dbPath);
489
+ db.exec("PRAGMA journal_mode = WAL");
490
+ db.exec("PRAGMA foreign_keys = ON");
491
+ return db;
492
+ }
493
+ catch (err) {
494
+ lastErr = err;
495
+ const msg = err instanceof Error ? err.message : String(err);
496
+ if (!/busy/i.test(msg) && !/locked/i.test(msg))
497
+ throw err;
498
+ // 50 ms busy-wait
499
+ const start = Date.now();
500
+ while (Date.now() - start < 50) {
501
+ /* spin */
502
+ }
503
+ }
504
+ }
505
+ const cause = lastErr instanceof Error ? lastErr.message : String(lastErr);
506
+ throw new Error(`failed to create FTS5 index: ${cause}`);
507
+ }
508
+ //# sourceMappingURL=sqlite-index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sqlite-index.js","sourceRoot":"","sources":["../../src/search/sqlite-index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Phase 5 — search index types.
3
+ *
4
+ * These types are shared by the index module and the HTTP endpoints. The
5
+ * `SearchIndex` is a closure carrying the SQLite handle; consumers obtain
6
+ * one from `openSumeruOcas` via the new `searchIndex` slice on the result
7
+ * (mirrored on `OcasConfig`).
8
+ */
9
+ import type { Hash, Store } from "@ocas/core";
10
+ import type { SessionStatus } from "../types.js";
11
+ /**
12
+ * Opaque handle to the FTS5 search index. Carries the SQLite database
13
+ * handle plus the prepared statements; created by `createSearchIndex`.
14
+ */
15
+ export type SearchIndex = {
16
+ /**
17
+ * Insert a row into `sumeru_session_index`. Idempotent
18
+ * (`ON CONFLICT DO NOTHING`).
19
+ */
20
+ indexSessionMeta: (meta: SessionMetaInput) => void;
21
+ /**
22
+ * Insert a row into `sumeru_turn_index` and bump the matching session
23
+ * row's `turn_count` and `last_active_at`. Idempotent on the turn hash.
24
+ */
25
+ indexTurn: (input: IndexTurnInput) => void;
26
+ /** Update `sumeru_session_index.status` for a session. Best-effort. */
27
+ markSessionClosed: (sessionId: string) => void;
28
+ /**
29
+ * Phase 6 (Refs #399): persist one durable row in `sumeru_session_turns`
30
+ * recording `(session_id, turn_index, turn_hash)`. Idempotent on
31
+ * `(session_id, turn_index)` (`ON CONFLICT DO NOTHING`). This is the
32
+ * canonical turn-list pointer the session store rehydrates from on boot;
33
+ * it is NOT touched by `rebuild`.
34
+ */
35
+ appendSessionTurn: (sessionId: string, turnIndex: number, turnHash: Hash) => void;
36
+ /**
37
+ * Phase 6 (Refs #399): read one session's ordered turn hashes from
38
+ * `sumeru_session_turns`, ordered by `turn_index ASC`. Returns `[]` for an
39
+ * unknown session.
40
+ */
41
+ listSessionTurns: (sessionId: string) => Hash[];
42
+ /**
43
+ * Phase 6 (Refs #399): bulk-load every session's ordered turn hashes in a
44
+ * single query, grouped by `session_id` and ordered by `turn_index ASC`.
45
+ * Used at store construction so rehydration is O(1) queries.
46
+ */
47
+ loadSessionTurnsBulk: () => Map<string, Hash[]>;
48
+ /**
49
+ * Phase 6 (Refs #399): read every persisted session row from
50
+ * `sumeru_session_index` (ordered by `created_at ASC`) so the store can
51
+ * rebuild its in-memory maps on boot.
52
+ */
53
+ loadSessionRows: () => PersistedSessionRow[];
54
+ /** Run a search and return aggregated session-granularity hits. */
55
+ search: (opts: SearchOptions) => SearchResult;
56
+ /** Walk the ocas store and rebuild every index row from scratch. */
57
+ rebuild: (ocas: SearchRebuildOcas) => void;
58
+ /** Number of rows currently in `sumeru_turn_index`. */
59
+ turnCount: () => number;
60
+ /** Close the underlying SQLite handle (test-only — never used by server). */
61
+ close: () => void;
62
+ };
63
+ /**
64
+ * Payload accepted by `indexSessionMeta`. Mirrors the public
65
+ * `@sumeru/session-meta` schema with `status` defaulted to `idle`.
66
+ *
67
+ * Phase 6 (Refs #399): carries `metaHash` so the persisted
68
+ * `sumeru_session_index` row records the immutable `@sumeru/session-meta`
69
+ * node hash. On boot the store re-reads `config` from that node. `null` is
70
+ * accepted for callers/tests that have no meta node (the column is nullable).
71
+ */
72
+ export type SessionMetaInput = {
73
+ sessionId: string;
74
+ gateway: string;
75
+ adapter: string;
76
+ createdAt: string;
77
+ metaHash: Hash | null;
78
+ };
79
+ /**
80
+ * One row read back from `sumeru_session_index` at boot (Refs #399). Used by
81
+ * the session store to rebuild its in-memory maps. `metaHash` is `null` for
82
+ * rows that predate the `meta_hash` column.
83
+ */
84
+ export type PersistedSessionRow = {
85
+ sessionId: string;
86
+ gateway: string;
87
+ adapter: string;
88
+ status: SessionStatus;
89
+ createdAt: string;
90
+ lastActiveAt: string;
91
+ turnCount: number;
92
+ metaHash: Hash | null;
93
+ };
94
+ /** Payload accepted by `indexTurn`. */
95
+ export type IndexTurnInput = {
96
+ turnHash: Hash;
97
+ sessionId: string;
98
+ gateway: string;
99
+ turnIndex: number;
100
+ role: "user" | "assistant" | "system";
101
+ content: string;
102
+ createdAt: string;
103
+ };
104
+ /** Options accepted by `searchSessions` (and `search`). */
105
+ export type SearchOptions = {
106
+ /** Raw user query — `searchSessions` trims and quotes it before binding. */
107
+ query: string;
108
+ /** Filter by gateway. `null` means cross-gateway. */
109
+ gateway: string | null;
110
+ /** 1 ≤ limit ≤ 100. */
111
+ limit: number;
112
+ /** ≥ 0. */
113
+ offset: number;
114
+ /** When true, FTS5 highlight markers (`<<` / `>>`) are stripped. */
115
+ stripHighlights: boolean;
116
+ };
117
+ /** Single hit in `SearchResult.results`. */
118
+ export type SearchHit = {
119
+ id: string;
120
+ gateway: string;
121
+ status: SessionStatus;
122
+ relevance: number;
123
+ matchContext: string;
124
+ turns: number;
125
+ lastActiveAt: string;
126
+ };
127
+ /** Internal-only result of `searchSessions`. */
128
+ export type SearchResult = {
129
+ query: string;
130
+ results: SearchHit[];
131
+ /** Total distinct sessions matching `query` (before limit/offset). */
132
+ total: number;
133
+ };
134
+ /**
135
+ * Subset of the ocas store exposed to `rebuild`. The rebuild walker needs
136
+ * only `cas` reads and the two schema hashes.
137
+ */
138
+ export type SearchRebuildOcas = {
139
+ store: Store;
140
+ turnSchemaHash: Hash;
141
+ sessionMetaSchemaHash: Hash;
142
+ };
143
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/search/types.ts"],"names":[],"mappings":"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"}