memorix 1.9.2 → 1.9.4

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 (39) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +1 -1
  3. package/README.zh-CN.md +1 -1
  4. package/dist/cli/index.js +358 -146
  5. package/dist/index.js +281 -125
  6. package/dist/maintenance-runner.js +219 -97
  7. package/dist/media-audio-runner.js +219 -97
  8. package/dist/media-video-runner.js +219 -97
  9. package/dist/memcode-runtime/CHANGELOG.md +27 -0
  10. package/dist/sdk.js +280 -124
  11. package/dist/vector-backfill-runner.js +219 -97
  12. package/docs/AGENT_OPERATOR_PLAYBOOK.md +15 -0
  13. package/docs/ARCHITECTURE.md +23 -0
  14. package/docs/DEVELOPMENT.md +1 -1
  15. package/docs/PERFORMANCE.md +50 -0
  16. package/docs/README.md +1 -1
  17. package/package.json +1 -1
  18. package/plugins/antigravity/memorix/plugin.json +1 -1
  19. package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
  20. package/plugins/codebuddy/memorix/.codebuddy-plugin/plugin.json +1 -1
  21. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  22. package/plugins/copilot/memorix/plugin.json +1 -1
  23. package/plugins/gemini/memorix/gemini-extension.json +1 -1
  24. package/plugins/hermes/memorix/plugin.yaml +1 -1
  25. package/plugins/omp/memorix/package.json +1 -1
  26. package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
  27. package/plugins/pi/memorix/package.json +1 -1
  28. package/server.json +2 -2
  29. package/src/cli/commands/doctor.ts +78 -17
  30. package/src/cli/commands/media.ts +2 -0
  31. package/src/media/minimax.ts +26 -2
  32. package/src/media/video-jobs.ts +9 -1
  33. package/src/server.ts +3 -1
  34. package/src/store/mini-skill-store.ts +31 -22
  35. package/src/store/obs-store.ts +46 -30
  36. package/src/store/session-store.ts +35 -21
  37. package/src/store/sqlite-db.ts +16 -7
  38. package/src/store/sqlite-reliability.ts +87 -0
  39. package/src/store/sqlite-store.ts +4 -1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memorix",
3
- "version": "1.9.2",
3
+ "version": "1.9.4",
4
4
  "mcpServers": {
5
5
  "memorix": {
6
6
  "command": "memorix",
@@ -1,5 +1,5 @@
1
1
  name: memorix
2
- version: "1.9.2"
2
+ version: "1.9.4"
3
3
  description: Shared workspace memory bridge for Hermes Agent.
4
4
  author: AVIDS2
5
5
  license: Apache-2.0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memorix-omp-package",
3
- "version": "1.9.2",
3
+ "version": "1.9.4",
4
4
  "description": "Memorix shared workspace memory package for Oh-my-Pi.",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memorix",
3
- "version": "1.9.2",
3
+ "version": "1.9.4",
4
4
  "description": "Shared workspace memory for OpenClaw and other AI coding agents.",
5
5
  "author": {
6
6
  "name": "AVIDS2",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memorix-pi-package",
3
- "version": "1.9.2",
3
+ "version": "1.9.4",
4
4
  "description": "Memorix shared workspace memory package for Pi coding agent.",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": [
package/server.json CHANGED
@@ -7,7 +7,7 @@
7
7
  "url": "https://github.com/AVIDS2/memorix",
8
8
  "source": "github"
9
9
  },
10
- "version": "1.9.2",
10
+ "version": "1.9.4",
11
11
  "websiteUrl": "https://mem.rglens.com",
12
12
  "icons": [
13
13
  {
@@ -22,7 +22,7 @@
22
22
  {
23
23
  "registryType": "npm",
24
24
  "identifier": "memorix",
25
- "version": "1.9.2",
25
+ "version": "1.9.4",
26
26
  "runtimeHint": "npx",
27
27
  "packageArguments": [
28
28
  {
@@ -283,11 +283,14 @@ export default defineCommand({
283
283
  let activeCount = 0;
284
284
  let ranCount = 0;
285
285
  let backendName = 'unknown';
286
+ let backendReason: string | undefined;
287
+ let observationFailure: string | undefined;
286
288
  try {
287
289
  const { initObservationStore, getObservationStore } = await import('../../store/obs-store.js');
288
290
  await initObservationStore(dataDir);
289
291
  const store = getObservationStore();
290
292
  backendName = store.getBackendName();
293
+ backendReason = store.getBackendReason?.();
291
294
  const { filterReadableObservations } = await import('../../memory/visibility.js');
292
295
  projectObservations = projectId
293
296
  ? filterReadableObservations(await store.loadAll(), { projectId })
@@ -295,12 +298,30 @@ export default defineCommand({
295
298
  obsCount = projectObservations.length;
296
299
  activeCount = projectObservations.filter((o: any) => (o.status ?? 'active') === 'active').length;
297
300
  ranCount = projectObservations.filter((o: any) => /^Ran:\s/i.test(o.title ?? '')).length;
298
- } catch { /* ignore */ }
301
+ } catch (error) {
302
+ // The store could not be opened at all. Without this the counters stay
303
+ // at zero and the branch below reports "0 total" as a healthy result,
304
+ // which is indistinguishable from a project that has no observations.
305
+ observationFailure = error instanceof Error ? error.message : String(error);
306
+ }
299
307
 
300
- if (backendName === 'degraded') {
301
- lines.push(warn('Observations: SQLite unavailable — degraded (read-only, no data)'));
302
- issues.push('SQLite backend unavailable — observations cannot be read or written.');
308
+ if (observationFailure) {
309
+ lines.push(fail(`Observations: storage unavailable — ${observationFailure}`));
310
+ issues.push(`Observation store could not be opened: ${observationFailure}`);
311
+ (report.data as any) = {
312
+ backend: 'unavailable',
313
+ observations: null,
314
+ active: null,
315
+ commandLogs: null,
316
+ error: observationFailure,
317
+ };
318
+ } else if (backendName === 'degraded') {
319
+ lines.push(fail(`Backend: degraded (read-only; writes disabled${backendReason ? `; ${backendReason}` : ''})`));
320
+ lines.push(fail('Observations: unavailable — count not reported'));
321
+ issues.push('SQLite backend unavailable — observation storage is in degraded read-only mode; observations are not being persisted.');
322
+ (report.data as any) = { backend: 'degraded', ...(backendReason ? { backendReason } : {}), observations: null, active: null, commandLogs: null };
303
323
  } else {
324
+ lines.push(ok('Backend: sqlite (read-write)'));
304
325
  lines.push(ok(`Observations: ${obsCount} total, ${activeCount} active`));
305
326
  if (ranCount > 0) {
306
327
  const pct = Math.round(ranCount / obsCount * 100);
@@ -310,32 +331,68 @@ export default defineCommand({
310
331
  }
311
332
  }
312
333
  }
313
- report.data = { observations: obsCount, active: activeCount, commandLogs: ranCount };
334
+ if (!report.data) {
335
+ report.data = { backend: backendName, observations: obsCount, active: activeCount, commandLogs: ranCount };
336
+ } else if (!(report.data as any).backend) {
337
+ (report.data as any).backend = backendName;
338
+ }
314
339
 
315
340
  // Sessions — read from SQLite (canonical store)
341
+ let sessionBackend = 'unknown';
342
+ let sessionReason: string | undefined;
316
343
  try {
317
344
  const { initSessionStore, getSessionStore } = await import('../../store/session-store.js');
318
345
  await initSessionStore(dataDir);
319
346
  const sessStore = getSessionStore();
320
- const sess = await sessStore.loadAll();
321
- const sessCount = sess.length;
322
- const activeSess = sess.filter((s: any) => s.status === 'active').length;
323
- lines.push(ok(`Sessions: ${sessCount} total, ${activeSess} active`));
324
- (report.data as any).sessions = sessCount;
325
- } catch { /* ignore */ }
347
+ sessionBackend = sessStore.getBackendName();
348
+ sessionReason = sessStore.getBackendReason?.();
349
+ if (sessionBackend === 'degraded') {
350
+ lines.push(fail(`Sessions: unavailable — read-only degraded backend${sessionReason ? `; ${sessionReason}` : ''}`));
351
+ issues.push('Session storage is in degraded read-only mode.');
352
+ (report.data as any).sessions = null;
353
+ if (sessionReason) (report.data as any).sessionBackendReason = sessionReason;
354
+ } else {
355
+ const sess = await sessStore.loadAll();
356
+ const sessCount = sess.length;
357
+ const activeSess = sess.filter((s: any) => s.status === 'active').length;
358
+ lines.push(ok(`Sessions: ${sessCount} total, ${activeSess} active`));
359
+ (report.data as any).sessions = sessCount;
360
+ }
361
+ } catch (error) {
362
+ const message = error instanceof Error ? error.message : String(error);
363
+ lines.push(fail(`Sessions: unavailable (${message})`));
364
+ issues.push(`Session storage is unavailable: ${message}`);
365
+ (report.data as any).sessions = null;
366
+ }
326
367
 
327
368
  // Mini-skills — read from SQLite (canonical store)
369
+ let skillBackend = 'unknown';
370
+ let skillReason: string | undefined;
328
371
  try {
329
372
  const { initMiniSkillStore, getMiniSkillStore } = await import('../../store/mini-skill-store.js');
330
373
  await initMiniSkillStore(dataDir);
331
374
  const skillStore = getMiniSkillStore();
332
- const skills = await skillStore.loadAll();
333
- const skillCount = skills.length;
334
- if (skillCount > 0) {
335
- lines.push(ok(`Mini-skills: ${skillCount}`));
336
- (report.data as any).miniSkills = skillCount;
375
+ skillBackend = skillStore.getBackendName();
376
+ skillReason = skillStore.getBackendReason?.();
377
+ if (skillBackend === 'degraded') {
378
+ lines.push(fail(`Mini-skills: unavailable — read-only degraded backend${skillReason ? `; ${skillReason}` : ''}`));
379
+ issues.push('Mini-skill storage is in degraded read-only mode.');
380
+ (report.data as any).miniSkills = null;
381
+ if (skillReason) (report.data as any).miniSkillBackendReason = skillReason;
382
+ } else {
383
+ const skills = await skillStore.loadAll();
384
+ const skillCount = skills.length;
385
+ if (skillCount > 0) {
386
+ lines.push(ok(`Mini-skills: ${skillCount}`));
387
+ (report.data as any).miniSkills = skillCount;
388
+ }
337
389
  }
338
- } catch { /* ignore */ }
390
+ } catch (error) {
391
+ const message = error instanceof Error ? error.message : String(error);
392
+ lines.push(fail(`Mini-skills: unavailable (${message})`));
393
+ issues.push(`Mini-skill storage is unavailable: ${message}`);
394
+ (report.data as any).miniSkills = null;
395
+ }
339
396
  } else {
340
397
  lines.push(warn('No data directory found for current project'));
341
398
  if (!projectId) {
@@ -633,5 +690,9 @@ export default defineCommand({
633
690
  } else {
634
691
  console.log(lines.join('\n'));
635
692
  }
693
+
694
+ if (issues.some((issue) => /^(Observation store could not be opened|Observation storage |Session storage |Mini-skill storage |SQLite backend unavailable)/.test(issue))) {
695
+ process.exitCode = 1;
696
+ }
636
697
  },
637
698
  });
@@ -146,6 +146,7 @@ export default defineCommand({
146
146
  concepts: { type: 'string', description: 'Comma-separated concepts when attaching' },
147
147
  visibility: { type: 'string', description: 'Observation visibility when attaching' },
148
148
  prompt: { type: 'string', description: 'Generation prompt' },
149
+ 'first-frame-image-url': { type: 'string', description: 'Public HTTPS first-frame image URL for MiniMax video generation; stored with the queued job' },
149
150
  image: { type: 'string', description: 'Path to a reference image for image-to-image generation' },
150
151
  model: { type: 'string', description: 'Provider model' },
151
152
  provider: { type: 'string', description: 'Audio transcription provider: openai or groq' },
@@ -201,6 +202,7 @@ export default defineCommand({
201
202
  dataDir,
202
203
  projectId: project.id,
203
204
  prompt,
205
+ firstFrameImageUrl: args['first-frame-image-url'] as string | undefined,
204
206
  model: parseMiniMaxVideoModel(args.model),
205
207
  region: parseMiniMaxRegion(args.region),
206
208
  duration: parseMiniMaxVideoDuration(args.duration),
@@ -47,6 +47,7 @@ export interface GeneratedMiniMaxImages {
47
47
 
48
48
  export interface MiniMaxVideoGenerationRequest {
49
49
  prompt: string;
50
+ firstFrameImageUrl?: string;
50
51
  model?: MiniMaxVideoModel;
51
52
  region?: MiniMaxRegion;
52
53
  apiKey?: string;
@@ -162,6 +163,7 @@ export function normalizeMiniMaxVideoRequest(input: MiniMaxVideoGenerationReques
162
163
  region: MiniMaxRegion;
163
164
  model: MiniMaxVideoModel;
164
165
  prompt: string;
166
+ firstFrameImageUrl?: string;
165
167
  resolution: MiniMaxVideoResolution;
166
168
  duration: 5 | 10;
167
169
  ratio: MiniMaxVideoRatio;
@@ -178,7 +180,21 @@ export function normalizeMiniMaxVideoRequest(input: MiniMaxVideoGenerationReques
178
180
  if (ratio !== 'adaptive' && ratio !== '16:9' && ratio !== '9:16' && ratio !== '1:1') {
179
181
  throw new Error('MiniMax video ratio must be adaptive, 16:9, 9:16, or 1:1');
180
182
  }
181
- return { region, model, prompt, resolution, duration, ratio };
183
+ let firstFrameImageUrl: string | undefined;
184
+ if (input.firstFrameImageUrl !== undefined) {
185
+ firstFrameImageUrl = input.firstFrameImageUrl.trim();
186
+ let parsed: URL;
187
+ try {
188
+ parsed = new URL(firstFrameImageUrl);
189
+ } catch {
190
+ throw new Error('MiniMax first-frame image must be a public HTTPS URL');
191
+ }
192
+ if (parsed.protocol !== 'https:' || parsed.username || parsed.password
193
+ || sanitizeCredentials(firstFrameImageUrl) !== firstFrameImageUrl) {
194
+ throw new Error('MiniMax first-frame image must be an HTTPS URL without credentials');
195
+ }
196
+ }
197
+ return { region, model, prompt, resolution, duration, ratio, ...(firstFrameImageUrl ? { firstFrameImageUrl } : {}) };
182
198
  }
183
199
 
184
200
  function videoRequestConfig(input: MiniMaxVideoGenerationRequest): {
@@ -187,6 +203,7 @@ function videoRequestConfig(input: MiniMaxVideoGenerationRequest): {
187
203
  baseUrl: string;
188
204
  model: MiniMaxVideoModel;
189
205
  prompt: string;
206
+ firstFrameImageUrl?: string;
190
207
  resolution: MiniMaxVideoResolution;
191
208
  duration: 5 | 10;
192
209
  ratio: MiniMaxVideoRatio;
@@ -284,7 +301,14 @@ export async function createMiniMaxVideoTask(
284
301
  headers: videoHeaders(config.apiKey),
285
302
  body: JSON.stringify({
286
303
  model: config.model,
287
- content: [{ type: 'text', text: config.prompt }],
304
+ content: [
305
+ { type: 'text', text: config.prompt },
306
+ ...(config.firstFrameImageUrl ? [{
307
+ type: 'image_url',
308
+ image_url: { url: config.firstFrameImageUrl },
309
+ role: 'first_frame',
310
+ }] : []),
311
+ ],
288
312
  resolution: config.resolution,
289
313
  duration: config.duration,
290
314
  ratio: config.ratio,
@@ -32,6 +32,7 @@ interface StoredMiniMaxVideoRequest {
32
32
  region: 'global' | 'cn';
33
33
  model: 'MiniMax-H3';
34
34
  prompt: string;
35
+ firstFrameImageUrl?: string;
35
36
  resolution: '2K';
36
37
  duration: 5 | 10;
37
38
  ratio: 'adaptive' | '16:9' | '9:16' | '1:1';
@@ -93,13 +94,18 @@ function parseStoredRequest(value: Record<string, unknown>): StoredMiniMaxVideoR
93
94
  || (region !== 'global' && region !== 'cn')
94
95
  || model !== 'MiniMax-H3'
95
96
  || typeof prompt !== 'string'
97
+ || (value.firstFrameImageUrl !== undefined && typeof value.firstFrameImageUrl !== 'string')
96
98
  || resolution !== '2K'
97
99
  || (duration !== 5 && duration !== 10)
98
100
  || (ratio !== 'adaptive' && ratio !== '16:9' && ratio !== '9:16' && ratio !== '1:1')
99
101
  || typeof maxBytes !== 'number' || !Number.isSafeInteger(maxBytes) || maxBytes < 1) {
100
102
  throw new Error('Media video job has an invalid request payload');
101
103
  }
102
- return { provider, region, model, prompt, resolution, duration, ratio, maxBytes };
104
+ const normalized = normalizeMiniMaxVideoRequest({
105
+ region, model, prompt, resolution, duration, ratio,
106
+ firstFrameImageUrl: value.firstFrameImageUrl as string | undefined,
107
+ });
108
+ return { provider, ...normalized, maxBytes };
103
109
  }
104
110
 
105
111
  function mediaJobIdFromMaintenance(job: MaintenanceJob): string {
@@ -113,6 +119,7 @@ function mediaJobIdFromMaintenance(job: MaintenanceJob): string {
113
119
  function toVideoRequest(request: StoredMiniMaxVideoRequest): MiniMaxVideoGenerationRequest {
114
120
  return {
115
121
  prompt: request.prompt,
122
+ firstFrameImageUrl: request.firstFrameImageUrl,
116
123
  model: request.model,
117
124
  region: request.region,
118
125
  resolution: request.resolution,
@@ -233,6 +240,7 @@ export function queueMiniMaxVideoGeneration(input: QueueMiniMaxVideoInput): Queu
233
240
  region: normalized.region,
234
241
  model: normalized.model,
235
242
  prompt: normalized.prompt,
243
+ ...(normalized.firstFrameImageUrl ? { firstFrameImageUrl: normalized.firstFrameImageUrl } : {}),
236
244
  resolution: normalized.resolution,
237
245
  duration: normalized.duration,
238
246
  ratio: normalized.ratio,
package/src/server.ts CHANGED
@@ -5208,6 +5208,7 @@ export async function createMemorixServer(
5208
5208
  } : {}),
5209
5209
  ...(mediaGenerationEnabled ? {
5210
5210
  image: z.string().optional().describe('Base64-encoded reference image for image-to-image generation.'),
5211
+ firstFrameImageUrl: z.string().optional().describe('Public HTTPS first-frame image URL for MiniMax video generation; stored with the queued job.'),
5211
5212
  region: z.enum(['global', 'cn']).optional().describe('MiniMax deployment region.'),
5212
5213
  n: z.number().int().min(1).max(4).optional().describe('Image output count.'),
5213
5214
  ratio: z.enum(['adaptive', '1:1', '16:9', '4:3', '3:2', '2:3', '3:4', '9:16', '21:9']).optional().describe('Image or video aspect ratio.'),
@@ -5217,7 +5218,7 @@ export async function createMemorixServer(
5217
5218
  } : {}),
5218
5219
  },
5219
5220
  },
5220
- async ({ action, path: assetPath, assetId, jobId, kind, limit, title, narrative, prompt, image, model, provider, language, region, n, ratio, width, height, duration, maxPages, maxChars, attach }) => {
5221
+ async ({ action, path: assetPath, assetId, jobId, kind, limit, title, narrative, prompt, image, firstFrameImageUrl, model, provider, language, region, n, ratio, width, height, duration, maxPages, maxChars, attach }) => {
5221
5222
  const unresolved = requireResolvedProject('manage controlled media');
5222
5223
  if (unresolved) return unresolved;
5223
5224
  const safeError = (error: unknown) => sanitizeCredentials(
@@ -5402,6 +5403,7 @@ export async function createMemorixServer(
5402
5403
  dataDir: projectDir,
5403
5404
  projectId: project.id,
5404
5405
  prompt,
5406
+ firstFrameImageUrl,
5405
5407
  ...(model === 'MiniMax-H3' ? { model } : {}),
5406
5408
  ...(region ? { region } : {}),
5407
5409
  ...(videoRatio ? { ratio: videoRatio } : {}),
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Backends:
5
5
  * - MiniSkillSqliteStore — canonical store, uses shared DB handle from sqlite-db.ts
6
- * - MiniSkillGracefulDegrade — no-op fallback when SQLite is unavailable
6
+ * - MiniSkillGracefulDegrade — explicit read-only fallback when SQLite is unavailable
7
7
  *
8
8
  * Phase 2 debt-zero: SQLite is the only canonical store for mini-skills.
9
9
  * JSON files are migration source only. No writable JSON fallback exists.
@@ -11,6 +11,7 @@
11
11
 
12
12
  import type { MiniSkill } from '../types.js';
13
13
  import { getDatabase } from './sqlite-db.js';
14
+ import { degradedReadError, degradedWriteError, initializeSqliteStore, isSqliteUnavailableError, retrySqliteBusy } from './sqlite-reliability.js';
14
15
  import {
15
16
  loadMiniSkillsJson,
16
17
  } from './persistence.js';
@@ -36,6 +37,7 @@ export interface MiniSkillStore {
36
37
  ensureFresh(): Promise<boolean>;
37
38
  getGeneration(): number;
38
39
  getBackendName(): 'sqlite' | 'degraded';
40
+ getBackendReason?(): string | undefined;
39
41
  }
40
42
 
41
43
  // ── Row <-> MiniSkill serialization ─────────────────────────────────
@@ -220,7 +222,7 @@ export class MiniSkillSqliteStore implements MiniSkillStore {
220
222
  * SQLite serializes write transactions, so concurrent calls are safely sequenced.
221
223
  */
222
224
  async atomicInsertWithId(skillWithoutId: Omit<MiniSkill, 'id'>): Promise<MiniSkill> {
223
- const result = this.db.transaction(() => {
225
+ return retrySqliteBusy(async () => this.db.transaction(() => {
224
226
  // 1. Read current counter inside the transaction
225
227
  const row = this.stmtGetMeta.get('mini_skills_next_id');
226
228
  const nextId = row ? parseInt(row.value, 10) : 1;
@@ -239,8 +241,7 @@ export class MiniSkillSqliteStore implements MiniSkillStore {
239
241
  this.knownGeneration = this.readGeneration();
240
242
 
241
243
  return skill;
242
- })();
243
- return result;
244
+ })(), 'Mini-skill promotion');
244
245
  }
245
246
 
246
247
  // ── Freshness ────────────────────────────────────────────────────
@@ -266,16 +267,21 @@ export class MiniSkillSqliteStore implements MiniSkillStore {
266
267
  // ── Graceful Degrade Fallback ────────────────────────────────────────
267
268
  //
268
269
  // Phase 2 debt-zero rule: mini-skills have NO writable JSON fallback.
269
- // In JSON-only environments (no better-sqlite3), reads return empty
270
- // and writes are no-ops with a warning. This prevents a parallel
271
- // canonical JSON write path from existing alongside SQLite.
270
+ // In environments without a supported SQLite runtime, reads return an
271
+ // explicit unavailable error and writes fail rather than pretending success.
272
272
 
273
273
  export class MiniSkillGracefulDegrade implements MiniSkillStore {
274
274
  private warned = false;
275
275
 
276
+ constructor(private readonly reason = 'SQLite runtime unavailable') {}
277
+
278
+ private unavailableRead(): Error {
279
+ return degradedReadError('mini-skills');
280
+ }
281
+
276
282
  private warn(): void {
277
283
  if (!this.warned) {
278
- console.error('[memorix] MiniSkillStore: SQLite unavailable — mini-skills are disabled (read-only empty). Install better-sqlite3 for full functionality.');
284
+ console.error('[memorix] MiniSkillStore: SQLite unavailable — mini-skills cannot be read or written. Install a supported SQLite runtime.');
279
285
  this.warned = true;
280
286
  }
281
287
  }
@@ -284,22 +290,23 @@ export class MiniSkillGracefulDegrade implements MiniSkillStore {
284
290
  this.warn();
285
291
  }
286
292
 
287
- async loadAll(): Promise<MiniSkill[]> { return []; }
288
- async loadByProject(_projectId: string): Promise<MiniSkill[]> { return []; }
289
- async loadIdCounter(): Promise<number> { return 1; }
293
+ async loadAll(): Promise<MiniSkill[]> { throw this.unavailableRead(); }
294
+ async loadByProject(_projectId: string): Promise<MiniSkill[]> { throw this.unavailableRead(); }
295
+ async loadIdCounter(): Promise<number> { throw this.unavailableRead(); }
290
296
 
291
- async insert(_skill: MiniSkill): Promise<void> { this.warn(); }
292
- async update(_skill: MiniSkill): Promise<void> { this.warn(); }
293
- async remove(_id: number): Promise<void> { this.warn(); }
294
- async saveIdCounter(_nextId: number): Promise<void> { /* no-op */ }
297
+ async insert(_skill: MiniSkill): Promise<void> { this.warn(); throw degradedWriteError('mini-skills'); }
298
+ async update(_skill: MiniSkill): Promise<void> { this.warn(); throw degradedWriteError('mini-skills'); }
299
+ async remove(_id: number): Promise<void> { this.warn(); throw degradedWriteError('mini-skills'); }
300
+ async saveIdCounter(_nextId: number): Promise<void> { this.warn(); throw degradedWriteError('mini-skills'); }
295
301
  async atomicInsertWithId(skillWithoutId: Omit<MiniSkill, 'id'>): Promise<MiniSkill> {
296
302
  this.warn();
297
- return { ...skillWithoutId, id: 0 } as MiniSkill;
303
+ throw degradedWriteError('mini-skills');
298
304
  }
299
305
 
300
306
  async ensureFresh(): Promise<boolean> { return false; }
301
307
  getGeneration(): number { return 0; }
302
308
  getBackendName(): 'sqlite' | 'degraded' { return 'degraded'; }
309
+ getBackendReason(): string { return this.reason; }
303
310
  }
304
311
 
305
312
  // ── Singleton access ────────────────────────────────────────────────
@@ -329,19 +336,21 @@ export async function initMiniSkillStore(dataDir: string): Promise<MiniSkillStor
329
336
  _store = null;
330
337
  _storeDataDir = null;
331
338
 
332
- // Try SQLite first
339
+ // Only a missing SQLite runtime may use the intentional read-only fallback.
340
+ let degradedReason = 'SQLite runtime unavailable';
333
341
  try {
334
- const store = new MiniSkillSqliteStore();
335
- await store.init(dataDir);
342
+ const store = await initializeSqliteStore(() => new MiniSkillSqliteStore(), dataDir, 'Mini-skill store');
336
343
  _store = store;
337
344
  _storeDataDir = dataDir;
338
345
  return store;
339
346
  } catch (err) {
340
- console.error(`[memorix] MiniSkillSqliteStore unavailable, running in degraded read-only mode: ${err instanceof Error ? err.message : err}`);
347
+ if (!isSqliteUnavailableError(err)) throw err;
348
+ degradedReason = err instanceof Error ? err.message : String(err);
349
+ console.error(`[memorix] MiniSkillSqliteStore unavailable, running in degraded read-only mode: ${degradedReason}`);
341
350
  }
342
351
 
343
- // Fallback: graceful degrade (no writable JSON backend per debt-zero rule)
344
- const store = new MiniSkillGracefulDegrade();
352
+ // Fallback: explicit read-only mode (no writable JSON backend per debt-zero rule)
353
+ const store = new MiniSkillGracefulDegrade(degradedReason);
345
354
  await store.init(dataDir);
346
355
  _store = store;
347
356
  _storeDataDir = dataDir;