memorix 1.2.1 → 1.2.3

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 (90) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +14 -2
  3. package/README.zh-CN.md +14 -2
  4. package/dist/cli/index.js +15424 -13780
  5. package/dist/cli/index.js.map +1 -1
  6. package/dist/index.js +1337 -536
  7. package/dist/index.js.map +1 -1
  8. package/dist/maintenance-runner.d.ts +1 -1
  9. package/dist/maintenance-runner.js +8458 -8087
  10. package/dist/maintenance-runner.js.map +1 -1
  11. package/dist/memcode-runtime/CHANGELOG.md +23 -0
  12. package/dist/sdk.d.ts +7 -2
  13. package/dist/sdk.js +1365 -542
  14. package/dist/sdk.js.map +1 -1
  15. package/dist/types.d.ts +49 -1
  16. package/dist/types.js.map +1 -1
  17. package/docs/1.2.2-MEMORY-CONTROL-PLANE.md +434 -0
  18. package/docs/AGENT_OPERATOR_PLAYBOOK.md +4 -0
  19. package/docs/API_REFERENCE.md +24 -4
  20. package/docs/README.md +1 -1
  21. package/docs/dev-log/progress.txt +101 -11
  22. package/package.json +1 -1
  23. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  24. package/src/cli/command-guide.ts +192 -0
  25. package/src/cli/commands/audit.ts +9 -4
  26. package/src/cli/commands/cleanup.ts +5 -1
  27. package/src/cli/commands/codegraph.ts +15 -5
  28. package/src/cli/commands/context.ts +3 -2
  29. package/src/cli/commands/doctor.ts +4 -2
  30. package/src/cli/commands/explain.ts +9 -3
  31. package/src/cli/commands/handoff.ts +21 -7
  32. package/src/cli/commands/identity.ts +116 -0
  33. package/src/cli/commands/ingest-image.ts +5 -3
  34. package/src/cli/commands/lock.ts +11 -10
  35. package/src/cli/commands/memory.ts +58 -21
  36. package/src/cli/commands/message.ts +19 -14
  37. package/src/cli/commands/operator-shared.ts +98 -3
  38. package/src/cli/commands/poll.ts +16 -6
  39. package/src/cli/commands/reasoning.ts +17 -3
  40. package/src/cli/commands/retention.ts +9 -4
  41. package/src/cli/commands/serve-http.ts +8 -2
  42. package/src/cli/commands/session.ts +44 -10
  43. package/src/cli/commands/skills.ts +10 -5
  44. package/src/cli/commands/status.ts +4 -3
  45. package/src/cli/commands/task.ts +26 -17
  46. package/src/cli/commands/team.ts +14 -10
  47. package/src/cli/commands/transfer.ts +63 -10
  48. package/src/cli/identity.ts +89 -0
  49. package/src/cli/index.ts +96 -19
  50. package/src/cli/invocation.ts +115 -0
  51. package/src/cli/tui/chat-service.ts +41 -18
  52. package/src/cli/tui/data.ts +23 -44
  53. package/src/cli/tui/operator-context.ts +60 -0
  54. package/src/cli/tui/session-service.ts +3 -2
  55. package/src/cli/tui/views/MemoryView.tsx +10 -8
  56. package/src/codegraph/auto-context.ts +31 -2
  57. package/src/codegraph/context-pack.ts +1 -0
  58. package/src/codegraph/project-context.ts +2 -0
  59. package/src/compact/engine.ts +26 -10
  60. package/src/compact/index-format.ts +25 -2
  61. package/src/dashboard/server.ts +46 -9
  62. package/src/hooks/admission.ts +117 -0
  63. package/src/hooks/handler.ts +98 -91
  64. package/src/knowledge/context-assembly.ts +97 -0
  65. package/src/knowledge/workset.ts +179 -10
  66. package/src/memory/admission.ts +57 -0
  67. package/src/memory/consolidation.ts +13 -2
  68. package/src/memory/disclosure-policy.ts +6 -1
  69. package/src/memory/export-import.ts +11 -3
  70. package/src/memory/graph-context.ts +8 -2
  71. package/src/memory/observations.ts +162 -4
  72. package/src/memory/quality-audit.ts +2 -0
  73. package/src/memory/retention.ts +22 -2
  74. package/src/memory/session.ts +29 -11
  75. package/src/memory/visibility.ts +80 -0
  76. package/src/orchestrate/memorix-bridge.ts +38 -0
  77. package/src/runtime/control-plane-maintenance.ts +1 -0
  78. package/src/runtime/isolated-maintenance.ts +1 -0
  79. package/src/runtime/lifecycle.ts +18 -0
  80. package/src/runtime/maintenance-jobs.ts +1 -0
  81. package/src/runtime/maintenance-runner.ts +2 -0
  82. package/src/runtime/project-maintenance.ts +89 -0
  83. package/src/sdk.ts +35 -5
  84. package/src/server.ts +267 -83
  85. package/src/store/orama-store.ts +61 -6
  86. package/src/store/sqlite-db.ts +23 -1
  87. package/src/store/sqlite-store.ts +12 -2
  88. package/src/team/handoff.ts +7 -0
  89. package/src/types.ts +51 -0
  90. package/src/wiki/generator.ts +2 -0
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 = [
@@ -3488,7 +3614,6 @@ export async function createMemorixServer(
3488
3614
  if (unresolved) return unresolved;
3489
3615
 
3490
3616
  const { startSession } = await import('./memory/session.js');
3491
- const result = await startSession(projectDir, project.id, { sessionId, agent });
3492
3617
 
3493
3618
  const llmStatus = isLLMEnabled()
3494
3619
  ? `LLM enhanced mode: ${getLLMConfig()?.provider}/${getLLMConfig()?.model} (fact extraction + auto-dedup active)`
@@ -3531,8 +3656,11 @@ export async function createMemorixServer(
3531
3656
  const lastSeen = registeredAgent.last_seen_obs_generation;
3532
3657
  const store = getObservationStore();
3533
3658
  const currentGen = store.getGeneration();
3534
- const projectObs = await withFreshIndex(() => getAllObservations().filter(
3535
- o => o.projectId === project.id && (o.writeGeneration ?? 0) > lastSeen,
3659
+ const projectObs = await withFreshIndex(() => filterReadableObservations(
3660
+ getAllObservations().filter(
3661
+ o => o.projectId === project.id && (o.writeGeneration ?? 0) > lastSeen,
3662
+ ),
3663
+ getObservationReader(),
3536
3664
  ));
3537
3665
  const wm = computeWatermark(lastSeen, currentGen, projectObs.length);
3538
3666
  if (wm.newObservationCount > 0) {
@@ -3561,6 +3689,12 @@ export async function createMemorixServer(
3561
3689
  }
3562
3690
  } catch { /* team auto-registration is best-effort */ }
3563
3691
 
3692
+ const result = await startSession(projectDir, project.id, {
3693
+ sessionId,
3694
+ agent,
3695
+ reader: getObservationReader(),
3696
+ });
3697
+
3564
3698
  const lines = [
3565
3699
  `[OK] Session started: ${result.session.id}`,
3566
3700
  `Project: ${project.name} (${project.id})`,
@@ -3696,7 +3830,7 @@ export async function createMemorixServer(
3696
3830
  async ({ limit }) => {
3697
3831
  const safeLimit = limit != null ? coerceNumber(limit, 3) : 3;
3698
3832
  const { getSessionContext, listSessions } = await import('./memory/session.js');
3699
- const context = await getSessionContext(projectDir, project.id, safeLimit);
3833
+ const context = await getSessionContext(projectDir, project.id, safeLimit, getObservationReader());
3700
3834
  const sessions = await listSessions(projectDir, project.id);
3701
3835
 
3702
3836
  const activeSessions = sessions.filter(s => s.status === 'active');
@@ -3735,7 +3869,7 @@ export async function createMemorixServer(
3735
3869
  title: 'Transfer Memories',
3736
3870
  description:
3737
3871
  'Export or import project memories. ' +
3738
- 'Action "export": export observations and sessions (JSON or Markdown). ' +
3872
+ 'Action "export": export observations visible to the current agent and project sessions (JSON or Markdown). ' +
3739
3873
  'Action "import": import from a JSON export (re-assigns IDs, skips duplicate topicKeys).',
3740
3874
  inputSchema: {
3741
3875
  action: z.enum(['export', 'import']).describe('Operation: export or import'),
@@ -3747,10 +3881,10 @@ export async function createMemorixServer(
3747
3881
  if (action === 'export') {
3748
3882
  const { exportAsJson, exportAsMarkdown } = await import('./memory/export-import.js');
3749
3883
  if (format === 'markdown') {
3750
- const md = await exportAsMarkdown(projectDir, project.id);
3884
+ const md = await exportAsMarkdown(projectDir, project.id, getObservationReader());
3751
3885
  return { content: [{ type: 'text' as const, text: md }] };
3752
3886
  }
3753
- const data = await exportAsJson(projectDir, project.id);
3887
+ const data = await exportAsJson(projectDir, project.id, getObservationReader());
3754
3888
  const json = JSON.stringify(data, null, 2);
3755
3889
  return {
3756
3890
  content: [{
@@ -3972,8 +4106,6 @@ export async function createMemorixServer(
3972
4106
 
3973
4107
  // Use shared TeamStore (from HTTP server) or create new one (stdio mode).
3974
4108
  // 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
4109
  if (teamFeaturesEnabled) {
3978
4110
  const { initTeamStore } = await import('./team/team-store.js');
3979
4111
  initTeamStoreForProject = initTeamStore;
@@ -4259,7 +4391,7 @@ export async function createMemorixServer(
4259
4391
  'Action "inbox": read this agent\'s inbox.',
4260
4392
  inputSchema: {
4261
4393
  action: z.enum(['send', 'broadcast', 'inbox']).describe('Operation to perform'),
4262
- from: z.string().optional().describe('Sender agent ID (for send/broadcast)'),
4394
+ from: z.string().optional().describe('Your sender agent ID. Omit it to use this session identity.'),
4263
4395
  to: z.string().optional().describe('Receiver agent ID (for send)'),
4264
4396
  type: z.enum(['request', 'response', 'info', 'announcement', 'contract', 'error', 'handoff']).optional().describe('Message type (for send/broadcast)'),
4265
4397
  content: z.string().optional().describe('Message content (for send/broadcast)'),
@@ -4270,13 +4402,20 @@ export async function createMemorixServer(
4270
4402
  },
4271
4403
  },
4272
4404
  async ({ action, from, to, type: msgType, content, agentId, markRead, toRole, handoffStatus }) => {
4405
+ const requireCurrentTeamAgent = () => {
4406
+ if (!currentAgentId) return null;
4407
+ const agent = teamStore.getAgent(currentAgentId);
4408
+ return agent?.project_id === project.id && agent.status === 'active' ? agent : null;
4409
+ };
4273
4410
  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 };
4411
+ const sender = requireCurrentTeamAgent();
4412
+ if (!sender || !msgType || !content) return { content: [{ type: 'text' as const, text: '[ERROR] active session identity, type, and content required for send' }], isError: true };
4413
+ if (from && from !== currentAgentId) return { content: [{ type: 'text' as const, text: '[ERROR] from must match the current session identity' }], isError: true };
4275
4414
  if (!to && !toRole) return { content: [{ type: 'text' as const, text: '[ERROR] either to (agent ID) or toRole is required for send' }], isError: true };
4276
4415
  if (content.length > 10_000) return { content: [{ type: 'text' as const, text: '[ERROR] Message too large (max 10KB)' }], isError: true };
4277
4416
  const msg = teamStore.sendMessage({
4278
4417
  projectId: project.id,
4279
- senderAgentId: from,
4418
+ senderAgentId: currentAgentId!,
4280
4419
  recipientAgentId: to ?? null,
4281
4420
  type: msgType,
4282
4421
  content,
@@ -4288,11 +4427,13 @@ export async function createMemorixServer(
4288
4427
  return { content: [{ type: 'text' as const, text: `Message sent (${msgType}) to ${target} | ID: ${msg.id.slice(0, 8)}…${toRole ? ` [role: ${toRole}]` : ''}` }] };
4289
4428
  }
4290
4429
  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 };
4430
+ const sender = requireCurrentTeamAgent();
4431
+ if (!sender || !msgType || !content) return { content: [{ type: 'text' as const, text: '[ERROR] active session identity, type, and content required for broadcast' }], isError: true };
4432
+ if (from && from !== currentAgentId) return { content: [{ type: 'text' as const, text: '[ERROR] from must match the current session identity' }], isError: true };
4292
4433
  if (content.length > 10_000) return { content: [{ type: 'text' as const, text: '[ERROR] Message too large (max 10KB)' }], isError: true };
4293
4434
  const msg = teamStore.sendMessage({
4294
4435
  projectId: project.id,
4295
- senderAgentId: from,
4436
+ senderAgentId: currentAgentId!,
4296
4437
  recipientAgentId: null,
4297
4438
  type: msgType,
4298
4439
  content,
@@ -4301,8 +4442,12 @@ export async function createMemorixServer(
4301
4442
  return { content: [{ type: 'text' as const, text: `Broadcast (${msgType}) | ID: ${msg.id.slice(0, 8)}…` }] };
4302
4443
  }
4303
4444
  // inbox
4304
- const inboxId = agentId || from || '';
4305
- if (!inboxId) return { content: [{ type: 'text' as const, text: '[ERROR] agentId required for inbox' }], isError: true };
4445
+ const inboxAgent = requireCurrentTeamAgent();
4446
+ if (!inboxAgent) return { content: [{ type: 'text' as const, text: '[ERROR] active session identity required for inbox' }], isError: true };
4447
+ if ((agentId && agentId !== currentAgentId) || (from && from !== currentAgentId)) {
4448
+ return { content: [{ type: 'text' as const, text: '[ERROR] inbox access is limited to the current session identity' }], isError: true };
4449
+ }
4450
+ const inboxId = currentAgentId!;
4306
4451
  const inbox = teamStore.getInbox(project.id, inboxId);
4307
4452
  const unread = teamStore.getUnreadCount(project.id, inboxId);
4308
4453
  if (inbox.length === 0) return { content: [{ type: 'text' as const, text: 'Inbox empty' }] };
@@ -4334,32 +4479,42 @@ export async function createMemorixServer(
4334
4479
  },
4335
4480
  async ({ agentId, markInboxRead }) => {
4336
4481
  const { computeWatermark, computePoll } = await import('./team/poll.js');
4482
+ if (agentId && agentId !== currentAgentId) {
4483
+ return {
4484
+ content: [{ type: 'text' as const, text: '[ERROR] agentId must match the current session identity.' }],
4485
+ isError: true as const,
4486
+ };
4487
+ }
4488
+ const effectiveAgentId = currentAgentId;
4337
4489
 
4338
4490
  // Compute watermark — requires observation store access
4339
4491
  let watermark = computeWatermark(0, 0, 0);
4340
- if (agentId) {
4341
- const agent = teamStore.getAgent(agentId);
4492
+ if (effectiveAgentId) {
4493
+ const agent = teamStore.getAgent(effectiveAgentId);
4342
4494
  if (agent) {
4343
4495
  const lastSeen = agent.last_seen_obs_generation;
4344
4496
  const store = getObservationStore();
4345
4497
  const currentGen = store.getGeneration();
4346
- const projectObs = await withFreshIndex(() => getAllObservations().filter(
4347
- o => o.projectId === project.id && (o.writeGeneration ?? 0) > lastSeen,
4498
+ const projectObs = await withFreshIndex(() => filterReadableObservations(
4499
+ getAllObservations().filter(
4500
+ o => o.projectId === project.id && (o.writeGeneration ?? 0) > lastSeen,
4501
+ ),
4502
+ getObservationReader(),
4348
4503
  ));
4349
4504
  watermark = computeWatermark(lastSeen, currentGen, projectObs.length);
4350
4505
 
4351
4506
  // Advance watermark so next poll sees only truly new observations
4352
- teamStore.updateWatermark(agentId, currentGen);
4507
+ teamStore.updateWatermark(effectiveAgentId, currentGen);
4353
4508
  // Heartbeat — proves this agent is alive
4354
- teamStore.heartbeat(agentId);
4509
+ teamStore.heartbeat(effectiveAgentId);
4355
4510
  }
4356
4511
  }
4357
4512
 
4358
- const poll = computePoll(teamStore, project.id, agentId ?? null, watermark);
4513
+ const poll = computePoll(teamStore, project.id, effectiveAgentId ?? null, watermark);
4359
4514
 
4360
4515
  // Optionally mark inbox as read
4361
- if (markInboxRead && agentId) {
4362
- teamStore.markAllRead(project.id, agentId);
4516
+ if (markInboxRead && effectiveAgentId) {
4517
+ teamStore.markAllRead(project.id, effectiveAgentId);
4363
4518
  }
4364
4519
 
4365
4520
  // Format as readable text
@@ -4432,7 +4587,8 @@ export async function createMemorixServer(
4432
4587
  title: 'Team Handoff — Agent Context Transfer',
4433
4588
  description:
4434
4589
  'Create a structured handoff artifact when passing work to another agent. ' +
4435
- 'The handoff is stored as a durable observation (searchable, immune to archival) ' +
4590
+ 'A targeted handoff is visible only to its sender and recipient; a broadcast handoff is team-visible. ' +
4591
+ 'The handoff is stored as a durable observation (immune to archival) ' +
4436
4592
  'and a notification message is sent to the recipient. ' +
4437
4593
  'Use this when completing a task and another agent should continue, ' +
4438
4594
  'or when you want to leave context for whoever works on this next.',
@@ -4448,6 +4604,34 @@ export async function createMemorixServer(
4448
4604
  },
4449
4605
  async ({ fromAgentId, summary, context, toAgentId, taskId, filesModified, concepts }) => {
4450
4606
  const { createHandoffArtifact } = await import('./team/handoff.js');
4607
+ if (!currentAgentId) {
4608
+ return {
4609
+ content: [{ type: 'text' as const, text: 'Create a coordination identity first: call memorix_session_start with joinTeam=true.' }],
4610
+ isError: true as const,
4611
+ };
4612
+ }
4613
+ if (fromAgentId !== currentAgentId) {
4614
+ return {
4615
+ 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.' }],
4616
+ isError: true as const,
4617
+ };
4618
+ }
4619
+ const sender = teamStore.getAgent(currentAgentId);
4620
+ if (!sender || sender.project_id !== project.id || sender.status !== 'active') {
4621
+ return {
4622
+ content: [{ type: 'text' as const, text: 'The current coordination identity is not an active member of this project.' }],
4623
+ isError: true as const,
4624
+ };
4625
+ }
4626
+ if (toAgentId) {
4627
+ const recipient = teamStore.getAgent(toAgentId);
4628
+ if (!recipient || recipient.project_id !== project.id) {
4629
+ return {
4630
+ content: [{ type: 'text' as const, text: 'The handoff recipient must be an agent registered in the current project.' }],
4631
+ isError: true as const,
4632
+ };
4633
+ }
4634
+ }
4451
4635
 
4452
4636
  const result = await createHandoffArtifact(
4453
4637
  {