@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,173 @@
1
+ /**
2
+ * Contextual retrieval (P1-3). Before a memory is embedded / FTS-indexed
3
+ * the framework can prepend a short *situating context* (entities,
4
+ * timeframe, topics) so a terse fact like "moved there in March" stays
5
+ * findable by "Where did Anna relocate?". This mirrors Anthropic's
6
+ * Contextual Retrieval, applied to both the vector embedding *and* the
7
+ * lexical index, while the canonical `text` shown to the user / audit
8
+ * trail is left untouched.
9
+ *
10
+ * Three modes:
11
+ * - `'off'` - index the bare canonical text (pre-P1-3 behaviour).
12
+ * - `'late-chunk'` - the **offline default**: prepend a context derived
13
+ * deterministically from the memory's own structured metadata. No LLM
14
+ * call, so the local-first write path is unchanged in cost.
15
+ * - `'llm'` - an opt-in, **consolidator-only** enrichment: one cheap
16
+ * provider call writes a 1-2 sentence situating prefix. Confined to the
17
+ * background consolidator (never the hot write path); a failure or empty
18
+ * completion degrades to {@link contextualize} so a write never wedges.
19
+ *
20
+ * @packageDocumentation
21
+ */
22
+
23
+ import type { Provider, ProviderRequest, Usage } from '@graphorin/core';
24
+
25
+ /**
26
+ * Contextual-retrieval mode. `'late-chunk'` is the offline default;
27
+ * `'llm'` is the opt-in, consolidator-only enrichment. P1-3.
28
+ *
29
+ * @stable
30
+ */
31
+ export type ContextualRetrievalMode = 'off' | 'late-chunk' | 'llm';
32
+
33
+ /**
34
+ * Structural subset of a fact / candidate consumed by the
35
+ * contextualizers. Only **author / extraction-supplied** signals are
36
+ * used - never framework-defaulted fields (e.g. a `validFrom` defaulted
37
+ * to write-time) and never opaque scope ids (random uuids would only
38
+ * add noise to the embedding). A candidate with no structured signals
39
+ * yields an empty context, so plain `remember({ text })` writes are
40
+ * left byte-identical.
41
+ *
42
+ * @internal
43
+ */
44
+ export interface ContextualizeInput {
45
+ readonly text: string;
46
+ readonly subject?: string;
47
+ readonly predicate?: string;
48
+ readonly object?: string;
49
+ readonly tags?: ReadonlyArray<string>;
50
+ /** Author-set validity start (ISO-8601). Framework-defaulted values are *not* passed. */
51
+ readonly validFrom?: string;
52
+ }
53
+
54
+ const SITUATING_CONTEXT_SYSTEM_PROMPT = [
55
+ 'You are a situating-context assistant for a long-running memory store.',
56
+ 'Given one memory and any structured hints, write a single short sentence (≤ 25 words) that situates it - the entities, timeframe, and topic it concerns - so it can be retrieved later by a vaguely-worded question.',
57
+ 'Do not restate the memory verbatim and do not add facts that are not implied. Return only the sentence, no preamble.',
58
+ ].join(' ');
59
+
60
+ /**
61
+ * Build the deterministic situating-context prefix from a candidate's
62
+ * structured signals. Returns `''` when nothing useful is available so
63
+ * the caller can short-circuit to the canonical text.
64
+ *
65
+ * @internal
66
+ */
67
+ export function buildSituatingContext(input: ContextualizeInput): string {
68
+ const parts: string[] = [];
69
+ const subject = trimToUndefined(input.subject);
70
+ const predicate = trimToUndefined(input.predicate);
71
+ const object = trimToUndefined(input.object);
72
+ if (subject !== undefined && predicate !== undefined && object !== undefined) {
73
+ parts.push(`${subject} ${predicate} ${object}`);
74
+ } else {
75
+ if (subject !== undefined) parts.push(subject);
76
+ if (object !== undefined) parts.push(object);
77
+ }
78
+ const validFrom = trimToUndefined(input.validFrom);
79
+ if (validFrom !== undefined) {
80
+ parts.push(`as of ${validFrom.length >= 10 ? validFrom.slice(0, 10) : validFrom}`);
81
+ }
82
+ const tags = (input.tags ?? []).map((t) => t.trim()).filter((t) => t.length > 0);
83
+ if (tags.length > 0) {
84
+ parts.push(`topics: ${tags.join(', ')}`);
85
+ }
86
+ if (parts.length === 0) return '';
87
+ return `[Context: ${parts.join('; ')}]`;
88
+ }
89
+
90
+ /**
91
+ * Late-chunking contextualizer (offline default). Prepends the
92
+ * deterministic situating context to the canonical text; a no-op when
93
+ * the candidate carries no structured signals. Pure - never calls a
94
+ * provider.
95
+ *
96
+ * @internal
97
+ */
98
+ export function contextualize(input: ContextualizeInput): string {
99
+ const ctx = buildSituatingContext(input);
100
+ return ctx === '' ? input.text : `${ctx}\n${input.text}`;
101
+ }
102
+
103
+ const ZERO_USAGE: Usage = Object.freeze({
104
+ promptTokens: 0,
105
+ completionTokens: 0,
106
+ totalTokens: 0,
107
+ });
108
+
109
+ /** Result of the LLM-context enrichment - the index text plus call usage for budgeting. */
110
+ export interface ContextualizeLlmResult {
111
+ readonly indexText: string;
112
+ readonly usage: Usage;
113
+ }
114
+
115
+ /**
116
+ * LLM-context contextualizer (opt-in, consolidator-only). Spends one
117
+ * cheap provider call to write a situating prefix, then prepends it to
118
+ * the canonical text. Resilient by design: an empty completion or a
119
+ * provider error degrades to {@link contextualize} (deterministic
120
+ * late-chunk) so the enrichment can never wedge a write. Returns the
121
+ * usage so the caller can record it against the budget.
122
+ *
123
+ * @internal
124
+ */
125
+ export async function contextualizeWithLlm(
126
+ input: ContextualizeInput,
127
+ provider: Provider,
128
+ opts: { readonly signal?: AbortSignal } = {},
129
+ ): Promise<ContextualizeLlmResult> {
130
+ const request: ProviderRequest = {
131
+ messages: [{ role: 'user', content: buildSituatingUserPrompt(input) }],
132
+ systemMessage: SITUATING_CONTEXT_SYSTEM_PROMPT,
133
+ temperature: 0,
134
+ // MCON-14: per-call output cap - a one-sentence situating prefix.
135
+ maxTokens: 256,
136
+ ...(opts.signal !== undefined ? { signal: opts.signal } : {}),
137
+ };
138
+ try {
139
+ const response = await provider.generate(request);
140
+ const prefix = (response.text ?? '').trim();
141
+ if (prefix.length === 0) {
142
+ return { indexText: contextualize(input), usage: response.usage };
143
+ }
144
+ return { indexText: `${prefix}\n${input.text}`, usage: response.usage };
145
+ } catch {
146
+ // Contextualization is an enhancement, not a correctness requirement -
147
+ // a provider failure must never wedge the underlying write. Fall back
148
+ // to the deterministic late-chunk prefix at zero cost.
149
+ return { indexText: contextualize(input), usage: ZERO_USAGE };
150
+ }
151
+ }
152
+
153
+ function buildSituatingUserPrompt(input: ContextualizeInput): string {
154
+ const hints: string[] = [];
155
+ const subject = trimToUndefined(input.subject);
156
+ const predicate = trimToUndefined(input.predicate);
157
+ const object = trimToUndefined(input.object);
158
+ if (subject !== undefined) hints.push(`subject=${subject}`);
159
+ if (predicate !== undefined) hints.push(`predicate=${predicate}`);
160
+ if (object !== undefined) hints.push(`object=${object}`);
161
+ const tags = (input.tags ?? []).map((t) => t.trim()).filter((t) => t.length > 0);
162
+ if (tags.length > 0) hints.push(`tags=${tags.join(', ')}`);
163
+ const validFrom = trimToUndefined(input.validFrom);
164
+ if (validFrom !== undefined) hints.push(`validFrom=${validFrom}`);
165
+ const hintLine = hints.length > 0 ? `\nStructured hints: ${hints.join('; ')}` : '';
166
+ return `Memory: ${input.text}${hintLine}`;
167
+ }
168
+
169
+ function trimToUndefined(value: string | undefined): string | undefined {
170
+ if (value === undefined) return undefined;
171
+ const trimmed = value.trim();
172
+ return trimmed.length === 0 ? undefined : trimmed;
173
+ }
@@ -0,0 +1,53 @@
1
+ import type { EmbedderProvider } from '@graphorin/core';
2
+ import type { EmbeddingMetaRegistryLike } from './storage-adapter.js';
3
+
4
+ /**
5
+ * Infer the canonical adapter family from the embedder id. The
6
+ * canonical id format is `'<adapter>:<model>@<dim>'`; we slice the
7
+ * adapter name off the front so the registry can index by family
8
+ * without needing the embedder to expose an extra property.
9
+ *
10
+ * @internal
11
+ */
12
+ export function inferAdapterKind(id: string): string {
13
+ const colonIndex = id.indexOf(':');
14
+ if (colonIndex < 0) return 'custom';
15
+ return id.slice(0, colonIndex);
16
+ }
17
+
18
+ /**
19
+ * Infer the model id portion of a canonical embedder id.
20
+ *
21
+ * @internal
22
+ */
23
+ export function inferModelName(id: string): string {
24
+ const colonIndex = id.indexOf(':');
25
+ const atIndex = id.lastIndexOf('@');
26
+ if (colonIndex < 0) return id;
27
+ if (atIndex < 0 || atIndex < colonIndex) return id.slice(colonIndex + 1);
28
+ return id.slice(colonIndex + 1, atIndex);
29
+ }
30
+
31
+ /**
32
+ * Register the supplied embedder with the storage layer's
33
+ * `embedding_meta` registry. Returns the canonical id surfaced to the
34
+ * memory writers. Idempotent - a registry that already knows the id
35
+ * with a matching `configHash` returns the existing row.
36
+ *
37
+ * @internal
38
+ */
39
+ export function bindEmbedder(
40
+ embedder: EmbedderProvider,
41
+ registry: EmbeddingMetaRegistryLike,
42
+ ): string {
43
+ const id = embedder.id();
44
+ registry.registerOrReturn({
45
+ id,
46
+ embedderKind: inferAdapterKind(id),
47
+ model: inferModelName(id),
48
+ dim: embedder.dim(),
49
+ configHash: embedder.configHash(),
50
+ distanceMetric: 'cosine',
51
+ });
52
+ return id;
53
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Untrusted-content envelope helpers shared across the memory package.
3
+ *
4
+ * Extracted from the CE-15 compaction-summary neutralization
5
+ * (`wrapSummaryAsDerived` in `context-engine/compaction/compactor.ts`)
6
+ * so every prompt that interpolates untrusted memory text (compaction
7
+ * summaries, reconcile neighbours, deep-phase judge candidates,
8
+ * reflection evidence) delimits it with ONE substitution scheme. The
9
+ * scheme is the same visible bracket-substitution `@graphorin/tools`
10
+ * applies in `wrapEnvelope` (W-030): the closing marker
11
+ * `<<</untrusted_content>>>` becomes `[[/untrusted_content]]` and the
12
+ * opening prefix `<<<untrusted_content` becomes `[[untrusted_content`,
13
+ * tolerant to case and whitespace. Zero-width insertion is not used -
14
+ * models read straight through zero-width splits (the reason
15
+ * `normalizeForMatching` strips them before injection scans).
16
+ *
17
+ * The helper is deliberately local: `@graphorin/memory` does not
18
+ * depend on `@graphorin/tools`, and the substitution scheme is a
19
+ * cross-package SPEC pinned by tests on literal-marker inputs.
20
+ *
21
+ * @packageDocumentation
22
+ */
23
+
24
+ /** Opening delimiter prefix of the untrusted-content envelope. */
25
+ export const UNTRUSTED_CONTENT_OPEN_PREFIX = '<<<untrusted_content';
26
+
27
+ /** Closing delimiter of the untrusted-content envelope. */
28
+ export const UNTRUSTED_CONTENT_CLOSE = '<<</untrusted_content>>>';
29
+
30
+ // Tolerant embedded-marker matching, mirroring the tools package: the
31
+ // closing form optionally consumes its `>>>` terminator so the full
32
+ // marker collapses to `[[/untrusted_content]]`.
33
+ const EMBEDDED_CLOSING_RE = /<<<\s*\/\s*untrusted_content(\s*>>>)?/gi;
34
+ const EMBEDDED_OPENING_RE = /<<<\s*untrusted_content/gi;
35
+
36
+ /**
37
+ * Replace envelope markers embedded in `body` with the visible `[[` /
38
+ * `]]` bracket-substitution so the body cannot prematurely close (or
39
+ * spoof a nested opening of) the envelope it is being wrapped into.
40
+ * Marker-free bodies are returned bytes-equal.
41
+ *
42
+ * @internal
43
+ */
44
+ export function neutralizeEnvelopeMarkers(body: string): string {
45
+ if (!body.includes('<<<')) return body;
46
+ return body
47
+ .replace(EMBEDDED_CLOSING_RE, (_match, terminator) =>
48
+ terminator === undefined ? '[[/untrusted_content' : '[[/untrusted_content]]',
49
+ )
50
+ .replace(EMBEDDED_OPENING_RE, '[[untrusted_content');
51
+ }
52
+
53
+ /**
54
+ * Wrap `body` in the untrusted-content envelope with the supplied
55
+ * attributes, neutralizing embedded markers first. Attribute insertion
56
+ * order is preserved, so
57
+ * `wrapUntrusted(body, { trust: 'derived', tool: 'compaction-summarizer' })`
58
+ * reproduces the historical CE-15 output byte-for-byte.
59
+ *
60
+ * @internal
61
+ */
62
+ export function wrapUntrusted(body: string, attrs?: Readonly<Record<string, string>>): string {
63
+ const attrStr =
64
+ attrs === undefined
65
+ ? ''
66
+ : Object.entries(attrs)
67
+ .map(([key, value]) => ` ${key}="${value.replace(/"/g, '&quot;')}"`)
68
+ .join('');
69
+ return `${UNTRUSTED_CONTENT_OPEN_PREFIX}${attrStr}>>>\n${neutralizeEnvelopeMarkers(body)}\n${UNTRUSTED_CONTENT_CLOSE}`;
70
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Stable id generator. Combines `Date.now()` (millisecond resolution),
3
+ * a per-process monotonic counter (resists collision inside the same
4
+ * millisecond), and a 36-bit crypto-quality random suffix (ties the id
5
+ * to the host so different runs do not collide either).
6
+ *
7
+ * The format is intentionally NOT a UUID - UUIDs are 16 bytes once
8
+ * decoded; these ids are 18 chars on average which is what the
9
+ * `audit.db` hash chain (Phase 03b) was sized for.
10
+ *
11
+ * @stable
12
+ */
13
+ import { randomBytes } from 'node:crypto';
14
+
15
+ let counter = 0;
16
+
17
+ export function newMemoryId(prefix: string): string {
18
+ counter = (counter + 1) & 0xffff;
19
+ const time = Date.now().toString(36);
20
+ const c = counter.toString(36).padStart(3, '0');
21
+ const rand = randomBytes(4).toString('hex');
22
+ return `${prefix}_${time}${c}${rand}`;
23
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Shared tolerant-parsing primitives for LLM text completions that are
3
+ * expected to carry JSON. Every consolidator phase and search transformer
4
+ * that parses a model response uses these two helpers; they used to be
5
+ * copy-pasted per module (7 copies of {@link stripFence}, 3 of
6
+ * {@link sliceJsonObject}) until the CodeQL ReDoS fix (06b7552) had to be
7
+ * applied to all of them at once - the canonical demonstration of why the
8
+ * single definition lives here now.
9
+ *
10
+ * Package-local (`internal/`): not part of the public `@graphorin/memory`
11
+ * API surface.
12
+ *
13
+ * @packageDocumentation
14
+ */
15
+
16
+ /**
17
+ * Strip a single surrounding Markdown code fence, if present. The
18
+ * info-string is matched with `[^\n]*` (never across lines) so a hostile
19
+ * megabyte-long first line cannot trigger polynomial backtracking; an
20
+ * unterminated fence leaves the text untouched and the caller's JSON
21
+ * parsing fails soft instead.
22
+ */
23
+ export function stripFence(text: string): string {
24
+ const match = /^```[^\n]*\n([\s\S]*?)\n```/u.exec(text.trim());
25
+ return match?.[1] ?? text;
26
+ }
27
+
28
+ /**
29
+ * Best-effort recovery slice for completions that wrap a JSON object in
30
+ * prose: everything from the first `{` to the last `}`. Returns `null`
31
+ * when no plausible object span exists.
32
+ */
33
+ export function sliceJsonObject(text: string): string | null {
34
+ const start = text.indexOf('{');
35
+ const end = text.lastIndexOf('}');
36
+ if (start < 0 || end < start) return null;
37
+ return text.slice(start, end + 1);
38
+ }
@@ -0,0 +1,35 @@
1
+ import type { SessionScope, SpanAttributes, SpanType, Tracer } from '@graphorin/core';
2
+
3
+ /**
4
+ * Sanitize a `SessionScope` for span attributes - the raw user id is
5
+ * surfaced as-is (it is a stable opaque identifier per DEC-005), but
6
+ * session and agent ids are emitted only when defined to avoid
7
+ * polluting the trace with `null` strings.
8
+ *
9
+ * @internal
10
+ */
11
+ export function scopeAttrs(scope: SessionScope): SpanAttributes {
12
+ const attrs: Record<string, string> = {
13
+ 'memory.scope.user_id': scope.userId,
14
+ };
15
+ if (scope.sessionId !== undefined) attrs['memory.scope.session_id'] = scope.sessionId;
16
+ if (scope.agentId !== undefined) attrs['memory.scope.agent_id'] = scope.agentId;
17
+ return Object.freeze(attrs);
18
+ }
19
+
20
+ /**
21
+ * Run an async function inside a `memory.*` span with the standard
22
+ * scope attributes already populated. Adds the span result count and
23
+ * status automatically.
24
+ *
25
+ * @internal
26
+ */
27
+ export async function withMemorySpan<T extends SpanType, R>(
28
+ tracer: Tracer,
29
+ type: T,
30
+ scope: SessionScope,
31
+ extras: SpanAttributes,
32
+ fn: (span: import('@graphorin/core').AISpan<T>) => Promise<R>,
33
+ ): Promise<R> {
34
+ return tracer.span({ type, attrs: { ...scopeAttrs(scope), ...extras } }, fn);
35
+ }