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/index.js CHANGED
@@ -675,6 +675,13 @@ var init_persistence = __esm({
675
675
  });
676
676
 
677
677
  // src/store/graph-store.ts
678
+ var graph_store_exports = {};
679
+ __export(graph_store_exports, {
680
+ GraphSqliteStore: () => GraphSqliteStore,
681
+ getGraphStore: () => getGraphStore,
682
+ initGraphStore: () => initGraphStore,
683
+ resetGraphStore: () => resetGraphStore
684
+ });
678
685
  import path6 from "path";
679
686
  import fs5 from "fs";
680
687
  function safeJsonParse(val, fallback) {
@@ -710,6 +717,16 @@ function getGraphStore() {
710
717
  if (!_graphStore) throw new Error("[memorix] GraphStore not initialized \u2014 call initGraphStore() first");
711
718
  return _graphStore;
712
719
  }
720
+ function resetGraphStore() {
721
+ if (_graphStore) {
722
+ try {
723
+ _graphStore.close();
724
+ } catch {
725
+ }
726
+ }
727
+ _graphStore = null;
728
+ _graphDataDir = null;
729
+ }
713
730
  var GraphSqliteStore, _graphStore, _graphDataDir;
714
731
  var init_graph_store = __esm({
715
732
  "src/store/graph-store.ts"() {
@@ -1041,7 +1058,8 @@ var init_types = __esm({
1041
1058
  "why-it-exists": "[WHY]",
1042
1059
  "decision": "[DECISION]",
1043
1060
  "trade-off": "[TRADEOFF]",
1044
- "reasoning": "[REASONING]"
1061
+ "reasoning": "[REASONING]",
1062
+ "probe": "[PROBE]"
1045
1063
  };
1046
1064
  TOPIC_KEY_FAMILIES = {
1047
1065
  "architecture": ["architecture", "design", "adr", "structure", "pattern"],
@@ -1055,6 +1073,15 @@ var init_types = __esm({
1055
1073
  });
1056
1074
 
1057
1075
  // src/store/mini-skill-store.ts
1076
+ var mini_skill_store_exports = {};
1077
+ __export(mini_skill_store_exports, {
1078
+ MiniSkillGracefulDegrade: () => MiniSkillGracefulDegrade,
1079
+ MiniSkillSqliteStore: () => MiniSkillSqliteStore,
1080
+ getMiniSkillStore: () => getMiniSkillStore,
1081
+ initMiniSkillStore: () => initMiniSkillStore,
1082
+ isMiniSkillStoreInitialized: () => isMiniSkillStoreInitialized,
1083
+ resetMiniSkillStore: () => resetMiniSkillStore
1084
+ });
1058
1085
  import path7 from "path";
1059
1086
  import fs6 from "fs";
1060
1087
  function skillToRow(skill) {
@@ -1108,6 +1135,10 @@ function getMiniSkillStore() {
1108
1135
  }
1109
1136
  return _store;
1110
1137
  }
1138
+ function resetMiniSkillStore() {
1139
+ _store = null;
1140
+ _storeDataDir = null;
1141
+ }
1111
1142
  async function initMiniSkillStore(dataDir) {
1112
1143
  if (_store && _storeDataDir === dataDir) return _store;
1113
1144
  _store = null;
@@ -1354,6 +1385,12 @@ async function promoteToMiniSkill(projectDir2, projectId, observations2, options
1354
1385
  `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.`
1355
1386
  );
1356
1387
  }
1388
+ const probes = observations2.filter((o) => o.type === "probe");
1389
+ if (probes.length > 0) {
1390
+ throw new Error(
1391
+ `Cannot promote probe observations \u2014 they are operational heartbeats, not durable knowledge. Blocked: ${probes.map((o) => `#${o.id} "${o.title.substring(0, 60)}"`).join(", ")}.`
1392
+ );
1393
+ }
1357
1394
  if (!options?.force) {
1358
1395
  const commandLogs = observations2.filter((o) => COMMAND_LOG_TITLE.test(o.title));
1359
1396
  if (commandLogs.length > 0) {
@@ -4407,6 +4444,10 @@ async function searchObservations(options) {
4407
4444
  if (statusFilter === "all") return true;
4408
4445
  const doc = hit.document;
4409
4446
  return (doc.status || "active") === statusFilter;
4447
+ }).filter((hit) => {
4448
+ if (options.type === "probe") return true;
4449
+ const doc = hit.document;
4450
+ return doc.type !== "probe";
4410
4451
  }).map((hit) => {
4411
4452
  const doc = hit.document;
4412
4453
  const obsType = doc.type;
@@ -6788,6 +6829,11 @@ function hasContradiction(oldText, newText) {
6788
6829
  ];
6789
6830
  return negationPatterns.some((p) => p.test(newText));
6790
6831
  }
6832
+ function normalizedSearchSimilarity(score) {
6833
+ if (!Number.isFinite(score) || score <= 0) return 0;
6834
+ if (score <= 1) return score;
6835
+ return 0;
6836
+ }
6791
6837
  function mergeNarratives(oldNarrative, newNarrative) {
6792
6838
  if (newNarrative.length > oldNarrative.length * 1.5) return newNarrative;
6793
6839
  if (oldNarrative.length > newNarrative.length * 1.5) return oldNarrative;
@@ -6878,12 +6924,13 @@ function scoreCandidate(extracted, candidate) {
6878
6924
  `${extracted.title} ${extracted.narrative}`,
6879
6925
  `${candidate.title} ${candidate.narrative}`
6880
6926
  );
6881
- const score = candidate.score * 0.6 + (entityMatch ? 0.2 : 0) + contentOverlap * 0.2;
6927
+ const searchSimilarity = normalizedSearchSimilarity(candidate.score);
6928
+ const score = searchSimilarity * 0.6 + (entityMatch ? 0.2 : 0) + contentOverlap * 0.2;
6882
6929
  const newLength = extracted.narrative.length + extracted.facts.join(" ").length;
6883
6930
  const oldLength = candidate.narrative.length + candidate.facts.length;
6884
6931
  const richer = newLength > oldLength * 1.15;
6885
6932
  const contradiction = hasContradiction(candidate.narrative, extracted.narrative);
6886
- return { score, entityMatch, richer, contradiction };
6933
+ return { score, searchSimilarity, entityMatch, richer, contradiction };
6887
6934
  }
6888
6935
  async function runResolve(extracted, projectId, searchMemories, getObservation2, useLLM = false) {
6889
6936
  const query = `${extracted.title} ${extracted.narrative.substring(0, 200)}`;
@@ -6906,7 +6953,7 @@ async function runResolve(extracted, projectId, searchMemories, getObservation2,
6906
6953
  }));
6907
6954
  scored.sort((a, b) => b.score - a.score);
6908
6955
  const best = scored[0];
6909
- if (best.hit.score >= SIMILARITY_DUPLICATE) {
6956
+ if (best.searchSimilarity >= SIMILARITY_DUPLICATE) {
6910
6957
  if (best.richer) {
6911
6958
  const existing = getObservation2(best.hit.observationId);
6912
6959
  const oldFacts = existing?.facts ?? best.hit.facts.split("\n").filter(Boolean);
@@ -7093,7 +7140,8 @@ var init_evaluate = __esm({
7093
7140
  "how-it-works": 0.6,
7094
7141
  "discovery": 0.55,
7095
7142
  "what-changed": 0.45,
7096
- "session-request": 0.4
7143
+ "session-request": 0.4,
7144
+ "probe": 0.1
7097
7145
  };
7098
7146
  SPECIFICITY_PATTERNS = [
7099
7147
  /\b\d+\.\d+\.\d+\b/,
@@ -7557,6 +7605,9 @@ function scoreObservationForSessionContext(obs, projectTokens, now = Date.now())
7557
7605
  if (obs.valueCategory === "core") {
7558
7606
  score += 2;
7559
7607
  }
7608
+ if (obs.type === "probe") {
7609
+ score -= 100;
7610
+ }
7560
7611
  return score;
7561
7612
  }
7562
7613
  async function startSession(projectDir2, projectId, opts) {
@@ -7765,7 +7816,8 @@ var init_session = __esm({
7765
7816
  "how-it-works": "[INFO]",
7766
7817
  "what-changed": "[CHANGE]",
7767
7818
  "why-it-exists": "[DECISION]",
7768
- "session-request": "[SESSION]"
7819
+ "session-request": "[SESSION]",
7820
+ "probe": "[PROBE]"
7769
7821
  };
7770
7822
  TYPE_WEIGHTS2 = {
7771
7823
  "gotcha": 6,
@@ -7844,11 +7896,17 @@ function getValueCategoryMultiplier(doc) {
7844
7896
  return 1;
7845
7897
  }
7846
7898
  function getEffectiveRetentionDays(doc) {
7899
+ const typeOverride = TYPE_RETENTION_OVERRIDE[doc.type];
7900
+ if (typeOverride !== void 0) {
7901
+ const raw2 = typeOverride * getSourceRetentionMultiplier(doc);
7902
+ return Math.max(MIN_RETENTION_DAYS, raw2);
7903
+ }
7847
7904
  const importance = getImportanceLevel(doc);
7848
7905
  const raw = RETENTION_DAYS[importance] * getSourceRetentionMultiplier(doc) * getValueCategoryMultiplier(doc);
7849
7906
  return Math.max(MIN_RETENTION_DAYS, raw);
7850
7907
  }
7851
7908
  function isImmune(doc) {
7909
+ if (doc.type === "probe") return false;
7852
7910
  if (doc.valueCategory === "core") return true;
7853
7911
  const importance = getImportanceLevel(doc);
7854
7912
  if (importance === "critical") return true;
@@ -7857,6 +7915,7 @@ function isImmune(doc) {
7857
7915
  return concepts.some((c) => PROTECTED_TAGS.has(c));
7858
7916
  }
7859
7917
  function getImmunityReason(doc) {
7918
+ if (doc.type === "probe") return null;
7860
7919
  if (doc.valueCategory === "core") return "core valueCategory (formation-classified)";
7861
7920
  const importance = getImportanceLevel(doc);
7862
7921
  if (importance === "critical") return "critical importance";
@@ -8013,7 +8072,7 @@ async function archiveExpired(projectDir2, referenceTime, accessMap) {
8013
8072
  return { archived: archivedCount, remaining: activeObs.length - archivedCount };
8014
8073
  });
8015
8074
  }
8016
- var RETENTION_DAYS, BASE_IMPORTANCE, TYPE_IMPORTANCE, PROTECTED_TAGS, MIN_ACCESS_FOR_IMMUNITY, MIN_RETENTION_DAYS;
8075
+ var RETENTION_DAYS, BASE_IMPORTANCE, TYPE_IMPORTANCE, TYPE_RETENTION_OVERRIDE, PROTECTED_TAGS, MIN_ACCESS_FOR_IMMUNITY, MIN_RETENTION_DAYS;
8017
8076
  var init_retention = __esm({
8018
8077
  "src/memory/retention.ts"() {
8019
8078
  "use strict";
@@ -8041,7 +8100,12 @@ var init_retention = __esm({
8041
8100
  "what-changed": "low",
8042
8101
  "why-it-exists": "medium",
8043
8102
  discovery: "low",
8044
- "session-request": "low"
8103
+ "session-request": "low",
8104
+ probe: "low"
8105
+ };
8106
+ TYPE_RETENTION_OVERRIDE = {
8107
+ probe: 7
8108
+ // Operational heartbeats: expire after ~7 days regardless of source/valueCategory
8045
8109
  };
8046
8110
  PROTECTED_TAGS = /* @__PURE__ */ new Set(["keep", "important", "pinned", "critical"]);
8047
8111
  MIN_ACCESS_FOR_IMMUNITY = 3;
@@ -9769,6 +9833,449 @@ var init_project_classification = __esm({
9769
9833
  }
9770
9834
  });
9771
9835
 
9836
+ // src/wiki/generator.ts
9837
+ var generator_exports = {};
9838
+ __export(generator_exports, {
9839
+ contextualHasSubstance: () => contextualHasSubstance,
9840
+ generateKnowledgeBase: () => generateKnowledgeBase,
9841
+ isCommandLog: () => isCommandLog,
9842
+ isEligible: () => isEligible,
9843
+ isExcludedType: () => isExcludedType
9844
+ });
9845
+ function isExcludedType(o) {
9846
+ return o.type === "probe";
9847
+ }
9848
+ function isCommandLog(o) {
9849
+ return COMMAND_LOG_TITLE3.test(o.title || "");
9850
+ }
9851
+ function isInactive(o) {
9852
+ const status = o.status ?? "active";
9853
+ return status !== "active";
9854
+ }
9855
+ function isOtherProject(o, projectId) {
9856
+ return o.projectId !== projectId;
9857
+ }
9858
+ function contextualHasSubstance(o) {
9859
+ if (o.valueCategory !== "contextual") return true;
9860
+ const hasFacts = (o.facts?.length ?? 0) > 0;
9861
+ const hasConcepts = (o.concepts?.length ?? 0) > 0;
9862
+ const hasFiles = (o.filesModified?.length ?? 0) > 0;
9863
+ const hasEntity = !!(o.entityName && o.entityName !== "quick-note" && o.entityName !== "unknown");
9864
+ return hasFacts || hasConcepts || hasFiles || hasEntity;
9865
+ }
9866
+ function isEligible(o, projectId) {
9867
+ if (isExcludedType(o)) return false;
9868
+ if (isCommandLog(o)) return false;
9869
+ if (isInactive(o)) return false;
9870
+ if (isOtherProject(o, projectId)) return false;
9871
+ if (o.valueCategory === "ephemeral") return false;
9872
+ if (!contextualHasSubstance(o)) return false;
9873
+ return true;
9874
+ }
9875
+ function obsToItem(o) {
9876
+ const ref = {
9877
+ kind: o.source === "git" ? "git" : "observation",
9878
+ id: `obs:${o.id}`,
9879
+ title: o.title
9880
+ };
9881
+ return {
9882
+ title: o.title,
9883
+ summary: o.narrative?.slice(0, 200) || "",
9884
+ type: o.type,
9885
+ entityName: o.entityName || void 0,
9886
+ refs: [ref]
9887
+ };
9888
+ }
9889
+ function skillToItem(s) {
9890
+ const ref = {
9891
+ kind: "mini-skill",
9892
+ id: `skill:${s.id}`,
9893
+ title: s.title
9894
+ };
9895
+ const obsRefs = s.sourceObservationIds.map(
9896
+ (oid) => ({ kind: "observation", id: `obs:${oid}` })
9897
+ );
9898
+ return {
9899
+ title: s.title,
9900
+ summary: s.instruction?.slice(0, 200) || "",
9901
+ type: "mini-skill",
9902
+ entityName: s.sourceEntity || void 0,
9903
+ refs: [ref, ...obsRefs]
9904
+ };
9905
+ }
9906
+ function buildGitSection(observations2) {
9907
+ const gitObs = observations2.filter(
9908
+ (o) => o.source === "git" && o.sourceDetail === "git-ingest"
9909
+ );
9910
+ const items = gitObs.map(obsToItem);
9911
+ return {
9912
+ id: "git-backed-facts",
9913
+ title: "Git-backed Facts",
9914
+ items,
9915
+ empty: items.length === 0
9916
+ };
9917
+ }
9918
+ function buildSkillsSection(skills) {
9919
+ const items = skills.map(skillToItem);
9920
+ return {
9921
+ id: "promoted-skills",
9922
+ title: "Promoted Skills",
9923
+ items,
9924
+ empty: items.length === 0
9925
+ };
9926
+ }
9927
+ function buildProjectOverview(projectId, eligibleObs, skills) {
9928
+ const refs = [
9929
+ ...eligibleObs.slice(0, 5).map((o) => ({
9930
+ kind: o.source === "git" ? "git" : "observation",
9931
+ id: `obs:${o.id}`,
9932
+ title: o.title
9933
+ })),
9934
+ ...skills.slice(0, 5).map((s) => ({
9935
+ kind: "mini-skill",
9936
+ id: `skill:${s.id}`,
9937
+ title: s.title
9938
+ }))
9939
+ ];
9940
+ if (refs.length === 0) {
9941
+ return {
9942
+ id: "project-overview",
9943
+ title: "Project Overview",
9944
+ items: [],
9945
+ empty: true
9946
+ };
9947
+ }
9948
+ const lines = [];
9949
+ lines.push(`Project: ${projectId}`);
9950
+ lines.push(`Observations in KB: ${eligibleObs.length}`);
9951
+ lines.push(`Promoted skills: ${skills.length}`);
9952
+ const entityCounts = /* @__PURE__ */ new Map();
9953
+ for (const o of eligibleObs) {
9954
+ if (o.entityName) {
9955
+ entityCounts.set(o.entityName, (entityCounts.get(o.entityName) || 0) + 1);
9956
+ }
9957
+ }
9958
+ const topEntities = [...entityCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5);
9959
+ if (topEntities.length > 0) {
9960
+ lines.push(`Top entities: ${topEntities.map(([name, count2]) => `${name} (${count2})`).join(", ")}`);
9961
+ }
9962
+ return {
9963
+ id: "project-overview",
9964
+ title: "Project Overview",
9965
+ items: [{
9966
+ title: projectId,
9967
+ summary: lines.join("\n"),
9968
+ type: "overview",
9969
+ refs
9970
+ }]
9971
+ };
9972
+ }
9973
+ function generateKnowledgeBase(options) {
9974
+ const { projectId, observations: observations2, miniSkills } = options;
9975
+ const eligible = observations2.filter((o) => isEligible(o, projectId));
9976
+ const scopedMiniSkills = miniSkills.filter((s) => s.projectId === projectId);
9977
+ const typedSections = SECTION_DEFS.map((def) => {
9978
+ const matched = eligible.filter(def.typeMatch);
9979
+ const items = matched.map(obsToItem);
9980
+ return {
9981
+ id: def.id,
9982
+ title: def.title,
9983
+ items,
9984
+ empty: items.length === 0
9985
+ };
9986
+ });
9987
+ const projectOverview = buildProjectOverview(projectId, eligible, scopedMiniSkills);
9988
+ const gitSection = buildGitSection(eligible);
9989
+ const skillsSection = buildSkillsSection(scopedMiniSkills);
9990
+ const sections = [
9991
+ projectOverview,
9992
+ ...typedSections,
9993
+ gitSection,
9994
+ skillsSection
9995
+ ];
9996
+ const allRefs = sections.flatMap((s) => s.items.flatMap((i) => i.refs));
9997
+ const obsRefCount = allRefs.filter((r) => r.kind === "observation" || r.kind === "git").length;
9998
+ const skillRefCount = allRefs.filter((r) => r.kind === "mini-skill").length;
9999
+ return {
10000
+ title: "Knowledge Base",
10001
+ subtitle: "LLM Wiki",
10002
+ projectId,
10003
+ generatedAt: options.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
10004
+ sections,
10005
+ stats: {
10006
+ observationsUsed: eligible.length,
10007
+ miniSkillsUsed: scopedMiniSkills.length,
10008
+ refs: obsRefCount + skillRefCount
10009
+ }
10010
+ };
10011
+ }
10012
+ var COMMAND_LOG_TITLE3, SECTION_DEFS;
10013
+ var init_generator = __esm({
10014
+ "src/wiki/generator.ts"() {
10015
+ "use strict";
10016
+ init_esm_shims();
10017
+ COMMAND_LOG_TITLE3 = /^(Ran:|Command:|Executed:)\s/i;
10018
+ SECTION_DEFS = [
10019
+ {
10020
+ id: "core-decisions",
10021
+ title: "Core Decisions",
10022
+ typeMatch: (o) => o.type === "decision" || o.type === "trade-off" || o.type === "reasoning"
10023
+ },
10024
+ {
10025
+ id: "operational-knowledge",
10026
+ title: "Operational Knowledge",
10027
+ typeMatch: (o) => o.type === "how-it-works" || o.type === "what-changed" || o.type === "why-it-exists" || o.type === "discovery" || o.type === "session-request"
10028
+ },
10029
+ {
10030
+ id: "known-gotchas",
10031
+ title: "Known Gotchas",
10032
+ typeMatch: (o) => o.type === "gotcha" || o.type === "problem-solution"
10033
+ }
10034
+ ];
10035
+ }
10036
+ });
10037
+
10038
+ // src/wiki/knowledge-graph.ts
10039
+ var knowledge_graph_exports = {};
10040
+ __export(knowledge_graph_exports, {
10041
+ generateKnowledgeGraph: () => generateKnowledgeGraph
10042
+ });
10043
+ function inferEdges(nodes, entityNameIndex) {
10044
+ const edges = [];
10045
+ const seen = /* @__PURE__ */ new Set();
10046
+ const maxRelatesEdgesPerEntitySection = 8;
10047
+ function addEdge(source, target, edgeType) {
10048
+ const key = `${source}:${edgeType}:${target}`;
10049
+ if (seen.has(key)) return;
10050
+ if (source === target) return;
10051
+ seen.add(key);
10052
+ edges.push({
10053
+ id: `e_${edges.length}_${source}_${target}`,
10054
+ source,
10055
+ target,
10056
+ edgeType
10057
+ });
10058
+ }
10059
+ function rankNodesForEntity(a, b) {
10060
+ const evidenceDiff = (b.evidenceCount || 0) - (a.evidenceCount || 0);
10061
+ if (evidenceDiff !== 0) return evidenceDiff;
10062
+ return a.id.localeCompare(b.id);
10063
+ }
10064
+ for (const [, group] of entityNameIndex) {
10065
+ if (group.length < 2) continue;
10066
+ const bySection = /* @__PURE__ */ new Map();
10067
+ for (const node of group) {
10068
+ const sectionGroup = bySection.get(node.sectionId) || [];
10069
+ sectionGroup.push(node);
10070
+ bySection.set(node.sectionId, sectionGroup);
10071
+ }
10072
+ for (const sectionGroup of bySection.values()) {
10073
+ const ranked = [...sectionGroup].sort(rankNodesForEntity);
10074
+ if (ranked.length === 2) {
10075
+ addEdge(ranked[0].id, ranked[1].id, "relates_to");
10076
+ addEdge(ranked[1].id, ranked[0].id, "relates_to");
10077
+ continue;
10078
+ }
10079
+ const anchor = ranked[0];
10080
+ for (const node of ranked.slice(1, maxRelatesEdgesPerEntitySection + 1)) {
10081
+ addEdge(anchor.id, node.id, "relates_to");
10082
+ }
10083
+ }
10084
+ const sectionAnchors = [...bySection.entries()].map(([sectionId, sectionGroup]) => ({
10085
+ sectionId,
10086
+ anchor: [...sectionGroup].sort(rankNodesForEntity)[0]
10087
+ })).sort((a, b) => sectionPriority(a.sectionId) - sectionPriority(b.sectionId));
10088
+ for (let i = 0; i < sectionAnchors.length; i++) {
10089
+ for (let j = i + 1; j < sectionAnchors.length; j++) {
10090
+ const from = sectionAnchors[i].anchor;
10091
+ const to = sectionAnchors[j].anchor;
10092
+ const edgeType = sectionPriority(sectionAnchors[i].sectionId) === sectionPriority(sectionAnchors[j].sectionId) ? "relates_to" : "supports";
10093
+ addEdge(from.id, to.id, edgeType);
10094
+ }
10095
+ }
10096
+ }
10097
+ return edges;
10098
+ }
10099
+ function sectionPriority(sectionId) {
10100
+ const order = ["core-decisions", "known-gotchas", "operational-knowledge", "git-backed-facts", "promoted-skills"];
10101
+ const idx = order.indexOf(sectionId);
10102
+ return idx >= 0 ? idx : order.length;
10103
+ }
10104
+ function sectionIdForObs(o) {
10105
+ if (GIT_SECTION.typeMatch(o)) return GIT_SECTION.id;
10106
+ for (const def of SECTION_DEFS2) {
10107
+ if (def.typeMatch(o)) return def.id;
10108
+ }
10109
+ return "operational-knowledge";
10110
+ }
10111
+ function mapRelationType(relationType) {
10112
+ const lower = relationType.toLowerCase();
10113
+ if (lower.includes("support") || lower.includes("depend")) return "supports";
10114
+ if (lower.includes("relat") || lower.includes("connect") || lower.includes("associat")) return "relates_to";
10115
+ if (lower.includes("mention") || lower.includes("refer")) return "mentions";
10116
+ if (lower.includes("deriv") || lower.includes("origin") || lower.includes("source")) return "derived_from";
10117
+ return "relates_to";
10118
+ }
10119
+ function generateKnowledgeGraph(options) {
10120
+ const { projectId, observations: observations2, miniSkills, graphEntities, graphRelations } = options;
10121
+ const eligible = observations2.filter((o) => isEligible(o, projectId));
10122
+ const scopedMiniSkills = miniSkills.filter((s) => s.projectId === projectId);
10123
+ const nodes = [];
10124
+ const entityNameIndex = /* @__PURE__ */ new Map();
10125
+ for (const o of eligible) {
10126
+ const sectionId = sectionIdForObs(o);
10127
+ const ref = {
10128
+ kind: o.source === "git" ? "git" : "observation",
10129
+ id: `obs:${o.id}`,
10130
+ title: o.title
10131
+ };
10132
+ const node = {
10133
+ id: `obs:${o.id}`,
10134
+ label: o.title,
10135
+ nodeType: o.type,
10136
+ sectionId,
10137
+ entityName: o.entityName || void 0,
10138
+ evidenceCount: (o.facts?.length ?? 0) + (o.concepts?.length ?? 0) + (o.filesModified?.length ?? 0),
10139
+ summary: o.narrative?.slice(0, 200) || "",
10140
+ refs: [ref]
10141
+ };
10142
+ nodes.push(node);
10143
+ if (o.entityName) {
10144
+ const group = entityNameIndex.get(o.entityName) || [];
10145
+ group.push(node);
10146
+ entityNameIndex.set(o.entityName, group);
10147
+ }
10148
+ }
10149
+ for (const s of scopedMiniSkills) {
10150
+ const ref = {
10151
+ kind: "mini-skill",
10152
+ id: `skill:${s.id}`,
10153
+ title: s.title
10154
+ };
10155
+ const obsRefs = s.sourceObservationIds.map(
10156
+ (oid) => ({ kind: "observation", id: `obs:${oid}` })
10157
+ );
10158
+ const node = {
10159
+ id: `skill:${s.id}`,
10160
+ label: s.title,
10161
+ nodeType: "mini-skill",
10162
+ sectionId: "promoted-skills",
10163
+ entityName: s.sourceEntity || void 0,
10164
+ evidenceCount: obsRefs.length,
10165
+ summary: s.instruction?.slice(0, 200) || "",
10166
+ refs: [ref, ...obsRefs]
10167
+ };
10168
+ nodes.push(node);
10169
+ if (s.sourceEntity) {
10170
+ const group = entityNameIndex.get(s.sourceEntity) || [];
10171
+ group.push(node);
10172
+ entityNameIndex.set(s.sourceEntity, group);
10173
+ }
10174
+ }
10175
+ const edges = inferEdges(nodes, entityNameIndex);
10176
+ for (const s of scopedMiniSkills) {
10177
+ const skillNodeId = `skill:${s.id}`;
10178
+ for (const oid of s.sourceObservationIds) {
10179
+ const obsNodeId = `obs:${oid}`;
10180
+ if (nodes.some((n) => n.id === obsNodeId)) {
10181
+ edges.push({
10182
+ id: `e_${edges.length}_${skillNodeId}_${obsNodeId}`,
10183
+ source: skillNodeId,
10184
+ target: obsNodeId,
10185
+ edgeType: "derived_from"
10186
+ });
10187
+ }
10188
+ }
10189
+ }
10190
+ if (graphRelations && graphRelations.length > 0) {
10191
+ const entityNodeMap = /* @__PURE__ */ new Map();
10192
+ for (const n of nodes) {
10193
+ if (n.entityName && !entityNodeMap.has(n.entityName)) {
10194
+ entityNodeMap.set(n.entityName, n.id);
10195
+ }
10196
+ }
10197
+ const seen = new Set(edges.map((e) => `${e.source}:${e.edgeType}:${e.target}`));
10198
+ for (const rel of graphRelations) {
10199
+ const srcId = entityNodeMap.get(rel.from);
10200
+ const tgtId = entityNodeMap.get(rel.to);
10201
+ if (!srcId || !tgtId) continue;
10202
+ if (srcId === tgtId) continue;
10203
+ const edgeType = mapRelationType(rel.relationType);
10204
+ const key = `${srcId}:${edgeType}:${tgtId}`;
10205
+ if (seen.has(key)) continue;
10206
+ seen.add(key);
10207
+ edges.push({
10208
+ id: `e_gr_${edges.length}_${srcId}_${tgtId}`,
10209
+ source: srcId,
10210
+ target: tgtId,
10211
+ edgeType
10212
+ });
10213
+ }
10214
+ }
10215
+ const sectionCounts = {};
10216
+ for (const n of nodes) {
10217
+ sectionCounts[n.sectionId] = (sectionCounts[n.sectionId] || 0) + 1;
10218
+ }
10219
+ const clusters = ALL_SECTIONS.filter((def) => (sectionCounts[def.id] ?? 0) > 0).map((def) => ({
10220
+ id: `cluster:${def.id}`,
10221
+ label: def.label,
10222
+ sectionId: def.id,
10223
+ nodeCount: sectionCounts[def.id] ?? 0
10224
+ }));
10225
+ const stats = {
10226
+ totalNodes: nodes.length,
10227
+ totalEdges: edges.length,
10228
+ clusterCount: clusters.length,
10229
+ sectionCounts
10230
+ };
10231
+ return {
10232
+ title: "Knowledge Graph",
10233
+ projectId,
10234
+ generatedAt: options.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
10235
+ nodes,
10236
+ edges,
10237
+ clusters,
10238
+ stats
10239
+ };
10240
+ }
10241
+ var SECTION_DEFS2, GIT_SECTION, SKILLS_SECTION, ALL_SECTIONS;
10242
+ var init_knowledge_graph = __esm({
10243
+ "src/wiki/knowledge-graph.ts"() {
10244
+ "use strict";
10245
+ init_esm_shims();
10246
+ init_generator();
10247
+ SECTION_DEFS2 = [
10248
+ {
10249
+ id: "core-decisions",
10250
+ label: "Core Decisions",
10251
+ typeMatch: (o) => o.type === "decision" || o.type === "trade-off" || o.type === "reasoning"
10252
+ },
10253
+ {
10254
+ id: "operational-knowledge",
10255
+ label: "Operational Knowledge",
10256
+ typeMatch: (o) => o.type === "how-it-works" || o.type === "what-changed" || o.type === "why-it-exists" || o.type === "discovery" || o.type === "session-request"
10257
+ },
10258
+ {
10259
+ id: "known-gotchas",
10260
+ label: "Known Gotchas",
10261
+ typeMatch: (o) => o.type === "gotcha" || o.type === "problem-solution"
10262
+ }
10263
+ ];
10264
+ GIT_SECTION = {
10265
+ id: "git-backed-facts",
10266
+ label: "Git-backed Facts",
10267
+ typeMatch: (o) => o.source === "git" && o.sourceDetail === "git-ingest"
10268
+ };
10269
+ SKILLS_SECTION = {
10270
+ id: "promoted-skills",
10271
+ label: "Promoted Skills",
10272
+ typeMatch: () => false
10273
+ // skills handled separately
10274
+ };
10275
+ ALL_SECTIONS = [...SECTION_DEFS2, GIT_SECTION, SKILLS_SECTION];
10276
+ }
10277
+ });
10278
+
9772
10279
  // src/dashboard/server.ts
9773
10280
  var server_exports = {};
9774
10281
  __export(server_exports, {
@@ -9910,6 +10417,7 @@ async function handleApi(req, res, dataDir, projectId, projectName, baseDir, pro
9910
10417
  const typeCounts = {};
9911
10418
  for (const obs of observations2) {
9912
10419
  const t = obs.type || "unknown";
10420
+ if (t === "probe") continue;
9913
10421
  typeCounts[t] = (typeCounts[t] || 0) + 1;
9914
10422
  }
9915
10423
  const sourceCounts = { git: 0, agent: 0, manual: 0 };
@@ -9951,7 +10459,7 @@ async function handleApi(req, res, dataDir, projectId, projectName, baseDir, pro
9951
10459
  else if (score >= 1) retentionSummary.stale++;
9952
10460
  else retentionSummary.archive++;
9953
10461
  }
9954
- const sorted = [...observations2].sort((a, b) => (b.id || 0) - (a.id || 0)).slice(0, 10);
10462
+ const sorted = [...observations2].filter((o) => o.type !== "probe").sort((a, b) => (b.id || 0) - (a.id || 0)).slice(0, 10);
9955
10463
  let embeddingStatus = { enabled: false, provider: "", dimensions: 0 };
9956
10464
  try {
9957
10465
  const { getEmbeddingProvider: getEmbeddingProvider2 } = await Promise.resolve().then(() => (init_provider(), provider_exports));
@@ -9991,7 +10499,7 @@ async function handleApi(req, res, dataDir, projectId, projectName, baseDir, pro
9991
10499
  const allObs = await getObservationStore().loadAll();
9992
10500
  const observations2 = filterActiveByProject(allObs, effectiveProjectId);
9993
10501
  const now = Date.now();
9994
- const scored = observations2.map((obs) => {
10502
+ const scored = observations2.filter((obs) => obs.type !== "probe").map((obs) => {
9995
10503
  const age = now - new Date(obs.createdAt || now).getTime();
9996
10504
  const ageHours = age / (1e3 * 60 * 60);
9997
10505
  const importance = obs.importance ?? 5;
@@ -10023,6 +10531,50 @@ async function handleApi(req, res, dataDir, projectId, projectName, baseDir, pro
10023
10531
  });
10024
10532
  break;
10025
10533
  }
10534
+ case "/knowledge": {
10535
+ const { generateKnowledgeBase: generateKnowledgeBase2 } = await Promise.resolve().then(() => (init_generator(), generator_exports));
10536
+ const { initObservations: initObservations2, getAllObservations: getAllObservations2 } = await Promise.resolve().then(() => (init_observations(), observations_exports));
10537
+ const { initMiniSkillStore: initMiniSkillStore2, getMiniSkillStore: getMiniSkillStore2 } = await Promise.resolve().then(() => (init_mini_skill_store(), mini_skill_store_exports));
10538
+ await initObservations2(effectiveDataDir);
10539
+ await initMiniSkillStore2(effectiveDataDir);
10540
+ const allObs = getAllObservations2();
10541
+ const skills = await getMiniSkillStore2().loadByProject(effectiveProjectId);
10542
+ const overview = generateKnowledgeBase2({
10543
+ projectId: effectiveProjectId,
10544
+ observations: allObs,
10545
+ miniSkills: skills
10546
+ });
10547
+ sendJson(res, overview);
10548
+ break;
10549
+ }
10550
+ case "/knowledge-graph": {
10551
+ const { generateKnowledgeGraph: generateKnowledgeGraph2 } = await Promise.resolve().then(() => (init_knowledge_graph(), knowledge_graph_exports));
10552
+ const { initObservations: initObservations2, getAllObservations: getAllObservations2 } = await Promise.resolve().then(() => (init_observations(), observations_exports));
10553
+ const { initMiniSkillStore: initMiniSkillStore2, getMiniSkillStore: getMiniSkillStore2 } = await Promise.resolve().then(() => (init_mini_skill_store(), mini_skill_store_exports));
10554
+ const { initGraphStore: initGraphStore2, getGraphStore: getGraphStore2 } = await Promise.resolve().then(() => (init_graph_store(), graph_store_exports));
10555
+ await initObservations2(effectiveDataDir);
10556
+ await initMiniSkillStore2(effectiveDataDir);
10557
+ await initGraphStore2(effectiveDataDir);
10558
+ const allObs = getAllObservations2();
10559
+ const skills = await getMiniSkillStore2().loadByProject(effectiveProjectId);
10560
+ const fullGraph = { entities: getGraphStore2().loadEntities(), relations: getGraphStore2().loadRelations() };
10561
+ const graphObs = await getObservationStore().loadAll();
10562
+ const projectEntityNames = new Set(
10563
+ graphObs.filter((o) => o.projectId === effectiveProjectId && (o.status ?? "active") === "active" && o.entityName).map((o) => o.entityName)
10564
+ );
10565
+ const scopedEntities = fullGraph.entities.filter((e) => projectEntityNames.has(e.name));
10566
+ const scopedEntityNameSet = new Set(scopedEntities.map((e) => e.name));
10567
+ const scopedRelations = fullGraph.relations.filter((r) => scopedEntityNameSet.has(r.from) && scopedEntityNameSet.has(r.to));
10568
+ const graph = generateKnowledgeGraph2({
10569
+ projectId: effectiveProjectId,
10570
+ observations: allObs,
10571
+ miniSkills: skills,
10572
+ graphEntities: scopedEntities,
10573
+ graphRelations: scopedRelations
10574
+ });
10575
+ sendJson(res, graph);
10576
+ break;
10577
+ }
10026
10578
  case "/config": {
10027
10579
  const os4 = await import("os");
10028
10580
  const { existsSync: existsSync10 } = await import("fs");
@@ -10259,13 +10811,20 @@ async function serveStatic(req, res, staticDir) {
10259
10811
  const ext = path14.extname(filePath);
10260
10812
  res.writeHead(200, {
10261
10813
  "Content-Type": MIME_TYPES[ext] || "application/octet-stream",
10262
- "Cache-Control": "no-cache"
10814
+ "Cache-Control": "no-store, no-cache, must-revalidate, max-age=0",
10815
+ "Pragma": "no-cache",
10816
+ "Expires": "0"
10263
10817
  });
10264
10818
  res.end(data);
10265
10819
  } catch {
10266
10820
  try {
10267
10821
  const indexData = await fs12.readFile(path14.join(staticDir, "index.html"));
10268
- res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
10822
+ res.writeHead(200, {
10823
+ "Content-Type": "text/html; charset=utf-8",
10824
+ "Cache-Control": "no-store, no-cache, must-revalidate, max-age=0",
10825
+ "Pragma": "no-cache",
10826
+ "Expires": "0"
10827
+ });
10269
10828
  res.end(indexData);
10270
10829
  } catch {
10271
10830
  sendError(res, "Not found", 404);
@@ -11736,6 +12295,8 @@ import { spawnSync } from 'node:child_process';
11736
12295
  export const MemorixPlugin = async ({ project, client, $, directory, worktree }) => {
11737
12296
  // Generate a stable session ID for this plugin lifetime
11738
12297
  const sessionId = \`opencode-\${Date.now().toString(36)}-\${Math.random().toString(36).slice(2, 8)}\`;
12298
+ let pendingAssistantResponse = null;
12299
+ let lastDeliveredAssistantKey = '';
11739
12300
 
11740
12301
  /**
11741
12302
  * Send event JSON to \`memorix hook\` via child_process.spawnSync.
@@ -11768,6 +12329,33 @@ export const MemorixPlugin = async ({ project, client, $, directory, worktree })
11768
12329
  }
11769
12330
  }
11770
12331
 
12332
+ function extractMessageInfo(input) {
12333
+ if (input && typeof input === 'object') {
12334
+ if (input.info && typeof input.info === 'object') return input.info;
12335
+ if (input.message && typeof input.message === 'object') return input.message;
12336
+ if (input.properties && typeof input.properties === 'object') {
12337
+ if (input.properties.info && typeof input.properties.info === 'object') return input.properties.info;
12338
+ if (input.properties.message && typeof input.properties.message === 'object') return input.properties.message;
12339
+ }
12340
+ }
12341
+ return input;
12342
+ }
12343
+
12344
+ function extractMessageText(message) {
12345
+ if (!message || typeof message !== 'object') return '';
12346
+ if (typeof message.content === 'string') return message.content.trim();
12347
+ const parts = Array.isArray(message.parts) ? message.parts : [];
12348
+ return parts
12349
+ .map((part) => {
12350
+ if (!part || typeof part !== 'object') return '';
12351
+ if (typeof part.text === 'string') return part.text;
12352
+ if (typeof part.content === 'string') return part.content;
12353
+ return '';
12354
+ })
12355
+ .join('')
12356
+ .trim();
12357
+ }
12358
+
11771
12359
  return {
11772
12360
  /** Session created \u2014 record session start */
11773
12361
  'session.created': async ({ session }) => {
@@ -11780,6 +12368,21 @@ export const MemorixPlugin = async ({ project, client, $, directory, worktree })
11780
12368
 
11781
12369
  /** Session idle \u2014 record session end */
11782
12370
  'session.idle': async ({ session }) => {
12371
+ if (pendingAssistantResponse?.text) {
12372
+ const deliveryKey = pendingAssistantResponse.id
12373
+ ? \`\${pendingAssistantResponse.id}:\${pendingAssistantResponse.text}\`
12374
+ : pendingAssistantResponse.text;
12375
+ if (deliveryKey !== lastDeliveredAssistantKey) {
12376
+ runHook({
12377
+ agent: 'opencode',
12378
+ hook_event_name: 'message.updated',
12379
+ ai_response: pendingAssistantResponse.text,
12380
+ message_id: pendingAssistantResponse.id,
12381
+ cwd: directory,
12382
+ });
12383
+ lastDeliveredAssistantKey = deliveryKey;
12384
+ }
12385
+ }
11783
12386
  runHook({
11784
12387
  agent: 'opencode',
11785
12388
  hook_event_name: 'session.idle',
@@ -11808,6 +12411,19 @@ export const MemorixPlugin = async ({ project, client, $, directory, worktree })
11808
12411
  });
11809
12412
  },
11810
12413
 
12414
+ /** Message updated \u2014 cache the latest assistant response until session.idle */
12415
+ 'message.updated': async (input, output) => {
12416
+ const message = extractMessageInfo(input);
12417
+ const role = message?.role ?? input?.role ?? input?.info?.role;
12418
+ if (role !== 'assistant') return;
12419
+ const text = extractMessageText(message);
12420
+ if (!text) return;
12421
+ pendingAssistantResponse = {
12422
+ id: message?.id ?? input?.id ?? input?.messageID,
12423
+ text,
12424
+ };
12425
+ },
12426
+
11811
12427
  /** Session compacted \u2014 record post-compact event */
11812
12428
  'session.compacted': async ({ session }) => {
11813
12429
  runHook({
@@ -12057,7 +12673,7 @@ async function installHooks(agent, projectRoot, global = false) {
12057
12673
  return {
12058
12674
  agent,
12059
12675
  configPath: pluginPath,
12060
- events: ["session_start", "session_end", "post_tool", "post_edit", "post_compact", "post_command"],
12676
+ events: ["session_start", "session_end", "post_tool", "post_edit", "post_compact", "post_command", "post_response"],
12061
12677
  generated: { note: "OpenCode plugin installed at " + pluginPath }
12062
12678
  };
12063
12679
  }
@@ -12515,7 +13131,7 @@ var init_installers = __esm({
12515
13131
  "src/hooks/installers/index.ts"() {
12516
13132
  "use strict";
12517
13133
  init_esm_shims();
12518
- OPENCODE_PLUGIN_VERSION = 5;
13134
+ OPENCODE_PLUGIN_VERSION = 6;
12519
13135
  }
12520
13136
  });
12521
13137
 
@@ -13046,7 +13662,8 @@ function getTypeIcon(type) {
13046
13662
  "why-it-exists": "[WHY]",
13047
13663
  "decision": "[DECISION]",
13048
13664
  "trade-off": "[TRADEOFF]",
13049
- "reasoning": "[REASONING]"
13665
+ "reasoning": "[REASONING]",
13666
+ "probe": "[PROBE]"
13050
13667
  };
13051
13668
  return icons[type] ?? "[UNKNOWN]";
13052
13669
  }
@@ -15545,7 +16162,8 @@ var OBSERVATION_TYPES = [
15545
16162
  "discovery",
15546
16163
  "why-it-exists",
15547
16164
  "decision",
15548
- "trade-off"
16165
+ "trade-off",
16166
+ "probe"
15549
16167
  ];
15550
16168
  function coerceNumberArray(val) {
15551
16169
  if (Array.isArray(val)) return val.map(Number);
@@ -15774,7 +16392,7 @@ The path should point to a directory containing a .git folder.`
15774
16392
  };
15775
16393
  const server = existingServer ?? new McpServer({
15776
16394
  name: "memorix",
15777
- version: true ? "1.0.8" : "1.0.1"
16395
+ version: true ? "1.0.9" : "1.0.1"
15778
16396
  });
15779
16397
  const originalRegisterTool = server.registerTool.bind(server);
15780
16398
  server.registerTool = ((name, ...args) => {