Skip to content

pr: durable session lifecycle deferred from #218's Stage A v5 plan #299

Description

@cameronsjo

Follow-up from #218. That issue's approved Stage A v5 plan specified a full PR-session lifecycle subsystem; the implementation landed only the plan's identity layer — the typed session key, the canonical codec, and the derived window-name codec — because that is what actually fixes #218's collision. Everything below is the deferred remainder, filed so the design work stays findable rather than evaporating with the lane that scoped down.

Plan of record: plans/forgectl-campaign-019ff8b3-wave5/218-stage-a-v5.md in the approving session's cadence plan directory (session 019ff8b3-b8e7-7cd2-8570-224e1fbabc8b), with the review verdict alongside it in 218-plan-review-v5.md.

Nothing here is a defect. forgectl pr works today; this is the durability and recoverability program the plan designed around the identity layer, and it should be scoped, sequenced, and re-reviewed on its own terms rather than inherited wholesale.

Deferred items

  1. Transaction lifecycle. The durable phase machine — intentpreparing:<step>preparedcreatingwindow-createdbindingactive, plus rolling-back, rolled-back, orphan, and needs-repair — with each phase fsynced before the mutation it authorizes.
  2. Kernel advisory lock. A single .pr-session-lifecycle.lock held from first durable intent through preparation, creation, active bind, or completed rollback, so a crash or a concurrent invocation cannot interleave.
  3. Strict monotonic record writer. One JSON codec for every record kind: duplicate-key and trailing-content rejection, revision compare-and-write under lock, private same-directory temp with exclusive no-follow open, checked full writes, file sync, destination revalidation, atomic rename, directory sync, and verified temp cleanup — with crash tests at each of those boundaries.
  4. Quarantine move-graph persistence. A read-only strict discovery API returning the complete deterministic From/To graph with identity preconditions, persisted as a monotonic revision before the first rename, then applied one entry at a time through a single-move API with an intent and a receipt around each. Recovery consumes the persisted graph and never rediscovers.
  5. Dry-run bootstrap contract. A first-run dry-run may create only the private state directory, the permanent lock, and the version header, under exclusive lock, converging across concurrent first runs — and is read-only after that, with the exception disclosed in the CLI and docs.
  6. forgectl pr repair CLI. Inspect-only without --apply; accepts a transaction UUID or a confined breadcrumb plus an explicit --adopt-window @N, --rollback, or --forget-if-absent, and never accepts a name as authority.
  7. v2 breadcrumb schema with legacy migration. A discriminator-first reader, no fallback from malformed v2 to legacy, old binaries failing closed, and a migration that validates the parent, generation, native id, and name twice before atomic replacement.
  8. PRSessionInstance persistence. Binding the logical key to a persisted tmux.SessionIdentity parent, a validated tmux.WindowIdentity child, and a transaction UUID — replacing today's resolve-by-name-on-use with a persisted, revalidated native identity.
  9. Eight-attempt shell-nonce machinery for pr open. A bounded shell-<nonce> name preflighted against live names, retried at most maxShellNameAttempts = 8, refusing before new-window after eight collisions, with an injectable nonce source for the first-collision-then-success and eight-collision-refusal cases.

Two decisions already made, worth carrying forward

The local key holds a short oid, not the full HEAD oid. The plan specified the full oid. The breadcrumb persists only the ref string and a locality flag, and Ref — which carries a 7-char abbreviation in Repo — is also a map key in WindowsLive, so a partially-populated new field would break lookups silently. The codec was built with a declared algorithm tag (short / sha1 / sha256) so widening later needs no format break; item 7 above is the natural place to do it. Marked in internal/pr/sessionkey.go with the upgrade trigger named.

The same-name collision refusal was removed, contingently. #237 made every tmux action target a native id, so a same-name window can no longer receive an action meant for another — the hazard was eliminated upstream. internal/pr/launch.go records that the removal is contingent on #237's targeting and must return if that is ever reverted. Item 1 above should not silently reintroduce it without re-deriving whether it is still needed.

Sizing

This is a multi-PR program, not one change. Items 1–3 are the substrate everything else sits on and should land together or in a tight sequence; 4 depends on 3; 6, 7, and 8 depend on 1–3; 5 and 9 are independent and small.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:forgectlComponent: forgectl binarykind:epicParent of independently deliverable children; carries no grades and no exec:*kind:featureNew or improved capability

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions