memorix 1.9.1 → 1.9.3
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 +22 -0
- package/README.md +17 -4
- package/README.zh-CN.md +8 -4
- package/dist/cli/index.js +448 -243
- package/dist/index.js +302 -165
- package/dist/maintenance-runner.js +194 -98
- package/dist/media-audio-runner.js +196 -100
- package/dist/media-video-runner.js +194 -98
- package/dist/memcode-runtime/CHANGELOG.md +22 -0
- package/dist/sdk.js +301 -164
- package/dist/vector-backfill-runner.js +194 -98
- package/docs/AGENT_OPERATOR_PLAYBOOK.md +15 -0
- package/docs/ARCHITECTURE.md +23 -0
- package/docs/DEVELOPMENT.md +1 -1
- package/docs/INTEGRATIONS.md +4 -0
- package/docs/PERFORMANCE.md +50 -0
- package/docs/README.md +1 -1
- package/docs/SETUP.md +5 -0
- package/package.json +1 -2
- 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/memory.ts +2 -1
- package/src/cli/default-mode.ts +15 -0
- package/src/cli/index.ts +10 -1
- package/src/codegraph/lite-provider.ts +10 -1
- package/src/dashboard/maintenance.ts +11 -2
- package/src/memory/consolidation.ts +15 -6
- 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/assets/star-history-dark.svg +0 -30
- package/assets/star-history-light.svg +0 -30
- package/assets/star-history.json +0 -712
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.3",
|
|
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.3",
|
|
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
|
});
|
|
@@ -280,7 +280,8 @@ export default defineCommand({
|
|
|
280
280
|
case 'deduplicate': {
|
|
281
281
|
const query = asStringArg(args.query);
|
|
282
282
|
const dryRun = !!args.dryRun || !!args['dry-run'];
|
|
283
|
-
const { isLLMEnabled } = await import('../../llm/provider.js');
|
|
283
|
+
const { initLLM, isLLMEnabled } = await import('../../llm/provider.js');
|
|
284
|
+
initLLM({ scope: 'memory', projectRoot: project.rootPath });
|
|
284
285
|
if (!isLLMEnabled()) {
|
|
285
286
|
emitResult(
|
|
286
287
|
{ project, available: false, usedLLM: false },
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface CliInteractivity {
|
|
2
|
+
stdinIsTTY?: boolean;
|
|
3
|
+
stdoutIsTTY?: boolean;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* MCP hosts start package commands with piped stdio, while a human terminal
|
|
8
|
+
* has TTYs on both streams. Keep the friendly memcode default for humans and
|
|
9
|
+
* make the package safe for registry probes and generic MCP launchers.
|
|
10
|
+
*/
|
|
11
|
+
export function shouldDefaultToMcp(options: CliInteractivity = {}): boolean {
|
|
12
|
+
const stdinIsTTY = options.stdinIsTTY ?? process.stdin.isTTY;
|
|
13
|
+
const stdoutIsTTY = options.stdoutIsTTY ?? process.stdout.isTTY;
|
|
14
|
+
return stdinIsTTY !== true || stdoutIsTTY !== true;
|
|
15
|
+
}
|
package/src/cli/index.ts
CHANGED
|
@@ -20,6 +20,7 @@ import { importBundledMemcode } from './memcode-bootstrap.js';
|
|
|
20
20
|
import { installCliPipeErrorGuard } from './pipe-errors.js';
|
|
21
21
|
import { normalizeCliInvocation } from './invocation.js';
|
|
22
22
|
import { printCliGuideForHelp, renderCliGuide } from './command-guide.js';
|
|
23
|
+
import { shouldDefaultToMcp } from './default-mode.js';
|
|
23
24
|
|
|
24
25
|
installCliPipeErrorGuard();
|
|
25
26
|
|
|
@@ -327,8 +328,16 @@ const main = defineCommand({
|
|
|
327
328
|
'background', 'bg', 'bs', 'doctor', 'repair', 'dashboard', 'cleanup', 'purge', 'uninstall', 'orchestrate'];
|
|
328
329
|
if (firstArg && knownSubs.includes(firstArg)) return;
|
|
329
330
|
|
|
330
|
-
// No subcommand provided —
|
|
331
|
+
// No subcommand provided — keep the interactive memcode default for a
|
|
332
|
+
// human terminal, but make piped package launches an MCP server. Generic
|
|
333
|
+
// MCP directories and clients commonly execute `npx memorix` without
|
|
334
|
+
// reading server.json's package arguments.
|
|
331
335
|
if (!firstArg) {
|
|
336
|
+
if (shouldDefaultToMcp()) {
|
|
337
|
+
const serve = await import('./commands/serve.js');
|
|
338
|
+
await serve.default.run?.({ args: {}, rawArgs: [], cmd: serve.default } as any);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
332
341
|
try {
|
|
333
342
|
const { runCli } = await importBundledMemcode();
|
|
334
343
|
await runCli(process.argv.slice(2));
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import { readdirSync, readFileSync, statSync, type Dirent } from 'node:fs';
|
|
2
|
+
import { existsSync, readdirSync, readFileSync, statSync, type Dirent } from 'node:fs';
|
|
3
3
|
import { join, relative } from 'node:path';
|
|
4
4
|
import type { CodeEdge, CodeFile, CodeStateSnapshot, CodeSymbol } from './types.js';
|
|
5
5
|
import { collectCodeStateSnapshot } from './code-state.js';
|
|
@@ -210,6 +210,14 @@ function resolveMaxFileBytes(value: number | undefined): number {
|
|
|
210
210
|
return Math.max(1, Math.floor(value!));
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
+
function isPythonVirtualenvDir(dir: string): boolean {
|
|
214
|
+
// PEP 405 virtualenvs place a `pyvenv.cfg` at their root regardless of the
|
|
215
|
+
// directory name (`.venv`, `.venv-leann`, `env`, uv/poetry envs, ...). Their
|
|
216
|
+
// `site-packages` trees are large and generated; descending into one can stall
|
|
217
|
+
// `codegraph refresh`, so skip virtualenvs by this marker rather than by name.
|
|
218
|
+
return existsSync(join(dir, 'pyvenv.cfg'));
|
|
219
|
+
}
|
|
220
|
+
|
|
213
221
|
function walk(root: string, exclude: string[], maxFiles: number): string[] {
|
|
214
222
|
const out: string[] = [];
|
|
215
223
|
const visit = (dir: string) => {
|
|
@@ -228,6 +236,7 @@ function walk(root: string, exclude: string[], maxFiles: number): string[] {
|
|
|
228
236
|
if (entry.isDirectory()) {
|
|
229
237
|
if (entry.name === '.git' || entry.name === '.worktrees' || entry.name === '.tmp' || entry.name === 'node_modules') continue;
|
|
230
238
|
if (rel === '.claude/worktrees' || rel.startsWith('.claude/worktrees/')) continue;
|
|
239
|
+
if (isPythonVirtualenvDir(abs)) continue;
|
|
231
240
|
visit(abs);
|
|
232
241
|
if (out.length >= maxFiles) return;
|
|
233
242
|
continue;
|
|
@@ -221,6 +221,7 @@ export async function previewConsolidate(context: DashboardMaintenanceContext) {
|
|
|
221
221
|
const clusters = await findConsolidationCandidates(
|
|
222
222
|
context.projectRoot ?? context.dataDir,
|
|
223
223
|
context.projectId,
|
|
224
|
+
{ store: context.store },
|
|
224
225
|
);
|
|
225
226
|
const payload: ConsolidateTokenPayload = { clusterIds: normalizeClusters(clusters.map((cluster) => cluster.ids)) };
|
|
226
227
|
return {
|
|
@@ -248,11 +249,19 @@ export async function executeConsolidate(
|
|
|
248
249
|
const clusterIds = record.clusterIds.map((ids, index) => payloadIds(ids, `clusterIds[${index}]`));
|
|
249
250
|
const normalized: ConsolidateTokenPayload = { clusterIds: normalizeClusters(clusterIds) };
|
|
250
251
|
verify('consolidate', context.projectId, normalized, token);
|
|
251
|
-
const current = await findConsolidationCandidates(
|
|
252
|
+
const current = await findConsolidationCandidates(
|
|
253
|
+
context.projectRoot ?? context.dataDir,
|
|
254
|
+
context.projectId,
|
|
255
|
+
{ store: context.store },
|
|
256
|
+
);
|
|
252
257
|
if (JSON.stringify(normalized.clusterIds) !== JSON.stringify(normalizeClusters(current.map((cluster) => cluster.ids)))) {
|
|
253
258
|
throw new DashboardMaintenanceError('Memory changed after the preview. Refresh before consolidating.', 409);
|
|
254
259
|
}
|
|
255
|
-
const result = await executeConsolidation(
|
|
260
|
+
const result = await executeConsolidation(
|
|
261
|
+
context.projectRoot ?? context.dataDir,
|
|
262
|
+
context.projectId,
|
|
263
|
+
{ store: context.store },
|
|
264
|
+
);
|
|
256
265
|
return { action: 'consolidate', projectId: context.projectId, ...result };
|
|
257
266
|
}
|
|
258
267
|
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import type { Observation } from '../types.js';
|
|
19
|
-
import { getObservationStore } from '../store/obs-store.js';
|
|
19
|
+
import { getObservationStore, type ObservationStore } from '../store/obs-store.js';
|
|
20
20
|
import { isEligibleForAutomaticDelivery } from './admission.js';
|
|
21
21
|
import { resolveObservationVisibility } from './visibility.js';
|
|
22
22
|
|
|
@@ -108,6 +108,14 @@ interface ConsolidationPage {
|
|
|
108
108
|
nextCursor?: number;
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
export interface ConsolidationOptions {
|
|
112
|
+
threshold?: number;
|
|
113
|
+
limit?: number;
|
|
114
|
+
afterId?: number;
|
|
115
|
+
/** Use a request-scoped store when running inside a multi-project host. */
|
|
116
|
+
store?: ObservationStore;
|
|
117
|
+
}
|
|
118
|
+
|
|
111
119
|
function clampBatchSize(limit: number | undefined): number {
|
|
112
120
|
if (!Number.isFinite(limit)) return MAX_BATCH_SIZE;
|
|
113
121
|
return Math.min(MAX_BATCH_SIZE, Math.max(1, Math.floor(limit!)));
|
|
@@ -120,10 +128,11 @@ function clampCursor(afterId: number | undefined): number {
|
|
|
120
128
|
|
|
121
129
|
async function loadConsolidationPage(
|
|
122
130
|
projectId: string,
|
|
123
|
-
options:
|
|
131
|
+
options: ConsolidationOptions,
|
|
124
132
|
): Promise<ConsolidationPage> {
|
|
125
133
|
const limit = clampBatchSize(options.limit);
|
|
126
|
-
const
|
|
134
|
+
const store = options.store ?? getObservationStore();
|
|
135
|
+
const page = await store.loadByProject(projectId, {
|
|
127
136
|
status: 'active',
|
|
128
137
|
afterId: clampCursor(options.afterId),
|
|
129
138
|
limit: limit + 1,
|
|
@@ -205,7 +214,7 @@ function findClusters(observations: Observation[], threshold: number): Consolida
|
|
|
205
214
|
export async function findConsolidationCandidates(
|
|
206
215
|
_projectDir: string,
|
|
207
216
|
projectId: string,
|
|
208
|
-
opts?:
|
|
217
|
+
opts?: ConsolidationOptions,
|
|
209
218
|
): Promise<ConsolidationCluster[]> {
|
|
210
219
|
const threshold = opts?.threshold ?? DEFAULT_SIMILARITY_THRESHOLD;
|
|
211
220
|
const page = await loadConsolidationPage(projectId, opts ?? {});
|
|
@@ -224,9 +233,9 @@ export async function findConsolidationCandidates(
|
|
|
224
233
|
export async function executeConsolidation(
|
|
225
234
|
_projectDir: string,
|
|
226
235
|
projectId: string,
|
|
227
|
-
opts?:
|
|
236
|
+
opts?: ConsolidationOptions,
|
|
228
237
|
): Promise<ConsolidationResult> {
|
|
229
|
-
const store = getObservationStore();
|
|
238
|
+
const store = opts?.store ?? getObservationStore();
|
|
230
239
|
const page = await loadConsolidationPage(projectId, opts ?? {});
|
|
231
240
|
const clusters = findClusters(page.observations, opts?.threshold ?? DEFAULT_SIMILARITY_THRESHOLD);
|
|
232
241
|
|
|
@@ -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;
|