stratagate-dsh 0.2.36 → 0.2.38

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/index.js CHANGED
@@ -15,7 +15,8 @@ var Config = z.object({
15
15
  ingestSubagents: z.boolean().default(false),
16
16
  provider: z.string(),
17
17
  model: z.string(),
18
- maxOutputTokens: z.natural().min(256).default(1e4)
18
+ maxOutputTokens: z.natural().min(256).default(2048),
19
+ structuredTaskTimeoutMs: z.natural().min(1e3).default(45e3)
19
20
  });
20
21
  function resolveConfig(config) {
21
22
  const database = config.database?.trim() ?? "";
@@ -36,7 +37,8 @@ function resolveConfig(config) {
36
37
  blockDecayLambda: Math.max(0, config.blockDecayLambda ?? 0.3),
37
38
  ingestSubagents: config.ingestSubagents ?? false,
38
39
  ...provider && model ? { provider, model } : {},
39
- maxOutputTokens: Math.max(256, Math.floor(config.maxOutputTokens ?? 1e4))
40
+ maxOutputTokens: Math.max(256, Math.floor(config.maxOutputTokens ?? 2048)),
41
+ structuredTaskTimeoutMs: Math.max(1e3, Math.floor(config.structuredTaskTimeoutMs ?? 45e3))
40
42
  };
41
43
  }
42
44
 
@@ -45,7 +47,7 @@ import { AsyncLocalStorage } from "node:async_hooks";
45
47
  import { BlockAssembler, createUserMessage } from "@deepseek-ai/dsh-llm";
46
48
  import { parameterSchemaSpecToJsonSchema, validateArgs } from "@deepseek-ai/dsh-tools";
47
49
 
48
- // ../../src/blocks.ts
50
+ // packages/core/src/blocks.ts
49
51
  var DEFAULT_BLOCK_TURN_SIZE = 12;
50
52
  var BLOCK_MAX_LEVEL = 5;
51
53
  var BLOCK_DECAY_LAMBDA = 0.3;
@@ -252,7 +254,7 @@ function deterministicBlockLayers(messages) {
252
254
  };
253
255
  }
254
256
 
255
- // ../../src/search.ts
257
+ // packages/core/src/search.ts
256
258
  var wordSegmenter = new Intl.Segmenter(void 0, { granularity: "word" });
257
259
  function normalizeSearchText(value) {
258
260
  return value.normalize("NFKC").toLocaleLowerCase().replace(/\s+/g, " ").trim();
@@ -329,7 +331,7 @@ function rrfRank(rankings) {
329
331
  return [...fused.values()].sort((left, right) => right.score - left.score || left.bestRank - right.bestRank || left.item.id.localeCompare(right.item.id)).map(({ item, score }) => ({ item, score }));
330
332
  }
331
333
 
332
- // ../../src/graph.ts
334
+ // packages/core/src/graph.ts
333
335
  var NODE_TYPES = /* @__PURE__ */ new Set(["person", "project", "organization", "tool", "place"]);
334
336
  var STATUSES = /* @__PURE__ */ new Set(["active", "superseded", "disputed", "archived"]);
335
337
  function text(value, limit = 240) {
@@ -467,7 +469,7 @@ function applyGraphProjection(options) {
467
469
  return { nodeIds: [...touchedNodes], edgeIds: [...touchedEdges] };
468
470
  }
469
471
 
470
- // ../../src/events.ts
472
+ // packages/core/src/events.ts
471
473
  function normalizeStandardEventType(value) {
472
474
  const normalized = normalizeSearchText(value ?? "").replace(/[\s_-]+/g, "");
473
475
  const aliases = {
@@ -505,7 +507,7 @@ function normalizeStandardEventType(value) {
505
507
  return aliases[normalized] ?? "other";
506
508
  }
507
509
 
508
- // ../../src/external-memory.ts
510
+ // packages/core/src/external-memory.ts
509
511
  var EXTERNAL_MEMORY_EXPORT_SCHEMA = "stratagate.external-memory.v2";
510
512
  var EXTERNAL_MEMORY_DECIDER_PROMPT_ZH_CN = `
511
513
  \u4F60\u662F StrataGate \u7684\u5916\u90E8\u8BB0\u5FC6\u5408\u5E76\u88C1\u51B3\u5668\u3002\u8F93\u5165\u5305\u542B\u4E00\u4E2A\u5916\u90E8 candidate Event\uFF0C\u4EE5\u53CA\u672C\u5730\u68C0\u7D22\u5F97\u5230\u7684 Top-K existing Events\u3002\u53EA\u80FD\u4F9D\u636E candidate \u548C existing Events \u5224\u65AD\uFF0C\u4E0D\u5F97\u5F15\u5165\u5217\u8868\u4E4B\u5916\u7684\u672C\u5730\u4E8B\u4EF6\uFF0C\u4E5F\u4E0D\u5F97\u51ED\u5E38\u8BC6\u8865\u5168\u65F6\u95F4\u3002
@@ -1184,7 +1186,7 @@ function parseExternalMemoryExport(text3) {
1184
1186
  }
1185
1187
  var externalMemoryJsonExtractor = async ({ text: text3 }) => parseExternalMemoryExport(text3);
1186
1188
 
1187
- // ../../src/retrieval.ts
1189
+ // packages/core/src/retrieval.ts
1188
1190
  var RETRIEVAL_STRATEGIES = [
1189
1191
  "answer",
1190
1192
  "search_events",
@@ -1260,8 +1262,8 @@ function normalizeRetrievalAssessment(input, batchEvidenceRefs) {
1260
1262
  };
1261
1263
  }
1262
1264
 
1263
- // ../../src/storage.ts
1264
- var STRATAGATE_STORAGE_SCHEMA_VERSION = 8;
1265
+ // packages/core/src/storage.ts
1266
+ var STRATAGATE_STORAGE_SCHEMA_VERSION = 9;
1265
1267
  var KNOWLEDGE_GRAPH_PROJECTOR_VERSION = 1;
1266
1268
  var StorageConflictError = class extends Error {
1267
1269
  constructor(namespace, expectedRevision, actualRevision) {
@@ -1278,6 +1280,12 @@ var StorageConflictError = class extends Error {
1278
1280
  function cloneSnapshot(snapshot) {
1279
1281
  return structuredClone(snapshot);
1280
1282
  }
1283
+ function readyLegacyBlocks(blocks) {
1284
+ return blocks.map((block) => ({ ...structuredClone(block), processingStatus: "ready" }));
1285
+ }
1286
+ function legacyExtractionJobs(jobs) {
1287
+ return jobs.map((job) => ({ ...structuredClone(job), nextRetryAt: null }));
1288
+ }
1281
1289
  function emptyGraph() {
1282
1290
  return { graphNodes: [], graphEdges: [], graphProjectionJobs: [] };
1283
1291
  }
@@ -1285,7 +1293,7 @@ function migrateLegacyBlocks(blocks) {
1285
1293
  return blocks.map((block) => {
1286
1294
  const { pointerAnchorTurn, ...current } = block;
1287
1295
  const position = blocks.filter((candidate) => candidate.threadId === block.threadId && candidate.endTurn <= pointerAnchorTurn).length;
1288
- return { ...current, pointerAnchorBlockPosition: Math.max(1, position), lastLiftedBy: null };
1296
+ return { ...current, processingStatus: "ready", pointerAnchorBlockPosition: Math.max(1, position), lastLiftedBy: null };
1289
1297
  });
1290
1298
  }
1291
1299
  function normalizeSnapshot(value) {
@@ -1299,6 +1307,8 @@ function normalizeSnapshot(value) {
1299
1307
  schemaVersion: STRATAGATE_STORAGE_SCHEMA_VERSION,
1300
1308
  blockDecayLambda: BLOCK_DECAY_LAMBDA,
1301
1309
  blocks: migrateLegacyBlocks(legacy.blocks),
1310
+ summaryJobs: [],
1311
+ extractionJobs: legacyExtractionJobs(legacy.extractionJobs),
1302
1312
  elements: [],
1303
1313
  elementProjectionJobs: [],
1304
1314
  usageReceipts: Array.isArray(legacy.usageReceipts) ? legacy.usageReceipts.map((receipt) => ({ ...receipt, elementIds: [] })) : [],
@@ -1312,6 +1322,8 @@ function normalizeSnapshot(value) {
1312
1322
  schemaVersion: STRATAGATE_STORAGE_SCHEMA_VERSION,
1313
1323
  blockDecayLambda: BLOCK_DECAY_LAMBDA,
1314
1324
  blocks: migrateLegacyBlocks(legacy.blocks),
1325
+ summaryJobs: [],
1326
+ extractionJobs: legacyExtractionJobs(legacy.extractionJobs),
1315
1327
  ingestionReceipts: [],
1316
1328
  ...emptyGraph()
1317
1329
  };
@@ -1322,6 +1334,8 @@ function normalizeSnapshot(value) {
1322
1334
  schemaVersion: STRATAGATE_STORAGE_SCHEMA_VERSION,
1323
1335
  blockDecayLambda: BLOCK_DECAY_LAMBDA,
1324
1336
  blocks: migrateLegacyBlocks(legacy.blocks),
1337
+ summaryJobs: [],
1338
+ extractionJobs: legacyExtractionJobs(legacy.extractionJobs),
1325
1339
  ...emptyGraph()
1326
1340
  };
1327
1341
  } else if (schemaVersion === 4) {
@@ -1331,6 +1345,8 @@ function normalizeSnapshot(value) {
1331
1345
  schemaVersion: STRATAGATE_STORAGE_SCHEMA_VERSION,
1332
1346
  blockDecayLambda: BLOCK_DECAY_LAMBDA,
1333
1347
  blocks: migrateLegacyBlocks(legacy.blocks),
1348
+ summaryJobs: [],
1349
+ extractionJobs: legacyExtractionJobs(legacy.extractionJobs),
1334
1350
  ...emptyGraph()
1335
1351
  };
1336
1352
  } else if (schemaVersion === 5) {
@@ -1340,6 +1356,8 @@ function normalizeSnapshot(value) {
1340
1356
  schemaVersion: STRATAGATE_STORAGE_SCHEMA_VERSION,
1341
1357
  blockDecayLambda: BLOCK_DECAY_LAMBDA,
1342
1358
  blocks: migrateLegacyBlocks(legacy.blocks),
1359
+ summaryJobs: [],
1360
+ extractionJobs: legacyExtractionJobs(legacy.extractionJobs),
1343
1361
  ...emptyGraph()
1344
1362
  };
1345
1363
  } else if (schemaVersion === 6) {
@@ -1347,12 +1365,30 @@ function normalizeSnapshot(value) {
1347
1365
  snapshot = {
1348
1366
  ...structuredClone(legacy),
1349
1367
  schemaVersion: STRATAGATE_STORAGE_SCHEMA_VERSION,
1350
- blocks: legacy.blocks.map((block) => ({ ...structuredClone(block), lastLiftedBy: null })),
1368
+ blocks: readyLegacyBlocks(legacy.blocks.map((block) => ({ ...structuredClone(block), lastLiftedBy: null }))),
1369
+ summaryJobs: [],
1370
+ extractionJobs: legacyExtractionJobs(legacy.extractionJobs),
1351
1371
  ...emptyGraph()
1352
1372
  };
1353
1373
  } else if (schemaVersion === 7) {
1354
1374
  const legacy = value;
1355
- snapshot = { ...structuredClone(legacy), schemaVersion: STRATAGATE_STORAGE_SCHEMA_VERSION, ...emptyGraph() };
1375
+ snapshot = {
1376
+ ...structuredClone(legacy),
1377
+ schemaVersion: STRATAGATE_STORAGE_SCHEMA_VERSION,
1378
+ blocks: readyLegacyBlocks(legacy.blocks),
1379
+ summaryJobs: [],
1380
+ extractionJobs: legacyExtractionJobs(legacy.extractionJobs),
1381
+ ...emptyGraph()
1382
+ };
1383
+ } else if (schemaVersion === 8) {
1384
+ const legacy = value;
1385
+ snapshot = {
1386
+ ...structuredClone(legacy),
1387
+ schemaVersion: STRATAGATE_STORAGE_SCHEMA_VERSION,
1388
+ blocks: readyLegacyBlocks(legacy.blocks),
1389
+ summaryJobs: [],
1390
+ extractionJobs: legacyExtractionJobs(legacy.extractionJobs)
1391
+ };
1356
1392
  } else if (schemaVersion === STRATAGATE_STORAGE_SCHEMA_VERSION) {
1357
1393
  snapshot = structuredClone(value);
1358
1394
  } else {
@@ -1367,7 +1403,7 @@ function normalizeSnapshot(value) {
1367
1403
  if (!Number.isFinite(snapshot.blockDecayLambda) || snapshot.blockDecayLambda < 0) {
1368
1404
  throw new TypeError("Invalid StrataGate snapshot: blockDecayLambda must be a non-negative finite number");
1369
1405
  }
1370
- for (const key of ["openTail", "blocks", "events", "graphNodes", "graphEdges", "graphProjectionJobs", "elements", "extractionJobs", "elementProjectionJobs", "usageReceipts", "ingestionReceipts"]) {
1406
+ for (const key of ["openTail", "blocks", "summaryJobs", "events", "graphNodes", "graphEdges", "graphProjectionJobs", "elements", "extractionJobs", "elementProjectionJobs", "usageReceipts", "ingestionReceipts"]) {
1371
1407
  if (!Array.isArray(snapshot[key])) throw new TypeError(`Invalid StrataGate snapshot: ${key} must be an array`);
1372
1408
  }
1373
1409
  if (!Array.isArray(snapshot.successfulModelResponses)) snapshot.successfulModelResponses = [];
@@ -1375,6 +1411,12 @@ function normalizeSnapshot(value) {
1375
1411
  event.temporal = { ...event.temporal, eventType: normalizeStandardEventType(event.temporal.eventType) };
1376
1412
  }
1377
1413
  for (const block of snapshot.blocks) {
1414
+ if (block.processingStatus !== "pending" && block.processingStatus !== "ready") {
1415
+ throw new TypeError("Invalid StrataGate snapshot: Block processingStatus must be pending or ready");
1416
+ }
1417
+ if (block.processingStatus === "ready" && (!block.l0Title || !block.l1Summary || !Array.isArray(block.l0Tags) || !Array.isArray(block.l2Keypoints) || typeof block.shouldExtract !== "boolean")) {
1418
+ throw new TypeError("Invalid StrataGate snapshot: ready Block must contain validated L0-L2 layers");
1419
+ }
1378
1420
  if (!Number.isSafeInteger(block.pointerAnchorBlockPosition) || block.pointerAnchorBlockPosition < 1) {
1379
1421
  throw new TypeError("Invalid StrataGate snapshot: pointerAnchorBlockPosition must be a positive integer");
1380
1422
  }
@@ -1391,7 +1433,7 @@ function assertValidSnapshot(value) {
1391
1433
  normalizeSnapshot(value);
1392
1434
  }
1393
1435
 
1394
- // ../../src/weights.ts
1436
+ // packages/core/src/weights.ts
1395
1437
  var BASE_DECAY = 0.15;
1396
1438
  var REHEARSAL_FACTOR = 1.5;
1397
1439
  function criticalityFloor(criticality) {
@@ -1410,7 +1452,7 @@ function memoryWeightAt(memory, currentTurn) {
1410
1452
  return memory.weight.pinned ? 1 : capped;
1411
1453
  }
1412
1454
 
1413
- // ../../src/elements.ts
1455
+ // packages/core/src/elements.ts
1414
1456
  var ELEMENT_TYPES = /* @__PURE__ */ new Set(["person", "project", "organization", "tool", "place"]);
1415
1457
  var FACT_MODES = /* @__PURE__ */ new Set(["state", "set", "relation"]);
1416
1458
  function compactText(value, limit) {
@@ -1533,10 +1575,10 @@ function elementViewAt(element, at) {
1533
1575
  return view;
1534
1576
  }
1535
1577
 
1536
- // ../../src/sqlite.ts
1578
+ // packages/core/src/sqlite.ts
1537
1579
  import { DatabaseSync } from "node:sqlite";
1538
1580
 
1539
- // ../../src/time.ts
1581
+ // packages/core/src/time.ts
1540
1582
  var UTC8_OFFSET_MS = 8 * 60 * 60 * 1e3;
1541
1583
  function toUtc8Iso(value = /* @__PURE__ */ new Date()) {
1542
1584
  const date = value instanceof Date ? new Date(value.getTime()) : new Date(value);
@@ -1547,7 +1589,7 @@ function nowUtc8() {
1547
1589
  return toUtc8Iso(/* @__PURE__ */ new Date());
1548
1590
  }
1549
1591
 
1550
- // ../../src/sqlite.ts
1592
+ // packages/core/src/sqlite.ts
1551
1593
  var SCHEMA = `
1552
1594
  CREATE TABLE IF NOT EXISTS memory_spaces (
1553
1595
  namespace TEXT PRIMARY KEY,
@@ -1580,6 +1622,7 @@ CREATE TABLE IF NOT EXISTS blocks (
1580
1622
  pointer_anchor_block_position INTEGER NOT NULL,
1581
1623
  last_lifted_at TEXT,
1582
1624
  last_lifted_by TEXT CHECK (last_lifted_by IS NULL OR last_lifted_by IN ('user', 'agent')),
1625
+ processing_status TEXT NOT NULL CHECK (processing_status IN ('pending', 'ready')),
1583
1626
  PRIMARY KEY (namespace, id),
1584
1627
  UNIQUE (namespace, sequence),
1585
1628
  FOREIGN KEY (namespace) REFERENCES memory_spaces(namespace) ON DELETE CASCADE
@@ -1704,6 +1747,19 @@ CREATE TABLE IF NOT EXISTS extraction_jobs (
1704
1747
  status TEXT NOT NULL,
1705
1748
  attempts INTEGER NOT NULL,
1706
1749
  last_error TEXT,
1750
+ next_retry_at TEXT,
1751
+ updated_at TEXT NOT NULL,
1752
+ PRIMARY KEY (namespace, block_id),
1753
+ FOREIGN KEY (namespace, block_id) REFERENCES blocks(namespace, id) ON DELETE CASCADE
1754
+ ) STRICT;
1755
+
1756
+ CREATE TABLE IF NOT EXISTS block_summary_jobs (
1757
+ namespace TEXT NOT NULL,
1758
+ block_id TEXT NOT NULL,
1759
+ status TEXT NOT NULL,
1760
+ attempts INTEGER NOT NULL,
1761
+ last_error TEXT,
1762
+ next_retry_at TEXT,
1707
1763
  updated_at TEXT NOT NULL,
1708
1764
  PRIMARY KEY (namespace, block_id),
1709
1765
  FOREIGN KEY (namespace, block_id) REFERENCES blocks(namespace, id) ON DELETE CASCADE
@@ -1844,11 +1900,14 @@ var SqliteStorage = class {
1844
1900
  startTurn: row.start_turn,
1845
1901
  endTurn: row.end_turn,
1846
1902
  createdAt: row.created_at,
1847
- shouldExtract: Boolean(row.should_extract),
1848
- l0Title: row.l0_title,
1849
- l0Tags: parseJson(row.l0_tags_json, "blocks.l0_tags_json"),
1850
- l1Summary: row.l1_summary,
1851
- l2Keypoints: parseJson(row.l2_keypoints_json, "blocks.l2_keypoints_json"),
1903
+ processingStatus: row.processing_status,
1904
+ ...row.l0_title ? {
1905
+ shouldExtract: Boolean(row.should_extract),
1906
+ l0Title: row.l0_title,
1907
+ l0Tags: parseJson(row.l0_tags_json, "blocks.l0_tags_json"),
1908
+ l1Summary: row.l1_summary,
1909
+ l2Keypoints: parseJson(row.l2_keypoints_json, "blocks.l2_keypoints_json")
1910
+ } : {},
1852
1911
  l3Condensed: row.l3_condensed,
1853
1912
  l4Readable: row.l4_readable,
1854
1913
  l5Raw: messagesByBlock.get(row.id) ?? [],
@@ -1858,6 +1917,17 @@ var SqliteStorage = class {
1858
1917
  lastLiftedAt: row.last_lifted_at,
1859
1918
  lastLiftedBy: row.last_lifted_by
1860
1919
  }));
1920
+ const summaryJobs = this.database.prepare(`
1921
+ SELECT block_id, status, attempts, last_error, next_retry_at, updated_at
1922
+ FROM block_summary_jobs WHERE namespace = ? ORDER BY block_id
1923
+ `).all(key).map((row) => ({
1924
+ blockId: row.block_id,
1925
+ status: row.status,
1926
+ attempts: row.attempts,
1927
+ lastError: row.last_error,
1928
+ nextRetryAt: row.next_retry_at,
1929
+ updatedAt: row.updated_at
1930
+ }));
1861
1931
  const sourceRows = this.database.prepare(`
1862
1932
  SELECT event_id, message_id, position FROM event_sources
1863
1933
  WHERE namespace = ? ORDER BY event_id, position
@@ -1969,13 +2039,14 @@ var SqliteStorage = class {
1969
2039
  };
1970
2040
  });
1971
2041
  const extractionJobs = this.database.prepare(`
1972
- SELECT block_id, status, attempts, last_error, updated_at
2042
+ SELECT block_id, status, attempts, last_error, next_retry_at, updated_at
1973
2043
  FROM extraction_jobs WHERE namespace = ? ORDER BY block_id
1974
2044
  `).all(key).map((row) => ({
1975
2045
  blockId: row.block_id,
1976
2046
  status: row.status,
1977
2047
  attempts: row.attempts,
1978
2048
  lastError: row.last_error,
2049
+ nextRetryAt: row.next_retry_at,
1979
2050
  updatedAt: row.updated_at
1980
2051
  }));
1981
2052
  const elementProjectionJobs = this.database.prepare(`
@@ -2034,6 +2105,7 @@ var SqliteStorage = class {
2034
2105
  blockDecayLambda: space.block_decay_lambda,
2035
2106
  openTail,
2036
2107
  blocks,
2108
+ summaryJobs,
2037
2109
  events,
2038
2110
  graphNodes,
2039
2111
  graphEdges,
@@ -2104,8 +2176,9 @@ var SqliteStorage = class {
2104
2176
  INSERT INTO blocks (
2105
2177
  namespace, id, thread_id, sequence, start_turn, end_turn, created_at, should_extract,
2106
2178
  l0_title, l0_tags_json, l1_summary, l2_keypoints_json, l3_condensed, l4_readable,
2107
- pointer_current_level, pointer_anchor_level, pointer_anchor_block_position, last_lifted_at, last_lifted_by
2108
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2179
+ pointer_current_level, pointer_anchor_level, pointer_anchor_block_position, last_lifted_at, last_lifted_by,
2180
+ processing_status
2181
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2109
2182
  ON CONFLICT (namespace, id) DO UPDATE SET
2110
2183
  thread_id = excluded.thread_id,
2111
2184
  sequence = excluded.sequence,
@@ -2123,7 +2196,8 @@ var SqliteStorage = class {
2123
2196
  pointer_anchor_level = excluded.pointer_anchor_level,
2124
2197
  pointer_anchor_block_position = excluded.pointer_anchor_block_position,
2125
2198
  last_lifted_at = excluded.last_lifted_at,
2126
- last_lifted_by = excluded.last_lifted_by
2199
+ last_lifted_by = excluded.last_lifted_by,
2200
+ processing_status = excluded.processing_status
2127
2201
  `);
2128
2202
  for (const block of snapshot.blocks) {
2129
2203
  insertBlock.run(
@@ -2134,18 +2208,19 @@ var SqliteStorage = class {
2134
2208
  block.startTurn,
2135
2209
  block.endTurn,
2136
2210
  block.createdAt,
2137
- Number(block.shouldExtract),
2138
- block.l0Title,
2139
- JSON.stringify(block.l0Tags),
2140
- block.l1Summary,
2141
- JSON.stringify(block.l2Keypoints),
2211
+ Number(block.shouldExtract ?? false),
2212
+ block.l0Title ?? "",
2213
+ JSON.stringify(block.l0Tags ?? []),
2214
+ block.l1Summary ?? "",
2215
+ JSON.stringify(block.l2Keypoints ?? []),
2142
2216
  block.l3Condensed,
2143
2217
  block.l4Readable,
2144
2218
  block.pointerCurrentLevel,
2145
2219
  block.pointerAnchorLevel,
2146
2220
  block.pointerAnchorBlockPosition,
2147
2221
  block.lastLiftedAt,
2148
- block.lastLiftedBy
2222
+ block.lastLiftedBy,
2223
+ block.processingStatus
2149
2224
  );
2150
2225
  }
2151
2226
  const insertMessage = this.database.prepare(`
@@ -2331,16 +2406,31 @@ var SqliteStorage = class {
2331
2406
  }
2332
2407
  const insertJob = this.database.prepare(`
2333
2408
  INSERT INTO extraction_jobs (
2334
- namespace, block_id, status, attempts, last_error, updated_at
2335
- ) VALUES (?, ?, ?, ?, ?, ?)
2409
+ namespace, block_id, status, attempts, last_error, next_retry_at, updated_at
2410
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)
2336
2411
  ON CONFLICT (namespace, block_id) DO UPDATE SET
2337
2412
  status = excluded.status,
2338
2413
  attempts = excluded.attempts,
2339
2414
  last_error = excluded.last_error,
2415
+ next_retry_at = excluded.next_retry_at,
2340
2416
  updated_at = excluded.updated_at
2341
2417
  `);
2342
2418
  for (const job of snapshot.extractionJobs) {
2343
- insertJob.run(namespace, job.blockId, job.status, job.attempts, job.lastError, job.updatedAt);
2419
+ insertJob.run(namespace, job.blockId, job.status, job.attempts, job.lastError, job.nextRetryAt, job.updatedAt);
2420
+ }
2421
+ const insertSummaryJob = this.database.prepare(`
2422
+ INSERT INTO block_summary_jobs (
2423
+ namespace, block_id, status, attempts, last_error, next_retry_at, updated_at
2424
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)
2425
+ ON CONFLICT (namespace, block_id) DO UPDATE SET
2426
+ status = excluded.status,
2427
+ attempts = excluded.attempts,
2428
+ last_error = excluded.last_error,
2429
+ next_retry_at = excluded.next_retry_at,
2430
+ updated_at = excluded.updated_at
2431
+ `);
2432
+ for (const job of snapshot.summaryJobs) {
2433
+ insertSummaryJob.run(namespace, job.blockId, job.status, job.attempts, job.lastError, job.nextRetryAt, job.updatedAt);
2344
2434
  }
2345
2435
  const insertElementProjectionJob = this.database.prepare(`
2346
2436
  INSERT INTO element_projection_jobs (
@@ -2429,7 +2519,7 @@ var SqliteStorage = class {
2429
2519
  this.database.exec(THREAD_INDEXES);
2430
2520
  this.database.exec(`PRAGMA user_version = ${STRATAGATE_STORAGE_SCHEMA_VERSION}`);
2431
2521
  });
2432
- } else if (version === 1 || version === 2 || version === 3 || version === 4 || version === 5 || version === 6 || version === 7) {
2522
+ } else if (version >= 1 && version < STRATAGATE_STORAGE_SCHEMA_VERSION) {
2433
2523
  this.immediateTransaction(() => {
2434
2524
  this.database.exec(SCHEMA);
2435
2525
  if (version === 1) {
@@ -2465,6 +2555,13 @@ var SqliteStorage = class {
2465
2555
  if (!blockColumns.some(({ name: name2 }) => name2 === "last_lifted_by")) {
2466
2556
  this.database.exec("ALTER TABLE blocks ADD COLUMN last_lifted_by TEXT CHECK (last_lifted_by IS NULL OR last_lifted_by IN ('user', 'agent'))");
2467
2557
  }
2558
+ if (!blockColumns.some(({ name: name2 }) => name2 === "processing_status")) {
2559
+ this.database.exec("ALTER TABLE blocks ADD COLUMN processing_status TEXT NOT NULL DEFAULT 'ready' CHECK (processing_status IN ('pending', 'ready'))");
2560
+ }
2561
+ const extractionColumns = this.database.prepare("PRAGMA table_info('extraction_jobs')").all();
2562
+ if (!extractionColumns.some(({ name: name2 }) => name2 === "next_retry_at")) {
2563
+ this.database.exec("ALTER TABLE extraction_jobs ADD COLUMN next_retry_at TEXT");
2564
+ }
2468
2565
  const messageColumns = this.database.prepare("PRAGMA table_info('messages')").all();
2469
2566
  if (!messageColumns.some(({ name: name2 }) => name2 === "thread_id")) {
2470
2567
  this.database.exec("ALTER TABLE messages ADD COLUMN thread_id TEXT");
@@ -2510,9 +2607,13 @@ var SqliteStorage = class {
2510
2607
  this.assertOpen();
2511
2608
  return this.database.prepare("SELECT namespace FROM memory_spaces ORDER BY namespace").all().map(({ namespace }) => namespace);
2512
2609
  }
2610
+ listNamespaceRevisions() {
2611
+ this.assertOpen();
2612
+ return this.database.prepare("SELECT namespace, revision FROM memory_spaces ORDER BY namespace").all();
2613
+ }
2513
2614
  };
2514
2615
 
2515
- // ../../src/store.ts
2616
+ // packages/core/src/store.ts
2516
2617
  function defaultIdFactory(prefix) {
2517
2618
  return `${prefix}_${crypto.randomUUID()}`;
2518
2619
  }
@@ -2522,21 +2623,13 @@ function defaultElementIdFactory(prefix) {
2522
2623
  function defaultGraphIdFactory(prefix) {
2523
2624
  return `${prefix}_${crypto.randomUUID()}`;
2524
2625
  }
2525
- function defaultSummary(messages) {
2526
- const natural = messages.filter((message) => message.role === "user" || message.role === "assistant");
2527
- const firstUser = natural.find((message) => message.role === "user");
2528
- return {
2529
- l0Title: (firstUser?.content ?? "Conversation block").replace(/\s+/g, " ").trim().slice(0, 80),
2530
- l0Tags: [],
2531
- l1Summary: natural.slice(0, 4).map((message) => message.content.replace(/\s+/g, " ").trim()).join(" ").slice(0, 500),
2532
- l2Keypoints: natural.slice(0, 8).map((message) => message.content.replace(/\s+/g, " ").trim().slice(0, 160)),
2533
- shouldExtract: false
2534
- };
2535
- }
2536
2626
  function renderBlock(block, level) {
2627
+ if (block.processingStatus !== "ready" || !block.l0Title || !block.l0Tags || !block.l1Summary || !block.l2Keypoints) {
2628
+ throw new Error(`Block ${block.id} is not ready for rendering`);
2629
+ }
2537
2630
  if (level === 0) return `${block.l0Title}
2538
2631
  Tags: ${block.l0Tags.join(", ") || "none"}`;
2539
- if (level === 1) return block.l1Summary || block.l0Title;
2632
+ if (level === 1) return block.l1Summary;
2540
2633
  if (level === 2) return block.l2Keypoints.map((point) => `- ${point}`).join("\n") || block.l1Summary;
2541
2634
  if (level === 3) return block.l3Condensed;
2542
2635
  if (level === 4) return block.l4Readable;
@@ -2553,6 +2646,8 @@ function errorMessage(error) {
2553
2646
  return error instanceof Error ? error.message : String(error);
2554
2647
  }
2555
2648
  var STRATAGATE_CONSTRUCTOR_TOKEN = /* @__PURE__ */ Symbol("StrataGate constructor");
2649
+ var DERIVATION_MAX_ATTEMPTS = 3;
2650
+ var DERIVATION_BACKOFF_MS = 1e3;
2556
2651
  var StrataGate = class _StrataGate {
2557
2652
  blockTurnSizeValue;
2558
2653
  blockDecayLambdaValue;
@@ -2572,6 +2667,7 @@ var StrataGate = class _StrataGate {
2572
2667
  graphNodes = [];
2573
2668
  graphEdges = [];
2574
2669
  extractionJobs = /* @__PURE__ */ new Map();
2670
+ summaryJobs = /* @__PURE__ */ new Map();
2575
2671
  elementProjectionJobs = /* @__PURE__ */ new Map();
2576
2672
  graphProjectionJobs = /* @__PURE__ */ new Map();
2577
2673
  usageReceipts = /* @__PURE__ */ new Map();
@@ -2680,6 +2776,21 @@ var StrataGate = class _StrataGate {
2680
2776
  if (loaded && loadedSnapshot) {
2681
2777
  memory.restoreSnapshot(loadedSnapshot);
2682
2778
  memory.revision = loadedRevision;
2779
+ const interruptedSummaries = [...memory.summaryJobs.values()].filter((job) => job.status === "running");
2780
+ if (interruptedSummaries.length > 0) {
2781
+ await memory.commitMutation(() => {
2782
+ const now = toUtc8Iso(memory.now());
2783
+ for (const job of interruptedSummaries) {
2784
+ memory.summaryJobs.set(job.blockId, {
2785
+ ...job,
2786
+ status: "failed",
2787
+ lastError: "Block summarization was interrupted before completion.",
2788
+ nextRetryAt: now,
2789
+ updatedAt: now
2790
+ });
2791
+ }
2792
+ });
2793
+ }
2683
2794
  const interrupted = [...memory.extractionJobs.values()].filter((job) => job.status === "running");
2684
2795
  if (interrupted.length > 0) {
2685
2796
  await memory.commitMutation(() => {
@@ -2689,6 +2800,7 @@ var StrataGate = class _StrataGate {
2689
2800
  ...job,
2690
2801
  status: "failed",
2691
2802
  lastError: "Extraction was interrupted before completion.",
2803
+ nextRetryAt: now,
2692
2804
  updatedAt: now
2693
2805
  });
2694
2806
  }
@@ -2780,6 +2892,9 @@ var StrataGate = class _StrataGate {
2780
2892
  listExtractionJobs() {
2781
2893
  return [...this.extractionJobs.values()];
2782
2894
  }
2895
+ listSummaryJobs() {
2896
+ return [...this.summaryJobs.values()];
2897
+ }
2783
2898
  listElementProjectionJobs() {
2784
2899
  return [...this.elementProjectionJobs.values()];
2785
2900
  }
@@ -2812,6 +2927,7 @@ var StrataGate = class _StrataGate {
2812
2927
  blockDecayLambda: this.blockDecayLambda,
2813
2928
  openTail: this.openTail,
2814
2929
  blocks: this.blocks,
2930
+ summaryJobs: [...this.summaryJobs.values()],
2815
2931
  events: this.events,
2816
2932
  graphNodes: this.graphNodes,
2817
2933
  graphEdges: this.graphEdges,
@@ -2860,57 +2976,55 @@ var StrataGate = class _StrataGate {
2860
2976
  if (receiptId) this.ingestionReceipts.set(receiptId, { id: receiptId, createdAt });
2861
2977
  return true;
2862
2978
  });
2863
- if (!appended) return { sealedBlock: null, extractedEvents: [], projectedElements: [] };
2979
+ if (!appended) return { sealedBlock: null, readyBlocks: [], extractedEvents: [], projectedElements: [] };
2864
2980
  if (options.deferProcessing === true) {
2865
- return { sealedBlock: null, extractedEvents: [], projectedElements: [] };
2981
+ return { sealedBlock: null, readyBlocks: [], extractedEvents: [], projectedElements: [] };
2866
2982
  }
2867
2983
  if (this.threadOpenTail(threadId).filter((message) => message.role === "user").length < this.blockTurnSize) {
2868
2984
  const projectedElements2 = await this.projectEligibleElements() ?? [];
2869
2985
  await this.projectEligibleGraph();
2870
- return { sealedBlock: null, extractedEvents: [], projectedElements: projectedElements2 };
2986
+ return { sealedBlock: null, readyBlocks: [], extractedEvents: [], projectedElements: projectedElements2 };
2871
2987
  }
2872
2988
  const sealedBlock = await this.sealOpenTail(threadId);
2873
- const extractedEvents = await this.extractEligibleBlock() ?? [];
2989
+ if (options.deferDerivation === true) {
2990
+ return { sealedBlock, readyBlocks: [], extractedEvents: [], projectedElements: [] };
2991
+ }
2992
+ const beforeReady = sealedBlock.processingStatus === "ready";
2993
+ const extractedEvents = await this.processBlock(sealedBlock, { retryFailed: false });
2994
+ const readyBlocks = !beforeReady && sealedBlock.processingStatus === "ready" ? [sealedBlock] : [];
2874
2995
  const projectedElements = await this.projectEligibleElements() ?? [];
2875
2996
  await this.projectEligibleGraph();
2876
- return { sealedBlock, extractedEvents, projectedElements };
2997
+ return { sealedBlock, readyBlocks, extractedEvents, projectedElements };
2877
2998
  }
2878
2999
  async resumePendingWork(options = {}) {
2879
3000
  const sealedBlocks = [];
3001
+ const readyBlocks = [];
2880
3002
  const extractedEvents = [];
2881
3003
  const projectedElements = [];
2882
3004
  while (true) {
2883
3005
  const sealable = this.nextSealableThread();
2884
3006
  if (sealable === null) break;
2885
3007
  sealedBlocks.push(await this.sealOpenTail(sealable.threadId));
2886
- extractedEvents.push(...await this.extractEligibleBlock() ?? []);
2887
- projectedElements.push(...await this.projectEligibleElements() ?? []);
2888
- await this.projectEligibleGraph();
2889
3008
  }
2890
- while (true) {
2891
- const extracted = await this.extractEligibleBlock();
2892
- if (extracted === null) break;
3009
+ if (options.deferDerivation === true) {
3010
+ return { sealedBlocks, readyBlocks, extractedEvents, projectedElements };
3011
+ }
3012
+ for (const block of this.blocks) {
3013
+ if (options.threadId !== void 0 && block.threadId !== options.threadId) continue;
3014
+ if (block.processingStatus === "ready") continue;
3015
+ const extracted = await this.processBlock(block, { retryFailed: options.retryFailed === true });
2893
3016
  extractedEvents.push(...extracted);
3017
+ if (this.blocks.find((candidate) => candidate.id === block.id)?.processingStatus === "ready") readyBlocks.push(block);
2894
3018
  projectedElements.push(...await this.projectEligibleElements() ?? []);
2895
3019
  await this.projectEligibleGraph();
2896
3020
  }
2897
- if (options.retrySkipped === true) {
2898
- const skippedBlockIds = this.blocks.filter((block) => this.nextBlockInThread(block) !== null && block.shouldExtract && this.extractionJobs.get(block.id)?.status === "skipped").map((block) => block.id);
2899
- for (const blockId of skippedBlockIds) {
2900
- const extracted = await this.extractEligibleBlock({ blockId, includeSkipped: true });
2901
- if (extracted === null) continue;
2902
- extractedEvents.push(...extracted);
2903
- projectedElements.push(...await this.projectEligibleElements() ?? []);
2904
- await this.projectEligibleGraph();
2905
- }
2906
- }
2907
3021
  while (true) {
2908
3022
  const projected = await this.projectEligibleElements();
2909
3023
  if (projected === null) break;
2910
3024
  projectedElements.push(...projected);
2911
3025
  }
2912
3026
  await this.projectEligibleGraph();
2913
- return { sealedBlocks, extractedEvents, projectedElements };
3027
+ return { sealedBlocks, readyBlocks, extractedEvents, projectedElements };
2914
3028
  }
2915
3029
  async addEvent(input) {
2916
3030
  return this.commitMutation(() => {
@@ -3272,9 +3386,9 @@ var StrataGate = class _StrataGate {
3272
3386
  * StrataGate namespace (never another namespace).
3273
3387
  */
3274
3388
  getBlockContext(threadId) {
3275
- const blocks = threadId === void 0 ? this.blocks : this.blocks.filter((block) => block.threadId === threadId);
3389
+ const blocks = threadId === void 0 ? this.blocks.filter((block) => block.processingStatus === "ready") : this.blocks.filter((block) => block.threadId === threadId && block.processingStatus === "ready");
3276
3390
  return blocks.map((block) => {
3277
- const threadBlocks = this.threadBlocks(block.threadId);
3391
+ const threadBlocks = this.threadBlocks(block.threadId).filter((candidate) => candidate.processingStatus === "ready");
3278
3392
  const latestBlockPosition = threadBlocks.length;
3279
3393
  const blockPosition = threadBlocks.indexOf(block) + 1;
3280
3394
  const age = Math.max(0, latestBlockPosition - blockPosition);
@@ -3300,8 +3414,10 @@ var StrataGate = class _StrataGate {
3300
3414
  return this.commitMutation(() => {
3301
3415
  const block = this.blocks.find((candidate) => candidate.id === id);
3302
3416
  if (!block) throw new Error(`Unknown block: ${id}`);
3303
- const latestBlockPosition = this.threadBlocks(block.threadId).length;
3304
- const blockPosition = this.threadBlocks(block.threadId).indexOf(block) + 1;
3417
+ if (block.processingStatus !== "ready") throw new Error(`Block ${id} is not ready for decay or expansion`);
3418
+ const readyBlocks = this.threadBlocks(block.threadId).filter((candidate) => candidate.processingStatus === "ready");
3419
+ const latestBlockPosition = readyBlocks.length;
3420
+ const blockPosition = readyBlocks.indexOf(block) + 1;
3305
3421
  const current = getDecayedBlockLevel(
3306
3422
  block.pointerAnchorLevel,
3307
3423
  block.pointerAnchorBlockPosition,
@@ -3420,6 +3536,7 @@ var StrataGate = class _StrataGate {
3420
3536
  l1Summary: text3.replace(/\s+/gu, " ").trim().slice(0, 500),
3421
3537
  l2Keypoints: text3.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean).slice(0, 8),
3422
3538
  shouldExtract: false,
3539
+ processingStatus: "ready",
3423
3540
  ...deterministicBlockLayers([message]),
3424
3541
  pointerCurrentLevel: 5,
3425
3542
  pointerAnchorLevel: 5,
@@ -3636,7 +3753,6 @@ var StrataGate = class _StrataGate {
3636
3753
  if (raw.filter((message) => message.role === "user").length < this.blockTurnSize) {
3637
3754
  throw new Error("Open tail does not contain enough turns to seal a block");
3638
3755
  }
3639
- const generated = this.summarizer ? await this.summarizer(raw) : defaultSummary(raw);
3640
3756
  const deterministic = deterministicBlockLayers(raw);
3641
3757
  const sequence = this.blocks.length + 1;
3642
3758
  const threadBlocks = this.threadBlocks(threadId);
@@ -3656,11 +3772,7 @@ var StrataGate = class _StrataGate {
3656
3772
  startTurn,
3657
3773
  endTurn,
3658
3774
  createdAt: raw.at(-1)?.createdAt ?? toUtc8Iso(this.now()),
3659
- l0Title: generated.l0Title,
3660
- l0Tags: generated.l0Tags,
3661
- l1Summary: generated.l1Summary,
3662
- l2Keypoints: generated.l2Keypoints,
3663
- shouldExtract: generated.shouldExtract,
3775
+ processingStatus: "pending",
3664
3776
  ...deterministic,
3665
3777
  pointerCurrentLevel: 5,
3666
3778
  pointerAnchorLevel: 5,
@@ -3672,27 +3784,132 @@ var StrataGate = class _StrataGate {
3672
3784
  const remaining = this.openTail.filter((message) => !sealedIds.has(message.id));
3673
3785
  this.openTail.splice(0, this.openTail.length, ...remaining);
3674
3786
  this.blocks.push(block);
3787
+ const updatedAt = toUtc8Iso(this.now());
3788
+ this.summaryJobs.set(block.id, {
3789
+ blockId: block.id,
3790
+ status: "pending",
3791
+ attempts: 0,
3792
+ lastError: null,
3793
+ nextRetryAt: null,
3794
+ updatedAt
3795
+ });
3675
3796
  return block;
3676
3797
  });
3677
3798
  }
3799
+ async processBlock(block, options) {
3800
+ if (block.processingStatus === "ready") return [];
3801
+ const summary = this.summaryJobs.get(block.id);
3802
+ if (!summary || summary.status !== "succeeded") {
3803
+ if (!this.summarizer || !this.jobCanRun(summary, options.retryFailed)) return [];
3804
+ const claimed = await this.commitMutation(() => {
3805
+ const current = this.summaryJobs.get(block.id);
3806
+ if (!current || !this.jobCanRun(current, options.retryFailed)) return false;
3807
+ this.summaryJobs.set(block.id, {
3808
+ ...current,
3809
+ status: "running",
3810
+ attempts: current.attempts + 1,
3811
+ lastError: null,
3812
+ nextRetryAt: null,
3813
+ updatedAt: toUtc8Iso(this.now())
3814
+ });
3815
+ return true;
3816
+ });
3817
+ if (!claimed) return [];
3818
+ try {
3819
+ const generated = await this.summarizer(block.l5Raw);
3820
+ if (!generated.l0Title.trim() || !generated.l1Summary.trim() || !Array.isArray(generated.l0Tags) || !Array.isArray(generated.l2Keypoints) || typeof generated.shouldExtract !== "boolean") {
3821
+ throw new Error("Block summarizer returned invalid L0-L2 layers");
3822
+ }
3823
+ await this.commitMutation(() => {
3824
+ block.l0Title = generated.l0Title;
3825
+ block.l0Tags = [...generated.l0Tags];
3826
+ block.l1Summary = generated.l1Summary;
3827
+ block.l2Keypoints = [...generated.l2Keypoints];
3828
+ block.shouldExtract = generated.shouldExtract;
3829
+ const current = this.summaryJobs.get(block.id);
3830
+ if (!current) throw new Error(`Missing summary job for block: ${block.id}`);
3831
+ this.summaryJobs.set(block.id, {
3832
+ ...current,
3833
+ status: "succeeded",
3834
+ lastError: null,
3835
+ nextRetryAt: null,
3836
+ updatedAt: toUtc8Iso(this.now())
3837
+ });
3838
+ });
3839
+ } catch (error) {
3840
+ await this.failSummary(block.id, error);
3841
+ return [];
3842
+ }
3843
+ }
3844
+ if (block.shouldExtract === false) {
3845
+ await this.commitMutation(() => {
3846
+ const now = toUtc8Iso(this.now());
3847
+ this.extractionJobs.set(block.id, {
3848
+ blockId: block.id,
3849
+ status: "skipped",
3850
+ attempts: this.extractionJobs.get(block.id)?.attempts ?? 0,
3851
+ lastError: null,
3852
+ nextRetryAt: null,
3853
+ updatedAt: now
3854
+ });
3855
+ this.markBlockReady(block);
3856
+ });
3857
+ return [];
3858
+ }
3859
+ try {
3860
+ const extracted = await this.extractEligibleBlock({ blockId: block.id, retryFailed: options.retryFailed });
3861
+ return extracted ?? [];
3862
+ } catch {
3863
+ return [];
3864
+ }
3865
+ }
3866
+ jobCanRun(job, force) {
3867
+ if (!job || job.attempts >= DERIVATION_MAX_ATTEMPTS || job.status === "running" || job.status === "succeeded") return false;
3868
+ return force || job.nextRetryAt === null || Date.parse(job.nextRetryAt) <= this.now().getTime();
3869
+ }
3870
+ async failSummary(blockId, error) {
3871
+ await this.commitMutation(() => {
3872
+ const job = this.summaryJobs.get(blockId);
3873
+ if (!job) return;
3874
+ this.summaryJobs.set(blockId, {
3875
+ ...job,
3876
+ status: "failed",
3877
+ lastError: errorMessage(error),
3878
+ nextRetryAt: this.retryAt(job.attempts),
3879
+ updatedAt: toUtc8Iso(this.now())
3880
+ });
3881
+ });
3882
+ }
3883
+ retryAt(attempts) {
3884
+ if (attempts >= DERIVATION_MAX_ATTEMPTS) return null;
3885
+ return toUtc8Iso(new Date(this.now().getTime() + DERIVATION_BACKOFF_MS * 2 ** Math.max(0, attempts - 1)));
3886
+ }
3887
+ markBlockReady(block) {
3888
+ if (!block.l0Title || !block.l0Tags || !block.l1Summary || !block.l2Keypoints || typeof block.shouldExtract !== "boolean") {
3889
+ throw new Error(`Block ${block.id} cannot become ready without validated L0-L2 layers`);
3890
+ }
3891
+ const ready = this.threadBlocks(block.threadId).filter((candidate) => candidate.processingStatus === "ready");
3892
+ block.processingStatus = "ready";
3893
+ block.pointerCurrentLevel = 5;
3894
+ block.pointerAnchorLevel = 5;
3895
+ block.pointerAnchorBlockPosition = ready.length + 1;
3896
+ }
3678
3897
  async extractEligibleBlock(options = {}) {
3679
- if (!this.extractor || this.blocks.length < 2) return null;
3898
+ if (!this.extractor) return null;
3680
3899
  const target = this.blocks.find((block) => {
3681
- if (this.nextBlockInThread(block) === null || !block.shouldExtract) return false;
3900
+ if (block.processingStatus === "ready" || block.shouldExtract !== true) return false;
3682
3901
  if (options.blockId !== void 0 && block.id !== options.blockId) return false;
3683
- const status = this.extractionJobs.get(block.id)?.status;
3684
- return status === void 0 || status === "failed" || options.includeSkipped === true && status === "skipped";
3902
+ const job = this.extractionJobs.get(block.id);
3903
+ return job === void 0 || this.jobCanRun(job, options.retryFailed === true);
3685
3904
  });
3686
3905
  if (!target) return null;
3687
3906
  const threadBlocks = this.threadBlocks(target.threadId);
3688
3907
  const targetIndex = threadBlocks.indexOf(target);
3689
- const next = threadBlocks[targetIndex + 1];
3690
- if (!next) return null;
3908
+ const next = threadBlocks.slice(targetIndex + 1).find((block) => block.l2Keypoints !== void 0) ?? null;
3691
3909
  const existing = this.extractionJobs.get(target.id);
3692
3910
  await this.commitMutation(() => {
3693
3911
  const currentStatus = this.extractionJobs.get(target.id)?.status;
3694
- const canRetrySkipped = options.includeSkipped === true && currentStatus === "skipped";
3695
- if (currentStatus !== void 0 && currentStatus !== "failed" && !canRetrySkipped) {
3912
+ if (currentStatus !== void 0 && currentStatus !== "failed") {
3696
3913
  throw new Error(`Extraction block ${target.id} is already ${currentStatus}`);
3697
3914
  }
3698
3915
  this.extractionJobs.set(target.id, {
@@ -3700,13 +3917,14 @@ var StrataGate = class _StrataGate {
3700
3917
  status: "running",
3701
3918
  attempts: (existing?.attempts ?? 0) + 1,
3702
3919
  lastError: null,
3920
+ nextRetryAt: null,
3703
3921
  updatedAt: toUtc8Iso(this.now())
3704
3922
  });
3705
3923
  });
3706
3924
  let result;
3707
3925
  try {
3708
3926
  result = await this.extractor({
3709
- previous: threadBlocks[targetIndex - 1] ?? null,
3927
+ previous: threadBlocks.slice(0, targetIndex).reverse().find((block) => block.l2Keypoints !== void 0) ?? null,
3710
3928
  target,
3711
3929
  next,
3712
3930
  timeline: this.events.map((event) => ({ id: event.id, title: event.title, temporal: event.temporal }))
@@ -3719,6 +3937,7 @@ var StrataGate = class _StrataGate {
3719
3937
  ...job,
3720
3938
  status: "failed",
3721
3939
  lastError: errorMessage(error),
3940
+ nextRetryAt: this.retryAt(job.attempts),
3722
3941
  updatedAt: toUtc8Iso(this.now())
3723
3942
  });
3724
3943
  });
@@ -3733,6 +3952,7 @@ var StrataGate = class _StrataGate {
3733
3952
  ...job,
3734
3953
  status: "failed",
3735
3954
  lastError: reason,
3955
+ nextRetryAt: this.retryAt(job.attempts),
3736
3956
  updatedAt: toUtc8Iso(this.now())
3737
3957
  });
3738
3958
  });
@@ -3751,8 +3971,10 @@ var StrataGate = class _StrataGate {
3751
3971
  ...job,
3752
3972
  status: result.shouldExtract ? "succeeded" : "skipped",
3753
3973
  lastError: null,
3974
+ nextRetryAt: null,
3754
3975
  updatedAt: toUtc8Iso(this.now())
3755
3976
  });
3977
+ this.markBlockReady(target);
3756
3978
  return extracted;
3757
3979
  });
3758
3980
  }
@@ -3821,6 +4043,8 @@ var StrataGate = class _StrataGate {
3821
4043
  this.currentTurn = copy.currentTurn;
3822
4044
  this.openTail.splice(0, this.openTail.length, ...copy.openTail);
3823
4045
  this.blocks.splice(0, this.blocks.length, ...copy.blocks);
4046
+ this.summaryJobs.clear();
4047
+ for (const job of copy.summaryJobs) this.summaryJobs.set(job.blockId, job);
3824
4048
  this.events.splice(0, this.events.length, ...copy.events);
3825
4049
  this.graphNodes.splice(0, this.graphNodes.length, ...copy.graphNodes);
3826
4050
  this.graphEdges.splice(0, this.graphEdges.length, ...copy.graphEdges);
@@ -3867,6 +4091,9 @@ var StrataGate = class _StrataGate {
3867
4091
  for (const job of this.extractionJobs.values()) {
3868
4092
  if (!blockIds.has(job.blockId)) throw new Error(`Unknown extraction job block in snapshot: ${job.blockId}`);
3869
4093
  }
4094
+ for (const job of this.summaryJobs.values()) {
4095
+ if (!blockIds.has(job.blockId)) throw new Error(`Unknown summary job block in snapshot: ${job.blockId}`);
4096
+ }
3870
4097
  const elementIds = /* @__PURE__ */ new Set();
3871
4098
  for (const element of this.elements) {
3872
4099
  if (elementIds.has(element.id)) throw new Error(`Duplicate element ID in snapshot: ${element.id}`);
@@ -4095,7 +4322,7 @@ function extractorPayload(context) {
4095
4322
  }
4096
4323
  var JSON_RESPONSE_ATTEMPTS = 2;
4097
4324
  var JSON_RETRY_INSTRUCTION = "Your previous response did not make one valid call to the requested tool. Do not spend output on analysis or reasoning. Immediately call that tool exactly once with complete arguments. Do not return an answer as text or markdown.";
4098
- var RETRY_MAX_TOKENS = 1e4;
4325
+ var DEFAULT_STRUCTURED_TIMEOUT_MS = 45e3;
4099
4326
  var STRUCTURED_FIELDS = {
4100
4327
  summarizer: ["l0Title", "l0Tags", "l1Summary", "l2Keypoints", "shouldExtract"],
4101
4328
  extractor: ["shouldExtract", "reason", "events"],
@@ -4246,11 +4473,15 @@ var DshModelBridge = class {
4246
4473
  constructor(ctx, config) {
4247
4474
  this.ctx = ctx;
4248
4475
  this.config = config;
4476
+ this.ctx.on?.("llm/adapters-updated", () => {
4477
+ this.offCapabilities.clear();
4478
+ });
4249
4479
  }
4250
4480
  ctx;
4251
4481
  config;
4252
4482
  sessions = new AsyncLocalStorage();
4253
4483
  successfulResponses = [];
4484
+ offCapabilities = /* @__PURE__ */ new Map();
4254
4485
  run(session, operation) {
4255
4486
  return this.sessions.run(session, operation);
4256
4487
  }
@@ -4265,7 +4496,7 @@ var DshModelBridge = class {
4265
4496
  { messages }
4266
4497
  ));
4267
4498
  return {
4268
- l0Title: text2(raw.l0Title, "Conversation block").slice(0, 120),
4499
+ l0Title: text2(raw.l0Title).slice(0, 120),
4269
4500
  l0Tags: strings2(raw.l0Tags).slice(0, 12),
4270
4501
  l1Summary: text2(raw.l1Summary).slice(0, 2e3),
4271
4502
  l2Keypoints: strings2(raw.l2Keypoints).slice(0, 20),
@@ -4397,10 +4628,11 @@ var DshModelBridge = class {
4397
4628
  async callStructured(kind, system, payload) {
4398
4629
  const session = this.sessions.getStore();
4399
4630
  if (!session) throw new Error("StrataGate model callback ran without a DSH session");
4400
- const route = this.resolveRoute(session, true);
4631
+ const baseRoute = this.resolveRoute(session);
4632
+ const routeKey = `${baseRoute.provider}\0${baseRoute.model}`;
4633
+ let useOff = await this.shouldUseOff(baseRoute);
4401
4634
  let lastError;
4402
4635
  let lastResponse = "";
4403
- let retryMaxTokens = this.config.maxOutputTokens;
4404
4636
  for (let attempt = 1; attempt <= JSON_RESPONSE_ATTEMPTS; attempt += 1) {
4405
4637
  const message = createUserMessage({
4406
4638
  content: [{ type: "text", text: JSON.stringify(payload) }],
@@ -4408,7 +4640,8 @@ var DshModelBridge = class {
4408
4640
  });
4409
4641
  const assembler = new BlockAssembler();
4410
4642
  const request = {
4411
- ...route,
4643
+ ...baseRoute,
4644
+ ...useOff ? { reasoningEffort: "off" } : {},
4412
4645
  messages: [message],
4413
4646
  system: attempt === 1 ? system : `${system}
4414
4647
 
@@ -4422,15 +4655,35 @@ ${JSON_RETRY_INSTRUCTION}`,
4422
4655
  type: "function",
4423
4656
  function: { name: STRUCTURED_TOOLS[kind].name }
4424
4657
  },
4425
- maxTokens: retryMaxTokens,
4658
+ maxTokens: this.config.maxOutputTokens,
4426
4659
  sessionId: session.id,
4427
4660
  purpose: "compaction"
4428
4661
  };
4429
- for await (const chunk of this.ctx.llm.stream(request)) assembler.push(chunk);
4662
+ try {
4663
+ await this.consumeStructuredStream(request, assembler);
4664
+ } catch (error) {
4665
+ if (useOff && isOffRejection(error)) {
4666
+ this.offCapabilities.set(routeKey, "unsupported");
4667
+ useOff = false;
4668
+ this.ctx.logger.warn(`stratagate-memory ${baseRoute.provider}/${baseRoute.model} rejected reasoningEffort=off; retrying once without it`);
4669
+ attempt -= 1;
4670
+ continue;
4671
+ }
4672
+ throw error;
4673
+ }
4430
4674
  const finish = assembler.finish;
4431
4675
  if (finish.kind === "error" || finish.kind === "aborted") {
4432
- throw new Error(`StrataGate model call failed: ${finish.failure.message}`);
4676
+ const failure = new Error(`StrataGate model call failed [${finish.failure.code}]: ${finish.failure.message}`);
4677
+ if (useOff && isOffRejection(finish.failure)) {
4678
+ this.offCapabilities.set(routeKey, "unsupported");
4679
+ useOff = false;
4680
+ this.ctx.logger.warn(`stratagate-memory ${baseRoute.provider}/${baseRoute.model} rejected reasoningEffort=off; retrying once without it`);
4681
+ attempt -= 1;
4682
+ continue;
4683
+ }
4684
+ throw failure;
4433
4685
  }
4686
+ if (useOff) this.offCapabilities.set(routeKey, "supported");
4434
4687
  const blocks = assembler.blocks();
4435
4688
  const calls = blocks.filter((block) => block.type === "tool-call");
4436
4689
  const responseForError = `${renderBlocksForDiagnostics(blocks, finish.kind)}
@@ -4466,6 +4719,15 @@ ${JSON_RETRY_INSTRUCTION}`,
4466
4719
  { response: responseForError }
4467
4720
  );
4468
4721
  }
4722
+ if (kind === "summarizer") {
4723
+ const summary = object(parsed);
4724
+ if (!text2(summary.l0Title) || !text2(summary.l1Summary)) {
4725
+ throw new ModelJsonResponseError(
4726
+ `StrataGate ${expectedTool} arguments were invalid: l0Title and l1Summary must not be empty`,
4727
+ { response: responseForError }
4728
+ );
4729
+ }
4730
+ }
4469
4731
  this.successfulResponses.push({
4470
4732
  id: `model_response_${crypto.randomUUID()}`,
4471
4733
  kind,
@@ -4480,7 +4742,6 @@ ${JSON_RETRY_INSTRUCTION}`,
4480
4742
  `StrataGate ${STRUCTURED_TOOLS[kind].name} call was truncated before valid arguments`,
4481
4743
  { cause: error, response: responseForError }
4482
4744
  ) : error;
4483
- if (finish.kind === "max-tokens") retryMaxTokens = Math.max(this.config.maxOutputTokens, RETRY_MAX_TOKENS);
4484
4745
  if (attempt < JSON_RESPONSE_ATTEMPTS) {
4485
4746
  this.ctx.logger.warn(`stratagate-memory model returned an invalid structured tool call; retrying (${attempt}/${JSON_RESPONSE_ATTEMPTS})`);
4486
4747
  }
@@ -4491,25 +4752,77 @@ ${JSON_RETRY_INSTRUCTION}`,
4491
4752
  { cause: lastError, response: lastResponse }
4492
4753
  );
4493
4754
  }
4494
- resolveRoute(session, structured = false) {
4495
- const request = session.requestHeader()?.config;
4496
- const requestedReasoningEffort = request?.reasoningEffort;
4497
- const withReasoningEffort = (route) => ({
4498
- ...route,
4499
- ...structured ? { reasoningEffort: "off" } : requestedReasoningEffort !== void 0 ? { reasoningEffort: requestedReasoningEffort } : {}
4755
+ async shouldUseOff(route) {
4756
+ const key = `${route.provider}\0${route.model}`;
4757
+ const cached = this.offCapabilities.get(key);
4758
+ if (cached) return cached === "supported";
4759
+ if (typeof this.ctx.llm.resolveModelInfo !== "function") return true;
4760
+ const controller = new AbortController();
4761
+ const lookupTimeoutMs = Math.min(5e3, this.config.structuredTaskTimeoutMs ?? DEFAULT_STRUCTURED_TIMEOUT_MS);
4762
+ let timer;
4763
+ try {
4764
+ const info = await Promise.race([
4765
+ this.ctx.llm.resolveModelInfo(route.provider, route.model, controller.signal),
4766
+ new Promise((_resolve, reject) => {
4767
+ timer = setTimeout(() => {
4768
+ controller.abort();
4769
+ reject(new Error(`Model capability lookup timed out after ${lookupTimeoutMs}ms`));
4770
+ }, lookupTimeoutMs);
4771
+ timer.unref?.();
4772
+ })
4773
+ ]);
4774
+ if (!info.reasoning) return true;
4775
+ const supported = info.reasoning.efforts.some(({ id }) => String(id) === "off");
4776
+ this.offCapabilities.set(key, supported ? "supported" : "unsupported");
4777
+ return supported;
4778
+ } catch {
4779
+ return true;
4780
+ } finally {
4781
+ if (timer) clearTimeout(timer);
4782
+ }
4783
+ }
4784
+ async consumeStructuredStream(request, assembler) {
4785
+ const timeoutMs = this.config.structuredTaskTimeoutMs ?? DEFAULT_STRUCTURED_TIMEOUT_MS;
4786
+ const controller = new AbortController();
4787
+ const timedRequest = { ...request, signal: controller.signal };
4788
+ let timer;
4789
+ const timeout = new Promise((_resolve, reject) => {
4790
+ timer = setTimeout(() => {
4791
+ controller.abort(new Error(`StrataGate structured model task timed out after ${timeoutMs}ms`));
4792
+ reject(new Error(`StrataGate structured model task timed out after ${timeoutMs}ms`));
4793
+ }, timeoutMs);
4794
+ timer.unref?.();
4500
4795
  });
4796
+ try {
4797
+ await Promise.race([
4798
+ (async () => {
4799
+ for await (const chunk of this.ctx.llm.stream(timedRequest)) assembler.push(chunk);
4800
+ })(),
4801
+ timeout
4802
+ ]);
4803
+ } finally {
4804
+ if (timer) clearTimeout(timer);
4805
+ }
4806
+ }
4807
+ resolveRoute(session) {
4808
+ const request = session.requestHeader()?.config;
4501
4809
  if (this.config.provider && this.config.model) {
4502
- return withReasoningEffort({ provider: this.config.provider, model: this.config.model });
4810
+ return { provider: this.config.provider, model: this.config.model };
4503
4811
  }
4504
- if (request) return withReasoningEffort({ provider: request.provider, model: request.model });
4812
+ if (request) return { provider: request.provider, model: request.model };
4505
4813
  const fallback = this.ctx.agentDefaultModel.currentSelection();
4506
- return {
4507
- provider: fallback.provider,
4508
- model: fallback.model,
4509
- ...structured ? { reasoningEffort: "off" } : fallback.reasoningEffort !== void 0 ? { reasoningEffort: fallback.reasoningEffort } : {}
4510
- };
4814
+ return { provider: fallback.provider, model: fallback.model };
4511
4815
  }
4512
4816
  };
4817
+ function isOffRejection(error) {
4818
+ let detail;
4819
+ try {
4820
+ detail = typeof error === "string" ? error : error && typeof error === "object" ? JSON.stringify(error, Object.getOwnPropertyNames(error)) : String(error);
4821
+ } catch {
4822
+ detail = error instanceof Error ? error.message : String(error);
4823
+ }
4824
+ return /(?:reasoning[_ -]?effort|reasoning).{0,100}\boff\b|\boff\b.{0,100}(?:reasoning[_ -]?effort|reasoning)/iu.test(detail) && /(?:unsupported|not supported|invalid|not allowed|unknown|unrecognized|reject|must be|expected)/iu.test(detail);
4825
+ }
4513
4826
 
4514
4827
  // src/runtime.ts
4515
4828
  import { createHash } from "node:crypto";
@@ -4736,6 +5049,9 @@ var StrataGateRuntime = class {
4736
5049
  latestBatchIds = /* @__PURE__ */ new Map();
4737
5050
  workspaceNames = /* @__PURE__ */ new Map();
4738
5051
  migrationTimers = /* @__PURE__ */ new Map();
5052
+ derivationTimers = /* @__PURE__ */ new Map();
5053
+ derivationRuns = /* @__PURE__ */ new Map();
5054
+ adminSnapshotCache = /* @__PURE__ */ new Map();
4739
5055
  ingestTail = Promise.resolve();
4740
5056
  settingsTail = Promise.resolve();
4741
5057
  batchSequence = 0;
@@ -4752,15 +5068,8 @@ var StrataGateRuntime = class {
4752
5068
  }).then(async () => {
4753
5069
  const memory = await this.space(session);
4754
5070
  try {
4755
- const result = await this.models.run(session, () => memory.appendTurn(turn));
4756
- if (result.sealedBlock) {
4757
- const contexts = memory.getBlockContext(String(session.id));
4758
- const sealedContext = contexts.find(({ id }) => id === result.sealedBlock.id);
4759
- if (!sealedContext) throw new Error(`Missing context for sealed StrataGate block ${result.sealedBlock.id}`);
4760
- this.replaceSealedSurface(session, result.sealedBlock, sealedContext, turn.dshTurn);
4761
- this.syncDecayedBlockSurface(session, contexts);
4762
- await this.flushNativeSession(session);
4763
- }
5071
+ const result = await memory.appendTurn(turn, { deferDerivation: true });
5072
+ if (result.sealedBlock) this.scheduleBlockDerivation(session, memory);
4764
5073
  } finally {
4765
5074
  await this.persistSuccessfulResponses(memory);
4766
5075
  }
@@ -5074,6 +5383,8 @@ var StrataGateRuntime = class {
5074
5383
  this.closed = true;
5075
5384
  for (const timer of this.migrationTimers.values()) clearTimeout(timer);
5076
5385
  this.migrationTimers.clear();
5386
+ for (const timer of this.derivationTimers.values()) clearTimeout(timer);
5387
+ this.derivationTimers.clear();
5077
5388
  let flushError;
5078
5389
  try {
5079
5390
  await this.flush();
@@ -5081,6 +5392,7 @@ var StrataGateRuntime = class {
5081
5392
  flushError = error;
5082
5393
  }
5083
5394
  const settled = await Promise.allSettled(this.spaces.values());
5395
+ await Promise.allSettled(this.derivationRuns.values());
5084
5396
  await Promise.all(settled.flatMap((result) => result.status === "fulfilled" ? [result.value.close()] : []));
5085
5397
  if (flushError !== void 0) throw flushError;
5086
5398
  }
@@ -5099,6 +5411,42 @@ var StrataGateRuntime = class {
5099
5411
  await storage.close();
5100
5412
  }
5101
5413
  }
5414
+ async adminSnapshotEntries() {
5415
+ if (this.config.database === ":memory:" || !existsSync(this.config.database)) return [];
5416
+ const storage = new SqliteStorage({ filename: this.config.database, readonly: true });
5417
+ try {
5418
+ const entries = [];
5419
+ for (const { namespace, revision } of storage.listNamespaceRevisions()) {
5420
+ const opening = this.spaces.get(namespace);
5421
+ if (opening) {
5422
+ const memory = await opening;
5423
+ const currentRevision = memory.storageRevision;
5424
+ const cached2 = this.adminSnapshotCache.get(namespace);
5425
+ const entry2 = cached2?.revision === currentRevision ? cached2 : { namespace, revision: currentRevision, snapshot: memory.exportSnapshot() };
5426
+ this.adminSnapshotCache.set(namespace, entry2);
5427
+ entries.push(entry2);
5428
+ continue;
5429
+ }
5430
+ const cached = this.adminSnapshotCache.get(namespace);
5431
+ if (cached?.revision === revision) {
5432
+ entries.push(cached);
5433
+ continue;
5434
+ }
5435
+ const loaded = await storage.load(namespace);
5436
+ if (!loaded) continue;
5437
+ const entry = { namespace, revision: loaded.revision, snapshot: loaded.snapshot };
5438
+ this.adminSnapshotCache.set(namespace, entry);
5439
+ entries.push(entry);
5440
+ }
5441
+ const activeNamespaces = new Set(entries.map(({ namespace }) => namespace));
5442
+ for (const namespace of this.adminSnapshotCache.keys()) {
5443
+ if (!activeNamespaces.has(namespace)) this.adminSnapshotCache.delete(namespace);
5444
+ }
5445
+ return entries;
5446
+ } finally {
5447
+ await storage.close();
5448
+ }
5449
+ }
5102
5450
  async syncConfiguredSettings() {
5103
5451
  if (this.config.database === ":memory:" || !existsSync(this.config.database)) return;
5104
5452
  const metadata = new DshMetadataStore(this.config.database);
@@ -5127,9 +5475,27 @@ var StrataGateRuntime = class {
5127
5475
  const key = namespace.trim();
5128
5476
  if (!key) throw new TypeError("StrataGate admin namespace must not be empty");
5129
5477
  if (this.config.database === ":memory:" || !existsSync(this.config.database)) return null;
5478
+ const opening = this.spaces.get(key);
5479
+ if (opening) {
5480
+ const memory = await opening;
5481
+ const revision = memory.storageRevision;
5482
+ const cached = this.adminSnapshotCache.get(key);
5483
+ if (cached?.revision === revision) return cached.snapshot;
5484
+ const entry = { namespace: key, revision, snapshot: memory.exportSnapshot() };
5485
+ this.adminSnapshotCache.set(key, entry);
5486
+ return entry.snapshot;
5487
+ }
5130
5488
  const storage = new SqliteStorage({ filename: this.config.database, readonly: true });
5131
5489
  try {
5132
- return (await storage.load(key))?.snapshot ?? null;
5490
+ const head = storage.listNamespaceRevisions().find(({ namespace: namespace2 }) => namespace2 === key);
5491
+ if (!head) return null;
5492
+ const cached = this.adminSnapshotCache.get(key);
5493
+ if (cached?.revision === head.revision) return cached.snapshot;
5494
+ const loaded = await storage.load(key);
5495
+ if (!loaded) return null;
5496
+ const entry = { namespace: key, revision: loaded.revision, snapshot: loaded.snapshot };
5497
+ this.adminSnapshotCache.set(key, entry);
5498
+ return entry.snapshot;
5133
5499
  } finally {
5134
5500
  await storage.close();
5135
5501
  }
@@ -5319,23 +5685,14 @@ var StrataGateRuntime = class {
5319
5685
  }).then(async (memory) => {
5320
5686
  try {
5321
5687
  try {
5322
- const resumed = await this.models.run(session, () => memory.resumePendingWork({ retrySkipped: true }));
5688
+ await memory.resumePendingWork({ deferDerivation: true, threadId: String(session.id) });
5323
5689
  const contexts = memory.getBlockContext(String(session.id));
5324
- for (const block of resumed.sealedBlocks) {
5325
- if (block.threadId !== String(session.id)) continue;
5326
- const endTurn = dshTurnAtBlockEnd(session, block);
5327
- const context = contexts.find(({ id }) => id === block.id);
5328
- if (!context) throw new Error(`Missing context for recovered StrataGate block ${block.id}`);
5329
- this.replaceSealedSurface(session, block, context, endTurn);
5330
- }
5331
5690
  this.syncDecayedBlockSurface(session, contexts);
5332
- if (resumed.sealedBlocks.some((block) => block.threadId === String(session.id))) {
5333
- await this.flushNativeSession(session);
5334
- }
5335
5691
  } finally {
5336
5692
  await this.persistSuccessfulResponses(memory);
5337
5693
  }
5338
5694
  this.scheduleGraphMigration(session, memory);
5695
+ this.scheduleBlockDerivation(session, memory);
5339
5696
  return memory;
5340
5697
  } catch (error) {
5341
5698
  await memory.close().catch(() => {
@@ -5369,6 +5726,43 @@ var StrataGateRuntime = class {
5369
5726
  target: { eventIds: [.../* @__PURE__ */ new Set([...node.sourceEventIds, ...edges.flatMap(({ sourceEventIds }) => sourceEventIds)])], elementIds: [] }
5370
5727
  }], { node, edges });
5371
5728
  }
5729
+ scheduleBlockDerivation(session, memory) {
5730
+ const threadId = String(session.id);
5731
+ const key = `${this.namespaceFor(session)}\0${threadId}`;
5732
+ if (this.closed || this.derivationTimers.has(key) || this.derivationRuns.has(key)) return;
5733
+ const pendingBlockIds = new Set(memory.listBlocks().filter((block) => block.threadId === threadId && block.processingStatus === "pending").map((block) => block.id));
5734
+ if (pendingBlockIds.size === 0) return;
5735
+ const retryTimes = [
5736
+ ...memory.listSummaryJobs(),
5737
+ ...memory.listExtractionJobs()
5738
+ ].filter((job) => pendingBlockIds.has(job.blockId) && (job.status === "pending" || job.status === "failed" && job.nextRetryAt !== null)).map((job) => job.nextRetryAt ? Date.parse(job.nextRetryAt) : Date.now()).filter(Number.isFinite);
5739
+ if (retryTimes.length === 0) return;
5740
+ const delay = Math.max(0, Math.min(...retryTimes) - Date.now());
5741
+ const timer = setTimeout(() => {
5742
+ this.derivationTimers.delete(key);
5743
+ if (this.closed) return;
5744
+ const run = this.models.run(session, () => memory.resumePendingWork({ threadId })).then(async (resumed) => {
5745
+ await this.persistSuccessfulResponses(memory);
5746
+ const contexts = memory.getBlockContext(threadId);
5747
+ for (const block of resumed.readyBlocks) {
5748
+ if (block.threadId !== threadId) continue;
5749
+ const context = contexts.find(({ id }) => id === block.id);
5750
+ if (!context) throw new Error(`Missing context for ready StrataGate block ${block.id}`);
5751
+ this.replaceSealedSurface(session, block, context, dshTurnAtBlockEnd(session, block));
5752
+ }
5753
+ const changed = this.syncDecayedBlockSurface(session, contexts);
5754
+ if (resumed.readyBlocks.length > 0 || changed) await this.flushNativeSession(session);
5755
+ }).catch((error) => {
5756
+ this.onIngestError(error);
5757
+ }).finally(() => {
5758
+ this.derivationRuns.delete(key);
5759
+ this.scheduleBlockDerivation(session, memory);
5760
+ });
5761
+ this.derivationRuns.set(key, run);
5762
+ }, delay);
5763
+ timer.unref?.();
5764
+ this.derivationTimers.set(key, timer);
5765
+ }
5372
5766
  scheduleGraphMigration(session, memory) {
5373
5767
  const namespace = this.namespaceFor(session);
5374
5768
  if (this.closed || this.migrationTimers.has(namespace)) return;
@@ -5895,6 +6289,7 @@ function registerMemoryTools(ctx, runtime) {
5895
6289
  }
5896
6290
 
5897
6291
  // src/web.ts
6292
+ import { createHash as createHash2 } from "node:crypto";
5898
6293
  import { createRequire } from "node:module";
5899
6294
 
5900
6295
  // src/graph-clustering.ts
@@ -6064,6 +6459,13 @@ function sourceMessages(snapshot, ids) {
6064
6459
  return output;
6065
6460
  }
6066
6461
  function blockLayers(block) {
6462
+ if (block.processingStatus !== "ready" || !block.l0Title || !block.l0Tags || !block.l1Summary || !block.l2Keypoints) {
6463
+ return [
6464
+ { level: 3, content: block.l3Condensed },
6465
+ { level: 4, content: block.l4Readable },
6466
+ { level: 5, content: block.l5Raw.map((message) => `${message.role}: ${message.content}`).join("\n\n") }
6467
+ ];
6468
+ }
6067
6469
  return [
6068
6470
  { level: 0, content: `${block.l0Title}
6069
6471
  \u6807\u7B7E\uFF1A${block.l0Tags.join("\u3001") || "\u65E0"}` },
@@ -6125,14 +6527,17 @@ var AdminHttpError = class extends Error {
6125
6527
  }
6126
6528
  status;
6127
6529
  };
6128
- async function overview(runtime) {
6129
- const namespaces = await runtime.adminNamespaces();
6530
+ async function overview(runtime, cachedEntries) {
6531
+ const entries = cachedEntries ?? await Promise.all((await runtime.adminNamespaces()).map(async (namespace) => ({
6532
+ namespace,
6533
+ revision: 0,
6534
+ snapshot: await runtime.adminSnapshot(namespace)
6535
+ })));
6130
6536
  const rows = [];
6131
- for (const namespace of namespaces) {
6132
- const snapshot = await runtime.adminSnapshot(namespace);
6537
+ for (const { namespace, snapshot } of entries) {
6133
6538
  if (!snapshot) continue;
6134
6539
  const failedJobs = snapshot.extractionJobs.filter(({ status }) => status === "failed").length + snapshot.graphProjectionJobs.filter(({ status }) => status === "failed").length;
6135
- const processingJobs = snapshot.extractionJobs.filter(({ status }) => status === "running").length + snapshot.graphProjectionJobs.filter(({ status }) => status === "pending" || status === "running").length;
6540
+ 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(({ status }) => status === "pending" || status === "running").length;
6136
6541
  const failedJobDetails = [
6137
6542
  ...snapshot.extractionJobs.filter(({ status }) => status === "failed").map((job) => ({
6138
6543
  id: job.blockId,
@@ -6601,6 +7006,73 @@ async function audit(runtime, url) {
6601
7006
  items: receipts.slice(offset, offset + limit).map((receipt) => receiptSources(snapshot, receipt))
6602
7007
  };
6603
7008
  }
7009
+ function requestHeader(req, name2) {
7010
+ const headers = req.headers ?? {};
7011
+ const key = Object.keys(headers).find((candidate) => candidate.toLocaleLowerCase() === name2.toLocaleLowerCase());
7012
+ const value = key ? headers[key] : void 0;
7013
+ return Array.isArray(value) ? value.join(", ") : value ?? "";
7014
+ }
7015
+ async function dashboard(runtime, url, ifNoneMatch) {
7016
+ const entries = await runtime.adminSnapshotEntries();
7017
+ const requestedNamespace = url.searchParams.get("namespace")?.trim() ?? "";
7018
+ const selected = entries.find(({ namespace }) => namespace === requestedNamespace) ?? entries[0];
7019
+ const threadId = url.searchParams.get("threadId")?.trim() ?? "";
7020
+ const revisionKey = entries.map(({ namespace, revision }) => `${namespace}:${revision}`).join("|");
7021
+ const etag = `"${createHash2("sha256").update(`${revisionKey}\0${selected?.namespace ?? ""}\0${threadId}`).digest("base64url").slice(0, 24)}"`;
7022
+ if (ifNoneMatch.split(",").map((value) => value.trim()).includes(etag)) return { etag, notModified: true };
7023
+ const overviewValue = await overview(runtime, entries);
7024
+ if (!selected) {
7025
+ return { etag, notModified: false, body: { namespace: null, overview: overviewValue, data: null, processing: false } };
7026
+ }
7027
+ const snapshotRuntime = {
7028
+ adminSnapshot: async (namespace) => namespace === selected.namespace ? selected.snapshot : null
7029
+ };
7030
+ const memoryUrl = (kind, limit) => {
7031
+ const target = new URL(url);
7032
+ target.searchParams.set("namespace", selected.namespace);
7033
+ target.searchParams.set("kind", kind);
7034
+ if (limit) target.searchParams.set("limit", limit);
7035
+ return target;
7036
+ };
7037
+ const [eventResult, graphResult, blockResult, auditResult] = await Promise.all([
7038
+ memories(snapshotRuntime, memoryUrl("events", "200")),
7039
+ memories(snapshotRuntime, memoryUrl("graph")),
7040
+ memories(snapshotRuntime, memoryUrl("blocks", "200")),
7041
+ audit(snapshotRuntime, memoryUrl("audit", "100"))
7042
+ ]);
7043
+ const selectedOverview = overviewValue.namespaces?.find(({ namespace }) => namespace === selected.namespace);
7044
+ return {
7045
+ etag,
7046
+ notModified: false,
7047
+ body: {
7048
+ namespace: selected.namespace,
7049
+ revision: selected.revision,
7050
+ overview: overviewValue,
7051
+ processing: Number(selectedOverview?.processingJobs ?? 0) > 0,
7052
+ data: {
7053
+ events: eventResult.items ?? [],
7054
+ graph: graphResult,
7055
+ blocks: blockResult.items ?? [],
7056
+ openBlock: blockResult.openBlock ?? null,
7057
+ conversations: blockResult.conversations ?? [],
7058
+ activeThreadId: blockResult.activeThreadId ?? null,
7059
+ audit: auditResult.items ?? []
7060
+ }
7061
+ }
7062
+ };
7063
+ }
7064
+ function sendDashboard(res, result) {
7065
+ res.setHeader("ETag", result.etag);
7066
+ res.setHeader("Cache-Control", "private, no-cache");
7067
+ if (result.notModified) {
7068
+ res.statusCode = 304;
7069
+ res.end("");
7070
+ return;
7071
+ }
7072
+ res.statusCode = 200;
7073
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
7074
+ res.end(JSON.stringify(redactValue(result.body)));
7075
+ }
6604
7076
  async function handleAdminRequest(runtime, req, res) {
6605
7077
  try {
6606
7078
  const url = new URL(req.url ?? "/", "http://localhost");
@@ -6616,6 +7088,7 @@ async function handleAdminRequest(runtime, req, res) {
6616
7088
  else if (req.method === "POST") sendJson(res, 200, await importExternalMemory(runtime, req));
6617
7089
  else throw new AdminHttpError(405, "External memory import requires GET or POST");
6618
7090
  } else if (req.method !== "GET") throw new AdminHttpError(405, "StrataGate memory data is read-only");
7091
+ else if (path === "/api/stratagate/dashboard") sendDashboard(res, await dashboard(runtime, url, requestHeader(req, "if-none-match")));
6619
7092
  else if (path === "/api/stratagate/overview") sendJson(res, 200, await overview(runtime));
6620
7093
  else if (path === "/api/stratagate/memories") sendJson(res, 200, await memories(runtime, url));
6621
7094
  else if (path === "/api/stratagate/sources") sendJson(res, 200, await sources(runtime, url));