memorix 1.2.3 → 1.2.5

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 (52) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +3 -3
  3. package/README.zh-CN.md +3 -3
  4. package/dist/cli/index.js +5273 -4730
  5. package/dist/cli/index.js.map +1 -1
  6. package/dist/index.js +506 -99
  7. package/dist/index.js.map +1 -1
  8. package/dist/maintenance-runner.js +176 -53
  9. package/dist/maintenance-runner.js.map +1 -1
  10. package/dist/memcode-runtime/CHANGELOG.md +33 -0
  11. package/dist/sdk.d.ts +4 -2
  12. package/dist/sdk.js +507 -99
  13. package/dist/sdk.js.map +1 -1
  14. package/dist/types.d.ts +8 -1
  15. package/dist/types.js.map +1 -1
  16. package/docs/1.2.4-PERSISTENT-MEMORY-DELIVERY.md +86 -0
  17. package/docs/1.2.5-RETRIEVAL-PERFORMANCE.md +85 -0
  18. package/docs/AGENT_OPERATOR_PLAYBOOK.md +13 -1
  19. package/docs/API_REFERENCE.md +13 -3
  20. package/docs/PERFORMANCE.md +22 -1
  21. package/docs/dev-log/progress.txt +73 -7
  22. package/package.json +2 -1
  23. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  24. package/src/cli/capability-map.ts +1 -1
  25. package/src/cli/command-guide.ts +4 -1
  26. package/src/cli/commands/agent-integrations.ts +5 -1
  27. package/src/cli/commands/codegraph.ts +1 -1
  28. package/src/cli/commands/context.ts +9 -1
  29. package/src/cli/commands/memory.ts +12 -3
  30. package/src/cli/commands/operator-shared.ts +74 -2
  31. package/src/cli/commands/resume.ts +31 -0
  32. package/src/cli/index.ts +5 -1
  33. package/src/codegraph/auto-context.ts +54 -1
  34. package/src/codegraph/task-lens.ts +29 -0
  35. package/src/compact/token-budget.ts +16 -1
  36. package/src/config/resolved-config.ts +29 -4
  37. package/src/config/toml-loader.ts +9 -5
  38. package/src/hooks/handler.ts +127 -66
  39. package/src/hooks/installers/index.ts +5 -4
  40. package/src/hooks/official-skills.ts +6 -4
  41. package/src/hooks/rules/memorix-agent-rules.md +9 -7
  42. package/src/knowledge/context-assembly.ts +4 -1
  43. package/src/knowledge/workset.ts +89 -1
  44. package/src/memory/session.ts +144 -10
  45. package/src/runtime/project-maintenance.ts +1 -14
  46. package/src/sdk.ts +4 -0
  47. package/src/server.ts +144 -25
  48. package/src/store/bun-sqlite-compat.ts +118 -15
  49. package/src/store/orama-store.ts +39 -18
  50. package/src/store/sqlite-db.ts +3 -3
  51. package/src/timeout.ts +23 -0
  52. package/src/types.ts +8 -0
package/dist/index.js CHANGED
@@ -35,36 +35,111 @@ var init_esm_shims = __esm({
35
35
 
36
36
  // src/store/bun-sqlite-compat.ts
37
37
  import { createRequire } from "module";
38
+ function loadNodeSqlite() {
39
+ const nodeSqlite = requireFromHere("node:sqlite");
40
+ return nodeSqlite.DatabaseSync;
41
+ }
42
+ function isNativeBindingFailure(error) {
43
+ const message = error instanceof Error ? error.message : String(error);
44
+ return /could not locate the bindings file|better_sqlite3\.node|module did not self-register/i.test(message);
45
+ }
46
+ function addCompatibility(db2) {
47
+ if (!db2.pragma) {
48
+ db2.pragma = function(pragma, options) {
49
+ const statement = db2.prepare(`PRAGMA ${pragma}`);
50
+ if (options?.simple) {
51
+ const result = statement.get();
52
+ return result ? Object.values(result)[0] : void 0;
53
+ }
54
+ return statement.all();
55
+ };
56
+ }
57
+ if (!db2.transaction) {
58
+ let depth = 0;
59
+ let sequence = 0;
60
+ db2.transaction = function(fn) {
61
+ return (...args) => {
62
+ const outermost = depth === 0;
63
+ const savepoint = `memorix_tx_${++sequence}`;
64
+ if (outermost) {
65
+ db2.exec("BEGIN IMMEDIATE");
66
+ } else {
67
+ db2.exec(`SAVEPOINT ${savepoint}`);
68
+ }
69
+ depth += 1;
70
+ try {
71
+ const result = fn(...args);
72
+ if (result && typeof result.then === "function") {
73
+ throw new Error("[memorix] SQLite transactions must be synchronous");
74
+ }
75
+ depth -= 1;
76
+ if (outermost) {
77
+ db2.exec("COMMIT");
78
+ } else {
79
+ db2.exec(`RELEASE SAVEPOINT ${savepoint}`);
80
+ }
81
+ return result;
82
+ } catch (error) {
83
+ depth -= 1;
84
+ try {
85
+ if (outermost) {
86
+ db2.exec("ROLLBACK");
87
+ } else {
88
+ db2.exec(`ROLLBACK TO SAVEPOINT ${savepoint}`);
89
+ db2.exec(`RELEASE SAVEPOINT ${savepoint}`);
90
+ }
91
+ } catch {
92
+ }
93
+ throw error;
94
+ }
95
+ };
96
+ };
97
+ }
98
+ return db2;
99
+ }
100
+ function instantiateDatabase(Sqlite, filePath, options) {
101
+ return driver === "node:sqlite" && options === void 0 ? new Sqlite(filePath) : new Sqlite(filePath, options);
102
+ }
38
103
  function loadSqlite() {
39
104
  if (Database) return Database;
105
+ if (process.env.MEMORIX_SQLITE_DRIVER === "node") {
106
+ Database = loadNodeSqlite();
107
+ driver = "node:sqlite";
108
+ return Database;
109
+ }
40
110
  try {
41
111
  Database = requireFromHere("better-sqlite3");
112
+ driver = "better-sqlite3";
113
+ return Database;
114
+ } catch {
115
+ }
116
+ try {
117
+ Database = loadNodeSqlite();
118
+ driver = "node:sqlite";
42
119
  return Database;
43
120
  } catch {
44
121
  }
45
122
  try {
46
123
  const bunSqlite = requireFromHere("bun:sqlite");
47
124
  Database = bunSqlite.Database;
125
+ driver = "bun:sqlite";
48
126
  return Database;
49
127
  } catch {
50
- throw new Error("[memorix] Neither better-sqlite3 nor bun:sqlite is available");
128
+ throw new Error("[memorix] SQLite is unavailable (better-sqlite3, node:sqlite, and bun:sqlite failed)");
51
129
  }
52
130
  }
53
131
  function createDatabase(path25, options) {
54
132
  const Sqlite = loadSqlite();
55
- const db2 = new Sqlite(path25, options);
56
- if (!db2.pragma) {
57
- db2.pragma = function(pragma, options2) {
58
- if (options2 && options2.simple) {
59
- const result = db2.prepare(`PRAGMA ${pragma}`).get();
60
- return result ? Object.values(result)[0] : void 0;
61
- }
62
- return db2.prepare(`PRAGMA ${pragma}`).all();
63
- };
133
+ try {
134
+ return addCompatibility(instantiateDatabase(Sqlite, path25, options));
135
+ } catch (error) {
136
+ if (driver !== "better-sqlite3" || !isNativeBindingFailure(error)) throw error;
137
+ Database = loadNodeSqlite();
138
+ driver = "node:sqlite";
139
+ return addCompatibility(instantiateDatabase(Database, path25, options));
64
140
  }
65
- return db2;
66
141
  }
67
- var Database, requireFromHere;
142
+ var Database, driver, requireFromHere;
68
143
  var init_bun_sqlite_compat = __esm({
69
144
  "src/store/bun-sqlite-compat.ts"() {
70
145
  "use strict";
@@ -83,7 +158,7 @@ function loadBetterSqlite3() {
83
158
  BetterSqlite3 = loadSqlite();
84
159
  return BetterSqlite3;
85
160
  } catch {
86
- throw new Error("[memorix] SQLite is not available (neither better-sqlite3 nor bun:sqlite)");
161
+ throw new Error("[memorix] SQLite is not available (better-sqlite3, node:sqlite, and bun:sqlite all failed)");
87
162
  }
88
163
  }
89
164
  function hasColumn(db2, table, column) {
@@ -1886,7 +1961,16 @@ function truncateToTokenBudget(text, budget) {
1886
1961
  result = result.slice(0, Math.floor(result.length * 0.9));
1887
1962
  }
1888
1963
  if (result.length < text.length) {
1889
- result += "...";
1964
+ const nextCharacter = text.charAt(result.length);
1965
+ if (nextCharacter && !/[\s.,;:!?)}\]]/.test(nextCharacter)) {
1966
+ const boundary = result.search(/\s+\S*$/);
1967
+ result = boundary > 0 ? result.slice(0, boundary).trimEnd() : "";
1968
+ }
1969
+ while (result && fitsInBudget(result + "...", budget) === false) {
1970
+ const boundary = result.lastIndexOf(" ");
1971
+ result = boundary > 0 ? result.slice(0, boundary).trimEnd() : "";
1972
+ }
1973
+ result = result ? result + "..." : "...";
1890
1974
  }
1891
1975
  }
1892
1976
  return result;
@@ -2591,6 +2675,9 @@ function parseTomlValue(raw, filePath, line) {
2591
2675
  if (raw.startsWith('"') && raw.endsWith('"')) {
2592
2676
  return raw.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, "\\");
2593
2677
  }
2678
+ if (raw.startsWith("'") && raw.endsWith("'")) {
2679
+ return raw.slice(1, -1);
2680
+ }
2594
2681
  if (raw === "true") return true;
2595
2682
  if (raw === "false") return false;
2596
2683
  if (/^-?\d+$/.test(raw)) return Number.parseInt(raw, 10);
@@ -2603,7 +2690,7 @@ function parseTomlValue(raw, filePath, line) {
2603
2690
  throw new Error(`Unsupported TOML value in ${filePath}:${line}`);
2604
2691
  }
2605
2692
  function stripComment(line) {
2606
- let inString = false;
2693
+ let quote = null;
2607
2694
  let escaped = false;
2608
2695
  for (let i = 0; i < line.length; i++) {
2609
2696
  const char = line[i];
@@ -2611,15 +2698,16 @@ function stripComment(line) {
2611
2698
  escaped = false;
2612
2699
  continue;
2613
2700
  }
2614
- if (char === "\\" && inString) {
2701
+ if (char === "\\" && quote === '"') {
2615
2702
  escaped = true;
2616
2703
  continue;
2617
2704
  }
2618
- if (char === '"') {
2619
- inString = !inString;
2705
+ if (char === '"' || char === "'") {
2706
+ if (quote === char) quote = null;
2707
+ else if (quote === null) quote = char;
2620
2708
  continue;
2621
2709
  }
2622
- if (char === "#" && !inString) {
2710
+ if (char === "#" && quote === null) {
2623
2711
  return line.slice(0, i);
2624
2712
  }
2625
2713
  }
@@ -2715,6 +2803,25 @@ function getResolvedConfig(options = {}) {
2715
2803
  const legacy = loadFileConfig();
2716
2804
  const embeddingBaseUrl = first(process.env.MEMORIX_EMBEDDING_BASE_URL, toml.embedding?.base_url, yaml2.embedding?.baseUrl, legacy.embeddingApi?.baseUrl);
2717
2805
  const openRouterEmbeddingApiKey = isOpenRouterUrl(embeddingBaseUrl) ? process.env.OPENROUTER_API_KEY : void 0;
2806
+ const memoryLlmProvider = first(
2807
+ process.env.MEMORIX_LLM_PROVIDER,
2808
+ toml.memory?.llm?.provider,
2809
+ yaml2.llm?.provider,
2810
+ legacy.llm?.provider
2811
+ );
2812
+ const memoryLlmModel = first(
2813
+ process.env.MEMORIX_LLM_MODEL,
2814
+ toml.memory?.llm?.model,
2815
+ yaml2.llm?.model,
2816
+ legacy.llm?.model
2817
+ );
2818
+ const memoryLlmBaseUrl = first(
2819
+ process.env.MEMORIX_LLM_BASE_URL,
2820
+ toml.memory?.llm?.base_url,
2821
+ yaml2.llm?.baseUrl,
2822
+ legacy.llm?.baseUrl
2823
+ );
2824
+ const openRouterMemoryLlmApiKey = isOpenRouterMemoryLane(memoryLlmProvider, memoryLlmBaseUrl) ? process.env.OPENROUTER_API_KEY : void 0;
2718
2825
  const resolved = {
2719
2826
  agent: {
2720
2827
  provider: first(
@@ -2752,9 +2859,9 @@ function getResolvedConfig(options = {}) {
2752
2859
  autoCleanup: firstBool(toml.memory?.auto_cleanup, yaml2.behavior?.autoCleanup),
2753
2860
  syncAdvisory: firstBool(toml.memory?.sync_advisory, yaml2.behavior?.syncAdvisory),
2754
2861
  llm: {
2755
- provider: first(process.env.MEMORIX_LLM_PROVIDER, toml.memory?.llm?.provider, yaml2.llm?.provider, legacy.llm?.provider),
2756
- model: first(process.env.MEMORIX_LLM_MODEL, toml.memory?.llm?.model, yaml2.llm?.model, legacy.llm?.model),
2757
- baseUrl: first(process.env.MEMORIX_LLM_BASE_URL, toml.memory?.llm?.base_url, yaml2.llm?.baseUrl, legacy.llm?.baseUrl),
2862
+ provider: memoryLlmProvider,
2863
+ model: memoryLlmModel,
2864
+ baseUrl: memoryLlmBaseUrl,
2758
2865
  apiKey: first(
2759
2866
  process.env.MEMORIX_LLM_API_KEY,
2760
2867
  process.env.MEMORIX_API_KEY,
@@ -2763,7 +2870,7 @@ function getResolvedConfig(options = {}) {
2763
2870
  legacy.llm?.apiKey,
2764
2871
  process.env.OPENAI_API_KEY,
2765
2872
  process.env.ANTHROPIC_API_KEY,
2766
- process.env.OPENROUTER_API_KEY
2873
+ openRouterMemoryLlmApiKey
2767
2874
  )
2768
2875
  }
2769
2876
  },
@@ -2900,6 +3007,9 @@ function isOpenRouterUrl(value) {
2900
3007
  return /(^|\.)openrouter\.ai(?::|\/|$)/i.test(value);
2901
3008
  }
2902
3009
  }
3010
+ function isOpenRouterMemoryLane(provider2, baseUrl) {
3011
+ return provider2?.trim().toLowerCase() === "openrouter" || isOpenRouterUrl(baseUrl);
3012
+ }
2903
3013
  function normalizeExternalContext(value) {
2904
3014
  const normalized = value?.trim().toLowerCase();
2905
3015
  if (normalized === "auto" || normalized === "off") return normalized;
@@ -4947,6 +5057,29 @@ Rules:
4947
5057
  }
4948
5058
  });
4949
5059
 
5060
+ // src/timeout.ts
5061
+ function withTimeout(promise, ms, label) {
5062
+ return new Promise((resolve2, reject) => {
5063
+ const timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
5064
+ promise.then(
5065
+ (value) => {
5066
+ clearTimeout(timer);
5067
+ resolve2(value);
5068
+ },
5069
+ (error) => {
5070
+ clearTimeout(timer);
5071
+ reject(error);
5072
+ }
5073
+ );
5074
+ });
5075
+ }
5076
+ var init_timeout = __esm({
5077
+ "src/timeout.ts"() {
5078
+ "use strict";
5079
+ init_esm_shims();
5080
+ }
5081
+ });
5082
+
4950
5083
  // src/project/aliases.ts
4951
5084
  var aliases_exports = {};
4952
5085
  __export(aliases_exports, {
@@ -5712,8 +5845,12 @@ async function searchObservations(options) {
5712
5845
  }
5713
5846
  };
5714
5847
  const modeKey = options.projectId ?? SEARCH_MODE_DEFAULT_KEY;
5715
- lastSearchModeByProject.set(modeKey, embeddingEnabled ? "hybrid" : "fulltext");
5848
+ const quality = options.quality ?? "balanced";
5716
5849
  const database = await getDb();
5850
+ lastSearchModeByProject.set(
5851
+ modeKey,
5852
+ quality === "fast" ? "fulltext (fast profile)" : embeddingEnabled ? "hybrid" : "fulltext"
5853
+ );
5717
5854
  let projectIds = null;
5718
5855
  if (options.projectId) {
5719
5856
  try {
@@ -5735,11 +5872,15 @@ async function searchObservations(options) {
5735
5872
  }
5736
5873
  const hasQuery = options.query && options.query.trim().length > 0;
5737
5874
  const originalQuery = options.query;
5738
- const tier = hasQuery ? classifyQueryTier(originalQuery) : "fast";
5875
+ const tier = quality === "fast" ? "fast" : hasQuery ? classifyQueryTier(originalQuery) : "fast";
5739
5876
  mark(`tier=${tier}`);
5740
- const expandedEmbeddingQuery = tier === "heavy" ? await maybeExpandSearchQuery(options.query) : options.query;
5877
+ if (quality === "thorough" && hasQuery) {
5878
+ const { initLLM: initLLM2, isLLMEnabled: isLLMEnabled2 } = await Promise.resolve().then(() => (init_provider2(), provider_exports2));
5879
+ if (!isLLMEnabled2()) initLLM2();
5880
+ }
5881
+ const expandedEmbeddingQuery = quality === "thorough" && tier === "heavy" ? await maybeExpandSearchQuery(options.query) : options.query;
5741
5882
  mark("queryExpansion");
5742
- const intentResult = hasQuery ? detectQueryIntent(originalQuery) : null;
5883
+ const intentResult = quality === "fast" || !hasQuery ? null : detectQueryIntent(originalQuery);
5743
5884
  const requestLimit = projectIds ? (options.limit ?? 20) * 3 : options.limit ?? 20;
5744
5885
  const defaultBoost = {
5745
5886
  title: 3,
@@ -5753,17 +5894,21 @@ async function searchObservations(options) {
5753
5894
  let searchParams = {
5754
5895
  term: originalQuery,
5755
5896
  limit: requestLimit,
5756
- includeVectors: true,
5897
+ // Fast search never uses vectors, so returning them only adds work and
5898
+ // payload pressure to an explicitly latency-sensitive path.
5899
+ includeVectors: quality !== "fast",
5757
5900
  ...Object.keys(filters).length > 0 ? { where: filters } : {},
5758
5901
  // Search specific fields (not tokens, accessCount, etc.)
5759
5902
  properties: ["title", "entityName", "narrative", "facts", "concepts", "filesModified"],
5760
5903
  // Field boosting: intent-aware or default
5761
5904
  boost: fieldBoost,
5762
5905
  // Fuzzy tolerance: allow 1-char typos for short queries, 2 for longer
5763
- ...hasQuery ? { tolerance: originalQuery.length > 6 ? 2 : 1 } : {}
5906
+ // Fuzzy matching is useful for ordinary recall, but grows sharply with
5907
+ // corpus size. The fast profile deliberately uses exact lexical matching.
5908
+ ...hasQuery && quality !== "fast" ? { tolerance: originalQuery.length > 6 ? 2 : 1 } : {}
5764
5909
  };
5765
5910
  let queryVector = null;
5766
- if (embeddingEnabled && hasQuery && tier !== "fast") {
5911
+ if (quality !== "fast" && embeddingEnabled && hasQuery && tier !== "fast") {
5767
5912
  try {
5768
5913
  const provider2 = await getEmbeddingProvider();
5769
5914
  if (provider2) {
@@ -5778,11 +5923,11 @@ async function searchObservations(options) {
5778
5923
  );
5779
5924
  } else {
5780
5925
  const EMBEDDING_TIMEOUT_MS = 15e3;
5781
- const embedPromise = provider2.embed(expandedEmbeddingQuery);
5782
- const timeoutPromise = new Promise(
5783
- (_, reject) => setTimeout(() => reject(new Error(`Embedding timeout after ${EMBEDDING_TIMEOUT_MS}ms`)), EMBEDDING_TIMEOUT_MS)
5926
+ queryVector = await withTimeout(
5927
+ provider2.embed(expandedEmbeddingQuery),
5928
+ EMBEDDING_TIMEOUT_MS,
5929
+ "Embedding"
5784
5930
  );
5785
- queryVector = await Promise.race([embedPromise, timeoutPromise]);
5786
5931
  mark("embedding");
5787
5932
  const cjkRatio = (originalQuery.match(/[\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff\uac00-\ud7af]/g) || []).length / originalQuery.length;
5788
5933
  const isCJKHeavy = cjkRatio > 0.3;
@@ -6023,7 +6168,7 @@ async function searchObservations(options) {
6023
6168
  }
6024
6169
  }
6025
6170
  }
6026
- const shouldRerank = tier === "heavy" && hasQuery && intermediate.length > 2 && (() => {
6171
+ const shouldRerank = quality === "thorough" && tier === "heavy" && hasQuery && intermediate.length > 2 && (() => {
6027
6172
  const top = intermediate[0]?.score ?? 0;
6028
6173
  const second = intermediate[1]?.score ?? 0;
6029
6174
  return top > 0 && second / top > 0.7;
@@ -6047,11 +6192,11 @@ async function searchObservations(options) {
6047
6192
  }));
6048
6193
  const _parsedRerank = parseInt(process.env.MEMORIX_RERANK_TIMEOUT_MS || "", 10);
6049
6194
  const RERANK_TIMEOUT_MS = Number.isFinite(_parsedRerank) && _parsedRerank > 0 ? _parsedRerank : 5e3;
6050
- const rerankPromise = rerankResults2(originalQuery, candidates);
6051
- const timeoutPromise = new Promise(
6052
- (_, reject) => setTimeout(() => reject(new Error(`LLM rerank timeout after ${RERANK_TIMEOUT_MS}ms`)), RERANK_TIMEOUT_MS)
6195
+ const { reranked, usedLLM } = await withTimeout(
6196
+ rerankResults2(originalQuery, candidates),
6197
+ RERANK_TIMEOUT_MS,
6198
+ "LLM rerank"
6053
6199
  );
6054
- const { reranked, usedLLM } = await Promise.race([rerankPromise, timeoutPromise]);
6055
6200
  mark(`rerank(usedLLM=${usedLLM})`);
6056
6201
  if (usedLLM) {
6057
6202
  lastSearchModeByProject.set(modeKey, (lastSearchModeByProject.get(modeKey) ?? "fulltext") + " + LLM rerank");
@@ -6243,6 +6388,7 @@ var init_orama_store = __esm({
6243
6388
  init_project_affinity();
6244
6389
  init_intent_detector();
6245
6390
  init_query_expansion();
6391
+ init_timeout();
6246
6392
  db = null;
6247
6393
  dbInitPromise = null;
6248
6394
  dbGeneration = 0;
@@ -13691,6 +13837,7 @@ var init_workflow_store = __esm({
13691
13837
  var task_lens_exports = {};
13692
13838
  __export(task_lens_exports, {
13693
13839
  containsTaskKeyword: () => containsTaskKeyword,
13840
+ isContinuationTask: () => isContinuationTask,
13694
13841
  lensPathCandidates: () => lensPathCandidates,
13695
13842
  lensVerificationHints: () => lensVerificationHints,
13696
13843
  rankLensPaths: () => rankLensPaths,
@@ -13756,6 +13903,12 @@ function resolveTaskLens(task) {
13756
13903
  }
13757
13904
  return best.score > 0 ? LENSES[best.id] : LENSES.general;
13758
13905
  }
13906
+ function isContinuationTask(task) {
13907
+ const normalized = (task ?? "").trim().toLowerCase();
13908
+ return normalized.length > 0 && CONTINUATION_KEYWORDS.some(
13909
+ (keyword) => containsTaskKeyword(normalized, keyword)
13910
+ );
13911
+ }
13759
13912
  function pathKindScore(path25, lens) {
13760
13913
  const normalized = normalizePath2(path25).toLowerCase();
13761
13914
  const name = normalized.split("/").pop() ?? normalized;
@@ -13899,7 +14052,7 @@ function shouldShowLensSource(source, lens, task) {
13899
14052
  if (lens.id === "onboarding" || lens.id === "release" || lens.id === "docs") return score >= 40;
13900
14053
  return score > 0;
13901
14054
  }
13902
- var STOP_WORDS, LENSES, KEYWORDS, LENS_PRIORITY;
14055
+ var STOP_WORDS, LENSES, KEYWORDS, CONTINUATION_KEYWORDS, LENS_PRIORITY;
13903
14056
  var init_task_lens = __esm({
13904
14057
  "src/codegraph/task-lens.ts"() {
13905
14058
  "use strict";
@@ -14002,6 +14155,7 @@ var init_task_lens = __esm({
14002
14155
  "fail",
14003
14156
  "failing",
14004
14157
  "fix",
14158
+ "fixing",
14005
14159
  "issue",
14006
14160
  "regression",
14007
14161
  "repro",
@@ -14019,6 +14173,18 @@ var init_task_lens = __esm({
14019
14173
  docs: ["doc", "docs", "readme", "\u6587\u6863", "\u8BF4\u660E"],
14020
14174
  test: ["coverage", "fixture", "smoke", "spec", "test", "tests", "testing", "vitest", "\u6D4B\u8BD5"]
14021
14175
  };
14176
+ CONTINUATION_KEYWORDS = [
14177
+ "continue",
14178
+ "resume",
14179
+ "pick up",
14180
+ "carry on",
14181
+ "previous session",
14182
+ "\u7EE7\u7EED",
14183
+ "\u63A5\u624B",
14184
+ "\u6062\u590D",
14185
+ "\u5EF6\u7EED",
14186
+ "\u4E0A\u6B21\u4F1A\u8BDD"
14187
+ ];
14022
14188
  LENS_PRIORITY = [
14023
14189
  "bugfix",
14024
14190
  "release",
@@ -14747,21 +14913,6 @@ async function loadWorkspaceForMaintenance(projectId, projectDir2, mode) {
14747
14913
  ]);
14748
14914
  return versioned ?? local;
14749
14915
  }
14750
- function withTimeout(promise, ms, label) {
14751
- return new Promise((resolve2, reject) => {
14752
- const timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
14753
- promise.then(
14754
- (value) => {
14755
- clearTimeout(timer);
14756
- resolve2(value);
14757
- },
14758
- (error) => {
14759
- clearTimeout(timer);
14760
- reject(error);
14761
- }
14762
- );
14763
- });
14764
- }
14765
14916
  async function runAutomaticConsolidation(projectId, projectDir2, options) {
14766
14917
  const { isLLMEnabled: isLLMEnabled2 } = await Promise.resolve().then(() => (init_provider2(), provider_exports2));
14767
14918
  if (!isLLMEnabled2()) {
@@ -15146,6 +15297,7 @@ var init_project_maintenance = __esm({
15146
15297
  init_esm_shims();
15147
15298
  init_isolated_maintenance();
15148
15299
  init_lifecycle();
15300
+ init_timeout();
15149
15301
  DEFAULT_VECTOR_BATCH_SIZE = 12;
15150
15302
  DEFAULT_RETENTION_BATCH_SIZE = 100;
15151
15303
  DEFAULT_CONSOLIDATION_BATCH_SIZE = 200;
@@ -15229,6 +15381,7 @@ __export(session_exports, {
15229
15381
  endSession: () => endSession,
15230
15382
  getActiveSession: () => getActiveSession,
15231
15383
  getSessionContext: () => getSessionContext,
15384
+ getSessionResumeBrief: () => getSessionResumeBrief,
15232
15385
  listSessions: () => listSessions,
15233
15386
  scoreObservationForSessionContext: () => scoreObservationForSessionContext,
15234
15387
  startSession: () => startSession
@@ -15296,6 +15449,62 @@ function isSystemSelfObservation(obs) {
15296
15449
  const text = stringifyObservation(obs, false);
15297
15450
  return SYSTEM_SELF_PATTERNS.some((pattern) => pattern.test(text));
15298
15451
  }
15452
+ function readerForAlias(reader, aliases, observation) {
15453
+ if (!reader) return void 0;
15454
+ return reader.projectId && aliases.has(observation.projectId) ? { ...reader, projectId: observation.projectId } : reader;
15455
+ }
15456
+ function readableAliasObservations(observations2, aliases, reader) {
15457
+ return reader ? observations2.filter((observation) => canReadObservation(
15458
+ observation,
15459
+ readerForAlias(reader, aliases, observation)
15460
+ )) : observations2;
15461
+ }
15462
+ function isUsefulSessionSummary(summary) {
15463
+ return Boolean(summary) && !NOISE_PATTERNS2.some((pattern) => pattern.test(summary)) && !SYSTEM_SELF_PATTERNS.some((pattern) => pattern.test(summary));
15464
+ }
15465
+ function continuationTaskTokens(task) {
15466
+ return [...new Set(
15467
+ (task?.toLowerCase().match(/[a-z0-9_./-]+|[\u4e00-\u9fff]+/g) ?? []).map((token) => token.trim()).filter((token) => token.length > 1 && !["continue", "resume", "\u7EE7\u7EED", "\u63A5\u624B", "\u6062\u590D"].includes(token))
15468
+ )].slice(0, 8);
15469
+ }
15470
+ function continuationScore(observation, projectTokens, task) {
15471
+ let score = scoreObservationForSessionContext(observation, projectTokens);
15472
+ if (observation.type === "session-request" || observation.type === "what-changed") score += 1;
15473
+ const text = stringifyObservation(observation);
15474
+ const matches = continuationTaskTokens(task).filter((token) => text.includes(token)).length;
15475
+ score += Math.min(matches, 2) * 2;
15476
+ return score;
15477
+ }
15478
+ async function getSessionResumeBrief(projectId, task, reader) {
15479
+ const aliasSet = await resolveProjectIds(projectId);
15480
+ const [sessions, allObs] = await Promise.all([
15481
+ loadAliasSessions(aliasSet),
15482
+ loadAliasActiveObservations(aliasSet)
15483
+ ]);
15484
+ const readableObs = readableAliasObservations(allObs, aliasSet, reader);
15485
+ const previous = sessions.filter((session) => session.status === "completed").sort((a, b) => new Date(b.endedAt || b.startedAt).getTime() - new Date(a.endedAt || a.startedAt).getTime()).find((session) => session.summary && session.summary !== "(session ended implicitly by new session start)" && isUsefulSessionSummary(session.summary));
15486
+ const projectTokens = tokenizeProjectId(projectId);
15487
+ const memories = readableObs.filter((observation) => RESUME_TYPES.has(observation.type)).filter((observation) => classifyLayer(observation) === "L2").filter((observation) => !isNoiseObservation(observation) && !isSystemSelfObservation(observation)).map((observation) => ({
15488
+ observation,
15489
+ score: continuationScore(observation, projectTokens, task)
15490
+ })).sort((a, b) => b.score - a.score || new Date(b.observation.createdAt).getTime() - new Date(a.observation.createdAt).getTime() || a.observation.id - b.observation.id).slice(0, 3).map(({ observation }) => ({
15491
+ id: observation.id,
15492
+ title: sanitizeCredentials(observation.title),
15493
+ type: observation.type,
15494
+ ...observation.facts?.[0] ? { detail: sanitizeCredentials(observation.facts[0]) } : observation.narrative ? { detail: sanitizeCredentials(observation.narrative) } : {}
15495
+ }));
15496
+ return {
15497
+ ...previous?.summary ? {
15498
+ previousSession: {
15499
+ id: previous.id,
15500
+ ...previous.agent ? { agent: previous.agent } : {},
15501
+ ...previous.endedAt ? { endedAt: previous.endedAt } : {},
15502
+ summary: sanitizeCredentials(previous.summary)
15503
+ }
15504
+ } : {},
15505
+ memories
15506
+ };
15507
+ }
15299
15508
  function scoreObservationForSessionContext(obs, projectTokens, now3 = Date.now()) {
15300
15509
  let score = TYPE_WEIGHTS2[obs.type] ?? 1;
15301
15510
  const text = stringifyObservation(obs);
@@ -15366,10 +15575,7 @@ async function getSessionContext(projectDir2, projectId, limit = 3, reader) {
15366
15575
  loadAliasSessions(aliasSet),
15367
15576
  loadAliasActiveObservations(aliasSet)
15368
15577
  ]);
15369
- const readableObs = reader ? allObs.filter((observation) => {
15370
- const observationReader = reader.projectId && aliasSet.has(observation.projectId) ? { ...reader, projectId: observation.projectId } : reader;
15371
- return canReadObservation(observation, observationReader);
15372
- }) : allObs;
15578
+ const readableObs = readableAliasObservations(allObs, aliasSet, reader);
15373
15579
  const isNoisySummary = (summary) => {
15374
15580
  if (!summary) return false;
15375
15581
  return NOISE_PATTERNS2.some((p) => p.test(summary)) || SYSTEM_SELF_PATTERNS.some((p) => p.test(summary));
@@ -15520,7 +15726,7 @@ async function getActiveSession(projectDir2, projectId) {
15520
15726
  const sessions = await loadAliasActiveSessions(aliasSet);
15521
15727
  return sessions.sort((a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime())[0] ?? null;
15522
15728
  }
15523
- var PRIORITY_TYPES, TYPE_EMOJI, TYPE_WEIGHTS2, NOISE_PATTERNS2, COMMAND_TRACE_PATTERNS, SYSTEM_SELF_PATTERNS;
15729
+ var PRIORITY_TYPES, RESUME_TYPES, TYPE_EMOJI, TYPE_WEIGHTS2, NOISE_PATTERNS2, COMMAND_TRACE_PATTERNS, SYSTEM_SELF_PATTERNS;
15524
15730
  var init_session = __esm({
15525
15731
  "src/memory/session.ts"() {
15526
15732
  "use strict";
@@ -15534,6 +15740,17 @@ var init_session = __esm({
15534
15740
  init_secret_filter();
15535
15741
  init_visibility();
15536
15742
  PRIORITY_TYPES = /* @__PURE__ */ new Set(["gotcha", "decision", "problem-solution", "trade-off", "discovery"]);
15743
+ RESUME_TYPES = /* @__PURE__ */ new Set([
15744
+ "gotcha",
15745
+ "decision",
15746
+ "problem-solution",
15747
+ "trade-off",
15748
+ "discovery",
15749
+ "how-it-works",
15750
+ "what-changed",
15751
+ "reasoning",
15752
+ "session-request"
15753
+ ]);
15537
15754
  TYPE_EMOJI = {
15538
15755
  "gotcha": "[DISCOVERY]",
15539
15756
  "decision": "[WHY]",
@@ -15694,6 +15911,7 @@ function freshnessForMemory(status) {
15694
15911
  return "unknown";
15695
15912
  }
15696
15913
  function receiptOmissionKind(raw) {
15914
+ if (raw.includes("continuation")) return "continuation";
15697
15915
  if (raw.includes("task")) return "task";
15698
15916
  if (raw.includes("fact")) return "current-fact";
15699
15917
  if (raw.includes("state")) return "code-state";
@@ -15743,6 +15961,48 @@ function renderTaskWorksetPrompt(input) {
15743
15961
  trust: "source-backed"
15744
15962
  });
15745
15963
  appendLine(lines, "Task lens: " + input.lens, maxTokens, omitted, "lens");
15964
+ const hasContinuation = Boolean(
15965
+ input.continuation?.previousSession || (input.continuation?.memories.length ?? 0) > 0
15966
+ );
15967
+ if (hasContinuation && input.continuation) {
15968
+ appendLine(lines, "", maxTokens, omitted, "continuation-heading");
15969
+ appendLine(lines, "Resume from prior work", maxTokens, omitted, "continuation-heading");
15970
+ if (input.continuation.previousSession) {
15971
+ const session = input.continuation.previousSession;
15972
+ const source = [session.agent, session.endedAt ? session.endedAt.slice(0, 10) : void 0].filter(Boolean).join(", ");
15973
+ appendLine(
15974
+ lines,
15975
+ "- Previous session" + (source ? ` (${source})` : "") + ": " + short(session.summary, 44),
15976
+ maxTokens,
15977
+ omitted,
15978
+ "continuation-session",
15979
+ selected,
15980
+ {
15981
+ kind: "continuation",
15982
+ id: "session:" + session.id,
15983
+ reason: "latest meaningful project session summary",
15984
+ trust: "historical"
15985
+ }
15986
+ );
15987
+ }
15988
+ for (const memory of input.continuation.memories.slice(0, 3)) {
15989
+ const detail = memory.detail ? ": " + short(memory.detail, CONTINUATION_DETAIL_TOKEN_BUDGET) : "";
15990
+ appendLine(
15991
+ lines,
15992
+ "- #" + memory.id + " " + memory.type + ": " + short(memory.title, 18) + detail,
15993
+ maxTokens,
15994
+ omitted,
15995
+ "continuation-memory",
15996
+ selected,
15997
+ {
15998
+ kind: "continuation",
15999
+ id: "memory:" + memory.id,
16000
+ reason: "durable prior-work memory",
16001
+ trust: "historical"
16002
+ }
16003
+ );
16004
+ }
16005
+ }
15746
16006
  if (input.cautions.length > 0 || input.cautionMemory.length > 0) {
15747
16007
  appendLine(lines, "", maxTokens, omitted, "caution-heading");
15748
16008
  appendLine(lines, "Cautions", maxTokens, omitted, "caution-heading");
@@ -16011,11 +16271,25 @@ async function buildTaskWorkset(input) {
16011
16271
  ...input.verificationHints
16012
16272
  ]).slice(0, 4);
16013
16273
  const normalizedCautions = unique(cautions.map((caution) => caution.kind)).map((kind) => cautions.find((caution) => caution.kind === kind)).slice(0, 6);
16274
+ const continuation = input.continuation && (input.continuation.previousSession || input.continuation.memories.length > 0) ? {
16275
+ ...input.continuation.previousSession ? {
16276
+ previousSession: {
16277
+ ...input.continuation.previousSession,
16278
+ summary: short(input.continuation.previousSession.summary, 52)
16279
+ }
16280
+ } : {},
16281
+ memories: input.continuation.memories.slice(0, 3).map((memory) => ({
16282
+ ...memory,
16283
+ title: short(memory.title, 20),
16284
+ ...memory.detail ? { detail: short(memory.detail, CONTINUATION_DETAIL_TOKEN_BUDGET) } : {}
16285
+ }))
16286
+ } : void 0;
16014
16287
  const base = {
16015
16288
  version: "1.2",
16016
16289
  task,
16017
16290
  lens: input.lens,
16018
16291
  currentFacts: input.currentFacts?.map((fact) => fact.startsWith("Historical note:") ? short(fact, 48) : short(fact, 28)).slice(0, 4) ?? [],
16292
+ ...continuation ? { continuation } : {},
16019
16293
  ...input.codeState ? { codeState: short(input.codeState, 28) } : {},
16020
16294
  startHere: unique(input.startHere).slice(0, 5),
16021
16295
  ...input.semanticCode ? { semanticCode: input.semanticCode } : {},
@@ -16040,7 +16314,7 @@ async function buildTaskWorkset(input) {
16040
16314
  budget: { maxTokens }
16041
16315
  });
16042
16316
  const receipt = {
16043
- version: "1.2.2",
16317
+ version: "1.2.4",
16044
16318
  target: input.deliveryTarget ?? "project-context",
16045
16319
  elapsedMs: Math.max(0, Date.now() - startedAt),
16046
16320
  budget: {
@@ -16062,6 +16336,7 @@ async function buildTaskWorkset(input) {
16062
16336
  prompt: rendered.prompt
16063
16337
  };
16064
16338
  }
16339
+ var CONTINUATION_DETAIL_TOKEN_BUDGET;
16065
16340
  var init_workset = __esm({
16066
16341
  "src/knowledge/workset.ts"() {
16067
16342
  "use strict";
@@ -16074,6 +16349,7 @@ var init_workset = __esm({
16074
16349
  init_workspace();
16075
16350
  init_workflow_store();
16076
16351
  init_workflows();
16352
+ CONTINUATION_DETAIL_TOKEN_BUDGET = 20;
16077
16353
  }
16078
16354
  });
16079
16355
 
@@ -16866,6 +17142,7 @@ async function buildAutoProjectContext(input) {
16866
17142
  const now3 = input.now ?? /* @__PURE__ */ new Date();
16867
17143
  const task = input.task?.trim();
16868
17144
  const lens = resolveTaskLens(task);
17145
+ const continuationRequested = input.continuation === "always" || input.continuation !== "never" && isContinuationTask(task);
16869
17146
  const codegraphConfig = getResolvedConfig({ projectRoot: input.project.rootPath }).codegraph;
16870
17147
  const exclude = input.exclude ?? codegraphConfig.excludePatterns;
16871
17148
  const maxFileBytes = input.maxFileBytes ?? codegraphConfig.maxFileBytes;
@@ -17002,6 +17279,11 @@ async function buildAutoProjectContext(input) {
17002
17279
  if (externalCaution) {
17003
17280
  runtimeCautions.push({ kind: "external-codegraph-fallback", message: externalCaution });
17004
17281
  }
17282
+ let continuation;
17283
+ if (continuationRequested) {
17284
+ await initSessionStore(input.dataDir);
17285
+ continuation = await getSessionResumeBrief(input.project.id, task, input.reader);
17286
+ }
17005
17287
  const workset = await buildTaskWorkset({
17006
17288
  projectId: input.project.id,
17007
17289
  dataDir: input.dataDir,
@@ -17011,6 +17293,7 @@ async function buildAutoProjectContext(input) {
17011
17293
  ...externalOutline ? { semanticCode: externalOutline } : {},
17012
17294
  providerQuality: providerQuality2,
17013
17295
  currentFacts: worksetFactLines(currentFacts),
17296
+ ...continuation ? { continuation } : {},
17014
17297
  codeState: codeStateLine(overview),
17015
17298
  reliableMemory: sourceSets.reliableSources.slice(0, lens.sourceLimit).map((source) => ({
17016
17299
  id: source.observationId,
@@ -17055,12 +17338,19 @@ async function buildAutoProjectContext(input) {
17055
17338
  explain,
17056
17339
  refresh,
17057
17340
  providerQuality: providerQuality2,
17341
+ ...continuationRequested && workset.continuation ? { continuation: workset.continuation } : {},
17058
17342
  workset
17059
17343
  };
17060
17344
  }
17061
17345
  function formatLanguages(overview) {
17062
17346
  return overview.code.languages.length > 0 ? overview.code.languages.map((item) => `${item.language} ${item.files}`).join(", ") : "none indexed yet";
17063
17347
  }
17348
+ function compactContinuationText(text, budget) {
17349
+ return truncateToTokenBudget(
17350
+ sanitizeCredentials(text).replace(/\s+/g, " ").trim(),
17351
+ budget
17352
+ );
17353
+ }
17064
17354
  function codeStateLine(overview) {
17065
17355
  const snapshot = overview.code.latestSnapshot;
17066
17356
  if (!snapshot) return "- Code state: no completed snapshot yet";
@@ -17200,6 +17490,23 @@ function formatAutoProjectContextSummary(context) {
17200
17490
  "Reliable memory",
17201
17491
  reliableSources.length > 0 ? `- ${reliableSources.length} current code-bound memory link(s)` : "- none yet"
17202
17492
  );
17493
+ const continuation = context.workset.continuation;
17494
+ if (continuation?.previousSession || continuation?.memories.length) {
17495
+ lines.push("", "Resume from prior work");
17496
+ if (continuation.previousSession) {
17497
+ const session = continuation.previousSession;
17498
+ const source = [session.agent, session.endedAt ? session.endedAt.slice(0, 10) : void 0].filter(Boolean).join(", ");
17499
+ lines.push(
17500
+ "- Previous session" + (source ? ` (${source})` : "") + ": " + compactContinuationText(session.summary, 44)
17501
+ );
17502
+ }
17503
+ for (const memory of continuation.memories.slice(0, 3)) {
17504
+ const detail = memory.detail ? ": " + compactContinuationText(memory.detail, 20) : "";
17505
+ lines.push(
17506
+ "- #" + memory.id + " " + memory.type + ": " + compactContinuationText(memory.title, 18) + detail
17507
+ );
17508
+ }
17509
+ }
17203
17510
  return lines.join("\n");
17204
17511
  }
17205
17512
  function formatAutoProjectContextPrompt(context) {
@@ -17270,8 +17577,10 @@ var init_auto_context = __esm({
17270
17577
  "src/codegraph/auto-context.ts"() {
17271
17578
  "use strict";
17272
17579
  init_esm_shims();
17580
+ init_token_budget();
17273
17581
  init_resolved_config();
17274
17582
  init_workset();
17583
+ init_secret_filter();
17275
17584
  init_binder();
17276
17585
  init_current_facts();
17277
17586
  init_lite_provider();
@@ -17279,6 +17588,8 @@ var init_auto_context = __esm({
17279
17588
  init_project_context();
17280
17589
  init_store();
17281
17590
  init_admission();
17591
+ init_session();
17592
+ init_session_store();
17282
17593
  init_task_lens();
17283
17594
  DEFAULT_MAX_AGE_MS = 10 * 60 * 1e3;
17284
17595
  }
@@ -21473,7 +21784,8 @@ var init_official_skills = __esm({
21473
21784
  "",
21474
21785
  "| Situation | Prefer | CLI fallback |",
21475
21786
  "|---|---|---|",
21476
- '| Starting or continuing a coding task and needing the best task-lensed project map | `memorix_project_context` with the user\'s task | `memorix context --task "<task>"` |',
21787
+ '| Starting a coding task and needing the best task-lensed project map | `memorix_project_context` with the user\'s task | `memorix context "<task>" --json` |',
21788
+ '| Continuing prior project work | `memorix_project_context` with the user\'s task | `memorix resume "<task>" --json` |',
21477
21789
  '| Need structured refs/freshness for code-bound memories | `memorix_context_pack` | `memorix codegraph context-pack --task "<topic>"` |',
21478
21790
  '| Explicit memory graph question | `memorix_graph_context` | `memorix memory graph-context --query "<topic>"` |',
21479
21791
  '| Specific past decision, bug, file, or change | `memorix_search` | `memorix memory search --query "<topic>"` |',
@@ -21486,9 +21798,10 @@ var init_official_skills = __esm({
21486
21798
  "",
21487
21799
  "- Default first step for non-trivial coding work: call `memorix_project_context` before progress files, dev-log reads, ad-hoc file reads, or git archaeology. Pass the user's actual task so Memorix can choose a bugfix, feature, release, onboarding, refactor, docs, test, or general brief. Start by reading its suggested files.",
21488
21800
  "- In Claude Code headless/print-mode, an initial MCP `pending` status is not a failure by itself. Prefer MCP first: use the client's tool discovery/dynamic loading to find `memorix_project_context` when it is not in the first visible tool list.",
21489
- '- Run `memorix context --task "<task>"` from the shell only after MCP is unavailable, disabled, or not discoverable, and pass the user\'s real task text. Do not skip memory, wait indefinitely on `pending`, or hand-write tool-call syntax.',
21490
- "- Use `memorix_search` for a specific decision, bug, file, or prior change that the project context did not answer.",
21491
- "- Fetch detail before relying on a specific memory.",
21801
+ '- Continuation fallback is mandatory: when the user asks to continue, resume, take over, or explain prior work and MCP cannot be called in this turn, run `memorix resume "<task>" --json` exactly once before inspecting files, Git history, progress notes, or guessing. The absence of `.memorix` or visible memory files never proves project memory is empty. For a new task, use `memorix context "<task>" --json` instead. If that one command fails, report it and proceed normally; do not probe help, enumerate commands, or chain broad searches.',
21802
+ "- After a successful `memorix_project_context`, the brief is the default retrieval boundary. Do not call more Memorix retrieval tools after a complete brief. Use Context Pack, search, or detail only when the brief lacks a specific reference, freshness field, or fact needed for the task, or when the user explicitly asks for deeper history. In MCP, name that missing fact in `purpose` when intentionally expanding beyond the brief. Do not retrieve the same decision twice just to confirm an already-complete brief.",
21803
+ "- If the user asks for read-only work or says not to modify files, do not call `memorix_store` just to record an assessment. Store only when the user explicitly asks to preserve it.",
21804
+ "- Use `memorix_search` for a specific decision, bug, file, or prior change that the project context did not answer. Fetch detail only for a result you still need after the brief.",
21492
21805
  "- Treat current code-bound memory as a map. Treat stale, suspect, or unbound memory as a lead that must be verified against the current code.",
21493
21806
  "- Skip memory lookup for greetings, tiny one-off edits, or questions fully answered by the current file.",
21494
21807
  "- If a fresh project has no memories, proceed normally and do not repeat the same empty search in the same turn.",
@@ -22709,17 +23022,18 @@ alwaysApply: true
22709
23022
  "",
22710
23023
  "## Start with Memory Autopilot",
22711
23024
  "",
22712
- `Default first step for non-trivial coding work: call \`memorix_project_context\` with the user's actual task before progress files, dev-log reads, ad-hoc file reads, or git archaeology. Memorix will choose a task-lensed brief (bugfix, feature, release, onboarding, refactor, docs, test, or general). Treat its "Start here" files as the first ${contextNoun} files to inspect.`,
23025
+ `Default first step for non-trivial coding work: call \`memorix_project_context\` with the user's actual task before progress files, dev-log reads, ad-hoc file reads, or git archaeology. Memorix will choose a task-lensed brief (bugfix, feature, release, onboarding, refactor, docs, test, or general). When the task is continuing prior work, the same brief also includes a bounded prior-work projection. Treat its "Start here" files as the first ${contextNoun} files to inspect.`,
22713
23026
  "",
22714
- 'If the MCP tool is not visible yet but the client supports tool discovery or dynamic loading, search/select `memorix_project_context` first. Run `memorix context --task "<task>"` only after MCP is unavailable, disabled, or not discoverable, and pass the user\'s real task text. Do not skip memory, wait indefinitely on MCP startup, or hand-write tool-call syntax.',
23027
+ 'If the MCP tool is not visible yet but the client supports tool discovery or dynamic loading, search/select `memorix_project_context` first. Continuation fallback is mandatory: when the user asks to continue, resume, take over, or explain prior work and MCP cannot be called in this turn, run exactly one CLI brief with the user\'s real task before inspecting files, Git history, progress notes, or guessing: `memorix resume "<task>" --json`. For a new task, use `memorix context "<task>" --json` instead. The absence of `.memorix` or visible memory files never proves project memory is empty. If that one command fails, report it and proceed normally. Do not probe help, enumerate commands, chain broad searches, wait indefinitely on MCP startup, or hand-write tool-call syntax.',
22715
23028
  "",
22716
- "Use `memorix_context_pack` when you need structured refs and freshness for code-bound memories.",
23029
+ "After a successful `memorix_project_context` result, the brief is the default retrieval boundary. Do not call more Memorix retrieval tools after a complete brief. Use `memorix_context_pack`, `memorix_search`, or `memorix_detail` only when the brief lacks a specific reference, freshness field, or fact needed for the task, or when the user explicitly asks for deeper history. In MCP, name that missing fact in `purpose` when intentionally expanding beyond the brief. Do not retrieve the same decision twice just to confirm an already-complete brief.",
23030
+ "If the user asks for read-only work or says not to modify files, do not call `memorix_store` just to record an assessment. Store only when the user explicitly asks to preserve it.",
22717
23031
  "",
22718
23032
  "## When to search memory",
22719
23033
  "",
22720
23034
  "Use `memorix_graph_context` for explicit memory graph questions or broad graph overview after the autopilot brief is not enough.",
22721
23035
  "",
22722
- `Use \`memorix_search\` when prior ${contextNoun} context would help \u2014 for example:`,
23036
+ `Use \`memorix_search\` when prior ${contextNoun} context would help and the Autopilot brief did not already answer the question \u2014 for example:`,
22723
23037
  "- The user asks about a past decision, bug, or change",
22724
23038
  "- You need to understand why something was designed a certain way",
22725
23039
  "- You're continuing work that started in a previous session",
@@ -26619,18 +26933,10 @@ function parseFormationTimeoutMs(raw) {
26619
26933
  }
26620
26934
 
26621
26935
  // src/server.ts
26936
+ init_timeout();
26622
26937
  var FORMATION_TIMEOUT_MS = parseFormationTimeoutMs(process.env.MEMORIX_FORMATION_TIMEOUT_MS);
26623
26938
  var COMPACT_ON_WRITE_TIMEOUT_MS = 12e3;
26624
26939
  var COMPRESSION_TIMEOUT_MS = 5e3;
26625
- function withTimeout2(promise, ms, label) {
26626
- let timer;
26627
- return Promise.race([
26628
- promise,
26629
- new Promise((_, reject) => {
26630
- timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
26631
- })
26632
- ]).finally(() => clearTimeout(timer));
26633
- }
26634
26940
  function formatFormationStageDurations(stageDurationsMs) {
26635
26941
  const orderedStages = ["extract", "resolve", "evaluate"];
26636
26942
  const parts = orderedStages.filter((stage) => stageDurationsMs[stage] !== void 0).map((stage) => `${stage}=${stageDurationsMs[stage]}ms`);
@@ -26734,6 +27040,8 @@ var BOOTSTRAP_SAFE_TOOL_NAMES = /* @__PURE__ */ new Set([
26734
27040
  "memorix_graph_context",
26735
27041
  "memorix_context_pack"
26736
27042
  ]);
27043
+ var AUTOPILOT_RETRIEVAL_BOUNDARY_TTL_MS = 2 * 60 * 1e3;
27044
+ var READ_ONLY_TASK_PATTERN = /\b(?:do not|don't|never)\s+(?:modify|edit|change|write)\b|\bread[- ]only\b|(?:不要|不准|勿|禁止).{0,6}(?:修改|编辑|写入)|只读/i;
26737
27045
  function shouldAwaitProjectRuntime(toolName) {
26738
27046
  return !BOOTSTRAP_SAFE_TOOL_NAMES.has(toolName);
26739
27047
  }
@@ -27004,7 +27312,7 @@ The path should point to a directory containing a .git folder.`
27004
27312
  };
27005
27313
  const server = existingServer ?? new McpServer({
27006
27314
  name: "memorix",
27007
- version: true ? "1.2.3" : "1.0.1"
27315
+ version: true ? "1.2.5" : "1.0.1"
27008
27316
  });
27009
27317
  const originalRegisterTool = server.registerTool.bind(server);
27010
27318
  server.registerTool = ((name, ...args) => {
@@ -27039,11 +27347,52 @@ The path should point to a directory containing a .git folder.`
27039
27347
  isTeamMember
27040
27348
  };
27041
27349
  };
27350
+ let autopilotRetrievalBoundary = null;
27351
+ const getActiveAutopilotRetrievalBoundary = () => {
27352
+ const boundary = autopilotRetrievalBoundary;
27353
+ if (!boundary) return null;
27354
+ if (boundary.projectId === project.id && Date.now() - boundary.issuedAt <= AUTOPILOT_RETRIEVAL_BOUNDARY_TTL_MS) {
27355
+ return boundary;
27356
+ }
27357
+ autopilotRetrievalBoundary = null;
27358
+ return null;
27359
+ };
27360
+ const requireExplicitAutopilotExpansion = (toolName, purpose) => {
27361
+ if (!getActiveAutopilotRetrievalBoundary() || purpose?.trim()) return null;
27362
+ return {
27363
+ content: [{
27364
+ type: "text",
27365
+ text: "Memorix Autopilot retrieval boundary: the latest `memorix_project_context` already supplied the default bounded brief for this coding turn, so no additional memory was retrieved. Verify the current project first. To intentionally expand beyond that brief, call `" + toolName + "` again with `purpose` naming the specific missing fact or the user's explicit request."
27366
+ }]
27367
+ };
27368
+ };
27369
+ const blockCoveredAutopilotEvidence = (toolName, observationIds, force) => {
27370
+ const boundary = getActiveAutopilotRetrievalBoundary();
27371
+ if (!boundary || force || observationIds.length === 0 || !observationIds.every((id) => boundary.coveredObservationIds.has(id))) {
27372
+ return null;
27373
+ }
27374
+ return {
27375
+ content: [{
27376
+ type: "text",
27377
+ text: "Memorix Autopilot retrieval boundary: every requested memory is already represented in the latest project brief, so no duplicate detail was retrieved. Inspect the current project or seek a new source. Retry with `force: true` only when the user explicitly asks to read the underlying record in full."
27378
+ }]
27379
+ };
27380
+ };
27381
+ const blockReadOnlyAutopilotWrite = (overrideReadOnly) => {
27382
+ const boundary = getActiveAutopilotRetrievalBoundary();
27383
+ if (!boundary?.readOnly || overrideReadOnly) return null;
27384
+ return {
27385
+ content: [{
27386
+ type: "text",
27387
+ text: "Memorix write boundary: the latest task is read-only or asks not to modify files, so no memory was stored. Persist a record only when the user explicitly asks to save it, then retry with `overrideReadOnly: true`."
27388
+ }]
27389
+ };
27390
+ };
27042
27391
  server.registerTool(
27043
27392
  "memorix_store",
27044
27393
  {
27045
27394
  title: "Store Memory",
27046
- description: "Store a new observation/memory. Automatically indexed for search. Use type to classify: gotcha ([GOTCHA] critical pitfall), decision ([DECISION] architecture choice), problem-solution ([FIX] bug fix), how-it-works ([INFO] explanation), what-changed ([CHANGE] change), discovery ([DISCOVERY] insight), why-it-exists ([WHY] rationale), trade-off ([TRADEOFF] compromise), session-request ([SESSION] original goal). Project visibility is the default. Personal or team visibility requires an explicitly joined coordination identity.",
27395
+ description: "Store a new observation/memory. Automatically indexed for search. Use type to classify: gotcha ([GOTCHA] critical pitfall), decision ([DECISION] architecture choice), problem-solution ([FIX] bug fix), how-it-works ([INFO] explanation), what-changed ([CHANGE] change), discovery ([DISCOVERY] insight), why-it-exists ([WHY] rationale), trade-off ([TRADEOFF] compromise), session-request ([SESSION] original goal). Project visibility is the default. Personal or team visibility requires an explicitly joined coordination identity. For a read-only task, do not store unless the user explicitly asks to save a record.",
27047
27396
  inputSchema: {
27048
27397
  entityName: z2.string().describe('The entity this observation belongs to (e.g., "auth-module", "port-config")'),
27049
27398
  type: z2.enum(OBSERVATION_TYPES).describe("Observation type for classification"),
@@ -27064,12 +27413,17 @@ The path should point to a directory containing a .git folder.`
27064
27413
  relatedEntities: z2.array(z2.string()).optional().describe("Other entity names this memory cross-references"),
27065
27414
  visibility: z2.enum(["personal", "project", "team"]).optional().describe(
27066
27415
  "Retrieval scope. Project is the normal shared default; personal/team require memorix_session_start with joinTeam=true."
27416
+ ),
27417
+ overrideReadOnly: z2.boolean().optional().describe(
27418
+ "Use only when the user explicitly asks to save memory during a read-only or no-modification task."
27067
27419
  )
27068
27420
  }
27069
27421
  },
27070
- async ({ entityName: rawEntityName, type: rawType, title: rawTitle, narrative, facts, filesModified, concepts, topicKey, progress, relatedCommits, relatedEntities, visibility }) => {
27422
+ async ({ entityName: rawEntityName, type: rawType, title: rawTitle, narrative, facts, filesModified, concepts, topicKey, progress, relatedCommits, relatedEntities, visibility, overrideReadOnly }) => {
27071
27423
  const unresolved = requireResolvedProject("store memory in the current project");
27072
27424
  if (unresolved) return unresolved;
27425
+ const readOnlyBoundary = blockReadOnlyAutopilotWrite(overrideReadOnly);
27426
+ if (readOnlyBoundary) return readOnlyBoundary;
27073
27427
  const requestedVisibility = visibility ?? "project";
27074
27428
  const reader = getObservationReader();
27075
27429
  if (requestedVisibility !== "project" && !currentAgentId) {
@@ -27155,7 +27509,7 @@ The path should point to a directory containing a .git folder.`
27155
27509
  getEntityNames: () => graphManager.getEntityNames(),
27156
27510
  onStageEvent: onFormationStageEvent
27157
27511
  };
27158
- formationResult = await withTimeout2(
27512
+ formationResult = await withTimeout(
27159
27513
  runFormation({
27160
27514
  entityName,
27161
27515
  type,
@@ -27276,7 +27630,7 @@ ${modeIcon} Formation[active]: ${formationResult.evaluation.category} (${formati
27276
27630
  facts: d.facts,
27277
27631
  score: similarEntries[i]?.score ?? 0
27278
27632
  }));
27279
- const decision = await withTimeout2(
27633
+ const decision = await withTimeout(
27280
27634
  compactOnWrite(
27281
27635
  { title, narrative, facts: safeFacts ?? [] },
27282
27636
  existingMemories
@@ -27372,7 +27726,7 @@ Mode: ${decision.usedLLM ? "LLM" : "heuristic"}`
27372
27726
  let compressionNote = "";
27373
27727
  try {
27374
27728
  const { compressNarrative: compressNarrative2 } = await Promise.resolve().then(() => (init_quality(), quality_exports));
27375
- const { compressed, saved, usedLLM } = await withTimeout2(
27729
+ const { compressed, saved, usedLLM } = await withTimeout(
27376
27730
  compressNarrative2(narrative, safeFacts, type),
27377
27731
  COMPRESSION_TIMEOUT_MS,
27378
27732
  "Narrative compression"
@@ -27494,7 +27848,7 @@ Auto-enriched: ${enrichmentParts.join(", ")}` : "";
27494
27848
  },
27495
27849
  getEntityNames: () => graphManager.getEntityNames()
27496
27850
  };
27497
- const formed = await withTimeout2(
27851
+ const formed = await withTimeout(
27498
27852
  runFormation({ entityName, type, title, narrative, facts: safeFacts, projectId: project.id, source: "explicit", topicKey }, formationConfig),
27499
27853
  FORMATION_TIMEOUT_MS,
27500
27854
  "Shadow formation"
@@ -27568,7 +27922,7 @@ Use this as the \`topicKey\` parameter in \`memorix_store\` to enable upsert beh
27568
27922
  "memorix_search",
27569
27923
  {
27570
27924
  title: "Search Memory",
27571
- description: "Search project memory. Returns a compact index (~50-100 tokens/result). Use memorix_detail to fetch full content for specific IDs. Use memorix_timeline to see chronological context. Searches across all observations stored from any IDE session \u2014 enabling cross-session and cross-agent context retrieval.",
27925
+ description: "Search project memory. Returns a compact index (~50-100 tokens/result). Do not use as a follow-up to a complete memorix_project_context brief unless a specific fact is still missing or the user asks for deeper history; provide purpose when intentionally expanding. Use memorix_detail to fetch full content for specific IDs. Use memorix_timeline to see chronological context. Searches across all observations stored from any IDE session \u2014 enabling cross-session and cross-agent context retrieval.",
27572
27926
  inputSchema: {
27573
27927
  query: z2.string().describe("Search query (natural language or keywords)"),
27574
27928
  limit: z2.number().optional().describe("Max results (default: 20)"),
@@ -27584,14 +27938,27 @@ Use this as the \`topicKey\` parameter in \`memorix_store\` to enable upsert beh
27584
27938
  ),
27585
27939
  source: z2.enum(["agent", "git", "manual"]).optional().describe(
27586
27940
  'Filter by memory source. "git" returns only commit-derived ground truth memories. Omit for all sources.'
27941
+ ),
27942
+ quality: z2.enum(["fast", "balanced", "thorough"]).optional().default("balanced").describe(
27943
+ "Retrieval profile: fast stays local, balanced uses configured embeddings, thorough explicitly permits optional LLM refinement."
27944
+ ),
27945
+ purpose: z2.string().optional().describe(
27946
+ "Why this must expand beyond the latest Autopilot brief. Name the missing fact or the user's explicit request."
27947
+ ),
27948
+ force: z2.boolean().optional().describe(
27949
+ "Use only when the user explicitly asks to read a record already represented in the latest Autopilot brief."
27587
27950
  )
27588
27951
  }
27589
27952
  },
27590
- async ({ query, limit, type, maxTokens, scope, since, until, status, source }) => {
27953
+ async ({ query, limit, type, maxTokens, scope, since, until, status, source, quality, purpose, force }) => {
27591
27954
  if (scope !== "global") {
27592
27955
  const unresolved = requireResolvedProject("search the current project");
27593
27956
  if (unresolved) return unresolved;
27594
27957
  }
27958
+ if (scope !== "global") {
27959
+ const boundary = requireExplicitAutopilotExpansion("memorix_search", purpose);
27960
+ if (boundary) return boundary;
27961
+ }
27595
27962
  return withFreshIndex(async () => {
27596
27963
  const safeLimit = limit != null ? coerceNumber(limit, 20) : void 0;
27597
27964
  const safeMaxTokens = maxTokens != null ? coerceNumber(maxTokens, 0) : void 0;
@@ -27608,16 +27975,14 @@ Use this as the \`topicKey\` parameter in \`memorix_store\` to enable upsert beh
27608
27975
  projectId: scope === "global" ? void 0 : project.id,
27609
27976
  status: status ?? "active",
27610
27977
  source,
27978
+ quality,
27611
27979
  reader: getObservationReader(scope === "global" ? "global" : "project")
27612
27980
  });
27613
- const timeoutPromise = new Promise(
27614
- (_, reject) => setTimeout(() => reject(new Error(`Search timeout after ${TIMEOUT_MS}ms`)), TIMEOUT_MS)
27615
- );
27616
27981
  let result;
27617
27982
  try {
27618
- result = await Promise.race([searchPromise, timeoutPromise]);
27983
+ result = await withTimeout(searchPromise, TIMEOUT_MS, "Search");
27619
27984
  } catch (error) {
27620
- if (error instanceof Error && error.message.includes("timeout")) {
27985
+ if (error instanceof Error && /\btimeout\b|timed out/i.test(error.message)) {
27621
27986
  return {
27622
27987
  content: [
27623
27988
  {
@@ -27630,6 +27995,11 @@ Use this as the \`topicKey\` parameter in \`memorix_store\` to enable upsert beh
27630
27995
  }
27631
27996
  throw error;
27632
27997
  }
27998
+ const activeBoundary = getActiveAutopilotRetrievalBoundary();
27999
+ if (scope !== "global" && activeBoundary && !force && result.entries.length > 0 && result.entries.every((entry) => activeBoundary.coveredObservationIds.has(entry.id))) {
28000
+ const duplicate = blockCoveredAutopilotEvidence("memorix_search", result.entries.map((entry) => entry.id), force);
28001
+ if (duplicate) return duplicate;
28002
+ }
27633
28003
  let text = result.formatted;
27634
28004
  try {
27635
28005
  const { getLastSearchMode: getLastSearchMode2 } = await Promise.resolve().then(() => (init_orama_store(), orama_store_exports));
@@ -27731,6 +28101,7 @@ _Search mode: ${getLastSearchMode2(project.id)}_`;
27731
28101
  observations: observations2,
27732
28102
  task,
27733
28103
  refresh: refresh ?? "auto",
28104
+ reader: getObservationReader(),
27734
28105
  enqueueRefresh: () => {
27735
28106
  enqueueCodegraphRefresh2({
27736
28107
  dataDir: projectDir2,
@@ -27742,6 +28113,16 @@ _Search mode: ${getLastSearchMode2(project.id)}_`;
27742
28113
  }
27743
28114
  });
27744
28115
  const text = format === "json" ? JSON.stringify({ ...context, brief: buildAutoProjectBrief2(context) }, null, 2) : format === "summary" ? formatAutoProjectContextSummary2(context) : formatAutoProjectContextPrompt2(context);
28116
+ autopilotRetrievalBoundary = {
28117
+ projectId: project.id,
28118
+ issuedAt: Date.now(),
28119
+ coveredObservationIds: /* @__PURE__ */ new Set([
28120
+ ...context.workset.continuation?.memories.map((memory) => memory.id) ?? [],
28121
+ ...context.workset.reliableMemory.map((memory) => memory.id),
28122
+ ...context.workset.cautionMemory.map((memory) => memory.id)
28123
+ ]),
28124
+ readOnly: READ_ONLY_TASK_PATTERN.test(task ?? "")
28125
+ };
27745
28126
  return {
27746
28127
  content: [{ type: "text", text }]
27747
28128
  };
@@ -27781,18 +28162,23 @@ _Search mode: ${getLastSearchMode2(project.id)}_`;
27781
28162
  "memorix_context_pack",
27782
28163
  {
27783
28164
  title: "Context Pack",
27784
- description: "Build a prompt-ready working context pack for a coding task. Combines relevant memories, CodeGraph Memory facts, freshness warnings, suggested reads, and verification hints.",
28165
+ description: "Build a prompt-ready working context pack for a coding task. Combines relevant memories, CodeGraph Memory facts, freshness warnings, suggested reads, and verification hints. After a complete memorix_project_context brief, provide purpose only when deliberately expanding beyond it.",
27785
28166
  inputSchema: {
27786
28167
  task: z2.string().describe("Current coding task or question"),
27787
28168
  limit: z2.preprocess(
27788
28169
  (value) => typeof value === "string" && value.trim() !== "" ? Number(value) : value,
27789
28170
  z2.number().int().positive().max(100)
27790
- ).optional().describe("Max active memories to inspect before code-ref filtering (default: 20)")
28171
+ ).optional().describe("Max active memories to inspect before code-ref filtering (default: 20)"),
28172
+ purpose: z2.string().optional().describe(
28173
+ "Why this must expand beyond the latest Autopilot brief. Name the missing fact or the user's explicit request."
28174
+ )
27791
28175
  }
27792
28176
  },
27793
- async ({ task, limit }) => {
28177
+ async ({ task, limit, purpose }) => {
27794
28178
  const unresolved = requireResolvedProject("build a context pack for the current project");
27795
28179
  if (unresolved) return unresolved;
28180
+ const boundary = requireExplicitAutopilotExpansion("memorix_context_pack", purpose);
28181
+ if (boundary) return boundary;
27796
28182
  const [
27797
28183
  { CodeGraphStore: CodeGraphStore2 },
27798
28184
  { assembleContextPackForTask: assembleContextPackForTask2, attachTaskWorkset: attachTaskWorkset2, buildContextPackPrompt: buildContextPackPrompt2 },
@@ -28537,7 +28923,7 @@ ${actions.join("\n")}`
28537
28923
  "memorix_detail",
28538
28924
  {
28539
28925
  title: "Memory Details",
28540
- description: 'Fetch full observation or mini-skill details \u2014 includes source kind (explicit memory / hook trace / git evidence), value category, and cross-references (~500-1000 tokens each). Always use memorix_search first to find relevant IDs, then fetch only what you need. Accepts typed refs from search results (e.g. "obs:42", "skill:3") via the typedRefs field, or legacy numeric ids / object refs for backward compatibility.',
28926
+ description: 'Fetch full observation or mini-skill details \u2014 includes source kind (explicit memory / hook trace / git evidence), value category, and cross-references (~500-1000 tokens each). Do not re-fetch content already covered by a complete memorix_project_context brief unless a specific fact is still missing or the user asks for deeper history; provide purpose when intentionally expanding. Always use memorix_search first to find relevant IDs, then fetch only what you need. Accepts typed refs from search results (e.g. "obs:42", "skill:3") via the typedRefs field, or legacy numeric ids / object refs for backward compatibility.',
28541
28927
  inputSchema: {
28542
28928
  ids: z2.array(z2.number()).optional().describe("Observation IDs to fetch (legacy, from memorix_search results)"),
28543
28929
  refs: z2.array(
@@ -28546,16 +28932,36 @@ ${actions.join("\n")}`
28546
28932
  projectId: z2.string().optional().describe("Project ID for global-search disambiguation")
28547
28933
  })
28548
28934
  ).optional().describe("Explicit observation refs. Prefer this for global search results."),
28549
- typedRefs: z2.array(z2.string()).optional().describe('Typed memory refs from search results, e.g. "obs:42", "skill:3", "obs:42@org/proj"')
28935
+ typedRefs: z2.array(z2.string()).optional().describe('Typed memory refs from search results, e.g. "obs:42", "skill:3", "obs:42@org/proj"'),
28936
+ purpose: z2.string().optional().describe(
28937
+ "Why this must expand beyond the latest Autopilot brief. Name the missing fact or the user's explicit request."
28938
+ ),
28939
+ force: z2.boolean().optional().describe(
28940
+ "Use only when the user explicitly asks to read a record already represented in the latest Autopilot brief."
28941
+ )
28550
28942
  }
28551
28943
  },
28552
- async ({ ids, refs, typedRefs }) => {
28944
+ async ({ ids, refs, typedRefs, purpose, force }) => {
28553
28945
  const safeIds = coerceNumberArray(ids);
28554
28946
  const safeRefs = coerceObservationRefs(refs);
28555
28947
  const safeTypedRefs = coerceStringArray(typedRefs);
28948
+ const hasCrossProjectRef = safeRefs.some((ref) => ref.projectId && ref.projectId !== project.id) || safeTypedRefs.some((ref) => ref.includes("@") && !ref.endsWith(`@${project.id}`));
28949
+ if (!hasCrossProjectRef) {
28950
+ const boundary = requireExplicitAutopilotExpansion("memorix_detail", purpose);
28951
+ if (boundary) return boundary;
28952
+ const requestedObservationIds = [
28953
+ ...safeIds,
28954
+ ...safeRefs.map((ref) => ref.id),
28955
+ ...safeTypedRefs.flatMap((ref) => {
28956
+ const match = /^obs:(\d+)(?:@.+)?$/i.exec(ref.trim());
28957
+ return match ? [Number(match[1])] : [];
28958
+ })
28959
+ ];
28960
+ const duplicate = blockCoveredAutopilotEvidence("memorix_detail", requestedObservationIds, force);
28961
+ if (duplicate) return duplicate;
28962
+ }
28556
28963
  let result;
28557
28964
  try {
28558
- const hasCrossProjectRef = safeRefs.some((ref) => ref.projectId && ref.projectId !== project.id) || safeTypedRefs.some((ref) => ref.includes("@") && !ref.endsWith(`@${project.id}`));
28559
28965
  const reader = getObservationReader(hasCrossProjectRef ? "global" : "project");
28560
28966
  if (safeTypedRefs.length > 0) {
28561
28967
  result = await compactDetail(safeTypedRefs, { reader });
@@ -30581,6 +30987,7 @@ fi
30581
30987
  if (newCanonicalId === project.id && projectResolved) return false;
30582
30988
  console.error(`[memorix] Switching project: ${project.id} \u2192 ${newCanonicalId}`);
30583
30989
  currentAgentId = void 0;
30990
+ autopilotRetrievalBoundary = null;
30584
30991
  const canonicalProjectDir = newCanonicalId !== newDetected.id ? await getProjectDataDir(newCanonicalId) : newProjectDir;
30585
30992
  projectResolved = true;
30586
30993
  projectResolutionError = null;