stratagate-dsh 0.2.21 → 0.2.26

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 = 7;
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,6 +553,9 @@ 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;
@@ -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,15 +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()
434
619
  };
435
620
  } else if (schemaVersion === 6) {
436
621
  const legacy = value;
437
622
  snapshot = {
438
623
  ...structuredClone(legacy),
439
624
  schemaVersion: STRATAGATE_STORAGE_SCHEMA_VERSION,
440
- blocks: legacy.blocks.map((block) => ({ ...structuredClone(block), lastLiftedBy: null }))
625
+ blocks: legacy.blocks.map((block) => ({ ...structuredClone(block), lastLiftedBy: null })),
626
+ ...emptyGraph()
441
627
  };
628
+ } else if (schemaVersion === 7) {
629
+ const legacy = value;
630
+ snapshot = { ...structuredClone(legacy), schemaVersion: STRATAGATE_STORAGE_SCHEMA_VERSION, ...emptyGraph() };
442
631
  } else if (schemaVersion === STRATAGATE_STORAGE_SCHEMA_VERSION) {
443
632
  snapshot = structuredClone(value);
444
633
  } else {
@@ -453,10 +642,13 @@ function normalizeSnapshot(value) {
453
642
  if (!Number.isFinite(snapshot.blockDecayLambda) || snapshot.blockDecayLambda < 0) {
454
643
  throw new TypeError("Invalid StrataGate snapshot: blockDecayLambda must be a non-negative finite number");
455
644
  }
456
- 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"]) {
457
646
  if (!Array.isArray(snapshot[key])) throw new TypeError(`Invalid StrataGate snapshot: ${key} must be an array`);
458
647
  }
459
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
+ }
460
652
  for (const block of snapshot.blocks) {
461
653
  if (!Number.isSafeInteger(block.pointerAnchorBlockPosition) || block.pointerAnchorBlockPosition < 1) {
462
654
  throw new TypeError("Invalid StrataGate snapshot: pointerAnchorBlockPosition must be a positive integer");
@@ -817,6 +1009,15 @@ CREATE TABLE IF NOT EXISTS element_projection_jobs (
817
1009
  FOREIGN KEY (namespace) REFERENCES memory_spaces(namespace) ON DELETE CASCADE
818
1010
  ) STRICT;
819
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
+
820
1021
  CREATE TABLE IF NOT EXISTS usage_receipts (
821
1022
  namespace TEXT NOT NULL,
822
1023
  receipt_id TEXT NOT NULL,
@@ -954,7 +1155,10 @@ var SqliteStorage = class {
954
1155
  quotes: parseJson(row.quotes_json, "events.quotes_json"),
955
1156
  sourceMessageIds: sourcesByEvent.get(row.id) ?? [],
956
1157
  sourceBlockId: row.source_block_id,
957
- 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
+ })(),
958
1162
  scope: row.scope,
959
1163
  criticality: row.criticality,
960
1164
  confidence: row.confidence,
@@ -1092,6 +1296,12 @@ var SqliteStorage = class {
1092
1296
  id: row.receipt_id,
1093
1297
  createdAt: row.created_at
1094
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") : [];
1095
1305
  const snapshot = {
1096
1306
  schemaVersion: STRATAGATE_STORAGE_SCHEMA_VERSION,
1097
1307
  currentTurn: space.current_turn,
@@ -1100,6 +1310,9 @@ var SqliteStorage = class {
1100
1310
  openTail,
1101
1311
  blocks,
1102
1312
  events,
1313
+ graphNodes,
1314
+ graphEdges,
1315
+ graphProjectionJobs,
1103
1316
  elements,
1104
1317
  extractionJobs,
1105
1318
  elementProjectionJobs,
@@ -1432,6 +1645,21 @@ var SqliteStorage = class {
1432
1645
  job.updatedAt
1433
1646
  );
1434
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
+ );
1435
1663
  const insertReceipt = this.database.prepare(`
1436
1664
  INSERT INTO usage_receipts (namespace, receipt_id, event_ids_json, element_ids_json, audit_json, created_at)
1437
1665
  VALUES (?, ?, ?, ?, ?, ?)
@@ -1476,7 +1704,7 @@ var SqliteStorage = class {
1476
1704
  this.database.exec(THREAD_INDEXES);
1477
1705
  this.database.exec(`PRAGMA user_version = ${STRATAGATE_STORAGE_SCHEMA_VERSION}`);
1478
1706
  });
1479
- } else if (version === 1 || version === 2 || version === 3 || version === 4 || version === 5 || version === 6) {
1707
+ } else if (version === 1 || version === 2 || version === 3 || version === 4 || version === 5 || version === 6 || version === 7) {
1480
1708
  this.immediateTransaction(() => {
1481
1709
  this.database.exec(SCHEMA);
1482
1710
  if (version === 1) {
@@ -1566,6 +1794,9 @@ function defaultIdFactory(prefix) {
1566
1794
  function defaultElementIdFactory(prefix) {
1567
1795
  return `${prefix}_${crypto.randomUUID()}`;
1568
1796
  }
1797
+ function defaultGraphIdFactory(prefix) {
1798
+ return `${prefix}_${crypto.randomUUID()}`;
1799
+ }
1569
1800
  function defaultSummary(messages) {
1570
1801
  const natural = messages.filter((message) => message.role === "user" || message.role === "assistant");
1571
1802
  const firstUser = natural.find((message) => message.role === "user");
@@ -1603,15 +1834,21 @@ var StrataGate = class _StrataGate {
1603
1834
  summarizer;
1604
1835
  extractor;
1605
1836
  elementProjector;
1837
+ disableElementProjection;
1838
+ graphProjector;
1606
1839
  now;
1607
1840
  idFactory;
1608
1841
  elementIdFactory;
1842
+ graphIdFactory;
1609
1843
  openTail = [];
1610
1844
  blocks = [];
1611
1845
  events = [];
1612
1846
  elements = [];
1847
+ graphNodes = [];
1848
+ graphEdges = [];
1613
1849
  extractionJobs = /* @__PURE__ */ new Map();
1614
1850
  elementProjectionJobs = /* @__PURE__ */ new Map();
1851
+ graphProjectionJobs = /* @__PURE__ */ new Map();
1615
1852
  usageReceipts = /* @__PURE__ */ new Map();
1616
1853
  successfulModelResponses = [];
1617
1854
  ingestionReceipts = /* @__PURE__ */ new Map();
@@ -1633,9 +1870,12 @@ var StrataGate = class _StrataGate {
1633
1870
  this.summarizer = options.summarizer;
1634
1871
  this.extractor = options.extractor;
1635
1872
  this.elementProjector = options.elementProjector;
1873
+ this.disableElementProjection = options.disableElementProjection ?? false;
1874
+ this.graphProjector = options.graphProjector;
1636
1875
  this.now = options.now ?? (() => /* @__PURE__ */ new Date());
1637
1876
  this.idFactory = options.idFactory ?? defaultIdFactory;
1638
1877
  this.elementIdFactory = options.elementIdFactory ?? defaultElementIdFactory;
1878
+ this.graphIdFactory = options.graphIdFactory ?? defaultGraphIdFactory;
1639
1879
  }
1640
1880
  static inMemory(options = {}) {
1641
1881
  return new _StrataGate(options, STRATAGATE_CONSTRUCTOR_TOKEN);
@@ -1656,9 +1896,12 @@ var StrataGate = class _StrataGate {
1656
1896
  ...options.summarizer ? { summarizer: options.summarizer } : {},
1657
1897
  ...options.extractor ? { extractor: options.extractor } : {},
1658
1898
  ...options.elementProjector ? { elementProjector: options.elementProjector } : {},
1899
+ ...options.disableElementProjection !== void 0 ? { disableElementProjection: options.disableElementProjection } : {},
1900
+ ...options.graphProjector ? { graphProjector: options.graphProjector } : {},
1659
1901
  ...options.now ? { now: options.now } : {},
1660
1902
  ...options.idFactory ? { idFactory: options.idFactory } : {},
1661
- ...options.elementIdFactory ? { elementIdFactory: options.elementIdFactory } : {}
1903
+ ...options.elementIdFactory ? { elementIdFactory: options.elementIdFactory } : {},
1904
+ ...options.graphIdFactory ? { graphIdFactory: options.graphIdFactory } : {}
1662
1905
  });
1663
1906
  } catch (error) {
1664
1907
  await storage.close();
@@ -1700,9 +1943,12 @@ var StrataGate = class _StrataGate {
1700
1943
  if (options.summarizer) memoryOptions.summarizer = options.summarizer;
1701
1944
  if (options.extractor) memoryOptions.extractor = options.extractor;
1702
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;
1703
1948
  if (options.now) memoryOptions.now = options.now;
1704
1949
  if (options.idFactory) memoryOptions.idFactory = options.idFactory;
1705
1950
  if (options.elementIdFactory) memoryOptions.elementIdFactory = options.elementIdFactory;
1951
+ if (options.graphIdFactory) memoryOptions.graphIdFactory = options.graphIdFactory;
1706
1952
  const memory = new _StrataGate(memoryOptions, STRATAGATE_CONSTRUCTOR_TOKEN);
1707
1953
  memory.storage = options.storage;
1708
1954
  memory.namespace = namespace;
@@ -1737,6 +1983,21 @@ var StrataGate = class _StrataGate {
1737
1983
  }
1738
1984
  });
1739
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());
1740
2001
  } else {
1741
2002
  await memory.persist();
1742
2003
  }
@@ -1769,6 +2030,12 @@ var StrataGate = class _StrataGate {
1769
2030
  listElements() {
1770
2031
  return this.elements;
1771
2032
  }
2033
+ listGraphNodes() {
2034
+ return this.graphNodes;
2035
+ }
2036
+ listGraphEdges() {
2037
+ return this.graphEdges;
2038
+ }
1772
2039
  listOpenTail(threadId) {
1773
2040
  if (threadId === void 0) return this.openTail;
1774
2041
  return this.openTail.filter((message) => message.threadId === threadId);
@@ -1779,6 +2046,9 @@ var StrataGate = class _StrataGate {
1779
2046
  listElementProjectionJobs() {
1780
2047
  return [...this.elementProjectionJobs.values()];
1781
2048
  }
2049
+ listGraphProjectionJobs() {
2050
+ return [...this.graphProjectionJobs.values()];
2051
+ }
1782
2052
  listUsageReceipts() {
1783
2053
  return [...this.usageReceipts.values()];
1784
2054
  }
@@ -1806,6 +2076,9 @@ var StrataGate = class _StrataGate {
1806
2076
  openTail: this.openTail,
1807
2077
  blocks: this.blocks,
1808
2078
  events: this.events,
2079
+ graphNodes: this.graphNodes,
2080
+ graphEdges: this.graphEdges,
2081
+ graphProjectionJobs: [...this.graphProjectionJobs.values()],
1809
2082
  elements: this.elements,
1810
2083
  extractionJobs: [...this.extractionJobs.values()],
1811
2084
  elementProjectionJobs: [...this.elementProjectionJobs.values()],
@@ -1856,11 +2129,13 @@ var StrataGate = class _StrataGate {
1856
2129
  }
1857
2130
  if (this.threadOpenTail(threadId).filter((message) => message.role === "user").length < this.blockTurnSize) {
1858
2131
  const projectedElements2 = await this.projectEligibleElements() ?? [];
2132
+ await this.projectEligibleGraph();
1859
2133
  return { sealedBlock: null, extractedEvents: [], projectedElements: projectedElements2 };
1860
2134
  }
1861
2135
  const sealedBlock = await this.sealOpenTail(threadId);
1862
2136
  const extractedEvents = await this.extractEligibleBlock() ?? [];
1863
2137
  const projectedElements = await this.projectEligibleElements() ?? [];
2138
+ await this.projectEligibleGraph();
1864
2139
  return { sealedBlock, extractedEvents, projectedElements };
1865
2140
  }
1866
2141
  async resumePendingWork(options = {}) {
@@ -1873,12 +2148,14 @@ var StrataGate = class _StrataGate {
1873
2148
  sealedBlocks.push(await this.sealOpenTail(sealable.threadId));
1874
2149
  extractedEvents.push(...await this.extractEligibleBlock() ?? []);
1875
2150
  projectedElements.push(...await this.projectEligibleElements() ?? []);
2151
+ await this.projectEligibleGraph();
1876
2152
  }
1877
2153
  while (true) {
1878
2154
  const extracted = await this.extractEligibleBlock();
1879
2155
  if (extracted === null) break;
1880
2156
  extractedEvents.push(...extracted);
1881
2157
  projectedElements.push(...await this.projectEligibleElements() ?? []);
2158
+ await this.projectEligibleGraph();
1882
2159
  }
1883
2160
  if (options.retrySkipped === true) {
1884
2161
  const skippedBlockIds = this.blocks.filter((block) => this.nextBlockInThread(block) !== null && block.shouldExtract && this.extractionJobs.get(block.id)?.status === "skipped").map((block) => block.id);
@@ -1887,6 +2164,7 @@ var StrataGate = class _StrataGate {
1887
2164
  if (extracted === null) continue;
1888
2165
  extractedEvents.push(...extracted);
1889
2166
  projectedElements.push(...await this.projectEligibleElements() ?? []);
2167
+ await this.projectEligibleGraph();
1890
2168
  }
1891
2169
  }
1892
2170
  while (true) {
@@ -1894,12 +2172,14 @@ var StrataGate = class _StrataGate {
1894
2172
  if (projected === null) break;
1895
2173
  projectedElements.push(...projected);
1896
2174
  }
2175
+ await this.projectEligibleGraph();
1897
2176
  return { sealedBlocks, extractedEvents, projectedElements };
1898
2177
  }
1899
2178
  async addEvent(input) {
1900
2179
  return this.commitMutation(() => {
1901
2180
  const event = this.addEventInMemory(input);
1902
2181
  this.queueElementProjection([event.id]);
2182
+ this.queueGraphProjection([event.id], 1e3);
1903
2183
  return event;
1904
2184
  });
1905
2185
  }
@@ -1929,10 +2209,10 @@ var StrataGate = class _StrataGate {
1929
2209
  [event.temporal.originalText ?? "", 4],
1930
2210
  [`${event.temporal.happenedStart ?? ""} ${event.temporal.happenedEnd ?? ""}`, 4]
1931
2211
  ])).map(({ item }) => item);
1932
- 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;
1933
2213
  const structured = (items) => [...items].sort((left, right) => {
1934
- if (options.temporalIntent === "first") return chronology(left).localeCompare(chronology(right));
1935
- 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));
1936
2216
  return memoryWeightAt(right, this.currentTurn) - memoryWeightAt(left, this.currentTurn) || right.updatedAt.localeCompare(left.updatedAt);
1937
2217
  });
1938
2218
  const participantIds = new Set(participantMatches.map(({ id }) => id));
@@ -2018,6 +2298,66 @@ var StrataGate = class _StrataGate {
2018
2298
  job.updatedAt = toUtc8Iso(this.now());
2019
2299
  });
2020
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
+ }
2021
2361
  async searchElements(query, options = {}) {
2022
2362
  const normalizedName = normalizeSearchText(options.name ?? "");
2023
2363
  const candidates = this.elements.flatMap((element) => element.facts.map((fact) => ({
@@ -2239,7 +2579,10 @@ var StrataGate = class _StrataGate {
2239
2579
  quotes: [...new Set(input.quotes ?? [])].slice(0, 12),
2240
2580
  sourceMessageIds,
2241
2581
  sourceBlockId: sourceBlock.id,
2242
- temporal: input.temporal ? { ...input.temporal } : { mentionedAt: now },
2582
+ temporal: {
2583
+ ...input.temporal ? { ...input.temporal } : { mentionedAt: now },
2584
+ eventType: normalizeStandardEventType(input.temporal?.eventType)
2585
+ },
2243
2586
  scope: input.scope ?? "user",
2244
2587
  criticality,
2245
2588
  confidence: Math.max(0, Math.min(1, input.confidence ?? 1)),
@@ -2278,7 +2621,59 @@ var StrataGate = class _StrataGate {
2278
2621
  if (!job) throw new Error(`Unknown element projection: ${id}`);
2279
2622
  return job;
2280
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
+ }
2281
2675
  queueElementProjection(sourceEventIds) {
2676
+ if (this.disableElementProjection) return null;
2282
2677
  const ids = [...new Set(sourceEventIds.filter((id) => this.events.some((event) => event.id === id)))];
2283
2678
  if (ids.length === 0) return null;
2284
2679
  const now = toUtc8Iso(this.now());
@@ -2447,7 +2842,11 @@ var StrataGate = class _StrataGate {
2447
2842
  }
2448
2843
  return this.commitMutation(() => {
2449
2844
  const extracted = result.shouldExtract ? result.events.map((event) => this.addEventInMemory({ ...event, sourceBlockId: target.id })) : [];
2450
- 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
+ }
2451
2850
  const job = this.extractionJobs.get(target.id);
2452
2851
  if (!job) throw new Error(`Missing extraction job for block: ${target.id}`);
2453
2852
  this.extractionJobs.set(target.id, {
@@ -2471,6 +2870,17 @@ var StrataGate = class _StrataGate {
2471
2870
  throw error;
2472
2871
  }
2473
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
+ }
2474
2884
  async commitMutation(mutation) {
2475
2885
  const previous = this.mutationQueue;
2476
2886
  let release;
@@ -2514,11 +2924,15 @@ var StrataGate = class _StrataGate {
2514
2924
  this.openTail.splice(0, this.openTail.length, ...copy.openTail);
2515
2925
  this.blocks.splice(0, this.blocks.length, ...copy.blocks);
2516
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);
2517
2929
  this.elements.splice(0, this.elements.length, ...copy.elements);
2518
2930
  this.extractionJobs.clear();
2519
2931
  for (const job of copy.extractionJobs) this.extractionJobs.set(job.blockId, job);
2520
2932
  this.elementProjectionJobs.clear();
2521
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);
2522
2936
  this.usageReceipts.clear();
2523
2937
  for (const receipt of copy.usageReceipts) this.usageReceipts.set(receipt.id, receipt);
2524
2938
  this.ingestionReceipts.clear();
@@ -2582,6 +2996,42 @@ var StrataGate = class _StrataGate {
2582
2996
  if (!elementIds.has(elementId)) throw new Error(`Element projection ${job.id} references unknown element ${elementId}`);
2583
2997
  }
2584
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
+ }
2585
3035
  }
2586
3036
  };
2587
3037
 
@@ -2718,10 +3168,10 @@ var CRITICALITIES = /* @__PURE__ */ new Set(["routine", "preference", "identity"
2718
3168
  function object(value) {
2719
3169
  return value && typeof value === "object" && !Array.isArray(value) ? value : {};
2720
3170
  }
2721
- function strings(value) {
3171
+ function strings2(value) {
2722
3172
  return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
2723
3173
  }
2724
- function text(value, fallback = "") {
3174
+ function text2(value, fallback = "") {
2725
3175
  return typeof value === "string" ? value.trim() : fallback;
2726
3176
  }
2727
3177
  function l2Neighbor(block) {
@@ -2751,7 +3201,8 @@ var RETRY_MAX_TOKENS = 1e4;
2751
3201
  var STRUCTURED_FIELDS = {
2752
3202
  summarizer: ["l0Title", "l0Tags", "l1Summary", "l2Keypoints", "shouldExtract"],
2753
3203
  extractor: ["shouldExtract", "reason", "events"],
2754
- projector: ["reason", "changes"]
3204
+ projector: ["reason", "changes"],
3205
+ graphProjector: ["reason", "nodes", "edges"]
2755
3206
  };
2756
3207
  var STRING_ARRAY = { type: "array", items: { type: "string" } };
2757
3208
  var OPEN_OBJECT = { type: "object", additionalProperties: true };
@@ -2817,6 +3268,47 @@ var PROJECTOR_PARAMETERS = {
2817
3268
  reason: { type: "string", required: true },
2818
3269
  changes: { type: "array", items: ELEMENT_CHANGE, required: true }
2819
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
+ };
2820
3312
  var STRUCTURED_TOOLS = {
2821
3313
  summarizer: {
2822
3314
  name: "stratagate_summarize_block",
@@ -2832,6 +3324,11 @@ var STRUCTURED_TOOLS = {
2832
3324
  name: "stratagate_project_element_cards",
2833
3325
  description: "Submit element-card changes supported by the supplied event cards.",
2834
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
2835
3332
  }
2836
3333
  };
2837
3334
  function toolSchema(kind) {
@@ -2869,10 +3366,10 @@ var DshModelBridge = class {
2869
3366
  { messages }
2870
3367
  ));
2871
3368
  return {
2872
- l0Title: text(raw.l0Title, "Conversation block").slice(0, 120),
2873
- l0Tags: strings(raw.l0Tags).slice(0, 12),
2874
- l1Summary: text(raw.l1Summary).slice(0, 2e3),
2875
- 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),
2876
3373
  shouldExtract: raw.shouldExtract === true
2877
3374
  };
2878
3375
  };
@@ -2880,21 +3377,21 @@ var DshModelBridge = class {
2880
3377
  const validMessageIds = new Set(context.target.l5Raw.map((message) => message.id));
2881
3378
  const raw = object(await this.callStructured(
2882
3379
  "extractor",
2883
- `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.`,
2884
3381
  extractorPayload(context)
2885
3382
  ));
2886
3383
  const events = (Array.isArray(raw.events) ? raw.events : []).map((candidate) => {
2887
3384
  const item = object(candidate);
2888
- const sourceMessageIds = strings(item.sourceMessageIds).filter((id) => validMessageIds.has(id));
3385
+ const sourceMessageIds = strings2(item.sourceMessageIds).filter((id) => validMessageIds.has(id));
2889
3386
  const scope = SCOPES.has(item.scope) ? item.scope : "project";
2890
3387
  const criticality = CRITICALITIES.has(item.criticality) ? item.criticality : "routine";
2891
- 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;
2892
3389
  return {
2893
- title: text(item.title).slice(0, 200),
2894
- summary: text(item.summary).slice(0, 1e3),
2895
- narrative: text(item.narrative),
2896
- tags: strings(item.tags).slice(0, 16),
2897
- 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),
2898
3395
  sourceMessageIds,
2899
3396
  sourceBlockId: context.target.id,
2900
3397
  temporal: object(item.temporal),
@@ -2905,7 +3402,7 @@ var DshModelBridge = class {
2905
3402
  }).filter((event) => event !== null);
2906
3403
  return {
2907
3404
  shouldExtract: raw.shouldExtract === true,
2908
- 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."),
2909
3406
  events
2910
3407
  };
2911
3408
  };
@@ -2920,27 +3417,82 @@ var DshModelBridge = class {
2920
3417
  const item = object(candidate);
2921
3418
  const element = object(item.element);
2922
3419
  const type = element.type;
2923
- const sourceEventIds = strings(item.sourceEventIds).filter((id) => eventIds.has(id));
3420
+ const sourceEventIds = strings2(item.sourceEventIds).filter((id) => eventIds.has(id));
2924
3421
  const operation = item.operation;
2925
3422
  const mode = item.mode;
2926
3423
  const value = item.value;
2927
- 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 [];
2928
3425
  if (!["set_state", "add_set_item", "set_relation"].includes(String(operation))) return [];
2929
3426
  if (!["state", "set", "relation"].includes(String(mode))) return [];
2930
3427
  if (!(typeof value === "string" || Array.isArray(value) && value.every((entry) => typeof entry === "string"))) return [];
2931
3428
  return [{
2932
- element: { name: text(element.name), type, aliases: strings(element.aliases) },
3429
+ element: { name: text2(element.name), type, aliases: strings2(element.aliases) },
2933
3430
  operation,
2934
- key: text(item.key, "state"),
3431
+ key: text2(item.key, "state"),
2935
3432
  mode,
2936
3433
  value,
2937
- ...text(item.validFrom) ? { validFrom: text(item.validFrom) } : {},
2938
- ...text(item.validTo) ? { validTo: text(item.validTo) } : {},
3434
+ ...text2(item.validFrom) ? { validFrom: text2(item.validFrom) } : {},
3435
+ ...text2(item.validTo) ? { validTo: text2(item.validTo) } : {},
2939
3436
  sourceEventIds,
2940
3437
  ...typeof item.confidence === "number" ? { confidence: item.confidence } : {}
2941
3438
  }];
2942
3439
  });
2943
- 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 };
2944
3496
  };
2945
3497
  async callStructured(kind, system, payload) {
2946
3498
  const session = this.sessions.getStore();
@@ -3063,6 +3615,7 @@ ${JSON_RETRY_INSTRUCTION}`,
3063
3615
  import { createHash } from "node:crypto";
3064
3616
  import { existsSync } from "node:fs";
3065
3617
  import { resolve } from "node:path";
3618
+ import { createUserMessage as createUserMessage2 } from "@deepseek-ai/dsh-llm";
3066
3619
 
3067
3620
  // src/fold.ts
3068
3621
  function renderBlocks(blocks) {
@@ -3116,13 +3669,13 @@ var TurnFolder = class {
3116
3669
  if (event.data.source.kind !== "user") return null;
3117
3670
  const turn = this.activeTurn.get(sessionId);
3118
3671
  if (turn === void 0) return null;
3119
- const text2 = renderBlocks(event.data.content);
3120
- 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);
3121
3674
  return null;
3122
3675
  }
3123
3676
  case "assistant/message": {
3124
- const text2 = renderBlocks(event.data.message.content);
3125
- 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);
3126
3679
  return null;
3127
3680
  }
3128
3681
  case "tool/call": {
@@ -3158,7 +3711,8 @@ var TurnFolder = class {
3158
3711
  threadId: sessionId,
3159
3712
  assistantToolCalls: [...pending.tools.values()],
3160
3713
  createdAt: toUtc8Iso(event.time),
3161
- receiptId: `dsh:${sessionId}:turn:${event.data.turn}`
3714
+ receiptId: `dsh:${sessionId}:turn:${event.data.turn}`,
3715
+ dshTurn: event.data.turn
3162
3716
  };
3163
3717
  }
3164
3718
  default:
@@ -3238,6 +3792,7 @@ var DshMetadataStore = class {
3238
3792
  var AUTO_EVENT_LIMIT = 4;
3239
3793
  var AUTO_ELEMENT_LIMIT = 4;
3240
3794
  var AUTO_MEMORY_TOKEN_BUDGET = 900;
3795
+ var COMPACTION_SOURCE_PLUGIN = "stratagate-memory";
3241
3796
  function projectKey(cwd) {
3242
3797
  const canonical = resolve(cwd ?? process.cwd()).replaceAll("\\", "/").toLowerCase();
3243
3798
  return createHash("sha256").update(canonical).digest("hex").slice(0, 20);
@@ -3248,21 +3803,25 @@ function workspaceDisplayName(cwd) {
3248
3803
  }
3249
3804
  var StrataGateRuntime = class {
3250
3805
  constructor(config, models, onIngestError = () => {
3806
+ }, flushNativeSession = async () => {
3251
3807
  }) {
3252
3808
  this.config = config;
3253
3809
  this.models = models;
3254
3810
  this.onIngestError = onIngestError;
3811
+ this.flushNativeSession = flushNativeSession;
3255
3812
  this.blockDecayLambda = config.blockDecayLambda;
3256
3813
  }
3257
3814
  config;
3258
3815
  models;
3259
3816
  onIngestError;
3817
+ flushNativeSession;
3260
3818
  folder = new TurnFolder();
3261
3819
  spaces = /* @__PURE__ */ new Map();
3262
3820
  batches = /* @__PURE__ */ new Map();
3263
3821
  adopted = /* @__PURE__ */ new Map();
3264
3822
  pendingUse = /* @__PURE__ */ new Set();
3265
3823
  workspaceNames = /* @__PURE__ */ new Map();
3824
+ migrationTimers = /* @__PURE__ */ new Map();
3266
3825
  ingestTail = Promise.resolve();
3267
3826
  settingsTail = Promise.resolve();
3268
3827
  batchSequence = 0;
@@ -3278,7 +3837,15 @@ var StrataGateRuntime = class {
3278
3837
  }).then(async () => {
3279
3838
  const memory = await this.space(session);
3280
3839
  try {
3281
- 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
+ }
3282
3849
  } finally {
3283
3850
  await this.persistSuccessfulResponses(memory);
3284
3851
  }
@@ -3436,23 +4003,78 @@ var StrataGateRuntime = class {
3436
4003
  await this.flush();
3437
4004
  const memory = await this.space(session);
3438
4005
  const threadId = String(session.id);
4006
+ const blockContexts = memory.getBlockContext(threadId);
4007
+ if (this.syncDecayedBlockSurface(session, blockContexts)) {
4008
+ await this.flushNativeSession(session);
4009
+ }
3439
4010
  const openTail = memory.listOpenTail(threadId);
3440
4011
  const activationQuery = [currentUserMessage(session), renderMessages(recentTurns(openTail, 2))].filter(Boolean).join("\n\n");
3441
- const [eventHits, elementHits] = activationQuery ? await Promise.all([
3442
- memory.searchEvents(activationQuery, { limit: 20 }),
3443
- memory.searchElements(activationQuery, { limit: 12 })
3444
- ]) : [[], []];
3445
- const events = activatedEvents(memory, eventHits).slice(0, AUTO_EVENT_LIMIT);
3446
- const elements = activatedElements(memory, elementHits).slice(0, AUTO_ELEMENT_LIMIT);
3447
- return [
3448
- "[Current conversation]",
3449
- openTail.length > 0 ? renderMessages(openTail) : "(open tail is empty)",
3450
- "",
3451
- "[Decayed memory blocks]",
3452
- renderBlocks2(memory.getBlockContext(threadId)),
3453
- "",
3454
- renderActivatedMemory(events, elements)
3455
- ].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;
3456
4078
  }
3457
4079
  // Keep the ingestion error for callers that explicitly require a flushed run.
3458
4080
  async settleIngestion() {
@@ -3464,6 +4086,8 @@ var StrataGateRuntime = class {
3464
4086
  async close() {
3465
4087
  if (this.closed) return;
3466
4088
  this.closed = true;
4089
+ for (const timer of this.migrationTimers.values()) clearTimeout(timer);
4090
+ this.migrationTimers.clear();
3467
4091
  let flushError;
3468
4092
  try {
3469
4093
  await this.flush();
@@ -3564,7 +4188,8 @@ var StrataGateRuntime = class {
3564
4188
  blockDecayLambda: this.blockDecayLambda,
3565
4189
  summarizer: this.models.summarizer,
3566
4190
  extractor: this.models.extractor,
3567
- elementProjector: this.models.projector
4191
+ graphProjector: this.models.graphProjector,
4192
+ disableElementProjection: true
3568
4193
  });
3569
4194
  try {
3570
4195
  return await memory.expandBlock(id, target, "user");
@@ -3620,14 +4245,28 @@ var StrataGateRuntime = class {
3620
4245
  blockDecayLambda: this.blockDecayLambda,
3621
4246
  summarizer: this.models.summarizer,
3622
4247
  extractor: this.models.extractor,
3623
- elementProjector: this.models.projector
4248
+ graphProjector: this.models.graphProjector,
4249
+ disableElementProjection: true
3624
4250
  }).then(async (memory) => {
3625
4251
  try {
3626
4252
  try {
3627
- 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
+ }
3628
4266
  } finally {
3629
4267
  await this.persistSuccessfulResponses(memory);
3630
4268
  }
4269
+ this.scheduleGraphMigration(session, memory);
3631
4270
  return memory;
3632
4271
  } catch (error) {
3633
4272
  await memory.close().catch(() => {
@@ -3642,6 +4281,46 @@ var StrataGateRuntime = class {
3642
4281
  }
3643
4282
  return opening;
3644
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
+ }
3645
4324
  rememberWorkspace(namespace, cwd) {
3646
4325
  const name2 = workspaceDisplayName(cwd);
3647
4326
  this.workspaceNames.set(namespace, name2);
@@ -3710,12 +4389,75 @@ function renderMessages(messages) {
3710
4389
  return details.join("\n");
3711
4390
  }).join("\n\n");
3712
4391
  }
3713
- function renderBlocks2(blocks) {
3714
- if (blocks.length === 0) return "(no sealed blocks)";
3715
- return blocks.map((block) => [
3716
- `block ${block.id} | turns ${block.turnRange[0]}-${block.turnRange[1]} | age ${block.age} | L${block.level}`,
3717
- block.content
3718
- ].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;
3719
4461
  }
3720
4462
  function activatedEvents(memory, relevance) {
3721
4463
  const allowed = new Map(relevance.map(({ event }) => [event.id, event]));
@@ -3759,7 +4501,7 @@ function activatedElements(memory, relevance) {
3759
4501
  weight
3760
4502
  ]).map(({ item }) => item);
3761
4503
  }
3762
- function renderActivatedMemory(events, elements) {
4504
+ function renderActivatedMemory(events, graphNodes) {
3763
4505
  const heading = [
3764
4506
  "[Activated long-term memory]",
3765
4507
  "Historical memory context.",
@@ -3769,7 +4511,7 @@ function renderActivatedMemory(events, elements) {
3769
4511
  const lines = [...heading];
3770
4512
  let tokens = estimateTokens(lines.join("\n"));
3771
4513
  let eventCount = 0;
3772
- let elementCount = 0;
4514
+ let nodeCount = 0;
3773
4515
  for (const event of events) {
3774
4516
  const rendered = JSON.stringify({
3775
4517
  id: event.id,
@@ -3788,25 +4530,24 @@ Events:
3788
4530
  tokens += cost;
3789
4531
  eventCount += 1;
3790
4532
  }
3791
- for (const element of elements) {
4533
+ for (const node of graphNodes) {
3792
4534
  const rendered = JSON.stringify({
3793
- elementId: element.elementId,
3794
- name: element.name,
3795
- key: element.fact.key,
3796
- value: element.fact.value,
3797
- validFrom: element.fact.validFrom,
3798
- 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 }))
3799
4540
  });
3800
4541
  const cost = estimateTokens(`
3801
- ElementFacts:
4542
+ KnowledgeGraph:
3802
4543
  - ${rendered}`);
3803
4544
  if (tokens + cost > AUTO_MEMORY_TOKEN_BUDGET) break;
3804
- if (elementCount === 0) lines.push("ElementFacts:");
4545
+ if (nodeCount === 0) lines.push("KnowledgeGraph:");
3805
4546
  lines.push(`- ${rendered}`);
3806
4547
  tokens += cost;
3807
- elementCount += 1;
4548
+ nodeCount += 1;
3808
4549
  }
3809
- if (eventCount === 0 && elementCount === 0) lines.push("(no activated memory)");
4550
+ if (eventCount === 0 && nodeCount === 0) lines.push("(no activated memory)");
3810
4551
  return lines.join("\n");
3811
4552
  }
3812
4553
  function estimateTokens(value) {
@@ -3866,9 +4607,26 @@ function registerMemoryTools(ctx, runtime) {
3866
4607
  ...args.participants ? { participants: args.participants } : {}
3867
4608
  })
3868
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
+ }));
3869
4627
  ctx.tools.register(defineTool({
3870
4628
  name: "memory_search_elements",
3871
- 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.",
3872
4630
  parameters: {
3873
4631
  query: { type: "string", required: true },
3874
4632
  limit: { type: "integer" },
@@ -3938,7 +4696,7 @@ function registerMemoryTools(ctx, runtime) {
3938
4696
  missing: { type: "string", required: true },
3939
4697
  next_strategy: {
3940
4698
  type: "string",
3941
- 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"],
3942
4700
  required: true
3943
4701
  }
3944
4702
  },
@@ -3962,7 +4720,7 @@ function registerMemoryTools(ctx, runtime) {
3962
4720
 
3963
4721
  // src/web.ts
3964
4722
  import { createRequire } from "node:module";
3965
- var STRATAGATE_DSH_VERSION = "0.2.21";
4723
+ var STRATAGATE_DSH_VERSION = "0.2.22";
3966
4724
  var LEGACY_THREAD_ID = "__legacy__";
3967
4725
  var nodeRequire = createRequire(import.meta.url);
3968
4726
  function installedPackageVersion(names) {
@@ -3985,8 +4743,8 @@ function numeric(value, fallback, minimum, maximum) {
3985
4743
  const parsed = Number(value);
3986
4744
  return Number.isFinite(parsed) ? Math.min(maximum, Math.max(minimum, Math.floor(parsed))) : fallback;
3987
4745
  }
3988
- function redact(text2) {
3989
- 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]");
3990
4748
  }
3991
4749
  function redactValue(value) {
3992
4750
  if (typeof value === "string") return redact(value);
@@ -4033,6 +4791,7 @@ function eventSummary(event) {
4033
4791
  id: event.id,
4034
4792
  title: event.title,
4035
4793
  summary: event.summary,
4794
+ narrative: event.narrative,
4036
4795
  tags: event.tags,
4037
4796
  sourceBlockId: event.sourceBlockId,
4038
4797
  sourceMessageIds: event.sourceMessageIds,
@@ -4084,8 +4843,8 @@ async function overview(runtime) {
4084
4843
  for (const namespace of namespaces) {
4085
4844
  const snapshot = await runtime.adminSnapshot(namespace);
4086
4845
  if (!snapshot) continue;
4087
- const failedJobs = snapshot.extractionJobs.filter(({ status }) => status === "failed").length + snapshot.elementProjectionJobs.filter(({ status }) => status === "failed").length;
4088
- 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;
4089
4848
  const failedJobDetails = [
4090
4849
  ...snapshot.extractionJobs.filter(({ status }) => status === "failed").map((job) => ({
4091
4850
  id: job.blockId,
@@ -4095,9 +4854,9 @@ async function overview(runtime) {
4095
4854
  lastErrorFull: job.lastError,
4096
4855
  updatedAt: job.updatedAt
4097
4856
  })),
4098
- ...snapshot.elementProjectionJobs.filter(({ status }) => status === "failed").map((job) => ({
4857
+ ...snapshot.graphProjectionJobs.filter(({ status }) => status === "failed").map((job) => ({
4099
4858
  id: job.id,
4100
- kind: "element-projection",
4859
+ kind: "graph-projection",
4101
4860
  attempts: job.attempts,
4102
4861
  lastError: job.lastError?.slice(0, 500) ?? null,
4103
4862
  lastErrorFull: job.lastError,
@@ -4108,6 +4867,7 @@ async function overview(runtime) {
4108
4867
  ...snapshot.blocks.map(({ createdAt }) => createdAt),
4109
4868
  ...snapshot.events.map(({ updatedAt }) => updatedAt),
4110
4869
  ...snapshot.elements.map(({ updatedAt }) => updatedAt),
4870
+ ...snapshot.graphNodes.map(({ updatedAt }) => updatedAt),
4111
4871
  ...snapshot.usageReceipts.map(({ createdAt }) => createdAt)
4112
4872
  ].sort();
4113
4873
  rows.push({
@@ -4122,6 +4882,15 @@ async function overview(runtime) {
4122
4882
  events: snapshot.events.length,
4123
4883
  activeEvents: snapshot.events.filter(({ status }) => status === "active").length,
4124
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
+ })(),
4125
4894
  usageReceipts: snapshot.usageReceipts.length,
4126
4895
  memoryUseCount: snapshot.usageReceipts.filter((receipt) => receipt.eventIds.length > 0 || receipt.elementIds.length > 0).length,
4127
4896
  failedJobs,
@@ -4276,11 +5045,38 @@ async function memories(runtime, url) {
4276
5045
  const offset = numeric(url.searchParams.get("offset"), 0, 0, Number.MAX_SAFE_INTEGER);
4277
5046
  const limit = numeric(url.searchParams.get("limit"), 100, 1, 200);
4278
5047
  let values;
4279
- 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) => ({
4280
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 })),
4281
5054
  relatedElements: snapshot.elements.filter(({ sourceEventIds }) => sourceEventIds.includes(event.id)).map(({ id, name: name2 }) => ({ id, name: name2 }))
4282
5055
  }));
4283
- else if (kind === "elements") values = snapshot.elements.map(elementSummary);
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);
4284
5080
  else if (kind === "blocks") {
4285
5081
  const recovered = recoverSnapshotView(snapshot);
4286
5082
  const conversations = conversationRows(snapshot, recovered);
@@ -4293,8 +5089,8 @@ async function memories(runtime, url) {
4293
5089
  const blockMessageIds = new Set(block.messages.map(({ id }) => id));
4294
5090
  const relatedEvents = snapshot.events.filter((event) => event.sourceBlockId === source.id && (!block.virtual || event.sourceMessageIds.some((id) => blockMessageIds.has(id))));
4295
5091
  const eventIds = new Set(relatedEvents.map(({ id }) => id));
4296
- const projections = snapshot.elementProjectionJobs.filter(({ sourceEventIds }) => sourceEventIds.some((id) => eventIds.has(id)));
4297
- const relatedElements = snapshot.elements.filter(({ sourceEventIds }) => sourceEventIds.some((id) => eventIds.has(id))).map(({ id, name: name2 }) => ({ id, name: name2 }));
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 }));
4298
5094
  const failedProjection = projections.find(({ status: status2 }) => status2 === "failed");
4299
5095
  const pendingProjection = projections.some(({ status: status2 }) => status2 === "pending" || status2 === "running");
4300
5096
  const needsExtraction = source.shouldExtract === true;
@@ -4331,13 +5127,13 @@ async function memories(runtime, url) {
4331
5127
  updatedAt: extraction.updatedAt,
4332
5128
  lastError: extraction.lastError
4333
5129
  } : null,
4334
- elementProjection: projections.length ? {
5130
+ graphProjection: projections.length ? {
4335
5131
  status: failedProjection ? "failed" : pendingProjection ? "processing" : "completed",
4336
5132
  jobs: projections.length,
4337
5133
  lastError: failedProjection?.lastError ?? null
4338
5134
  } : null,
4339
5135
  relatedEvents: relatedEvents.map(eventSummary),
4340
- relatedElements
5136
+ relatedNodes
4341
5137
  };
4342
5138
  });
4343
5139
  const filtered2 = values.filter((value) => matchesQuery(value, query));
@@ -4368,6 +5164,7 @@ async function sources(runtime, url) {
4368
5164
  if (!namespace) throw new AdminHttpError(400, "namespace is required");
4369
5165
  const snapshot = await requiredSnapshot(runtime, namespace);
4370
5166
  const eventId = url.searchParams.get("eventId");
5167
+ const nodeId = url.searchParams.get("nodeId");
4371
5168
  const elementId = url.searchParams.get("elementId");
4372
5169
  const blockId = url.searchParams.get("blockId");
4373
5170
  let events = [];
@@ -4378,6 +5175,18 @@ async function sources(runtime, url) {
4378
5175
  if (!event) throw new AdminHttpError(404, `Unknown event: ${eventId}`);
4379
5176
  events = [event];
4380
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
+ };
4381
5190
  } else if (elementId) {
4382
5191
  const element = snapshot.elements.find(({ id }) => id === elementId);
4383
5192
  if (!element) throw new AdminHttpError(404, `Unknown element: ${elementId}`);
@@ -4402,12 +5211,13 @@ async function sources(runtime, url) {
4402
5211
  virtual: displayBlock?.virtual ?? false
4403
5212
  };
4404
5213
  } else {
4405
- throw new AdminHttpError(400, "eventId, elementId, or blockId is required");
5214
+ throw new AdminHttpError(400, "eventId, nodeId, elementId, or blockId is required");
4406
5215
  }
4407
5216
  return {
4408
5217
  namespace,
4409
5218
  events: events.map(eventSummary),
4410
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 })),
4411
5221
  messages: sourceMessages(snapshot, ids)
4412
5222
  };
4413
5223
  }
@@ -4488,7 +5298,7 @@ var MEMORY_PROTOCOL = `[StrataGate memory protocol]
4488
5298
  StrataGate provides durable, evidence-gated memory through memory_* tools.
4489
5299
 
4490
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.
4491
- - 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.
4492
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.
4493
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.
4494
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.
@@ -4502,6 +5312,8 @@ async function apply(ctx, config) {
4502
5312
  const models = new DshModelBridge(ctx, resolved);
4503
5313
  const runtime = new StrataGateRuntime(resolved, models, (error) => {
4504
5314
  ctx.logger.error(`stratagate-memory ingestion failed: ${renderError(error)}`);
5315
+ }, async (session) => {
5316
+ await ctx.sessions.flush(session);
4505
5317
  });
4506
5318
  await runtime.syncConfiguredSettings();
4507
5319
  ctx.systemPrompt.section({ name: "tool:stratagate-memory", order: 113, text: MEMORY_PROTOCOL });
@@ -4510,10 +5322,10 @@ async function apply(ctx, config) {
4510
5322
  const session = context.agent?.session;
4511
5323
  if (!session) return assembled;
4512
5324
  try {
4513
- const text2 = await runtime.buildAutoContext(session);
5325
+ const text3 = await runtime.buildAutoContext(session);
4514
5326
  return {
4515
5327
  ...assembled,
4516
- contexts: [...assembled.contexts, { name: "stratagate:auto-memory", text: text2 }]
5328
+ contexts: [...assembled.contexts, { name: "stratagate:auto-memory", text: text3 }]
4517
5329
  };
4518
5330
  } catch (error) {
4519
5331
  ctx.logger.warn(`stratagate-memory auto-context failed: ${renderError(error)}`);
@@ -4522,7 +5334,7 @@ async function apply(ctx, config) {
4522
5334
  });
4523
5335
  ctx.on("agent/turn-stopping", ({ agent }) => {
4524
5336
  if (!runtime.needsRecordUse(agent.session)) return;
4525
- agent.steer(createUserMessage2({
5337
+ agent.steer(createUserMessage3({
4526
5338
  content: [{
4527
5339
  type: "text",
4528
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."