@graphorin/memory 0.6.1 → 0.7.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 (227) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/README.md +1 -1
  3. package/dist/conflict/index.js +0 -1
  4. package/dist/conflict/pipeline.js +0 -1
  5. package/dist/conflict/pipeline.js.map +1 -1
  6. package/dist/conflict/stages/stage3-heuristic-regex.js +0 -1
  7. package/dist/conflict/stages/stage3-heuristic-regex.js.map +1 -1
  8. package/dist/conflict/types.d.ts +1 -1
  9. package/dist/conflict/types.js.map +1 -1
  10. package/dist/consolidator/index.js +2 -0
  11. package/dist/consolidator/index.js.map +1 -1
  12. package/dist/consolidator/lock.js +7 -1
  13. package/dist/consolidator/lock.js.map +1 -1
  14. package/dist/consolidator/phases/deep.js +19 -12
  15. package/dist/consolidator/phases/deep.js.map +1 -1
  16. package/dist/consolidator/phases/induce.d.ts.map +1 -1
  17. package/dist/consolidator/phases/induce.js +2 -3
  18. package/dist/consolidator/phases/induce.js.map +1 -1
  19. package/dist/consolidator/phases/reflect.js +35 -8
  20. package/dist/consolidator/phases/reflect.js.map +1 -1
  21. package/dist/consolidator/phases/standard.js +199 -28
  22. package/dist/consolidator/phases/standard.js.map +1 -1
  23. package/dist/consolidator/reconcile.js +16 -15
  24. package/dist/consolidator/reconcile.js.map +1 -1
  25. package/dist/consolidator/runtime.d.ts.map +1 -1
  26. package/dist/consolidator/runtime.js +129 -47
  27. package/dist/consolidator/runtime.js.map +1 -1
  28. package/dist/consolidator/scheduler.d.ts +17 -3
  29. package/dist/consolidator/scheduler.d.ts.map +1 -1
  30. package/dist/consolidator/scheduler.js.map +1 -1
  31. package/dist/consolidator/types.d.ts +30 -1
  32. package/dist/consolidator/types.d.ts.map +1 -1
  33. package/dist/consolidator/types.js +10 -0
  34. package/dist/consolidator/types.js.map +1 -1
  35. package/dist/context-engine/compaction/clear-tool-results.d.ts +1 -1
  36. package/dist/context-engine/compaction/clear-tool-results.js +1 -1
  37. package/dist/context-engine/compaction/clear-tool-results.js.map +1 -1
  38. package/dist/context-engine/compaction/compactor.d.ts +1 -0
  39. package/dist/context-engine/compaction/compactor.d.ts.map +1 -1
  40. package/dist/context-engine/compaction/compactor.js +32 -8
  41. package/dist/context-engine/compaction/compactor.js.map +1 -1
  42. package/dist/context-engine/compaction/hooks/types.d.ts +1 -1
  43. package/dist/context-engine/compaction/index.d.ts +1 -0
  44. package/dist/context-engine/compaction/templates/marker.d.ts +33 -0
  45. package/dist/context-engine/compaction/templates/marker.d.ts.map +1 -0
  46. package/dist/context-engine/compaction/templates/marker.js +34 -0
  47. package/dist/context-engine/compaction/templates/marker.js.map +1 -0
  48. package/dist/context-engine/compaction/templates/summary-9-section.d.ts +1 -1
  49. package/dist/context-engine/compaction/templates/summary-9-section.d.ts.map +1 -1
  50. package/dist/context-engine/compaction/templates/summary-9-section.js +4 -3
  51. package/dist/context-engine/compaction/templates/summary-9-section.js.map +1 -1
  52. package/dist/context-engine/engine.d.ts +7 -176
  53. package/dist/context-engine/engine.d.ts.map +1 -1
  54. package/dist/context-engine/engine.js +0 -2
  55. package/dist/context-engine/engine.js.map +1 -1
  56. package/dist/context-engine/index.d.ts +8 -49
  57. package/dist/context-engine/io-types.d.ts +144 -0
  58. package/dist/context-engine/io-types.d.ts.map +1 -0
  59. package/dist/context-engine/locale-packs/types.d.ts +1 -1
  60. package/dist/context-engine/locale-packs/types.js.map +1 -1
  61. package/dist/context-engine/privacy-filter.d.ts +1 -2
  62. package/dist/context-engine/privacy-filter.d.ts.map +1 -1
  63. package/dist/context-engine/privacy-filter.js +1 -2
  64. package/dist/context-engine/privacy-filter.js.map +1 -1
  65. package/dist/errors/index.d.ts +2 -2
  66. package/dist/errors/index.js +2 -2
  67. package/dist/errors/index.js.map +1 -1
  68. package/dist/facade.d.ts +43 -52
  69. package/dist/facade.d.ts.map +1 -1
  70. package/dist/facade.js +5 -3
  71. package/dist/facade.js.map +1 -1
  72. package/dist/graph/entity-resolver.js +1 -1
  73. package/dist/graph/entity-resolver.js.map +1 -1
  74. package/dist/index.d.ts +16 -13
  75. package/dist/index.d.ts.map +1 -1
  76. package/dist/index.js +2 -5
  77. package/dist/index.js.map +1 -1
  78. package/dist/internal/envelope.js +57 -0
  79. package/dist/internal/envelope.js.map +1 -0
  80. package/dist/internal/injection-heuristics.js +0 -0
  81. package/dist/internal/injection-heuristics.js.map +1 -1
  82. package/dist/internal/llm-json.js +40 -0
  83. package/dist/internal/llm-json.js.map +1 -0
  84. package/dist/internal/storage-adapter.d.ts +23 -10
  85. package/dist/internal/storage-adapter.d.ts.map +1 -1
  86. package/dist/memory-interface.d.ts +131 -0
  87. package/dist/memory-interface.d.ts.map +1 -0
  88. package/dist/package.js +1 -1
  89. package/dist/package.js.map +1 -1
  90. package/dist/search/iterative.d.ts.map +1 -1
  91. package/dist/search/iterative.js +2 -3
  92. package/dist/search/iterative.js.map +1 -1
  93. package/dist/search/query-transform.d.ts.map +1 -1
  94. package/dist/search/query-transform.js +2 -3
  95. package/dist/search/query-transform.js.map +1 -1
  96. package/dist/tiers/episodic-memory.js +3 -3
  97. package/dist/tiers/episodic-memory.js.map +1 -1
  98. package/dist/tiers/index.d.ts +2 -2
  99. package/dist/tiers/insight-memory.js +1 -1
  100. package/dist/tiers/insight-memory.js.map +1 -1
  101. package/dist/tiers/procedural-memory.d.ts +1 -1
  102. package/dist/tiers/procedural-memory.js +2 -2
  103. package/dist/tiers/procedural-memory.js.map +1 -1
  104. package/dist/tiers/semantic-memory.d.ts +58 -4
  105. package/dist/tiers/semantic-memory.d.ts.map +1 -1
  106. package/dist/tiers/semantic-memory.js +103 -20
  107. package/dist/tiers/semantic-memory.js.map +1 -1
  108. package/dist/tools/block-tools.d.ts +19 -57
  109. package/dist/tools/block-tools.d.ts.map +1 -1
  110. package/dist/tools/block-tools.js.map +1 -1
  111. package/dist/tools/fact-tools.d.ts +53 -214
  112. package/dist/tools/fact-tools.d.ts.map +1 -1
  113. package/dist/tools/fact-tools.js +3 -2
  114. package/dist/tools/fact-tools.js.map +1 -1
  115. package/dist/tools/recall-tools.d.ts +34 -175
  116. package/dist/tools/recall-tools.d.ts.map +1 -1
  117. package/dist/tools/recall-tools.js +5 -4
  118. package/dist/tools/recall-tools.js.map +1 -1
  119. package/dist/tools/runbook-tools.d.ts +15 -58
  120. package/dist/tools/runbook-tools.d.ts.map +1 -1
  121. package/dist/tools/runbook-tools.js.map +1 -1
  122. package/dist/tools/types.d.ts +1 -1
  123. package/package.json +18 -17
  124. package/src/conflict/index.ts +29 -0
  125. package/src/conflict/locale-packs/en.ts +142 -0
  126. package/src/conflict/locale-packs/index.ts +17 -0
  127. package/src/conflict/locale-packs/types.ts +138 -0
  128. package/src/conflict/pipeline.ts +371 -0
  129. package/src/conflict/stages/helpers.ts +65 -0
  130. package/src/conflict/stages/stage1-exact-dedup.ts +40 -0
  131. package/src/conflict/stages/stage2-embedding-three-zone.ts +54 -0
  132. package/src/conflict/stages/stage3-heuristic-regex.ts +51 -0
  133. package/src/conflict/stages/stage4-subject-predicate.ts +46 -0
  134. package/src/conflict/stages/stage5-defer-to-deep.ts +34 -0
  135. package/src/conflict/types.ts +263 -0
  136. package/src/consolidator/budget.ts +312 -0
  137. package/src/consolidator/decay.ts +246 -0
  138. package/src/consolidator/dlq.ts +61 -0
  139. package/src/consolidator/errors.ts +87 -0
  140. package/src/consolidator/idempotency.ts +27 -0
  141. package/src/consolidator/index.ts +269 -0
  142. package/src/consolidator/lock.ts +141 -0
  143. package/src/consolidator/noise-filter.ts +235 -0
  144. package/src/consolidator/phases/deep.ts +308 -0
  145. package/src/consolidator/phases/induce.ts +423 -0
  146. package/src/consolidator/phases/learned-context.ts +233 -0
  147. package/src/consolidator/phases/light.ts +166 -0
  148. package/src/consolidator/phases/reflect.ts +504 -0
  149. package/src/consolidator/phases/standard.ts +1002 -0
  150. package/src/consolidator/reconcile.ts +273 -0
  151. package/src/consolidator/runtime.ts +1049 -0
  152. package/src/consolidator/scheduler.ts +182 -0
  153. package/src/consolidator/triggers.ts +134 -0
  154. package/src/consolidator/types.ts +622 -0
  155. package/src/context-engine/annotations.ts +161 -0
  156. package/src/context-engine/auto-recall.ts +92 -0
  157. package/src/context-engine/compaction/clear-tool-results.ts +232 -0
  158. package/src/context-engine/compaction/compactor.ts +403 -0
  159. package/src/context-engine/compaction/hooks/reanchor-persona-block.ts +55 -0
  160. package/src/context-engine/compaction/hooks/reanchor-pinned-facts.ts +66 -0
  161. package/src/context-engine/compaction/hooks/reanchor-project-rules.ts +57 -0
  162. package/src/context-engine/compaction/hooks/reanchor-recent-results.ts +116 -0
  163. package/src/context-engine/compaction/hooks/types.ts +68 -0
  164. package/src/context-engine/compaction/index.ts +60 -0
  165. package/src/context-engine/compaction/templates/marker.ts +33 -0
  166. package/src/context-engine/compaction/templates/summary-9-section.ts +223 -0
  167. package/src/context-engine/compaction/thresholds.ts +107 -0
  168. package/src/context-engine/compaction/types.ts +255 -0
  169. package/src/context-engine/engine.ts +970 -0
  170. package/src/context-engine/index.ts +118 -0
  171. package/src/context-engine/io-types.ts +162 -0
  172. package/src/context-engine/locale-packs/en.ts +107 -0
  173. package/src/context-engine/locale-packs/index.ts +19 -0
  174. package/src/context-engine/locale-packs/resolver.ts +197 -0
  175. package/src/context-engine/locale-packs/types.ts +177 -0
  176. package/src/context-engine/metadata.ts +114 -0
  177. package/src/context-engine/preambles/inbound-en.ts +19 -0
  178. package/src/context-engine/privacy-filter.ts +190 -0
  179. package/src/context-engine/templates/base-en.ts +19 -0
  180. package/src/context-engine/templates/composer.ts +91 -0
  181. package/src/context-engine/token-budget.ts +319 -0
  182. package/src/context-engine/token-counter.ts +157 -0
  183. package/src/errors/index.ts +273 -0
  184. package/src/facade.ts +750 -0
  185. package/src/graph/entity-resolver.ts +364 -0
  186. package/src/graph/index.ts +24 -0
  187. package/src/index.ts +259 -0
  188. package/src/internal/contextualize.ts +173 -0
  189. package/src/internal/embedder-binding.ts +53 -0
  190. package/src/internal/envelope.ts +70 -0
  191. package/src/internal/id.ts +23 -0
  192. package/src/internal/injection-heuristics.ts +0 -0
  193. package/src/internal/llm-json.ts +38 -0
  194. package/src/internal/spans.ts +35 -0
  195. package/src/internal/storage-adapter.ts +963 -0
  196. package/src/memory-interface.ts +158 -0
  197. package/src/migration/embedder-migration.ts +237 -0
  198. package/src/migration/index.ts +13 -0
  199. package/src/search/explain.ts +123 -0
  200. package/src/search/fit-weights.ts +109 -0
  201. package/src/search/graph-ppr.ts +38 -0
  202. package/src/search/index.ts +55 -0
  203. package/src/search/iterative.ts +658 -0
  204. package/src/search/query-transform.ts +285 -0
  205. package/src/search/rrf.ts +225 -0
  206. package/src/search/trust.ts +55 -0
  207. package/src/search/types.ts +50 -0
  208. package/src/tiers/episodic-memory.ts +463 -0
  209. package/src/tiers/index.ts +50 -0
  210. package/src/tiers/insight-memory.ts +208 -0
  211. package/src/tiers/procedural-memory.ts +482 -0
  212. package/src/tiers/semantic-memory.ts +1729 -0
  213. package/src/tiers/session-memory.ts +271 -0
  214. package/src/tiers/shared-memory.ts +62 -0
  215. package/src/tiers/working-memory.ts +368 -0
  216. package/src/tools/block-tools.ts +172 -0
  217. package/src/tools/fact-tools.ts +473 -0
  218. package/src/tools/index.ts +109 -0
  219. package/src/tools/recall-tools.ts +303 -0
  220. package/src/tools/runbook-tools.ts +107 -0
  221. package/src/tools/taint.ts +39 -0
  222. package/src/tools/types.ts +37 -0
  223. package/dist/conflict/locale-packs/index.js +0 -4
  224. package/dist/context-engine/compaction/index.js +0 -10
  225. package/dist/context-engine/index.d.ts.map +0 -1
  226. package/dist/context-engine/index.js +0 -25
  227. package/dist/context-engine/locale-packs/index.js +0 -4
@@ -0,0 +1,463 @@
1
+ import type {
2
+ EmbedderProvider,
3
+ Episode,
4
+ MemoryHit,
5
+ MemoryOwner,
6
+ MemoryProvenance,
7
+ MemoryStatus,
8
+ Sensitivity,
9
+ SessionScope,
10
+ Tracer,
11
+ } from '@graphorin/core';
12
+ import { QuarantinePromotionRefusedError } from '../errors/index.js';
13
+ import { newMemoryId } from '../internal/id.js';
14
+ import { detectMemoryInjection } from '../internal/injection-heuristics.js';
15
+ import { withMemorySpan } from '../internal/spans.js';
16
+ import type { MemoryStoreAdapter } from '../internal/storage-adapter.js';
17
+
18
+ /**
19
+ * Author-time episode payload. The framework derives `id`,
20
+ * `kind: 'episodic'`, `userId`, `createdAt`, `updatedAt`, and the
21
+ * `embedder_id` from the surrounding `EpisodicMemory.record(...)`
22
+ * call.
23
+ *
24
+ * @stable
25
+ */
26
+ export interface EpisodeInput {
27
+ readonly summary: string;
28
+ readonly startedAt: string;
29
+ readonly endedAt: string;
30
+ /** Optional importance score in `[0, 1]`. */
31
+ readonly importance?: number;
32
+ readonly sensitivity?: Sensitivity;
33
+ readonly tags?: ReadonlyArray<string>;
34
+ /**
35
+ * Trust-provenance tag (P1-4). Episodes auto-formed by the
36
+ * consolidator pass `'extraction'` so they land quarantined; omit
37
+ * (defaults to first-party `active`) for user-authored episodes.
38
+ */
39
+ readonly provenance?: MemoryProvenance;
40
+ /**
41
+ * Retrieval-trust state (P1-4). Defaults to `active`; the
42
+ * consolidator records auto-formed episodes as `'quarantined'` so
43
+ * they are excluded from action-driving recall until validated.
44
+ */
45
+ readonly status?: MemoryStatus;
46
+ /** Principal dimension (D3). `'agent'` on auto-formed episodes. */
47
+ readonly owner?: MemoryOwner;
48
+ }
49
+
50
+ /**
51
+ * Triple-signal episode retrieval weights. Defaults match DEC-105:
52
+ * `recency 0.3`, `relevance 0.5`, `importance 0.2`. Implementations
53
+ * normalize the weighted sum back to `[0, 1]`.
54
+ *
55
+ * @stable
56
+ */
57
+ export interface EpisodeRetrievalWeights {
58
+ readonly recency: number;
59
+ readonly relevance: number;
60
+ readonly importance: number;
61
+ }
62
+
63
+ const DEFAULT_WEIGHTS: EpisodeRetrievalWeights = Object.freeze({
64
+ recency: 0.3,
65
+ relevance: 0.5,
66
+ importance: 0.2,
67
+ });
68
+
69
+ /**
70
+ * Per-call options accepted by {@link EpisodicMemory.search}.
71
+ *
72
+ * @stable
73
+ */
74
+ export interface EpisodeSearchOptions {
75
+ readonly topK?: number;
76
+ readonly signal?: AbortSignal;
77
+ readonly weights?: EpisodeRetrievalWeights;
78
+ readonly dateRange?: { readonly from?: string; readonly to?: string };
79
+ /**
80
+ * Point-in-time ("as of") read. When set, only episodes that had
81
+ * started by this instant (`started_at <= asOf`) are returned.
82
+ * ISO-8601. Absent ⇒ current behaviour is unchanged. P0-2.
83
+ *
84
+ * @stable
85
+ */
86
+ readonly asOf?: string;
87
+ /**
88
+ * Include quarantined episodes in the result set (P1-4). Defaults to
89
+ * `false`: action-driving recall never returns quarantined rows. Set
90
+ * `true` only for the validation / inspector path - never for
91
+ * auto-recall fed back into the model. Auto-formed episodes (P1-2)
92
+ * land quarantined, so this is how an operator surfaces them for
93
+ * review.
94
+ *
95
+ * @stable
96
+ */
97
+ readonly includeQuarantined?: boolean;
98
+ }
99
+
100
+ /**
101
+ * `EpisodicMemory` - record + retrieve summarized stretches of past
102
+ * activity. Stored embeddings power triple-signal retrieval (recency
103
+ * × relevance × importance).
104
+ *
105
+ * @stable
106
+ */
107
+ export class EpisodicMemory {
108
+ readonly #store: MemoryStoreAdapter;
109
+ readonly #tracer: Tracer;
110
+ readonly #embedder: EmbedderProvider | null;
111
+ readonly #embedderIdProvider: () => string | null;
112
+
113
+ constructor(args: {
114
+ store: MemoryStoreAdapter;
115
+ tracer: Tracer;
116
+ embedder: EmbedderProvider | null;
117
+ embedderIdProvider: () => string | null;
118
+ }) {
119
+ this.#store = args.store;
120
+ this.#tracer = args.tracer;
121
+ this.#embedder = args.embedder;
122
+ this.#embedderIdProvider = args.embedderIdProvider;
123
+ }
124
+
125
+ /** Persist an episode + its embedding (when an embedder is configured). */
126
+ async record(scope: SessionScope, input: EpisodeInput): Promise<Episode> {
127
+ return withMemorySpan(this.#tracer, 'memory.write.episodic', scope, {}, async (span) => {
128
+ const now = new Date().toISOString();
129
+ const episode: Episode = {
130
+ id: newMemoryId('ep'),
131
+ kind: 'episodic',
132
+ userId: scope.userId,
133
+ ...(scope.sessionId !== undefined ? { sessionId: scope.sessionId } : {}),
134
+ ...(scope.agentId !== undefined ? { agentId: scope.agentId } : {}),
135
+ sensitivity: input.sensitivity ?? 'internal',
136
+ summary: input.summary,
137
+ startedAt: input.startedAt,
138
+ endedAt: input.endedAt,
139
+ ...(input.importance !== undefined ? { importance: input.importance } : {}),
140
+ ...(input.tags !== undefined ? { tags: Object.freeze([...input.tags]) } : {}),
141
+ ...(input.provenance !== undefined ? { provenance: input.provenance } : {}),
142
+ ...(input.status !== undefined ? { status: input.status } : {}),
143
+ ...(input.owner !== undefined ? { owner: input.owner } : {}),
144
+ createdAt: now,
145
+ updatedAt: now,
146
+ };
147
+ const embedderId = this.#embedderIdProvider();
148
+ if (
149
+ this.#embedder !== null &&
150
+ embedderId !== null &&
151
+ typeof this.#store.episodic.putWithEmbedding === 'function'
152
+ ) {
153
+ // PS-10: a stored episode summary is the `passage` role for E5.
154
+ const [vector] = await this.#embedder.embed([input.summary], { taskType: 'passage' });
155
+ if (vector !== undefined) {
156
+ await this.#store.episodic.putWithEmbedding(episode, {
157
+ embedding: { embedderId, vector },
158
+ });
159
+ } else {
160
+ await this.#store.episodic.put(episode);
161
+ }
162
+ } else {
163
+ await this.#store.episodic.put(episode);
164
+ }
165
+ span.setAttributes({
166
+ 'memory.episodic.summary_length': input.summary.length,
167
+ ...(input.importance !== undefined
168
+ ? { 'memory.episodic.importance': input.importance }
169
+ : {}),
170
+ });
171
+ return episode;
172
+ });
173
+ }
174
+
175
+ /** Lookup a single episode by id. */
176
+ async get(id: string): Promise<Episode | null> {
177
+ return this.#store.episodic.get(id);
178
+ }
179
+
180
+ /**
181
+ * Triple-signal episode retrieval (`recency × relevance ×
182
+ * importance`). The vector signal is computed on demand when an
183
+ * embedder is configured AND the storage adapter exposes
184
+ * `searchVector`; otherwise the FTS5 BM25 score is fed into the
185
+ * relevance term as a normalized fallback.
186
+ */
187
+ async search(
188
+ scope: SessionScope,
189
+ query: string,
190
+ opts: EpisodeSearchOptions = {},
191
+ ): Promise<ReadonlyArray<MemoryHit<Episode>>> {
192
+ return withMemorySpan(
193
+ this.#tracer,
194
+ 'memory.search.episodic',
195
+ scope,
196
+ { 'memory.search.query_length': query.length },
197
+ async (span) => {
198
+ const topK = opts.topK ?? 10;
199
+ const weights = opts.weights ?? DEFAULT_WEIGHTS;
200
+ const ftsHits = await this.#store.episodic.search(scope, {
201
+ query,
202
+ topK: topK * 2,
203
+ ...(opts.asOf !== undefined ? { asOf: opts.asOf } : {}),
204
+ // MRET-4: forward the date filter - it was accepted by the
205
+ // recall_episodes tool and silently dropped right here.
206
+ ...(opts.dateRange !== undefined ? { dateRange: opts.dateRange } : {}),
207
+ ...(opts.signal !== undefined ? { signal: opts.signal } : {}),
208
+ ...(opts.includeQuarantined !== undefined
209
+ ? { includeQuarantined: opts.includeQuarantined }
210
+ : {}),
211
+ });
212
+ const vectorHits = await this.#tryVectorSearch(
213
+ scope,
214
+ query,
215
+ topK * 2,
216
+ opts.asOf,
217
+ opts.includeQuarantined,
218
+ );
219
+ // MRET-4: the vector leg's store signature is positional (no
220
+ // dateRange parameter) - apply the same overlap semantics to its
221
+ // hits so both legs agree before the merge.
222
+ const from = opts.dateRange?.from !== undefined ? Date.parse(opts.dateRange.from) : null;
223
+ const to = opts.dateRange?.to !== undefined ? Date.parse(opts.dateRange.to) : null;
224
+ const rangedVectorHits =
225
+ from !== null || to !== null
226
+ ? vectorHits.filter((h) => {
227
+ if (from !== null && Date.parse(h.record.endedAt) < from) return false;
228
+ if (to !== null && Date.parse(h.record.startedAt) > to) return false;
229
+ return true;
230
+ })
231
+ : vectorHits;
232
+ const merged = mergeRecency(ftsHits, rangedVectorHits, weights);
233
+ const finalHits = merged.slice(0, topK);
234
+ span.setAttributes({
235
+ 'memory.search.episodic.fts_count': ftsHits.length,
236
+ 'memory.search.episodic.vector_count': vectorHits.length,
237
+ 'memory.search.episodic.final_count': finalHits.length,
238
+ ...(opts.asOf !== undefined ? { 'memory.search.episodic.as_of': opts.asOf } : {}),
239
+ });
240
+ return finalHits;
241
+ },
242
+ );
243
+ }
244
+
245
+ /**
246
+ * Soft-archive an episode. Storage adapters that implement
247
+ * `EpisodicMemoryStoreExt.archive(...)` mark the row archived in
248
+ * place. Adapters that do not expose the extension surface a
249
+ * friendly `TypeError` so the operator can opt the storage layer
250
+ * in (or call `episodic.put(...)` with the archived state set
251
+ * manually).
252
+ */
253
+ async archive(scope: SessionScope, episodeId: string, reason?: string): Promise<void> {
254
+ await withMemorySpan(
255
+ this.#tracer,
256
+ 'memory.write.episodic',
257
+ scope,
258
+ { 'memory.episodic.action': 'archive', 'memory.episodic.episode_id': episodeId },
259
+ async () => {
260
+ if (typeof this.#store.episodic.archive !== 'function') {
261
+ throw new TypeError(
262
+ '[graphorin/memory] EpisodicMemory.archive(...) requires a storage adapter that implements `episodic.archive(id)`. ' +
263
+ 'The default `@graphorin/store-sqlite` adapter implements it; custom adapters can opt in via EpisodicMemoryStoreExt.',
264
+ );
265
+ }
266
+ await this.#store.episodic.archive(episodeId, reason, scope);
267
+ },
268
+ );
269
+ }
270
+
271
+ /**
272
+ * Most-recent episodes by end time (newest first), with no embedding / FTS
273
+ * query (MCON-1). Requires `EpisodicMemoryStoreExt.listRecent` - the default
274
+ * `@graphorin/store-sqlite` adapter implements it. Optionally includes
275
+ * quarantined episodes (the importance source for the reflection gate).
276
+ */
277
+ async listRecent(
278
+ scope: SessionScope,
279
+ limit: number,
280
+ opts: { includeQuarantined?: boolean } = {},
281
+ ): Promise<ReadonlyArray<Episode>> {
282
+ return withMemorySpan(this.#tracer, 'memory.read.episodic', scope, {}, async (span) => {
283
+ const store = this.#store.episodic;
284
+ if (typeof store.listRecent !== 'function') {
285
+ throw new Error(
286
+ '[graphorin/memory] EpisodicMemory.listRecent requires EpisodicMemoryStoreExt.listRecent. ' +
287
+ 'The default `@graphorin/store-sqlite` adapter implements it; custom adapters can opt in.',
288
+ );
289
+ }
290
+ const out = await store.listRecent(scope, limit, opts);
291
+ span.setAttributes({ 'memory.read.episodic.count': out.length });
292
+ return out;
293
+ });
294
+ }
295
+
296
+ /** List the most recent episodes (no embedding required). */
297
+ async recent(scope: SessionScope, opts: { topK?: number } = {}): Promise<ReadonlyArray<Episode>> {
298
+ return this.listRecent(scope, opts.topK ?? 10, {});
299
+ }
300
+
301
+ /**
302
+ * Promote a quarantined episode into default recall (MCON-2). Mirrors
303
+ * {@link SemanticMemory.validate}: re-derives the injection verdict from the
304
+ * stored summary and **refuses** promotion of an injection-flagged episode
305
+ * (`QuarantinePromotionRefusedError`) unless an operator passes
306
+ * `{ force: true }` from a trusted, non-agent context.
307
+ */
308
+ async validate(
309
+ scope: SessionScope,
310
+ episodeId: string,
311
+ reason?: string,
312
+ options?: { readonly force?: boolean },
313
+ ): Promise<void> {
314
+ await withMemorySpan(
315
+ this.#tracer,
316
+ 'memory.write.episodic',
317
+ scope,
318
+ { 'memory.episodic.action': 'validate', 'memory.episodic.episode_id': episodeId },
319
+ async (span) => {
320
+ const store = this.#store.episodic;
321
+ if (typeof store.setStatus !== 'function') {
322
+ throw new TypeError(
323
+ '[graphorin/memory] EpisodicMemory.validate(...) requires a storage adapter that implements `episodic.setStatus(id, status)`. ' +
324
+ 'The default `@graphorin/store-sqlite` adapter implements it; custom adapters can opt in via EpisodicMemoryStoreExt.',
325
+ );
326
+ }
327
+ const force = options?.force === true;
328
+ const existing = await store.get(episodeId);
329
+ if (existing !== null && !force) {
330
+ const injection = detectMemoryInjection(existing.summary);
331
+ if (injection.flagged) {
332
+ span.setAttributes({ 'memory.episodic.validate.refused': true });
333
+ throw new QuarantinePromotionRefusedError(episodeId, injection.markers);
334
+ }
335
+ }
336
+ span.setAttributes({ 'memory.episodic.validate.forced': force });
337
+ await store.setStatus(episodeId, 'active', reason, scope);
338
+ },
339
+ );
340
+ }
341
+
342
+ async #tryVectorSearch(
343
+ scope: SessionScope,
344
+ query: string,
345
+ topK: number,
346
+ asOf?: string,
347
+ includeQuarantined?: boolean,
348
+ ): Promise<ReadonlyArray<MemoryHit<Episode>>> {
349
+ const embedderId = this.#embedderIdProvider();
350
+ if (
351
+ this.#embedder === null ||
352
+ embedderId === null ||
353
+ typeof this.#store.episodic.searchVector !== 'function'
354
+ ) {
355
+ return [];
356
+ }
357
+ // PS-10: a search query is the `query` role for E5.
358
+ const [vector] = await this.#embedder.embed([query], { taskType: 'query' });
359
+ if (vector === undefined) return [];
360
+ return this.#store.episodic.searchVector(
361
+ scope,
362
+ vector,
363
+ embedderId,
364
+ topK,
365
+ asOf,
366
+ includeQuarantined,
367
+ );
368
+ }
369
+ }
370
+
371
+ function mergeRecency(
372
+ ftsHits: ReadonlyArray<MemoryHit<Episode>>,
373
+ vectorHits: ReadonlyArray<MemoryHit<Episode>>,
374
+ weights: EpisodeRetrievalWeights,
375
+ ): ReadonlyArray<MemoryHit<Episode>> {
376
+ const now = Date.now();
377
+ const aggregates = new Map<
378
+ string,
379
+ {
380
+ record: Episode;
381
+ relevance: number;
382
+ recency: number;
383
+ importance: number;
384
+ score: number;
385
+ signals: Record<string, number>;
386
+ }
387
+ >();
388
+ for (const hit of ftsHits) {
389
+ const id = hit.record.id;
390
+ const recency = computeRecency(now, hit.record);
391
+ const importance = hit.record.importance ?? 0;
392
+ const relevance = normalizeRelevance(hit.score);
393
+ const score =
394
+ weights.recency * recency + weights.relevance * relevance + weights.importance * importance;
395
+ aggregates.set(id, {
396
+ record: hit.record,
397
+ relevance,
398
+ recency,
399
+ importance,
400
+ score,
401
+ signals: { fts: relevance, recency, importance },
402
+ });
403
+ }
404
+ for (const hit of vectorHits) {
405
+ const id = hit.record.id;
406
+ const cosine = clamp01(hit.score);
407
+ const existing = aggregates.get(id);
408
+ if (existing === undefined) {
409
+ const recency = computeRecency(now, hit.record);
410
+ const importance = hit.record.importance ?? 0;
411
+ const score =
412
+ weights.recency * recency + weights.relevance * cosine + weights.importance * importance;
413
+ aggregates.set(id, {
414
+ record: hit.record,
415
+ relevance: cosine,
416
+ recency,
417
+ importance,
418
+ score,
419
+ signals: { vector: cosine, recency, importance },
420
+ });
421
+ } else {
422
+ const blended = Math.max(existing.relevance, cosine);
423
+ existing.relevance = blended;
424
+ existing.score =
425
+ weights.recency * existing.recency +
426
+ weights.relevance * blended +
427
+ weights.importance * existing.importance;
428
+ existing.signals.vector = cosine;
429
+ existing.signals['rrf-blend'] = blended;
430
+ }
431
+ }
432
+ const out = Array.from(aggregates.values()).map((a) => ({
433
+ record: a.record,
434
+ score: a.score,
435
+ signals: Object.freeze({ ...a.signals }),
436
+ }));
437
+ out.sort((a, b) => b.score - a.score);
438
+ return out;
439
+ }
440
+
441
+ function computeRecency(now: number, episode: Episode): number {
442
+ const ended = Date.parse(episode.endedAt);
443
+ if (!Number.isFinite(ended)) return 0;
444
+ const elapsedDays = Math.max(0, (now - ended) / (24 * 60 * 60 * 1000));
445
+ return Math.exp(-elapsedDays / 30);
446
+ }
447
+
448
+ function normalizeRelevance(score: number): number {
449
+ // MRET-5/MST-7: the store returns `score = -bm25(...)` - POSITIVE for
450
+ // every FTS match (SQLite bm25 is always negative), so the old
451
+ // `1 / (1 + max(0, -score))` collapsed every lexical hit to exactly
452
+ // 1.0 and the DEC-105 triple-signal ranking degraded to
453
+ // recency + importance. Saturating ratio: 0 at no signal, → 1 as the
454
+ // match strengthens, graduated in between (k = 1 half-point).
455
+ return clamp01(Math.max(0, score) / (Math.max(0, score) + 1));
456
+ }
457
+
458
+ function clamp01(value: number): number {
459
+ if (Number.isNaN(value)) return 0;
460
+ if (value < 0) return 0;
461
+ if (value > 1) return 1;
462
+ return value;
463
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Six tier sub-modules for `@graphorin/memory`. Imported by the
3
+ * `createMemory()` facade; consumers can also instantiate a tier
4
+ * directly when wiring the package against a custom `MemoryStore`.
5
+ *
6
+ * @packageDocumentation
7
+ */
8
+
9
+ export {
10
+ type EpisodeInput,
11
+ type EpisodeRetrievalWeights,
12
+ type EpisodeSearchOptions,
13
+ EpisodicMemory,
14
+ } from './episodic-memory.js';
15
+ export {
16
+ capInsightsBelowFacts,
17
+ type InsightListOptions,
18
+ InsightMemory,
19
+ type InsightSearchOptions,
20
+ } from './insight-memory.js';
21
+ export {
22
+ type InduceOptions,
23
+ ProceduralMemory,
24
+ type RuleActivationContext,
25
+ type RuleInput,
26
+ } from './procedural-memory.js';
27
+ export {
28
+ type FactInput,
29
+ type FactRememberOptions,
30
+ type FactSearchOptions,
31
+ type FusionStrategy,
32
+ type FusionWeights,
33
+ type IterativeRecallResult,
34
+ type IterativeSearchOptions,
35
+ type RememberOutcome,
36
+ SemanticMemory,
37
+ type SemanticSearchDefaults,
38
+ } from './semantic-memory.js';
39
+ export {
40
+ type SessionCompactionPolicy,
41
+ type SessionCompactionResult,
42
+ SessionMemory,
43
+ } from './session-memory.js';
44
+ export { SharedMemory } from './shared-memory.js';
45
+ export {
46
+ type BlockDefinition,
47
+ type BlockSpec,
48
+ defineBlock,
49
+ WorkingMemory,
50
+ } from './working-memory.js';