memorix 1.4.0 → 1.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/CLAUDE.md +9 -9
  3. package/README.md +7 -2
  4. package/README.zh-CN.md +6 -2
  5. package/dist/cli/index.js +1216 -485
  6. package/dist/index.js +984 -337
  7. package/dist/maintenance-runner.js +564 -180
  8. package/dist/media-video-runner.js +564 -180
  9. package/dist/memcode/index.js +53 -21
  10. package/dist/memcode-runtime/CHANGELOG.md +31 -0
  11. package/dist/sdk.js +983 -336
  12. package/dist/vector-backfill-runner.js +564 -180
  13. package/docs/1.4.2-EVIDENCE-GOVERNED-MEMORY-SPEC.md +434 -0
  14. package/docs/API_REFERENCE.md +1 -1
  15. package/docs/KNOWN_ISSUES_AND_ROADMAP.md +52 -128
  16. package/package.json +5 -2
  17. package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
  18. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  19. package/plugins/copilot/memorix/plugin.json +1 -1
  20. package/plugins/gemini/memorix/gemini-extension.json +1 -1
  21. package/plugins/omp/memorix/package.json +1 -1
  22. package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
  23. package/plugins/pi/memorix/package.json +1 -1
  24. package/server.json +2 -2
  25. package/src/cli/commands/codegraph.ts +83 -2
  26. package/src/codegraph/auto-context.ts +20 -0
  27. package/src/codegraph/current-facts.ts +1 -0
  28. package/src/codegraph/external-provider.ts +1 -0
  29. package/src/codegraph/store.ts +216 -5
  30. package/src/codegraph/types.ts +42 -0
  31. package/src/knowledge/context-assembly.ts +25 -0
  32. package/src/knowledge/governor.ts +156 -0
  33. package/src/knowledge/outcome-store.ts +102 -0
  34. package/src/knowledge/outcome-types.ts +49 -0
  35. package/src/knowledge/workflows.ts +32 -1
  36. package/src/knowledge/workset.ts +203 -13
  37. package/src/store/sqlite-db.ts +42 -0
  38. package/docs/dev-log/progress.txt +0 -388
@@ -43,6 +43,22 @@ export interface ContextReceiptOmission {
43
43
  count: number;
44
44
  }
45
45
 
46
+ export interface ContextReceiptGovernanceDecision {
47
+ kind: ContextCandidateKind;
48
+ id?: string;
49
+ disposition: 'include' | 'compact' | 'defer' | 'exclude';
50
+ reasons: string[];
51
+ }
52
+
53
+ /** Privacy-safe evidence qualification, never appended to the agent prompt. */
54
+ export interface ContextReceiptGovernance {
55
+ /** Current implementation qualifies optional memory/knowledge, not caller task facts. */
56
+ scope: 'optional-evidence';
57
+ mode: 'abstain' | 'card' | 'workset';
58
+ decisions: ContextReceiptGovernanceDecision[];
59
+ cautions: string[];
60
+ }
61
+
46
62
  export interface ContextReceipt {
47
63
  version: '1.3';
48
64
  target: ContextDeliveryTarget;
@@ -53,6 +69,7 @@ export interface ContextReceipt {
53
69
  };
54
70
  selected: ContextReceiptSelection[];
55
71
  omitted: ContextReceiptOmission[];
72
+ governance?: ContextReceiptGovernance;
56
73
  scheduledActions: string[];
57
74
  }
58
75
 
@@ -92,6 +109,14 @@ export function formatContextReceipt(receipt: ContextReceipt): string {
92
109
  }
93
110
  }
94
111
 
112
+ if (receipt.governance?.decisions.length) {
113
+ lines.push('', 'Optional evidence qualification');
114
+ for (const item of receipt.governance.decisions.slice(0, 20)) {
115
+ const id = item.id ? ` ${item.id}` : '';
116
+ lines.push(`- ${item.kind}${id}: ${item.disposition} (${item.reasons.join(', ')})`);
117
+ }
118
+ }
119
+
95
120
  if (receipt.scheduledActions.length > 0) {
96
121
  lines.push('', 'Scheduled follow-up');
97
122
  for (const action of receipt.scheduledActions) lines.push(`- ${action}`);
@@ -0,0 +1,156 @@
1
+ import type {
2
+ ContextCandidateFreshness,
3
+ ContextCandidateKind,
4
+ ContextCandidateTrust,
5
+ } from './context-assembly.js';
6
+
7
+ /**
8
+ * A small, serializable description of an item competing for agent context.
9
+ * This is intentionally independent from stores and prompt rendering so every
10
+ * delivery surface can apply the same evidence rules.
11
+ */
12
+ export interface GovernanceCandidate {
13
+ kind: ContextCandidateKind;
14
+ id?: string;
15
+ estimatedTokens: number;
16
+ relevance: number;
17
+ scopeAllowed: boolean;
18
+ freshness?: ContextCandidateFreshness;
19
+ trust?: ContextCandidateTrust;
20
+ quality?: 'verified' | 'probationary' | 'degraded' | 'blocked';
21
+ conflict?: 'none' | 'possible' | 'confirmed';
22
+ evidenceCount?: number;
23
+ }
24
+
25
+ export type GovernanceDisposition = 'include' | 'compact' | 'defer' | 'exclude';
26
+
27
+ export type GovernanceReason =
28
+ | 'scope-forbidden'
29
+ | 'confirmed-conflict'
30
+ | 'blocked-quality'
31
+ | 'degraded-quality'
32
+ | 'stale-evidence'
33
+ | 'unknown-freshness'
34
+ | 'possible-conflict'
35
+ | 'probationary-evidence'
36
+ | 'token-budget'
37
+ | 'current-source-backed-evidence'
38
+ | 'relevant-evidence'
39
+ | 'no-qualified-evidence';
40
+
41
+ export interface GovernanceDecision {
42
+ candidate: GovernanceCandidate;
43
+ disposition: GovernanceDisposition;
44
+ reasons: GovernanceReason[];
45
+ }
46
+
47
+ export interface GovernancePlan {
48
+ mode: 'abstain' | 'card' | 'workset';
49
+ budget: { maxTokens: number; usedTokens: number };
50
+ decisions: GovernanceDecision[];
51
+ cautions: GovernanceReason[];
52
+ }
53
+
54
+ function clampRelevance(value: number): number {
55
+ if (!Number.isFinite(value)) return 0;
56
+ return Math.max(0, Math.min(1, value));
57
+ }
58
+
59
+ function classify(candidate: GovernanceCandidate): Omit<GovernanceDecision, 'candidate'> {
60
+ const freshness = candidate.freshness ?? 'unknown';
61
+ const trust = candidate.trust ?? 'historical';
62
+ const quality = candidate.quality ?? 'probationary';
63
+ const conflict = candidate.conflict ?? 'none';
64
+
65
+ if (!candidate.scopeAllowed) {
66
+ return { disposition: 'exclude', reasons: ['scope-forbidden'] };
67
+ }
68
+ if (conflict === 'confirmed') {
69
+ return { disposition: 'exclude', reasons: ['confirmed-conflict'] };
70
+ }
71
+ if (quality === 'blocked') {
72
+ return { disposition: 'exclude', reasons: ['blocked-quality'] };
73
+ }
74
+ if (quality === 'degraded') {
75
+ return { disposition: 'defer', reasons: ['degraded-quality'] };
76
+ }
77
+ if (freshness === 'stale') {
78
+ return { disposition: 'defer', reasons: ['stale-evidence'] };
79
+ }
80
+
81
+ const reasons: GovernanceReason[] = [];
82
+ let disposition: GovernanceDisposition = 'include';
83
+ if (freshness === 'unknown') {
84
+ disposition = 'compact';
85
+ reasons.push('unknown-freshness');
86
+ }
87
+ if (conflict === 'possible') {
88
+ disposition = 'compact';
89
+ reasons.push('possible-conflict');
90
+ }
91
+ if (quality === 'probationary') {
92
+ disposition = 'compact';
93
+ reasons.push('probationary-evidence');
94
+ }
95
+ if (disposition === 'include' && freshness === 'current' && trust === 'source-backed') {
96
+ reasons.push('current-source-backed-evidence');
97
+ } else if (reasons.length === 0) {
98
+ reasons.push('relevant-evidence');
99
+ }
100
+ return { disposition, reasons };
101
+ }
102
+
103
+ function priority(decision: GovernanceDecision): number {
104
+ const dispositionWeight: Record<GovernanceDisposition, number> = {
105
+ include: 3,
106
+ compact: 2,
107
+ defer: 1,
108
+ exclude: 0,
109
+ };
110
+ const evidence = Math.min(3, decision.candidate.evidenceCount ?? 0) / 10;
111
+ return dispositionWeight[decision.disposition] + clampRelevance(decision.candidate.relevance) + evidence;
112
+ }
113
+
114
+ /**
115
+ * Decide which evidence may enter a bounded context response. The result never
116
+ * mutates memory and deliberately prefers an explicit abstention to a generic
117
+ * history dump.
118
+ */
119
+ export function governContextCandidates(
120
+ candidates: GovernanceCandidate[],
121
+ maxTokens: number,
122
+ ): GovernancePlan {
123
+ const safeBudget = Math.max(0, Math.floor(maxTokens));
124
+ const decisions = candidates.map(candidate => ({ candidate, ...classify(candidate) }));
125
+ const eligible = decisions
126
+ .filter(decision => decision.disposition === 'include' || decision.disposition === 'compact')
127
+ .sort((left, right) => priority(right) - priority(left));
128
+ let usedTokens = 0;
129
+
130
+ for (const decision of eligible) {
131
+ const tokens = Math.max(0, Math.ceil(decision.candidate.estimatedTokens));
132
+ if (usedTokens + tokens <= safeBudget) {
133
+ usedTokens += tokens;
134
+ continue;
135
+ }
136
+ decision.disposition = 'defer';
137
+ decision.reasons = [...decision.reasons, 'token-budget'];
138
+ }
139
+
140
+ const delivered = decisions.filter(decision => decision.disposition === 'include' || decision.disposition === 'compact');
141
+ const mode = delivered.length === 0
142
+ ? 'abstain'
143
+ : delivered.some(decision => decision.disposition === 'include')
144
+ ? 'workset'
145
+ : 'card';
146
+ const cautions = [...new Set(decisions
147
+ .filter(decision => decision.disposition !== 'include')
148
+ .flatMap(decision => decision.reasons))];
149
+
150
+ return {
151
+ mode,
152
+ budget: { maxTokens: safeBudget, usedTokens },
153
+ decisions,
154
+ cautions: cautions.length > 0 ? cautions : ['no-qualified-evidence'],
155
+ };
156
+ }
@@ -0,0 +1,102 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { sanitizeCredentials } from '../memory/secret-filter.js';
3
+ import { getDatabase } from '../store/sqlite-db.js';
4
+ import {
5
+ MEMORY_OUTCOME_CANDIDATE_KINDS,
6
+ MEMORY_OUTCOME_SIGNAL_KINDS,
7
+ type MemoryOutcomeCandidateKind,
8
+ type MemoryOutcomeSignal,
9
+ type MemoryOutcomeSignalKind,
10
+ } from './outcome-types.js';
11
+
12
+ function optionalText(value: unknown): string | undefined {
13
+ return typeof value === 'string' && value.trim() ? value : undefined;
14
+ }
15
+
16
+ function text(value: string, field: string, max = 1_000): string {
17
+ const safe = sanitizeCredentials(value).replace(/\s+/g, ' ').trim();
18
+ if (!safe) throw new Error('Outcome signal ' + field + ' is required.');
19
+ if (safe.length > max) throw new Error('Outcome signal ' + field + ' is too long.');
20
+ return safe;
21
+ }
22
+
23
+ function oneOf<T extends readonly string[]>(value: unknown, values: T, field: string): T[number] {
24
+ if (typeof value !== 'string' || !(values as readonly string[]).includes(value)) {
25
+ throw new Error('Outcome signal ' + field + ' is invalid.');
26
+ }
27
+ return value as T[number];
28
+ }
29
+
30
+ function rowToSignal(row: any): MemoryOutcomeSignal {
31
+ return {
32
+ id: row.id,
33
+ projectId: row.projectId,
34
+ candidateKind: row.candidateKind,
35
+ candidateId: row.candidateId,
36
+ kind: row.kind,
37
+ sourceRef: row.sourceRef,
38
+ ...(optionalText(row.snapshotId) ? { snapshotId: row.snapshotId } : {}),
39
+ ...(optionalText(row.detail) ? { detail: row.detail } : {}),
40
+ observedAt: row.observedAt,
41
+ } as MemoryOutcomeSignal;
42
+ }
43
+
44
+ /** Persistence only; callers decide which real-world event is worth recording. */
45
+ export class OutcomeStore {
46
+ private db: any = null;
47
+
48
+ async init(dataDir: string): Promise<void> {
49
+ this.db = getDatabase(dataDir);
50
+ }
51
+
52
+ record(input: Omit<MemoryOutcomeSignal, 'id' | 'observedAt'> & { observedAt?: string }): MemoryOutcomeSignal {
53
+ const signal: MemoryOutcomeSignal = {
54
+ id: randomUUID(),
55
+ projectId: text(input.projectId, 'project id'),
56
+ candidateKind: oneOf(input.candidateKind, MEMORY_OUTCOME_CANDIDATE_KINDS, 'candidate kind'),
57
+ candidateId: text(input.candidateId, 'candidate id'),
58
+ kind: oneOf(input.kind, MEMORY_OUTCOME_SIGNAL_KINDS, 'kind'),
59
+ sourceRef: text(input.sourceRef, 'source reference'),
60
+ ...(input.snapshotId ? { snapshotId: text(input.snapshotId, 'snapshot id') } : {}),
61
+ ...(input.detail ? { detail: text(input.detail, 'detail') } : {}),
62
+ observedAt: input.observedAt ?? new Date().toISOString(),
63
+ };
64
+ this.db.prepare(`
65
+ INSERT INTO memory_outcome_signals (
66
+ id, projectId, candidateKind, candidateId, kind, sourceRef, snapshotId, detail, observedAt
67
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
68
+ `).run(
69
+ signal.id,
70
+ signal.projectId,
71
+ signal.candidateKind,
72
+ signal.candidateId,
73
+ signal.kind,
74
+ signal.sourceRef,
75
+ signal.snapshotId ?? null,
76
+ signal.detail ?? null,
77
+ signal.observedAt,
78
+ );
79
+ return signal;
80
+ }
81
+
82
+ latestForCandidates(
83
+ projectId: string,
84
+ candidateKind: MemoryOutcomeCandidateKind,
85
+ candidateIds: string[],
86
+ ): Map<string, MemoryOutcomeSignal> {
87
+ const ids = [...new Set(candidateIds.filter(Boolean))];
88
+ if (ids.length === 0) return new Map();
89
+ const placeholders = ids.map(() => '?').join(', ');
90
+ const rows = this.db.prepare(`
91
+ SELECT * FROM memory_outcome_signals
92
+ WHERE projectId = ? AND candidateKind = ? AND candidateId IN (${placeholders})
93
+ ORDER BY observedAt DESC, id DESC
94
+ `).all(projectId, candidateKind, ...ids);
95
+ const latest = new Map<string, MemoryOutcomeSignal>();
96
+ for (const row of rows) {
97
+ const signal = rowToSignal(row);
98
+ if (!latest.has(signal.candidateId)) latest.set(signal.candidateId, signal);
99
+ }
100
+ return latest;
101
+ }
102
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Append-only evidence about whether a context artifact helped or was
3
+ * contradicted. Signals never replace the artifact they describe.
4
+ */
5
+ export const MEMORY_OUTCOME_CANDIDATE_KINDS = ['claim', 'durable-memory', 'workflow'] as const;
6
+ export type MemoryOutcomeCandidateKind = typeof MEMORY_OUTCOME_CANDIDATE_KINDS[number];
7
+
8
+ export const MEMORY_OUTCOME_SIGNAL_KINDS = [
9
+ 'verification-passed',
10
+ 'verification-failed',
11
+ 'verified-reuse',
12
+ 'user-pin',
13
+ 'user-correction',
14
+ 'source-changed',
15
+ 'conflict-confirmed',
16
+ 'manual-review',
17
+ ] as const;
18
+ export type MemoryOutcomeSignalKind = typeof MEMORY_OUTCOME_SIGNAL_KINDS[number];
19
+
20
+ export interface MemoryOutcomeSignal {
21
+ id: string;
22
+ projectId: string;
23
+ candidateKind: MemoryOutcomeCandidateKind;
24
+ candidateId: string;
25
+ kind: MemoryOutcomeSignalKind;
26
+ sourceRef: string;
27
+ snapshotId?: string;
28
+ detail?: string;
29
+ observedAt: string;
30
+ }
31
+
32
+ export type OutcomeQuality = 'verified' | 'probationary' | 'degraded';
33
+
34
+ /** The newest auditable signal wins; missing signals leave lifecycle state in charge. */
35
+ export function qualityFromOutcome(signal?: Pick<MemoryOutcomeSignal, 'kind'>): OutcomeQuality | undefined {
36
+ if (!signal) return undefined;
37
+ if (signal.kind === 'verification-failed'
38
+ || signal.kind === 'user-correction'
39
+ || signal.kind === 'source-changed'
40
+ || signal.kind === 'conflict-confirmed') {
41
+ return 'degraded';
42
+ }
43
+ if (signal.kind === 'verification-passed'
44
+ || signal.kind === 'verified-reuse'
45
+ || signal.kind === 'user-pin') {
46
+ return 'verified';
47
+ }
48
+ return undefined;
49
+ }
@@ -7,6 +7,8 @@ import { atomicWriteFile, withFileLock } from '../store/file-lock.js';
7
7
  import { WorkflowSyncer } from '../workspace/workflow-sync.js';
8
8
  import { getKnowledgeWorkspacePaths, resolveKnowledgeWorkspaceFile } from './workspace.js';
9
9
  import { WorkflowStore } from './workflow-store.js';
10
+ import { OutcomeStore } from './outcome-store.js';
11
+ import type { MemoryOutcomeCandidateKind, MemoryOutcomeSignalKind } from './outcome-types.js';
10
12
  import { containsTaskKeyword } from '../codegraph/task-lens.js';
11
13
  import type {
12
14
  WorkflowAdapterPreview,
@@ -751,7 +753,36 @@ export async function recordWorkflowRun(input: {
751
753
  throw new Error('Workflow was not found for this workspace');
752
754
  }
753
755
  if (!input.run.task.trim()) throw new Error('Workflow run task is required');
754
- return store.recordRun(input.run);
756
+ const run = store.recordRun(input.run);
757
+ const kind: MemoryOutcomeSignalKind | undefined = run.outcome === 'passed' && run.verificationVerdict === 'passed'
758
+ ? 'verification-passed'
759
+ : run.outcome === 'failed' || run.verificationVerdict === 'failed'
760
+ ? 'verification-failed'
761
+ : undefined;
762
+ if (!kind) return run;
763
+
764
+ const evidence = new Map<string, MemoryOutcomeCandidateKind>();
765
+ evidence.set(workflow.id, 'workflow');
766
+ for (const reference of run.selectedEvidence) {
767
+ const match = /^(claim|durable):(.+)$/.exec(reference);
768
+ if (!match) continue;
769
+ evidence.set(match[2], match[1] === 'claim' ? 'claim' : 'durable-memory');
770
+ }
771
+ const outcomes = new OutcomeStore();
772
+ await outcomes.init(workflowStoreDataDir(input.workspace));
773
+ for (const [candidateId, candidateKind] of evidence) {
774
+ outcomes.record({
775
+ projectId: run.projectId,
776
+ candidateKind,
777
+ candidateId,
778
+ kind,
779
+ sourceRef: 'workflow-run:' + run.id,
780
+ ...(run.startingSnapshotId ? { snapshotId: run.startingSnapshotId } : {}),
781
+ ...(run.failureReason ? { detail: run.failureReason } : {}),
782
+ observedAt: run.completedAt ?? run.startedAt,
783
+ });
784
+ }
785
+ return run;
755
786
  }
756
787
 
757
788
  export async function selectWorkspaceWorkflows(input: {