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.
- package/CHANGELOG.md +23 -0
- package/README.md +14 -2
- package/README.zh-CN.md +14 -2
- package/dist/cli/index.js +15424 -13780
- package/dist/cli/index.js.map +1 -1
- package/dist/index.js +1337 -536
- package/dist/index.js.map +1 -1
- package/dist/maintenance-runner.d.ts +1 -1
- package/dist/maintenance-runner.js +8458 -8087
- package/dist/maintenance-runner.js.map +1 -1
- package/dist/memcode-runtime/CHANGELOG.md +23 -0
- package/dist/sdk.d.ts +7 -2
- package/dist/sdk.js +1365 -542
- package/dist/sdk.js.map +1 -1
- package/dist/types.d.ts +49 -1
- package/dist/types.js.map +1 -1
- package/docs/1.2.2-MEMORY-CONTROL-PLANE.md +434 -0
- package/docs/AGENT_OPERATOR_PLAYBOOK.md +4 -0
- package/docs/API_REFERENCE.md +24 -4
- package/docs/README.md +1 -1
- package/docs/dev-log/progress.txt +101 -11
- package/package.json +1 -1
- package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
- package/src/cli/command-guide.ts +192 -0
- package/src/cli/commands/audit.ts +9 -4
- package/src/cli/commands/cleanup.ts +5 -1
- package/src/cli/commands/codegraph.ts +15 -5
- package/src/cli/commands/context.ts +3 -2
- package/src/cli/commands/doctor.ts +4 -2
- package/src/cli/commands/explain.ts +9 -3
- package/src/cli/commands/handoff.ts +21 -7
- package/src/cli/commands/identity.ts +116 -0
- package/src/cli/commands/ingest-image.ts +5 -3
- package/src/cli/commands/lock.ts +11 -10
- package/src/cli/commands/memory.ts +58 -21
- package/src/cli/commands/message.ts +19 -14
- package/src/cli/commands/operator-shared.ts +98 -3
- package/src/cli/commands/poll.ts +16 -6
- package/src/cli/commands/reasoning.ts +17 -3
- package/src/cli/commands/retention.ts +9 -4
- package/src/cli/commands/serve-http.ts +8 -2
- package/src/cli/commands/session.ts +44 -10
- package/src/cli/commands/skills.ts +10 -5
- package/src/cli/commands/status.ts +4 -3
- package/src/cli/commands/task.ts +26 -17
- package/src/cli/commands/team.ts +14 -10
- package/src/cli/commands/transfer.ts +63 -10
- package/src/cli/identity.ts +89 -0
- package/src/cli/index.ts +96 -19
- package/src/cli/invocation.ts +115 -0
- package/src/cli/tui/chat-service.ts +41 -18
- package/src/cli/tui/data.ts +23 -44
- package/src/cli/tui/operator-context.ts +60 -0
- package/src/cli/tui/session-service.ts +3 -2
- package/src/cli/tui/views/MemoryView.tsx +10 -8
- package/src/codegraph/auto-context.ts +31 -2
- package/src/codegraph/context-pack.ts +1 -0
- package/src/codegraph/project-context.ts +2 -0
- package/src/compact/engine.ts +26 -10
- package/src/compact/index-format.ts +25 -2
- package/src/dashboard/server.ts +46 -9
- package/src/hooks/admission.ts +117 -0
- package/src/hooks/handler.ts +98 -91
- package/src/knowledge/context-assembly.ts +97 -0
- package/src/knowledge/workset.ts +179 -10
- package/src/memory/admission.ts +57 -0
- package/src/memory/consolidation.ts +13 -2
- package/src/memory/disclosure-policy.ts +6 -1
- package/src/memory/export-import.ts +11 -3
- package/src/memory/graph-context.ts +8 -2
- package/src/memory/observations.ts +162 -4
- package/src/memory/quality-audit.ts +2 -0
- package/src/memory/retention.ts +22 -2
- package/src/memory/session.ts +29 -11
- package/src/memory/visibility.ts +80 -0
- package/src/orchestrate/memorix-bridge.ts +38 -0
- package/src/runtime/control-plane-maintenance.ts +1 -0
- package/src/runtime/isolated-maintenance.ts +1 -0
- package/src/runtime/lifecycle.ts +18 -0
- package/src/runtime/maintenance-jobs.ts +1 -0
- package/src/runtime/maintenance-runner.ts +2 -0
- package/src/runtime/project-maintenance.ts +89 -0
- package/src/sdk.ts +35 -5
- package/src/server.ts +267 -83
- package/src/store/orama-store.ts +61 -6
- package/src/store/sqlite-db.ts +23 -1
- package/src/store/sqlite-store.ts +12 -2
- package/src/team/handoff.ts +7 -0
- package/src/types.ts +51 -0
- 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
|
-
|
|
265
|
-
|
|
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
|
-
'
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
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,
|
|
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
|
|
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
|
-
|
|
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 =
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
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
|
|
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(() =>
|
|
3535
|
-
|
|
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('
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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
|
|
4305
|
-
if (!
|
|
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 (
|
|
4341
|
-
const agent = teamStore.getAgent(
|
|
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(() =>
|
|
4347
|
-
|
|
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(
|
|
4507
|
+
teamStore.updateWatermark(effectiveAgentId, currentGen);
|
|
4353
4508
|
// Heartbeat — proves this agent is alive
|
|
4354
|
-
teamStore.heartbeat(
|
|
4509
|
+
teamStore.heartbeat(effectiveAgentId);
|
|
4355
4510
|
}
|
|
4356
4511
|
}
|
|
4357
4512
|
|
|
4358
|
-
const poll = computePoll(teamStore, project.id,
|
|
4513
|
+
const poll = computePoll(teamStore, project.id, effectiveAgentId ?? null, watermark);
|
|
4359
4514
|
|
|
4360
4515
|
// Optionally mark inbox as read
|
|
4361
|
-
if (markInboxRead &&
|
|
4362
|
-
teamStore.markAllRead(project.id,
|
|
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
|
-
'
|
|
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
|
{
|