memorix 1.7.3 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/cli/index.js +1407 -192
- package/dist/dashboard/static/app.js +88 -4
- package/dist/dashboard/static/index.html +10 -0
- package/dist/dashboard/static/style.css +86 -0
- package/dist/index.js +875 -164
- package/dist/maintenance-runner.js +607 -68
- package/dist/media-audio-runner.js +602 -63
- package/dist/media-video-runner.js +607 -68
- package/dist/memcode-runtime/CHANGELOG.md +32 -0
- package/dist/sdk.js +874 -163
- package/dist/types.d.ts +8 -0
- package/dist/vector-backfill-runner.js +607 -68
- package/docs/1.2.4-PERSISTENT-MEMORY-DELIVERY.md +1 -1
- package/docs/1.8.0-RELEASE-CANDIDATE-SPEC.md +75 -0
- package/docs/API_REFERENCE.md +37 -1
- package/docs/DASHBOARD.md +10 -0
- package/docs/DEVELOPMENT.md +3 -2
- package/docs/INTEGRATIONS.md +1 -1
- package/docs/README.md +11 -1
- package/package.json +1 -1
- package/plugins/antigravity/memorix/rules/memorix.md +1 -1
- package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
- package/plugins/claude/memorix/SKILL.md +1 -1
- package/plugins/claude/memorix/skills/memorix-memory/SKILL.md +2 -2
- package/plugins/codebuddy/memorix/.codebuddy-plugin/plugin.json +1 -1
- package/plugins/codebuddy/memorix/skills/memorix-memory/SKILL.md +2 -2
- package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
- package/plugins/codex/memorix/skills/memorix-memory/SKILL.md +2 -2
- package/plugins/copilot/memorix/plugin.json +1 -1
- package/plugins/copilot/memorix/skills/memorix-memory/SKILL.md +2 -2
- package/plugins/cursor/memorix/skills/memorix-memory/SKILL.md +2 -2
- package/plugins/gemini/memorix/GEMINI.md +1 -1
- package/plugins/gemini/memorix/gemini-extension.json +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/plugins/pi/memorix/skills/memorix-memory/SKILL.md +2 -2
- package/server.json +2 -2
- package/src/cli/capability-map.ts +2 -0
- package/src/cli/commands/context.ts +11 -0
- package/src/cli/commands/evidence.ts +56 -0
- package/src/cli/commands/feedback.ts +58 -0
- package/src/cli/commands/resume.ts +2 -0
- package/src/cli/commands/serve-http.ts +201 -12
- package/src/cli/fallback-budget.ts +59 -0
- package/src/cli/index.ts +5 -1
- package/src/compact/engine.ts +26 -1
- package/src/config/dotenv-loader.ts +17 -0
- package/src/dashboard/config-provenance.ts +17 -0
- package/src/dashboard/evidence.ts +63 -0
- package/src/dashboard/server.ts +93 -68
- package/src/dashboard/static/index.html +10 -0
- package/src/dashboard/static/style.css +86 -0
- package/src/hooks/installers/index.ts +1 -1
- package/src/hooks/official-skills.ts +3 -3
- package/src/hooks/rules/memorix-agent-rules.md +1 -1
- package/src/memory/feedback-model.ts +4 -0
- package/src/memory/feedback.ts +220 -0
- package/src/memory/observations.ts +19 -0
- package/src/memory/retention.ts +38 -5
- package/src/server/mcp-binding-store.ts +79 -0
- package/src/server/mcp-diagnostics.ts +40 -0
- package/src/server/tool-profile.ts +4 -0
- package/src/server.ts +121 -1
- package/src/store/evidence-store.ts +261 -0
- package/src/store/sqlite-db.ts +104 -0
- package/src/types.ts +8 -0
package/src/server.ts
CHANGED
|
@@ -36,6 +36,8 @@ import { detectProject } from './project/detector.js';
|
|
|
36
36
|
import { homeProjectRootError, isHomeDirectory, writeLastProjectRoot } from './project/launch-root.js';
|
|
37
37
|
import { registerAlias, initAliasRegistry, resolveAliases, autoMergeByBaseName } from './project/aliases.js';
|
|
38
38
|
import { getProjectDataDir } from './store/persistence.js';
|
|
39
|
+
import { EvidenceCardStore } from './store/evidence-store.js';
|
|
40
|
+
import { MemoryFeedbackStore } from './memory/feedback.js';
|
|
39
41
|
import type { ObservationType, RuleSource, AgentTarget, MCPServerEntry, ObservationReader } from './types.js';
|
|
40
42
|
import { RulesSyncer } from './rules/syncer.js';
|
|
41
43
|
import { WorkspaceSyncEngine } from './workspace/engine.js';
|
|
@@ -4445,7 +4447,7 @@ export async function createMemorixServer(
|
|
|
4445
4447
|
process.platform === 'win32' ? `start "" "${url}"` :
|
|
4446
4448
|
process.platform === 'darwin' ? `open "${url}"` :
|
|
4447
4449
|
`xdg-open "${url}"`;
|
|
4448
|
-
execCmd(openCmd, () => { });
|
|
4450
|
+
execCmd(openCmd, { windowsHide: true }, () => { });
|
|
4449
4451
|
|
|
4450
4452
|
return {
|
|
4451
4453
|
content: [{
|
|
@@ -4474,6 +4476,124 @@ export async function createMemorixServer(
|
|
|
4474
4476
|
// Orchestration Coordination Tools (Multi-Agent) - SQLite-backed
|
|
4475
4477
|
// ================================================================
|
|
4476
4478
|
|
|
4479
|
+
server.registerTool(
|
|
4480
|
+
'memorix_evidence',
|
|
4481
|
+
{
|
|
4482
|
+
title: 'Evidence Cards',
|
|
4483
|
+
description:
|
|
4484
|
+
'Inspect the bounded, project-scoped provenance cards behind Memorix context. ' +
|
|
4485
|
+
'Use list for a compact index, get/events for one source, sync to repair the projection, ' +
|
|
4486
|
+
'and stale when known source files changed. Cards expose sourceRef, locator, capturedHash, and freshness.',
|
|
4487
|
+
inputSchema: {
|
|
4488
|
+
action: z.enum(['list', 'get', 'sync', 'stale', 'events']).describe('Evidence operation'),
|
|
4489
|
+
candidateKind: z.enum(['observation', 'code', 'git', 'test', 'document', 'workflow', 'graph']).optional().default('observation'),
|
|
4490
|
+
candidateId: z.string().optional().describe('Candidate id, usually an observation id'),
|
|
4491
|
+
query: z.string().optional().describe('Optional title/source query for list'),
|
|
4492
|
+
paths: z.array(z.string()).max(200).optional().describe('Changed source paths for stale'),
|
|
4493
|
+
reason: z.string().max(500).optional().describe('Why evidence became stale'),
|
|
4494
|
+
limit: z.number().int().positive().max(100).optional().default(20),
|
|
4495
|
+
},
|
|
4496
|
+
},
|
|
4497
|
+
async ({ action, candidateKind, candidateId, query, paths, reason, limit }) => {
|
|
4498
|
+
const unresolved = requireResolvedProject('inspect evidence cards for the current project');
|
|
4499
|
+
if (unresolved) return unresolved;
|
|
4500
|
+
const cards = new EvidenceCardStore();
|
|
4501
|
+
await cards.init(projectDir);
|
|
4502
|
+
if (action === 'sync') {
|
|
4503
|
+
const observations = await getObservationStore().loadByProject(project.id);
|
|
4504
|
+
const synced = cards.syncObservations(observations);
|
|
4505
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ projectId: project.id, synced: synced.length, cards: synced.slice(0, limit ?? 20) }, null, 2) }] };
|
|
4506
|
+
}
|
|
4507
|
+
if (action === 'stale') {
|
|
4508
|
+
const changed = cards.markStaleForPaths(project.id, paths ?? [], reason ?? 'source file changed');
|
|
4509
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ projectId: project.id, staleMarked: changed, paths: paths ?? [] }, null, 2) }] };
|
|
4510
|
+
}
|
|
4511
|
+
if (action === 'get') {
|
|
4512
|
+
if (!candidateId?.trim()) return { content: [{ type: 'text' as const, text: 'candidateId is required for evidence get.' }], isError: true };
|
|
4513
|
+
const card = cards.get(project.id, candidateKind ?? 'observation', candidateId.trim());
|
|
4514
|
+
if (!card) return { content: [{ type: 'text' as const, text: 'Evidence card not found.' }], isError: true };
|
|
4515
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ card, events: cards.listEvents(card.id), projectId: project.id }, null, 2) }] };
|
|
4516
|
+
}
|
|
4517
|
+
if (action === 'events') {
|
|
4518
|
+
if (!candidateId?.trim()) return { content: [{ type: 'text' as const, text: 'candidateId is required for evidence events.' }], isError: true };
|
|
4519
|
+
const card = cards.get(project.id, candidateKind ?? 'observation', candidateId.trim());
|
|
4520
|
+
if (!card) return { content: [{ type: 'text' as const, text: 'Evidence card not found.' }], isError: true };
|
|
4521
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify(cards.listEvents(card.id, limit), null, 2) }] };
|
|
4522
|
+
}
|
|
4523
|
+
const needle = query?.trim().toLowerCase();
|
|
4524
|
+
const listed = cards.list(project.id, { limit: Math.max(limit ?? 20, 100) })
|
|
4525
|
+
.filter((card) => !needle || [card.title, card.summary, card.sourceRef, card.locator ?? ''].join(' ').toLowerCase().includes(needle))
|
|
4526
|
+
.slice(0, limit ?? 20);
|
|
4527
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ projectId: project.id, cards: listed }, null, 2) }] };
|
|
4528
|
+
},
|
|
4529
|
+
);
|
|
4530
|
+
|
|
4531
|
+
server.registerTool(
|
|
4532
|
+
'memorix_feedback',
|
|
4533
|
+
{
|
|
4534
|
+
title: 'Memory Feedback',
|
|
4535
|
+
description:
|
|
4536
|
+
'Record or inspect auditable memory outcomes. Positive verification/use strengthens recall; correction, conflict, source change, or weaken lowers it. ' +
|
|
4537
|
+
'Use revoke with targetEventId to undo one prior signal without deleting audit history.',
|
|
4538
|
+
inputSchema: {
|
|
4539
|
+
action: z.enum(['record', 'show', 'audit']).describe('Feedback operation'),
|
|
4540
|
+
candidateKind: z.enum(['observation', 'claim', 'durable-memory', 'workflow']).optional().default('observation'),
|
|
4541
|
+
candidateId: z.string().describe('Memory or knowledge candidate id'),
|
|
4542
|
+
signal: z.enum(['user-correction', 'verification-success', 'verification-failure', 'code-change', 'used', 'not-used', 'code-conflict', 'strengthen', 'weaken', 'revoke']).optional(),
|
|
4543
|
+
sourceRef: z.string().max(500).optional().describe('Source of this feedback, such as test:run-42 or user:chat'),
|
|
4544
|
+
actor: z.string().max(200).optional(),
|
|
4545
|
+
note: z.string().max(2_000).optional(),
|
|
4546
|
+
targetEventId: z.string().optional().describe('Existing event id when action=record and signal=revoke'),
|
|
4547
|
+
limit: z.number().int().positive().max(200).optional().default(100),
|
|
4548
|
+
},
|
|
4549
|
+
},
|
|
4550
|
+
async ({ action, candidateKind, candidateId, signal, sourceRef, actor, note, targetEventId, limit }) => {
|
|
4551
|
+
const unresolved = requireResolvedProject('record memory feedback for the current project');
|
|
4552
|
+
if (unresolved) return unresolved;
|
|
4553
|
+
const feedback = new MemoryFeedbackStore();
|
|
4554
|
+
await feedback.init(projectDir);
|
|
4555
|
+
const kind = candidateKind ?? 'observation';
|
|
4556
|
+
if (action === 'show') {
|
|
4557
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify(feedback.getState(project.id, kind, candidateId), null, 2) }] };
|
|
4558
|
+
}
|
|
4559
|
+
if (action === 'audit') {
|
|
4560
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify(feedback.audit(project.id, kind, candidateId, limit), null, 2) }] };
|
|
4561
|
+
}
|
|
4562
|
+
if (!signal || !sourceRef?.trim()) {
|
|
4563
|
+
return { content: [{ type: 'text' as const, text: 'record requires signal and sourceRef.' }], isError: true };
|
|
4564
|
+
}
|
|
4565
|
+
try {
|
|
4566
|
+
const result = feedback.record({
|
|
4567
|
+
projectId: project.id,
|
|
4568
|
+
candidateKind: kind,
|
|
4569
|
+
candidateId,
|
|
4570
|
+
signal,
|
|
4571
|
+
sourceRef,
|
|
4572
|
+
...(actor || currentAgentId ? { actor: actor ?? currentAgentId } : {}),
|
|
4573
|
+
...(note ? { note } : {}),
|
|
4574
|
+
...(targetEventId ? { targetEventId } : {}),
|
|
4575
|
+
});
|
|
4576
|
+
if (kind === 'observation') {
|
|
4577
|
+
const cards = new EvidenceCardStore();
|
|
4578
|
+
await cards.init(projectDir);
|
|
4579
|
+
const card = cards.get(project.id, 'observation', candidateId);
|
|
4580
|
+
if (card) {
|
|
4581
|
+
const verification = signal === 'verification-success' || signal === 'strengthen' ? 'verified' : signal === 'user-correction' || signal === 'code-conflict' || signal === 'weaken' ? 'conflicted' : card.verification;
|
|
4582
|
+
cards.recordEvent(project.id, card.id, `feedback:${signal}`, note);
|
|
4583
|
+
if (verification !== card.verification) {
|
|
4584
|
+
// Keep the card projection in sync with the feedback state while
|
|
4585
|
+
// retaining the append-only feedback audit as the source of truth.
|
|
4586
|
+
cards.setVerification(project.id, card.id, verification);
|
|
4587
|
+
}
|
|
4588
|
+
}
|
|
4589
|
+
}
|
|
4590
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }] };
|
|
4591
|
+
} catch (error) {
|
|
4592
|
+
return { content: [{ type: 'text' as const, text: error instanceof Error ? error.message : String(error) }], isError: true };
|
|
4593
|
+
}
|
|
4594
|
+
},
|
|
4595
|
+
);
|
|
4596
|
+
|
|
4477
4597
|
// Use shared TeamStore (from HTTP server) or create new one (stdio mode).
|
|
4478
4598
|
// All team state is canonical in SQLite — no JSON persistence, no sync/flush.
|
|
4479
4599
|
if (teamFeaturesEnabled) {
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import type { Observation } from '../types.js';
|
|
3
|
+
import { getDatabase } from './sqlite-db.js';
|
|
4
|
+
|
|
5
|
+
export type EvidenceCandidateKind = 'observation' | 'code' | 'git' | 'test' | 'document' | 'workflow' | 'graph';
|
|
6
|
+
export type EvidenceVerification = 'unverified' | 'verified' | 'conflicted';
|
|
7
|
+
export type EvidenceFreshness = 'current' | 'stale' | 'unknown';
|
|
8
|
+
|
|
9
|
+
export interface EvidenceCardRecord {
|
|
10
|
+
id: string;
|
|
11
|
+
projectId: string;
|
|
12
|
+
candidateKind: EvidenceCandidateKind;
|
|
13
|
+
candidateId: string;
|
|
14
|
+
title: string;
|
|
15
|
+
summary: string;
|
|
16
|
+
entityName?: string;
|
|
17
|
+
sourceKind: string;
|
|
18
|
+
sourceRef: string;
|
|
19
|
+
locator?: string;
|
|
20
|
+
capturedHash?: string;
|
|
21
|
+
sessionId?: string;
|
|
22
|
+
status: string;
|
|
23
|
+
verification: EvidenceVerification;
|
|
24
|
+
freshness: EvidenceFreshness;
|
|
25
|
+
staleReason?: string;
|
|
26
|
+
files: string[];
|
|
27
|
+
relatedEntities: string[];
|
|
28
|
+
createdAt: string;
|
|
29
|
+
updatedAt: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface EvidenceCardEvent {
|
|
33
|
+
id: string;
|
|
34
|
+
projectId: string;
|
|
35
|
+
cardId: string;
|
|
36
|
+
kind: string;
|
|
37
|
+
detail?: string;
|
|
38
|
+
createdAt: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function safeJson<T>(value: unknown, fallback: T): T {
|
|
42
|
+
if (typeof value !== 'string' || value.length === 0) return fallback;
|
|
43
|
+
try {
|
|
44
|
+
return JSON.parse(value) as T;
|
|
45
|
+
} catch {
|
|
46
|
+
return fallback;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function rowToCard(row: any): EvidenceCardRecord {
|
|
51
|
+
return {
|
|
52
|
+
id: String(row.id),
|
|
53
|
+
projectId: String(row.project_id),
|
|
54
|
+
candidateKind: row.candidate_kind,
|
|
55
|
+
candidateId: String(row.candidate_id),
|
|
56
|
+
title: String(row.title),
|
|
57
|
+
summary: String(row.summary ?? ''),
|
|
58
|
+
...(row.entity_name ? { entityName: String(row.entity_name) } : {}),
|
|
59
|
+
sourceKind: String(row.source_kind),
|
|
60
|
+
sourceRef: String(row.source_ref),
|
|
61
|
+
...(row.locator ? { locator: String(row.locator) } : {}),
|
|
62
|
+
...(row.captured_hash ? { capturedHash: String(row.captured_hash) } : {}),
|
|
63
|
+
...(row.session_id ? { sessionId: String(row.session_id) } : {}),
|
|
64
|
+
status: String(row.status ?? 'active'),
|
|
65
|
+
verification: row.verification,
|
|
66
|
+
freshness: row.freshness,
|
|
67
|
+
...(row.stale_reason ? { staleReason: String(row.stale_reason) } : {}),
|
|
68
|
+
files: safeJson(row.files_json, []),
|
|
69
|
+
relatedEntities: safeJson(row.related_entities_json, []),
|
|
70
|
+
createdAt: String(row.created_at),
|
|
71
|
+
updatedAt: String(row.updated_at),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function observationHash(observation: Observation): string {
|
|
76
|
+
return createHash('sha256')
|
|
77
|
+
.update(JSON.stringify({
|
|
78
|
+
projectId: observation.projectId,
|
|
79
|
+
id: observation.id,
|
|
80
|
+
title: observation.title,
|
|
81
|
+
narrative: observation.narrative,
|
|
82
|
+
facts: observation.facts,
|
|
83
|
+
files: observation.filesModified,
|
|
84
|
+
relatedEntities: observation.relatedEntities,
|
|
85
|
+
}))
|
|
86
|
+
.digest('hex');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function sourceKind(observation: Observation): string {
|
|
90
|
+
if (observation.source === 'git' || observation.sourceDetail === 'git-ingest') return 'git';
|
|
91
|
+
if (observation.sourceDetail === 'hook') return 'hook';
|
|
92
|
+
return observation.source ?? 'agent';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** SQLite-backed provenance cards used by MCP, CLI, and Dashboard surfaces. */
|
|
96
|
+
export class EvidenceCardStore {
|
|
97
|
+
private db: any = null;
|
|
98
|
+
|
|
99
|
+
async init(dataDir: string): Promise<void> {
|
|
100
|
+
this.db = getDatabase(dataDir);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private requireDb(): any {
|
|
104
|
+
if (!this.db) throw new Error('EvidenceCardStore is not initialized.');
|
|
105
|
+
return this.db;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
upsertObservation(observation: Observation): EvidenceCardRecord {
|
|
109
|
+
const db = this.requireDb();
|
|
110
|
+
const now = new Date().toISOString();
|
|
111
|
+
const card: EvidenceCardRecord = {
|
|
112
|
+
id: `evidence:observation:${observation.projectId}:${observation.id}`,
|
|
113
|
+
projectId: observation.projectId,
|
|
114
|
+
candidateKind: 'observation',
|
|
115
|
+
candidateId: String(observation.id),
|
|
116
|
+
title: observation.title,
|
|
117
|
+
summary: observation.narrative.slice(0, 500),
|
|
118
|
+
entityName: observation.entityName,
|
|
119
|
+
sourceKind: sourceKind(observation),
|
|
120
|
+
sourceRef: `observation:${observation.id}`,
|
|
121
|
+
locator: observation.filesModified?.join(', ') || `observation:${observation.id}`,
|
|
122
|
+
capturedHash: observationHash(observation),
|
|
123
|
+
...(observation.sessionId ? { sessionId: observation.sessionId } : {}),
|
|
124
|
+
status: observation.status ?? 'active',
|
|
125
|
+
verification: observation.status === 'resolved' ? 'verified' : 'unverified',
|
|
126
|
+
freshness: 'current',
|
|
127
|
+
files: observation.filesModified ?? [],
|
|
128
|
+
relatedEntities: observation.relatedEntities ?? [],
|
|
129
|
+
createdAt: observation.createdAt,
|
|
130
|
+
updatedAt: now,
|
|
131
|
+
};
|
|
132
|
+
db.prepare(`
|
|
133
|
+
INSERT INTO evidence_cards (
|
|
134
|
+
id, project_id, candidate_kind, candidate_id, title, summary, entity_name,
|
|
135
|
+
source_kind, source_ref, locator, captured_hash, session_id, status,
|
|
136
|
+
verification, freshness, stale_reason, files_json, related_entities_json,
|
|
137
|
+
created_at, updated_at
|
|
138
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?)
|
|
139
|
+
ON CONFLICT(project_id, candidate_kind, candidate_id) DO UPDATE SET
|
|
140
|
+
title = excluded.title,
|
|
141
|
+
summary = excluded.summary,
|
|
142
|
+
entity_name = excluded.entity_name,
|
|
143
|
+
source_kind = excluded.source_kind,
|
|
144
|
+
source_ref = excluded.source_ref,
|
|
145
|
+
locator = excluded.locator,
|
|
146
|
+
captured_hash = excluded.captured_hash,
|
|
147
|
+
session_id = excluded.session_id,
|
|
148
|
+
status = excluded.status,
|
|
149
|
+
freshness = 'current',
|
|
150
|
+
stale_reason = NULL,
|
|
151
|
+
files_json = excluded.files_json,
|
|
152
|
+
related_entities_json = excluded.related_entities_json,
|
|
153
|
+
updated_at = excluded.updated_at
|
|
154
|
+
`).run(
|
|
155
|
+
card.id,
|
|
156
|
+
card.projectId,
|
|
157
|
+
card.candidateKind,
|
|
158
|
+
card.candidateId,
|
|
159
|
+
card.title,
|
|
160
|
+
card.summary,
|
|
161
|
+
card.entityName ?? null,
|
|
162
|
+
card.sourceKind,
|
|
163
|
+
card.sourceRef,
|
|
164
|
+
card.locator ?? null,
|
|
165
|
+
card.capturedHash ?? null,
|
|
166
|
+
card.sessionId ?? null,
|
|
167
|
+
card.status,
|
|
168
|
+
card.verification,
|
|
169
|
+
card.freshness,
|
|
170
|
+
JSON.stringify(card.files),
|
|
171
|
+
JSON.stringify(card.relatedEntities),
|
|
172
|
+
card.createdAt,
|
|
173
|
+
card.updatedAt,
|
|
174
|
+
);
|
|
175
|
+
return this.get(card.projectId, card.candidateKind, card.candidateId) ?? card;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
get(projectId: string, candidateKind: EvidenceCandidateKind, candidateId: string): EvidenceCardRecord | undefined {
|
|
179
|
+
const row = this.requireDb().prepare(`
|
|
180
|
+
SELECT * FROM evidence_cards WHERE project_id = ? AND candidate_kind = ? AND candidate_id = ?
|
|
181
|
+
`).get(projectId, candidateKind, candidateId);
|
|
182
|
+
return row ? rowToCard(row) : undefined;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
list(projectId: string, options: { limit?: number; freshness?: EvidenceFreshness; status?: string } = {}): EvidenceCardRecord[] {
|
|
186
|
+
const limit = Math.min(100, Math.max(1, Math.floor(options.limit ?? 20)));
|
|
187
|
+
const clauses = ['project_id = ?'];
|
|
188
|
+
const args: unknown[] = [projectId];
|
|
189
|
+
if (options.freshness) { clauses.push('freshness = ?'); args.push(options.freshness); }
|
|
190
|
+
if (options.status) { clauses.push('status = ?'); args.push(options.status); }
|
|
191
|
+
args.push(limit);
|
|
192
|
+
const rows = this.requireDb().prepare(`
|
|
193
|
+
SELECT * FROM evidence_cards WHERE ${clauses.join(' AND ')} ORDER BY updated_at DESC, id DESC LIMIT ?
|
|
194
|
+
`).all(...args);
|
|
195
|
+
return rows.map(rowToCard);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
syncObservations(observations: Observation[]): EvidenceCardRecord[] {
|
|
199
|
+
return observations.map((observation) => this.upsertObservation(observation));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
markStaleForPaths(projectId: string, paths: string[], reason = 'source file changed'): number {
|
|
203
|
+
const normalized = new Set(paths.map((value) => value.replace(/\\/g, '/').toLowerCase()));
|
|
204
|
+
if (normalized.size === 0) return 0;
|
|
205
|
+
const db = this.requireDb();
|
|
206
|
+
const rows = db.prepare(`SELECT * FROM evidence_cards WHERE project_id = ? AND freshness != 'stale'`).all(projectId);
|
|
207
|
+
const update = db.prepare(`
|
|
208
|
+
UPDATE evidence_cards SET freshness = 'stale', stale_reason = ?, updated_at = ?
|
|
209
|
+
WHERE id = ?
|
|
210
|
+
`);
|
|
211
|
+
let changed = 0;
|
|
212
|
+
const now = new Date().toISOString();
|
|
213
|
+
const run = db.transaction(() => {
|
|
214
|
+
for (const row of rows) {
|
|
215
|
+
const files = safeJson<string[]>(row.files_json, []);
|
|
216
|
+
if (!files.some((file) => normalized.has(file.replace(/\\/g, '/').toLowerCase()))) continue;
|
|
217
|
+
update.run(reason, now, row.id);
|
|
218
|
+
changed++;
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
run();
|
|
222
|
+
return changed;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
setVerification(projectId: string, cardId: string, verification: EvidenceVerification): void {
|
|
226
|
+
this.requireDb().prepare(`
|
|
227
|
+
UPDATE evidence_cards SET verification = ?, updated_at = ? WHERE project_id = ? AND id = ?
|
|
228
|
+
`).run(verification, new Date().toISOString(), projectId, cardId);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
recordEvent(projectId: string, cardId: string, kind: string, detail?: string): EvidenceCardEvent {
|
|
232
|
+
const event: EvidenceCardEvent = {
|
|
233
|
+
id: randomUUID(),
|
|
234
|
+
projectId,
|
|
235
|
+
cardId,
|
|
236
|
+
kind,
|
|
237
|
+
...(detail ? { detail: detail.slice(0, 2_000) } : {}),
|
|
238
|
+
createdAt: new Date().toISOString(),
|
|
239
|
+
};
|
|
240
|
+
this.requireDb().prepare(`
|
|
241
|
+
INSERT INTO evidence_card_events (id, project_id, card_id, kind, detail, created_at)
|
|
242
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
243
|
+
`).run(event.id, event.projectId, event.cardId, event.kind, event.detail ?? null, event.createdAt);
|
|
244
|
+
return event;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
listEvents(cardId: string, limit = 50): EvidenceCardEvent[] {
|
|
248
|
+
const rows = this.requireDb().prepare(`
|
|
249
|
+
SELECT * FROM evidence_card_events WHERE card_id = ? ORDER BY created_at DESC LIMIT ?
|
|
250
|
+
`).all(cardId, Math.min(200, Math.max(1, Math.floor(limit))));
|
|
251
|
+
return rows.map((row: any) => ({
|
|
252
|
+
id: String(row.id), projectId: String(row.project_id), cardId: String(row.card_id),
|
|
253
|
+
kind: String(row.kind), ...(row.detail ? { detail: String(row.detail) } : {}),
|
|
254
|
+
createdAt: String(row.created_at),
|
|
255
|
+
}));
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function buildEvidenceCardId(projectId: string, candidateKind: EvidenceCandidateKind, candidateId: string): string {
|
|
260
|
+
return `evidence:${candidateKind}:${projectId}:${candidateId}`;
|
|
261
|
+
}
|
package/src/store/sqlite-db.ts
CHANGED
|
@@ -742,6 +742,88 @@ CREATE TABLE IF NOT EXISTS compaction_checkpoints (
|
|
|
742
742
|
);
|
|
743
743
|
`;
|
|
744
744
|
|
|
745
|
+
// 1.8 context-control plane: persisted provenance cards, feedback state, and
|
|
746
|
+
// explicit stateless MCP project handles. These are append/update records in
|
|
747
|
+
// the same SQLite database as observations so a process restart cannot lose
|
|
748
|
+
// the context-control boundary.
|
|
749
|
+
const CREATE_EVIDENCE_CARDS_TABLE = `
|
|
750
|
+
CREATE TABLE IF NOT EXISTS evidence_cards (
|
|
751
|
+
id TEXT PRIMARY KEY,
|
|
752
|
+
project_id TEXT NOT NULL,
|
|
753
|
+
candidate_kind TEXT NOT NULL,
|
|
754
|
+
candidate_id TEXT NOT NULL,
|
|
755
|
+
title TEXT NOT NULL,
|
|
756
|
+
summary TEXT NOT NULL DEFAULT '',
|
|
757
|
+
entity_name TEXT,
|
|
758
|
+
source_kind TEXT NOT NULL,
|
|
759
|
+
source_ref TEXT NOT NULL,
|
|
760
|
+
locator TEXT,
|
|
761
|
+
captured_hash TEXT,
|
|
762
|
+
session_id TEXT,
|
|
763
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
764
|
+
verification TEXT NOT NULL DEFAULT 'unverified',
|
|
765
|
+
freshness TEXT NOT NULL DEFAULT 'current',
|
|
766
|
+
stale_reason TEXT,
|
|
767
|
+
files_json TEXT NOT NULL DEFAULT '[]',
|
|
768
|
+
related_entities_json TEXT NOT NULL DEFAULT '[]',
|
|
769
|
+
created_at TEXT NOT NULL,
|
|
770
|
+
updated_at TEXT NOT NULL,
|
|
771
|
+
UNIQUE(project_id, candidate_kind, candidate_id)
|
|
772
|
+
);
|
|
773
|
+
`;
|
|
774
|
+
|
|
775
|
+
const CREATE_EVIDENCE_CARD_EVENTS_TABLE = `
|
|
776
|
+
CREATE TABLE IF NOT EXISTS evidence_card_events (
|
|
777
|
+
id TEXT PRIMARY KEY,
|
|
778
|
+
project_id TEXT NOT NULL,
|
|
779
|
+
card_id TEXT NOT NULL,
|
|
780
|
+
kind TEXT NOT NULL,
|
|
781
|
+
detail TEXT,
|
|
782
|
+
created_at TEXT NOT NULL,
|
|
783
|
+
FOREIGN KEY (card_id) REFERENCES evidence_cards(id) ON DELETE CASCADE
|
|
784
|
+
);
|
|
785
|
+
`;
|
|
786
|
+
|
|
787
|
+
const CREATE_MEMORY_FEEDBACK_STATES_TABLE = `
|
|
788
|
+
CREATE TABLE IF NOT EXISTS memory_feedback_states (
|
|
789
|
+
project_id TEXT NOT NULL,
|
|
790
|
+
candidate_kind TEXT NOT NULL,
|
|
791
|
+
candidate_id TEXT NOT NULL,
|
|
792
|
+
weight REAL NOT NULL DEFAULT 1.0,
|
|
793
|
+
lifecycle_status TEXT NOT NULL DEFAULT 'active',
|
|
794
|
+
updated_at TEXT NOT NULL,
|
|
795
|
+
PRIMARY KEY (project_id, candidate_kind, candidate_id)
|
|
796
|
+
);
|
|
797
|
+
`;
|
|
798
|
+
|
|
799
|
+
const CREATE_MEMORY_FEEDBACK_EVENTS_TABLE = `
|
|
800
|
+
CREATE TABLE IF NOT EXISTS memory_feedback_events (
|
|
801
|
+
id TEXT PRIMARY KEY,
|
|
802
|
+
project_id TEXT NOT NULL,
|
|
803
|
+
candidate_kind TEXT NOT NULL,
|
|
804
|
+
candidate_id TEXT NOT NULL,
|
|
805
|
+
signal TEXT NOT NULL,
|
|
806
|
+
actor TEXT,
|
|
807
|
+
note TEXT,
|
|
808
|
+
source_ref TEXT NOT NULL,
|
|
809
|
+
target_event_id TEXT,
|
|
810
|
+
delta REAL NOT NULL DEFAULT 0,
|
|
811
|
+
observed_at TEXT NOT NULL
|
|
812
|
+
);
|
|
813
|
+
`;
|
|
814
|
+
|
|
815
|
+
const CREATE_MCP_BINDINGS_TABLE = `
|
|
816
|
+
CREATE TABLE IF NOT EXISTS mcp_bindings (
|
|
817
|
+
handle_id TEXT PRIMARY KEY,
|
|
818
|
+
project_id TEXT NOT NULL,
|
|
819
|
+
project_root TEXT NOT NULL,
|
|
820
|
+
data_dir TEXT NOT NULL,
|
|
821
|
+
created_at TEXT NOT NULL,
|
|
822
|
+
last_used_at TEXT NOT NULL,
|
|
823
|
+
expires_at TEXT
|
|
824
|
+
);
|
|
825
|
+
`;
|
|
826
|
+
|
|
745
827
|
const CREATE_INDEXES = `
|
|
746
828
|
CREATE INDEX IF NOT EXISTS idx_observations_projectId ON observations(projectId);
|
|
747
829
|
CREATE INDEX IF NOT EXISTS idx_observations_topicKey ON observations(projectId, topicKey);
|
|
@@ -803,6 +885,12 @@ CREATE INDEX IF NOT EXISTS idx_maintenance_targets_updated ON maintenance_target
|
|
|
803
885
|
CREATE INDEX IF NOT EXISTS idx_compaction_checkpoints_project_recent ON compaction_checkpoints(project_id, status, completed_at DESC, pre_captured_at DESC);
|
|
804
886
|
CREATE INDEX IF NOT EXISTS idx_compaction_checkpoints_session_pending ON compaction_checkpoints(project_id, session_id, agent, phase, status, pre_captured_at DESC);
|
|
805
887
|
CREATE INDEX IF NOT EXISTS idx_compaction_checkpoints_source ON compaction_checkpoints(project_id, source_key);
|
|
888
|
+
CREATE INDEX IF NOT EXISTS idx_evidence_cards_project ON evidence_cards(project_id, status, updated_at DESC);
|
|
889
|
+
CREATE INDEX IF NOT EXISTS idx_evidence_cards_candidate ON evidence_cards(project_id, candidate_kind, candidate_id);
|
|
890
|
+
CREATE INDEX IF NOT EXISTS idx_evidence_card_events_card ON evidence_card_events(card_id, created_at);
|
|
891
|
+
CREATE INDEX IF NOT EXISTS idx_memory_feedback_states_project ON memory_feedback_states(project_id, candidate_kind, updated_at DESC);
|
|
892
|
+
CREATE INDEX IF NOT EXISTS idx_memory_feedback_events_candidate ON memory_feedback_events(project_id, candidate_kind, candidate_id, observed_at DESC);
|
|
893
|
+
CREATE INDEX IF NOT EXISTS idx_mcp_bindings_project ON mcp_bindings(project_id, last_used_at DESC);
|
|
806
894
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_maintenance_jobs_active_dedupe
|
|
807
895
|
ON maintenance_jobs(project_id, kind, dedupe_key)
|
|
808
896
|
WHERE status IN ('pending', 'running', 'retry');
|
|
@@ -967,6 +1055,22 @@ const SCHEMA_MIGRATIONS: SchemaMigration[] = [
|
|
|
967
1055
|
db.exec('CREATE INDEX IF NOT EXISTS idx_media_jobs_source_asset ON media_jobs(project_id, source_asset_id, status)');
|
|
968
1056
|
},
|
|
969
1057
|
},
|
|
1058
|
+
{
|
|
1059
|
+
id: '1.8-context-control-plane',
|
|
1060
|
+
apply: (db) => {
|
|
1061
|
+
db.exec(CREATE_EVIDENCE_CARDS_TABLE);
|
|
1062
|
+
db.exec(CREATE_EVIDENCE_CARD_EVENTS_TABLE);
|
|
1063
|
+
db.exec(CREATE_MEMORY_FEEDBACK_STATES_TABLE);
|
|
1064
|
+
db.exec(CREATE_MEMORY_FEEDBACK_EVENTS_TABLE);
|
|
1065
|
+
db.exec(CREATE_MCP_BINDINGS_TABLE);
|
|
1066
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_evidence_cards_project ON evidence_cards(project_id, status, updated_at DESC)');
|
|
1067
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_evidence_cards_candidate ON evidence_cards(project_id, candidate_kind, candidate_id)');
|
|
1068
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_evidence_card_events_card ON evidence_card_events(card_id, created_at)');
|
|
1069
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_memory_feedback_states_project ON memory_feedback_states(project_id, candidate_kind, updated_at DESC)');
|
|
1070
|
+
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)');
|
|
1071
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_mcp_bindings_project ON mcp_bindings(project_id, last_used_at DESC)');
|
|
1072
|
+
},
|
|
1073
|
+
},
|
|
970
1074
|
];
|
|
971
1075
|
|
|
972
1076
|
function applySchemaMigrations(db: any): void {
|
package/src/types.ts
CHANGED
|
@@ -280,6 +280,10 @@ export interface IndexEntry {
|
|
|
280
280
|
documentType?: DocumentType;
|
|
281
281
|
/** Knowledge layer for layer-aware ranking (Phase 3a) */
|
|
282
282
|
knowledgeLayer?: KnowledgeLayer;
|
|
283
|
+
/** Feedback materialized weight used by the context-control plane. */
|
|
284
|
+
feedbackWeight?: number;
|
|
285
|
+
/** Feedback lifecycle projection used to avoid recalling conflicted records. */
|
|
286
|
+
feedbackStatus?: ObservationStatus;
|
|
283
287
|
}
|
|
284
288
|
|
|
285
289
|
/** Explicit reference to an observation, optionally scoped to a project. */
|
|
@@ -390,6 +394,10 @@ export interface MemorixDocument {
|
|
|
390
394
|
documentType?: DocumentType;
|
|
391
395
|
/** Knowledge layer for layer-aware ranking (Phase 3a) */
|
|
392
396
|
knowledgeLayer?: KnowledgeLayer;
|
|
397
|
+
/** Feedback materialized weight used by context-control retrieval. */
|
|
398
|
+
feedbackWeight?: number;
|
|
399
|
+
/** Feedback lifecycle projection used by retention and retrieval. */
|
|
400
|
+
feedbackStatus?: ObservationStatus;
|
|
393
401
|
}
|
|
394
402
|
|
|
395
403
|
// ============================================================
|