@lapis-notes/ai 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 (266) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/LICENSE.md +8 -0
  3. package/README.md +20 -0
  4. package/dist/ai-plugin.d.ts +88 -0
  5. package/dist/ai-plugin.js +742 -0
  6. package/dist/bootstrap/build.d.ts +15 -0
  7. package/dist/bootstrap/build.js +28 -0
  8. package/dist/bootstrap/folder-instructions.d.ts +4 -0
  9. package/dist/bootstrap/folder-instructions.js +49 -0
  10. package/dist/bootstrap/format.d.ts +2 -0
  11. package/dist/bootstrap/format.js +46 -0
  12. package/dist/bootstrap/instructions.d.ts +1 -0
  13. package/dist/bootstrap/instructions.js +8 -0
  14. package/dist/bootstrap/types.d.ts +36 -0
  15. package/dist/bootstrap/types.js +3 -0
  16. package/dist/catalog/ai-catalog-panel.svelte +742 -0
  17. package/dist/catalog/ai-catalog-panel.svelte.d.ts +13 -0
  18. package/dist/catalog/ai-catalog-view-type.d.ts +1 -0
  19. package/dist/catalog/ai-catalog-view-type.js +1 -0
  20. package/dist/catalog/ai-catalog-view.d.ts +22 -0
  21. package/dist/catalog/ai-catalog-view.js +48 -0
  22. package/dist/catalog/filter.d.ts +3 -0
  23. package/dist/catalog/filter.js +24 -0
  24. package/dist/catalog/inventory.d.ts +21 -0
  25. package/dist/catalog/inventory.js +255 -0
  26. package/dist/catalog/tree.d.ts +12 -0
  27. package/dist/catalog/tree.js +60 -0
  28. package/dist/catalog/types.d.ts +44 -0
  29. package/dist/catalog/types.js +1 -0
  30. package/dist/chat/ai-approval-card.svelte +54 -0
  31. package/dist/chat/ai-approval-card.svelte.d.ts +9 -0
  32. package/dist/chat/ai-chat-panel.svelte +1288 -0
  33. package/dist/chat/ai-chat-panel.svelte.d.ts +50 -0
  34. package/dist/chat/ai-inventory-result.css +55 -0
  35. package/dist/chat/ai-inventory-result.svelte +63 -0
  36. package/dist/chat/ai-inventory-result.svelte.d.ts +5 -0
  37. package/dist/chat/ai-question-card.svelte +132 -0
  38. package/dist/chat/ai-question-card.svelte.d.ts +9 -0
  39. package/dist/chat/ai-view-bootstrap.d.ts +13 -0
  40. package/dist/chat/ai-view-bootstrap.js +86 -0
  41. package/dist/chat/ai-view-panel.svelte +89 -0
  42. package/dist/chat/ai-view-panel.svelte.d.ts +13 -0
  43. package/dist/chat/ai-view-type.d.ts +1 -0
  44. package/dist/chat/ai-view-type.js +1 -0
  45. package/dist/chat/ai-view.d.ts +60 -0
  46. package/dist/chat/ai-view.js +121 -0
  47. package/dist/chat/chat-controller.svelte.d.ts +82 -0
  48. package/dist/chat/chat-controller.svelte.js +1926 -0
  49. package/dist/chat/chat-items.d.ts +88 -0
  50. package/dist/chat/chat-items.js +9 -0
  51. package/dist/chat/chat-mentions.d.ts +15 -0
  52. package/dist/chat/chat-mentions.js +77 -0
  53. package/dist/chat/chat-result-views.d.ts +5 -0
  54. package/dist/chat/chat-result-views.js +15 -0
  55. package/dist/chat/chat-session.d.ts +28 -0
  56. package/dist/chat/chat-session.js +50 -0
  57. package/dist/chat/chat-status.d.ts +1 -0
  58. package/dist/chat/chat-status.js +3 -0
  59. package/dist/chat/chat-time.d.ts +18 -0
  60. package/dist/chat/chat-time.js +90 -0
  61. package/dist/chat/chat-tool-display.d.ts +17 -0
  62. package/dist/chat/chat-tool-display.js +213 -0
  63. package/dist/chat/chat-tool-identity.d.ts +4 -0
  64. package/dist/chat/chat-tool-identity.js +42 -0
  65. package/dist/chat/chat-trace.d.ts +6 -0
  66. package/dist/chat/chat-trace.js +256 -0
  67. package/dist/chat/live-runtime-unavailable.d.ts +1 -0
  68. package/dist/chat/live-runtime-unavailable.js +1 -0
  69. package/dist/commands/agent.d.ts +8 -0
  70. package/dist/commands/agent.js +38 -0
  71. package/dist/commands/catalog.d.ts +18 -0
  72. package/dist/commands/catalog.js +115 -0
  73. package/dist/commands/discovery.d.ts +36 -0
  74. package/dist/commands/discovery.js +151 -0
  75. package/dist/commands/groups.d.ts +16 -0
  76. package/dist/commands/groups.js +121 -0
  77. package/dist/commands/inspect.d.ts +18 -0
  78. package/dist/commands/inspect.js +29 -0
  79. package/dist/commands/markdown.d.ts +21 -0
  80. package/dist/commands/markdown.js +117 -0
  81. package/dist/commands/parser.d.ts +4 -0
  82. package/dist/commands/parser.js +24 -0
  83. package/dist/commands/reserved.d.ts +4 -0
  84. package/dist/commands/reserved.js +107 -0
  85. package/dist/commands/router.d.ts +17 -0
  86. package/dist/commands/router.js +170 -0
  87. package/dist/commands/seed.d.ts +4 -0
  88. package/dist/commands/seed.js +8 -0
  89. package/dist/commands/types.d.ts +54 -0
  90. package/dist/commands/types.js +1 -0
  91. package/dist/commands/user-agents-node.d.ts +3 -0
  92. package/dist/commands/user-agents-node.js +52 -0
  93. package/dist/commands/user-agents.d.ts +22 -0
  94. package/dist/commands/user-agents.js +40 -0
  95. package/dist/conversations/approval-grants.d.ts +20 -0
  96. package/dist/conversations/approval-grants.js +75 -0
  97. package/dist/conversations/binding-state.d.ts +6 -0
  98. package/dist/conversations/binding-state.js +35 -0
  99. package/dist/conversations/context-handoff.d.ts +25 -0
  100. package/dist/conversations/context-handoff.js +207 -0
  101. package/dist/conversations/conversation-index-coordinator.d.ts +18 -0
  102. package/dist/conversations/conversation-index-coordinator.js +81 -0
  103. package/dist/conversations/conversation-index.d.ts +16 -0
  104. package/dist/conversations/conversation-index.js +125 -0
  105. package/dist/conversations/conversation-locator.d.ts +2 -0
  106. package/dist/conversations/conversation-locator.js +20 -0
  107. package/dist/conversations/conversation-palette.d.ts +18 -0
  108. package/dist/conversations/conversation-palette.js +29 -0
  109. package/dist/conversations/conversation-repository.d.ts +39 -0
  110. package/dist/conversations/conversation-repository.js +213 -0
  111. package/dist/conversations/handoff-summary.d.ts +48 -0
  112. package/dist/conversations/handoff-summary.js +269 -0
  113. package/dist/conversations/hashes.d.ts +5 -0
  114. package/dist/conversations/hashes.js +40 -0
  115. package/dist/conversations/history-creation-scope.d.ts +12 -0
  116. package/dist/conversations/history-creation-scope.js +44 -0
  117. package/dist/conversations/jsonl.d.ts +10 -0
  118. package/dist/conversations/jsonl.js +41 -0
  119. package/dist/conversations/memory-transcript-store.d.ts +15 -0
  120. package/dist/conversations/memory-transcript-store.js +119 -0
  121. package/dist/conversations/paths.d.ts +14 -0
  122. package/dist/conversations/paths.js +92 -0
  123. package/dist/conversations/redaction.d.ts +13 -0
  124. package/dist/conversations/redaction.js +94 -0
  125. package/dist/conversations/reveal-scope.d.ts +3 -0
  126. package/dist/conversations/reveal-scope.js +8 -0
  127. package/dist/conversations/scope-resolver.d.ts +11 -0
  128. package/dist/conversations/scope-resolver.js +18 -0
  129. package/dist/conversations/scope-tree.d.ts +10 -0
  130. package/dist/conversations/scope-tree.js +71 -0
  131. package/dist/conversations/transcript-projection.d.ts +9 -0
  132. package/dist/conversations/transcript-projection.js +409 -0
  133. package/dist/conversations/transcript-store.d.ts +24 -0
  134. package/dist/conversations/transcript-store.js +27 -0
  135. package/dist/conversations/types.d.ts +224 -0
  136. package/dist/conversations/types.js +12 -0
  137. package/dist/conversations/validation.d.ts +4 -0
  138. package/dist/conversations/validation.js +383 -0
  139. package/dist/conversations/vault-transcript-store.d.ts +22 -0
  140. package/dist/conversations/vault-transcript-store.js +219 -0
  141. package/dist/core/event-queue.d.ts +6 -0
  142. package/dist/core/event-queue.js +39 -0
  143. package/dist/core/types.d.ts +241 -0
  144. package/dist/core/types.js +50 -0
  145. package/dist/history/ai-history-panel.svelte +937 -0
  146. package/dist/history/ai-history-panel.svelte.d.ts +17 -0
  147. package/dist/history/ai-history-view-type.d.ts +1 -0
  148. package/dist/history/ai-history-view-type.js +1 -0
  149. package/dist/history/ai-history-view.d.ts +26 -0
  150. package/dist/history/ai-history-view.js +51 -0
  151. package/dist/history/conversation-history-tree.d.ts +15 -0
  152. package/dist/history/conversation-history-tree.js +71 -0
  153. package/dist/host/agent-workspace.d.ts +6 -0
  154. package/dist/host/agent-workspace.js +16 -0
  155. package/dist/host/create-host-runtimes.d.ts +2 -0
  156. package/dist/host/create-host-runtimes.js +14 -0
  157. package/dist/host/desktop-process-host.d.ts +6 -0
  158. package/dist/host/desktop-process-host.js +74 -0
  159. package/dist/host/host-runtime-availability.d.ts +1 -0
  160. package/dist/host/host-runtime-availability.js +3 -0
  161. package/dist/host/process-host.d.ts +31 -0
  162. package/dist/host/process-host.js +6 -0
  163. package/dist/index.d.ts +69 -0
  164. package/dist/index.js +49 -0
  165. package/dist/jsonl/ai-jsonl-preview.d.ts +27 -0
  166. package/dist/jsonl/ai-jsonl-preview.js +68 -0
  167. package/dist/jsonl/ai-jsonl-view-type.d.ts +1 -0
  168. package/dist/jsonl/ai-jsonl-view-type.js +1 -0
  169. package/dist/jsonl/ai-jsonl-view.d.ts +16 -0
  170. package/dist/jsonl/ai-jsonl-view.js +57 -0
  171. package/dist/jsonl/ai-jsonl-view.svelte +260 -0
  172. package/dist/jsonl/ai-jsonl-view.svelte.d.ts +9 -0
  173. package/dist/memory/candidate-extractor.d.ts +25 -0
  174. package/dist/memory/candidate-extractor.js +191 -0
  175. package/dist/memory/forget-confirmation.d.ts +3 -0
  176. package/dist/memory/forget-confirmation.js +45 -0
  177. package/dist/memory/hashes.d.ts +5 -0
  178. package/dist/memory/hashes.js +37 -0
  179. package/dist/memory/memory-ingestion-coordinator.d.ts +19 -0
  180. package/dist/memory/memory-ingestion-coordinator.js +108 -0
  181. package/dist/memory/memory-record-store.d.ts +62 -0
  182. package/dist/memory/memory-record-store.js +369 -0
  183. package/dist/memory/memory-scheduler.d.ts +21 -0
  184. package/dist/memory/memory-scheduler.js +143 -0
  185. package/dist/memory/memory-tools.d.ts +61 -0
  186. package/dist/memory/memory-tools.js +99 -0
  187. package/dist/memory/native-memory-service.d.ts +72 -0
  188. package/dist/memory/native-memory-service.js +1256 -0
  189. package/dist/memory/paths.d.ts +13 -0
  190. package/dist/memory/paths.js +60 -0
  191. package/dist/memory/runtime-consolidation-provider.d.ts +18 -0
  192. package/dist/memory/runtime-consolidation-provider.js +154 -0
  193. package/dist/memory/types.d.ts +139 -0
  194. package/dist/memory/types.js +1 -0
  195. package/dist/providers/acp-model-provider.d.ts +11 -0
  196. package/dist/providers/acp-model-provider.js +99 -0
  197. package/dist/providers/codex-model-catalog.d.ts +7 -0
  198. package/dist/providers/codex-model-catalog.js +179 -0
  199. package/dist/providers/codex-model-provider.d.ts +12 -0
  200. package/dist/providers/codex-model-provider.js +42 -0
  201. package/dist/providers/model-provider.d.ts +26 -0
  202. package/dist/providers/model-provider.js +26 -0
  203. package/dist/public-components.d.ts +3 -0
  204. package/dist/public-components.js +3 -0
  205. package/dist/registry/runtime-registry.d.ts +11 -0
  206. package/dist/registry/runtime-registry.js +71 -0
  207. package/dist/registry/select-runtime.d.ts +9 -0
  208. package/dist/registry/select-runtime.js +31 -0
  209. package/dist/runtime-adapters.d.ts +16 -0
  210. package/dist/runtime-adapters.js +9 -0
  211. package/dist/runtimes/acp/acp-event-mapper.d.ts +56 -0
  212. package/dist/runtimes/acp/acp-event-mapper.js +237 -0
  213. package/dist/runtimes/acp/acp-runtime.d.ts +46 -0
  214. package/dist/runtimes/acp/acp-runtime.js +165 -0
  215. package/dist/runtimes/acp/desktop-acp-backend.d.ts +16 -0
  216. package/dist/runtimes/acp/desktop-acp-backend.js +243 -0
  217. package/dist/runtimes/codex/app-server-protocol.d.ts +28 -0
  218. package/dist/runtimes/codex/app-server-protocol.js +274 -0
  219. package/dist/runtimes/codex/codex-runtime.d.ts +24 -0
  220. package/dist/runtimes/codex/codex-runtime.js +318 -0
  221. package/dist/runtimes/fake/fake-runtime.d.ts +51 -0
  222. package/dist/runtimes/fake/fake-runtime.js +257 -0
  223. package/dist/sessions/plugin-data.d.ts +8 -0
  224. package/dist/sessions/plugin-data.js +29 -0
  225. package/dist/sessions/session-store.d.ts +47 -0
  226. package/dist/sessions/session-store.js +117 -0
  227. package/dist/settings/acp-agents.d.ts +8 -0
  228. package/dist/settings/acp-agents.js +11 -0
  229. package/dist/settings/ai-settings-tab.d.ts +8 -0
  230. package/dist/settings/ai-settings-tab.js +249 -0
  231. package/dist/settings/ai-settings.d.ts +43 -0
  232. package/dist/settings/ai-settings.js +147 -0
  233. package/dist/settings/app-tool-setting-rows.d.ts +16 -0
  234. package/dist/settings/app-tool-setting-rows.js +30 -0
  235. package/dist/settings/register-ai-settings.d.ts +3 -0
  236. package/dist/settings/register-ai-settings.js +393 -0
  237. package/dist/skills/activation.d.ts +2 -0
  238. package/dist/skills/activation.js +20 -0
  239. package/dist/skills/bundled/lapis-notes.d.ts +3 -0
  240. package/dist/skills/bundled/lapis-notes.js +52 -0
  241. package/dist/skills/bundled/research.d.ts +6 -0
  242. package/dist/skills/bundled/research.js +29 -0
  243. package/dist/skills/discovery.d.ts +21 -0
  244. package/dist/skills/discovery.js +227 -0
  245. package/dist/skills/manifest.d.ts +3 -0
  246. package/dist/skills/manifest.js +39 -0
  247. package/dist/skills/parser.d.ts +12 -0
  248. package/dist/skills/parser.js +124 -0
  249. package/dist/skills/registry.d.ts +21 -0
  250. package/dist/skills/registry.js +89 -0
  251. package/dist/skills/seed.d.ts +5 -0
  252. package/dist/skills/seed.js +18 -0
  253. package/dist/skills/skill-tools.d.ts +8 -0
  254. package/dist/skills/skill-tools.js +113 -0
  255. package/dist/skills/types.d.ts +67 -0
  256. package/dist/skills/types.js +1 -0
  257. package/dist/styles.css +514 -0
  258. package/dist/tools/app-tool-host.d.ts +47 -0
  259. package/dist/tools/app-tool-host.js +419 -0
  260. package/dist/tools/desktop-app-tool-bridge.d.ts +23 -0
  261. package/dist/tools/desktop-app-tool-bridge.js +223 -0
  262. package/dist/tools/mcp-server-registry.d.ts +7 -0
  263. package/dist/tools/mcp-server-registry.js +30 -0
  264. package/manifest.json +34 -0
  265. package/package.json +83 -0
  266. package/styles.css +1 -0
@@ -0,0 +1,1256 @@
1
+ import { conversationLocationFromSourcePath } from "../conversations/conversation-index-coordinator";
2
+ import { conversationDirectory } from "../conversations/paths";
3
+ import { canonicalJson, deterministicMemoryId, sha256Text, transcriptEntryHash, } from "./hashes";
4
+ import { AI_MEMORY_CURATED_PROVIDER_ID, AI_MEMORY_EPISODIC_PROVIDER_ID, conversationMemoryScope, curatedMemoryPath, curatedScopePrefix, currentMemoryScopes, episodicMemoryPath, episodicScopePrefix, scopeProximity, } from "./paths";
5
+ import { InMemoryMemoryRecordStore, } from "./memory-record-store";
6
+ import { extractDeterministicCandidates, isSecretBearingMemoryText, } from "./candidate-extractor";
7
+ const MAX_INGEST_BATCH = 200;
8
+ const MAX_RESULTS_PER_SCOPE = 40;
9
+ const MAX_TOOL_RESULTS = 20;
10
+ const DEFAULT_TOOL_RESULTS = 8;
11
+ const MAX_SNIPPET_CHARS = 600;
12
+ const MMR_LAMBDA = 0.75;
13
+ export class NativeMemoryService {
14
+ repository;
15
+ database;
16
+ #now;
17
+ #yieldToApp;
18
+ #recordStore;
19
+ #consolidationProvider;
20
+ constructor(repository, database, options = {}) {
21
+ this.repository = repository;
22
+ this.database = database;
23
+ this.#now = options.now ?? (() => new Date());
24
+ this.#yieldToApp =
25
+ options.yieldToApp ??
26
+ (() => new Promise((resolve) => setTimeout(resolve, 0)));
27
+ this.#recordStore = options.recordStore ?? new InMemoryMemoryRecordStore();
28
+ this.#consolidationProvider = options.consolidationProvider;
29
+ }
30
+ async ingestConversation(location) {
31
+ if ((await this.#recordStore.listExcludedConversationIds()).has(location.conversationId)) {
32
+ await this.removeStaleConversationDocuments(location.conversationId);
33
+ await this.database.deleteMemoryCandidatesByConversation(location.conversationId);
34
+ await this.database.deleteMemorySourceState(location.conversationId);
35
+ return;
36
+ }
37
+ const duplicates = (await this.repository.listAll()).filter((entry) => entry.location.conversationId === location.conversationId);
38
+ if (duplicates.length !== 1) {
39
+ await this.database.upsertMemorySourceState({
40
+ sourceKey: location.conversationId,
41
+ sourcePath: transcriptPath(location),
42
+ sourceHash: "",
43
+ status: "inconsistent",
44
+ indexedAt: this.#now().getTime(),
45
+ errorCode: duplicates.length > 1 ? "duplicate-conversation-id" : "missing-source",
46
+ });
47
+ return;
48
+ }
49
+ const snapshot = await this.repository.read(location);
50
+ const episodes = await projectEpisodes(snapshot);
51
+ const duplicateEntry = firstDuplicateEntry(episodes);
52
+ if (duplicateEntry) {
53
+ await this.markInconsistent(location, episodes, "duplicate-entry-id");
54
+ return;
55
+ }
56
+ const previous = await this.database.getMemorySourceState(location.conversationId);
57
+ const previousLocation = previous
58
+ ? conversationLocationFromSourcePath(previous.sourcePath)
59
+ : null;
60
+ let startIndex = 0;
61
+ if (previous?.lastEntryId) {
62
+ const checkpointIndex = episodes.findIndex((episode) => episode.entry.id === previous.lastEntryId);
63
+ const checkpoint = episodes[checkpointIndex];
64
+ if (!checkpoint || checkpoint.entryHash !== previous.lastEntryHash) {
65
+ await this.markInconsistent(location, episodes, "entry-hash-mismatch");
66
+ return;
67
+ }
68
+ startIndex = checkpointIndex;
69
+ const indexedLocation = previousLocation ?? location;
70
+ for (const episode of episodes.slice(0, checkpointIndex + 1)) {
71
+ const document = await this.database.getSearchDocument(episodicMemoryPath(conversationMemoryScope(indexedLocation.scopeDir), indexedLocation.conversationId, episode.entry.id));
72
+ if (!document) {
73
+ startIndex = 0;
74
+ break;
75
+ }
76
+ const indexed = decodeEpisodicMetadata(document);
77
+ if (indexed?.entryHash !== episode.entryHash) {
78
+ await this.markInconsistent(location, episodes, "entry-hash-mismatch");
79
+ return;
80
+ }
81
+ }
82
+ }
83
+ const moved = Boolean(previousLocation &&
84
+ (previousLocation.scopeDir !== location.scopeDir ||
85
+ previousLocation.conversationId !== location.conversationId));
86
+ if (moved) {
87
+ await this.removeStaleConversationDocuments(location.conversationId);
88
+ startIndex = 0;
89
+ }
90
+ const sourceHash = await sha256Text(episodes.map((episode) => `${episode.entry.id}:${episode.entryHash}`).join("\n"));
91
+ if (episodes.length === 0) {
92
+ await this.database.upsertMemorySourceState({
93
+ sourceKey: location.conversationId,
94
+ sourcePath: transcriptPath(location),
95
+ sourceHash,
96
+ status: "ready",
97
+ indexedAt: this.#now().getTime(),
98
+ });
99
+ return;
100
+ }
101
+ await this.database.beginSearchIndexingBatch();
102
+ try {
103
+ for (let offset = startIndex; offset < episodes.length; offset += MAX_INGEST_BATCH) {
104
+ const batch = episodes.slice(offset, offset + MAX_INGEST_BATCH);
105
+ for (const episode of batch) {
106
+ const document = episodeSearchDocument(snapshot.location, episode);
107
+ const existing = await this.database.getSearchDocument(document.path);
108
+ if (existing?.checksum !== document.checksum) {
109
+ await this.database.upsertSearchDocument(document);
110
+ }
111
+ }
112
+ await extractDeterministicCandidates(snapshot, this.database, {
113
+ entries: batch.map((episode) => episode.entry),
114
+ });
115
+ const cursor = batch.at(-1);
116
+ await this.database.upsertMemorySourceState({
117
+ sourceKey: location.conversationId,
118
+ sourcePath: transcriptPath(location),
119
+ sourceHash,
120
+ lastEntryId: cursor.entry.id,
121
+ lastEntryHash: cursor.entryHash,
122
+ status: "ready",
123
+ indexedAt: this.#now().getTime(),
124
+ });
125
+ if (offset + MAX_INGEST_BATCH < episodes.length) {
126
+ await this.#yieldToApp();
127
+ }
128
+ }
129
+ }
130
+ finally {
131
+ await this.database.endSearchIndexingBatch();
132
+ }
133
+ }
134
+ async search(query, context, signal) {
135
+ const text = query.query.trim();
136
+ if (!text)
137
+ return [];
138
+ for (const [name, value] of [
139
+ ["since", query.since],
140
+ ["before", query.before],
141
+ ]) {
142
+ if (value !== undefined && !Number.isFinite(Date.parse(value))) {
143
+ throw new Error(`Memory ${name} must be a valid date or timestamp`);
144
+ }
145
+ }
146
+ if (query.since &&
147
+ query.before &&
148
+ Date.parse(query.since) >= Date.parse(query.before)) {
149
+ return [];
150
+ }
151
+ signal?.throwIfAborted();
152
+ const limit = Math.max(1, Math.min(query.limit ?? DEFAULT_TOOL_RESULTS, MAX_TOOL_RESULTS));
153
+ const corpus = query.corpus ?? "all";
154
+ const ranked = [];
155
+ const searchNow = Math.floor(this.#now().getTime() / 60_000) * 60_000;
156
+ if (corpus === "all" || corpus === "curated") {
157
+ for (const scope of currentMemoryScopes(context.scopeDir)) {
158
+ signal?.throwIfAborted();
159
+ const results = await this.database.searchDocuments(text, {
160
+ sourceProviderIds: [AI_MEMORY_CURATED_PROVIDER_ID],
161
+ pathPrefix: curatedScopePrefix(scope),
162
+ mode: "auto",
163
+ limit: MAX_RESULTS_PER_SCOPE,
164
+ snippetLength: MAX_SNIPPET_CHARS,
165
+ });
166
+ ranked.push(...decodeCuratedResults(results, context.scopeDir, query));
167
+ }
168
+ }
169
+ if (corpus === "all" || corpus === "episodic") {
170
+ const scopes = currentMemoryScopes(context.scopeDir).filter((scope) => scope.kind !== "user");
171
+ if (query.scope === "workspace") {
172
+ const sourceScopes = new Map();
173
+ for (const source of await this.database.listMemorySourceStates()) {
174
+ const location = conversationLocationFromSourcePath(source.sourcePath);
175
+ if (!location)
176
+ continue;
177
+ const sourceScope = conversationMemoryScope(location.scopeDir);
178
+ sourceScopes.set(JSON.stringify(sourceScope), sourceScope);
179
+ }
180
+ for (const scope of sourceScopes.values()) {
181
+ signal?.throwIfAborted();
182
+ const results = await this.database.searchDocuments(text, {
183
+ sourceProviderIds: [AI_MEMORY_EPISODIC_PROVIDER_ID],
184
+ pathPrefix: episodicScopePrefix(scope),
185
+ mode: "auto",
186
+ limit: MAX_RESULTS_PER_SCOPE,
187
+ snippetLength: MAX_SNIPPET_CHARS,
188
+ });
189
+ ranked.push(...decodeEpisodicResults(results, context.scopeDir, scopeProximity(scope, context.scopeDir), query, searchNow));
190
+ }
191
+ }
192
+ else {
193
+ for (const scope of scopes) {
194
+ signal?.throwIfAborted();
195
+ const results = await this.database.searchDocuments(text, {
196
+ sourceProviderIds: [AI_MEMORY_EPISODIC_PROVIDER_ID],
197
+ pathPrefix: episodicScopePrefix(scope),
198
+ mode: "auto",
199
+ limit: MAX_RESULTS_PER_SCOPE,
200
+ snippetLength: MAX_SNIPPET_CHARS,
201
+ });
202
+ ranked.push(...decodeEpisodicResults(results, context.scopeDir, scopeProximity(scope, context.scopeDir), query, searchNow));
203
+ }
204
+ }
205
+ }
206
+ const bestByPath = new Map();
207
+ for (const item of ranked) {
208
+ const current = bestByPath.get(item.path);
209
+ if (!current || item.result.score > current.result.score) {
210
+ bestByPath.set(item.path, item);
211
+ }
212
+ }
213
+ const candidates = [...bestByPath.values()].sort((left, right) => right.result.score - left.result.score);
214
+ const selected = selectMmr(candidates, limit);
215
+ return selected.map((item) => item.result);
216
+ }
217
+ async recall(query, context, signal) {
218
+ if (!query.trim() || signal?.aborted)
219
+ return [];
220
+ const deadline = new AbortController();
221
+ const timer = setTimeout(() => deadline.abort(), 150);
222
+ const abort = () => deadline.abort();
223
+ signal?.addEventListener("abort", abort, { once: true });
224
+ try {
225
+ const results = await raceWithAbort(this.search({
226
+ query: automaticRecallQuery(query),
227
+ corpus: "curated",
228
+ scope: "current",
229
+ limit: 8,
230
+ }, context, deadline.signal), deadline.signal);
231
+ const selected = [];
232
+ let estimatedTokens = 0;
233
+ for (const result of results) {
234
+ if (result.status !== "active" ||
235
+ result.effectiveTrust !== "owner" ||
236
+ result.corpus !== "curated") {
237
+ continue;
238
+ }
239
+ const tokens = Math.ceil(recallContent(result).length / 4);
240
+ if (estimatedTokens + tokens > 900)
241
+ continue;
242
+ selected.push(result);
243
+ estimatedTokens += tokens;
244
+ if (selected.length === 3)
245
+ break;
246
+ }
247
+ if (selected.length > 0) {
248
+ void this.recordRecallSignals(query, selected).catch(() => undefined);
249
+ }
250
+ return selected.flatMap((result) => {
251
+ const ref = parseCuratedRef(result.ref);
252
+ if (!ref)
253
+ return [];
254
+ return [
255
+ {
256
+ kind: "memory-recall",
257
+ id: result.ref,
258
+ content: recallContent(result),
259
+ metadata: {
260
+ memoryId: ref.id,
261
+ revision: ref.revision,
262
+ scope: result.scope.kind,
263
+ },
264
+ },
265
+ ];
266
+ });
267
+ }
268
+ catch {
269
+ return [];
270
+ }
271
+ finally {
272
+ clearTimeout(timer);
273
+ signal?.removeEventListener("abort", abort);
274
+ }
275
+ }
276
+ async get(ref, _context, signal) {
277
+ signal?.throwIfAborted();
278
+ const curated = parseCuratedRef(ref);
279
+ if (curated)
280
+ return this.getCurated(curated.id, curated.revision, signal);
281
+ const parsed = parseEpisodeRef(ref);
282
+ if (!parsed) {
283
+ return {
284
+ ref,
285
+ corpus: "episodic",
286
+ status: "missing",
287
+ evidenceMessages: [],
288
+ };
289
+ }
290
+ const matches = (await this.repository.listAll()).filter((entry) => entry.location.conversationId === parsed.conversationId);
291
+ if (matches.length > 1) {
292
+ return {
293
+ ref,
294
+ corpus: "episodic",
295
+ status: "ambiguous",
296
+ evidenceMessages: [],
297
+ };
298
+ }
299
+ const match = matches[0];
300
+ if (!match || match.unavailableReason) {
301
+ return {
302
+ ref,
303
+ corpus: "episodic",
304
+ status: "missing",
305
+ evidenceMessages: [],
306
+ };
307
+ }
308
+ const snapshot = await this.repository.read(match.location);
309
+ const episode = (await projectEpisodes(snapshot)).find((candidate) => candidate.entry.id === parsed.entryId);
310
+ if (!episode) {
311
+ return {
312
+ ref,
313
+ corpus: "episodic",
314
+ status: "missing",
315
+ currentLocation: snapshot.location,
316
+ evidenceMessages: [],
317
+ };
318
+ }
319
+ const currentPath = episodicMemoryPath(conversationMemoryScope(snapshot.location.scopeDir), snapshot.location.conversationId, episode.entry.id);
320
+ let document = await this.database.getSearchDocument(currentPath);
321
+ if (!document) {
322
+ const source = await this.database.getMemorySourceState(snapshot.location.conversationId);
323
+ const previousLocation = source
324
+ ? conversationLocationFromSourcePath(source.sourcePath)
325
+ : null;
326
+ if (previousLocation) {
327
+ document = await this.database.getSearchDocument(episodicMemoryPath(conversationMemoryScope(previousLocation.scopeDir), previousLocation.conversationId, episode.entry.id));
328
+ }
329
+ }
330
+ const indexed = document ? decodeEpisodicMetadata(document) : null;
331
+ const modified = Boolean(indexed && indexed.entryHash !== episode.entryHash);
332
+ const moved = Boolean(indexed && indexed.location.scopeDir !== snapshot.location.scopeDir);
333
+ const result = episodeResultFromDocument(document ?? episodeSearchDocument(snapshot.location, episode), 1);
334
+ return {
335
+ ref,
336
+ corpus: "episodic",
337
+ status: modified ? "modified" : moved ? "moved" : "available",
338
+ result: result?.result,
339
+ currentLocation: snapshot.location,
340
+ evidenceMessages: [episodeEvidenceMessage(episode, modified)],
341
+ };
342
+ }
343
+ async rebuild(scope) {
344
+ const listed = [...(await this.repository.listAll())].sort((left, right) => `${left.location.scopeDir}\u0000${left.location.conversationId}`.localeCompare(`${right.location.scopeDir}\u0000${right.location.conversationId}`));
345
+ const counts = new Map();
346
+ for (const entry of listed) {
347
+ counts.set(entry.location.conversationId, (counts.get(entry.location.conversationId) ?? 0) + 1);
348
+ }
349
+ const expected = new Set();
350
+ let conversations = 0;
351
+ let episodes = 0;
352
+ let skipped = 0;
353
+ let inconsistent = 0;
354
+ const exclusions = await this.#recordStore.listExcludedConversationIds();
355
+ for (const entry of listed) {
356
+ const entryScope = conversationMemoryScope(entry.location.scopeDir);
357
+ if (scope && !sameScope(scope, entryScope))
358
+ continue;
359
+ if (entry.unavailableReason) {
360
+ skipped += 1;
361
+ continue;
362
+ }
363
+ if (exclusions.has(entry.location.conversationId)) {
364
+ skipped += 1;
365
+ await this.removeStaleConversationDocuments(entry.location.conversationId);
366
+ await this.database.deleteMemoryCandidatesByConversation(entry.location.conversationId);
367
+ await this.database.deleteMemorySourceState(entry.location.conversationId);
368
+ continue;
369
+ }
370
+ if ((counts.get(entry.location.conversationId) ?? 0) > 1) {
371
+ inconsistent += 1;
372
+ await this.database.upsertMemorySourceState({
373
+ sourceKey: entry.location.conversationId,
374
+ sourcePath: transcriptPath(entry.location),
375
+ sourceHash: "",
376
+ status: "inconsistent",
377
+ indexedAt: this.#now().getTime(),
378
+ errorCode: "duplicate-conversation-id",
379
+ });
380
+ continue;
381
+ }
382
+ await this.ingestConversation(entry.location);
383
+ const snapshot = await this.repository.read(entry.location);
384
+ for (const episode of await projectEpisodes(snapshot)) {
385
+ expected.add(episodicMemoryPath(entryScope, entry.location.conversationId, episode.entry.id));
386
+ episodes += 1;
387
+ }
388
+ conversations += 1;
389
+ await this.#yieldToApp();
390
+ }
391
+ const listedConversationIds = new Set(listed.map((entry) => entry.location.conversationId));
392
+ for (const source of await this.database.listMemorySourceStates()) {
393
+ if (listedConversationIds.has(source.sourceKey))
394
+ continue;
395
+ const priorLocation = conversationLocationFromSourcePath(source.sourcePath);
396
+ if (scope &&
397
+ priorLocation &&
398
+ !sameScope(scope, conversationMemoryScope(priorLocation.scopeDir))) {
399
+ continue;
400
+ }
401
+ await this.removeStaleConversationDocuments(source.sourceKey);
402
+ await this.database.deleteMemoryCandidatesByConversation(source.sourceKey);
403
+ await this.database.deleteMemorySourceState(source.sourceKey);
404
+ }
405
+ const indexed = await this.database.listSearchDocuments();
406
+ for (const document of indexed) {
407
+ if (document.sourceProviderId !== AI_MEMORY_EPISODIC_PROVIDER_ID)
408
+ continue;
409
+ const metadata = decodeEpisodicMetadata(document);
410
+ if (scope && metadata && !sameScope(scope, metadata.scope))
411
+ continue;
412
+ if (!expected.has(document.path)) {
413
+ await this.database.deleteSearchDocument(document.path);
414
+ }
415
+ }
416
+ const curatedRecords = await this.#recordStore.list(scope);
417
+ const expectedCurated = new Set();
418
+ for (const stored of curatedRecords) {
419
+ const document = await this.curatedSearchDocument(stored);
420
+ if (!document)
421
+ continue;
422
+ expectedCurated.add(document.path);
423
+ const existing = await this.database.getSearchDocument(document.path);
424
+ if (existing?.checksum !== document.checksum) {
425
+ await this.database.upsertSearchDocument(document);
426
+ }
427
+ }
428
+ for (const document of indexed) {
429
+ if (document.sourceProviderId !== AI_MEMORY_CURATED_PROVIDER_ID)
430
+ continue;
431
+ const metadata = decodeCuratedMetadata(document);
432
+ if (scope && metadata && !sameScope(scope, metadata.record.scope))
433
+ continue;
434
+ if (!expectedCurated.has(document.path)) {
435
+ await this.database.deleteSearchDocument(document.path);
436
+ }
437
+ }
438
+ return { conversations, episodes, skipped, inconsistent };
439
+ }
440
+ async previewForgetConversation(location) {
441
+ const episodeRefs = [];
442
+ for (const document of await this.database.listSearchDocuments()) {
443
+ const metadata = decodeEpisodicMetadata(document);
444
+ if (metadata?.location.conversationId === location.conversationId) {
445
+ episodeRefs.push(episodeRef(location.conversationId, metadata.entryId));
446
+ }
447
+ }
448
+ const candidates = await this.database.queryMemoryCandidates({
449
+ limit: 10_000,
450
+ });
451
+ const candidateIds = candidates
452
+ .filter((candidate) => candidate.origins.some((origin) => origin.conversationId === location.conversationId))
453
+ .map((candidate) => candidate.candidate.id);
454
+ const memoryIds = (await this.#recordStore.list())
455
+ .filter((stored) => stored.record.evidence.some((evidence) => evidence.conversationId === location.conversationId))
456
+ .map((stored) => stored.record.id);
457
+ return {
458
+ conversationId: location.conversationId,
459
+ episodeRefs: [...new Set(episodeRefs)].sort(),
460
+ candidateIds: [...new Set(candidateIds)].sort(),
461
+ memoryIds: [...new Set(memoryIds)].sort(),
462
+ };
463
+ }
464
+ async forgetConversation(location) {
465
+ const preview = await this.previewForgetConversation(location);
466
+ const exclusionPath = await this.#recordStore.writeConversationExclusion(location);
467
+ await this.removeStaleConversationDocuments(location.conversationId);
468
+ await this.database.deleteMemoryCandidatesByConversation(location.conversationId);
469
+ await this.database.deleteMemorySourceState(location.conversationId);
470
+ let retracted = 0;
471
+ let needsReview = 0;
472
+ for (const stored of await this.#recordStore.list()) {
473
+ if (stored.record.status !== "active" ||
474
+ !stored.record.evidence.some((evidence) => evidence.conversationId === location.conversationId)) {
475
+ continue;
476
+ }
477
+ const next = {
478
+ ...stored.record,
479
+ revision: stored.record.revision + 1,
480
+ status: "retracted",
481
+ updatedAt: this.#now().toISOString(),
482
+ previousRevisionHash: stored.hash,
483
+ };
484
+ try {
485
+ const retractedRecord = await this.#recordStore.write(next, {
486
+ expectedCurrentHash: stored.hash,
487
+ });
488
+ const document = await this.curatedSearchDocument(retractedRecord);
489
+ if (document)
490
+ await this.database.upsertSearchDocument(document);
491
+ retracted += 1;
492
+ }
493
+ catch {
494
+ needsReview += 1;
495
+ }
496
+ }
497
+ return {
498
+ ...preview,
499
+ exclusionPath,
500
+ retracted,
501
+ needsReview,
502
+ };
503
+ }
504
+ async previewConsolidation(scope) {
505
+ const candidates = await this.database.queryMemoryCandidates({
506
+ scopeKind: scope.kind,
507
+ scopePath: scope.kind === "project" ? scope.projectDir : "",
508
+ states: ["staged", "review"],
509
+ limit: 10,
510
+ });
511
+ return {
512
+ scope,
513
+ candidateIds: candidates.map((candidate) => candidate.candidate.id),
514
+ proposals: candidates.filter((candidate) => candidate.candidate.state === "staged" &&
515
+ candidate.candidate.originClass === "owner").length,
516
+ };
517
+ }
518
+ async consolidate(scope) {
519
+ const preview = await this.previewConsolidation(scope);
520
+ const candidates = await this.database.queryMemoryCandidates({
521
+ scopeKind: scope.kind,
522
+ scopePath: scope.kind === "project" ? scope.projectDir : "",
523
+ states: ["staged", "review"],
524
+ limit: 10,
525
+ });
526
+ const groundedEvidence = new Map();
527
+ for (const candidate of candidates) {
528
+ if (candidate.candidate.state !== "staged" ||
529
+ candidate.candidate.originClass !== "owner") {
530
+ continue;
531
+ }
532
+ const evidence = await this.resolveCandidateEvidence(candidate.origins);
533
+ if (evidence?.length) {
534
+ groundedEvidence.set(candidate.candidate.id, evidence);
535
+ }
536
+ }
537
+ const consolidationProvider = typeof this.#consolidationProvider === "function"
538
+ ? this.#consolidationProvider()
539
+ : this.#consolidationProvider;
540
+ let providerProposals = new Map();
541
+ if (consolidationProvider && groundedEvidence.size > 0) {
542
+ try {
543
+ const proposal = await consolidationProvider.propose({
544
+ scope,
545
+ candidates: candidates.flatMap((candidate) => {
546
+ const evidence = groundedEvidence.get(candidate.candidate.id);
547
+ return evidence
548
+ ? [
549
+ {
550
+ id: candidate.candidate.id,
551
+ claim: candidate.candidate.normalizedClaim,
552
+ evidence,
553
+ },
554
+ ]
555
+ : [];
556
+ }),
557
+ });
558
+ providerProposals = validateConsolidationProposal(proposal, scope, new Set(groundedEvidence.keys()));
559
+ }
560
+ catch {
561
+ return {
562
+ ...preview,
563
+ written: 0,
564
+ needsReview: groundedEvidence.size,
565
+ };
566
+ }
567
+ }
568
+ let written = 0;
569
+ let needsReview = 0;
570
+ for (const candidate of candidates) {
571
+ if (written >= 5)
572
+ break;
573
+ if (candidate.candidate.state !== "staged" ||
574
+ candidate.candidate.originClass !== "owner") {
575
+ needsReview += 1;
576
+ continue;
577
+ }
578
+ const evidence = groundedEvidence.get(candidate.candidate.id);
579
+ if (!evidence || evidence.length === 0) {
580
+ needsReview += 1;
581
+ await this.writeConsolidationReview(scope, candidate.candidate.id, "needs-review", candidate.origins, "Evidence was missing, ambiguous, modified, or not owner-authored.");
582
+ continue;
583
+ }
584
+ const allStored = await this.#recordStore.list(scope);
585
+ const providerProposal = providerProposals.get(candidate.candidate.id);
586
+ if (consolidationProvider && !providerProposal) {
587
+ needsReview += 1;
588
+ continue;
589
+ }
590
+ const proposedSummary = providerProposal?.summary ?? candidate.candidate.normalizedClaim;
591
+ const proposedSupersessionKey = providerProposal?.supersessionKey ??
592
+ candidate.candidate.supersessionKey;
593
+ const existing = proposedSupersessionKey
594
+ ? allStored.find((stored) => stored.record.supersessionKey ===
595
+ proposedSupersessionKey)
596
+ : undefined;
597
+ if (existing &&
598
+ contradictory(existing.record.summary, proposedSummary)) {
599
+ needsReview += 1;
600
+ await this.writeConsolidationReview(scope, candidate.candidate.id, "needs-review", candidate.origins, "Conflicting evidence matched the current supersession key.", { expectedPreimageHash: existing.hash });
601
+ continue;
602
+ }
603
+ const id = existing?.record.id ??
604
+ (await deterministicMemoryId("memory", scope.kind, scope.kind === "project" ? scope.projectDir : "", proposedSupersessionKey ?? candidate.candidate.claimHash));
605
+ const now = this.#now().toISOString();
606
+ const combinedEvidence = dedupeEvidence([
607
+ ...(existing?.record.evidence ?? []),
608
+ ...evidence,
609
+ ]);
610
+ if (existing &&
611
+ existing.record.summary === proposedSummary &&
612
+ combinedEvidence.length === existing.record.evidence.length) {
613
+ await this.markCandidatePromoted(candidate, id);
614
+ continue;
615
+ }
616
+ const record = {
617
+ schemaVersion: 1,
618
+ id,
619
+ revision: existing ? existing.record.revision + 1 : 1,
620
+ kind: providerProposal?.kind ?? candidate.candidate.kind,
621
+ scope,
622
+ status: "active",
623
+ ...(proposedSupersessionKey
624
+ ? { supersessionKey: proposedSupersessionKey }
625
+ : {}),
626
+ importance: providerProposal?.importance ?? candidate.candidate.importance,
627
+ triggers: providerProposal?.triggers ?? candidate.candidate.triggers,
628
+ summary: proposedSummary,
629
+ evidence: combinedEvidence,
630
+ createdBy: providerProposal
631
+ ? "consolidator"
632
+ : "deterministic-promotion",
633
+ createdAt: existing?.record.createdAt ?? now,
634
+ updatedAt: now,
635
+ ...(existing ? { previousRevisionHash: existing.hash } : {}),
636
+ };
637
+ try {
638
+ const stored = await this.#recordStore.write(record, {
639
+ ...(existing ? { expectedCurrentHash: existing.hash } : {}),
640
+ });
641
+ const document = await this.curatedSearchDocument(stored);
642
+ if (document)
643
+ await this.database.upsertSearchDocument(document);
644
+ await this.markCandidatePromoted(candidate, id);
645
+ await this.writeConsolidationReview(scope, candidate.candidate.id, "written", candidate.origins, "Grounded candidate produced a durable memory revision.", {
646
+ expectedPreimageHash: existing?.hash,
647
+ memoryId: id,
648
+ resultingRevision: record.revision,
649
+ resultingRevisionHash: stored.hash,
650
+ });
651
+ written += 1;
652
+ }
653
+ catch {
654
+ needsReview += 1;
655
+ await this.writeConsolidationReview(scope, candidate.candidate.id, "needs-review", candidate.origins, "The current memory changed or the proposed revision failed validation.");
656
+ }
657
+ }
658
+ return { ...preview, written, needsReview };
659
+ }
660
+ async markInconsistent(location, episodes, errorCode) {
661
+ await this.database.upsertMemorySourceState({
662
+ sourceKey: location.conversationId,
663
+ sourcePath: transcriptPath(location),
664
+ sourceHash: await sha256Text(episodes.map((episode) => `${episode.entry.id}:${episode.entryHash}`).join("\n")),
665
+ status: "inconsistent",
666
+ indexedAt: this.#now().getTime(),
667
+ errorCode,
668
+ });
669
+ }
670
+ async removeStaleConversationDocuments(conversationId) {
671
+ const documents = await this.database.listSearchDocuments();
672
+ for (const document of documents) {
673
+ if (document.sourceProviderId !== AI_MEMORY_EPISODIC_PROVIDER_ID)
674
+ continue;
675
+ if (decodeEpisodicMetadata(document)?.location.conversationId === conversationId) {
676
+ await this.database.deleteSearchDocument(document.path);
677
+ }
678
+ }
679
+ }
680
+ async curatedSearchDocument(stored) {
681
+ const verification = await this.verifyEvidence(stored.record.evidence);
682
+ const ownerAuthoredWithoutEvidence = stored.record.createdBy === "owner-ui" &&
683
+ stored.record.evidence.length === 0;
684
+ if (!ownerAuthoredWithoutEvidence && !verification.allMatching)
685
+ return null;
686
+ const metadata = {
687
+ schemaVersion: 1,
688
+ corpus: "curated",
689
+ record: stored.record,
690
+ effectiveTrust: ownerAuthoredWithoutEvidence
691
+ ? "owner"
692
+ : verification.effectiveTrust,
693
+ evidenceVerified: verification.allMatching,
694
+ };
695
+ const path = curatedMemoryPath(stored.record.scope, stored.record.id);
696
+ return {
697
+ path,
698
+ sourceProviderId: AI_MEMORY_CURATED_PROVIDER_ID,
699
+ name: stored.record.summary.slice(0, 80),
700
+ extension: "ai-memory",
701
+ checksum: stored.hash,
702
+ content: [stored.record.summary, ...stored.record.triggers].join("\n"),
703
+ metadataText: canonicalJson(metadata),
704
+ tags: stored.record.triggers,
705
+ tagParts: [],
706
+ tagHierarchy: [],
707
+ chunks: [
708
+ {
709
+ id: `${path}#summary`,
710
+ text: stored.record.summary,
711
+ startOffset: 0,
712
+ endOffset: stored.record.summary.length,
713
+ kind: "paragraph",
714
+ },
715
+ ],
716
+ };
717
+ }
718
+ async verifyEvidence(evidence) {
719
+ const messages = [];
720
+ const origins = new Set();
721
+ let allMatching = true;
722
+ for (const ref of evidence) {
723
+ const detail = await this.get(episodeRef(ref.conversationId, ref.entryId), { scopeDir: ref.scopeDirAtObservation });
724
+ const message = detail.evidenceMessages[0];
725
+ if (!message) {
726
+ allMatching = false;
727
+ continue;
728
+ }
729
+ const verification = message.hash === ref.entryHash ? "matching" : "modified";
730
+ origins.add(message.originClass);
731
+ if (messages.length < 6)
732
+ messages.push({ ...message, verification });
733
+ if (verification !== "matching")
734
+ allMatching = false;
735
+ }
736
+ const effectiveTrust = origins.has("owner")
737
+ ? "owner"
738
+ : origins.has("agent")
739
+ ? "agent"
740
+ : origins.has("untrusted")
741
+ ? "untrusted"
742
+ : "system";
743
+ return {
744
+ allMatching,
745
+ effectiveTrust,
746
+ messages,
747
+ };
748
+ }
749
+ async getCurated(id, revision, signal) {
750
+ signal?.throwIfAborted();
751
+ const matches = await this.#recordStore.find(id);
752
+ if (matches.length > 1) {
753
+ return { ref: curatedRef(id, revision), corpus: "curated", status: "ambiguous", evidenceMessages: [] };
754
+ }
755
+ const current = matches[0];
756
+ if (!current) {
757
+ return { ref: curatedRef(id, revision), corpus: "curated", status: "missing", evidenceMessages: [] };
758
+ }
759
+ const history = await this.#recordStore.listHistory(id, current.record.scope);
760
+ const record = current.record.revision === revision
761
+ ? current.record
762
+ : history.find((candidate) => candidate.revision === revision);
763
+ if (!record) {
764
+ return { ref: curatedRef(id, revision), corpus: "curated", status: "missing", evidenceMessages: [], history };
765
+ }
766
+ const verification = await this.verifyEvidence(record.evidence);
767
+ const status = verification.messages.some((message) => message.verification === "modified")
768
+ ? "modified"
769
+ : verification.messages.length < record.evidence.slice(0, 6).length
770
+ ? "missing"
771
+ : "available";
772
+ return {
773
+ ref: curatedRef(id, revision),
774
+ corpus: "curated",
775
+ status,
776
+ result: curatedResult(record, verification.effectiveTrust, 1),
777
+ evidenceMessages: verification.messages,
778
+ history,
779
+ };
780
+ }
781
+ async resolveCandidateEvidence(origins) {
782
+ const refs = [];
783
+ for (const origin of origins) {
784
+ const matches = (await this.repository.listAll()).filter((entry) => entry.location.conversationId === origin.conversationId);
785
+ if (matches.length !== 1 || matches[0]?.unavailableReason)
786
+ return null;
787
+ const snapshot = await this.repository.read(matches[0].location);
788
+ const entry = snapshot.transcript.find((candidate) => candidate.id === origin.entryId);
789
+ if (!entry || entry.type !== "message" || entry.role !== "user")
790
+ return null;
791
+ const hash = await transcriptEntryHash(entry);
792
+ if (hash !== origin.entryHash)
793
+ return null;
794
+ refs.push({
795
+ conversationId: origin.conversationId,
796
+ entryId: origin.entryId,
797
+ entryHash: hash,
798
+ observedAt: entry.createdAt,
799
+ scopeDirAtObservation: snapshot.location.scopeDir,
800
+ });
801
+ }
802
+ return dedupeEvidence(refs);
803
+ }
804
+ async markCandidatePromoted(candidate, memoryId) {
805
+ await this.database.upsertMemoryCandidate({
806
+ candidate: {
807
+ ...candidate.candidate,
808
+ state: "promoted",
809
+ promotedMemoryId: memoryId,
810
+ },
811
+ origins: candidate.origins,
812
+ });
813
+ }
814
+ async writeConsolidationReview(scope, candidateId, outcome, origins, reason, details = {}) {
815
+ const jobId = await deterministicMemoryId("review", candidateId, this.#now().toISOString());
816
+ const frontmatter = {
817
+ schemaVersion: 1,
818
+ jobId,
819
+ scope,
820
+ candidateIds: [candidateId],
821
+ outcome,
822
+ ...details,
823
+ evidence: origins.map((origin) => ({
824
+ conversationId: origin.conversationId,
825
+ entryId: origin.entryId,
826
+ entryHash: origin.entryHash,
827
+ })),
828
+ createdAt: this.#now().toISOString(),
829
+ };
830
+ await this.#recordStore.writeReview(scope, jobId, `---\n${canonicalJson(frontmatter)}\n---\n\n# Memory consolidation review\n\n${reason}\n`).catch(() => undefined);
831
+ }
832
+ async recordRecallSignals(query, results) {
833
+ const key = "ai.memory.query-fingerprint-salt";
834
+ let salt = await this.database.getMeta(key);
835
+ if (!salt) {
836
+ salt = crypto.randomUUID();
837
+ await this.database.setMeta(key, salt);
838
+ }
839
+ const queryFingerprint = await sha256Text(`${salt}\u0000${query.trim().toLocaleLowerCase()}`);
840
+ const day = this.#now().toISOString().slice(0, 10);
841
+ for (const result of results) {
842
+ await this.database.recordMemoryRecallSignal({
843
+ targetRef: result.ref,
844
+ queryFingerprint,
845
+ day,
846
+ bestScore: result.score,
847
+ hitCount: 1,
848
+ });
849
+ }
850
+ }
851
+ }
852
+ async function projectEpisodes(snapshot) {
853
+ const projected = [];
854
+ for (const entry of snapshot.transcript) {
855
+ if (entry.type !== "message" && entry.type !== "tool")
856
+ continue;
857
+ const text = episodeText(entry);
858
+ if (text === null)
859
+ continue;
860
+ projected.push({
861
+ entry,
862
+ entryHash: await transcriptEntryHash(entry),
863
+ text,
864
+ originClass: entry.type === "tool" ? "untrusted" : entry.role === "user" ? "owner" : "agent",
865
+ });
866
+ }
867
+ return projected;
868
+ }
869
+ function episodeText(entry) {
870
+ if (entry.type === "message")
871
+ return entry.text.trim() ? entry.text : null;
872
+ if (entry.type === "tool") {
873
+ const text = entry.output ?? entry.input;
874
+ return text?.trim() ? text : null;
875
+ }
876
+ return null;
877
+ }
878
+ function firstDuplicateEntry(episodes) {
879
+ const seen = new Map();
880
+ for (const episode of episodes) {
881
+ const prior = seen.get(episode.entry.id);
882
+ if (prior !== undefined)
883
+ return episode.entry.id;
884
+ seen.set(episode.entry.id, episode.entryHash);
885
+ }
886
+ return null;
887
+ }
888
+ function episodeSearchDocument(location, episode) {
889
+ const scope = conversationMemoryScope(location.scopeDir);
890
+ const metadata = {
891
+ schemaVersion: 1,
892
+ corpus: "episodic",
893
+ location,
894
+ entryId: episode.entry.id,
895
+ entryHash: episode.entryHash,
896
+ observedAt: episode.entry.createdAt,
897
+ originClass: episode.originClass,
898
+ scope,
899
+ };
900
+ const path = episodicMemoryPath(scope, location.conversationId, episode.entry.id);
901
+ return {
902
+ path,
903
+ sourceProviderId: AI_MEMORY_EPISODIC_PROVIDER_ID,
904
+ name: `Memory episode ${episode.entry.id}`,
905
+ extension: "ai-memory-episode",
906
+ checksum: episode.entryHash,
907
+ content: episode.text,
908
+ metadataText: canonicalJson(metadata),
909
+ tags: [],
910
+ tagParts: [],
911
+ tagHierarchy: [],
912
+ chunks: [
913
+ {
914
+ id: `${path}#entry`,
915
+ text: episode.text,
916
+ startOffset: 0,
917
+ endOffset: episode.text.length,
918
+ kind: "paragraph",
919
+ },
920
+ ],
921
+ };
922
+ }
923
+ function decodeEpisodicMetadata(document) {
924
+ if (document.sourceProviderId !== AI_MEMORY_EPISODIC_PROVIDER_ID ||
925
+ !document.metadataText) {
926
+ return null;
927
+ }
928
+ try {
929
+ const parsed = JSON.parse(document.metadataText);
930
+ return parsed.schemaVersion === 1 &&
931
+ parsed.corpus === "episodic" &&
932
+ typeof parsed.location?.conversationId === "string" &&
933
+ typeof parsed.entryId === "string" &&
934
+ typeof parsed.entryHash === "string"
935
+ ? parsed
936
+ : null;
937
+ }
938
+ catch {
939
+ return null;
940
+ }
941
+ }
942
+ function decodeCuratedMetadata(document) {
943
+ if (document.sourceProviderId !== AI_MEMORY_CURATED_PROVIDER_ID ||
944
+ !document.metadataText) {
945
+ return null;
946
+ }
947
+ try {
948
+ const parsed = JSON.parse(document.metadataText);
949
+ return parsed.schemaVersion === 1 &&
950
+ parsed.corpus === "curated" &&
951
+ typeof parsed.record?.id === "string" &&
952
+ typeof parsed.record?.revision === "number"
953
+ ? parsed
954
+ : null;
955
+ }
956
+ catch {
957
+ return null;
958
+ }
959
+ }
960
+ function decodeCuratedResults(results, currentScopeDir, query) {
961
+ const maxRetrieval = Math.max(1, ...results.map((result) => result.score));
962
+ return results.flatMap((searchResult) => {
963
+ const metadata = decodeCuratedMetadata(searchResult.document);
964
+ if (!metadata)
965
+ return [];
966
+ const record = metadata.record;
967
+ if (record.status !== "active" && !query.includeSuperseded)
968
+ return [];
969
+ if (!withinDateBounds(record.updatedAt, query))
970
+ return [];
971
+ const retrieval = Math.max(0, searchResult.score / maxRetrieval);
972
+ const score = 0.75 * retrieval +
973
+ 0.15 * scopeProximity(record.scope, currentScopeDir) +
974
+ 0.1 * (record.importance / 5);
975
+ return [
976
+ {
977
+ path: searchResult.document.path,
978
+ retrievalScore: searchResult.score,
979
+ result: curatedResult(record, metadata.effectiveTrust, score, searchResult.snippets.find((snippet) => snippet.field === "content")
980
+ ?.text),
981
+ },
982
+ ];
983
+ });
984
+ }
985
+ function curatedResult(record, effectiveTrust, score, snippet = record.summary) {
986
+ return {
987
+ ref: curatedRef(record.id, record.revision),
988
+ corpus: "curated",
989
+ snippet: truncateSnippet(snippet),
990
+ kind: record.kind,
991
+ scope: record.scope,
992
+ observedAt: record.updatedAt,
993
+ effectiveTrust,
994
+ status: record.status,
995
+ score,
996
+ evidence: structuredClone(record.evidence),
997
+ };
998
+ }
999
+ function decodeEpisodicResults(results, currentScopeDir, proximity, query, now) {
1000
+ const maxRetrieval = Math.max(1, ...results.map((result) => result.score));
1001
+ return results.flatMap((searchResult) => {
1002
+ const metadata = decodeEpisodicMetadata(searchResult.document);
1003
+ if (!metadata || !withinDateBounds(metadata.observedAt, query))
1004
+ return [];
1005
+ const retrieval = Math.max(0, searchResult.score / maxRetrieval);
1006
+ const recency = recencyScore(metadata.observedAt, now);
1007
+ const result = episodeResultFromDocument(searchResult.document, 0.75 * retrieval + 0.15 * Math.max(proximity, scopeProximity(metadata.scope, currentScopeDir)) + 0.1 * recency, searchResult);
1008
+ return result ? [result] : [];
1009
+ });
1010
+ }
1011
+ function episodeResultFromDocument(document, score, searchResult) {
1012
+ const metadata = decodeEpisodicMetadata(document);
1013
+ if (!metadata)
1014
+ return null;
1015
+ const snippet = searchResult?.snippets.find((value) => value.field === "content")?.text ??
1016
+ document.content;
1017
+ const evidence = {
1018
+ conversationId: metadata.location.conversationId,
1019
+ entryId: metadata.entryId,
1020
+ entryHash: metadata.entryHash,
1021
+ observedAt: metadata.observedAt,
1022
+ scopeDirAtObservation: metadata.location.scopeDir,
1023
+ };
1024
+ return {
1025
+ path: document.path,
1026
+ retrievalScore: searchResult?.score ?? score,
1027
+ result: {
1028
+ ref: episodeRef(metadata.location.conversationId, metadata.entryId),
1029
+ corpus: "episodic",
1030
+ snippet: truncateSnippet(snippet),
1031
+ kind: "episode",
1032
+ scope: metadata.scope,
1033
+ observedAt: metadata.observedAt,
1034
+ effectiveTrust: metadata.originClass,
1035
+ status: "active",
1036
+ score,
1037
+ evidence: [evidence],
1038
+ },
1039
+ };
1040
+ }
1041
+ function episodeEvidenceMessage(episode, modified) {
1042
+ return {
1043
+ id: episode.entry.id,
1044
+ type: episode.entry.type,
1045
+ ...(episode.entry.type === "message" ? { role: episode.entry.role } : {}),
1046
+ text: episode.text,
1047
+ observedAt: episode.entry.createdAt,
1048
+ originClass: episode.originClass,
1049
+ hash: episode.entryHash,
1050
+ verification: modified ? "modified" : "matching",
1051
+ };
1052
+ }
1053
+ function episodeRef(conversationId, entryId) {
1054
+ return `episode:${encodeURIComponent(conversationId)}:${encodeURIComponent(entryId)}`;
1055
+ }
1056
+ function curatedRef(id, revision) {
1057
+ return `memory:${encodeURIComponent(id)}:${revision}`;
1058
+ }
1059
+ function parseCuratedRef(ref) {
1060
+ const match = /^memory:([^:]+):(\d+)$/u.exec(ref);
1061
+ if (!match)
1062
+ return null;
1063
+ const revision = Number(match[2]);
1064
+ if (!Number.isSafeInteger(revision) || revision < 1)
1065
+ return null;
1066
+ try {
1067
+ return { id: decodeURIComponent(match[1]), revision };
1068
+ }
1069
+ catch {
1070
+ return null;
1071
+ }
1072
+ }
1073
+ function parseEpisodeRef(ref) {
1074
+ const match = /^episode:([^:]+):([^:]+)$/u.exec(ref);
1075
+ if (!match)
1076
+ return null;
1077
+ try {
1078
+ return {
1079
+ conversationId: decodeURIComponent(match[1]),
1080
+ entryId: decodeURIComponent(match[2]),
1081
+ };
1082
+ }
1083
+ catch {
1084
+ return null;
1085
+ }
1086
+ }
1087
+ function transcriptPath(location) {
1088
+ return `${conversationDirectory(location)}/transcript.jsonl`;
1089
+ }
1090
+ function withinDateBounds(observedAt, query) {
1091
+ const value = Date.parse(observedAt);
1092
+ if (!Number.isFinite(value))
1093
+ return false;
1094
+ if (query.since && value < Date.parse(query.since))
1095
+ return false;
1096
+ if (query.before && value >= Date.parse(query.before))
1097
+ return false;
1098
+ return true;
1099
+ }
1100
+ function raceWithAbort(promise, signal) {
1101
+ if (signal.aborted)
1102
+ return Promise.reject(signal.reason);
1103
+ return new Promise((resolve, reject) => {
1104
+ const onAbort = () => reject(signal.reason);
1105
+ signal.addEventListener("abort", onAbort, { once: true });
1106
+ void promise.then((value) => {
1107
+ signal.removeEventListener("abort", onAbort);
1108
+ resolve(value);
1109
+ }, (error) => {
1110
+ signal.removeEventListener("abort", onAbort);
1111
+ reject(error);
1112
+ });
1113
+ });
1114
+ }
1115
+ function recencyScore(observedAt, now) {
1116
+ const ageDays = Math.max(0, (now - Date.parse(observedAt)) / 86_400_000);
1117
+ return Math.exp((-Math.LN2 * ageDays) / 30);
1118
+ }
1119
+ function truncateSnippet(value) {
1120
+ const normalized = value.replace(/\s+/gu, " ").trim();
1121
+ return normalized.length <= MAX_SNIPPET_CHARS
1122
+ ? normalized
1123
+ : `${normalized.slice(0, MAX_SNIPPET_CHARS - 1)}…`;
1124
+ }
1125
+ function selectMmr(candidates, limit) {
1126
+ const remaining = [...candidates];
1127
+ const selected = [];
1128
+ while (remaining.length > 0 && selected.length < limit) {
1129
+ let bestIndex = 0;
1130
+ let bestScore = Number.NEGATIVE_INFINITY;
1131
+ for (let index = 0; index < remaining.length; index += 1) {
1132
+ const candidate = remaining[index];
1133
+ const similarity = Math.max(0, ...selected.map((chosen) => tokenJaccard(candidate.result.snippet, chosen.result.snippet)));
1134
+ const score = MMR_LAMBDA * candidate.result.score - (1 - MMR_LAMBDA) * similarity;
1135
+ if (score > bestScore) {
1136
+ bestScore = score;
1137
+ bestIndex = index;
1138
+ }
1139
+ }
1140
+ selected.push(remaining.splice(bestIndex, 1)[0]);
1141
+ }
1142
+ return selected;
1143
+ }
1144
+ function tokenJaccard(left, right) {
1145
+ const tokenize = (value) => new Set(value.toLocaleLowerCase().match(/[\p{L}\p{N}_-]+/gu) ?? []);
1146
+ const leftTokens = tokenize(left);
1147
+ const rightTokens = tokenize(right);
1148
+ if (leftTokens.size === 0 || rightTokens.size === 0) {
1149
+ return left === right ? 1 : 0;
1150
+ }
1151
+ let intersection = 0;
1152
+ for (const token of leftTokens) {
1153
+ if (rightTokens.has(token))
1154
+ intersection += 1;
1155
+ }
1156
+ return intersection / (leftTokens.size + rightTokens.size - intersection);
1157
+ }
1158
+ function sameScope(left, right) {
1159
+ return left.kind === right.kind &&
1160
+ (left.kind !== "project" ||
1161
+ (right.kind === "project" && left.projectDir === right.projectDir));
1162
+ }
1163
+ function dedupeEvidence(evidence) {
1164
+ return [...new Map(evidence.map((ref) => [
1165
+ `${ref.conversationId}\u0000${ref.entryId}`,
1166
+ ref,
1167
+ ])).values()].sort((left, right) => `${left.conversationId}\u0000${left.entryId}`.localeCompare(`${right.conversationId}\u0000${right.entryId}`));
1168
+ }
1169
+ function contradictory(current, proposed) {
1170
+ const negated = (value) => /\b(?:not|never|no longer|do not|don't|mustn't|shouldn't)\b/iu.test(value);
1171
+ return current.toLocaleLowerCase() !== proposed.toLocaleLowerCase() &&
1172
+ negated(current) !== negated(proposed);
1173
+ }
1174
+ function recallContent(result) {
1175
+ return [
1176
+ result.snippet,
1177
+ `Provenance: ${result.evidence.map((evidence) => `${evidence.conversationId}/${evidence.entryId}`).join(", ") || "owner-authored memory"}`,
1178
+ ].join("\n");
1179
+ }
1180
+ function automaticRecallQuery(query) {
1181
+ const stop = new Set([
1182
+ "about",
1183
+ "could",
1184
+ "have",
1185
+ "please",
1186
+ "should",
1187
+ "that",
1188
+ "this",
1189
+ "use",
1190
+ "what",
1191
+ "when",
1192
+ "where",
1193
+ "which",
1194
+ "with",
1195
+ "would",
1196
+ ]);
1197
+ const tokens = query.toLocaleLowerCase().match(/[\p{L}\p{N}_-]+/gu) ?? [];
1198
+ const meaningful = tokens.filter((token) => token.length > 2 && !stop.has(token));
1199
+ return meaningful.slice(0, 12).join(" ") || query;
1200
+ }
1201
+ function validateConsolidationProposal(value, scope, candidateIds) {
1202
+ if (!value || typeof value !== "object" || !Array.isArray(value.memories)) {
1203
+ throw new Error("Consolidation proposal must contain a memories array");
1204
+ }
1205
+ if (value.memories.length > 5) {
1206
+ throw new Error("Consolidation proposal exceeds the write limit");
1207
+ }
1208
+ const validated = new Map();
1209
+ for (const proposed of value.memories) {
1210
+ if (!proposed ||
1211
+ typeof proposed !== "object" ||
1212
+ !Array.isArray(proposed.candidateIds) ||
1213
+ proposed.candidateIds.length !== 1) {
1214
+ throw new Error("Each proposed memory must reference one candidate");
1215
+ }
1216
+ const candidateId = proposed.candidateIds[0];
1217
+ if (!candidateIds.has(candidateId) || validated.has(candidateId)) {
1218
+ throw new Error("Consolidation proposal references an unknown candidate");
1219
+ }
1220
+ if (!sameScope(scope, proposed.scope)) {
1221
+ throw new Error("Consolidation proposal escalates memory scope");
1222
+ }
1223
+ if (!["preference", "fact", "decision", "procedure", "constraint"].includes(proposed.kind) ||
1224
+ ![1, 2, 3, 4, 5].includes(proposed.importance) ||
1225
+ typeof proposed.summary !== "string" ||
1226
+ !proposed.summary.trim() ||
1227
+ proposed.summary.length > 2_000 ||
1228
+ isSecretBearingMemoryText(proposed.summary) ||
1229
+ !Array.isArray(proposed.triggers) ||
1230
+ proposed.triggers.length > 12 ||
1231
+ proposed.triggers.some((trigger) => typeof trigger !== "string" ||
1232
+ trigger.length > 64 ||
1233
+ isSecretBearingMemoryText(trigger)) ||
1234
+ (proposed.supersessionKey !== undefined &&
1235
+ (typeof proposed.supersessionKey !== "string" ||
1236
+ proposed.supersessionKey.length > 256))) {
1237
+ throw new Error("Consolidation proposal failed validation");
1238
+ }
1239
+ validated.set(candidateId, {
1240
+ ...proposed,
1241
+ summary: proposed.summary.trim(),
1242
+ triggers: [...new Set(proposed.triggers.map((trigger) => trigger.trim()))]
1243
+ .filter(Boolean),
1244
+ });
1245
+ }
1246
+ return validated;
1247
+ }
1248
+ export const __memoryServiceInternals = {
1249
+ contradictory,
1250
+ curatedRef,
1251
+ episodeRef,
1252
+ parseCuratedRef,
1253
+ parseEpisodeRef,
1254
+ projectEpisodes,
1255
+ selectMmr,
1256
+ };