🌐 US-Proxy
class="logged-out env-production page-responsive" style="word-wrap: break-word;" >
Skip to content

fix: infer workspace claim time from build history for /agents delete dialog - #25057

Merged
kylecarbs merged 2 commits into
mainfrom
kyle/workspace-claimed-at
May 10, 2026
Merged

fix: infer workspace claim time from build history for /agents delete dialog#25057
kylecarbs merged 2 commits into
mainfrom
kyle/workspace-claimed-at

Conversation

@kylecarbs

@kylecarbs kylecarbs commented May 7, 2026

Copy link
Copy Markdown
Member

Closes CODAGT-317.

Problem

The /agents archive-and-delete molly-guard (typing the workspace name) was firing for chats that had clearly created their own workspace. The heuristic in resolveArchiveAndDeleteAction decides whether confirmation is needed by comparing the workspace's created_at against the chat's created_at:

return new Date(workspaceCreatedAt) >= new Date(chatCreatedAt);

That assumption breaks for prebuilt workspaces. ClaimPrebuiltWorkspace rewrites owner_id, name, updated_at, last_used_at, etc., but never touches created_at, which still reflects when the prebuild was provisioned by the reconciler, often hours before the chat exists. Result: every prebuild-claimed workspace looks pre-existing, so the molly-guard fires.

Concrete example from a real chat:

Field Value
chat.created_at 2026-05-07T15:12:23Z
workspace.created_at (provision) 2026-05-07T14:22:24Z
latest_build.created_at (claim) 2026-05-07T15:19:09Z

14:22:24 < 15:12:23 so isWorkspaceAutoCreated returned false even though the chat issued the claim.

Fix (frontend-only)

Derive the moment a workspace was acquired from existing build history rather than relying on workspace.created_at:

  • Build chore: Initial GHA workflow #1 initiator = prebuilds system user → workspace was a prebuild → use build_2.created_at (the claim build) as the acquisition time.
  • Build chore: Initial GHA workflow #1 initiator = real user → workspace was created from scratch → use workspace.created_at (unchanged behavior).
  • Unclaimed prebuild or no build history → return null (force confirmation; safe degradation for a destructive flow).

The resolver fetches the build list via the existing getWorkspaceBuilds endpoint when the dialog might fire. No new column, no migration, no schema change. Works retroactively for all existing claimed prebuilds; no backfill needed.

The prebuilds system user UUID is exposed via codersdk.PrebuildsSystemUserID and typegen'd to typesGenerated.ts. coderd/database.PrebuildsSystemUserID parses that constant via uuid.MustParse so the two cannot drift; if the codersdk literal ever changes, package init fails fast.

History

The first draft of this PR added a workspaces.claimed_at column populated by ClaimPrebuiltWorkspace. After review feedback from @johnstcn pointing out that the same fact is already implicit in build history, I pivoted to the frontend-only approach. Subsequent review notes consolidated the prebuilds system user UUID into a single typegen'd constant.

Why not the other open PRs

Test plan

  • pnpm vitest run --project=unit src/pages/AgentsPage/utils/agentWorkspaceUtils.test.ts — 40/40 pass; new cases cover prebuild claim before/after chat, unclaimed prebuild, missing-build-history fallback, and the fetch-skip when the chat is not in cache.
  • pnpm lint:types, pnpm check, make pre-commit.
Disclosure

Opened on behalf of @kylecarbs by Coder Agents.

@github-actions

github-actions Bot commented May 7, 2026

Copy link
Copy Markdown

Docs preview

📖 View docs preview for docs/admin/security/audit-logs.md

@kylecarbs
kylecarbs requested a review from johnstcn May 7, 2026 18:48
@kylecarbs
kylecarbs force-pushed the kyle/workspace-claimed-at branch from abac1b2 to c486b1d Compare May 7, 2026 19:53

@johnstcn johnstcn left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I looked at the up migration and started thinking about how to best backfill this data:

  • Look at build number 1 and check if it was a start transition initiated by the prebuilds user. If not, claimed_at = NULL.
  • Otherwise, check build number 2, validate that it was not initiated by the prebuilds user, and use build_2.created_at as claimed_at.

However, if that's the case, can we not just compute that in the UI?

… history

Replaces the previous draft of this PR (a backend workspaces.claimed_at
column plus migration plus SDK plumbing) with a frontend-only
heuristic per Cian's review.

The /agents archive-and-delete molly-guard previously compared
workspace.created_at against chat.created_at to decide whether to
require typing the workspace name. ClaimPrebuiltWorkspace never
updates workspace.created_at, so claimed prebuilds always looked
pre-existing and the dialog misfired.

Build history already records the truth: build #1's initiator is the
prebuilds system user iff the workspace was a prebuild, and build #2
is the claim. Compute that in the resolver and compare its created_at
against the chat. From-scratch workspaces fall through to
workspace.created_at as before.

The prebuilds system user UUID is hardcoded on the frontend; it lives
in coderd/database/constants.go on the backend and has not changed
since the prebuild feature shipped. If it ever moves, both sides have
to move together.

🤖 Generated with the help of Coder Agents.
@kylecarbs
kylecarbs force-pushed the kyle/workspace-claimed-at branch from c486b1d to c1b07bf Compare May 7, 2026 20:53
@kylecarbs kylecarbs changed the title feat: record claimed_at on workspaces and use it in agents UI fix(site/src/pages/AgentsPage): infer workspace claim time from build history May 7, 2026

Copy link
Copy Markdown
Member Author

@johnstcn good call, you're right that the column is unnecessary. Pivoted the PR to the frontend-only approach you sketched: derive the acquisition time from build history (build_1.initiator_id == prebuilds_system_user → use build_2.created_at, otherwise workspace.created_at). No migration, no SDK change, works retroactively for existing claimed prebuilds.

The one wart is hardcoding the prebuilds system user UUID on the frontend. Open to alternatives if you want a backend-exposed signal instead, but it's been stable since the prebuild feature shipped.

Force-pushed away the column-based draft; new diff is at c1b07bf60. Updated PR description has the rationale. PTAL.

Comment authored by Coder Agents on behalf of @kylecarbs.

@DanielleMaywood DanielleMaywood 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.

The change seems reasonable enough

Comment on lines +4 to +12
// The hard-coded UUID of the Coder prebuilds system user. Prebuilt
// workspaces are owned by this user until claim. Build #1 of a
// claimed workspace is permanently attributed to this user as the
// initiator, which is how we recognize prebuild claims after the
// fact.
//
// This UUID is stable and lives in coderd/database/constants.go on
// the backend. If it ever changes, both sides must move in lockstep.
const PREBUILDS_SYSTEM_USER_ID = "c42fdf75-3097-471c-8c33-fb52454d81c0";

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.

I know we're unlikely to change the prebuilds system user id but it kinda sucks we've got this second source of truth (We have another constant in the backend).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We could typegen + export it

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done in c2d2f5c. Added PrebuildsSystemUserID as a string constant in codersdk/prebuilds.go, switched coderd/database/constants.go to parse that constant via uuid.MustParse(codersdk.PrebuildsSystemUserID) so the two cannot drift, and the frontend now imports the typegen'd value from typesGenerated.ts.

Comment authored by Coder Agents on behalf of @kylecarbs.

Comment on lines +49 to +50
const build2 = builds.find((b) => b.build_number === 2);
return build2 ? build2.created_at : null;

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.

I can't entirely remember how prebuilds work but is it possible for a prebuild to have two builds?

@johnstcn johnstcn May 8, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

A number of prebuilds queries filter for workspace_builds using WHERE build_number = 1 so I think the assumption is safe.

workspaceAcquiredAt,
} from "./agentWorkspaceUtils";

const PREBUILDS_USER = "c42fdf75-3097-471c-8c33-fb52454d81c0";

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: just use PREBUILDS_SYSTEM_USER_ID?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done. Test now imports PrebuildsSystemUserID from typesGenerated.ts instead of redeclaring the literal.

Comment authored by Coder Agents on behalf of @kylecarbs.

Comment on lines +4 to +12
// The hard-coded UUID of the Coder prebuilds system user. Prebuilt
// workspaces are owned by this user until claim. Build #1 of a
// claimed workspace is permanently attributed to this user as the
// initiator, which is how we recognize prebuild claims after the
// fact.
//
// This UUID is stable and lives in coderd/database/constants.go on
// the backend. If it ever changes, both sides must move in lockstep.
const PREBUILDS_SYSTEM_USER_ID = "c42fdf75-3097-471c-8c33-fb52454d81c0";

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We could typegen + export it

Comment on lines +49 to +50
const build2 = builds.find((b) => b.build_number === 2);
return build2 ? build2.created_at : null;

@johnstcn johnstcn May 8, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

A number of prebuilds queries filter for workspace_builds using WHERE build_number = 1 so I think the assumption is safe.

Per Cian's review, expose the prebuilds system user UUID through
codersdk so it gets typegen'd into typesGenerated.ts. Single source
of truth: codersdk.PrebuildsSystemUserID is the string constant, and
coderd/database.PrebuildsSystemUserID parses it at package init via
uuid.MustParse(codersdk.PrebuildsSystemUserID), so any drift fails
fast at load time.

The frontend agentWorkspaceUtils helper and its test now both import
PrebuildsSystemUserID from typesGenerated instead of duplicating the
literal.

🤖 Generated with the help of Coder Agents.
@kylecarbs kylecarbs changed the title fix(site/src/pages/AgentsPage): infer workspace claim time from build history fix: infer workspace claim time from build history for /agents delete dialog May 10, 2026
@kylecarbs
kylecarbs marked this pull request as ready for review May 10, 2026 13:26
@kylecarbs
kylecarbs merged commit aaa0dac into main May 10, 2026
37 of 38 checks passed
@kylecarbs
kylecarbs deleted the kyle/workspace-claimed-at branch May 10, 2026 15:04
@github-actions github-actions Bot locked and limited conversation to collaborators May 10, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants