stratagate-dsh 0.2.19 → 0.2.25

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
@@ -1,7 +1,7 @@
1
1
  // src/index.ts
2
2
  import { mkdir } from "node:fs/promises";
3
3
  import { dirname } from "node:path";
4
- import { createUserMessage as createUserMessage2 } from "@deepseek-ai/dsh-llm";
4
+ import { createUserMessage as createUserMessage3 } from "@deepseek-ai/dsh-llm";
5
5
 
6
6
  // src/config.ts
7
7
  import z from "@deepseek-ai/schemastery";
@@ -122,12 +122,12 @@ function isFillerSentence(value) {
122
122
  }
123
123
  function resultSummary(value) {
124
124
  if (typeof value === "string") {
125
- const text2 = value.replace(/\s+/g, " ").trim();
126
- if (!text2) return "";
125
+ const text3 = value.replace(/\s+/g, " ").trim();
126
+ if (!text3) return "";
127
127
  try {
128
- return resultSummary(JSON.parse(text2));
128
+ return resultSummary(JSON.parse(text3));
129
129
  } catch {
130
- return text2.slice(0, 160);
130
+ return text3.slice(0, 160);
131
131
  }
132
132
  }
133
133
  if (typeof value === "number" || typeof value === "boolean") return String(value);
@@ -199,9 +199,9 @@ function condenseMessage(content) {
199
199
  const source = content.trim();
200
200
  const toolSummary = summarizeToolJson(source);
201
201
  if (toolSummary) return [{ text: toolSummary, pasteCandidate: false }];
202
- return splitTextAndCode(source).flatMap(({ text: text2, isCode }) => {
203
- if (isCode) return text2.trim() ? [{ text: text2.trim(), pasteCandidate: true }] : [];
204
- return text2.split(/\n\s*\n+/u).map(removeStandaloneFillers).filter(Boolean).map((paragraph) => ({
202
+ return splitTextAndCode(source).flatMap(({ text: text3, isCode }) => {
203
+ if (isCode) return text3.trim() ? [{ text: text3.trim(), pasteCandidate: true }] : [];
204
+ return text3.split(/\n\s*\n+/u).map(removeStandaloneFillers).filter(Boolean).map((paragraph) => ({
205
205
  text: paragraph,
206
206
  pasteCandidate: looksLikeCode(paragraph) || normalizedParagraph(paragraph).length >= REPEATED_PASTE_MIN_CHARS
207
207
  }));
@@ -209,8 +209,8 @@ function condenseMessage(content) {
209
209
  }
210
210
  function formatReadableTranscript(messages) {
211
211
  return messages.filter((message) => message.role !== "system").flatMap((message) => {
212
- const text2 = message.content.trim();
213
- const inline = message.role === "tool" ? summarizeToolJson(text2) ?? text2 : text2;
212
+ const text3 = message.content.trim();
213
+ const inline = message.role === "tool" ? summarizeToolJson(text3) ?? text3 : text3;
214
214
  const lines = inline ? [`${roleLabel(message.role)}: ${inline}`] : [];
215
215
  lines.push(...(message.toolCalls ?? []).map(summarizeToolTrace));
216
216
  return lines;
@@ -252,36 +252,6 @@ function deterministicBlockLayers(messages) {
252
252
  };
253
253
  }
254
254
 
255
- // ../../src/retrieval.ts
256
- var RETRIEVAL_STRATEGIES = [
257
- "answer",
258
- "search_events",
259
- "expand_event",
260
- "search_elements",
261
- "expand_element",
262
- "search_raw_memory",
263
- "expand_block"
264
- ];
265
- function shortText(value) {
266
- return typeof value === "string" ? value.trim().replace(/\s+/g, " ").slice(0, 160) : "";
267
- }
268
- function normalizeStrategy(value) {
269
- return typeof value === "string" && RETRIEVAL_STRATEGIES.includes(value) ? value : "search_events";
270
- }
271
- function normalizeRetrievalAssessment(input, latestEvidenceRefs) {
272
- const requestedVerdict = input.verdict === "sufficient" || input.verdict === "wrong" ? input.verdict : "partial";
273
- const evidenceRefs = Array.isArray(input.evidence_refs) ? [...new Set(input.evidence_refs.filter((id) => typeof id === "string" && latestEvidenceRefs.has(id)))].slice(0, 8) : [];
274
- const requestedStrategy = normalizeStrategy(input.next_strategy);
275
- const sufficient = requestedVerdict === "sufficient" && evidenceRefs.length > 0 && requestedStrategy === "answer";
276
- return {
277
- verdict: sufficient ? "sufficient" : requestedVerdict === "wrong" ? "wrong" : "partial",
278
- evidenceRefs,
279
- fit: shortText(input.fit),
280
- missing: sufficient ? "" : shortText(input.missing) || "Direct evidence required to answer the question is still missing.",
281
- nextStrategy: sufficient ? "answer" : requestedStrategy === "answer" ? "search_events" : requestedStrategy
282
- };
283
- }
284
-
285
255
  // ../../src/search.ts
286
256
  var wordSegmenter = new Intl.Segmenter(void 0, { granularity: "word" });
287
257
  function normalizeSearchText(value) {
@@ -359,8 +329,215 @@ function rrfRank(rankings) {
359
329
  return [...fused.values()].sort((left, right) => right.score - left.score || left.bestRank - right.bestRank || left.item.id.localeCompare(right.item.id)).map(({ item, score }) => ({ item, score }));
360
330
  }
361
331
 
332
+ // ../../src/graph.ts
333
+ var NODE_TYPES = /* @__PURE__ */ new Set(["person", "project", "organization", "tool", "place"]);
334
+ var STATUSES = /* @__PURE__ */ new Set(["active", "superseded", "disputed", "archived"]);
335
+ function text(value, limit = 240) {
336
+ return typeof value === "string" ? value.trim().replace(/\s+/g, " ").slice(0, limit) : "";
337
+ }
338
+ function strings(value, limit = 32) {
339
+ return Array.isArray(value) ? [...new Set(value.map((item) => text(item)).filter(Boolean))].slice(0, limit) : [];
340
+ }
341
+ function confidence(value) {
342
+ return typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.min(1, value)) : 0.8;
343
+ }
344
+ function chronology(events, ids, fallback) {
345
+ return ids.flatMap((id) => events.find((event) => event.id === id) ?? []).map((event) => event.temporal.happenedStart ?? event.temporal.happenedEnd ?? event.temporal.mentionedAt ?? event.createdAt).sort().at(-1) ?? fallback;
346
+ }
347
+ function sameEntity(node, proposal) {
348
+ if (node.type !== proposal.type) return false;
349
+ const key = (value) => normalizeSearchText(value).replace(/[\s_-]+/g, "");
350
+ const names = new Set([proposal.name, ...proposal.aliases ?? []].map(key));
351
+ return [node.name, ...node.aliases].some((name2) => names.has(key(name2)));
352
+ }
353
+ function applyGraphProjection(options) {
354
+ const refs = /* @__PURE__ */ new Map();
355
+ const touchedNodes = /* @__PURE__ */ new Set();
356
+ const touchedEdges = /* @__PURE__ */ new Set();
357
+ const validSources = (value) => {
358
+ const requested = strings(value, 64);
359
+ return requested.length > 0 && requested.every((id) => options.allowedEventIds.has(id)) ? requested : [];
360
+ };
361
+ for (const proposal of Array.isArray(options.result.nodes) ? options.result.nodes : []) {
362
+ const ref = text(proposal.ref, 120);
363
+ const name2 = text(proposal.name, 160);
364
+ const sources2 = validSources(proposal.sourceEventIds);
365
+ if (!ref || !name2 || !NODE_TYPES.has(proposal.type) || sources2.length === 0) continue;
366
+ const aliases = strings(proposal.aliases, 20).filter((alias) => normalizeSearchText(alias) !== normalizeSearchText(name2));
367
+ let node = options.nodes.find((candidate) => sameEntity(candidate, { ...proposal, name: name2, aliases }));
368
+ if (!node) {
369
+ node = {
370
+ id: options.idFactory("node"),
371
+ name: name2,
372
+ type: proposal.type,
373
+ aliases: [],
374
+ currentState: "",
375
+ facts: [],
376
+ status: "active",
377
+ confidence: confidence(proposal.confidence),
378
+ sourceEventIds: [],
379
+ createdAt: options.now,
380
+ updatedAt: options.now
381
+ };
382
+ options.nodes.push(node);
383
+ }
384
+ node.aliases = [.../* @__PURE__ */ new Set([...node.aliases, ...aliases])];
385
+ node.status = STATUSES.has(proposal.status ?? "active") ? proposal.status ?? "active" : "active";
386
+ node.confidence = confidence(proposal.confidence);
387
+ node.sourceEventIds = [.../* @__PURE__ */ new Set([...node.sourceEventIds, ...sources2])];
388
+ const validFrom = text(proposal.validFrom, 80) || chronology(options.events, sources2, options.now);
389
+ const validTo = text(proposal.validTo, 80) || void 0;
390
+ const facts = [
391
+ ...text(proposal.state, 1200) ? [{ key: "state", value: text(proposal.state, 1200) }] : [],
392
+ ...Array.isArray(proposal.facts) ? proposal.facts : []
393
+ ];
394
+ for (const rawFact of facts) {
395
+ const key = text(rawFact.key, 160);
396
+ const value = Array.isArray(rawFact.value) ? strings(rawFact.value, 40) : text(rawFact.value, 1200);
397
+ if (!key || (Array.isArray(value) ? value.length === 0 : !value)) continue;
398
+ const factSources = "sourceEventIds" in rawFact ? validSources(rawFact.sourceEventIds) : sources2;
399
+ if (factSources.length === 0) continue;
400
+ for (const old of node.facts.filter((fact2) => fact2.status === "active" && fact2.key === key)) {
401
+ old.status = "superseded";
402
+ if (!old.validTo) old.validTo = validFrom;
403
+ old.updatedAt = options.now;
404
+ }
405
+ const fact = {
406
+ id: options.idFactory("gfact"),
407
+ key,
408
+ value,
409
+ status: node.status,
410
+ validFrom,
411
+ ...validTo ? { validTo } : {},
412
+ confidence: node.confidence,
413
+ sourceEventIds: factSources,
414
+ createdAt: options.now,
415
+ updatedAt: options.now
416
+ };
417
+ node.facts.push(fact);
418
+ }
419
+ node.currentState = node.facts.filter((fact) => fact.status === "active").map((fact) => `${fact.key}: ${Array.isArray(fact.value) ? fact.value.join("\u3001") : fact.value}`).join("\n");
420
+ node.updatedAt = options.now;
421
+ refs.set(ref, node);
422
+ touchedNodes.add(node.id);
423
+ }
424
+ for (const proposal of Array.isArray(options.result.edges) ? options.result.edges : []) {
425
+ const from = refs.get(text(proposal.fromRef, 120));
426
+ const to = refs.get(text(proposal.toRef, 120));
427
+ const relation = text(proposal.relation, 100);
428
+ const sources2 = validSources(proposal.sourceEventIds);
429
+ if (!from || !to || from.id === to.id || !relation || sources2.length === 0) continue;
430
+ const status = STATUSES.has(proposal.status ?? "active") ? proposal.status ?? "active" : "active";
431
+ const validFrom = text(proposal.validFrom, 80) || chronology(options.events, sources2, options.now);
432
+ const validTo = text(proposal.validTo, 80) || void 0;
433
+ for (const old of options.edges.filter((edge2) => edge2.status === "active" && edge2.fromNodeId === from.id && edge2.relation === relation && edge2.toNodeId !== to.id)) {
434
+ old.status = "superseded";
435
+ if (!old.validTo) old.validTo = validFrom;
436
+ old.updatedAt = options.now;
437
+ }
438
+ let edge = options.edges.find((candidate) => candidate.fromNodeId === from.id && candidate.toNodeId === to.id && candidate.relation === relation && candidate.status === status);
439
+ if (!edge) {
440
+ edge = {
441
+ id: options.idFactory("edge"),
442
+ fromNodeId: from.id,
443
+ toNodeId: to.id,
444
+ relation,
445
+ status,
446
+ validFrom,
447
+ ...validTo ? { validTo } : {},
448
+ confidence: confidence(proposal.confidence),
449
+ sourceEventIds: sources2,
450
+ createdAt: options.now,
451
+ updatedAt: options.now
452
+ };
453
+ options.edges.push(edge);
454
+ } else {
455
+ edge.sourceEventIds = [.../* @__PURE__ */ new Set([...edge.sourceEventIds, ...sources2])];
456
+ edge.confidence = confidence(proposal.confidence);
457
+ edge.updatedAt = options.now;
458
+ }
459
+ touchedEdges.add(edge.id);
460
+ }
461
+ for (const event of options.events.filter((candidate) => options.allowedEventIds.has(candidate.id))) {
462
+ const participantNodeIds = options.nodes.filter((node) => node.sourceEventIds.includes(event.id)).map(({ id }) => id);
463
+ event.temporal.participantNodeIds = [.../* @__PURE__ */ new Set([...event.temporal.participantNodeIds ?? [], ...participantNodeIds])];
464
+ }
465
+ return { nodeIds: [...touchedNodes], edgeIds: [...touchedEdges] };
466
+ }
467
+
468
+ // ../../src/events.ts
469
+ function normalizeStandardEventType(value) {
470
+ const normalized = normalizeSearchText(value ?? "").replace(/[\s_-]+/g, "");
471
+ const aliases = {
472
+ "\u53D1\u5E03": "release",
473
+ "\u7248\u672C\u53D1\u5E03": "release",
474
+ release: "release",
475
+ released: "release",
476
+ "\u51B3\u5B9A": "decision",
477
+ "\u51B3\u7B56": "decision",
478
+ decision: "decision",
479
+ "\u5B8C\u6210": "task_completed",
480
+ "\u4EFB\u52A1\u5B8C\u6210": "task_completed",
481
+ taskcompleted: "task_completed",
482
+ completed: "task_completed",
483
+ "\u8BA1\u5212": "plan",
484
+ plan: "plan",
485
+ planned: "plan",
486
+ "\u53D8\u66F4": "change",
487
+ "\u4FEE\u6539": "change",
488
+ change: "change",
489
+ "\u53D6\u6D88": "cancellation",
490
+ cancellation: "cancellation",
491
+ cancelled: "cancellation",
492
+ canceled: "cancellation",
493
+ "\u6545\u969C": "incident",
494
+ incident: "incident",
495
+ "\u4F1A\u8BAE": "meeting",
496
+ meeting: "meeting",
497
+ "\u534F\u4F5C": "collaboration",
498
+ collaboration: "collaboration",
499
+ "\u8FC1\u79FB": "migration",
500
+ migration: "migration",
501
+ other: "other"
502
+ };
503
+ return aliases[normalized] ?? "other";
504
+ }
505
+
506
+ // ../../src/retrieval.ts
507
+ var RETRIEVAL_STRATEGIES = [
508
+ "answer",
509
+ "search_events",
510
+ "expand_event",
511
+ "search_graph",
512
+ "expand_graph_node",
513
+ "search_elements",
514
+ "expand_element",
515
+ "search_raw_memory",
516
+ "expand_block"
517
+ ];
518
+ function shortText(value) {
519
+ return typeof value === "string" ? value.trim().replace(/\s+/g, " ").slice(0, 160) : "";
520
+ }
521
+ function normalizeStrategy(value) {
522
+ return typeof value === "string" && RETRIEVAL_STRATEGIES.includes(value) ? value : "search_events";
523
+ }
524
+ function normalizeRetrievalAssessment(input, latestEvidenceRefs) {
525
+ const requestedVerdict = input.verdict === "sufficient" || input.verdict === "wrong" ? input.verdict : "partial";
526
+ const evidenceRefs = Array.isArray(input.evidence_refs) ? [...new Set(input.evidence_refs.filter((id) => typeof id === "string" && latestEvidenceRefs.has(id)))].slice(0, 8) : [];
527
+ const requestedStrategy = normalizeStrategy(input.next_strategy);
528
+ const sufficient = requestedVerdict === "sufficient" && evidenceRefs.length > 0 && requestedStrategy === "answer";
529
+ return {
530
+ verdict: sufficient ? "sufficient" : requestedVerdict === "wrong" ? "wrong" : "partial",
531
+ evidenceRefs,
532
+ fit: shortText(input.fit),
533
+ missing: sufficient ? "" : shortText(input.missing) || "Direct evidence required to answer the question is still missing.",
534
+ nextStrategy: sufficient ? "answer" : requestedStrategy === "answer" ? "search_events" : requestedStrategy
535
+ };
536
+ }
537
+
362
538
  // ../../src/storage.ts
363
- var STRATAGATE_STORAGE_SCHEMA_VERSION = 6;
539
+ var STRATAGATE_STORAGE_SCHEMA_VERSION = 8;
540
+ var KNOWLEDGE_GRAPH_PROJECTOR_VERSION = 1;
364
541
  var StorageConflictError = class extends Error {
365
542
  constructor(namespace, expectedRevision, actualRevision) {
366
543
  super(`Storage revision conflict for ${namespace}: expected ${expectedRevision}, found ${actualRevision ?? "missing"}`);
@@ -376,11 +553,14 @@ var StorageConflictError = class extends Error {
376
553
  function cloneSnapshot(snapshot) {
377
554
  return structuredClone(snapshot);
378
555
  }
556
+ function emptyGraph() {
557
+ return { graphNodes: [], graphEdges: [], graphProjectionJobs: [] };
558
+ }
379
559
  function migrateLegacyBlocks(blocks) {
380
560
  return blocks.map((block) => {
381
561
  const { pointerAnchorTurn, ...current } = block;
382
562
  const position = blocks.filter((candidate) => candidate.threadId === block.threadId && candidate.endTurn <= pointerAnchorTurn).length;
383
- return { ...current, pointerAnchorBlockPosition: Math.max(1, position) };
563
+ return { ...current, pointerAnchorBlockPosition: Math.max(1, position), lastLiftedBy: null };
384
564
  });
385
565
  }
386
566
  function normalizeSnapshot(value) {
@@ -397,7 +577,8 @@ function normalizeSnapshot(value) {
397
577
  elements: [],
398
578
  elementProjectionJobs: [],
399
579
  usageReceipts: Array.isArray(legacy.usageReceipts) ? legacy.usageReceipts.map((receipt) => ({ ...receipt, elementIds: [] })) : [],
400
- ingestionReceipts: []
580
+ ingestionReceipts: [],
581
+ ...emptyGraph()
401
582
  };
402
583
  } else if (schemaVersion === 2) {
403
584
  const legacy = value;
@@ -406,7 +587,8 @@ function normalizeSnapshot(value) {
406
587
  schemaVersion: STRATAGATE_STORAGE_SCHEMA_VERSION,
407
588
  blockDecayLambda: BLOCK_DECAY_LAMBDA,
408
589
  blocks: migrateLegacyBlocks(legacy.blocks),
409
- ingestionReceipts: []
590
+ ingestionReceipts: [],
591
+ ...emptyGraph()
410
592
  };
411
593
  } else if (schemaVersion === 3) {
412
594
  const legacy = value;
@@ -414,7 +596,8 @@ function normalizeSnapshot(value) {
414
596
  ...structuredClone(legacy),
415
597
  schemaVersion: STRATAGATE_STORAGE_SCHEMA_VERSION,
416
598
  blockDecayLambda: BLOCK_DECAY_LAMBDA,
417
- blocks: migrateLegacyBlocks(legacy.blocks)
599
+ blocks: migrateLegacyBlocks(legacy.blocks),
600
+ ...emptyGraph()
418
601
  };
419
602
  } else if (schemaVersion === 4) {
420
603
  const legacy = value;
@@ -422,7 +605,8 @@ function normalizeSnapshot(value) {
422
605
  ...structuredClone(legacy),
423
606
  schemaVersion: STRATAGATE_STORAGE_SCHEMA_VERSION,
424
607
  blockDecayLambda: BLOCK_DECAY_LAMBDA,
425
- blocks: migrateLegacyBlocks(legacy.blocks)
608
+ blocks: migrateLegacyBlocks(legacy.blocks),
609
+ ...emptyGraph()
426
610
  };
427
611
  } else if (schemaVersion === 5) {
428
612
  const legacy = value;
@@ -430,8 +614,20 @@ function normalizeSnapshot(value) {
430
614
  ...structuredClone(legacy),
431
615
  schemaVersion: STRATAGATE_STORAGE_SCHEMA_VERSION,
432
616
  blockDecayLambda: BLOCK_DECAY_LAMBDA,
433
- blocks: migrateLegacyBlocks(legacy.blocks)
617
+ blocks: migrateLegacyBlocks(legacy.blocks),
618
+ ...emptyGraph()
619
+ };
620
+ } else if (schemaVersion === 6) {
621
+ const legacy = value;
622
+ snapshot = {
623
+ ...structuredClone(legacy),
624
+ schemaVersion: STRATAGATE_STORAGE_SCHEMA_VERSION,
625
+ blocks: legacy.blocks.map((block) => ({ ...structuredClone(block), lastLiftedBy: null })),
626
+ ...emptyGraph()
434
627
  };
628
+ } else if (schemaVersion === 7) {
629
+ const legacy = value;
630
+ snapshot = { ...structuredClone(legacy), schemaVersion: STRATAGATE_STORAGE_SCHEMA_VERSION, ...emptyGraph() };
435
631
  } else if (schemaVersion === STRATAGATE_STORAGE_SCHEMA_VERSION) {
436
632
  snapshot = structuredClone(value);
437
633
  } else {
@@ -446,14 +642,20 @@ function normalizeSnapshot(value) {
446
642
  if (!Number.isFinite(snapshot.blockDecayLambda) || snapshot.blockDecayLambda < 0) {
447
643
  throw new TypeError("Invalid StrataGate snapshot: blockDecayLambda must be a non-negative finite number");
448
644
  }
449
- for (const key of ["openTail", "blocks", "events", "elements", "extractionJobs", "elementProjectionJobs", "usageReceipts", "ingestionReceipts"]) {
645
+ for (const key of ["openTail", "blocks", "events", "graphNodes", "graphEdges", "graphProjectionJobs", "elements", "extractionJobs", "elementProjectionJobs", "usageReceipts", "ingestionReceipts"]) {
450
646
  if (!Array.isArray(snapshot[key])) throw new TypeError(`Invalid StrataGate snapshot: ${key} must be an array`);
451
647
  }
452
648
  if (!Array.isArray(snapshot.successfulModelResponses)) snapshot.successfulModelResponses = [];
649
+ for (const event of snapshot.events) {
650
+ event.temporal = { ...event.temporal, eventType: normalizeStandardEventType(event.temporal.eventType) };
651
+ }
453
652
  for (const block of snapshot.blocks) {
454
653
  if (!Number.isSafeInteger(block.pointerAnchorBlockPosition) || block.pointerAnchorBlockPosition < 1) {
455
654
  throw new TypeError("Invalid StrataGate snapshot: pointerAnchorBlockPosition must be a positive integer");
456
655
  }
656
+ if (block.lastLiftedBy !== null && block.lastLiftedBy !== "user" && block.lastLiftedBy !== "agent") {
657
+ throw new TypeError("Invalid StrataGate snapshot: lastLiftedBy must be user, agent, or null");
658
+ }
457
659
  }
458
660
  if (snapshot.successfulModelResponses.length > 5) {
459
661
  snapshot.successfulModelResponses = snapshot.successfulModelResponses.slice(-5);
@@ -652,6 +854,7 @@ CREATE TABLE IF NOT EXISTS blocks (
652
854
  pointer_anchor_level INTEGER NOT NULL,
653
855
  pointer_anchor_block_position INTEGER NOT NULL,
654
856
  last_lifted_at TEXT,
857
+ last_lifted_by TEXT CHECK (last_lifted_by IS NULL OR last_lifted_by IN ('user', 'agent')),
655
858
  PRIMARY KEY (namespace, id),
656
859
  UNIQUE (namespace, sequence),
657
860
  FOREIGN KEY (namespace) REFERENCES memory_spaces(namespace) ON DELETE CASCADE
@@ -806,6 +1009,15 @@ CREATE TABLE IF NOT EXISTS element_projection_jobs (
806
1009
  FOREIGN KEY (namespace) REFERENCES memory_spaces(namespace) ON DELETE CASCADE
807
1010
  ) STRICT;
808
1011
 
1012
+ CREATE TABLE IF NOT EXISTS graph_state (
1013
+ namespace TEXT PRIMARY KEY,
1014
+ nodes_json TEXT NOT NULL DEFAULT '[]',
1015
+ edges_json TEXT NOT NULL DEFAULT '[]',
1016
+ jobs_json TEXT NOT NULL DEFAULT '[]',
1017
+ updated_at TEXT NOT NULL,
1018
+ FOREIGN KEY (namespace) REFERENCES memory_spaces(namespace) ON DELETE CASCADE
1019
+ ) STRICT;
1020
+
809
1021
  CREATE TABLE IF NOT EXISTS usage_receipts (
810
1022
  namespace TEXT NOT NULL,
811
1023
  receipt_id TEXT NOT NULL,
@@ -918,7 +1130,8 @@ var SqliteStorage = class {
918
1130
  pointerCurrentLevel: row.pointer_current_level,
919
1131
  pointerAnchorLevel: row.pointer_anchor_level,
920
1132
  pointerAnchorBlockPosition: row.pointer_anchor_block_position,
921
- lastLiftedAt: row.last_lifted_at
1133
+ lastLiftedAt: row.last_lifted_at,
1134
+ lastLiftedBy: row.last_lifted_by
922
1135
  }));
923
1136
  const sourceRows = this.database.prepare(`
924
1137
  SELECT event_id, message_id, position FROM event_sources
@@ -942,7 +1155,10 @@ var SqliteStorage = class {
942
1155
  quotes: parseJson(row.quotes_json, "events.quotes_json"),
943
1156
  sourceMessageIds: sourcesByEvent.get(row.id) ?? [],
944
1157
  sourceBlockId: row.source_block_id,
945
- temporal: parseJson(row.temporal_json, "events.temporal_json"),
1158
+ temporal: (() => {
1159
+ const temporal = parseJson(row.temporal_json, "events.temporal_json");
1160
+ return { ...temporal, eventType: normalizeStandardEventType(temporal.eventType) };
1161
+ })(),
946
1162
  scope: row.scope,
947
1163
  criticality: row.criticality,
948
1164
  confidence: row.confidence,
@@ -1080,6 +1296,12 @@ var SqliteStorage = class {
1080
1296
  id: row.receipt_id,
1081
1297
  createdAt: row.created_at
1082
1298
  }));
1299
+ const graphState = this.database.prepare(`
1300
+ SELECT nodes_json, edges_json, jobs_json FROM graph_state WHERE namespace = ?
1301
+ `).get(key);
1302
+ const graphNodes = graphState ? parseJson(graphState.nodes_json, "graph_state.nodes_json") : [];
1303
+ const graphEdges = graphState ? parseJson(graphState.edges_json, "graph_state.edges_json") : [];
1304
+ const graphProjectionJobs = graphState ? parseJson(graphState.jobs_json, "graph_state.jobs_json") : [];
1083
1305
  const snapshot = {
1084
1306
  schemaVersion: STRATAGATE_STORAGE_SCHEMA_VERSION,
1085
1307
  currentTurn: space.current_turn,
@@ -1088,6 +1310,9 @@ var SqliteStorage = class {
1088
1310
  openTail,
1089
1311
  blocks,
1090
1312
  events,
1313
+ graphNodes,
1314
+ graphEdges,
1315
+ graphProjectionJobs,
1091
1316
  elements,
1092
1317
  extractionJobs,
1093
1318
  elementProjectionJobs,
@@ -1154,8 +1379,8 @@ var SqliteStorage = class {
1154
1379
  INSERT INTO blocks (
1155
1380
  namespace, id, thread_id, sequence, start_turn, end_turn, created_at, should_extract,
1156
1381
  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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1382
+ pointer_current_level, pointer_anchor_level, pointer_anchor_block_position, last_lifted_at, last_lifted_by
1383
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1159
1384
  ON CONFLICT (namespace, id) DO UPDATE SET
1160
1385
  thread_id = excluded.thread_id,
1161
1386
  sequence = excluded.sequence,
@@ -1172,7 +1397,8 @@ var SqliteStorage = class {
1172
1397
  pointer_current_level = excluded.pointer_current_level,
1173
1398
  pointer_anchor_level = excluded.pointer_anchor_level,
1174
1399
  pointer_anchor_block_position = excluded.pointer_anchor_block_position,
1175
- last_lifted_at = excluded.last_lifted_at
1400
+ last_lifted_at = excluded.last_lifted_at,
1401
+ last_lifted_by = excluded.last_lifted_by
1176
1402
  `);
1177
1403
  for (const block of snapshot.blocks) {
1178
1404
  insertBlock.run(
@@ -1193,7 +1419,8 @@ var SqliteStorage = class {
1193
1419
  block.pointerCurrentLevel,
1194
1420
  block.pointerAnchorLevel,
1195
1421
  block.pointerAnchorBlockPosition,
1196
- block.lastLiftedAt
1422
+ block.lastLiftedAt,
1423
+ block.lastLiftedBy
1197
1424
  );
1198
1425
  }
1199
1426
  const insertMessage = this.database.prepare(`
@@ -1418,6 +1645,21 @@ var SqliteStorage = class {
1418
1645
  job.updatedAt
1419
1646
  );
1420
1647
  }
1648
+ this.database.prepare(`
1649
+ INSERT INTO graph_state (namespace, nodes_json, edges_json, jobs_json, updated_at)
1650
+ VALUES (?, ?, ?, ?, ?)
1651
+ ON CONFLICT (namespace) DO UPDATE SET
1652
+ nodes_json = excluded.nodes_json,
1653
+ edges_json = excluded.edges_json,
1654
+ jobs_json = excluded.jobs_json,
1655
+ updated_at = excluded.updated_at
1656
+ `).run(
1657
+ namespace,
1658
+ JSON.stringify(snapshot.graphNodes),
1659
+ JSON.stringify(snapshot.graphEdges),
1660
+ JSON.stringify(snapshot.graphProjectionJobs),
1661
+ updatedAt
1662
+ );
1421
1663
  const insertReceipt = this.database.prepare(`
1422
1664
  INSERT INTO usage_receipts (namespace, receipt_id, event_ids_json, element_ids_json, audit_json, created_at)
1423
1665
  VALUES (?, ?, ?, ?, ?, ?)
@@ -1462,7 +1704,7 @@ var SqliteStorage = class {
1462
1704
  this.database.exec(THREAD_INDEXES);
1463
1705
  this.database.exec(`PRAGMA user_version = ${STRATAGATE_STORAGE_SCHEMA_VERSION}`);
1464
1706
  });
1465
- } else if (version === 1 || version === 2 || version === 3 || version === 4 || version === 5) {
1707
+ } else if (version === 1 || version === 2 || version === 3 || version === 4 || version === 5 || version === 6 || version === 7) {
1466
1708
  this.immediateTransaction(() => {
1467
1709
  this.database.exec(SCHEMA);
1468
1710
  if (version === 1) {
@@ -1495,6 +1737,9 @@ var SqliteStorage = class {
1495
1737
  ))
1496
1738
  `);
1497
1739
  }
1740
+ if (!blockColumns.some(({ name: name2 }) => name2 === "last_lifted_by")) {
1741
+ 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'))");
1742
+ }
1498
1743
  const messageColumns = this.database.prepare("PRAGMA table_info('messages')").all();
1499
1744
  if (!messageColumns.some(({ name: name2 }) => name2 === "thread_id")) {
1500
1745
  this.database.exec("ALTER TABLE messages ADD COLUMN thread_id TEXT");
@@ -1549,6 +1794,9 @@ function defaultIdFactory(prefix) {
1549
1794
  function defaultElementIdFactory(prefix) {
1550
1795
  return `${prefix}_${crypto.randomUUID()}`;
1551
1796
  }
1797
+ function defaultGraphIdFactory(prefix) {
1798
+ return `${prefix}_${crypto.randomUUID()}`;
1799
+ }
1552
1800
  function defaultSummary(messages) {
1553
1801
  const natural = messages.filter((message) => message.role === "user" || message.role === "assistant");
1554
1802
  const firstUser = natural.find((message) => message.role === "user");
@@ -1586,15 +1834,21 @@ var StrataGate = class _StrataGate {
1586
1834
  summarizer;
1587
1835
  extractor;
1588
1836
  elementProjector;
1837
+ disableElementProjection;
1838
+ graphProjector;
1589
1839
  now;
1590
1840
  idFactory;
1591
1841
  elementIdFactory;
1842
+ graphIdFactory;
1592
1843
  openTail = [];
1593
1844
  blocks = [];
1594
1845
  events = [];
1595
1846
  elements = [];
1847
+ graphNodes = [];
1848
+ graphEdges = [];
1596
1849
  extractionJobs = /* @__PURE__ */ new Map();
1597
1850
  elementProjectionJobs = /* @__PURE__ */ new Map();
1851
+ graphProjectionJobs = /* @__PURE__ */ new Map();
1598
1852
  usageReceipts = /* @__PURE__ */ new Map();
1599
1853
  successfulModelResponses = [];
1600
1854
  ingestionReceipts = /* @__PURE__ */ new Map();
@@ -1616,9 +1870,12 @@ var StrataGate = class _StrataGate {
1616
1870
  this.summarizer = options.summarizer;
1617
1871
  this.extractor = options.extractor;
1618
1872
  this.elementProjector = options.elementProjector;
1873
+ this.disableElementProjection = options.disableElementProjection ?? false;
1874
+ this.graphProjector = options.graphProjector;
1619
1875
  this.now = options.now ?? (() => /* @__PURE__ */ new Date());
1620
1876
  this.idFactory = options.idFactory ?? defaultIdFactory;
1621
1877
  this.elementIdFactory = options.elementIdFactory ?? defaultElementIdFactory;
1878
+ this.graphIdFactory = options.graphIdFactory ?? defaultGraphIdFactory;
1622
1879
  }
1623
1880
  static inMemory(options = {}) {
1624
1881
  return new _StrataGate(options, STRATAGATE_CONSTRUCTOR_TOKEN);
@@ -1639,9 +1896,12 @@ var StrataGate = class _StrataGate {
1639
1896
  ...options.summarizer ? { summarizer: options.summarizer } : {},
1640
1897
  ...options.extractor ? { extractor: options.extractor } : {},
1641
1898
  ...options.elementProjector ? { elementProjector: options.elementProjector } : {},
1899
+ ...options.disableElementProjection !== void 0 ? { disableElementProjection: options.disableElementProjection } : {},
1900
+ ...options.graphProjector ? { graphProjector: options.graphProjector } : {},
1642
1901
  ...options.now ? { now: options.now } : {},
1643
1902
  ...options.idFactory ? { idFactory: options.idFactory } : {},
1644
- ...options.elementIdFactory ? { elementIdFactory: options.elementIdFactory } : {}
1903
+ ...options.elementIdFactory ? { elementIdFactory: options.elementIdFactory } : {},
1904
+ ...options.graphIdFactory ? { graphIdFactory: options.graphIdFactory } : {}
1645
1905
  });
1646
1906
  } catch (error) {
1647
1907
  await storage.close();
@@ -1683,9 +1943,12 @@ var StrataGate = class _StrataGate {
1683
1943
  if (options.summarizer) memoryOptions.summarizer = options.summarizer;
1684
1944
  if (options.extractor) memoryOptions.extractor = options.extractor;
1685
1945
  if (options.elementProjector) memoryOptions.elementProjector = options.elementProjector;
1946
+ if (options.disableElementProjection !== void 0) memoryOptions.disableElementProjection = options.disableElementProjection;
1947
+ if (options.graphProjector) memoryOptions.graphProjector = options.graphProjector;
1686
1948
  if (options.now) memoryOptions.now = options.now;
1687
1949
  if (options.idFactory) memoryOptions.idFactory = options.idFactory;
1688
1950
  if (options.elementIdFactory) memoryOptions.elementIdFactory = options.elementIdFactory;
1951
+ if (options.graphIdFactory) memoryOptions.graphIdFactory = options.graphIdFactory;
1689
1952
  const memory = new _StrataGate(memoryOptions, STRATAGATE_CONSTRUCTOR_TOKEN);
1690
1953
  memory.storage = options.storage;
1691
1954
  memory.namespace = namespace;
@@ -1720,6 +1983,21 @@ var StrataGate = class _StrataGate {
1720
1983
  }
1721
1984
  });
1722
1985
  }
1986
+ const interruptedGraphProjections = [...memory.graphProjectionJobs.values()].filter((job) => job.status === "running");
1987
+ if (interruptedGraphProjections.length > 0) {
1988
+ await memory.commitMutation(() => {
1989
+ const now = toUtc8Iso(memory.now());
1990
+ for (const job of interruptedGraphProjections) {
1991
+ memory.graphProjectionJobs.set(job.id, {
1992
+ ...job,
1993
+ status: "failed",
1994
+ lastError: "Graph projection was interrupted before completion.",
1995
+ updatedAt: now
1996
+ });
1997
+ }
1998
+ });
1999
+ }
2000
+ if (memory.graphProjector) await memory.commitMutation(() => memory.queueMissingGraphProjections());
1723
2001
  } else {
1724
2002
  await memory.persist();
1725
2003
  }
@@ -1752,6 +2030,12 @@ var StrataGate = class _StrataGate {
1752
2030
  listElements() {
1753
2031
  return this.elements;
1754
2032
  }
2033
+ listGraphNodes() {
2034
+ return this.graphNodes;
2035
+ }
2036
+ listGraphEdges() {
2037
+ return this.graphEdges;
2038
+ }
1755
2039
  listOpenTail(threadId) {
1756
2040
  if (threadId === void 0) return this.openTail;
1757
2041
  return this.openTail.filter((message) => message.threadId === threadId);
@@ -1762,6 +2046,9 @@ var StrataGate = class _StrataGate {
1762
2046
  listElementProjectionJobs() {
1763
2047
  return [...this.elementProjectionJobs.values()];
1764
2048
  }
2049
+ listGraphProjectionJobs() {
2050
+ return [...this.graphProjectionJobs.values()];
2051
+ }
1765
2052
  listUsageReceipts() {
1766
2053
  return [...this.usageReceipts.values()];
1767
2054
  }
@@ -1789,6 +2076,9 @@ var StrataGate = class _StrataGate {
1789
2076
  openTail: this.openTail,
1790
2077
  blocks: this.blocks,
1791
2078
  events: this.events,
2079
+ graphNodes: this.graphNodes,
2080
+ graphEdges: this.graphEdges,
2081
+ graphProjectionJobs: [...this.graphProjectionJobs.values()],
1792
2082
  elements: this.elements,
1793
2083
  extractionJobs: [...this.extractionJobs.values()],
1794
2084
  elementProjectionJobs: [...this.elementProjectionJobs.values()],
@@ -1839,11 +2129,13 @@ var StrataGate = class _StrataGate {
1839
2129
  }
1840
2130
  if (this.threadOpenTail(threadId).filter((message) => message.role === "user").length < this.blockTurnSize) {
1841
2131
  const projectedElements2 = await this.projectEligibleElements() ?? [];
2132
+ await this.projectEligibleGraph();
1842
2133
  return { sealedBlock: null, extractedEvents: [], projectedElements: projectedElements2 };
1843
2134
  }
1844
2135
  const sealedBlock = await this.sealOpenTail(threadId);
1845
2136
  const extractedEvents = await this.extractEligibleBlock() ?? [];
1846
2137
  const projectedElements = await this.projectEligibleElements() ?? [];
2138
+ await this.projectEligibleGraph();
1847
2139
  return { sealedBlock, extractedEvents, projectedElements };
1848
2140
  }
1849
2141
  async resumePendingWork(options = {}) {
@@ -1856,12 +2148,14 @@ var StrataGate = class _StrataGate {
1856
2148
  sealedBlocks.push(await this.sealOpenTail(sealable.threadId));
1857
2149
  extractedEvents.push(...await this.extractEligibleBlock() ?? []);
1858
2150
  projectedElements.push(...await this.projectEligibleElements() ?? []);
2151
+ await this.projectEligibleGraph();
1859
2152
  }
1860
2153
  while (true) {
1861
2154
  const extracted = await this.extractEligibleBlock();
1862
2155
  if (extracted === null) break;
1863
2156
  extractedEvents.push(...extracted);
1864
2157
  projectedElements.push(...await this.projectEligibleElements() ?? []);
2158
+ await this.projectEligibleGraph();
1865
2159
  }
1866
2160
  if (options.retrySkipped === true) {
1867
2161
  const skippedBlockIds = this.blocks.filter((block) => this.nextBlockInThread(block) !== null && block.shouldExtract && this.extractionJobs.get(block.id)?.status === "skipped").map((block) => block.id);
@@ -1870,6 +2164,7 @@ var StrataGate = class _StrataGate {
1870
2164
  if (extracted === null) continue;
1871
2165
  extractedEvents.push(...extracted);
1872
2166
  projectedElements.push(...await this.projectEligibleElements() ?? []);
2167
+ await this.projectEligibleGraph();
1873
2168
  }
1874
2169
  }
1875
2170
  while (true) {
@@ -1877,12 +2172,14 @@ var StrataGate = class _StrataGate {
1877
2172
  if (projected === null) break;
1878
2173
  projectedElements.push(...projected);
1879
2174
  }
2175
+ await this.projectEligibleGraph();
1880
2176
  return { sealedBlocks, extractedEvents, projectedElements };
1881
2177
  }
1882
2178
  async addEvent(input) {
1883
2179
  return this.commitMutation(() => {
1884
2180
  const event = this.addEventInMemory(input);
1885
2181
  this.queueElementProjection([event.id]);
2182
+ this.queueGraphProjection([event.id], 1e3);
1886
2183
  return event;
1887
2184
  });
1888
2185
  }
@@ -1912,10 +2209,10 @@ var StrataGate = class _StrataGate {
1912
2209
  [event.temporal.originalText ?? "", 4],
1913
2210
  [`${event.temporal.happenedStart ?? ""} ${event.temporal.happenedEnd ?? ""}`, 4]
1914
2211
  ])).map(({ item }) => item);
1915
- const chronology = (event) => event.temporal.happenedStart ?? event.temporal.happenedEnd ?? event.temporal.mentionedAt ?? event.createdAt;
2212
+ const chronology2 = (event) => event.temporal.happenedStart ?? event.temporal.happenedEnd ?? event.temporal.mentionedAt ?? event.createdAt;
1916
2213
  const structured = (items) => [...items].sort((left, right) => {
1917
- if (options.temporalIntent === "first") return chronology(left).localeCompare(chronology(right));
1918
- if (options.temporalIntent === "latest") return chronology(right).localeCompare(chronology(left));
2214
+ if (options.temporalIntent === "first") return chronology2(left).localeCompare(chronology2(right));
2215
+ if (options.temporalIntent === "latest") return chronology2(right).localeCompare(chronology2(left));
1919
2216
  return memoryWeightAt(right, this.currentTurn) - memoryWeightAt(left, this.currentTurn) || right.updatedAt.localeCompare(left.updatedAt);
1920
2217
  });
1921
2218
  const participantIds = new Set(participantMatches.map(({ id }) => id));
@@ -2001,6 +2298,66 @@ var StrataGate = class _StrataGate {
2001
2298
  job.updatedAt = toUtc8Iso(this.now());
2002
2299
  });
2003
2300
  }
2301
+ async claimNextGraphProjection() {
2302
+ return this.commitMutation(() => {
2303
+ const job = [...this.graphProjectionJobs.values()].filter((candidate) => candidate.status === "pending" || candidate.status === "failed").sort((left, right) => right.priority - left.priority || left.createdAt.localeCompare(right.createdAt))[0];
2304
+ if (!job) return null;
2305
+ const events = job.sourceEventIds.flatMap((id) => this.events.find((event) => event.id === id) ?? []);
2306
+ if (events.length === 0) throw new Error(`Graph projection ${job.id} has no available source events`);
2307
+ job.status = "running";
2308
+ job.attempts += 1;
2309
+ job.lastError = null;
2310
+ job.updatedAt = toUtc8Iso(this.now());
2311
+ const eventText = normalizeSearchText(events.map((event) => [
2312
+ event.title,
2313
+ event.summary,
2314
+ event.tags.join(" "),
2315
+ (event.temporal.participants ?? []).join(" ")
2316
+ ].join(" ")).join(" "));
2317
+ const relevantNodes = this.graphNodes.filter((node) => [node.name, ...node.aliases].some((name2) => eventText.includes(normalizeSearchText(name2)))).concat([...this.graphNodes].sort((left, right) => right.updatedAt.localeCompare(left.updatedAt)).slice(0, 24));
2318
+ const existingNodes = [...new Map(relevantNodes.map((node) => [node.id, node])).values()].slice(0, 80);
2319
+ const nodeIds = new Set(existingNodes.map(({ id }) => id));
2320
+ return {
2321
+ jobId: job.id,
2322
+ projectorVersion: job.projectorVersion,
2323
+ events: structuredClone(events),
2324
+ existingNodes: structuredClone(existingNodes),
2325
+ existingEdges: structuredClone(this.graphEdges.filter(({ fromNodeId, toNodeId }) => nodeIds.has(fromNodeId) && nodeIds.has(toNodeId)).slice(-120))
2326
+ };
2327
+ });
2328
+ }
2329
+ async completeGraphProjection(jobId, result) {
2330
+ return this.commitMutation(() => {
2331
+ const job = this.requireGraphProjectionJob(jobId);
2332
+ if (job.status === "completed") return { nodeIds: job.nodeIds, edgeIds: job.edgeIds };
2333
+ if (job.status !== "running") throw new Error(`Graph projection ${job.id} is ${job.status}, not running`);
2334
+ const touched = applyGraphProjection({
2335
+ nodes: this.graphNodes,
2336
+ edges: this.graphEdges,
2337
+ events: this.events,
2338
+ result,
2339
+ allowedEventIds: new Set(job.sourceEventIds),
2340
+ now: toUtc8Iso(this.now()),
2341
+ idFactory: this.graphIdFactory
2342
+ });
2343
+ job.status = "completed";
2344
+ job.nodeIds = touched.nodeIds;
2345
+ job.edgeIds = touched.edgeIds;
2346
+ job.reason = typeof result.reason === "string" ? result.reason.trim().replace(/\s+/g, " ").slice(0, 500) || null : null;
2347
+ job.lastError = null;
2348
+ job.updatedAt = toUtc8Iso(this.now());
2349
+ return touched;
2350
+ });
2351
+ }
2352
+ async failGraphProjection(jobId, error) {
2353
+ await this.commitMutation(() => {
2354
+ const job = this.requireGraphProjectionJob(jobId);
2355
+ if (job.status === "completed") return;
2356
+ job.status = "failed";
2357
+ job.lastError = errorMessage(error);
2358
+ job.updatedAt = toUtc8Iso(this.now());
2359
+ });
2360
+ }
2004
2361
  async searchElements(query, options = {}) {
2005
2362
  const normalizedName = normalizeSearchText(options.name ?? "");
2006
2363
  const candidates = this.elements.flatMap((element) => element.facts.map((fact) => ({
@@ -2108,7 +2465,7 @@ var StrataGate = class _StrataGate {
2108
2465
  };
2109
2466
  });
2110
2467
  }
2111
- async expandBlock(id, target = "next") {
2468
+ async expandBlock(id, target = "next", source = "agent") {
2112
2469
  return this.commitMutation(() => {
2113
2470
  const block = this.blocks.find((candidate) => candidate.id === id);
2114
2471
  if (!block) throw new Error(`Unknown block: ${id}`);
@@ -2125,6 +2482,7 @@ var StrataGate = class _StrataGate {
2125
2482
  block.pointerAnchorLevel = level;
2126
2483
  block.pointerAnchorBlockPosition = latestBlockPosition;
2127
2484
  block.lastLiftedAt = toUtc8Iso(this.now());
2485
+ block.lastLiftedBy = source;
2128
2486
  return {
2129
2487
  id: block.id,
2130
2488
  ...block.threadId ? { threadId: block.threadId } : {},
@@ -2221,7 +2579,10 @@ var StrataGate = class _StrataGate {
2221
2579
  quotes: [...new Set(input.quotes ?? [])].slice(0, 12),
2222
2580
  sourceMessageIds,
2223
2581
  sourceBlockId: sourceBlock.id,
2224
- temporal: input.temporal ? { ...input.temporal } : { mentionedAt: now },
2582
+ temporal: {
2583
+ ...input.temporal ? { ...input.temporal } : { mentionedAt: now },
2584
+ eventType: normalizeStandardEventType(input.temporal?.eventType)
2585
+ },
2225
2586
  scope: input.scope ?? "user",
2226
2587
  criticality,
2227
2588
  confidence: Math.max(0, Math.min(1, input.confidence ?? 1)),
@@ -2260,7 +2621,59 @@ var StrataGate = class _StrataGate {
2260
2621
  if (!job) throw new Error(`Unknown element projection: ${id}`);
2261
2622
  return job;
2262
2623
  }
2624
+ async searchGraphNodes(query, limit = 8) {
2625
+ const candidates = this.graphNodes.filter((node) => node.status === "active" || node.status === "disputed");
2626
+ const ranked = bm25Rank(candidates, query, (node) => weightedSearchTokens([
2627
+ [node.name, 6],
2628
+ [node.aliases.join(" "), 5],
2629
+ [node.type, 2],
2630
+ [node.currentState, 4],
2631
+ [node.facts.map((fact) => `${fact.key} ${Array.isArray(fact.value) ? fact.value.join(" ") : fact.value}`).join(" "), 4],
2632
+ [this.graphEdges.filter((edge) => edge.fromNodeId === node.id || edge.toNodeId === node.id).map(({ relation }) => relation).join(" "), 3]
2633
+ ])).slice(0, Math.max(1, Math.min(20, limit)));
2634
+ if (searchTokens(query).length > 0 && ranked.length === 0) return [];
2635
+ return ranked.map(({ item: node, score }) => ({ node, score }));
2636
+ }
2637
+ requireGraphProjectionJob(id) {
2638
+ const job = this.graphProjectionJobs.get(id);
2639
+ if (!job) throw new Error(`Unknown graph projection: ${id}`);
2640
+ return job;
2641
+ }
2642
+ queueGraphProjection(sourceEventIds, priority) {
2643
+ if (!this.graphProjector) return null;
2644
+ const completed = new Set([...this.graphProjectionJobs.values()].filter((job2) => job2.projectorVersion === KNOWLEDGE_GRAPH_PROJECTOR_VERSION && job2.status === "completed").flatMap((job2) => job2.sourceEventIds));
2645
+ const queued = new Set([...this.graphProjectionJobs.values()].filter((job2) => job2.projectorVersion === KNOWLEDGE_GRAPH_PROJECTOR_VERSION && job2.status !== "completed").flatMap((job2) => job2.sourceEventIds));
2646
+ const ids = [...new Set(sourceEventIds.filter((id) => this.events.some((event) => event.id === id) && !completed.has(id) && !queued.has(id)))];
2647
+ if (ids.length === 0) return null;
2648
+ const now = toUtc8Iso(this.now());
2649
+ const job = {
2650
+ id: this.graphIdFactory("gproj"),
2651
+ sourceEventIds: ids,
2652
+ projectorVersion: KNOWLEDGE_GRAPH_PROJECTOR_VERSION,
2653
+ status: "pending",
2654
+ attempts: 0,
2655
+ priority,
2656
+ nodeIds: [],
2657
+ edgeIds: [],
2658
+ reason: null,
2659
+ lastError: null,
2660
+ createdAt: now,
2661
+ updatedAt: now
2662
+ };
2663
+ this.graphProjectionJobs.set(job.id, job);
2664
+ return job;
2665
+ }
2666
+ queueMissingGraphProjections() {
2667
+ const candidates = [...this.events].filter((event) => event.status !== "forgotten" && event.status !== "archived").sort((left, right) => {
2668
+ const score = (event) => (event.status === "active" ? 1e4 : 0) + event.weight.mentionCount * 100 + (event.scope === "project" ? 500 : 0) + (Date.parse(event.temporal.happenedStart ?? event.temporal.mentionedAt ?? event.updatedAt) || 0) / 1e12;
2669
+ return score(right) - score(left);
2670
+ });
2671
+ for (let index = 0; index < candidates.length; index += 8) {
2672
+ this.queueGraphProjection(candidates.slice(index, index + 8).map(({ id }) => id), candidates.length - index);
2673
+ }
2674
+ }
2263
2675
  queueElementProjection(sourceEventIds) {
2676
+ if (this.disableElementProjection) return null;
2264
2677
  const ids = [...new Set(sourceEventIds.filter((id) => this.events.some((event) => event.id === id)))];
2265
2678
  if (ids.length === 0) return null;
2266
2679
  const now = toUtc8Iso(this.now());
@@ -2354,7 +2767,8 @@ var StrataGate = class _StrataGate {
2354
2767
  pointerCurrentLevel: 5,
2355
2768
  pointerAnchorLevel: 5,
2356
2769
  pointerAnchorBlockPosition: blockPosition,
2357
- lastLiftedAt: null
2770
+ lastLiftedAt: null,
2771
+ lastLiftedBy: null
2358
2772
  };
2359
2773
  const sealedIds = new Set(raw.map((message) => message.id));
2360
2774
  const remaining = this.openTail.filter((message) => !sealedIds.has(message.id));
@@ -2428,7 +2842,11 @@ var StrataGate = class _StrataGate {
2428
2842
  }
2429
2843
  return this.commitMutation(() => {
2430
2844
  const extracted = result.shouldExtract ? result.events.map((event) => this.addEventInMemory({ ...event, sourceBlockId: target.id })) : [];
2431
- if (extracted.length > 0) this.queueElementProjection(extracted.map(({ id }) => id));
2845
+ if (extracted.length > 0) {
2846
+ const ids = extracted.map(({ id }) => id);
2847
+ this.queueElementProjection(ids);
2848
+ this.queueGraphProjection(ids, 1e3);
2849
+ }
2432
2850
  const job = this.extractionJobs.get(target.id);
2433
2851
  if (!job) throw new Error(`Missing extraction job for block: ${target.id}`);
2434
2852
  this.extractionJobs.set(target.id, {
@@ -2452,6 +2870,17 @@ var StrataGate = class _StrataGate {
2452
2870
  throw error;
2453
2871
  }
2454
2872
  }
2873
+ async projectEligibleGraph() {
2874
+ if (!this.graphProjector) return null;
2875
+ const batch = await this.claimNextGraphProjection();
2876
+ if (!batch) return null;
2877
+ try {
2878
+ return await this.completeGraphProjection(batch.jobId, await this.graphProjector(batch));
2879
+ } catch (error) {
2880
+ await this.failGraphProjection(batch.jobId, error);
2881
+ return null;
2882
+ }
2883
+ }
2455
2884
  async commitMutation(mutation) {
2456
2885
  const previous = this.mutationQueue;
2457
2886
  let release;
@@ -2495,11 +2924,15 @@ var StrataGate = class _StrataGate {
2495
2924
  this.openTail.splice(0, this.openTail.length, ...copy.openTail);
2496
2925
  this.blocks.splice(0, this.blocks.length, ...copy.blocks);
2497
2926
  this.events.splice(0, this.events.length, ...copy.events);
2927
+ this.graphNodes.splice(0, this.graphNodes.length, ...copy.graphNodes);
2928
+ this.graphEdges.splice(0, this.graphEdges.length, ...copy.graphEdges);
2498
2929
  this.elements.splice(0, this.elements.length, ...copy.elements);
2499
2930
  this.extractionJobs.clear();
2500
2931
  for (const job of copy.extractionJobs) this.extractionJobs.set(job.blockId, job);
2501
2932
  this.elementProjectionJobs.clear();
2502
2933
  for (const job of copy.elementProjectionJobs) this.elementProjectionJobs.set(job.id, job);
2934
+ this.graphProjectionJobs.clear();
2935
+ for (const job of copy.graphProjectionJobs) this.graphProjectionJobs.set(job.id, job);
2503
2936
  this.usageReceipts.clear();
2504
2937
  for (const receipt of copy.usageReceipts) this.usageReceipts.set(receipt.id, receipt);
2505
2938
  this.ingestionReceipts.clear();
@@ -2563,6 +2996,42 @@ var StrataGate = class _StrataGate {
2563
2996
  if (!elementIds.has(elementId)) throw new Error(`Element projection ${job.id} references unknown element ${elementId}`);
2564
2997
  }
2565
2998
  }
2999
+ const graphNodeIds = /* @__PURE__ */ new Set();
3000
+ for (const node of this.graphNodes) {
3001
+ if (graphNodeIds.has(node.id)) throw new Error(`Duplicate graph node ID in snapshot: ${node.id}`);
3002
+ graphNodeIds.add(node.id);
3003
+ for (const eventId of node.sourceEventIds) {
3004
+ if (!eventIds.has(eventId)) throw new Error(`Graph node ${node.id} references unknown event ${eventId}`);
3005
+ }
3006
+ for (const fact of node.facts) for (const eventId of fact.sourceEventIds) {
3007
+ if (!eventIds.has(eventId)) throw new Error(`Graph fact ${fact.id} references unknown event ${eventId}`);
3008
+ }
3009
+ }
3010
+ const graphEdgeIds = /* @__PURE__ */ new Set();
3011
+ for (const edge of this.graphEdges) {
3012
+ if (graphEdgeIds.has(edge.id)) throw new Error(`Duplicate graph edge ID in snapshot: ${edge.id}`);
3013
+ graphEdgeIds.add(edge.id);
3014
+ if (!graphNodeIds.has(edge.fromNodeId) || !graphNodeIds.has(edge.toNodeId)) {
3015
+ throw new Error(`Graph edge ${edge.id} references an unknown node`);
3016
+ }
3017
+ for (const eventId of edge.sourceEventIds) {
3018
+ if (!eventIds.has(eventId)) throw new Error(`Graph edge ${edge.id} references unknown event ${eventId}`);
3019
+ }
3020
+ }
3021
+ for (const event of this.events) for (const nodeId of event.temporal.participantNodeIds ?? []) {
3022
+ if (!graphNodeIds.has(nodeId)) throw new Error(`Event ${event.id} references unknown graph node ${nodeId}`);
3023
+ }
3024
+ for (const job of this.graphProjectionJobs.values()) {
3025
+ for (const eventId of job.sourceEventIds) if (!eventIds.has(eventId)) {
3026
+ throw new Error(`Graph projection ${job.id} references unknown event ${eventId}`);
3027
+ }
3028
+ for (const nodeId of job.nodeIds) if (!graphNodeIds.has(nodeId)) {
3029
+ throw new Error(`Graph projection ${job.id} references unknown node ${nodeId}`);
3030
+ }
3031
+ for (const edgeId of job.edgeIds) if (!graphEdgeIds.has(edgeId)) {
3032
+ throw new Error(`Graph projection ${job.id} references unknown edge ${edgeId}`);
3033
+ }
3034
+ }
2566
3035
  }
2567
3036
  };
2568
3037
 
@@ -2699,10 +3168,10 @@ var CRITICALITIES = /* @__PURE__ */ new Set(["routine", "preference", "identity"
2699
3168
  function object(value) {
2700
3169
  return value && typeof value === "object" && !Array.isArray(value) ? value : {};
2701
3170
  }
2702
- function strings(value) {
3171
+ function strings2(value) {
2703
3172
  return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
2704
3173
  }
2705
- function text(value, fallback = "") {
3174
+ function text2(value, fallback = "") {
2706
3175
  return typeof value === "string" ? value.trim() : fallback;
2707
3176
  }
2708
3177
  function l2Neighbor(block) {
@@ -2732,7 +3201,8 @@ var RETRY_MAX_TOKENS = 1e4;
2732
3201
  var STRUCTURED_FIELDS = {
2733
3202
  summarizer: ["l0Title", "l0Tags", "l1Summary", "l2Keypoints", "shouldExtract"],
2734
3203
  extractor: ["shouldExtract", "reason", "events"],
2735
- projector: ["reason", "changes"]
3204
+ projector: ["reason", "changes"],
3205
+ graphProjector: ["reason", "nodes", "edges"]
2736
3206
  };
2737
3207
  var STRING_ARRAY = { type: "array", items: { type: "string" } };
2738
3208
  var OPEN_OBJECT = { type: "object", additionalProperties: true };
@@ -2798,6 +3268,47 @@ var PROJECTOR_PARAMETERS = {
2798
3268
  reason: { type: "string", required: true },
2799
3269
  changes: { type: "array", items: ELEMENT_CHANGE, required: true }
2800
3270
  };
3271
+ var GRAPH_FACT = {
3272
+ type: "object",
3273
+ additionalProperties: false,
3274
+ properties: { key: { type: "string", required: true }, value: { ...VALUE, required: true }, sourceEventIds: { ...STRING_ARRAY, required: true } }
3275
+ };
3276
+ var GRAPH_NODE = {
3277
+ type: "object",
3278
+ additionalProperties: false,
3279
+ properties: {
3280
+ ref: { type: "string", required: true },
3281
+ name: { type: "string", required: true },
3282
+ type: { type: "string", enum: ["person", "project", "organization", "tool", "place"], required: true },
3283
+ aliases: STRING_ARRAY,
3284
+ state: { type: "string" },
3285
+ facts: { type: "array", items: GRAPH_FACT },
3286
+ status: { type: "string", enum: ["active", "superseded", "disputed", "archived"] },
3287
+ validFrom: { type: "string" },
3288
+ validTo: { type: "string" },
3289
+ confidence: { type: "number" },
3290
+ sourceEventIds: { ...STRING_ARRAY, required: true }
3291
+ }
3292
+ };
3293
+ var GRAPH_EDGE = {
3294
+ type: "object",
3295
+ additionalProperties: false,
3296
+ properties: {
3297
+ fromRef: { type: "string", required: true },
3298
+ toRef: { type: "string", required: true },
3299
+ relation: { type: "string", required: true },
3300
+ status: { type: "string", enum: ["active", "superseded", "disputed", "archived"] },
3301
+ validFrom: { type: "string" },
3302
+ validTo: { type: "string" },
3303
+ confidence: { type: "number" },
3304
+ sourceEventIds: { ...STRING_ARRAY, required: true }
3305
+ }
3306
+ };
3307
+ var GRAPH_PROJECTOR_PARAMETERS = {
3308
+ reason: { type: "string", required: true },
3309
+ nodes: { type: "array", items: GRAPH_NODE, required: true },
3310
+ edges: { type: "array", items: GRAPH_EDGE, required: true }
3311
+ };
2801
3312
  var STRUCTURED_TOOLS = {
2802
3313
  summarizer: {
2803
3314
  name: "stratagate_summarize_block",
@@ -2813,6 +3324,11 @@ var STRUCTURED_TOOLS = {
2813
3324
  name: "stratagate_project_element_cards",
2814
3325
  description: "Submit element-card changes supported by the supplied event cards.",
2815
3326
  parameters: PROJECTOR_PARAMETERS
3327
+ },
3328
+ graphProjector: {
3329
+ name: "stratagate_project_knowledge_graph",
3330
+ description: "Project stable graph nodes and directed edges from supplied event evidence.",
3331
+ parameters: GRAPH_PROJECTOR_PARAMETERS
2816
3332
  }
2817
3333
  };
2818
3334
  function toolSchema(kind) {
@@ -2850,10 +3366,10 @@ var DshModelBridge = class {
2850
3366
  { messages }
2851
3367
  ));
2852
3368
  return {
2853
- l0Title: text(raw.l0Title, "Conversation block").slice(0, 120),
2854
- l0Tags: strings(raw.l0Tags).slice(0, 12),
2855
- l1Summary: text(raw.l1Summary).slice(0, 2e3),
2856
- l2Keypoints: strings(raw.l2Keypoints).slice(0, 20),
3369
+ l0Title: text2(raw.l0Title, "Conversation block").slice(0, 120),
3370
+ l0Tags: strings2(raw.l0Tags).slice(0, 12),
3371
+ l1Summary: text2(raw.l1Summary).slice(0, 2e3),
3372
+ l2Keypoints: strings2(raw.l2Keypoints).slice(0, 20),
2857
3373
  shouldExtract: raw.shouldExtract === true
2858
3374
  };
2859
3375
  };
@@ -2861,21 +3377,21 @@ var DshModelBridge = class {
2861
3377
  const validMessageIds = new Set(context.target.l5Raw.map((message) => message.id));
2862
3378
  const raw = object(await this.callStructured(
2863
3379
  "extractor",
2864
- `Extract only durable, evidence-backed events from target.l5Raw, then call ${STRUCTURED_TOOLS.extractor.name} exactly once. The target block is the only legal source of new facts, quotations, and sourceMessageIds. neighbors.previous and neighbors.next are context-only L2 summaries; never extract from them. Every sourceMessageIds entry must exactly match allowedSourceMessageIds. If a fact appears only in a neighbor, do not extract it in this call. Events must be understandable later without the original chat. Use project scope for repository decisions, user scope for stable preferences/identity, and session scope for temporary task state. Use ISO-8601 timestamps with the explicit +08:00 offset in temporal fields. Do not turn an assistant statement that merely recalls older memory into a new event; require new human input or a new observable task/tool outcome from target.l5Raw. Do not return the result as text.`,
3380
+ `Extract only durable, evidence-backed events from target.l5Raw, then call ${STRUCTURED_TOOLS.extractor.name} exactly once. The target block is the only legal source of new facts, quotations, and sourceMessageIds. neighbors.previous and neighbors.next are context-only L2 summaries; never extract from them. Every sourceMessageIds entry must exactly match allowedSourceMessageIds. If a fact appears only in a neighbor, do not extract it in this call. Events must be understandable later without the original chat. Use project scope for repository decisions, user scope for stable preferences/identity, and session scope for temporary task state. temporal.eventType must use exactly one stable value: decision, release, task_completed, plan, change, cancellation, incident, meeting, collaboration, migration, or other. temporal.participants contains canonical entity names. Use ISO-8601 timestamps with the explicit +08:00 offset in temporal fields. Keep happened time separate from mentionedAt; when happened time is unknown omit it and set precision/basis to unknown. Do not turn an assistant statement that merely recalls older memory into a new event; require new human input or a new observable task/tool outcome from target.l5Raw. Do not return the result as text.`,
2865
3381
  extractorPayload(context)
2866
3382
  ));
2867
3383
  const events = (Array.isArray(raw.events) ? raw.events : []).map((candidate) => {
2868
3384
  const item = object(candidate);
2869
- const sourceMessageIds = strings(item.sourceMessageIds).filter((id) => validMessageIds.has(id));
3385
+ const sourceMessageIds = strings2(item.sourceMessageIds).filter((id) => validMessageIds.has(id));
2870
3386
  const scope = SCOPES.has(item.scope) ? item.scope : "project";
2871
3387
  const criticality = CRITICALITIES.has(item.criticality) ? item.criticality : "routine";
2872
- if (!text(item.title) || !text(item.summary) || sourceMessageIds.length === 0) return null;
3388
+ if (!text2(item.title) || !text2(item.summary) || sourceMessageIds.length === 0) return null;
2873
3389
  return {
2874
- title: text(item.title).slice(0, 200),
2875
- summary: text(item.summary).slice(0, 1e3),
2876
- narrative: text(item.narrative),
2877
- tags: strings(item.tags).slice(0, 16),
2878
- quotes: strings(item.quotes).slice(0, 12),
3390
+ title: text2(item.title).slice(0, 200),
3391
+ summary: text2(item.summary).slice(0, 1e3),
3392
+ narrative: text2(item.narrative),
3393
+ tags: strings2(item.tags).slice(0, 16),
3394
+ quotes: strings2(item.quotes).slice(0, 12),
2879
3395
  sourceMessageIds,
2880
3396
  sourceBlockId: context.target.id,
2881
3397
  temporal: object(item.temporal),
@@ -2886,7 +3402,7 @@ var DshModelBridge = class {
2886
3402
  }).filter((event) => event !== null);
2887
3403
  return {
2888
3404
  shouldExtract: raw.shouldExtract === true,
2889
- reason: text(raw.reason, events.length ? "Durable evidence extracted." : "No durable evidence."),
3405
+ reason: text2(raw.reason, events.length ? "Durable evidence extracted." : "No durable evidence."),
2890
3406
  events
2891
3407
  };
2892
3408
  };
@@ -2901,27 +3417,82 @@ var DshModelBridge = class {
2901
3417
  const item = object(candidate);
2902
3418
  const element = object(item.element);
2903
3419
  const type = element.type;
2904
- const sourceEventIds = strings(item.sourceEventIds).filter((id) => eventIds.has(id));
3420
+ const sourceEventIds = strings2(item.sourceEventIds).filter((id) => eventIds.has(id));
2905
3421
  const operation = item.operation;
2906
3422
  const mode = item.mode;
2907
3423
  const value = item.value;
2908
- if (!text(element.name) || !ELEMENT_TYPES2.has(type) || sourceEventIds.length === 0) return [];
3424
+ if (!text2(element.name) || !ELEMENT_TYPES2.has(type) || sourceEventIds.length === 0) return [];
2909
3425
  if (!["set_state", "add_set_item", "set_relation"].includes(String(operation))) return [];
2910
3426
  if (!["state", "set", "relation"].includes(String(mode))) return [];
2911
3427
  if (!(typeof value === "string" || Array.isArray(value) && value.every((entry) => typeof entry === "string"))) return [];
2912
3428
  return [{
2913
- element: { name: text(element.name), type, aliases: strings(element.aliases) },
3429
+ element: { name: text2(element.name), type, aliases: strings2(element.aliases) },
2914
3430
  operation,
2915
- key: text(item.key, "state"),
3431
+ key: text2(item.key, "state"),
2916
3432
  mode,
2917
3433
  value,
2918
- ...text(item.validFrom) ? { validFrom: text(item.validFrom) } : {},
2919
- ...text(item.validTo) ? { validTo: text(item.validTo) } : {},
3434
+ ...text2(item.validFrom) ? { validFrom: text2(item.validFrom) } : {},
3435
+ ...text2(item.validTo) ? { validTo: text2(item.validTo) } : {},
2920
3436
  sourceEventIds,
2921
3437
  ...typeof item.confidence === "number" ? { confidence: item.confidence } : {}
2922
3438
  }];
2923
3439
  });
2924
- return { reason: text(raw.reason, "Projected event evidence."), changes };
3440
+ return { reason: text2(raw.reason, "Projected event evidence."), changes };
3441
+ };
3442
+ graphProjector = async (context) => {
3443
+ const eventIds = new Set(context.events.map((event) => event.id));
3444
+ const raw = object(await this.callStructured(
3445
+ "graphProjector",
3446
+ `Project the supplied Events into the current Knowledge Graph, then call ${STRUCTURED_TOOLS.graphProjector.name} exactly once. Events are the sole source of truth; never use legacy Element data. Return stable entity nodes for people, projects, organizations, tools, and places. Use aliases to merge spelling/case/separator variants. Put attributes in node facts and every relationship in a directed edge using fromRef/toRef\u2014never encode a relationship as a fact string. Prefer concise canonical Chinese relation labels such as \u4F7F\u7528\u3001\u5C5E\u4E8E\u3001\u521B\u5EFA\u3001\u53C2\u4E0E\u3001\u8D21\u732E\u3001\u4F9D\u8D56\u3001\u4F4D\u4E8E\u3001\u76F8\u5173. Every node, fact, and edge must cite only supplied Event ids. Do not return text.`,
3447
+ context
3448
+ ));
3449
+ const nodes = (Array.isArray(raw.nodes) ? raw.nodes : []).flatMap((candidate) => {
3450
+ const item = object(candidate);
3451
+ const sourceEventIds = strings2(item.sourceEventIds).filter((id) => eventIds.has(id));
3452
+ const type = item.type;
3453
+ if (!text2(item.ref) || !text2(item.name) || !ELEMENT_TYPES2.has(type) || sourceEventIds.length === 0) return [];
3454
+ const facts = (Array.isArray(item.facts) ? item.facts : []).flatMap((candidateFact) => {
3455
+ const fact = object(candidateFact);
3456
+ const value = fact.value;
3457
+ if (!text2(fact.key) || !(typeof value === "string" || Array.isArray(value) && value.every((entry) => typeof entry === "string"))) return [];
3458
+ const sourceEventIds2 = strings2(fact.sourceEventIds).filter((id) => eventIds.has(id));
3459
+ if (sourceEventIds2.length === 0) return [];
3460
+ return [{ key: text2(fact.key), value, sourceEventIds: sourceEventIds2 }];
3461
+ });
3462
+ return [{
3463
+ ref: text2(item.ref),
3464
+ name: text2(item.name),
3465
+ type,
3466
+ aliases: strings2(item.aliases),
3467
+ ...text2(item.state) ? { state: text2(item.state) } : {},
3468
+ facts,
3469
+ ...typeof item.status === "string" ? { status: item.status } : {},
3470
+ ...text2(item.validFrom) ? { validFrom: text2(item.validFrom) } : {},
3471
+ ...text2(item.validTo) ? { validTo: text2(item.validTo) } : {},
3472
+ ...typeof item.confidence === "number" ? { confidence: item.confidence } : {},
3473
+ sourceEventIds
3474
+ }];
3475
+ });
3476
+ const refs = new Set(nodes.map(({ ref }) => ref));
3477
+ const edges = (Array.isArray(raw.edges) ? raw.edges : []).flatMap((candidate) => {
3478
+ const item = object(candidate);
3479
+ const sourceEventIds = strings2(item.sourceEventIds).filter((id) => eventIds.has(id));
3480
+ const fromRef = text2(item.fromRef);
3481
+ const toRef = text2(item.toRef);
3482
+ const relation = text2(item.relation);
3483
+ if (!refs.has(fromRef) || !refs.has(toRef) || !relation || sourceEventIds.length === 0) return [];
3484
+ return [{
3485
+ fromRef,
3486
+ toRef,
3487
+ relation,
3488
+ ...typeof item.status === "string" ? { status: item.status } : {},
3489
+ ...text2(item.validFrom) ? { validFrom: text2(item.validFrom) } : {},
3490
+ ...text2(item.validTo) ? { validTo: text2(item.validTo) } : {},
3491
+ ...typeof item.confidence === "number" ? { confidence: item.confidence } : {},
3492
+ sourceEventIds
3493
+ }];
3494
+ });
3495
+ return { reason: text2(raw.reason, "Projected Event evidence into the Knowledge Graph."), nodes, edges };
2925
3496
  };
2926
3497
  async callStructured(kind, system, payload) {
2927
3498
  const session = this.sessions.getStore();
@@ -3044,6 +3615,7 @@ ${JSON_RETRY_INSTRUCTION}`,
3044
3615
  import { createHash } from "node:crypto";
3045
3616
  import { existsSync } from "node:fs";
3046
3617
  import { resolve } from "node:path";
3618
+ import { createUserMessage as createUserMessage2 } from "@deepseek-ai/dsh-llm";
3047
3619
 
3048
3620
  // src/fold.ts
3049
3621
  function renderBlocks(blocks) {
@@ -3097,13 +3669,13 @@ var TurnFolder = class {
3097
3669
  if (event.data.source.kind !== "user") return null;
3098
3670
  const turn = this.activeTurn.get(sessionId);
3099
3671
  if (turn === void 0) return null;
3100
- const text2 = renderBlocks(event.data.content);
3101
- if (text2) this.pending(sessionId, turn).user.push(text2);
3672
+ const text3 = renderBlocks(event.data.content);
3673
+ if (text3) this.pending(sessionId, turn).user.push(text3);
3102
3674
  return null;
3103
3675
  }
3104
3676
  case "assistant/message": {
3105
- const text2 = renderBlocks(event.data.message.content);
3106
- if (text2) this.pending(sessionId, event.data.turn).assistant.push(text2);
3677
+ const text3 = renderBlocks(event.data.message.content);
3678
+ if (text3) this.pending(sessionId, event.data.turn).assistant.push(text3);
3107
3679
  return null;
3108
3680
  }
3109
3681
  case "tool/call": {
@@ -3139,7 +3711,8 @@ var TurnFolder = class {
3139
3711
  threadId: sessionId,
3140
3712
  assistantToolCalls: [...pending.tools.values()],
3141
3713
  createdAt: toUtc8Iso(event.time),
3142
- receiptId: `dsh:${sessionId}:turn:${event.data.turn}`
3714
+ receiptId: `dsh:${sessionId}:turn:${event.data.turn}`,
3715
+ dshTurn: event.data.turn
3143
3716
  };
3144
3717
  }
3145
3718
  default:
@@ -3219,6 +3792,7 @@ var DshMetadataStore = class {
3219
3792
  var AUTO_EVENT_LIMIT = 4;
3220
3793
  var AUTO_ELEMENT_LIMIT = 4;
3221
3794
  var AUTO_MEMORY_TOKEN_BUDGET = 900;
3795
+ var COMPACTION_SOURCE_PLUGIN = "stratagate-memory";
3222
3796
  function projectKey(cwd) {
3223
3797
  const canonical = resolve(cwd ?? process.cwd()).replaceAll("\\", "/").toLowerCase();
3224
3798
  return createHash("sha256").update(canonical).digest("hex").slice(0, 20);
@@ -3229,21 +3803,25 @@ function workspaceDisplayName(cwd) {
3229
3803
  }
3230
3804
  var StrataGateRuntime = class {
3231
3805
  constructor(config, models, onIngestError = () => {
3806
+ }, flushNativeSession = async () => {
3232
3807
  }) {
3233
3808
  this.config = config;
3234
3809
  this.models = models;
3235
3810
  this.onIngestError = onIngestError;
3811
+ this.flushNativeSession = flushNativeSession;
3236
3812
  this.blockDecayLambda = config.blockDecayLambda;
3237
3813
  }
3238
3814
  config;
3239
3815
  models;
3240
3816
  onIngestError;
3817
+ flushNativeSession;
3241
3818
  folder = new TurnFolder();
3242
3819
  spaces = /* @__PURE__ */ new Map();
3243
3820
  batches = /* @__PURE__ */ new Map();
3244
3821
  adopted = /* @__PURE__ */ new Map();
3245
3822
  pendingUse = /* @__PURE__ */ new Set();
3246
3823
  workspaceNames = /* @__PURE__ */ new Map();
3824
+ migrationTimers = /* @__PURE__ */ new Map();
3247
3825
  ingestTail = Promise.resolve();
3248
3826
  settingsTail = Promise.resolve();
3249
3827
  batchSequence = 0;
@@ -3259,7 +3837,15 @@ var StrataGateRuntime = class {
3259
3837
  }).then(async () => {
3260
3838
  const memory = await this.space(session);
3261
3839
  try {
3262
- await this.models.run(session, () => memory.appendTurn(turn));
3840
+ const result = await this.models.run(session, () => memory.appendTurn(turn));
3841
+ if (result.sealedBlock) {
3842
+ const contexts = memory.getBlockContext(String(session.id));
3843
+ const sealedContext = contexts.find(({ id }) => id === result.sealedBlock.id);
3844
+ if (!sealedContext) throw new Error(`Missing context for sealed StrataGate block ${result.sealedBlock.id}`);
3845
+ this.replaceSealedSurface(session, result.sealedBlock, sealedContext, turn.dshTurn);
3846
+ this.syncDecayedBlockSurface(session, contexts);
3847
+ await this.flushNativeSession(session);
3848
+ }
3263
3849
  } finally {
3264
3850
  await this.persistSuccessfulResponses(memory);
3265
3851
  }
@@ -3302,7 +3888,7 @@ var StrataGateRuntime = class {
3302
3888
  }
3303
3889
  async expandBlock(session, id, target) {
3304
3890
  await this.flush();
3305
- const result = await (await this.space(session)).expandBlock(id, target);
3891
+ const result = await (await this.space(session)).expandBlock(id, target, "agent");
3306
3892
  return this.batch(session, [{
3307
3893
  ref: `block:${result.id}:level:${result.level}`,
3308
3894
  target: { eventIds: [], elementIds: [] }
@@ -3417,23 +4003,78 @@ var StrataGateRuntime = class {
3417
4003
  await this.flush();
3418
4004
  const memory = await this.space(session);
3419
4005
  const threadId = String(session.id);
4006
+ const blockContexts = memory.getBlockContext(threadId);
4007
+ if (this.syncDecayedBlockSurface(session, blockContexts)) {
4008
+ await this.flushNativeSession(session);
4009
+ }
3420
4010
  const openTail = memory.listOpenTail(threadId);
3421
4011
  const activationQuery = [currentUserMessage(session), renderMessages(recentTurns(openTail, 2))].filter(Boolean).join("\n\n");
3422
- const [eventHits, elementHits] = activationQuery ? await Promise.all([
3423
- memory.searchEvents(activationQuery, { limit: 20 }),
3424
- memory.searchElements(activationQuery, { limit: 12 })
3425
- ]) : [[], []];
3426
- const events = activatedEvents(memory, eventHits).slice(0, AUTO_EVENT_LIMIT);
3427
- const elements = activatedElements(memory, elementHits).slice(0, AUTO_ELEMENT_LIMIT);
3428
- return [
3429
- "[Current conversation]",
3430
- openTail.length > 0 ? renderMessages(openTail) : "(open tail is empty)",
3431
- "",
3432
- "[Decayed memory blocks]",
3433
- renderBlocks2(memory.getBlockContext(threadId)),
3434
- "",
3435
- renderActivatedMemory(events, elements)
3436
- ].join("\n");
4012
+ const eventHits = activationQuery ? await memory.searchEvents(activationQuery, { limit: 20 }) : [];
4013
+ let graphNodes = [];
4014
+ if (activationQuery && typeof memory.searchGraphNodes === "function") {
4015
+ graphNodes = (await memory.searchGraphNodes(activationQuery, 12)).map(({ node }) => node);
4016
+ } else if (activationQuery) {
4017
+ const legacy = activatedElements(memory, await memory.searchElements(activationQuery, { limit: 12 }));
4018
+ graphNodes = legacy.map((item) => ({
4019
+ id: item.elementId,
4020
+ name: item.name,
4021
+ type: item.type,
4022
+ aliases: [],
4023
+ currentState: "",
4024
+ status: "active",
4025
+ confidence: item.fact.confidence ?? 0.8,
4026
+ sourceEventIds: item.fact.sourceEventIds,
4027
+ facts: [{ ...item.fact, confidence: item.fact.confidence ?? 0.8, status: item.fact.status === "disputed" ? "disputed" : item.fact.status === "superseded" ? "superseded" : "active" }],
4028
+ createdAt: item.fact.createdAt,
4029
+ updatedAt: item.fact.updatedAt
4030
+ }));
4031
+ }
4032
+ const events = activatedEvents(memory, eventHits);
4033
+ const currentBlockIds = new Set(memory.listBlocks().filter((block) => block.threadId === threadId).map(({ id }) => id));
4034
+ const currentEventIds = new Set(memory.listEvents().filter((event) => currentBlockIds.has(event.sourceBlockId)).map(({ id }) => id));
4035
+ const longTermEvents = events.filter((event) => !currentEventIds.has(event.id)).slice(0, AUTO_EVENT_LIMIT);
4036
+ graphNodes = graphNodes.filter((node) => !node.sourceEventIds.some((id) => currentEventIds.has(id))).slice(0, AUTO_ELEMENT_LIMIT);
4037
+ return renderActivatedMemory(longTermEvents, graphNodes);
4038
+ }
4039
+ /**
4040
+ * Replace the just-sealed DSH turns with one native surface message. The raw
4041
+ * events remain in the append-only log for transcript/evidence provenance,
4042
+ * while deriveMessages() sees only this compressed checkpoint.
4043
+ */
4044
+ replaceSealedSurface(session, block, context, endTurn) {
4045
+ const sourceEventSeqs = sealedSurfaceSeqs(session, endTurn, block.endTurn - block.startTurn + 1);
4046
+ const start = sourceEventSeqs[0];
4047
+ const end = sourceEventSeqs.at(-1);
4048
+ if (start === void 0 || end === void 0) {
4049
+ throw new Error(`Cannot compact StrataGate block ${block.id}: no DSH surface range was found`);
4050
+ }
4051
+ session.append("user/message", createUserMessage2({
4052
+ content: [{ type: "text", text: renderBlockSurfaceMessage(context) }],
4053
+ source: { kind: "plugin", plugin: COMPACTION_SOURCE_PLUGIN }
4054
+ }), {
4055
+ surfaceOp: { op: "replace", start, end },
4056
+ sourceEventSeqs
4057
+ });
4058
+ }
4059
+ /** Keep each native Block checkpoint synchronized with its current decay pointer. */
4060
+ syncDecayedBlockSurface(session, contexts) {
4061
+ const current = currentBlockSurfaceMessages(session);
4062
+ let changed = false;
4063
+ for (const context of contexts) {
4064
+ const node = current.get(context.id);
4065
+ if (!node) continue;
4066
+ const text3 = renderBlockSurfaceMessage(context);
4067
+ if (node.text === text3) continue;
4068
+ session.append("user/message", createUserMessage2({
4069
+ content: [{ type: "text", text: text3 }],
4070
+ source: { kind: "plugin", plugin: COMPACTION_SOURCE_PLUGIN }
4071
+ }), {
4072
+ surfaceOp: { op: "replace", start: node.seq, end: node.seq },
4073
+ sourceEventSeqs: [node.seq]
4074
+ });
4075
+ changed = true;
4076
+ }
4077
+ return changed;
3437
4078
  }
3438
4079
  // Keep the ingestion error for callers that explicitly require a flushed run.
3439
4080
  async settleIngestion() {
@@ -3445,6 +4086,8 @@ var StrataGateRuntime = class {
3445
4086
  async close() {
3446
4087
  if (this.closed) return;
3447
4088
  this.closed = true;
4089
+ for (const timer of this.migrationTimers.values()) clearTimeout(timer);
4090
+ this.migrationTimers.clear();
3448
4091
  let flushError;
3449
4092
  try {
3450
4093
  await this.flush();
@@ -3527,6 +4170,38 @@ var StrataGateRuntime = class {
3527
4170
  await update;
3528
4171
  return value;
3529
4172
  }
4173
+ async adminExpandBlock(namespace, id, target) {
4174
+ const key = namespace.trim();
4175
+ if (!key) throw new TypeError("StrataGate admin namespace must not be empty");
4176
+ const update = this.settingsTail.catch(() => {
4177
+ }).then(async () => {
4178
+ await this.flush();
4179
+ const active = this.spaces.get(key);
4180
+ if (active) return (await active).expandBlock(id, target, "user");
4181
+ if (this.config.database === ":memory:" || !existsSync(this.config.database)) {
4182
+ throw new Error(`Unknown StrataGate namespace: ${key}`);
4183
+ }
4184
+ const memory = await StrataGate.open({
4185
+ database: this.config.database,
4186
+ namespace: key,
4187
+ blockTurnSize: this.config.blockTurnSize,
4188
+ blockDecayLambda: this.blockDecayLambda,
4189
+ summarizer: this.models.summarizer,
4190
+ extractor: this.models.extractor,
4191
+ graphProjector: this.models.graphProjector,
4192
+ disableElementProjection: true
4193
+ });
4194
+ try {
4195
+ return await memory.expandBlock(id, target, "user");
4196
+ } finally {
4197
+ await memory.close();
4198
+ }
4199
+ });
4200
+ this.settingsTail = update.then(() => {
4201
+ }, () => {
4202
+ });
4203
+ return update;
4204
+ }
3530
4205
  async applyBlockDecayLambda(value) {
3531
4206
  await this.flush();
3532
4207
  this.blockDecayLambda = value;
@@ -3570,14 +4245,28 @@ var StrataGateRuntime = class {
3570
4245
  blockDecayLambda: this.blockDecayLambda,
3571
4246
  summarizer: this.models.summarizer,
3572
4247
  extractor: this.models.extractor,
3573
- elementProjector: this.models.projector
4248
+ graphProjector: this.models.graphProjector,
4249
+ disableElementProjection: true
3574
4250
  }).then(async (memory) => {
3575
4251
  try {
3576
4252
  try {
3577
- await this.models.run(session, () => memory.resumePendingWork({ retrySkipped: true }));
4253
+ const resumed = await this.models.run(session, () => memory.resumePendingWork({ retrySkipped: true }));
4254
+ const contexts = memory.getBlockContext(String(session.id));
4255
+ for (const block of resumed.sealedBlocks) {
4256
+ if (block.threadId !== String(session.id)) continue;
4257
+ const endTurn = dshTurnAtBlockEnd(session, block);
4258
+ const context = contexts.find(({ id }) => id === block.id);
4259
+ if (!context) throw new Error(`Missing context for recovered StrataGate block ${block.id}`);
4260
+ this.replaceSealedSurface(session, block, context, endTurn);
4261
+ }
4262
+ this.syncDecayedBlockSurface(session, contexts);
4263
+ if (resumed.sealedBlocks.some((block) => block.threadId === String(session.id))) {
4264
+ await this.flushNativeSession(session);
4265
+ }
3578
4266
  } finally {
3579
4267
  await this.persistSuccessfulResponses(memory);
3580
4268
  }
4269
+ this.scheduleGraphMigration(session, memory);
3581
4270
  return memory;
3582
4271
  } catch (error) {
3583
4272
  await memory.close().catch(() => {
@@ -3592,6 +4281,46 @@ var StrataGateRuntime = class {
3592
4281
  }
3593
4282
  return opening;
3594
4283
  }
4284
+ async searchGraph(session, query, limit = 8) {
4285
+ await this.flush();
4286
+ const results = await (await this.space(session)).searchGraphNodes(query, limit);
4287
+ return this.batch(session, results.map(({ node }) => ({
4288
+ ref: `graph-node:${node.id}`,
4289
+ target: { eventIds: node.sourceEventIds, elementIds: [] }
4290
+ })), results);
4291
+ }
4292
+ async expandGraphNode(session, id) {
4293
+ await this.flush();
4294
+ const memory = await this.space(session);
4295
+ const node = memory.listGraphNodes().find((candidate) => candidate.id === id);
4296
+ if (!node) throw new Error(`Unknown graph node: ${id}`);
4297
+ const edges = memory.listGraphEdges().filter(({ fromNodeId, toNodeId }) => fromNodeId === id || toNodeId === id);
4298
+ return this.batch(session, [{
4299
+ ref: `graph-node:${node.id}:expanded`,
4300
+ target: { eventIds: [.../* @__PURE__ */ new Set([...node.sourceEventIds, ...edges.flatMap(({ sourceEventIds }) => sourceEventIds)])], elementIds: [] }
4301
+ }], { node, edges });
4302
+ }
4303
+ scheduleGraphMigration(session, memory) {
4304
+ const namespace = this.namespaceFor(session);
4305
+ if (this.closed || this.migrationTimers.has(namespace)) return;
4306
+ if (typeof memory.listGraphProjectionJobs !== "function") return;
4307
+ const pending = memory.listGraphProjectionJobs().some(({ status }) => status === "pending" || status === "failed");
4308
+ if (!pending) return;
4309
+ const timer = setTimeout(() => {
4310
+ this.migrationTimers.delete(namespace);
4311
+ if (this.closed) return;
4312
+ const completedBefore = memory.listGraphProjectionJobs().filter(({ status }) => status === "completed").length;
4313
+ void this.models.run(session, () => memory.resumePendingWork()).then(async () => {
4314
+ await this.persistSuccessfulResponses(memory);
4315
+ const completedAfter = memory.listGraphProjectionJobs().filter(({ status }) => status === "completed").length;
4316
+ if (completedAfter > completedBefore) this.scheduleGraphMigration(session, memory);
4317
+ }).catch((error) => {
4318
+ this.onIngestError(error);
4319
+ });
4320
+ }, 1500);
4321
+ timer.unref?.();
4322
+ this.migrationTimers.set(namespace, timer);
4323
+ }
3595
4324
  rememberWorkspace(namespace, cwd) {
3596
4325
  const name2 = workspaceDisplayName(cwd);
3597
4326
  this.workspaceNames.set(namespace, name2);
@@ -3660,12 +4389,75 @@ function renderMessages(messages) {
3660
4389
  return details.join("\n");
3661
4390
  }).join("\n\n");
3662
4391
  }
3663
- function renderBlocks2(blocks) {
3664
- if (blocks.length === 0) return "(no sealed blocks)";
3665
- return blocks.map((block) => [
3666
- `block ${block.id} | turns ${block.turnRange[0]}-${block.turnRange[1]} | age ${block.age} | L${block.level}`,
3667
- block.content
3668
- ].join("\n")).join("\n\n");
4392
+ function dshTurnAtBlockEnd(session, block) {
4393
+ const blockEnd = Date.parse(block.createdAt);
4394
+ for (let index = session.events.length - 1; index >= 0; index -= 1) {
4395
+ const event = session.events[index];
4396
+ if (event?.type === "turn/end" && event.time === blockEnd) return event.data.turn;
4397
+ }
4398
+ throw new Error(`Cannot match StrataGate block ${block.id} to its completed DSH turn`);
4399
+ }
4400
+ function sealedSurfaceSeqs(session, endTurn, turnCount) {
4401
+ const currentSurface = [...session.surface.nodes];
4402
+ const currentSet = new Set(currentSurface);
4403
+ const completed = [];
4404
+ let open;
4405
+ for (const event of session.events) {
4406
+ if (event.type === "turn/start") {
4407
+ open = { turn: event.data.turn, start: event.seq };
4408
+ continue;
4409
+ }
4410
+ if (event.type !== "turn/end" || !open || event.data.turn !== open.turn) continue;
4411
+ const turnEvents = session.events.slice(open.start + 1, event.seq);
4412
+ const hasHumanMessage = turnEvents.some((candidate) => candidate.type === "user/message" && candidate.data.source.kind === "user");
4413
+ if (hasHumanMessage && event.data.turn <= endTurn) {
4414
+ completed.push({
4415
+ turn: event.data.turn,
4416
+ start: open.start,
4417
+ end: event.seq,
4418
+ nodes: currentSurface.filter((seq) => seq > open.start && seq < event.seq && currentSet.has(seq))
4419
+ });
4420
+ }
4421
+ open = void 0;
4422
+ }
4423
+ const selected = completed.slice(-turnCount);
4424
+ if (selected.length !== turnCount || selected.at(-1)?.turn !== endTurn) {
4425
+ throw new Error(`Cannot identify ${turnCount} completed DSH turns ending at turn ${endTurn}`);
4426
+ }
4427
+ const emptyTurn = selected.find((turn) => turn.nodes.length === 0);
4428
+ if (emptyTurn) {
4429
+ throw new Error(`Cannot compact DSH turn ${emptyTurn.turn}: its original messages are no longer on the surface`);
4430
+ }
4431
+ const start = selected[0].nodes[0];
4432
+ const end = selected.at(-1).nodes.at(-1);
4433
+ const startIndex = currentSurface.indexOf(start);
4434
+ const endIndex = currentSurface.indexOf(end);
4435
+ if (startIndex < 0 || endIndex < startIndex) {
4436
+ throw new Error(`Cannot identify a contiguous DSH surface range ending at turn ${endTurn}`);
4437
+ }
4438
+ return currentSurface.slice(startIndex, endIndex + 1);
4439
+ }
4440
+ function renderBlockSurfaceMessage(context) {
4441
+ return [
4442
+ "[StrataGate conversation block]",
4443
+ `Block: ${context.id}`,
4444
+ `Turns: ${context.turnRange[0]}-${context.turnRange[1]}`,
4445
+ `Level: L${context.level} (${context.label})`,
4446
+ "",
4447
+ context.content
4448
+ ].join("\n");
4449
+ }
4450
+ function currentBlockSurfaceMessages(session) {
4451
+ const blocks = /* @__PURE__ */ new Map();
4452
+ if (!session.surface?.nodes) return blocks;
4453
+ for (const seq of session.surface.nodes) {
4454
+ const event = session.events[seq];
4455
+ if (event?.type !== "user/message" || event.data.source.kind !== "plugin" || event.data.source.plugin !== COMPACTION_SOURCE_PLUGIN) continue;
4456
+ const text3 = event.data.content.flatMap((block) => block.type === "text" ? [block.text] : []).join("\n");
4457
+ const blockId = text3.match(/^\[StrataGate conversation block\]\nBlock: ([^\n]+)/u)?.[1] ?? text3.match(/^\[StrataGate compressed conversation\]\nBlock ([^;\n]+);/u)?.[1];
4458
+ if (blockId) blocks.set(blockId, { seq, text: text3 });
4459
+ }
4460
+ return blocks;
3669
4461
  }
3670
4462
  function activatedEvents(memory, relevance) {
3671
4463
  const allowed = new Map(relevance.map(({ event }) => [event.id, event]));
@@ -3709,7 +4501,7 @@ function activatedElements(memory, relevance) {
3709
4501
  weight
3710
4502
  ]).map(({ item }) => item);
3711
4503
  }
3712
- function renderActivatedMemory(events, elements) {
4504
+ function renderActivatedMemory(events, graphNodes) {
3713
4505
  const heading = [
3714
4506
  "[Activated long-term memory]",
3715
4507
  "Historical memory context.",
@@ -3719,7 +4511,7 @@ function renderActivatedMemory(events, elements) {
3719
4511
  const lines = [...heading];
3720
4512
  let tokens = estimateTokens(lines.join("\n"));
3721
4513
  let eventCount = 0;
3722
- let elementCount = 0;
4514
+ let nodeCount = 0;
3723
4515
  for (const event of events) {
3724
4516
  const rendered = JSON.stringify({
3725
4517
  id: event.id,
@@ -3738,25 +4530,24 @@ Events:
3738
4530
  tokens += cost;
3739
4531
  eventCount += 1;
3740
4532
  }
3741
- for (const element of elements) {
4533
+ for (const node of graphNodes) {
3742
4534
  const rendered = JSON.stringify({
3743
- elementId: element.elementId,
3744
- name: element.name,
3745
- key: element.fact.key,
3746
- value: element.fact.value,
3747
- validFrom: element.fact.validFrom,
3748
- validTo: element.fact.validTo
4535
+ nodeId: node.id,
4536
+ name: node.name,
4537
+ type: node.type,
4538
+ currentState: node.currentState,
4539
+ facts: node.facts.filter(({ status }) => status === "active").map(({ key, value, validFrom, validTo }) => ({ key, value, validFrom, validTo }))
3749
4540
  });
3750
4541
  const cost = estimateTokens(`
3751
- ElementFacts:
4542
+ KnowledgeGraph:
3752
4543
  - ${rendered}`);
3753
4544
  if (tokens + cost > AUTO_MEMORY_TOKEN_BUDGET) break;
3754
- if (elementCount === 0) lines.push("ElementFacts:");
4545
+ if (nodeCount === 0) lines.push("KnowledgeGraph:");
3755
4546
  lines.push(`- ${rendered}`);
3756
4547
  tokens += cost;
3757
- elementCount += 1;
4548
+ nodeCount += 1;
3758
4549
  }
3759
- if (eventCount === 0 && elementCount === 0) lines.push("(no activated memory)");
4550
+ if (eventCount === 0 && nodeCount === 0) lines.push("(no activated memory)");
3760
4551
  return lines.join("\n");
3761
4552
  }
3762
4553
  function estimateTokens(value) {
@@ -3816,9 +4607,26 @@ function registerMemoryTools(ctx, runtime) {
3816
4607
  ...args.participants ? { participants: args.participants } : {}
3817
4608
  })
3818
4609
  }));
4610
+ ctx.tools.register(defineTool({
4611
+ name: "memory_search_graph",
4612
+ description: "Search the current Event-backed Knowledge Graph for people, projects, organizations, tools, places, facts, and relations.",
4613
+ parameters: {
4614
+ query: { type: "string", required: true },
4615
+ limit: { type: "integer", description: "Maximum results, 1-20." }
4616
+ },
4617
+ output: jsonOutput,
4618
+ execute: async (args, exec) => runtime.searchGraph(sessionOf(exec), args.query, args.limit ?? 8)
4619
+ }));
4620
+ ctx.tools.register(defineTool({
4621
+ name: "memory_expand_graph_node",
4622
+ description: "Expand one Knowledge Graph node with its current facts, directed edges, and supporting Event evidence.",
4623
+ parameters: { id: { type: "string", required: true } },
4624
+ output: jsonOutput,
4625
+ execute: async (args, exec) => runtime.expandGraphNode(sessionOf(exec), args.id)
4626
+ }));
3819
4627
  ctx.tools.register(defineTool({
3820
4628
  name: "memory_search_elements",
3821
- description: "Search current Element-card facts about people, projects, organizations, tools, or places. Returns evidenceRefs that must be assessed before use.",
4629
+ description: "Deprecated compatibility search for legacy Element-card data. Prefer memory_search_graph.",
3822
4630
  parameters: {
3823
4631
  query: { type: "string", required: true },
3824
4632
  limit: { type: "integer" },
@@ -3888,7 +4696,7 @@ function registerMemoryTools(ctx, runtime) {
3888
4696
  missing: { type: "string", required: true },
3889
4697
  next_strategy: {
3890
4698
  type: "string",
3891
- enum: ["answer", "search_events", "expand_event", "search_elements", "expand_element", "search_raw_memory", "expand_block"],
4699
+ enum: ["answer", "search_events", "expand_event", "search_graph", "expand_graph_node", "search_elements", "expand_element", "search_raw_memory", "expand_block"],
3892
4700
  required: true
3893
4701
  }
3894
4702
  },
@@ -3911,6 +4719,20 @@ function registerMemoryTools(ctx, runtime) {
3911
4719
  }
3912
4720
 
3913
4721
  // src/web.ts
4722
+ import { createRequire } from "node:module";
4723
+ var STRATAGATE_DSH_VERSION = "0.2.22";
4724
+ var LEGACY_THREAD_ID = "__legacy__";
4725
+ var nodeRequire = createRequire(import.meta.url);
4726
+ function installedPackageVersion(names) {
4727
+ for (const name2 of names) {
4728
+ try {
4729
+ const value = nodeRequire(`${name2}/package.json`);
4730
+ if (typeof value.version === "string" && value.version.trim()) return value.version;
4731
+ } catch {
4732
+ }
4733
+ }
4734
+ return "unknown";
4735
+ }
3914
4736
  function sendJson(res, status, body) {
3915
4737
  res.statusCode = status;
3916
4738
  res.setHeader("Content-Type", "application/json; charset=utf-8");
@@ -3921,8 +4743,8 @@ function numeric(value, fallback, minimum, maximum) {
3921
4743
  const parsed = Number(value);
3922
4744
  return Number.isFinite(parsed) ? Math.min(maximum, Math.max(minimum, Math.floor(parsed))) : fallback;
3923
4745
  }
3924
- function redact(text2) {
3925
- return text2.replace(/\b(?:sk|gh[opasu]|github_pat)_[A-Za-z0-9_-]{12,}\b/g, "[REDACTED_TOKEN]").replace(/\b(Bearer\s+)[A-Za-z0-9._~+/-]{12,}={0,2}\b/gi, "$1[REDACTED]").replace(/\b(api[_-]?key|token|password|secret)\s*[:=]\s*([^\s,;]+)/gi, "$1=[REDACTED]");
4746
+ function redact(text3) {
4747
+ return text3.replace(/\b(?:sk|gh[opasu]|github_pat)_[A-Za-z0-9_-]{12,}\b/g, "[REDACTED_TOKEN]").replace(/\b(Bearer\s+)[A-Za-z0-9._~+/-]{12,}={0,2}\b/gi, "$1[REDACTED]").replace(/\b(api[_-]?key|token|password|secret)\s*[:=]\s*([^\s,;]+)/gi, "$1=[REDACTED]");
3926
4748
  }
3927
4749
  function redactValue(value) {
3928
4750
  if (typeof value === "string") return redact(value);
@@ -3953,11 +4775,23 @@ function sourceMessages(snapshot, ids) {
3953
4775
  }
3954
4776
  return output;
3955
4777
  }
4778
+ function blockLayers(block) {
4779
+ return [
4780
+ { level: 0, content: `${block.l0Title}
4781
+ \u6807\u7B7E\uFF1A${block.l0Tags.join("\u3001") || "\u65E0"}` },
4782
+ { level: 1, content: block.l1Summary || block.l0Title },
4783
+ { level: 2, content: block.l2Keypoints.map((point) => `\u2022 ${point}`).join("\n") || block.l1Summary || block.l0Title },
4784
+ { level: 3, content: block.l3Condensed || block.l2Keypoints.join("\n") || block.l1Summary },
4785
+ { level: 4, content: block.l4Readable || block.l3Condensed },
4786
+ { level: 5, content: block.l5Raw.map((message) => `${message.role}: ${message.content}`).join("\n\n") }
4787
+ ];
4788
+ }
3956
4789
  function eventSummary(event) {
3957
4790
  return {
3958
4791
  id: event.id,
3959
4792
  title: event.title,
3960
4793
  summary: event.summary,
4794
+ narrative: event.narrative,
3961
4795
  tags: event.tags,
3962
4796
  sourceBlockId: event.sourceBlockId,
3963
4797
  sourceMessageIds: event.sourceMessageIds,
@@ -4009,8 +4843,8 @@ async function overview(runtime) {
4009
4843
  for (const namespace of namespaces) {
4010
4844
  const snapshot = await runtime.adminSnapshot(namespace);
4011
4845
  if (!snapshot) continue;
4012
- const failedJobs = snapshot.extractionJobs.filter(({ status }) => status === "failed").length + snapshot.elementProjectionJobs.filter(({ status }) => status === "failed").length;
4013
- const processingJobs = snapshot.extractionJobs.filter(({ status }) => status === "running").length + snapshot.elementProjectionJobs.filter(({ status }) => status === "pending" || status === "running").length;
4846
+ const failedJobs = snapshot.extractionJobs.filter(({ status }) => status === "failed").length + snapshot.graphProjectionJobs.filter(({ status }) => status === "failed").length;
4847
+ const processingJobs = snapshot.extractionJobs.filter(({ status }) => status === "running").length + snapshot.graphProjectionJobs.filter(({ status }) => status === "pending" || status === "running").length;
4014
4848
  const failedJobDetails = [
4015
4849
  ...snapshot.extractionJobs.filter(({ status }) => status === "failed").map((job) => ({
4016
4850
  id: job.blockId,
@@ -4020,9 +4854,9 @@ async function overview(runtime) {
4020
4854
  lastErrorFull: job.lastError,
4021
4855
  updatedAt: job.updatedAt
4022
4856
  })),
4023
- ...snapshot.elementProjectionJobs.filter(({ status }) => status === "failed").map((job) => ({
4857
+ ...snapshot.graphProjectionJobs.filter(({ status }) => status === "failed").map((job) => ({
4024
4858
  id: job.id,
4025
- kind: "element-projection",
4859
+ kind: "graph-projection",
4026
4860
  attempts: job.attempts,
4027
4861
  lastError: job.lastError?.slice(0, 500) ?? null,
4028
4862
  lastErrorFull: job.lastError,
@@ -4033,6 +4867,7 @@ async function overview(runtime) {
4033
4867
  ...snapshot.blocks.map(({ createdAt }) => createdAt),
4034
4868
  ...snapshot.events.map(({ updatedAt }) => updatedAt),
4035
4869
  ...snapshot.elements.map(({ updatedAt }) => updatedAt),
4870
+ ...snapshot.graphNodes.map(({ updatedAt }) => updatedAt),
4036
4871
  ...snapshot.usageReceipts.map(({ createdAt }) => createdAt)
4037
4872
  ].sort();
4038
4873
  rows.push({
@@ -4047,6 +4882,15 @@ async function overview(runtime) {
4047
4882
  events: snapshot.events.length,
4048
4883
  activeEvents: snapshot.events.filter(({ status }) => status === "active").length,
4049
4884
  elements: snapshot.elements.length,
4885
+ graphNodes: snapshot.graphNodes.length,
4886
+ graphEdges: snapshot.graphEdges.length,
4887
+ graphMigration: (() => {
4888
+ const projected = new Set(snapshot.graphProjectionJobs.filter(({ status, projectorVersion }) => status === "completed" && projectorVersion === KNOWLEDGE_GRAPH_PROJECTOR_VERSION).flatMap(({ sourceEventIds }) => sourceEventIds)).size;
4889
+ const failed = snapshot.graphProjectionJobs.filter(({ status }) => status === "failed").length;
4890
+ const running = snapshot.graphProjectionJobs.filter(({ status }) => status === "running").length;
4891
+ const total = snapshot.events.filter(({ status }) => status !== "forgotten" && status !== "archived").length;
4892
+ return { projected, total, failed, running, complete: projected >= total };
4893
+ })(),
4050
4894
  usageReceipts: snapshot.usageReceipts.length,
4051
4895
  memoryUseCount: snapshot.usageReceipts.filter((receipt) => receipt.eventIds.length > 0 || receipt.elementIds.length > 0).length,
4052
4896
  failedJobs,
@@ -4056,7 +4900,13 @@ async function overview(runtime) {
4056
4900
  lastActivityAt: timestamps.at(-1) ?? null
4057
4901
  });
4058
4902
  }
4059
- return { readonly: true, settingsWritable: true, namespaces: rows };
4903
+ return {
4904
+ readonly: true,
4905
+ settingsWritable: true,
4906
+ pluginVersion: STRATAGATE_DSH_VERSION,
4907
+ harnessVersion: installedPackageVersion(["@deepseek-ai/dsh", "@deepseek-ai/dsh-session"]),
4908
+ namespaces: rows
4909
+ };
4060
4910
  }
4061
4911
  async function updateSettings(runtime, url) {
4062
4912
  const raw = url.searchParams.get("blockDecayLambda")?.trim() ?? "";
@@ -4066,6 +4916,126 @@ async function updateSettings(runtime, url) {
4066
4916
  }
4067
4917
  return { blockDecayLambda: await runtime.adminSetBlockDecayLambda(value) };
4068
4918
  }
4919
+ function receiptThreadId(id) {
4920
+ const match = /^dsh:(.+):turn:\d+$/.exec(id);
4921
+ return match?.[1]?.trim() || null;
4922
+ }
4923
+ function timestampKey(value) {
4924
+ const parsed = Date.parse(value);
4925
+ return Number.isFinite(parsed) ? String(parsed) : value;
4926
+ }
4927
+ function recoverSnapshotView(snapshot) {
4928
+ const receiptThreads = /* @__PURE__ */ new Map();
4929
+ const receiptActivity = /* @__PURE__ */ new Map();
4930
+ const receiptCandidates = /* @__PURE__ */ new Map();
4931
+ for (const receipt of snapshot.ingestionReceipts) {
4932
+ const threadId = receiptThreadId(receipt.id);
4933
+ if (!threadId) continue;
4934
+ receiptThreads.set(receipt.id, threadId);
4935
+ const currentActivity = receiptActivity.get(threadId);
4936
+ if (!currentActivity || receipt.createdAt > currentActivity) receiptActivity.set(threadId, receipt.createdAt);
4937
+ const key = timestampKey(receipt.createdAt);
4938
+ const candidates = receiptCandidates.get(key) ?? /* @__PURE__ */ new Set();
4939
+ candidates.add(threadId);
4940
+ receiptCandidates.set(key, candidates);
4941
+ }
4942
+ const exactThreadAt = new Map([...receiptCandidates].filter(([, ids]) => ids.size === 1).map(([createdAt, ids]) => [createdAt, [...ids][0]]));
4943
+ const recoverMessages = (messages) => {
4944
+ let precedingThreadId = null;
4945
+ return messages.map((message) => {
4946
+ const explicit = message.threadId?.trim();
4947
+ const exact = exactThreadAt.get(timestampKey(message.createdAt));
4948
+ const recovered = explicit || exact || (message.role === "assistant" ? precedingThreadId : null);
4949
+ const threadId = recovered || LEGACY_THREAD_ID;
4950
+ if (message.role === "user" || explicit || exact) precedingThreadId = threadId;
4951
+ return { message, threadId };
4952
+ });
4953
+ };
4954
+ const blocks = [];
4955
+ for (const source of snapshot.blocks) {
4956
+ const recovered = recoverMessages(source.l5Raw);
4957
+ const groups = /* @__PURE__ */ new Map();
4958
+ for (const item of recovered) {
4959
+ const messages = groups.get(item.threadId) ?? [];
4960
+ messages.push(item.message);
4961
+ groups.set(item.threadId, messages);
4962
+ }
4963
+ const entries = [...groups];
4964
+ for (const [threadId, messages] of entries) {
4965
+ const virtual = !source.threadId && (entries.length > 1 || threadId !== LEGACY_THREAD_ID);
4966
+ blocks.push({
4967
+ id: entries.length > 1 ? `virtual:${source.id}:${encodeURIComponent(threadId)}` : source.id,
4968
+ source,
4969
+ threadId,
4970
+ messages,
4971
+ virtual,
4972
+ turnRange: [0, 0]
4973
+ });
4974
+ }
4975
+ }
4976
+ const turnCounters = /* @__PURE__ */ new Map();
4977
+ for (const block of blocks) {
4978
+ if (block.source.threadId) {
4979
+ block.turnRange = [block.source.startTurn, block.source.endTurn];
4980
+ turnCounters.set(block.threadId, Math.max(turnCounters.get(block.threadId) ?? 0, block.source.endTurn));
4981
+ continue;
4982
+ }
4983
+ const turns = Math.max(1, block.messages.filter(({ role }) => role === "user").length);
4984
+ const start = (turnCounters.get(block.threadId) ?? 0) + 1;
4985
+ block.turnRange = [start, start + turns - 1];
4986
+ turnCounters.set(block.threadId, start + turns - 1);
4987
+ }
4988
+ return {
4989
+ blocks,
4990
+ openMessages: recoverMessages(snapshot.openTail),
4991
+ receiptThreads,
4992
+ receiptActivity
4993
+ };
4994
+ }
4995
+ function virtualBlockLayers(block) {
4996
+ if (!block.virtual || block.messages.length === block.source.l5Raw.length) return blockLayers(block.source);
4997
+ const deterministic = deterministicBlockLayers(block.messages);
4998
+ const natural = block.messages.filter(({ role }) => role === "user" || role === "assistant");
4999
+ const firstUser = natural.find(({ role, content }) => role === "user" && content.trim());
5000
+ const title = firstUser?.content.replace(/\s+/g, " ").trim().slice(0, 80) || "\u65E7\u4F1A\u8BDD\u7247\u6BB5";
5001
+ const summary = natural.map(({ content }) => content.replace(/\s+/g, " ").trim()).filter(Boolean).join(" ").slice(0, 500);
5002
+ const keypoints = natural.filter(({ role }) => role === "user").map(({ content }) => content.replace(/\s+/g, " ").trim().slice(0, 160));
5003
+ return [
5004
+ { level: 0, content: title },
5005
+ { level: 1, content: summary || title },
5006
+ { level: 2, content: keypoints.map((point) => `\u2022 ${point}`).join("\n") || summary || title },
5007
+ { level: 3, content: deterministic.l3Condensed },
5008
+ { level: 4, content: deterministic.l4Readable },
5009
+ { level: 5, content: block.messages.map((message) => `${message.role}: ${message.content}`).join("\n\n") }
5010
+ ];
5011
+ }
5012
+ function conversationRows(snapshot, view = recoverSnapshotView(snapshot)) {
5013
+ const ids = /* @__PURE__ */ new Set([
5014
+ ...view.blocks.map((block) => block.threadId),
5015
+ ...view.openMessages.map(({ threadId }) => threadId),
5016
+ ...view.receiptThreads.values()
5017
+ ]);
5018
+ return [...ids].map((id) => {
5019
+ const blocks = view.blocks.filter((block) => block.threadId === id);
5020
+ const messages = [
5021
+ ...blocks.flatMap((block) => block.messages),
5022
+ ...view.openMessages.filter((message) => message.threadId === id).map(({ message }) => message)
5023
+ ];
5024
+ const firstUser = messages.find(({ role, content }) => role === "user" && content.trim());
5025
+ const title = firstUser?.content.replace(/\s+/g, " ").trim().slice(0, 28);
5026
+ const timestamps = [
5027
+ ...blocks.map(({ source }) => source.createdAt),
5028
+ ...messages.map(({ createdAt }) => createdAt),
5029
+ ...view.receiptActivity.get(id) ? [view.receiptActivity.get(id)] : []
5030
+ ].sort();
5031
+ return {
5032
+ id,
5033
+ label: id === LEGACY_THREAD_ID ? "\u5386\u53F2\u5BF9\u8BDD" : title || `\u5BF9\u8BDD ${id.slice(0, 8)}`,
5034
+ blocks: blocks.length,
5035
+ lastActivityAt: timestamps.at(-1) ?? null
5036
+ };
5037
+ }).sort((left, right) => String(right.lastActivityAt).localeCompare(String(left.lastActivityAt)));
5038
+ }
4069
5039
  async function memories(runtime, url) {
4070
5040
  const namespace = url.searchParams.get("namespace")?.trim() ?? "";
4071
5041
  if (!namespace) throw new AdminHttpError(400, "namespace is required");
@@ -4075,49 +5045,117 @@ async function memories(runtime, url) {
4075
5045
  const offset = numeric(url.searchParams.get("offset"), 0, 0, Number.MAX_SAFE_INTEGER);
4076
5046
  const limit = numeric(url.searchParams.get("limit"), 100, 1, 200);
4077
5047
  let values;
4078
- if (kind === "events") values = snapshot.events.map((event) => ({
5048
+ if (kind === "events") values = [...snapshot.events].sort((left, right) => {
5049
+ const time = (event) => event.temporal.happenedStart ?? event.temporal.happenedEnd ?? event.temporal.mentionedAt ?? event.createdAt;
5050
+ return time(right).localeCompare(time(left));
5051
+ }).map((event) => ({
4079
5052
  ...eventSummary(event),
5053
+ relatedNodes: snapshot.graphNodes.filter(({ id, sourceEventIds }) => sourceEventIds.includes(event.id) || (event.temporal.participantNodeIds ?? []).includes(id)).map(({ id, name: name2, type }) => ({ id, name: name2, type })),
4080
5054
  relatedElements: snapshot.elements.filter(({ sourceEventIds }) => sourceEventIds.includes(event.id)).map(({ id, name: name2 }) => ({ id, name: name2 }))
4081
5055
  }));
4082
- 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";
5056
+ else if (kind === "graph") {
5057
+ const eventMap = new Map(snapshot.events.map((event) => [event.id, event]));
5058
+ return {
5059
+ namespace,
5060
+ kind,
5061
+ projectorVersion: KNOWLEDGE_GRAPH_PROJECTOR_VERSION,
5062
+ nodes: snapshot.graphNodes.map((node) => ({
5063
+ ...node,
5064
+ supportingEvents: node.sourceEventIds.flatMap((id) => eventMap.get(id) ?? []).map(eventSummary)
5065
+ })),
5066
+ edges: snapshot.graphEdges,
5067
+ migration: (() => {
5068
+ const projected = new Set(snapshot.graphProjectionJobs.filter(({ status, projectorVersion }) => status === "completed" && projectorVersion === KNOWLEDGE_GRAPH_PROJECTOR_VERSION).flatMap(({ sourceEventIds }) => sourceEventIds)).size;
5069
+ return {
5070
+ projected,
5071
+ total: snapshot.events.filter(({ status }) => status !== "forgotten" && status !== "archived").length,
5072
+ pending: snapshot.graphProjectionJobs.filter(({ status }) => status === "pending").length,
5073
+ running: snapshot.graphProjectionJobs.filter(({ status }) => status === "running").length,
5074
+ failed: snapshot.graphProjectionJobs.filter(({ status }) => status === "failed").length,
5075
+ complete: projected >= snapshot.events.filter(({ status }) => status !== "forgotten" && status !== "archived").length
5076
+ };
5077
+ })()
5078
+ };
5079
+ } else if (kind === "elements") values = snapshot.elements.map(elementSummary);
5080
+ else if (kind === "blocks") {
5081
+ const recovered = recoverSnapshotView(snapshot);
5082
+ const conversations = conversationRows(snapshot, recovered);
5083
+ const requestedThreadId = url.searchParams.get("threadId")?.trim() ?? "";
5084
+ const activeThreadId = requestedThreadId || conversations[0]?.id || null;
5085
+ const scopedBlocks = activeThreadId ? recovered.blocks.filter((block) => block.threadId === activeThreadId) : [];
5086
+ values = scopedBlocks.map((block) => {
5087
+ const source = block.source;
5088
+ const extraction = snapshot.extractionJobs.find(({ blockId }) => blockId === source.id);
5089
+ const blockMessageIds = new Set(block.messages.map(({ id }) => id));
5090
+ const relatedEvents = snapshot.events.filter((event) => event.sourceBlockId === source.id && (!block.virtual || event.sourceMessageIds.some((id) => blockMessageIds.has(id))));
5091
+ const eventIds = new Set(relatedEvents.map(({ id }) => id));
5092
+ const projections = snapshot.graphProjectionJobs.filter(({ sourceEventIds }) => sourceEventIds.some((id) => eventIds.has(id)));
5093
+ const relatedNodes = snapshot.graphNodes.filter(({ sourceEventIds }) => sourceEventIds.some((id) => eventIds.has(id))).map(({ id, name: name2, type }) => ({ id, name: name2, type }));
5094
+ const failedProjection = projections.find(({ status: status2 }) => status2 === "failed");
5095
+ const pendingProjection = projections.some(({ status: status2 }) => status2 === "pending" || status2 === "running");
5096
+ const needsExtraction = source.shouldExtract === true;
5097
+ const status = extraction?.status === "failed" || failedProjection ? "failed" : extraction?.status === "succeeded" || extraction?.status === "skipped" ? pendingProjection ? "processing" : "organized" : needsExtraction ? "waiting" : "organized";
5098
+ const blockPosition = scopedBlocks.findIndex(({ id }) => id === block.id) + 1;
5099
+ const latestBlockPosition = scopedBlocks.length;
5100
+ const currentLevel = getDecayedBlockLevel(
5101
+ source.pointerAnchorLevel,
5102
+ source.threadId ? source.pointerAnchorBlockPosition : Math.min(source.pointerAnchorBlockPosition, blockPosition),
5103
+ latestBlockPosition,
5104
+ snapshot.blockDecayLambda
5105
+ );
5106
+ return {
5107
+ id: block.id,
5108
+ sourceBlockId: source.id,
5109
+ threadId: block.threadId,
5110
+ sequence: source.sequence,
5111
+ turnRange: block.turnRange,
5112
+ 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,
5113
+ tags: source.l0Tags,
5114
+ summary: source.l1Summary,
5115
+ keypoints: source.l2Keypoints,
5116
+ currentLevel,
5117
+ distanceFromLatest: Math.max(0, latestBlockPosition - blockPosition),
5118
+ expansionSource: source.lastLiftedAt ? source.lastLiftedBy ?? "legacy" : null,
5119
+ lastLiftedAt: source.lastLiftedAt,
5120
+ sourceMessages: block.messages.length,
5121
+ createdAt: source.createdAt,
5122
+ virtual: block.virtual,
5123
+ status,
5124
+ eventExtraction: extraction ? {
5125
+ status: extraction.status,
5126
+ attempts: extraction.attempts,
5127
+ updatedAt: extraction.updatedAt,
5128
+ lastError: extraction.lastError
5129
+ } : null,
5130
+ graphProjection: projections.length ? {
5131
+ status: failedProjection ? "failed" : pendingProjection ? "processing" : "completed",
5132
+ jobs: projections.length,
5133
+ lastError: failedProjection?.lastError ?? null
5134
+ } : null,
5135
+ relatedEvents: relatedEvents.map(eventSummary),
5136
+ relatedNodes
5137
+ };
5138
+ });
5139
+ const filtered2 = values.filter((value) => matchesQuery(value, query));
5140
+ const latestSealedTurn = scopedBlocks.reduce((latest, block) => Math.max(latest, block.turnRange[1]), 0);
5141
+ const openMessages = activeThreadId ? recovered.openMessages.filter((message) => message.threadId === activeThreadId).map(({ message }) => message) : [];
5142
+ const openTurns = openMessages.filter(({ role }) => role === "user").length;
4093
5143
  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
5144
+ namespace,
5145
+ kind,
5146
+ total: filtered2.length,
5147
+ offset,
5148
+ limit,
5149
+ items: filtered2.slice(offset, offset + limit),
5150
+ openBlock: {
5151
+ turnRange: openTurns > 0 ? [latestSealedTurn + 1, latestSealedTurn + openTurns] : null,
5152
+ messages: openMessages.length,
5153
+ status: "open"
5154
+ },
5155
+ conversations,
5156
+ activeThreadId
4118
5157
  };
4119
- });
4120
- else throw new AdminHttpError(400, `Unsupported memory kind: ${kind}`);
5158
+ } else throw new AdminHttpError(400, `Unsupported memory kind: ${kind}`);
4121
5159
  const filtered = values.filter((value) => matchesQuery(value, query));
4122
5160
  return { namespace, kind, total: filtered.length, offset, limit, items: filtered.slice(offset, offset + limit) };
4123
5161
  }
@@ -4126,6 +5164,7 @@ async function sources(runtime, url) {
4126
5164
  if (!namespace) throw new AdminHttpError(400, "namespace is required");
4127
5165
  const snapshot = await requiredSnapshot(runtime, namespace);
4128
5166
  const eventId = url.searchParams.get("eventId");
5167
+ const nodeId = url.searchParams.get("nodeId");
4129
5168
  const elementId = url.searchParams.get("elementId");
4130
5169
  const blockId = url.searchParams.get("blockId");
4131
5170
  let events = [];
@@ -4136,6 +5175,18 @@ async function sources(runtime, url) {
4136
5175
  if (!event) throw new AdminHttpError(404, `Unknown event: ${eventId}`);
4137
5176
  events = [event];
4138
5177
  ids = new Set(event.sourceMessageIds);
5178
+ } else if (nodeId) {
5179
+ const node = snapshot.graphNodes.find(({ id }) => id === nodeId);
5180
+ if (!node) throw new AdminHttpError(404, `Unknown graph node: ${nodeId}`);
5181
+ events = snapshot.events.filter(({ id }) => node.sourceEventIds.includes(id));
5182
+ ids = new Set(events.flatMap(({ sourceMessageIds }) => sourceMessageIds));
5183
+ return {
5184
+ namespace,
5185
+ node,
5186
+ edges: snapshot.graphEdges.filter(({ fromNodeId, toNodeId }) => fromNodeId === node.id || toNodeId === node.id),
5187
+ events: events.map(eventSummary),
5188
+ messages: sourceMessages(snapshot, ids)
5189
+ };
4139
5190
  } else if (elementId) {
4140
5191
  const element = snapshot.elements.find(({ id }) => id === elementId);
4141
5192
  if (!element) throw new AdminHttpError(404, `Unknown element: ${elementId}`);
@@ -4143,22 +5194,43 @@ async function sources(runtime, url) {
4143
5194
  events = snapshot.events.filter(({ id }) => element.sourceEventIds.includes(id));
4144
5195
  ids = new Set(events.flatMap(({ sourceMessageIds }) => sourceMessageIds));
4145
5196
  } else if (blockId) {
4146
- const block = snapshot.blocks.find(({ id }) => id === blockId);
5197
+ const displayBlock = recoverSnapshotView(snapshot).blocks.find(({ id }) => id === blockId);
5198
+ const block = displayBlock?.source ?? snapshot.blocks.find(({ id }) => id === blockId);
4147
5199
  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);
5200
+ const messages = displayBlock?.messages ?? block.l5Raw;
5201
+ ids = new Set(messages.map(({ id }) => id));
5202
+ events = snapshot.events.filter((event) => event.sourceBlockId === block.id && (!displayBlock?.virtual || event.sourceMessageIds.some((id) => ids.has(id))));
4150
5203
  const eventIds = new Set(events.map(({ id }) => id));
4151
5204
  elements = snapshot.elements.filter(({ sourceEventIds }) => sourceEventIds.some((id) => eventIds.has(id)));
5205
+ return {
5206
+ namespace,
5207
+ events: events.map(eventSummary),
5208
+ elements: elements.map(elementSummary),
5209
+ messages: sourceMessages(snapshot, ids),
5210
+ layers: displayBlock ? virtualBlockLayers(displayBlock) : blockLayers(block),
5211
+ virtual: displayBlock?.virtual ?? false
5212
+ };
4152
5213
  } else {
4153
- throw new AdminHttpError(400, "eventId, elementId, or blockId is required");
5214
+ throw new AdminHttpError(400, "eventId, nodeId, elementId, or blockId is required");
4154
5215
  }
4155
5216
  return {
4156
5217
  namespace,
4157
5218
  events: events.map(eventSummary),
4158
5219
  elements: elements.map(elementSummary),
5220
+ blocks: events.map((event) => snapshot.blocks.find(({ id }) => id === event.sourceBlockId)).filter((block) => Boolean(block)).map((block) => ({ id: block.id, title: block.l0Title, createdAt: block.createdAt })),
4159
5221
  messages: sourceMessages(snapshot, ids)
4160
5222
  };
4161
5223
  }
5224
+ async function expandBlock(runtime, url) {
5225
+ const namespace = url.searchParams.get("namespace")?.trim() ?? "";
5226
+ const blockId = url.searchParams.get("blockId")?.trim() ?? "";
5227
+ const target = url.searchParams.get("level")?.trim() ?? "";
5228
+ if (!namespace) throw new AdminHttpError(400, "namespace is required");
5229
+ if (!blockId) throw new AdminHttpError(400, "blockId is required");
5230
+ if (blockId.startsWith("virtual:")) throw new AdminHttpError(409, "Recovered legacy fragments are read-only display data");
5231
+ if (!/^L?[0-5]$/i.test(target)) throw new AdminHttpError(400, "level must be L0 through L5");
5232
+ return runtime.adminExpandBlock(namespace, blockId, target);
5233
+ }
4162
5234
  function receiptSources(snapshot, receipt) {
4163
5235
  const events = snapshot.events.filter(({ id }) => receipt.eventIds.includes(id));
4164
5236
  const elements = snapshot.elements.filter(({ id }) => receipt.elementIds.includes(id));
@@ -4194,6 +5266,9 @@ async function handleAdminRequest(runtime, req, res) {
4194
5266
  if (path === "/api/stratagate/settings") {
4195
5267
  if (req.method !== "PATCH") throw new AdminHttpError(405, "StrataGate settings require PATCH");
4196
5268
  sendJson(res, 200, await updateSettings(runtime, url));
5269
+ } else if (path === "/api/stratagate/blocks/expand") {
5270
+ if (req.method !== "PATCH") throw new AdminHttpError(405, "StrataGate Block expansion requires PATCH");
5271
+ sendJson(res, 200, await expandBlock(runtime, url));
4197
5272
  } else if (req.method !== "GET") throw new AdminHttpError(405, "StrataGate memory data is read-only");
4198
5273
  else if (path === "/api/stratagate/overview") sendJson(res, 200, await overview(runtime));
4199
5274
  else if (path === "/api/stratagate/memories") sendJson(res, 200, await memories(runtime, url));
@@ -4223,7 +5298,7 @@ var MEMORY_PROTOCOL = `[StrataGate memory protocol]
4223
5298
  StrataGate provides durable, evidence-gated memory through memory_* tools.
4224
5299
 
4225
5300
  - Search memory when the current task could depend on prior project decisions, user preferences, people, tools, historical outcomes, or unresolved work. Do not search for facts already established in the current conversation.
4226
- - Start with memory_search_events for decisions and history, or memory_search_elements for the current state of a person/project/tool/place/organization.
5301
+ - Start with memory_search_events for decisions and history, or memory_search_graph for the current state of a person/project/tool/place/organization.
4227
5302
  - Every retrieval replaces the latest batch. Call memory_assess after each batch before relying on it. Cite only evidenceRefs returned by that exact latest batch.
4228
5303
  - If assessment is partial or wrong, follow nextStrategy: refine the search, expand an Element/block, or search raw memory. Do not present uncertain memory as fact.
4229
5304
  - Every retrieval batch must be closed with memory_record_use before the turn can end. Pass evidence_refs containing exactly the refs actually used, or [] when no retrieved evidence was used. Non-empty refs require a sufficient assessment of that latest batch. Never use a numeric increment; StrataGate applies one reinforcement per selected card.
@@ -4237,6 +5312,8 @@ async function apply(ctx, config) {
4237
5312
  const models = new DshModelBridge(ctx, resolved);
4238
5313
  const runtime = new StrataGateRuntime(resolved, models, (error) => {
4239
5314
  ctx.logger.error(`stratagate-memory ingestion failed: ${renderError(error)}`);
5315
+ }, async (session) => {
5316
+ await ctx.sessions.flush(session);
4240
5317
  });
4241
5318
  await runtime.syncConfiguredSettings();
4242
5319
  ctx.systemPrompt.section({ name: "tool:stratagate-memory", order: 113, text: MEMORY_PROTOCOL });
@@ -4245,10 +5322,10 @@ async function apply(ctx, config) {
4245
5322
  const session = context.agent?.session;
4246
5323
  if (!session) return assembled;
4247
5324
  try {
4248
- const text2 = await runtime.buildAutoContext(session);
5325
+ const text3 = await runtime.buildAutoContext(session);
4249
5326
  return {
4250
5327
  ...assembled,
4251
- contexts: [...assembled.contexts, { name: "stratagate:auto-memory", text: text2 }]
5328
+ contexts: [...assembled.contexts, { name: "stratagate:auto-memory", text: text3 }]
4252
5329
  };
4253
5330
  } catch (error) {
4254
5331
  ctx.logger.warn(`stratagate-memory auto-context failed: ${renderError(error)}`);
@@ -4257,7 +5334,7 @@ async function apply(ctx, config) {
4257
5334
  });
4258
5335
  ctx.on("agent/turn-stopping", ({ agent }) => {
4259
5336
  if (!runtime.needsRecordUse(agent.session)) return;
4260
- agent.steer(createUserMessage2({
5337
+ agent.steer(createUserMessage3({
4261
5338
  content: [{
4262
5339
  type: "text",
4263
5340
  text: "A StrataGate retrieval batch is still unresolved. Before ending this turn, call memory_record_use with evidence_refs set to exactly the retrieved refs used in the answer, or [] if none were used."