stratagate-dsh 0.2.72 → 0.2.74

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}`,
@@ -7940,8 +8145,31 @@ function currentPluginVersion() {
7940
8145
  }
7941
8146
  var STRATAGATE_DSH_VERSION = currentPluginVersion();
7942
8147
  function graphProjectionIsProcessing(job) {
7943
- if (job.status === "running") return true;
7944
- return job.attempts < DERIVATION_MAX_ATTEMPTS && (job.status === "pending" || job.status === "failed" && job.nextRetryAt !== null);
8148
+ return derivationJobState(job, false) === "processing" || derivationJobState(job, false) === "retryable";
8149
+ }
8150
+ function derivationJobState(job, completed) {
8151
+ if (completed || job.status === "succeeded" || job.status === "skipped" || job.status === "completed") return "completed";
8152
+ if (job.status === "running") return "processing";
8153
+ if (job.status === "pending") return job.attempts < DERIVATION_MAX_ATTEMPTS ? "processing" : "blocked";
8154
+ if (job.status === "failed") {
8155
+ const retryable = job.attempts < DERIVATION_MAX_ATTEMPTS && job.nextRetryAt !== null && Number.isFinite(Date.parse(job.nextRetryAt));
8156
+ return retryable ? "retryable" : "terminal-failed";
8157
+ }
8158
+ return "blocked";
8159
+ }
8160
+ function summarizeDerivationJobs(jobs, completed) {
8161
+ const result = { processing: 0, retryable: 0, terminalFailed: 0, completed: 0, blocked: 0 };
8162
+ for (const job of jobs) {
8163
+ const state = derivationJobState(job, completed(job));
8164
+ if (state === "processing") result.processing += 1;
8165
+ else if (state === "retryable") {
8166
+ result.retryable += 1;
8167
+ result.processing += 1;
8168
+ } else if (state === "terminal-failed") result.terminalFailed += 1;
8169
+ else if (state === "completed") result.completed += 1;
8170
+ else result.blocked += 1;
8171
+ }
8172
+ return result;
7945
8173
  }
7946
8174
  function installedPackageVersion(names) {
7947
8175
  for (const name2 of names) {
@@ -8236,10 +8464,12 @@ async function overview(runtime, cachedEntries) {
8236
8464
  }] : [];
8237
8465
  });
8238
8466
  const describeBlockJob = (kind, job) => {
8467
+ const state = derivationJobState(job, kind === "block-summary" ? job.status === "succeeded" : job.status === "succeeded" || job.status === "skipped");
8239
8468
  const details = blockDetails([job.blockId]);
8240
8469
  return {
8241
8470
  id: job.blockId,
8242
8471
  kind,
8472
+ state,
8243
8473
  status: job.status,
8244
8474
  attempts: job.attempts,
8245
8475
  nextRetryAt: job.nextRetryAt,
@@ -8255,11 +8485,13 @@ async function overview(runtime, cachedEntries) {
8255
8485
  };
8256
8486
  };
8257
8487
  const describeGraphJob = (job) => {
8488
+ const state = derivationJobState(job, job.status === "completed");
8258
8489
  const blockIds = [...new Set(job.sourceEventIds.flatMap((eventId) => snapshot.events.find(({ id }) => id === eventId)?.sourceBlockId ?? []))];
8259
8490
  const details = blockDetails(blockIds);
8260
8491
  return {
8261
8492
  id: job.id,
8262
8493
  kind: "graph-projection",
8494
+ state,
8263
8495
  status: job.status,
8264
8496
  attempts: job.attempts,
8265
8497
  nextRetryAt: job.nextRetryAt,
@@ -8273,16 +8505,19 @@ async function overview(runtime, cachedEntries) {
8273
8505
  sourceEventIds: job.sourceEventIds
8274
8506
  };
8275
8507
  };
8276
- const failedJobs = snapshot.summaryJobs.filter(({ status }) => status === "failed").length + snapshot.extractionJobs.filter(({ status }) => status === "failed").length + snapshot.graphProjectionJobs.filter(({ status }) => status === "failed").length;
8277
- const processingJobs = snapshot.summaryJobs.filter(({ status, nextRetryAt }) => status === "pending" || status === "running" || status === "failed" && nextRetryAt !== null).length + snapshot.extractionJobs.filter(({ status, nextRetryAt }) => status === "running" || status === "failed" && nextRetryAt !== null).length + snapshot.graphProjectionJobs.filter(graphProjectionIsProcessing).length;
8508
+ const summaryStatus = summarizeDerivationJobs(snapshot.summaryJobs, (job) => job.status === "succeeded");
8509
+ const extractionStatus = summarizeDerivationJobs(snapshot.extractionJobs, (job) => job.status === "succeeded" || job.status === "skipped");
8510
+ const graphStatus = summarizeDerivationJobs(snapshot.graphProjectionJobs, (job) => job.status === "completed");
8511
+ const failedJobs = summaryStatus.terminalFailed + extractionStatus.terminalFailed + graphStatus.terminalFailed;
8512
+ const processingJobs = summaryStatus.processing + extractionStatus.processing + graphStatus.processing;
8278
8513
  const failedJobDetails = [
8279
8514
  ...snapshot.summaryJobs.filter(({ status }) => status === "failed").map((job) => describeBlockJob("block-summary", job)),
8280
8515
  ...snapshot.extractionJobs.filter(({ status }) => status === "failed").map((job) => describeBlockJob("event-extraction", job)),
8281
8516
  ...snapshot.graphProjectionJobs.filter(({ status }) => status === "failed").map(describeGraphJob)
8282
8517
  ];
8283
8518
  const processingJobDetails = [
8284
- ...snapshot.summaryJobs.filter(({ status, nextRetryAt }) => status === "pending" || status === "running" || status === "failed" && nextRetryAt !== null).map((job) => describeBlockJob("block-summary", job)),
8285
- ...snapshot.extractionJobs.filter(({ status, nextRetryAt }) => status === "running" || status === "failed" && nextRetryAt !== null).map((job) => describeBlockJob("event-extraction", job)),
8519
+ ...snapshot.summaryJobs.filter((job) => derivationJobState(job, job.status === "succeeded") === "processing" || derivationJobState(job, job.status === "succeeded") === "retryable").map((job) => describeBlockJob("block-summary", job)),
8520
+ ...snapshot.extractionJobs.filter((job) => derivationJobState(job, job.status === "succeeded" || job.status === "skipped") === "processing" || derivationJobState(job, job.status === "succeeded" || job.status === "skipped") === "retryable").map((job) => describeBlockJob("event-extraction", job)),
8286
8521
  ...snapshot.graphProjectionJobs.filter(graphProjectionIsProcessing).map(describeGraphJob)
8287
8522
  ];
8288
8523
  const timestamps = [
@@ -8306,12 +8541,27 @@ async function overview(runtime, cachedEntries) {
8306
8541
  elements: snapshot.elements.length,
8307
8542
  graphNodes: snapshot.graphNodes.length,
8308
8543
  graphEdges: snapshot.graphEdges.length,
8544
+ taskStatus: {
8545
+ blockSummary: summaryStatus,
8546
+ eventExtraction: extractionStatus,
8547
+ graphProjection: graphStatus
8548
+ },
8309
8549
  graphMigration: (() => {
8310
8550
  const projected = new Set(snapshot.graphProjectionJobs.filter(({ status, projectorVersion }) => status === "completed" && projectorVersion === KNOWLEDGE_GRAPH_PROJECTOR_VERSION).flatMap(({ sourceEventIds }) => sourceEventIds)).size;
8311
- const failed = snapshot.graphProjectionJobs.filter(({ status }) => status === "failed").length;
8312
- const running = snapshot.graphProjectionJobs.filter(({ status }) => status === "running").length;
8313
8551
  const total = snapshot.events.filter(({ status }) => status !== "forgotten" && status !== "archived").length;
8314
- return { projected, total, failed, running, complete: projected >= total };
8552
+ const complete = projected >= total;
8553
+ const state = complete ? "complete" : graphStatus.processing > 0 ? "processing" : graphStatus.terminalFailed > 0 ? "failed" : "incomplete";
8554
+ return {
8555
+ projected,
8556
+ total,
8557
+ pending: snapshot.graphProjectionJobs.filter(({ status }) => status === "pending").length,
8558
+ running: snapshot.graphProjectionJobs.filter(({ status }) => status === "running").length,
8559
+ failed: graphStatus.terminalFailed,
8560
+ retryable: graphStatus.retryable,
8561
+ processing: graphStatus.processing,
8562
+ state,
8563
+ complete
8564
+ };
8315
8565
  })(),
8316
8566
  usageReceipts: snapshot.usageReceipts.length,
8317
8567
  memoryUseCount: snapshot.usageReceipts.filter((receipt) => receipt.eventIds.length > 0 || receipt.elementIds.length > 0).length,
@@ -8650,13 +8900,19 @@ async function memories(runtime, url) {
8650
8900
  clusters: clusterKnowledgeGraph(snapshot.graphNodes, snapshot.graphEdges),
8651
8901
  migration: (() => {
8652
8902
  const projected = new Set(snapshot.graphProjectionJobs.filter(({ status, projectorVersion }) => status === "completed" && projectorVersion === KNOWLEDGE_GRAPH_PROJECTOR_VERSION).flatMap(({ sourceEventIds }) => sourceEventIds)).size;
8903
+ const graphStatus = summarizeDerivationJobs(snapshot.graphProjectionJobs, (job) => job.status === "completed");
8904
+ const total = snapshot.events.filter(({ status }) => status !== "forgotten" && status !== "archived").length;
8905
+ const complete = projected >= total;
8653
8906
  return {
8654
8907
  projected,
8655
- total: snapshot.events.filter(({ status }) => status !== "forgotten" && status !== "archived").length,
8908
+ total,
8656
8909
  pending: snapshot.graphProjectionJobs.filter(({ status }) => status === "pending").length,
8657
8910
  running: snapshot.graphProjectionJobs.filter(({ status }) => status === "running").length,
8658
- failed: snapshot.graphProjectionJobs.filter(({ status }) => status === "failed").length,
8659
- complete: projected >= snapshot.events.filter(({ status }) => status !== "forgotten" && status !== "archived").length
8911
+ failed: graphStatus.terminalFailed,
8912
+ retryable: graphStatus.retryable,
8913
+ processing: graphStatus.processing,
8914
+ state: complete ? "complete" : graphStatus.processing > 0 ? "processing" : graphStatus.terminalFailed > 0 ? "failed" : "incomplete",
8915
+ complete
8660
8916
  };
8661
8917
  })()
8662
8918
  };