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.
Files changed (64) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.md +6 -1
  3. package/README.zh-CN.md +6 -1
  4. package/dist/cli/index.js +1743 -348
  5. package/dist/index.d.ts +12 -7
  6. package/dist/index.js +1048 -266
  7. package/dist/maintenance-runner.js +339 -187
  8. package/dist/media-audio-runner.js +339 -187
  9. package/dist/media-video-runner.js +339 -187
  10. package/dist/memcode-runtime/CHANGELOG.md +53 -0
  11. package/dist/sdk.js +967 -241
  12. package/dist/vector-backfill-runner.js +339 -187
  13. package/docs/API_REFERENCE.md +36 -0
  14. package/docs/CONFIGURATION.md +43 -0
  15. package/docs/DEVELOPMENT.md +1 -1
  16. package/docs/KNOWN_ISSUES_AND_ROADMAP.md +49 -14
  17. package/docs/PERFORMANCE.md +46 -2
  18. package/docs/README.md +2 -1
  19. package/docs/SETUP.md +1 -1
  20. package/package.json +2 -1
  21. package/plugins/antigravity/memorix/plugin.json +1 -1
  22. package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
  23. package/plugins/codebuddy/memorix/.codebuddy-plugin/plugin.json +1 -1
  24. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  25. package/plugins/copilot/memorix/plugin.json +1 -1
  26. package/plugins/gemini/memorix/gemini-extension.json +1 -1
  27. package/plugins/hermes/memorix/plugin.yaml +1 -1
  28. package/plugins/omp/memorix/package.json +1 -1
  29. package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
  30. package/plugins/pi/memorix/package.json +1 -1
  31. package/server.json +2 -2
  32. package/src/cli/capability-map.ts +1 -0
  33. package/src/cli/command-guide.ts +13 -1
  34. package/src/cli/commands/context.ts +2 -0
  35. package/src/cli/commands/continuity.ts +155 -0
  36. package/src/cli/commands/init.ts +5 -2
  37. package/src/cli/commands/serve-http.ts +242 -42
  38. package/src/cli/index.ts +3 -1
  39. package/src/cli/tui/ConfigureView.tsx +2 -0
  40. package/src/codegraph/auto-context.ts +17 -0
  41. package/src/config/legacy-loader.ts +2 -2
  42. package/src/config/resolved-config.ts +8 -5
  43. package/src/config/yaml-loader.ts +20 -10
  44. package/src/embedding/api-provider.ts +4 -2
  45. package/src/embedding/fastembed-provider.ts +47 -14
  46. package/src/embedding/transformers-provider.ts +29 -7
  47. package/src/evaluation/memory-quality-gate.ts +90 -0
  48. package/src/hooks/official-skills.ts +2 -0
  49. package/src/knowledge/context-assembly.ts +1 -0
  50. package/src/knowledge/context-receipt.ts +15 -0
  51. package/src/knowledge/task-continuity.ts +413 -0
  52. package/src/knowledge/workset.ts +77 -0
  53. package/src/llm/provider.ts +2 -1
  54. package/src/orchestrate/coordinator.ts +60 -0
  55. package/src/orchestrate/effect-ledger.ts +160 -0
  56. package/src/orchestrate/pipeline-trace.ts +40 -6
  57. package/src/runtime/maintenance-jobs.ts +12 -6
  58. package/src/server/modern-mcp-bridge.ts +153 -27
  59. package/src/server/tool-profile.ts +3 -2
  60. package/src/server.ts +129 -5
  61. package/src/store/orama-store.ts +16 -1
  62. package/src/store/sqlite-db.ts +144 -4
  63. package/src/store/sqlite-fts.ts +38 -16
  64. package/src/team/team-store.ts +62 -27
@@ -92,8 +92,8 @@ export function getResolvedConfig(options: ResolvedLaneOptions = {}): ResolvedMe
92
92
  loadDotenv(projectRoot === null ? undefined : projectRoot ?? undefined, { userHomeDir: homeDir });
93
93
 
94
94
  const toml = loadTomlConfig({ projectRoot: projectRoot ?? null, homeDir });
95
- const yaml = loadYamlConfig(projectRoot ?? null);
96
- const legacy = loadFileConfig();
95
+ const yaml = loadYamlConfig(projectRoot ?? null, { homeDir });
96
+ const legacy = loadFileConfig(homeDir);
97
97
  const embeddingBaseUrl = first(process.env.MEMORIX_EMBEDDING_BASE_URL, toml.embedding?.base_url, yaml.embedding?.baseUrl, legacy.embeddingApi?.baseUrl);
98
98
  const openRouterEmbeddingApiKey = isOpenRouterUrl(embeddingBaseUrl) ? process.env.OPENROUTER_API_KEY : undefined;
99
99
  const memoryLlmProvider = first(
@@ -117,15 +117,17 @@ export function getResolvedConfig(options: ResolvedLaneOptions = {}): ResolvedMe
117
117
  const openRouterMemoryLlmApiKey = isOpenRouterMemoryLane(memoryLlmProvider, memoryLlmBaseUrl)
118
118
  ? process.env.OPENROUTER_API_KEY
119
119
  : undefined;
120
+ const isAtlasCloud = memoryLlmProvider === 'atlascloud';
120
121
  const memoryLlmApiKey = first(
121
122
  process.env.MEMORIX_LLM_API_KEY,
122
123
  process.env.MEMORIX_API_KEY,
123
124
  toml.memory?.llm?.api_key,
124
125
  yaml.llm?.apiKey,
125
126
  legacy.llm?.apiKey,
126
- process.env.OPENAI_API_KEY,
127
- process.env.ANTHROPIC_API_KEY,
128
- openRouterMemoryLlmApiKey,
127
+ // Never borrow another provider's credentials for the Atlas Cloud preset.
128
+ isAtlasCloud ? process.env.ATLASCLOUD_API_KEY : process.env.OPENAI_API_KEY,
129
+ isAtlasCloud ? undefined : process.env.ANTHROPIC_API_KEY,
130
+ isAtlasCloud ? undefined : openRouterMemoryLlmApiKey,
129
131
  );
130
132
 
131
133
  const resolved: ResolvedMemorixConfig = {
@@ -321,6 +323,7 @@ function getEnvSourceNames(): string[] {
321
323
  'MEMORIX_CODEGRAPH_EXTERNAL_COMMAND',
322
324
  'MEMORIX_CODEGRAPH_EXTERNAL_TIMEOUT_MS',
323
325
  'OPENROUTER_API_KEY',
326
+ 'ATLASCLOUD_API_KEY',
324
327
  ].filter((name) => process.env[name]);
325
328
  }
326
329
 
@@ -125,9 +125,17 @@ export interface MemorixYamlConfig {
125
125
 
126
126
  // ─── Loader ──────────────────────────────────────────────────────────
127
127
 
128
- // Per-project config cache — keyed by resolved projectRoot string.
129
- // null key = user-level-only config (no project root).
130
- const configCache = new Map<string | null, MemorixYamlConfig>();
128
+ interface LoadYamlOptions {
129
+ homeDir?: string;
130
+ }
131
+
132
+ // Cache entries are scoped by both the user home and project root so test and
133
+ // long-lived multi-project callers cannot reuse another configuration scope.
134
+ const configCache = new Map<string, MemorixYamlConfig>();
135
+
136
+ function cacheKey(homeDir: string, projectRoot: string | null): string {
137
+ return `${homeDir}\0${projectRoot ?? ''}`;
138
+ }
131
139
  /** Stored project root — set once by server init, used by all no-arg loadYamlConfig() calls */
132
140
  let globalProjectRoot: string | null = null;
133
141
 
@@ -142,7 +150,7 @@ let globalProjectRoot: string | null = null;
142
150
  export function initProjectRoot(root: string): void {
143
151
  globalProjectRoot = root;
144
152
  // Invalidate this project's cache entry so file changes are picked up
145
- configCache.delete(root);
153
+ configCache.delete(cacheKey(homedir(), root));
146
154
  }
147
155
 
148
156
  /**
@@ -157,16 +165,18 @@ export function clearProjectRoot(): void {
157
165
  * Load memorix.yml from project root and/or user home.
158
166
  * Project-level overrides user-level (shallow merge per top-level key).
159
167
  */
160
- export function loadYamlConfig(projectRoot?: string | null): MemorixYamlConfig {
168
+ export function loadYamlConfig(projectRoot?: string | null, options: LoadYamlOptions = {}): MemorixYamlConfig {
169
+ const homeDir = options.homeDir ?? homedir();
161
170
  // When null is explicitly passed, skip global fallback (user-level config only).
162
171
  // When undefined (no arg), fall back to globally-initialized project root.
163
172
  const resolvedRoot = projectRoot === null ? null : (projectRoot ?? globalProjectRoot ?? null);
173
+ const key = cacheKey(homeDir, resolvedRoot);
164
174
 
165
175
  // Per-project cache hit
166
- const cached = configCache.get(resolvedRoot ?? null);
176
+ const cached = configCache.get(key);
167
177
  if (cached) return cached;
168
178
 
169
- const userYaml = getGlobalYamlPath(homedir());
179
+ const userYaml = getGlobalYamlPath(homeDir);
170
180
  const projectYaml = resolvedRoot ? getProjectYamlPath(resolvedRoot) : null;
171
181
 
172
182
  let userConfig: MemorixYamlConfig = {};
@@ -205,7 +215,7 @@ export function loadYamlConfig(projectRoot?: string | null): MemorixYamlConfig {
205
215
  server: { ...userConfig.server, ...projectConfig.server },
206
216
  team: { ...userConfig.team, ...projectConfig.team },
207
217
  };
208
- configCache.set(resolvedRoot ?? null, merged);
218
+ configCache.set(key, merged);
209
219
 
210
220
  return merged;
211
221
  }
@@ -214,9 +224,9 @@ export function loadYamlConfig(projectRoot?: string | null): MemorixYamlConfig {
214
224
  * Reset cached YAML config (for testing or project switching).
215
225
  * Invalidates all cached entries, or a specific projectRoot if provided.
216
226
  */
217
- export function resetYamlConfigCache(projectRoot?: string | null): void {
227
+ export function resetYamlConfigCache(projectRoot?: string | null, homeDir = homedir()): void {
218
228
  if (projectRoot !== undefined) {
219
- configCache.delete(projectRoot ?? null);
229
+ configCache.delete(cacheKey(homeDir, projectRoot ?? null));
220
230
  } else {
221
231
  configCache.clear();
222
232
  }
@@ -99,7 +99,8 @@ function evictOldest(): void {
99
99
  if (old) cachePayloadBytes -= payloadBytes(old);
100
100
  }
101
101
 
102
- function evictToFit(incomingBytes: number, incomingDims: number): void {
102
+ function evictToFit(incomingBytes: number, incomingDims: number): boolean {
103
+ if (incomingBytes > maxCachePayloadBytes()) return false;
103
104
  const limit = maxEntriesForDimensions(incomingDims);
104
105
  while (
105
106
  cache.size > 0 &&
@@ -107,6 +108,7 @@ function evictToFit(incomingBytes: number, incomingDims: number): void {
107
108
  ) {
108
109
  evictOldest();
109
110
  }
111
+ return true;
110
112
  }
111
113
 
112
114
  function isNumberArray(value: unknown): value is number[] {
@@ -139,7 +141,7 @@ function cacheInsert(hash: string, value: number[], dirty: boolean): void {
139
141
  cachePayloadBytes -= payloadBytes(existing);
140
142
  cache.delete(hash);
141
143
  }
142
- evictToFit(incoming, value.length || cacheDimensions || DEFAULT_ASSUMED_DIMENSIONS);
144
+ if (!evictToFit(incoming, value.length || cacheDimensions || DEFAULT_ASSUMED_DIMENSIONS)) return;
143
145
  cache.set(hash, value);
144
146
  cachePayloadBytes += incoming;
145
147
  if (dirty) diskCacheDirty = true;
@@ -30,6 +30,9 @@ const CACHE_FILE = join(CACHE_DIR, '.embedding-cache.json');
30
30
  // In-memory cache keyed by text hash → embedding
31
31
  const cache = new Map<string, number[]>();
32
32
  const MAX_CACHE_SIZE = 5000;
33
+ const DEFAULT_CACHE_PAYLOAD_BYTES = 64 * 1024 * 1024;
34
+ const MAX_CACHE_FILE_BYTES = 128 * 1024 * 1024;
35
+ let cachePayloadBytes = 0;
33
36
  let diskCacheDirty = false;
34
37
 
35
38
  function textHash(text: string): string {
@@ -39,14 +42,53 @@ function textHash(text: string): string {
39
42
  async function loadDiskCache(): Promise<void> {
40
43
  try {
41
44
  const raw = await readFile(CACHE_FILE, 'utf-8');
42
- const entries: [string, number[]][] = JSON.parse(raw);
43
- for (const [k, v] of entries) cache.set(k, v);
44
- console.error(`[memorix] Loaded ${entries.length} cached embeddings from disk`);
45
+ if (Buffer.byteLength(raw, 'utf8') > MAX_CACHE_FILE_BYTES) {
46
+ console.error('[memorix] FastEmbed cache exceeds the disk load limit; starting with an empty cache');
47
+ return;
48
+ }
49
+ const entries: unknown = JSON.parse(raw);
50
+ if (!Array.isArray(entries)) return;
51
+ for (const entry of entries) {
52
+ if (!Array.isArray(entry) || typeof entry[0] !== 'string' || !isVector(entry[1])) continue;
53
+ cacheSet(entry[0], entry[1], false);
54
+ }
55
+ console.error(`[memorix] Loaded ${cache.size} cached embeddings from disk`);
45
56
  } catch {
46
57
  // No cache file or corrupt — start fresh
47
58
  }
48
59
  }
49
60
 
61
+ function isVector(value: unknown): value is number[] {
62
+ return Array.isArray(value) && value.every((entry) => typeof entry === 'number');
63
+ }
64
+
65
+ function maxCachePayloadBytes(): number {
66
+ const raw = Number.parseInt(process.env.MEMORIX_EMBEDDING_CACHE_MAX_BYTES ?? '', 10);
67
+ return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_CACHE_PAYLOAD_BYTES;
68
+ }
69
+
70
+ function cacheSet(hash: string, value: number[], markDirty = true): void {
71
+ const incomingBytes = value.length * 8;
72
+ if (incomingBytes > maxCachePayloadBytes()) return;
73
+ const existing = cache.get(hash);
74
+ if (existing) cachePayloadBytes -= existing.length * 8;
75
+ else {
76
+ while (
77
+ cache.size >= MAX_CACHE_SIZE
78
+ || cachePayloadBytes + incomingBytes > maxCachePayloadBytes()
79
+ ) {
80
+ const firstKey = cache.keys().next().value;
81
+ if (firstKey === undefined) break;
82
+ const old = cache.get(firstKey);
83
+ cache.delete(firstKey);
84
+ if (old) cachePayloadBytes -= old.length * 8;
85
+ }
86
+ }
87
+ cache.set(hash, value);
88
+ cachePayloadBytes += incomingBytes;
89
+ if (markDirty) diskCacheDirty = true;
90
+ }
91
+
50
92
  async function saveDiskCache(): Promise<void> {
51
93
  if (!diskCacheDirty) return;
52
94
  try {
@@ -109,7 +151,7 @@ export class FastEmbedProvider implements EmbeddingProvider {
109
151
  if (result.length !== this.dimensions) {
110
152
  throw new Error(`Expected ${this.dimensions}d embedding, got ${result.length}d`);
111
153
  }
112
- this.cacheSet(hash, result);
154
+ cacheSet(hash, result);
113
155
  return result;
114
156
  }
115
157
 
@@ -139,7 +181,7 @@ export class FastEmbedProvider implements EmbeddingProvider {
139
181
  const originalIdx = uncachedIndices[batchIdx];
140
182
  const plain = Array.from(vec) as number[];
141
183
  results[originalIdx] = plain;
142
- this.cacheSet(textHash(uncachedTexts[batchIdx]), plain);
184
+ cacheSet(textHash(uncachedTexts[batchIdx]), plain);
143
185
  batchIdx++;
144
186
  }
145
187
  }
@@ -150,13 +192,4 @@ export class FastEmbedProvider implements EmbeddingProvider {
150
192
  return results;
151
193
  }
152
194
 
153
- private cacheSet(hash: string, value: number[]): void {
154
- // Evict oldest entries if cache is full
155
- if (cache.size >= MAX_CACHE_SIZE) {
156
- const firstKey = cache.keys().next().value;
157
- if (firstKey !== undefined) cache.delete(firstKey);
158
- }
159
- cache.set(hash, value);
160
- diskCacheDirty = true;
161
- }
162
195
  }
@@ -23,10 +23,22 @@ import {
23
23
  type EmbeddingOptions,
24
24
  type EmbeddingProvider,
25
25
  } from './provider.js';
26
+ import { createHash } from 'node:crypto';
26
27
 
27
28
  // In-memory LRU cache
28
29
  const cache = new Map<string, number[]>();
29
30
  const MAX_CACHE_SIZE = 5000;
31
+ const DEFAULT_CACHE_PAYLOAD_BYTES = 64 * 1024 * 1024;
32
+ let cachePayloadBytes = 0;
33
+
34
+ function cacheKey(text: string): string {
35
+ return createHash('sha256').update(text).digest('hex');
36
+ }
37
+
38
+ function maxCachePayloadBytes(): number {
39
+ const raw = Number.parseInt(process.env.MEMORIX_EMBEDDING_CACHE_MAX_BYTES ?? '', 10);
40
+ return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_CACHE_PAYLOAD_BYTES;
41
+ }
30
42
 
31
43
  export class TransformersProvider implements EmbeddingProvider {
32
44
  readonly name = 'transformers-minilm';
@@ -68,7 +80,7 @@ export class TransformersProvider implements EmbeddingProvider {
68
80
 
69
81
  async embed(text: string): Promise<number[]> {
70
82
  // Check cache first
71
- const cached = cache.get(text);
83
+ const cached = cache.get(cacheKey(text));
72
84
  if (cached) return cached;
73
85
 
74
86
  const output = await this.extractor(text, {
@@ -82,7 +94,7 @@ export class TransformersProvider implements EmbeddingProvider {
82
94
  throw new Error(`Expected ${this.dimensions}d embedding, got ${result.length}d`);
83
95
  }
84
96
 
85
- this.cacheSet(text, result);
97
+ this.cacheSet(cacheKey(text), result);
86
98
  return result;
87
99
  }
88
100
 
@@ -93,7 +105,7 @@ export class TransformersProvider implements EmbeddingProvider {
93
105
 
94
106
  // Check cache for each text
95
107
  for (let i = 0; i < texts.length; i++) {
96
- const cached = cache.get(texts[i]);
108
+ const cached = cache.get(cacheKey(texts[i]));
97
109
  if (cached) {
98
110
  results[i] = cached;
99
111
  } else {
@@ -114,7 +126,7 @@ export class TransformersProvider implements EmbeddingProvider {
114
126
  const vec = Array.from(allVecs[i]) as number[];
115
127
  const originalIdx = uncachedIndices[i];
116
128
  results[originalIdx] = vec;
117
- this.cacheSet(uncachedTexts[i], vec);
129
+ this.cacheSet(cacheKey(uncachedTexts[i]), vec);
118
130
  }
119
131
  }
120
132
 
@@ -122,10 +134,20 @@ export class TransformersProvider implements EmbeddingProvider {
122
134
  }
123
135
 
124
136
  private cacheSet(key: string, value: number[]): void {
125
- if (cache.size >= MAX_CACHE_SIZE) {
126
- const firstKey = cache.keys().next().value;
127
- if (firstKey !== undefined) cache.delete(firstKey);
137
+ const incomingBytes = value.length * 8;
138
+ if (incomingBytes > maxCachePayloadBytes()) return;
139
+ const existing = cache.get(key);
140
+ if (existing) cachePayloadBytes -= existing.length * 8;
141
+ else {
142
+ while (cache.size >= MAX_CACHE_SIZE || cachePayloadBytes + incomingBytes > maxCachePayloadBytes()) {
143
+ const firstKey = cache.keys().next().value;
144
+ if (firstKey === undefined) break;
145
+ const old = cache.get(firstKey);
146
+ cache.delete(firstKey);
147
+ if (old) cachePayloadBytes -= old.length * 8;
148
+ }
128
149
  }
129
150
  cache.set(key, value);
151
+ cachePayloadBytes += incomingBytes;
130
152
  }
131
153
  }
@@ -0,0 +1,90 @@
1
+ /** Deterministic, model-free quality gate for cross-session memory delivery. */
2
+
3
+ export interface MemoryQualitySample {
4
+ id: string;
5
+ expectedIds: string[];
6
+ shownIds: string[];
7
+ leakedProjectRows?: number;
8
+ staleRowsShown?: number;
9
+ shouldAbstain?: boolean;
10
+ tokens: number;
11
+ tokenBudget: number;
12
+ latencyMs: number;
13
+ rssBytes?: number;
14
+ }
15
+
16
+ export interface MemoryQualityGateScore {
17
+ cases: number;
18
+ recallAtK: number;
19
+ precisionAtK: number;
20
+ scopeIsolation: number;
21
+ staleRejection: number;
22
+ abstentionAccuracy: number;
23
+ overBudgetCases: number;
24
+ latencyP50Ms: number;
25
+ latencyP95Ms: number;
26
+ peakRssBytes: number;
27
+ passed: boolean;
28
+ failures: string[];
29
+ }
30
+
31
+ function rate(numerator: number, denominator: number): number {
32
+ return denominator === 0 ? 1 : numerator / denominator;
33
+ }
34
+
35
+ function percentile(values: number[], p: number): number {
36
+ if (values.length === 0) return 0;
37
+ const sorted = [...values].sort((a, b) => a - b);
38
+ const index = Math.min(sorted.length - 1, Math.ceil((p / 100) * sorted.length) - 1);
39
+ return sorted[Math.max(0, index)];
40
+ }
41
+
42
+ export function scoreMemoryQualityGate(samples: MemoryQualitySample[]): MemoryQualityGateScore {
43
+ let recalls = 0;
44
+ let precisionNumerator = 0;
45
+ let precisionDenominator = 0;
46
+ let isolated = 0;
47
+ let staleRejected = 0;
48
+ let abstentionCorrect = 0;
49
+ let overBudgetCases = 0;
50
+ const latencies = samples.map(sample => sample.latencyMs);
51
+ const failures: string[] = [];
52
+
53
+ for (const sample of samples) {
54
+ const expected = new Set(sample.expectedIds);
55
+ const shown = new Set(sample.shownIds);
56
+ if ([...expected].every(id => shown.has(id))) recalls++;
57
+ precisionNumerator += [...shown].filter(id => expected.has(id)).length;
58
+ precisionDenominator += shown.size;
59
+ if (!sample.leakedProjectRows) isolated++;
60
+ if (!sample.staleRowsShown) staleRejected++;
61
+ const abstained = shown.size === 0;
62
+ if (Boolean(sample.shouldAbstain) === abstained) abstentionCorrect++;
63
+ if (sample.tokens > sample.tokenBudget) overBudgetCases++;
64
+ if (sample.shouldAbstain && !abstained) failures.push(`${sample.id}: failed to abstain`);
65
+ if (!sample.shouldAbstain && ![...expected].every(id => shown.has(id))) failures.push(`${sample.id}: expected memory missing`);
66
+ if (sample.leakedProjectRows) failures.push(`${sample.id}: cross-project leakage`);
67
+ if (sample.staleRowsShown) failures.push(`${sample.id}: stale memory surfaced`);
68
+ }
69
+
70
+ const score: MemoryQualityGateScore = {
71
+ cases: samples.length,
72
+ recallAtK: rate(recalls, samples.length),
73
+ precisionAtK: rate(precisionNumerator, precisionDenominator),
74
+ scopeIsolation: rate(isolated, samples.length),
75
+ staleRejection: rate(staleRejected, samples.length),
76
+ abstentionAccuracy: rate(abstentionCorrect, samples.length),
77
+ overBudgetCases,
78
+ latencyP50Ms: percentile(latencies, 50),
79
+ latencyP95Ms: percentile(latencies, 95),
80
+ peakRssBytes: Math.max(0, ...samples.map(sample => sample.rssBytes ?? 0)),
81
+ passed: false,
82
+ failures,
83
+ };
84
+ score.passed = score.recallAtK === 1
85
+ && score.scopeIsolation === 1
86
+ && score.staleRejection === 1
87
+ && score.abstentionAccuracy === 1
88
+ && score.overBudgetCases === 0;
89
+ return score;
90
+ }
@@ -40,6 +40,7 @@ export const OFFICIAL_MEMORIX_SKILLS: OfficialMemorixSkill[] = [
40
40
  '| Need the sequence around one memory | `memorix_timeline` | `memorix memory timeline --id <id>` |',
41
41
  '| Check source and freshness for a memory | `memorix_evidence` | `memorix evidence list|get|events` |',
42
42
  '| Record whether a memory helped, conflicted, or was corrected | `memorix_feedback` | `memorix feedback record|show|audit` |',
43
+ '| Track task requirements, decisions, verification, and outcome | `memorix_continuity` | `memorix continuity start|record|show|close` |',
43
44
  '| Inspect or import controlled media | `memorix_media` | `memorix media import|attach|list|show|status` |',
44
45
  '| Learned reusable project knowledge | `memorix_store` | `memorix memory store --type <type> --entity <name> --title "<title>" "<text>"` |',
45
46
  '| Task or bug is complete/outdated | `memorix_resolve` | `memorix memory resolve --ids <ids>` |',
@@ -63,6 +64,7 @@ export const OFFICIAL_MEMORIX_SKILLS: OfficialMemorixSkill[] = [
63
64
  '3. Use stale or unbound memory only as a warning or search lead.',
64
65
  '4. Store durable decisions, fixes, and gotchas after the project state changes.',
65
66
  '5. Resolve completed or obsolete memories so future briefs stay small.',
67
+ '6. For multi-step work, start a continuity ledger and pass its taskId back to memorix_project_context.',
66
68
  '',
67
69
  '## Store Rules',
68
70
  '',
@@ -12,6 +12,7 @@ export type ContextDeliveryTarget =
12
12
 
13
13
  export type ContextCandidateKind =
14
14
  | 'task'
15
+ | 'continuity'
15
16
  | 'continuation'
16
17
  | 'current-fact'
17
18
  | 'code-state'
@@ -1,5 +1,6 @@
1
1
  import type { CodeGraphProviderQuality } from '../codegraph/types.js';
2
2
  import type { TaskWorkset } from './workset.js';
3
+ import type { TaskContinuityLedger } from './task-continuity.js';
3
4
 
4
5
  /**
5
6
  * A small machine-readable counterpart to the agent-facing Workset prompt.
@@ -18,6 +19,11 @@ export interface BoundedContextReceipt {
18
19
  snapshotId?: string;
19
20
  };
20
21
  loadout?: TaskWorkset['agentLoadout'];
22
+ continuity?: {
23
+ taskId: string;
24
+ status: TaskContinuityLedger['status'];
25
+ outcome: TaskContinuityLedger['outcome'];
26
+ };
21
27
  }
22
28
 
23
29
  export function buildBoundedContextReceipt(input: {
@@ -37,5 +43,14 @@ export function buildBoundedContextReceipt(input: {
37
43
  ...(input.workset.provenance.snapshotId ? { snapshotId: input.workset.provenance.snapshotId } : {}),
38
44
  },
39
45
  ...(input.workset.agentLoadout ? { loadout: input.workset.agentLoadout } : {}),
46
+ ...(input.workset.continuity
47
+ ? {
48
+ continuity: {
49
+ taskId: input.workset.continuity.taskId,
50
+ status: input.workset.continuity.status,
51
+ outcome: input.workset.continuity.outcome,
52
+ },
53
+ }
54
+ : {}),
40
55
  };
41
56
  }