| β¦ | β¦ | @@ -422,8 +505,12 @@ export class Launcher { | |
| 508 | + | ownershipProtocol: 1, | |
| 425 | 509 | childVersion: version, | |
| 426 | 510 | ...(update === undefined ? {} : { update }), | |
| 511 | + | ...(role === "trial" && this.#trialOwnership !== undefined | |
π High src/serviceLauncher.ts:511
A rollback-capable trial can start an older runtime that ignores ownership, allowing it to claim the owner record without validating previousOwnerId; if another server writes during the handoff and the trial exits, #returnToPrevious restores the stale snapshot over those writes. Before starting a trial, require the target runtime to support and acknowledge the ownership protocol.
No longer relevant as of b05c4c47e1f138963a5d75634d81d2f896d3dbd1
Note
Written by gpt-6.1-sol on behalf of Maria
fixed in b60c8024f0. before accepting an update, the launcher runs the target's persistence-free __service-preflight and requires its exact version plus ownershipProtocol: 1. recovered pending trials repeat that check before snapshotting. the regression rejects a target missing the acknowledgement without creating a backup or changing the database.
| β¦ | β¦ | @@ -312,9 +344,45 @@ export class Launcher { | |
| 371 | + | // stale snapshot over the intervening owner's accepted writes. | |
| 372 | + | const next: ServiceState = { | |
| 373 | + | ...this.#state, | |
| 374 | + | update: terminalUpdate({ pending, status: "failed", reason: "state-dir-owned" }), | |
π‘ Medium src/serviceLauncher.ts:374
#suspendForOwnership can overwrite a newer launcher's pending update with this launcher's failed record, causing recovery to select the wrong active version and lose the newer update state. During overlapping handoff, this launcher writes its in-memory pending record without verifying that service-state.json still contains the same update; serialize these transitions or re-read and compare the update ID before writing.
π€ Copy this AI Prompt to have your agent fix this:
| 1 | In file @apps/server/src/serviceLauncher.ts around line 374: |
| 2 | |
| 3 | `#suspendForOwnership` can overwrite a newer launcher's pending update with this launcher's `failed` record, causing recovery to select the wrong active version and lose the newer update state. During overlapping handoff, this launcher writes its in-memory `pending` record without verifying that `service-state.json` still contains the same update; serialize these transitions or re-read and compare the update ID before writing. |
Note
Written by gpt-6.1-sol on behalf of Maria
fixed in b60c8024f0. a dedicated lifetime launcher SQLite lock serializes service-state transitions across canonical data-directory aliases. service state is refreshed after acquiring it. a refused contender parks without changing state or stop markers; the regression verifies it does not spawn another child.
| β¦ | β¦ | @@ -828,6 +847,10 @@ export const make = Effect.fn("cloud.boot_service.make")(function* (input: { | |
| 830 | 849 | ||
| 850 | + | // Stopping the unit does not stop an unmanaged desktop, SSH, or CLI server. | |
| 851 | + | // Updating only installed launcher files with start=false remains allowed. | |
| 852 | + | if (!installed || start) yield* requireStopped; | |
π High cloud/bootService.ts:852
After manager.stop releases the service, a competing desktop/CLI server can acquire ownership before requireStopped runs; install() or restart then returns ServerAlreadyRunningError while the previously working background service remains stopped. The post-stop ownership check is outside the existing Effect.tapError recovery, so apply the same restart-on-error recovery to this check (or otherwise restart the old unit when ownership is refused).
π€ Copy this AI Prompt to have your agent fix this:
| 1 | In file @apps/server/src/cloud/bootService.ts around line 852: |
| 2 | |
| 3 | After `manager.stop` releases the service, a competing desktop/CLI server can acquire ownership before `requireStopped` runs; `install()` or `restart` then returns `ServerAlreadyRunningError` while the previously working background service remains stopped. The post-stop ownership check is outside the existing `Effect.tapError` recovery, so apply the same restart-on-error recovery to this check (or otherwise restart the old unit when ownership is refused). |
Note
Written by gpt-6.1-sol on behalf of Maria
fixed in b60c8024f0. install and restart now include the post-stop ownership check inside the existing restart-on-error recovery. the focused regression verifies both operations restart the stopped unit when ownership is refused.
| β¦ | β¦ | @@ -301,6 +301,11 @@ export const layer = Layer.effect( | |
| 301 | 301 | ), | |
| 302 | 302 | onShutdown: () => desktopWindow.handleBackendNotReady, | |
| 303 | 303 | onPreflightFailed: handlePrimaryPreflightFailure, | |
| 304 | + | onStateDirOwned: () => | |
π High backend/DesktopBackendPool.ts:304
In WSL-only mode, an ownership failure leaves the frameless showConnectingSplash visible after the modal closes, so the app remains stuck on the non-closable βConnecting to WSLβ screen: the manager sets desiredRunning: false, and this callback neither quits the app nor transitions the window out of startup. Make this terminal failure path close/quit the desktop or explicitly dismiss the splash.
No longer relevant as of b60c8024f0166c92c175a5fa66f3a2cbabe1c468
Note
Written by gpt-6.1-sol on behalf of Maria
fixed in b60c8024f0. after the primary ownership error dialog closes, the desktop quits instead of leaving the connecting splash visible. desktop typecheck and existing backend pool checks pass; the packaged native dialog remains unverified on Windows/WSL.
| β¦ | β¦ | @@ -0,0 +1,144 @@ | |
| 37 | + | } catch (cause) { | |
| 38 | + | if (!(cause instanceof Error && "code" in cause && cause.code === "ENOENT")) throw cause; | |
| 39 | + | } | |
| 40 | + | const state = contents === undefined ? undefined : parseServiceState(contents); | |
π High src/serverOwnershipLock.ts:40
An interrupted update written with an older launcher protocol is treated as absent, so the server/CLI proceeds even when runtime/db-backup/<id> exists and the database requires manual recovery. parseServiceState(contents) returns undefined for non-current protocols; detect pending updates with serviceStateHasPendingUpdate independently of the protocol while retaining the backup-ID validation.
No longer relevant as of b60c8024f0166c92c175a5fa66f3a2cbabe1c468
Note
Written by gpt-6.1-sol on behalf of Maria
fixed in b60c8024f0. server and CLI guards detect pending updates across launcher protocol revisions, validate the backup directory ID, and refuse recovery independently of the current protocol decoder. the existing recovery test now covers protocols 1, 2, 3, and 4.