Skip to content

Crank savepoint ordering is an unexpressed invariant, not a nameable one #1014

Description

@sirtimid

Problem

#1012 gave each crank two savepoints, crank (outer) and delivery (inner), and the correctness of the whole scheme rests on an invariant the types can't express:

crank is created first, delivery second, and only delivery is ever rolled back.

createCrankSavepoint(name: string) and rollbackCrank(savepoint: string) in packages/ocap-kernel/src/store/methods/crank.ts both take bare string. Today the invariant is held by a comment in KernelQueue.#runLoop and one test assertion.

Why a string literal union is the wrong fix

There are two typo classes, and a 'crank' | 'delivery' union only catches the harmless one:

  • Misspelling ('deivery') — rollbackCrank finds no match and hits Fail`no such savepoint as ...` . Loud. Kills the run loop, corrupts nothing.
  • Swapped creation order (delivery created first) — delivery becomes ordinal 0 → t0 → rolling it back rolls back the outermost savepoint, _spStack empties, rollbackIfNeeded() fires, and the transaction is discarded. Silent, and precisely the hazard the two-savepoint split exists to prevent.

A union type accepts the swapped order happily. The load-bearing invariant is ordinal, not nominal.

KernelStore is also public API (exported from packages/ocap-kernel/src/index.ts), so narrowing the parameter is a breaking change that pushes a KernelQueue policy into a store method that legitimately supports arbitrary nesting depth.

Proposal

Remove the parameter instead. Have startCrank() create both savepoints itself and expose rollbackDelivery():

Fallback if that's too invasive: two module-level constants in KernelQueue.ts. Same narrow protection as a union, without the breaking API change.

Related, same file

ctx.savepoints: string[] lives on StoreContext, shared by every method module, with the index↔tN correspondence held by convention across three functions. Only crank.ts touches it (plus the initializer in store/index.ts), so it could move into the getCrankMethods closure and off StoreContext entirely. The truncation idioms ctx.savepoints.length = ordinal and = 0 do meaning-bearing work unnamed — and getting one of them wrong is exactly the bug #1012 fixed.

Only cost: crank.test.ts pokes context.savepoints directly and would need to drive through the public methods, which is arguably an improvement.

Surfaced by review agents on #1012.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions