feat(team): share leader memory with team members (read-only)

Team members dispatched via team tasks can now read the leader's memory
files (MEMORY.md, memory/*.md) as a fallback when they have no own
memory. Memory writes are blocked for members — only the leader can
save memory files. This follows the existing team_workspace context
propagation pattern: dispatch metadata → RunRequest → context → tools.

Affected tools: read_file (interceptor), memory_get, memory_search,
list_files (interceptor). All include leader fallback with per-user →
global scope cascade.
This commit is contained in:
viettranx committed 2026-03-27 22:45:40 +07:00
1 parent e9342533e0
commit ee9f5b8aaf
10 files changed
+523 -7

No files matched your search

+37 -3
View File
@@ -117,7 +117,16 @@ func (m *MemoryInterceptor) ReadFile(ctx context.Context, path string) (string,
content, err = m.memStore.GetDocument(ctx, agentStr, "", relPath)
}
if err != nil {
// Not found is OK — return empty
// Not found in own memory — try leader's memory as fallback (team members only).
if leaderID := LeaderAgentIDFromCtx(ctx); leaderID != "" && leaderID != agentStr {
leaderContent, lerr := m.memStore.GetDocument(ctx, leaderID, userID, relPath)
if lerr != nil && userID != "" {
leaderContent, lerr = m.memStore.GetDocument(ctx, leaderID, "", relPath)
}
if lerr == nil && leaderContent != "" {
return leaderContent, true, nil
}
}
slog.Debug("memory interceptor: document not found", "path", path, "agent", agentStr)
return "", true, nil
}
@@ -147,11 +156,17 @@ func (m *MemoryInterceptor) WriteFile(ctx context.Context, path, content string,
return MemoryWriteResult{}, nil // no agent context
}
// Block memory writes for team members — memory is leader-only.
agentStr := agentID.String()
if leaderID := LeaderAgentIDFromCtx(ctx); leaderID != "" && leaderID != agentStr {
return MemoryWriteResult{Handled: true}, fmt.Errorf(
"memory is read-only for team members — only the team leader can save memory files")
}
// Normalize absolute path to workspace-relative for DB storage
relPath := normalizeToRelative(path, ws)
userID := store.MemoryUserID(ctx)
agentStr := agentID.String()
var previousContent string
@@ -208,11 +223,30 @@ func (m *MemoryInterceptor) ListFiles(ctx context.Context, path string) (string,
}
userID := store.MemoryUserID(ctx)
docs, err := m.memStore.ListDocuments(ctx, agentID.String(), userID)
agentStr := agentID.String()
docs, err := m.memStore.ListDocuments(ctx, agentStr, userID)
if err != nil {
return "", true, err
}
// Merge leader's documents for team members (fallback, deduplicate by path).
if leaderID := LeaderAgentIDFromCtx(ctx); leaderID != "" && leaderID != agentStr {
seen := make(map[string]bool, len(docs))
for _, d := range docs {
seen[d.Path] = true
}
leaderDocs, _ := m.memStore.ListDocuments(ctx, leaderID, userID)
// Also try global scope if per-user returned nothing.
if len(leaderDocs) == 0 && userID != "" {
leaderDocs, _ = m.memStore.ListDocuments(ctx, leaderID, "")
}
for _, d := range leaderDocs {
if !seen[d.Path] {
docs = append(docs, d)
}
}
}
if len(docs) == 0 {
return "", true, nil
}