stratagate-dsh 0.2.16 → 0.2.19

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 = 6;
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) };
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,45 @@ 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;
421
+ snapshot = {
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;
403
429
  snapshot = {
404
- ...structuredClone(value),
405
- schemaVersion: STRATAGATE_STORAGE_SCHEMA_VERSION
430
+ ...structuredClone(legacy),
431
+ schemaVersion: STRATAGATE_STORAGE_SCHEMA_VERSION,
432
+ blockDecayLambda: BLOCK_DECAY_LAMBDA,
433
+ blocks: migrateLegacyBlocks(legacy.blocks)
406
434
  };
407
435
  } else if (schemaVersion === STRATAGATE_STORAGE_SCHEMA_VERSION) {
408
436
  snapshot = structuredClone(value);
@@ -415,10 +443,18 @@ function normalizeSnapshot(value) {
415
443
  if (!Number.isSafeInteger(snapshot.blockTurnSize) || (snapshot.blockTurnSize ?? 0) < 1) {
416
444
  throw new TypeError("Invalid StrataGate snapshot: blockTurnSize must be a positive integer");
417
445
  }
446
+ if (!Number.isFinite(snapshot.blockDecayLambda) || snapshot.blockDecayLambda < 0) {
447
+ throw new TypeError("Invalid StrataGate snapshot: blockDecayLambda must be a non-negative finite number");
448
+ }
418
449
  for (const key of ["openTail", "blocks", "events", "elements", "extractionJobs", "elementProjectionJobs", "usageReceipts", "ingestionReceipts"]) {
419
450
  if (!Array.isArray(snapshot[key])) throw new TypeError(`Invalid StrataGate snapshot: ${key} must be an array`);
420
451
  }
421
452
  if (!Array.isArray(snapshot.successfulModelResponses)) snapshot.successfulModelResponses = [];
453
+ for (const block of snapshot.blocks) {
454
+ if (!Number.isSafeInteger(block.pointerAnchorBlockPosition) || block.pointerAnchorBlockPosition < 1) {
455
+ throw new TypeError("Invalid StrataGate snapshot: pointerAnchorBlockPosition must be a positive integer");
456
+ }
457
+ }
422
458
  if (snapshot.successfulModelResponses.length > 5) {
423
459
  snapshot.successfulModelResponses = snapshot.successfulModelResponses.slice(-5);
424
460
  }
@@ -592,6 +628,7 @@ CREATE TABLE IF NOT EXISTS memory_spaces (
592
628
  revision INTEGER NOT NULL,
593
629
  current_turn INTEGER NOT NULL,
594
630
  block_turn_size INTEGER NOT NULL,
631
+ block_decay_lambda REAL NOT NULL,
595
632
  created_at TEXT NOT NULL,
596
633
  updated_at TEXT NOT NULL
597
634
  ) STRICT;
@@ -613,7 +650,7 @@ CREATE TABLE IF NOT EXISTS blocks (
613
650
  l4_readable TEXT NOT NULL,
614
651
  pointer_current_level INTEGER NOT NULL,
615
652
  pointer_anchor_level INTEGER NOT NULL,
616
- pointer_anchor_turn INTEGER NOT NULL,
653
+ pointer_anchor_block_position INTEGER NOT NULL,
617
654
  last_lifted_at TEXT,
618
655
  PRIMARY KEY (namespace, id),
619
656
  UNIQUE (namespace, sequence),
@@ -831,7 +868,7 @@ var SqliteStorage = class {
831
868
  this.assertOpen();
832
869
  const key = nonEmptyNamespace(namespace);
833
870
  const space = this.database.prepare(`
834
- SELECT schema_version, revision, current_turn, block_turn_size
871
+ SELECT schema_version, revision, current_turn, block_turn_size, block_decay_lambda
835
872
  FROM memory_spaces WHERE namespace = ?
836
873
  `).get(key);
837
874
  if (!space) return null;
@@ -880,7 +917,7 @@ var SqliteStorage = class {
880
917
  l5Raw: messagesByBlock.get(row.id) ?? [],
881
918
  pointerCurrentLevel: row.pointer_current_level,
882
919
  pointerAnchorLevel: row.pointer_anchor_level,
883
- pointerAnchorTurn: row.pointer_anchor_turn,
920
+ pointerAnchorBlockPosition: row.pointer_anchor_block_position,
884
921
  lastLiftedAt: row.last_lifted_at
885
922
  }));
886
923
  const sourceRows = this.database.prepare(`
@@ -1047,6 +1084,7 @@ var SqliteStorage = class {
1047
1084
  schemaVersion: STRATAGATE_STORAGE_SCHEMA_VERSION,
1048
1085
  currentTurn: space.current_turn,
1049
1086
  blockTurnSize: space.block_turn_size,
1087
+ blockDecayLambda: space.block_decay_lambda,
1050
1088
  openTail,
1051
1089
  blocks,
1052
1090
  events,
@@ -1085,27 +1123,29 @@ var SqliteStorage = class {
1085
1123
  if (current) {
1086
1124
  this.database.prepare(`
1087
1125
  UPDATE memory_spaces
1088
- SET schema_version = ?, revision = ?, current_turn = ?, block_turn_size = ?, updated_at = ?
1126
+ SET schema_version = ?, revision = ?, current_turn = ?, block_turn_size = ?, block_decay_lambda = ?, updated_at = ?
1089
1127
  WHERE namespace = ?
1090
1128
  `).run(
1091
1129
  snapshot.schemaVersion,
1092
1130
  nextRevision,
1093
1131
  snapshot.currentTurn,
1094
1132
  snapshot.blockTurnSize,
1133
+ snapshot.blockDecayLambda,
1095
1134
  updatedAt,
1096
1135
  namespace
1097
1136
  );
1098
1137
  } else {
1099
1138
  this.database.prepare(`
1100
1139
  INSERT INTO memory_spaces (
1101
- namespace, schema_version, revision, current_turn, block_turn_size, created_at, updated_at
1102
- ) VALUES (?, ?, ?, ?, ?, ?, ?)
1140
+ namespace, schema_version, revision, current_turn, block_turn_size, block_decay_lambda, created_at, updated_at
1141
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1103
1142
  `).run(
1104
1143
  namespace,
1105
1144
  snapshot.schemaVersion,
1106
1145
  nextRevision,
1107
1146
  snapshot.currentTurn,
1108
1147
  snapshot.blockTurnSize,
1148
+ snapshot.blockDecayLambda,
1109
1149
  updatedAt,
1110
1150
  updatedAt
1111
1151
  );
@@ -1114,7 +1154,7 @@ var SqliteStorage = class {
1114
1154
  INSERT INTO blocks (
1115
1155
  namespace, id, thread_id, sequence, start_turn, end_turn, created_at, should_extract,
1116
1156
  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
1157
+ pointer_current_level, pointer_anchor_level, pointer_anchor_block_position, last_lifted_at
1118
1158
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1119
1159
  ON CONFLICT (namespace, id) DO UPDATE SET
1120
1160
  thread_id = excluded.thread_id,
@@ -1131,7 +1171,7 @@ var SqliteStorage = class {
1131
1171
  l4_readable = excluded.l4_readable,
1132
1172
  pointer_current_level = excluded.pointer_current_level,
1133
1173
  pointer_anchor_level = excluded.pointer_anchor_level,
1134
- pointer_anchor_turn = excluded.pointer_anchor_turn,
1174
+ pointer_anchor_block_position = excluded.pointer_anchor_block_position,
1135
1175
  last_lifted_at = excluded.last_lifted_at
1136
1176
  `);
1137
1177
  for (const block of snapshot.blocks) {
@@ -1152,7 +1192,7 @@ var SqliteStorage = class {
1152
1192
  block.l4Readable,
1153
1193
  block.pointerCurrentLevel,
1154
1194
  block.pointerAnchorLevel,
1155
- block.pointerAnchorTurn,
1195
+ block.pointerAnchorBlockPosition,
1156
1196
  block.lastLiftedAt
1157
1197
  );
1158
1198
  }
@@ -1422,8 +1462,9 @@ var SqliteStorage = class {
1422
1462
  this.database.exec(THREAD_INDEXES);
1423
1463
  this.database.exec(`PRAGMA user_version = ${STRATAGATE_STORAGE_SCHEMA_VERSION}`);
1424
1464
  });
1425
- } else if (version === 1 || version === 2 || version === 3 || version === 4) {
1465
+ } else if (version === 1 || version === 2 || version === 3 || version === 4 || version === 5) {
1426
1466
  this.immediateTransaction(() => {
1467
+ this.database.exec(SCHEMA);
1427
1468
  if (version === 1) {
1428
1469
  const receiptColumns2 = this.database.prepare("PRAGMA table_info('usage_receipts')").all();
1429
1470
  if (!receiptColumns2.some(({ name: name2 }) => name2 === "element_ids_json")) {
@@ -1434,11 +1475,26 @@ var SqliteStorage = class {
1434
1475
  if (!receiptColumns.some(({ name: name2 }) => name2 === "audit_json")) {
1435
1476
  this.database.exec("ALTER TABLE usage_receipts ADD COLUMN audit_json TEXT NOT NULL DEFAULT '{}'");
1436
1477
  }
1437
- this.database.exec(SCHEMA);
1478
+ const spaceColumns = this.database.prepare("PRAGMA table_info('memory_spaces')").all();
1479
+ if (!spaceColumns.some(({ name: name2 }) => name2 === "block_decay_lambda")) {
1480
+ this.database.exec("ALTER TABLE memory_spaces ADD COLUMN block_decay_lambda REAL NOT NULL DEFAULT 0.3");
1481
+ }
1438
1482
  const blockColumns = this.database.prepare("PRAGMA table_info('blocks')").all();
1439
1483
  if (!blockColumns.some(({ name: name2 }) => name2 === "thread_id")) {
1440
1484
  this.database.exec("ALTER TABLE blocks ADD COLUMN thread_id TEXT");
1441
1485
  }
1486
+ if (!blockColumns.some(({ name: name2 }) => name2 === "pointer_anchor_block_position")) {
1487
+ this.database.exec("ALTER TABLE blocks RENAME COLUMN pointer_anchor_turn TO pointer_anchor_block_position");
1488
+ this.database.exec(`
1489
+ UPDATE blocks AS target
1490
+ SET pointer_anchor_block_position = MAX(1, (
1491
+ SELECT COUNT(*) FROM blocks AS candidate
1492
+ WHERE candidate.namespace = target.namespace
1493
+ AND candidate.thread_id IS target.thread_id
1494
+ AND candidate.end_turn <= target.pointer_anchor_block_position
1495
+ ))
1496
+ `);
1497
+ }
1442
1498
  const messageColumns = this.database.prepare("PRAGMA table_info('messages')").all();
1443
1499
  if (!messageColumns.some(({ name: name2 }) => name2 === "thread_id")) {
1444
1500
  this.database.exec("ALTER TABLE messages ADD COLUMN thread_id TEXT");
@@ -1526,6 +1582,7 @@ function errorMessage(error) {
1526
1582
  var STRATAGATE_CONSTRUCTOR_TOKEN = /* @__PURE__ */ Symbol("StrataGate constructor");
1527
1583
  var StrataGate = class _StrataGate {
1528
1584
  blockTurnSize;
1585
+ blockDecayLambdaValue;
1529
1586
  summarizer;
1530
1587
  extractor;
1531
1588
  elementProjector;
@@ -1551,6 +1608,11 @@ var StrataGate = class _StrataGate {
1551
1608
  throw new TypeError("Use StrataGate.open() for SQLite or StrataGate.inMemory() for explicit ephemeral storage");
1552
1609
  }
1553
1610
  this.blockTurnSize = Math.max(1, Math.floor(options.blockTurnSize ?? DEFAULT_BLOCK_TURN_SIZE));
1611
+ const blockDecayLambda = options.blockDecayLambda ?? BLOCK_DECAY_LAMBDA;
1612
+ if (!Number.isFinite(blockDecayLambda) || blockDecayLambda < 0) {
1613
+ throw new TypeError("blockDecayLambda must be a non-negative finite number");
1614
+ }
1615
+ this.blockDecayLambdaValue = blockDecayLambda;
1554
1616
  this.summarizer = options.summarizer;
1555
1617
  this.extractor = options.extractor;
1556
1618
  this.elementProjector = options.elementProjector;
@@ -1573,6 +1635,7 @@ var StrataGate = class _StrataGate {
1573
1635
  storage,
1574
1636
  namespace: options.namespace,
1575
1637
  ...options.blockTurnSize !== void 0 ? { blockTurnSize: options.blockTurnSize } : {},
1638
+ ...options.blockDecayLambda !== void 0 ? { blockDecayLambda: options.blockDecayLambda } : {},
1576
1639
  ...options.summarizer ? { summarizer: options.summarizer } : {},
1577
1640
  ...options.extractor ? { extractor: options.extractor } : {},
1578
1641
  ...options.elementProjector ? { elementProjector: options.elementProjector } : {},
@@ -1591,17 +1654,32 @@ var StrataGate = class _StrataGate {
1591
1654
  const loaded = await options.storage.load(namespace);
1592
1655
  const loadedSnapshot = loaded ? normalizeSnapshot(loaded.snapshot) : null;
1593
1656
  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);
1657
+ if (loaded && loadedSnapshot) {
1658
+ let settingsChanged = false;
1659
+ if (options.blockTurnSize !== void 0) {
1660
+ const requested = Math.max(1, Math.floor(options.blockTurnSize));
1661
+ if (requested !== loadedSnapshot.blockTurnSize) {
1662
+ loadedSnapshot.blockTurnSize = requested;
1663
+ settingsChanged = true;
1664
+ }
1600
1665
  }
1666
+ if (options.blockDecayLambda !== void 0) {
1667
+ const requested = options.blockDecayLambda;
1668
+ if (!Number.isFinite(requested) || requested < 0) {
1669
+ throw new TypeError("blockDecayLambda must be a non-negative finite number");
1670
+ }
1671
+ if (requested !== loadedSnapshot.blockDecayLambda) {
1672
+ loadedSnapshot.blockDecayLambda = requested;
1673
+ settingsChanged = true;
1674
+ }
1675
+ }
1676
+ if (settingsChanged) loadedRevision = await options.storage.save(namespace, loadedSnapshot, loadedRevision);
1601
1677
  }
1602
1678
  const memoryOptions = {};
1603
1679
  if (loadedSnapshot) memoryOptions.blockTurnSize = loadedSnapshot.blockTurnSize;
1604
1680
  else if (options.blockTurnSize !== void 0) memoryOptions.blockTurnSize = options.blockTurnSize;
1681
+ if (loadedSnapshot) memoryOptions.blockDecayLambda = loadedSnapshot.blockDecayLambda;
1682
+ else if (options.blockDecayLambda !== void 0) memoryOptions.blockDecayLambda = options.blockDecayLambda;
1605
1683
  if (options.summarizer) memoryOptions.summarizer = options.summarizer;
1606
1684
  if (options.extractor) memoryOptions.extractor = options.extractor;
1607
1685
  if (options.elementProjector) memoryOptions.elementProjector = options.elementProjector;
@@ -1653,6 +1731,18 @@ var StrataGate = class _StrataGate {
1653
1731
  get storageRevision() {
1654
1732
  return this.revision;
1655
1733
  }
1734
+ get blockDecayLambda() {
1735
+ return this.blockDecayLambdaValue;
1736
+ }
1737
+ async setBlockDecayLambda(value) {
1738
+ if (!Number.isFinite(value) || value < 0) {
1739
+ throw new TypeError("blockDecayLambda must be a non-negative finite number");
1740
+ }
1741
+ if (value === this.blockDecayLambdaValue) return;
1742
+ await this.commitMutation(() => {
1743
+ this.blockDecayLambdaValue = value;
1744
+ });
1745
+ }
1656
1746
  listBlocks() {
1657
1747
  return this.blocks;
1658
1748
  }
@@ -1695,6 +1785,7 @@ var StrataGate = class _StrataGate {
1695
1785
  schemaVersion: STRATAGATE_STORAGE_SCHEMA_VERSION,
1696
1786
  currentTurn: this.currentTurn,
1697
1787
  blockTurnSize: this.blockTurnSize,
1788
+ blockDecayLambda: this.blockDecayLambda,
1698
1789
  openTail: this.openTail,
1699
1790
  blocks: this.blocks,
1700
1791
  events: this.events,
@@ -1995,13 +2086,22 @@ var StrataGate = class _StrataGate {
1995
2086
  getBlockContext(threadId) {
1996
2087
  const blocks = threadId === void 0 ? this.blocks : this.blocks.filter((block) => block.threadId === threadId);
1997
2088
  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);
2089
+ const threadBlocks = this.threadBlocks(block.threadId);
2090
+ const latestBlockPosition = threadBlocks.length;
2091
+ const blockPosition = threadBlocks.indexOf(block) + 1;
2092
+ const age = Math.max(0, latestBlockPosition - blockPosition);
2093
+ const level = getDecayedBlockLevel(
2094
+ block.pointerAnchorLevel,
2095
+ block.pointerAnchorBlockPosition,
2096
+ latestBlockPosition,
2097
+ this.blockDecayLambda
2098
+ );
2000
2099
  block.pointerCurrentLevel = level;
2001
2100
  return {
2002
2101
  id: block.id,
2003
2102
  ...block.threadId ? { threadId: block.threadId } : {},
2004
2103
  turnRange: [block.startTurn, block.endTurn],
2104
+ age,
2005
2105
  level,
2006
2106
  label: blockLevelLabel(level),
2007
2107
  content: renderBlock(block, level)
@@ -2012,17 +2112,24 @@ var StrataGate = class _StrataGate {
2012
2112
  return this.commitMutation(() => {
2013
2113
  const block = this.blocks.find((candidate) => candidate.id === id);
2014
2114
  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);
2115
+ const latestBlockPosition = this.threadBlocks(block.threadId).length;
2116
+ const blockPosition = this.threadBlocks(block.threadId).indexOf(block) + 1;
2117
+ const current = getDecayedBlockLevel(
2118
+ block.pointerAnchorLevel,
2119
+ block.pointerAnchorBlockPosition,
2120
+ latestBlockPosition,
2121
+ this.blockDecayLambda
2122
+ );
2017
2123
  const level = normalizeBlockLevel(target, current);
2018
2124
  block.pointerCurrentLevel = level;
2019
2125
  block.pointerAnchorLevel = level;
2020
- block.pointerAnchorTurn = currentTurn;
2126
+ block.pointerAnchorBlockPosition = latestBlockPosition;
2021
2127
  block.lastLiftedAt = toUtc8Iso(this.now());
2022
2128
  return {
2023
2129
  id: block.id,
2024
2130
  ...block.threadId ? { threadId: block.threadId } : {},
2025
2131
  turnRange: [block.startTurn, block.endTurn],
2132
+ age: Math.max(0, latestBlockPosition - blockPosition),
2026
2133
  level,
2027
2134
  label: blockLevelLabel(level),
2028
2135
  content: renderBlock(block, level)
@@ -2221,7 +2328,9 @@ var StrataGate = class _StrataGate {
2221
2328
  const generated = this.summarizer ? await this.summarizer(raw) : defaultSummary(raw);
2222
2329
  const deterministic = deterministicBlockLayers(raw);
2223
2330
  const sequence = this.blocks.length + 1;
2224
- const previous = this.threadBlocks(threadId).at(-1);
2331
+ const threadBlocks = this.threadBlocks(threadId);
2332
+ const previous = threadBlocks.at(-1);
2333
+ const blockPosition = threadBlocks.length + 1;
2225
2334
  const startTurn = previous ? previous.endTurn + 1 : 1;
2226
2335
  const endTurn = startTurn + this.blockTurnSize - 1;
2227
2336
  return this.commitMutation(() => {
@@ -2244,7 +2353,7 @@ var StrataGate = class _StrataGate {
2244
2353
  ...deterministic,
2245
2354
  pointerCurrentLevel: 5,
2246
2355
  pointerAnchorLevel: 5,
2247
- pointerAnchorTurn: endTurn,
2356
+ pointerAnchorBlockPosition: blockPosition,
2248
2357
  lastLiftedAt: null
2249
2358
  };
2250
2359
  const sealedIds = new Set(raw.map((message) => message.id));
@@ -2381,6 +2490,7 @@ var StrataGate = class _StrataGate {
2381
2490
  throw new Error(`Snapshot blockTurnSize ${normalized.blockTurnSize} does not match ${this.blockTurnSize}`);
2382
2491
  }
2383
2492
  const copy = cloneSnapshot(normalized);
2493
+ this.blockDecayLambdaValue = copy.blockDecayLambda;
2384
2494
  this.currentTurn = copy.currentTurn;
2385
2495
  this.openTail.splice(0, this.openTail.length, ...copy.openTail);
2386
2496
  this.blocks.splice(0, this.blocks.length, ...copy.blocks);
@@ -3051,6 +3161,60 @@ var TurnFolder = class {
3051
3161
  }
3052
3162
  };
3053
3163
 
3164
+ // src/metadata.ts
3165
+ import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
3166
+ var METADATA_SCHEMA = `
3167
+ CREATE TABLE IF NOT EXISTS stratagate_dsh_settings (
3168
+ key TEXT PRIMARY KEY,
3169
+ value TEXT NOT NULL,
3170
+ updated_at TEXT NOT NULL
3171
+ ) STRICT;
3172
+
3173
+ CREATE TABLE IF NOT EXISTS stratagate_dsh_workspaces (
3174
+ namespace TEXT PRIMARY KEY,
3175
+ display_name TEXT NOT NULL,
3176
+ updated_at TEXT NOT NULL
3177
+ ) STRICT;
3178
+ `;
3179
+ var DshMetadataStore = class {
3180
+ database;
3181
+ constructor(filename) {
3182
+ this.database = new DatabaseSync2(filename);
3183
+ this.database.exec(METADATA_SCHEMA);
3184
+ }
3185
+ blockDecayLambda() {
3186
+ const row = this.database.prepare("SELECT value FROM stratagate_dsh_settings WHERE key = 'blockDecayLambda'").get();
3187
+ const value = Number(row?.value);
3188
+ return Number.isFinite(value) && value >= 0 ? value : null;
3189
+ }
3190
+ setBlockDecayLambda(value) {
3191
+ if (!Number.isFinite(value) || value < 0) {
3192
+ throw new TypeError("blockDecayLambda must be a non-negative finite number");
3193
+ }
3194
+ this.database.prepare(`
3195
+ INSERT INTO stratagate_dsh_settings (key, value, updated_at)
3196
+ VALUES ('blockDecayLambda', ?, ?)
3197
+ ON CONFLICT (key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at
3198
+ `).run(String(value), (/* @__PURE__ */ new Date()).toISOString());
3199
+ }
3200
+ workspaceName(namespace) {
3201
+ const row = this.database.prepare("SELECT display_name FROM stratagate_dsh_workspaces WHERE namespace = ?").get(namespace);
3202
+ return row?.display_name ?? null;
3203
+ }
3204
+ rememberWorkspace(namespace, displayName) {
3205
+ const name2 = displayName.trim();
3206
+ if (!namespace.trim() || !name2) return;
3207
+ this.database.prepare(`
3208
+ INSERT INTO stratagate_dsh_workspaces (namespace, display_name, updated_at)
3209
+ VALUES (?, ?, ?)
3210
+ ON CONFLICT (namespace) DO UPDATE SET display_name = excluded.display_name, updated_at = excluded.updated_at
3211
+ `).run(namespace, name2, (/* @__PURE__ */ new Date()).toISOString());
3212
+ }
3213
+ close() {
3214
+ this.database.close();
3215
+ }
3216
+ };
3217
+
3054
3218
  // src/runtime.ts
3055
3219
  var AUTO_EVENT_LIMIT = 4;
3056
3220
  var AUTO_ELEMENT_LIMIT = 4;
@@ -3059,12 +3223,17 @@ function projectKey(cwd) {
3059
3223
  const canonical = resolve(cwd ?? process.cwd()).replaceAll("\\", "/").toLowerCase();
3060
3224
  return createHash("sha256").update(canonical).digest("hex").slice(0, 20);
3061
3225
  }
3226
+ function workspaceDisplayName(cwd) {
3227
+ const canonical = (cwd ?? process.cwd()).replace(/[\\/]+$/, "");
3228
+ return canonical.split(/[\\/]/).at(-1) || "\u5F53\u524D\u5DE5\u4F5C\u533A";
3229
+ }
3062
3230
  var StrataGateRuntime = class {
3063
3231
  constructor(config, models, onIngestError = () => {
3064
3232
  }) {
3065
3233
  this.config = config;
3066
3234
  this.models = models;
3067
3235
  this.onIngestError = onIngestError;
3236
+ this.blockDecayLambda = config.blockDecayLambda;
3068
3237
  }
3069
3238
  config;
3070
3239
  models;
@@ -3074,10 +3243,13 @@ var StrataGateRuntime = class {
3074
3243
  batches = /* @__PURE__ */ new Map();
3075
3244
  adopted = /* @__PURE__ */ new Map();
3076
3245
  pendingUse = /* @__PURE__ */ new Set();
3246
+ workspaceNames = /* @__PURE__ */ new Map();
3077
3247
  ingestTail = Promise.resolve();
3248
+ settingsTail = Promise.resolve();
3078
3249
  batchSequence = 0;
3079
3250
  closed = false;
3080
3251
  ingestError;
3252
+ blockDecayLambda;
3081
3253
  acceptEvent(session, event) {
3082
3254
  if (this.closed) return;
3083
3255
  if (!this.config.ingestSubagents && session.header.origin === "subagent") return;
@@ -3298,16 +3470,23 @@ var StrataGateRuntime = class {
3298
3470
  await storage.close();
3299
3471
  }
3300
3472
  }
3301
- async syncConfiguredBlockTurnSize() {
3473
+ async syncConfiguredSettings() {
3302
3474
  if (this.config.database === ":memory:" || !existsSync(this.config.database)) return;
3475
+ const metadata = new DshMetadataStore(this.config.database);
3476
+ try {
3477
+ this.blockDecayLambda = metadata.blockDecayLambda() ?? this.config.blockDecayLambda;
3478
+ } finally {
3479
+ metadata.close();
3480
+ }
3303
3481
  const storage = new SqliteStorage({ filename: this.config.database });
3304
3482
  try {
3305
3483
  for (const namespace of storage.listNamespaces()) {
3306
3484
  const loaded = await storage.load(namespace);
3307
- if (!loaded || loaded.snapshot.blockTurnSize === this.config.blockTurnSize) continue;
3485
+ if (!loaded || loaded.snapshot.blockTurnSize === this.config.blockTurnSize && loaded.snapshot.blockDecayLambda === this.blockDecayLambda) continue;
3308
3486
  await storage.save(namespace, {
3309
3487
  ...loaded.snapshot,
3310
- blockTurnSize: this.config.blockTurnSize
3488
+ blockTurnSize: this.config.blockTurnSize,
3489
+ blockDecayLambda: this.blockDecayLambda
3311
3490
  }, loaded.revision);
3312
3491
  }
3313
3492
  } finally {
@@ -3325,14 +3504,70 @@ var StrataGateRuntime = class {
3325
3504
  await storage.close();
3326
3505
  }
3327
3506
  }
3507
+ adminWorkspaceName(namespace) {
3508
+ const remembered = this.workspaceNames.get(namespace);
3509
+ if (remembered) return remembered;
3510
+ if (this.config.database === ":memory:" || !existsSync(this.config.database)) return null;
3511
+ const metadata = new DshMetadataStore(this.config.database);
3512
+ try {
3513
+ return metadata.workspaceName(namespace);
3514
+ } finally {
3515
+ metadata.close();
3516
+ }
3517
+ }
3518
+ async adminSetBlockDecayLambda(value) {
3519
+ if (!Number.isFinite(value) || value < 0) {
3520
+ throw new TypeError("blockDecayLambda must be a non-negative finite number");
3521
+ }
3522
+ const update = this.settingsTail.catch(() => {
3523
+ }).then(() => this.applyBlockDecayLambda(value));
3524
+ this.settingsTail = update.then(() => {
3525
+ }, () => {
3526
+ });
3527
+ await update;
3528
+ return value;
3529
+ }
3530
+ async applyBlockDecayLambda(value) {
3531
+ await this.flush();
3532
+ this.blockDecayLambda = value;
3533
+ if (this.config.database !== ":memory:") {
3534
+ const metadata = new DshMetadataStore(this.config.database);
3535
+ try {
3536
+ metadata.setBlockDecayLambda(value);
3537
+ } finally {
3538
+ metadata.close();
3539
+ }
3540
+ }
3541
+ const openNamespaces = /* @__PURE__ */ new Set();
3542
+ for (const [namespace, opening] of this.spaces) {
3543
+ const memory = await opening;
3544
+ await memory.setBlockDecayLambda(value);
3545
+ openNamespaces.add(namespace);
3546
+ }
3547
+ if (this.config.database !== ":memory:" && existsSync(this.config.database)) {
3548
+ const storage = new SqliteStorage({ filename: this.config.database });
3549
+ try {
3550
+ for (const namespace of storage.listNamespaces()) {
3551
+ if (openNamespaces.has(namespace)) continue;
3552
+ const loaded = await storage.load(namespace);
3553
+ if (!loaded || loaded.snapshot.blockDecayLambda === value) continue;
3554
+ await storage.save(namespace, { ...loaded.snapshot, blockDecayLambda: value }, loaded.revision);
3555
+ }
3556
+ } finally {
3557
+ await storage.close();
3558
+ }
3559
+ }
3560
+ }
3328
3561
  space(session) {
3329
3562
  const namespace = this.namespaceFor(session);
3563
+ this.rememberWorkspace(namespace, session.header.cwd);
3330
3564
  let opening = this.spaces.get(namespace);
3331
3565
  if (!opening) {
3332
3566
  opening = StrataGate.open({
3333
3567
  database: this.config.database,
3334
3568
  namespace,
3335
3569
  blockTurnSize: this.config.blockTurnSize,
3570
+ blockDecayLambda: this.blockDecayLambda,
3336
3571
  summarizer: this.models.summarizer,
3337
3572
  extractor: this.models.extractor,
3338
3573
  elementProjector: this.models.projector
@@ -3357,6 +3592,21 @@ var StrataGateRuntime = class {
3357
3592
  }
3358
3593
  return opening;
3359
3594
  }
3595
+ rememberWorkspace(namespace, cwd) {
3596
+ const name2 = workspaceDisplayName(cwd);
3597
+ this.workspaceNames.set(namespace, name2);
3598
+ if (this.config.database === ":memory:") return;
3599
+ try {
3600
+ const metadata = new DshMetadataStore(this.config.database);
3601
+ try {
3602
+ metadata.rememberWorkspace(namespace, name2);
3603
+ } finally {
3604
+ metadata.close();
3605
+ }
3606
+ } catch (error) {
3607
+ this.onIngestError(error);
3608
+ }
3609
+ }
3360
3610
  async persistSuccessfulResponses(memory) {
3361
3611
  if (typeof this.models.takeSuccessfulResponses !== "function") return;
3362
3612
  const responses = this.models.takeSuccessfulResponses();
@@ -3413,7 +3663,7 @@ function renderMessages(messages) {
3413
3663
  function renderBlocks2(blocks) {
3414
3664
  if (blocks.length === 0) return "(no sealed blocks)";
3415
3665
  return blocks.map((block) => [
3416
- `block ${block.id} | turns ${block.turnRange[0]}-${block.turnRange[1]} | L${block.level}`,
3666
+ `block ${block.id} | turns ${block.turnRange[0]}-${block.turnRange[1]} | age ${block.age} | L${block.level}`,
3417
3667
  block.content
3418
3668
  ].join("\n")).join("\n\n");
3419
3669
  }
@@ -3787,15 +4037,18 @@ async function overview(runtime) {
3787
4037
  ].sort();
3788
4038
  rows.push({
3789
4039
  namespace,
4040
+ workspaceName: runtime.adminWorkspaceName(namespace) ?? "\u5F53\u524D\u5DE5\u4F5C\u533A",
3790
4041
  schemaVersion: snapshot.schemaVersion,
3791
4042
  currentTurn: snapshot.currentTurn,
3792
4043
  blockTurnSize: snapshot.blockTurnSize,
4044
+ blockDecayLambda: snapshot.blockDecayLambda,
3793
4045
  blocks: snapshot.blocks.length,
3794
4046
  openTailMessages: snapshot.openTail.length,
3795
4047
  events: snapshot.events.length,
3796
4048
  activeEvents: snapshot.events.filter(({ status }) => status === "active").length,
3797
4049
  elements: snapshot.elements.length,
3798
4050
  usageReceipts: snapshot.usageReceipts.length,
4051
+ memoryUseCount: snapshot.usageReceipts.filter((receipt) => receipt.eventIds.length > 0 || receipt.elementIds.length > 0).length,
3799
4052
  failedJobs,
3800
4053
  processingJobs,
3801
4054
  failedJobDetails,
@@ -3803,7 +4056,15 @@ async function overview(runtime) {
3803
4056
  lastActivityAt: timestamps.at(-1) ?? null
3804
4057
  });
3805
4058
  }
3806
- return { readonly: true, namespaces: rows };
4059
+ return { readonly: true, settingsWritable: true, namespaces: rows };
4060
+ }
4061
+ async function updateSettings(runtime, url) {
4062
+ const raw = url.searchParams.get("blockDecayLambda")?.trim() ?? "";
4063
+ const value = Number(raw);
4064
+ if (!raw || !Number.isFinite(value) || value < 0) {
4065
+ throw new AdminHttpError(400, "blockDecayLambda must be a non-negative finite number");
4066
+ }
4067
+ return { blockDecayLambda: await runtime.adminSetBlockDecayLambda(value) };
3807
4068
  }
3808
4069
  async function memories(runtime, url) {
3809
4070
  const namespace = url.searchParams.get("namespace")?.trim() ?? "";
@@ -3928,10 +4189,13 @@ async function audit(runtime, url) {
3928
4189
  }
3929
4190
  async function handleAdminRequest(runtime, req, res) {
3930
4191
  try {
3931
- if (req.method !== "GET") throw new AdminHttpError(405, "StrataGate Memory UI is read-only");
3932
4192
  const url = new URL(req.url ?? "/", "http://localhost");
3933
4193
  const path = url.pathname.replace(/\/$/, "");
3934
- if (path === "/api/stratagate/overview") sendJson(res, 200, await overview(runtime));
4194
+ if (path === "/api/stratagate/settings") {
4195
+ if (req.method !== "PATCH") throw new AdminHttpError(405, "StrataGate settings require PATCH");
4196
+ sendJson(res, 200, await updateSettings(runtime, url));
4197
+ } else if (req.method !== "GET") throw new AdminHttpError(405, "StrataGate memory data is read-only");
4198
+ else if (path === "/api/stratagate/overview") sendJson(res, 200, await overview(runtime));
3935
4199
  else if (path === "/api/stratagate/memories") sendJson(res, 200, await memories(runtime, url));
3936
4200
  else if (path === "/api/stratagate/sources") sendJson(res, 200, await sources(runtime, url));
3937
4201
  else if (path === "/api/stratagate/audit") sendJson(res, 200, await audit(runtime, url));
@@ -3974,7 +4238,7 @@ async function apply(ctx, config) {
3974
4238
  const runtime = new StrataGateRuntime(resolved, models, (error) => {
3975
4239
  ctx.logger.error(`stratagate-memory ingestion failed: ${renderError(error)}`);
3976
4240
  });
3977
- await runtime.syncConfiguredBlockTurnSize();
4241
+ await runtime.syncConfiguredSettings();
3978
4242
  ctx.systemPrompt.section({ name: "tool:stratagate-memory", order: 113, text: MEMORY_PROTOCOL });
3979
4243
  ctx.on("system-prompt/assemble", async (_assembly, context, next) => {
3980
4244
  const assembled = await next();