Skip to content

feat: include agent metadata in workspace list responses - #27934

Open
Emyrk wants to merge 6 commits into
mainfrom
steven/workspaces-include-agent-metadata
Open

feat: include agent metadata in workspace list responses#27934
Emyrk wants to merge 6 commits into
mainfrom
steven/workspaces-include-agent-metadata

Conversation

@Emyrk

@Emyrk Emyrk commented Aug 6, 2026

Copy link
Copy Markdown
Member

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:

GET /api/v2/workspaces?q=param:"pool=demo" include_agent_metadata:task_status
  • New include_agent_metadata search key, repeatable and key-scoped. It expands the response, it does not filter workspaces.
  • GetWorkspaces aggregates the requested keys as JSON behind a CASE: without opt-in the response is unchanged and the subquery never runs. Runs only for the returned page, inside the same authorized query.
  • Agents in the response gain metadata ([]codersdk.WorkspaceAgentMetadata, omitempty), mapped by the workspace_agent_id each element carries. The collection script is omitted; it can be long.
  • codersdk.WorkspaceFilter gains IncludeAgentMetadata []string.
  • No wildcard, no schema change, no migration.

Authored by Coder Agents on behalf of @Emyrk.

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
Comment thread coderd/workspaces.go Outdated
…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.
@linear-code

linear-code Bot commented Aug 6, 2026

Copy link
Copy Markdown

PLAT-453

Emyrk added 4 commits August 6, 2026 22:05
…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.
@Emyrk
Emyrk marked this pull request as ready for review August 6, 2026 22:31
@Emyrk
Emyrk requested a review from DanielleMaywood as a code owner August 6, 2026 22:31
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[])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lowercase normalization is applied to the key but not the input array. Might be surprising to callers.

Comment thread coderd/workspaces.go
// @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."

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing doc on new filter param.

@jscottmiller

Copy link
Copy Markdown
Contributor

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.

BLOCKER (Priya) — collected_at produces unparseable JSON on non-UTC Postgres sessions. jsonb_build_object renders timestamptz in the session TimeZone; collected_at defaults to year 1 and is never set by InsertWorkspaceAgentMetadata, so a named non-UTC zone yields an LMT offset with seconds (0001-12-31T19:03:58-04:56:02 BC), which Go refuses. That 500s the entire list page. Coder never pins TimeZone in the DSN and initdb inherits the host zone. The new test always sets collected_at, so it structurally cannot catch this. Priya reproduced it end to end. Fix: to_char(... AT TIME ZONE 'UTC', ...) in SQL. Dana independently flagged the year-1 timestamp as a shape problem (age ≈ 6.4e10), which corroborates the underlying condition even though only Priya traced it to a 500.

@BobbyHo BobbyHo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Include agent metadata in workspace list responses

3 participants