memorix 1.2.2 → 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 (163) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +3 -3
  3. package/README.zh-CN.md +3 -3
  4. package/TEAM.md +86 -86
  5. package/dist/cli/index.js +5199 -4726
  6. package/dist/cli/index.js.map +1 -1
  7. package/dist/index.js +428 -49
  8. package/dist/index.js.map +1 -1
  9. package/dist/maintenance-runner.js +97 -18
  10. package/dist/maintenance-runner.js.map +1 -1
  11. package/dist/memcode-runtime/CHANGELOG.md +27 -0
  12. package/dist/sdk.js +428 -49
  13. package/dist/sdk.js.map +1 -1
  14. package/docs/1.2.4-PERSISTENT-MEMORY-DELIVERY.md +86 -0
  15. package/docs/AGENT_OPERATOR_PLAYBOOK.md +13 -1
  16. package/docs/API_REFERENCE.md +13 -3
  17. package/docs/DESIGN_DECISIONS.md +357 -357
  18. package/docs/dev-log/progress.txt +60 -9
  19. package/package.json +1 -1
  20. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  21. package/src/audit/index.ts +156 -156
  22. package/src/cli/capability-map.ts +1 -1
  23. package/src/cli/command-guide.ts +4 -1
  24. package/src/cli/commands/agent-integrations.ts +5 -1
  25. package/src/cli/commands/audit-list.ts +89 -89
  26. package/src/cli/commands/background.ts +659 -659
  27. package/src/cli/commands/codegraph.ts +1 -1
  28. package/src/cli/commands/context.ts +9 -1
  29. package/src/cli/commands/formation.ts +48 -48
  30. package/src/cli/commands/git-hook-install.ts +111 -111
  31. package/src/cli/commands/handoff.ts +54 -54
  32. package/src/cli/commands/hooks-status.ts +63 -63
  33. package/src/cli/commands/ingest-commit.ts +153 -153
  34. package/src/cli/commands/ingest-image.ts +66 -66
  35. package/src/cli/commands/ingest-log.ts +180 -180
  36. package/src/cli/commands/ingest.ts +44 -44
  37. package/src/cli/commands/integrate-shared.ts +15 -15
  38. package/src/cli/commands/lock.ts +82 -82
  39. package/src/cli/commands/message.ts +104 -104
  40. package/src/cli/commands/poll.ts +58 -58
  41. package/src/cli/commands/purge-all-memory.ts +85 -85
  42. package/src/cli/commands/purge-project-memory.ts +83 -83
  43. package/src/cli/commands/reasoning.ts +118 -118
  44. package/src/cli/commands/resume.ts +31 -0
  45. package/src/cli/commands/serve-shared.ts +118 -118
  46. package/src/cli/commands/session.ts +15 -7
  47. package/src/cli/commands/skills.ts +114 -114
  48. package/src/cli/commands/task.ts +167 -167
  49. package/src/cli/commands/transfer.ts +47 -47
  50. package/src/cli/commands/uninstall-project-artifacts.ts +85 -85
  51. package/src/cli/index.ts +3 -1
  52. package/src/cli/tui/ChatView.tsx +234 -234
  53. package/src/cli/tui/CommandBar.tsx +312 -312
  54. package/src/cli/tui/ContextRail.tsx +118 -118
  55. package/src/cli/tui/HeaderBar.tsx +72 -72
  56. package/src/cli/tui/LogoBanner.tsx +51 -51
  57. package/src/cli/tui/Sidebar.tsx +179 -179
  58. package/src/cli/tui/index.ts +41 -41
  59. package/src/cli/tui/markdown-render.tsx +371 -371
  60. package/src/cli/tui/session-service.ts +3 -2
  61. package/src/cli/tui/use-mouse.ts +157 -157
  62. package/src/cli/tui/useNavigation.ts +56 -56
  63. package/src/cli/update-checker.ts +211 -211
  64. package/src/cli/version.ts +7 -7
  65. package/src/cli/workbench.ts +1 -1
  66. package/src/codegraph/auto-context.ts +54 -1
  67. package/src/codegraph/task-lens.ts +29 -0
  68. package/src/compact/token-budget.ts +89 -74
  69. package/src/config/toml-loader.ts +9 -5
  70. package/src/dashboard/project-classification.ts +64 -64
  71. package/src/embedding/fastembed-provider.ts +142 -142
  72. package/src/embedding/transformers-provider.ts +111 -111
  73. package/src/git/extractor.ts +209 -209
  74. package/src/git/hooks-path.ts +85 -85
  75. package/src/hooks/handler.ts +127 -66
  76. package/src/hooks/installers/index.ts +5 -4
  77. package/src/hooks/official-skills.ts +6 -4
  78. package/src/hooks/pattern-detector.ts +173 -173
  79. package/src/hooks/rules/memorix-agent-rules.md +9 -7
  80. package/src/hooks/significance-filter.ts +250 -250
  81. package/src/knowledge/context-assembly.ts +4 -1
  82. package/src/knowledge/workset.ts +89 -1
  83. package/src/llm/memory-manager.ts +328 -328
  84. package/src/llm/provider.ts +885 -885
  85. package/src/llm/quality.ts +248 -248
  86. package/src/memory/attribution-guard.ts +249 -249
  87. package/src/memory/disclosure-policy.ts +135 -135
  88. package/src/memory/entity-extractor.ts +197 -197
  89. package/src/memory/formation/evaluate.ts +217 -217
  90. package/src/memory/formation/extract.ts +361 -361
  91. package/src/memory/formation/index.ts +417 -417
  92. package/src/memory/formation/resolve.ts +344 -344
  93. package/src/memory/formation/types.ts +315 -315
  94. package/src/memory/freshness.ts +122 -122
  95. package/src/memory/graph.ts +197 -197
  96. package/src/memory/refs.ts +94 -94
  97. package/src/memory/secret-filter.ts +79 -79
  98. package/src/memory/session.ts +158 -9
  99. package/src/multimodal/image-loader.ts +143 -143
  100. package/src/orchestrate/adapters/claude-stream.ts +192 -192
  101. package/src/orchestrate/adapters/claude.ts +111 -111
  102. package/src/orchestrate/adapters/codex-stream.ts +134 -134
  103. package/src/orchestrate/adapters/codex.ts +41 -41
  104. package/src/orchestrate/adapters/gemini-stream.ts +166 -166
  105. package/src/orchestrate/adapters/gemini.ts +42 -42
  106. package/src/orchestrate/adapters/index.ts +73 -73
  107. package/src/orchestrate/adapters/opencode-stream.ts +143 -143
  108. package/src/orchestrate/adapters/opencode.ts +47 -47
  109. package/src/orchestrate/adapters/spawn-helper.ts +286 -286
  110. package/src/orchestrate/adapters/types.ts +77 -77
  111. package/src/orchestrate/capability-router.ts +284 -284
  112. package/src/orchestrate/context-compact.ts +188 -188
  113. package/src/orchestrate/cost-tracker.ts +219 -219
  114. package/src/orchestrate/error-recovery.ts +191 -191
  115. package/src/orchestrate/evidence.ts +140 -140
  116. package/src/orchestrate/ledger.ts +110 -110
  117. package/src/orchestrate/memorix-bridge.ts +343 -343
  118. package/src/orchestrate/output-budget.ts +80 -80
  119. package/src/orchestrate/permission.ts +152 -152
  120. package/src/orchestrate/pipeline-trace.ts +131 -131
  121. package/src/orchestrate/prompt-builder.ts +155 -155
  122. package/src/orchestrate/ring-buffer.ts +37 -37
  123. package/src/orchestrate/task-graph.ts +389 -389
  124. package/src/orchestrate/worktree.ts +232 -232
  125. package/src/project/aliases.ts +374 -374
  126. package/src/project/detector.ts +268 -268
  127. package/src/rules/adapters/claude-code.ts +99 -99
  128. package/src/rules/adapters/codex.ts +97 -97
  129. package/src/rules/adapters/copilot.ts +124 -124
  130. package/src/rules/adapters/cursor.ts +114 -114
  131. package/src/rules/adapters/kiro.ts +126 -126
  132. package/src/rules/adapters/trae.ts +56 -56
  133. package/src/rules/adapters/windsurf.ts +83 -83
  134. package/src/rules/syncer.ts +235 -235
  135. package/src/sdk.ts +299 -299
  136. package/src/search/intent-detector.ts +289 -289
  137. package/src/search/query-expansion.ts +52 -52
  138. package/src/server/formation-timeout.ts +27 -27
  139. package/src/server.ts +144 -10
  140. package/src/skills/mini-skills.ts +386 -386
  141. package/src/store/bun-sqlite-compat.ts +118 -15
  142. package/src/store/chat-store.ts +119 -119
  143. package/src/store/graph-store.ts +249 -249
  144. package/src/store/mini-skill-store.ts +349 -349
  145. package/src/store/persistence-json.ts +212 -212
  146. package/src/store/persistence.ts +291 -291
  147. package/src/store/project-affinity.ts +195 -195
  148. package/src/store/sqlite-db.ts +3 -3
  149. package/src/team/event-bus.ts +76 -76
  150. package/src/team/file-locks.ts +173 -173
  151. package/src/team/handoff.ts +161 -161
  152. package/src/team/messages.ts +203 -203
  153. package/src/team/poll.ts +132 -132
  154. package/src/team/tasks.ts +211 -211
  155. package/src/workspace/mcp-adapters/codex.ts +191 -191
  156. package/src/workspace/mcp-adapters/copilot.ts +105 -105
  157. package/src/workspace/mcp-adapters/cursor.ts +53 -53
  158. package/src/workspace/mcp-adapters/kiro.ts +64 -64
  159. package/src/workspace/mcp-adapters/opencode.ts +123 -123
  160. package/src/workspace/mcp-adapters/trae.ts +134 -134
  161. package/src/workspace/mcp-adapters/windsurf.ts +91 -91
  162. package/src/workspace/sanitizer.ts +60 -60
  163. package/src/workspace/workflow-sync.ts +131 -131
@@ -1,27 +1,27 @@
1
- export const DEFAULT_FORMATION_TIMEOUT_MS = 12_000;
2
- export const FORMATION_TIMEOUT_MIN_MS = 1_000;
3
- export const FORMATION_TIMEOUT_MAX_MS = 300_000;
4
-
5
- /**
6
- * Parse and validate MEMORIX_FORMATION_TIMEOUT_MS.
7
- * - Must be a valid integer in the range 1000-300000ms.
8
- * - Invalid values fall back to the default and log a warning.
9
- * - Out-of-range values are clamped to the nearest bound.
10
- * Default: 12000ms (12s).
11
- */
12
- export function parseFormationTimeoutMs(raw: string | undefined): number {
13
- const value = raw?.trim();
14
- if (!value) return DEFAULT_FORMATION_TIMEOUT_MS;
15
-
16
- const parsed = Number(value);
17
- if (!Number.isInteger(parsed) || Number.isNaN(parsed)) {
18
- console.warn(
19
- `[memorix] MEMORIX_FORMATION_TIMEOUT_MS="${raw}" is invalid (must be a positive integer between ${FORMATION_TIMEOUT_MIN_MS}-${FORMATION_TIMEOUT_MAX_MS}ms). Using default ${DEFAULT_FORMATION_TIMEOUT_MS}ms.`,
20
- );
21
- return DEFAULT_FORMATION_TIMEOUT_MS;
22
- }
23
-
24
- if (parsed < FORMATION_TIMEOUT_MIN_MS) return FORMATION_TIMEOUT_MIN_MS;
25
- if (parsed > FORMATION_TIMEOUT_MAX_MS) return FORMATION_TIMEOUT_MAX_MS;
26
- return parsed;
27
- }
1
+ export const DEFAULT_FORMATION_TIMEOUT_MS = 12_000;
2
+ export const FORMATION_TIMEOUT_MIN_MS = 1_000;
3
+ export const FORMATION_TIMEOUT_MAX_MS = 300_000;
4
+
5
+ /**
6
+ * Parse and validate MEMORIX_FORMATION_TIMEOUT_MS.
7
+ * - Must be a valid integer in the range 1000-300000ms.
8
+ * - Invalid values fall back to the default and log a warning.
9
+ * - Out-of-range values are clamped to the nearest bound.
10
+ * Default: 12000ms (12s).
11
+ */
12
+ export function parseFormationTimeoutMs(raw: string | undefined): number {
13
+ const value = raw?.trim();
14
+ if (!value) return DEFAULT_FORMATION_TIMEOUT_MS;
15
+
16
+ const parsed = Number(value);
17
+ if (!Number.isInteger(parsed) || Number.isNaN(parsed)) {
18
+ console.warn(
19
+ `[memorix] MEMORIX_FORMATION_TIMEOUT_MS="${raw}" is invalid (must be a positive integer between ${FORMATION_TIMEOUT_MIN_MS}-${FORMATION_TIMEOUT_MAX_MS}ms). Using default ${DEFAULT_FORMATION_TIMEOUT_MS}ms.`,
20
+ );
21
+ return DEFAULT_FORMATION_TIMEOUT_MS;
22
+ }
23
+
24
+ if (parsed < FORMATION_TIMEOUT_MIN_MS) return FORMATION_TIMEOUT_MIN_MS;
25
+ if (parsed > FORMATION_TIMEOUT_MAX_MS) return FORMATION_TIMEOUT_MAX_MS;
26
+ return parsed;
27
+ }
package/src/server.ts CHANGED
@@ -228,6 +228,9 @@ export const BOOTSTRAP_SAFE_TOOL_NAMES = new Set([
228
228
  'memorix_context_pack',
229
229
  ]);
230
230
 
231
+ const AUTOPILOT_RETRIEVAL_BOUNDARY_TTL_MS = 2 * 60 * 1000;
232
+ const READ_ONLY_TASK_PATTERN = /\b(?:do not|don't|never)\s+(?:modify|edit|change|write)\b|\bread[- ]only\b|(?:不要|不准|勿|禁止).{0,6}(?:修改|编辑|写入)|只读/i;
233
+
231
234
  export function shouldAwaitProjectRuntime(toolName: string): boolean {
232
235
  return !BOOTSTRAP_SAFE_TOOL_NAMES.has(toolName);
233
236
  }
@@ -602,6 +605,63 @@ export async function createMemorixServer(
602
605
  };
603
606
  };
604
607
 
608
+ // A complete Autopilot brief is the normal retrieval boundary for one coding
609
+ // turn. Keep accidental search/detail loops cheap, while preserving an
610
+ // explicit escape hatch when a caller really needs deeper history.
611
+ let autopilotRetrievalBoundary: {
612
+ projectId: string;
613
+ issuedAt: number;
614
+ coveredObservationIds: Set<number>;
615
+ readOnly: boolean;
616
+ } | null = null;
617
+ const getActiveAutopilotRetrievalBoundary = () => {
618
+ const boundary = autopilotRetrievalBoundary;
619
+ if (!boundary) return null;
620
+ if (boundary.projectId === project.id && Date.now() - boundary.issuedAt <= AUTOPILOT_RETRIEVAL_BOUNDARY_TTL_MS) {
621
+ return boundary;
622
+ }
623
+ autopilotRetrievalBoundary = null;
624
+ return null;
625
+ };
626
+ const requireExplicitAutopilotExpansion = (toolName: string, purpose?: string) => {
627
+ if (!getActiveAutopilotRetrievalBoundary() || purpose?.trim()) return null;
628
+
629
+ return {
630
+ content: [{
631
+ type: 'text' as const,
632
+ text:
633
+ '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. ' +
634
+ '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.',
635
+ }],
636
+ };
637
+ };
638
+ const blockCoveredAutopilotEvidence = (toolName: string, observationIds: number[], force?: boolean) => {
639
+ const boundary = getActiveAutopilotRetrievalBoundary();
640
+ if (!boundary || force || observationIds.length === 0 || !observationIds.every(id => boundary.coveredObservationIds.has(id))) {
641
+ return null;
642
+ }
643
+ return {
644
+ content: [{
645
+ type: 'text' as const,
646
+ text:
647
+ 'Memorix Autopilot retrieval boundary: every requested memory is already represented in the latest project brief, so no duplicate detail was retrieved. ' +
648
+ '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.',
649
+ }],
650
+ };
651
+ };
652
+ const blockReadOnlyAutopilotWrite = (overrideReadOnly?: boolean) => {
653
+ const boundary = getActiveAutopilotRetrievalBoundary();
654
+ if (!boundary?.readOnly || overrideReadOnly) return null;
655
+ return {
656
+ content: [{
657
+ type: 'text' as const,
658
+ text:
659
+ 'Memorix write boundary: the latest task is read-only or asks not to modify files, so no memory was stored. ' +
660
+ 'Persist a record only when the user explicitly asks to save it, then retry with `overrideReadOnly: true`.',
661
+ }],
662
+ };
663
+ };
664
+
605
665
  // ================================================================
606
666
  // Memorix Extended Tools (3-layer Progressive Disclosure)
607
667
  // ================================================================
@@ -622,7 +682,8 @@ export async function createMemorixServer(
622
682
  'problem-solution ([FIX] bug fix), how-it-works ([INFO] explanation), what-changed ([CHANGE] change), ' +
623
683
  'discovery ([DISCOVERY] insight), why-it-exists ([WHY] rationale), trade-off ([TRADEOFF] compromise), ' +
624
684
  'session-request ([SESSION] original goal). ' +
625
- 'Project visibility is the default. Personal or team visibility requires an explicitly joined coordination identity.',
685
+ 'Project visibility is the default. Personal or team visibility requires an explicitly joined coordination identity. ' +
686
+ 'For a read-only task, do not store unless the user explicitly asks to save a record.',
626
687
  inputSchema: {
627
688
  entityName: z.string().describe('The entity this observation belongs to (e.g., "auth-module", "port-config")'),
628
689
  type: z.enum(OBSERVATION_TYPES).describe('Observation type for classification'),
@@ -646,11 +707,16 @@ export async function createMemorixServer(
646
707
  visibility: z.enum(['personal', 'project', 'team']).optional().describe(
647
708
  'Retrieval scope. Project is the normal shared default; personal/team require memorix_session_start with joinTeam=true.',
648
709
  ),
710
+ overrideReadOnly: z.boolean().optional().describe(
711
+ 'Use only when the user explicitly asks to save memory during a read-only or no-modification task.',
712
+ ),
649
713
  },
650
714
  },
651
- async ({ entityName: rawEntityName, type: rawType, title: rawTitle, narrative, facts, filesModified, concepts, topicKey, progress, relatedCommits, relatedEntities, visibility }) => {
715
+ async ({ entityName: rawEntityName, type: rawType, title: rawTitle, narrative, facts, filesModified, concepts, topicKey, progress, relatedCommits, relatedEntities, visibility, overrideReadOnly }) => {
652
716
  const unresolved = requireResolvedProject('store memory in the current project');
653
717
  if (unresolved) return unresolved;
718
+ const readOnlyBoundary = blockReadOnlyAutopilotWrite(overrideReadOnly);
719
+ if (readOnlyBoundary) return readOnlyBoundary;
654
720
  const requestedVisibility = (visibility ?? 'project') as 'personal' | 'project' | 'team';
655
721
  const reader = getObservationReader();
656
722
  if (requestedVisibility !== 'project' && !currentAgentId) {
@@ -1224,6 +1290,7 @@ export async function createMemorixServer(
1224
1290
  title: 'Search Memory',
1225
1291
  description:
1226
1292
  'Search project memory. Returns a compact index (~50-100 tokens/result). ' +
1293
+ '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. ' +
1227
1294
  'Use memorix_detail to fetch full content for specific IDs. ' +
1228
1295
  'Use memorix_timeline to see chronological context. ' +
1229
1296
  'Searches across all observations stored from any IDE session — enabling cross-session and cross-agent context retrieval.',
@@ -1243,13 +1310,23 @@ export async function createMemorixServer(
1243
1310
  source: z.enum(['agent', 'git', 'manual']).optional().describe(
1244
1311
  'Filter by memory source. "git" returns only commit-derived ground truth memories. Omit for all sources.',
1245
1312
  ),
1313
+ purpose: z.string().optional().describe(
1314
+ 'Why this must expand beyond the latest Autopilot brief. Name the missing fact or the user\'s explicit request.',
1315
+ ),
1316
+ force: z.boolean().optional().describe(
1317
+ 'Use only when the user explicitly asks to read a record already represented in the latest Autopilot brief.',
1318
+ ),
1246
1319
  },
1247
1320
  },
1248
- async ({ query, limit, type, maxTokens, scope, since, until, status, source }) => {
1321
+ async ({ query, limit, type, maxTokens, scope, since, until, status, source, purpose, force }) => {
1249
1322
  if (scope !== 'global') {
1250
1323
  const unresolved = requireResolvedProject('search the current project');
1251
1324
  if (unresolved) return unresolved;
1252
1325
  }
1326
+ if (scope !== 'global') {
1327
+ const boundary = requireExplicitAutopilotExpansion('memorix_search', purpose);
1328
+ if (boundary) return boundary;
1329
+ }
1253
1330
  return withFreshIndex(async () => {
1254
1331
 
1255
1332
  const safeLimit = limit != null ? coerceNumber(limit, 20) : undefined;
@@ -1295,6 +1372,18 @@ export async function createMemorixServer(
1295
1372
  throw error;
1296
1373
  }
1297
1374
 
1375
+ const activeBoundary = getActiveAutopilotRetrievalBoundary();
1376
+ if (
1377
+ scope !== 'global'
1378
+ && activeBoundary
1379
+ && !force
1380
+ && result.entries.length > 0
1381
+ && result.entries.every(entry => activeBoundary.coveredObservationIds.has(entry.id))
1382
+ ) {
1383
+ const duplicate = blockCoveredAutopilotEvidence('memorix_search', result.entries.map(entry => entry.id), force);
1384
+ if (duplicate) return duplicate;
1385
+ }
1386
+
1298
1387
  // Append retrieval diagnostics only; do not mix workspace-sync guidance into memory results.
1299
1388
  let text = result.formatted;
1300
1389
  try {
@@ -1414,6 +1503,7 @@ export async function createMemorixServer(
1414
1503
  observations,
1415
1504
  task,
1416
1505
  refresh: refresh ?? 'auto',
1506
+ reader: getObservationReader(),
1417
1507
  enqueueRefresh: () => {
1418
1508
  enqueueCodegraphRefresh({
1419
1509
  dataDir: projectDir,
@@ -1430,6 +1520,17 @@ export async function createMemorixServer(
1430
1520
  ? formatAutoProjectContextSummary(context)
1431
1521
  : formatAutoProjectContextPrompt(context);
1432
1522
 
1523
+ autopilotRetrievalBoundary = {
1524
+ projectId: project.id,
1525
+ issuedAt: Date.now(),
1526
+ coveredObservationIds: new Set([
1527
+ ...(context.workset.continuation?.memories.map(memory => memory.id) ?? []),
1528
+ ...context.workset.reliableMemory.map(memory => memory.id),
1529
+ ...context.workset.cautionMemory.map(memory => memory.id),
1530
+ ]),
1531
+ readOnly: READ_ONLY_TASK_PATTERN.test(task ?? ''),
1532
+ };
1533
+
1433
1534
  return {
1434
1535
  content: [{ type: 'text' as const, text }],
1435
1536
  };
@@ -1475,18 +1576,24 @@ export async function createMemorixServer(
1475
1576
  title: 'Context Pack',
1476
1577
  description:
1477
1578
  'Build a prompt-ready working context pack for a coding task. ' +
1478
- 'Combines relevant memories, CodeGraph Memory facts, freshness warnings, suggested reads, and verification hints.',
1579
+ 'Combines relevant memories, CodeGraph Memory facts, freshness warnings, suggested reads, and verification hints. ' +
1580
+ 'After a complete memorix_project_context brief, provide purpose only when deliberately expanding beyond it.',
1479
1581
  inputSchema: {
1480
1582
  task: z.string().describe('Current coding task or question'),
1481
1583
  limit: z.preprocess(
1482
1584
  value => (typeof value === 'string' && value.trim() !== '' ? Number(value) : value),
1483
1585
  z.number().int().positive().max(100),
1484
1586
  ).optional().describe('Max active memories to inspect before code-ref filtering (default: 20)'),
1587
+ purpose: z.string().optional().describe(
1588
+ 'Why this must expand beyond the latest Autopilot brief. Name the missing fact or the user\'s explicit request.',
1589
+ ),
1485
1590
  },
1486
1591
  },
1487
- async ({ task, limit }) => {
1592
+ async ({ task, limit, purpose }) => {
1488
1593
  const unresolved = requireResolvedProject('build a context pack for the current project');
1489
1594
  if (unresolved) return unresolved;
1595
+ const boundary = requireExplicitAutopilotExpansion('memorix_context_pack', purpose);
1596
+ if (boundary) return boundary;
1490
1597
 
1491
1598
  const [
1492
1599
  { CodeGraphStore },
@@ -2380,6 +2487,7 @@ export async function createMemorixServer(
2380
2487
  description:
2381
2488
  'Fetch full observation or mini-skill details — includes source kind (explicit memory / hook trace / git evidence), ' +
2382
2489
  'value category, and cross-references (~500-1000 tokens each). ' +
2490
+ '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. ' +
2383
2491
  'Always use memorix_search first to find relevant IDs, then fetch only what you need. ' +
2384
2492
  'Accepts typed refs from search results (e.g. "obs:42", "skill:3") via the typedRefs field, ' +
2385
2493
  'or legacy numeric ids / object refs for backward compatibility.',
@@ -2392,19 +2500,39 @@ export async function createMemorixServer(
2392
2500
  }),
2393
2501
  ).optional().describe('Explicit observation refs. Prefer this for global search results.'),
2394
2502
  typedRefs: z.array(z.string()).optional().describe('Typed memory refs from search results, e.g. "obs:42", "skill:3", "obs:42@org/proj"'),
2503
+ purpose: z.string().optional().describe(
2504
+ 'Why this must expand beyond the latest Autopilot brief. Name the missing fact or the user\'s explicit request.',
2505
+ ),
2506
+ force: z.boolean().optional().describe(
2507
+ 'Use only when the user explicitly asks to read a record already represented in the latest Autopilot brief.',
2508
+ ),
2395
2509
  },
2396
2510
  },
2397
- async ({ ids, refs, typedRefs }) => {
2511
+ async ({ ids, refs, typedRefs, purpose, force }) => {
2398
2512
  // Defensive coercion: Claude Code CLI + GLM may send "[16]" instead of [16]
2399
2513
  const safeIds = coerceNumberArray(ids);
2400
2514
  const safeRefs = coerceObservationRefs(refs);
2401
2515
  const safeTypedRefs = coerceStringArray(typedRefs);
2516
+ const hasCrossProjectRef = safeRefs.some((ref) => ref.projectId && ref.projectId !== project.id)
2517
+ || safeTypedRefs.some((ref) => ref.includes('@') && !ref.endsWith(`@${project.id}`));
2518
+ if (!hasCrossProjectRef) {
2519
+ const boundary = requireExplicitAutopilotExpansion('memorix_detail', purpose);
2520
+ if (boundary) return boundary;
2521
+ const requestedObservationIds = [
2522
+ ...safeIds,
2523
+ ...safeRefs.map(ref => ref.id),
2524
+ ...safeTypedRefs.flatMap((ref) => {
2525
+ const match = /^obs:(\d+)(?:@.+)?$/i.exec(ref.trim());
2526
+ return match ? [Number(match[1])] : [];
2527
+ }),
2528
+ ];
2529
+ const duplicate = blockCoveredAutopilotEvidence('memorix_detail', requestedObservationIds, force);
2530
+ if (duplicate) return duplicate;
2531
+ }
2402
2532
 
2403
2533
  // Priority: typedRefs > refs > ids (each is a complete, homogeneous input path)
2404
2534
  let result;
2405
2535
  try {
2406
- const hasCrossProjectRef = safeRefs.some((ref) => ref.projectId && ref.projectId !== project.id)
2407
- || safeTypedRefs.some((ref) => ref.includes('@') && !ref.endsWith(`@${project.id}`));
2408
2536
  const reader = getObservationReader(hasCrossProjectRef ? 'global' : 'project');
2409
2537
  if (safeTypedRefs.length > 0) {
2410
2538
  // Pass typed ref strings directly — compactDetail handles parsing
@@ -3614,7 +3742,6 @@ export async function createMemorixServer(
3614
3742
  if (unresolved) return unresolved;
3615
3743
 
3616
3744
  const { startSession } = await import('./memory/session.js');
3617
- const result = await startSession(projectDir, project.id, { sessionId, agent });
3618
3745
 
3619
3746
  const llmStatus = isLLMEnabled()
3620
3747
  ? `LLM enhanced mode: ${getLLMConfig()?.provider}/${getLLMConfig()?.model} (fact extraction + auto-dedup active)`
@@ -3690,6 +3817,12 @@ export async function createMemorixServer(
3690
3817
  }
3691
3818
  } catch { /* team auto-registration is best-effort */ }
3692
3819
 
3820
+ const result = await startSession(projectDir, project.id, {
3821
+ sessionId,
3822
+ agent,
3823
+ reader: getObservationReader(),
3824
+ });
3825
+
3693
3826
  const lines = [
3694
3827
  `[OK] Session started: ${result.session.id}`,
3695
3828
  `Project: ${project.name} (${project.id})`,
@@ -3825,7 +3958,7 @@ export async function createMemorixServer(
3825
3958
  async ({ limit }) => {
3826
3959
  const safeLimit = limit != null ? coerceNumber(limit, 3) : 3;
3827
3960
  const { getSessionContext, listSessions } = await import('./memory/session.js');
3828
- const context = await getSessionContext(projectDir, project.id, safeLimit);
3961
+ const context = await getSessionContext(projectDir, project.id, safeLimit, getObservationReader());
3829
3962
  const sessions = await listSessions(projectDir, project.id);
3830
3963
 
3831
3964
  const activeSessions = sessions.filter(s => s.status === 'active');
@@ -4820,6 +4953,7 @@ export async function createMemorixServer(
4820
4953
 
4821
4954
  // Phase 4a: clear agent identity — old project's agent is not valid in new project
4822
4955
  currentAgentId = undefined;
4956
+ autopilotRetrievalBoundary = null;
4823
4957
 
4824
4958
  // Re-resolve data dir with canonical ID (may differ from raw detected ID)
4825
4959
  const canonicalProjectDir = newCanonicalId !== newDetected.id