Repository navigation
Conversation
… (tests only) Pin today's behavior of sbx-mgr's writers of the node-local SQLite `sandboxes.state` (and their cleanup-episode and `sandbox_quarantine_*` companion rows), so the planned `update_state_from` helper and its caller conversions can prove they are behavior-preserving. Nothing here touches the control-plane Postgres `public.sandboxes.lifecycle`. - `update_state` outcome matrix: every (from, to) pair of the 18 lifecycle states, for a plain row, a `delete_row` intent, an armed create episode, an armed cold-start episode and a requested episode. Each pair runs once with a transition receipt for the target and once with a receipt for another state. The matrix also covers a row the production quarantine writer quarantined and a missing row. Each case asserts the outcome and the whole-database diff. The expected outcomes are literal grids indexed by a wildcard-free match over `SandboxState`, not derived from `can_transition_to`. - Whole-database accepted-shape diffs for each specialized writer that still has a production caller, keyed by primary key and diffed column by column, so a later move onto a shared helper must stay column-identical. Seeds carry a failure reason where production can, a transition receipt, a runtime with its spaced endpoint proof, route identity, artifact pins and an active teardown-suspend job. - Direct tests, with the accepting case and each refusing predicate in its own case, for restore_checkpoint_state, update_state_superseding_activation_in_transaction, settle_startup_resident, restore_adopt_orphan_or_insert_sandbox, adopt_quarantine_cleanup_episode and pin_suspended_artifact_for_cleanup. - Stop's cleanup admission function from every source state, with and without a persisted intent: admitted from 16 states, refused from `deleting` and `quarantined`. `creating`, `created`, `stopped` and `suspended` are admission-only. The Stop RPC selects only a live runtime, and from every state it admits nothing without one. The new DB modules live under `db/tests/`. The Stop module is wired by one `#[cfg(test)] mod` line in `service/cleanup.rs` and builds its service with `SandboxManagerServiceOptions::for_test_in`. Known races in today's writers are documented in the pull request, not asserted. No production code changes.
…om (no callers)
Add db/state_write.rs for the node-local SQLite sandboxes.state column:
- StateSet, a bitset over the 18 lifecycle states.
- EDGES: 73 data-only edge constants (from-set, target, kind) built from
every production writer at this head, each naming its writer functions.
Kinds are Request, Completion, Compensation, Recovery and KnownRace (the
stale or unvalidated writes generic update_state callers can make).
SandboxState::can_transition_to stays the RPC-admission machine.
- update_state_from(conn, id, &'static Edge) -> Applied { from } |
Refused { found, refusal } | Missing. Call sites name a table edge;
KnownRace and insert edges are refused. It applies update_state's
quarantine and cleanup-ownership guards, now generic_state_write_refusal
in this module, then writes with the edge's from-set predicate in the
UPDATE itself. A missed write from a source state is an invariant error.
It runs in a caller's transaction or opens its own IMMEDIATE one in
autocommit, never a nested one. It has no production callers yet.
update_state calls the moved guard; its behavior is unchanged, and
state_transitions.rs shrinks below its archcheck cap, which is lowered.
Tests:
- With every state expected, the writer matches update_state on every
cell of the existing outcome matrix and its whole-database diffs,
standalone and inside a transaction, by running the matrix's own grids
and sweeps against both writers.
- A from-set without the row's state refuses with every row unchanged, for
every (from, to) pair, with accepting controls; every writable edge
writes only from its own sources; refused edge kinds change nothing.
- The invariant error and the autocommit atomicity of a deleting write and
its teardown-job failure are pinned with temporary triggers.
- Every disagreement between the edge table and can_transition_to (85
pairs) is in a checked-in allowlist with a reason, so a new one fails.
The sbx-mgr AGENTS.md writer rule now points new transitions at
update_state_from with an edge, or a specialized compare-and-set writer.
ac-pplx
force-pushed
the
ac/sbxmgr-sw-pr1b-refusal-status
branch
from
October 14, 2026 00:39
7e2e41c to
db2ca8b
Compare
Each update_state_from conversion wrote its own StateWriteOutcome-to-Status
helper, with different names, message text and policies. Add one shared
mapping and best-effort writer in service/cleanup_support.rs, with no
production callers; the conversions adopt them and delete their copies.
- require_state_write_from(result, context) -> Result<(), Status>. A
from-state refusal is Aborted "{context}: sandbox state write was
superseded: sandbox is {found}". A quarantine or cleanup-ownership refusal
and a missing row keep the exact Aborted status require_state_write
returns for update_state (blocked_by_quarantine,
superseded_by_cleanup_episode, missing). A database error is Internal
"{context}: {error}", as before.
- state_write_result(outcome) -> Result<SandboxState, StateWriteRejected> is
the typed form. It returns the overwritten state, or one of FromState
{ found }, Guard { refusal, found } and Missing, with into_status(context).
A caller that answers one case differently, such as warm start with
FailedPrecondition, matches it and passes the rest on. Guard carries
found for callers that decide by edge.from().contains(found).
- update_state_from_best_effort(db, id, edge) -> Option<StateWriteOutcome>
is update_state_best_effort for update_state_from, with a wildcard-free
match. It logs refusals and errors with that function's messages and
field names, but logs outcome with Debug where the legacy used Display.
Only an applied write marks the sandbox dirty. None means a poisoned
connection or a database error.
The legacy require_state_write is unchanged. Outside tests the new items
expect dead_code. Adopting require_state_write_from leaves several of these
expectations unfulfilled at once: the function, StateWriteRejected,
StateWriteGuard, state_write_result and the impl. unfulfilled_lint_expectations
is warn-by-default and an error under CI's clippy -D warnings, so the
adopting conversion must drop those attributes.
Tests:
- Wildcard-free matches cover every StateWriteOutcome and StateWriteRefusal
and assert each one's typed rejection and exact code and message. The
refusal list is built from such a match.
- The same row gets the same status from the new mapping and from the
update_state caller path, and every row stays unchanged, for: a
quarantined row, a stopping row revived to started, a cleanup row with a
delete_row intent, and a missing row. The row's own state is expected, and
the messages match literals copied from 8f12bd6.
- The best-effort writer applies a write and marks only that sandbox dirty.
A refusal and a database error return their result and change nothing. A
poisoned connection is None, changes nothing, and wakes the dirty-outbox
worker, which exits on the poisoned connection and fails its critical-task
guard. The test pauses the clock so the worker's poll timer cannot fire.
The sbx-mgr AGENTS.md writer rule names both helpers and the override.
ac-pplx
force-pushed
the
ac/sbxmgr-sw-pr1b-refusal-status
branch
from
October 14, 2026 00:51
db2ca8b to
f1d1c9b
Compare
This was referenced Oct 14, 2026
ac-pplx
force-pushed
the
ac/sbxmgr-state-write-pr1-helper
branch
from
October 14, 2026 03:11
2fc6457 to
8646f74
Compare
Base automatically changed from
ac/sbxmgr-state-write-pr1-helper
to
ac/sbxmgr-state-write-pr0-tests
October 14, 2026 03:30
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.
Stacked on #388. Each
update_state_fromconversion stacked on #388 wrote its ownStateWriteOutcome→Statushelper, with different names, message text and policies. This adds one shared mapping and one best-effort writer inservice/cleanup_support.rs, with no production callers. The conversions rebase onto this PR and delete their copies. The legacyrequire_state_writeforupdate_statestays unchanged.API
require_state_write_from(result, context) -> Result<(), Status>is the primary mapping.state_write_result(outcome) -> Result<SandboxState, StateWriteRejected>is the typed form. It returns the overwritten state, or one ofFromState { found },Guard { refusal: StateWriteGuard, found }andMissing.into_status(context)turns a rejection into a status.FailedPrecondition) matches that case and passes the rest tointo_status.Guardcarriesfoundfor callers that decide byedge.from().contains(found).update_state_from_best_effort(db, sandbox_id, edge) -> Option<StateWriteOutcome>isupdate_state_best_effortforupdate_state_from, with a wildcard-free match.outcomeis logged withDebug(?outcome), where the legacy function usesDisplay.Appliedmarks the sandbox dirty.Nonemeans a poisoned connection (failure signalled) or a database error.Messages
Applied { from }Ok(())(state_write_result:Ok(from))Refused { refusal: FromState, found }Aborted{context}: sandbox state write was superseded: sandbox is {found}Refused { refusal: Quarantined, .. }Aborted{context}: sandbox state write was not applied: blocked_by_quarantineRefused { refusal: CleanupOwned, .. }Aborted{context}: sandbox state write was not applied: superseded_by_cleanup_episodeMissingAborted{context}: sandbox state write was not applied: missingInternal{context}: {error}The guard, missing and database-error rows are byte-for-byte what
require_state_writereturns forupdate_state, at 8f12bd6 and now.Dead-code expectations
Outside tests the new items are
#[cfg_attr(not(test), expect(dead_code, ...))]. Adoptingrequire_state_write_fromleaves several expectations unfulfilled at once: the function,StateWriteRejected,StateWriteGuard,state_write_resultand theimpl.unfulfilled_lint_expectationsis warn-by-default and an error only under CI's clippy-D warnings, so the adopting conversion must drop those attributes.update_state_from_best_effortkeeps its own expectation until a caller uses it.Deferred:
StateWriteGuardduplicatesStateWriteRefusal::{Quarantined, CleanupOwned}, and the legacySandboxStateWriteOutcomeis a third copy. The fix nests them:generic_state_write_refusal -> Option<StateWriteGuard>andStateWriteRefusal::Guard(StateWriteGuard). PR 6, which retires the legacy enum, does that, so this PR doesn't churn #388's types under the rebasing siblings.Tests (
service/cleanup_support/tests.rs)StateWriteOutcome×StateWriteRefusal× state assert the typed rejection and the exact code and message. The refusal list itself comes from a wildcard-free successor match, so a new variant does not compile until it is swept.update_statecaller path return the same status, and every row stays unchanged. The rows are a quarantined row, astoppingrow revived tostarted, acleanup_requiredrow with adelete_rowintent, and a missing row. The row's own state is in the from-set, so the guard is what refuses. The messages are also checked against literals copied from 8f12bd6.stopped), a cleanup-owned row and a missing row;Noneand changes nothing on a database error.Noneand changes nothing. It also wakes the dirty-outbox worker, which exits on the poisoned connection and fails its critical-task guard. The test pauses the clock and only yields, so the worker's poll timer cannot fire and only the signal can wake it.Validation
cargo fmt --all -- --checkcargo clippy --workspace --locked --all-targets -- -D warningscargo archSPACE_CI_REQUIRE_TEST_DEPS=1 cargo nextest run -p pplx-space-sbx-mgr×3#[ignore]root-onlydistinct_service_uids_can_exchange_downloads; 0 dependency skipsfounddropped fromGuard, and each best-effort dirty, return and poison-signal arm)🤖 Generated with Claude Code