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/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
  }
@@ -13691,6 +13779,7 @@ var init_workflow_store = __esm({
13691
13779
  var task_lens_exports = {};
13692
13780
  __export(task_lens_exports, {
13693
13781
  containsTaskKeyword: () => containsTaskKeyword,
13782
+ isContinuationTask: () => isContinuationTask,
13694
13783
  lensPathCandidates: () => lensPathCandidates,
13695
13784
  lensVerificationHints: () => lensVerificationHints,
13696
13785
  rankLensPaths: () => rankLensPaths,
@@ -13756,6 +13845,12 @@ function resolveTaskLens(task) {
13756
13845
  }
13757
13846
  return best.score > 0 ? LENSES[best.id] : LENSES.general;
13758
13847
  }
13848
+ function isContinuationTask(task) {
13849
+ const normalized = (task ?? "").trim().toLowerCase();
13850
+ return normalized.length > 0 && CONTINUATION_KEYWORDS.some(
13851
+ (keyword) => containsTaskKeyword(normalized, keyword)
13852
+ );
13853
+ }
13759
13854
  function pathKindScore(path25, lens) {
13760
13855
  const normalized = normalizePath2(path25).toLowerCase();
13761
13856
  const name = normalized.split("/").pop() ?? normalized;
@@ -13899,7 +13994,7 @@ function shouldShowLensSource(source, lens, task) {
13899
13994
  if (lens.id === "onboarding" || lens.id === "release" || lens.id === "docs") return score >= 40;
13900
13995
  return score > 0;
13901
13996
  }
13902
- var STOP_WORDS, LENSES, KEYWORDS, LENS_PRIORITY;
13997
+ var STOP_WORDS, LENSES, KEYWORDS, CONTINUATION_KEYWORDS, LENS_PRIORITY;
13903
13998
  var init_task_lens = __esm({
13904
13999
  "src/codegraph/task-lens.ts"() {
13905
14000
  "use strict";
@@ -14002,6 +14097,7 @@ var init_task_lens = __esm({
14002
14097
  "fail",
14003
14098
  "failing",
14004
14099
  "fix",
14100
+ "fixing",
14005
14101
  "issue",
14006
14102
  "regression",
14007
14103
  "repro",
@@ -14019,6 +14115,18 @@ var init_task_lens = __esm({
14019
14115
  docs: ["doc", "docs", "readme", "\u6587\u6863", "\u8BF4\u660E"],
14020
14116
  test: ["coverage", "fixture", "smoke", "spec", "test", "tests", "testing", "vitest", "\u6D4B\u8BD5"]
14021
14117
  };
14118
+ CONTINUATION_KEYWORDS = [
14119
+ "continue",
14120
+ "resume",
14121
+ "pick up",
14122
+ "carry on",
14123
+ "previous session",
14124
+ "\u7EE7\u7EED",
14125
+ "\u63A5\u624B",
14126
+ "\u6062\u590D",
14127
+ "\u5EF6\u7EED",
14128
+ "\u4E0A\u6B21\u4F1A\u8BDD"
14129
+ ];
14022
14130
  LENS_PRIORITY = [
14023
14131
  "bugfix",
14024
14132
  "release",
@@ -15229,6 +15337,7 @@ __export(session_exports, {
15229
15337
  endSession: () => endSession,
15230
15338
  getActiveSession: () => getActiveSession,
15231
15339
  getSessionContext: () => getSessionContext,
15340
+ getSessionResumeBrief: () => getSessionResumeBrief,
15232
15341
  listSessions: () => listSessions,
15233
15342
  scoreObservationForSessionContext: () => scoreObservationForSessionContext,
15234
15343
  startSession: () => startSession
@@ -15296,6 +15405,62 @@ function isSystemSelfObservation(obs) {
15296
15405
  const text = stringifyObservation(obs, false);
15297
15406
  return SYSTEM_SELF_PATTERNS.some((pattern) => pattern.test(text));
15298
15407
  }
15408
+ function readerForAlias(reader, aliases, observation) {
15409
+ if (!reader) return void 0;
15410
+ return reader.projectId && aliases.has(observation.projectId) ? { ...reader, projectId: observation.projectId } : reader;
15411
+ }
15412
+ function readableAliasObservations(observations2, aliases, reader) {
15413
+ return reader ? observations2.filter((observation) => canReadObservation(
15414
+ observation,
15415
+ readerForAlias(reader, aliases, observation)
15416
+ )) : observations2;
15417
+ }
15418
+ function isUsefulSessionSummary(summary) {
15419
+ return Boolean(summary) && !NOISE_PATTERNS2.some((pattern) => pattern.test(summary)) && !SYSTEM_SELF_PATTERNS.some((pattern) => pattern.test(summary));
15420
+ }
15421
+ function continuationTaskTokens(task) {
15422
+ return [...new Set(
15423
+ (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))
15424
+ )].slice(0, 8);
15425
+ }
15426
+ function continuationScore(observation, projectTokens, task) {
15427
+ let score = scoreObservationForSessionContext(observation, projectTokens);
15428
+ if (observation.type === "session-request" || observation.type === "what-changed") score += 1;
15429
+ const text = stringifyObservation(observation);
15430
+ const matches = continuationTaskTokens(task).filter((token) => text.includes(token)).length;
15431
+ score += Math.min(matches, 2) * 2;
15432
+ return score;
15433
+ }
15434
+ async function getSessionResumeBrief(projectId, task, reader) {
15435
+ const aliasSet = await resolveProjectIds(projectId);
15436
+ const [sessions, allObs] = await Promise.all([
15437
+ loadAliasSessions(aliasSet),
15438
+ loadAliasActiveObservations(aliasSet)
15439
+ ]);
15440
+ const readableObs = readableAliasObservations(allObs, aliasSet, reader);
15441
+ 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));
15442
+ const projectTokens = tokenizeProjectId(projectId);
15443
+ const memories = readableObs.filter((observation) => RESUME_TYPES.has(observation.type)).filter((observation) => classifyLayer(observation) === "L2").filter((observation) => !isNoiseObservation(observation) && !isSystemSelfObservation(observation)).map((observation) => ({
15444
+ observation,
15445
+ score: continuationScore(observation, projectTokens, task)
15446
+ })).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 }) => ({
15447
+ id: observation.id,
15448
+ title: sanitizeCredentials(observation.title),
15449
+ type: observation.type,
15450
+ ...observation.facts?.[0] ? { detail: sanitizeCredentials(observation.facts[0]) } : observation.narrative ? { detail: sanitizeCredentials(observation.narrative) } : {}
15451
+ }));
15452
+ return {
15453
+ ...previous?.summary ? {
15454
+ previousSession: {
15455
+ id: previous.id,
15456
+ ...previous.agent ? { agent: previous.agent } : {},
15457
+ ...previous.endedAt ? { endedAt: previous.endedAt } : {},
15458
+ summary: sanitizeCredentials(previous.summary)
15459
+ }
15460
+ } : {},
15461
+ memories
15462
+ };
15463
+ }
15299
15464
  function scoreObservationForSessionContext(obs, projectTokens, now3 = Date.now()) {
15300
15465
  let score = TYPE_WEIGHTS2[obs.type] ?? 1;
15301
15466
  const text = stringifyObservation(obs);
@@ -15366,10 +15531,7 @@ async function getSessionContext(projectDir2, projectId, limit = 3, reader) {
15366
15531
  loadAliasSessions(aliasSet),
15367
15532
  loadAliasActiveObservations(aliasSet)
15368
15533
  ]);
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;
15534
+ const readableObs = readableAliasObservations(allObs, aliasSet, reader);
15373
15535
  const isNoisySummary = (summary) => {
15374
15536
  if (!summary) return false;
15375
15537
  return NOISE_PATTERNS2.some((p) => p.test(summary)) || SYSTEM_SELF_PATTERNS.some((p) => p.test(summary));
@@ -15520,7 +15682,7 @@ async function getActiveSession(projectDir2, projectId) {
15520
15682
  const sessions = await loadAliasActiveSessions(aliasSet);
15521
15683
  return sessions.sort((a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime())[0] ?? null;
15522
15684
  }
15523
- var PRIORITY_TYPES, TYPE_EMOJI, TYPE_WEIGHTS2, NOISE_PATTERNS2, COMMAND_TRACE_PATTERNS, SYSTEM_SELF_PATTERNS;
15685
+ var PRIORITY_TYPES, RESUME_TYPES, TYPE_EMOJI, TYPE_WEIGHTS2, NOISE_PATTERNS2, COMMAND_TRACE_PATTERNS, SYSTEM_SELF_PATTERNS;
15524
15686
  var init_session = __esm({
15525
15687
  "src/memory/session.ts"() {
15526
15688
  "use strict";
@@ -15534,6 +15696,17 @@ var init_session = __esm({
15534
15696
  init_secret_filter();
15535
15697
  init_visibility();
15536
15698
  PRIORITY_TYPES = /* @__PURE__ */ new Set(["gotcha", "decision", "problem-solution", "trade-off", "discovery"]);
15699
+ RESUME_TYPES = /* @__PURE__ */ new Set([
15700
+ "gotcha",
15701
+ "decision",
15702
+ "problem-solution",
15703
+ "trade-off",
15704
+ "discovery",
15705
+ "how-it-works",
15706
+ "what-changed",
15707
+ "reasoning",
15708
+ "session-request"
15709
+ ]);
15537
15710
  TYPE_EMOJI = {
15538
15711
  "gotcha": "[DISCOVERY]",
15539
15712
  "decision": "[WHY]",
@@ -15694,6 +15867,7 @@ function freshnessForMemory(status) {
15694
15867
  return "unknown";
15695
15868
  }
15696
15869
  function receiptOmissionKind(raw) {
15870
+ if (raw.includes("continuation")) return "continuation";
15697
15871
  if (raw.includes("task")) return "task";
15698
15872
  if (raw.includes("fact")) return "current-fact";
15699
15873
  if (raw.includes("state")) return "code-state";
@@ -15743,6 +15917,48 @@ function renderTaskWorksetPrompt(input) {
15743
15917
  trust: "source-backed"
15744
15918
  });
15745
15919
  appendLine(lines, "Task lens: " + input.lens, maxTokens, omitted, "lens");
15920
+ const hasContinuation = Boolean(
15921
+ input.continuation?.previousSession || (input.continuation?.memories.length ?? 0) > 0
15922
+ );
15923
+ if (hasContinuation && input.continuation) {
15924
+ appendLine(lines, "", maxTokens, omitted, "continuation-heading");
15925
+ appendLine(lines, "Resume from prior work", maxTokens, omitted, "continuation-heading");
15926
+ if (input.continuation.previousSession) {
15927
+ const session = input.continuation.previousSession;
15928
+ const source = [session.agent, session.endedAt ? session.endedAt.slice(0, 10) : void 0].filter(Boolean).join(", ");
15929
+ appendLine(
15930
+ lines,
15931
+ "- Previous session" + (source ? ` (${source})` : "") + ": " + short(session.summary, 44),
15932
+ maxTokens,
15933
+ omitted,
15934
+ "continuation-session",
15935
+ selected,
15936
+ {
15937
+ kind: "continuation",
15938
+ id: "session:" + session.id,
15939
+ reason: "latest meaningful project session summary",
15940
+ trust: "historical"
15941
+ }
15942
+ );
15943
+ }
15944
+ for (const memory of input.continuation.memories.slice(0, 3)) {
15945
+ const detail = memory.detail ? ": " + short(memory.detail, CONTINUATION_DETAIL_TOKEN_BUDGET) : "";
15946
+ appendLine(
15947
+ lines,
15948
+ "- #" + memory.id + " " + memory.type + ": " + short(memory.title, 18) + detail,
15949
+ maxTokens,
15950
+ omitted,
15951
+ "continuation-memory",
15952
+ selected,
15953
+ {
15954
+ kind: "continuation",
15955
+ id: "memory:" + memory.id,
15956
+ reason: "durable prior-work memory",
15957
+ trust: "historical"
15958
+ }
15959
+ );
15960
+ }
15961
+ }
15746
15962
  if (input.cautions.length > 0 || input.cautionMemory.length > 0) {
15747
15963
  appendLine(lines, "", maxTokens, omitted, "caution-heading");
15748
15964
  appendLine(lines, "Cautions", maxTokens, omitted, "caution-heading");
@@ -16011,11 +16227,25 @@ async function buildTaskWorkset(input) {
16011
16227
  ...input.verificationHints
16012
16228
  ]).slice(0, 4);
16013
16229
  const normalizedCautions = unique(cautions.map((caution) => caution.kind)).map((kind) => cautions.find((caution) => caution.kind === kind)).slice(0, 6);
16230
+ const continuation = input.continuation && (input.continuation.previousSession || input.continuation.memories.length > 0) ? {
16231
+ ...input.continuation.previousSession ? {
16232
+ previousSession: {
16233
+ ...input.continuation.previousSession,
16234
+ summary: short(input.continuation.previousSession.summary, 52)
16235
+ }
16236
+ } : {},
16237
+ memories: input.continuation.memories.slice(0, 3).map((memory) => ({
16238
+ ...memory,
16239
+ title: short(memory.title, 20),
16240
+ ...memory.detail ? { detail: short(memory.detail, CONTINUATION_DETAIL_TOKEN_BUDGET) } : {}
16241
+ }))
16242
+ } : void 0;
16014
16243
  const base = {
16015
16244
  version: "1.2",
16016
16245
  task,
16017
16246
  lens: input.lens,
16018
16247
  currentFacts: input.currentFacts?.map((fact) => fact.startsWith("Historical note:") ? short(fact, 48) : short(fact, 28)).slice(0, 4) ?? [],
16248
+ ...continuation ? { continuation } : {},
16019
16249
  ...input.codeState ? { codeState: short(input.codeState, 28) } : {},
16020
16250
  startHere: unique(input.startHere).slice(0, 5),
16021
16251
  ...input.semanticCode ? { semanticCode: input.semanticCode } : {},
@@ -16040,7 +16270,7 @@ async function buildTaskWorkset(input) {
16040
16270
  budget: { maxTokens }
16041
16271
  });
16042
16272
  const receipt = {
16043
- version: "1.2.2",
16273
+ version: "1.2.4",
16044
16274
  target: input.deliveryTarget ?? "project-context",
16045
16275
  elapsedMs: Math.max(0, Date.now() - startedAt),
16046
16276
  budget: {
@@ -16062,6 +16292,7 @@ async function buildTaskWorkset(input) {
16062
16292
  prompt: rendered.prompt
16063
16293
  };
16064
16294
  }
16295
+ var CONTINUATION_DETAIL_TOKEN_BUDGET;
16065
16296
  var init_workset = __esm({
16066
16297
  "src/knowledge/workset.ts"() {
16067
16298
  "use strict";
@@ -16074,6 +16305,7 @@ var init_workset = __esm({
16074
16305
  init_workspace();
16075
16306
  init_workflow_store();
16076
16307
  init_workflows();
16308
+ CONTINUATION_DETAIL_TOKEN_BUDGET = 20;
16077
16309
  }
16078
16310
  });
16079
16311
 
@@ -16866,6 +17098,7 @@ async function buildAutoProjectContext(input) {
16866
17098
  const now3 = input.now ?? /* @__PURE__ */ new Date();
16867
17099
  const task = input.task?.trim();
16868
17100
  const lens = resolveTaskLens(task);
17101
+ const continuationRequested = input.continuation === "always" || input.continuation !== "never" && isContinuationTask(task);
16869
17102
  const codegraphConfig = getResolvedConfig({ projectRoot: input.project.rootPath }).codegraph;
16870
17103
  const exclude = input.exclude ?? codegraphConfig.excludePatterns;
16871
17104
  const maxFileBytes = input.maxFileBytes ?? codegraphConfig.maxFileBytes;
@@ -17002,6 +17235,11 @@ async function buildAutoProjectContext(input) {
17002
17235
  if (externalCaution) {
17003
17236
  runtimeCautions.push({ kind: "external-codegraph-fallback", message: externalCaution });
17004
17237
  }
17238
+ let continuation;
17239
+ if (continuationRequested) {
17240
+ await initSessionStore(input.dataDir);
17241
+ continuation = await getSessionResumeBrief(input.project.id, task, input.reader);
17242
+ }
17005
17243
  const workset = await buildTaskWorkset({
17006
17244
  projectId: input.project.id,
17007
17245
  dataDir: input.dataDir,
@@ -17011,6 +17249,7 @@ async function buildAutoProjectContext(input) {
17011
17249
  ...externalOutline ? { semanticCode: externalOutline } : {},
17012
17250
  providerQuality: providerQuality2,
17013
17251
  currentFacts: worksetFactLines(currentFacts),
17252
+ ...continuation ? { continuation } : {},
17014
17253
  codeState: codeStateLine(overview),
17015
17254
  reliableMemory: sourceSets.reliableSources.slice(0, lens.sourceLimit).map((source) => ({
17016
17255
  id: source.observationId,
@@ -17055,12 +17294,19 @@ async function buildAutoProjectContext(input) {
17055
17294
  explain,
17056
17295
  refresh,
17057
17296
  providerQuality: providerQuality2,
17297
+ ...continuationRequested && workset.continuation ? { continuation: workset.continuation } : {},
17058
17298
  workset
17059
17299
  };
17060
17300
  }
17061
17301
  function formatLanguages(overview) {
17062
17302
  return overview.code.languages.length > 0 ? overview.code.languages.map((item) => `${item.language} ${item.files}`).join(", ") : "none indexed yet";
17063
17303
  }
17304
+ function compactContinuationText(text, budget) {
17305
+ return truncateToTokenBudget(
17306
+ sanitizeCredentials(text).replace(/\s+/g, " ").trim(),
17307
+ budget
17308
+ );
17309
+ }
17064
17310
  function codeStateLine(overview) {
17065
17311
  const snapshot = overview.code.latestSnapshot;
17066
17312
  if (!snapshot) return "- Code state: no completed snapshot yet";
@@ -17200,6 +17446,23 @@ function formatAutoProjectContextSummary(context) {
17200
17446
  "Reliable memory",
17201
17447
  reliableSources.length > 0 ? `- ${reliableSources.length} current code-bound memory link(s)` : "- none yet"
17202
17448
  );
17449
+ const continuation = context.workset.continuation;
17450
+ if (continuation?.previousSession || continuation?.memories.length) {
17451
+ lines.push("", "Resume from prior work");
17452
+ if (continuation.previousSession) {
17453
+ const session = continuation.previousSession;
17454
+ const source = [session.agent, session.endedAt ? session.endedAt.slice(0, 10) : void 0].filter(Boolean).join(", ");
17455
+ lines.push(
17456
+ "- Previous session" + (source ? ` (${source})` : "") + ": " + compactContinuationText(session.summary, 44)
17457
+ );
17458
+ }
17459
+ for (const memory of continuation.memories.slice(0, 3)) {
17460
+ const detail = memory.detail ? ": " + compactContinuationText(memory.detail, 20) : "";
17461
+ lines.push(
17462
+ "- #" + memory.id + " " + memory.type + ": " + compactContinuationText(memory.title, 18) + detail
17463
+ );
17464
+ }
17465
+ }
17203
17466
  return lines.join("\n");
17204
17467
  }
17205
17468
  function formatAutoProjectContextPrompt(context) {
@@ -17270,8 +17533,10 @@ var init_auto_context = __esm({
17270
17533
  "src/codegraph/auto-context.ts"() {
17271
17534
  "use strict";
17272
17535
  init_esm_shims();
17536
+ init_token_budget();
17273
17537
  init_resolved_config();
17274
17538
  init_workset();
17539
+ init_secret_filter();
17275
17540
  init_binder();
17276
17541
  init_current_facts();
17277
17542
  init_lite_provider();
@@ -17279,6 +17544,8 @@ var init_auto_context = __esm({
17279
17544
  init_project_context();
17280
17545
  init_store();
17281
17546
  init_admission();
17547
+ init_session();
17548
+ init_session_store();
17282
17549
  init_task_lens();
17283
17550
  DEFAULT_MAX_AGE_MS = 10 * 60 * 1e3;
17284
17551
  }
@@ -21473,7 +21740,8 @@ var init_official_skills = __esm({
21473
21740
  "",
21474
21741
  "| Situation | Prefer | CLI fallback |",
21475
21742
  "|---|---|---|",
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>"` |',
21743
+ '| Starting a coding task and needing the best task-lensed project map | `memorix_project_context` with the user\'s task | `memorix context "<task>" --json` |',
21744
+ '| Continuing prior project work | `memorix_project_context` with the user\'s task | `memorix resume "<task>" --json` |',
21477
21745
  '| Need structured refs/freshness for code-bound memories | `memorix_context_pack` | `memorix codegraph context-pack --task "<topic>"` |',
21478
21746
  '| Explicit memory graph question | `memorix_graph_context` | `memorix memory graph-context --query "<topic>"` |',
21479
21747
  '| Specific past decision, bug, file, or change | `memorix_search` | `memorix memory search --query "<topic>"` |',
@@ -21486,9 +21754,10 @@ var init_official_skills = __esm({
21486
21754
  "",
21487
21755
  "- 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
21756
  "- 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.",
21757
+ '- 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.',
21758
+ "- 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.",
21759
+ "- 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.",
21760
+ "- 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
21761
  "- 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
21762
  "- Skip memory lookup for greetings, tiny one-off edits, or questions fully answered by the current file.",
21494
21763
  "- If a fresh project has no memories, proceed normally and do not repeat the same empty search in the same turn.",
@@ -22709,17 +22978,18 @@ alwaysApply: true
22709
22978
  "",
22710
22979
  "## Start with Memory Autopilot",
22711
22980
  "",
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.`,
22981
+ `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
22982
  "",
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.',
22983
+ '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
22984
  "",
22716
- "Use `memorix_context_pack` when you need structured refs and freshness for code-bound memories.",
22985
+ "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.",
22986
+ "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
22987
  "",
22718
22988
  "## When to search memory",
22719
22989
  "",
22720
22990
  "Use `memorix_graph_context` for explicit memory graph questions or broad graph overview after the autopilot brief is not enough.",
22721
22991
  "",
22722
- `Use \`memorix_search\` when prior ${contextNoun} context would help \u2014 for example:`,
22992
+ `Use \`memorix_search\` when prior ${contextNoun} context would help and the Autopilot brief did not already answer the question \u2014 for example:`,
22723
22993
  "- The user asks about a past decision, bug, or change",
22724
22994
  "- You need to understand why something was designed a certain way",
22725
22995
  "- You're continuing work that started in a previous session",
@@ -26734,6 +27004,8 @@ var BOOTSTRAP_SAFE_TOOL_NAMES = /* @__PURE__ */ new Set([
26734
27004
  "memorix_graph_context",
26735
27005
  "memorix_context_pack"
26736
27006
  ]);
27007
+ var AUTOPILOT_RETRIEVAL_BOUNDARY_TTL_MS = 2 * 60 * 1e3;
27008
+ var READ_ONLY_TASK_PATTERN = /\b(?:do not|don't|never)\s+(?:modify|edit|change|write)\b|\bread[- ]only\b|(?:不要|不准|勿|禁止).{0,6}(?:修改|编辑|写入)|只读/i;
26737
27009
  function shouldAwaitProjectRuntime(toolName) {
26738
27010
  return !BOOTSTRAP_SAFE_TOOL_NAMES.has(toolName);
26739
27011
  }
@@ -27004,7 +27276,7 @@ The path should point to a directory containing a .git folder.`
27004
27276
  };
27005
27277
  const server = existingServer ?? new McpServer({
27006
27278
  name: "memorix",
27007
- version: true ? "1.2.3" : "1.0.1"
27279
+ version: true ? "1.2.4" : "1.0.1"
27008
27280
  });
27009
27281
  const originalRegisterTool = server.registerTool.bind(server);
27010
27282
  server.registerTool = ((name, ...args) => {
@@ -27039,11 +27311,52 @@ The path should point to a directory containing a .git folder.`
27039
27311
  isTeamMember
27040
27312
  };
27041
27313
  };
27314
+ let autopilotRetrievalBoundary = null;
27315
+ const getActiveAutopilotRetrievalBoundary = () => {
27316
+ const boundary = autopilotRetrievalBoundary;
27317
+ if (!boundary) return null;
27318
+ if (boundary.projectId === project.id && Date.now() - boundary.issuedAt <= AUTOPILOT_RETRIEVAL_BOUNDARY_TTL_MS) {
27319
+ return boundary;
27320
+ }
27321
+ autopilotRetrievalBoundary = null;
27322
+ return null;
27323
+ };
27324
+ const requireExplicitAutopilotExpansion = (toolName, purpose) => {
27325
+ if (!getActiveAutopilotRetrievalBoundary() || purpose?.trim()) return null;
27326
+ return {
27327
+ content: [{
27328
+ type: "text",
27329
+ 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."
27330
+ }]
27331
+ };
27332
+ };
27333
+ const blockCoveredAutopilotEvidence = (toolName, observationIds, force) => {
27334
+ const boundary = getActiveAutopilotRetrievalBoundary();
27335
+ if (!boundary || force || observationIds.length === 0 || !observationIds.every((id) => boundary.coveredObservationIds.has(id))) {
27336
+ return null;
27337
+ }
27338
+ return {
27339
+ content: [{
27340
+ type: "text",
27341
+ 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."
27342
+ }]
27343
+ };
27344
+ };
27345
+ const blockReadOnlyAutopilotWrite = (overrideReadOnly) => {
27346
+ const boundary = getActiveAutopilotRetrievalBoundary();
27347
+ if (!boundary?.readOnly || overrideReadOnly) return null;
27348
+ return {
27349
+ content: [{
27350
+ type: "text",
27351
+ 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`."
27352
+ }]
27353
+ };
27354
+ };
27042
27355
  server.registerTool(
27043
27356
  "memorix_store",
27044
27357
  {
27045
27358
  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.",
27359
+ 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
27360
  inputSchema: {
27048
27361
  entityName: z2.string().describe('The entity this observation belongs to (e.g., "auth-module", "port-config")'),
27049
27362
  type: z2.enum(OBSERVATION_TYPES).describe("Observation type for classification"),
@@ -27064,12 +27377,17 @@ The path should point to a directory containing a .git folder.`
27064
27377
  relatedEntities: z2.array(z2.string()).optional().describe("Other entity names this memory cross-references"),
27065
27378
  visibility: z2.enum(["personal", "project", "team"]).optional().describe(
27066
27379
  "Retrieval scope. Project is the normal shared default; personal/team require memorix_session_start with joinTeam=true."
27380
+ ),
27381
+ overrideReadOnly: z2.boolean().optional().describe(
27382
+ "Use only when the user explicitly asks to save memory during a read-only or no-modification task."
27067
27383
  )
27068
27384
  }
27069
27385
  },
27070
- async ({ entityName: rawEntityName, type: rawType, title: rawTitle, narrative, facts, filesModified, concepts, topicKey, progress, relatedCommits, relatedEntities, visibility }) => {
27386
+ async ({ entityName: rawEntityName, type: rawType, title: rawTitle, narrative, facts, filesModified, concepts, topicKey, progress, relatedCommits, relatedEntities, visibility, overrideReadOnly }) => {
27071
27387
  const unresolved = requireResolvedProject("store memory in the current project");
27072
27388
  if (unresolved) return unresolved;
27389
+ const readOnlyBoundary = blockReadOnlyAutopilotWrite(overrideReadOnly);
27390
+ if (readOnlyBoundary) return readOnlyBoundary;
27073
27391
  const requestedVisibility = visibility ?? "project";
27074
27392
  const reader = getObservationReader();
27075
27393
  if (requestedVisibility !== "project" && !currentAgentId) {
@@ -27568,7 +27886,7 @@ Use this as the \`topicKey\` parameter in \`memorix_store\` to enable upsert beh
27568
27886
  "memorix_search",
27569
27887
  {
27570
27888
  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.",
27889
+ 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
27890
  inputSchema: {
27573
27891
  query: z2.string().describe("Search query (natural language or keywords)"),
27574
27892
  limit: z2.number().optional().describe("Max results (default: 20)"),
@@ -27584,14 +27902,24 @@ Use this as the \`topicKey\` parameter in \`memorix_store\` to enable upsert beh
27584
27902
  ),
27585
27903
  source: z2.enum(["agent", "git", "manual"]).optional().describe(
27586
27904
  'Filter by memory source. "git" returns only commit-derived ground truth memories. Omit for all sources.'
27905
+ ),
27906
+ purpose: z2.string().optional().describe(
27907
+ "Why this must expand beyond the latest Autopilot brief. Name the missing fact or the user's explicit request."
27908
+ ),
27909
+ force: z2.boolean().optional().describe(
27910
+ "Use only when the user explicitly asks to read a record already represented in the latest Autopilot brief."
27587
27911
  )
27588
27912
  }
27589
27913
  },
27590
- async ({ query, limit, type, maxTokens, scope, since, until, status, source }) => {
27914
+ async ({ query, limit, type, maxTokens, scope, since, until, status, source, purpose, force }) => {
27591
27915
  if (scope !== "global") {
27592
27916
  const unresolved = requireResolvedProject("search the current project");
27593
27917
  if (unresolved) return unresolved;
27594
27918
  }
27919
+ if (scope !== "global") {
27920
+ const boundary = requireExplicitAutopilotExpansion("memorix_search", purpose);
27921
+ if (boundary) return boundary;
27922
+ }
27595
27923
  return withFreshIndex(async () => {
27596
27924
  const safeLimit = limit != null ? coerceNumber(limit, 20) : void 0;
27597
27925
  const safeMaxTokens = maxTokens != null ? coerceNumber(maxTokens, 0) : void 0;
@@ -27630,6 +27958,11 @@ Use this as the \`topicKey\` parameter in \`memorix_store\` to enable upsert beh
27630
27958
  }
27631
27959
  throw error;
27632
27960
  }
27961
+ const activeBoundary = getActiveAutopilotRetrievalBoundary();
27962
+ if (scope !== "global" && activeBoundary && !force && result.entries.length > 0 && result.entries.every((entry) => activeBoundary.coveredObservationIds.has(entry.id))) {
27963
+ const duplicate = blockCoveredAutopilotEvidence("memorix_search", result.entries.map((entry) => entry.id), force);
27964
+ if (duplicate) return duplicate;
27965
+ }
27633
27966
  let text = result.formatted;
27634
27967
  try {
27635
27968
  const { getLastSearchMode: getLastSearchMode2 } = await Promise.resolve().then(() => (init_orama_store(), orama_store_exports));
@@ -27731,6 +28064,7 @@ _Search mode: ${getLastSearchMode2(project.id)}_`;
27731
28064
  observations: observations2,
27732
28065
  task,
27733
28066
  refresh: refresh ?? "auto",
28067
+ reader: getObservationReader(),
27734
28068
  enqueueRefresh: () => {
27735
28069
  enqueueCodegraphRefresh2({
27736
28070
  dataDir: projectDir2,
@@ -27742,6 +28076,16 @@ _Search mode: ${getLastSearchMode2(project.id)}_`;
27742
28076
  }
27743
28077
  });
27744
28078
  const text = format === "json" ? JSON.stringify({ ...context, brief: buildAutoProjectBrief2(context) }, null, 2) : format === "summary" ? formatAutoProjectContextSummary2(context) : formatAutoProjectContextPrompt2(context);
28079
+ autopilotRetrievalBoundary = {
28080
+ projectId: project.id,
28081
+ issuedAt: Date.now(),
28082
+ coveredObservationIds: /* @__PURE__ */ new Set([
28083
+ ...context.workset.continuation?.memories.map((memory) => memory.id) ?? [],
28084
+ ...context.workset.reliableMemory.map((memory) => memory.id),
28085
+ ...context.workset.cautionMemory.map((memory) => memory.id)
28086
+ ]),
28087
+ readOnly: READ_ONLY_TASK_PATTERN.test(task ?? "")
28088
+ };
27745
28089
  return {
27746
28090
  content: [{ type: "text", text }]
27747
28091
  };
@@ -27781,18 +28125,23 @@ _Search mode: ${getLastSearchMode2(project.id)}_`;
27781
28125
  "memorix_context_pack",
27782
28126
  {
27783
28127
  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.",
28128
+ 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
28129
  inputSchema: {
27786
28130
  task: z2.string().describe("Current coding task or question"),
27787
28131
  limit: z2.preprocess(
27788
28132
  (value) => typeof value === "string" && value.trim() !== "" ? Number(value) : value,
27789
28133
  z2.number().int().positive().max(100)
27790
- ).optional().describe("Max active memories to inspect before code-ref filtering (default: 20)")
28134
+ ).optional().describe("Max active memories to inspect before code-ref filtering (default: 20)"),
28135
+ purpose: z2.string().optional().describe(
28136
+ "Why this must expand beyond the latest Autopilot brief. Name the missing fact or the user's explicit request."
28137
+ )
27791
28138
  }
27792
28139
  },
27793
- async ({ task, limit }) => {
28140
+ async ({ task, limit, purpose }) => {
27794
28141
  const unresolved = requireResolvedProject("build a context pack for the current project");
27795
28142
  if (unresolved) return unresolved;
28143
+ const boundary = requireExplicitAutopilotExpansion("memorix_context_pack", purpose);
28144
+ if (boundary) return boundary;
27796
28145
  const [
27797
28146
  { CodeGraphStore: CodeGraphStore2 },
27798
28147
  { assembleContextPackForTask: assembleContextPackForTask2, attachTaskWorkset: attachTaskWorkset2, buildContextPackPrompt: buildContextPackPrompt2 },
@@ -28537,7 +28886,7 @@ ${actions.join("\n")}`
28537
28886
  "memorix_detail",
28538
28887
  {
28539
28888
  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.',
28889
+ 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
28890
  inputSchema: {
28542
28891
  ids: z2.array(z2.number()).optional().describe("Observation IDs to fetch (legacy, from memorix_search results)"),
28543
28892
  refs: z2.array(
@@ -28546,16 +28895,36 @@ ${actions.join("\n")}`
28546
28895
  projectId: z2.string().optional().describe("Project ID for global-search disambiguation")
28547
28896
  })
28548
28897
  ).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"')
28898
+ typedRefs: z2.array(z2.string()).optional().describe('Typed memory refs from search results, e.g. "obs:42", "skill:3", "obs:42@org/proj"'),
28899
+ purpose: z2.string().optional().describe(
28900
+ "Why this must expand beyond the latest Autopilot brief. Name the missing fact or the user's explicit request."
28901
+ ),
28902
+ force: z2.boolean().optional().describe(
28903
+ "Use only when the user explicitly asks to read a record already represented in the latest Autopilot brief."
28904
+ )
28550
28905
  }
28551
28906
  },
28552
- async ({ ids, refs, typedRefs }) => {
28907
+ async ({ ids, refs, typedRefs, purpose, force }) => {
28553
28908
  const safeIds = coerceNumberArray(ids);
28554
28909
  const safeRefs = coerceObservationRefs(refs);
28555
28910
  const safeTypedRefs = coerceStringArray(typedRefs);
28911
+ const hasCrossProjectRef = safeRefs.some((ref) => ref.projectId && ref.projectId !== project.id) || safeTypedRefs.some((ref) => ref.includes("@") && !ref.endsWith(`@${project.id}`));
28912
+ if (!hasCrossProjectRef) {
28913
+ const boundary = requireExplicitAutopilotExpansion("memorix_detail", purpose);
28914
+ if (boundary) return boundary;
28915
+ const requestedObservationIds = [
28916
+ ...safeIds,
28917
+ ...safeRefs.map((ref) => ref.id),
28918
+ ...safeTypedRefs.flatMap((ref) => {
28919
+ const match = /^obs:(\d+)(?:@.+)?$/i.exec(ref.trim());
28920
+ return match ? [Number(match[1])] : [];
28921
+ })
28922
+ ];
28923
+ const duplicate = blockCoveredAutopilotEvidence("memorix_detail", requestedObservationIds, force);
28924
+ if (duplicate) return duplicate;
28925
+ }
28556
28926
  let result;
28557
28927
  try {
28558
- const hasCrossProjectRef = safeRefs.some((ref) => ref.projectId && ref.projectId !== project.id) || safeTypedRefs.some((ref) => ref.includes("@") && !ref.endsWith(`@${project.id}`));
28559
28928
  const reader = getObservationReader(hasCrossProjectRef ? "global" : "project");
28560
28929
  if (safeTypedRefs.length > 0) {
28561
28930
  result = await compactDetail(safeTypedRefs, { reader });
@@ -30581,6 +30950,7 @@ fi
30581
30950
  if (newCanonicalId === project.id && projectResolved) return false;
30582
30951
  console.error(`[memorix] Switching project: ${project.id} \u2192 ${newCanonicalId}`);
30583
30952
  currentAgentId = void 0;
30953
+ autopilotRetrievalBoundary = null;
30584
30954
  const canonicalProjectDir = newCanonicalId !== newDetected.id ? await getProjectDataDir(newCanonicalId) : newProjectDir;
30585
30955
  projectResolved = true;
30586
30956
  projectResolutionError = null;