feat(mcp): #1754 broadcast ACTIVITY_LOGGED on MCP memory writes #1795

Merged
hongming merged 1 commits from feat/issue-1754-mcp-memory-activity-broadcast into main 2026-05-24 10:16:07 +00:00
@@ -31,6 +31,7 @@ import (
"strings"
"time"
"github.com/Molecule-AI/molecule-monorepo/platform/internal/events"
"github.com/Molecule-AI/molecule-monorepo/platform/internal/memory/client"
"github.com/Molecule-AI/molecule-monorepo/platform/internal/memory/contract"
"github.com/Molecule-AI/molecule-monorepo/platform/internal/memory/namespace"
@@ -154,6 +155,14 @@ func (h *MCPHandler) toolCommitMemoryV2(ctx context.Context, workspaceID string,
}
}
// Broadcast ACTIVITY_LOGGED so the canvas Memory tab live-refreshes
// (issue #1754). Fire-and-forget — broadcast failure must not roll
// back a successful plugin write. Source=agent because this is the
// MCP path (vs canvas user-driven POST /memories which the legacy
// handler logs with `memory_write_global`).
summary := "commit_memory to " + ns
logMemoryMCPActivity(ctx, h.broadcaster, workspaceID, "memory_write", resp.ID, ns, &summary)
out, _ := json.Marshal(resp)
return string(out), nil
}
@@ -258,6 +267,11 @@ func (h *MCPHandler) toolCommitSummary(ctx context.Context, workspaceID string,
if err != nil {
return "", fmt.Errorf("plugin commit: %w", err)
}
// Broadcast ACTIVITY_LOGGED for live canvas refresh (#1754).
summary := "commit_summary to " + ns
logMemoryMCPActivity(ctx, h.broadcaster, workspaceID, "memory_summary_write", resp.ID, ns, &summary)
out, _ := json.Marshal(resp)
return string(out), nil
}
@@ -320,6 +334,13 @@ func (h *MCPHandler) toolForgetMemory(ctx context.Context, workspaceID string, a
}); err != nil {
return "", fmt.Errorf("plugin forget: %w", err)
}
// Broadcast ACTIVITY_LOGGED for live canvas refresh (#1754).
// On forget, target_id carries the forgotten memory's id so the
// canvas can remove the row optimistically.
summary := "forget_memory " + memID + " from " + ns
logMemoryMCPActivity(ctx, h.broadcaster, workspaceID, "memory_delete", memID, ns, &summary)
return `{"forgotten":true}`, nil
}
@@ -393,3 +414,43 @@ func pickStringSlice(args map[string]interface{}, key string) []string {
}
return nil
}
// logMemoryMCPActivity fires the ACTIVITY_LOGGED broadcast for MCP-tool
// memory writes/deletes (issue #1754). The canvas Memory tab subscribes
// to memory_* activity_types via the WS feed; without this call, the
// panel only refreshes when the user hits the manual reload button.
//
// Fire-and-forget: broadcast failure must never roll back a successful
// plugin write. The underlying LogActivity helper already logs insert
// errors and swallows them — wrap with a nil-broadcaster guard so test
// fixtures without an events.Broadcaster don't nil-deref.
//
// IMPORTANT: takes *events.Broadcaster (concrete pointer), not
// events.EventEmitter (interface). Passing a nil *Broadcaster as
// EventEmitter creates a typed-nil interface that does NOT compare
// equal to nil — and would derefence inside LogActivity. Keeping the
// concrete type here lets the nil-check work reliably.
//
// Args:
// - activityType: "memory_write" / "memory_summary_write" / "memory_delete"
// - targetID: the affected memory's id (the canvas uses this for
// optimistic add/remove without a full refetch)
// - namespace: the v2 namespace the row landed in (canvas filters
// its display by readable-namespace intersection)
func logMemoryMCPActivity(ctx context.Context, broadcaster *events.Broadcaster, workspaceID, activityType, targetID, namespace string, summary *string) {
if broadcaster == nil {
return
}
source := workspaceID
target := targetID
method := namespace
LogActivity(ctx, broadcaster, ActivityParams{
WorkspaceID: workspaceID,
ActivityType: activityType,
SourceID: &source,
TargetID: &target,
Method: &method,
Summary: summary,
Status: "ok",
})
}