@chendpoc/pi-memory 0.1.0

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.
Files changed (177) hide show
  1. package/README.md +180 -0
  2. package/dist/adapters/piComplete.d.ts +17 -0
  3. package/dist/adapters/piComplete.d.ts.map +1 -0
  4. package/dist/adapters/piComplete.js +169 -0
  5. package/dist/adapters/piComplete.js.map +1 -0
  6. package/dist/bundle/install.d.ts +34 -0
  7. package/dist/bundle/install.d.ts.map +1 -0
  8. package/dist/bundle/install.js +183 -0
  9. package/dist/bundle/install.js.map +1 -0
  10. package/dist/cli.d.ts +3 -0
  11. package/dist/cli.d.ts.map +1 -0
  12. package/dist/cli.js +245 -0
  13. package/dist/cli.js.map +1 -0
  14. package/dist/config.d.ts +27 -0
  15. package/dist/config.d.ts.map +1 -0
  16. package/dist/config.js +49 -0
  17. package/dist/config.js.map +1 -0
  18. package/dist/errclass.d.ts +7 -0
  19. package/dist/errclass.d.ts.map +1 -0
  20. package/dist/errclass.js +32 -0
  21. package/dist/errclass.js.map +1 -0
  22. package/dist/extension.d.ts +24 -0
  23. package/dist/extension.d.ts.map +1 -0
  24. package/dist/extension.js +7 -0
  25. package/dist/extension.js.map +1 -0
  26. package/dist/fallback/index.d.ts +11 -0
  27. package/dist/fallback/index.d.ts.map +1 -0
  28. package/dist/fallback/index.js +16 -0
  29. package/dist/fallback/index.js.map +1 -0
  30. package/dist/fallback/llmRerank.d.ts +19 -0
  31. package/dist/fallback/llmRerank.d.ts.map +1 -0
  32. package/dist/fallback/llmRerank.js +60 -0
  33. package/dist/fallback/llmRerank.js.map +1 -0
  34. package/dist/fallback/memoryMd.d.ts +6 -0
  35. package/dist/fallback/memoryMd.d.ts.map +1 -0
  36. package/dist/fallback/memoryMd.js +35 -0
  37. package/dist/fallback/memoryMd.js.map +1 -0
  38. package/dist/fallback/sessionIndex.d.ts +35 -0
  39. package/dist/fallback/sessionIndex.d.ts.map +1 -0
  40. package/dist/fallback/sessionIndex.js +222 -0
  41. package/dist/fallback/sessionIndex.js.map +1 -0
  42. package/dist/fallback/sessionSearch.d.ts +18 -0
  43. package/dist/fallback/sessionSearch.d.ts.map +1 -0
  44. package/dist/fallback/sessionSearch.js +161 -0
  45. package/dist/fallback/sessionSearch.js.map +1 -0
  46. package/dist/index.d.ts +25 -0
  47. package/dist/index.d.ts.map +1 -0
  48. package/dist/index.js +24 -0
  49. package/dist/index.js.map +1 -0
  50. package/dist/paths.d.ts +7 -0
  51. package/dist/paths.d.ts.map +1 -0
  52. package/dist/paths.js +26 -0
  53. package/dist/paths.js.map +1 -0
  54. package/dist/pi-extension.d.ts +6 -0
  55. package/dist/pi-extension.d.ts.map +1 -0
  56. package/dist/pi-extension.js +224 -0
  57. package/dist/pi-extension.js.map +1 -0
  58. package/dist/preflight/detectIntents.d.ts +102 -0
  59. package/dist/preflight/detectIntents.d.ts.map +1 -0
  60. package/dist/preflight/detectIntents.js +624 -0
  61. package/dist/preflight/detectIntents.js.map +1 -0
  62. package/dist/preflight/hook.d.ts +58 -0
  63. package/dist/preflight/hook.d.ts.map +1 -0
  64. package/dist/preflight/hook.js +77 -0
  65. package/dist/preflight/hook.js.map +1 -0
  66. package/dist/preflight/render.d.ts +21 -0
  67. package/dist/preflight/render.d.ts.map +1 -0
  68. package/dist/preflight/render.js +132 -0
  69. package/dist/preflight/render.js.map +1 -0
  70. package/dist/preflight/strip.d.ts +11 -0
  71. package/dist/preflight/strip.d.ts.map +1 -0
  72. package/dist/preflight/strip.js +46 -0
  73. package/dist/preflight/strip.js.map +1 -0
  74. package/dist/service.d.ts +56 -0
  75. package/dist/service.d.ts.map +1 -0
  76. package/dist/service.js +158 -0
  77. package/dist/service.js.map +1 -0
  78. package/dist/sidecar/bundle.d.ts +19 -0
  79. package/dist/sidecar/bundle.d.ts.map +1 -0
  80. package/dist/sidecar/bundle.js +39 -0
  81. package/dist/sidecar/bundle.js.map +1 -0
  82. package/dist/sidecar/client.d.ts +17 -0
  83. package/dist/sidecar/client.d.ts.map +1 -0
  84. package/dist/sidecar/client.js +107 -0
  85. package/dist/sidecar/client.js.map +1 -0
  86. package/dist/sidecar/process.d.ts +14 -0
  87. package/dist/sidecar/process.d.ts.map +1 -0
  88. package/dist/sidecar/process.js +126 -0
  89. package/dist/sidecar/process.js.map +1 -0
  90. package/dist/tools/memoryAppend.d.ts +37 -0
  91. package/dist/tools/memoryAppend.d.ts.map +1 -0
  92. package/dist/tools/memoryAppend.js +99 -0
  93. package/dist/tools/memoryAppend.js.map +1 -0
  94. package/dist/tools/memoryRecall.d.ts +113 -0
  95. package/dist/tools/memoryRecall.d.ts.map +1 -0
  96. package/dist/tools/memoryRecall.js +325 -0
  97. package/dist/tools/memoryRecall.js.map +1 -0
  98. package/dist/trainer/bundleBuilder.d.ts +30 -0
  99. package/dist/trainer/bundleBuilder.d.ts.map +1 -0
  100. package/dist/trainer/bundleBuilder.js +106 -0
  101. package/dist/trainer/bundleBuilder.js.map +1 -0
  102. package/dist/trainer/bundleLoader.d.ts +12 -0
  103. package/dist/trainer/bundleLoader.d.ts.map +1 -0
  104. package/dist/trainer/bundleLoader.js +59 -0
  105. package/dist/trainer/bundleLoader.js.map +1 -0
  106. package/dist/trainer/deltaMerge.d.ts +38 -0
  107. package/dist/trainer/deltaMerge.d.ts.map +1 -0
  108. package/dist/trainer/deltaMerge.js +183 -0
  109. package/dist/trainer/deltaMerge.js.map +1 -0
  110. package/dist/trainer/entityResolver.d.ts +27 -0
  111. package/dist/trainer/entityResolver.d.ts.map +1 -0
  112. package/dist/trainer/entityResolver.js +92 -0
  113. package/dist/trainer/entityResolver.js.map +1 -0
  114. package/dist/trainer/extractFacts.d.ts +67 -0
  115. package/dist/trainer/extractFacts.d.ts.map +1 -0
  116. package/dist/trainer/extractFacts.js +213 -0
  117. package/dist/trainer/extractFacts.js.map +1 -0
  118. package/dist/trainer/index.d.ts +54 -0
  119. package/dist/trainer/index.d.ts.map +1 -0
  120. package/dist/trainer/index.js +82 -0
  121. package/dist/trainer/index.js.map +1 -0
  122. package/dist/trainer/llmExtractor.d.ts +16 -0
  123. package/dist/trainer/llmExtractor.d.ts.map +1 -0
  124. package/dist/trainer/llmExtractor.js +146 -0
  125. package/dist/trainer/llmExtractor.js.map +1 -0
  126. package/dist/trainer/marker.d.ts +10 -0
  127. package/dist/trainer/marker.d.ts.map +1 -0
  128. package/dist/trainer/marker.js +28 -0
  129. package/dist/trainer/marker.js.map +1 -0
  130. package/dist/trainer/scheduler.d.ts +31 -0
  131. package/dist/trainer/scheduler.d.ts.map +1 -0
  132. package/dist/trainer/scheduler.js +72 -0
  133. package/dist/trainer/scheduler.js.map +1 -0
  134. package/dist/trainer/sessionLoader.d.ts +23 -0
  135. package/dist/trainer/sessionLoader.d.ts.map +1 -0
  136. package/dist/trainer/sessionLoader.js +106 -0
  137. package/dist/trainer/sessionLoader.js.map +1 -0
  138. package/dist/types.d.ts +135 -0
  139. package/dist/types.d.ts.map +1 -0
  140. package/dist/types.js +8 -0
  141. package/dist/types.js.map +1 -0
  142. package/package.json +78 -0
  143. package/src/adapters/piComplete.ts +233 -0
  144. package/src/bundle/install.ts +206 -0
  145. package/src/cli.ts +254 -0
  146. package/src/config.ts +92 -0
  147. package/src/errclass.ts +37 -0
  148. package/src/extension.ts +23 -0
  149. package/src/fallback/index.ts +24 -0
  150. package/src/fallback/llmRerank.ts +90 -0
  151. package/src/fallback/memoryMd.ts +36 -0
  152. package/src/fallback/sessionIndex.ts +289 -0
  153. package/src/fallback/sessionSearch.ts +181 -0
  154. package/src/index.ts +213 -0
  155. package/src/paths.ts +28 -0
  156. package/src/pi-extension.ts +276 -0
  157. package/src/preflight/detectIntents.ts +654 -0
  158. package/src/preflight/hook.ts +136 -0
  159. package/src/preflight/render.ts +185 -0
  160. package/src/preflight/strip.ts +50 -0
  161. package/src/service.ts +202 -0
  162. package/src/sidecar/bundle.ts +52 -0
  163. package/src/sidecar/client.ts +166 -0
  164. package/src/sidecar/process.ts +145 -0
  165. package/src/tools/memoryAppend.ts +113 -0
  166. package/src/tools/memoryRecall.ts +364 -0
  167. package/src/trainer/bundleBuilder.ts +192 -0
  168. package/src/trainer/bundleLoader.ts +105 -0
  169. package/src/trainer/deltaMerge.ts +221 -0
  170. package/src/trainer/entityResolver.ts +140 -0
  171. package/src/trainer/extractFacts.ts +312 -0
  172. package/src/trainer/index.ts +147 -0
  173. package/src/trainer/llmExtractor.ts +206 -0
  174. package/src/trainer/marker.ts +30 -0
  175. package/src/trainer/scheduler.ts +104 -0
  176. package/src/trainer/sessionLoader.ts +139 -0
  177. package/src/types.ts +168 -0
@@ -0,0 +1,221 @@
1
+ import type { ResolvedGraph, ResolvedEntity, ResolvedRelation } from "./entityResolver.js";
2
+ import type { ExtractedEvent } from "./extractFacts.js";
3
+ import { UPDATE_SIGNAL_RE } from "./extractFacts.js";
4
+
5
+ export type DeltaOp = "add" | "update" | "delete" | "skip";
6
+
7
+ export interface DeltaLogEntry {
8
+ op: DeltaOp;
9
+ kind: "entity" | "edge" | "event";
10
+ detail: string;
11
+ }
12
+
13
+ export interface DeltaLog {
14
+ entries: DeltaLogEntry[];
15
+ added: number;
16
+ updated: number;
17
+ deleted: number;
18
+ skipped: number;
19
+ }
20
+
21
+ export interface MergeResult {
22
+ graph: ResolvedGraph;
23
+ events: ExtractedEvent[];
24
+ delta: DeltaLog;
25
+ }
26
+
27
+ function edgeKey(headId: string, relation: string, tailId: string): string {
28
+ return `${headId}\0${relation}\0${tailId}`;
29
+ }
30
+
31
+ function headRelKey(headId: string, relation: string): string {
32
+ return `${headId}\0${relation}`;
33
+ }
34
+
35
+ /**
36
+ * Merge new extracted graph + events into an existing bundle graph,
37
+ * producing a combined result with delta operation log.
38
+ *
39
+ * Operations:
40
+ * - SKIP: entity+relation+tail already present with same value
41
+ * - ADD: genuinely new entity/edge/event
42
+ * - UPDATE: same head+relation but different tail, with update signal in evidence
43
+ * - DELETE: negated relation matches existing edge
44
+ */
45
+ export function deltaMerge(
46
+ existing: { graph: ResolvedGraph; events: ExtractedEvent[] },
47
+ incoming: { graph: ResolvedGraph; events: ExtractedEvent[] },
48
+ ): MergeResult {
49
+ const delta: DeltaLog = { entries: [], added: 0, updated: 0, deleted: 0, skipped: 0 };
50
+
51
+ function log(op: DeltaOp, kind: DeltaLogEntry["kind"], detail: string): void {
52
+ delta.entries.push({ op, kind, detail });
53
+ switch (op) {
54
+ case "add": delta.added++; break;
55
+ case "update": delta.updated++; break;
56
+ case "delete": delta.deleted++; break;
57
+ case "skip": delta.skipped++; break;
58
+ }
59
+ }
60
+
61
+ // ── Entities ──
62
+ const entityMap = new Map<string, ResolvedEntity>();
63
+ for (const ent of existing.graph.entities) {
64
+ entityMap.set(ent.id, { ...ent, mentions: [...ent.mentions] });
65
+ }
66
+
67
+ for (const ent of incoming.graph.entities) {
68
+ const prev = entityMap.get(ent.id);
69
+ if (prev) {
70
+ const newMentions = ent.mentions.filter(
71
+ (m) => !prev.mentions.some(
72
+ (pm) => pm.sessionId === m.sessionId && pm.turnIndex === m.turnIndex,
73
+ ),
74
+ );
75
+ if (newMentions.length > 0) {
76
+ prev.mentions.push(...newMentions);
77
+ log("update", "entity", `${ent.canonicalName}: +${newMentions.length} mentions`);
78
+ } else {
79
+ log("skip", "entity", ent.canonicalName);
80
+ }
81
+ } else {
82
+ entityMap.set(ent.id, { ...ent, mentions: [...ent.mentions] });
83
+ log("add", "entity", ent.canonicalName);
84
+ }
85
+ }
86
+
87
+ // ── Edges: collect negations and normal edges ──
88
+ const existingEdgeMap = new Map<string, ResolvedRelation>();
89
+ const existingByHeadRel = new Map<string, ResolvedRelation[]>();
90
+ for (const edge of existing.graph.relations) {
91
+ const key = edgeKey(edge.headEntityId, edge.relation, edge.tailEntityId);
92
+ existingEdgeMap.set(key, edge);
93
+ const hrKey = headRelKey(edge.headEntityId, edge.relation);
94
+ let arr = existingByHeadRel.get(hrKey);
95
+ if (!arr) { arr = []; existingByHeadRel.set(hrKey, arr); }
96
+ arr.push(edge);
97
+ }
98
+
99
+ const negatedRelations: ResolvedRelation[] = [];
100
+ const normalRelations: ResolvedRelation[] = [];
101
+ for (const rel of incoming.graph.relations) {
102
+ if (rel.negated) {
103
+ negatedRelations.push(rel);
104
+ } else {
105
+ normalRelations.push(rel);
106
+ }
107
+ }
108
+
109
+ // Process negations → DELETE from existing
110
+ const deletedKeys = new Set<string>();
111
+ for (const neg of negatedRelations) {
112
+ if (neg.headEntityId) {
113
+ const key = edgeKey(neg.headEntityId, neg.relation, neg.tailEntityId);
114
+ if (existingEdgeMap.has(key)) {
115
+ deletedKeys.add(key);
116
+ const ent = entityMap.get(neg.tailEntityId);
117
+ log("delete", "edge", `${neg.headEntityId}:${neg.relation}:${ent?.canonicalName ?? neg.tailEntityId}`);
118
+ }
119
+ } else {
120
+ for (const [key, edge] of existingEdgeMap) {
121
+ if (edge.relation === neg.relation && edge.tailEntityId === neg.tailEntityId) {
122
+ deletedKeys.add(key);
123
+ const headEnt = entityMap.get(edge.headEntityId);
124
+ const tailEnt = entityMap.get(edge.tailEntityId);
125
+ log("delete", "edge", `${headEnt?.canonicalName ?? edge.headEntityId}:${neg.relation}:${tailEnt?.canonicalName ?? edge.tailEntityId}`);
126
+ }
127
+ }
128
+ }
129
+ }
130
+
131
+ // Process normal edges → ADD / SKIP / UPDATE
132
+ const mergedEdges: ResolvedRelation[] = [];
133
+
134
+ for (const [key, edge] of existingEdgeMap) {
135
+ if (!deletedKeys.has(key)) {
136
+ mergedEdges.push(edge);
137
+ }
138
+ }
139
+
140
+ for (const rel of normalRelations) {
141
+ const key = edgeKey(rel.headEntityId, rel.relation, rel.tailEntityId);
142
+
143
+ if (existingEdgeMap.has(key) && !deletedKeys.has(key)) {
144
+ const tailEnt = entityMap.get(rel.tailEntityId);
145
+ log("skip", "edge", `${rel.headEntityId}:${rel.relation}:${tailEnt?.canonicalName ?? rel.tailEntityId}`);
146
+ continue;
147
+ }
148
+
149
+ if (deletedKeys.has(key)) {
150
+ // Re-adding a previously-deleted edge is still an add
151
+ }
152
+
153
+ const hasUpdateSignal = UPDATE_SIGNAL_RE.test(rel.evidence);
154
+ if (hasUpdateSignal) {
155
+ const hrKey = headRelKey(rel.headEntityId, rel.relation);
156
+ const existingForHR = existingByHeadRel.get(hrKey);
157
+ if (existingForHR) {
158
+ for (const oldEdge of existingForHR) {
159
+ if (oldEdge.tailEntityId !== rel.tailEntityId) {
160
+ const oldKey = edgeKey(oldEdge.headEntityId, oldEdge.relation, oldEdge.tailEntityId);
161
+ if (!deletedKeys.has(oldKey)) {
162
+ deletedKeys.add(oldKey);
163
+ const idx = mergedEdges.findIndex(
164
+ (e) => e.headEntityId === oldEdge.headEntityId &&
165
+ e.relation === oldEdge.relation &&
166
+ e.tailEntityId === oldEdge.tailEntityId,
167
+ );
168
+ if (idx !== -1) mergedEdges.splice(idx, 1);
169
+ const oldTailEnt = entityMap.get(oldEdge.tailEntityId);
170
+ const newTailEnt = entityMap.get(rel.tailEntityId);
171
+ log("update", "edge",
172
+ `${rel.relation}: ${oldTailEnt?.canonicalName ?? oldEdge.tailEntityId} → ${newTailEnt?.canonicalName ?? rel.tailEntityId}`);
173
+ }
174
+ }
175
+ }
176
+ }
177
+ }
178
+
179
+ if (!mergedEdges.some(
180
+ (e) => e.headEntityId === rel.headEntityId &&
181
+ e.relation === rel.relation &&
182
+ e.tailEntityId === rel.tailEntityId,
183
+ )) {
184
+ mergedEdges.push(rel);
185
+ if (!hasUpdateSignal || !existingByHeadRel.has(headRelKey(rel.headEntityId, rel.relation))) {
186
+ const tailEnt = entityMap.get(rel.tailEntityId);
187
+ log("add", "edge", `${rel.headEntityId}:${rel.relation}:${tailEnt?.canonicalName ?? rel.tailEntityId}`);
188
+ }
189
+ }
190
+ }
191
+
192
+ // ── Events ──
193
+ const eventSigSet = new Set<string>();
194
+ const mergedEvents: ExtractedEvent[] = [];
195
+
196
+ for (const ev of existing.events) {
197
+ const sig = `${ev.sessionId}\0${ev.turnIndex}\0${ev.description.slice(0, 80)}`;
198
+ eventSigSet.add(sig);
199
+ mergedEvents.push(ev);
200
+ }
201
+
202
+ for (const ev of incoming.events) {
203
+ const sig = `${ev.sessionId}\0${ev.turnIndex}\0${ev.description.slice(0, 80)}`;
204
+ if (eventSigSet.has(sig)) {
205
+ log("skip", "event", ev.description.slice(0, 60));
206
+ } else {
207
+ eventSigSet.add(sig);
208
+ mergedEvents.push(ev);
209
+ log("add", "event", ev.description.slice(0, 60));
210
+ }
211
+ }
212
+
213
+ return {
214
+ graph: {
215
+ entities: [...entityMap.values()],
216
+ relations: mergedEdges,
217
+ },
218
+ events: mergedEvents,
219
+ delta,
220
+ };
221
+ }
@@ -0,0 +1,140 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ import type {
4
+ ExtractedEntity,
5
+ ExtractedRelation,
6
+ EntityMention,
7
+ EntityType,
8
+ } from "./extractFacts.js";
9
+
10
+ export interface ResolvedEntity {
11
+ id: string;
12
+ canonicalName: string;
13
+ type: EntityType;
14
+ aliases: string[];
15
+ mentions: EntityMention[];
16
+ }
17
+
18
+ export interface ResolvedRelation {
19
+ headEntityId: string;
20
+ relation: string;
21
+ tailEntityId: string;
22
+ sessionId: string;
23
+ turnIndex: number;
24
+ evidence: string;
25
+ /** True when the session explicitly negates this relation. */
26
+ negated?: boolean;
27
+ }
28
+
29
+ export interface ResolvedGraph {
30
+ entities: ResolvedEntity[];
31
+ relations: ResolvedRelation[];
32
+ }
33
+
34
+ function normalizeKey(name: string): string {
35
+ return name
36
+ .toLowerCase()
37
+ .replace(/[''`]/g, "'")
38
+ .replace(/[""]/g, '"')
39
+ .replace(/\s+/g, " ")
40
+ .replace(/[.\-_]/g, "")
41
+ .trim();
42
+ }
43
+
44
+ function entityHash(normalizedKey: string): string {
45
+ return "ent_" + createHash("sha256").update(normalizedKey).digest("hex").slice(0, 12);
46
+ }
47
+
48
+ function pickBestType(types: EntityType[]): EntityType {
49
+ const priority: EntityType[] = [
50
+ "Person", "Company", "Organization", "Project", "Tool",
51
+ "Language", "Product", "Platform", "Website", "Location",
52
+ "Document", "File", "Concept", "Unknown",
53
+ ];
54
+ for (const t of priority) {
55
+ if (types.includes(t)) return t;
56
+ }
57
+ return "Unknown";
58
+ }
59
+
60
+ /**
61
+ * Cross-session entity dedup: normalize names, merge by key, assign stable IDs.
62
+ */
63
+ export function resolveEntities(
64
+ entities: ExtractedEntity[],
65
+ relations: ExtractedRelation[],
66
+ ): ResolvedGraph {
67
+ const keyMap = new Map<string, {
68
+ names: Set<string>;
69
+ types: EntityType[];
70
+ mentions: EntityMention[];
71
+ }>();
72
+
73
+ for (const ent of entities) {
74
+ const key = normalizeKey(ent.name);
75
+ if (!key) continue;
76
+
77
+ let bucket = keyMap.get(key);
78
+ if (!bucket) {
79
+ bucket = { names: new Set(), types: [], mentions: [] };
80
+ keyMap.set(key, bucket);
81
+ }
82
+ bucket.names.add(ent.name);
83
+ bucket.types.push(ent.type);
84
+ bucket.mentions.push(...ent.mentions);
85
+ }
86
+
87
+ const nameToId = new Map<string, string>();
88
+ const resolved: ResolvedEntity[] = [];
89
+
90
+ for (const [key, bucket] of keyMap) {
91
+ const id = entityHash(key);
92
+ const namesArr = [...bucket.names];
93
+ const canonical = namesArr.sort((a, b) => b.length - a.length)[0] ?? key;
94
+
95
+ for (const name of namesArr) {
96
+ nameToId.set(normalizeKey(name), id);
97
+ }
98
+
99
+ resolved.push({
100
+ id,
101
+ canonicalName: canonical,
102
+ type: pickBestType(bucket.types),
103
+ aliases: namesArr.filter((n) => n !== canonical),
104
+ mentions: bucket.mentions,
105
+ });
106
+ }
107
+
108
+ const resolvedRelations: ResolvedRelation[] = [];
109
+ for (const rel of relations) {
110
+ const tailId = nameToId.get(normalizeKey(rel.tailName));
111
+ if (!tailId) continue;
112
+
113
+ if (rel.negated) {
114
+ resolvedRelations.push({
115
+ headEntityId: rel.headName ? (nameToId.get(normalizeKey(rel.headName)) ?? "") : "",
116
+ relation: rel.relation,
117
+ tailEntityId: tailId,
118
+ sessionId: rel.sessionId,
119
+ turnIndex: rel.turnIndex,
120
+ evidence: rel.evidence,
121
+ negated: true,
122
+ });
123
+ continue;
124
+ }
125
+
126
+ const headId = nameToId.get(normalizeKey(rel.headName));
127
+ if (!headId) continue;
128
+
129
+ resolvedRelations.push({
130
+ headEntityId: headId,
131
+ relation: rel.relation,
132
+ tailEntityId: tailId,
133
+ sessionId: rel.sessionId,
134
+ turnIndex: rel.turnIndex,
135
+ evidence: rel.evidence,
136
+ });
137
+ }
138
+
139
+ return { entities: resolved, relations: resolvedRelations };
140
+ }
@@ -0,0 +1,312 @@
1
+ import type { SessionTurn, LoadedSession } from "./sessionLoader.js";
2
+
3
+ /** Relation catalog mirroring Kocoro compactMemoryRelationCatalog. */
4
+ export const RELATION_CATALOG = {
5
+ people_and_social: [
6
+ "employed_at", "previously_employed_at", "works_on", "affiliated_with",
7
+ "studied_under", "studied_at", "collaborates_with", "follows_person",
8
+ "followed_by_person", "commented_on", "knows_about", "has_handle_on",
9
+ "has_email",
10
+ ],
11
+ ownership_and_company: [
12
+ "created", "created_by", "maintained_by", "develops",
13
+ "developed_by_org", "owns", "owned_by", "acquired", "acquired_by",
14
+ "subsidiary_of", "parent_of", "founded", "founded_by", "invested_in",
15
+ "received_investment_from", "customer_of", "has_customer",
16
+ "competes_with", "banking_relationship",
17
+ ],
18
+ technical_and_project: [
19
+ "uses", "used_by", "depends_on", "implemented_in", "runs_on",
20
+ "integrates_with", "supports", "powered_by", "loaded_via",
21
+ "has_component", "part_of", "has_property", "has_path", "stored_at",
22
+ "monitors", "targets", "enables", "enabled_by", "generates",
23
+ "generated_from", "implements", "implemented_by", "excludes",
24
+ "deleted_from",
25
+ ],
26
+ content_and_metadata: [
27
+ "published_on", "released", "latest_release_tag", "forked_from",
28
+ "inspired_by", "succeeds", "preceded_by", "describes", "described_in",
29
+ "category", "has_alias", "has_url", "located_in", "scheduled_for",
30
+ "ranked_on", "listed_on", "features_project",
31
+ ],
32
+ generic_fallback: ["related_to", "other"],
33
+ } as const;
34
+
35
+ export const ALL_RELATIONS = Object.values(RELATION_CATALOG).flat();
36
+
37
+ export type EntityType =
38
+ | "Person" | "Company" | "Organization" | "Project" | "Tool"
39
+ | "Language" | "Concept" | "Document" | "File" | "Location"
40
+ | "Product" | "Website" | "Platform" | "Unknown";
41
+
42
+ export interface ExtractedEntity {
43
+ name: string;
44
+ type: EntityType;
45
+ mentions: EntityMention[];
46
+ }
47
+
48
+ export interface EntityMention {
49
+ sessionId: string;
50
+ turnIndex: number;
51
+ snippet: string;
52
+ }
53
+
54
+ export interface ExtractedRelation {
55
+ headName: string;
56
+ relation: string;
57
+ tailName: string;
58
+ sessionId: string;
59
+ turnIndex: number;
60
+ evidence: string;
61
+ /** True when the session explicitly negates this relation ("no longer uses X"). */
62
+ negated?: boolean;
63
+ }
64
+
65
+ export interface ExtractedEvent {
66
+ description: string;
67
+ sessionId: string;
68
+ timestamp: string;
69
+ turnIndex: number;
70
+ }
71
+
72
+ export interface ExtractionResult {
73
+ entities: ExtractedEntity[];
74
+ relations: ExtractedRelation[];
75
+ events: ExtractedEvent[];
76
+ }
77
+
78
+ /**
79
+ * Optional LLM-backed extractor interface. The default implementation
80
+ * uses regex/heuristic patterns only and does not require an LLM.
81
+ */
82
+ export interface LLMFactExtractor {
83
+ extractFacts(
84
+ turns: SessionTurn[],
85
+ sessionId: string,
86
+ ): Promise<{
87
+ entities: ExtractedEntity[];
88
+ relations: ExtractedRelation[];
89
+ events: ExtractedEvent[];
90
+ }>;
91
+ }
92
+
93
+ export interface ExtractFactsOptions {
94
+ llmExtractor?: LLMFactExtractor | null;
95
+ }
96
+
97
+ // ── Heuristic patterns ──
98
+
99
+ const CAPITALIZED_NAME_RE = /\b([A-Z][a-z]+(?:\s+[A-Z][a-z]+)+)\b/g;
100
+
101
+ const TOOL_PROJECT_RE =
102
+ /\b(React|Vue|Angular|Svelte|Next\.js|Nuxt|Django|Flask|Rails|Spring|Express|FastAPI|Rust|Go|Python|TypeScript|JavaScript|Node\.js|Docker|Kubernetes|Redis|PostgreSQL|MySQL|MongoDB|SQLite|Terraform|AWS|GCP|Azure|GitHub|GitLab|Slack|Figma|Notion|Linear|Jira|Vercel|Netlify|Supabase|Firebase|Anthropic|OpenAI|Claude|GPT|Gemini|Ollama|Tailwind|GSAP|Prisma|Drizzle|Vite|Webpack|ESLint|Prettier|Vitest|Jest|Playwright|Cypress)\b/gi;
103
+
104
+ const RELATION_PATTERNS: Array<{
105
+ re: RegExp;
106
+ relation: string;
107
+ headGroup: number;
108
+ tailGroup: number;
109
+ }> = [
110
+ { re: /\b(\w[\w\s]*?)\s+(?:uses?|is using|使用)\s+(\w[\w\s.]*?\b)/gi, relation: "uses", headGroup: 1, tailGroup: 2 },
111
+ { re: /\b(\w[\w\s]*?)\s+(?:created|built|made|开发了|创建了)\s+(\w[\w\s.]*?\b)/gi, relation: "created", headGroup: 1, tailGroup: 2 },
112
+ { re: /\b(\w[\w\s]*?)\s+(?:works?\s+(?:at|for)|在.+工作)\s+(\w[\w\s.]*?\b)/gi, relation: "employed_at", headGroup: 1, tailGroup: 2 },
113
+ { re: /\b(\w[\w\s]*?)\s+(?:works?\s+on|负责)\s+(\w[\w\s.]*?\b)/gi, relation: "works_on", headGroup: 1, tailGroup: 2 },
114
+ { re: /\b(\w[\w\s]*?)\s+(?:collaborates?\s+with|合作)\s+(\w[\w\s.]*?\b)/gi, relation: "collaborates_with", headGroup: 1, tailGroup: 2 },
115
+ { re: /\b(\w[\w\s]*?)\s+(?:depends?\s+on|依赖)\s+(\w[\w\s.]*?\b)/gi, relation: "depends_on", headGroup: 1, tailGroup: 2 },
116
+ { re: /\b(\w[\w\s]*?)\s+(?:runs?\s+on|运行在)\s+(\w[\w\s.]*?\b)/gi, relation: "runs_on", headGroup: 1, tailGroup: 2 },
117
+ { re: /\b(\w[\w\s]*?)\s+(?:integrates?\s+with|集成)\s+(\w[\w\s.]*?\b)/gi, relation: "integrates_with", headGroup: 1, tailGroup: 2 },
118
+ { re: /\b(\w[\w\s]*?)\s+(?:founded|创立了)\s+(\w[\w\s.]*?\b)/gi, relation: "founded", headGroup: 1, tailGroup: 2 },
119
+ { re: /\b(\w[\w\s]*?)\s+(?:implements?|实现了)\s+(\w[\w\s.]*?\b)/gi, relation: "implements", headGroup: 1, tailGroup: 2 },
120
+ ];
121
+
122
+ const NEGATION_PATTERNS: Array<{
123
+ re: RegExp;
124
+ relation: string;
125
+ entityGroup: number;
126
+ }> = [
127
+ { re: /\b(?:no longer|stopped|quit|dropped|gave up|abandoned)\s+(?:using|uses?)\s+(\w[\w\s.]*?\b)/gi, relation: "uses", entityGroup: 1 },
128
+ { re: /(?:不再|停止|放弃)(?:使用|用)\s*([A-Za-z][\w\s.]*\w)/gi, relation: "uses", entityGroup: 1 },
129
+ { re: /\b(?:left|quit|resigned from|no longer (?:works?|employed) (?:at|for))\s+(\w[\w\s.]*?\b)/gi, relation: "employed_at", entityGroup: 1 },
130
+ { re: /(?:不再|离开了|辞去了?)(?:在)?\s*([A-Za-z][\w\s.]*\w)/gi, relation: "employed_at", entityGroup: 1 },
131
+ { re: /\b(?:stopped|quit|no longer)\s+(?:working on|developing|maintaining)\s+(\w[\w\s.]*?\b)/gi, relation: "works_on", entityGroup: 1 },
132
+ { re: /(?:不再|停止)\s*(?:负责|开发|维护)\s*([A-Za-z][\w\s.]*\w)/gi, relation: "works_on", entityGroup: 1 },
133
+ { re: /\b(?:removed|dropped|deleted|uninstalled)\s+(\w[\w\s.]*?\b)/gi, relation: "uses", entityGroup: 1 },
134
+ { re: /\b(?:switched|migrated|moved)\s+(?:from)\s+(\w[\w\s.]*?)\s+(?:to)\s+\w/gi, relation: "uses", entityGroup: 1 },
135
+ { re: /(?:从)\s*([A-Za-z][\w\s.]*\w)\s*(?:迁移|切换|换)(?:到|成)/gi, relation: "uses", entityGroup: 1 },
136
+ ];
137
+
138
+ export const UPDATE_SIGNAL_RE = /\b(?:now|switched to|migrated to|moved to|changed to|upgraded to|replaced with|改用|换成|切换到|迁移到|升级到)\b/i;
139
+
140
+ const EVENT_PATTERNS: RegExp[] = [
141
+ /\b(?:decided|agreed|shipped|released|launched|deployed|migrated|completed|finished|merged|approved|resolved)\b/i,
142
+ /\b(?:决定|完成|上线|发布|部署|迁移|合并|通过)\b/,
143
+ ];
144
+
145
+ const NOISE_WORDS = new Set([
146
+ "the", "a", "an", "this", "that", "it", "i", "we", "they", "he", "she",
147
+ "my", "our", "their", "you", "your", "its",
148
+ "is", "are", "was", "were", "be", "been", "being",
149
+ "has", "have", "had", "do", "does", "did",
150
+ "will", "would", "could", "should", "can", "may", "might",
151
+ "not", "no", "yes", "ok", "sure", "thanks", "thank",
152
+ "if", "but", "and", "or", "so", "then", "also",
153
+ ]);
154
+
155
+ function isNoiseName(name: string): boolean {
156
+ const lower = name.toLowerCase().trim();
157
+ if (lower.length < 2 || lower.length > 60) return true;
158
+ const words = lower.split(/\s+/);
159
+ return words.every((w) => NOISE_WORDS.has(w));
160
+ }
161
+
162
+ function inferEntityType(name: string): EntityType {
163
+ const lower = name.toLowerCase();
164
+ const toolMatch = name.match(TOOL_PROJECT_RE);
165
+ if (toolMatch && toolMatch[0]!.toLowerCase() === lower) return "Tool";
166
+
167
+ if (/\b(?:inc|corp|ltd|llc|gmbh|co|company|group)\b/i.test(name)) return "Company";
168
+ if (/\b(?:university|institute|foundation|org)\b/i.test(name)) return "Organization";
169
+ if (/^[A-Z][a-z]+(?:\s+[A-Z][a-z]+)+$/.test(name)) return "Person";
170
+
171
+ return "Unknown";
172
+ }
173
+
174
+ function snippet(text: string, maxLen = 120): string {
175
+ if (text.length <= maxLen) return text;
176
+ return text.slice(0, maxLen) + "...";
177
+ }
178
+
179
+ /**
180
+ * Extract structured facts from a single session's turns using
181
+ * regex/heuristic patterns. For deeper extraction, pass an LLMFactExtractor.
182
+ */
183
+ export async function extractFacts(
184
+ session: LoadedSession,
185
+ opts?: ExtractFactsOptions,
186
+ ): Promise<ExtractionResult> {
187
+ if (opts?.llmExtractor) {
188
+ return opts.llmExtractor.extractFacts(session.turns, session.id);
189
+ }
190
+
191
+ const entMap = new Map<string, ExtractedEntity>();
192
+ const relations: ExtractedRelation[] = [];
193
+ const events: ExtractedEvent[] = [];
194
+
195
+ function addEntity(
196
+ name: string,
197
+ type: EntityType,
198
+ turn: SessionTurn,
199
+ ): void {
200
+ const trimmed = name.trim();
201
+ if (isNoiseName(trimmed)) return;
202
+ const key = trimmed.toLowerCase();
203
+ let ent = entMap.get(key);
204
+ if (!ent) {
205
+ ent = { name: trimmed, type, mentions: [] };
206
+ entMap.set(key, ent);
207
+ }
208
+ if (type !== "Unknown" && ent.type === "Unknown") {
209
+ ent.type = type;
210
+ }
211
+ ent.mentions.push({
212
+ sessionId: session.id,
213
+ turnIndex: turn.turnIndex,
214
+ snippet: snippet(turn.content),
215
+ });
216
+ }
217
+
218
+ for (const turn of session.turns) {
219
+ const text = turn.content;
220
+
221
+ // Extract capitalized multi-word names (likely Person names)
222
+ for (const m of text.matchAll(CAPITALIZED_NAME_RE)) {
223
+ addEntity(m[1]!, inferEntityType(m[1]!), turn);
224
+ }
225
+
226
+ // Extract known tools/projects
227
+ for (const m of text.matchAll(TOOL_PROJECT_RE)) {
228
+ addEntity(m[0]!, "Tool", turn);
229
+ }
230
+
231
+ // Extract relations
232
+ for (const pattern of RELATION_PATTERNS) {
233
+ for (const m of text.matchAll(pattern.re)) {
234
+ const head = m[pattern.headGroup]?.trim() ?? "";
235
+ const tail = m[pattern.tailGroup]?.trim() ?? "";
236
+ if (isNoiseName(head) || isNoiseName(tail)) continue;
237
+ addEntity(head, inferEntityType(head), turn);
238
+ addEntity(tail, inferEntityType(tail), turn);
239
+ relations.push({
240
+ headName: head,
241
+ relation: pattern.relation,
242
+ tailName: tail,
243
+ sessionId: session.id,
244
+ turnIndex: turn.turnIndex,
245
+ evidence: snippet(text),
246
+ });
247
+ }
248
+ }
249
+
250
+ // Extract negated relations ("no longer uses X", "stopped using X")
251
+ for (const pattern of NEGATION_PATTERNS) {
252
+ for (const m of text.matchAll(pattern.re)) {
253
+ const entityName = m[pattern.entityGroup]?.trim() ?? "";
254
+ if (isNoiseName(entityName)) continue;
255
+ addEntity(entityName, inferEntityType(entityName), turn);
256
+ relations.push({
257
+ headName: "",
258
+ relation: pattern.relation,
259
+ tailName: entityName,
260
+ sessionId: session.id,
261
+ turnIndex: turn.turnIndex,
262
+ evidence: snippet(text),
263
+ negated: true,
264
+ });
265
+ }
266
+ }
267
+
268
+ // Detect events (decisions, milestones)
269
+ for (const re of EVENT_PATTERNS) {
270
+ if (re.test(text)) {
271
+ events.push({
272
+ description: snippet(text, 200),
273
+ sessionId: session.id,
274
+ timestamp: session.createdAt || new Date().toISOString(),
275
+ turnIndex: turn.turnIndex,
276
+ });
277
+ break;
278
+ }
279
+ }
280
+ }
281
+
282
+ return {
283
+ entities: [...entMap.values()],
284
+ relations,
285
+ events,
286
+ };
287
+ }
288
+
289
+ /**
290
+ * Extract facts from multiple sessions and merge results.
291
+ */
292
+ export async function extractFactsFromSessions(
293
+ sessions: LoadedSession[],
294
+ opts?: ExtractFactsOptions,
295
+ ): Promise<ExtractionResult> {
296
+ const allEntities: ExtractedEntity[] = [];
297
+ const allRelations: ExtractedRelation[] = [];
298
+ const allEvents: ExtractedEvent[] = [];
299
+
300
+ for (const session of sessions) {
301
+ const result = await extractFacts(session, opts);
302
+ allEntities.push(...result.entities);
303
+ allRelations.push(...result.relations);
304
+ allEvents.push(...result.events);
305
+ }
306
+
307
+ return {
308
+ entities: allEntities,
309
+ relations: allRelations,
310
+ events: allEvents,
311
+ };
312
+ }