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,439 @@
1
+ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
2
+ import { basename, dirname, isAbsolute, join, relative } from "node:path";
3
+ import { CONFIG, getExplicitContainerTagPrefix } from "../config.js";
4
+ import { log } from "./logger.js";
5
+ import { copyFilesWithVerification, timestampSlug, walkTree, } from "./legacy-migration.js";
6
+ import { countOpencodePrefixedRows, enumerateShardTargets, FROM_PREFIX_WITH_SEP, } from "./tag-prefix-scan.js";
7
+ import { withCrossProcessWriteLock } from "./turso/cross-process-write-lock.js";
8
+ import { tursoConnectionManager } from "./turso/connection-manager.js";
9
+ import { tursoShardManager } from "./turso/shard-manager.js";
10
+ /**
11
+ * One-time migration of the stored container tag prefix from `opencode_` to
12
+ * `omms_` (OpenSpec change migrate-container-tag-prefix-to-omms).
13
+ *
14
+ * Safety rules (design.md, in priority order):
15
+ * 1. A timestamped, checksum-verified backup of the ENTIRE store directory is
16
+ * created before any shard is rewritten. Backup failure aborts loudly with
17
+ * nothing rewritten.
18
+ * 2. Each shard rewrite is a single SQL UPDATE inside the shard's write
19
+ * transaction, under the existing per-scope cross-process write lock, with
20
+ * before/after invariants (row count, id set, rewritten count, zero
21
+ * remaining opencode_ rows). Any mismatch rolls the shard back and aborts
22
+ * the run; already-committed shards stay migrated and the next run resumes.
23
+ * 3. Store-level completion is recorded in a `tag_prefix_migration` table in
24
+ * metadata.db; per-shard completion in each shard's shard_metadata table,
25
+ * committed with the rewrite. The scan, not the marker, is the source of
26
+ * truth: a marker-less store with zero opencode_ rows completes as a
27
+ * no-op rewrite.
28
+ * 4. The whole migration holds a store-level PID-liveness gate lock so two
29
+ * hosts starting together cannot both rewrite; normal writers are
30
+ * serialised per scope by the existing cross-process write lock.
31
+ * 5. The backup and the pre-migration state are never deleted or modified.
32
+ * Rollback is restore-from-backup (docs/omms-migration.md).
33
+ */
34
+ const FROM_PREFIX = "opencode";
35
+ const TO_PREFIX = "omms";
36
+ const TO_PREFIX_WITH_SEP = "omms_";
37
+ const METADATA_DB_NAME = "metadata.db";
38
+ const MANIFEST_FILE = "manifest.json";
39
+ const COMPLETION_TABLE = "tag_prefix_migration";
40
+ const SHARD_MARKER_TABLE = "shard_metadata";
41
+ const BACKUPS_DIR_NAME = "backups";
42
+ const BACKUP_SLUG_PREFIX = "tag-prefix-";
43
+ const GATE_LOCK_TIMEOUT_MS = 10 * 60 * 1000;
44
+ const GATE_LOCK_POLL_MS = 50;
45
+ export const TAG_PREFIX_MIGRATION_LOCK_FILE = ".tag-prefix-migration.lock";
46
+ export const SHARD_MARKER_KEY = "tag_prefix_migration";
47
+ export { countOpencodePrefixedRows } from "./tag-prefix-scan.js";
48
+ function isProcessAlive(pid) {
49
+ try {
50
+ process.kill(pid, 0);
51
+ return true;
52
+ }
53
+ catch {
54
+ return false;
55
+ }
56
+ }
57
+ function gateLockPath(storePath) {
58
+ return join(storePath, TAG_PREFIX_MIGRATION_LOCK_FILE);
59
+ }
60
+ function readLiveGateLock(path) {
61
+ if (!existsSync(path))
62
+ return null;
63
+ try {
64
+ const state = JSON.parse(readFileSync(path, "utf-8"));
65
+ if (Number.isInteger(state.pid) && state.pid > 0 && isProcessAlive(state.pid)) {
66
+ return state;
67
+ }
68
+ }
69
+ catch {
70
+ // Corrupt lock files are stale; remove below.
71
+ }
72
+ try {
73
+ unlinkSync(path);
74
+ }
75
+ catch {
76
+ // Already removed by a racing acquirer.
77
+ }
78
+ return null;
79
+ }
80
+ /**
81
+ * Acquire the store-level migration gate lock (PID-liveness, same pattern as
82
+ * the legacy directory migration). Blocks while a live holder runs; fails
83
+ * loudly after GATE_LOCK_TIMEOUT_MS so a wedged holder can never deadlock
84
+ * every host permanently. A stale lock (crashed holder) is taken over.
85
+ */
86
+ async function acquireGateLock(storePath) {
87
+ const path = gateLockPath(storePath);
88
+ const state = { pid: process.pid, timestamp: new Date().toISOString() };
89
+ const deadline = Date.now() + GATE_LOCK_TIMEOUT_MS;
90
+ for (;;) {
91
+ try {
92
+ writeFileSync(path, JSON.stringify(state), { flag: "wx" });
93
+ break;
94
+ }
95
+ catch {
96
+ const holder = readLiveGateLock(path);
97
+ if (!holder)
98
+ continue;
99
+ if (Date.now() > deadline) {
100
+ throw new Error(`Timed out waiting for the container tag prefix migration gate lock at ${path}: ` +
101
+ `held by pid ${holder.pid} since ${holder.timestamp}. Close the other omms/opencode ` +
102
+ `process, or remove the lock file if that process has crashed, then start again.`);
103
+ }
104
+ await new Promise((resolvePromise) => setTimeout(resolvePromise, GATE_LOCK_POLL_MS + Math.random() * 10));
105
+ }
106
+ }
107
+ return () => {
108
+ try {
109
+ unlinkSync(path);
110
+ }
111
+ catch {
112
+ // Best effort: a stale file is cleaned up by the next acquirer.
113
+ }
114
+ };
115
+ }
116
+ async function ensureCompletionTable(db) {
117
+ await db.batch([
118
+ {
119
+ sql: `
120
+ CREATE TABLE IF NOT EXISTS ${COMPLETION_TABLE} (
121
+ id INTEGER PRIMARY KEY CHECK (id = 1),
122
+ status TEXT NOT NULL,
123
+ from_prefix TEXT NOT NULL,
124
+ to_prefix TEXT NOT NULL,
125
+ backup_path TEXT,
126
+ shards_rewritten INTEGER NOT NULL,
127
+ rows_rewritten INTEGER NOT NULL,
128
+ started_at TEXT NOT NULL,
129
+ completed_at TEXT NOT NULL
130
+ )
131
+ `,
132
+ },
133
+ ]);
134
+ }
135
+ async function readCompletion(db) {
136
+ await ensureCompletionTable(db);
137
+ const row = await db.get(`SELECT * FROM ${COMPLETION_TABLE} WHERE id = 1`);
138
+ if (!row)
139
+ return null;
140
+ return {
141
+ completedAt: String(row.completed_at),
142
+ backupPath: row.backup_path === null ? null : String(row.backup_path),
143
+ shardsRewritten: Number(row.shards_rewritten),
144
+ rowsRewritten: Number(row.rows_rewritten),
145
+ };
146
+ }
147
+ /**
148
+ * Read the store-level completion marker from `<storePath>/metadata.db`.
149
+ * Returns null when the store has not completed the tag prefix migration.
150
+ * `storePath` must equal CONFIG.storagePath (the connection manager refuses
151
+ * paths outside it).
152
+ */
153
+ export async function readTagPrefixMigrationCompletion(storePath) {
154
+ const db = await tursoConnectionManager.getConnection(join(storePath, METADATA_DB_NAME));
155
+ return readCompletion(db);
156
+ }
157
+ async function writeCompletionMarker(db, entry) {
158
+ await db.run(`
159
+ INSERT OR REPLACE INTO ${COMPLETION_TABLE}
160
+ (id, status, from_prefix, to_prefix, backup_path, shards_rewritten, rows_rewritten, started_at, completed_at)
161
+ VALUES (1, 'completed', ?, ?, ?, ?, ?, ?, ?)
162
+ `, [
163
+ FROM_PREFIX,
164
+ TO_PREFIX,
165
+ entry.backupPath,
166
+ entry.shardsRewritten,
167
+ entry.rowsRewritten,
168
+ entry.startedAt,
169
+ new Date().toISOString(),
170
+ ]);
171
+ }
172
+ async function countLike(tx, prefixWithSep) {
173
+ const result = await tx.execute({
174
+ sql: `SELECT COUNT(*) as count FROM memories WHERE container_tag LIKE ? ESCAPE '\\'`,
175
+ args: [`${prefixWithSep.slice(0, -1)}\\_%`],
176
+ });
177
+ return Number(result.rows[0]?.count ?? 0);
178
+ }
179
+ async function selectIdSet(tx) {
180
+ const result = await tx.execute({ sql: `SELECT id FROM memories`, args: [] });
181
+ return new Set(result.rows.map((row) => String(row.id)));
182
+ }
183
+ function shardLabel(target) {
184
+ return `${basename(target.dbPath)} (${target.scope}/${target.scopeHash})`;
185
+ }
186
+ /**
187
+ * Rewrite one shard's opencode_ rows to omms_ inside the shard's write
188
+ * transaction, under the existing per-scope cross-process write lock, with
189
+ * per-shard before/after verification (design D3). Any invariant failure
190
+ * throws, which rolls the transaction back; the abort names the shard.
191
+ */
192
+ async function rewriteShard(target) {
193
+ return tursoShardManager.withScopeWriteLock(target.scope, target.scopeHash, async () => {
194
+ const db = await tursoConnectionManager.getConnection(target.dbPath);
195
+ try {
196
+ return await db.transaction("write", async (tx) => {
197
+ const beforeOld = await countLike(tx, FROM_PREFIX_WITH_SEP);
198
+ if (beforeOld === 0) {
199
+ return { rewritten: 0, skipped: true };
200
+ }
201
+ const beforeNew = await countLike(tx, TO_PREFIX_WITH_SEP);
202
+ const beforeAllResult = await tx.execute({
203
+ sql: `SELECT COUNT(*) as count FROM memories`,
204
+ args: [],
205
+ });
206
+ const beforeAll = Number(beforeAllResult.rows[0]?.count ?? 0);
207
+ const idsBefore = await selectIdSet(tx);
208
+ const update = await tx.execute({
209
+ sql: `
210
+ UPDATE memories
211
+ SET container_tag = ? || substr(container_tag, ?)
212
+ WHERE container_tag LIKE ? ESCAPE '\\'
213
+ `,
214
+ args: [TO_PREFIX_WITH_SEP, FROM_PREFIX_WITH_SEP.length + 1, `${FROM_PREFIX}\\_%`],
215
+ });
216
+ const afterOld = await countLike(tx, FROM_PREFIX_WITH_SEP);
217
+ const afterNew = await countLike(tx, TO_PREFIX_WITH_SEP);
218
+ const afterAllResult = await tx.execute({
219
+ sql: `SELECT COUNT(*) as count FROM memories`,
220
+ args: [],
221
+ });
222
+ const afterAll = Number(afterAllResult.rows[0]?.count ?? 0);
223
+ const idsAfter = await selectIdSet(tx);
224
+ const failures = [];
225
+ if (Number(update.rowsAffected ?? 0) !== beforeOld) {
226
+ failures.push(`rewritten row count ${Number(update.rowsAffected ?? 0)} != observed ${beforeOld}`);
227
+ }
228
+ if (afterOld !== 0) {
229
+ failures.push(`${afterOld} opencode_ rows remain`);
230
+ }
231
+ if (afterNew !== beforeNew + beforeOld) {
232
+ failures.push(`omms_ count ${afterNew} != ${beforeNew} + ${beforeOld} rewritten`);
233
+ }
234
+ if (afterAll !== beforeAll) {
235
+ failures.push(`row count changed: ${beforeAll} -> ${afterAll}`);
236
+ }
237
+ if (idsBefore.size !== idsAfter.size || ![...idsBefore].every((id) => idsAfter.has(id))) {
238
+ failures.push("memory id set changed");
239
+ }
240
+ if (failures.length > 0) {
241
+ throw new Error(`Container tag prefix migration verification failed for shard ` +
242
+ `${shardLabel(target)}: ${failures.join("; ")}. ` +
243
+ `The shard's transaction was rolled back; the migration aborted before ` +
244
+ `writing the completion marker. Fix the cause and start again; the ` +
245
+ `backup taken before the rewrite is untouched.`);
246
+ }
247
+ await tx.execute({
248
+ sql: `INSERT OR REPLACE INTO ${SHARD_MARKER_TABLE} (key, value) VALUES (?, ?)`,
249
+ args: [
250
+ SHARD_MARKER_KEY,
251
+ JSON.stringify({
252
+ from: FROM_PREFIX,
253
+ to: TO_PREFIX,
254
+ rewritten: beforeOld,
255
+ completedAt: new Date().toISOString(),
256
+ }),
257
+ ],
258
+ });
259
+ return { rewritten: beforeOld, skipped: false };
260
+ });
261
+ }
262
+ catch (error) {
263
+ // The transaction is already rolled back (TursoDb.transaction rethrows
264
+ // after rollback). Surface every failure with the shard named so the
265
+ // operator can find it; already-committed shards stay migrated.
266
+ throw new Error(`Container tag prefix migration failed for shard ${shardLabel(target)}: ` +
267
+ `${error instanceof Error ? error.message : String(error)}. ` +
268
+ `The shard's transaction was rolled back; the migration aborted before ` +
269
+ `writing the completion marker. Rerun to resume; the backup taken ` +
270
+ `before the rewrite is untouched.`, { cause: error });
271
+ }
272
+ });
273
+ }
274
+ function relativeStorePath(storePath, filePath) {
275
+ const rel = relative(storePath, filePath).split("\\").join("/");
276
+ if (rel === "" || rel.startsWith("..") || isAbsolute(rel))
277
+ return null;
278
+ return rel;
279
+ }
280
+ /**
281
+ * Create the timestamped, checksum-verified backup of the entire store. Each
282
+ * scope group's files are copied while holding that group's cross-process
283
+ * write lock, so a concurrent writer in another host can never leave a shard
284
+ * file mid-transaction in the copy. Returns the backup directory path.
285
+ * Never deletes or modifies the backup afterwards.
286
+ */
287
+ async function createVerifiedBackup(storePath, targets) {
288
+ const backupRoot = join(dirname(storePath), BACKUPS_DIR_NAME, `${BACKUP_SLUG_PREFIX}${timestampSlug()}`);
289
+ const { files, dirs } = walkTree(storePath);
290
+ mkdirSync(backupRoot, { recursive: true });
291
+ for (const rel of dirs) {
292
+ mkdirSync(join(backupRoot, rel), { recursive: true });
293
+ }
294
+ const filesByScope = new Map();
295
+ for (const target of targets) {
296
+ const groupKey = `${target.scope}_${target.scopeHash}`;
297
+ const rel = relativeStorePath(storePath, target.dbPath);
298
+ if (rel === null)
299
+ continue;
300
+ const group = filesByScope.get(groupKey) ?? [];
301
+ group.push(rel);
302
+ filesByScope.set(groupKey, group);
303
+ }
304
+ const lockedFiles = new Set([...filesByScope.values()].flat());
305
+ const remaining = files.filter((rel) => !lockedFiles.has(rel));
306
+ const manifest = [];
307
+ for (const [groupKey, relPaths] of filesByScope) {
308
+ const [scope, scopeHash] = groupKey.split("_", 2);
309
+ const copied = await withCrossProcessWriteLock(scope, scopeHash, async () => copyFilesWithVerification(storePath, backupRoot, relPaths));
310
+ manifest.push(...copied);
311
+ }
312
+ manifest.push(...copyFilesWithVerification(storePath, backupRoot, remaining));
313
+ writeFileSync(join(backupRoot, MANIFEST_FILE), JSON.stringify({
314
+ createdAt: new Date().toISOString(),
315
+ kind: "tag-prefix-migration",
316
+ source: storePath,
317
+ fileCount: manifest.length,
318
+ files: manifest,
319
+ }, null, 2), "utf-8");
320
+ return backupRoot;
321
+ }
322
+ /**
323
+ * Run the one-time container tag prefix migration against CONFIG.storagePath.
324
+ * See the module comment for the safety rules. Throws on any failure; a throw
325
+ * leaves already-committed shards migrated (resume on the next run) and the
326
+ * completion marker absent.
327
+ */
328
+ export async function runTagPrefixMigration() {
329
+ const storePath = CONFIG.storagePath;
330
+ if (!existsSync(storePath)) {
331
+ mkdirSync(storePath, { recursive: true });
332
+ }
333
+ const metadataDb = await tursoConnectionManager.getConnection(join(storePath, METADATA_DB_NAME));
334
+ if (await readCompletion(metadataDb)) {
335
+ return { status: "already-migrated" };
336
+ }
337
+ const targets = await enumerateShardTargets(storePath);
338
+ const preScanTotal = await countOpencodePrefixedRows(storePath);
339
+ const releaseGate = await acquireGateLock(storePath);
340
+ try {
341
+ // Another process may have completed the migration while we waited for
342
+ // the gate lock; re-read the marker before touching anything.
343
+ if (await readCompletion(metadataDb)) {
344
+ return { status: "already-migrated" };
345
+ }
346
+ if (preScanTotal === 0) {
347
+ // Fresh store, or a crash between the last shard rewrite and the marker
348
+ // write: the scan is the source of truth, so complete without rewriting.
349
+ await writeCompletionMarker(metadataDb, {
350
+ backupPath: null,
351
+ shardsRewritten: 0,
352
+ rowsRewritten: 0,
353
+ startedAt: new Date().toISOString(),
354
+ });
355
+ return { status: "not-needed", shardsScanned: targets.length };
356
+ }
357
+ const startedAt = new Date().toISOString();
358
+ const backupPath = await createVerifiedBackup(storePath, targets);
359
+ const outcomes = [];
360
+ for (const target of targets) {
361
+ const { rewritten, skipped } = await rewriteShard(target);
362
+ outcomes.push({
363
+ dbPath: target.dbPath,
364
+ scope: target.scope,
365
+ scopeHash: target.scopeHash,
366
+ shardIndex: target.shardIndex,
367
+ rewritten,
368
+ skipped,
369
+ });
370
+ }
371
+ const remaining = await countOpencodePrefixedRows(storePath);
372
+ if (remaining !== 0) {
373
+ throw new Error(`Container tag prefix migration finished with ${remaining} opencode_ row(s) still ` +
374
+ `present in ${storePath}. No completion marker was written; rerun to resume.`);
375
+ }
376
+ const rowsRewritten = outcomes.reduce((sum, outcome) => sum + outcome.rewritten, 0);
377
+ const shardsRewritten = outcomes.filter((outcome) => !outcome.skipped).length;
378
+ const completedAt = new Date().toISOString();
379
+ await writeCompletionMarker(metadataDb, {
380
+ backupPath,
381
+ shardsRewritten,
382
+ rowsRewritten,
383
+ startedAt,
384
+ });
385
+ log("Container tag prefix migration to omms_ complete", {
386
+ store: storePath,
387
+ backupPath,
388
+ shardsScanned: targets.length,
389
+ shardsRewritten,
390
+ rowsRewritten,
391
+ });
392
+ return {
393
+ status: "migrated",
394
+ backupPath,
395
+ shardsScanned: targets.length,
396
+ shardsRewritten,
397
+ rowsRewritten,
398
+ shards: outcomes,
399
+ startedAt,
400
+ completedAt,
401
+ };
402
+ }
403
+ finally {
404
+ releaseGate();
405
+ }
406
+ }
407
+ /**
408
+ * D6 warning for an explicit `containerTagPrefix: "opencode"` override after
409
+ * the migration: stored rows carry omms_, so the override matches no rows.
410
+ * Pure function; returns null for every other value (including unset).
411
+ */
412
+ export function explicitOpencodePrefixWarning(explicitPrefix) {
413
+ if (explicitPrefix !== FROM_PREFIX)
414
+ return null;
415
+ return (`containerTagPrefix is explicitly set to "opencode", but stored memory rows carry ` +
416
+ `the "omms_" prefix after the tag migration, so the override matches no rows. ` +
417
+ `Remove the override, or restore the pre-migration backup to roll back ` +
418
+ `(see docs/omms-migration.md).`);
419
+ }
420
+ /**
421
+ * Startup gate: before any read or write is served, migrate any remaining
422
+ * opencode_ rows to omms_ (or abort loudly). Also emits the D6 warning once
423
+ * per start when the config explicitly overrides the prefix to "opencode".
424
+ * Disabled under OMMS_SKIP_TAG_PREFIX_MIGRATION=1 (set by tests/preload.ts so
425
+ * bun test never rewrites the developer's real store).
426
+ */
427
+ export async function runStartupTagPrefixGate() {
428
+ if (process.env.OMMS_SKIP_TAG_PREFIX_MIGRATION === "1")
429
+ return;
430
+ const completion = await readTagPrefixMigrationCompletion(CONFIG.storagePath);
431
+ if (!completion) {
432
+ const report = await runTagPrefixMigration();
433
+ log(`Tag prefix migration gate: ${report.status}`);
434
+ }
435
+ const warning = explicitOpencodePrefixWarning(getExplicitContainerTagPrefix());
436
+ if (warning) {
437
+ log(warning);
438
+ }
439
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Shard-discovery and opencode_ row-counting helpers for the container tag
3
+ * prefix migration (see tag-prefix-migration.ts for the migration itself).
4
+ */
5
+ export declare const FROM_PREFIX_WITH_SEP = "opencode_";
6
+ export interface ShardTarget {
7
+ dbPath: string;
8
+ scope: "user" | "project";
9
+ scopeHash: string;
10
+ shardIndex: number;
11
+ }
12
+ /**
13
+ * Enumerate every shard file to migrate: the metadata.db registry first (the
14
+ * authoritative set), then any on-disk shard file in users/ or projects/ not
15
+ * present in the registry (orphans still need rewriting). Deduplicated by
16
+ * resolved path, ordered deterministically.
17
+ */
18
+ export declare function enumerateShardTargets(storePath: string): Promise<ShardTarget[]>;
19
+ /**
20
+ * Read-only helper: total rows still carrying the `opencode_` prefix across
21
+ * every shard file in the store. Also fails loudly when a non-shard .db file
22
+ * in users/ or projects/ still carries opencode_ rows: such a file cannot be
23
+ * locked per scope, and silently skipping it would leave a mixed-prefix
24
+ * namespace (design D2 forbids that).
25
+ */
26
+ export declare function countOpencodePrefixedRows(storePath: string): Promise<number>;
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Shard-discovery and opencode_ row-counting helpers for the container tag
3
+ * prefix migration (see tag-prefix-migration.ts for the migration itself).
4
+ */
5
+ import { existsSync, readdirSync } from "node:fs";
6
+ import { basename, join } from "node:path";
7
+ import { tursoConnectionManager } from "./turso/connection-manager.js";
8
+ import { tursoShardManager } from "./turso/shard-manager.js";
9
+ export const FROM_PREFIX_WITH_SEP = "opencode_";
10
+ /** Matches the shard file naming used by tursoShardManager.getShardPath. */
11
+ const SHARD_FILENAME_RE = /^(user|project)_([a-f0-9]{16})_shard_(\d+)\.db$/;
12
+ function listShardDirFiles(storePath, scopeDir) {
13
+ const dir = join(storePath, scopeDir);
14
+ if (!existsSync(dir))
15
+ return [];
16
+ return readdirSync(dir)
17
+ .filter((file) => file.endsWith(".db") && !file.includes(".bak") && !file.includes(".tmp"))
18
+ .map((file) => join(dir, file));
19
+ }
20
+ /**
21
+ * Enumerate every shard file to migrate: the metadata.db registry first (the
22
+ * authoritative set), then any on-disk shard file in users/ or projects/ not
23
+ * present in the registry (orphans still need rewriting). Deduplicated by
24
+ * resolved path, ordered deterministically.
25
+ */
26
+ export async function enumerateShardTargets(storePath) {
27
+ const byPath = new Map();
28
+ for (const scope of ["user", "project"]) {
29
+ for (const shard of await tursoShardManager.getAllShards(scope, "")) {
30
+ byPath.set(shard.dbPath, {
31
+ dbPath: shard.dbPath,
32
+ scope,
33
+ scopeHash: shard.scopeHash,
34
+ shardIndex: shard.shardIndex,
35
+ });
36
+ }
37
+ }
38
+ for (const scopeDir of ["users", "projects"]) {
39
+ for (const dbPath of listShardDirFiles(storePath, scopeDir)) {
40
+ const match = SHARD_FILENAME_RE.exec(basename(dbPath));
41
+ if (!match)
42
+ continue;
43
+ const resolved = dbPath;
44
+ if (byPath.has(resolved))
45
+ continue;
46
+ byPath.set(resolved, {
47
+ dbPath,
48
+ scope: match[1],
49
+ scopeHash: match[2],
50
+ shardIndex: Number(match[3]),
51
+ });
52
+ }
53
+ }
54
+ return [...byPath.values()].sort((a, b) => a.dbPath.localeCompare(b.dbPath));
55
+ }
56
+ async function countPrefixedInConnection(db, prefixWithSep) {
57
+ try {
58
+ const row = await db.get(`SELECT COUNT(*) as count FROM memories WHERE container_tag LIKE ? ESCAPE '\\'`, [`${prefixWithSep.slice(0, -1)}\\_%`]);
59
+ return Number(row?.count ?? 0);
60
+ }
61
+ catch {
62
+ // Files without a memories table (or unreadable ones) contribute nothing.
63
+ return null;
64
+ }
65
+ }
66
+ /**
67
+ * Read-only helper: total rows still carrying the `opencode_` prefix across
68
+ * every shard file in the store. Also fails loudly when a non-shard .db file
69
+ * in users/ or projects/ still carries opencode_ rows: such a file cannot be
70
+ * locked per scope, and silently skipping it would leave a mixed-prefix
71
+ * namespace (design D2 forbids that).
72
+ */
73
+ export async function countOpencodePrefixedRows(storePath) {
74
+ let total = 0;
75
+ for (const scopeDir of ["users", "projects"]) {
76
+ for (const dbPath of listShardDirFiles(storePath, scopeDir)) {
77
+ const db = await tursoConnectionManager.getConnection(dbPath);
78
+ const count = await countPrefixedInConnection(db, FROM_PREFIX_WITH_SEP);
79
+ if (count === null)
80
+ continue;
81
+ const matchesNaming = SHARD_FILENAME_RE.test(basename(dbPath));
82
+ if (!matchesNaming && count > 0) {
83
+ throw new Error(`Shard file ${dbPath} does not match the shard filename pattern ` +
84
+ `(scope_hash_shard_N.db) and still carries ${count} opencode_ row(s). ` +
85
+ `Rename or inspect the file manually, then rerun the migration.`);
86
+ }
87
+ total += count;
88
+ }
89
+ }
90
+ return total;
91
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Walk up from `directory` (inclusive) to the filesystem root looking for the
3
+ * {@link PROJECT_MARKERS}. Returns the first directory that contains it, or
4
+ * `null` when no marker is found so the caller can fall back to git detection.
5
+ */
6
+ export declare function findMarkerProjectRoot(directory: string): string | null;
7
+ export interface TagInfo {
8
+ tag: string;
9
+ displayName: string;
10
+ userName?: string;
11
+ userEmail?: string;
12
+ projectPath?: string;
13
+ projectName?: string;
14
+ gitRepoUrl?: string;
15
+ }
16
+ export declare function getGitEmail(directory?: string): string | null;
17
+ export declare function getGitName(directory?: string): string | null;
18
+ export declare function getGitRepoUrl(directory: string): string | null;
19
+ export declare function getGitCommonDir(directory: string): string | null;
20
+ export declare function getGitTopLevel(directory: string): string | null;
21
+ export declare function getProjectRoot(directory: string): string;
22
+ export declare function getProjectIdentity(directory: string): string;
23
+ export declare function getProjectName(directory: string): string;
24
+ export declare function getUserTagInfo(directory?: string): TagInfo;
25
+ export declare function getProjectTagInfo(directory: string): TagInfo;
26
+ export declare function getTags(directory: string): {
27
+ user: TagInfo;
28
+ project: TagInfo;
29
+ };