From 076b333ec79dfc3f38ab8f37cf2369fb839a924b Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Thu, 3 Sep 2026 02:57:54 -0700 Subject: [PATCH] docs(plans): the sweep completes a stale activating row that already holds a code (Vera) Unconditional demotion of a stale activating row could strand a live, redeemable connect code under an error parent (crash between the mint and the final parent write). The sweep now looks before it demotes: an activating row whose Integration is already active is completed to active with write 3, fenced on the claimId it read; only an inactive Integration row is demoted. The sweep never mints and never unprojects. Test 6 pins both branches. Co-Authored-By: Claude Fable 5.1 --- docs/plans/connector-as-installable-app.md | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/docs/plans/connector-as-installable-app.md b/docs/plans/connector-as-installable-app.md index e1f61bf2b..5133bc838 100644 --- a/docs/plans/connector-as-installable-app.md +++ b/docs/plans/connector-as-installable-app.md @@ -293,12 +293,18 @@ the slash-command / external-webhook tracks; naming them here keeps the enum hon `active` installation, every component's `projectionIds` must resolve to a live row; a missing row marks the component `stale` (never re-creates silently — a stale connector must not mint a code nobody asked for). For every `uninstalled` installation, projections must be inactive. -For every **`installing` or `activating`** installation whose `claimedAt` is older than -`INSTALL_LOCK_TTL_MS`, +For every **`installing`** installation whose `claimedAt` is older than `INSTALL_LOCK_TTL_MS`, the sweep sets `status: 'error'` with `errorMessage: 'install lock expired'` — fenced on the `claimId` it read, so it cannot race a takeover that happened between its read and its write -— and the row becomes -the ordinary retryable case, and the board-facing state stops lying about work in progress. +— and the row becomes the ordinary retryable case, and the board-facing state stops lying +about work in progress. For a stale **`activating`** installation the sweep must **look +before it demotes**, because write 2 may already have run (Vera, 2026-09-03, on the merged +text: an unconditional demotion strands a live, redeemable code under an `error` parent): if +the projected Integration row is `isActive: true` with a code, the sweep **finishes** the +install — write 3, `activating → active`, fenced on the `claimId` it read — so the code the +user was handed is the code the row reports; only if the Integration row is still inactive +does it demote to `error` as above. The sweep never mints and never unprojects; it only ever +completes or demotes, and both writes are fenced. The sweep is the backstop; the claim filter above is the primary path, so a stuck lock is recoverable by the next install attempt even between sweeps. Log a count line, the H3 pattern: the exit condition is the number reaching zero. @@ -396,7 +402,10 @@ Unit (`backend/__tests__/unit/services/installable/`): 5. Non-member of the chosen pod → 403, nothing written. 6. Reconciler: a deleted Integration under an active installation marks the component `stale`, creates nothing. An `installing` row with `claimedAt` older than the TTL is swept to - `error` with `'install lock expired'`. + `error` with `'install lock expired'`. A stale `activating` row whose Integration is still + inactive is swept to `error`; a stale `activating` row whose Integration is already active + with a code is **completed to `active`**, the code unchanged, `mintConnectCode` not called + — never demoted, so no redeemable code ever sits under an `error` parent. 6b. **Lock takeover.** An `installing` row with a stale `claimedAt` (owner died): the next install claims it (same `_id`, new `claimId`, new `claimedAt`, new `installedBy`), runs projectors, reuses the inactive Integration row, activates and mints exactly once. A fresh