@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,108 @@
1
+ import { conversationLocationFromSourcePath } from "../conversations/conversation-index-coordinator";
2
+ export class MemoryIngestionCoordinator {
3
+ repository;
4
+ memory;
5
+ onError;
6
+ delay;
7
+ #pending = new Map();
8
+ #timer;
9
+ #unsubscribe;
10
+ #disposed = false;
11
+ constructor(repository, memory, onError = () => { }, delay = 150) {
12
+ this.repository = repository;
13
+ this.memory = memory;
14
+ this.onError = onError;
15
+ this.delay = delay;
16
+ this.#unsubscribe = repository.subscribe((change) => this.handleRepositoryChange(change));
17
+ }
18
+ handleRepositoryChange(change) {
19
+ if (change.type === "delete") {
20
+ this.#pending.clear();
21
+ this.#pending.set("rebuild", { type: "rebuild" });
22
+ }
23
+ else {
24
+ this.#pending.set(locationKey(change.location), {
25
+ type: "ingest",
26
+ location: change.location,
27
+ requireTerminal: true,
28
+ });
29
+ }
30
+ this.schedule();
31
+ }
32
+ handleVaultChange(path, oldPath) {
33
+ const current = conversationLocationFromSourcePath(path);
34
+ const previous = oldPath
35
+ ? conversationLocationFromSourcePath(oldPath)
36
+ : null;
37
+ if (previous && (!current || locationKey(previous) !== locationKey(current))) {
38
+ this.#pending.set("rebuild", { type: "rebuild" });
39
+ }
40
+ if (current && path.endsWith("/transcript.jsonl")) {
41
+ this.#pending.set(locationKey(current), {
42
+ type: "ingest",
43
+ location: current,
44
+ requireTerminal: false,
45
+ });
46
+ }
47
+ if (current || previous)
48
+ this.schedule();
49
+ }
50
+ startCatchUp() {
51
+ this.#pending.set("rebuild", { type: "rebuild" });
52
+ this.schedule();
53
+ }
54
+ async flush() {
55
+ if (this.#timer)
56
+ clearTimeout(this.#timer);
57
+ this.#timer = undefined;
58
+ const actions = [...this.#pending.values()];
59
+ this.#pending.clear();
60
+ for (const action of actions) {
61
+ if (this.#disposed)
62
+ return;
63
+ try {
64
+ if (action.type === "rebuild") {
65
+ await this.memory.rebuild();
66
+ }
67
+ else if (!action.requireTerminal ||
68
+ (await this.hasTerminalTranscriptBoundary(action.location))) {
69
+ await this.memory.ingestConversation(action.location);
70
+ }
71
+ }
72
+ catch (error) {
73
+ this.onError(action.type, action.type === "ingest" ? action.location : undefined, error);
74
+ }
75
+ }
76
+ }
77
+ dispose() {
78
+ this.#disposed = true;
79
+ if (this.#timer)
80
+ clearTimeout(this.#timer);
81
+ this.#timer = undefined;
82
+ this.#pending.clear();
83
+ this.#unsubscribe?.();
84
+ this.#unsubscribe = undefined;
85
+ }
86
+ schedule() {
87
+ if (this.#disposed)
88
+ return;
89
+ if (this.#timer)
90
+ clearTimeout(this.#timer);
91
+ this.#timer = setTimeout(() => {
92
+ this.#timer = undefined;
93
+ void this.flush();
94
+ }, this.delay);
95
+ }
96
+ async hasTerminalTranscriptBoundary(location) {
97
+ const snapshot = await this.repository.read(location);
98
+ const last = snapshot.transcript.at(-1);
99
+ return Boolean(last &&
100
+ ((last.type === "message" && last.role === "assistant") ||
101
+ last.type === "tool" ||
102
+ last.type === "error" ||
103
+ last.type === "cancelled"));
104
+ }
105
+ }
106
+ function locationKey(location) {
107
+ return `${location.scopeDir}\u0000${location.conversationId}`;
108
+ }
@@ -0,0 +1,62 @@
1
+ import { type Vault } from "@lapis-notes/api/vault";
2
+ import { canonicalJson } from "./hashes";
3
+ import type { ConversationLocation } from "../conversations/types";
4
+ import type { DurableMemoryRecord, MemoryScope } from "./types";
5
+ export type StoredMemoryRecord = {
6
+ record: DurableMemoryRecord;
7
+ path: string;
8
+ hash: string;
9
+ content: string;
10
+ };
11
+ export type MemoryRecordWriteOptions = {
12
+ expectedCurrentHash?: string;
13
+ };
14
+ export interface MemoryRecordStore {
15
+ list(scope?: MemoryScope): Promise<StoredMemoryRecord[]>;
16
+ find(id: string): Promise<StoredMemoryRecord[]>;
17
+ write(record: DurableMemoryRecord, options?: MemoryRecordWriteOptions): Promise<StoredMemoryRecord>;
18
+ listHistory(id: string, scope: MemoryScope): Promise<DurableMemoryRecord[]>;
19
+ writeReview(scope: MemoryScope, jobId: string, markdown: string): Promise<string>;
20
+ listExcludedConversationIds(): Promise<Set<string>>;
21
+ writeConversationExclusion(location: ConversationLocation): Promise<string>;
22
+ }
23
+ export declare class MemoryRecordConflictError extends Error {
24
+ constructor(message: string);
25
+ }
26
+ export declare class VaultMemoryRecordStore implements MemoryRecordStore {
27
+ private readonly vault;
28
+ constructor(vault: Vault);
29
+ list(scope?: MemoryScope): Promise<StoredMemoryRecord[]>;
30
+ find(id: string): Promise<StoredMemoryRecord[]>;
31
+ write(record: DurableMemoryRecord, options?: MemoryRecordWriteOptions): Promise<StoredMemoryRecord>;
32
+ listHistory(id: string, scope: MemoryScope): Promise<DurableMemoryRecord[]>;
33
+ writeReview(scope: MemoryScope, jobId: string, markdown: string): Promise<string>;
34
+ listExcludedConversationIds(): Promise<Set<string>>;
35
+ writeConversationExclusion(location: ConversationLocation): Promise<string>;
36
+ private writeImmutablePreimage;
37
+ }
38
+ export declare class InMemoryMemoryRecordStore implements MemoryRecordStore {
39
+ #private;
40
+ list(scope?: MemoryScope): Promise<StoredMemoryRecord[]>;
41
+ find(id: string): Promise<StoredMemoryRecord[]>;
42
+ write(record: DurableMemoryRecord, options?: MemoryRecordWriteOptions): Promise<StoredMemoryRecord>;
43
+ listHistory(id: string, scope: MemoryScope): Promise<DurableMemoryRecord[]>;
44
+ writeReview(scope: MemoryScope, jobId: string, markdown: string): Promise<string>;
45
+ listExcludedConversationIds(): Promise<Set<string>>;
46
+ writeConversationExclusion(location: ConversationLocation): Promise<string>;
47
+ }
48
+ export declare function memoryRoot(scope: MemoryScope): string;
49
+ export declare function recordDirectory(scope: MemoryScope): string;
50
+ export declare function recordPath(scope: MemoryScope, id: string): string;
51
+ declare function revisionDirectory(scope: MemoryScope, id: string): string;
52
+ export declare function serializeMemoryRecord(record: DurableMemoryRecord): string;
53
+ declare function parseStoredRecord(path: string, content: string): Promise<StoredMemoryRecord>;
54
+ export declare function validateMemoryRecord(value: unknown): DurableMemoryRecord;
55
+ declare function scopeKey(scope: MemoryScope): string;
56
+ export declare const __memoryRecordStoreInternals: {
57
+ parseStoredRecord: typeof parseStoredRecord;
58
+ revisionDirectory: typeof revisionDirectory;
59
+ scopeKey: typeof scopeKey;
60
+ canonicalJson: typeof canonicalJson;
61
+ };
62
+ export {};
@@ -0,0 +1,369 @@
1
+ import { TFile, TFolder } from "@lapis-notes/api/vault";
2
+ import { parse, stringify } from "yaml";
3
+ import { canonicalJson, sha256Text } from "./hashes";
4
+ import { hasHiddenApplicationSegment, normalizeConversationScope, } from "../conversations/paths";
5
+ const MEMORY_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,127}$/u;
6
+ const MAX_MEMORY_FILE_BYTES = 512 * 1024;
7
+ export class MemoryRecordConflictError extends Error {
8
+ constructor(message) {
9
+ super(message);
10
+ this.name = "MemoryRecordConflictError";
11
+ }
12
+ }
13
+ export class VaultMemoryRecordStore {
14
+ vault;
15
+ constructor(vault) {
16
+ this.vault = vault;
17
+ }
18
+ async list(scope) {
19
+ const files = scope
20
+ ? this.vault.getFilesByGlob(`${recordDirectory(scope)}/*.md`)
21
+ : [
22
+ ...this.vault.getFilesByGlob(".lapis/agents/memory/user/records/*.md"),
23
+ ...this.vault.getFilesByGlob(".lapis/agents/memory/workspace/records/*.md"),
24
+ ...this.vault.getFilesByGlob("**/.lapis/agents/memory/project/records/*.md"),
25
+ ];
26
+ const unique = new Map(files.map((file) => [file.path, file]));
27
+ const records = [];
28
+ for (const file of [...unique.values()].sort((left, right) => left.path.localeCompare(right.path))) {
29
+ try {
30
+ const content = await this.vault.read(file);
31
+ const stored = await parseStoredRecord(file.path, content);
32
+ if (!scope || sameScope(scope, stored.record.scope))
33
+ records.push(stored);
34
+ }
35
+ catch {
36
+ // A malformed owner-edited memory file is omitted from active retrieval.
37
+ }
38
+ }
39
+ return records;
40
+ }
41
+ async find(id) {
42
+ assertMemoryId(id);
43
+ return (await this.list()).filter((stored) => stored.record.id === id);
44
+ }
45
+ async write(record, options = {}) {
46
+ validateMemoryRecord(record);
47
+ const path = recordPath(record.scope, record.id);
48
+ const currentFile = this.vault.getAbstractFileByPath(path);
49
+ if (currentFile instanceof TFolder) {
50
+ throw new MemoryRecordConflictError(`${path} is a folder`);
51
+ }
52
+ const current = currentFile instanceof TFile
53
+ ? await parseStoredRecord(path, await this.vault.read(currentFile))
54
+ : undefined;
55
+ if (!current && (await this.find(record.id)).length > 0) {
56
+ throw new MemoryRecordConflictError(`Memory ${record.id} already exists in another scope`);
57
+ }
58
+ if (options.expectedCurrentHash !== undefined &&
59
+ current?.hash !== options.expectedCurrentHash) {
60
+ throw new MemoryRecordConflictError(`Memory ${record.id} changed after consolidation preview`);
61
+ }
62
+ if (current) {
63
+ if (current.record.id !== record.id || !sameScope(current.record.scope, record.scope)) {
64
+ throw new MemoryRecordConflictError("Memory identity or scope changed externally");
65
+ }
66
+ if (record.revision !== current.record.revision + 1) {
67
+ throw new MemoryRecordConflictError(`Memory revision must advance from ${current.record.revision} to ${current.record.revision + 1}`);
68
+ }
69
+ if (record.previousRevisionHash !== current.hash) {
70
+ throw new MemoryRecordConflictError("Memory previousRevisionHash does not match the current preimage");
71
+ }
72
+ await this.writeImmutablePreimage(current);
73
+ }
74
+ else if (record.revision !== 1 || record.previousRevisionHash !== undefined) {
75
+ throw new MemoryRecordConflictError("A new memory must begin at revision 1 without a previous hash");
76
+ }
77
+ const content = serializeMemoryRecord(record);
78
+ await this.vault.mkpath(recordDirectory(record.scope));
79
+ if (currentFile instanceof TFile)
80
+ await this.vault.modify(currentFile, content);
81
+ else
82
+ await this.vault.create(path, content);
83
+ return {
84
+ record: structuredClone(record),
85
+ path,
86
+ hash: await sha256Text(content),
87
+ content,
88
+ };
89
+ }
90
+ async listHistory(id, scope) {
91
+ assertMemoryId(id);
92
+ const files = this.vault
93
+ .getFilesByGlob(`${revisionDirectory(scope, id)}/*.md`)
94
+ .sort((left, right) => left.path.localeCompare(right.path));
95
+ const records = [];
96
+ for (const file of files) {
97
+ try {
98
+ records.push((await parseStoredRecord(file.path, await this.vault.read(file))).record);
99
+ }
100
+ catch {
101
+ // Corrupt external revision files stay visible in the vault but not active data.
102
+ }
103
+ }
104
+ return records.sort((left, right) => left.revision - right.revision);
105
+ }
106
+ async writeReview(scope, jobId, markdown) {
107
+ assertMemoryId(jobId);
108
+ const directory = `${memoryRoot(scope)}/reviews`;
109
+ const path = `${directory}/${jobId}.md`;
110
+ if (this.vault.getAbstractFileByPath(path)) {
111
+ throw new MemoryRecordConflictError(`Review already exists: ${jobId}`);
112
+ }
113
+ await this.vault.mkpath(directory);
114
+ await this.vault.create(path, markdown);
115
+ return path;
116
+ }
117
+ async listExcludedConversationIds() {
118
+ const files = [
119
+ ...this.vault.getFilesByGlob(".lapis/agents/memory/exclusions/*.yaml"),
120
+ ...this.vault.getFilesByGlob("**/.lapis/agents/memory/exclusions/*.yaml"),
121
+ ];
122
+ return new Set(files
123
+ .map((file) => file.name.replace(/\.yaml$/u, ""))
124
+ .filter(Boolean));
125
+ }
126
+ async writeConversationExclusion(location) {
127
+ const scope = location.scopeDir
128
+ ? { kind: "project", projectDir: location.scopeDir }
129
+ : { kind: "workspace" };
130
+ const directory = `${memoryRoot(scope)}/exclusions`;
131
+ const path = `${directory}/${location.conversationId}.yaml`;
132
+ const content = stringify({
133
+ schemaVersion: 1,
134
+ conversationId: location.conversationId,
135
+ scopeDirAtExclusion: location.scopeDir,
136
+ excludedAt: new Date().toISOString(),
137
+ }, { lineWidth: 0 });
138
+ await this.vault.mkpath(directory);
139
+ const existing = this.vault.getAbstractFileByPath(path);
140
+ if (existing instanceof TFolder)
141
+ throw new MemoryRecordConflictError(`${path} is a folder`);
142
+ if (existing instanceof TFile)
143
+ return path;
144
+ await this.vault.create(path, content);
145
+ return path;
146
+ }
147
+ async writeImmutablePreimage(current) {
148
+ const directory = revisionDirectory(current.record.scope, current.record.id);
149
+ const path = `${directory}/${current.record.revision}.md`;
150
+ const content = current.content;
151
+ const existing = this.vault.getAbstractFileByPath(path);
152
+ if (existing instanceof TFolder) {
153
+ throw new MemoryRecordConflictError(`${path} is a folder`);
154
+ }
155
+ if (existing instanceof TFile) {
156
+ const existingContent = await this.vault.read(existing);
157
+ if ((await sha256Text(existingContent)) !== current.hash) {
158
+ throw new MemoryRecordConflictError(`Immutable memory revision ${current.record.id}/${current.record.revision} conflicts`);
159
+ }
160
+ return;
161
+ }
162
+ await this.vault.mkpath(directory);
163
+ await this.vault.create(path, content);
164
+ }
165
+ }
166
+ export class InMemoryMemoryRecordStore {
167
+ #records = new Map();
168
+ #history = new Map();
169
+ #reviews = new Map();
170
+ #exclusions = new Map();
171
+ async list(scope) {
172
+ return [...this.#records.values()]
173
+ .filter((stored) => !scope || sameScope(scope, stored.record.scope))
174
+ .map((stored) => structuredClone(stored));
175
+ }
176
+ async find(id) {
177
+ return (await this.list()).filter((stored) => stored.record.id === id);
178
+ }
179
+ async write(record, options = {}) {
180
+ validateMemoryRecord(record);
181
+ const key = `${scopeKey(record.scope)}\u0000${record.id}`;
182
+ const current = this.#records.get(key);
183
+ if (!current &&
184
+ [...this.#records.values()].some((stored) => stored.record.id === record.id)) {
185
+ throw new MemoryRecordConflictError(`Memory ${record.id} already exists in another scope`);
186
+ }
187
+ if (options.expectedCurrentHash !== undefined &&
188
+ current?.hash !== options.expectedCurrentHash) {
189
+ throw new MemoryRecordConflictError("Memory changed after preview");
190
+ }
191
+ if (current) {
192
+ if (record.revision !== current.record.revision + 1 ||
193
+ record.previousRevisionHash !== current.hash) {
194
+ throw new MemoryRecordConflictError("Invalid memory revision preimage");
195
+ }
196
+ const history = this.#history.get(key) ?? [];
197
+ history.push(structuredClone(current.record));
198
+ this.#history.set(key, history);
199
+ }
200
+ else if (record.revision !== 1 || record.previousRevisionHash) {
201
+ throw new MemoryRecordConflictError("Invalid first memory revision");
202
+ }
203
+ const content = serializeMemoryRecord(record);
204
+ const stored = {
205
+ record: structuredClone(record),
206
+ path: recordPath(record.scope, record.id),
207
+ hash: await sha256Text(content),
208
+ content,
209
+ };
210
+ this.#records.set(key, stored);
211
+ return structuredClone(stored);
212
+ }
213
+ async listHistory(id, scope) {
214
+ return structuredClone(this.#history.get(`${scopeKey(scope)}\u0000${id}`) ?? []);
215
+ }
216
+ async writeReview(scope, jobId, markdown) {
217
+ const path = `${memoryRoot(scope)}/reviews/${jobId}.md`;
218
+ if (this.#reviews.has(path))
219
+ throw new MemoryRecordConflictError("Review exists");
220
+ this.#reviews.set(path, markdown);
221
+ return path;
222
+ }
223
+ async listExcludedConversationIds() {
224
+ return new Set(this.#exclusions.keys());
225
+ }
226
+ async writeConversationExclusion(location) {
227
+ const scope = location.scopeDir
228
+ ? { kind: "project", projectDir: location.scopeDir }
229
+ : { kind: "workspace" };
230
+ const path = `${memoryRoot(scope)}/exclusions/${location.conversationId}.yaml`;
231
+ this.#exclusions.set(location.conversationId, path);
232
+ return path;
233
+ }
234
+ }
235
+ export function memoryRoot(scope) {
236
+ return [
237
+ scope.kind === "project" ? scope.projectDir : "",
238
+ ".lapis",
239
+ "agents",
240
+ "memory",
241
+ ]
242
+ .filter(Boolean)
243
+ .join("/");
244
+ }
245
+ export function recordDirectory(scope) {
246
+ return `${memoryRoot(scope)}/${scope.kind}/records`;
247
+ }
248
+ export function recordPath(scope, id) {
249
+ assertMemoryId(id);
250
+ return `${recordDirectory(scope)}/${id}.md`;
251
+ }
252
+ function revisionDirectory(scope, id) {
253
+ return `${memoryRoot(scope)}/revisions/${id}`;
254
+ }
255
+ export function serializeMemoryRecord(record) {
256
+ validateMemoryRecord(record);
257
+ const { summary, ...frontmatter } = record;
258
+ return `---\n${stringify(frontmatter, { lineWidth: 0 })}---\n\n${summary.trim()}\n`;
259
+ }
260
+ async function parseStoredRecord(path, content) {
261
+ if (new TextEncoder().encode(content).byteLength > MAX_MEMORY_FILE_BYTES) {
262
+ throw new Error("Memory record exceeds the maximum supported size");
263
+ }
264
+ const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n(?:\r?\n)?([\s\S]*)$/u.exec(content);
265
+ if (!match)
266
+ throw new Error("Memory record requires YAML frontmatter");
267
+ const metadata = parse(match[1], { maxAliasCount: 10 });
268
+ const record = validateMemoryRecord({
269
+ ...metadata,
270
+ summary: match[2].trim(),
271
+ });
272
+ const expectedPath = recordPath(record.scope, record.id);
273
+ const isRevision = path.startsWith(`${revisionDirectory(record.scope, record.id)}/`);
274
+ if (path !== expectedPath && !isRevision) {
275
+ throw new Error("Memory record path does not match its scope and identity");
276
+ }
277
+ return { record, path, hash: await sha256Text(content), content };
278
+ }
279
+ export function validateMemoryRecord(value) {
280
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
281
+ throw new Error("Memory record must be an object");
282
+ }
283
+ const record = value;
284
+ if (record.schemaVersion !== 1)
285
+ throw new Error("Unsupported memory schema");
286
+ assertMemoryId(record.id);
287
+ if (!Number.isSafeInteger(record.revision) || record.revision < 1) {
288
+ throw new Error("Memory revision is invalid");
289
+ }
290
+ if (!["preference", "fact", "decision", "procedure", "constraint"].includes(record.kind)) {
291
+ throw new Error("Memory kind is invalid");
292
+ }
293
+ validateScope(record.scope);
294
+ if (record.status !== "active" && record.status !== "retracted") {
295
+ throw new Error("Memory status is invalid");
296
+ }
297
+ if (![1, 2, 3, 4, 5].includes(record.importance)) {
298
+ throw new Error("Memory importance is invalid");
299
+ }
300
+ if (!Array.isArray(record.triggers) || record.triggers.some((item) => typeof item !== "string")) {
301
+ throw new Error("Memory triggers are invalid");
302
+ }
303
+ if (typeof record.summary !== "string" || !record.summary.trim()) {
304
+ throw new Error("Memory summary is required");
305
+ }
306
+ if (!Array.isArray(record.evidence))
307
+ throw new Error("Memory evidence is invalid");
308
+ for (const evidence of record.evidence)
309
+ validateEvidence(evidence);
310
+ if (record.createdBy !== "owner-ui" && record.evidence.length === 0) {
311
+ throw new Error("Automatically generated memory requires evidence");
312
+ }
313
+ if (!["owner-ui", "deterministic-promotion", "consolidator"].includes(record.createdBy)) {
314
+ throw new Error("Memory creator is invalid");
315
+ }
316
+ for (const timestamp of [record.createdAt, record.updatedAt]) {
317
+ if (!Number.isFinite(Date.parse(timestamp)))
318
+ throw new Error("Memory timestamp is invalid");
319
+ }
320
+ if (record.previousRevisionHash !== undefined &&
321
+ !/^[0-9a-f]{64}$/u.test(record.previousRevisionHash)) {
322
+ throw new Error("Memory previous revision hash is invalid");
323
+ }
324
+ return structuredClone(record);
325
+ }
326
+ function validateEvidence(evidence) {
327
+ if (!evidence ||
328
+ typeof evidence.conversationId !== "string" ||
329
+ typeof evidence.entryId !== "string" ||
330
+ !/^[0-9a-f]{64}$/u.test(evidence.entryHash) ||
331
+ !Number.isFinite(Date.parse(evidence.observedAt)) ||
332
+ typeof evidence.scopeDirAtObservation !== "string") {
333
+ throw new Error("Memory evidence reference is invalid");
334
+ }
335
+ }
336
+ function validateScope(scope) {
337
+ if (!scope || !["user", "workspace", "project"].includes(scope.kind)) {
338
+ throw new Error("Memory scope is invalid");
339
+ }
340
+ if (scope.kind === "project") {
341
+ let normalized;
342
+ try {
343
+ normalized = normalizeConversationScope(scope.projectDir);
344
+ }
345
+ catch {
346
+ throw new Error("Project memory scope is invalid");
347
+ }
348
+ if (normalized !== scope.projectDir ||
349
+ hasHiddenApplicationSegment(normalized)) {
350
+ throw new Error("Project memory scope is invalid");
351
+ }
352
+ }
353
+ }
354
+ function assertMemoryId(id) {
355
+ if (!MEMORY_ID_PATTERN.test(id))
356
+ throw new Error("Memory ID is invalid");
357
+ }
358
+ function sameScope(left, right) {
359
+ return scopeKey(left) === scopeKey(right);
360
+ }
361
+ function scopeKey(scope) {
362
+ return scope.kind === "project" ? `project:${scope.projectDir}` : `${scope.kind}:`;
363
+ }
364
+ export const __memoryRecordStoreInternals = {
365
+ parseStoredRecord,
366
+ revisionDirectory,
367
+ scopeKey,
368
+ canonicalJson,
369
+ };
@@ -0,0 +1,21 @@
1
+ import type { AppDatabase } from "@lapis-notes/api";
2
+ import type { ConsolidationResult, MemoryScope, MemoryService } from "./types";
3
+ export type MemorySchedulerOptions = {
4
+ ownerId?: string;
5
+ idleMs?: number;
6
+ dailyMs?: number;
7
+ leaseMs?: number;
8
+ now?: () => number;
9
+ onError?: (operation: "consolidate", scope: MemoryScope, error: unknown) => void;
10
+ };
11
+ export declare class MemoryMaintenanceScheduler {
12
+ #private;
13
+ private readonly database;
14
+ private readonly memory;
15
+ constructor(database: AppDatabase, memory: MemoryService, options?: MemorySchedulerOptions);
16
+ start(): void;
17
+ noteActivity(scope: MemoryScope): void;
18
+ run(scope: MemoryScope): Promise<ConsolidationResult | null>;
19
+ dispose(): void;
20
+ private runDaily;
21
+ }