Skip to content

fix(rungate): resume arm always binds a run epoch; unknown --run-epoch gets a named refusal (change 0463) - #345

Merged
danielhanold merged 17 commits into
mainfrom
fix/resume-gate-armed-line-is-ambiguous-when-no-epoch-exists-dis
Sep 28, 2026
Merged

danielhanold merged 17 commits into
mainfrom
fix/resume-gate-armed-line-is-ambiguous-when-no-epoch-exists-dis

Conversation

@danielhanold

@danielhanold danielhanold commented Sep 27, 2026 •

Copy link
Copy Markdown
Owner

↩ Change 0463 — Resume gate-armed line is ambiguous when no epoch exists — dispatch context gets passed as --run-epoch

Summary

Resuming a change whose earlier run was never armed printed gate-armed <key> <dispatch-context>, a two-token line. Parents read the dispatch context as the epoch, the gate refused with a generic invalid-request, and the resumed run finished with no epoch linkage (change 0382). This PR:

  • mints and binds a run epoch on an epochless run gate-before --resume, so the armed line is always gate-armed <key> <epoch> <dispatch-context>
  • makes an unknown --run-epoch refuse with unknown-run-epoch on gate drive start, gate drive prepare-scope (existence check only) and agent enter (checked before launch, including a lone --run-epoch)
  • lets run cancel accept the resume-verified owner (review blocker; without it the new epoch could never be cancelled and every later resume was stuck). Needs design sign-off against ADR-0111.
  • makes an epochless resume refuse resume-active-run when a live epoch already owns the worktree

Results: docs/results/2026-09-27-resume-gate-armed-line-is-ambiguous-when-no-epoch-exists-dis-results.md

Review

Rung: docket-review-deep. Task 1 routed premium, and the diff is over 1500 lines.

# Severity Finding Disposition
1 blocker A resume-minted epoch can't be cancelled (claim-unconfirmed), so later resumes wedge fixed, 100de73
2 important The cancel-cycle test forced the cancelled state instead of calling the real run cancel fixed, 100de73
3 important agent enter skipped the preflight when --run-epoch was passed alone fixed, 1aa636b
4 important Concurrent epochless resumes could leave two live owners of one worktree, blocking fenced mutations fixed, 06040db (a small residual race is noted in results)
5 minor Edited comments were not re-wrapped fixed, 6c1ccd2 (also swaps two bare t.TempDir() calls for the repoguard fixture)

Follow-up reported for human triage: a check-after-bind re-scan to close the remaining concurrent-resume race window.

command: go run ./cmd/docket development test
result: green
head_sha: ad25610
ran_at: 2026-09-28T12:32:23Z

@danielhanold
danielhanold force-pushed the fix/resume-gate-armed-line-is-ambiguous-when-no-epoch-exists-dis branch from e7f9063 to 7007a61 Compare September 28, 2026 06:01
Docket-Plan-Path: docs/superpowers/plans/2026-09-27-resume-gate-armed-line-is-ambiguous-when-no-epoch-exists-dis.md
…cle test drives the real cancel path (change 0463, review fix)

BLOCKER: the epoch an epochless `gate-before --resume` mints binds the change
and stays active, but runCancel required a confirmed claim binding, which a
resume arm never gets (change.claim requires proposed). The next resume
refused resume-active-run with a `run cancel` remedy that always refused
claim-unconfirmed: a permanent wedge. runCancel now accepts the
resume-verified shape resolveGateOwnership already exempts (AttributedID set,
no claim-binding file at all), still refusing claim-mismatch against the
epoch's change and claim-unconfirmed when an unconfirmed reservation exists.
This also makes resume replacement epochs cancellable.

IMPORTANT: TestEpochlessResumeEpochJoinsCancelCycle no longer forces the
epoch cancelled with a direct epochCAS; it cancels through runCancel with the
arm's key and epoch, asserts `cancelled`, arms the replacement, and cancels
that too. TestRunCancelResumeAuthorityFailsClosed pins the fail-closed edges.
…efore launch (change 0463, review fix)

Review finding: the agent.enter epoch preflight ran only when both
--run-gate-key and --run-epoch were given, but AGENTS.md threads only
--run-epoch into agent.enter. A lone --run-epoch - including the 0382
misroute where the dispatch context is passed as the epoch - skipped
CheckRunEpochLinkage and launched unlinked with no warning. A lone
--run-epoch now runs the same resolvability check prepare-scope uses
(runEpochLocator / findEpochDirByID) and refuses unknown-run-epoch before
anything is spawned; an existing lone epoch still proceeds.
…inting (change 0463, review fix)

Review finding: RunGateBefore step (6a) resume mint binds ChangeID+Worktree without
checking whether another live epoch already owns the verified worktree. Two live
owners make findEpochByWorktree return ErrEpochOwnerAmbiguous, so admitWorkflowMutation
refuses every fenced mutation (PR publish, workspace publish) in that worktree.

Step (4b) now resolves the worktree's current owner with findEpochByWorktree before
any scope or record is minted: an active/completing owner naming no or another change
refuses resume-active-run with its locator; an ambiguous/unreadable owner set refuses
resume-epoch-unreadable; a fenced owner does not block. The decision-4 comment and
TestConcurrentEpochlessResumeEpochsFailSafe now state the residual race window and the
run.cancel recovery.
…re reserving (change 0463, post-review)

A claim under a resume-attributed gate context left an unconfirmed reservation
(the resumed change is not proposed) or a confirmed binding for another change;
either made run.cancel refuse the resume epoch (claim-unconfirmed or
claim-mismatch), bringing back the wedge the resume-verified cancel authority
removed. ChangeClaim now refuses gate-context-conflict before ReserveGateClaim.
The resume-verified shape is one predicate, GateRecord.resumeAttributed, shared
by ChangeClaim, runCancel, and resolveGateOwnership.
…-arm remedy (change 0463, post-review)

Concurrent epochless resume arms of one change each passed the no-prior-epoch
check and each minted and bound a live epoch (12 of 12 concurrent arms armed in
test). A per-change resume lock, taken once the resume id is verified and held
to the end of the arm, serializes check and bind; losing arms refuse
resume-active-run. It lives under <git-common-dir>/docket/rungate-resume so the
gate-key scanners never see it.

A resume arm binds its epoch when armed, so an undispatched earlier arm refuses
the next resume. Nothing records whether an agent uses an epoch, so the refusal
now names both remedies explicitly (cancel if never dispatched or exited,
gate-verdict if still running); edge-paths.md says the same.
…pus (change 0463, post-rebase repair)

The rebase onto change 0465 landed 0463's tests as the pre-0465 layout left
them: in the DEFAULT internal/app corpus, which 0465's no-real-git guard
(internal/app/nogit_guard_test.go) forbids from starting real git, and under
names with no TestIntegration prefix inside tagged files, which
tests/test_go_integration_contract.sh checks (2)/(4) reject.

Applies 0465's own partition treatment to each of 0463's additions:

- TestCheckRunEpochLinkage (real git via newGateRepo, helper isEpochKind is
  integration-only) moves from rungate_epoch_refusal_test.go into
  rungate_epoch_integration_test.go as
  TestIntegrationGateEpochCheckRunEpochLinkage. The two pure classifier tests
  stay in the default corpus.
- rungate_epochless_resume_e2e_test.go (real git via newWorkingRepo, helper
  resumeInspectService is integration-only) becomes
  rungate_epochless_resume_e2e_integration_test.go behind //go:build
  integration, its test renamed TestIntegrationGateArmEpochlessResumeEndToEnd0382.
- The two gate-context claim tests move from change_claim_test.go into
  change_claim_integration_test.go under the TestIntegrationRecordOps prefix.
- The ten tests 0463 added to the rungate_before* integration files gain the
  shard prefix their runner selects: TestIntegrationGateArm… for the resume-arm
  family, TestIntegrationGateCancel… for the runCancel authority test, and
  TestRaceIntegrationAppConcurrency… for the goroutine race over concurrent
  epochless resume arms.

No assertion is weakened and no test is dropped; every moved body is byte-identical
apart from its name. The results file's pointer follows the renamed test and file.
@danielhanold
danielhanold force-pushed the fix/resume-gate-armed-line-is-ambiguous-when-no-epoch-exists-dis branch from 7007a61 to ad25610 Compare September 28, 2026 12:09
@danielhanold

Copy link
Copy Markdown
Owner Author

Finalize rebased PR #345 onto main baa0fb5 (change 0465). One conflict in internal/app/change_claim_test.go was resolved by docket-rebase-resolver. The rebased suite went red: change 0465 had moved the real-git internal/app tests behind //go:build integration with TestIntegration*/TestRaceIntegration* prefixes, so 0463s new tests did not compile (undefined isEpochKind, resumeInspectService) and broke the integration contract. docket-integration-repair added commit ad25610, which only moves and renames tests (8 files, +127/-127, test bodies unchanged). One judgment call: rungate_epoch_refusal_test.go was split, and its two pure tests stay in the default corpus. It also retargeted one test reference in the results doc. Suite at ad25610: SUITE files=66 passed=66 failed=0. The repaired head is published to the PR.

@danielhanold
danielhanold merged commit 7e0edd1 into main Sep 28, 2026
2 of 4 checks passed
@danielhanold
danielhanold deleted the fix/resume-gate-armed-line-is-ambiguous-when-no-epoch-exists-dis branch September 28, 2026 12:33
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