om-memory-system 3.0.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 (221) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +587 -0
  3. package/dist/adapters/opencode/auto-capture-host.d.ts +3 -0
  4. package/dist/adapters/opencode/auto-capture-host.js +29 -0
  5. package/dist/adapters/opencode/auto-capture-summary.d.ts +4 -0
  6. package/dist/adapters/opencode/auto-capture-summary.js +147 -0
  7. package/dist/adapters/opencode/conversation.d.ts +2 -0
  8. package/dist/adapters/opencode/conversation.js +50 -0
  9. package/dist/adapters/opencode/user-prompt.d.ts +9 -0
  10. package/dist/adapters/opencode/user-prompt.js +26 -0
  11. package/dist/adapters/pi/capture.d.ts +41 -0
  12. package/dist/adapters/pi/capture.js +91 -0
  13. package/dist/adapters/pi/conversation.d.ts +38 -0
  14. package/dist/adapters/pi/conversation.js +118 -0
  15. package/dist/adapters/pi/extension.d.ts +18 -0
  16. package/dist/adapters/pi/extension.js +234 -0
  17. package/dist/adapters/pi/import-command.d.ts +29 -0
  18. package/dist/adapters/pi/import-command.js +232 -0
  19. package/dist/adapters/pi/profile.d.ts +39 -0
  20. package/dist/adapters/pi/profile.js +148 -0
  21. package/dist/adapters/pi/provider.d.ts +50 -0
  22. package/dist/adapters/pi/provider.js +77 -0
  23. package/dist/config.d.ts +290 -0
  24. package/dist/config.js +742 -0
  25. package/dist/core/capture-context.d.ts +4 -0
  26. package/dist/core/capture-context.js +132 -0
  27. package/dist/core/capture.d.ts +15 -0
  28. package/dist/core/capture.js +68 -0
  29. package/dist/core/extraction.d.ts +80 -0
  30. package/dist/core/extraction.js +140 -0
  31. package/dist/core/host.d.ts +52 -0
  32. package/dist/core/host.js +1 -0
  33. package/dist/core/memory-operations.d.ts +27 -0
  34. package/dist/core/memory-operations.js +247 -0
  35. package/dist/core/retrieval.d.ts +12 -0
  36. package/dist/core/retrieval.js +72 -0
  37. package/dist/importer/discovery.d.ts +31 -0
  38. package/dist/importer/discovery.js +88 -0
  39. package/dist/importer/importer.d.ts +87 -0
  40. package/dist/importer/importer.js +260 -0
  41. package/dist/importer/ledger.d.ts +44 -0
  42. package/dist/importer/ledger.js +137 -0
  43. package/dist/importer/session-loader.d.ts +19 -0
  44. package/dist/importer/session-loader.js +12 -0
  45. package/dist/index.d.ts +24 -0
  46. package/dist/index.js +601 -0
  47. package/dist/plugin.d.ts +9 -0
  48. package/dist/plugin.js +11 -0
  49. package/dist/services/ai/ai-provider-factory.d.ts +7 -0
  50. package/dist/services/ai/ai-provider-factory.js +41 -0
  51. package/dist/services/ai/api-key-placeholder.d.ts +1 -0
  52. package/dist/services/ai/api-key-placeholder.js +14 -0
  53. package/dist/services/ai/internal-capture-sessions.d.ts +6 -0
  54. package/dist/services/ai/internal-capture-sessions.js +33 -0
  55. package/dist/services/ai/opencode-diagnostics.d.ts +7 -0
  56. package/dist/services/ai/opencode-diagnostics.js +36 -0
  57. package/dist/services/ai/opencode-host-config.d.ts +10 -0
  58. package/dist/services/ai/opencode-host-config.js +51 -0
  59. package/dist/services/ai/opencode-provider-loader.d.ts +3 -0
  60. package/dist/services/ai/opencode-provider-loader.js +4 -0
  61. package/dist/services/ai/opencode-provider.d.ts +92 -0
  62. package/dist/services/ai/opencode-provider.js +419 -0
  63. package/dist/services/ai/opencode-sdk-client.d.ts +6 -0
  64. package/dist/services/ai/opencode-sdk-client.js +38 -0
  65. package/dist/services/ai/profile-llm-client.d.ts +2 -0
  66. package/dist/services/ai/profile-llm-client.js +27 -0
  67. package/dist/services/ai/provider-config.d.ts +17 -0
  68. package/dist/services/ai/provider-config.js +30 -0
  69. package/dist/services/ai/providers/anthropic-messages.d.ts +34 -0
  70. package/dist/services/ai/providers/anthropic-messages.js +220 -0
  71. package/dist/services/ai/providers/base-provider.d.ts +24 -0
  72. package/dist/services/ai/providers/base-provider.js +24 -0
  73. package/dist/services/ai/providers/google-gemini.d.ts +15 -0
  74. package/dist/services/ai/providers/google-gemini.js +228 -0
  75. package/dist/services/ai/providers/minimax.d.ts +35 -0
  76. package/dist/services/ai/providers/minimax.js +55 -0
  77. package/dist/services/ai/providers/openai-chat-completion.d.ts +22 -0
  78. package/dist/services/ai/providers/openai-chat-completion.js +387 -0
  79. package/dist/services/ai/providers/openai-responses.d.ts +13 -0
  80. package/dist/services/ai/providers/openai-responses.js +182 -0
  81. package/dist/services/ai/providers/orcarouter.d.ts +51 -0
  82. package/dist/services/ai/providers/orcarouter.js +66 -0
  83. package/dist/services/ai/session/ai-session-manager.d.ts +24 -0
  84. package/dist/services/ai/session/ai-session-manager.js +211 -0
  85. package/dist/services/ai/session/session-types.d.ts +42 -0
  86. package/dist/services/ai/session/session-types.js +1 -0
  87. package/dist/services/ai/tools/tool-schema.d.ts +40 -0
  88. package/dist/services/ai/tools/tool-schema.js +24 -0
  89. package/dist/services/ai/validators/user-profile-validator.d.ts +12 -0
  90. package/dist/services/ai/validators/user-profile-validator.js +114 -0
  91. package/dist/services/api-handlers.d.ts +186 -0
  92. package/dist/services/api-handlers.js +1284 -0
  93. package/dist/services/auth-token.d.ts +14 -0
  94. package/dist/services/auth-token.js +59 -0
  95. package/dist/services/auto-capture.d.ts +3 -0
  96. package/dist/services/auto-capture.js +127 -0
  97. package/dist/services/cleanup-service.d.ts +22 -0
  98. package/dist/services/cleanup-service.js +109 -0
  99. package/dist/services/client.d.ts +134 -0
  100. package/dist/services/client.js +327 -0
  101. package/dist/services/context.d.ts +10 -0
  102. package/dist/services/context.js +27 -0
  103. package/dist/services/cors.d.ts +12 -0
  104. package/dist/services/cors.js +52 -0
  105. package/dist/services/deduplication-service.d.ts +28 -0
  106. package/dist/services/deduplication-service.js +111 -0
  107. package/dist/services/embedding.d.ts +36 -0
  108. package/dist/services/embedding.js +200 -0
  109. package/dist/services/jsonc.d.ts +6 -0
  110. package/dist/services/jsonc.js +76 -0
  111. package/dist/services/language-detector.d.ts +2 -0
  112. package/dist/services/language-detector.js +33 -0
  113. package/dist/services/legacy-migration.d.ts +77 -0
  114. package/dist/services/legacy-migration.js +343 -0
  115. package/dist/services/logger.d.ts +1 -0
  116. package/dist/services/logger.js +119 -0
  117. package/dist/services/memory-portability-service.d.ts +37 -0
  118. package/dist/services/memory-portability-service.js +340 -0
  119. package/dist/services/memory-scope.d.ts +16 -0
  120. package/dist/services/memory-scope.js +43 -0
  121. package/dist/services/migration-service.d.ts +42 -0
  122. package/dist/services/migration-service.js +307 -0
  123. package/dist/services/onnxruntime-resolve.d.ts +24 -0
  124. package/dist/services/onnxruntime-resolve.js +174 -0
  125. package/dist/services/privacy.d.ts +26 -0
  126. package/dist/services/privacy.js +69 -0
  127. package/dist/services/request-timeouts.d.ts +9 -0
  128. package/dist/services/request-timeouts.js +11 -0
  129. package/dist/services/runtime-require.d.ts +17 -0
  130. package/dist/services/runtime-require.js +75 -0
  131. package/dist/services/secret-resolver.d.ts +1 -0
  132. package/dist/services/secret-resolver.js +65 -0
  133. package/dist/services/shard-inventory-service.d.ts +69 -0
  134. package/dist/services/shard-inventory-service.js +315 -0
  135. package/dist/services/shard-path-migration-service.d.ts +31 -0
  136. package/dist/services/shard-path-migration-service.js +367 -0
  137. package/dist/services/tag-prefix-migration.d.ts +60 -0
  138. package/dist/services/tag-prefix-migration.js +439 -0
  139. package/dist/services/tag-prefix-scan.d.ts +26 -0
  140. package/dist/services/tag-prefix-scan.js +91 -0
  141. package/dist/services/tags.d.ts +29 -0
  142. package/dist/services/tags.js +251 -0
  143. package/dist/services/turso/connection-manager.d.ts +15 -0
  144. package/dist/services/turso/connection-manager.js +163 -0
  145. package/dist/services/turso/cross-process-write-lock.d.ts +1 -0
  146. package/dist/services/turso/cross-process-write-lock.js +89 -0
  147. package/dist/services/turso/legacy-migrator.d.ts +1 -0
  148. package/dist/services/turso/legacy-migrator.js +540 -0
  149. package/dist/services/turso/lifecycle.d.ts +1 -0
  150. package/dist/services/turso/lifecycle.js +14 -0
  151. package/dist/services/turso/operation-lock.d.ts +2 -0
  152. package/dist/services/turso/operation-lock.js +61 -0
  153. package/dist/services/turso/ready.d.ts +2 -0
  154. package/dist/services/turso/ready.js +36 -0
  155. package/dist/services/turso/shard-manager.d.ts +38 -0
  156. package/dist/services/turso/shard-manager.js +496 -0
  157. package/dist/services/turso/sqlite-handle-release.d.ts +16 -0
  158. package/dist/services/turso/sqlite-handle-release.js +56 -0
  159. package/dist/services/turso/turso-db.d.ts +18 -0
  160. package/dist/services/turso/turso-db.js +53 -0
  161. package/dist/services/turso/types.d.ts +42 -0
  162. package/dist/services/turso/types.js +1 -0
  163. package/dist/services/turso/vector-search.d.ts +41 -0
  164. package/dist/services/turso/vector-search.js +384 -0
  165. package/dist/services/turso/vector-utils.d.ts +7 -0
  166. package/dist/services/turso/vector-utils.js +67 -0
  167. package/dist/services/user-memory-learning.d.ts +97 -0
  168. package/dist/services/user-memory-learning.js +673 -0
  169. package/dist/services/user-profile/ai-cleanup.d.ts +58 -0
  170. package/dist/services/user-profile/ai-cleanup.js +486 -0
  171. package/dist/services/user-profile/profile-context.d.ts +1 -0
  172. package/dist/services/user-profile/profile-context.js +105 -0
  173. package/dist/services/user-profile/profile-utils.d.ts +2 -0
  174. package/dist/services/user-profile/profile-utils.js +45 -0
  175. package/dist/services/user-profile/types.d.ts +92 -0
  176. package/dist/services/user-profile/types.js +1 -0
  177. package/dist/services/user-profile/user-profile-manager.d.ts +65 -0
  178. package/dist/services/user-profile/user-profile-manager.js +1660 -0
  179. package/dist/services/user-prompt/user-prompt-manager.d.ts +53 -0
  180. package/dist/services/user-prompt/user-prompt-manager.js +303 -0
  181. package/dist/services/web-api-auth.d.ts +3 -0
  182. package/dist/services/web-api-auth.js +25 -0
  183. package/dist/services/web-auth.d.ts +46 -0
  184. package/dist/services/web-auth.js +105 -0
  185. package/dist/services/web-server.d.ts +57 -0
  186. package/dist/services/web-server.js +659 -0
  187. package/dist/shared/api/index.d.ts +4 -0
  188. package/dist/shared/api/index.js +2 -0
  189. package/dist/shared/api/portability-schemas.d.ts +56 -0
  190. package/dist/shared/api/portability-schemas.js +39 -0
  191. package/dist/shared/api/schemas.d.ts +295 -0
  192. package/dist/shared/api/schemas.js +134 -0
  193. package/dist/shared/api/types.d.ts +28 -0
  194. package/dist/shared/api/types.js +1 -0
  195. package/dist/types/index.d.ts +27 -0
  196. package/dist/types/index.js +1 -0
  197. package/dist/utils/context-limit.d.ts +10 -0
  198. package/dist/utils/context-limit.js +43 -0
  199. package/dist/utils/math.d.ts +7 -0
  200. package/dist/utils/math.js +48 -0
  201. package/dist/utils/profile.d.ts +2 -0
  202. package/dist/utils/profile.js +15 -0
  203. package/dist/v2/adapter.d.ts +5 -0
  204. package/dist/v2/adapter.js +218 -0
  205. package/dist/v2/legacy-client.d.ts +15 -0
  206. package/dist/v2/legacy-client.js +257 -0
  207. package/dist/v2/memory-bridge.d.ts +18 -0
  208. package/dist/v2/memory-bridge.js +29 -0
  209. package/dist/v2/plugin.d.ts +3 -0
  210. package/dist/v2/plugin.js +24 -0
  211. package/dist/web/assets/index-DPjEpD1h.css +2 -0
  212. package/dist/web/assets/index-do26c4JJ.js +122 -0
  213. package/dist/web/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
  214. package/dist/web/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
  215. package/dist/web/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
  216. package/dist/web/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
  217. package/dist/web/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
  218. package/dist/web/favicon.ico +0 -0
  219. package/dist/web/index.html +14 -0
  220. package/dist/web/omms-icon.png +0 -0
  221. package/package.json +138 -0
@@ -0,0 +1,48 @@
1
+ export function cosineSimilarity(a, b) {
2
+ if (a.length !== b.length)
3
+ return 0;
4
+ let dot = 0;
5
+ let magA = 0;
6
+ let magB = 0;
7
+ for (let i = 0; i < a.length; i++) {
8
+ const av = a[i] ?? 0;
9
+ const bv = b[i] ?? 0;
10
+ dot += av * bv;
11
+ magA += av * av;
12
+ magB += bv * bv;
13
+ }
14
+ if (magA === 0 || magB === 0)
15
+ return 0;
16
+ return dot / (Math.sqrt(magA) * Math.sqrt(magB));
17
+ }
18
+ /**
19
+ * Cosine similarity for number[] (JSON-stored centroids), otherwise
20
+ * identical to the Float32Array version above.
21
+ */
22
+ export function cosineSimilarityNumbers(a, b) {
23
+ if (a.length !== b.length)
24
+ return 0;
25
+ let dot = 0;
26
+ let magA = 0;
27
+ let magB = 0;
28
+ for (let i = 0; i < a.length; i++) {
29
+ const av = a[i] ?? 0;
30
+ const bv = b[i] ?? 0;
31
+ dot += av * bv;
32
+ magA += av * av;
33
+ magB += bv * bv;
34
+ }
35
+ if (magA === 0 || magB === 0)
36
+ return 0;
37
+ return dot / (Math.sqrt(magA) * Math.sqrt(magB));
38
+ }
39
+ export function l2Normalize(vec) {
40
+ let norm = 0;
41
+ for (let i = 0; i < vec.length; i++) {
42
+ norm += (vec[i] ?? 0) * (vec[i] ?? 0);
43
+ }
44
+ norm = Math.sqrt(norm);
45
+ if (norm === 0)
46
+ return vec;
47
+ return vec.map((v) => v / norm);
48
+ }
@@ -0,0 +1,2 @@
1
+ export declare function isFrozen(item: any): boolean;
2
+ export declare function sortProfileItems(items: any[], metric: "confidence" | "frequency"): any[];
@@ -0,0 +1,15 @@
1
+ export function isFrozen(item) {
2
+ return (item.driftBelowCount || 0) >= 2;
3
+ }
4
+ export function sortProfileItems(items, metric) {
5
+ return [...items].sort((a, b) => {
6
+ const aFrozen = isFrozen(a) ? 1 : 0;
7
+ const bFrozen = isFrozen(b) ? 1 : 0;
8
+ if (aFrozen !== bFrozen)
9
+ return aFrozen - bFrozen;
10
+ if (metric === "confidence") {
11
+ return (b.confidence || 0) - (a.confidence || 0) || (b.frequency || 1) - (a.frequency || 1);
12
+ }
13
+ return (b.frequency || 0) - (a.frequency || 0);
14
+ });
15
+ }
@@ -0,0 +1,5 @@
1
+ import type { Context } from "@opencode/plugin/promise/plugin";
2
+ import type { V2MemoryBridge } from "./memory-bridge.js";
3
+ /** Per-session state is bounded so long-running servers cannot grow it without limit. */
4
+ export declare const MAX_TRACKED_SESSIONS = 256;
5
+ export declare function registerV2Adapter(ctx: Context, legacy: any, memory: V2MemoryBridge): Promise<() => Promise<void>>;
@@ -0,0 +1,218 @@
1
+ import { log } from "../services/logger.js";
2
+ import { eventBelongsToLocation, legacyToolResult, toLegacyEvent } from "./legacy-client.js";
3
+ const memoryInput = {
4
+ type: "object",
5
+ properties: {
6
+ mode: {
7
+ type: "string",
8
+ enum: [
9
+ "add",
10
+ "search",
11
+ "profile",
12
+ "list",
13
+ "forget",
14
+ "help",
15
+ "migrate",
16
+ "list-shards",
17
+ "export",
18
+ "import",
19
+ ],
20
+ },
21
+ content: { type: "string" },
22
+ query: { type: "string" },
23
+ tags: { type: "string" },
24
+ type: { type: "string" },
25
+ memoryId: { type: "string" },
26
+ limit: { type: "number" },
27
+ scope: { type: "string", enum: ["project", "all-projects"] },
28
+ fromPath: { type: "string" },
29
+ fromHash: { type: "string" },
30
+ outputPath: { type: "string" },
31
+ inputPath: { type: "string" },
32
+ dryRun: { type: "boolean" },
33
+ allowLinkedSource: { type: "boolean" },
34
+ },
35
+ additionalProperties: false,
36
+ };
37
+ /** Per-session state is bounded so long-running servers cannot grow it without limit. */
38
+ export const MAX_TRACKED_SESSIONS = 256;
39
+ /** Unadmitted prompts kept per session before the oldest is dropped. */
40
+ const MAX_PENDING_PROMPTS = 16;
41
+ /** Compaction is restored natively through the `compaction` hook, never via the V1 event path. */
42
+ const COMPACTION_EVENT_TYPES = new Set(["session.compacted", "session.compaction.ended"]);
43
+ function setBounded(map, key, value) {
44
+ map.delete(key);
45
+ map.set(key, value);
46
+ while (map.size > MAX_TRACKED_SESSIONS) {
47
+ const oldest = map.keys().next().value;
48
+ if (oldest === undefined)
49
+ break;
50
+ map.delete(oldest);
51
+ }
52
+ }
53
+ function withTimeout(promise, ms, fallback) {
54
+ let timer;
55
+ const timeout = new Promise((resolve) => {
56
+ timer = setTimeout(() => resolve(fallback), ms);
57
+ });
58
+ return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
59
+ }
60
+ function deletedSessionID(event) {
61
+ if (event.type !== "session.deleted")
62
+ return undefined;
63
+ const props = event.properties ?? {};
64
+ return props.sessionID ?? props.info?.id ?? props.session?.id;
65
+ }
66
+ export async function registerV2Adapter(ctx, legacy, memory) {
67
+ const retrievals = new Map();
68
+ const restored = new Map();
69
+ const messageIDs = new Map();
70
+ // The prompt hook runs before OpenCode admits a prompt, so prompts wait here
71
+ // until their message appears in a model request's history (the context
72
+ // hook), and are only then recorded for auto-capture and profile learning.
73
+ const pendingPrompts = new Map();
74
+ const forgetSession = (sessionID) => {
75
+ retrievals.delete(sessionID);
76
+ restored.delete(sessionID);
77
+ messageIDs.delete(sessionID);
78
+ pendingPrompts.delete(sessionID);
79
+ };
80
+ const recordAdmittedPrompts = async (sessionID, messages) => {
81
+ const pending = pendingPrompts.get(sessionID);
82
+ if (!pending?.length)
83
+ return;
84
+ const admitted = new Set(messages.map((message) => message.id).filter(Boolean));
85
+ const remaining = [];
86
+ for (const prompt of pending) {
87
+ if (!admitted.has(prompt.messageID)) {
88
+ remaining.push(prompt);
89
+ continue;
90
+ }
91
+ try {
92
+ await memory.recordPrompt(sessionID, prompt.messageID, prompt.text);
93
+ }
94
+ catch (error) {
95
+ log("v2 context: failed to record user prompt", { sessionID, error: String(error) });
96
+ }
97
+ }
98
+ if (remaining.length > 0)
99
+ pendingPrompts.set(sessionID, remaining);
100
+ else
101
+ pendingPrompts.delete(sessionID);
102
+ };
103
+ // Resolve a prompt's retrieval once; every model step for that prompt then
104
+ // reuses the same section, even if the search finishes after the timeout.
105
+ const settledRetrieval = async (retrieval) => {
106
+ if (!retrieval.settled) {
107
+ const value = await withTimeout(retrieval.promise, memory.retrievalTimeoutMs, null);
108
+ retrieval.settled ??= { value };
109
+ }
110
+ return retrieval.settled.value;
111
+ };
112
+ await ctx.tool.transform((editor) => editor.add({
113
+ name: "memory",
114
+ description: legacy.tool.memory.description,
115
+ input: memoryInput,
116
+ // A direct tool like OpenCode's own read/grep/skill, not a Code Mode catalog entry,
117
+ // so the model can always see and call it (as it can on V1 and Pi).
118
+ options: { codemode: false },
119
+ execute: async (args, toolContext) => legacyToolResult(await legacy.tool.memory.execute(args, {
120
+ sessionID: toolContext.sessionID,
121
+ messageID: toolContext.messageID,
122
+ agent: toolContext.agent,
123
+ directory: ctx.location.directory,
124
+ worktree: ctx.location.project.directory,
125
+ abort: new AbortController().signal,
126
+ metadata() { },
127
+ async ask() { },
128
+ })),
129
+ }));
130
+ await ctx.session.hook("prompt", async (event) => {
131
+ const sessionID = event.sessionID;
132
+ const text = event.prompt.text ?? "";
133
+ setBounded(messageIDs, sessionID, event.messageID);
134
+ retrievals.delete(sessionID);
135
+ if (!memory.isConfigured() || memory.isInternalPrompt(sessionID, text))
136
+ return;
137
+ if (text.trim()) {
138
+ const pending = [
139
+ ...(pendingPrompts.get(sessionID) ?? []),
140
+ { messageID: event.messageID, text },
141
+ ].slice(-MAX_PENDING_PROMPTS);
142
+ setBounded(pendingPrompts, sessionID, pending);
143
+ }
144
+ if (!memory.isInjectionEnabled() || !text.trim())
145
+ return;
146
+ // Start the search now so it overlaps prompt admission; `context` awaits it.
147
+ const promise = memory.retrieve(text, sessionID).catch((error) => {
148
+ log("v2 prompt: memory retrieval failed", { sessionID, error: String(error) });
149
+ return null;
150
+ });
151
+ setBounded(retrievals, sessionID, { promise });
152
+ });
153
+ await ctx.session.hook("context", async (event) => {
154
+ const sessionID = event.sessionID;
155
+ // Before chat.params, which stores the model on the recorded prompt row.
156
+ await recordAdmittedPrompts(sessionID, event.messages);
157
+ const retrieval = retrievals.get(sessionID);
158
+ if (retrieval) {
159
+ const section = await settledRetrieval(retrieval);
160
+ if (section)
161
+ event.system.push({ type: "text", text: section });
162
+ }
163
+ const restoredSection = restored.get(sessionID);
164
+ if (restoredSection)
165
+ event.system.push({ type: "text", text: restoredSection });
166
+ if (legacy["chat.params"]) {
167
+ await legacy["chat.params"]({
168
+ message: { id: messageIDs.get(sessionID) ?? sessionID },
169
+ model: { providerID: event.model.providerID, id: event.model.id },
170
+ });
171
+ }
172
+ });
173
+ await ctx.session.hook("compaction", async (event) => {
174
+ if (!memory.isCompactionEnabled())
175
+ return;
176
+ const sessionID = event.sessionID;
177
+ try {
178
+ const section = await memory.restoreSession(sessionID);
179
+ if (section)
180
+ setBounded(restored, sessionID, section);
181
+ else
182
+ restored.delete(sessionID);
183
+ }
184
+ catch (error) {
185
+ log("v2 compaction: failed to load session memories", { sessionID, error: String(error) });
186
+ }
187
+ });
188
+ const controller = new AbortController();
189
+ const watcher = (async () => {
190
+ try {
191
+ for await (const raw of ctx.event.subscribe({ signal: controller.signal })) {
192
+ const event = toLegacyEvent(raw);
193
+ const deleted = deletedSessionID(event);
194
+ if (deleted)
195
+ forgetSession(deleted);
196
+ if (!legacy.event || COMPACTION_EVENT_TYPES.has(event.type))
197
+ continue;
198
+ if (await eventBelongsToLocation(ctx, raw)) {
199
+ await legacy.event({ event });
200
+ }
201
+ }
202
+ }
203
+ catch (error) {
204
+ if (!controller.signal.aborted) {
205
+ log("omms v2 event bridge failed", { error: String(error) });
206
+ }
207
+ }
208
+ })();
209
+ return async () => {
210
+ controller.abort();
211
+ await watcher;
212
+ retrievals.clear();
213
+ restored.clear();
214
+ messageIDs.clear();
215
+ pendingPrompts.clear();
216
+ await legacy.dispose?.();
217
+ };
218
+ }
@@ -0,0 +1,15 @@
1
+ import type { Context } from "@opencode/plugin/promise/plugin";
2
+ /**
3
+ * Adapts the released OpenCode v2 plugin context to the V1 client shape used
4
+ * by the shared omms implementation.
5
+ */
6
+ export declare function createLegacyClient(ctx: Context): any;
7
+ export declare function legacyToolResult(value: unknown): {
8
+ content: string;
9
+ metadata?: unknown;
10
+ };
11
+ export declare function toLegacyEvent(raw: any): {
12
+ type: string;
13
+ properties: any;
14
+ };
15
+ export declare function eventBelongsToLocation(ctx: Context, raw: any): Promise<boolean>;
@@ -0,0 +1,257 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { resolve } from "node:path";
3
+ function textFromParts(parts = []) {
4
+ return parts
5
+ .filter((part) => part.type === "text")
6
+ .map((part) => part.text ?? "")
7
+ .join("\n");
8
+ }
9
+ function parseJson(text) {
10
+ const trimmed = text.trim();
11
+ const fenced = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i)?.[1];
12
+ const candidate = fenced ?? trimmed;
13
+ try {
14
+ return JSON.parse(candidate);
15
+ }
16
+ catch {
17
+ const start = candidate.indexOf("{");
18
+ const end = candidate.lastIndexOf("}");
19
+ if (start >= 0 && end > start) {
20
+ return JSON.parse(candidate.slice(start, end + 1));
21
+ }
22
+ throw new Error("Model did not return a JSON object");
23
+ }
24
+ }
25
+ function legacyMessage(message, sessionID) {
26
+ if (message.type === "user" || message.type === "synthetic" || message.type === "system") {
27
+ return {
28
+ info: { id: message.id, sessionID, role: "user", agent: message.agent },
29
+ parts: [
30
+ {
31
+ id: `${message.id}-text`,
32
+ sessionID,
33
+ messageID: message.id,
34
+ type: "text",
35
+ text: message.text ?? "",
36
+ synthetic: message.type !== "user",
37
+ },
38
+ ],
39
+ };
40
+ }
41
+ if (message.type === "assistant") {
42
+ return {
43
+ info: {
44
+ id: message.id,
45
+ sessionID,
46
+ role: "assistant",
47
+ agent: message.agent,
48
+ mode: message.agent,
49
+ },
50
+ parts: (message.content ?? []).map((part, index) => ({
51
+ id: part.id ?? `${message.id}-${index}`,
52
+ sessionID,
53
+ messageID: message.id,
54
+ ...part,
55
+ })),
56
+ };
57
+ }
58
+ if (message.type === "compaction") {
59
+ return {
60
+ info: {
61
+ id: message.id,
62
+ sessionID,
63
+ role: "assistant",
64
+ summary: true,
65
+ mode: "compaction",
66
+ },
67
+ parts: [],
68
+ };
69
+ }
70
+ return {
71
+ info: { id: message.id ?? randomUUID(), sessionID, role: "assistant" },
72
+ parts: [],
73
+ };
74
+ }
75
+ function sessionIDFrom(input) {
76
+ return input?.sessionID ?? input?.path?.id;
77
+ }
78
+ function bodyFrom(input) {
79
+ return input?.body ?? input ?? {};
80
+ }
81
+ function schemaPrompt(body) {
82
+ const sections = [body.system, textFromParts(body.parts)];
83
+ if (body.format?.type === "json_schema" && body.format.schema) {
84
+ sections.push([
85
+ "Return only one JSON object matching this JSON Schema.",
86
+ "Do not wrap the JSON in Markdown.",
87
+ JSON.stringify(body.format.schema),
88
+ ].join("\n"));
89
+ }
90
+ return sections.filter(Boolean).join("\n\n");
91
+ }
92
+ function toastFallback(input) {
93
+ const body = bodyFrom(input);
94
+ const message = [body.title, body.message].filter(Boolean).join(": ");
95
+ if (message) {
96
+ const method = body.variant === "error" ? "error" : body.variant === "warning" ? "warn" : "info";
97
+ console[method](`[omms] ${message}`);
98
+ }
99
+ return { data: false };
100
+ }
101
+ /**
102
+ * Adapts the released OpenCode v2 plugin context to the V1 client shape used
103
+ * by the shared omms implementation.
104
+ */
105
+ export function createLegacyClient(ctx) {
106
+ const generatedSessions = new Set();
107
+ const client = {
108
+ app: { log: async () => ({ data: true }) },
109
+ provider: {
110
+ list: async () => {
111
+ const models = await ctx.model.list();
112
+ const connected = [
113
+ ...new Set(models.data.map((model) => model.providerID).filter(Boolean)),
114
+ ];
115
+ return { data: { connected } };
116
+ },
117
+ },
118
+ tui: {
119
+ showToast: async (input) => toastFallback(input),
120
+ appendPrompt: async () => ({ data: false }),
121
+ submitPrompt: async () => ({ data: false }),
122
+ },
123
+ session: {
124
+ get: async (input) => {
125
+ const sessionID = sessionIDFrom(input);
126
+ if (!sessionID)
127
+ throw new Error("session id required");
128
+ return { data: await ctx.session.get({ sessionID }) };
129
+ },
130
+ messages: async (input) => {
131
+ const sessionID = sessionIDFrom(input);
132
+ if (!sessionID)
133
+ throw new Error("session id required");
134
+ return {
135
+ data: (await ctx.session.context({ sessionID })).map((message) => legacyMessage(message, sessionID)),
136
+ };
137
+ },
138
+ create: async () => {
139
+ // OpenCode v2's plugin Generate API is intentionally sessionless.
140
+ // Keep a synthetic id so the existing structured-output lifecycle can
141
+ // retain its create/prompt/delete contract without polluting history.
142
+ const id = randomUUID();
143
+ generatedSessions.add(id);
144
+ return { data: { id } };
145
+ },
146
+ prompt: async (input) => {
147
+ const sessionID = sessionIDFrom(input);
148
+ if (!sessionID)
149
+ throw new Error("session id required");
150
+ const body = bodyFrom(input);
151
+ if (generatedSessions.has(sessionID)) {
152
+ const model = body.model?.providerID && body.model?.modelID
153
+ ? { providerID: body.model.providerID, id: body.model.modelID }
154
+ : undefined;
155
+ const generated = await ctx.generate.text({
156
+ prompt: schemaPrompt(body),
157
+ ...(model ? { model } : {}),
158
+ });
159
+ const text = generated?.text ?? "";
160
+ const structured = body.format?.type === "json_schema" ? parseJson(text) : undefined;
161
+ return {
162
+ data: {
163
+ info: {
164
+ id: randomUUID(),
165
+ role: "assistant",
166
+ structured_output: structured,
167
+ structured,
168
+ },
169
+ parts: [{ type: "text", text }],
170
+ },
171
+ };
172
+ }
173
+ const text = textFromParts(body.parts);
174
+ if (body.noReply) {
175
+ const data = await ctx.session.synthetic({
176
+ sessionID,
177
+ text,
178
+ description: "memory context",
179
+ metadata: body.parts?.[0]?.metadata,
180
+ });
181
+ return { data, response: new Response(null, { status: 200 }) };
182
+ }
183
+ const data = await ctx.session.prompt({
184
+ sessionID,
185
+ text,
186
+ delivery: "queue",
187
+ metadata: body.parts?.[0]?.metadata,
188
+ });
189
+ return { data, response: new Response(null, { status: 200 }) };
190
+ },
191
+ abort: async (input) => {
192
+ const sessionID = sessionIDFrom(input);
193
+ if (sessionID && generatedSessions.has(sessionID)) {
194
+ generatedSessions.delete(sessionID);
195
+ return { data: true };
196
+ }
197
+ if (sessionID) {
198
+ const result = await ctx.session.interrupt({ sessionID });
199
+ return { data: result.interrupted };
200
+ }
201
+ return { data: false };
202
+ },
203
+ delete: async (input) => {
204
+ const sessionID = sessionIDFrom(input);
205
+ if (sessionID)
206
+ generatedSessions.delete(sessionID);
207
+ return { data: true };
208
+ },
209
+ },
210
+ };
211
+ return client;
212
+ }
213
+ export function legacyToolResult(value) {
214
+ if (typeof value === "string")
215
+ return { content: value };
216
+ if (value && typeof value === "object" && typeof value.output === "string") {
217
+ return { content: value.output, metadata: value.metadata };
218
+ }
219
+ return { content: JSON.stringify(value ?? null) };
220
+ }
221
+ export function toLegacyEvent(raw) {
222
+ const envelope = raw?.payload ?? raw;
223
+ const source = envelope?.type === "sync" && envelope.syncEvent ? envelope.syncEvent : envelope;
224
+ const rawType = typeof source?.type === "string" ? source.type.replace(/\.1$/, "") : source?.type;
225
+ const type = rawType === "session.compaction.ended" ? "session.compacted" : rawType;
226
+ const data = source?.data ?? {};
227
+ if (source && typeof source === "object" && "properties" in source) {
228
+ return { type, properties: source.properties };
229
+ }
230
+ if (type === "session.created" || type === "session.updated") {
231
+ return { type, properties: { info: data.session ?? data.info ?? data } };
232
+ }
233
+ return { type, properties: data };
234
+ }
235
+ export async function eventBelongsToLocation(ctx, raw) {
236
+ const directory = raw?.location?.directory ??
237
+ raw?.directory ??
238
+ raw?.payload?.location?.directory ??
239
+ raw?.payload?.directory ??
240
+ raw?.data?.info?.directory;
241
+ if (typeof directory === "string") {
242
+ return resolve(directory) === resolve(ctx.location.directory);
243
+ }
244
+ const data = raw?.data ?? raw?.payload?.data ?? raw?.properties;
245
+ const sessionID = data?.sessionID ?? data?.session?.id ?? data?.info?.id;
246
+ if (!sessionID)
247
+ return false;
248
+ try {
249
+ const session = await ctx.session.get({ sessionID });
250
+ const sessionDirectory = session?.location?.directory ?? session?.directory ?? session?.data?.directory;
251
+ return (typeof sessionDirectory === "string" &&
252
+ resolve(sessionDirectory) === resolve(ctx.location.directory));
253
+ }
254
+ catch {
255
+ return false;
256
+ }
257
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The memory operations the native OpenCode v2 hooks need, bound to the
3
+ * plugin's location. Kept separate from the adapter so hook wiring can be
4
+ * tested without a real store.
5
+ */
6
+ export interface V2MemoryBridge {
7
+ readonly retrievalTimeoutMs: number;
8
+ isConfigured(): boolean;
9
+ isInjectionEnabled(): boolean;
10
+ isCompactionEnabled(): boolean;
11
+ isInternalPrompt(sessionID: string, text: string): boolean;
12
+ recordPrompt(sessionID: string, messageID: string, text: string): Promise<void>;
13
+ /** Wrapped per-prompt retrieval section, or null when nothing is relevant. */
14
+ retrieve(prompt: string, sessionID: string): Promise<string | null>;
15
+ /** The session's own memories formatted for restore after compaction, or null. */
16
+ restoreSession(sessionID: string): Promise<string | null>;
17
+ }
18
+ export declare function createV2MemoryBridge(directory: string): V2MemoryBridge;
@@ -0,0 +1,29 @@
1
+ import { CONFIG, isConfigured } from "../config.js";
2
+ import { buildRetrievalSection, formatMemoriesForCompaction, wrapRetrievalSection, } from "../core/retrieval.js";
3
+ import { isInternalPrompt, recordUserPrompt } from "../adapters/opencode/user-prompt.js";
4
+ import { memoryClient } from "../services/client.js";
5
+ import { V2_RETRIEVAL_TIMEOUT_MS } from "../services/request-timeouts.js";
6
+ import { getTags } from "../services/tags.js";
7
+ export function createV2MemoryBridge(directory) {
8
+ return {
9
+ retrievalTimeoutMs: V2_RETRIEVAL_TIMEOUT_MS,
10
+ isConfigured,
11
+ isInjectionEnabled: () => isConfigured() && Boolean(CONFIG.chatMessage.enabled),
12
+ isCompactionEnabled: () => isConfigured() && Boolean(CONFIG.compaction.enabled),
13
+ isInternalPrompt,
14
+ async recordPrompt(sessionID, messageID, text) {
15
+ await recordUserPrompt(sessionID, messageID, directory, text);
16
+ },
17
+ async retrieve(prompt, sessionID) {
18
+ const section = await buildRetrievalSection(prompt, directory, sessionID);
19
+ return section ? wrapRetrievalSection(section) : null;
20
+ },
21
+ async restoreSession(sessionID) {
22
+ const tags = getTags(directory);
23
+ const result = await memoryClient.searchMemoriesBySessionID(sessionID, tags.project.tag, CONFIG.compaction.memoryLimit);
24
+ if (!result.success || result.results.length === 0)
25
+ return null;
26
+ return formatMemoriesForCompaction(result.results);
27
+ },
28
+ };
29
+ }
@@ -0,0 +1,3 @@
1
+ import type { Plugin } from "@opencode/plugin/promise/plugin";
2
+ declare const OmmsPluginV2: Plugin;
3
+ export default OmmsPluginV2;
@@ -0,0 +1,24 @@
1
+ import { OmmsPlugin } from "../index.js";
2
+ import { loadOpencodeProvider } from "../services/ai/opencode-provider-loader.js";
3
+ import { registerV2Adapter } from "./adapter.js";
4
+ import { createV2MemoryBridge } from "./memory-bridge.js";
5
+ import { createLegacyClient } from "./legacy-client.js";
6
+ const OmmsPluginV2 = {
7
+ id: "omms",
8
+ async setup(ctx) {
9
+ const legacyClient = createLegacyClient(ctx);
10
+ const legacy = (await OmmsPlugin({
11
+ client: legacyClient,
12
+ directory: ctx.location.directory,
13
+ worktree: ctx.location.project.directory,
14
+ project: ctx.location.project,
15
+ serverUrl: undefined,
16
+ }));
17
+ // The V1 initializer cannot discover a server URL from a native V2
18
+ // context. Route internal structured-output calls through the adapter.
19
+ const { setV2Client } = await loadOpencodeProvider();
20
+ setV2Client(legacyClient);
21
+ return registerV2Adapter(ctx, legacy, createV2MemoryBridge(ctx.location.directory));
22
+ },
23
+ };
24
+ export default OmmsPluginV2;