memorix 1.2.1 → 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 (199) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +14 -2
  3. package/README.zh-CN.md +14 -2
  4. package/TEAM.md +86 -86
  5. package/dist/cli/index.js +15407 -13779
  6. package/dist/cli/index.js.map +1 -1
  7. package/dist/index.js +1321 -529
  8. package/dist/index.js.map +1 -1
  9. package/dist/maintenance-runner.d.ts +1 -1
  10. package/dist/maintenance-runner.js +8458 -8087
  11. package/dist/maintenance-runner.js.map +1 -1
  12. package/dist/memcode-runtime/CHANGELOG.md +16 -0
  13. package/dist/sdk.d.ts +7 -2
  14. package/dist/sdk.js +1349 -535
  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 +24 -4
  21. package/docs/DESIGN_DECISIONS.md +357 -357
  22. package/docs/README.md +1 -1
  23. package/docs/dev-log/progress.txt +91 -11
  24. package/package.json +1 -1
  25. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  26. package/src/audit/index.ts +156 -156
  27. package/src/cli/command-guide.ts +192 -0
  28. package/src/cli/commands/audit-list.ts +89 -89
  29. package/src/cli/commands/audit.ts +9 -4
  30. package/src/cli/commands/background.ts +659 -659
  31. package/src/cli/commands/cleanup.ts +5 -1
  32. package/src/cli/commands/codegraph.ts +15 -5
  33. package/src/cli/commands/context.ts +3 -2
  34. package/src/cli/commands/doctor.ts +4 -2
  35. package/src/cli/commands/explain.ts +9 -3
  36. package/src/cli/commands/formation.ts +48 -48
  37. package/src/cli/commands/git-hook-install.ts +111 -111
  38. package/src/cli/commands/handoff.ts +75 -61
  39. package/src/cli/commands/hooks-status.ts +63 -63
  40. package/src/cli/commands/identity.ts +116 -0
  41. package/src/cli/commands/ingest-commit.ts +153 -153
  42. package/src/cli/commands/ingest-image.ts +71 -69
  43. package/src/cli/commands/ingest-log.ts +180 -180
  44. package/src/cli/commands/ingest.ts +44 -44
  45. package/src/cli/commands/integrate-shared.ts +15 -15
  46. package/src/cli/commands/lock.ts +93 -92
  47. package/src/cli/commands/memory.ts +58 -21
  48. package/src/cli/commands/message.ts +123 -118
  49. package/src/cli/commands/operator-shared.ts +98 -3
  50. package/src/cli/commands/poll.ts +74 -64
  51. package/src/cli/commands/purge-all-memory.ts +85 -85
  52. package/src/cli/commands/purge-project-memory.ts +83 -83
  53. package/src/cli/commands/reasoning.ts +135 -121
  54. package/src/cli/commands/retention.ts +9 -4
  55. package/src/cli/commands/serve-http.ts +8 -2
  56. package/src/cli/commands/serve-shared.ts +118 -118
  57. package/src/cli/commands/session.ts +29 -3
  58. package/src/cli/commands/skills.ts +124 -119
  59. package/src/cli/commands/status.ts +4 -3
  60. package/src/cli/commands/task.ts +193 -184
  61. package/src/cli/commands/team.ts +14 -10
  62. package/src/cli/commands/transfer.ts +108 -55
  63. package/src/cli/commands/uninstall-project-artifacts.ts +85 -85
  64. package/src/cli/identity.ts +89 -0
  65. package/src/cli/index.ts +96 -19
  66. package/src/cli/invocation.ts +115 -0
  67. package/src/cli/tui/ChatView.tsx +234 -234
  68. package/src/cli/tui/CommandBar.tsx +312 -312
  69. package/src/cli/tui/ContextRail.tsx +118 -118
  70. package/src/cli/tui/HeaderBar.tsx +72 -72
  71. package/src/cli/tui/LogoBanner.tsx +51 -51
  72. package/src/cli/tui/Sidebar.tsx +179 -179
  73. package/src/cli/tui/chat-service.ts +41 -18
  74. package/src/cli/tui/data.ts +23 -44
  75. package/src/cli/tui/index.ts +41 -41
  76. package/src/cli/tui/markdown-render.tsx +371 -371
  77. package/src/cli/tui/operator-context.ts +60 -0
  78. package/src/cli/tui/use-mouse.ts +157 -157
  79. package/src/cli/tui/useNavigation.ts +56 -56
  80. package/src/cli/tui/views/MemoryView.tsx +10 -8
  81. package/src/cli/update-checker.ts +211 -211
  82. package/src/cli/version.ts +7 -7
  83. package/src/cli/workbench.ts +1 -1
  84. package/src/codegraph/auto-context.ts +31 -2
  85. package/src/codegraph/context-pack.ts +1 -0
  86. package/src/codegraph/project-context.ts +2 -0
  87. package/src/compact/engine.ts +26 -10
  88. package/src/compact/index-format.ts +25 -2
  89. package/src/compact/token-budget.ts +74 -74
  90. package/src/dashboard/project-classification.ts +64 -64
  91. package/src/dashboard/server.ts +46 -9
  92. package/src/embedding/fastembed-provider.ts +142 -142
  93. package/src/embedding/transformers-provider.ts +111 -111
  94. package/src/git/extractor.ts +209 -209
  95. package/src/git/hooks-path.ts +85 -85
  96. package/src/hooks/admission.ts +117 -0
  97. package/src/hooks/handler.ts +98 -91
  98. package/src/hooks/pattern-detector.ts +173 -173
  99. package/src/hooks/significance-filter.ts +250 -250
  100. package/src/knowledge/context-assembly.ts +97 -0
  101. package/src/knowledge/workset.ts +179 -10
  102. package/src/llm/memory-manager.ts +328 -328
  103. package/src/llm/provider.ts +885 -885
  104. package/src/llm/quality.ts +248 -248
  105. package/src/memory/admission.ts +57 -0
  106. package/src/memory/attribution-guard.ts +249 -249
  107. package/src/memory/consolidation.ts +13 -2
  108. package/src/memory/disclosure-policy.ts +140 -135
  109. package/src/memory/entity-extractor.ts +197 -197
  110. package/src/memory/export-import.ts +11 -3
  111. package/src/memory/formation/evaluate.ts +217 -217
  112. package/src/memory/formation/extract.ts +361 -361
  113. package/src/memory/formation/index.ts +417 -417
  114. package/src/memory/formation/resolve.ts +344 -344
  115. package/src/memory/formation/types.ts +315 -315
  116. package/src/memory/freshness.ts +122 -122
  117. package/src/memory/graph-context.ts +8 -2
  118. package/src/memory/graph.ts +197 -197
  119. package/src/memory/observations.ts +162 -4
  120. package/src/memory/quality-audit.ts +2 -0
  121. package/src/memory/refs.ts +94 -94
  122. package/src/memory/retention.ts +22 -2
  123. package/src/memory/secret-filter.ts +79 -79
  124. package/src/memory/session.ts +5 -2
  125. package/src/memory/visibility.ts +80 -0
  126. package/src/multimodal/image-loader.ts +143 -143
  127. package/src/orchestrate/adapters/claude-stream.ts +192 -192
  128. package/src/orchestrate/adapters/claude.ts +111 -111
  129. package/src/orchestrate/adapters/codex-stream.ts +134 -134
  130. package/src/orchestrate/adapters/codex.ts +41 -41
  131. package/src/orchestrate/adapters/gemini-stream.ts +166 -166
  132. package/src/orchestrate/adapters/gemini.ts +42 -42
  133. package/src/orchestrate/adapters/index.ts +73 -73
  134. package/src/orchestrate/adapters/opencode-stream.ts +143 -143
  135. package/src/orchestrate/adapters/opencode.ts +47 -47
  136. package/src/orchestrate/adapters/spawn-helper.ts +286 -286
  137. package/src/orchestrate/adapters/types.ts +77 -77
  138. package/src/orchestrate/capability-router.ts +284 -284
  139. package/src/orchestrate/context-compact.ts +188 -188
  140. package/src/orchestrate/cost-tracker.ts +219 -219
  141. package/src/orchestrate/error-recovery.ts +191 -191
  142. package/src/orchestrate/evidence.ts +140 -140
  143. package/src/orchestrate/ledger.ts +110 -110
  144. package/src/orchestrate/memorix-bridge.ts +378 -340
  145. package/src/orchestrate/output-budget.ts +80 -80
  146. package/src/orchestrate/permission.ts +152 -152
  147. package/src/orchestrate/pipeline-trace.ts +131 -131
  148. package/src/orchestrate/prompt-builder.ts +155 -155
  149. package/src/orchestrate/ring-buffer.ts +37 -37
  150. package/src/orchestrate/task-graph.ts +389 -389
  151. package/src/orchestrate/worktree.ts +232 -232
  152. package/src/project/aliases.ts +374 -374
  153. package/src/project/detector.ts +268 -268
  154. package/src/rules/adapters/claude-code.ts +99 -99
  155. package/src/rules/adapters/codex.ts +97 -97
  156. package/src/rules/adapters/copilot.ts +124 -124
  157. package/src/rules/adapters/cursor.ts +114 -114
  158. package/src/rules/adapters/kiro.ts +126 -126
  159. package/src/rules/adapters/trae.ts +56 -56
  160. package/src/rules/adapters/windsurf.ts +83 -83
  161. package/src/rules/syncer.ts +235 -235
  162. package/src/runtime/control-plane-maintenance.ts +1 -0
  163. package/src/runtime/isolated-maintenance.ts +1 -0
  164. package/src/runtime/lifecycle.ts +18 -0
  165. package/src/runtime/maintenance-jobs.ts +1 -0
  166. package/src/runtime/maintenance-runner.ts +2 -0
  167. package/src/runtime/project-maintenance.ts +89 -0
  168. package/src/sdk.ts +334 -304
  169. package/src/search/intent-detector.ts +289 -289
  170. package/src/search/query-expansion.ts +52 -52
  171. package/src/server/formation-timeout.ts +27 -27
  172. package/src/server.ts +260 -81
  173. package/src/skills/mini-skills.ts +386 -386
  174. package/src/store/chat-store.ts +119 -119
  175. package/src/store/graph-store.ts +249 -249
  176. package/src/store/mini-skill-store.ts +349 -349
  177. package/src/store/orama-store.ts +61 -6
  178. package/src/store/persistence-json.ts +212 -212
  179. package/src/store/persistence.ts +291 -291
  180. package/src/store/project-affinity.ts +195 -195
  181. package/src/store/sqlite-db.ts +23 -1
  182. package/src/store/sqlite-store.ts +12 -2
  183. package/src/team/event-bus.ts +76 -76
  184. package/src/team/file-locks.ts +173 -173
  185. package/src/team/handoff.ts +168 -161
  186. package/src/team/messages.ts +203 -203
  187. package/src/team/poll.ts +132 -132
  188. package/src/team/tasks.ts +211 -211
  189. package/src/types.ts +51 -0
  190. package/src/wiki/generator.ts +2 -0
  191. package/src/workspace/mcp-adapters/codex.ts +191 -191
  192. package/src/workspace/mcp-adapters/copilot.ts +105 -105
  193. package/src/workspace/mcp-adapters/cursor.ts +53 -53
  194. package/src/workspace/mcp-adapters/kiro.ts +64 -64
  195. package/src/workspace/mcp-adapters/opencode.ts +123 -123
  196. package/src/workspace/mcp-adapters/trae.ts +134 -134
  197. package/src/workspace/mcp-adapters/windsurf.ts +91 -91
  198. package/src/workspace/sanitizer.ts +60 -60
  199. package/src/workspace/workflow-sync.ts +131 -131
package/src/server.ts CHANGED
@@ -29,12 +29,13 @@ import { checkProjectAttribution, auditProjectObservations } from './memory/attr
29
29
  import { createAutoRelations } from './memory/auto-relations.js';
30
30
  import { extractEntities } from './memory/entity-extractor.js';
31
31
  import { scopeKnowledgeGraphToProject } from './memory/graph-scope.js';
32
+ import { canManageObservation, canReadObservation, filterReadableObservations, resolveObservationVisibility } from './memory/visibility.js';
32
33
  import { compactSearch, compactTimeline, compactDetail } from './compact/engine.js';
33
34
  import { buildGraphContextPacket, formatGraphContextPrompt } from './memory/graph-context.js';
34
35
  import { detectProject } from './project/detector.js';
35
36
  import { registerAlias, initAliasRegistry, resolveAliases, autoMergeByBaseName } from './project/aliases.js';
36
37
  import { getProjectDataDir } from './store/persistence.js';
37
- import type { ObservationType, RuleSource, AgentTarget, MCPServerEntry } from './types.js';
38
+ import type { ObservationType, RuleSource, AgentTarget, MCPServerEntry, ObservationReader } from './types.js';
38
39
  import { RulesSyncer } from './rules/syncer.js';
39
40
  import { WorkspaceSyncEngine } from './workspace/engine.js';
40
41
  import {
@@ -261,8 +262,10 @@ export async function createMemorixServer(
261
262
  let rawProject: import('./types.js').ProjectInfo;
262
263
  let projectResolved = true;
263
264
  let projectResolutionError: string | null = null;
264
- let explicitProjectBound = false; // Set true when memorix_session_start binds via projectRoot
265
- 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;
266
269
  if (detectedProject) {
267
270
  rawProject = detectedProject;
268
271
  } else {
@@ -582,6 +585,23 @@ export async function createMemorixServer(
582
585
  return (originalRegisterTool as (...innerArgs: unknown[]) => unknown)(name, ...args) as never;
583
586
  }) as typeof server.registerTool;
584
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
+
585
605
  // ================================================================
586
606
  // Memorix Extended Tools (3-layer Progressive Disclosure)
587
607
  // ================================================================
@@ -602,7 +622,7 @@ export async function createMemorixServer(
602
622
  'problem-solution ([FIX] bug fix), how-it-works ([INFO] explanation), what-changed ([CHANGE] change), ' +
603
623
  'discovery ([DISCOVERY] insight), why-it-exists ([WHY] rationale), trade-off ([TRADEOFF] compromise), ' +
604
624
  'session-request ([SESSION] original goal). ' +
605
- '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.',
606
626
  inputSchema: {
607
627
  entityName: z.string().describe('The entity this observation belongs to (e.g., "auth-module", "port-config")'),
608
628
  type: z.enum(OBSERVATION_TYPES).describe('Observation type for classification'),
@@ -623,12 +643,30 @@ export async function createMemorixServer(
623
643
  }).optional().describe('Progress tracking for task/feature observations'),
624
644
  relatedCommits: z.array(z.string()).optional().describe('Git commit hashes this memory relates to (links ground truth ↔ reasoning)'),
625
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
+ ),
626
649
  },
627
650
  },
628
- 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 }) => {
629
652
  const unresolved = requireResolvedProject('store memory in the current project');
630
653
  if (unresolved) return unresolved;
631
- 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 () => {
632
670
 
633
671
  // Mutable copies — Formation Pipeline may improve these
634
672
  let entityName = rawEntityName;
@@ -678,9 +716,9 @@ export async function createMemorixServer(
678
716
  useLLM: isLLMEnabled(),
679
717
  minValueScore: 0.3,
680
718
  searchMemories: async (q: string, limit: number, pid: string): Promise<SearchHit[]> => {
681
- const result = await compactSearch({ query: q, limit, projectId: pid, status: 'active' });
719
+ const result = await compactSearch({ query: q, limit, projectId: pid, status: 'active', reader });
682
720
  if (result.entries.length === 0) return [];
683
- const details = await compactDetail(result.entries.map(e => e.id));
721
+ const details = await compactDetail(result.entries.map(e => e.id), { reader });
684
722
  return details.documents.map((d, i) => ({
685
723
  id: Number(d.id.replace('obs-', '')),
686
724
  observationId: d.observationId,
@@ -694,7 +732,7 @@ export async function createMemorixServer(
694
732
  },
695
733
  getObservation: (id: number) => {
696
734
  const o = getObservation(id);
697
- if (!o) return null;
735
+ if (!o || o.projectId !== project.id || !canReadObservation(o, reader)) return null;
698
736
  return {
699
737
  id: o.id,
700
738
  entityName: o.entityName,
@@ -753,7 +791,7 @@ export async function createMemorixServer(
753
791
  if (action === 'merge' && targetId) {
754
792
  // Merge into existing observation
755
793
  const targetObs = getObservation(targetId);
756
- if (targetObs) {
794
+ if (targetObs && targetObs.projectId === project.id && canReadObservation(targetObs, reader)) {
757
795
  await storeObservation({
758
796
  entityName: targetObs.entityName,
759
797
  type: targetObs.type,
@@ -767,6 +805,8 @@ export async function createMemorixServer(
767
805
  progress: progress as import('./types.js').ProgressInfo | undefined,
768
806
  sourceDetail: 'explicit',
769
807
  createdByAgentId: currentAgentId,
808
+ visibility,
809
+ visibilityReader: reader,
770
810
  });
771
811
  return {
772
812
  content: [{
@@ -778,7 +818,7 @@ export async function createMemorixServer(
778
818
  } else if (action === 'evolve' && targetId) {
779
819
  // Evolve existing observation
780
820
  const targetObs = getObservation(targetId);
781
- if (targetObs) {
821
+ if (targetObs && targetObs.projectId === project.id && canReadObservation(targetObs, reader)) {
782
822
  await storeObservation({
783
823
  entityName: targetObs.entityName,
784
824
  type: targetObs.type,
@@ -792,6 +832,8 @@ export async function createMemorixServer(
792
832
  progress: progress as import('./types.js').ProgressInfo | undefined,
793
833
  sourceDetail: 'explicit',
794
834
  createdByAgentId: currentAgentId,
835
+ visibility,
836
+ visibilityReader: reader,
795
837
  });
796
838
  return {
797
839
  content: [{
@@ -824,12 +866,13 @@ export async function createMemorixServer(
824
866
  limit: 5,
825
867
  projectId: project.id,
826
868
  status: 'active',
869
+ reader,
827
870
  });
828
871
  const similarEntries = searchResult.entries.map(e => e);
829
872
  if (similarEntries.length > 0) {
830
873
  // Fetch full details for comparison
831
874
  const similarIds = similarEntries.map(e => e.id);
832
- const details = await compactDetail(similarIds);
875
+ const details = await compactDetail(similarIds, { reader });
833
876
  const existingMemories: ExistingMemory[] = details.documents.map((d, i) => ({
834
877
  id: d.observationId,
835
878
  title: d.title,
@@ -850,7 +893,7 @@ export async function createMemorixServer(
850
893
  if (decision.action === 'UPDATE' && decision.targetId) {
851
894
  // Merge into existing memory (Mem0-style UPDATE)
852
895
  const targetObs = getObservation(decision.targetId);
853
- if (targetObs) {
896
+ if (targetObs && targetObs.projectId === project.id && canReadObservation(targetObs, reader)) {
854
897
  await storeObservation({
855
898
  entityName: targetObs.entityName,
856
899
  type: targetObs.type,
@@ -864,6 +907,8 @@ export async function createMemorixServer(
864
907
  progress: progress as import('./types.js').ProgressInfo | undefined,
865
908
  sourceDetail: 'explicit',
866
909
  createdByAgentId: currentAgentId,
910
+ visibility,
911
+ visibilityReader: reader,
867
912
  });
868
913
  compactAction = `[UPDATED] Compact UPDATE: merged into #${decision.targetId} (${decision.reason})`;
869
914
  compactMerged = true;
@@ -963,7 +1008,11 @@ export async function createMemorixServer(
963
1008
  // in a different project — signals a potential wrong-bucket write.
964
1009
  let attributionWarning = '';
965
1010
  try {
966
- const attrCheck = await checkProjectAttribution(entityName, project.id, getAllObservations());
1011
+ const attrCheck = await checkProjectAttribution(
1012
+ entityName,
1013
+ project.id,
1014
+ filterReadableObservations(getAllObservations(), reader),
1015
+ );
967
1016
  if (attrCheck.suspicious) {
968
1017
  attributionWarning = `\n[WARN] Attribution notice: entity "${entityName}" has 0 observations in ` +
969
1018
  `"${project.id}" but ${attrCheck.count} in "${attrCheck.knownIn}" ` +
@@ -989,6 +1038,8 @@ export async function createMemorixServer(
989
1038
  sourceDetail: 'explicit',
990
1039
  valueCategory: formationResult?.evaluation.category,
991
1040
  createdByAgentId: currentAgentId,
1041
+ visibility,
1042
+ visibilityReader: reader,
992
1043
  });
993
1044
 
994
1045
  // Add a reference to the entity's observations
@@ -1023,14 +1074,15 @@ export async function createMemorixServer(
1023
1074
  const compactStart = Date.now();
1024
1075
  const searchResult = await compactSearch({
1025
1076
  query: `${title} ${narrative.substring(0, 200)}`,
1026
- limit: 5,
1027
- projectId: project.id,
1028
- status: 'active',
1077
+ limit: 5,
1078
+ projectId: project.id,
1079
+ status: 'active',
1080
+ reader,
1029
1081
  });
1030
1082
  const similarEntries = searchResult.entries.map(e => e);
1031
1083
  if (similarEntries.length > 0) {
1032
1084
  const similarIds = similarEntries.map(e => e.id);
1033
- const details = await compactDetail(similarIds);
1085
+ const details = await compactDetail(similarIds, { reader });
1034
1086
  const existingMemories: ExistingMemory[] = details.documents.map((d, i) => ({
1035
1087
  id: d.observationId,
1036
1088
  title: d.title,
@@ -1056,9 +1108,9 @@ export async function createMemorixServer(
1056
1108
  useLLM: isLLMEnabled(),
1057
1109
  minValueScore: 0.3,
1058
1110
  searchMemories: async (q: string, limit: number, pid: string): Promise<SearchHit[]> => {
1059
- const result = await compactSearch({ query: q, limit, projectId: pid, status: 'active' });
1111
+ const result = await compactSearch({ query: q, limit, projectId: pid, status: 'active', reader });
1060
1112
  if (result.entries.length === 0) return [];
1061
- const details = await compactDetail(result.entries.map(e => e.id));
1113
+ const details = await compactDetail(result.entries.map(e => e.id), { reader });
1062
1114
  return details.documents.map((d, i) => ({
1063
1115
  id: Number(d.id.replace('obs-', '')),
1064
1116
  observationId: d.observationId,
@@ -1072,7 +1124,7 @@ export async function createMemorixServer(
1072
1124
  },
1073
1125
  getObservation: (id: number) => {
1074
1126
  const o = getObservation(id);
1075
- if (!o) return null;
1127
+ if (!o || o.projectId !== project.id || !canReadObservation(o, reader)) return null;
1076
1128
  return { id: o.id, entityName: o.entityName, type: o.type, title: o.title, narrative: o.narrative, facts: o.facts, topicKey: o.topicKey };
1077
1129
  },
1078
1130
  getEntityNames: () => graphManager.getEntityNames(),
@@ -1112,6 +1164,15 @@ export async function createMemorixServer(
1112
1164
  ],
1113
1165
  };
1114
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
+ }
1115
1176
  },
1116
1177
  );
1117
1178
 
@@ -1208,6 +1269,7 @@ export async function createMemorixServer(
1208
1269
  projectId: scope === 'global' ? undefined : project.id,
1209
1270
  status: (status as 'active' | 'resolved' | 'archived' | 'all') ?? 'active',
1210
1271
  source: source as 'agent' | 'git' | 'manual' | undefined,
1272
+ reader: getObservationReader(scope === 'global' ? 'global' : 'project'),
1211
1273
  });
1212
1274
 
1213
1275
  const timeoutPromise = new Promise<never>((_, reject) =>
@@ -1279,7 +1341,10 @@ export async function createMemorixServer(
1279
1341
  if (unresolved) return unresolved;
1280
1342
 
1281
1343
  const { getObservationStore } = await import('./store/obs-store.js');
1282
- const observations = await getObservationStore().loadByProject(project.id);
1344
+ const observations = filterReadableObservations(
1345
+ await getObservationStore().loadByProject(project.id),
1346
+ getObservationReader(),
1347
+ );
1283
1348
  const packet = buildGraphContextPacket(observations, {
1284
1349
  projectId: project.id,
1285
1350
  query,
@@ -1339,7 +1404,10 @@ export async function createMemorixServer(
1339
1404
  import('./runtime/maintenance-jobs.js'),
1340
1405
  import('./runtime/lifecycle.js'),
1341
1406
  ]);
1342
- const observations = await getObservationStore().loadByProject(project.id, { status: 'active' });
1407
+ const observations = filterReadableObservations(
1408
+ await getObservationStore().loadByProject(project.id, { status: 'active' }),
1409
+ getObservationReader(),
1410
+ );
1343
1411
  const context = await buildAutoProjectContext({
1344
1412
  project,
1345
1413
  dataDir: projectDir,
@@ -1441,7 +1509,10 @@ export async function createMemorixServer(
1441
1509
  await store.init(projectDir);
1442
1510
  const codegraphConfig = getResolvedConfig({ projectRoot: project.rootPath }).codegraph;
1443
1511
  const exclude = codegraphConfig.excludePatterns;
1444
- const observations = await getObservationStore().loadByProject(project.id, { status: 'active' });
1512
+ const observations = filterReadableObservations(
1513
+ await getObservationStore().loadByProject(project.id, { status: 'active' }),
1514
+ getObservationReader(),
1515
+ );
1445
1516
  observations.reverse();
1446
1517
  const basePack = assembleContextPackForTask({
1447
1518
  store,
@@ -1863,9 +1934,15 @@ export async function createMemorixServer(
1863
1934
  },
1864
1935
  },
1865
1936
  async ({ ids, status }) => {
1866
- const { resolveObservations } = await import('./memory/observations.js');
1937
+ const { resolveObservations, getObservation } = await import('./memory/observations.js');
1867
1938
  const safeIds = (Array.isArray(ids) ? ids : [ids]).map(id => coerceNumber(id, 0)).filter(id => id > 0);
1868
- 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;
1869
1946
 
1870
1947
  const parts: string[] = [];
1871
1948
  if (result.resolved.length > 0) {
@@ -1874,6 +1951,9 @@ export async function createMemorixServer(
1874
1951
  if (result.notFound.length > 0) {
1875
1952
  parts.push(`[WARN] Not found: #${result.notFound.join(', #')}`);
1876
1953
  }
1954
+ if (deniedCount > 0) {
1955
+ parts.push(`[WARN] Skipped ${deniedCount} observation(s) outside this session's write scope.`);
1956
+ }
1877
1957
  parts.push('\nResolved memories are hidden from default search. Use status="all" to include them.');
1878
1958
  parts.push('[STATS] Run `memorix_retention` with `action: "report"` to check remaining cleanup status.');
1879
1959
 
@@ -1919,6 +1999,7 @@ export async function createMemorixServer(
1919
1999
  async ({ entityName, decision, alternatives, rationale, constraints, expectedOutcome, risks, concepts, filesModified, relatedCommits, relatedEntities }) => {
1920
2000
  const unresolved = requireResolvedProject('store reasoning in the current project');
1921
2001
  if (unresolved) return unresolved;
2002
+ const reader = getObservationReader();
1922
2003
  return withFreshIndex(async () => {
1923
2004
 
1924
2005
  // Build structured narrative from reasoning fields
@@ -1948,7 +2029,11 @@ export async function createMemorixServer(
1948
2029
  // ── Attribution guard (passive, non-blocking) ─────────────────
1949
2030
  let reasoningAttributionWarning = '';
1950
2031
  try {
1951
- const attrCheck = await checkProjectAttribution(entityName, project.id, getAllObservations());
2032
+ const attrCheck = await checkProjectAttribution(
2033
+ entityName,
2034
+ project.id,
2035
+ filterReadableObservations(getAllObservations(), reader),
2036
+ );
1952
2037
  if (attrCheck.suspicious) {
1953
2038
  reasoningAttributionWarning = `\n[WARN] Attribution notice: entity "${entityName}" has 0 observations in ` +
1954
2039
  `"${project.id}" but ${attrCheck.count} in "${attrCheck.knownIn}" ` +
@@ -1970,6 +2055,7 @@ export async function createMemorixServer(
1970
2055
  relatedEntities,
1971
2056
  sourceDetail: 'explicit',
1972
2057
  createdByAgentId: currentAgentId,
2058
+ visibility: 'project',
1973
2059
  });
1974
2060
 
1975
2061
  await graphManager.addObservations([
@@ -2013,7 +2099,11 @@ export async function createMemorixServer(
2013
2099
  const minCount = threshold ?? 2;
2014
2100
  let entries: import('./memory/attribution-guard.js').AuditEntry[];
2015
2101
  try {
2016
- 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
+ );
2017
2107
  } catch (err) {
2018
2108
  return {
2019
2109
  content: [{
@@ -2096,6 +2186,7 @@ export async function createMemorixServer(
2096
2186
  type: 'reasoning' as ObservationType,
2097
2187
  projectId: scope === 'global' ? undefined : project.id,
2098
2188
  status: 'active',
2189
+ reader: getObservationReader(scope === 'global' ? 'global' : 'project'),
2099
2190
  }));
2100
2191
 
2101
2192
  if (result.entries.length === 0) {
@@ -2132,7 +2223,11 @@ export async function createMemorixServer(
2132
2223
  },
2133
2224
  async ({ query, dryRun }) => {
2134
2225
  const { getAllObservations, resolveObservations } = await import('./memory/observations.js');
2135
- 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)));
2136
2231
 
2137
2232
  if (allObs.length < 2) {
2138
2233
  return { content: [{ type: 'text' as const, text: 'Not enough active memories to deduplicate.' }] };
@@ -2151,7 +2246,7 @@ export async function createMemorixServer(
2151
2246
  // If query provided, search for relevant memories; otherwise take latest 20
2152
2247
  let candidates: typeof allObs;
2153
2248
  if (query) {
2154
- 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 });
2155
2250
  const idSet = new Set(searchResult.entries.map(e => e.id));
2156
2251
  candidates = allObs.filter(o => idSet.has(o.id));
2157
2252
  } else {
@@ -2257,6 +2352,7 @@ export async function createMemorixServer(
2257
2352
  project.id,
2258
2353
  safeBefore,
2259
2354
  safeAfter,
2355
+ getObservationReader(),
2260
2356
  );
2261
2357
 
2262
2358
  return {
@@ -2307,14 +2403,17 @@ export async function createMemorixServer(
2307
2403
  // Priority: typedRefs > refs > ids (each is a complete, homogeneous input path)
2308
2404
  let result;
2309
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');
2310
2409
  if (safeTypedRefs.length > 0) {
2311
2410
  // Pass typed ref strings directly — compactDetail handles parsing
2312
- result = await compactDetail(safeTypedRefs);
2411
+ result = await compactDetail(safeTypedRefs, { reader });
2313
2412
  } else if (safeRefs.length > 0) {
2314
- result = await compactDetail(safeRefs);
2413
+ result = await compactDetail(safeRefs, { reader });
2315
2414
  } else {
2316
2415
  // Bare numeric IDs are scoped to the current project
2317
- result = await compactDetail(safeIds.map(id => ({ id, projectId: project.id })));
2416
+ result = await compactDetail(safeIds.map(id => ({ id, projectId: project.id })), { reader });
2318
2417
  }
2319
2418
  } catch (err) {
2320
2419
  return { content: [{ type: 'text' as const, text: err instanceof Error ? err.message : String(err) }], isError: true };
@@ -2363,13 +2462,17 @@ export async function createMemorixServer(
2363
2462
  },
2364
2463
  async (args: { action?: string }) => {
2365
2464
  const action = args.action ?? 'report';
2366
- const { getRetentionSummary, getArchiveCandidates, rankByRelevance, archiveExpired, getRetentionZone, explainRetention } = await import('./memory/retention.js');
2465
+ const { getRetentionSummary, getArchiveCandidates, rankByRelevance, getRetentionZone, explainRetention } = await import('./memory/retention.js');
2367
2466
  const { getDb } = await import('./store/orama-store.js');
2368
2467
  const { search } = await import('@orama/orama');
2369
2468
 
2370
2469
  // Shared: build MemorixDocument[] from in-memory observations
2371
- const { getAllObservations } = await import('./memory/observations.js');
2372
- 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
+ ));
2373
2476
 
2374
2477
  // Pull current access metadata from the live Orama index so access-based
2375
2478
  // immunity (e.g. accessCount >= 3) still works in retention/report/archive
@@ -2393,19 +2496,6 @@ export async function createMemorixServer(
2393
2496
  // less precise immunity/reporting.
2394
2497
  }
2395
2498
 
2396
- // Handle archive action
2397
- if (action === 'archive') {
2398
- const result = await archiveExpired(projectDir, undefined, accessMap, project.id);
2399
- if (result.archived === 0) {
2400
- return {
2401
- content: [{ type: 'text' as const, text: '[OK] No expired observations to archive. All memories are within their retention period.' }],
2402
- };
2403
- }
2404
- return {
2405
- 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".` }],
2406
- };
2407
- }
2408
-
2409
2499
  const docs: import('./types.js').MemorixDocument[] = allObs.map(obs => ({
2410
2500
  id: `obs-${obs.id}`,
2411
2501
  observationId: obs.id,
@@ -2425,6 +2515,11 @@ export async function createMemorixServer(
2425
2515
  source: obs.source ?? 'agent',
2426
2516
  sourceDetail: obs.sourceDetail ?? '',
2427
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 ?? []),
2428
2523
  }));
2429
2524
 
2430
2525
  if (docs.length === 0) {
@@ -2433,6 +2528,24 @@ export async function createMemorixServer(
2433
2528
  };
2434
2529
  }
2435
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
+
2436
2549
  // ── action="stale": full table of stale observations with explanation ──
2437
2550
  if (action === 'stale') {
2438
2551
  const staleDocs = docs.filter(d => getRetentionZone(d) === 'stale');
@@ -2807,7 +2920,10 @@ export async function createMemorixServer(
2807
2920
  const allObs = await withFreshIndex(() => getAllObservations());
2808
2921
  const scoped = scopeKnowledgeGraphToProject(
2809
2922
  graph,
2810
- allObs.filter(observation => observation.projectId === project.id),
2923
+ filterReadableObservations(
2924
+ allObs.filter(observation => observation.projectId === project.id),
2925
+ getObservationReader(),
2926
+ ),
2811
2927
  );
2812
2928
  return { entities: scoped.entities, relations: scoped.relations };
2813
2929
  }
@@ -3149,12 +3265,10 @@ export async function createMemorixServer(
3149
3265
 
3150
3266
  // action === 'generate'
3151
3267
  const { getObservationStore: getStore } = await import('./store/obs-store.js');
3152
- const allObs = await getStore().loadAll() as Array<{
3153
- id?: number; entityName?: string; type?: string; title?: string;
3154
- narrative?: string; facts?: string[]; concepts?: string[];
3155
- filesModified?: string[]; createdAt?: string;
3156
- status?: string; source?: 'agent' | 'git' | 'manual';
3157
- }>;
3268
+ const allObs = filterReadableObservations(
3269
+ (await getStore().loadAll()).filter((observation) => observation.projectId === project.id),
3270
+ getObservationReader(),
3271
+ );
3158
3272
 
3159
3273
  const obsData = allObs.map(o => ({
3160
3274
  id: o.id || 0,
@@ -3286,7 +3400,11 @@ export async function createMemorixServer(
3286
3400
  // Load observations by ID — only active observations can be promoted
3287
3401
  const { getAllObservations } = await import('./memory/observations.js');
3288
3402
  const allObs = await withFreshIndex(() => getAllObservations());
3289
- 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
+ );
3290
3408
 
3291
3409
  if (matched.length === 0) {
3292
3410
  return { content: [{ type: 'text' as const, text: `No observations found for IDs: [${observationIds.join(', ')}]. Use \`memorix_search\` to find valid IDs.` }], isError: true };
@@ -3298,6 +3416,14 @@ export async function createMemorixServer(
3298
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 };
3299
3417
  }
3300
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
+
3301
3427
  const skill = await promoteToMiniSkill(projectDir, project.id, matched, { trigger, instruction, tags });
3302
3428
 
3303
3429
  const lines = [
@@ -3531,8 +3657,11 @@ export async function createMemorixServer(
3531
3657
  const lastSeen = registeredAgent.last_seen_obs_generation;
3532
3658
  const store = getObservationStore();
3533
3659
  const currentGen = store.getGeneration();
3534
- const projectObs = await withFreshIndex(() => getAllObservations().filter(
3535
- 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(),
3536
3665
  ));
3537
3666
  const wm = computeWatermark(lastSeen, currentGen, projectObs.length);
3538
3667
  if (wm.newObservationCount > 0) {
@@ -3735,7 +3864,7 @@ export async function createMemorixServer(
3735
3864
  title: 'Transfer Memories',
3736
3865
  description:
3737
3866
  'Export or import project memories. ' +
3738
- '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). ' +
3739
3868
  'Action "import": import from a JSON export (re-assigns IDs, skips duplicate topicKeys).',
3740
3869
  inputSchema: {
3741
3870
  action: z.enum(['export', 'import']).describe('Operation: export or import'),
@@ -3747,10 +3876,10 @@ export async function createMemorixServer(
3747
3876
  if (action === 'export') {
3748
3877
  const { exportAsJson, exportAsMarkdown } = await import('./memory/export-import.js');
3749
3878
  if (format === 'markdown') {
3750
- const md = await exportAsMarkdown(projectDir, project.id);
3879
+ const md = await exportAsMarkdown(projectDir, project.id, getObservationReader());
3751
3880
  return { content: [{ type: 'text' as const, text: md }] };
3752
3881
  }
3753
- const data = await exportAsJson(projectDir, project.id);
3882
+ const data = await exportAsJson(projectDir, project.id, getObservationReader());
3754
3883
  const json = JSON.stringify(data, null, 2);
3755
3884
  return {
3756
3885
  content: [{
@@ -3972,8 +4101,6 @@ export async function createMemorixServer(
3972
4101
 
3973
4102
  // Use shared TeamStore (from HTTP server) or create new one (stdio mode).
3974
4103
  // All team state is canonical in SQLite — no JSON persistence, no sync/flush.
3975
- let teamStore!: import('./team/team-store.js').TeamStore;
3976
- let initTeamStoreForProject: ((dataDir: string) => Promise<import('./team/team-store.js').TeamStore>) | undefined;
3977
4104
  if (teamFeaturesEnabled) {
3978
4105
  const { initTeamStore } = await import('./team/team-store.js');
3979
4106
  initTeamStoreForProject = initTeamStore;
@@ -4259,7 +4386,7 @@ export async function createMemorixServer(
4259
4386
  'Action "inbox": read this agent\'s inbox.',
4260
4387
  inputSchema: {
4261
4388
  action: z.enum(['send', 'broadcast', 'inbox']).describe('Operation to perform'),
4262
- 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.'),
4263
4390
  to: z.string().optional().describe('Receiver agent ID (for send)'),
4264
4391
  type: z.enum(['request', 'response', 'info', 'announcement', 'contract', 'error', 'handoff']).optional().describe('Message type (for send/broadcast)'),
4265
4392
  content: z.string().optional().describe('Message content (for send/broadcast)'),
@@ -4270,13 +4397,20 @@ export async function createMemorixServer(
4270
4397
  },
4271
4398
  },
4272
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
+ };
4273
4405
  if (action === 'send') {
4274
- 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 };
4275
4409
  if (!to && !toRole) return { content: [{ type: 'text' as const, text: '[ERROR] either to (agent ID) or toRole is required for send' }], isError: true };
4276
4410
  if (content.length > 10_000) return { content: [{ type: 'text' as const, text: '[ERROR] Message too large (max 10KB)' }], isError: true };
4277
4411
  const msg = teamStore.sendMessage({
4278
4412
  projectId: project.id,
4279
- senderAgentId: from,
4413
+ senderAgentId: currentAgentId!,
4280
4414
  recipientAgentId: to ?? null,
4281
4415
  type: msgType,
4282
4416
  content,
@@ -4288,11 +4422,13 @@ export async function createMemorixServer(
4288
4422
  return { content: [{ type: 'text' as const, text: `Message sent (${msgType}) to ${target} | ID: ${msg.id.slice(0, 8)}…${toRole ? ` [role: ${toRole}]` : ''}` }] };
4289
4423
  }
4290
4424
  if (action === 'broadcast') {
4291
- 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 };
4292
4428
  if (content.length > 10_000) return { content: [{ type: 'text' as const, text: '[ERROR] Message too large (max 10KB)' }], isError: true };
4293
4429
  const msg = teamStore.sendMessage({
4294
4430
  projectId: project.id,
4295
- senderAgentId: from,
4431
+ senderAgentId: currentAgentId!,
4296
4432
  recipientAgentId: null,
4297
4433
  type: msgType,
4298
4434
  content,
@@ -4301,8 +4437,12 @@ export async function createMemorixServer(
4301
4437
  return { content: [{ type: 'text' as const, text: `Broadcast (${msgType}) | ID: ${msg.id.slice(0, 8)}…` }] };
4302
4438
  }
4303
4439
  // inbox
4304
- const inboxId = agentId || from || '';
4305
- 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!;
4306
4446
  const inbox = teamStore.getInbox(project.id, inboxId);
4307
4447
  const unread = teamStore.getUnreadCount(project.id, inboxId);
4308
4448
  if (inbox.length === 0) return { content: [{ type: 'text' as const, text: 'Inbox empty' }] };
@@ -4334,32 +4474,42 @@ export async function createMemorixServer(
4334
4474
  },
4335
4475
  async ({ agentId, markInboxRead }) => {
4336
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;
4337
4484
 
4338
4485
  // Compute watermark — requires observation store access
4339
4486
  let watermark = computeWatermark(0, 0, 0);
4340
- if (agentId) {
4341
- const agent = teamStore.getAgent(agentId);
4487
+ if (effectiveAgentId) {
4488
+ const agent = teamStore.getAgent(effectiveAgentId);
4342
4489
  if (agent) {
4343
4490
  const lastSeen = agent.last_seen_obs_generation;
4344
4491
  const store = getObservationStore();
4345
4492
  const currentGen = store.getGeneration();
4346
- const projectObs = await withFreshIndex(() => getAllObservations().filter(
4347
- 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(),
4348
4498
  ));
4349
4499
  watermark = computeWatermark(lastSeen, currentGen, projectObs.length);
4350
4500
 
4351
4501
  // Advance watermark so next poll sees only truly new observations
4352
- teamStore.updateWatermark(agentId, currentGen);
4502
+ teamStore.updateWatermark(effectiveAgentId, currentGen);
4353
4503
  // Heartbeat — proves this agent is alive
4354
- teamStore.heartbeat(agentId);
4504
+ teamStore.heartbeat(effectiveAgentId);
4355
4505
  }
4356
4506
  }
4357
4507
 
4358
- const poll = computePoll(teamStore, project.id, agentId ?? null, watermark);
4508
+ const poll = computePoll(teamStore, project.id, effectiveAgentId ?? null, watermark);
4359
4509
 
4360
4510
  // Optionally mark inbox as read
4361
- if (markInboxRead && agentId) {
4362
- teamStore.markAllRead(project.id, agentId);
4511
+ if (markInboxRead && effectiveAgentId) {
4512
+ teamStore.markAllRead(project.id, effectiveAgentId);
4363
4513
  }
4364
4514
 
4365
4515
  // Format as readable text
@@ -4432,7 +4582,8 @@ export async function createMemorixServer(
4432
4582
  title: 'Team Handoff — Agent Context Transfer',
4433
4583
  description:
4434
4584
  'Create a structured handoff artifact when passing work to another agent. ' +
4435
- '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) ' +
4436
4587
  'and a notification message is sent to the recipient. ' +
4437
4588
  'Use this when completing a task and another agent should continue, ' +
4438
4589
  'or when you want to leave context for whoever works on this next.',
@@ -4448,6 +4599,34 @@ export async function createMemorixServer(
4448
4599
  },
4449
4600
  async ({ fromAgentId, summary, context, toAgentId, taskId, filesModified, concepts }) => {
4450
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
+ }
4451
4630
 
4452
4631
  const result = await createHandoffArtifact(
4453
4632
  {