feat: include agent metadata in workspace list responses - #27934
Conversation
Agent metadata could only be read by opening a watch stream per agent, so a consumer inspecting N workspaces made N+1 requests per pass for state coderd already stores. The workspaces list query now aggregates the requested keys as JSON when the new include_agent_metadata search key opts in, and the response attaches them to each agent as metadata. The expansion is key-scoped and opt-in because values can be 64KiB each; without it the response is unchanged and the aggregate subquery never runs. Closes #27933
…data The script is the collection command, not collected state; it can be long and list consumers want values. The description's script field is always empty on the workspaces list endpoint.
…a aggregate The latest_build lateral already resolves the provisioner job; propagate it through the CTE chain as latest_build_provisioner_job_id so the agent_metadata aggregate joins resources by job ID instead of re-deriving the latest build with a max(build_number) lookup.
…base package sqlc column overrides require tablename.colname against a real relation, and agent_metadata is a query expression, so a types.go Scanner cannot be wired to the generated row. Instead the row gains ParseAgentMetadata, keeping the JSON handling in the database package and the handler free of unmarshaling.
… ACL columns AgentMetadataAggregate moves to types.go with Scan/Value, matching how ConvertWorkspaceRows handles user_acl and group_acl; the handler scans the row's raw JSON into the typed aggregate.
| workspace_resources.job_id = fwos.latest_build_provisioner_job_id | ||
| -- Filter out deleted sub agents. | ||
| AND workspace_agents.deleted = FALSE | ||
| AND LOWER(workspace_agent_metadata.key) = ANY(@include_agent_metadata :: text[]) |
There was a problem hiding this comment.
Lowercase normalization is applied to the key but not the input array. Might be surprising to callers.
| // @Security CoderSessionToken | ||
| // @Produce json | ||
| // @Tags Workspaces | ||
| // @Param q query string false "Search query in the format `key:value`. Available keys are: owner, template, name, status, has-agent, dormant, last_used_after, last_used_before, has-ai-task, has_external_agent, healthy." |
There was a problem hiding this comment.
Missing doc on new filter param.
|
Some small issues found with meat+agents. One potential timezone serialization issue depending on postgres defaults - probably worth fixing but I'll leave it to you. Agent note below.
|
BobbyHo
left a comment
There was a problem hiding this comment.
Nice change — batching this instead of making N per-workspace watch-stream calls is a great optimization. The changes LGTM overall. I just have one question related to behavior at larger-scale deployments, but it’s non-blocking.
| -- workspace_agent_id so multi-agent workspaces can map values onto | ||
| -- the right agent. Keys match case-insensitively because search | ||
| -- queries are lowercased. | ||
| CASE WHEN cardinality(@include_agent_metadata :: text[]) > 0 THEN |
There was a problem hiding this comment.
Question (not a blocker):
if I understand correctly, this:
LIMIT
CASE
WHEN @limit_::integer > 0 THEN
@limit_
END
means no LIMIT is applied when limit_ <= 0 (the CASE evaluates to NULL), which matches ParsePagination's 0 = no limit convention used by other list endpoints.
agent_metadata's jsonb_agg also seems to be the first subquery here whose output size can grow with the amount of matching data, rather than collapsing to a fixed-size value like the other filters.
Do we feel comfortable extending the existing unbounded-query behavior with a per-row payload that can also grow unbounded? Or would it be worth running an EXPLAIN ANALYZE benchmark against a large, multi-thousand-workspace fixture with include_agent_metadata + limit=0 first, just to see whether we need a guardrail for larger deployments?
Closes #27933. Related: #27897 (single-agent GET).
Agent metadata is only readable via a per-agent watch stream, so reading it across N workspaces costs N+1 requests. This adds a batch read to the list endpoint:
include_agent_metadatasearch key, repeatable and key-scoped. It expands the response, it does not filter workspaces.GetWorkspacesaggregates the requested keys as JSON behind aCASE: without opt-in the response is unchanged and the subquery never runs. Runs only for the returned page, inside the same authorized query.metadata([]codersdk.WorkspaceAgentMetadata,omitempty), mapped by theworkspace_agent_ideach element carries. The collection script is omitted; it can be long.codersdk.WorkspaceFiltergainsIncludeAgentMetadata []string.Authored by Coder Agents on behalf of @Emyrk.