memorix 1.2.0 → 1.2.2

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 (212) hide show
  1. package/CHANGELOG.md +30 -1
  2. package/README.md +18 -4
  3. package/README.zh-CN.md +18 -4
  4. package/TEAM.md +86 -86
  5. package/dist/cli/index.js +15919 -14055
  6. package/dist/cli/index.js.map +1 -1
  7. package/dist/index.js +1997 -1021
  8. package/dist/index.js.map +1 -1
  9. package/dist/maintenance-runner.d.ts +1 -1
  10. package/dist/maintenance-runner.js +8481 -8005
  11. package/dist/maintenance-runner.js.map +1 -1
  12. package/dist/memcode-runtime/CHANGELOG.md +30 -1
  13. package/dist/sdk.d.ts +7 -2
  14. package/dist/sdk.js +2022 -1024
  15. package/dist/sdk.js.map +1 -1
  16. package/dist/types.d.ts +49 -1
  17. package/dist/types.js.map +1 -1
  18. package/docs/1.2.2-MEMORY-CONTROL-PLANE.md +434 -0
  19. package/docs/AGENT_OPERATOR_PLAYBOOK.md +4 -0
  20. package/docs/API_REFERENCE.md +27 -5
  21. package/docs/DESIGN_DECISIONS.md +357 -357
  22. package/docs/DEVELOPMENT.md +4 -0
  23. package/docs/README.md +1 -1
  24. package/docs/SETUP.md +7 -1
  25. package/docs/dev-log/progress.txt +91 -11
  26. package/docs/knowledge/workflows/memorix-release.md +57 -0
  27. package/package.json +1 -1
  28. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  29. package/src/audit/index.ts +156 -156
  30. package/src/cli/command-guide.ts +192 -0
  31. package/src/cli/commands/audit-list.ts +89 -89
  32. package/src/cli/commands/audit.ts +9 -4
  33. package/src/cli/commands/background.ts +659 -659
  34. package/src/cli/commands/cleanup.ts +5 -1
  35. package/src/cli/commands/codegraph.ts +17 -8
  36. package/src/cli/commands/context.ts +3 -2
  37. package/src/cli/commands/doctor.ts +4 -2
  38. package/src/cli/commands/explain.ts +9 -3
  39. package/src/cli/commands/formation.ts +48 -48
  40. package/src/cli/commands/git-hook-install.ts +111 -111
  41. package/src/cli/commands/handoff.ts +75 -61
  42. package/src/cli/commands/hooks-status.ts +63 -63
  43. package/src/cli/commands/identity.ts +116 -0
  44. package/src/cli/commands/ingest-commit.ts +153 -153
  45. package/src/cli/commands/ingest-image.ts +71 -69
  46. package/src/cli/commands/ingest-log.ts +180 -180
  47. package/src/cli/commands/ingest.ts +44 -44
  48. package/src/cli/commands/integrate-shared.ts +15 -15
  49. package/src/cli/commands/knowledge.ts +40 -0
  50. package/src/cli/commands/lock.ts +93 -92
  51. package/src/cli/commands/memory.ts +58 -21
  52. package/src/cli/commands/message.ts +123 -118
  53. package/src/cli/commands/operator-shared.ts +98 -3
  54. package/src/cli/commands/poll.ts +74 -64
  55. package/src/cli/commands/purge-all-memory.ts +85 -85
  56. package/src/cli/commands/purge-project-memory.ts +83 -83
  57. package/src/cli/commands/reasoning.ts +135 -121
  58. package/src/cli/commands/retention.ts +9 -4
  59. package/src/cli/commands/serve-http.ts +22 -43
  60. package/src/cli/commands/serve-shared.ts +118 -118
  61. package/src/cli/commands/session.ts +29 -3
  62. package/src/cli/commands/setup.ts +9 -3
  63. package/src/cli/commands/skills.ts +124 -119
  64. package/src/cli/commands/status.ts +4 -3
  65. package/src/cli/commands/task.ts +193 -184
  66. package/src/cli/commands/team.ts +14 -10
  67. package/src/cli/commands/transfer.ts +108 -55
  68. package/src/cli/commands/uninstall-project-artifacts.ts +85 -85
  69. package/src/cli/identity.ts +89 -0
  70. package/src/cli/index.ts +96 -19
  71. package/src/cli/invocation.ts +115 -0
  72. package/src/cli/tui/ChatView.tsx +234 -234
  73. package/src/cli/tui/CommandBar.tsx +312 -312
  74. package/src/cli/tui/ContextRail.tsx +118 -118
  75. package/src/cli/tui/HeaderBar.tsx +72 -72
  76. package/src/cli/tui/LogoBanner.tsx +51 -51
  77. package/src/cli/tui/Sidebar.tsx +179 -179
  78. package/src/cli/tui/chat-service.ts +41 -18
  79. package/src/cli/tui/data.ts +23 -44
  80. package/src/cli/tui/index.ts +41 -41
  81. package/src/cli/tui/markdown-render.tsx +371 -371
  82. package/src/cli/tui/operator-context.ts +60 -0
  83. package/src/cli/tui/use-mouse.ts +157 -157
  84. package/src/cli/tui/useNavigation.ts +56 -56
  85. package/src/cli/tui/views/MemoryView.tsx +10 -8
  86. package/src/cli/update-checker.ts +211 -211
  87. package/src/cli/version.ts +7 -7
  88. package/src/cli/workbench.ts +1 -1
  89. package/src/codegraph/auto-context.ts +34 -17
  90. package/src/codegraph/context-pack.ts +1 -0
  91. package/src/codegraph/current-facts.ts +19 -1
  92. package/src/codegraph/project-context.ts +2 -0
  93. package/src/codegraph/task-lens.ts +49 -5
  94. package/src/compact/engine.ts +26 -10
  95. package/src/compact/index-format.ts +25 -2
  96. package/src/compact/token-budget.ts +74 -74
  97. package/src/dashboard/project-classification.ts +64 -64
  98. package/src/dashboard/server.ts +58 -52
  99. package/src/embedding/fastembed-provider.ts +142 -142
  100. package/src/embedding/transformers-provider.ts +111 -111
  101. package/src/git/extractor.ts +209 -209
  102. package/src/git/hooks-path.ts +85 -85
  103. package/src/hooks/admission.ts +117 -0
  104. package/src/hooks/handler.ts +98 -91
  105. package/src/hooks/pattern-detector.ts +173 -173
  106. package/src/hooks/significance-filter.ts +250 -250
  107. package/src/knowledge/claims.ts +51 -1
  108. package/src/knowledge/context-assembly.ts +97 -0
  109. package/src/knowledge/types.ts +1 -0
  110. package/src/knowledge/workflows.ts +34 -3
  111. package/src/knowledge/workset.ts +179 -10
  112. package/src/llm/memory-manager.ts +328 -328
  113. package/src/llm/provider.ts +885 -885
  114. package/src/llm/quality.ts +248 -248
  115. package/src/memory/admission.ts +57 -0
  116. package/src/memory/attribution-guard.ts +249 -249
  117. package/src/memory/auto-relations.ts +21 -0
  118. package/src/memory/consolidation.ts +13 -2
  119. package/src/memory/disclosure-policy.ts +140 -135
  120. package/src/memory/entity-extractor.ts +197 -197
  121. package/src/memory/export-import.ts +11 -3
  122. package/src/memory/formation/evaluate.ts +217 -217
  123. package/src/memory/formation/extract.ts +361 -361
  124. package/src/memory/formation/index.ts +417 -417
  125. package/src/memory/formation/resolve.ts +344 -344
  126. package/src/memory/formation/types.ts +315 -315
  127. package/src/memory/freshness.ts +122 -122
  128. package/src/memory/graph-context.ts +8 -2
  129. package/src/memory/graph-scope.ts +46 -0
  130. package/src/memory/graph.ts +197 -197
  131. package/src/memory/observations.ts +162 -4
  132. package/src/memory/quality-audit.ts +2 -0
  133. package/src/memory/refs.ts +94 -94
  134. package/src/memory/retention.ts +22 -2
  135. package/src/memory/secret-filter.ts +79 -79
  136. package/src/memory/session.ts +5 -2
  137. package/src/memory/visibility.ts +80 -0
  138. package/src/multimodal/image-loader.ts +143 -143
  139. package/src/orchestrate/adapters/claude-stream.ts +192 -192
  140. package/src/orchestrate/adapters/claude.ts +111 -111
  141. package/src/orchestrate/adapters/codex-stream.ts +134 -134
  142. package/src/orchestrate/adapters/codex.ts +41 -41
  143. package/src/orchestrate/adapters/gemini-stream.ts +166 -166
  144. package/src/orchestrate/adapters/gemini.ts +42 -42
  145. package/src/orchestrate/adapters/index.ts +73 -73
  146. package/src/orchestrate/adapters/opencode-stream.ts +143 -143
  147. package/src/orchestrate/adapters/opencode.ts +47 -47
  148. package/src/orchestrate/adapters/spawn-helper.ts +286 -286
  149. package/src/orchestrate/adapters/types.ts +77 -77
  150. package/src/orchestrate/capability-router.ts +284 -284
  151. package/src/orchestrate/context-compact.ts +188 -188
  152. package/src/orchestrate/cost-tracker.ts +219 -219
  153. package/src/orchestrate/error-recovery.ts +191 -191
  154. package/src/orchestrate/evidence.ts +140 -140
  155. package/src/orchestrate/ledger.ts +110 -110
  156. package/src/orchestrate/memorix-bridge.ts +378 -340
  157. package/src/orchestrate/output-budget.ts +80 -80
  158. package/src/orchestrate/permission.ts +152 -152
  159. package/src/orchestrate/pipeline-trace.ts +131 -131
  160. package/src/orchestrate/prompt-builder.ts +155 -155
  161. package/src/orchestrate/ring-buffer.ts +37 -37
  162. package/src/orchestrate/task-graph.ts +389 -389
  163. package/src/orchestrate/verify-gate.ts +33 -10
  164. package/src/orchestrate/worktree.ts +232 -232
  165. package/src/project/aliases.ts +374 -374
  166. package/src/project/detector.ts +268 -268
  167. package/src/rules/adapters/claude-code.ts +99 -99
  168. package/src/rules/adapters/codex.ts +97 -97
  169. package/src/rules/adapters/copilot.ts +124 -124
  170. package/src/rules/adapters/cursor.ts +114 -114
  171. package/src/rules/adapters/kiro.ts +126 -126
  172. package/src/rules/adapters/trae.ts +56 -56
  173. package/src/rules/adapters/windsurf.ts +83 -83
  174. package/src/rules/syncer.ts +235 -235
  175. package/src/runtime/control-plane-maintenance.ts +1 -0
  176. package/src/runtime/isolated-maintenance.ts +1 -0
  177. package/src/runtime/lifecycle.ts +18 -0
  178. package/src/runtime/maintenance-jobs.ts +1 -0
  179. package/src/runtime/maintenance-runner.ts +2 -0
  180. package/src/runtime/project-maintenance.ts +89 -0
  181. package/src/sdk.ts +334 -304
  182. package/src/search/intent-detector.ts +289 -289
  183. package/src/search/query-expansion.ts +52 -52
  184. package/src/server/formation-timeout.ts +27 -27
  185. package/src/server.ts +334 -93
  186. package/src/skills/mini-skills.ts +386 -386
  187. package/src/store/chat-store.ts +119 -119
  188. package/src/store/graph-store.ts +249 -249
  189. package/src/store/mini-skill-store.ts +349 -349
  190. package/src/store/orama-store.ts +61 -6
  191. package/src/store/persistence-json.ts +212 -212
  192. package/src/store/persistence.ts +291 -291
  193. package/src/store/project-affinity.ts +195 -195
  194. package/src/store/sqlite-db.ts +23 -1
  195. package/src/store/sqlite-store.ts +12 -2
  196. package/src/team/event-bus.ts +76 -76
  197. package/src/team/file-locks.ts +173 -173
  198. package/src/team/handoff.ts +168 -161
  199. package/src/team/messages.ts +203 -203
  200. package/src/team/poll.ts +132 -132
  201. package/src/team/tasks.ts +211 -211
  202. package/src/types.ts +51 -0
  203. package/src/wiki/generator.ts +2 -0
  204. package/src/workspace/mcp-adapters/codex.ts +191 -191
  205. package/src/workspace/mcp-adapters/copilot.ts +105 -105
  206. package/src/workspace/mcp-adapters/cursor.ts +53 -53
  207. package/src/workspace/mcp-adapters/kiro.ts +64 -64
  208. package/src/workspace/mcp-adapters/opencode.ts +123 -123
  209. package/src/workspace/mcp-adapters/trae.ts +134 -134
  210. package/src/workspace/mcp-adapters/windsurf.ts +91 -91
  211. package/src/workspace/sanitizer.ts +60 -60
  212. package/src/workspace/workflow-sync.ts +131 -131
package/src/server.ts CHANGED
@@ -28,12 +28,14 @@ import { initSessionStore } from './store/session-store.js';
28
28
  import { checkProjectAttribution, auditProjectObservations } from './memory/attribution-guard.js';
29
29
  import { createAutoRelations } from './memory/auto-relations.js';
30
30
  import { extractEntities } from './memory/entity-extractor.js';
31
+ import { scopeKnowledgeGraphToProject } from './memory/graph-scope.js';
32
+ import { canManageObservation, canReadObservation, filterReadableObservations, resolveObservationVisibility } from './memory/visibility.js';
31
33
  import { compactSearch, compactTimeline, compactDetail } from './compact/engine.js';
32
34
  import { buildGraphContextPacket, formatGraphContextPrompt } from './memory/graph-context.js';
33
35
  import { detectProject } from './project/detector.js';
34
36
  import { registerAlias, initAliasRegistry, resolveAliases, autoMergeByBaseName } from './project/aliases.js';
35
37
  import { getProjectDataDir } from './store/persistence.js';
36
- import type { ObservationType, RuleSource, AgentTarget, MCPServerEntry } from './types.js';
38
+ import type { ObservationType, RuleSource, AgentTarget, MCPServerEntry, ObservationReader } from './types.js';
37
39
  import { RulesSyncer } from './rules/syncer.js';
38
40
  import { WorkspaceSyncEngine } from './workspace/engine.js';
39
41
  import {
@@ -260,8 +262,10 @@ export async function createMemorixServer(
260
262
  let rawProject: import('./types.js').ProjectInfo;
261
263
  let projectResolved = true;
262
264
  let projectResolutionError: string | null = null;
263
- let explicitProjectBound = false; // Set true when memorix_session_start binds via projectRoot
264
- let currentAgentId: string | undefined; // Session-scoped coordination identity for attribution after explicit join
265
+ let explicitProjectBound = false; // Set true when memorix_session_start binds via projectRoot
266
+ let currentAgentId: string | undefined; // Session-scoped coordination identity for attribution after explicit join
267
+ let teamStore!: import('./team/team-store.js').TeamStore;
268
+ let initTeamStoreForProject: ((dataDir: string) => Promise<import('./team/team-store.js').TeamStore>) | undefined;
265
269
  if (detectedProject) {
266
270
  rawProject = detectedProject;
267
271
  } else {
@@ -581,6 +585,23 @@ export async function createMemorixServer(
581
585
  return (originalRegisterTool as (...innerArgs: unknown[]) => unknown)(name, ...args) as never;
582
586
  }) as typeof server.registerTool;
583
587
 
588
+ const getObservationReader = (scope: 'project' | 'global' = 'project'): ObservationReader => {
589
+ let isTeamMember = false;
590
+ if (teamFeaturesEnabled && currentAgentId) {
591
+ try {
592
+ const agent = teamStore.getAgent(currentAgentId);
593
+ isTeamMember = agent?.project_id === project.id && agent.status === 'active';
594
+ } catch {
595
+ // A missing coordination store must never grant team visibility.
596
+ }
597
+ }
598
+ return {
599
+ ...(scope === 'project' ? { projectId: project.id } : {}),
600
+ ...(currentAgentId ? { agentId: currentAgentId } : {}),
601
+ isTeamMember,
602
+ };
603
+ };
604
+
584
605
  // ================================================================
585
606
  // Memorix Extended Tools (3-layer Progressive Disclosure)
586
607
  // ================================================================
@@ -601,7 +622,7 @@ export async function createMemorixServer(
601
622
  'problem-solution ([FIX] bug fix), how-it-works ([INFO] explanation), what-changed ([CHANGE] change), ' +
602
623
  'discovery ([DISCOVERY] insight), why-it-exists ([WHY] rationale), trade-off ([TRADEOFF] compromise), ' +
603
624
  'session-request ([SESSION] original goal). ' +
604
- 'Stored memories persist across sessions and are shared with other IDEs and agents (Cursor, Windsurf, Claude Code, Codex, Copilot, Gemini CLI, OpenCode, OpenClaw, Hermes Agent, Oh-my-Pi, Kiro, Antigravity, Trae) via the same local data directory.',
625
+ 'Project visibility is the default. Personal or team visibility requires an explicitly joined coordination identity.',
605
626
  inputSchema: {
606
627
  entityName: z.string().describe('The entity this observation belongs to (e.g., "auth-module", "port-config")'),
607
628
  type: z.enum(OBSERVATION_TYPES).describe('Observation type for classification'),
@@ -622,12 +643,30 @@ export async function createMemorixServer(
622
643
  }).optional().describe('Progress tracking for task/feature observations'),
623
644
  relatedCommits: z.array(z.string()).optional().describe('Git commit hashes this memory relates to (links ground truth ↔ reasoning)'),
624
645
  relatedEntities: z.array(z.string()).optional().describe('Other entity names this memory cross-references'),
646
+ visibility: z.enum(['personal', 'project', 'team']).optional().describe(
647
+ 'Retrieval scope. Project is the normal shared default; personal/team require memorix_session_start with joinTeam=true.',
648
+ ),
625
649
  },
626
650
  },
627
- async ({ entityName: rawEntityName, type: rawType, title: rawTitle, narrative, facts, filesModified, concepts, topicKey, progress, relatedCommits, relatedEntities }) => {
651
+ async ({ entityName: rawEntityName, type: rawType, title: rawTitle, narrative, facts, filesModified, concepts, topicKey, progress, relatedCommits, relatedEntities, visibility }) => {
628
652
  const unresolved = requireResolvedProject('store memory in the current project');
629
653
  if (unresolved) return unresolved;
630
- return withFreshIndex(async () => {
654
+ const requestedVisibility = (visibility ?? 'project') as 'personal' | 'project' | 'team';
655
+ const reader = getObservationReader();
656
+ if (requestedVisibility !== 'project' && !currentAgentId) {
657
+ return {
658
+ content: [{ type: 'text' as const, text: 'Personal or team memory requires memorix_session_start with joinTeam=true so Memorix can bind an owner.' }],
659
+ isError: true as const,
660
+ };
661
+ }
662
+ if (requestedVisibility === 'team' && !reader.isTeamMember) {
663
+ return {
664
+ content: [{ type: 'text' as const, text: 'Team memory requires an active coordination membership in the current project.' }],
665
+ isError: true as const,
666
+ };
667
+ }
668
+ try {
669
+ return await withFreshIndex(async () => {
631
670
 
632
671
  // Mutable copies — Formation Pipeline may improve these
633
672
  let entityName = rawEntityName;
@@ -677,9 +716,9 @@ export async function createMemorixServer(
677
716
  useLLM: isLLMEnabled(),
678
717
  minValueScore: 0.3,
679
718
  searchMemories: async (q: string, limit: number, pid: string): Promise<SearchHit[]> => {
680
- const result = await compactSearch({ query: q, limit, projectId: pid, status: 'active' });
719
+ const result = await compactSearch({ query: q, limit, projectId: pid, status: 'active', reader });
681
720
  if (result.entries.length === 0) return [];
682
- const details = await compactDetail(result.entries.map(e => e.id));
721
+ const details = await compactDetail(result.entries.map(e => e.id), { reader });
683
722
  return details.documents.map((d, i) => ({
684
723
  id: Number(d.id.replace('obs-', '')),
685
724
  observationId: d.observationId,
@@ -693,7 +732,7 @@ export async function createMemorixServer(
693
732
  },
694
733
  getObservation: (id: number) => {
695
734
  const o = getObservation(id);
696
- if (!o) return null;
735
+ if (!o || o.projectId !== project.id || !canReadObservation(o, reader)) return null;
697
736
  return {
698
737
  id: o.id,
699
738
  entityName: o.entityName,
@@ -752,7 +791,7 @@ export async function createMemorixServer(
752
791
  if (action === 'merge' && targetId) {
753
792
  // Merge into existing observation
754
793
  const targetObs = getObservation(targetId);
755
- if (targetObs) {
794
+ if (targetObs && targetObs.projectId === project.id && canReadObservation(targetObs, reader)) {
756
795
  await storeObservation({
757
796
  entityName: targetObs.entityName,
758
797
  type: targetObs.type,
@@ -766,6 +805,8 @@ export async function createMemorixServer(
766
805
  progress: progress as import('./types.js').ProgressInfo | undefined,
767
806
  sourceDetail: 'explicit',
768
807
  createdByAgentId: currentAgentId,
808
+ visibility,
809
+ visibilityReader: reader,
769
810
  });
770
811
  return {
771
812
  content: [{
@@ -777,7 +818,7 @@ export async function createMemorixServer(
777
818
  } else if (action === 'evolve' && targetId) {
778
819
  // Evolve existing observation
779
820
  const targetObs = getObservation(targetId);
780
- if (targetObs) {
821
+ if (targetObs && targetObs.projectId === project.id && canReadObservation(targetObs, reader)) {
781
822
  await storeObservation({
782
823
  entityName: targetObs.entityName,
783
824
  type: targetObs.type,
@@ -791,6 +832,8 @@ export async function createMemorixServer(
791
832
  progress: progress as import('./types.js').ProgressInfo | undefined,
792
833
  sourceDetail: 'explicit',
793
834
  createdByAgentId: currentAgentId,
835
+ visibility,
836
+ visibilityReader: reader,
794
837
  });
795
838
  return {
796
839
  content: [{
@@ -823,12 +866,13 @@ export async function createMemorixServer(
823
866
  limit: 5,
824
867
  projectId: project.id,
825
868
  status: 'active',
869
+ reader,
826
870
  });
827
871
  const similarEntries = searchResult.entries.map(e => e);
828
872
  if (similarEntries.length > 0) {
829
873
  // Fetch full details for comparison
830
874
  const similarIds = similarEntries.map(e => e.id);
831
- const details = await compactDetail(similarIds);
875
+ const details = await compactDetail(similarIds, { reader });
832
876
  const existingMemories: ExistingMemory[] = details.documents.map((d, i) => ({
833
877
  id: d.observationId,
834
878
  title: d.title,
@@ -849,7 +893,7 @@ export async function createMemorixServer(
849
893
  if (decision.action === 'UPDATE' && decision.targetId) {
850
894
  // Merge into existing memory (Mem0-style UPDATE)
851
895
  const targetObs = getObservation(decision.targetId);
852
- if (targetObs) {
896
+ if (targetObs && targetObs.projectId === project.id && canReadObservation(targetObs, reader)) {
853
897
  await storeObservation({
854
898
  entityName: targetObs.entityName,
855
899
  type: targetObs.type,
@@ -863,6 +907,8 @@ export async function createMemorixServer(
863
907
  progress: progress as import('./types.js').ProgressInfo | undefined,
864
908
  sourceDetail: 'explicit',
865
909
  createdByAgentId: currentAgentId,
910
+ visibility,
911
+ visibilityReader: reader,
866
912
  });
867
913
  compactAction = `[UPDATED] Compact UPDATE: merged into #${decision.targetId} (${decision.reason})`;
868
914
  compactMerged = true;
@@ -962,7 +1008,11 @@ export async function createMemorixServer(
962
1008
  // in a different project — signals a potential wrong-bucket write.
963
1009
  let attributionWarning = '';
964
1010
  try {
965
- const attrCheck = await checkProjectAttribution(entityName, project.id, getAllObservations());
1011
+ const attrCheck = await checkProjectAttribution(
1012
+ entityName,
1013
+ project.id,
1014
+ filterReadableObservations(getAllObservations(), reader),
1015
+ );
966
1016
  if (attrCheck.suspicious) {
967
1017
  attributionWarning = `\n[WARN] Attribution notice: entity "${entityName}" has 0 observations in ` +
968
1018
  `"${project.id}" but ${attrCheck.count} in "${attrCheck.knownIn}" ` +
@@ -988,6 +1038,8 @@ export async function createMemorixServer(
988
1038
  sourceDetail: 'explicit',
989
1039
  valueCategory: formationResult?.evaluation.category,
990
1040
  createdByAgentId: currentAgentId,
1041
+ visibility,
1042
+ visibilityReader: reader,
991
1043
  });
992
1044
 
993
1045
  // Add a reference to the entity's observations
@@ -1022,14 +1074,15 @@ export async function createMemorixServer(
1022
1074
  const compactStart = Date.now();
1023
1075
  const searchResult = await compactSearch({
1024
1076
  query: `${title} ${narrative.substring(0, 200)}`,
1025
- limit: 5,
1026
- projectId: project.id,
1027
- status: 'active',
1077
+ limit: 5,
1078
+ projectId: project.id,
1079
+ status: 'active',
1080
+ reader,
1028
1081
  });
1029
1082
  const similarEntries = searchResult.entries.map(e => e);
1030
1083
  if (similarEntries.length > 0) {
1031
1084
  const similarIds = similarEntries.map(e => e.id);
1032
- const details = await compactDetail(similarIds);
1085
+ const details = await compactDetail(similarIds, { reader });
1033
1086
  const existingMemories: ExistingMemory[] = details.documents.map((d, i) => ({
1034
1087
  id: d.observationId,
1035
1088
  title: d.title,
@@ -1055,9 +1108,9 @@ export async function createMemorixServer(
1055
1108
  useLLM: isLLMEnabled(),
1056
1109
  minValueScore: 0.3,
1057
1110
  searchMemories: async (q: string, limit: number, pid: string): Promise<SearchHit[]> => {
1058
- const result = await compactSearch({ query: q, limit, projectId: pid, status: 'active' });
1111
+ const result = await compactSearch({ query: q, limit, projectId: pid, status: 'active', reader });
1059
1112
  if (result.entries.length === 0) return [];
1060
- const details = await compactDetail(result.entries.map(e => e.id));
1113
+ const details = await compactDetail(result.entries.map(e => e.id), { reader });
1061
1114
  return details.documents.map((d, i) => ({
1062
1115
  id: Number(d.id.replace('obs-', '')),
1063
1116
  observationId: d.observationId,
@@ -1071,7 +1124,7 @@ export async function createMemorixServer(
1071
1124
  },
1072
1125
  getObservation: (id: number) => {
1073
1126
  const o = getObservation(id);
1074
- if (!o) return null;
1127
+ if (!o || o.projectId !== project.id || !canReadObservation(o, reader)) return null;
1075
1128
  return { id: o.id, entityName: o.entityName, type: o.type, title: o.title, narrative: o.narrative, facts: o.facts, topicKey: o.topicKey };
1076
1129
  },
1077
1130
  getEntityNames: () => graphManager.getEntityNames(),
@@ -1111,6 +1164,15 @@ export async function createMemorixServer(
1111
1164
  ],
1112
1165
  };
1113
1166
  }); // withFreshIndex
1167
+ } catch (error) {
1168
+ return {
1169
+ content: [{
1170
+ type: 'text' as const,
1171
+ text: error instanceof Error ? error.message : 'Failed to store memory.',
1172
+ }],
1173
+ isError: true as const,
1174
+ };
1175
+ }
1114
1176
  },
1115
1177
  );
1116
1178
 
@@ -1207,6 +1269,7 @@ export async function createMemorixServer(
1207
1269
  projectId: scope === 'global' ? undefined : project.id,
1208
1270
  status: (status as 'active' | 'resolved' | 'archived' | 'all') ?? 'active',
1209
1271
  source: source as 'agent' | 'git' | 'manual' | undefined,
1272
+ reader: getObservationReader(scope === 'global' ? 'global' : 'project'),
1210
1273
  });
1211
1274
 
1212
1275
  const timeoutPromise = new Promise<never>((_, reject) =>
@@ -1278,7 +1341,10 @@ export async function createMemorixServer(
1278
1341
  if (unresolved) return unresolved;
1279
1342
 
1280
1343
  const { getObservationStore } = await import('./store/obs-store.js');
1281
- const observations = await getObservationStore().loadByProject(project.id);
1344
+ const observations = filterReadableObservations(
1345
+ await getObservationStore().loadByProject(project.id),
1346
+ getObservationReader(),
1347
+ );
1282
1348
  const packet = buildGraphContextPacket(observations, {
1283
1349
  projectId: project.id,
1284
1350
  query,
@@ -1338,7 +1404,10 @@ export async function createMemorixServer(
1338
1404
  import('./runtime/maintenance-jobs.js'),
1339
1405
  import('./runtime/lifecycle.js'),
1340
1406
  ]);
1341
- const observations = await getObservationStore().loadByProject(project.id, { status: 'active' });
1407
+ const observations = filterReadableObservations(
1408
+ await getObservationStore().loadByProject(project.id, { status: 'active' }),
1409
+ getObservationReader(),
1410
+ );
1342
1411
  const context = await buildAutoProjectContext({
1343
1412
  project,
1344
1413
  dataDir: projectDir,
@@ -1425,7 +1494,7 @@ export async function createMemorixServer(
1425
1494
  { getResolvedConfig },
1426
1495
  { getExternalCodeGraphContext },
1427
1496
  { getObservationStore },
1428
- { collectCurrentProjectFacts },
1497
+ { collectCurrentProjectFacts, formatGitFact },
1429
1498
  { resolveTaskLens },
1430
1499
  ] = await Promise.all([
1431
1500
  import('./codegraph/store.js'),
@@ -1440,7 +1509,10 @@ export async function createMemorixServer(
1440
1509
  await store.init(projectDir);
1441
1510
  const codegraphConfig = getResolvedConfig({ projectRoot: project.rootPath }).codegraph;
1442
1511
  const exclude = codegraphConfig.excludePatterns;
1443
- const observations = await getObservationStore().loadByProject(project.id, { status: 'active' });
1512
+ const observations = filterReadableObservations(
1513
+ await getObservationStore().loadByProject(project.id, { status: 'active' }),
1514
+ getObservationReader(),
1515
+ );
1444
1516
  observations.reverse();
1445
1517
  const basePack = assembleContextPackForTask({
1446
1518
  store,
@@ -1467,8 +1539,7 @@ export async function createMemorixServer(
1467
1539
  worksetFacts.push('Latest changelog: ' + currentFacts.latestChangelog.version
1468
1540
  + (currentFacts.latestChangelog.date ? ' (' + currentFacts.latestChangelog.date + ')' : ''));
1469
1541
  }
1470
- worksetFacts.push('Git: ' + (currentFacts.git.branch ? 'branch ' + currentFacts.git.branch + ', ' : '')
1471
- + (currentFacts.git.dirty ? 'dirty worktree' : 'clean worktree'));
1542
+ worksetFacts.push(formatGitFact(currentFacts.git));
1472
1543
  const codeState = snapshot
1473
1544
  ? '- Code state: ' + (snapshot.baseRevision ? snapshot.baseRevision.slice(0, 12) : 'Git unavailable')
1474
1545
  + ', ' + snapshot.worktreeState + ' worktree'
@@ -1513,11 +1584,13 @@ export async function createMemorixServer(
1513
1584
  {
1514
1585
  title: 'Knowledge Workspace',
1515
1586
  description:
1516
- 'Manage the reviewable project Knowledge Workspace. Use it only for deliberate knowledge operations: initialize a local or versioned workspace, inspect status, compile source-backed proposals, lint, apply a reviewed proposal, or manage canonical workflows. It is intentionally absent from the default micro/lite profiles.',
1587
+ 'Manage the reviewable project Knowledge Workspace. Use it only for deliberate knowledge operations: initialize a local or versioned workspace, review source-backed claims, compile proposals, lint, apply a reviewed proposal, or manage canonical workflows. It is intentionally absent from the default micro/lite profiles.',
1517
1588
  inputSchema: {
1518
1589
  action: z.enum([
1519
1590
  'workspace_init',
1520
1591
  'status',
1592
+ 'claim_list',
1593
+ 'claim_review',
1521
1594
  'compile',
1522
1595
  'lint',
1523
1596
  'proposal_apply',
@@ -1532,6 +1605,9 @@ export async function createMemorixServer(
1532
1605
  path: z.string().optional().describe('Explicit versioned workspace path, used only by workspace_init'),
1533
1606
  proposalId: z.string().optional().describe('Pending proposal id for proposal_apply'),
1534
1607
  allowManualOverwrite: z.boolean().optional().default(false).describe('Explicitly allow proposal_apply to replace a manually edited page'),
1608
+ claimId: z.string().optional().describe('Source-backed claim id for claim_review'),
1609
+ claimReviewState: z.enum(['approved', 'rejected']).optional().describe('Deliberate review verdict for claim_review'),
1610
+ reviewDetail: z.string().max(2_000).optional().describe('Evidence check performed before approving or rejecting a claim'),
1535
1611
  workflowId: z.string().optional().describe('Canonical workflow id for workflow preview, apply, or run'),
1536
1612
  agent: z.string().optional().describe('Target agent for a workflow adapter'),
1537
1613
  task: z.string().optional().describe('Task text for workflow selection or a workflow run'),
@@ -1548,6 +1624,9 @@ export async function createMemorixServer(
1548
1624
  path: workspacePath,
1549
1625
  proposalId,
1550
1626
  allowManualOverwrite,
1627
+ claimId,
1628
+ claimReviewState,
1629
+ reviewDetail,
1551
1630
  workflowId,
1552
1631
  agent,
1553
1632
  task,
@@ -1572,6 +1651,7 @@ export async function createMemorixServer(
1572
1651
 
1573
1652
  const [
1574
1653
  { ClaimStore },
1654
+ { reviewClaim },
1575
1655
  { CodeGraphStore },
1576
1656
  { initializeKnowledgeWorkspace, loadKnowledgeWorkspace },
1577
1657
  { KnowledgeWorkspaceStore },
@@ -1587,6 +1667,7 @@ export async function createMemorixServer(
1587
1667
  },
1588
1668
  ] = await Promise.all([
1589
1669
  import('./knowledge/claim-store.js'),
1670
+ import('./knowledge/claims.js'),
1590
1671
  import('./codegraph/store.js'),
1591
1672
  import('./knowledge/workspace.js'),
1592
1673
  import('./knowledge/workspace-store.js'),
@@ -1642,7 +1723,55 @@ export async function createMemorixServer(
1642
1723
  reason: proposal.reason,
1643
1724
  createdAt: proposal.createdAt,
1644
1725
  })),
1726
+ reviewableClaims: claims.listClaims(project.id, { limit: 100 })
1727
+ .filter(claim => claim.reviewState === 'needs-review')
1728
+ .map(claim => ({ id: claim.id, subject: claim.subject, predicate: claim.predicate, objectValue: claim.objectValue })),
1729
+ },
1730
+ });
1731
+ }
1732
+
1733
+ if (action === 'claim_list') {
1734
+ return text({
1735
+ claims: claims.listClaims(project.id, { limit: 100 }).map(claim => ({
1736
+ id: claim.id,
1737
+ subject: claim.subject,
1738
+ predicate: claim.predicate,
1739
+ objectValue: claim.objectValue,
1740
+ status: claim.status,
1741
+ reviewState: claim.reviewState,
1742
+ origin: claim.origin,
1743
+ confidence: claim.confidence,
1744
+ evidenceCount: claims.listEvidence(claim.id).length,
1745
+ })),
1746
+ next: 'Approve only after checking the linked evidence. Rejected claims stay out of retrieval and publication.',
1747
+ });
1748
+ }
1749
+
1750
+ if (action === 'claim_review') {
1751
+ const requestedClaimId = requireText(claimId, 'claimId');
1752
+ const detail = requireText(reviewDetail, 'reviewDetail');
1753
+ if (!requestedClaimId || !claimReviewState || !detail) {
1754
+ return text({ error: 'claimId, claimReviewState, and reviewDetail are required for claim_review.' }, true);
1755
+ }
1756
+ const existing = claims.getClaim(requestedClaimId);
1757
+ if (!existing || existing.projectId !== project.id) {
1758
+ return text({ error: 'Claim was not found for the current project.' }, true);
1759
+ }
1760
+ const claim = reviewClaim(claims, {
1761
+ claimId: requestedClaimId,
1762
+ reviewState: claimReviewState,
1763
+ detail,
1764
+ });
1765
+ return text({
1766
+ claim: {
1767
+ id: claim.id,
1768
+ status: claim.status,
1769
+ reviewState: claim.reviewState,
1770
+ updatedAt: claim.updatedAt,
1645
1771
  },
1772
+ next: claim.reviewState === 'approved'
1773
+ ? 'This approved source-backed claim can now be considered by knowledge compilation.'
1774
+ : 'This rejected claim is excluded from retrieval and knowledge compilation.',
1646
1775
  });
1647
1776
  }
1648
1777
 
@@ -1688,7 +1817,13 @@ export async function createMemorixServer(
1688
1817
  if (action === 'workflow_import') {
1689
1818
  const result = await importWindsurfWorkflows({ workspace, projectRoot });
1690
1819
  return text({
1691
- imported: result.imported.map(workflow => ({ id: workflow.id, title: workflow.title, sourcePath: workflow.sourcePath })),
1820
+ imported: result.imported.map(workflow => ({
1821
+ id: workflow.id,
1822
+ title: workflow.title,
1823
+ sourcePath: workflow.sourcePath,
1824
+ importedFrom: workflow.importedFrom,
1825
+ verificationGates: workflow.verificationGates,
1826
+ })),
1692
1827
  skipped: result.skipped,
1693
1828
  });
1694
1829
  }
@@ -1702,6 +1837,8 @@ export async function createMemorixServer(
1702
1837
  status: workflow.status,
1703
1838
  taskLenses: workflow.taskLenses,
1704
1839
  sourcePath: workflow.sourcePath,
1840
+ importedFrom: workflow.importedFrom,
1841
+ verificationGates: workflow.verificationGates,
1705
1842
  })),
1706
1843
  parseErrors: synced.errors,
1707
1844
  });
@@ -1797,9 +1934,15 @@ export async function createMemorixServer(
1797
1934
  },
1798
1935
  },
1799
1936
  async ({ ids, status }) => {
1800
- const { resolveObservations } = await import('./memory/observations.js');
1937
+ const { resolveObservations, getObservation } = await import('./memory/observations.js');
1801
1938
  const safeIds = (Array.isArray(ids) ? ids : [ids]).map(id => coerceNumber(id, 0)).filter(id => id > 0);
1802
- const result = await resolveObservations(safeIds, (status as 'resolved' | 'archived') ?? 'resolved');
1939
+ const reader = getObservationReader();
1940
+ const authorizedIds = safeIds.filter((id) => {
1941
+ const observation = getObservation(id, project.id);
1942
+ return observation ? canManageObservation(observation, reader) : false;
1943
+ });
1944
+ const result = await resolveObservations(authorizedIds, (status as 'resolved' | 'archived') ?? 'resolved');
1945
+ const deniedCount = safeIds.length - authorizedIds.length;
1803
1946
 
1804
1947
  const parts: string[] = [];
1805
1948
  if (result.resolved.length > 0) {
@@ -1808,6 +1951,9 @@ export async function createMemorixServer(
1808
1951
  if (result.notFound.length > 0) {
1809
1952
  parts.push(`[WARN] Not found: #${result.notFound.join(', #')}`);
1810
1953
  }
1954
+ if (deniedCount > 0) {
1955
+ parts.push(`[WARN] Skipped ${deniedCount} observation(s) outside this session's write scope.`);
1956
+ }
1811
1957
  parts.push('\nResolved memories are hidden from default search. Use status="all" to include them.');
1812
1958
  parts.push('[STATS] Run `memorix_retention` with `action: "report"` to check remaining cleanup status.');
1813
1959
 
@@ -1853,6 +1999,7 @@ export async function createMemorixServer(
1853
1999
  async ({ entityName, decision, alternatives, rationale, constraints, expectedOutcome, risks, concepts, filesModified, relatedCommits, relatedEntities }) => {
1854
2000
  const unresolved = requireResolvedProject('store reasoning in the current project');
1855
2001
  if (unresolved) return unresolved;
2002
+ const reader = getObservationReader();
1856
2003
  return withFreshIndex(async () => {
1857
2004
 
1858
2005
  // Build structured narrative from reasoning fields
@@ -1882,7 +2029,11 @@ export async function createMemorixServer(
1882
2029
  // ── Attribution guard (passive, non-blocking) ─────────────────
1883
2030
  let reasoningAttributionWarning = '';
1884
2031
  try {
1885
- const attrCheck = await checkProjectAttribution(entityName, project.id, getAllObservations());
2032
+ const attrCheck = await checkProjectAttribution(
2033
+ entityName,
2034
+ project.id,
2035
+ filterReadableObservations(getAllObservations(), reader),
2036
+ );
1886
2037
  if (attrCheck.suspicious) {
1887
2038
  reasoningAttributionWarning = `\n[WARN] Attribution notice: entity "${entityName}" has 0 observations in ` +
1888
2039
  `"${project.id}" but ${attrCheck.count} in "${attrCheck.knownIn}" ` +
@@ -1904,6 +2055,7 @@ export async function createMemorixServer(
1904
2055
  relatedEntities,
1905
2056
  sourceDetail: 'explicit',
1906
2057
  createdByAgentId: currentAgentId,
2058
+ visibility: 'project',
1907
2059
  });
1908
2060
 
1909
2061
  await graphManager.addObservations([
@@ -1947,7 +2099,11 @@ export async function createMemorixServer(
1947
2099
  const minCount = threshold ?? 2;
1948
2100
  let entries: import('./memory/attribution-guard.js').AuditEntry[];
1949
2101
  try {
1950
- entries = await auditProjectObservations(project.id, await withFreshIndex(() => getAllObservations()), minCount);
2102
+ entries = await auditProjectObservations(
2103
+ project.id,
2104
+ await withFreshIndex(() => filterReadableObservations(getAllObservations(), getObservationReader())),
2105
+ minCount,
2106
+ );
1951
2107
  } catch (err) {
1952
2108
  return {
1953
2109
  content: [{
@@ -2030,6 +2186,7 @@ export async function createMemorixServer(
2030
2186
  type: 'reasoning' as ObservationType,
2031
2187
  projectId: scope === 'global' ? undefined : project.id,
2032
2188
  status: 'active',
2189
+ reader: getObservationReader(scope === 'global' ? 'global' : 'project'),
2033
2190
  }));
2034
2191
 
2035
2192
  if (result.entries.length === 0) {
@@ -2066,7 +2223,11 @@ export async function createMemorixServer(
2066
2223
  },
2067
2224
  async ({ query, dryRun }) => {
2068
2225
  const { getAllObservations, resolveObservations } = await import('./memory/observations.js');
2069
- const allObs = await withFreshIndex(() => getAllObservations().filter(o => (o.status ?? 'active') === 'active' && o.projectId === project.id));
2226
+ const reader = getObservationReader();
2227
+ const allObs = await withFreshIndex(() => filterReadableObservations(
2228
+ getAllObservations().filter(o => (o.status ?? 'active') === 'active' && o.projectId === project.id),
2229
+ reader,
2230
+ ).filter((observation) => canManageObservation(observation, reader)));
2070
2231
 
2071
2232
  if (allObs.length < 2) {
2072
2233
  return { content: [{ type: 'text' as const, text: 'Not enough active memories to deduplicate.' }] };
@@ -2085,7 +2246,7 @@ export async function createMemorixServer(
2085
2246
  // If query provided, search for relevant memories; otherwise take latest 20
2086
2247
  let candidates: typeof allObs;
2087
2248
  if (query) {
2088
- const searchResult = await compactSearch({ query, limit: 20, projectId: project.id, status: 'active' });
2249
+ const searchResult = await compactSearch({ query, limit: 20, projectId: project.id, status: 'active', reader });
2089
2250
  const idSet = new Set(searchResult.entries.map(e => e.id));
2090
2251
  candidates = allObs.filter(o => idSet.has(o.id));
2091
2252
  } else {
@@ -2191,6 +2352,7 @@ export async function createMemorixServer(
2191
2352
  project.id,
2192
2353
  safeBefore,
2193
2354
  safeAfter,
2355
+ getObservationReader(),
2194
2356
  );
2195
2357
 
2196
2358
  return {
@@ -2241,14 +2403,17 @@ export async function createMemorixServer(
2241
2403
  // Priority: typedRefs > refs > ids (each is a complete, homogeneous input path)
2242
2404
  let result;
2243
2405
  try {
2406
+ const hasCrossProjectRef = safeRefs.some((ref) => ref.projectId && ref.projectId !== project.id)
2407
+ || safeTypedRefs.some((ref) => ref.includes('@') && !ref.endsWith(`@${project.id}`));
2408
+ const reader = getObservationReader(hasCrossProjectRef ? 'global' : 'project');
2244
2409
  if (safeTypedRefs.length > 0) {
2245
2410
  // Pass typed ref strings directly — compactDetail handles parsing
2246
- result = await compactDetail(safeTypedRefs);
2411
+ result = await compactDetail(safeTypedRefs, { reader });
2247
2412
  } else if (safeRefs.length > 0) {
2248
- result = await compactDetail(safeRefs);
2413
+ result = await compactDetail(safeRefs, { reader });
2249
2414
  } else {
2250
2415
  // Bare numeric IDs are scoped to the current project
2251
- result = await compactDetail(safeIds.map(id => ({ id, projectId: project.id })));
2416
+ result = await compactDetail(safeIds.map(id => ({ id, projectId: project.id })), { reader });
2252
2417
  }
2253
2418
  } catch (err) {
2254
2419
  return { content: [{ type: 'text' as const, text: err instanceof Error ? err.message : String(err) }], isError: true };
@@ -2297,13 +2462,17 @@ export async function createMemorixServer(
2297
2462
  },
2298
2463
  async (args: { action?: string }) => {
2299
2464
  const action = args.action ?? 'report';
2300
- const { getRetentionSummary, getArchiveCandidates, rankByRelevance, archiveExpired, getRetentionZone, explainRetention } = await import('./memory/retention.js');
2465
+ const { getRetentionSummary, getArchiveCandidates, rankByRelevance, getRetentionZone, explainRetention } = await import('./memory/retention.js');
2301
2466
  const { getDb } = await import('./store/orama-store.js');
2302
2467
  const { search } = await import('@orama/orama');
2303
2468
 
2304
2469
  // Shared: build MemorixDocument[] from in-memory observations
2305
- const { getAllObservations } = await import('./memory/observations.js');
2306
- const allObs = await withFreshIndex(() => getAllObservations());
2470
+ const { getAllObservations, resolveObservations } = await import('./memory/observations.js');
2471
+ const reader = getObservationReader();
2472
+ const allObs = await withFreshIndex(() => filterReadableObservations(
2473
+ getAllObservations().filter((observation) => observation.projectId === project.id),
2474
+ reader,
2475
+ ));
2307
2476
 
2308
2477
  // Pull current access metadata from the live Orama index so access-based
2309
2478
  // immunity (e.g. accessCount >= 3) still works in retention/report/archive
@@ -2327,19 +2496,6 @@ export async function createMemorixServer(
2327
2496
  // less precise immunity/reporting.
2328
2497
  }
2329
2498
 
2330
- // Handle archive action
2331
- if (action === 'archive') {
2332
- const result = await archiveExpired(projectDir, undefined, accessMap, project.id);
2333
- if (result.archived === 0) {
2334
- return {
2335
- content: [{ type: 'text' as const, text: '[OK] No expired observations to archive. All memories are within their retention period.' }],
2336
- };
2337
- }
2338
- return {
2339
- content: [{ type: 'text' as const, text: `[ARCHIVED] Archived ${result.archived} expired observations (status set to 'archived' in-place)\n${result.remaining} active observations remaining.\n\nArchived memories are hidden from default search but can be found with status: "all".` }],
2340
- };
2341
- }
2342
-
2343
2499
  const docs: import('./types.js').MemorixDocument[] = allObs.map(obs => ({
2344
2500
  id: `obs-${obs.id}`,
2345
2501
  observationId: obs.id,
@@ -2359,6 +2515,11 @@ export async function createMemorixServer(
2359
2515
  source: obs.source ?? 'agent',
2360
2516
  sourceDetail: obs.sourceDetail ?? '',
2361
2517
  valueCategory: obs.valueCategory ?? '',
2518
+ admissionState: obs.admissionState ?? '',
2519
+ admissionReason: obs.admissionReason ?? '',
2520
+ visibility: obs.visibility ?? 'project',
2521
+ createdByAgentId: obs.createdByAgentId ?? '',
2522
+ sharedWithAgentIds: JSON.stringify(obs.sharedWithAgentIds ?? []),
2362
2523
  }));
2363
2524
 
2364
2525
  if (docs.length === 0) {
@@ -2367,6 +2528,24 @@ export async function createMemorixServer(
2367
2528
  };
2368
2529
  }
2369
2530
 
2531
+ if (action === 'archive') {
2532
+ const managedIds = new Set(
2533
+ allObs
2534
+ .filter((observation) => canManageObservation(observation, reader))
2535
+ .map((observation) => observation.id),
2536
+ );
2537
+ const candidates = getArchiveCandidates(docs).filter((document) => managedIds.has(document.observationId));
2538
+ if (candidates.length === 0) {
2539
+ return {
2540
+ content: [{ type: 'text' as const, text: '[OK] No expired memories in this session\'s write scope to archive.' }],
2541
+ };
2542
+ }
2543
+ const result = await resolveObservations(candidates.map((document) => document.observationId), 'archived');
2544
+ return {
2545
+ content: [{ type: 'text' as const, text: `[ARCHIVED] Archived ${result.resolved.length} expired observation(s) in this session's write scope.\n${Math.max(0, managedIds.size - result.resolved.length)} visible writable observations remaining.` }],
2546
+ };
2547
+ }
2548
+
2370
2549
  // ── action="stale": full table of stale observations with explanation ──
2371
2550
  if (action === 'stale') {
2372
2551
  const staleDocs = docs.filter(d => getRetentionZone(d) === 'stale');
@@ -2739,15 +2918,14 @@ export async function createMemorixServer(
2739
2918
  async function scopeGraphToProject(graph: { entities: any[]; relations: any[] }) {
2740
2919
  const { getAllObservations } = await import('./memory/observations.js');
2741
2920
  const allObs = await withFreshIndex(() => getAllObservations());
2742
- const projectEntityNames = new Set(
2743
- allObs
2744
- .filter(o => o.projectId === project.id && (o.status ?? 'active') === 'active' && o.entityName)
2745
- .map(o => o.entityName),
2921
+ const scoped = scopeKnowledgeGraphToProject(
2922
+ graph,
2923
+ filterReadableObservations(
2924
+ allObs.filter(observation => observation.projectId === project.id),
2925
+ getObservationReader(),
2926
+ ),
2746
2927
  );
2747
- const entities = graph.entities.filter((e: any) => projectEntityNames.has(e.name));
2748
- const entityNameSet = new Set(entities.map((e: any) => e.name));
2749
- const relations = graph.relations.filter((r: any) => entityNameSet.has(r.from) && entityNameSet.has(r.to));
2750
- return { entities, relations };
2928
+ return { entities: scoped.entities, relations: scoped.relations };
2751
2929
  }
2752
2930
 
2753
2931
  /** read_graph — MCP Official compatible */
@@ -3087,12 +3265,10 @@ export async function createMemorixServer(
3087
3265
 
3088
3266
  // action === 'generate'
3089
3267
  const { getObservationStore: getStore } = await import('./store/obs-store.js');
3090
- const allObs = await getStore().loadAll() as Array<{
3091
- id?: number; entityName?: string; type?: string; title?: string;
3092
- narrative?: string; facts?: string[]; concepts?: string[];
3093
- filesModified?: string[]; createdAt?: string;
3094
- status?: string; source?: 'agent' | 'git' | 'manual';
3095
- }>;
3268
+ const allObs = filterReadableObservations(
3269
+ (await getStore().loadAll()).filter((observation) => observation.projectId === project.id),
3270
+ getObservationReader(),
3271
+ );
3096
3272
 
3097
3273
  const obsData = allObs.map(o => ({
3098
3274
  id: o.id || 0,
@@ -3224,7 +3400,11 @@ export async function createMemorixServer(
3224
3400
  // Load observations by ID — only active observations can be promoted
3225
3401
  const { getAllObservations } = await import('./memory/observations.js');
3226
3402
  const allObs = await withFreshIndex(() => getAllObservations());
3227
- const matched = allObs.filter(o => observationIds.includes(o.id));
3403
+ const reader = getObservationReader();
3404
+ const matched = filterReadableObservations(
3405
+ allObs.filter((observation) => observation.projectId === project.id && observationIds.includes(observation.id)),
3406
+ reader,
3407
+ );
3228
3408
 
3229
3409
  if (matched.length === 0) {
3230
3410
  return { content: [{ type: 'text' as const, text: `No observations found for IDs: [${observationIds.join(', ')}]. Use \`memorix_search\` to find valid IDs.` }], isError: true };
@@ -3236,6 +3416,14 @@ export async function createMemorixServer(
3236
3416
  return { content: [{ type: 'text' as const, text: `Cannot promote: ${nonActive.length} observation(s) are not active: ${nonActive.map(o => `#${o.id} (${o.status})`).join(', ')}. Only active observations can be promoted to permanent knowledge.` }], isError: true };
3237
3417
  }
3238
3418
 
3419
+ const nonProjectShared = matched.filter((observation) => resolveObservationVisibility(observation) !== 'project');
3420
+ if (nonProjectShared.length > 0) {
3421
+ return {
3422
+ content: [{ type: 'text' as const, text: `Cannot promote private or team-scoped observations: ${nonProjectShared.map((observation) => `#${observation.id}`).join(', ')}. Promote only deliberate project-shared knowledge.` }],
3423
+ isError: true,
3424
+ };
3425
+ }
3426
+
3239
3427
  const skill = await promoteToMiniSkill(projectDir, project.id, matched, { trigger, instruction, tags });
3240
3428
 
3241
3429
  const lines = [
@@ -3469,8 +3657,11 @@ export async function createMemorixServer(
3469
3657
  const lastSeen = registeredAgent.last_seen_obs_generation;
3470
3658
  const store = getObservationStore();
3471
3659
  const currentGen = store.getGeneration();
3472
- const projectObs = await withFreshIndex(() => getAllObservations().filter(
3473
- o => o.projectId === project.id && (o.writeGeneration ?? 0) > lastSeen,
3660
+ const projectObs = await withFreshIndex(() => filterReadableObservations(
3661
+ getAllObservations().filter(
3662
+ o => o.projectId === project.id && (o.writeGeneration ?? 0) > lastSeen,
3663
+ ),
3664
+ getObservationReader(),
3474
3665
  ));
3475
3666
  const wm = computeWatermark(lastSeen, currentGen, projectObs.length);
3476
3667
  if (wm.newObservationCount > 0) {
@@ -3673,7 +3864,7 @@ export async function createMemorixServer(
3673
3864
  title: 'Transfer Memories',
3674
3865
  description:
3675
3866
  'Export or import project memories. ' +
3676
- 'Action "export": export observations and sessions (JSON or Markdown). ' +
3867
+ 'Action "export": export observations visible to the current agent and project sessions (JSON or Markdown). ' +
3677
3868
  'Action "import": import from a JSON export (re-assigns IDs, skips duplicate topicKeys).',
3678
3869
  inputSchema: {
3679
3870
  action: z.enum(['export', 'import']).describe('Operation: export or import'),
@@ -3685,10 +3876,10 @@ export async function createMemorixServer(
3685
3876
  if (action === 'export') {
3686
3877
  const { exportAsJson, exportAsMarkdown } = await import('./memory/export-import.js');
3687
3878
  if (format === 'markdown') {
3688
- const md = await exportAsMarkdown(projectDir, project.id);
3879
+ const md = await exportAsMarkdown(projectDir, project.id, getObservationReader());
3689
3880
  return { content: [{ type: 'text' as const, text: md }] };
3690
3881
  }
3691
- const data = await exportAsJson(projectDir, project.id);
3882
+ const data = await exportAsJson(projectDir, project.id, getObservationReader());
3692
3883
  const json = JSON.stringify(data, null, 2);
3693
3884
  return {
3694
3885
  content: [{
@@ -3910,8 +4101,6 @@ export async function createMemorixServer(
3910
4101
 
3911
4102
  // Use shared TeamStore (from HTTP server) or create new one (stdio mode).
3912
4103
  // All team state is canonical in SQLite — no JSON persistence, no sync/flush.
3913
- let teamStore!: import('./team/team-store.js').TeamStore;
3914
- let initTeamStoreForProject: ((dataDir: string) => Promise<import('./team/team-store.js').TeamStore>) | undefined;
3915
4104
  if (teamFeaturesEnabled) {
3916
4105
  const { initTeamStore } = await import('./team/team-store.js');
3917
4106
  initTeamStoreForProject = initTeamStore;
@@ -4197,7 +4386,7 @@ export async function createMemorixServer(
4197
4386
  'Action "inbox": read this agent\'s inbox.',
4198
4387
  inputSchema: {
4199
4388
  action: z.enum(['send', 'broadcast', 'inbox']).describe('Operation to perform'),
4200
- from: z.string().optional().describe('Sender agent ID (for send/broadcast)'),
4389
+ from: z.string().optional().describe('Your sender agent ID. Omit it to use this session identity.'),
4201
4390
  to: z.string().optional().describe('Receiver agent ID (for send)'),
4202
4391
  type: z.enum(['request', 'response', 'info', 'announcement', 'contract', 'error', 'handoff']).optional().describe('Message type (for send/broadcast)'),
4203
4392
  content: z.string().optional().describe('Message content (for send/broadcast)'),
@@ -4208,13 +4397,20 @@ export async function createMemorixServer(
4208
4397
  },
4209
4398
  },
4210
4399
  async ({ action, from, to, type: msgType, content, agentId, markRead, toRole, handoffStatus }) => {
4400
+ const requireCurrentTeamAgent = () => {
4401
+ if (!currentAgentId) return null;
4402
+ const agent = teamStore.getAgent(currentAgentId);
4403
+ return agent?.project_id === project.id && agent.status === 'active' ? agent : null;
4404
+ };
4211
4405
  if (action === 'send') {
4212
- if (!from || !msgType || !content) return { content: [{ type: 'text' as const, text: '[ERROR] from, type, and content required for send' }], isError: true };
4406
+ const sender = requireCurrentTeamAgent();
4407
+ if (!sender || !msgType || !content) return { content: [{ type: 'text' as const, text: '[ERROR] active session identity, type, and content required for send' }], isError: true };
4408
+ if (from && from !== currentAgentId) return { content: [{ type: 'text' as const, text: '[ERROR] from must match the current session identity' }], isError: true };
4213
4409
  if (!to && !toRole) return { content: [{ type: 'text' as const, text: '[ERROR] either to (agent ID) or toRole is required for send' }], isError: true };
4214
4410
  if (content.length > 10_000) return { content: [{ type: 'text' as const, text: '[ERROR] Message too large (max 10KB)' }], isError: true };
4215
4411
  const msg = teamStore.sendMessage({
4216
4412
  projectId: project.id,
4217
- senderAgentId: from,
4413
+ senderAgentId: currentAgentId!,
4218
4414
  recipientAgentId: to ?? null,
4219
4415
  type: msgType,
4220
4416
  content,
@@ -4226,11 +4422,13 @@ export async function createMemorixServer(
4226
4422
  return { content: [{ type: 'text' as const, text: `Message sent (${msgType}) to ${target} | ID: ${msg.id.slice(0, 8)}…${toRole ? ` [role: ${toRole}]` : ''}` }] };
4227
4423
  }
4228
4424
  if (action === 'broadcast') {
4229
- if (!from || !msgType || !content) return { content: [{ type: 'text' as const, text: '[ERROR] from, type, and content required for broadcast' }], isError: true };
4425
+ const sender = requireCurrentTeamAgent();
4426
+ if (!sender || !msgType || !content) return { content: [{ type: 'text' as const, text: '[ERROR] active session identity, type, and content required for broadcast' }], isError: true };
4427
+ if (from && from !== currentAgentId) return { content: [{ type: 'text' as const, text: '[ERROR] from must match the current session identity' }], isError: true };
4230
4428
  if (content.length > 10_000) return { content: [{ type: 'text' as const, text: '[ERROR] Message too large (max 10KB)' }], isError: true };
4231
4429
  const msg = teamStore.sendMessage({
4232
4430
  projectId: project.id,
4233
- senderAgentId: from,
4431
+ senderAgentId: currentAgentId!,
4234
4432
  recipientAgentId: null,
4235
4433
  type: msgType,
4236
4434
  content,
@@ -4239,8 +4437,12 @@ export async function createMemorixServer(
4239
4437
  return { content: [{ type: 'text' as const, text: `Broadcast (${msgType}) | ID: ${msg.id.slice(0, 8)}…` }] };
4240
4438
  }
4241
4439
  // inbox
4242
- const inboxId = agentId || from || '';
4243
- if (!inboxId) return { content: [{ type: 'text' as const, text: '[ERROR] agentId required for inbox' }], isError: true };
4440
+ const inboxAgent = requireCurrentTeamAgent();
4441
+ if (!inboxAgent) return { content: [{ type: 'text' as const, text: '[ERROR] active session identity required for inbox' }], isError: true };
4442
+ if ((agentId && agentId !== currentAgentId) || (from && from !== currentAgentId)) {
4443
+ return { content: [{ type: 'text' as const, text: '[ERROR] inbox access is limited to the current session identity' }], isError: true };
4444
+ }
4445
+ const inboxId = currentAgentId!;
4244
4446
  const inbox = teamStore.getInbox(project.id, inboxId);
4245
4447
  const unread = teamStore.getUnreadCount(project.id, inboxId);
4246
4448
  if (inbox.length === 0) return { content: [{ type: 'text' as const, text: 'Inbox empty' }] };
@@ -4272,32 +4474,42 @@ export async function createMemorixServer(
4272
4474
  },
4273
4475
  async ({ agentId, markInboxRead }) => {
4274
4476
  const { computeWatermark, computePoll } = await import('./team/poll.js');
4477
+ if (agentId && agentId !== currentAgentId) {
4478
+ return {
4479
+ content: [{ type: 'text' as const, text: '[ERROR] agentId must match the current session identity.' }],
4480
+ isError: true as const,
4481
+ };
4482
+ }
4483
+ const effectiveAgentId = currentAgentId;
4275
4484
 
4276
4485
  // Compute watermark — requires observation store access
4277
4486
  let watermark = computeWatermark(0, 0, 0);
4278
- if (agentId) {
4279
- const agent = teamStore.getAgent(agentId);
4487
+ if (effectiveAgentId) {
4488
+ const agent = teamStore.getAgent(effectiveAgentId);
4280
4489
  if (agent) {
4281
4490
  const lastSeen = agent.last_seen_obs_generation;
4282
4491
  const store = getObservationStore();
4283
4492
  const currentGen = store.getGeneration();
4284
- const projectObs = await withFreshIndex(() => getAllObservations().filter(
4285
- o => o.projectId === project.id && (o.writeGeneration ?? 0) > lastSeen,
4493
+ const projectObs = await withFreshIndex(() => filterReadableObservations(
4494
+ getAllObservations().filter(
4495
+ o => o.projectId === project.id && (o.writeGeneration ?? 0) > lastSeen,
4496
+ ),
4497
+ getObservationReader(),
4286
4498
  ));
4287
4499
  watermark = computeWatermark(lastSeen, currentGen, projectObs.length);
4288
4500
 
4289
4501
  // Advance watermark so next poll sees only truly new observations
4290
- teamStore.updateWatermark(agentId, currentGen);
4502
+ teamStore.updateWatermark(effectiveAgentId, currentGen);
4291
4503
  // Heartbeat — proves this agent is alive
4292
- teamStore.heartbeat(agentId);
4504
+ teamStore.heartbeat(effectiveAgentId);
4293
4505
  }
4294
4506
  }
4295
4507
 
4296
- const poll = computePoll(teamStore, project.id, agentId ?? null, watermark);
4508
+ const poll = computePoll(teamStore, project.id, effectiveAgentId ?? null, watermark);
4297
4509
 
4298
4510
  // Optionally mark inbox as read
4299
- if (markInboxRead && agentId) {
4300
- teamStore.markAllRead(project.id, agentId);
4511
+ if (markInboxRead && effectiveAgentId) {
4512
+ teamStore.markAllRead(project.id, effectiveAgentId);
4301
4513
  }
4302
4514
 
4303
4515
  // Format as readable text
@@ -4370,7 +4582,8 @@ export async function createMemorixServer(
4370
4582
  title: 'Team Handoff — Agent Context Transfer',
4371
4583
  description:
4372
4584
  'Create a structured handoff artifact when passing work to another agent. ' +
4373
- 'The handoff is stored as a durable observation (searchable, immune to archival) ' +
4585
+ 'A targeted handoff is visible only to its sender and recipient; a broadcast handoff is team-visible. ' +
4586
+ 'The handoff is stored as a durable observation (immune to archival) ' +
4374
4587
  'and a notification message is sent to the recipient. ' +
4375
4588
  'Use this when completing a task and another agent should continue, ' +
4376
4589
  'or when you want to leave context for whoever works on this next.',
@@ -4386,6 +4599,34 @@ export async function createMemorixServer(
4386
4599
  },
4387
4600
  async ({ fromAgentId, summary, context, toAgentId, taskId, filesModified, concepts }) => {
4388
4601
  const { createHandoffArtifact } = await import('./team/handoff.js');
4602
+ if (!currentAgentId) {
4603
+ return {
4604
+ content: [{ type: 'text' as const, text: 'Create a coordination identity first: call memorix_session_start with joinTeam=true.' }],
4605
+ isError: true as const,
4606
+ };
4607
+ }
4608
+ if (fromAgentId !== currentAgentId) {
4609
+ return {
4610
+ content: [{ type: 'text' as const, text: 'fromAgentId must match the identity returned for this session. Memorix will not create a handoff on behalf of another agent.' }],
4611
+ isError: true as const,
4612
+ };
4613
+ }
4614
+ const sender = teamStore.getAgent(currentAgentId);
4615
+ if (!sender || sender.project_id !== project.id || sender.status !== 'active') {
4616
+ return {
4617
+ content: [{ type: 'text' as const, text: 'The current coordination identity is not an active member of this project.' }],
4618
+ isError: true as const,
4619
+ };
4620
+ }
4621
+ if (toAgentId) {
4622
+ const recipient = teamStore.getAgent(toAgentId);
4623
+ if (!recipient || recipient.project_id !== project.id) {
4624
+ return {
4625
+ content: [{ type: 'text' as const, text: 'The handoff recipient must be an agent registered in the current project.' }],
4626
+ isError: true as const,
4627
+ };
4628
+ }
4629
+ }
4389
4630
 
4390
4631
  const result = await createHandoffArtifact(
4391
4632
  {