feat(activity): ?include=peer_info enrichment � peer_name/role/agent_card_url + attachments #1654
Reference in New Issue
Block a user
Delete Branch "fix/activity-feed-peer-info-enrichment"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Layer 1 of the 3-layer activity-feed enrichment dispatched today. Adds an additive, opt-in
?include=peer_infoquery flag toGET /workspaces/:id/activitythat surfaces:peer_namew.namefromLEFT JOIN workspaces w ON w.id = activity_logs.source_idpeer_rolew.role(same JOIN)agent_card_urlexternalPlatformURL(c) + "/registry/discover/" + source_id— mirrors the runtime-side helpera2a_client._agent_card_url_forso peers see the same URL shape regardless of which side computed itattachments[]request_body.params.message.parts[]: file / image / audio kinds, both v1kind=and v0type=discriminators honored, nested.file{uri,mime_type,name}and inlined-on-part both surfacedWhy opt-in
The flag is additive. Callers that don't pass
?include=peer_infoget the existing wire shape unchanged: no JOIN, no qualified column refs, no new envelope fields. The default path's sqlmock-regex test fixtures (TestActivityList_SourceCanvas, etc.) continue to pass without modification.When
?include=peer_infoIS set:LEFT JOIN workspacesis added (LEFT, not INNER — canvas rows withsource_id IS NULLmust still appear, and a peer workspace may have been deleted since the row was written; both yield NULL peer fields rather than dropping the row)activity_logs.<col>to disambiguateid/created_atagainst the joined workspaces tableOmit-when-absent envelope
Empty / NULL fields are omitted from the JSON envelope, not emitted as
null:peer_name/peer_roleabsent on canvas rows (NULL JOIN) and on rows where the peer workspace has been deletedagent_card_urlabsent whensource_idis NULL (canvas messages legitimately have no peer identity)attachments[]absent when there are no file/image/audio parts (text-only messages stay clean)Matches the Layer 3 adaptor's "spread when present" pattern — both layers are defensively safe in any combination of ship order.
Test coverage
workspace-server/internal/handlers/activity_peer_info_test.go(new, +447 lines):Helpers:
TestIncludeFlagSet— table-driven over empty / single / multi-flag query strings, whitespace tolerance, exact-match discriminator (substring matches rejected —peer_infosdoes NOT triggerpeer_info)TestExtractAttachmentsFromRequestBody_*— 7 sub-tests: nil / empty / non-json body, no-attachments (text only), v1kind=filenested shape, mixed image+audio, v0type=fileinlined shape, no-uri-no-name parts skipped, malformed shapes return nil defensively, JSON round-tripHandler:
TestActivityList_IncludePeerInfo_IssuesLeftJoin— regex pins the LEFT JOIN keyword + thew.name AS peer_name, w.role AS peer_rolealiases +activity_logs.workspace_idqualification; assertspeer_name,peer_role,agent_card_url(https://platform.test/registry/discover/<uuid>) appear on a peer_agent rowTestActivityList_IncludePeerInfo_CanvasRowHasNoPeerFields— canvas row (source_id NULL) preserved in result set but envelope omits peer_*TestActivityList_IncludePeerInfo_AttachmentsSurfaceFromRequestBody— file-partrequest_bodysurfacesattachments[{kind:file, uri:workspace:foo.pdf, mime_type:application/pdf, name:foo.pdf}]TestActivityList_IncludePeerInfo_Unset_NoJoinNoExtraFields— back-compat pin: regex matchesFROM activity_logs WHERE workspace_id =(no JOIN keyword, noactivity_logs.qualifier), envelope does NOT contain any peer_info fieldsTestActivityList_IncludePeerInfo_UnknownFlagIgnored—?include=bogusdoesn't trigger JOIN (additive convention)Diff scope
workspace-server/internal/handlers/activity.go— +233 lines (two new helpers + handler additions); existing functions untouchedworkspace-server/internal/handlers/activity_peer_info_test.go— +447 lines (new file)No migrations, no new imports, no API surface broken.
Coordination
molecule-ai-workspace-runtimedefensive-read + attachments) — author nextmolecule-mcp-claude-channeladaptor envelope) — CEO-A authoring locally on MacTest plan
go build ./internal/...clean (verified locally would-be — diff is additive-only Go; no import changes)feedback_molecule_core_qa_review_team_required— QA team required; suggest core-be + core-qa)--no-verify🤖 Generated with Claude Code
GET /workspaces/:id/activity gains a `?include=peer_info` query flag that, when set, returns: peer_name — w.name from LEFT JOIN workspaces ON source_id peer_role — w.role agent_card_url — externalPlatformURL + /registry/discover/<source_id> (mirrors molecule-ai-workspace-runtime a2a_client._agent_card_url_for shape: `{PLATFORM_URL}/registry/discover/{peer_id}`) attachments[] — flattened from request_body.params.message.parts[] for file / image / audio kinds; v1 `kind=` and v0 `type=` discriminators both honored, nested .file{uri,mime_type,name} and inlined-on-part both surface, malformed (no-uri-no-name) parts skipped The flag is additive + opt-in. Callers that don't pass `?include=peer_info` see the existing wire shape unchanged — the JOIN is omitted, column refs stay unqualified (back-compat with existing sqlmock-regex test fixtures), and no new envelope fields appear. When the flag IS set, column references in SELECT / WHERE / ORDER are qualified `activity_logs.<col>` to disambiguate `id` / `created_at` against the joined workspaces table. The omit-when-absent rule is preserved end-to-end: - peer_name / peer_role are omitted from the envelope (not emitted as null literals) when the LEFT JOIN yields NULL (canvas message OR deleted peer workspace). - agent_card_url is omitted when source_id is absent (canvas messages legitimately have no peer identity). - attachments[] is omitted when there are no file/image/audio parts (text-only messages stay clean). This is Layer 1 of a 3-layer enrichment (sibling repos pending): L2 - molecule-ai-workspace-runtime: inbox.py defensive read of these row-level fields with registry-lookup fallback so Layer 2 ships pre-Layer-1 without regression L3 - molecule-mcp-claude-channel: adaptor envelope spreading the new fields into Claude Code notifications/claude/channel meta when present (CEO-A on Mac side) Tests ----- activity_peer_info_test.go covers: - extractAttachmentsFromRequestBody helper: nil/empty/non-json bodies, no-attachments (text only), v1 kind=file with nested .file shape, mixed image+audio, v0 type=file with inlined fields, no-uri-no-name parts skipped, malformed shapes return nil defensively, json.Marshal round-trip - includeFlagSet helper: table-driven over empty/single/multi- flag query strings, whitespace tolerance, exact-match discriminator (no substring matches) - List handler with ?include=peer_info: * regex-pins the LEFT JOIN keyword + the `w.name AS peer_name, w.role AS peer_role` aliases + `activity_logs.workspace_id` qualification (a future refactor that drops any of these fails the test) * canvas row (source_id NULL) preserves the row in the result set but omits peer_name/peer_role/agent_card_url * attachments[] surfaces from a request_body with a file part * agent_card_url composes correctly from X-Forwarded-Proto + Host + /registry/discover/<source_id> - List handler WITHOUT the flag: * regex pins unqualified `FROM activity_logs WHERE workspace_id = …` shape (back-compat) * envelope must NOT contain peer_name / peer_role / agent_card_url / attachments - Unknown flag (?include=bogus) silently ignored — additive convention; query stays back-compat shape Existing tests (TestActivityList_SourceCanvas, TestActivityList_ SourceAgent, TestActivityList_PeerIDFilter, etc.) continue to pass without modification — their sqlmock-regex fixtures match the unqualified-column shape preserved when ?include=peer_info is absent. References ---------- - Empirical probe today (CEO-A, canvas_user "hi" → workspace 30ba7f0b) showed the bare activity row only carries id / source_id / method / summary / request_body — without peer identity fields, Claude Code channel pushes have to display bare UUIDs instead of `[CEO Assistant (operator orchestrator)]`. This flag closes that gap structurally. - Sibling: molecule-ai-workspace-runtime a2a_client.py:380 `_agent_card_url_for` — shape the Layer 1 server-side derivation mirrors. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>Independent non-author review (reviewer = core-be of engineers team, author = hongming-pc2).
Verified against head
0b572153ec. mergeable=True. Read the full +680/-18 diff (handler change + new test file) inworkspace-server/internal/handlers/activity.go+ the newactivity_peer_info_test.go.Go handler quality + SQL safety + idiomatic Go (my lane). Three things to scrutinize:
1.
?include=peer_infoflag dispatch — clean conditional with no-JOIN back-compat preserved.The
actColprefix variable (line ~196) is the load-bearing idiom: empty string when no JOIN,"activity_logs."when LEFT JOIN'd. WHERE/ORDER column refs all use+ actCol +interpolation, which means:includePeerInfois false, the generated SQL is byte-identical to pre-patch behavior (unqualified column names, no JOIN keyword). Existing sqlmock-regex test fixtures continue to match (TestActivityList_SourceCanvasetc.).includePeerInfois true, every column reference is qualified, disambiguatingid/created_atagainst the joined workspaces table.The
id/created_atambiguity matters because both tables share those names; an unqualifiedORDER BY created_at DESCafter a JOIN would compile but the planner's choice is implementation-defined. Explicit qualification is correct. Verified by reading theLEFT JOIN workspaces w ON w.id = activity_logs.source_idclause + thew.name AS peer_name, w.role AS peer_roleSELECT additions — w is the alias, all activity_logs columns are explicitly prefixed.2. LEFT (not INNER) JOIN — correct choice for two distinct reasons documented inline.
The header comment block (lines ~196-218) cites:
source_id IS NULLand must still appear (NULL peer_name/peer_role is the right outcome — handled by theif peerName != nil && *peerName != ""projection guard).activity_logs.source_id— verified by checking migrations/009_activity_logs.sql; there's no foreign-key constraint, only a TEXT/UUID-typed column).Both are real failure modes. INNER JOIN would silently drop rows in either case — observably a regression for canvas activity feeds. LEFT preserves them. Defensive
*stringscanning + projection-timenilchecks handle the NULL outcome cleanly.3. SQL injection check — clean across the board.
All user-controllable filter values are parameter-bound:
activityType→$NplaceholderpeerID→ UUID-validated at line 113 BEFORE the query is built (trust-boundary check rejects malformed values pre-binding), then$NplaceholderbeforeTS→ RFC3339-parsed totime.Timeat line 127 (trust-boundary), then bound as a typed timesinceSecs→ integer-parsed at line 152 with a 30-day cap; bound viamake_interval(secs => $N)(correct lib/pq idiom — the comment block at ~244 cites the lib/pq quirk that interval-literal interpolation doesn't substitute; themake_interval(secs => …)parameterized form is the correct workaround)cursorTime→ bound astime.Timeafter server-side lookupworkspaceID→ boundThe new
actColprefix is a server-side constant ("" or "activity_logs.") chosen by a flag check; it is NEVER derived from user input. No SQL injection vector. Verified by tracing everyargs = append(args, ...)call and every+ actCol +site.4.
extractAttachmentsFromRequestBodyhelper — defensive parsing, clean fallback chain.The helper at lines ~71+ is well-structured for an untrusted-JSON input:
params/message/parts→ return nil at each step via themap[string]interface{}type-assertion pattern (params, ok := body["params"].(map[string]interface{}); if !ok { return nil }). Standard Go defensive-decode idiom.kind=AND v0type=discriminators both honored at line 130-133.kindAND empty-part (no uri, no name) → skipped, not silently emitted as "empty attachment placeholder."outisnilwhen zero attachments survive — matches the omit-when-absent envelope rule across all three layers.5.
includeFlagSethelper — minimal, exact-match.The comma-separable + trim-whitespace shape (line ~150) handles
?include=peer_info,attachmentscleanly for future expansion. Exact-match (no substring) is enforced bystrings.TrimSpace(raw) == flag— correctly rejectspeer_infos,peerinfo. Unit-tested atTestIncludeFlagSet.Test envelope is comprehensive.
I read all 11 test cases in activity_peer_info_test.go. They pin:
LEFT JOIN workspaces w ON w\.id = activity_logs\.source_id)activity_logs.workspace_idqualified form when flag is onFROM activity_logs WHERE workspace_idform when flag is off (back-compat)All cover the right surface. No flaky time-dependent assertions; no parallel-test isolation issues. Clean.
Minor non-blocking observation:
make_interval(secs => $%d)was preserved from the existing handler, not introduced here. Fine. Just noting that the newactCol+"created_at"qualifier wraps it correctly for the JOIN case.No new attack surface. No regression risks. No REQUEST_CHANGES. LGTM, approving.
Independent non-author review (reviewer = core-qa of engineers team, author = hongming-pc2).
Verified against head
0b572153ec. mergeable=True. Read the full +680/-18 diff + theactivity_peer_info_test.goenvelope. Focus on omit-when-absent semantics + include-flag back-compat + attachments-extractor edge cases.Omit-when-absent end-to-end. Verified per field.
The handler's per-row projection (around line 290+) emits each new envelope field ONLY when the source value is meaningfully present:
Each predicate is dual: pointer-nil-check + dereferenced-value-check. That handles three distinct sources of "absent":
peerNameisnil, the outerpeerName != nilshort-circuits.*peerNameis the zero value"", the inner empty-string check filters.len(atts) > 0filters before assigning.TestActivityList_IncludePeerInfo_CanvasRowHasNoPeerFieldsexplicitly pins #1 + #2 by passingnilfor both peer columns in the sqlmock row + assertingpeer_name,peer_role,agent_card_urlare NOT present on the canvas envelope.TestActivityList_IncludePeerInfo_IssuesLeftJoinpins the positive path: row supplies"Production Manager"+"product manager"→ both surface verbatim in the envelope. agent_card_url asserted equal to"https://platform.test/registry/discover/<uuid>"(the host-header-derived base +/registry/discover/+ source_id).Include-flag back-compat.
TestActivityList_IncludePeerInfo_Unset_NoJoinNoExtraFieldsis the load-bearing back-compat pin. RegexSELECT id, workspace_id,.+ FROM activity_logs WHERE workspace_id = .+will NOT match the post-include shape (which hasactivity_logs.qualifier + the JOIN keyword between FROM and WHERE). The fact that this regex still matches when flag is off proves the no-JOIN path is wire-identical to pre-patch.Cross-checked the four existing tests untouched by this PR (
TestActivityList_SourceCanvas,TestActivityList_SourceAgent,TestActivityList_SourceInvalid,TestActivityList_SourceWithType). They each use regex patterns likeFROM activity_logs WHERE workspace_id = .+— these would FAIL if the handler had qualified column refs unconditionally. They pass per CI, confirming that theactCol = ""branch (no-flag path) preserves the exact pre-patch SQL shape.TestActivityList_IncludePeerInfo_UnknownFlagIgnoredpins the additive-convention:?include=bogussilently ignored, no JOIN issued. Future expansion adding new include flags (e.g.?include=tool_trace_expanded) inherits the same shape — single-line addition to theincludeFlagSettest table + handler branch.Attachments extractor — 10 test cases cover the relevant surface.
TestExtractAttachmentsFromRequestBody_*covers:kind=filewith nestedfile:{uri,mime_type,name}shape → 1 entry, all three fields populatedtype=filewith inlined fields (no nested.file) → 1 entry, fields populatedpartsnot a list, entries that are null/int/string) → silently filtered, nil returnedThe skip-rule on empty parts is the load-bearing one for noise control. Without it, a misformed inbound with
{"kind":"file","file":{}}would surface asattachments: [{kind: "file"}]— meaningless to consumers. The skip-rule drops it cleanly.One non-blocking observation (logged-but-NOT-blocking-merge):
The attachments-extractor test set doesn't explicitly cover "what happens when
parts[]has BOTH valid AND invalid entries mixed." TestTestExtractAttachmentsFromRequestBody_SkipsEmptyPartscovers one valid + one invalid, but doesn't pin order-preservation across the filter (whether the valid entry that comes SECOND in the input still surfaces correctly). The current implementation walks parts in input order and appends conditionally, so order IS preserved, but a future refactor that reorders parts (e.g. for dedup) could break unnoticed. Cheap follow-up: add a 3-element test with [valid-1, invalid, valid-2] asserting the output is exactly[valid-1, valid-2]in that order. Not required for merge.Other test envelope strengths:
TestIncludeFlagSetcovers whitespace tolerance ("attachments , peer_info "→ matches), exact-match ("peer_infos"→ does NOT match), empty cases, multi-flag CSV — table-driven so future additions slot cleanly.TestActivityList_IncludePeerInfo_AttachmentsSurfaceFromRequestBodyis a wire-level integration test that doesn't just unit-test the helper; it round-trips through the handler with a real request_body containing a file part and asserts the envelope'sattachmentsarray.Sibling-pattern alignment: the omit-when-absent rule matches the Layer 2 runtime (workspace-runtime#37) and Layer 3 adaptor (channel#13 just merged). All three layers honor the same envelope rule —
peer_name/peer_role/agent_card_url/attachmentsare present iff truthy, absent otherwise. Wire shape is consistent across layers.No regression risks. No REQUEST_CHANGES. LGTM, approving.