stratagate-dsh 0.2.72 → 0.2.73

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/plugin.js CHANGED
@@ -1905,6 +1905,29 @@ CREATE TABLE IF NOT EXISTS external_memory_import_jobs (
1905
1905
  PRIMARY KEY (namespace, id),
1906
1906
  FOREIGN KEY (namespace) REFERENCES memory_spaces(namespace) ON DELETE CASCADE
1907
1907
  ) STRICT;
1908
+
1909
+ CREATE TABLE IF NOT EXISTS raw_message_fts_meta (
1910
+ namespace TEXT NOT NULL,
1911
+ message_id TEXT NOT NULL,
1912
+ tokens TEXT NOT NULL,
1913
+ fts_rowid INTEGER NOT NULL,
1914
+ PRIMARY KEY (namespace, message_id),
1915
+ FOREIGN KEY (namespace, message_id) REFERENCES messages(namespace, id) ON DELETE CASCADE
1916
+ ) STRICT;
1917
+
1918
+ CREATE TABLE IF NOT EXISTS raw_message_fts_state (
1919
+ namespace TEXT PRIMARY KEY,
1920
+ backfill_complete INTEGER NOT NULL DEFAULT 0,
1921
+ FOREIGN KEY (namespace) REFERENCES memory_spaces(namespace) ON DELETE CASCADE
1922
+ ) STRICT;
1923
+ `;
1924
+ var RAW_MESSAGE_FTS_SCHEMA = `
1925
+ CREATE VIRTUAL TABLE IF NOT EXISTS raw_message_fts USING fts5(
1926
+ namespace UNINDEXED,
1927
+ message_id UNINDEXED,
1928
+ tokens,
1929
+ tokenize = 'unicode61'
1930
+ );
1908
1931
  `;
1909
1932
  var THREAD_INDEXES = `
1910
1933
  CREATE INDEX IF NOT EXISTS messages_thread_idx ON messages(namespace, thread_id, position);
@@ -1922,8 +1945,15 @@ function nonEmptyNamespace(namespace) {
1922
1945
  if (!normalized) throw new TypeError("Storage namespace must not be empty");
1923
1946
  return normalized;
1924
1947
  }
1948
+ function encodeRawSearchToken(token) {
1949
+ return `t${Buffer.from(token, "utf8").toString("hex")}`;
1950
+ }
1951
+ function encodedRawSearchTokens(content) {
1952
+ return searchTokens(content).map(encodeRawSearchToken).join(" ");
1953
+ }
1925
1954
  var SqliteStorage = class {
1926
1955
  database;
1956
+ rawSearchFtsAvailable = false;
1927
1957
  closed = false;
1928
1958
  constructor(options) {
1929
1959
  if (!options.filename.trim()) throw new TypeError("SQLite filename must not be empty");
@@ -2213,21 +2243,46 @@ var SqliteStorage = class {
2213
2243
  };
2214
2244
  return { snapshot: cloneSnapshot(normalizeSnapshot(snapshot)), revision: space.revision };
2215
2245
  }
2216
- async save(namespace, snapshot, expectedRevision) {
2246
+ async save(namespace, snapshot, expectedRevision, rawMessageIndexDelta) {
2217
2247
  this.assertOpen();
2218
2248
  assertValidSnapshot(snapshot);
2219
2249
  if (!Number.isSafeInteger(expectedRevision) || expectedRevision < 0) {
2220
2250
  throw new TypeError("expectedRevision must be a non-negative integer");
2221
2251
  }
2222
2252
  const key = nonEmptyNamespace(namespace);
2223
- return this.immediateTransaction(() => this.persistSnapshot(key, snapshot, expectedRevision));
2253
+ return this.immediateTransaction(() => this.persistSnapshot(key, snapshot, expectedRevision, rawMessageIndexDelta));
2224
2254
  }
2225
2255
  async close() {
2226
2256
  if (this.closed) return;
2227
2257
  this.database.close();
2228
2258
  this.closed = true;
2229
2259
  }
2230
- persistSnapshot(namespace, snapshot, expectedRevision) {
2260
+ searchRawMessageIds(namespace, tokens, limit, threadId, includeUnthreaded = false) {
2261
+ this.assertOpen();
2262
+ if (!this.rawSearchFtsAvailable || tokens.length === 0) return null;
2263
+ const key = nonEmptyNamespace(namespace);
2264
+ const boundedLimit = Math.max(1, Math.min(5e3, Math.floor(limit)));
2265
+ const match = tokens.map(encodeRawSearchToken).filter(Boolean).map((token) => `"${token}"`).join(" OR ");
2266
+ if (!match) return [];
2267
+ try {
2268
+ const rows = this.database.prepare(`
2269
+ SELECT f.message_id
2270
+ FROM raw_message_fts AS f
2271
+ INNER JOIN messages AS m
2272
+ ON m.namespace = f.namespace AND m.id = f.message_id
2273
+ WHERE raw_message_fts MATCH ?
2274
+ AND f.namespace = ?
2275
+ ${threadId === void 0 ? "" : includeUnthreaded ? "AND (m.thread_id = ? OR m.thread_id IS NULL)" : "AND m.thread_id = ?"}
2276
+ ORDER BY rank
2277
+ LIMIT ?
2278
+ `).all(...threadId === void 0 ? [match, key, boundedLimit] : [match, key, threadId, boundedLimit]);
2279
+ return rows.map(({ message_id }) => message_id);
2280
+ } catch {
2281
+ this.rawSearchFtsAvailable = false;
2282
+ return null;
2283
+ }
2284
+ }
2285
+ persistSnapshot(namespace, snapshot, expectedRevision, rawMessageIndexDelta) {
2231
2286
  const current = this.database.prepare("SELECT revision FROM memory_spaces WHERE namespace = ?").get(namespace);
2232
2287
  const actualRevision = current?.revision ?? null;
2233
2288
  if ((actualRevision ?? 0) !== expectedRevision || actualRevision === null && expectedRevision !== 0) {
@@ -2346,6 +2401,14 @@ var SqliteStorage = class {
2346
2401
  };
2347
2402
  insertMessages(snapshot.openTail, null);
2348
2403
  for (const block of snapshot.blocks) insertMessages(block.l5Raw, block.id);
2404
+ this.syncRawSearchIndex(namespace, rawMessageIndexDelta ?? {
2405
+ upsert: snapshot.blocks.flatMap(({ l5Raw }) => l5Raw),
2406
+ deleteIds: []
2407
+ });
2408
+ if (rawMessageIndexDelta) {
2409
+ const deleteMessage = this.database.prepare("DELETE FROM messages WHERE namespace = ? AND id = ?");
2410
+ for (const messageId of rawMessageIndexDelta.deleteIds) deleteMessage.run(namespace, messageId);
2411
+ }
2349
2412
  const insertEvent = this.database.prepare(`
2350
2413
  INSERT INTO events (
2351
2414
  namespace, id, position, title, summary, narrative, tags_json, quotes_json, source_block_id,
@@ -2620,6 +2683,7 @@ var SqliteStorage = class {
2620
2683
  this.immediateTransaction(() => {
2621
2684
  this.database.exec(SCHEMA);
2622
2685
  this.database.exec(THREAD_INDEXES);
2686
+ this.enableRawSearchFts();
2623
2687
  this.database.exec(`PRAGMA user_version = ${STRATAGATE_STORAGE_SCHEMA_VERSION}`);
2624
2688
  });
2625
2689
  } else if (version >= 1 && version < STRATAGATE_STORAGE_SCHEMA_VERSION) {
@@ -2687,15 +2751,95 @@ var SqliteStorage = class {
2687
2751
  this.database.exec("ALTER TABLE messages ADD COLUMN thread_id TEXT");
2688
2752
  }
2689
2753
  this.database.exec(THREAD_INDEXES);
2754
+ this.enableRawSearchFts();
2690
2755
  this.database.prepare("UPDATE memory_spaces SET schema_version = ? WHERE schema_version < ?").run(STRATAGATE_STORAGE_SCHEMA_VERSION, STRATAGATE_STORAGE_SCHEMA_VERSION);
2691
2756
  this.database.exec(`PRAGMA user_version = ${STRATAGATE_STORAGE_SCHEMA_VERSION}`);
2692
2757
  });
2693
2758
  } else if (version === STRATAGATE_STORAGE_SCHEMA_VERSION) {
2694
2759
  this.database.exec(SCHEMA);
2695
2760
  this.database.exec(THREAD_INDEXES);
2761
+ this.enableRawSearchFts();
2696
2762
  }
2697
2763
  this.assertSchemaVersion();
2698
2764
  }
2765
+ enableRawSearchFts() {
2766
+ try {
2767
+ this.database.exec(RAW_MESSAGE_FTS_SCHEMA);
2768
+ this.rawSearchFtsAvailable = true;
2769
+ const namespaces = this.database.prepare("SELECT namespace FROM memory_spaces").all();
2770
+ for (const { namespace } of namespaces) {
2771
+ const state = this.database.prepare(
2772
+ "SELECT backfill_complete FROM raw_message_fts_state WHERE namespace = ?"
2773
+ ).get(namespace);
2774
+ const messageCount = this.database.prepare(
2775
+ "SELECT COUNT(*) AS count FROM messages WHERE namespace = ? AND block_id IS NOT NULL"
2776
+ ).get(namespace);
2777
+ const indexedCount = this.database.prepare(
2778
+ "SELECT COUNT(*) AS count FROM raw_message_fts_meta WHERE namespace = ?"
2779
+ ).get(namespace);
2780
+ if (state?.backfill_complete === 1 && messageCount.count === indexedCount.count) continue;
2781
+ const messages = state?.backfill_complete === 1 ? this.database.prepare(`
2782
+ SELECT m.id, m.content
2783
+ FROM messages AS m
2784
+ LEFT JOIN raw_message_fts_meta AS i
2785
+ ON i.namespace = m.namespace AND i.message_id = m.id
2786
+ WHERE m.namespace = ? AND m.block_id IS NOT NULL AND i.message_id IS NULL
2787
+ `).all(namespace) : this.database.prepare(
2788
+ "SELECT id, content FROM messages WHERE namespace = ? AND block_id IS NOT NULL"
2789
+ ).all(namespace);
2790
+ this.syncRawSearchIndex(namespace, { upsert: messages, deleteIds: [] });
2791
+ this.database.prepare(`
2792
+ INSERT INTO raw_message_fts_state (namespace, backfill_complete)
2793
+ VALUES (?, 1)
2794
+ ON CONFLICT (namespace) DO UPDATE SET backfill_complete = 1
2795
+ `).run(namespace);
2796
+ }
2797
+ } catch {
2798
+ this.rawSearchFtsAvailable = false;
2799
+ }
2800
+ }
2801
+ syncRawSearchIndex(namespace, delta) {
2802
+ if (!this.rawSearchFtsAvailable) return;
2803
+ const findMeta = this.database.prepare(`
2804
+ SELECT message_id, tokens, fts_rowid
2805
+ FROM raw_message_fts_meta WHERE namespace = ? AND message_id = ?
2806
+ `);
2807
+ const deleteFts = this.database.prepare("DELETE FROM raw_message_fts WHERE rowid = ?");
2808
+ const deleteMeta = this.database.prepare("DELETE FROM raw_message_fts_meta WHERE namespace = ? AND message_id = ?");
2809
+ for (const messageId of delta.deleteIds) {
2810
+ const row = findMeta.get(namespace, messageId);
2811
+ if (!row) continue;
2812
+ deleteFts.run(row.fts_rowid);
2813
+ deleteMeta.run(namespace, messageId);
2814
+ }
2815
+ const insertFts = this.database.prepare(
2816
+ "INSERT INTO raw_message_fts (namespace, message_id, tokens) VALUES (?, ?, ?)"
2817
+ );
2818
+ const insertMeta = this.database.prepare(`
2819
+ INSERT INTO raw_message_fts_meta (namespace, message_id, tokens, fts_rowid)
2820
+ VALUES (?, ?, ?, ?)
2821
+ ON CONFLICT (namespace, message_id) DO UPDATE SET tokens = excluded.tokens, fts_rowid = excluded.fts_rowid
2822
+ `);
2823
+ for (const message of delta.upsert) {
2824
+ const messageId = message.id;
2825
+ const tokens = encodedRawSearchTokens(message.content);
2826
+ const previous = findMeta.get(namespace, messageId);
2827
+ if (previous?.tokens === tokens) continue;
2828
+ if (previous) deleteFts.run(previous.fts_rowid);
2829
+ if (!tokens) {
2830
+ if (previous) deleteMeta.run(namespace, messageId);
2831
+ continue;
2832
+ }
2833
+ insertFts.run(namespace, messageId, tokens);
2834
+ const row = this.database.prepare("SELECT last_insert_rowid() AS rowid").get();
2835
+ insertMeta.run(namespace, messageId, tokens, row.rowid);
2836
+ }
2837
+ this.database.prepare(`
2838
+ INSERT INTO raw_message_fts_state (namespace, backfill_complete)
2839
+ VALUES (?, 1)
2840
+ ON CONFLICT (namespace) DO UPDATE SET backfill_complete = 1
2841
+ `).run(namespace);
2842
+ }
2699
2843
  assertSchemaVersion() {
2700
2844
  const version = this.userVersion();
2701
2845
  if (version !== STRATAGATE_STORAGE_SCHEMA_VERSION) {
@@ -2799,6 +2943,9 @@ var StrataGate = class _StrataGate {
2799
2943
  successfulModelResponses = [];
2800
2944
  ingestionReceipts = /* @__PURE__ */ new Map();
2801
2945
  externalMemoryImportJobs = /* @__PURE__ */ new Map();
2946
+ rawMessageLookup = /* @__PURE__ */ new Map();
2947
+ pendingRawUpserts = /* @__PURE__ */ new Map();
2948
+ pendingRawDeletes = /* @__PURE__ */ new Set();
2802
2949
  currentTurn = 0;
2803
2950
  storage;
2804
2951
  namespace;
@@ -3643,6 +3790,7 @@ var StrataGate = class _StrataGate {
3643
3790
  if (receipt.eventIds.length === 0 && receipt.elementIds.length === 0) this.usageReceipts.delete(receiptId);
3644
3791
  }
3645
3792
  this.blocks.splice(sourceIndex, 1);
3793
+ this.markRawMessagesDeleted(sourceMessageIds);
3646
3794
  return {
3647
3795
  sourceBlockId: id,
3648
3796
  removedEventIds: [...importedEventIds],
@@ -3894,24 +4042,40 @@ var StrataGate = class _StrataGate {
3894
4042
  if (!element) throw new Error(`Unknown element: ${id}`);
3895
4043
  return elementViewAt(element, at);
3896
4044
  }
3897
- searchRawMemory(query, limit = 6) {
4045
+ searchRawMemory(query, limit = 6, options = {}) {
3898
4046
  const tokens = searchTokens(query);
3899
4047
  if (tokens.length === 0) return [];
3900
- const hits = [];
3901
- for (const block of this.blocks) {
3902
- for (const [index, message] of block.l5Raw.entries()) {
3903
- const messageTokens = new Set(searchTokens(message.content));
3904
- if (!tokens.some((token) => messageTokens.has(token))) continue;
3905
- hits.push({
3906
- blockId: block.id,
3907
- turnRange: [block.startTurn, block.endTurn],
3908
- message,
3909
- nearby: block.l5Raw.slice(Math.max(0, index - 1), index + 2)
3910
- });
3911
- if (hits.length >= limit) return hits;
3912
- }
4048
+ const boundedLimit = Math.max(1, Math.floor(limit));
4049
+ let candidateIds;
4050
+ try {
4051
+ candidateIds = this.storage?.searchRawMessageIds?.(
4052
+ this.namespace ?? "",
4053
+ tokens,
4054
+ Math.min(5e3, Math.max(100, boundedLimit * 20)),
4055
+ options.threadId,
4056
+ options.includeUnthreaded
4057
+ );
4058
+ } catch {
4059
+ candidateIds = null;
3913
4060
  }
3914
- return hits;
4061
+ const candidates = candidateIds === null || candidateIds === void 0 ? this.blocks.flatMap((block) => block.l5Raw.map((message, index) => ({
4062
+ id: message.id,
4063
+ block,
4064
+ index,
4065
+ message
4066
+ }))).filter(({ message }) => options.threadId === void 0 || message.threadId === options.threadId || options.includeUnthreaded === true && message.threadId === void 0) : [...new Set(candidateIds)].flatMap((id) => {
4067
+ const item = this.rawMessageLookup.get(id);
4068
+ if (!item) return [];
4069
+ if (options.threadId !== void 0 && item.message.threadId !== options.threadId && !(options.includeUnthreaded === true && item.message.threadId === void 0)) return [];
4070
+ return [{ id, ...item }];
4071
+ });
4072
+ const ranked = bm25Rank(candidates, query, ({ message }) => searchTokens(message.content)).sort((left, right) => right.score - left.score || right.item.message.createdAt.localeCompare(left.item.message.createdAt) || right.item.block.sequence - left.item.block.sequence || right.item.index - left.item.index || left.item.id.localeCompare(right.item.id));
4073
+ return ranked.slice(0, boundedLimit).map(({ item }) => ({
4074
+ blockId: item.block.id,
4075
+ turnRange: [item.block.startTurn, item.block.endTurn],
4076
+ message: item.message,
4077
+ nearby: item.block.l5Raw.slice(Math.max(0, item.index - 1), item.index + 2)
4078
+ }));
3915
4079
  }
3916
4080
  /**
3917
4081
  * Return decayed block views. Passing a threadId limits the result to that
@@ -4083,6 +4247,8 @@ var StrataGate = class _StrataGate {
4083
4247
  lastLiftedBy: null
4084
4248
  };
4085
4249
  this.blocks.push(block);
4250
+ this.indexRawBlock(block);
4251
+ this.markRawMessagesForUpsert(block.l5Raw);
4086
4252
  return block;
4087
4253
  }
4088
4254
  addEventInMemory(input) {
@@ -4247,6 +4413,28 @@ var StrataGate = class _StrataGate {
4247
4413
  this.elementProjectionJobs.set(job.id, job);
4248
4414
  return job;
4249
4415
  }
4416
+ indexRawBlock(block) {
4417
+ for (const [index, message] of block.l5Raw.entries()) {
4418
+ this.rawMessageLookup.set(message.id, { block, index, message });
4419
+ }
4420
+ }
4421
+ markRawMessagesForUpsert(messages) {
4422
+ for (const message of messages) {
4423
+ this.pendingRawDeletes.delete(message.id);
4424
+ this.pendingRawUpserts.set(message.id, structuredClone(message));
4425
+ }
4426
+ }
4427
+ markRawMessagesDeleted(ids) {
4428
+ for (const id of ids) {
4429
+ this.pendingRawUpserts.delete(id);
4430
+ this.pendingRawDeletes.add(id);
4431
+ this.rawMessageLookup.delete(id);
4432
+ }
4433
+ }
4434
+ rebuildRawMessageLookup() {
4435
+ this.rawMessageLookup.clear();
4436
+ for (const block of this.blocks) this.indexRawBlock(block);
4437
+ }
4250
4438
  threadOpenTail(threadId) {
4251
4439
  return this.openTail.filter((message) => message.threadId === threadId);
4252
4440
  }
@@ -4325,6 +4513,8 @@ var StrataGate = class _StrataGate {
4325
4513
  const remaining = this.openTail.filter((message) => !sealedIds.has(message.id));
4326
4514
  this.openTail.splice(0, this.openTail.length, ...remaining);
4327
4515
  this.blocks.push(block);
4516
+ this.indexRawBlock(block);
4517
+ this.markRawMessagesForUpsert(block.l5Raw);
4328
4518
  const updatedAt = toUtc8Iso(this.now());
4329
4519
  this.summaryJobs.set(block.id, {
4330
4520
  blockId: block.id,
@@ -4558,6 +4748,8 @@ var StrataGate = class _StrataGate {
4558
4748
  }
4559
4749
  const before = this.exportSnapshot();
4560
4750
  const beforeRevision = this.revision;
4751
+ const beforeRawUpserts = new Map(this.pendingRawUpserts);
4752
+ const beforeRawDeletes = new Set(this.pendingRawDeletes);
4561
4753
  try {
4562
4754
  const result = await mutation();
4563
4755
  await this.persist();
@@ -4565,6 +4757,10 @@ var StrataGate = class _StrataGate {
4565
4757
  } catch (error) {
4566
4758
  this.restoreSnapshot(before);
4567
4759
  this.revision = beforeRevision;
4760
+ this.pendingRawUpserts.clear();
4761
+ for (const [id, message] of beforeRawUpserts) this.pendingRawUpserts.set(id, message);
4762
+ this.pendingRawDeletes.clear();
4763
+ for (const id of beforeRawDeletes) this.pendingRawDeletes.add(id);
4568
4764
  throw error;
4569
4765
  } finally {
4570
4766
  release();
@@ -4572,7 +4768,13 @@ var StrataGate = class _StrataGate {
4572
4768
  }
4573
4769
  async persist() {
4574
4770
  if (!this.storage || !this.namespace) return;
4575
- this.revision = await this.storage.save(this.namespace, this.exportSnapshot(), this.revision);
4771
+ const delta = {
4772
+ upsert: [...this.pendingRawUpserts.values()],
4773
+ deleteIds: [...this.pendingRawDeletes]
4774
+ };
4775
+ this.revision = await this.storage.save(this.namespace, this.exportSnapshot(), this.revision, delta);
4776
+ this.pendingRawUpserts.clear();
4777
+ this.pendingRawDeletes.clear();
4576
4778
  }
4577
4779
  restoreSnapshot(snapshot) {
4578
4780
  const normalized = normalizeSnapshot(snapshot);
@@ -4584,6 +4786,9 @@ var StrataGate = class _StrataGate {
4584
4786
  this.currentTurn = copy.currentTurn;
4585
4787
  this.openTail.splice(0, this.openTail.length, ...copy.openTail);
4586
4788
  this.blocks.splice(0, this.blocks.length, ...copy.blocks);
4789
+ this.rebuildRawMessageLookup();
4790
+ this.pendingRawUpserts.clear();
4791
+ this.pendingRawDeletes.clear();
4587
4792
  this.summaryJobs.clear();
4588
4793
  for (const job of copy.summaryJobs) this.summaryJobs.set(job.blockId, job);
4589
4794
  this.events.splice(0, this.events.length, ...copy.events);
@@ -6118,7 +6323,7 @@ var StrataGateRuntime = class {
6118
6323
  await this.flush();
6119
6324
  const memory = await this.space(session);
6120
6325
  const threadId = String(session.id);
6121
- const results = memory.searchRawMemory(query, scope === "namespace" ? limit : Number.MAX_SAFE_INTEGER).filter((result) => scope === "namespace" || result.message.threadId === threadId || result.message.threadId === void 0).slice(0, limit);
6326
+ const results = memory.searchRawMemory(query, limit, scope === "namespace" ? {} : { threadId, includeUnthreaded: true }).slice(0, limit);
6122
6327
  const blockTitles = new Map(memory.listBlocks().map((block) => [block.id, blockCitationTitle(block)]));
6123
6328
  return this.batch(session, results.map((result, index) => ({
6124
6329
  ref: `raw:${result.blockId}:${result.message.id}:${index}`,