Skip to content

Delete a run/session completely #23

Description

@ableinc

From the UI, give the user the option to completely delete the history of a run/session completely. There should no longer be a reference or history of it in the database. Add a confirmation dialog before completely deleting it. Also, add a button to "stop" a specific run/session if its currently running. If the user wants to delete a running session, then it should be stopped first then deleted.

Activity

  1. ableinc commented on Sep 6, 2026

    @ableinc
    OwnerAuthor

    Plan

    I have everything needed to write the plan.

    Delete a run/session completely, with confirmation + stop-if-running

    Context

    Issue #23 asks for two related controls in the web console: (1) a per-run "Delete" action that removes a run and all trace of it from the database, gated behind a confirmation dialog, and (2) a per-run "Stop" action for runs currently in flight. Deleting a currently-running run must stop it first, then delete it. Today the console can only Cancel an in-flight run (which marks it canceled in the DB but keeps the row/history forever); there is no delete path anywhere in the code (grep for DeleteRun/DELETE FROM runs finds nothing) and no confirmation-dialog component exists in the frontend at all.

    Backend changes

    internal/store/store.go — add a DeleteRun method next to the other run CRUD (after FailRun/GetRun, ~line 588-634), following the exact pattern already used by ReleaseClaim (line 379-386) and ClearGate (line 892):

    // DeleteRun permanently removes a run and every trace of it: its events,
    // its sessions row, and its on-disk transcript. There is no undo.
    func (s *Store) DeleteRun(ctx context.Context, runID string) error {
        tx, err := s.db.BeginTx(ctx, nil)
        ...
        tx.ExecContext(ctx, `DELETE FROM events WHERE run_id = ?`, runID)
        tx.ExecContext(ctx, `DELETE FROM sessions WHERE run_id = ?`, runID)
        res, err := tx.ExecContext(ctx, `DELETE FROM runs WHERE id = ?`, runID)
        // if rows affected == 0, return ErrNotFound
        tx.Commit()
    }

    Since there are no declared foreign keys between runs, events, and sessions (confirmed — PRAGMA foreign_keys(1) is set at line 170 but nothing is declared to enforce), cleanup of events and sessions rows must be done manually inside a transaction, not relied upon via cascade. Return store.ErrNotFound (the existing sentinel, line 601) if the runs delete affected 0 rows, matching GetRun's behavior.

    The transcript file itself (run.LogPath, e.g. internal/orchestrator/loop.go:454) should also be removed. Do this in the server handler, not the store, since the store package doesn't currently reach into the workspace/logs filesystem beyond what's already passed in — fetch the run first (GetRun), delete the DB rows, then os.Remove(run.LogPath) best-effort (ignore os.IsNotExist).

    internal/server/server.go:

    • Add deleteRun handler near cancelRun (line 410-421):
      • Fetch the run via s.store.GetRun — 404 via s.fail if errors.Is(err, store.ErrNotFound).
      • If !store.IsTerminal(run.Status) (i.e., it's in flight), call s.ctrl.Cancel(id) first. Cancel is async (it only cancels the context; the orchestrator's own goroutine does FailRun moments later — see loop.go 150-159, 1065), so don't require it to succeed or block waiting for the terminal state — the delete should proceed regardless, since deleting the row doesn't depend on what status it eventually lands on. If s.ctrl is nil (no controller attached, e.g. in some deployment modes) just skip straight to delete.
      • Call s.store.DeleteRun(c.Context(), id), then best-effort os.Remove(run.LogPath).
      • Return c.JSON(fiber.Map{"deleted": true, "run": id}).
    • Register the route in routes() (line 94-122). Following house style — every mutation in this codebase is POST, never a DELETE verb (/pause, /resume, /runs/:id/cancel are all POST) — add:
      s.app.Post("/runs/:id/delete", s.deleteRun)
      (Design decision, flagged for the reviewer: a real DELETE /runs/:id would be more RESTful, but POST .../delete matches the zero precedent-breaking convention already established for /runs/:id/cancel. Recommend sticking with POST /runs/:id/delete unless the reviewer prefers introducing the DELETE verb.)
    • No new Controller interface method is needed — Cancel (already in the interface, line 37) is sufficient.

    Tests — internal/server/server_test.go: add TestDeleteRun following TestCancelRun (line 221-236)/TestRunsEndpoints (line 154-206) shape: seed a run with st.CreateRun, POST to /runs/:id/delete, assert 200 and that st.GetRun now returns ErrNotFound. Add a second case for deleting an in-flight run: seed a run with a non-terminal status, use fakeController (line 22-40) to assert Cancel was called with the right run ID before/alongside the delete. internal/store/store_test.go: add TestDeleteRun calling the store method directly, seeding an event and a session row for the same run_id and asserting all three are gone afterward, plus a not-found case.

    Frontend changes (internal/web/assets/app.js)

    Confirmation dialog — none exists in this codebase (confirmed: no <dialog>, no modal CSS, no window.confirm anywhere). Given the project's explicit "no build step, no dependencies" style (see file's own header comment, line 1-3), use the simplest option consistent with that: window.confirm("Delete this run permanently? This cannot be undone.") before calling the delete API. This avoids adding a whole modal component/CSS for a single use case. (Flagging as a judgment call — a custom <dialog>-based confirm would look nicer and is reusable, but is meaningfully more code for a one-off; recommend the native confirm unless the reviewer wants the nicer UX enough to justify building a modal component now.)

    Runs list page — runsTableBody (line 647-668) and the header row in renderRuns (line 634-641) currently have no actions column. Add an "Actions" header and, per row, a container with:

    • A "Stop" button, shown only when !IsTerminal-equivalent (mirror the terminal-status list already used for the status filter dropdown, line 605, or reuse the four in-flight status strings claimed/working/verifying/pushed), calling api.post('/runs/:id/cancel') — same call the dashboard's inFlightTable cancel button makes (line 552-561).
    • A "Delete" button (class: "btn btn-danger", matching cancelBtn's class at line 552) that runs the window.confirm above, then api.post('/runs/:id/delete'), then re-renders the table (call renderRuns(false) the same way applyBtn's handler does at line 624, or for the dashboard inFlightTable case call refreshStatus() as cancelBtn does at line 557).
      Follow the exact button-disable-during-request pattern already used at lines 553-561 (btn.disabled = true in a try/finally).

    Run detail page — renderRunDetail (line 672-766) currently has zero action buttons. Add a small button row near the top (after the <h1> at line 685) with the same Stop/Delete buttons, conditioned on run.Status for Stop, and navigating back to #/runs (window.location.hash = "#/runs") after a successful delete, since the detail page for a deleted run no longer has anything to show.

    Dashboard in-flight table (inFlightTable, line 538-575) — this table only ever shows in-flight runs, so every row is stoppable by definition; no change needed to add a "Stop" label distinct from today's "Cancel" — issue #23's "stop" and the existing "Cancel" are the same operation. Optionally add a Delete button next to the existing Cancel button here too for convenience, reusing the same stop-then-delete client-side sequence (call cancel, then delete) — but this is optional since the Runs list/detail pages are the primary places "delete a run" naturally lives; flagging as a nice-to-have, not required by the issue.

    CSS (internal/web/assets/app.css) — no new classes strictly required if reusing .btn/.btn-danger (lines 155-179) for the new buttons; add a minor .row-actions { display: flex; gap: 6px; } wrapper style if the actions cell needs multiple buttons side by side.

    Verification

    • go test ./internal/store/... ./internal/server/... — covers the new DeleteRun store method and the new /runs/:id/delete handler including the stop-then-delete path via fakeController.
    • Manual UI check: go run ./cmd/agent.go (or however the daemon's normal dev entrypoint is invoked) with server.ui: true, open the console, start/observe an in-flight run, click Delete on a running run and confirm it disappears from both the dashboard and the runs list, and that a GET /runs/:id afterward 404s. Also verify a terminal (already-finished) run can be deleted without hitting the cancel path, and that its transcript file is removed from the logs directory.
    • Confirm the confirmation dialog actually blocks deletion when "Cancel" is clicked in the native confirm() popup.

    Open questions for the reviewer

    1. Route verb: POST /runs/:id/delete (matches house convention) vs. a true DELETE /runs/:id — recommend the former unless told otherwise.
    2. Confirmation UX: native window.confirm() vs. a custom modal — recommend the former for minimal footprint given zero existing modal infrastructure.
    3. Should deleting a run also delete its sessions table row(s)? Recommend yes (a session row is meaningless without its parent run), and the plan above does this via DELETE FROM sessions WHERE run_id = ? inside the same transaction.
    4. Whether to add Delete to the dashboard's in-flight table too, or only to the Runs list/detail pages — recommend the latter as sufficient to satisfy the issue, with the former as optional polish.

    Reply with exactly implement to approve this plan and start the change. Reply with anything else and the plan will be revised to address it.

    coding-agent-loop run ad78deb9-d9ee-485e-b8ea-01a366f7ad6e, model claude-sonnet-5, cost $0.7201

  2. ableinc commented on Sep 6, 2026

    @ableinc
    OwnerAuthor

    implement

  3. ableinc commented on Sep 6, 2026

    @ableinc
    OwnerAuthor

    Opened a draft pull request for this issue: #25

    Tests failed (make test) — see the PR for output.

    Comment implement again if you want another attempt at this issue.

    coding-agent-loop run f858df05-637d-4f9b-8dbf-c9200f2c67aa

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

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions