stratagate-dsh 0.2.35 → 0.2.37

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");
@@ -2512,7 +2609,7 @@ var SqliteStorage = class {
2512
2609
  }
2513
2610
  };
2514
2611
 
2515
- // ../../src/store.ts
2612
+ // packages/core/src/store.ts
2516
2613
  function defaultIdFactory(prefix) {
2517
2614
  return `${prefix}_${crypto.randomUUID()}`;
2518
2615
  }
@@ -2522,21 +2619,13 @@ function defaultElementIdFactory(prefix) {
2522
2619
  function defaultGraphIdFactory(prefix) {
2523
2620
  return `${prefix}_${crypto.randomUUID()}`;
2524
2621
  }
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
2622
  function renderBlock(block, level) {
2623
+ if (block.processingStatus !== "ready" || !block.l0Title || !block.l0Tags || !block.l1Summary || !block.l2Keypoints) {
2624
+ throw new Error(`Block ${block.id} is not ready for rendering`);
2625
+ }
2537
2626
  if (level === 0) return `${block.l0Title}
2538
2627
  Tags: ${block.l0Tags.join(", ") || "none"}`;
2539
- if (level === 1) return block.l1Summary || block.l0Title;
2628
+ if (level === 1) return block.l1Summary;
2540
2629
  if (level === 2) return block.l2Keypoints.map((point) => `- ${point}`).join("\n") || block.l1Summary;
2541
2630
  if (level === 3) return block.l3Condensed;
2542
2631
  if (level === 4) return block.l4Readable;
@@ -2553,8 +2642,10 @@ function errorMessage(error) {
2553
2642
  return error instanceof Error ? error.message : String(error);
2554
2643
  }
2555
2644
  var STRATAGATE_CONSTRUCTOR_TOKEN = /* @__PURE__ */ Symbol("StrataGate constructor");
2645
+ var DERIVATION_MAX_ATTEMPTS = 3;
2646
+ var DERIVATION_BACKOFF_MS = 1e3;
2556
2647
  var StrataGate = class _StrataGate {
2557
- blockTurnSize;
2648
+ blockTurnSizeValue;
2558
2649
  blockDecayLambdaValue;
2559
2650
  summarizer;
2560
2651
  extractor;
@@ -2572,6 +2663,7 @@ var StrataGate = class _StrataGate {
2572
2663
  graphNodes = [];
2573
2664
  graphEdges = [];
2574
2665
  extractionJobs = /* @__PURE__ */ new Map();
2666
+ summaryJobs = /* @__PURE__ */ new Map();
2575
2667
  elementProjectionJobs = /* @__PURE__ */ new Map();
2576
2668
  graphProjectionJobs = /* @__PURE__ */ new Map();
2577
2669
  usageReceipts = /* @__PURE__ */ new Map();
@@ -2586,7 +2678,7 @@ var StrataGate = class _StrataGate {
2586
2678
  if (token !== STRATAGATE_CONSTRUCTOR_TOKEN) {
2587
2679
  throw new TypeError("Use StrataGate.open() for SQLite or StrataGate.inMemory() for explicit ephemeral storage");
2588
2680
  }
2589
- this.blockTurnSize = Math.max(1, Math.floor(options.blockTurnSize ?? DEFAULT_BLOCK_TURN_SIZE));
2681
+ this.blockTurnSizeValue = Math.max(1, Math.floor(options.blockTurnSize ?? DEFAULT_BLOCK_TURN_SIZE));
2590
2682
  const blockDecayLambda = options.blockDecayLambda ?? BLOCK_DECAY_LAMBDA;
2591
2683
  if (!Number.isFinite(blockDecayLambda) || blockDecayLambda < 0) {
2592
2684
  throw new TypeError("blockDecayLambda must be a non-negative finite number");
@@ -2680,6 +2772,21 @@ var StrataGate = class _StrataGate {
2680
2772
  if (loaded && loadedSnapshot) {
2681
2773
  memory.restoreSnapshot(loadedSnapshot);
2682
2774
  memory.revision = loadedRevision;
2775
+ const interruptedSummaries = [...memory.summaryJobs.values()].filter((job) => job.status === "running");
2776
+ if (interruptedSummaries.length > 0) {
2777
+ await memory.commitMutation(() => {
2778
+ const now = toUtc8Iso(memory.now());
2779
+ for (const job of interruptedSummaries) {
2780
+ memory.summaryJobs.set(job.blockId, {
2781
+ ...job,
2782
+ status: "failed",
2783
+ lastError: "Block summarization was interrupted before completion.",
2784
+ nextRetryAt: now,
2785
+ updatedAt: now
2786
+ });
2787
+ }
2788
+ });
2789
+ }
2683
2790
  const interrupted = [...memory.extractionJobs.values()].filter((job) => job.status === "running");
2684
2791
  if (interrupted.length > 0) {
2685
2792
  await memory.commitMutation(() => {
@@ -2689,6 +2796,7 @@ var StrataGate = class _StrataGate {
2689
2796
  ...job,
2690
2797
  status: "failed",
2691
2798
  lastError: "Extraction was interrupted before completion.",
2799
+ nextRetryAt: now,
2692
2800
  updatedAt: now
2693
2801
  });
2694
2802
  }
@@ -2734,6 +2842,18 @@ var StrataGate = class _StrataGate {
2734
2842
  get storageRevision() {
2735
2843
  return this.revision;
2736
2844
  }
2845
+ get blockTurnSize() {
2846
+ return this.blockTurnSizeValue;
2847
+ }
2848
+ async setBlockTurnSize(value) {
2849
+ if (!Number.isSafeInteger(value) || value < 1) {
2850
+ throw new TypeError("blockTurnSize must be a positive integer");
2851
+ }
2852
+ if (value === this.blockTurnSizeValue) return;
2853
+ await this.commitMutation(() => {
2854
+ this.blockTurnSizeValue = value;
2855
+ });
2856
+ }
2737
2857
  get blockDecayLambda() {
2738
2858
  return this.blockDecayLambdaValue;
2739
2859
  }
@@ -2768,6 +2888,9 @@ var StrataGate = class _StrataGate {
2768
2888
  listExtractionJobs() {
2769
2889
  return [...this.extractionJobs.values()];
2770
2890
  }
2891
+ listSummaryJobs() {
2892
+ return [...this.summaryJobs.values()];
2893
+ }
2771
2894
  listElementProjectionJobs() {
2772
2895
  return [...this.elementProjectionJobs.values()];
2773
2896
  }
@@ -2800,6 +2923,7 @@ var StrataGate = class _StrataGate {
2800
2923
  blockDecayLambda: this.blockDecayLambda,
2801
2924
  openTail: this.openTail,
2802
2925
  blocks: this.blocks,
2926
+ summaryJobs: [...this.summaryJobs.values()],
2803
2927
  events: this.events,
2804
2928
  graphNodes: this.graphNodes,
2805
2929
  graphEdges: this.graphEdges,
@@ -2848,57 +2972,55 @@ var StrataGate = class _StrataGate {
2848
2972
  if (receiptId) this.ingestionReceipts.set(receiptId, { id: receiptId, createdAt });
2849
2973
  return true;
2850
2974
  });
2851
- if (!appended) return { sealedBlock: null, extractedEvents: [], projectedElements: [] };
2975
+ if (!appended) return { sealedBlock: null, readyBlocks: [], extractedEvents: [], projectedElements: [] };
2852
2976
  if (options.deferProcessing === true) {
2853
- return { sealedBlock: null, extractedEvents: [], projectedElements: [] };
2977
+ return { sealedBlock: null, readyBlocks: [], extractedEvents: [], projectedElements: [] };
2854
2978
  }
2855
2979
  if (this.threadOpenTail(threadId).filter((message) => message.role === "user").length < this.blockTurnSize) {
2856
2980
  const projectedElements2 = await this.projectEligibleElements() ?? [];
2857
2981
  await this.projectEligibleGraph();
2858
- return { sealedBlock: null, extractedEvents: [], projectedElements: projectedElements2 };
2982
+ return { sealedBlock: null, readyBlocks: [], extractedEvents: [], projectedElements: projectedElements2 };
2859
2983
  }
2860
2984
  const sealedBlock = await this.sealOpenTail(threadId);
2861
- const extractedEvents = await this.extractEligibleBlock() ?? [];
2985
+ if (options.deferDerivation === true) {
2986
+ return { sealedBlock, readyBlocks: [], extractedEvents: [], projectedElements: [] };
2987
+ }
2988
+ const beforeReady = sealedBlock.processingStatus === "ready";
2989
+ const extractedEvents = await this.processBlock(sealedBlock, { retryFailed: false });
2990
+ const readyBlocks = !beforeReady && sealedBlock.processingStatus === "ready" ? [sealedBlock] : [];
2862
2991
  const projectedElements = await this.projectEligibleElements() ?? [];
2863
2992
  await this.projectEligibleGraph();
2864
- return { sealedBlock, extractedEvents, projectedElements };
2993
+ return { sealedBlock, readyBlocks, extractedEvents, projectedElements };
2865
2994
  }
2866
2995
  async resumePendingWork(options = {}) {
2867
2996
  const sealedBlocks = [];
2997
+ const readyBlocks = [];
2868
2998
  const extractedEvents = [];
2869
2999
  const projectedElements = [];
2870
3000
  while (true) {
2871
3001
  const sealable = this.nextSealableThread();
2872
3002
  if (sealable === null) break;
2873
3003
  sealedBlocks.push(await this.sealOpenTail(sealable.threadId));
2874
- extractedEvents.push(...await this.extractEligibleBlock() ?? []);
2875
- projectedElements.push(...await this.projectEligibleElements() ?? []);
2876
- await this.projectEligibleGraph();
2877
3004
  }
2878
- while (true) {
2879
- const extracted = await this.extractEligibleBlock();
2880
- if (extracted === null) break;
3005
+ if (options.deferDerivation === true) {
3006
+ return { sealedBlocks, readyBlocks, extractedEvents, projectedElements };
3007
+ }
3008
+ for (const block of this.blocks) {
3009
+ if (options.threadId !== void 0 && block.threadId !== options.threadId) continue;
3010
+ if (block.processingStatus === "ready") continue;
3011
+ const extracted = await this.processBlock(block, { retryFailed: options.retryFailed === true });
2881
3012
  extractedEvents.push(...extracted);
3013
+ if (this.blocks.find((candidate) => candidate.id === block.id)?.processingStatus === "ready") readyBlocks.push(block);
2882
3014
  projectedElements.push(...await this.projectEligibleElements() ?? []);
2883
3015
  await this.projectEligibleGraph();
2884
3016
  }
2885
- if (options.retrySkipped === true) {
2886
- const skippedBlockIds = this.blocks.filter((block) => this.nextBlockInThread(block) !== null && block.shouldExtract && this.extractionJobs.get(block.id)?.status === "skipped").map((block) => block.id);
2887
- for (const blockId of skippedBlockIds) {
2888
- const extracted = await this.extractEligibleBlock({ blockId, includeSkipped: true });
2889
- if (extracted === null) continue;
2890
- extractedEvents.push(...extracted);
2891
- projectedElements.push(...await this.projectEligibleElements() ?? []);
2892
- await this.projectEligibleGraph();
2893
- }
2894
- }
2895
3017
  while (true) {
2896
3018
  const projected = await this.projectEligibleElements();
2897
3019
  if (projected === null) break;
2898
3020
  projectedElements.push(...projected);
2899
3021
  }
2900
3022
  await this.projectEligibleGraph();
2901
- return { sealedBlocks, extractedEvents, projectedElements };
3023
+ return { sealedBlocks, readyBlocks, extractedEvents, projectedElements };
2902
3024
  }
2903
3025
  async addEvent(input) {
2904
3026
  return this.commitMutation(() => {
@@ -3260,9 +3382,9 @@ var StrataGate = class _StrataGate {
3260
3382
  * StrataGate namespace (never another namespace).
3261
3383
  */
3262
3384
  getBlockContext(threadId) {
3263
- const blocks = threadId === void 0 ? this.blocks : this.blocks.filter((block) => block.threadId === threadId);
3385
+ const blocks = threadId === void 0 ? this.blocks.filter((block) => block.processingStatus === "ready") : this.blocks.filter((block) => block.threadId === threadId && block.processingStatus === "ready");
3264
3386
  return blocks.map((block) => {
3265
- const threadBlocks = this.threadBlocks(block.threadId);
3387
+ const threadBlocks = this.threadBlocks(block.threadId).filter((candidate) => candidate.processingStatus === "ready");
3266
3388
  const latestBlockPosition = threadBlocks.length;
3267
3389
  const blockPosition = threadBlocks.indexOf(block) + 1;
3268
3390
  const age = Math.max(0, latestBlockPosition - blockPosition);
@@ -3288,8 +3410,10 @@ var StrataGate = class _StrataGate {
3288
3410
  return this.commitMutation(() => {
3289
3411
  const block = this.blocks.find((candidate) => candidate.id === id);
3290
3412
  if (!block) throw new Error(`Unknown block: ${id}`);
3291
- const latestBlockPosition = this.threadBlocks(block.threadId).length;
3292
- const blockPosition = this.threadBlocks(block.threadId).indexOf(block) + 1;
3413
+ if (block.processingStatus !== "ready") throw new Error(`Block ${id} is not ready for decay or expansion`);
3414
+ const readyBlocks = this.threadBlocks(block.threadId).filter((candidate) => candidate.processingStatus === "ready");
3415
+ const latestBlockPosition = readyBlocks.length;
3416
+ const blockPosition = readyBlocks.indexOf(block) + 1;
3293
3417
  const current = getDecayedBlockLevel(
3294
3418
  block.pointerAnchorLevel,
3295
3419
  block.pointerAnchorBlockPosition,
@@ -3408,6 +3532,7 @@ var StrataGate = class _StrataGate {
3408
3532
  l1Summary: text3.replace(/\s+/gu, " ").trim().slice(0, 500),
3409
3533
  l2Keypoints: text3.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean).slice(0, 8),
3410
3534
  shouldExtract: false,
3535
+ processingStatus: "ready",
3411
3536
  ...deterministicBlockLayers([message]),
3412
3537
  pointerCurrentLevel: 5,
3413
3538
  pointerAnchorLevel: 5,
@@ -3624,7 +3749,6 @@ var StrataGate = class _StrataGate {
3624
3749
  if (raw.filter((message) => message.role === "user").length < this.blockTurnSize) {
3625
3750
  throw new Error("Open tail does not contain enough turns to seal a block");
3626
3751
  }
3627
- const generated = this.summarizer ? await this.summarizer(raw) : defaultSummary(raw);
3628
3752
  const deterministic = deterministicBlockLayers(raw);
3629
3753
  const sequence = this.blocks.length + 1;
3630
3754
  const threadBlocks = this.threadBlocks(threadId);
@@ -3644,11 +3768,7 @@ var StrataGate = class _StrataGate {
3644
3768
  startTurn,
3645
3769
  endTurn,
3646
3770
  createdAt: raw.at(-1)?.createdAt ?? toUtc8Iso(this.now()),
3647
- l0Title: generated.l0Title,
3648
- l0Tags: generated.l0Tags,
3649
- l1Summary: generated.l1Summary,
3650
- l2Keypoints: generated.l2Keypoints,
3651
- shouldExtract: generated.shouldExtract,
3771
+ processingStatus: "pending",
3652
3772
  ...deterministic,
3653
3773
  pointerCurrentLevel: 5,
3654
3774
  pointerAnchorLevel: 5,
@@ -3660,27 +3780,132 @@ var StrataGate = class _StrataGate {
3660
3780
  const remaining = this.openTail.filter((message) => !sealedIds.has(message.id));
3661
3781
  this.openTail.splice(0, this.openTail.length, ...remaining);
3662
3782
  this.blocks.push(block);
3783
+ const updatedAt = toUtc8Iso(this.now());
3784
+ this.summaryJobs.set(block.id, {
3785
+ blockId: block.id,
3786
+ status: "pending",
3787
+ attempts: 0,
3788
+ lastError: null,
3789
+ nextRetryAt: null,
3790
+ updatedAt
3791
+ });
3663
3792
  return block;
3664
3793
  });
3665
3794
  }
3795
+ async processBlock(block, options) {
3796
+ if (block.processingStatus === "ready") return [];
3797
+ const summary = this.summaryJobs.get(block.id);
3798
+ if (!summary || summary.status !== "succeeded") {
3799
+ if (!this.summarizer || !this.jobCanRun(summary, options.retryFailed)) return [];
3800
+ const claimed = await this.commitMutation(() => {
3801
+ const current = this.summaryJobs.get(block.id);
3802
+ if (!current || !this.jobCanRun(current, options.retryFailed)) return false;
3803
+ this.summaryJobs.set(block.id, {
3804
+ ...current,
3805
+ status: "running",
3806
+ attempts: current.attempts + 1,
3807
+ lastError: null,
3808
+ nextRetryAt: null,
3809
+ updatedAt: toUtc8Iso(this.now())
3810
+ });
3811
+ return true;
3812
+ });
3813
+ if (!claimed) return [];
3814
+ try {
3815
+ const generated = await this.summarizer(block.l5Raw);
3816
+ if (!generated.l0Title.trim() || !generated.l1Summary.trim() || !Array.isArray(generated.l0Tags) || !Array.isArray(generated.l2Keypoints) || typeof generated.shouldExtract !== "boolean") {
3817
+ throw new Error("Block summarizer returned invalid L0-L2 layers");
3818
+ }
3819
+ await this.commitMutation(() => {
3820
+ block.l0Title = generated.l0Title;
3821
+ block.l0Tags = [...generated.l0Tags];
3822
+ block.l1Summary = generated.l1Summary;
3823
+ block.l2Keypoints = [...generated.l2Keypoints];
3824
+ block.shouldExtract = generated.shouldExtract;
3825
+ const current = this.summaryJobs.get(block.id);
3826
+ if (!current) throw new Error(`Missing summary job for block: ${block.id}`);
3827
+ this.summaryJobs.set(block.id, {
3828
+ ...current,
3829
+ status: "succeeded",
3830
+ lastError: null,
3831
+ nextRetryAt: null,
3832
+ updatedAt: toUtc8Iso(this.now())
3833
+ });
3834
+ });
3835
+ } catch (error) {
3836
+ await this.failSummary(block.id, error);
3837
+ return [];
3838
+ }
3839
+ }
3840
+ if (block.shouldExtract === false) {
3841
+ await this.commitMutation(() => {
3842
+ const now = toUtc8Iso(this.now());
3843
+ this.extractionJobs.set(block.id, {
3844
+ blockId: block.id,
3845
+ status: "skipped",
3846
+ attempts: this.extractionJobs.get(block.id)?.attempts ?? 0,
3847
+ lastError: null,
3848
+ nextRetryAt: null,
3849
+ updatedAt: now
3850
+ });
3851
+ this.markBlockReady(block);
3852
+ });
3853
+ return [];
3854
+ }
3855
+ try {
3856
+ const extracted = await this.extractEligibleBlock({ blockId: block.id, retryFailed: options.retryFailed });
3857
+ return extracted ?? [];
3858
+ } catch {
3859
+ return [];
3860
+ }
3861
+ }
3862
+ jobCanRun(job, force) {
3863
+ if (!job || job.attempts >= DERIVATION_MAX_ATTEMPTS || job.status === "running" || job.status === "succeeded") return false;
3864
+ return force || job.nextRetryAt === null || Date.parse(job.nextRetryAt) <= this.now().getTime();
3865
+ }
3866
+ async failSummary(blockId, error) {
3867
+ await this.commitMutation(() => {
3868
+ const job = this.summaryJobs.get(blockId);
3869
+ if (!job) return;
3870
+ this.summaryJobs.set(blockId, {
3871
+ ...job,
3872
+ status: "failed",
3873
+ lastError: errorMessage(error),
3874
+ nextRetryAt: this.retryAt(job.attempts),
3875
+ updatedAt: toUtc8Iso(this.now())
3876
+ });
3877
+ });
3878
+ }
3879
+ retryAt(attempts) {
3880
+ if (attempts >= DERIVATION_MAX_ATTEMPTS) return null;
3881
+ return toUtc8Iso(new Date(this.now().getTime() + DERIVATION_BACKOFF_MS * 2 ** Math.max(0, attempts - 1)));
3882
+ }
3883
+ markBlockReady(block) {
3884
+ if (!block.l0Title || !block.l0Tags || !block.l1Summary || !block.l2Keypoints || typeof block.shouldExtract !== "boolean") {
3885
+ throw new Error(`Block ${block.id} cannot become ready without validated L0-L2 layers`);
3886
+ }
3887
+ const ready = this.threadBlocks(block.threadId).filter((candidate) => candidate.processingStatus === "ready");
3888
+ block.processingStatus = "ready";
3889
+ block.pointerCurrentLevel = 5;
3890
+ block.pointerAnchorLevel = 5;
3891
+ block.pointerAnchorBlockPosition = ready.length + 1;
3892
+ }
3666
3893
  async extractEligibleBlock(options = {}) {
3667
- if (!this.extractor || this.blocks.length < 2) return null;
3894
+ if (!this.extractor) return null;
3668
3895
  const target = this.blocks.find((block) => {
3669
- if (this.nextBlockInThread(block) === null || !block.shouldExtract) return false;
3896
+ if (block.processingStatus === "ready" || block.shouldExtract !== true) return false;
3670
3897
  if (options.blockId !== void 0 && block.id !== options.blockId) return false;
3671
- const status = this.extractionJobs.get(block.id)?.status;
3672
- return status === void 0 || status === "failed" || options.includeSkipped === true && status === "skipped";
3898
+ const job = this.extractionJobs.get(block.id);
3899
+ return job === void 0 || this.jobCanRun(job, options.retryFailed === true);
3673
3900
  });
3674
3901
  if (!target) return null;
3675
3902
  const threadBlocks = this.threadBlocks(target.threadId);
3676
3903
  const targetIndex = threadBlocks.indexOf(target);
3677
- const next = threadBlocks[targetIndex + 1];
3678
- if (!next) return null;
3904
+ const next = threadBlocks.slice(targetIndex + 1).find((block) => block.l2Keypoints !== void 0) ?? null;
3679
3905
  const existing = this.extractionJobs.get(target.id);
3680
3906
  await this.commitMutation(() => {
3681
3907
  const currentStatus = this.extractionJobs.get(target.id)?.status;
3682
- const canRetrySkipped = options.includeSkipped === true && currentStatus === "skipped";
3683
- if (currentStatus !== void 0 && currentStatus !== "failed" && !canRetrySkipped) {
3908
+ if (currentStatus !== void 0 && currentStatus !== "failed") {
3684
3909
  throw new Error(`Extraction block ${target.id} is already ${currentStatus}`);
3685
3910
  }
3686
3911
  this.extractionJobs.set(target.id, {
@@ -3688,13 +3913,14 @@ var StrataGate = class _StrataGate {
3688
3913
  status: "running",
3689
3914
  attempts: (existing?.attempts ?? 0) + 1,
3690
3915
  lastError: null,
3916
+ nextRetryAt: null,
3691
3917
  updatedAt: toUtc8Iso(this.now())
3692
3918
  });
3693
3919
  });
3694
3920
  let result;
3695
3921
  try {
3696
3922
  result = await this.extractor({
3697
- previous: threadBlocks[targetIndex - 1] ?? null,
3923
+ previous: threadBlocks.slice(0, targetIndex).reverse().find((block) => block.l2Keypoints !== void 0) ?? null,
3698
3924
  target,
3699
3925
  next,
3700
3926
  timeline: this.events.map((event) => ({ id: event.id, title: event.title, temporal: event.temporal }))
@@ -3707,6 +3933,7 @@ var StrataGate = class _StrataGate {
3707
3933
  ...job,
3708
3934
  status: "failed",
3709
3935
  lastError: errorMessage(error),
3936
+ nextRetryAt: this.retryAt(job.attempts),
3710
3937
  updatedAt: toUtc8Iso(this.now())
3711
3938
  });
3712
3939
  });
@@ -3721,6 +3948,7 @@ var StrataGate = class _StrataGate {
3721
3948
  ...job,
3722
3949
  status: "failed",
3723
3950
  lastError: reason,
3951
+ nextRetryAt: this.retryAt(job.attempts),
3724
3952
  updatedAt: toUtc8Iso(this.now())
3725
3953
  });
3726
3954
  });
@@ -3739,8 +3967,10 @@ var StrataGate = class _StrataGate {
3739
3967
  ...job,
3740
3968
  status: result.shouldExtract ? "succeeded" : "skipped",
3741
3969
  lastError: null,
3970
+ nextRetryAt: null,
3742
3971
  updatedAt: toUtc8Iso(this.now())
3743
3972
  });
3973
+ this.markBlockReady(target);
3744
3974
  return extracted;
3745
3975
  });
3746
3976
  }
@@ -3809,6 +4039,8 @@ var StrataGate = class _StrataGate {
3809
4039
  this.currentTurn = copy.currentTurn;
3810
4040
  this.openTail.splice(0, this.openTail.length, ...copy.openTail);
3811
4041
  this.blocks.splice(0, this.blocks.length, ...copy.blocks);
4042
+ this.summaryJobs.clear();
4043
+ for (const job of copy.summaryJobs) this.summaryJobs.set(job.blockId, job);
3812
4044
  this.events.splice(0, this.events.length, ...copy.events);
3813
4045
  this.graphNodes.splice(0, this.graphNodes.length, ...copy.graphNodes);
3814
4046
  this.graphEdges.splice(0, this.graphEdges.length, ...copy.graphEdges);
@@ -3855,6 +4087,9 @@ var StrataGate = class _StrataGate {
3855
4087
  for (const job of this.extractionJobs.values()) {
3856
4088
  if (!blockIds.has(job.blockId)) throw new Error(`Unknown extraction job block in snapshot: ${job.blockId}`);
3857
4089
  }
4090
+ for (const job of this.summaryJobs.values()) {
4091
+ if (!blockIds.has(job.blockId)) throw new Error(`Unknown summary job block in snapshot: ${job.blockId}`);
4092
+ }
3858
4093
  const elementIds = /* @__PURE__ */ new Set();
3859
4094
  for (const element of this.elements) {
3860
4095
  if (elementIds.has(element.id)) throw new Error(`Duplicate element ID in snapshot: ${element.id}`);
@@ -4083,7 +4318,7 @@ function extractorPayload(context) {
4083
4318
  }
4084
4319
  var JSON_RESPONSE_ATTEMPTS = 2;
4085
4320
  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.";
4086
- var RETRY_MAX_TOKENS = 1e4;
4321
+ var DEFAULT_STRUCTURED_TIMEOUT_MS = 45e3;
4087
4322
  var STRUCTURED_FIELDS = {
4088
4323
  summarizer: ["l0Title", "l0Tags", "l1Summary", "l2Keypoints", "shouldExtract"],
4089
4324
  extractor: ["shouldExtract", "reason", "events"],
@@ -4234,11 +4469,15 @@ var DshModelBridge = class {
4234
4469
  constructor(ctx, config) {
4235
4470
  this.ctx = ctx;
4236
4471
  this.config = config;
4472
+ this.ctx.on?.("llm/adapters-updated", () => {
4473
+ this.offCapabilities.clear();
4474
+ });
4237
4475
  }
4238
4476
  ctx;
4239
4477
  config;
4240
4478
  sessions = new AsyncLocalStorage();
4241
4479
  successfulResponses = [];
4480
+ offCapabilities = /* @__PURE__ */ new Map();
4242
4481
  run(session, operation) {
4243
4482
  return this.sessions.run(session, operation);
4244
4483
  }
@@ -4253,7 +4492,7 @@ var DshModelBridge = class {
4253
4492
  { messages }
4254
4493
  ));
4255
4494
  return {
4256
- l0Title: text2(raw.l0Title, "Conversation block").slice(0, 120),
4495
+ l0Title: text2(raw.l0Title).slice(0, 120),
4257
4496
  l0Tags: strings2(raw.l0Tags).slice(0, 12),
4258
4497
  l1Summary: text2(raw.l1Summary).slice(0, 2e3),
4259
4498
  l2Keypoints: strings2(raw.l2Keypoints).slice(0, 20),
@@ -4385,10 +4624,11 @@ var DshModelBridge = class {
4385
4624
  async callStructured(kind, system, payload) {
4386
4625
  const session = this.sessions.getStore();
4387
4626
  if (!session) throw new Error("StrataGate model callback ran without a DSH session");
4388
- const route = this.resolveRoute(session, true);
4627
+ const baseRoute = this.resolveRoute(session);
4628
+ const routeKey = `${baseRoute.provider}\0${baseRoute.model}`;
4629
+ let useOff = await this.shouldUseOff(baseRoute);
4389
4630
  let lastError;
4390
4631
  let lastResponse = "";
4391
- let retryMaxTokens = this.config.maxOutputTokens;
4392
4632
  for (let attempt = 1; attempt <= JSON_RESPONSE_ATTEMPTS; attempt += 1) {
4393
4633
  const message = createUserMessage({
4394
4634
  content: [{ type: "text", text: JSON.stringify(payload) }],
@@ -4396,7 +4636,8 @@ var DshModelBridge = class {
4396
4636
  });
4397
4637
  const assembler = new BlockAssembler();
4398
4638
  const request = {
4399
- ...route,
4639
+ ...baseRoute,
4640
+ ...useOff ? { reasoningEffort: "off" } : {},
4400
4641
  messages: [message],
4401
4642
  system: attempt === 1 ? system : `${system}
4402
4643
 
@@ -4410,15 +4651,35 @@ ${JSON_RETRY_INSTRUCTION}`,
4410
4651
  type: "function",
4411
4652
  function: { name: STRUCTURED_TOOLS[kind].name }
4412
4653
  },
4413
- maxTokens: retryMaxTokens,
4654
+ maxTokens: this.config.maxOutputTokens,
4414
4655
  sessionId: session.id,
4415
4656
  purpose: "compaction"
4416
4657
  };
4417
- for await (const chunk of this.ctx.llm.stream(request)) assembler.push(chunk);
4658
+ try {
4659
+ await this.consumeStructuredStream(request, assembler);
4660
+ } catch (error) {
4661
+ if (useOff && isOffRejection(error)) {
4662
+ this.offCapabilities.set(routeKey, "unsupported");
4663
+ useOff = false;
4664
+ this.ctx.logger.warn(`stratagate-memory ${baseRoute.provider}/${baseRoute.model} rejected reasoningEffort=off; retrying once without it`);
4665
+ attempt -= 1;
4666
+ continue;
4667
+ }
4668
+ throw error;
4669
+ }
4418
4670
  const finish = assembler.finish;
4419
4671
  if (finish.kind === "error" || finish.kind === "aborted") {
4420
- throw new Error(`StrataGate model call failed: ${finish.failure.message}`);
4672
+ const failure = new Error(`StrataGate model call failed [${finish.failure.code}]: ${finish.failure.message}`);
4673
+ if (useOff && isOffRejection(finish.failure)) {
4674
+ this.offCapabilities.set(routeKey, "unsupported");
4675
+ useOff = false;
4676
+ this.ctx.logger.warn(`stratagate-memory ${baseRoute.provider}/${baseRoute.model} rejected reasoningEffort=off; retrying once without it`);
4677
+ attempt -= 1;
4678
+ continue;
4679
+ }
4680
+ throw failure;
4421
4681
  }
4682
+ if (useOff) this.offCapabilities.set(routeKey, "supported");
4422
4683
  const blocks = assembler.blocks();
4423
4684
  const calls = blocks.filter((block) => block.type === "tool-call");
4424
4685
  const responseForError = `${renderBlocksForDiagnostics(blocks, finish.kind)}
@@ -4454,6 +4715,15 @@ ${JSON_RETRY_INSTRUCTION}`,
4454
4715
  { response: responseForError }
4455
4716
  );
4456
4717
  }
4718
+ if (kind === "summarizer") {
4719
+ const summary = object(parsed);
4720
+ if (!text2(summary.l0Title) || !text2(summary.l1Summary)) {
4721
+ throw new ModelJsonResponseError(
4722
+ `StrataGate ${expectedTool} arguments were invalid: l0Title and l1Summary must not be empty`,
4723
+ { response: responseForError }
4724
+ );
4725
+ }
4726
+ }
4457
4727
  this.successfulResponses.push({
4458
4728
  id: `model_response_${crypto.randomUUID()}`,
4459
4729
  kind,
@@ -4468,7 +4738,6 @@ ${JSON_RETRY_INSTRUCTION}`,
4468
4738
  `StrataGate ${STRUCTURED_TOOLS[kind].name} call was truncated before valid arguments`,
4469
4739
  { cause: error, response: responseForError }
4470
4740
  ) : error;
4471
- if (finish.kind === "max-tokens") retryMaxTokens = Math.max(this.config.maxOutputTokens, RETRY_MAX_TOKENS);
4472
4741
  if (attempt < JSON_RESPONSE_ATTEMPTS) {
4473
4742
  this.ctx.logger.warn(`stratagate-memory model returned an invalid structured tool call; retrying (${attempt}/${JSON_RESPONSE_ATTEMPTS})`);
4474
4743
  }
@@ -4479,25 +4748,77 @@ ${JSON_RETRY_INSTRUCTION}`,
4479
4748
  { cause: lastError, response: lastResponse }
4480
4749
  );
4481
4750
  }
4482
- resolveRoute(session, structured = false) {
4483
- const request = session.requestHeader()?.config;
4484
- const requestedReasoningEffort = request?.reasoningEffort;
4485
- const withReasoningEffort = (route) => ({
4486
- ...route,
4487
- ...structured ? { reasoningEffort: "off" } : requestedReasoningEffort !== void 0 ? { reasoningEffort: requestedReasoningEffort } : {}
4751
+ async shouldUseOff(route) {
4752
+ const key = `${route.provider}\0${route.model}`;
4753
+ const cached = this.offCapabilities.get(key);
4754
+ if (cached) return cached === "supported";
4755
+ if (typeof this.ctx.llm.resolveModelInfo !== "function") return true;
4756
+ const controller = new AbortController();
4757
+ const lookupTimeoutMs = Math.min(5e3, this.config.structuredTaskTimeoutMs ?? DEFAULT_STRUCTURED_TIMEOUT_MS);
4758
+ let timer;
4759
+ try {
4760
+ const info = await Promise.race([
4761
+ this.ctx.llm.resolveModelInfo(route.provider, route.model, controller.signal),
4762
+ new Promise((_resolve, reject) => {
4763
+ timer = setTimeout(() => {
4764
+ controller.abort();
4765
+ reject(new Error(`Model capability lookup timed out after ${lookupTimeoutMs}ms`));
4766
+ }, lookupTimeoutMs);
4767
+ timer.unref?.();
4768
+ })
4769
+ ]);
4770
+ if (!info.reasoning) return true;
4771
+ const supported = info.reasoning.efforts.some(({ id }) => String(id) === "off");
4772
+ this.offCapabilities.set(key, supported ? "supported" : "unsupported");
4773
+ return supported;
4774
+ } catch {
4775
+ return true;
4776
+ } finally {
4777
+ if (timer) clearTimeout(timer);
4778
+ }
4779
+ }
4780
+ async consumeStructuredStream(request, assembler) {
4781
+ const timeoutMs = this.config.structuredTaskTimeoutMs ?? DEFAULT_STRUCTURED_TIMEOUT_MS;
4782
+ const controller = new AbortController();
4783
+ const timedRequest = { ...request, signal: controller.signal };
4784
+ let timer;
4785
+ const timeout = new Promise((_resolve, reject) => {
4786
+ timer = setTimeout(() => {
4787
+ controller.abort(new Error(`StrataGate structured model task timed out after ${timeoutMs}ms`));
4788
+ reject(new Error(`StrataGate structured model task timed out after ${timeoutMs}ms`));
4789
+ }, timeoutMs);
4790
+ timer.unref?.();
4488
4791
  });
4792
+ try {
4793
+ await Promise.race([
4794
+ (async () => {
4795
+ for await (const chunk of this.ctx.llm.stream(timedRequest)) assembler.push(chunk);
4796
+ })(),
4797
+ timeout
4798
+ ]);
4799
+ } finally {
4800
+ if (timer) clearTimeout(timer);
4801
+ }
4802
+ }
4803
+ resolveRoute(session) {
4804
+ const request = session.requestHeader()?.config;
4489
4805
  if (this.config.provider && this.config.model) {
4490
- return withReasoningEffort({ provider: this.config.provider, model: this.config.model });
4806
+ return { provider: this.config.provider, model: this.config.model };
4491
4807
  }
4492
- if (request) return withReasoningEffort({ provider: request.provider, model: request.model });
4808
+ if (request) return { provider: request.provider, model: request.model };
4493
4809
  const fallback = this.ctx.agentDefaultModel.currentSelection();
4494
- return {
4495
- provider: fallback.provider,
4496
- model: fallback.model,
4497
- ...structured ? { reasoningEffort: "off" } : fallback.reasoningEffort !== void 0 ? { reasoningEffort: fallback.reasoningEffort } : {}
4498
- };
4810
+ return { provider: fallback.provider, model: fallback.model };
4499
4811
  }
4500
4812
  };
4813
+ function isOffRejection(error) {
4814
+ let detail;
4815
+ try {
4816
+ detail = typeof error === "string" ? error : error && typeof error === "object" ? JSON.stringify(error, Object.getOwnPropertyNames(error)) : String(error);
4817
+ } catch {
4818
+ detail = error instanceof Error ? error.message : String(error);
4819
+ }
4820
+ 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);
4821
+ }
4501
4822
 
4502
4823
  // src/runtime.ts
4503
4824
  import { createHash } from "node:crypto";
@@ -4643,6 +4964,17 @@ var DshMetadataStore = class {
4643
4964
  this.database = new DatabaseSync2(filename);
4644
4965
  this.database.exec(METADATA_SCHEMA);
4645
4966
  }
4967
+ blockTurnSize() {
4968
+ const row = this.database.prepare("SELECT value FROM stratagate_dsh_settings WHERE key = 'blockTurnSize'").get();
4969
+ const value = Number(row?.value);
4970
+ return Number.isSafeInteger(value) && value >= 1 ? value : null;
4971
+ }
4972
+ setBlockTurnSize(value) {
4973
+ if (!Number.isSafeInteger(value) || value < 1) {
4974
+ throw new TypeError("blockTurnSize must be a positive integer");
4975
+ }
4976
+ this.setSetting("blockTurnSize", value);
4977
+ }
4646
4978
  blockDecayLambda() {
4647
4979
  const row = this.database.prepare("SELECT value FROM stratagate_dsh_settings WHERE key = 'blockDecayLambda'").get();
4648
4980
  const value = Number(row?.value);
@@ -4652,11 +4984,14 @@ var DshMetadataStore = class {
4652
4984
  if (!Number.isFinite(value) || value < 0) {
4653
4985
  throw new TypeError("blockDecayLambda must be a non-negative finite number");
4654
4986
  }
4987
+ this.setSetting("blockDecayLambda", value);
4988
+ }
4989
+ setSetting(key, value) {
4655
4990
  this.database.prepare(`
4656
4991
  INSERT INTO stratagate_dsh_settings (key, value, updated_at)
4657
- VALUES ('blockDecayLambda', ?, ?)
4992
+ VALUES (?, ?, ?)
4658
4993
  ON CONFLICT (key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at
4659
- `).run(String(value), (/* @__PURE__ */ new Date()).toISOString());
4994
+ `).run(key, String(value), (/* @__PURE__ */ new Date()).toISOString());
4660
4995
  }
4661
4996
  workspaceName(namespace) {
4662
4997
  const row = this.database.prepare("SELECT display_name FROM stratagate_dsh_workspaces WHERE namespace = ?").get(namespace);
@@ -4697,6 +5032,7 @@ var StrataGateRuntime = class {
4697
5032
  this.models = models;
4698
5033
  this.onIngestError = onIngestError;
4699
5034
  this.flushNativeSession = flushNativeSession;
5035
+ this.blockTurnSize = config.blockTurnSize;
4700
5036
  this.blockDecayLambda = config.blockDecayLambda;
4701
5037
  }
4702
5038
  config;
@@ -4709,11 +5045,14 @@ var StrataGateRuntime = class {
4709
5045
  latestBatchIds = /* @__PURE__ */ new Map();
4710
5046
  workspaceNames = /* @__PURE__ */ new Map();
4711
5047
  migrationTimers = /* @__PURE__ */ new Map();
5048
+ derivationTimers = /* @__PURE__ */ new Map();
5049
+ derivationRuns = /* @__PURE__ */ new Map();
4712
5050
  ingestTail = Promise.resolve();
4713
5051
  settingsTail = Promise.resolve();
4714
5052
  batchSequence = 0;
4715
5053
  closed = false;
4716
5054
  ingestError;
5055
+ blockTurnSize;
4717
5056
  blockDecayLambda;
4718
5057
  acceptEvent(session, event) {
4719
5058
  if (this.closed) return;
@@ -4724,15 +5063,8 @@ var StrataGateRuntime = class {
4724
5063
  }).then(async () => {
4725
5064
  const memory = await this.space(session);
4726
5065
  try {
4727
- const result = await this.models.run(session, () => memory.appendTurn(turn));
4728
- if (result.sealedBlock) {
4729
- const contexts = memory.getBlockContext(String(session.id));
4730
- const sealedContext = contexts.find(({ id }) => id === result.sealedBlock.id);
4731
- if (!sealedContext) throw new Error(`Missing context for sealed StrataGate block ${result.sealedBlock.id}`);
4732
- this.replaceSealedSurface(session, result.sealedBlock, sealedContext, turn.dshTurn);
4733
- this.syncDecayedBlockSurface(session, contexts);
4734
- await this.flushNativeSession(session);
4735
- }
5066
+ const result = await memory.appendTurn(turn, { deferDerivation: true });
5067
+ if (result.sealedBlock) this.scheduleBlockDerivation(session, memory);
4736
5068
  } finally {
4737
5069
  await this.persistSuccessfulResponses(memory);
4738
5070
  }
@@ -5046,6 +5378,8 @@ var StrataGateRuntime = class {
5046
5378
  this.closed = true;
5047
5379
  for (const timer of this.migrationTimers.values()) clearTimeout(timer);
5048
5380
  this.migrationTimers.clear();
5381
+ for (const timer of this.derivationTimers.values()) clearTimeout(timer);
5382
+ this.derivationTimers.clear();
5049
5383
  let flushError;
5050
5384
  try {
5051
5385
  await this.flush();
@@ -5053,6 +5387,7 @@ var StrataGateRuntime = class {
5053
5387
  flushError = error;
5054
5388
  }
5055
5389
  const settled = await Promise.allSettled(this.spaces.values());
5390
+ await Promise.allSettled(this.derivationRuns.values());
5056
5391
  await Promise.all(settled.flatMap((result) => result.status === "fulfilled" ? [result.value.close()] : []));
5057
5392
  if (flushError !== void 0) throw flushError;
5058
5393
  }
@@ -5075,6 +5410,7 @@ var StrataGateRuntime = class {
5075
5410
  if (this.config.database === ":memory:" || !existsSync(this.config.database)) return;
5076
5411
  const metadata = new DshMetadataStore(this.config.database);
5077
5412
  try {
5413
+ this.blockTurnSize = metadata.blockTurnSize() ?? this.config.blockTurnSize;
5078
5414
  this.blockDecayLambda = metadata.blockDecayLambda() ?? this.config.blockDecayLambda;
5079
5415
  } finally {
5080
5416
  metadata.close();
@@ -5083,10 +5419,10 @@ var StrataGateRuntime = class {
5083
5419
  try {
5084
5420
  for (const namespace of storage.listNamespaces()) {
5085
5421
  const loaded = await storage.load(namespace);
5086
- if (!loaded || loaded.snapshot.blockTurnSize === this.config.blockTurnSize && loaded.snapshot.blockDecayLambda === this.blockDecayLambda) continue;
5422
+ if (!loaded || loaded.snapshot.blockTurnSize === this.blockTurnSize && loaded.snapshot.blockDecayLambda === this.blockDecayLambda) continue;
5087
5423
  await storage.save(namespace, {
5088
5424
  ...loaded.snapshot,
5089
- blockTurnSize: this.config.blockTurnSize,
5425
+ blockTurnSize: this.blockTurnSize,
5090
5426
  blockDecayLambda: this.blockDecayLambda
5091
5427
  }, loaded.revision);
5092
5428
  }
@@ -5122,7 +5458,7 @@ var StrataGateRuntime = class {
5122
5458
  memory = await StrataGate.open({
5123
5459
  database: this.config.database,
5124
5460
  namespace: key,
5125
- blockTurnSize: this.config.blockTurnSize,
5461
+ blockTurnSize: this.blockTurnSize,
5126
5462
  blockDecayLambda: this.blockDecayLambda,
5127
5463
  graphProjector: this.models.graphProjector,
5128
5464
  disableElementProjection: true
@@ -5167,6 +5503,18 @@ var StrataGateRuntime = class {
5167
5503
  await update;
5168
5504
  return value;
5169
5505
  }
5506
+ async adminSetBlockTurnSize(value) {
5507
+ if (!Number.isSafeInteger(value) || value < 1) {
5508
+ throw new TypeError("blockTurnSize must be a positive integer");
5509
+ }
5510
+ const update = this.settingsTail.catch(() => {
5511
+ }).then(() => this.applyBlockTurnSize(value));
5512
+ this.settingsTail = update.then(() => {
5513
+ }, () => {
5514
+ });
5515
+ await update;
5516
+ return value;
5517
+ }
5170
5518
  async adminExpandBlock(namespace, id, target) {
5171
5519
  const key = namespace.trim();
5172
5520
  if (!key) throw new TypeError("StrataGate admin namespace must not be empty");
@@ -5181,7 +5529,7 @@ var StrataGateRuntime = class {
5181
5529
  const memory = await StrataGate.open({
5182
5530
  database: this.config.database,
5183
5531
  namespace: key,
5184
- blockTurnSize: this.config.blockTurnSize,
5532
+ blockTurnSize: this.blockTurnSize,
5185
5533
  blockDecayLambda: this.blockDecayLambda,
5186
5534
  summarizer: this.models.summarizer,
5187
5535
  extractor: this.models.extractor,
@@ -5230,6 +5578,37 @@ var StrataGateRuntime = class {
5230
5578
  }
5231
5579
  }
5232
5580
  }
5581
+ async applyBlockTurnSize(value) {
5582
+ await this.flush();
5583
+ this.blockTurnSize = value;
5584
+ if (this.config.database !== ":memory:") {
5585
+ const metadata = new DshMetadataStore(this.config.database);
5586
+ try {
5587
+ metadata.setBlockTurnSize(value);
5588
+ } finally {
5589
+ metadata.close();
5590
+ }
5591
+ }
5592
+ const openNamespaces = /* @__PURE__ */ new Set();
5593
+ for (const [namespace, opening] of this.spaces) {
5594
+ const memory = await opening;
5595
+ await memory.setBlockTurnSize(value);
5596
+ openNamespaces.add(namespace);
5597
+ }
5598
+ if (this.config.database !== ":memory:" && existsSync(this.config.database)) {
5599
+ const storage = new SqliteStorage({ filename: this.config.database });
5600
+ try {
5601
+ for (const namespace of storage.listNamespaces()) {
5602
+ if (openNamespaces.has(namespace)) continue;
5603
+ const loaded = await storage.load(namespace);
5604
+ if (!loaded || loaded.snapshot.blockTurnSize === value) continue;
5605
+ await storage.save(namespace, { ...loaded.snapshot, blockTurnSize: value }, loaded.revision);
5606
+ }
5607
+ } finally {
5608
+ await storage.close();
5609
+ }
5610
+ }
5611
+ }
5233
5612
  space(session) {
5234
5613
  const namespace = this.namespaceFor(session);
5235
5614
  this.rememberWorkspace(namespace, session.header.cwd);
@@ -5238,7 +5617,7 @@ var StrataGateRuntime = class {
5238
5617
  opening = StrataGate.open({
5239
5618
  database: this.config.database,
5240
5619
  namespace,
5241
- blockTurnSize: this.config.blockTurnSize,
5620
+ blockTurnSize: this.blockTurnSize,
5242
5621
  blockDecayLambda: this.blockDecayLambda,
5243
5622
  summarizer: this.models.summarizer,
5244
5623
  extractor: this.models.extractor,
@@ -5247,23 +5626,14 @@ var StrataGateRuntime = class {
5247
5626
  }).then(async (memory) => {
5248
5627
  try {
5249
5628
  try {
5250
- const resumed = await this.models.run(session, () => memory.resumePendingWork({ retrySkipped: true }));
5629
+ await memory.resumePendingWork({ deferDerivation: true, threadId: String(session.id) });
5251
5630
  const contexts = memory.getBlockContext(String(session.id));
5252
- for (const block of resumed.sealedBlocks) {
5253
- if (block.threadId !== String(session.id)) continue;
5254
- const endTurn = dshTurnAtBlockEnd(session, block);
5255
- const context = contexts.find(({ id }) => id === block.id);
5256
- if (!context) throw new Error(`Missing context for recovered StrataGate block ${block.id}`);
5257
- this.replaceSealedSurface(session, block, context, endTurn);
5258
- }
5259
5631
  this.syncDecayedBlockSurface(session, contexts);
5260
- if (resumed.sealedBlocks.some((block) => block.threadId === String(session.id))) {
5261
- await this.flushNativeSession(session);
5262
- }
5263
5632
  } finally {
5264
5633
  await this.persistSuccessfulResponses(memory);
5265
5634
  }
5266
5635
  this.scheduleGraphMigration(session, memory);
5636
+ this.scheduleBlockDerivation(session, memory);
5267
5637
  return memory;
5268
5638
  } catch (error) {
5269
5639
  await memory.close().catch(() => {
@@ -5297,6 +5667,43 @@ var StrataGateRuntime = class {
5297
5667
  target: { eventIds: [.../* @__PURE__ */ new Set([...node.sourceEventIds, ...edges.flatMap(({ sourceEventIds }) => sourceEventIds)])], elementIds: [] }
5298
5668
  }], { node, edges });
5299
5669
  }
5670
+ scheduleBlockDerivation(session, memory) {
5671
+ const threadId = String(session.id);
5672
+ const key = `${this.namespaceFor(session)}\0${threadId}`;
5673
+ if (this.closed || this.derivationTimers.has(key) || this.derivationRuns.has(key)) return;
5674
+ const pendingBlockIds = new Set(memory.listBlocks().filter((block) => block.threadId === threadId && block.processingStatus === "pending").map((block) => block.id));
5675
+ if (pendingBlockIds.size === 0) return;
5676
+ const retryTimes = [
5677
+ ...memory.listSummaryJobs(),
5678
+ ...memory.listExtractionJobs()
5679
+ ].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);
5680
+ if (retryTimes.length === 0) return;
5681
+ const delay = Math.max(0, Math.min(...retryTimes) - Date.now());
5682
+ const timer = setTimeout(() => {
5683
+ this.derivationTimers.delete(key);
5684
+ if (this.closed) return;
5685
+ const run = this.models.run(session, () => memory.resumePendingWork({ threadId })).then(async (resumed) => {
5686
+ await this.persistSuccessfulResponses(memory);
5687
+ const contexts = memory.getBlockContext(threadId);
5688
+ for (const block of resumed.readyBlocks) {
5689
+ if (block.threadId !== threadId) continue;
5690
+ const context = contexts.find(({ id }) => id === block.id);
5691
+ if (!context) throw new Error(`Missing context for ready StrataGate block ${block.id}`);
5692
+ this.replaceSealedSurface(session, block, context, dshTurnAtBlockEnd(session, block));
5693
+ }
5694
+ const changed = this.syncDecayedBlockSurface(session, contexts);
5695
+ if (resumed.readyBlocks.length > 0 || changed) await this.flushNativeSession(session);
5696
+ }).catch((error) => {
5697
+ this.onIngestError(error);
5698
+ }).finally(() => {
5699
+ this.derivationRuns.delete(key);
5700
+ this.scheduleBlockDerivation(session, memory);
5701
+ });
5702
+ this.derivationRuns.set(key, run);
5703
+ }, delay);
5704
+ timer.unref?.();
5705
+ this.derivationTimers.set(key, timer);
5706
+ }
5300
5707
  scheduleGraphMigration(session, memory) {
5301
5708
  const namespace = this.namespaceFor(session);
5302
5709
  if (this.closed || this.migrationTimers.has(namespace)) return;
@@ -5936,7 +6343,7 @@ function clusterKnowledgeGraph(rawNodes, rawEdges) {
5936
6343
  }
5937
6344
 
5938
6345
  // src/web.ts
5939
- var STRATAGATE_DSH_VERSION = "0.2.32";
6346
+ var STRATAGATE_DSH_VERSION = "0.2.36";
5940
6347
  var LEGACY_THREAD_ID = "__legacy__";
5941
6348
  var nodeRequire = createRequire(import.meta.url);
5942
6349
  function installedPackageVersion(names) {
@@ -5992,6 +6399,13 @@ function sourceMessages(snapshot, ids) {
5992
6399
  return output;
5993
6400
  }
5994
6401
  function blockLayers(block) {
6402
+ if (block.processingStatus !== "ready" || !block.l0Title || !block.l0Tags || !block.l1Summary || !block.l2Keypoints) {
6403
+ return [
6404
+ { level: 3, content: block.l3Condensed },
6405
+ { level: 4, content: block.l4Readable },
6406
+ { level: 5, content: block.l5Raw.map((message) => `${message.role}: ${message.content}`).join("\n\n") }
6407
+ ];
6408
+ }
5995
6409
  return [
5996
6410
  { level: 0, content: `${block.l0Title}
5997
6411
  \u6807\u7B7E\uFF1A${block.l0Tags.join("\u3001") || "\u65E0"}` },
@@ -6125,12 +6539,31 @@ async function overview(runtime) {
6125
6539
  };
6126
6540
  }
6127
6541
  async function updateSettings(runtime, url) {
6128
- const raw = url.searchParams.get("blockDecayLambda")?.trim() ?? "";
6129
- const value = Number(raw);
6130
- if (!raw || !Number.isFinite(value) || value < 0) {
6131
- throw new AdminHttpError(400, "blockDecayLambda must be a non-negative finite number");
6132
- }
6133
- return { blockDecayLambda: await runtime.adminSetBlockDecayLambda(value) };
6542
+ const rawTurnSize = url.searchParams.get("blockTurnSize")?.trim();
6543
+ const rawLambda = url.searchParams.get("blockDecayLambda")?.trim();
6544
+ if (rawTurnSize === void 0 && rawLambda === void 0) {
6545
+ throw new AdminHttpError(400, "blockTurnSize or blockDecayLambda is required");
6546
+ }
6547
+ let turnSize;
6548
+ let lambda;
6549
+ if (rawTurnSize !== void 0) {
6550
+ const value = Number(rawTurnSize);
6551
+ if (!rawTurnSize || !Number.isSafeInteger(value) || value < 1) {
6552
+ throw new AdminHttpError(400, "blockTurnSize must be a positive integer");
6553
+ }
6554
+ turnSize = value;
6555
+ }
6556
+ if (rawLambda !== void 0) {
6557
+ const value = Number(rawLambda);
6558
+ if (!rawLambda || !Number.isFinite(value) || value < 0) {
6559
+ throw new AdminHttpError(400, "blockDecayLambda must be a non-negative finite number");
6560
+ }
6561
+ lambda = value;
6562
+ }
6563
+ const result = {};
6564
+ if (turnSize !== void 0) result.blockTurnSize = await runtime.adminSetBlockTurnSize(turnSize);
6565
+ if (lambda !== void 0) result.blockDecayLambda = await runtime.adminSetBlockDecayLambda(lambda);
6566
+ return result;
6134
6567
  }
6135
6568
  async function importExternalMemory(runtime, req) {
6136
6569
  let suppliedBody = req.body;