Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions apps/mac/Sources/XBotRuntime/RuntimeController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,10 @@ public actor RuntimeController {
image = newImage

if await launchEngineWithoutAdoption(environment: environment) {
// Nothing reads the dump once the new image is up — there is no rollback after a
// success — so keeping it would leave a plain-SQL copy of the person's conversations
// and agents in Application Support until the next upgrade overwrote it.
removeDump()
return .succeeded
}

Expand All @@ -432,6 +436,14 @@ public actor RuntimeController {
return .failed
}

/// The dump and the log beside it, gone. Called when a restore can no longer be wanted.
private func removeDump() {
try? FileManager.default.removeItem(at: dumpURL)
try? FileManager.default.removeItem(
at: dumpURL.deletingLastPathComponent().appendingPathComponent("restore.log")
)
}

/// Whether a dump was taken before the last upgrade, and so whether a restore is possible.
private var dumpedBeforeUpgrade = false

Expand Down Expand Up @@ -588,10 +600,7 @@ public actor RuntimeController {
}
// The pre-upgrade dump is a plain-SQL copy of the database, kept outside the volume so a
// rollback can use it — which is exactly why removing the volumes never removed it.
try? FileManager.default.removeItem(at: dumpURL)
try? FileManager.default.removeItem(
at: dumpURL.deletingLastPathComponent().appendingPathComponent("restore.log")
)
removeDump()
// Back to stopped, not notDetected: the runtime is still installed and still working — it
// is only xBot's own data that is gone.
state = .stopped
Expand Down
33 changes: 33 additions & 0 deletions apps/mac/Tests/XBotRuntimeTests/RuntimeTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,39 @@ struct RuntimeControllerTests {
#expect(!(await driver.removedHandles.isEmpty))
}

/**
An upgrade that worked has no use left for the dump, so it should not keep it.

The dump exists for one reason: restoring a database the new image migrated past what the old
one can read. Nothing reads it once the new image is up — there is no roll back to a previous
engine after a successful upgrade — so keeping it leaves a plain-SQL copy of the person's
conversations and agents in Application Support until the next upgrade happens to overwrite it.
*/
@Test func aSucceededUpgradeDoesNotLeaveTheDumpBehind() async {
let dumpURL = isolatedDumpURL()
let controller = RuntimeController(
driver: FakeDriver(),
image: ImageReference(repository: "xbot/engine", tag: "1"),
health: { _ in EngineHealth(engineVersion: "0.0.5", schemaVersion: "0000") },
ports: isolatedPortStore(),
dumpURL: dumpURL
)
await controller.start(environment: environment)

let outcome = await controller.upgrade(
to: ImageReference(
repository: "ghcr.io/masteryoav/xbot-engine",
digest: "sha256:abc123def456"
),
rollingBackTo: ImageReference(repository: "xbot/engine", tag: "1"),
environment: environment
)

#expect(outcome == .succeeded)
// The fake writes one when the dump runs, so its absence here is a removal, not a no-op.
#expect(!FileManager.default.fileExists(atPath: dumpURL.path))
}

/**
A pull that fails must leave the running engine exactly where it was.

Expand Down
2 changes: 1 addition & 1 deletion docs/05-mac-app.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ See [06-onboarding.md](06-onboarding.md).
| Main | `Window` | Rail, conversation, panel. Onboarding crossfades in via `AppShellView` on first run |
| Onboarding | (in Main) | Fixed-size window mode during first run; not a separate scene |
| Settings | (in Main) | **Not a `Settings` scene.** In the window, in place of the conversation, via `AppState.isShowingSettings`. A floating panel put them somewhere the person had to go and find, and it hid the rail — so which agent was selected stopped being visible while its model was changed |
| Plugins admin | `Window` | `WKWebView` at engine `/admin/plugins` |
| Engine admin | `Window` | One `WKWebView` for every admin surface. Opens at `/admin/plugins` from **Plugins…** or `/admin` from **Engine admin…**, whose upstream sidebar reaches the rest |
| Agent screen (detached) | — | **Not built** |
| Menu bar | — | **Not built** — planned for runtime status when the main window is closed |

Expand Down
4 changes: 4 additions & 0 deletions docs/11-packaging-and-updates.md
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,10 @@ Two judgement calls in it:
attempts a restore if it was.
- **`--single-transaction` on the way back in**, so a restore that fails part-way leaves the
database as it was rather than half-replaced.
- **An upgrade that succeeded deletes the dump.** Nothing reads it once the new image is up, there
being no rollback after a success, so keeping it would leave a plain-SQL copy of somebody's
conversations and agents in Application Support until the next upgrade overwrote it. Uninstall
removes it too.

---

Expand Down
Loading