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.
- package/CHANGELOG.md +27 -0
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/cli/index.js +358 -146
- package/dist/index.js +281 -125
- package/dist/maintenance-runner.js +219 -97
- package/dist/media-audio-runner.js +219 -97
- package/dist/media-video-runner.js +219 -97
- package/dist/memcode-runtime/CHANGELOG.md +27 -0
- package/dist/sdk.js +280 -124
- package/dist/vector-backfill-runner.js +219 -97
- package/docs/AGENT_OPERATOR_PLAYBOOK.md +15 -0
- package/docs/ARCHITECTURE.md +23 -0
- package/docs/DEVELOPMENT.md +1 -1
- package/docs/PERFORMANCE.md +50 -0
- package/docs/README.md +1 -1
- package/package.json +1 -1
- package/plugins/antigravity/memorix/plugin.json +1 -1
- package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
- package/plugins/codebuddy/memorix/.codebuddy-plugin/plugin.json +1 -1
- package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
- package/plugins/copilot/memorix/plugin.json +1 -1
- package/plugins/gemini/memorix/gemini-extension.json +1 -1
- package/plugins/hermes/memorix/plugin.yaml +1 -1
- package/plugins/omp/memorix/package.json +1 -1
- package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
- package/plugins/pi/memorix/package.json +1 -1
- package/server.json +2 -2
- package/src/cli/commands/doctor.ts +78 -17
- package/src/cli/commands/media.ts +2 -0
- package/src/media/minimax.ts +26 -2
- package/src/media/video-jobs.ts +9 -1
- package/src/server.ts +3 -1
- package/src/store/mini-skill-store.ts +31 -22
- package/src/store/obs-store.ts +46 -30
- package/src/store/session-store.ts +35 -21
- package/src/store/sqlite-db.ts +16 -7
- package/src/store/sqlite-reliability.ts +87 -0
- package/src/store/sqlite-store.ts +4 -1
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.
|
|
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.
|
|
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 {
|
|
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 (
|
|
301
|
-
lines.push(
|
|
302
|
-
issues.push(
|
|
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
|
|
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
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
|
-
|
|
333
|
-
|
|
334
|
-
if (
|
|
335
|
-
lines.push(
|
|
336
|
-
(
|
|
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 {
|
|
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),
|
package/src/media/minimax.ts
CHANGED
|
@@ -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
|
-
|
|
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: [
|
|
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,
|
package/src/media/video-jobs.ts
CHANGED
|
@@ -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
|
-
|
|
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 —
|
|
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
|
-
|
|
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
|
|
270
|
-
// and writes
|
|
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
|
|
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[]> {
|
|
288
|
-
async loadByProject(_projectId: string): Promise<MiniSkill[]> {
|
|
289
|
-
async loadIdCounter(): Promise<number> {
|
|
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> {
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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:
|
|
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;
|