Skip to content

fix(runner): refuse unmetered sessions, guard reload during save (DEV-3147) - #394

Merged
demtario merged 2 commits into
masterfrom
fix/DEV-3147-session-gate-save-guard
Sep 30, 2026
Merged

demtario merged 2 commits into
masterfrom
fix/DEV-3147-session-gate-save-guard

Conversation

@demtario

@demtario demtario commented Sep 30, 2026 •

Copy link
Copy Markdown
Member

Context

Two of the three items in DEV-3147. The third (return from PATCH /api/demos/:id before the build finishes) is deliberately not in this PR: it needs an editor-side polling protocol and should be decided after profiling the snapshot.build duration_ms point.

Unmetered resurrected containers. The /api/session/:id/* gate only refused tombstoned sessions, and a tombstone lives 600 s. After that a stale /status call booted a container that no meter books, so the cost guardrails could not see it. The gate now also refuses an id with no meter (written at create, deleted at teardown): 410 for everything, 204 for a file delete. The check runs after the budget guard, so an unknown id at anon_blocked/new_blocked keeps its 401/503. The decision is a pure function (sessionGateVerdict) so it is unit-testable. startSessionMeter retries its KV write once, because a lost write would now make a live session look dead.

Reload during a save loses the edit. The edit page arms a beforeunload guard while edits are unsaved or a save is in flight. Edits typed during the 8-14 s rebuild are no longer marked clean when the PATCH resolves, since they are not in the request.

Known trade-off: the gate reads a KV key another location may not have replicated yet. The meter is written before the create returns, so a client's first poll normally hits the same location; a stale null would show as "The session was closed."

Types of changes

  • New example
  • Update to an existing example
  • README / documentation change
  • Demo runner (runner/) change
  • CI / tooling change

How was this verified?

  • New pipeline/session-unmetered-gate.test.mjs drives the real worker router (status and file write refused, file delete 204, tombstone wins over a meter, KV failure fails open, budget 401/503 preserved). Reverting the gate to master makes the three refusal tests fail.
  • New e2e edit page guards a reload while edits are unsaved or a save is in flight (clean, unsaved, save in flight, typed mid-save, saved). It fails with the guard removed.
  • pipeline/*.test.mjs 2561/2561, pnpm typecheck clean, full Playwright suite 247 passed (182 live-gated skips) against a build served on its own port.
  • Ran a /code-review pass and fixed its findings.

Checklist

  • New/renamed example: added to runner/config/frameworks.json (see CONTRIBUTING.md); otherwise it won't appear on demos.handsontable.com
  • New example: added a row to the tables in README.md
  • Ran pnpm build (and pnpm dev) in the affected example/server-example locally

Not applicable: no example added or renamed. The runner app and worker were built, type-checked and tested instead.

Related issue(s):

  1. DEV-3147

Note

Medium Risk
Changes central session routing and metering gates that can block or allow container boots; edit-page save/dirty logic affects data-loss UX but is localized to authoring.

Overview
DEV-3147 tightens two places where edits or spend could slip through: the edit page and session subroutes on the API worker.

On /edit/:id, a beforeunload handler runs while the workspace is dirty or a save is in flight (8–14 s rebuild), so reload/close can trigger the browser’s leave confirmation. onSave only calls clearDirty() when filesRef.current still matches the snapshot sent in the PATCH, so keystrokes during an in-flight save keep Save • and stay protected.

On the API, the /api/session/:id/* resurrection gate now refuses IDs with no session meter (not only tombstones), via testable sessionGateVerdict: 410 for most calls, 204 for file delete. Order stays tombstone → budget guard → meter check. startSessionMeter retries its KV write once so a lost meter write does not brick a live session. Pipeline and e2e tests cover the gate and the edit-page guard.

Reviewed by Cursor Bugbot for commit 573068f. Bugbot is set up for automated code reviews on this repo. Configure here.

demtario and others added 2 commits September 30, 2026 12:09
…-3147)

A /api/session/:id/* call that arrived after the session's 600 s tombstone
expired booted a container the cost guardrails never metered. The gate now
also refuses an id with no meter (written at create, deleted at teardown):
410 for everything, 204 for a file delete. The decision lives in
session-lifecycle.ts so it is testable.

The edit page now arms a beforeunload guard while edits are unsaved or a
save is in flight, so a reload during the 8-14 s rebuild no longer drops
the edit silently.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…147)

Run the meter check after the budget guard so an unknown id at
anon_blocked/new_blocked keeps its 401/503, retry the meter write once
because the gate now depends on it, keep edits typed during a save dirty
(and the reload guard armed), and refresh the stale meter comments.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@demtario demtario self-assigned this Sep 30, 2026
@demtario
demtario merged commit 0a82aec into master Sep 30, 2026
10 checks passed
@demtario
demtario deleted the fix/DEV-3147-session-gate-save-guard branch September 30, 2026 10:28
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.

1 participant