memorix 1.9.5 → 1.9.7
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 +53 -0
- package/README.md +6 -1
- package/README.zh-CN.md +6 -1
- package/dist/cli/index.js +1743 -348
- package/dist/index.d.ts +12 -7
- package/dist/index.js +1048 -266
- package/dist/maintenance-runner.js +339 -187
- package/dist/media-audio-runner.js +339 -187
- package/dist/media-video-runner.js +339 -187
- package/dist/memcode-runtime/CHANGELOG.md +53 -0
- package/dist/sdk.js +967 -241
- package/dist/vector-backfill-runner.js +339 -187
- package/docs/API_REFERENCE.md +36 -0
- package/docs/CONFIGURATION.md +43 -0
- package/docs/DEVELOPMENT.md +1 -1
- package/docs/KNOWN_ISSUES_AND_ROADMAP.md +49 -14
- package/docs/PERFORMANCE.md +46 -2
- package/docs/README.md +2 -1
- package/docs/SETUP.md +1 -1
- package/package.json +2 -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/capability-map.ts +1 -0
- package/src/cli/command-guide.ts +13 -1
- package/src/cli/commands/context.ts +2 -0
- package/src/cli/commands/continuity.ts +155 -0
- package/src/cli/commands/init.ts +5 -2
- package/src/cli/commands/serve-http.ts +242 -42
- package/src/cli/index.ts +3 -1
- package/src/cli/tui/ConfigureView.tsx +2 -0
- package/src/codegraph/auto-context.ts +17 -0
- package/src/config/legacy-loader.ts +2 -2
- package/src/config/resolved-config.ts +8 -5
- package/src/config/yaml-loader.ts +20 -10
- package/src/embedding/api-provider.ts +4 -2
- package/src/embedding/fastembed-provider.ts +47 -14
- package/src/embedding/transformers-provider.ts +29 -7
- package/src/evaluation/memory-quality-gate.ts +90 -0
- package/src/hooks/official-skills.ts +2 -0
- package/src/knowledge/context-assembly.ts +1 -0
- package/src/knowledge/context-receipt.ts +15 -0
- package/src/knowledge/task-continuity.ts +413 -0
- package/src/knowledge/workset.ts +77 -0
- package/src/llm/provider.ts +2 -1
- package/src/orchestrate/coordinator.ts +60 -0
- package/src/orchestrate/effect-ledger.ts +160 -0
- package/src/orchestrate/pipeline-trace.ts +40 -6
- package/src/runtime/maintenance-jobs.ts +12 -6
- package/src/server/modern-mcp-bridge.ts +153 -27
- package/src/server/tool-profile.ts +3 -2
- package/src/server.ts +129 -5
- package/src/store/orama-store.ts +16 -1
- package/src/store/sqlite-db.ts +144 -4
- package/src/store/sqlite-fts.ts +38 -16
- package/src/team/team-store.ts +62 -27
package/src/server.ts
CHANGED
|
@@ -253,7 +253,9 @@ export async function createMemorixServer(
|
|
|
253
253
|
graphManager: KnowledgeGraphManager;
|
|
254
254
|
projectId: string;
|
|
255
255
|
deferredInit: () => Promise<void>;
|
|
256
|
-
|
|
256
|
+
activateProjectRuntime: () => Promise<void>;
|
|
257
|
+
switchProject: (newCwd: string, source?: ProjectBindingSource) => Promise<boolean>;
|
|
258
|
+
getProjectDataDir: () => string;
|
|
257
259
|
isExplicitlyBound: () => boolean;
|
|
258
260
|
getRequestContext: () => import('./server/request-context.js').MemorixRequestContext;
|
|
259
261
|
handleTransportClose: () => void;
|
|
@@ -497,6 +499,31 @@ export async function createMemorixServer(
|
|
|
497
499
|
await projectRuntimeInitPromise;
|
|
498
500
|
};
|
|
499
501
|
|
|
502
|
+
/**
|
|
503
|
+
* Restore process-wide stores/config before a pooled modern runtime handles
|
|
504
|
+
* a tool call. The durable database is shared, but project-root config and
|
|
505
|
+
* the in-memory observation facade are process-global legacy state.
|
|
506
|
+
*/
|
|
507
|
+
const activateProjectRuntime = async (): Promise<void> => {
|
|
508
|
+
if (!projectResolved) return;
|
|
509
|
+
|
|
510
|
+
await initObservationStore(projectDir);
|
|
511
|
+
await initMiniSkillStore(projectDir);
|
|
512
|
+
await initSessionStore(projectDir);
|
|
513
|
+
await initObservations(projectDir, { skipCorpusLoad: true });
|
|
514
|
+
|
|
515
|
+
try {
|
|
516
|
+
const { initProjectRoot } = await import('./config/yaml-loader.js');
|
|
517
|
+
const { resetDotenv, loadDotenv } = await import('./config/dotenv-loader.js');
|
|
518
|
+
initProjectRoot(project.rootPath);
|
|
519
|
+
resetDotenv();
|
|
520
|
+
loadDotenv(project.rootPath);
|
|
521
|
+
} catch {
|
|
522
|
+
// Config activation is best-effort; durable project scoping remains explicit.
|
|
523
|
+
}
|
|
524
|
+
initLLM({ projectRoot: project.rootPath });
|
|
525
|
+
};
|
|
526
|
+
|
|
500
527
|
let maintenanceWorker: { stop(): void } | null = null;
|
|
501
528
|
const startProjectMaintenanceWorker = async (autoCleanup = false): Promise<void> => {
|
|
502
529
|
maintenanceWorker?.stop();
|
|
@@ -838,8 +865,9 @@ export async function createMemorixServer(
|
|
|
838
865
|
currentFormationStage = event.stage;
|
|
839
866
|
if (event.stageDurationMs !== undefined) {
|
|
840
867
|
completedFormationStages[event.stage] = event.stageDurationMs;
|
|
841
|
-
|
|
842
|
-
|
|
868
|
+
}
|
|
869
|
+
};
|
|
870
|
+
|
|
843
871
|
try {
|
|
844
872
|
const formationConfig: FormationConfig = {
|
|
845
873
|
mode: 'active',
|
|
@@ -1593,6 +1621,7 @@ export async function createMemorixServer(
|
|
|
1593
1621
|
'reliable code-bound memories, stale/suspect cautions, and verification hints. Use this at the start of a new coding turn or after switching tasks.',
|
|
1594
1622
|
inputSchema: {
|
|
1595
1623
|
task: z.string().optional().describe('Current coding task or question'),
|
|
1624
|
+
taskId: z.string().max(120).optional().describe('Optional task continuity ledger id to include in the brief'),
|
|
1596
1625
|
refresh: z.enum(['auto', 'always', 'never']).optional().default('auto').describe(
|
|
1597
1626
|
'Code Memory refresh policy. auto refreshes only when missing or stale.',
|
|
1598
1627
|
),
|
|
@@ -1606,7 +1635,7 @@ export async function createMemorixServer(
|
|
|
1606
1635
|
limit: z.number().optional().describe('Reserved for future source limits; current prompt stays compact by default.'),
|
|
1607
1636
|
},
|
|
1608
1637
|
},
|
|
1609
|
-
async ({ task, refresh, format, agent }) => {
|
|
1638
|
+
async ({ task, taskId, refresh, format, agent }) => {
|
|
1610
1639
|
const unresolved = requireResolvedProject('build project context for the current project');
|
|
1611
1640
|
if (unresolved) return unresolved;
|
|
1612
1641
|
|
|
@@ -1637,6 +1666,7 @@ export async function createMemorixServer(
|
|
|
1637
1666
|
dataDir: projectDir,
|
|
1638
1667
|
observations,
|
|
1639
1668
|
task,
|
|
1669
|
+
taskId,
|
|
1640
1670
|
agent,
|
|
1641
1671
|
refresh: refresh ?? 'auto',
|
|
1642
1672
|
reader: getObservationReader(),
|
|
@@ -4594,6 +4624,99 @@ export async function createMemorixServer(
|
|
|
4594
4624
|
},
|
|
4595
4625
|
);
|
|
4596
4626
|
|
|
4627
|
+
server.registerTool(
|
|
4628
|
+
'memorix_continuity',
|
|
4629
|
+
{
|
|
4630
|
+
title: 'Task Continuity Ledger',
|
|
4631
|
+
description:
|
|
4632
|
+
'Maintain a bounded, append-only continuity ledger for a coding task. ' +
|
|
4633
|
+
'Use start for requirements, record for decisions/verification/risks, ' +
|
|
4634
|
+
'show or list to inspect, and close for the final outcome. Pass the returned taskId ' +
|
|
4635
|
+
'to memorix_project_context so the current brief includes this ledger.',
|
|
4636
|
+
inputSchema: {
|
|
4637
|
+
action: z.enum(['start', 'record', 'show', 'list', 'close']).describe('Continuity operation'),
|
|
4638
|
+
taskId: z.string().max(120).optional().describe('Continuity task id'),
|
|
4639
|
+
task: z.string().max(1_000).optional().describe('Task description for start'),
|
|
4640
|
+
requirements: z.array(z.string().max(2_000)).max(8).optional().describe('Requirements for start'),
|
|
4641
|
+
kind: z.enum(['requirement', 'decision', 'verification', 'risk']).optional().describe('Entry kind for record'),
|
|
4642
|
+
content: z.string().max(2_000).optional().describe('Requirement, decision, verification, risk, or outcome text'),
|
|
4643
|
+
verificationStatus: z.enum(['pending', 'passed', 'failed', 'skipped']).optional(),
|
|
4644
|
+
verificationId: z.string().max(120).optional().describe('Stable verification obligation id; reuse it to move pending to passed/failed'),
|
|
4645
|
+
idempotencyKey: z.string().max(200).optional().describe('Stable retry key; repeating the same operation returns the original event'),
|
|
4646
|
+
status: z.enum(['completed', 'blocked', 'abandoned']).optional().describe('Outcome status for close'),
|
|
4647
|
+
sourceRef: z.string().max(500).optional().describe('Evidence or source reference'),
|
|
4648
|
+
evidenceRefs: z.array(z.string().max(500)).max(8).optional(),
|
|
4649
|
+
actor: z.string().max(200).optional(),
|
|
4650
|
+
limit: z.number().int().positive().max(100).optional().default(20),
|
|
4651
|
+
},
|
|
4652
|
+
},
|
|
4653
|
+
async ({ action, taskId, task, requirements, kind, content, verificationStatus, verificationId, idempotencyKey, status, sourceRef, evidenceRefs, actor, limit }) => {
|
|
4654
|
+
const unresolved = requireResolvedProject('manage task continuity for the current project');
|
|
4655
|
+
if (unresolved) return unresolved;
|
|
4656
|
+
const { TaskContinuityStore } = await import('./knowledge/task-continuity.js');
|
|
4657
|
+
const continuity = new TaskContinuityStore();
|
|
4658
|
+
await continuity.init(projectDir);
|
|
4659
|
+
const effectiveActor = actor ?? currentAgentId;
|
|
4660
|
+
try {
|
|
4661
|
+
if (action === 'start') {
|
|
4662
|
+
if (!task?.trim()) return { content: [{ type: 'text' as const, text: 'task is required for continuity start.' }], isError: true };
|
|
4663
|
+
const result = continuity.start({
|
|
4664
|
+
projectId: project.id,
|
|
4665
|
+
task,
|
|
4666
|
+
...(taskId ? { taskId } : {}),
|
|
4667
|
+
requirements,
|
|
4668
|
+
...(effectiveActor ? { actor: effectiveActor } : {}),
|
|
4669
|
+
...(idempotencyKey ? { idempotencyKey } : {}),
|
|
4670
|
+
});
|
|
4671
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ projectId: project.id, ...result }, null, 2) }] };
|
|
4672
|
+
}
|
|
4673
|
+
if (action === 'list') {
|
|
4674
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ projectId: project.id, ledgers: continuity.list(project.id, limit), }, null, 2) }] };
|
|
4675
|
+
}
|
|
4676
|
+
if (!taskId?.trim()) return { content: [{ type: 'text' as const, text: 'taskId is required for this continuity action.' }], isError: true };
|
|
4677
|
+
if (action === 'show') {
|
|
4678
|
+
const ledger = continuity.get(project.id, taskId);
|
|
4679
|
+
if (!ledger) return { content: [{ type: 'text' as const, text: 'Task continuity ledger not found.' }], isError: true };
|
|
4680
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ projectId: project.id, ledger }, null, 2) }] };
|
|
4681
|
+
}
|
|
4682
|
+
if (action === 'record') {
|
|
4683
|
+
if (!kind || !content?.trim()) {
|
|
4684
|
+
return { content: [{ type: 'text' as const, text: 'record requires kind and content.' }], isError: true };
|
|
4685
|
+
}
|
|
4686
|
+
const result = continuity.record({
|
|
4687
|
+
projectId: project.id,
|
|
4688
|
+
taskId,
|
|
4689
|
+
kind,
|
|
4690
|
+
content,
|
|
4691
|
+
...(verificationStatus ? { verificationStatus } : {}),
|
|
4692
|
+
...(sourceRef ? { sourceRef } : {}),
|
|
4693
|
+
...(evidenceRefs ? { evidenceRefs } : {}),
|
|
4694
|
+
...(effectiveActor ? { actor: effectiveActor } : {}),
|
|
4695
|
+
...(verificationId ? { verificationId } : {}),
|
|
4696
|
+
...(idempotencyKey ? { idempotencyKey } : {}),
|
|
4697
|
+
});
|
|
4698
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ projectId: project.id, ...result }, null, 2) }] };
|
|
4699
|
+
}
|
|
4700
|
+
if (!status || !content?.trim()) {
|
|
4701
|
+
return { content: [{ type: 'text' as const, text: 'close requires status and content.' }], isError: true };
|
|
4702
|
+
}
|
|
4703
|
+
const result = continuity.close({
|
|
4704
|
+
projectId: project.id,
|
|
4705
|
+
taskId,
|
|
4706
|
+
status,
|
|
4707
|
+
content,
|
|
4708
|
+
...(sourceRef ? { sourceRef } : {}),
|
|
4709
|
+
...(evidenceRefs ? { evidenceRefs } : {}),
|
|
4710
|
+
...(effectiveActor ? { actor: effectiveActor } : {}),
|
|
4711
|
+
...(idempotencyKey ? { idempotencyKey } : {}),
|
|
4712
|
+
});
|
|
4713
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ projectId: project.id, ...result }, null, 2) }] };
|
|
4714
|
+
} catch (error) {
|
|
4715
|
+
return { content: [{ type: 'text' as const, text: error instanceof Error ? error.message : String(error) }], isError: true };
|
|
4716
|
+
}
|
|
4717
|
+
},
|
|
4718
|
+
);
|
|
4719
|
+
|
|
4597
4720
|
// Use shared TeamStore (from HTTP server) or create new one (stdio mode).
|
|
4598
4721
|
// All team state is canonical in SQLite — no JSON persistence, no sync/flush.
|
|
4599
4722
|
if (teamFeaturesEnabled) {
|
|
@@ -5725,7 +5848,8 @@ export async function createMemorixServer(
|
|
|
5725
5848
|
};
|
|
5726
5849
|
|
|
5727
5850
|
return {
|
|
5728
|
-
server, graphManager, projectId: project.id, deferredInit, switchProject,
|
|
5851
|
+
server, graphManager, projectId: project.id, deferredInit, activateProjectRuntime, switchProject,
|
|
5852
|
+
getProjectDataDir: () => projectDir,
|
|
5729
5853
|
isExplicitlyBound: () => projectBinding.isExplicit(),
|
|
5730
5854
|
getRequestContext,
|
|
5731
5855
|
handleTransportClose,
|
package/src/store/orama-store.ts
CHANGED
|
@@ -909,6 +909,11 @@ export async function searchObservations(options: SearchOptions): Promise<IndexE
|
|
|
909
909
|
// Fast-tier queries skip embedding entirely (fulltext is sufficient)
|
|
910
910
|
let queryVector: number[] | null = null;
|
|
911
911
|
let embeddingProviderForSearch: EmbeddingProvider | null = null;
|
|
912
|
+
// Distinct from `embeddingEnabled` (static config): this tracks whether the
|
|
913
|
+
// current query can use embeddings (provider unavailable, incompatible
|
|
914
|
+
// dimensions, TLS, network, or timeout). `canUsePersistentLexical` below
|
|
915
|
+
// must fall back whenever this is true.
|
|
916
|
+
let embeddingUnavailable = false;
|
|
912
917
|
if (quality !== 'fast' && embeddingEnabled && hasQuery && tier !== 'fast') {
|
|
913
918
|
try {
|
|
914
919
|
const provider = await getEmbeddingProvider();
|
|
@@ -916,6 +921,7 @@ export async function searchObservations(options: SearchOptions): Promise<IndexE
|
|
|
916
921
|
embeddingProviderForSearch = provider;
|
|
917
922
|
const activeVectorDimensions = getVectorDimensions();
|
|
918
923
|
if (activeVectorDimensions !== null && provider.dimensions !== activeVectorDimensions) {
|
|
924
|
+
embeddingUnavailable = true;
|
|
919
925
|
rememberSearchMode(
|
|
920
926
|
modeKey,
|
|
921
927
|
`fulltext (embedding dimension mismatch: provider ${provider.dimensions}d vs index ${activeVectorDimensions}d)`,
|
|
@@ -961,9 +967,12 @@ export async function searchObservations(options: SearchOptions): Promise<IndexE
|
|
|
961
967
|
: { text: 0.6, vector: 0.4 },
|
|
962
968
|
};
|
|
963
969
|
}
|
|
970
|
+
} else {
|
|
971
|
+
embeddingUnavailable = true;
|
|
964
972
|
}
|
|
965
973
|
} catch (error) {
|
|
966
974
|
// Fallback to fulltext if embedding fails or times out
|
|
975
|
+
embeddingUnavailable = true;
|
|
967
976
|
rememberSearchMode(modeKey, 'fulltext (embedding unavailable)');
|
|
968
977
|
console.error('[memorix] Embedding failed or timed out, falling back to fulltext search');
|
|
969
978
|
}
|
|
@@ -972,8 +981,14 @@ export async function searchObservations(options: SearchOptions): Promise<IndexE
|
|
|
972
981
|
mark('preSearch');
|
|
973
982
|
let results: SearchResultLike | Awaited<ReturnType<typeof search>>;
|
|
974
983
|
const persistentCorpusEligible = await shouldUsePersistentSearch(projectIds, options.projectId);
|
|
984
|
+
// `embeddingEnabled` alone misses the common case: embedding is configured
|
|
985
|
+
// (enabled) but the current provider is unavailable, has incompatible
|
|
986
|
+
// dimensions, or failed at runtime. Without this per-query state, those
|
|
987
|
+
// failures leave queryVector null AND canUsePersistentLexical false, so
|
|
988
|
+
// neither the semantic nor lexical persistent path runs — a silent,
|
|
989
|
+
// deterministic empty result despite the FTS5 index having real matches.
|
|
975
990
|
const canUsePersistentLexical = persistentCorpusEligible && Boolean(hasQuery) && !queryVector &&
|
|
976
|
-
(quality === 'fast' || tier === 'fast' || !embeddingEnabled);
|
|
991
|
+
(quality === 'fast' || tier === 'fast' || !embeddingEnabled || embeddingUnavailable);
|
|
977
992
|
let persistent = persistentCorpusEligible && queryVector && embeddingProviderForSearch
|
|
978
993
|
? await searchPersistentSemantically(options, projectIds, requestLimit, queryVector, embeddingProviderForSearch)
|
|
979
994
|
: canUsePersistentLexical
|
package/src/store/sqlite-db.ts
CHANGED
|
@@ -821,6 +821,45 @@ CREATE TABLE IF NOT EXISTS memory_feedback_events (
|
|
|
821
821
|
);
|
|
822
822
|
`;
|
|
823
823
|
|
|
824
|
+
const CREATE_TASK_CONTINUITY_EVENTS_TABLE = `
|
|
825
|
+
CREATE TABLE IF NOT EXISTS task_continuity_events (
|
|
826
|
+
id TEXT PRIMARY KEY,
|
|
827
|
+
project_id TEXT NOT NULL,
|
|
828
|
+
task_id TEXT NOT NULL,
|
|
829
|
+
task TEXT NOT NULL,
|
|
830
|
+
kind TEXT NOT NULL,
|
|
831
|
+
content TEXT NOT NULL,
|
|
832
|
+
item_status TEXT,
|
|
833
|
+
source_ref TEXT,
|
|
834
|
+
evidence_json TEXT NOT NULL DEFAULT '[]',
|
|
835
|
+
actor TEXT,
|
|
836
|
+
created_at TEXT NOT NULL,
|
|
837
|
+
event_key TEXT,
|
|
838
|
+
item_key TEXT,
|
|
839
|
+
supersedes_id TEXT
|
|
840
|
+
);
|
|
841
|
+
`;
|
|
842
|
+
|
|
843
|
+
const CREATE_AGENT_EFFECTS_TABLE = `
|
|
844
|
+
CREATE TABLE IF NOT EXISTS agent_effects (
|
|
845
|
+
id TEXT PRIMARY KEY,
|
|
846
|
+
pipeline_id TEXT NOT NULL,
|
|
847
|
+
task_id TEXT NOT NULL,
|
|
848
|
+
attempt INTEGER NOT NULL,
|
|
849
|
+
effect_key TEXT NOT NULL,
|
|
850
|
+
tool TEXT NOT NULL,
|
|
851
|
+
risk_tier TEXT NOT NULL,
|
|
852
|
+
replay_policy TEXT NOT NULL,
|
|
853
|
+
status TEXT NOT NULL,
|
|
854
|
+
call_id TEXT,
|
|
855
|
+
idempotency_key TEXT NOT NULL,
|
|
856
|
+
input_fingerprint TEXT,
|
|
857
|
+
detail TEXT,
|
|
858
|
+
started_at INTEGER NOT NULL,
|
|
859
|
+
finished_at INTEGER
|
|
860
|
+
);
|
|
861
|
+
`;
|
|
862
|
+
|
|
824
863
|
const CREATE_MCP_BINDINGS_TABLE = `
|
|
825
864
|
CREATE TABLE IF NOT EXISTS mcp_bindings (
|
|
826
865
|
handle_id TEXT PRIMARY KEY,
|
|
@@ -899,7 +938,18 @@ CREATE INDEX IF NOT EXISTS idx_evidence_cards_candidate ON evidence_cards(projec
|
|
|
899
938
|
CREATE INDEX IF NOT EXISTS idx_evidence_card_events_card ON evidence_card_events(card_id, created_at);
|
|
900
939
|
CREATE INDEX IF NOT EXISTS idx_memory_feedback_states_project ON memory_feedback_states(project_id, candidate_kind, updated_at DESC);
|
|
901
940
|
CREATE INDEX IF NOT EXISTS idx_memory_feedback_events_candidate ON memory_feedback_events(project_id, candidate_kind, candidate_id, observed_at DESC);
|
|
941
|
+
CREATE INDEX IF NOT EXISTS idx_task_continuity_project_task ON task_continuity_events(project_id, task_id, created_at ASC);
|
|
942
|
+
CREATE INDEX IF NOT EXISTS idx_task_continuity_project_recent ON task_continuity_events(project_id, created_at DESC);
|
|
943
|
+
CREATE INDEX IF NOT EXISTS idx_task_continuity_item ON task_continuity_events(project_id, task_id, kind, item_key, created_at DESC);
|
|
944
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_task_continuity_event_key
|
|
945
|
+
ON task_continuity_events(project_id, task_id, event_key)
|
|
946
|
+
WHERE event_key IS NOT NULL;
|
|
947
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_agent_effect_identity
|
|
948
|
+
ON agent_effects(pipeline_id, task_id, attempt, effect_key);
|
|
949
|
+
CREATE INDEX IF NOT EXISTS idx_agent_effect_pipeline ON agent_effects(pipeline_id, started_at ASC);
|
|
902
950
|
CREATE INDEX IF NOT EXISTS idx_mcp_bindings_project ON mcp_bindings(project_id, last_used_at DESC);
|
|
951
|
+
CREATE INDEX IF NOT EXISTS idx_mcp_bindings_root ON mcp_bindings(project_root, last_used_at DESC);
|
|
952
|
+
CREATE INDEX IF NOT EXISTS idx_mcp_bindings_expiry ON mcp_bindings(expires_at);
|
|
903
953
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_maintenance_jobs_active_dedupe
|
|
904
954
|
ON maintenance_jobs(project_id, kind, dedupe_key)
|
|
905
955
|
WHERE status IN ('pending', 'running', 'retry');
|
|
@@ -1078,6 +1128,8 @@ const SCHEMA_MIGRATIONS: SchemaMigration[] = [
|
|
|
1078
1128
|
db.exec('CREATE INDEX IF NOT EXISTS idx_memory_feedback_states_project ON memory_feedback_states(project_id, candidate_kind, updated_at DESC)');
|
|
1079
1129
|
db.exec('CREATE INDEX IF NOT EXISTS idx_memory_feedback_events_candidate ON memory_feedback_events(project_id, candidate_kind, candidate_id, observed_at DESC)');
|
|
1080
1130
|
db.exec('CREATE INDEX IF NOT EXISTS idx_mcp_bindings_project ON mcp_bindings(project_id, last_used_at DESC)');
|
|
1131
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_mcp_bindings_root ON mcp_bindings(project_root, last_used_at DESC)');
|
|
1132
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_mcp_bindings_expiry ON mcp_bindings(expires_at)');
|
|
1081
1133
|
},
|
|
1082
1134
|
},
|
|
1083
1135
|
{
|
|
@@ -1089,6 +1141,18 @@ const SCHEMA_MIGRATIONS: SchemaMigration[] = [
|
|
|
1089
1141
|
addColumnIfMissing(db, 'code_edges', 'source', 'source TEXT');
|
|
1090
1142
|
},
|
|
1091
1143
|
},
|
|
1144
|
+
{
|
|
1145
|
+
id: '1.9-continuity-v2',
|
|
1146
|
+
apply: (db) => {
|
|
1147
|
+
addColumnIfMissing(db, 'task_continuity_events', 'event_key', 'event_key TEXT');
|
|
1148
|
+
addColumnIfMissing(db, 'task_continuity_events', 'item_key', 'item_key TEXT');
|
|
1149
|
+
addColumnIfMissing(db, 'task_continuity_events', 'supersedes_id', 'supersedes_id TEXT');
|
|
1150
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_task_continuity_item ON task_continuity_events(project_id, task_id, kind, item_key, created_at DESC)');
|
|
1151
|
+
db.exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_task_continuity_event_key
|
|
1152
|
+
ON task_continuity_events(project_id, task_id, event_key)
|
|
1153
|
+
WHERE event_key IS NOT NULL`);
|
|
1154
|
+
},
|
|
1155
|
+
},
|
|
1092
1156
|
];
|
|
1093
1157
|
|
|
1094
1158
|
function applySchemaMigrations(db: any): void {
|
|
@@ -1110,6 +1174,28 @@ function applySchemaMigrations(db: any): void {
|
|
|
1110
1174
|
// ── Singleton cache ─────────────────────────────────────────────────
|
|
1111
1175
|
|
|
1112
1176
|
const _dbCache = new Map<string, any>();
|
|
1177
|
+
const _dbLeases = new Map<string, number>();
|
|
1178
|
+
|
|
1179
|
+
export interface DatabaseLease {
|
|
1180
|
+
dataDir: string;
|
|
1181
|
+
db: any;
|
|
1182
|
+
release: () => void;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
export interface DatabaseStats {
|
|
1186
|
+
cached: number;
|
|
1187
|
+
leased: number;
|
|
1188
|
+
leasedDataDirs: number;
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
function normalizeDataDir(dataDir: string): string {
|
|
1192
|
+
return path.resolve(dataDir);
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
function touchDatabase(normalized: string, db: any): void {
|
|
1196
|
+
_dbCache.delete(normalized);
|
|
1197
|
+
_dbCache.set(normalized, db);
|
|
1198
|
+
}
|
|
1113
1199
|
|
|
1114
1200
|
/**
|
|
1115
1201
|
* Get or create a shared SQLite database handle for the given data directory.
|
|
@@ -1120,10 +1206,13 @@ const _dbCache = new Map<string, any>();
|
|
|
1120
1206
|
* Callers must NOT close the returned handle directly — use closeDatabase().
|
|
1121
1207
|
*/
|
|
1122
1208
|
export function getDatabase(dataDir: string): any {
|
|
1123
|
-
const normalized =
|
|
1209
|
+
const normalized = normalizeDataDir(dataDir);
|
|
1124
1210
|
assertNotHomeDataDir(normalized);
|
|
1125
1211
|
const existing = _dbCache.get(normalized);
|
|
1126
|
-
if (existing)
|
|
1212
|
+
if (existing) {
|
|
1213
|
+
touchDatabase(normalized, existing);
|
|
1214
|
+
return existing;
|
|
1215
|
+
}
|
|
1127
1216
|
|
|
1128
1217
|
loadBetterSqlite3();
|
|
1129
1218
|
fs.mkdirSync(dataDir, { recursive: true });
|
|
@@ -1161,6 +1250,8 @@ export function getDatabase(dataDir: string): any {
|
|
|
1161
1250
|
db.exec(CREATE_MAINTENANCE_JOBS_TABLE);
|
|
1162
1251
|
db.exec(CREATE_MAINTENANCE_TARGETS_TABLE);
|
|
1163
1252
|
db.exec(CREATE_COMPACTION_CHECKPOINTS_TABLE);
|
|
1253
|
+
db.exec(CREATE_TASK_CONTINUITY_EVENTS_TABLE);
|
|
1254
|
+
db.exec(CREATE_AGENT_EFFECTS_TABLE);
|
|
1164
1255
|
|
|
1165
1256
|
// Phase 3a migration: add sourceSnapshot + updatedAt to mini_skills
|
|
1166
1257
|
// Idempotent — ALTER TABLE ADD COLUMN throws if column already exists
|
|
@@ -1194,7 +1285,7 @@ export function getDatabase(dataDir: string): any {
|
|
|
1194
1285
|
db.prepare(`INSERT OR IGNORE INTO meta (key, value) VALUES ('next_id', '1')`).run();
|
|
1195
1286
|
db.prepare(`INSERT OR IGNORE INTO meta (key, value) VALUES ('mini_skills_generation', '0')`).run();
|
|
1196
1287
|
|
|
1197
|
-
|
|
1288
|
+
touchDatabase(normalized, db);
|
|
1198
1289
|
return db;
|
|
1199
1290
|
} catch (error) {
|
|
1200
1291
|
// A schema/migration/FTS failure must not leave an uncached connection
|
|
@@ -1204,12 +1295,60 @@ export function getDatabase(dataDir: string): any {
|
|
|
1204
1295
|
}
|
|
1205
1296
|
}
|
|
1206
1297
|
|
|
1298
|
+
/**
|
|
1299
|
+
* Hold a database handle across an HTTP/session lifetime. Idle eviction only
|
|
1300
|
+
* closes handles with no active leases, so cache pressure cannot invalidate a
|
|
1301
|
+
* store that is still serving a request.
|
|
1302
|
+
*/
|
|
1303
|
+
export function acquireDatabase(dataDir: string): DatabaseLease {
|
|
1304
|
+
const normalized = normalizeDataDir(dataDir);
|
|
1305
|
+
const db = getDatabase(normalized);
|
|
1306
|
+
_dbLeases.set(normalized, (_dbLeases.get(normalized) ?? 0) + 1);
|
|
1307
|
+
let released = false;
|
|
1308
|
+
return {
|
|
1309
|
+
dataDir: normalized,
|
|
1310
|
+
db,
|
|
1311
|
+
release: () => {
|
|
1312
|
+
if (released) return;
|
|
1313
|
+
released = true;
|
|
1314
|
+
const count = (_dbLeases.get(normalized) ?? 0) - 1;
|
|
1315
|
+
if (count > 0) _dbLeases.set(normalized, count);
|
|
1316
|
+
else _dbLeases.delete(normalized);
|
|
1317
|
+
},
|
|
1318
|
+
};
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
/** Return bounded resource information for health and diagnostics. */
|
|
1322
|
+
export function getDatabaseStats(): DatabaseStats {
|
|
1323
|
+
let leased = 0;
|
|
1324
|
+
for (const count of _dbLeases.values()) leased += count;
|
|
1325
|
+
return { cached: _dbCache.size, leased, leasedDataDirs: _dbLeases.size };
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
/**
|
|
1329
|
+
* Evict least-recently-used unleased handles until the cache is within its
|
|
1330
|
+
* target. Stores that need a handle across awaits must hold a lease first.
|
|
1331
|
+
*/
|
|
1332
|
+
export function evictIdleDatabases(maxCached: number): number {
|
|
1333
|
+
const target = Math.max(0, Math.floor(maxCached));
|
|
1334
|
+
let evicted = 0;
|
|
1335
|
+
for (const [normalized, db] of [..._dbCache]) {
|
|
1336
|
+
if (_dbCache.size <= target) break;
|
|
1337
|
+
if ((_dbLeases.get(normalized) ?? 0) > 0) continue;
|
|
1338
|
+
try { db.close(); } catch { /* best-effort */ }
|
|
1339
|
+
_dbCache.delete(normalized);
|
|
1340
|
+
evicted++;
|
|
1341
|
+
}
|
|
1342
|
+
return evicted;
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1207
1345
|
/**
|
|
1208
1346
|
* Close and remove a cached database handle for the given data directory.
|
|
1209
1347
|
* Safe to call even if no handle exists.
|
|
1210
1348
|
*/
|
|
1211
1349
|
export function closeDatabase(dataDir: string): void {
|
|
1212
|
-
const normalized =
|
|
1350
|
+
const normalized = normalizeDataDir(dataDir);
|
|
1351
|
+
if ((_dbLeases.get(normalized) ?? 0) > 0) return;
|
|
1213
1352
|
const db = _dbCache.get(normalized);
|
|
1214
1353
|
if (db) {
|
|
1215
1354
|
try { db.close(); } catch { /* best-effort */ }
|
|
@@ -1225,6 +1364,7 @@ export function closeAllDatabases(): void {
|
|
|
1225
1364
|
try { db.close(); } catch { /* best-effort */ }
|
|
1226
1365
|
_dbCache.delete(key);
|
|
1227
1366
|
}
|
|
1367
|
+
_dbLeases.clear();
|
|
1228
1368
|
}
|
|
1229
1369
|
|
|
1230
1370
|
/**
|
package/src/store/sqlite-fts.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Observation } from '../types.js';
|
|
2
|
+
import { isTransientSqliteError } from './sqlite-reliability.js';
|
|
2
3
|
|
|
3
4
|
/** Rows returned by the derived FTS5 index before normal Observation parsing. */
|
|
4
5
|
export interface SqliteLexicalHit {
|
|
@@ -56,31 +57,52 @@ export function buildFtsQuery(query: string): string | null {
|
|
|
56
57
|
return unique.map((token) => `"${normalizeFtsToken(token)}"`).join(' OR ');
|
|
57
58
|
}
|
|
58
59
|
|
|
60
|
+
/**
|
|
61
|
+
* initializeObservationLexicalIndex runs exactly once per db handle (called
|
|
62
|
+
* from getDatabase's synchronous setup), so a transient cross-process BUSY
|
|
63
|
+
* here would otherwise be cached as a permanent "unavailable" for that
|
|
64
|
+
* handle's entire lifetime, with no other call site ever re-checking it.
|
|
65
|
+
*/
|
|
66
|
+
const FTS_INIT_MAX_ATTEMPTS = 3;
|
|
67
|
+
|
|
59
68
|
/**
|
|
60
69
|
* Create and reconcile the external-content FTS5 index for one SQLite handle.
|
|
61
|
-
* FTS5 is an optional SQLite capability, so
|
|
62
|
-
* normal fallback rather than a reason to make the durable store
|
|
70
|
+
* FTS5 is an optional SQLite capability, so a genuinely unavailable extension
|
|
71
|
+
* is a normal fallback rather than a reason to make the durable store
|
|
72
|
+
* unusable. Cross-process lock contention (SQLITE_BUSY) is not that case —
|
|
73
|
+
* it is retried a few times, mirroring the retry already applied to the
|
|
74
|
+
* write paths in sqlite-store.ts / session-store.ts / mini-skill-store.ts,
|
|
75
|
+
* since the caller here is synchronous and cannot await retrySqliteBusy.
|
|
63
76
|
*/
|
|
64
77
|
export function initializeObservationLexicalIndex(db: any): boolean {
|
|
65
78
|
const known = ftsAvailability.get(db);
|
|
66
79
|
if (known !== undefined) return known;
|
|
67
80
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
db.
|
|
81
|
+
let lastError: unknown;
|
|
82
|
+
for (let attempt = 1; attempt <= FTS_INIT_MAX_ATTEMPTS; attempt++) {
|
|
83
|
+
try {
|
|
84
|
+
db.exec(CREATE_OBSERVATION_FTS);
|
|
85
|
+
const observationCount = Number(db.prepare('SELECT COUNT(*) AS count FROM observations').get()?.count ?? 0);
|
|
86
|
+
const indexedCount = Number(db.prepare(`SELECT COUNT(*) AS count FROM ${FTS_TABLE}`).get()?.count ?? 0);
|
|
87
|
+
if (observationCount !== indexedCount) {
|
|
88
|
+
db.exec(`INSERT INTO ${FTS_TABLE}(${FTS_TABLE}) VALUES ('rebuild')`);
|
|
89
|
+
}
|
|
90
|
+
ftsAvailability.set(db, true);
|
|
91
|
+
return true;
|
|
92
|
+
} catch (error) {
|
|
93
|
+
lastError = error;
|
|
94
|
+
if (!isTransientSqliteError(error) || attempt === FTS_INIT_MAX_ATTEMPTS) break;
|
|
95
|
+
// better-sqlite3 is synchronous, so there is no await-able delay here;
|
|
96
|
+
// re-entering db.exec/prepare re-enters SQLite's own busy_timeout wait
|
|
97
|
+
// (already configured in sqlite-db.ts), which is the retry itself.
|
|
74
98
|
}
|
|
75
|
-
ftsAvailability.set(db, true);
|
|
76
|
-
return true;
|
|
77
|
-
} catch (error) {
|
|
78
|
-
ftsAvailability.set(db, false);
|
|
79
|
-
console.warn(
|
|
80
|
-
`[memorix] SQLite FTS5 unavailable; using the existing search fallback: ${error instanceof Error ? error.message : String(error)}`,
|
|
81
|
-
);
|
|
82
|
-
return false;
|
|
83
99
|
}
|
|
100
|
+
|
|
101
|
+
ftsAvailability.set(db, false);
|
|
102
|
+
console.warn(
|
|
103
|
+
`[memorix] SQLite FTS5 unavailable; using the existing search fallback: ${lastError instanceof Error ? lastError.message : String(lastError)}`,
|
|
104
|
+
);
|
|
105
|
+
return false;
|
|
84
106
|
}
|
|
85
107
|
|
|
86
108
|
export function isObservationLexicalIndexEnabled(db: any): boolean {
|