Repository navigation
fix(openshell): resend an unapplied git detach under its request id - #3272
Conversation
Expiry never converged after a detach intent that never took effect: a crash between beginDetach and the send, an UNAVAILABLE or DEADLINE send, or an aborted pass left the provider attached with a recorded request id and no epoch, and every later pass answered pending. The intent now also returns the Sandbox version it is conditional on. While the provider stays attached at that version, the next pass repeats the same request (same id, same precondition) after the usual pre-send checks. OpenShell admits a request id at most once: it replays a completed detach, refuses one whose outcome it cannot tell (FAILED_PRECONDITION, which retires the runtime) and runs one it never saw under the same version precondition. A Sandbox version that moved since the intent retires the runtime without a send. Finding 2219.
Review follow-ups: a repeated send the gateway holds without a result (FAILED_PRECONDITION) may have applied since; read the Sandbox again and observe an absent provider instead of retiring. The fake gateway now enforces the version precondition (ABORTED, claim left unresolved) and the payload fingerprint, and collects request mismatches instead of asserting inside the call, where a failure read as a transport error. New cases: a first send still applying, and a Sandbox that moves just before the send.
|
🦞👀 Pull request received. I will update this pull request when review starts. ClawSweeper review completeClawSweeper finished reviewing this revision. The review result is being finalized. |
|
Codex review: needs real behavior proof before merge. What this changesThis PR retries an unapplied OpenShell Git detach using its recorded request ID and Sandbox resource version. Example: A detach records Sandbox version 13, but its first RPC never reaches the gateway.
Review scores
ProductKind: Bug fix · Worth it: Yes · Fix scope: Complete Merge readiness⛔ Blocked before merge - 1 item remains Keep this PR open: it addresses a current-main recovery loop, and inspection found no introduced correctness defect. Priority: P2 Before merge
FindingsNone. Tests
Agent review detailsHow this fits togetherThe controller’s repository-expiry workflow reads durable OCC delivery state, invokes the OpenShell gateway, and returns withdrawn, pending, or retire-runtime after checking the original Sandbox and retained model execution. flowchart TD
A[Repository expiry worker] --> B[OCC delivery state]
B --> C[Git expiry reconciler]
C --> D[OpenShell gateway client]
D --> E[Gateway authorization and replay]
E --> F[Sandbox provider attachment]
F --> C
Technical reviewBest possible solution: Retain the focused retry in the existing expiry owner, preserving exact request identity, conditional mutation, and observed removal. Do we have a high-confidence way to reproduce the issue? Current-main source establishes the loop: an attached provider plus a recorded request without an epoch returns pending on every pass; no target code was executed during this review. Is this the best way to solve the issue? The repair uses the existing lifecycle and storage owners, preserves the gateway’s request fingerprint, and retires rather than resending when the recorded precondition no longer holds. AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning medium; reviewed against 38ecfca18762. Provenance checked
TestingProof path: in-process harness. SecurityNone. EvidenceWhat I checked:
LabelsLabel changes: No label changes. Label justifications:
Rank-up movesOptional improvements that raise the rating; they are not merge blockers.
Rating scale6/6 🦀 challenger crab · 5/6 🦞 diamond lobster · 4/6 🐚 platinum hermit · 3/6 🦐 gold shrimp · 2/6 🦪 silver shellfish · 1/6 🧂 unranked krab. Overall follows the weaker of proof and patch quality; ✨ marks media proof (a screenshot, video, or linked artifact) that directly shows the changed behavior. WorkflowClawSweeper edits this one comment on every review. Comment HistoryReview history (1 earlier review cycle)
Reviewed October 23, 2026, 12:28 AM ET / 04:28 UTC (Revision 2). |
|
@clawsweeper re-review |
|
🦞👀 Re-review progress:
|
Problem
Git expiry never converged after a detach intent that never took effect.
subtractExpiredOpenShellGitreturnedpendingwhenever the Git provider was still attached and a request ID was recorded without a confirmed epoch. Every later pass did the same, so the worker recheckedREPOSITORY_WITHDRAWAL_UNCONFIRMEDforever. Three ways to get there:beginDetachand the send;The "intent without effect" conformance case pinned that loop.
Decision (provisional; a maintainer can overrule it)
Resend the same request. When the provider is still attached at the recorded Sandbox version, the next pass runs the usual pre-send checks and then repeats the identical
DetachSandboxProviderrequest: same request ID, sameexpectedResourceVersion. If the Sandbox version moved since the intent, the runtime is retired without a send.Why this is safe, from OpenShell 0.1.5 at the pinned revision (
bdb956f2):DetachSandboxProviderruns under durable request-ID mutation replay (crates/openshell-server/src/grpc/mutation_replay.rs;proto/openshell.proto;docs/sdk/api-errors.mdx).FAILED_PRECONDITION(REQUEST_OUTCOME_UNCERTAIN), which retires the runtime.FAILED_PRECONDITION, which also retires the runtime.The alternative was to retire the runtime after a bounded number of passes. It would also converge, but it would end the conversation in the common case: a send that simply never reached the gateway.
Change
CredentialDeliveryReductions.desire()also returns the recordedexpected_version. The column is already written bybegin(), and migration 0062 requires it whenever there is a request ID. The route store'sexpire()returns it asdetach.resourceVersion.subtractExpiredOpenShellGit, while the provider is still attached:attachment-after-send-intent).beforeevidence and double inspection as a first send, then sends the recorded request again (repeated-conditional-detach). A first send still records a new intent first.FAILED_PRECONDITION(it holds the earlier request without a result, which may still be applying) reads the Sandbox once more. If the provider is now absent, the existing OBSERVE path settles it; otherwise the runtime retires.Verification
tests/conformance/openshell-git-runtime.test.mjs, "Git expiry observes the original model...":FAILED_PRECONDITION, and runs an unseen request under its version precondition. On a mismatch, the claim stays unresolved and the gateway answersABORTED.pending, thenwithdrawn. Two sends, one admitted.pending, thenretire-runtime.pending, thenwithdrawn. The repeat is held as uncertain while the first send lands, and the provider is then observed absent.pending, thenretire-runtime, with no second send.retire-runtime(ABORTED).ABORTEDtaken as pending, and an uncertain repeat retiring without a fresh read.tests/integration/postgres-openshell-git-routes.test.ts:expire()returns the recordedresourceVersionbeside the request ID.postgres-openshell-git-routes(8/8),postgres-openshell-standing-git-transfer(its lost-reply journey still sends one detach) andpostgres-openshell-standing-route: all passed (standing-git-transfer1/1 on the final head;standing-route6/6 on the first commit, before the uncertain-repeat change).tsc --build(repo + tests), ESLint and Prettier on the touched files: clean.tests/conformance/openshell-git-runtime.test.mjspassed 20/20 runs at 2 CPUs each, on the final head.Risks
PERMISSION_DENIED) before any mutation, and the runtime retires. The mutation itself can only remove Git access.pendinguntil slots free up. The first send had the same exposure; this change adds no new bound for that case.