memorix 1.0.8 → 1.0.9

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/sdk.js CHANGED
@@ -674,6 +674,13 @@ var init_persistence = __esm({
674
674
  });
675
675
 
676
676
  // src/store/graph-store.ts
677
+ var graph_store_exports = {};
678
+ __export(graph_store_exports, {
679
+ GraphSqliteStore: () => GraphSqliteStore,
680
+ getGraphStore: () => getGraphStore,
681
+ initGraphStore: () => initGraphStore,
682
+ resetGraphStore: () => resetGraphStore
683
+ });
677
684
  import path6 from "path";
678
685
  import fs5 from "fs";
679
686
  function safeJsonParse(val, fallback) {
@@ -709,6 +716,16 @@ function getGraphStore() {
709
716
  if (!_graphStore) throw new Error("[memorix] GraphStore not initialized \u2014 call initGraphStore() first");
710
717
  return _graphStore;
711
718
  }
719
+ function resetGraphStore() {
720
+ if (_graphStore) {
721
+ try {
722
+ _graphStore.close();
723
+ } catch {
724
+ }
725
+ }
726
+ _graphStore = null;
727
+ _graphDataDir = null;
728
+ }
712
729
  var GraphSqliteStore, _graphStore, _graphDataDir;
713
730
  var init_graph_store = __esm({
714
731
  "src/store/graph-store.ts"() {
@@ -1040,7 +1057,8 @@ var init_types = __esm({
1040
1057
  "why-it-exists": "[WHY]",
1041
1058
  "decision": "[DECISION]",
1042
1059
  "trade-off": "[TRADEOFF]",
1043
- "reasoning": "[REASONING]"
1060
+ "reasoning": "[REASONING]",
1061
+ "probe": "[PROBE]"
1044
1062
  };
1045
1063
  TOPIC_KEY_FAMILIES = {
1046
1064
  "architecture": ["architecture", "design", "adr", "structure", "pattern"],
@@ -1059,6 +1077,15 @@ var init_types = __esm({
1059
1077
  });
1060
1078
 
1061
1079
  // src/store/mini-skill-store.ts
1080
+ var mini_skill_store_exports = {};
1081
+ __export(mini_skill_store_exports, {
1082
+ MiniSkillGracefulDegrade: () => MiniSkillGracefulDegrade,
1083
+ MiniSkillSqliteStore: () => MiniSkillSqliteStore,
1084
+ getMiniSkillStore: () => getMiniSkillStore,
1085
+ initMiniSkillStore: () => initMiniSkillStore,
1086
+ isMiniSkillStoreInitialized: () => isMiniSkillStoreInitialized,
1087
+ resetMiniSkillStore: () => resetMiniSkillStore
1088
+ });
1062
1089
  import path7 from "path";
1063
1090
  import fs6 from "fs";
1064
1091
  function skillToRow(skill) {
@@ -1112,6 +1139,10 @@ function getMiniSkillStore() {
1112
1139
  }
1113
1140
  return _store;
1114
1141
  }
1142
+ function resetMiniSkillStore() {
1143
+ _store = null;
1144
+ _storeDataDir = null;
1145
+ }
1115
1146
  async function initMiniSkillStore(dataDir) {
1116
1147
  if (_store && _storeDataDir === dataDir) return _store;
1117
1148
  _store = null;
@@ -1358,6 +1389,12 @@ async function promoteToMiniSkill(projectDir2, projectId, observations2, options
1358
1389
  `Cannot promote: ${nonActive.length} observation(s) are not active. Blocked: ${nonActive.map((o) => `#${o.id} (${o.status ?? "unknown"})`).join(", ")}. Only active observations can be promoted to permanent knowledge.`
1359
1390
  );
1360
1391
  }
1392
+ const probes = observations2.filter((o) => o.type === "probe");
1393
+ if (probes.length > 0) {
1394
+ throw new Error(
1395
+ `Cannot promote probe observations \u2014 they are operational heartbeats, not durable knowledge. Blocked: ${probes.map((o) => `#${o.id} "${o.title.substring(0, 60)}"`).join(", ")}.`
1396
+ );
1397
+ }
1361
1398
  if (!options?.force) {
1362
1399
  const commandLogs = observations2.filter((o) => COMMAND_LOG_TITLE.test(o.title));
1363
1400
  if (commandLogs.length > 0) {
@@ -4411,6 +4448,10 @@ async function searchObservations(options) {
4411
4448
  if (statusFilter === "all") return true;
4412
4449
  const doc = hit.document;
4413
4450
  return (doc.status || "active") === statusFilter;
4451
+ }).filter((hit) => {
4452
+ if (options.type === "probe") return true;
4453
+ const doc = hit.document;
4454
+ return doc.type !== "probe";
4414
4455
  }).map((hit) => {
4415
4456
  const doc = hit.document;
4416
4457
  const obsType = doc.type;
@@ -6792,6 +6833,11 @@ function hasContradiction(oldText, newText) {
6792
6833
  ];
6793
6834
  return negationPatterns.some((p) => p.test(newText));
6794
6835
  }
6836
+ function normalizedSearchSimilarity(score) {
6837
+ if (!Number.isFinite(score) || score <= 0) return 0;
6838
+ if (score <= 1) return score;
6839
+ return 0;
6840
+ }
6795
6841
  function mergeNarratives(oldNarrative, newNarrative) {
6796
6842
  if (newNarrative.length > oldNarrative.length * 1.5) return newNarrative;
6797
6843
  if (oldNarrative.length > newNarrative.length * 1.5) return oldNarrative;
@@ -6882,12 +6928,13 @@ function scoreCandidate(extracted, candidate) {
6882
6928
  `${extracted.title} ${extracted.narrative}`,
6883
6929
  `${candidate.title} ${candidate.narrative}`
6884
6930
  );
6885
- const score = candidate.score * 0.6 + (entityMatch ? 0.2 : 0) + contentOverlap * 0.2;
6931
+ const searchSimilarity = normalizedSearchSimilarity(candidate.score);
6932
+ const score = searchSimilarity * 0.6 + (entityMatch ? 0.2 : 0) + contentOverlap * 0.2;
6886
6933
  const newLength = extracted.narrative.length + extracted.facts.join(" ").length;
6887
6934
  const oldLength = candidate.narrative.length + candidate.facts.length;
6888
6935
  const richer = newLength > oldLength * 1.15;
6889
6936
  const contradiction = hasContradiction(candidate.narrative, extracted.narrative);
6890
- return { score, entityMatch, richer, contradiction };
6937
+ return { score, searchSimilarity, entityMatch, richer, contradiction };
6891
6938
  }
6892
6939
  async function runResolve(extracted, projectId, searchMemories, getObservation2, useLLM = false) {
6893
6940
  const query = `${extracted.title} ${extracted.narrative.substring(0, 200)}`;
@@ -6910,7 +6957,7 @@ async function runResolve(extracted, projectId, searchMemories, getObservation2,
6910
6957
  }));
6911
6958
  scored.sort((a, b) => b.score - a.score);
6912
6959
  const best = scored[0];
6913
- if (best.hit.score >= SIMILARITY_DUPLICATE) {
6960
+ if (best.searchSimilarity >= SIMILARITY_DUPLICATE) {
6914
6961
  if (best.richer) {
6915
6962
  const existing = getObservation2(best.hit.observationId);
6916
6963
  const oldFacts = existing?.facts ?? best.hit.facts.split("\n").filter(Boolean);
@@ -7097,7 +7144,8 @@ var init_evaluate = __esm({
7097
7144
  "how-it-works": 0.6,
7098
7145
  "discovery": 0.55,
7099
7146
  "what-changed": 0.45,
7100
- "session-request": 0.4
7147
+ "session-request": 0.4,
7148
+ "probe": 0.1
7101
7149
  };
7102
7150
  SPECIFICITY_PATTERNS = [
7103
7151
  /\b\d+\.\d+\.\d+\b/,
@@ -7561,6 +7609,9 @@ function scoreObservationForSessionContext(obs, projectTokens, now = Date.now())
7561
7609
  if (obs.valueCategory === "core") {
7562
7610
  score += 2;
7563
7611
  }
7612
+ if (obs.type === "probe") {
7613
+ score -= 100;
7614
+ }
7564
7615
  return score;
7565
7616
  }
7566
7617
  async function startSession(projectDir2, projectId, opts) {
@@ -7769,7 +7820,8 @@ var init_session = __esm({
7769
7820
  "how-it-works": "[INFO]",
7770
7821
  "what-changed": "[CHANGE]",
7771
7822
  "why-it-exists": "[DECISION]",
7772
- "session-request": "[SESSION]"
7823
+ "session-request": "[SESSION]",
7824
+ "probe": "[PROBE]"
7773
7825
  };
7774
7826
  TYPE_WEIGHTS2 = {
7775
7827
  "gotcha": 6,
@@ -7848,11 +7900,17 @@ function getValueCategoryMultiplier(doc) {
7848
7900
  return 1;
7849
7901
  }
7850
7902
  function getEffectiveRetentionDays(doc) {
7903
+ const typeOverride = TYPE_RETENTION_OVERRIDE[doc.type];
7904
+ if (typeOverride !== void 0) {
7905
+ const raw2 = typeOverride * getSourceRetentionMultiplier(doc);
7906
+ return Math.max(MIN_RETENTION_DAYS, raw2);
7907
+ }
7851
7908
  const importance = getImportanceLevel(doc);
7852
7909
  const raw = RETENTION_DAYS[importance] * getSourceRetentionMultiplier(doc) * getValueCategoryMultiplier(doc);
7853
7910
  return Math.max(MIN_RETENTION_DAYS, raw);
7854
7911
  }
7855
7912
  function isImmune(doc) {
7913
+ if (doc.type === "probe") return false;
7856
7914
  if (doc.valueCategory === "core") return true;
7857
7915
  const importance = getImportanceLevel(doc);
7858
7916
  if (importance === "critical") return true;
@@ -7861,6 +7919,7 @@ function isImmune(doc) {
7861
7919
  return concepts.some((c) => PROTECTED_TAGS.has(c));
7862
7920
  }
7863
7921
  function getImmunityReason(doc) {
7922
+ if (doc.type === "probe") return null;
7864
7923
  if (doc.valueCategory === "core") return "core valueCategory (formation-classified)";
7865
7924
  const importance = getImportanceLevel(doc);
7866
7925
  if (importance === "critical") return "critical importance";
@@ -8017,7 +8076,7 @@ async function archiveExpired(projectDir2, referenceTime, accessMap) {
8017
8076
  return { archived: archivedCount, remaining: activeObs.length - archivedCount };
8018
8077
  });
8019
8078
  }
8020
- var RETENTION_DAYS, BASE_IMPORTANCE, TYPE_IMPORTANCE, PROTECTED_TAGS, MIN_ACCESS_FOR_IMMUNITY, MIN_RETENTION_DAYS;
8079
+ var RETENTION_DAYS, BASE_IMPORTANCE, TYPE_IMPORTANCE, TYPE_RETENTION_OVERRIDE, PROTECTED_TAGS, MIN_ACCESS_FOR_IMMUNITY, MIN_RETENTION_DAYS;
8021
8080
  var init_retention = __esm({
8022
8081
  "src/memory/retention.ts"() {
8023
8082
  "use strict";
@@ -8045,7 +8104,12 @@ var init_retention = __esm({
8045
8104
  "what-changed": "low",
8046
8105
  "why-it-exists": "medium",
8047
8106
  discovery: "low",
8048
- "session-request": "low"
8107
+ "session-request": "low",
8108
+ probe: "low"
8109
+ };
8110
+ TYPE_RETENTION_OVERRIDE = {
8111
+ probe: 7
8112
+ // Operational heartbeats: expire after ~7 days regardless of source/valueCategory
8049
8113
  };
8050
8114
  PROTECTED_TAGS = /* @__PURE__ */ new Set(["keep", "important", "pinned", "critical"]);
8051
8115
  MIN_ACCESS_FOR_IMMUNITY = 3;
@@ -9773,6 +9837,449 @@ var init_project_classification = __esm({
9773
9837
  }
9774
9838
  });
9775
9839
 
9840
+ // src/wiki/generator.ts
9841
+ var generator_exports = {};
9842
+ __export(generator_exports, {
9843
+ contextualHasSubstance: () => contextualHasSubstance,
9844
+ generateKnowledgeBase: () => generateKnowledgeBase,
9845
+ isCommandLog: () => isCommandLog,
9846
+ isEligible: () => isEligible,
9847
+ isExcludedType: () => isExcludedType
9848
+ });
9849
+ function isExcludedType(o) {
9850
+ return o.type === "probe";
9851
+ }
9852
+ function isCommandLog(o) {
9853
+ return COMMAND_LOG_TITLE3.test(o.title || "");
9854
+ }
9855
+ function isInactive(o) {
9856
+ const status = o.status ?? "active";
9857
+ return status !== "active";
9858
+ }
9859
+ function isOtherProject(o, projectId) {
9860
+ return o.projectId !== projectId;
9861
+ }
9862
+ function contextualHasSubstance(o) {
9863
+ if (o.valueCategory !== "contextual") return true;
9864
+ const hasFacts = (o.facts?.length ?? 0) > 0;
9865
+ const hasConcepts = (o.concepts?.length ?? 0) > 0;
9866
+ const hasFiles = (o.filesModified?.length ?? 0) > 0;
9867
+ const hasEntity = !!(o.entityName && o.entityName !== "quick-note" && o.entityName !== "unknown");
9868
+ return hasFacts || hasConcepts || hasFiles || hasEntity;
9869
+ }
9870
+ function isEligible(o, projectId) {
9871
+ if (isExcludedType(o)) return false;
9872
+ if (isCommandLog(o)) return false;
9873
+ if (isInactive(o)) return false;
9874
+ if (isOtherProject(o, projectId)) return false;
9875
+ if (o.valueCategory === "ephemeral") return false;
9876
+ if (!contextualHasSubstance(o)) return false;
9877
+ return true;
9878
+ }
9879
+ function obsToItem(o) {
9880
+ const ref = {
9881
+ kind: o.source === "git" ? "git" : "observation",
9882
+ id: `obs:${o.id}`,
9883
+ title: o.title
9884
+ };
9885
+ return {
9886
+ title: o.title,
9887
+ summary: o.narrative?.slice(0, 200) || "",
9888
+ type: o.type,
9889
+ entityName: o.entityName || void 0,
9890
+ refs: [ref]
9891
+ };
9892
+ }
9893
+ function skillToItem(s) {
9894
+ const ref = {
9895
+ kind: "mini-skill",
9896
+ id: `skill:${s.id}`,
9897
+ title: s.title
9898
+ };
9899
+ const obsRefs = s.sourceObservationIds.map(
9900
+ (oid) => ({ kind: "observation", id: `obs:${oid}` })
9901
+ );
9902
+ return {
9903
+ title: s.title,
9904
+ summary: s.instruction?.slice(0, 200) || "",
9905
+ type: "mini-skill",
9906
+ entityName: s.sourceEntity || void 0,
9907
+ refs: [ref, ...obsRefs]
9908
+ };
9909
+ }
9910
+ function buildGitSection(observations2) {
9911
+ const gitObs = observations2.filter(
9912
+ (o) => o.source === "git" && o.sourceDetail === "git-ingest"
9913
+ );
9914
+ const items = gitObs.map(obsToItem);
9915
+ return {
9916
+ id: "git-backed-facts",
9917
+ title: "Git-backed Facts",
9918
+ items,
9919
+ empty: items.length === 0
9920
+ };
9921
+ }
9922
+ function buildSkillsSection(skills) {
9923
+ const items = skills.map(skillToItem);
9924
+ return {
9925
+ id: "promoted-skills",
9926
+ title: "Promoted Skills",
9927
+ items,
9928
+ empty: items.length === 0
9929
+ };
9930
+ }
9931
+ function buildProjectOverview(projectId, eligibleObs, skills) {
9932
+ const refs = [
9933
+ ...eligibleObs.slice(0, 5).map((o) => ({
9934
+ kind: o.source === "git" ? "git" : "observation",
9935
+ id: `obs:${o.id}`,
9936
+ title: o.title
9937
+ })),
9938
+ ...skills.slice(0, 5).map((s) => ({
9939
+ kind: "mini-skill",
9940
+ id: `skill:${s.id}`,
9941
+ title: s.title
9942
+ }))
9943
+ ];
9944
+ if (refs.length === 0) {
9945
+ return {
9946
+ id: "project-overview",
9947
+ title: "Project Overview",
9948
+ items: [],
9949
+ empty: true
9950
+ };
9951
+ }
9952
+ const lines = [];
9953
+ lines.push(`Project: ${projectId}`);
9954
+ lines.push(`Observations in KB: ${eligibleObs.length}`);
9955
+ lines.push(`Promoted skills: ${skills.length}`);
9956
+ const entityCounts = /* @__PURE__ */ new Map();
9957
+ for (const o of eligibleObs) {
9958
+ if (o.entityName) {
9959
+ entityCounts.set(o.entityName, (entityCounts.get(o.entityName) || 0) + 1);
9960
+ }
9961
+ }
9962
+ const topEntities = [...entityCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5);
9963
+ if (topEntities.length > 0) {
9964
+ lines.push(`Top entities: ${topEntities.map(([name, count2]) => `${name} (${count2})`).join(", ")}`);
9965
+ }
9966
+ return {
9967
+ id: "project-overview",
9968
+ title: "Project Overview",
9969
+ items: [{
9970
+ title: projectId,
9971
+ summary: lines.join("\n"),
9972
+ type: "overview",
9973
+ refs
9974
+ }]
9975
+ };
9976
+ }
9977
+ function generateKnowledgeBase(options) {
9978
+ const { projectId, observations: observations2, miniSkills } = options;
9979
+ const eligible = observations2.filter((o) => isEligible(o, projectId));
9980
+ const scopedMiniSkills = miniSkills.filter((s) => s.projectId === projectId);
9981
+ const typedSections = SECTION_DEFS.map((def) => {
9982
+ const matched = eligible.filter(def.typeMatch);
9983
+ const items = matched.map(obsToItem);
9984
+ return {
9985
+ id: def.id,
9986
+ title: def.title,
9987
+ items,
9988
+ empty: items.length === 0
9989
+ };
9990
+ });
9991
+ const projectOverview = buildProjectOverview(projectId, eligible, scopedMiniSkills);
9992
+ const gitSection = buildGitSection(eligible);
9993
+ const skillsSection = buildSkillsSection(scopedMiniSkills);
9994
+ const sections = [
9995
+ projectOverview,
9996
+ ...typedSections,
9997
+ gitSection,
9998
+ skillsSection
9999
+ ];
10000
+ const allRefs = sections.flatMap((s) => s.items.flatMap((i) => i.refs));
10001
+ const obsRefCount = allRefs.filter((r) => r.kind === "observation" || r.kind === "git").length;
10002
+ const skillRefCount = allRefs.filter((r) => r.kind === "mini-skill").length;
10003
+ return {
10004
+ title: "Knowledge Base",
10005
+ subtitle: "LLM Wiki",
10006
+ projectId,
10007
+ generatedAt: options.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
10008
+ sections,
10009
+ stats: {
10010
+ observationsUsed: eligible.length,
10011
+ miniSkillsUsed: scopedMiniSkills.length,
10012
+ refs: obsRefCount + skillRefCount
10013
+ }
10014
+ };
10015
+ }
10016
+ var COMMAND_LOG_TITLE3, SECTION_DEFS;
10017
+ var init_generator = __esm({
10018
+ "src/wiki/generator.ts"() {
10019
+ "use strict";
10020
+ init_esm_shims();
10021
+ COMMAND_LOG_TITLE3 = /^(Ran:|Command:|Executed:)\s/i;
10022
+ SECTION_DEFS = [
10023
+ {
10024
+ id: "core-decisions",
10025
+ title: "Core Decisions",
10026
+ typeMatch: (o) => o.type === "decision" || o.type === "trade-off" || o.type === "reasoning"
10027
+ },
10028
+ {
10029
+ id: "operational-knowledge",
10030
+ title: "Operational Knowledge",
10031
+ typeMatch: (o) => o.type === "how-it-works" || o.type === "what-changed" || o.type === "why-it-exists" || o.type === "discovery" || o.type === "session-request"
10032
+ },
10033
+ {
10034
+ id: "known-gotchas",
10035
+ title: "Known Gotchas",
10036
+ typeMatch: (o) => o.type === "gotcha" || o.type === "problem-solution"
10037
+ }
10038
+ ];
10039
+ }
10040
+ });
10041
+
10042
+ // src/wiki/knowledge-graph.ts
10043
+ var knowledge_graph_exports = {};
10044
+ __export(knowledge_graph_exports, {
10045
+ generateKnowledgeGraph: () => generateKnowledgeGraph
10046
+ });
10047
+ function inferEdges(nodes, entityNameIndex) {
10048
+ const edges = [];
10049
+ const seen = /* @__PURE__ */ new Set();
10050
+ const maxRelatesEdgesPerEntitySection = 8;
10051
+ function addEdge(source, target, edgeType) {
10052
+ const key = `${source}:${edgeType}:${target}`;
10053
+ if (seen.has(key)) return;
10054
+ if (source === target) return;
10055
+ seen.add(key);
10056
+ edges.push({
10057
+ id: `e_${edges.length}_${source}_${target}`,
10058
+ source,
10059
+ target,
10060
+ edgeType
10061
+ });
10062
+ }
10063
+ function rankNodesForEntity(a, b) {
10064
+ const evidenceDiff = (b.evidenceCount || 0) - (a.evidenceCount || 0);
10065
+ if (evidenceDiff !== 0) return evidenceDiff;
10066
+ return a.id.localeCompare(b.id);
10067
+ }
10068
+ for (const [, group] of entityNameIndex) {
10069
+ if (group.length < 2) continue;
10070
+ const bySection = /* @__PURE__ */ new Map();
10071
+ for (const node of group) {
10072
+ const sectionGroup = bySection.get(node.sectionId) || [];
10073
+ sectionGroup.push(node);
10074
+ bySection.set(node.sectionId, sectionGroup);
10075
+ }
10076
+ for (const sectionGroup of bySection.values()) {
10077
+ const ranked = [...sectionGroup].sort(rankNodesForEntity);
10078
+ if (ranked.length === 2) {
10079
+ addEdge(ranked[0].id, ranked[1].id, "relates_to");
10080
+ addEdge(ranked[1].id, ranked[0].id, "relates_to");
10081
+ continue;
10082
+ }
10083
+ const anchor = ranked[0];
10084
+ for (const node of ranked.slice(1, maxRelatesEdgesPerEntitySection + 1)) {
10085
+ addEdge(anchor.id, node.id, "relates_to");
10086
+ }
10087
+ }
10088
+ const sectionAnchors = [...bySection.entries()].map(([sectionId, sectionGroup]) => ({
10089
+ sectionId,
10090
+ anchor: [...sectionGroup].sort(rankNodesForEntity)[0]
10091
+ })).sort((a, b) => sectionPriority(a.sectionId) - sectionPriority(b.sectionId));
10092
+ for (let i = 0; i < sectionAnchors.length; i++) {
10093
+ for (let j = i + 1; j < sectionAnchors.length; j++) {
10094
+ const from = sectionAnchors[i].anchor;
10095
+ const to = sectionAnchors[j].anchor;
10096
+ const edgeType = sectionPriority(sectionAnchors[i].sectionId) === sectionPriority(sectionAnchors[j].sectionId) ? "relates_to" : "supports";
10097
+ addEdge(from.id, to.id, edgeType);
10098
+ }
10099
+ }
10100
+ }
10101
+ return edges;
10102
+ }
10103
+ function sectionPriority(sectionId) {
10104
+ const order = ["core-decisions", "known-gotchas", "operational-knowledge", "git-backed-facts", "promoted-skills"];
10105
+ const idx = order.indexOf(sectionId);
10106
+ return idx >= 0 ? idx : order.length;
10107
+ }
10108
+ function sectionIdForObs(o) {
10109
+ if (GIT_SECTION.typeMatch(o)) return GIT_SECTION.id;
10110
+ for (const def of SECTION_DEFS2) {
10111
+ if (def.typeMatch(o)) return def.id;
10112
+ }
10113
+ return "operational-knowledge";
10114
+ }
10115
+ function mapRelationType(relationType) {
10116
+ const lower = relationType.toLowerCase();
10117
+ if (lower.includes("support") || lower.includes("depend")) return "supports";
10118
+ if (lower.includes("relat") || lower.includes("connect") || lower.includes("associat")) return "relates_to";
10119
+ if (lower.includes("mention") || lower.includes("refer")) return "mentions";
10120
+ if (lower.includes("deriv") || lower.includes("origin") || lower.includes("source")) return "derived_from";
10121
+ return "relates_to";
10122
+ }
10123
+ function generateKnowledgeGraph(options) {
10124
+ const { projectId, observations: observations2, miniSkills, graphEntities, graphRelations } = options;
10125
+ const eligible = observations2.filter((o) => isEligible(o, projectId));
10126
+ const scopedMiniSkills = miniSkills.filter((s) => s.projectId === projectId);
10127
+ const nodes = [];
10128
+ const entityNameIndex = /* @__PURE__ */ new Map();
10129
+ for (const o of eligible) {
10130
+ const sectionId = sectionIdForObs(o);
10131
+ const ref = {
10132
+ kind: o.source === "git" ? "git" : "observation",
10133
+ id: `obs:${o.id}`,
10134
+ title: o.title
10135
+ };
10136
+ const node = {
10137
+ id: `obs:${o.id}`,
10138
+ label: o.title,
10139
+ nodeType: o.type,
10140
+ sectionId,
10141
+ entityName: o.entityName || void 0,
10142
+ evidenceCount: (o.facts?.length ?? 0) + (o.concepts?.length ?? 0) + (o.filesModified?.length ?? 0),
10143
+ summary: o.narrative?.slice(0, 200) || "",
10144
+ refs: [ref]
10145
+ };
10146
+ nodes.push(node);
10147
+ if (o.entityName) {
10148
+ const group = entityNameIndex.get(o.entityName) || [];
10149
+ group.push(node);
10150
+ entityNameIndex.set(o.entityName, group);
10151
+ }
10152
+ }
10153
+ for (const s of scopedMiniSkills) {
10154
+ const ref = {
10155
+ kind: "mini-skill",
10156
+ id: `skill:${s.id}`,
10157
+ title: s.title
10158
+ };
10159
+ const obsRefs = s.sourceObservationIds.map(
10160
+ (oid) => ({ kind: "observation", id: `obs:${oid}` })
10161
+ );
10162
+ const node = {
10163
+ id: `skill:${s.id}`,
10164
+ label: s.title,
10165
+ nodeType: "mini-skill",
10166
+ sectionId: "promoted-skills",
10167
+ entityName: s.sourceEntity || void 0,
10168
+ evidenceCount: obsRefs.length,
10169
+ summary: s.instruction?.slice(0, 200) || "",
10170
+ refs: [ref, ...obsRefs]
10171
+ };
10172
+ nodes.push(node);
10173
+ if (s.sourceEntity) {
10174
+ const group = entityNameIndex.get(s.sourceEntity) || [];
10175
+ group.push(node);
10176
+ entityNameIndex.set(s.sourceEntity, group);
10177
+ }
10178
+ }
10179
+ const edges = inferEdges(nodes, entityNameIndex);
10180
+ for (const s of scopedMiniSkills) {
10181
+ const skillNodeId = `skill:${s.id}`;
10182
+ for (const oid of s.sourceObservationIds) {
10183
+ const obsNodeId = `obs:${oid}`;
10184
+ if (nodes.some((n) => n.id === obsNodeId)) {
10185
+ edges.push({
10186
+ id: `e_${edges.length}_${skillNodeId}_${obsNodeId}`,
10187
+ source: skillNodeId,
10188
+ target: obsNodeId,
10189
+ edgeType: "derived_from"
10190
+ });
10191
+ }
10192
+ }
10193
+ }
10194
+ if (graphRelations && graphRelations.length > 0) {
10195
+ const entityNodeMap = /* @__PURE__ */ new Map();
10196
+ for (const n of nodes) {
10197
+ if (n.entityName && !entityNodeMap.has(n.entityName)) {
10198
+ entityNodeMap.set(n.entityName, n.id);
10199
+ }
10200
+ }
10201
+ const seen = new Set(edges.map((e) => `${e.source}:${e.edgeType}:${e.target}`));
10202
+ for (const rel of graphRelations) {
10203
+ const srcId = entityNodeMap.get(rel.from);
10204
+ const tgtId = entityNodeMap.get(rel.to);
10205
+ if (!srcId || !tgtId) continue;
10206
+ if (srcId === tgtId) continue;
10207
+ const edgeType = mapRelationType(rel.relationType);
10208
+ const key = `${srcId}:${edgeType}:${tgtId}`;
10209
+ if (seen.has(key)) continue;
10210
+ seen.add(key);
10211
+ edges.push({
10212
+ id: `e_gr_${edges.length}_${srcId}_${tgtId}`,
10213
+ source: srcId,
10214
+ target: tgtId,
10215
+ edgeType
10216
+ });
10217
+ }
10218
+ }
10219
+ const sectionCounts = {};
10220
+ for (const n of nodes) {
10221
+ sectionCounts[n.sectionId] = (sectionCounts[n.sectionId] || 0) + 1;
10222
+ }
10223
+ const clusters = ALL_SECTIONS.filter((def) => (sectionCounts[def.id] ?? 0) > 0).map((def) => ({
10224
+ id: `cluster:${def.id}`,
10225
+ label: def.label,
10226
+ sectionId: def.id,
10227
+ nodeCount: sectionCounts[def.id] ?? 0
10228
+ }));
10229
+ const stats = {
10230
+ totalNodes: nodes.length,
10231
+ totalEdges: edges.length,
10232
+ clusterCount: clusters.length,
10233
+ sectionCounts
10234
+ };
10235
+ return {
10236
+ title: "Knowledge Graph",
10237
+ projectId,
10238
+ generatedAt: options.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
10239
+ nodes,
10240
+ edges,
10241
+ clusters,
10242
+ stats
10243
+ };
10244
+ }
10245
+ var SECTION_DEFS2, GIT_SECTION, SKILLS_SECTION, ALL_SECTIONS;
10246
+ var init_knowledge_graph = __esm({
10247
+ "src/wiki/knowledge-graph.ts"() {
10248
+ "use strict";
10249
+ init_esm_shims();
10250
+ init_generator();
10251
+ SECTION_DEFS2 = [
10252
+ {
10253
+ id: "core-decisions",
10254
+ label: "Core Decisions",
10255
+ typeMatch: (o) => o.type === "decision" || o.type === "trade-off" || o.type === "reasoning"
10256
+ },
10257
+ {
10258
+ id: "operational-knowledge",
10259
+ label: "Operational Knowledge",
10260
+ typeMatch: (o) => o.type === "how-it-works" || o.type === "what-changed" || o.type === "why-it-exists" || o.type === "discovery" || o.type === "session-request"
10261
+ },
10262
+ {
10263
+ id: "known-gotchas",
10264
+ label: "Known Gotchas",
10265
+ typeMatch: (o) => o.type === "gotcha" || o.type === "problem-solution"
10266
+ }
10267
+ ];
10268
+ GIT_SECTION = {
10269
+ id: "git-backed-facts",
10270
+ label: "Git-backed Facts",
10271
+ typeMatch: (o) => o.source === "git" && o.sourceDetail === "git-ingest"
10272
+ };
10273
+ SKILLS_SECTION = {
10274
+ id: "promoted-skills",
10275
+ label: "Promoted Skills",
10276
+ typeMatch: () => false
10277
+ // skills handled separately
10278
+ };
10279
+ ALL_SECTIONS = [...SECTION_DEFS2, GIT_SECTION, SKILLS_SECTION];
10280
+ }
10281
+ });
10282
+
9776
10283
  // src/dashboard/server.ts
9777
10284
  var server_exports = {};
9778
10285
  __export(server_exports, {
@@ -9914,6 +10421,7 @@ async function handleApi(req, res, dataDir, projectId, projectName, baseDir, pro
9914
10421
  const typeCounts = {};
9915
10422
  for (const obs of observations2) {
9916
10423
  const t = obs.type || "unknown";
10424
+ if (t === "probe") continue;
9917
10425
  typeCounts[t] = (typeCounts[t] || 0) + 1;
9918
10426
  }
9919
10427
  const sourceCounts = { git: 0, agent: 0, manual: 0 };
@@ -9955,7 +10463,7 @@ async function handleApi(req, res, dataDir, projectId, projectName, baseDir, pro
9955
10463
  else if (score >= 1) retentionSummary.stale++;
9956
10464
  else retentionSummary.archive++;
9957
10465
  }
9958
- const sorted = [...observations2].sort((a, b) => (b.id || 0) - (a.id || 0)).slice(0, 10);
10466
+ const sorted = [...observations2].filter((o) => o.type !== "probe").sort((a, b) => (b.id || 0) - (a.id || 0)).slice(0, 10);
9959
10467
  let embeddingStatus = { enabled: false, provider: "", dimensions: 0 };
9960
10468
  try {
9961
10469
  const { getEmbeddingProvider: getEmbeddingProvider2 } = await Promise.resolve().then(() => (init_provider(), provider_exports));
@@ -9995,7 +10503,7 @@ async function handleApi(req, res, dataDir, projectId, projectName, baseDir, pro
9995
10503
  const allObs = await getObservationStore().loadAll();
9996
10504
  const observations2 = filterActiveByProject(allObs, effectiveProjectId);
9997
10505
  const now = Date.now();
9998
- const scored = observations2.map((obs) => {
10506
+ const scored = observations2.filter((obs) => obs.type !== "probe").map((obs) => {
9999
10507
  const age = now - new Date(obs.createdAt || now).getTime();
10000
10508
  const ageHours = age / (1e3 * 60 * 60);
10001
10509
  const importance = obs.importance ?? 5;
@@ -10027,6 +10535,50 @@ async function handleApi(req, res, dataDir, projectId, projectName, baseDir, pro
10027
10535
  });
10028
10536
  break;
10029
10537
  }
10538
+ case "/knowledge": {
10539
+ const { generateKnowledgeBase: generateKnowledgeBase2 } = await Promise.resolve().then(() => (init_generator(), generator_exports));
10540
+ const { initObservations: initObservations2, getAllObservations: getAllObservations2 } = await Promise.resolve().then(() => (init_observations(), observations_exports));
10541
+ const { initMiniSkillStore: initMiniSkillStore2, getMiniSkillStore: getMiniSkillStore2 } = await Promise.resolve().then(() => (init_mini_skill_store(), mini_skill_store_exports));
10542
+ await initObservations2(effectiveDataDir);
10543
+ await initMiniSkillStore2(effectiveDataDir);
10544
+ const allObs = getAllObservations2();
10545
+ const skills = await getMiniSkillStore2().loadByProject(effectiveProjectId);
10546
+ const overview = generateKnowledgeBase2({
10547
+ projectId: effectiveProjectId,
10548
+ observations: allObs,
10549
+ miniSkills: skills
10550
+ });
10551
+ sendJson(res, overview);
10552
+ break;
10553
+ }
10554
+ case "/knowledge-graph": {
10555
+ const { generateKnowledgeGraph: generateKnowledgeGraph2 } = await Promise.resolve().then(() => (init_knowledge_graph(), knowledge_graph_exports));
10556
+ const { initObservations: initObservations2, getAllObservations: getAllObservations2 } = await Promise.resolve().then(() => (init_observations(), observations_exports));
10557
+ const { initMiniSkillStore: initMiniSkillStore2, getMiniSkillStore: getMiniSkillStore2 } = await Promise.resolve().then(() => (init_mini_skill_store(), mini_skill_store_exports));
10558
+ const { initGraphStore: initGraphStore2, getGraphStore: getGraphStore2 } = await Promise.resolve().then(() => (init_graph_store(), graph_store_exports));
10559
+ await initObservations2(effectiveDataDir);
10560
+ await initMiniSkillStore2(effectiveDataDir);
10561
+ await initGraphStore2(effectiveDataDir);
10562
+ const allObs = getAllObservations2();
10563
+ const skills = await getMiniSkillStore2().loadByProject(effectiveProjectId);
10564
+ const fullGraph = { entities: getGraphStore2().loadEntities(), relations: getGraphStore2().loadRelations() };
10565
+ const graphObs = await getObservationStore().loadAll();
10566
+ const projectEntityNames = new Set(
10567
+ graphObs.filter((o) => o.projectId === effectiveProjectId && (o.status ?? "active") === "active" && o.entityName).map((o) => o.entityName)
10568
+ );
10569
+ const scopedEntities = fullGraph.entities.filter((e) => projectEntityNames.has(e.name));
10570
+ const scopedEntityNameSet = new Set(scopedEntities.map((e) => e.name));
10571
+ const scopedRelations = fullGraph.relations.filter((r) => scopedEntityNameSet.has(r.from) && scopedEntityNameSet.has(r.to));
10572
+ const graph = generateKnowledgeGraph2({
10573
+ projectId: effectiveProjectId,
10574
+ observations: allObs,
10575
+ miniSkills: skills,
10576
+ graphEntities: scopedEntities,
10577
+ graphRelations: scopedRelations
10578
+ });
10579
+ sendJson(res, graph);
10580
+ break;
10581
+ }
10030
10582
  case "/config": {
10031
10583
  const os4 = await import("os");
10032
10584
  const { existsSync: existsSync10 } = await import("fs");
@@ -10263,13 +10815,20 @@ async function serveStatic(req, res, staticDir) {
10263
10815
  const ext = path14.extname(filePath);
10264
10816
  res.writeHead(200, {
10265
10817
  "Content-Type": MIME_TYPES[ext] || "application/octet-stream",
10266
- "Cache-Control": "no-cache"
10818
+ "Cache-Control": "no-store, no-cache, must-revalidate, max-age=0",
10819
+ "Pragma": "no-cache",
10820
+ "Expires": "0"
10267
10821
  });
10268
10822
  res.end(data);
10269
10823
  } catch {
10270
10824
  try {
10271
10825
  const indexData = await fs12.readFile(path14.join(staticDir, "index.html"));
10272
- res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
10826
+ res.writeHead(200, {
10827
+ "Content-Type": "text/html; charset=utf-8",
10828
+ "Cache-Control": "no-store, no-cache, must-revalidate, max-age=0",
10829
+ "Pragma": "no-cache",
10830
+ "Expires": "0"
10831
+ });
10273
10832
  res.end(indexData);
10274
10833
  } catch {
10275
10834
  sendError(res, "Not found", 404);
@@ -11740,6 +12299,8 @@ import { spawnSync } from 'node:child_process';
11740
12299
  export const MemorixPlugin = async ({ project, client, $, directory, worktree }) => {
11741
12300
  // Generate a stable session ID for this plugin lifetime
11742
12301
  const sessionId = \`opencode-\${Date.now().toString(36)}-\${Math.random().toString(36).slice(2, 8)}\`;
12302
+ let pendingAssistantResponse = null;
12303
+ let lastDeliveredAssistantKey = '';
11743
12304
 
11744
12305
  /**
11745
12306
  * Send event JSON to \`memorix hook\` via child_process.spawnSync.
@@ -11772,6 +12333,33 @@ export const MemorixPlugin = async ({ project, client, $, directory, worktree })
11772
12333
  }
11773
12334
  }
11774
12335
 
12336
+ function extractMessageInfo(input) {
12337
+ if (input && typeof input === 'object') {
12338
+ if (input.info && typeof input.info === 'object') return input.info;
12339
+ if (input.message && typeof input.message === 'object') return input.message;
12340
+ if (input.properties && typeof input.properties === 'object') {
12341
+ if (input.properties.info && typeof input.properties.info === 'object') return input.properties.info;
12342
+ if (input.properties.message && typeof input.properties.message === 'object') return input.properties.message;
12343
+ }
12344
+ }
12345
+ return input;
12346
+ }
12347
+
12348
+ function extractMessageText(message) {
12349
+ if (!message || typeof message !== 'object') return '';
12350
+ if (typeof message.content === 'string') return message.content.trim();
12351
+ const parts = Array.isArray(message.parts) ? message.parts : [];
12352
+ return parts
12353
+ .map((part) => {
12354
+ if (!part || typeof part !== 'object') return '';
12355
+ if (typeof part.text === 'string') return part.text;
12356
+ if (typeof part.content === 'string') return part.content;
12357
+ return '';
12358
+ })
12359
+ .join('')
12360
+ .trim();
12361
+ }
12362
+
11775
12363
  return {
11776
12364
  /** Session created \u2014 record session start */
11777
12365
  'session.created': async ({ session }) => {
@@ -11784,6 +12372,21 @@ export const MemorixPlugin = async ({ project, client, $, directory, worktree })
11784
12372
 
11785
12373
  /** Session idle \u2014 record session end */
11786
12374
  'session.idle': async ({ session }) => {
12375
+ if (pendingAssistantResponse?.text) {
12376
+ const deliveryKey = pendingAssistantResponse.id
12377
+ ? \`\${pendingAssistantResponse.id}:\${pendingAssistantResponse.text}\`
12378
+ : pendingAssistantResponse.text;
12379
+ if (deliveryKey !== lastDeliveredAssistantKey) {
12380
+ runHook({
12381
+ agent: 'opencode',
12382
+ hook_event_name: 'message.updated',
12383
+ ai_response: pendingAssistantResponse.text,
12384
+ message_id: pendingAssistantResponse.id,
12385
+ cwd: directory,
12386
+ });
12387
+ lastDeliveredAssistantKey = deliveryKey;
12388
+ }
12389
+ }
11787
12390
  runHook({
11788
12391
  agent: 'opencode',
11789
12392
  hook_event_name: 'session.idle',
@@ -11812,6 +12415,19 @@ export const MemorixPlugin = async ({ project, client, $, directory, worktree })
11812
12415
  });
11813
12416
  },
11814
12417
 
12418
+ /** Message updated \u2014 cache the latest assistant response until session.idle */
12419
+ 'message.updated': async (input, output) => {
12420
+ const message = extractMessageInfo(input);
12421
+ const role = message?.role ?? input?.role ?? input?.info?.role;
12422
+ if (role !== 'assistant') return;
12423
+ const text = extractMessageText(message);
12424
+ if (!text) return;
12425
+ pendingAssistantResponse = {
12426
+ id: message?.id ?? input?.id ?? input?.messageID,
12427
+ text,
12428
+ };
12429
+ },
12430
+
11815
12431
  /** Session compacted \u2014 record post-compact event */
11816
12432
  'session.compacted': async ({ session }) => {
11817
12433
  runHook({
@@ -12061,7 +12677,7 @@ async function installHooks(agent, projectRoot, global = false) {
12061
12677
  return {
12062
12678
  agent,
12063
12679
  configPath: pluginPath,
12064
- events: ["session_start", "session_end", "post_tool", "post_edit", "post_compact", "post_command"],
12680
+ events: ["session_start", "session_end", "post_tool", "post_edit", "post_compact", "post_command", "post_response"],
12065
12681
  generated: { note: "OpenCode plugin installed at " + pluginPath }
12066
12682
  };
12067
12683
  }
@@ -12519,7 +13135,7 @@ var init_installers = __esm({
12519
13135
  "src/hooks/installers/index.ts"() {
12520
13136
  "use strict";
12521
13137
  init_esm_shims();
12522
- OPENCODE_PLUGIN_VERSION = 5;
13138
+ OPENCODE_PLUGIN_VERSION = 6;
12523
13139
  }
12524
13140
  });
12525
13141
 
@@ -12961,7 +13577,8 @@ function getTypeIcon(type) {
12961
13577
  "why-it-exists": "[WHY]",
12962
13578
  "decision": "[DECISION]",
12963
13579
  "trade-off": "[TRADEOFF]",
12964
- "reasoning": "[REASONING]"
13580
+ "reasoning": "[REASONING]",
13581
+ "probe": "[PROBE]"
12965
13582
  };
12966
13583
  return icons[type] ?? "[UNKNOWN]";
12967
13584
  }
@@ -15460,7 +16077,8 @@ var OBSERVATION_TYPES = [
15460
16077
  "discovery",
15461
16078
  "why-it-exists",
15462
16079
  "decision",
15463
- "trade-off"
16080
+ "trade-off",
16081
+ "probe"
15464
16082
  ];
15465
16083
  function coerceNumberArray(val) {
15466
16084
  if (Array.isArray(val)) return val.map(Number);
@@ -15689,7 +16307,7 @@ The path should point to a directory containing a .git folder.`
15689
16307
  };
15690
16308
  const server = existingServer ?? new McpServer({
15691
16309
  name: "memorix",
15692
- version: true ? "1.0.8" : "1.0.1"
16310
+ version: true ? "1.0.9" : "1.0.1"
15693
16311
  });
15694
16312
  const originalRegisterTool = server.registerTool.bind(server);
15695
16313
  server.registerTool = ((name, ...args) => {