Skip to content

Make checkpoint forks first-class workspace sessions (#311) - #324

Merged
oldsj-mainloop[bot] merged 3 commits into
mainfrom
mainloop/fork-binding-311-c
Oct 23, 2026
Merged

oldsj-mainloop[bot] merged 3 commits into
mainfrom
mainloop/fork-binding-311-c

Conversation

@oldsj-mainloop

Copy link
Copy Markdown
Contributor

Fixes #311, for the part scoped to Mainloop (see "Scope decision" below).

What changes

  1. Workspace row for a fork. commitFork (server/internal/sessions/checkpoints.go) now
    checks whether the checkpoint's source session is itself a workspace. If it is, the fork gets
    its own workspaces row 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
    ForkSession returned it), so the new workspace row needs no workspace_ops create 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 /agents session, or the main thread) gets no workspace row:
    there's no manifest to copy and nothing invents one.

  2. 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_ops intent row, then doDelete) finds sid != "" and calls kagent's
    DeleteSession directly, releasing pinned_checkpoint_id. No create is ever sent for this
    workspace, so there's nothing to replay first.

  3. Agent credential — not attempted, left explicit. Per the task's scope decision: kagent's
    ForkSessionRequest has no credential-reference field (checkpoints.proto, and
    sessions.proto's own comment that forks don't inherit credential bindings), so Mainloop alone
    can't issue the fork an MCP credential. The forked binding keeps mcp_grant_kind = 'none' (as
    it 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.

  4. Checkpoint delete tells the truth. DeleteCheckpoint now re-reads a checkpoint after kagent
    answers 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_forks records
    know it (checkpoint is pinned by fork session <id>), else the generic checkpoint is pinned by a fork. It never answers 200 for a checkpoint that still exists. api/openapi.yaml's
    description was updated and the Go types regenerated (go generate ./internal/api/...); the
    DELETE /checkpoints/{id} 409 response shape already existed in the contract.

  5. Tests (fake kagent + scratch PostgreSQL):

    • server/internal/sessions/checkpoints_test.go: fork of a workspace creates the workspace row
      with the copied manifest and no create op (TestForkOfAWorkspaceCreatesAWorkspaceRow); fork
      of a plain session gets none (TestForkOfAPlainSessionGetsNoWorkspaceRow); an already-gone
      checkpoint delete is still 200 (TestDeleteCheckpointAlreadyGoneIsStill200); a checkpoint
      still pinned by a fork is 409 naming it (TestDeleteCheckpointStillPinnedByAForkIs409) or
      generically 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/... (needs
    MAINLOOP_TEST_DATABASE_URL). Not measured against a live kagent; the supervisor verifies on
    mainloop-dev after merge, as instructed.

  6. Docs. docs/specs/sessions.md gets 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.md gets "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's
    checkpoint/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 (branch mainloop-substrate-0-4),
adding credential references to ForkSessionRequest. Once that lands and the vendored pin is
bumped, a follow-up Mainloop task vendors the new proto and issues per-fork credentials — the
binding's mcp_grant_kind field and the workspace-row shape here are both ready for that; no
interface changes anticipated.

Also noted per the task: if kagent were to answer FailedPrecondition instead of NotFound for a
pinned-checkpoint delete, DeleteCheckpoint's existing FailedPrecondition branch already maps
that 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 run on the changed
    packages: clean.
  • go generate ./...: no drift beyond the openapi.yaml-driven api.gen.go and the new
    ListForksByCheckpoint sqlc query.
  • pnpm dlx redocly lint api/openapi.yaml: valid (pre-existing warnings only).
  • trunk check --upstream origin/main: no issues.
  • Targeted tests above: passing against a scratch PostgreSQL.
  • dev_sync to mainloop-dev: succeeded, health/migrate ok, smoke 10 ok / 0 fail / 1 skip (sync id
    23155b732a7369f06b5daf430b42e074dd96f96ffa8cad11719603d8e4422431).

oldsj and others added 3 commits October 23, 2026 02:42
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>
@oldsj-mainloop
oldsj-mainloop Bot merged commit 003f05c into main Oct 23, 2026
13 checks passed
@oldsj-mainloop
oldsj-mainloop Bot deleted the mainloop/fork-binding-311-c branch October 23, 2026 03:11
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.

A forked session has no workspace or credential, and the checkpoint it pins can't be deleted

1 participant