stratagate-dsh 0.2.16 → 0.2.21

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
@@ -11,6 +11,7 @@ var Config = z.object({
11
11
  namespacePrefix: z.string().default("dsh"),
12
12
  globalNamespace: z.string().default("global"),
13
13
  blockTurnSize: z.natural().min(1).default(6),
14
+ blockDecayLambda: z.number().step(0.05).min(0).default(0.3).description("Block \u8870\u51CF\u7CFB\u6570 \u03BB").comment("\u9ED8\u8BA4 0.3\uFF1B\u6570\u5B57\u8D8A\u5C0F\uFF0C\u8BB0\u5FC6\u9057\u5FD8\u8D8A\u6162\uFF0C\u6D88\u8017 token \u8D8A\u591A\uFF0C\u4E0D\u5EFA\u8BAE\u5927\u4E8E 0.4\u3002"),
14
15
  ingestSubagents: z.boolean().default(false),
15
16
  provider: z.string(),
16
17
  model: z.string(),
@@ -32,6 +33,7 @@ function resolveConfig(config) {
32
33
  namespacePrefix,
33
34
  globalNamespace,
34
35
  blockTurnSize: Math.max(1, Math.floor(config.blockTurnSize ?? 6)),
36
+ blockDecayLambda: Math.max(0, config.blockDecayLambda ?? 0.3),
35
37
  ingestSubagents: config.ingestSubagents ?? false,
36
38
  ...provider && model ? { provider, model } : {},
37
39
  maxOutputTokens: Math.max(256, Math.floor(config.maxOutputTokens ?? 1e4))
@@ -46,7 +48,7 @@ import { parameterSchemaSpecToJsonSchema, validateArgs } from "@deepseek-ai/dsh-
46
48
  // ../../src/blocks.ts
47
49
  var DEFAULT_BLOCK_TURN_SIZE = 12;
48
50
  var BLOCK_MAX_LEVEL = 5;
49
- var BLOCK_DECAY_LAMBDA = 0.05;
51
+ var BLOCK_DECAY_LAMBDA = 0.3;
50
52
  var FILLER_ONLY = /* @__PURE__ */ new Set([
51
53
  "ok",
52
54
  "okay",
@@ -80,11 +82,11 @@ var REPEATED_PASTE_MARKER = "[repeated paste omitted; original remains in L5]";
80
82
  function asBlockLevel(value) {
81
83
  return Math.max(0, Math.min(BLOCK_MAX_LEVEL, Math.round(value)));
82
84
  }
83
- function getBlockWeight(anchorTurn, currentTurn) {
84
- return Math.exp(-BLOCK_DECAY_LAMBDA * Math.max(0, currentTurn - anchorTurn));
85
+ function getBlockWeight(anchorBlockPosition, latestBlockPosition, lambda = BLOCK_DECAY_LAMBDA) {
86
+ return Math.exp(-lambda * Math.max(0, latestBlockPosition - anchorBlockPosition));
85
87
  }
86
- function getDecayedBlockLevel(anchorLevel, anchorTurn, currentTurn) {
87
- const weight = getBlockWeight(anchorTurn, currentTurn);
88
+ function getDecayedBlockLevel(anchorLevel, anchorBlockPosition, latestBlockPosition, lambda = BLOCK_DECAY_LAMBDA) {
89
+ const weight = getBlockWeight(anchorBlockPosition, latestBlockPosition, lambda);
88
90
  const droppedLevels = weight > 0.7 ? 0 : weight > 0.5 ? 1 : weight > 0.3 ? 2 : weight > 0.15 ? 3 : weight > 0.08 ? 4 : 5;
89
91
  return asBlockLevel(anchorLevel - droppedLevels);
90
92
  }
@@ -358,7 +360,7 @@ function rrfRank(rankings) {
358
360
  }
359
361
 
360
362
  // ../../src/storage.ts
361
- var STRATAGATE_STORAGE_SCHEMA_VERSION = 5;
363
+ var STRATAGATE_STORAGE_SCHEMA_VERSION = 7;
362
364
  var StorageConflictError = class extends Error {
363
365
  constructor(namespace, expectedRevision, actualRevision) {
364
366
  super(`Storage revision conflict for ${namespace}: expected ${expectedRevision}, found ${actualRevision ?? "missing"}`);
@@ -374,6 +376,13 @@ var StorageConflictError = class extends Error {
374
376
  function cloneSnapshot(snapshot) {
375
377
  return structuredClone(snapshot);
376
378
  }
379
+ function migrateLegacyBlocks(blocks) {
380
+ return blocks.map((block) => {
381
+ const { pointerAnchorTurn, ...current } = block;
382
+ const position = blocks.filter((candidate) => candidate.threadId === block.threadId && candidate.endTurn <= pointerAnchorTurn).length;
383
+ return { ...current, pointerAnchorBlockPosition: Math.max(1, position), lastLiftedBy: null };
384
+ });
385
+ }
377
386
  function normalizeSnapshot(value) {
378
387
  if (!value || typeof value !== "object") throw new TypeError("Invalid StrataGate snapshot: expected an object");
379
388
  const schemaVersion = value.schemaVersion;
@@ -383,26 +392,52 @@ function normalizeSnapshot(value) {
383
392
  snapshot = {
384
393
  ...structuredClone(legacy),
385
394
  schemaVersion: STRATAGATE_STORAGE_SCHEMA_VERSION,
395
+ blockDecayLambda: BLOCK_DECAY_LAMBDA,
396
+ blocks: migrateLegacyBlocks(legacy.blocks),
386
397
  elements: [],
387
398
  elementProjectionJobs: [],
388
399
  usageReceipts: Array.isArray(legacy.usageReceipts) ? legacy.usageReceipts.map((receipt) => ({ ...receipt, elementIds: [] })) : [],
389
400
  ingestionReceipts: []
390
401
  };
391
402
  } else if (schemaVersion === 2) {
403
+ const legacy = value;
392
404
  snapshot = {
393
- ...structuredClone(value),
405
+ ...structuredClone(legacy),
394
406
  schemaVersion: STRATAGATE_STORAGE_SCHEMA_VERSION,
407
+ blockDecayLambda: BLOCK_DECAY_LAMBDA,
408
+ blocks: migrateLegacyBlocks(legacy.blocks),
395
409
  ingestionReceipts: []
396
410
  };
397
411
  } else if (schemaVersion === 3) {
412
+ const legacy = value;
398
413
  snapshot = {
399
- ...structuredClone(value),
400
- schemaVersion: STRATAGATE_STORAGE_SCHEMA_VERSION
414
+ ...structuredClone(legacy),
415
+ schemaVersion: STRATAGATE_STORAGE_SCHEMA_VERSION,
416
+ blockDecayLambda: BLOCK_DECAY_LAMBDA,
417
+ blocks: migrateLegacyBlocks(legacy.blocks)
401
418
  };
402
419
  } else if (schemaVersion === 4) {
420
+ const legacy = value;
403
421
  snapshot = {
404
- ...structuredClone(value),
405
- schemaVersion: STRATAGATE_STORAGE_SCHEMA_VERSION
422
+ ...structuredClone(legacy),
423
+ schemaVersion: STRATAGATE_STORAGE_SCHEMA_VERSION,
424
+ blockDecayLambda: BLOCK_DECAY_LAMBDA,
425
+ blocks: migrateLegacyBlocks(legacy.blocks)
426
+ };
427
+ } else if (schemaVersion === 5) {
428
+ const legacy = value;
429
+ snapshot = {
430
+ ...structuredClone(legacy),
431
+ schemaVersion: STRATAGATE_STORAGE_SCHEMA_VERSION,
432
+ blockDecayLambda: BLOCK_DECAY_LAMBDA,
433
+ blocks: migrateLegacyBlocks(legacy.blocks)
434
+ };
435
+ } else if (schemaVersion === 6) {
436
+ const legacy = value;
437
+ snapshot = {
438
+ ...structuredClone(legacy),
439
+ schemaVersion: STRATAGATE_STORAGE_SCHEMA_VERSION,
440
+ blocks: legacy.blocks.map((block) => ({ ...structuredClone(block), lastLiftedBy: null }))
406
441
  };
407
442
  } else if (schemaVersion === STRATAGATE_STORAGE_SCHEMA_VERSION) {
408
443
  snapshot = structuredClone(value);
@@ -415,10 +450,21 @@ function normalizeSnapshot(value) {
415
450
  if (!Number.isSafeInteger(snapshot.blockTurnSize) || (snapshot.blockTurnSize ?? 0) < 1) {
416
451
  throw new TypeError("Invalid StrataGate snapshot: blockTurnSize must be a positive integer");
417
452
  }
453
+ if (!Number.isFinite(snapshot.blockDecayLambda) || snapshot.blockDecayLambda < 0) {
454
+ throw new TypeError("Invalid StrataGate snapshot: blockDecayLambda must be a non-negative finite number");
455
+ }
418
456
  for (const key of ["openTail", "blocks", "events", "elements", "extractionJobs", "elementProjectionJobs", "usageReceipts", "ingestionReceipts"]) {
419
457
  if (!Array.isArray(snapshot[key])) throw new TypeError(`Invalid StrataGate snapshot: ${key} must be an array`);
420
458
  }
421
459
  if (!Array.isArray(snapshot.successfulModelResponses)) snapshot.successfulModelResponses = [];
460
+ for (const block of snapshot.blocks) {
461
+ if (!Number.isSafeInteger(block.pointerAnchorBlockPosition) || block.pointerAnchorBlockPosition < 1) {
462
+ throw new TypeError("Invalid StrataGate snapshot: pointerAnchorBlockPosition must be a positive integer");
463
+ }
464
+ if (block.lastLiftedBy !== null && block.lastLiftedBy !== "user" && block.lastLiftedBy !== "agent") {
465
+ throw new TypeError("Invalid StrataGate snapshot: lastLiftedBy must be user, agent, or null");
466
+ }
467
+ }
422
468
  if (snapshot.successfulModelResponses.length > 5) {
423
469
  snapshot.successfulModelResponses = snapshot.successfulModelResponses.slice(-5);
424
470
  }
@@ -592,6 +638,7 @@ CREATE TABLE IF NOT EXISTS memory_spaces (
592
638
  revision INTEGER NOT NULL,
593
639
  current_turn INTEGER NOT NULL,
594
640
  block_turn_size INTEGER NOT NULL,
641
+ block_decay_lambda REAL NOT NULL,
595
642
  created_at TEXT NOT NULL,
596
643
  updated_at TEXT NOT NULL
597
644
  ) STRICT;
@@ -613,8 +660,9 @@ CREATE TABLE IF NOT EXISTS blocks (
613
660
  l4_readable TEXT NOT NULL,
614
661
  pointer_current_level INTEGER NOT NULL,
615
662
  pointer_anchor_level INTEGER NOT NULL,
616
- pointer_anchor_turn INTEGER NOT NULL,
663
+ pointer_anchor_block_position INTEGER NOT NULL,
617
664
  last_lifted_at TEXT,
665
+ last_lifted_by TEXT CHECK (last_lifted_by IS NULL OR last_lifted_by IN ('user', 'agent')),
618
666
  PRIMARY KEY (namespace, id),
619
667
  UNIQUE (namespace, sequence),
620
668
  FOREIGN KEY (namespace) REFERENCES memory_spaces(namespace) ON DELETE CASCADE
@@ -831,7 +879,7 @@ var SqliteStorage = class {
831
879
  this.assertOpen();
832
880
  const key = nonEmptyNamespace(namespace);
833
881
  const space = this.database.prepare(`
834
- SELECT schema_version, revision, current_turn, block_turn_size
882
+ SELECT schema_version, revision, current_turn, block_turn_size, block_decay_lambda
835
883
  FROM memory_spaces WHERE namespace = ?
836
884
  `).get(key);
837
885
  if (!space) return null;
@@ -880,8 +928,9 @@ var SqliteStorage = class {
880
928
  l5Raw: messagesByBlock.get(row.id) ?? [],
881
929
  pointerCurrentLevel: row.pointer_current_level,
882
930
  pointerAnchorLevel: row.pointer_anchor_level,
883
- pointerAnchorTurn: row.pointer_anchor_turn,
884
- lastLiftedAt: row.last_lifted_at
931
+ pointerAnchorBlockPosition: row.pointer_anchor_block_position,
932
+ lastLiftedAt: row.last_lifted_at,
933
+ lastLiftedBy: row.last_lifted_by
885
934
  }));
886
935
  const sourceRows = this.database.prepare(`
887
936
  SELECT event_id, message_id, position FROM event_sources
@@ -1047,6 +1096,7 @@ var SqliteStorage = class {
1047
1096
  schemaVersion: STRATAGATE_STORAGE_SCHEMA_VERSION,
1048
1097
  currentTurn: space.current_turn,
1049
1098
  blockTurnSize: space.block_turn_size,
1099
+ blockDecayLambda: space.block_decay_lambda,
1050
1100
  openTail,
1051
1101
  blocks,
1052
1102
  events,
@@ -1085,27 +1135,29 @@ var SqliteStorage = class {
1085
1135
  if (current) {
1086
1136
  this.database.prepare(`
1087
1137
  UPDATE memory_spaces
1088
- SET schema_version = ?, revision = ?, current_turn = ?, block_turn_size = ?, updated_at = ?
1138
+ SET schema_version = ?, revision = ?, current_turn = ?, block_turn_size = ?, block_decay_lambda = ?, updated_at = ?
1089
1139
  WHERE namespace = ?
1090
1140
  `).run(
1091
1141
  snapshot.schemaVersion,
1092
1142
  nextRevision,
1093
1143
  snapshot.currentTurn,
1094
1144
  snapshot.blockTurnSize,
1145
+ snapshot.blockDecayLambda,
1095
1146
  updatedAt,
1096
1147
  namespace
1097
1148
  );
1098
1149
  } else {
1099
1150
  this.database.prepare(`
1100
1151
  INSERT INTO memory_spaces (
1101
- namespace, schema_version, revision, current_turn, block_turn_size, created_at, updated_at
1102
- ) VALUES (?, ?, ?, ?, ?, ?, ?)
1152
+ namespace, schema_version, revision, current_turn, block_turn_size, block_decay_lambda, created_at, updated_at
1153
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1103
1154
  `).run(
1104
1155
  namespace,
1105
1156
  snapshot.schemaVersion,
1106
1157
  nextRevision,
1107
1158
  snapshot.currentTurn,
1108
1159
  snapshot.blockTurnSize,
1160
+ snapshot.blockDecayLambda,
1109
1161
  updatedAt,
1110
1162
  updatedAt
1111
1163
  );
@@ -1114,8 +1166,8 @@ var SqliteStorage = class {
1114
1166
  INSERT INTO blocks (
1115
1167
  namespace, id, thread_id, sequence, start_turn, end_turn, created_at, should_extract,
1116
1168
  l0_title, l0_tags_json, l1_summary, l2_keypoints_json, l3_condensed, l4_readable,
1117
- pointer_current_level, pointer_anchor_level, pointer_anchor_turn, last_lifted_at
1118
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1169
+ pointer_current_level, pointer_anchor_level, pointer_anchor_block_position, last_lifted_at, last_lifted_by
1170
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1119
1171
  ON CONFLICT (namespace, id) DO UPDATE SET
1120
1172
  thread_id = excluded.thread_id,
1121
1173
  sequence = excluded.sequence,
@@ -1131,8 +1183,9 @@ var SqliteStorage = class {
1131
1183
  l4_readable = excluded.l4_readable,
1132
1184
  pointer_current_level = excluded.pointer_current_level,
1133
1185
  pointer_anchor_level = excluded.pointer_anchor_level,
1134
- pointer_anchor_turn = excluded.pointer_anchor_turn,
1135
- last_lifted_at = excluded.last_lifted_at
1186
+ pointer_anchor_block_position = excluded.pointer_anchor_block_position,
1187
+ last_lifted_at = excluded.last_lifted_at,
1188
+ last_lifted_by = excluded.last_lifted_by
1136
1189
  `);
1137
1190
  for (const block of snapshot.blocks) {
1138
1191
  insertBlock.run(
@@ -1152,8 +1205,9 @@ var SqliteStorage = class {
1152
1205
  block.l4Readable,
1153
1206
  block.pointerCurrentLevel,
1154
1207
  block.pointerAnchorLevel,
1155
- block.pointerAnchorTurn,
1156
- block.lastLiftedAt
1208
+ block.pointerAnchorBlockPosition,
1209
+ block.lastLiftedAt,
1210
+ block.lastLiftedBy
1157
1211
  );
1158
1212
  }
1159
1213
  const insertMessage = this.database.prepare(`
@@ -1422,8 +1476,9 @@ var SqliteStorage = class {
1422
1476
  this.database.exec(THREAD_INDEXES);
1423
1477
  this.database.exec(`PRAGMA user_version = ${STRATAGATE_STORAGE_SCHEMA_VERSION}`);
1424
1478
  });
1425
- } else if (version === 1 || version === 2 || version === 3 || version === 4) {
1479
+ } else if (version === 1 || version === 2 || version === 3 || version === 4 || version === 5 || version === 6) {
1426
1480
  this.immediateTransaction(() => {
1481
+ this.database.exec(SCHEMA);
1427
1482
  if (version === 1) {
1428
1483
  const receiptColumns2 = this.database.prepare("PRAGMA table_info('usage_receipts')").all();
1429
1484
  if (!receiptColumns2.some(({ name: name2 }) => name2 === "element_ids_json")) {
@@ -1434,11 +1489,29 @@ var SqliteStorage = class {
1434
1489
  if (!receiptColumns.some(({ name: name2 }) => name2 === "audit_json")) {
1435
1490
  this.database.exec("ALTER TABLE usage_receipts ADD COLUMN audit_json TEXT NOT NULL DEFAULT '{}'");
1436
1491
  }
1437
- this.database.exec(SCHEMA);
1492
+ const spaceColumns = this.database.prepare("PRAGMA table_info('memory_spaces')").all();
1493
+ if (!spaceColumns.some(({ name: name2 }) => name2 === "block_decay_lambda")) {
1494
+ this.database.exec("ALTER TABLE memory_spaces ADD COLUMN block_decay_lambda REAL NOT NULL DEFAULT 0.3");
1495
+ }
1438
1496
  const blockColumns = this.database.prepare("PRAGMA table_info('blocks')").all();
1439
1497
  if (!blockColumns.some(({ name: name2 }) => name2 === "thread_id")) {
1440
1498
  this.database.exec("ALTER TABLE blocks ADD COLUMN thread_id TEXT");
1441
1499
  }
1500
+ if (!blockColumns.some(({ name: name2 }) => name2 === "pointer_anchor_block_position")) {
1501
+ this.database.exec("ALTER TABLE blocks RENAME COLUMN pointer_anchor_turn TO pointer_anchor_block_position");
1502
+ this.database.exec(`
1503
+ UPDATE blocks AS target
1504
+ SET pointer_anchor_block_position = MAX(1, (
1505
+ SELECT COUNT(*) FROM blocks AS candidate
1506
+ WHERE candidate.namespace = target.namespace
1507
+ AND candidate.thread_id IS target.thread_id
1508
+ AND candidate.end_turn <= target.pointer_anchor_block_position
1509
+ ))
1510
+ `);
1511
+ }
1512
+ if (!blockColumns.some(({ name: name2 }) => name2 === "last_lifted_by")) {
1513
+ 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'))");
1514
+ }
1442
1515
  const messageColumns = this.database.prepare("PRAGMA table_info('messages')").all();
1443
1516
  if (!messageColumns.some(({ name: name2 }) => name2 === "thread_id")) {
1444
1517
  this.database.exec("ALTER TABLE messages ADD COLUMN thread_id TEXT");
@@ -1526,6 +1599,7 @@ function errorMessage(error) {
1526
1599
  var STRATAGATE_CONSTRUCTOR_TOKEN = /* @__PURE__ */ Symbol("StrataGate constructor");
1527
1600
  var StrataGate = class _StrataGate {
1528
1601
  blockTurnSize;
1602
+ blockDecayLambdaValue;
1529
1603
  summarizer;
1530
1604
  extractor;
1531
1605
  elementProjector;
@@ -1551,6 +1625,11 @@ var StrataGate = class _StrataGate {
1551
1625
  throw new TypeError("Use StrataGate.open() for SQLite or StrataGate.inMemory() for explicit ephemeral storage");
1552
1626
  }
1553
1627
  this.blockTurnSize = Math.max(1, Math.floor(options.blockTurnSize ?? DEFAULT_BLOCK_TURN_SIZE));
1628
+ const blockDecayLambda = options.blockDecayLambda ?? BLOCK_DECAY_LAMBDA;
1629
+ if (!Number.isFinite(blockDecayLambda) || blockDecayLambda < 0) {
1630
+ throw new TypeError("blockDecayLambda must be a non-negative finite number");
1631
+ }
1632
+ this.blockDecayLambdaValue = blockDecayLambda;
1554
1633
  this.summarizer = options.summarizer;
1555
1634
  this.extractor = options.extractor;
1556
1635
  this.elementProjector = options.elementProjector;
@@ -1573,6 +1652,7 @@ var StrataGate = class _StrataGate {
1573
1652
  storage,
1574
1653
  namespace: options.namespace,
1575
1654
  ...options.blockTurnSize !== void 0 ? { blockTurnSize: options.blockTurnSize } : {},
1655
+ ...options.blockDecayLambda !== void 0 ? { blockDecayLambda: options.blockDecayLambda } : {},
1576
1656
  ...options.summarizer ? { summarizer: options.summarizer } : {},
1577
1657
  ...options.extractor ? { extractor: options.extractor } : {},
1578
1658
  ...options.elementProjector ? { elementProjector: options.elementProjector } : {},
@@ -1591,17 +1671,32 @@ var StrataGate = class _StrataGate {
1591
1671
  const loaded = await options.storage.load(namespace);
1592
1672
  const loadedSnapshot = loaded ? normalizeSnapshot(loaded.snapshot) : null;
1593
1673
  let loadedRevision = loaded?.revision ?? 0;
1594
- if (loaded && options.blockTurnSize !== void 0) {
1595
- const requested = Math.max(1, Math.floor(options.blockTurnSize));
1596
- if (requested !== loadedSnapshot?.blockTurnSize) {
1597
- if (!loadedSnapshot) throw new Error("Loaded StrataGate state did not contain a snapshot");
1598
- loadedSnapshot.blockTurnSize = requested;
1599
- loadedRevision = await options.storage.save(namespace, loadedSnapshot, loadedRevision);
1674
+ if (loaded && loadedSnapshot) {
1675
+ let settingsChanged = false;
1676
+ if (options.blockTurnSize !== void 0) {
1677
+ const requested = Math.max(1, Math.floor(options.blockTurnSize));
1678
+ if (requested !== loadedSnapshot.blockTurnSize) {
1679
+ loadedSnapshot.blockTurnSize = requested;
1680
+ settingsChanged = true;
1681
+ }
1682
+ }
1683
+ if (options.blockDecayLambda !== void 0) {
1684
+ const requested = options.blockDecayLambda;
1685
+ if (!Number.isFinite(requested) || requested < 0) {
1686
+ throw new TypeError("blockDecayLambda must be a non-negative finite number");
1687
+ }
1688
+ if (requested !== loadedSnapshot.blockDecayLambda) {
1689
+ loadedSnapshot.blockDecayLambda = requested;
1690
+ settingsChanged = true;
1691
+ }
1600
1692
  }
1693
+ if (settingsChanged) loadedRevision = await options.storage.save(namespace, loadedSnapshot, loadedRevision);
1601
1694
  }
1602
1695
  const memoryOptions = {};
1603
1696
  if (loadedSnapshot) memoryOptions.blockTurnSize = loadedSnapshot.blockTurnSize;
1604
1697
  else if (options.blockTurnSize !== void 0) memoryOptions.blockTurnSize = options.blockTurnSize;
1698
+ if (loadedSnapshot) memoryOptions.blockDecayLambda = loadedSnapshot.blockDecayLambda;
1699
+ else if (options.blockDecayLambda !== void 0) memoryOptions.blockDecayLambda = options.blockDecayLambda;
1605
1700
  if (options.summarizer) memoryOptions.summarizer = options.summarizer;
1606
1701
  if (options.extractor) memoryOptions.extractor = options.extractor;
1607
1702
  if (options.elementProjector) memoryOptions.elementProjector = options.elementProjector;
@@ -1653,6 +1748,18 @@ var StrataGate = class _StrataGate {
1653
1748
  get storageRevision() {
1654
1749
  return this.revision;
1655
1750
  }
1751
+ get blockDecayLambda() {
1752
+ return this.blockDecayLambdaValue;
1753
+ }
1754
+ async setBlockDecayLambda(value) {
1755
+ if (!Number.isFinite(value) || value < 0) {
1756
+ throw new TypeError("blockDecayLambda must be a non-negative finite number");
1757
+ }
1758
+ if (value === this.blockDecayLambdaValue) return;
1759
+ await this.commitMutation(() => {
1760
+ this.blockDecayLambdaValue = value;
1761
+ });
1762
+ }
1656
1763
  listBlocks() {
1657
1764
  return this.blocks;
1658
1765
  }
@@ -1695,6 +1802,7 @@ var StrataGate = class _StrataGate {
1695
1802
  schemaVersion: STRATAGATE_STORAGE_SCHEMA_VERSION,
1696
1803
  currentTurn: this.currentTurn,
1697
1804
  blockTurnSize: this.blockTurnSize,
1805
+ blockDecayLambda: this.blockDecayLambda,
1698
1806
  openTail: this.openTail,
1699
1807
  blocks: this.blocks,
1700
1808
  events: this.events,
@@ -1995,34 +2103,51 @@ var StrataGate = class _StrataGate {
1995
2103
  getBlockContext(threadId) {
1996
2104
  const blocks = threadId === void 0 ? this.blocks : this.blocks.filter((block) => block.threadId === threadId);
1997
2105
  return blocks.map((block) => {
1998
- const currentTurn = block.threadId === void 0 ? this.currentTurn : this.threadTurn(block.threadId);
1999
- const level = getDecayedBlockLevel(block.pointerAnchorLevel, block.pointerAnchorTurn, currentTurn);
2106
+ const threadBlocks = this.threadBlocks(block.threadId);
2107
+ const latestBlockPosition = threadBlocks.length;
2108
+ const blockPosition = threadBlocks.indexOf(block) + 1;
2109
+ const age = Math.max(0, latestBlockPosition - blockPosition);
2110
+ const level = getDecayedBlockLevel(
2111
+ block.pointerAnchorLevel,
2112
+ block.pointerAnchorBlockPosition,
2113
+ latestBlockPosition,
2114
+ this.blockDecayLambda
2115
+ );
2000
2116
  block.pointerCurrentLevel = level;
2001
2117
  return {
2002
2118
  id: block.id,
2003
2119
  ...block.threadId ? { threadId: block.threadId } : {},
2004
2120
  turnRange: [block.startTurn, block.endTurn],
2121
+ age,
2005
2122
  level,
2006
2123
  label: blockLevelLabel(level),
2007
2124
  content: renderBlock(block, level)
2008
2125
  };
2009
2126
  });
2010
2127
  }
2011
- async expandBlock(id, target = "next") {
2128
+ async expandBlock(id, target = "next", source = "agent") {
2012
2129
  return this.commitMutation(() => {
2013
2130
  const block = this.blocks.find((candidate) => candidate.id === id);
2014
2131
  if (!block) throw new Error(`Unknown block: ${id}`);
2015
- const currentTurn = block.threadId === void 0 ? this.currentTurn : this.threadTurn(block.threadId);
2016
- const current = getDecayedBlockLevel(block.pointerAnchorLevel, block.pointerAnchorTurn, currentTurn);
2132
+ const latestBlockPosition = this.threadBlocks(block.threadId).length;
2133
+ const blockPosition = this.threadBlocks(block.threadId).indexOf(block) + 1;
2134
+ const current = getDecayedBlockLevel(
2135
+ block.pointerAnchorLevel,
2136
+ block.pointerAnchorBlockPosition,
2137
+ latestBlockPosition,
2138
+ this.blockDecayLambda
2139
+ );
2017
2140
  const level = normalizeBlockLevel(target, current);
2018
2141
  block.pointerCurrentLevel = level;
2019
2142
  block.pointerAnchorLevel = level;
2020
- block.pointerAnchorTurn = currentTurn;
2143
+ block.pointerAnchorBlockPosition = latestBlockPosition;
2021
2144
  block.lastLiftedAt = toUtc8Iso(this.now());
2145
+ block.lastLiftedBy = source;
2022
2146
  return {
2023
2147
  id: block.id,
2024
2148
  ...block.threadId ? { threadId: block.threadId } : {},
2025
2149
  turnRange: [block.startTurn, block.endTurn],
2150
+ age: Math.max(0, latestBlockPosition - blockPosition),
2026
2151
  level,
2027
2152
  label: blockLevelLabel(level),
2028
2153
  content: renderBlock(block, level)
@@ -2221,7 +2346,9 @@ var StrataGate = class _StrataGate {
2221
2346
  const generated = this.summarizer ? await this.summarizer(raw) : defaultSummary(raw);
2222
2347
  const deterministic = deterministicBlockLayers(raw);
2223
2348
  const sequence = this.blocks.length + 1;
2224
- const previous = this.threadBlocks(threadId).at(-1);
2349
+ const threadBlocks = this.threadBlocks(threadId);
2350
+ const previous = threadBlocks.at(-1);
2351
+ const blockPosition = threadBlocks.length + 1;
2225
2352
  const startTurn = previous ? previous.endTurn + 1 : 1;
2226
2353
  const endTurn = startTurn + this.blockTurnSize - 1;
2227
2354
  return this.commitMutation(() => {
@@ -2244,8 +2371,9 @@ var StrataGate = class _StrataGate {
2244
2371
  ...deterministic,
2245
2372
  pointerCurrentLevel: 5,
2246
2373
  pointerAnchorLevel: 5,
2247
- pointerAnchorTurn: endTurn,
2248
- lastLiftedAt: null
2374
+ pointerAnchorBlockPosition: blockPosition,
2375
+ lastLiftedAt: null,
2376
+ lastLiftedBy: null
2249
2377
  };
2250
2378
  const sealedIds = new Set(raw.map((message) => message.id));
2251
2379
  const remaining = this.openTail.filter((message) => !sealedIds.has(message.id));
@@ -2381,6 +2509,7 @@ var StrataGate = class _StrataGate {
2381
2509
  throw new Error(`Snapshot blockTurnSize ${normalized.blockTurnSize} does not match ${this.blockTurnSize}`);
2382
2510
  }
2383
2511
  const copy = cloneSnapshot(normalized);
2512
+ this.blockDecayLambdaValue = copy.blockDecayLambda;
2384
2513
  this.currentTurn = copy.currentTurn;
2385
2514
  this.openTail.splice(0, this.openTail.length, ...copy.openTail);
2386
2515
  this.blocks.splice(0, this.blocks.length, ...copy.blocks);
@@ -3051,6 +3180,60 @@ var TurnFolder = class {
3051
3180
  }
3052
3181
  };
3053
3182
 
3183
+ // src/metadata.ts
3184
+ import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
3185
+ var METADATA_SCHEMA = `
3186
+ CREATE TABLE IF NOT EXISTS stratagate_dsh_settings (
3187
+ key TEXT PRIMARY KEY,
3188
+ value TEXT NOT NULL,
3189
+ updated_at TEXT NOT NULL
3190
+ ) STRICT;
3191
+
3192
+ CREATE TABLE IF NOT EXISTS stratagate_dsh_workspaces (
3193
+ namespace TEXT PRIMARY KEY,
3194
+ display_name TEXT NOT NULL,
3195
+ updated_at TEXT NOT NULL
3196
+ ) STRICT;
3197
+ `;
3198
+ var DshMetadataStore = class {
3199
+ database;
3200
+ constructor(filename) {
3201
+ this.database = new DatabaseSync2(filename);
3202
+ this.database.exec(METADATA_SCHEMA);
3203
+ }
3204
+ blockDecayLambda() {
3205
+ const row = this.database.prepare("SELECT value FROM stratagate_dsh_settings WHERE key = 'blockDecayLambda'").get();
3206
+ const value = Number(row?.value);
3207
+ return Number.isFinite(value) && value >= 0 ? value : null;
3208
+ }
3209
+ setBlockDecayLambda(value) {
3210
+ if (!Number.isFinite(value) || value < 0) {
3211
+ throw new TypeError("blockDecayLambda must be a non-negative finite number");
3212
+ }
3213
+ this.database.prepare(`
3214
+ INSERT INTO stratagate_dsh_settings (key, value, updated_at)
3215
+ VALUES ('blockDecayLambda', ?, ?)
3216
+ ON CONFLICT (key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at
3217
+ `).run(String(value), (/* @__PURE__ */ new Date()).toISOString());
3218
+ }
3219
+ workspaceName(namespace) {
3220
+ const row = this.database.prepare("SELECT display_name FROM stratagate_dsh_workspaces WHERE namespace = ?").get(namespace);
3221
+ return row?.display_name ?? null;
3222
+ }
3223
+ rememberWorkspace(namespace, displayName) {
3224
+ const name2 = displayName.trim();
3225
+ if (!namespace.trim() || !name2) return;
3226
+ this.database.prepare(`
3227
+ INSERT INTO stratagate_dsh_workspaces (namespace, display_name, updated_at)
3228
+ VALUES (?, ?, ?)
3229
+ ON CONFLICT (namespace) DO UPDATE SET display_name = excluded.display_name, updated_at = excluded.updated_at
3230
+ `).run(namespace, name2, (/* @__PURE__ */ new Date()).toISOString());
3231
+ }
3232
+ close() {
3233
+ this.database.close();
3234
+ }
3235
+ };
3236
+
3054
3237
  // src/runtime.ts
3055
3238
  var AUTO_EVENT_LIMIT = 4;
3056
3239
  var AUTO_ELEMENT_LIMIT = 4;
@@ -3059,12 +3242,17 @@ function projectKey(cwd) {
3059
3242
  const canonical = resolve(cwd ?? process.cwd()).replaceAll("\\", "/").toLowerCase();
3060
3243
  return createHash("sha256").update(canonical).digest("hex").slice(0, 20);
3061
3244
  }
3245
+ function workspaceDisplayName(cwd) {
3246
+ const canonical = (cwd ?? process.cwd()).replace(/[\\/]+$/, "");
3247
+ return canonical.split(/[\\/]/).at(-1) || "\u5F53\u524D\u5DE5\u4F5C\u533A";
3248
+ }
3062
3249
  var StrataGateRuntime = class {
3063
3250
  constructor(config, models, onIngestError = () => {
3064
3251
  }) {
3065
3252
  this.config = config;
3066
3253
  this.models = models;
3067
3254
  this.onIngestError = onIngestError;
3255
+ this.blockDecayLambda = config.blockDecayLambda;
3068
3256
  }
3069
3257
  config;
3070
3258
  models;
@@ -3074,10 +3262,13 @@ var StrataGateRuntime = class {
3074
3262
  batches = /* @__PURE__ */ new Map();
3075
3263
  adopted = /* @__PURE__ */ new Map();
3076
3264
  pendingUse = /* @__PURE__ */ new Set();
3265
+ workspaceNames = /* @__PURE__ */ new Map();
3077
3266
  ingestTail = Promise.resolve();
3267
+ settingsTail = Promise.resolve();
3078
3268
  batchSequence = 0;
3079
3269
  closed = false;
3080
3270
  ingestError;
3271
+ blockDecayLambda;
3081
3272
  acceptEvent(session, event) {
3082
3273
  if (this.closed) return;
3083
3274
  if (!this.config.ingestSubagents && session.header.origin === "subagent") return;
@@ -3130,7 +3321,7 @@ var StrataGateRuntime = class {
3130
3321
  }
3131
3322
  async expandBlock(session, id, target) {
3132
3323
  await this.flush();
3133
- const result = await (await this.space(session)).expandBlock(id, target);
3324
+ const result = await (await this.space(session)).expandBlock(id, target, "agent");
3134
3325
  return this.batch(session, [{
3135
3326
  ref: `block:${result.id}:level:${result.level}`,
3136
3327
  target: { eventIds: [], elementIds: [] }
@@ -3298,16 +3489,23 @@ var StrataGateRuntime = class {
3298
3489
  await storage.close();
3299
3490
  }
3300
3491
  }
3301
- async syncConfiguredBlockTurnSize() {
3492
+ async syncConfiguredSettings() {
3302
3493
  if (this.config.database === ":memory:" || !existsSync(this.config.database)) return;
3494
+ const metadata = new DshMetadataStore(this.config.database);
3495
+ try {
3496
+ this.blockDecayLambda = metadata.blockDecayLambda() ?? this.config.blockDecayLambda;
3497
+ } finally {
3498
+ metadata.close();
3499
+ }
3303
3500
  const storage = new SqliteStorage({ filename: this.config.database });
3304
3501
  try {
3305
3502
  for (const namespace of storage.listNamespaces()) {
3306
3503
  const loaded = await storage.load(namespace);
3307
- if (!loaded || loaded.snapshot.blockTurnSize === this.config.blockTurnSize) continue;
3504
+ if (!loaded || loaded.snapshot.blockTurnSize === this.config.blockTurnSize && loaded.snapshot.blockDecayLambda === this.blockDecayLambda) continue;
3308
3505
  await storage.save(namespace, {
3309
3506
  ...loaded.snapshot,
3310
- blockTurnSize: this.config.blockTurnSize
3507
+ blockTurnSize: this.config.blockTurnSize,
3508
+ blockDecayLambda: this.blockDecayLambda
3311
3509
  }, loaded.revision);
3312
3510
  }
3313
3511
  } finally {
@@ -3325,14 +3523,101 @@ var StrataGateRuntime = class {
3325
3523
  await storage.close();
3326
3524
  }
3327
3525
  }
3526
+ adminWorkspaceName(namespace) {
3527
+ const remembered = this.workspaceNames.get(namespace);
3528
+ if (remembered) return remembered;
3529
+ if (this.config.database === ":memory:" || !existsSync(this.config.database)) return null;
3530
+ const metadata = new DshMetadataStore(this.config.database);
3531
+ try {
3532
+ return metadata.workspaceName(namespace);
3533
+ } finally {
3534
+ metadata.close();
3535
+ }
3536
+ }
3537
+ async adminSetBlockDecayLambda(value) {
3538
+ if (!Number.isFinite(value) || value < 0) {
3539
+ throw new TypeError("blockDecayLambda must be a non-negative finite number");
3540
+ }
3541
+ const update = this.settingsTail.catch(() => {
3542
+ }).then(() => this.applyBlockDecayLambda(value));
3543
+ this.settingsTail = update.then(() => {
3544
+ }, () => {
3545
+ });
3546
+ await update;
3547
+ return value;
3548
+ }
3549
+ async adminExpandBlock(namespace, id, target) {
3550
+ const key = namespace.trim();
3551
+ if (!key) throw new TypeError("StrataGate admin namespace must not be empty");
3552
+ const update = this.settingsTail.catch(() => {
3553
+ }).then(async () => {
3554
+ await this.flush();
3555
+ const active = this.spaces.get(key);
3556
+ if (active) return (await active).expandBlock(id, target, "user");
3557
+ if (this.config.database === ":memory:" || !existsSync(this.config.database)) {
3558
+ throw new Error(`Unknown StrataGate namespace: ${key}`);
3559
+ }
3560
+ const memory = await StrataGate.open({
3561
+ database: this.config.database,
3562
+ namespace: key,
3563
+ blockTurnSize: this.config.blockTurnSize,
3564
+ blockDecayLambda: this.blockDecayLambda,
3565
+ summarizer: this.models.summarizer,
3566
+ extractor: this.models.extractor,
3567
+ elementProjector: this.models.projector
3568
+ });
3569
+ try {
3570
+ return await memory.expandBlock(id, target, "user");
3571
+ } finally {
3572
+ await memory.close();
3573
+ }
3574
+ });
3575
+ this.settingsTail = update.then(() => {
3576
+ }, () => {
3577
+ });
3578
+ return update;
3579
+ }
3580
+ async applyBlockDecayLambda(value) {
3581
+ await this.flush();
3582
+ this.blockDecayLambda = value;
3583
+ if (this.config.database !== ":memory:") {
3584
+ const metadata = new DshMetadataStore(this.config.database);
3585
+ try {
3586
+ metadata.setBlockDecayLambda(value);
3587
+ } finally {
3588
+ metadata.close();
3589
+ }
3590
+ }
3591
+ const openNamespaces = /* @__PURE__ */ new Set();
3592
+ for (const [namespace, opening] of this.spaces) {
3593
+ const memory = await opening;
3594
+ await memory.setBlockDecayLambda(value);
3595
+ openNamespaces.add(namespace);
3596
+ }
3597
+ if (this.config.database !== ":memory:" && existsSync(this.config.database)) {
3598
+ const storage = new SqliteStorage({ filename: this.config.database });
3599
+ try {
3600
+ for (const namespace of storage.listNamespaces()) {
3601
+ if (openNamespaces.has(namespace)) continue;
3602
+ const loaded = await storage.load(namespace);
3603
+ if (!loaded || loaded.snapshot.blockDecayLambda === value) continue;
3604
+ await storage.save(namespace, { ...loaded.snapshot, blockDecayLambda: value }, loaded.revision);
3605
+ }
3606
+ } finally {
3607
+ await storage.close();
3608
+ }
3609
+ }
3610
+ }
3328
3611
  space(session) {
3329
3612
  const namespace = this.namespaceFor(session);
3613
+ this.rememberWorkspace(namespace, session.header.cwd);
3330
3614
  let opening = this.spaces.get(namespace);
3331
3615
  if (!opening) {
3332
3616
  opening = StrataGate.open({
3333
3617
  database: this.config.database,
3334
3618
  namespace,
3335
3619
  blockTurnSize: this.config.blockTurnSize,
3620
+ blockDecayLambda: this.blockDecayLambda,
3336
3621
  summarizer: this.models.summarizer,
3337
3622
  extractor: this.models.extractor,
3338
3623
  elementProjector: this.models.projector
@@ -3357,6 +3642,21 @@ var StrataGateRuntime = class {
3357
3642
  }
3358
3643
  return opening;
3359
3644
  }
3645
+ rememberWorkspace(namespace, cwd) {
3646
+ const name2 = workspaceDisplayName(cwd);
3647
+ this.workspaceNames.set(namespace, name2);
3648
+ if (this.config.database === ":memory:") return;
3649
+ try {
3650
+ const metadata = new DshMetadataStore(this.config.database);
3651
+ try {
3652
+ metadata.rememberWorkspace(namespace, name2);
3653
+ } finally {
3654
+ metadata.close();
3655
+ }
3656
+ } catch (error) {
3657
+ this.onIngestError(error);
3658
+ }
3659
+ }
3360
3660
  async persistSuccessfulResponses(memory) {
3361
3661
  if (typeof this.models.takeSuccessfulResponses !== "function") return;
3362
3662
  const responses = this.models.takeSuccessfulResponses();
@@ -3413,7 +3713,7 @@ function renderMessages(messages) {
3413
3713
  function renderBlocks2(blocks) {
3414
3714
  if (blocks.length === 0) return "(no sealed blocks)";
3415
3715
  return blocks.map((block) => [
3416
- `block ${block.id} | turns ${block.turnRange[0]}-${block.turnRange[1]} | L${block.level}`,
3716
+ `block ${block.id} | turns ${block.turnRange[0]}-${block.turnRange[1]} | age ${block.age} | L${block.level}`,
3417
3717
  block.content
3418
3718
  ].join("\n")).join("\n\n");
3419
3719
  }
@@ -3661,6 +3961,20 @@ function registerMemoryTools(ctx, runtime) {
3661
3961
  }
3662
3962
 
3663
3963
  // src/web.ts
3964
+ import { createRequire } from "node:module";
3965
+ var STRATAGATE_DSH_VERSION = "0.2.21";
3966
+ var LEGACY_THREAD_ID = "__legacy__";
3967
+ var nodeRequire = createRequire(import.meta.url);
3968
+ function installedPackageVersion(names) {
3969
+ for (const name2 of names) {
3970
+ try {
3971
+ const value = nodeRequire(`${name2}/package.json`);
3972
+ if (typeof value.version === "string" && value.version.trim()) return value.version;
3973
+ } catch {
3974
+ }
3975
+ }
3976
+ return "unknown";
3977
+ }
3664
3978
  function sendJson(res, status, body) {
3665
3979
  res.statusCode = status;
3666
3980
  res.setHeader("Content-Type", "application/json; charset=utf-8");
@@ -3703,6 +4017,17 @@ function sourceMessages(snapshot, ids) {
3703
4017
  }
3704
4018
  return output;
3705
4019
  }
4020
+ function blockLayers(block) {
4021
+ return [
4022
+ { level: 0, content: `${block.l0Title}
4023
+ \u6807\u7B7E\uFF1A${block.l0Tags.join("\u3001") || "\u65E0"}` },
4024
+ { level: 1, content: block.l1Summary || block.l0Title },
4025
+ { level: 2, content: block.l2Keypoints.map((point) => `\u2022 ${point}`).join("\n") || block.l1Summary || block.l0Title },
4026
+ { level: 3, content: block.l3Condensed || block.l2Keypoints.join("\n") || block.l1Summary },
4027
+ { level: 4, content: block.l4Readable || block.l3Condensed },
4028
+ { level: 5, content: block.l5Raw.map((message) => `${message.role}: ${message.content}`).join("\n\n") }
4029
+ ];
4030
+ }
3706
4031
  function eventSummary(event) {
3707
4032
  return {
3708
4033
  id: event.id,
@@ -3787,15 +4112,18 @@ async function overview(runtime) {
3787
4112
  ].sort();
3788
4113
  rows.push({
3789
4114
  namespace,
4115
+ workspaceName: runtime.adminWorkspaceName(namespace) ?? "\u5F53\u524D\u5DE5\u4F5C\u533A",
3790
4116
  schemaVersion: snapshot.schemaVersion,
3791
4117
  currentTurn: snapshot.currentTurn,
3792
4118
  blockTurnSize: snapshot.blockTurnSize,
4119
+ blockDecayLambda: snapshot.blockDecayLambda,
3793
4120
  blocks: snapshot.blocks.length,
3794
4121
  openTailMessages: snapshot.openTail.length,
3795
4122
  events: snapshot.events.length,
3796
4123
  activeEvents: snapshot.events.filter(({ status }) => status === "active").length,
3797
4124
  elements: snapshot.elements.length,
3798
4125
  usageReceipts: snapshot.usageReceipts.length,
4126
+ memoryUseCount: snapshot.usageReceipts.filter((receipt) => receipt.eventIds.length > 0 || receipt.elementIds.length > 0).length,
3799
4127
  failedJobs,
3800
4128
  processingJobs,
3801
4129
  failedJobDetails,
@@ -3803,7 +4131,141 @@ async function overview(runtime) {
3803
4131
  lastActivityAt: timestamps.at(-1) ?? null
3804
4132
  });
3805
4133
  }
3806
- return { readonly: true, namespaces: rows };
4134
+ return {
4135
+ readonly: true,
4136
+ settingsWritable: true,
4137
+ pluginVersion: STRATAGATE_DSH_VERSION,
4138
+ harnessVersion: installedPackageVersion(["@deepseek-ai/dsh", "@deepseek-ai/dsh-session"]),
4139
+ namespaces: rows
4140
+ };
4141
+ }
4142
+ async function updateSettings(runtime, url) {
4143
+ const raw = url.searchParams.get("blockDecayLambda")?.trim() ?? "";
4144
+ const value = Number(raw);
4145
+ if (!raw || !Number.isFinite(value) || value < 0) {
4146
+ throw new AdminHttpError(400, "blockDecayLambda must be a non-negative finite number");
4147
+ }
4148
+ return { blockDecayLambda: await runtime.adminSetBlockDecayLambda(value) };
4149
+ }
4150
+ function receiptThreadId(id) {
4151
+ const match = /^dsh:(.+):turn:\d+$/.exec(id);
4152
+ return match?.[1]?.trim() || null;
4153
+ }
4154
+ function timestampKey(value) {
4155
+ const parsed = Date.parse(value);
4156
+ return Number.isFinite(parsed) ? String(parsed) : value;
4157
+ }
4158
+ function recoverSnapshotView(snapshot) {
4159
+ const receiptThreads = /* @__PURE__ */ new Map();
4160
+ const receiptActivity = /* @__PURE__ */ new Map();
4161
+ const receiptCandidates = /* @__PURE__ */ new Map();
4162
+ for (const receipt of snapshot.ingestionReceipts) {
4163
+ const threadId = receiptThreadId(receipt.id);
4164
+ if (!threadId) continue;
4165
+ receiptThreads.set(receipt.id, threadId);
4166
+ const currentActivity = receiptActivity.get(threadId);
4167
+ if (!currentActivity || receipt.createdAt > currentActivity) receiptActivity.set(threadId, receipt.createdAt);
4168
+ const key = timestampKey(receipt.createdAt);
4169
+ const candidates = receiptCandidates.get(key) ?? /* @__PURE__ */ new Set();
4170
+ candidates.add(threadId);
4171
+ receiptCandidates.set(key, candidates);
4172
+ }
4173
+ const exactThreadAt = new Map([...receiptCandidates].filter(([, ids]) => ids.size === 1).map(([createdAt, ids]) => [createdAt, [...ids][0]]));
4174
+ const recoverMessages = (messages) => {
4175
+ let precedingThreadId = null;
4176
+ return messages.map((message) => {
4177
+ const explicit = message.threadId?.trim();
4178
+ const exact = exactThreadAt.get(timestampKey(message.createdAt));
4179
+ const recovered = explicit || exact || (message.role === "assistant" ? precedingThreadId : null);
4180
+ const threadId = recovered || LEGACY_THREAD_ID;
4181
+ if (message.role === "user" || explicit || exact) precedingThreadId = threadId;
4182
+ return { message, threadId };
4183
+ });
4184
+ };
4185
+ const blocks = [];
4186
+ for (const source of snapshot.blocks) {
4187
+ const recovered = recoverMessages(source.l5Raw);
4188
+ const groups = /* @__PURE__ */ new Map();
4189
+ for (const item of recovered) {
4190
+ const messages = groups.get(item.threadId) ?? [];
4191
+ messages.push(item.message);
4192
+ groups.set(item.threadId, messages);
4193
+ }
4194
+ const entries = [...groups];
4195
+ for (const [threadId, messages] of entries) {
4196
+ const virtual = !source.threadId && (entries.length > 1 || threadId !== LEGACY_THREAD_ID);
4197
+ blocks.push({
4198
+ id: entries.length > 1 ? `virtual:${source.id}:${encodeURIComponent(threadId)}` : source.id,
4199
+ source,
4200
+ threadId,
4201
+ messages,
4202
+ virtual,
4203
+ turnRange: [0, 0]
4204
+ });
4205
+ }
4206
+ }
4207
+ const turnCounters = /* @__PURE__ */ new Map();
4208
+ for (const block of blocks) {
4209
+ if (block.source.threadId) {
4210
+ block.turnRange = [block.source.startTurn, block.source.endTurn];
4211
+ turnCounters.set(block.threadId, Math.max(turnCounters.get(block.threadId) ?? 0, block.source.endTurn));
4212
+ continue;
4213
+ }
4214
+ const turns = Math.max(1, block.messages.filter(({ role }) => role === "user").length);
4215
+ const start = (turnCounters.get(block.threadId) ?? 0) + 1;
4216
+ block.turnRange = [start, start + turns - 1];
4217
+ turnCounters.set(block.threadId, start + turns - 1);
4218
+ }
4219
+ return {
4220
+ blocks,
4221
+ openMessages: recoverMessages(snapshot.openTail),
4222
+ receiptThreads,
4223
+ receiptActivity
4224
+ };
4225
+ }
4226
+ function virtualBlockLayers(block) {
4227
+ if (!block.virtual || block.messages.length === block.source.l5Raw.length) return blockLayers(block.source);
4228
+ const deterministic = deterministicBlockLayers(block.messages);
4229
+ const natural = block.messages.filter(({ role }) => role === "user" || role === "assistant");
4230
+ const firstUser = natural.find(({ role, content }) => role === "user" && content.trim());
4231
+ const title = firstUser?.content.replace(/\s+/g, " ").trim().slice(0, 80) || "\u65E7\u4F1A\u8BDD\u7247\u6BB5";
4232
+ const summary = natural.map(({ content }) => content.replace(/\s+/g, " ").trim()).filter(Boolean).join(" ").slice(0, 500);
4233
+ const keypoints = natural.filter(({ role }) => role === "user").map(({ content }) => content.replace(/\s+/g, " ").trim().slice(0, 160));
4234
+ return [
4235
+ { level: 0, content: title },
4236
+ { level: 1, content: summary || title },
4237
+ { level: 2, content: keypoints.map((point) => `\u2022 ${point}`).join("\n") || summary || title },
4238
+ { level: 3, content: deterministic.l3Condensed },
4239
+ { level: 4, content: deterministic.l4Readable },
4240
+ { level: 5, content: block.messages.map((message) => `${message.role}: ${message.content}`).join("\n\n") }
4241
+ ];
4242
+ }
4243
+ function conversationRows(snapshot, view = recoverSnapshotView(snapshot)) {
4244
+ const ids = /* @__PURE__ */ new Set([
4245
+ ...view.blocks.map((block) => block.threadId),
4246
+ ...view.openMessages.map(({ threadId }) => threadId),
4247
+ ...view.receiptThreads.values()
4248
+ ]);
4249
+ return [...ids].map((id) => {
4250
+ const blocks = view.blocks.filter((block) => block.threadId === id);
4251
+ const messages = [
4252
+ ...blocks.flatMap((block) => block.messages),
4253
+ ...view.openMessages.filter((message) => message.threadId === id).map(({ message }) => message)
4254
+ ];
4255
+ const firstUser = messages.find(({ role, content }) => role === "user" && content.trim());
4256
+ const title = firstUser?.content.replace(/\s+/g, " ").trim().slice(0, 28);
4257
+ const timestamps = [
4258
+ ...blocks.map(({ source }) => source.createdAt),
4259
+ ...messages.map(({ createdAt }) => createdAt),
4260
+ ...view.receiptActivity.get(id) ? [view.receiptActivity.get(id)] : []
4261
+ ].sort();
4262
+ return {
4263
+ id,
4264
+ label: id === LEGACY_THREAD_ID ? "\u5386\u53F2\u5BF9\u8BDD" : title || `\u5BF9\u8BDD ${id.slice(0, 8)}`,
4265
+ blocks: blocks.length,
4266
+ lastActivityAt: timestamps.at(-1) ?? null
4267
+ };
4268
+ }).sort((left, right) => String(right.lastActivityAt).localeCompare(String(left.lastActivityAt)));
3807
4269
  }
3808
4270
  async function memories(runtime, url) {
3809
4271
  const namespace = url.searchParams.get("namespace")?.trim() ?? "";
@@ -3819,44 +4281,85 @@ async function memories(runtime, url) {
3819
4281
  relatedElements: snapshot.elements.filter(({ sourceEventIds }) => sourceEventIds.includes(event.id)).map(({ id, name: name2 }) => ({ id, name: name2 }))
3820
4282
  }));
3821
4283
  else if (kind === "elements") values = snapshot.elements.map(elementSummary);
3822
- else if (kind === "blocks") values = snapshot.blocks.map((block) => {
3823
- const extraction = snapshot.extractionJobs.find(({ blockId }) => blockId === block.id);
3824
- const relatedEvents = snapshot.events.filter(({ sourceBlockId }) => sourceBlockId === block.id);
3825
- const eventIds = new Set(relatedEvents.map(({ id }) => id));
3826
- const projections = snapshot.elementProjectionJobs.filter(({ sourceEventIds }) => sourceEventIds.some((id) => eventIds.has(id)));
3827
- const relatedElements = snapshot.elements.filter(({ sourceEventIds }) => sourceEventIds.some((id) => eventIds.has(id))).map(({ id, name: name2 }) => ({ id, name: name2 }));
3828
- const failedProjection = projections.find(({ status: status2 }) => status2 === "failed");
3829
- const pendingProjection = projections.some(({ status: status2 }) => status2 === "pending" || status2 === "running");
3830
- const needsExtraction = block.shouldExtract === true;
3831
- const status = extraction?.status === "failed" || failedProjection ? "failed" : extraction?.status === "succeeded" || extraction?.status === "skipped" ? pendingProjection ? "processing" : "organized" : needsExtraction ? "waiting" : "organized";
4284
+ else if (kind === "blocks") {
4285
+ const recovered = recoverSnapshotView(snapshot);
4286
+ const conversations = conversationRows(snapshot, recovered);
4287
+ const requestedThreadId = url.searchParams.get("threadId")?.trim() ?? "";
4288
+ const activeThreadId = requestedThreadId || conversations[0]?.id || null;
4289
+ const scopedBlocks = activeThreadId ? recovered.blocks.filter((block) => block.threadId === activeThreadId) : [];
4290
+ values = scopedBlocks.map((block) => {
4291
+ const source = block.source;
4292
+ const extraction = snapshot.extractionJobs.find(({ blockId }) => blockId === source.id);
4293
+ const blockMessageIds = new Set(block.messages.map(({ id }) => id));
4294
+ const relatedEvents = snapshot.events.filter((event) => event.sourceBlockId === source.id && (!block.virtual || event.sourceMessageIds.some((id) => blockMessageIds.has(id))));
4295
+ const eventIds = new Set(relatedEvents.map(({ id }) => id));
4296
+ const projections = snapshot.elementProjectionJobs.filter(({ sourceEventIds }) => sourceEventIds.some((id) => eventIds.has(id)));
4297
+ const relatedElements = snapshot.elements.filter(({ sourceEventIds }) => sourceEventIds.some((id) => eventIds.has(id))).map(({ id, name: name2 }) => ({ id, name: name2 }));
4298
+ const failedProjection = projections.find(({ status: status2 }) => status2 === "failed");
4299
+ const pendingProjection = projections.some(({ status: status2 }) => status2 === "pending" || status2 === "running");
4300
+ const needsExtraction = source.shouldExtract === true;
4301
+ const status = extraction?.status === "failed" || failedProjection ? "failed" : extraction?.status === "succeeded" || extraction?.status === "skipped" ? pendingProjection ? "processing" : "organized" : needsExtraction ? "waiting" : "organized";
4302
+ const blockPosition = scopedBlocks.findIndex(({ id }) => id === block.id) + 1;
4303
+ const latestBlockPosition = scopedBlocks.length;
4304
+ const currentLevel = getDecayedBlockLevel(
4305
+ source.pointerAnchorLevel,
4306
+ source.threadId ? source.pointerAnchorBlockPosition : Math.min(source.pointerAnchorBlockPosition, blockPosition),
4307
+ latestBlockPosition,
4308
+ snapshot.blockDecayLambda
4309
+ );
4310
+ return {
4311
+ id: block.id,
4312
+ sourceBlockId: source.id,
4313
+ threadId: block.threadId,
4314
+ sequence: source.sequence,
4315
+ turnRange: block.turnRange,
4316
+ title: block.virtual && block.messages.length !== source.l5Raw.length ? block.messages.find(({ role }) => role === "user")?.content.replace(/\s+/g, " ").trim().slice(0, 80) || "\u65E7\u4F1A\u8BDD\u7247\u6BB5" : source.l0Title,
4317
+ tags: source.l0Tags,
4318
+ summary: source.l1Summary,
4319
+ keypoints: source.l2Keypoints,
4320
+ currentLevel,
4321
+ distanceFromLatest: Math.max(0, latestBlockPosition - blockPosition),
4322
+ expansionSource: source.lastLiftedAt ? source.lastLiftedBy ?? "legacy" : null,
4323
+ lastLiftedAt: source.lastLiftedAt,
4324
+ sourceMessages: block.messages.length,
4325
+ createdAt: source.createdAt,
4326
+ virtual: block.virtual,
4327
+ status,
4328
+ eventExtraction: extraction ? {
4329
+ status: extraction.status,
4330
+ attempts: extraction.attempts,
4331
+ updatedAt: extraction.updatedAt,
4332
+ lastError: extraction.lastError
4333
+ } : null,
4334
+ elementProjection: projections.length ? {
4335
+ status: failedProjection ? "failed" : pendingProjection ? "processing" : "completed",
4336
+ jobs: projections.length,
4337
+ lastError: failedProjection?.lastError ?? null
4338
+ } : null,
4339
+ relatedEvents: relatedEvents.map(eventSummary),
4340
+ relatedElements
4341
+ };
4342
+ });
4343
+ const filtered2 = values.filter((value) => matchesQuery(value, query));
4344
+ const latestSealedTurn = scopedBlocks.reduce((latest, block) => Math.max(latest, block.turnRange[1]), 0);
4345
+ const openMessages = activeThreadId ? recovered.openMessages.filter((message) => message.threadId === activeThreadId).map(({ message }) => message) : [];
4346
+ const openTurns = openMessages.filter(({ role }) => role === "user").length;
3832
4347
  return {
3833
- id: block.id,
3834
- sequence: block.sequence,
3835
- turnRange: [block.startTurn, block.endTurn],
3836
- title: block.l0Title,
3837
- tags: block.l0Tags,
3838
- summary: block.l1Summary,
3839
- keypoints: block.l2Keypoints,
3840
- currentLevel: block.pointerCurrentLevel,
3841
- sourceMessages: block.l5Raw.length,
3842
- createdAt: block.createdAt,
3843
- status,
3844
- eventExtraction: extraction ? {
3845
- status: extraction.status,
3846
- attempts: extraction.attempts,
3847
- updatedAt: extraction.updatedAt,
3848
- lastError: extraction.lastError
3849
- } : null,
3850
- elementProjection: projections.length ? {
3851
- status: failedProjection ? "failed" : pendingProjection ? "processing" : "completed",
3852
- jobs: projections.length,
3853
- lastError: failedProjection?.lastError ?? null
3854
- } : null,
3855
- relatedEvents: relatedEvents.map(eventSummary),
3856
- relatedElements
4348
+ namespace,
4349
+ kind,
4350
+ total: filtered2.length,
4351
+ offset,
4352
+ limit,
4353
+ items: filtered2.slice(offset, offset + limit),
4354
+ openBlock: {
4355
+ turnRange: openTurns > 0 ? [latestSealedTurn + 1, latestSealedTurn + openTurns] : null,
4356
+ messages: openMessages.length,
4357
+ status: "open"
4358
+ },
4359
+ conversations,
4360
+ activeThreadId
3857
4361
  };
3858
- });
3859
- else throw new AdminHttpError(400, `Unsupported memory kind: ${kind}`);
4362
+ } else throw new AdminHttpError(400, `Unsupported memory kind: ${kind}`);
3860
4363
  const filtered = values.filter((value) => matchesQuery(value, query));
3861
4364
  return { namespace, kind, total: filtered.length, offset, limit, items: filtered.slice(offset, offset + limit) };
3862
4365
  }
@@ -3882,12 +4385,22 @@ async function sources(runtime, url) {
3882
4385
  events = snapshot.events.filter(({ id }) => element.sourceEventIds.includes(id));
3883
4386
  ids = new Set(events.flatMap(({ sourceMessageIds }) => sourceMessageIds));
3884
4387
  } else if (blockId) {
3885
- const block = snapshot.blocks.find(({ id }) => id === blockId);
4388
+ const displayBlock = recoverSnapshotView(snapshot).blocks.find(({ id }) => id === blockId);
4389
+ const block = displayBlock?.source ?? snapshot.blocks.find(({ id }) => id === blockId);
3886
4390
  if (!block) throw new AdminHttpError(404, `Unknown block: ${blockId}`);
3887
- ids = new Set(block.l5Raw.map(({ id }) => id));
3888
- events = snapshot.events.filter(({ sourceBlockId }) => sourceBlockId === blockId);
4391
+ const messages = displayBlock?.messages ?? block.l5Raw;
4392
+ ids = new Set(messages.map(({ id }) => id));
4393
+ events = snapshot.events.filter((event) => event.sourceBlockId === block.id && (!displayBlock?.virtual || event.sourceMessageIds.some((id) => ids.has(id))));
3889
4394
  const eventIds = new Set(events.map(({ id }) => id));
3890
4395
  elements = snapshot.elements.filter(({ sourceEventIds }) => sourceEventIds.some((id) => eventIds.has(id)));
4396
+ return {
4397
+ namespace,
4398
+ events: events.map(eventSummary),
4399
+ elements: elements.map(elementSummary),
4400
+ messages: sourceMessages(snapshot, ids),
4401
+ layers: displayBlock ? virtualBlockLayers(displayBlock) : blockLayers(block),
4402
+ virtual: displayBlock?.virtual ?? false
4403
+ };
3891
4404
  } else {
3892
4405
  throw new AdminHttpError(400, "eventId, elementId, or blockId is required");
3893
4406
  }
@@ -3898,6 +4411,16 @@ async function sources(runtime, url) {
3898
4411
  messages: sourceMessages(snapshot, ids)
3899
4412
  };
3900
4413
  }
4414
+ async function expandBlock(runtime, url) {
4415
+ const namespace = url.searchParams.get("namespace")?.trim() ?? "";
4416
+ const blockId = url.searchParams.get("blockId")?.trim() ?? "";
4417
+ const target = url.searchParams.get("level")?.trim() ?? "";
4418
+ if (!namespace) throw new AdminHttpError(400, "namespace is required");
4419
+ if (!blockId) throw new AdminHttpError(400, "blockId is required");
4420
+ if (blockId.startsWith("virtual:")) throw new AdminHttpError(409, "Recovered legacy fragments are read-only display data");
4421
+ if (!/^L?[0-5]$/i.test(target)) throw new AdminHttpError(400, "level must be L0 through L5");
4422
+ return runtime.adminExpandBlock(namespace, blockId, target);
4423
+ }
3901
4424
  function receiptSources(snapshot, receipt) {
3902
4425
  const events = snapshot.events.filter(({ id }) => receipt.eventIds.includes(id));
3903
4426
  const elements = snapshot.elements.filter(({ id }) => receipt.elementIds.includes(id));
@@ -3928,10 +4451,16 @@ async function audit(runtime, url) {
3928
4451
  }
3929
4452
  async function handleAdminRequest(runtime, req, res) {
3930
4453
  try {
3931
- if (req.method !== "GET") throw new AdminHttpError(405, "StrataGate Memory UI is read-only");
3932
4454
  const url = new URL(req.url ?? "/", "http://localhost");
3933
4455
  const path = url.pathname.replace(/\/$/, "");
3934
- if (path === "/api/stratagate/overview") sendJson(res, 200, await overview(runtime));
4456
+ if (path === "/api/stratagate/settings") {
4457
+ if (req.method !== "PATCH") throw new AdminHttpError(405, "StrataGate settings require PATCH");
4458
+ sendJson(res, 200, await updateSettings(runtime, url));
4459
+ } else if (path === "/api/stratagate/blocks/expand") {
4460
+ if (req.method !== "PATCH") throw new AdminHttpError(405, "StrataGate Block expansion requires PATCH");
4461
+ sendJson(res, 200, await expandBlock(runtime, url));
4462
+ } else if (req.method !== "GET") throw new AdminHttpError(405, "StrataGate memory data is read-only");
4463
+ else if (path === "/api/stratagate/overview") sendJson(res, 200, await overview(runtime));
3935
4464
  else if (path === "/api/stratagate/memories") sendJson(res, 200, await memories(runtime, url));
3936
4465
  else if (path === "/api/stratagate/sources") sendJson(res, 200, await sources(runtime, url));
3937
4466
  else if (path === "/api/stratagate/audit") sendJson(res, 200, await audit(runtime, url));
@@ -3974,7 +4503,7 @@ async function apply(ctx, config) {
3974
4503
  const runtime = new StrataGateRuntime(resolved, models, (error) => {
3975
4504
  ctx.logger.error(`stratagate-memory ingestion failed: ${renderError(error)}`);
3976
4505
  });
3977
- await runtime.syncConfiguredBlockTurnSize();
4506
+ await runtime.syncConfiguredSettings();
3978
4507
  ctx.systemPrompt.section({ name: "tool:stratagate-memory", order: 113, text: MEMORY_PROTOCOL });
3979
4508
  ctx.on("system-prompt/assemble", async (_assembly, context, next) => {
3980
4509
  const assembled = await next();