memorix 1.2.3 → 1.2.4

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 (39) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +3 -3
  3. package/README.zh-CN.md +3 -3
  4. package/dist/cli/index.js +5187 -4730
  5. package/dist/cli/index.js.map +1 -1
  6. package/dist/index.js +416 -46
  7. package/dist/index.js.map +1 -1
  8. package/dist/maintenance-runner.js +97 -18
  9. package/dist/maintenance-runner.js.map +1 -1
  10. package/dist/memcode-runtime/CHANGELOG.md +20 -0
  11. package/dist/sdk.js +416 -46
  12. package/dist/sdk.js.map +1 -1
  13. package/docs/1.2.4-PERSISTENT-MEMORY-DELIVERY.md +86 -0
  14. package/docs/AGENT_OPERATOR_PLAYBOOK.md +13 -1
  15. package/docs/API_REFERENCE.md +13 -3
  16. package/docs/dev-log/progress.txt +48 -7
  17. package/package.json +1 -1
  18. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  19. package/src/cli/capability-map.ts +1 -1
  20. package/src/cli/command-guide.ts +4 -1
  21. package/src/cli/commands/agent-integrations.ts +5 -1
  22. package/src/cli/commands/codegraph.ts +1 -1
  23. package/src/cli/commands/context.ts +9 -1
  24. package/src/cli/commands/resume.ts +31 -0
  25. package/src/cli/index.ts +3 -1
  26. package/src/codegraph/auto-context.ts +54 -1
  27. package/src/codegraph/task-lens.ts +29 -0
  28. package/src/compact/token-budget.ts +16 -1
  29. package/src/config/toml-loader.ts +9 -5
  30. package/src/hooks/handler.ts +127 -66
  31. package/src/hooks/installers/index.ts +5 -4
  32. package/src/hooks/official-skills.ts +6 -4
  33. package/src/hooks/rules/memorix-agent-rules.md +9 -7
  34. package/src/knowledge/context-assembly.ts +4 -1
  35. package/src/knowledge/workset.ts +89 -1
  36. package/src/memory/session.ts +144 -10
  37. package/src/server.ts +137 -8
  38. package/src/store/bun-sqlite-compat.ts +118 -15
  39. package/src/store/sqlite-db.ts +3 -3
package/dist/sdk.js CHANGED
@@ -86,36 +86,111 @@ var init_visibility = __esm({
86
86
 
87
87
  // src/store/bun-sqlite-compat.ts
88
88
  import { createRequire } from "module";
89
+ function loadNodeSqlite() {
90
+ const nodeSqlite = requireFromHere("node:sqlite");
91
+ return nodeSqlite.DatabaseSync;
92
+ }
93
+ function isNativeBindingFailure(error) {
94
+ const message = error instanceof Error ? error.message : String(error);
95
+ return /could not locate the bindings file|better_sqlite3\.node|module did not self-register/i.test(message);
96
+ }
97
+ function addCompatibility(db2) {
98
+ if (!db2.pragma) {
99
+ db2.pragma = function(pragma, options) {
100
+ const statement = db2.prepare(`PRAGMA ${pragma}`);
101
+ if (options?.simple) {
102
+ const result = statement.get();
103
+ return result ? Object.values(result)[0] : void 0;
104
+ }
105
+ return statement.all();
106
+ };
107
+ }
108
+ if (!db2.transaction) {
109
+ let depth = 0;
110
+ let sequence = 0;
111
+ db2.transaction = function(fn) {
112
+ return (...args) => {
113
+ const outermost = depth === 0;
114
+ const savepoint = `memorix_tx_${++sequence}`;
115
+ if (outermost) {
116
+ db2.exec("BEGIN IMMEDIATE");
117
+ } else {
118
+ db2.exec(`SAVEPOINT ${savepoint}`);
119
+ }
120
+ depth += 1;
121
+ try {
122
+ const result = fn(...args);
123
+ if (result && typeof result.then === "function") {
124
+ throw new Error("[memorix] SQLite transactions must be synchronous");
125
+ }
126
+ depth -= 1;
127
+ if (outermost) {
128
+ db2.exec("COMMIT");
129
+ } else {
130
+ db2.exec(`RELEASE SAVEPOINT ${savepoint}`);
131
+ }
132
+ return result;
133
+ } catch (error) {
134
+ depth -= 1;
135
+ try {
136
+ if (outermost) {
137
+ db2.exec("ROLLBACK");
138
+ } else {
139
+ db2.exec(`ROLLBACK TO SAVEPOINT ${savepoint}`);
140
+ db2.exec(`RELEASE SAVEPOINT ${savepoint}`);
141
+ }
142
+ } catch {
143
+ }
144
+ throw error;
145
+ }
146
+ };
147
+ };
148
+ }
149
+ return db2;
150
+ }
151
+ function instantiateDatabase(Sqlite, filePath, options) {
152
+ return driver === "node:sqlite" && options === void 0 ? new Sqlite(filePath) : new Sqlite(filePath, options);
153
+ }
89
154
  function loadSqlite() {
90
155
  if (Database) return Database;
156
+ if (process.env.MEMORIX_SQLITE_DRIVER === "node") {
157
+ Database = loadNodeSqlite();
158
+ driver = "node:sqlite";
159
+ return Database;
160
+ }
91
161
  try {
92
162
  Database = requireFromHere("better-sqlite3");
163
+ driver = "better-sqlite3";
164
+ return Database;
165
+ } catch {
166
+ }
167
+ try {
168
+ Database = loadNodeSqlite();
169
+ driver = "node:sqlite";
93
170
  return Database;
94
171
  } catch {
95
172
  }
96
173
  try {
97
174
  const bunSqlite = requireFromHere("bun:sqlite");
98
175
  Database = bunSqlite.Database;
176
+ driver = "bun:sqlite";
99
177
  return Database;
100
178
  } catch {
101
- throw new Error("[memorix] Neither better-sqlite3 nor bun:sqlite is available");
179
+ throw new Error("[memorix] SQLite is unavailable (better-sqlite3, node:sqlite, and bun:sqlite failed)");
102
180
  }
103
181
  }
104
182
  function createDatabase(path25, options) {
105
183
  const Sqlite = loadSqlite();
106
- const db2 = new Sqlite(path25, options);
107
- if (!db2.pragma) {
108
- db2.pragma = function(pragma, options2) {
109
- if (options2 && options2.simple) {
110
- const result = db2.prepare(`PRAGMA ${pragma}`).get();
111
- return result ? Object.values(result)[0] : void 0;
112
- }
113
- return db2.prepare(`PRAGMA ${pragma}`).all();
114
- };
184
+ try {
185
+ return addCompatibility(instantiateDatabase(Sqlite, path25, options));
186
+ } catch (error) {
187
+ if (driver !== "better-sqlite3" || !isNativeBindingFailure(error)) throw error;
188
+ Database = loadNodeSqlite();
189
+ driver = "node:sqlite";
190
+ return addCompatibility(instantiateDatabase(Database, path25, options));
115
191
  }
116
- return db2;
117
192
  }
118
- var Database, requireFromHere;
193
+ var Database, driver, requireFromHere;
119
194
  var init_bun_sqlite_compat = __esm({
120
195
  "src/store/bun-sqlite-compat.ts"() {
121
196
  "use strict";
@@ -134,7 +209,7 @@ function loadBetterSqlite3() {
134
209
  BetterSqlite3 = loadSqlite();
135
210
  return BetterSqlite3;
136
211
  } catch {
137
- throw new Error("[memorix] SQLite is not available (neither better-sqlite3 nor bun:sqlite)");
212
+ throw new Error("[memorix] SQLite is not available (better-sqlite3, node:sqlite, and bun:sqlite all failed)");
138
213
  }
139
214
  }
140
215
  function hasColumn(db2, table, column) {
@@ -1942,7 +2017,16 @@ function truncateToTokenBudget(text, budget) {
1942
2017
  result = result.slice(0, Math.floor(result.length * 0.9));
1943
2018
  }
1944
2019
  if (result.length < text.length) {
1945
- result += "...";
2020
+ const nextCharacter = text.charAt(result.length);
2021
+ if (nextCharacter && !/[\s.,;:!?)}\]]/.test(nextCharacter)) {
2022
+ const boundary = result.search(/\s+\S*$/);
2023
+ result = boundary > 0 ? result.slice(0, boundary).trimEnd() : "";
2024
+ }
2025
+ while (result && fitsInBudget(result + "...", budget) === false) {
2026
+ const boundary = result.lastIndexOf(" ");
2027
+ result = boundary > 0 ? result.slice(0, boundary).trimEnd() : "";
2028
+ }
2029
+ result = result ? result + "..." : "...";
1946
2030
  }
1947
2031
  }
1948
2032
  return result;
@@ -2595,6 +2679,9 @@ function parseTomlValue(raw, filePath, line) {
2595
2679
  if (raw.startsWith('"') && raw.endsWith('"')) {
2596
2680
  return raw.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, "\\");
2597
2681
  }
2682
+ if (raw.startsWith("'") && raw.endsWith("'")) {
2683
+ return raw.slice(1, -1);
2684
+ }
2598
2685
  if (raw === "true") return true;
2599
2686
  if (raw === "false") return false;
2600
2687
  if (/^-?\d+$/.test(raw)) return Number.parseInt(raw, 10);
@@ -2607,7 +2694,7 @@ function parseTomlValue(raw, filePath, line) {
2607
2694
  throw new Error(`Unsupported TOML value in ${filePath}:${line}`);
2608
2695
  }
2609
2696
  function stripComment(line) {
2610
- let inString = false;
2697
+ let quote = null;
2611
2698
  let escaped = false;
2612
2699
  for (let i = 0; i < line.length; i++) {
2613
2700
  const char = line[i];
@@ -2615,15 +2702,16 @@ function stripComment(line) {
2615
2702
  escaped = false;
2616
2703
  continue;
2617
2704
  }
2618
- if (char === "\\" && inString) {
2705
+ if (char === "\\" && quote === '"') {
2619
2706
  escaped = true;
2620
2707
  continue;
2621
2708
  }
2622
- if (char === '"') {
2623
- inString = !inString;
2709
+ if (char === '"' || char === "'") {
2710
+ if (quote === char) quote = null;
2711
+ else if (quote === null) quote = char;
2624
2712
  continue;
2625
2713
  }
2626
- if (char === "#" && !inString) {
2714
+ if (char === "#" && quote === null) {
2627
2715
  return line.slice(0, i);
2628
2716
  }
2629
2717
  }
@@ -13695,6 +13783,7 @@ var init_workflow_store = __esm({
13695
13783
  var task_lens_exports = {};
13696
13784
  __export(task_lens_exports, {
13697
13785
  containsTaskKeyword: () => containsTaskKeyword,
13786
+ isContinuationTask: () => isContinuationTask,
13698
13787
  lensPathCandidates: () => lensPathCandidates,
13699
13788
  lensVerificationHints: () => lensVerificationHints,
13700
13789
  rankLensPaths: () => rankLensPaths,
@@ -13760,6 +13849,12 @@ function resolveTaskLens(task) {
13760
13849
  }
13761
13850
  return best.score > 0 ? LENSES[best.id] : LENSES.general;
13762
13851
  }
13852
+ function isContinuationTask(task) {
13853
+ const normalized = (task ?? "").trim().toLowerCase();
13854
+ return normalized.length > 0 && CONTINUATION_KEYWORDS.some(
13855
+ (keyword) => containsTaskKeyword(normalized, keyword)
13856
+ );
13857
+ }
13763
13858
  function pathKindScore(path25, lens) {
13764
13859
  const normalized = normalizePath2(path25).toLowerCase();
13765
13860
  const name = normalized.split("/").pop() ?? normalized;
@@ -13903,7 +13998,7 @@ function shouldShowLensSource(source, lens, task) {
13903
13998
  if (lens.id === "onboarding" || lens.id === "release" || lens.id === "docs") return score >= 40;
13904
13999
  return score > 0;
13905
14000
  }
13906
- var STOP_WORDS, LENSES, KEYWORDS, LENS_PRIORITY;
14001
+ var STOP_WORDS, LENSES, KEYWORDS, CONTINUATION_KEYWORDS, LENS_PRIORITY;
13907
14002
  var init_task_lens = __esm({
13908
14003
  "src/codegraph/task-lens.ts"() {
13909
14004
  "use strict";
@@ -14006,6 +14101,7 @@ var init_task_lens = __esm({
14006
14101
  "fail",
14007
14102
  "failing",
14008
14103
  "fix",
14104
+ "fixing",
14009
14105
  "issue",
14010
14106
  "regression",
14011
14107
  "repro",
@@ -14023,6 +14119,18 @@ var init_task_lens = __esm({
14023
14119
  docs: ["doc", "docs", "readme", "\u6587\u6863", "\u8BF4\u660E"],
14024
14120
  test: ["coverage", "fixture", "smoke", "spec", "test", "tests", "testing", "vitest", "\u6D4B\u8BD5"]
14025
14121
  };
14122
+ CONTINUATION_KEYWORDS = [
14123
+ "continue",
14124
+ "resume",
14125
+ "pick up",
14126
+ "carry on",
14127
+ "previous session",
14128
+ "\u7EE7\u7EED",
14129
+ "\u63A5\u624B",
14130
+ "\u6062\u590D",
14131
+ "\u5EF6\u7EED",
14132
+ "\u4E0A\u6B21\u4F1A\u8BDD"
14133
+ ];
14026
14134
  LENS_PRIORITY = [
14027
14135
  "bugfix",
14028
14136
  "release",
@@ -15233,6 +15341,7 @@ __export(session_exports, {
15233
15341
  endSession: () => endSession,
15234
15342
  getActiveSession: () => getActiveSession,
15235
15343
  getSessionContext: () => getSessionContext,
15344
+ getSessionResumeBrief: () => getSessionResumeBrief,
15236
15345
  listSessions: () => listSessions,
15237
15346
  scoreObservationForSessionContext: () => scoreObservationForSessionContext,
15238
15347
  startSession: () => startSession
@@ -15300,6 +15409,62 @@ function isSystemSelfObservation(obs) {
15300
15409
  const text = stringifyObservation(obs, false);
15301
15410
  return SYSTEM_SELF_PATTERNS.some((pattern) => pattern.test(text));
15302
15411
  }
15412
+ function readerForAlias(reader, aliases, observation) {
15413
+ if (!reader) return void 0;
15414
+ return reader.projectId && aliases.has(observation.projectId) ? { ...reader, projectId: observation.projectId } : reader;
15415
+ }
15416
+ function readableAliasObservations(observations2, aliases, reader) {
15417
+ return reader ? observations2.filter((observation) => canReadObservation(
15418
+ observation,
15419
+ readerForAlias(reader, aliases, observation)
15420
+ )) : observations2;
15421
+ }
15422
+ function isUsefulSessionSummary(summary) {
15423
+ return Boolean(summary) && !NOISE_PATTERNS2.some((pattern) => pattern.test(summary)) && !SYSTEM_SELF_PATTERNS.some((pattern) => pattern.test(summary));
15424
+ }
15425
+ function continuationTaskTokens(task) {
15426
+ return [...new Set(
15427
+ (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))
15428
+ )].slice(0, 8);
15429
+ }
15430
+ function continuationScore(observation, projectTokens, task) {
15431
+ let score = scoreObservationForSessionContext(observation, projectTokens);
15432
+ if (observation.type === "session-request" || observation.type === "what-changed") score += 1;
15433
+ const text = stringifyObservation(observation);
15434
+ const matches = continuationTaskTokens(task).filter((token) => text.includes(token)).length;
15435
+ score += Math.min(matches, 2) * 2;
15436
+ return score;
15437
+ }
15438
+ async function getSessionResumeBrief(projectId, task, reader) {
15439
+ const aliasSet = await resolveProjectIds(projectId);
15440
+ const [sessions, allObs] = await Promise.all([
15441
+ loadAliasSessions(aliasSet),
15442
+ loadAliasActiveObservations(aliasSet)
15443
+ ]);
15444
+ const readableObs = readableAliasObservations(allObs, aliasSet, reader);
15445
+ 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));
15446
+ const projectTokens = tokenizeProjectId(projectId);
15447
+ const memories = readableObs.filter((observation) => RESUME_TYPES.has(observation.type)).filter((observation) => classifyLayer(observation) === "L2").filter((observation) => !isNoiseObservation(observation) && !isSystemSelfObservation(observation)).map((observation) => ({
15448
+ observation,
15449
+ score: continuationScore(observation, projectTokens, task)
15450
+ })).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 }) => ({
15451
+ id: observation.id,
15452
+ title: sanitizeCredentials(observation.title),
15453
+ type: observation.type,
15454
+ ...observation.facts?.[0] ? { detail: sanitizeCredentials(observation.facts[0]) } : observation.narrative ? { detail: sanitizeCredentials(observation.narrative) } : {}
15455
+ }));
15456
+ return {
15457
+ ...previous?.summary ? {
15458
+ previousSession: {
15459
+ id: previous.id,
15460
+ ...previous.agent ? { agent: previous.agent } : {},
15461
+ ...previous.endedAt ? { endedAt: previous.endedAt } : {},
15462
+ summary: sanitizeCredentials(previous.summary)
15463
+ }
15464
+ } : {},
15465
+ memories
15466
+ };
15467
+ }
15303
15468
  function scoreObservationForSessionContext(obs, projectTokens, now3 = Date.now()) {
15304
15469
  let score = TYPE_WEIGHTS2[obs.type] ?? 1;
15305
15470
  const text = stringifyObservation(obs);
@@ -15370,10 +15535,7 @@ async function getSessionContext(projectDir2, projectId, limit = 3, reader) {
15370
15535
  loadAliasSessions(aliasSet),
15371
15536
  loadAliasActiveObservations(aliasSet)
15372
15537
  ]);
15373
- const readableObs = reader ? allObs.filter((observation) => {
15374
- const observationReader = reader.projectId && aliasSet.has(observation.projectId) ? { ...reader, projectId: observation.projectId } : reader;
15375
- return canReadObservation(observation, observationReader);
15376
- }) : allObs;
15538
+ const readableObs = readableAliasObservations(allObs, aliasSet, reader);
15377
15539
  const isNoisySummary = (summary) => {
15378
15540
  if (!summary) return false;
15379
15541
  return NOISE_PATTERNS2.some((p) => p.test(summary)) || SYSTEM_SELF_PATTERNS.some((p) => p.test(summary));
@@ -15524,7 +15686,7 @@ async function getActiveSession(projectDir2, projectId) {
15524
15686
  const sessions = await loadAliasActiveSessions(aliasSet);
15525
15687
  return sessions.sort((a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime())[0] ?? null;
15526
15688
  }
15527
- var PRIORITY_TYPES, TYPE_EMOJI, TYPE_WEIGHTS2, NOISE_PATTERNS2, COMMAND_TRACE_PATTERNS, SYSTEM_SELF_PATTERNS;
15689
+ var PRIORITY_TYPES, RESUME_TYPES, TYPE_EMOJI, TYPE_WEIGHTS2, NOISE_PATTERNS2, COMMAND_TRACE_PATTERNS, SYSTEM_SELF_PATTERNS;
15528
15690
  var init_session = __esm({
15529
15691
  "src/memory/session.ts"() {
15530
15692
  "use strict";
@@ -15538,6 +15700,17 @@ var init_session = __esm({
15538
15700
  init_secret_filter();
15539
15701
  init_visibility();
15540
15702
  PRIORITY_TYPES = /* @__PURE__ */ new Set(["gotcha", "decision", "problem-solution", "trade-off", "discovery"]);
15703
+ RESUME_TYPES = /* @__PURE__ */ new Set([
15704
+ "gotcha",
15705
+ "decision",
15706
+ "problem-solution",
15707
+ "trade-off",
15708
+ "discovery",
15709
+ "how-it-works",
15710
+ "what-changed",
15711
+ "reasoning",
15712
+ "session-request"
15713
+ ]);
15541
15714
  TYPE_EMOJI = {
15542
15715
  "gotcha": "[DISCOVERY]",
15543
15716
  "decision": "[WHY]",
@@ -15698,6 +15871,7 @@ function freshnessForMemory(status) {
15698
15871
  return "unknown";
15699
15872
  }
15700
15873
  function receiptOmissionKind(raw) {
15874
+ if (raw.includes("continuation")) return "continuation";
15701
15875
  if (raw.includes("task")) return "task";
15702
15876
  if (raw.includes("fact")) return "current-fact";
15703
15877
  if (raw.includes("state")) return "code-state";
@@ -15747,6 +15921,48 @@ function renderTaskWorksetPrompt(input) {
15747
15921
  trust: "source-backed"
15748
15922
  });
15749
15923
  appendLine(lines, "Task lens: " + input.lens, maxTokens, omitted, "lens");
15924
+ const hasContinuation = Boolean(
15925
+ input.continuation?.previousSession || (input.continuation?.memories.length ?? 0) > 0
15926
+ );
15927
+ if (hasContinuation && input.continuation) {
15928
+ appendLine(lines, "", maxTokens, omitted, "continuation-heading");
15929
+ appendLine(lines, "Resume from prior work", maxTokens, omitted, "continuation-heading");
15930
+ if (input.continuation.previousSession) {
15931
+ const session = input.continuation.previousSession;
15932
+ const source = [session.agent, session.endedAt ? session.endedAt.slice(0, 10) : void 0].filter(Boolean).join(", ");
15933
+ appendLine(
15934
+ lines,
15935
+ "- Previous session" + (source ? ` (${source})` : "") + ": " + short(session.summary, 44),
15936
+ maxTokens,
15937
+ omitted,
15938
+ "continuation-session",
15939
+ selected,
15940
+ {
15941
+ kind: "continuation",
15942
+ id: "session:" + session.id,
15943
+ reason: "latest meaningful project session summary",
15944
+ trust: "historical"
15945
+ }
15946
+ );
15947
+ }
15948
+ for (const memory of input.continuation.memories.slice(0, 3)) {
15949
+ const detail = memory.detail ? ": " + short(memory.detail, CONTINUATION_DETAIL_TOKEN_BUDGET) : "";
15950
+ appendLine(
15951
+ lines,
15952
+ "- #" + memory.id + " " + memory.type + ": " + short(memory.title, 18) + detail,
15953
+ maxTokens,
15954
+ omitted,
15955
+ "continuation-memory",
15956
+ selected,
15957
+ {
15958
+ kind: "continuation",
15959
+ id: "memory:" + memory.id,
15960
+ reason: "durable prior-work memory",
15961
+ trust: "historical"
15962
+ }
15963
+ );
15964
+ }
15965
+ }
15750
15966
  if (input.cautions.length > 0 || input.cautionMemory.length > 0) {
15751
15967
  appendLine(lines, "", maxTokens, omitted, "caution-heading");
15752
15968
  appendLine(lines, "Cautions", maxTokens, omitted, "caution-heading");
@@ -16015,11 +16231,25 @@ async function buildTaskWorkset(input) {
16015
16231
  ...input.verificationHints
16016
16232
  ]).slice(0, 4);
16017
16233
  const normalizedCautions = unique(cautions.map((caution) => caution.kind)).map((kind) => cautions.find((caution) => caution.kind === kind)).slice(0, 6);
16234
+ const continuation = input.continuation && (input.continuation.previousSession || input.continuation.memories.length > 0) ? {
16235
+ ...input.continuation.previousSession ? {
16236
+ previousSession: {
16237
+ ...input.continuation.previousSession,
16238
+ summary: short(input.continuation.previousSession.summary, 52)
16239
+ }
16240
+ } : {},
16241
+ memories: input.continuation.memories.slice(0, 3).map((memory) => ({
16242
+ ...memory,
16243
+ title: short(memory.title, 20),
16244
+ ...memory.detail ? { detail: short(memory.detail, CONTINUATION_DETAIL_TOKEN_BUDGET) } : {}
16245
+ }))
16246
+ } : void 0;
16018
16247
  const base = {
16019
16248
  version: "1.2",
16020
16249
  task,
16021
16250
  lens: input.lens,
16022
16251
  currentFacts: input.currentFacts?.map((fact) => fact.startsWith("Historical note:") ? short(fact, 48) : short(fact, 28)).slice(0, 4) ?? [],
16252
+ ...continuation ? { continuation } : {},
16023
16253
  ...input.codeState ? { codeState: short(input.codeState, 28) } : {},
16024
16254
  startHere: unique(input.startHere).slice(0, 5),
16025
16255
  ...input.semanticCode ? { semanticCode: input.semanticCode } : {},
@@ -16044,7 +16274,7 @@ async function buildTaskWorkset(input) {
16044
16274
  budget: { maxTokens }
16045
16275
  });
16046
16276
  const receipt = {
16047
- version: "1.2.2",
16277
+ version: "1.2.4",
16048
16278
  target: input.deliveryTarget ?? "project-context",
16049
16279
  elapsedMs: Math.max(0, Date.now() - startedAt),
16050
16280
  budget: {
@@ -16066,6 +16296,7 @@ async function buildTaskWorkset(input) {
16066
16296
  prompt: rendered.prompt
16067
16297
  };
16068
16298
  }
16299
+ var CONTINUATION_DETAIL_TOKEN_BUDGET;
16069
16300
  var init_workset = __esm({
16070
16301
  "src/knowledge/workset.ts"() {
16071
16302
  "use strict";
@@ -16078,6 +16309,7 @@ var init_workset = __esm({
16078
16309
  init_workspace();
16079
16310
  init_workflow_store();
16080
16311
  init_workflows();
16312
+ CONTINUATION_DETAIL_TOKEN_BUDGET = 20;
16081
16313
  }
16082
16314
  });
16083
16315
 
@@ -16870,6 +17102,7 @@ async function buildAutoProjectContext(input) {
16870
17102
  const now3 = input.now ?? /* @__PURE__ */ new Date();
16871
17103
  const task = input.task?.trim();
16872
17104
  const lens = resolveTaskLens(task);
17105
+ const continuationRequested = input.continuation === "always" || input.continuation !== "never" && isContinuationTask(task);
16873
17106
  const codegraphConfig = getResolvedConfig({ projectRoot: input.project.rootPath }).codegraph;
16874
17107
  const exclude = input.exclude ?? codegraphConfig.excludePatterns;
16875
17108
  const maxFileBytes = input.maxFileBytes ?? codegraphConfig.maxFileBytes;
@@ -17006,6 +17239,11 @@ async function buildAutoProjectContext(input) {
17006
17239
  if (externalCaution) {
17007
17240
  runtimeCautions.push({ kind: "external-codegraph-fallback", message: externalCaution });
17008
17241
  }
17242
+ let continuation;
17243
+ if (continuationRequested) {
17244
+ await initSessionStore(input.dataDir);
17245
+ continuation = await getSessionResumeBrief(input.project.id, task, input.reader);
17246
+ }
17009
17247
  const workset = await buildTaskWorkset({
17010
17248
  projectId: input.project.id,
17011
17249
  dataDir: input.dataDir,
@@ -17015,6 +17253,7 @@ async function buildAutoProjectContext(input) {
17015
17253
  ...externalOutline ? { semanticCode: externalOutline } : {},
17016
17254
  providerQuality: providerQuality2,
17017
17255
  currentFacts: worksetFactLines(currentFacts),
17256
+ ...continuation ? { continuation } : {},
17018
17257
  codeState: codeStateLine(overview),
17019
17258
  reliableMemory: sourceSets.reliableSources.slice(0, lens.sourceLimit).map((source) => ({
17020
17259
  id: source.observationId,
@@ -17059,12 +17298,19 @@ async function buildAutoProjectContext(input) {
17059
17298
  explain,
17060
17299
  refresh,
17061
17300
  providerQuality: providerQuality2,
17301
+ ...continuationRequested && workset.continuation ? { continuation: workset.continuation } : {},
17062
17302
  workset
17063
17303
  };
17064
17304
  }
17065
17305
  function formatLanguages(overview) {
17066
17306
  return overview.code.languages.length > 0 ? overview.code.languages.map((item) => `${item.language} ${item.files}`).join(", ") : "none indexed yet";
17067
17307
  }
17308
+ function compactContinuationText(text, budget) {
17309
+ return truncateToTokenBudget(
17310
+ sanitizeCredentials(text).replace(/\s+/g, " ").trim(),
17311
+ budget
17312
+ );
17313
+ }
17068
17314
  function codeStateLine(overview) {
17069
17315
  const snapshot = overview.code.latestSnapshot;
17070
17316
  if (!snapshot) return "- Code state: no completed snapshot yet";
@@ -17204,6 +17450,23 @@ function formatAutoProjectContextSummary(context) {
17204
17450
  "Reliable memory",
17205
17451
  reliableSources.length > 0 ? `- ${reliableSources.length} current code-bound memory link(s)` : "- none yet"
17206
17452
  );
17453
+ const continuation = context.workset.continuation;
17454
+ if (continuation?.previousSession || continuation?.memories.length) {
17455
+ lines.push("", "Resume from prior work");
17456
+ if (continuation.previousSession) {
17457
+ const session = continuation.previousSession;
17458
+ const source = [session.agent, session.endedAt ? session.endedAt.slice(0, 10) : void 0].filter(Boolean).join(", ");
17459
+ lines.push(
17460
+ "- Previous session" + (source ? ` (${source})` : "") + ": " + compactContinuationText(session.summary, 44)
17461
+ );
17462
+ }
17463
+ for (const memory of continuation.memories.slice(0, 3)) {
17464
+ const detail = memory.detail ? ": " + compactContinuationText(memory.detail, 20) : "";
17465
+ lines.push(
17466
+ "- #" + memory.id + " " + memory.type + ": " + compactContinuationText(memory.title, 18) + detail
17467
+ );
17468
+ }
17469
+ }
17207
17470
  return lines.join("\n");
17208
17471
  }
17209
17472
  function formatAutoProjectContextPrompt(context) {
@@ -17274,8 +17537,10 @@ var init_auto_context = __esm({
17274
17537
  "src/codegraph/auto-context.ts"() {
17275
17538
  "use strict";
17276
17539
  init_esm_shims();
17540
+ init_token_budget();
17277
17541
  init_resolved_config();
17278
17542
  init_workset();
17543
+ init_secret_filter();
17279
17544
  init_binder();
17280
17545
  init_current_facts();
17281
17546
  init_lite_provider();
@@ -17283,6 +17548,8 @@ var init_auto_context = __esm({
17283
17548
  init_project_context();
17284
17549
  init_store();
17285
17550
  init_admission();
17551
+ init_session();
17552
+ init_session_store();
17286
17553
  init_task_lens();
17287
17554
  DEFAULT_MAX_AGE_MS = 10 * 60 * 1e3;
17288
17555
  }
@@ -21477,7 +21744,8 @@ var init_official_skills = __esm({
21477
21744
  "",
21478
21745
  "| Situation | Prefer | CLI fallback |",
21479
21746
  "|---|---|---|",
21480
- '| 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>"` |',
21747
+ '| Starting a coding task and needing the best task-lensed project map | `memorix_project_context` with the user\'s task | `memorix context "<task>" --json` |',
21748
+ '| Continuing prior project work | `memorix_project_context` with the user\'s task | `memorix resume "<task>" --json` |',
21481
21749
  '| Need structured refs/freshness for code-bound memories | `memorix_context_pack` | `memorix codegraph context-pack --task "<topic>"` |',
21482
21750
  '| Explicit memory graph question | `memorix_graph_context` | `memorix memory graph-context --query "<topic>"` |',
21483
21751
  '| Specific past decision, bug, file, or change | `memorix_search` | `memorix memory search --query "<topic>"` |',
@@ -21490,9 +21758,10 @@ var init_official_skills = __esm({
21490
21758
  "",
21491
21759
  "- 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.",
21492
21760
  "- 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.",
21493
- '- 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.',
21494
- "- Use `memorix_search` for a specific decision, bug, file, or prior change that the project context did not answer.",
21495
- "- Fetch detail before relying on a specific memory.",
21761
+ '- 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.',
21762
+ "- 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.",
21763
+ "- 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.",
21764
+ "- 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.",
21496
21765
  "- 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.",
21497
21766
  "- Skip memory lookup for greetings, tiny one-off edits, or questions fully answered by the current file.",
21498
21767
  "- If a fresh project has no memories, proceed normally and do not repeat the same empty search in the same turn.",
@@ -22713,17 +22982,18 @@ alwaysApply: true
22713
22982
  "",
22714
22983
  "## Start with Memory Autopilot",
22715
22984
  "",
22716
- `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.`,
22985
+ `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.`,
22717
22986
  "",
22718
- '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.',
22987
+ '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.',
22719
22988
  "",
22720
- "Use `memorix_context_pack` when you need structured refs and freshness for code-bound memories.",
22989
+ "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.",
22990
+ "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.",
22721
22991
  "",
22722
22992
  "## When to search memory",
22723
22993
  "",
22724
22994
  "Use `memorix_graph_context` for explicit memory graph questions or broad graph overview after the autopilot brief is not enough.",
22725
22995
  "",
22726
- `Use \`memorix_search\` when prior ${contextNoun} context would help \u2014 for example:`,
22996
+ `Use \`memorix_search\` when prior ${contextNoun} context would help and the Autopilot brief did not already answer the question \u2014 for example:`,
22727
22997
  "- The user asks about a past decision, bug, or change",
22728
22998
  "- You need to understand why something was designed a certain way",
22729
22999
  "- You're continuing work that started in a previous session",
@@ -26650,6 +26920,8 @@ var BOOTSTRAP_SAFE_TOOL_NAMES = /* @__PURE__ */ new Set([
26650
26920
  "memorix_graph_context",
26651
26921
  "memorix_context_pack"
26652
26922
  ]);
26923
+ var AUTOPILOT_RETRIEVAL_BOUNDARY_TTL_MS = 2 * 60 * 1e3;
26924
+ var READ_ONLY_TASK_PATTERN = /\b(?:do not|don't|never)\s+(?:modify|edit|change|write)\b|\bread[- ]only\b|(?:不要|不准|勿|禁止).{0,6}(?:修改|编辑|写入)|只读/i;
26653
26925
  function shouldAwaitProjectRuntime(toolName) {
26654
26926
  return !BOOTSTRAP_SAFE_TOOL_NAMES.has(toolName);
26655
26927
  }
@@ -26920,7 +27192,7 @@ The path should point to a directory containing a .git folder.`
26920
27192
  };
26921
27193
  const server = existingServer ?? new McpServer({
26922
27194
  name: "memorix",
26923
- version: true ? "1.2.3" : "1.0.1"
27195
+ version: true ? "1.2.4" : "1.0.1"
26924
27196
  });
26925
27197
  const originalRegisterTool = server.registerTool.bind(server);
26926
27198
  server.registerTool = ((name, ...args) => {
@@ -26955,11 +27227,52 @@ The path should point to a directory containing a .git folder.`
26955
27227
  isTeamMember
26956
27228
  };
26957
27229
  };
27230
+ let autopilotRetrievalBoundary = null;
27231
+ const getActiveAutopilotRetrievalBoundary = () => {
27232
+ const boundary = autopilotRetrievalBoundary;
27233
+ if (!boundary) return null;
27234
+ if (boundary.projectId === project.id && Date.now() - boundary.issuedAt <= AUTOPILOT_RETRIEVAL_BOUNDARY_TTL_MS) {
27235
+ return boundary;
27236
+ }
27237
+ autopilotRetrievalBoundary = null;
27238
+ return null;
27239
+ };
27240
+ const requireExplicitAutopilotExpansion = (toolName, purpose) => {
27241
+ if (!getActiveAutopilotRetrievalBoundary() || purpose?.trim()) return null;
27242
+ return {
27243
+ content: [{
27244
+ type: "text",
27245
+ 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."
27246
+ }]
27247
+ };
27248
+ };
27249
+ const blockCoveredAutopilotEvidence = (toolName, observationIds, force) => {
27250
+ const boundary = getActiveAutopilotRetrievalBoundary();
27251
+ if (!boundary || force || observationIds.length === 0 || !observationIds.every((id) => boundary.coveredObservationIds.has(id))) {
27252
+ return null;
27253
+ }
27254
+ return {
27255
+ content: [{
27256
+ type: "text",
27257
+ 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."
27258
+ }]
27259
+ };
27260
+ };
27261
+ const blockReadOnlyAutopilotWrite = (overrideReadOnly) => {
27262
+ const boundary = getActiveAutopilotRetrievalBoundary();
27263
+ if (!boundary?.readOnly || overrideReadOnly) return null;
27264
+ return {
27265
+ content: [{
27266
+ type: "text",
27267
+ 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`."
27268
+ }]
27269
+ };
27270
+ };
26958
27271
  server.registerTool(
26959
27272
  "memorix_store",
26960
27273
  {
26961
27274
  title: "Store Memory",
26962
- 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.",
27275
+ 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.",
26963
27276
  inputSchema: {
26964
27277
  entityName: z2.string().describe('The entity this observation belongs to (e.g., "auth-module", "port-config")'),
26965
27278
  type: z2.enum(OBSERVATION_TYPES).describe("Observation type for classification"),
@@ -26980,12 +27293,17 @@ The path should point to a directory containing a .git folder.`
26980
27293
  relatedEntities: z2.array(z2.string()).optional().describe("Other entity names this memory cross-references"),
26981
27294
  visibility: z2.enum(["personal", "project", "team"]).optional().describe(
26982
27295
  "Retrieval scope. Project is the normal shared default; personal/team require memorix_session_start with joinTeam=true."
27296
+ ),
27297
+ overrideReadOnly: z2.boolean().optional().describe(
27298
+ "Use only when the user explicitly asks to save memory during a read-only or no-modification task."
26983
27299
  )
26984
27300
  }
26985
27301
  },
26986
- async ({ entityName: rawEntityName, type: rawType, title: rawTitle, narrative, facts, filesModified, concepts, topicKey, progress, relatedCommits, relatedEntities, visibility }) => {
27302
+ async ({ entityName: rawEntityName, type: rawType, title: rawTitle, narrative, facts, filesModified, concepts, topicKey, progress, relatedCommits, relatedEntities, visibility, overrideReadOnly }) => {
26987
27303
  const unresolved = requireResolvedProject("store memory in the current project");
26988
27304
  if (unresolved) return unresolved;
27305
+ const readOnlyBoundary = blockReadOnlyAutopilotWrite(overrideReadOnly);
27306
+ if (readOnlyBoundary) return readOnlyBoundary;
26989
27307
  const requestedVisibility = visibility ?? "project";
26990
27308
  const reader = getObservationReader();
26991
27309
  if (requestedVisibility !== "project" && !currentAgentId) {
@@ -27484,7 +27802,7 @@ Use this as the \`topicKey\` parameter in \`memorix_store\` to enable upsert beh
27484
27802
  "memorix_search",
27485
27803
  {
27486
27804
  title: "Search Memory",
27487
- 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.",
27805
+ 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.",
27488
27806
  inputSchema: {
27489
27807
  query: z2.string().describe("Search query (natural language or keywords)"),
27490
27808
  limit: z2.number().optional().describe("Max results (default: 20)"),
@@ -27500,14 +27818,24 @@ Use this as the \`topicKey\` parameter in \`memorix_store\` to enable upsert beh
27500
27818
  ),
27501
27819
  source: z2.enum(["agent", "git", "manual"]).optional().describe(
27502
27820
  'Filter by memory source. "git" returns only commit-derived ground truth memories. Omit for all sources.'
27821
+ ),
27822
+ purpose: z2.string().optional().describe(
27823
+ "Why this must expand beyond the latest Autopilot brief. Name the missing fact or the user's explicit request."
27824
+ ),
27825
+ force: z2.boolean().optional().describe(
27826
+ "Use only when the user explicitly asks to read a record already represented in the latest Autopilot brief."
27503
27827
  )
27504
27828
  }
27505
27829
  },
27506
- async ({ query, limit, type, maxTokens, scope, since, until, status, source }) => {
27830
+ async ({ query, limit, type, maxTokens, scope, since, until, status, source, purpose, force }) => {
27507
27831
  if (scope !== "global") {
27508
27832
  const unresolved = requireResolvedProject("search the current project");
27509
27833
  if (unresolved) return unresolved;
27510
27834
  }
27835
+ if (scope !== "global") {
27836
+ const boundary = requireExplicitAutopilotExpansion("memorix_search", purpose);
27837
+ if (boundary) return boundary;
27838
+ }
27511
27839
  return withFreshIndex(async () => {
27512
27840
  const safeLimit = limit != null ? coerceNumber(limit, 20) : void 0;
27513
27841
  const safeMaxTokens = maxTokens != null ? coerceNumber(maxTokens, 0) : void 0;
@@ -27546,6 +27874,11 @@ Use this as the \`topicKey\` parameter in \`memorix_store\` to enable upsert beh
27546
27874
  }
27547
27875
  throw error;
27548
27876
  }
27877
+ const activeBoundary = getActiveAutopilotRetrievalBoundary();
27878
+ if (scope !== "global" && activeBoundary && !force && result.entries.length > 0 && result.entries.every((entry) => activeBoundary.coveredObservationIds.has(entry.id))) {
27879
+ const duplicate = blockCoveredAutopilotEvidence("memorix_search", result.entries.map((entry) => entry.id), force);
27880
+ if (duplicate) return duplicate;
27881
+ }
27549
27882
  let text = result.formatted;
27550
27883
  try {
27551
27884
  const { getLastSearchMode: getLastSearchMode2 } = await Promise.resolve().then(() => (init_orama_store(), orama_store_exports));
@@ -27647,6 +27980,7 @@ _Search mode: ${getLastSearchMode2(project.id)}_`;
27647
27980
  observations: observations2,
27648
27981
  task,
27649
27982
  refresh: refresh ?? "auto",
27983
+ reader: getObservationReader(),
27650
27984
  enqueueRefresh: () => {
27651
27985
  enqueueCodegraphRefresh2({
27652
27986
  dataDir: projectDir2,
@@ -27658,6 +27992,16 @@ _Search mode: ${getLastSearchMode2(project.id)}_`;
27658
27992
  }
27659
27993
  });
27660
27994
  const text = format === "json" ? JSON.stringify({ ...context, brief: buildAutoProjectBrief2(context) }, null, 2) : format === "summary" ? formatAutoProjectContextSummary2(context) : formatAutoProjectContextPrompt2(context);
27995
+ autopilotRetrievalBoundary = {
27996
+ projectId: project.id,
27997
+ issuedAt: Date.now(),
27998
+ coveredObservationIds: /* @__PURE__ */ new Set([
27999
+ ...context.workset.continuation?.memories.map((memory) => memory.id) ?? [],
28000
+ ...context.workset.reliableMemory.map((memory) => memory.id),
28001
+ ...context.workset.cautionMemory.map((memory) => memory.id)
28002
+ ]),
28003
+ readOnly: READ_ONLY_TASK_PATTERN.test(task ?? "")
28004
+ };
27661
28005
  return {
27662
28006
  content: [{ type: "text", text }]
27663
28007
  };
@@ -27697,18 +28041,23 @@ _Search mode: ${getLastSearchMode2(project.id)}_`;
27697
28041
  "memorix_context_pack",
27698
28042
  {
27699
28043
  title: "Context Pack",
27700
- 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.",
28044
+ 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.",
27701
28045
  inputSchema: {
27702
28046
  task: z2.string().describe("Current coding task or question"),
27703
28047
  limit: z2.preprocess(
27704
28048
  (value) => typeof value === "string" && value.trim() !== "" ? Number(value) : value,
27705
28049
  z2.number().int().positive().max(100)
27706
- ).optional().describe("Max active memories to inspect before code-ref filtering (default: 20)")
28050
+ ).optional().describe("Max active memories to inspect before code-ref filtering (default: 20)"),
28051
+ purpose: z2.string().optional().describe(
28052
+ "Why this must expand beyond the latest Autopilot brief. Name the missing fact or the user's explicit request."
28053
+ )
27707
28054
  }
27708
28055
  },
27709
- async ({ task, limit }) => {
28056
+ async ({ task, limit, purpose }) => {
27710
28057
  const unresolved = requireResolvedProject("build a context pack for the current project");
27711
28058
  if (unresolved) return unresolved;
28059
+ const boundary = requireExplicitAutopilotExpansion("memorix_context_pack", purpose);
28060
+ if (boundary) return boundary;
27712
28061
  const [
27713
28062
  { CodeGraphStore: CodeGraphStore2 },
27714
28063
  { assembleContextPackForTask: assembleContextPackForTask2, attachTaskWorkset: attachTaskWorkset2, buildContextPackPrompt: buildContextPackPrompt2 },
@@ -28453,7 +28802,7 @@ ${actions.join("\n")}`
28453
28802
  "memorix_detail",
28454
28803
  {
28455
28804
  title: "Memory Details",
28456
- 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.',
28805
+ 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.',
28457
28806
  inputSchema: {
28458
28807
  ids: z2.array(z2.number()).optional().describe("Observation IDs to fetch (legacy, from memorix_search results)"),
28459
28808
  refs: z2.array(
@@ -28462,16 +28811,36 @@ ${actions.join("\n")}`
28462
28811
  projectId: z2.string().optional().describe("Project ID for global-search disambiguation")
28463
28812
  })
28464
28813
  ).optional().describe("Explicit observation refs. Prefer this for global search results."),
28465
- typedRefs: z2.array(z2.string()).optional().describe('Typed memory refs from search results, e.g. "obs:42", "skill:3", "obs:42@org/proj"')
28814
+ typedRefs: z2.array(z2.string()).optional().describe('Typed memory refs from search results, e.g. "obs:42", "skill:3", "obs:42@org/proj"'),
28815
+ purpose: z2.string().optional().describe(
28816
+ "Why this must expand beyond the latest Autopilot brief. Name the missing fact or the user's explicit request."
28817
+ ),
28818
+ force: z2.boolean().optional().describe(
28819
+ "Use only when the user explicitly asks to read a record already represented in the latest Autopilot brief."
28820
+ )
28466
28821
  }
28467
28822
  },
28468
- async ({ ids, refs, typedRefs }) => {
28823
+ async ({ ids, refs, typedRefs, purpose, force }) => {
28469
28824
  const safeIds = coerceNumberArray(ids);
28470
28825
  const safeRefs = coerceObservationRefs(refs);
28471
28826
  const safeTypedRefs = coerceStringArray(typedRefs);
28827
+ const hasCrossProjectRef = safeRefs.some((ref) => ref.projectId && ref.projectId !== project.id) || safeTypedRefs.some((ref) => ref.includes("@") && !ref.endsWith(`@${project.id}`));
28828
+ if (!hasCrossProjectRef) {
28829
+ const boundary = requireExplicitAutopilotExpansion("memorix_detail", purpose);
28830
+ if (boundary) return boundary;
28831
+ const requestedObservationIds = [
28832
+ ...safeIds,
28833
+ ...safeRefs.map((ref) => ref.id),
28834
+ ...safeTypedRefs.flatMap((ref) => {
28835
+ const match = /^obs:(\d+)(?:@.+)?$/i.exec(ref.trim());
28836
+ return match ? [Number(match[1])] : [];
28837
+ })
28838
+ ];
28839
+ const duplicate = blockCoveredAutopilotEvidence("memorix_detail", requestedObservationIds, force);
28840
+ if (duplicate) return duplicate;
28841
+ }
28472
28842
  let result;
28473
28843
  try {
28474
- const hasCrossProjectRef = safeRefs.some((ref) => ref.projectId && ref.projectId !== project.id) || safeTypedRefs.some((ref) => ref.includes("@") && !ref.endsWith(`@${project.id}`));
28475
28844
  const reader = getObservationReader(hasCrossProjectRef ? "global" : "project");
28476
28845
  if (safeTypedRefs.length > 0) {
28477
28846
  result = await compactDetail(safeTypedRefs, { reader });
@@ -30497,6 +30866,7 @@ fi
30497
30866
  if (newCanonicalId === project.id && projectResolved) return false;
30498
30867
  console.error(`[memorix] Switching project: ${project.id} \u2192 ${newCanonicalId}`);
30499
30868
  currentAgentId = void 0;
30869
+ autopilotRetrievalBoundary = null;
30500
30870
  const canonicalProjectDir = newCanonicalId !== newDetected.id ? await getProjectDataDir(newCanonicalId) : newProjectDir;
30501
30871
  projectResolved = true;
30502
30872
  projectResolutionError = null;