stratagate-dsh 0.2.19 → 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
@@ -360,7 +360,7 @@ function rrfRank(rankings) {
360
360
  }
361
361
 
362
362
  // ../../src/storage.ts
363
- var STRATAGATE_STORAGE_SCHEMA_VERSION = 6;
363
+ var STRATAGATE_STORAGE_SCHEMA_VERSION = 7;
364
364
  var StorageConflictError = class extends Error {
365
365
  constructor(namespace, expectedRevision, actualRevision) {
366
366
  super(`Storage revision conflict for ${namespace}: expected ${expectedRevision}, found ${actualRevision ?? "missing"}`);
@@ -380,7 +380,7 @@ function migrateLegacyBlocks(blocks) {
380
380
  return blocks.map((block) => {
381
381
  const { pointerAnchorTurn, ...current } = block;
382
382
  const position = blocks.filter((candidate) => candidate.threadId === block.threadId && candidate.endTurn <= pointerAnchorTurn).length;
383
- return { ...current, pointerAnchorBlockPosition: Math.max(1, position) };
383
+ return { ...current, pointerAnchorBlockPosition: Math.max(1, position), lastLiftedBy: null };
384
384
  });
385
385
  }
386
386
  function normalizeSnapshot(value) {
@@ -432,6 +432,13 @@ function normalizeSnapshot(value) {
432
432
  blockDecayLambda: BLOCK_DECAY_LAMBDA,
433
433
  blocks: migrateLegacyBlocks(legacy.blocks)
434
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 }))
441
+ };
435
442
  } else if (schemaVersion === STRATAGATE_STORAGE_SCHEMA_VERSION) {
436
443
  snapshot = structuredClone(value);
437
444
  } else {
@@ -454,6 +461,9 @@ function normalizeSnapshot(value) {
454
461
  if (!Number.isSafeInteger(block.pointerAnchorBlockPosition) || block.pointerAnchorBlockPosition < 1) {
455
462
  throw new TypeError("Invalid StrataGate snapshot: pointerAnchorBlockPosition must be a positive integer");
456
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
+ }
457
467
  }
458
468
  if (snapshot.successfulModelResponses.length > 5) {
459
469
  snapshot.successfulModelResponses = snapshot.successfulModelResponses.slice(-5);
@@ -652,6 +662,7 @@ CREATE TABLE IF NOT EXISTS blocks (
652
662
  pointer_anchor_level INTEGER NOT NULL,
653
663
  pointer_anchor_block_position INTEGER NOT NULL,
654
664
  last_lifted_at TEXT,
665
+ last_lifted_by TEXT CHECK (last_lifted_by IS NULL OR last_lifted_by IN ('user', 'agent')),
655
666
  PRIMARY KEY (namespace, id),
656
667
  UNIQUE (namespace, sequence),
657
668
  FOREIGN KEY (namespace) REFERENCES memory_spaces(namespace) ON DELETE CASCADE
@@ -918,7 +929,8 @@ var SqliteStorage = class {
918
929
  pointerCurrentLevel: row.pointer_current_level,
919
930
  pointerAnchorLevel: row.pointer_anchor_level,
920
931
  pointerAnchorBlockPosition: row.pointer_anchor_block_position,
921
- lastLiftedAt: row.last_lifted_at
932
+ lastLiftedAt: row.last_lifted_at,
933
+ lastLiftedBy: row.last_lifted_by
922
934
  }));
923
935
  const sourceRows = this.database.prepare(`
924
936
  SELECT event_id, message_id, position FROM event_sources
@@ -1154,8 +1166,8 @@ var SqliteStorage = class {
1154
1166
  INSERT INTO blocks (
1155
1167
  namespace, id, thread_id, sequence, start_turn, end_turn, created_at, should_extract,
1156
1168
  l0_title, l0_tags_json, l1_summary, l2_keypoints_json, l3_condensed, l4_readable,
1157
- pointer_current_level, pointer_anchor_level, pointer_anchor_block_position, last_lifted_at
1158
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1169
+ pointer_current_level, pointer_anchor_level, pointer_anchor_block_position, last_lifted_at, last_lifted_by
1170
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1159
1171
  ON CONFLICT (namespace, id) DO UPDATE SET
1160
1172
  thread_id = excluded.thread_id,
1161
1173
  sequence = excluded.sequence,
@@ -1172,7 +1184,8 @@ var SqliteStorage = class {
1172
1184
  pointer_current_level = excluded.pointer_current_level,
1173
1185
  pointer_anchor_level = excluded.pointer_anchor_level,
1174
1186
  pointer_anchor_block_position = excluded.pointer_anchor_block_position,
1175
- last_lifted_at = excluded.last_lifted_at
1187
+ last_lifted_at = excluded.last_lifted_at,
1188
+ last_lifted_by = excluded.last_lifted_by
1176
1189
  `);
1177
1190
  for (const block of snapshot.blocks) {
1178
1191
  insertBlock.run(
@@ -1193,7 +1206,8 @@ var SqliteStorage = class {
1193
1206
  block.pointerCurrentLevel,
1194
1207
  block.pointerAnchorLevel,
1195
1208
  block.pointerAnchorBlockPosition,
1196
- block.lastLiftedAt
1209
+ block.lastLiftedAt,
1210
+ block.lastLiftedBy
1197
1211
  );
1198
1212
  }
1199
1213
  const insertMessage = this.database.prepare(`
@@ -1462,7 +1476,7 @@ var SqliteStorage = class {
1462
1476
  this.database.exec(THREAD_INDEXES);
1463
1477
  this.database.exec(`PRAGMA user_version = ${STRATAGATE_STORAGE_SCHEMA_VERSION}`);
1464
1478
  });
1465
- } else if (version === 1 || version === 2 || version === 3 || version === 4 || version === 5) {
1479
+ } else if (version === 1 || version === 2 || version === 3 || version === 4 || version === 5 || version === 6) {
1466
1480
  this.immediateTransaction(() => {
1467
1481
  this.database.exec(SCHEMA);
1468
1482
  if (version === 1) {
@@ -1495,6 +1509,9 @@ var SqliteStorage = class {
1495
1509
  ))
1496
1510
  `);
1497
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
+ }
1498
1515
  const messageColumns = this.database.prepare("PRAGMA table_info('messages')").all();
1499
1516
  if (!messageColumns.some(({ name: name2 }) => name2 === "thread_id")) {
1500
1517
  this.database.exec("ALTER TABLE messages ADD COLUMN thread_id TEXT");
@@ -2108,7 +2125,7 @@ var StrataGate = class _StrataGate {
2108
2125
  };
2109
2126
  });
2110
2127
  }
2111
- async expandBlock(id, target = "next") {
2128
+ async expandBlock(id, target = "next", source = "agent") {
2112
2129
  return this.commitMutation(() => {
2113
2130
  const block = this.blocks.find((candidate) => candidate.id === id);
2114
2131
  if (!block) throw new Error(`Unknown block: ${id}`);
@@ -2125,6 +2142,7 @@ var StrataGate = class _StrataGate {
2125
2142
  block.pointerAnchorLevel = level;
2126
2143
  block.pointerAnchorBlockPosition = latestBlockPosition;
2127
2144
  block.lastLiftedAt = toUtc8Iso(this.now());
2145
+ block.lastLiftedBy = source;
2128
2146
  return {
2129
2147
  id: block.id,
2130
2148
  ...block.threadId ? { threadId: block.threadId } : {},
@@ -2354,7 +2372,8 @@ var StrataGate = class _StrataGate {
2354
2372
  pointerCurrentLevel: 5,
2355
2373
  pointerAnchorLevel: 5,
2356
2374
  pointerAnchorBlockPosition: blockPosition,
2357
- lastLiftedAt: null
2375
+ lastLiftedAt: null,
2376
+ lastLiftedBy: null
2358
2377
  };
2359
2378
  const sealedIds = new Set(raw.map((message) => message.id));
2360
2379
  const remaining = this.openTail.filter((message) => !sealedIds.has(message.id));
@@ -3302,7 +3321,7 @@ var StrataGateRuntime = class {
3302
3321
  }
3303
3322
  async expandBlock(session, id, target) {
3304
3323
  await this.flush();
3305
- const result = await (await this.space(session)).expandBlock(id, target);
3324
+ const result = await (await this.space(session)).expandBlock(id, target, "agent");
3306
3325
  return this.batch(session, [{
3307
3326
  ref: `block:${result.id}:level:${result.level}`,
3308
3327
  target: { eventIds: [], elementIds: [] }
@@ -3527,6 +3546,37 @@ var StrataGateRuntime = class {
3527
3546
  await update;
3528
3547
  return value;
3529
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
+ }
3530
3580
  async applyBlockDecayLambda(value) {
3531
3581
  await this.flush();
3532
3582
  this.blockDecayLambda = value;
@@ -3911,6 +3961,20 @@ function registerMemoryTools(ctx, runtime) {
3911
3961
  }
3912
3962
 
3913
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
+ }
3914
3978
  function sendJson(res, status, body) {
3915
3979
  res.statusCode = status;
3916
3980
  res.setHeader("Content-Type", "application/json; charset=utf-8");
@@ -3953,6 +4017,17 @@ function sourceMessages(snapshot, ids) {
3953
4017
  }
3954
4018
  return output;
3955
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
+ }
3956
4031
  function eventSummary(event) {
3957
4032
  return {
3958
4033
  id: event.id,
@@ -4056,7 +4131,13 @@ async function overview(runtime) {
4056
4131
  lastActivityAt: timestamps.at(-1) ?? null
4057
4132
  });
4058
4133
  }
4059
- return { readonly: true, settingsWritable: 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
+ };
4060
4141
  }
4061
4142
  async function updateSettings(runtime, url) {
4062
4143
  const raw = url.searchParams.get("blockDecayLambda")?.trim() ?? "";
@@ -4066,6 +4147,126 @@ async function updateSettings(runtime, url) {
4066
4147
  }
4067
4148
  return { blockDecayLambda: await runtime.adminSetBlockDecayLambda(value) };
4068
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)));
4269
+ }
4069
4270
  async function memories(runtime, url) {
4070
4271
  const namespace = url.searchParams.get("namespace")?.trim() ?? "";
4071
4272
  if (!namespace) throw new AdminHttpError(400, "namespace is required");
@@ -4080,44 +4281,85 @@ async function memories(runtime, url) {
4080
4281
  relatedElements: snapshot.elements.filter(({ sourceEventIds }) => sourceEventIds.includes(event.id)).map(({ id, name: name2 }) => ({ id, name: name2 }))
4081
4282
  }));
4082
4283
  else if (kind === "elements") values = snapshot.elements.map(elementSummary);
4083
- else if (kind === "blocks") values = snapshot.blocks.map((block) => {
4084
- const extraction = snapshot.extractionJobs.find(({ blockId }) => blockId === block.id);
4085
- const relatedEvents = snapshot.events.filter(({ sourceBlockId }) => sourceBlockId === block.id);
4086
- const eventIds = new Set(relatedEvents.map(({ id }) => id));
4087
- const projections = snapshot.elementProjectionJobs.filter(({ sourceEventIds }) => sourceEventIds.some((id) => eventIds.has(id)));
4088
- const relatedElements = snapshot.elements.filter(({ sourceEventIds }) => sourceEventIds.some((id) => eventIds.has(id))).map(({ id, name: name2 }) => ({ id, name: name2 }));
4089
- const failedProjection = projections.find(({ status: status2 }) => status2 === "failed");
4090
- const pendingProjection = projections.some(({ status: status2 }) => status2 === "pending" || status2 === "running");
4091
- const needsExtraction = block.shouldExtract === true;
4092
- 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;
4093
4347
  return {
4094
- id: block.id,
4095
- sequence: block.sequence,
4096
- turnRange: [block.startTurn, block.endTurn],
4097
- title: block.l0Title,
4098
- tags: block.l0Tags,
4099
- summary: block.l1Summary,
4100
- keypoints: block.l2Keypoints,
4101
- currentLevel: block.pointerCurrentLevel,
4102
- sourceMessages: block.l5Raw.length,
4103
- createdAt: block.createdAt,
4104
- status,
4105
- eventExtraction: extraction ? {
4106
- status: extraction.status,
4107
- attempts: extraction.attempts,
4108
- updatedAt: extraction.updatedAt,
4109
- lastError: extraction.lastError
4110
- } : null,
4111
- elementProjection: projections.length ? {
4112
- status: failedProjection ? "failed" : pendingProjection ? "processing" : "completed",
4113
- jobs: projections.length,
4114
- lastError: failedProjection?.lastError ?? null
4115
- } : null,
4116
- relatedEvents: relatedEvents.map(eventSummary),
4117
- 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
4118
4361
  };
4119
- });
4120
- else throw new AdminHttpError(400, `Unsupported memory kind: ${kind}`);
4362
+ } else throw new AdminHttpError(400, `Unsupported memory kind: ${kind}`);
4121
4363
  const filtered = values.filter((value) => matchesQuery(value, query));
4122
4364
  return { namespace, kind, total: filtered.length, offset, limit, items: filtered.slice(offset, offset + limit) };
4123
4365
  }
@@ -4143,12 +4385,22 @@ async function sources(runtime, url) {
4143
4385
  events = snapshot.events.filter(({ id }) => element.sourceEventIds.includes(id));
4144
4386
  ids = new Set(events.flatMap(({ sourceMessageIds }) => sourceMessageIds));
4145
4387
  } else if (blockId) {
4146
- 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);
4147
4390
  if (!block) throw new AdminHttpError(404, `Unknown block: ${blockId}`);
4148
- ids = new Set(block.l5Raw.map(({ id }) => id));
4149
- 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))));
4150
4394
  const eventIds = new Set(events.map(({ id }) => id));
4151
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
+ };
4152
4404
  } else {
4153
4405
  throw new AdminHttpError(400, "eventId, elementId, or blockId is required");
4154
4406
  }
@@ -4159,6 +4411,16 @@ async function sources(runtime, url) {
4159
4411
  messages: sourceMessages(snapshot, ids)
4160
4412
  };
4161
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
+ }
4162
4424
  function receiptSources(snapshot, receipt) {
4163
4425
  const events = snapshot.events.filter(({ id }) => receipt.eventIds.includes(id));
4164
4426
  const elements = snapshot.elements.filter(({ id }) => receipt.elementIds.includes(id));
@@ -4194,6 +4456,9 @@ async function handleAdminRequest(runtime, req, res) {
4194
4456
  if (path === "/api/stratagate/settings") {
4195
4457
  if (req.method !== "PATCH") throw new AdminHttpError(405, "StrataGate settings require PATCH");
4196
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));
4197
4462
  } else if (req.method !== "GET") throw new AdminHttpError(405, "StrataGate memory data is read-only");
4198
4463
  else if (path === "/api/stratagate/overview") sendJson(res, 200, await overview(runtime));
4199
4464
  else if (path === "/api/stratagate/memories") sendJson(res, 200, await memories(runtime, url));