Repository navigation
Make checkpoint forks first-class workspace sessions (#311) - #324
Merged
Merged
Conversation
A fork of a workspace session now gets its own workspace row (manifest
copied from the source), so GET/suspend/resume/idle-suspend/delete work
on it like any other workspace. Delete goes through the ordinary
reconciler-rule delete path and releases the forked kagent Session with
no create ever sent. DELETE /checkpoints/{id} re-reads a checkpoint
kagent answers NotFound for, so it answers 409 (naming the pinning fork
session when known) instead of 200 while a fork still pins it.
The fork's MCP credential gap (kagent's ForkSession has nothing to
attach a credential reference to) is left as a documented, explicit
limitation rather than a workaround; a separate kagent-side change adds
the needed reference for a follow-up task to vendor.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The server-side openapi.yaml description change for DELETE /checkpoints/{id}
(#311) needed the generated TypeScript schema regenerated too.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Local tsc -b passes cleanly on the regenerated schema.gen.ts; no web source changed. The "Type check, lint, test, build" check run was cancelled (not failed) after the previous push superseded it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #311, for the part scoped to Mainloop (see "Scope decision" below).
What changes
Workspace row for a fork.
commitFork(server/internal/sessions/checkpoints.go) nowchecks whether the checkpoint's source session is itself a workspace. If it is, the fork gets
its own
workspacesrow in the same transaction as its session and binding, with the manifest(repo, ref, branch, depth, agent kind, dev settings, development environment) copied from the
source. The forked binding already carries the forked kagent Session id at that point (kagent's
ForkSessionreturned it), so the new workspace row needs noworkspace_opscreate operation —there is nothing to retry or replay.
GET /workspaces/{fork}then works, and so do suspend,resume, idle suspend and delete, through the same code paths as any workspace. A fork of a
non-workspace session (a plain
/agentssession, or the main thread) gets no workspace row:there's no manifest to copy and nothing invents one.
Delete releases kagent. This needed no new code: once the fork has a workspace row with an
already-bound kagent Session id,
workspaces.Service.Delete's existing reconciler-rule path(
workspace_opsintent row, thendoDelete) findssid != ""and calls kagent'sDeleteSessiondirectly, releasingpinned_checkpoint_id. No create is ever sent for thisworkspace, so there's nothing to replay first.
Agent credential — not attempted, left explicit. Per the task's scope decision: kagent's
ForkSessionRequesthas no credential-reference field (checkpoints.proto, andsessions.proto's own comment that forks don't inherit credential bindings), so Mainloop alonecan't issue the fork an MCP credential. The forked binding keeps
mcp_grant_kind = 'none'(asit already did before this change) and gets no credential — the same field an ordinary
ungranted session carries, now meaning "no credential yet" rather than "never asked for one"
now that the fork is otherwise a normal workspace. No workaround attempted. Docs (below) say so
and link A forked session has no workspace or credential, and the checkpoint it pins can't be deleted #311.
Checkpoint delete tells the truth.
DeleteCheckpointnow re-reads a checkpoint after kagentanswers NotFound for deleting it (kagent's own delete skips a pinned checkpoint and answers
NotFound the same way it does for one that's genuinely gone). If the checkpoint is still there,
it answers 409, naming the pinning fork session when Mainloop's own
checkpoint_forksrecordsknow it (
checkpoint is pinned by fork session <id>), else the genericcheckpoint is pinned by a fork. It never answers 200 for a checkpoint that still exists.api/openapi.yaml'sdescription was updated and the Go types regenerated (
go generate ./internal/api/...); theDELETE /checkpoints/{id}409 response shape already existed in the contract.Tests (fake kagent + scratch PostgreSQL):
server/internal/sessions/checkpoints_test.go: fork of a workspace creates the workspace rowwith the copied manifest and no create op (
TestForkOfAWorkspaceCreatesAWorkspaceRow); forkof a plain session gets none (
TestForkOfAPlainSessionGetsNoWorkspaceRow); an already-gonecheckpoint delete is still 200 (
TestDeleteCheckpointAlreadyGoneIsStill200); a checkpointstill pinned by a fork is 409 naming it (
TestDeleteCheckpointStillPinnedByAForkIs409) orgenerically when the pinning fork isn't known
(
TestDeleteCheckpointStillPinnedWithNoKnownForkNamesItGenerically).server/internal/workspaces/fork_test.go: a workspace row built the way a fork leaves it(already bound, no create op) has its kagent Session deleted by the ordinary delete path
(
TestDeleteReleasesAForkedWorkspacesKagentSession).Run from
server/:go test ./internal/sessions/... ./internal/workspaces/...(needsMAINLOOP_TEST_DATABASE_URL). Not measured against a live kagent; the supervisor verifies onmainloop-devafter merge, as instructed.Docs.
docs/specs/sessions.mdgets a new "Forked sessions (issue A forked session has no workspace or credential, and the checkpoint it pins can't be deleted #311)" section;docs/specs/workspaces.mdgets "Forked workspaces (issue A forked session has no workspace or credential, and the checkpoint it pins can't be deleted #311)";docs/specs/warm-state.md'scheckpoint/fork API description and its "Open, A forked session has no workspace or credential, and the checkpoint it pins can't be deleted #311" bullet are updated to describe the
implemented behavior, with what's proved by the fake-backed tests above and what isn't (live
kagent).
Follow-up (not in this PR)
Fork authentication is a separate kagent task on
oldsj/kagent(branchmainloop-substrate-0-4),adding credential references to
ForkSessionRequest. Once that lands and the vendored pin isbumped, a follow-up Mainloop task vendors the new proto and issues per-fork credentials — the
binding's
mcp_grant_kindfield and the workspace-row shape here are both ready for that; nointerface changes anticipated.
Also noted per the task: if kagent were to answer
FailedPreconditioninstead ofNotFoundfor apinned-checkpoint delete,
DeleteCheckpoint's existingFailedPreconditionbranch already mapsthat straight to 409 with kagent's own detail — a possible kagent-side improvement, not made here.
Verification
go build ./...,go vet ./...,go mod tidy(no drift),golangci-lint runon the changedpackages: clean.
go generate ./...: no drift beyond the openapi.yaml-drivenapi.gen.goand the newListForksByCheckpointsqlc query.pnpm dlx redocly lint api/openapi.yaml: valid (pre-existing warnings only).trunk check --upstream origin/main: no issues.dev_syncto mainloop-dev: succeeded, health/migrate ok, smoke 10 ok / 0 fail / 1 skip (sync id23155b732a7369f06b5daf430b42e074dd96f96ffa8cad11719603d8e4422431).