From 2c0f6c6d326ae7abfbdf851a135ff79470e00fdc Mon Sep 17 00:00:00 2001 From: MasterYoav Date: Tue, 29 Sep 2026 07:59:07 +0300 Subject: [PATCH 1/3] The engine survives a recreate and an unclean stop. A recreated container lost the Postgres password file while the cluster kept the password; postgres-init now sets a new one when none is beside the cluster. migrate raced crash recovery after SIGKILL (57P03); it now waits for pg_isready, up to 60s. --- docs/13-launch-checklist.md | 12 ++++++++++++ engine/docker/s6/scripts/migrate.sh | 13 +++++++++++++ engine/docker/s6/scripts/postgres-init.sh | 15 +++++++++++++++ 3 files changed, 40 insertions(+) diff --git a/docs/13-launch-checklist.md b/docs/13-launch-checklist.md index c69a60c..94e0041 100644 --- a/docs/13-launch-checklist.md +++ b/docs/13-launch-checklist.md @@ -162,6 +162,18 @@ logging proxy in the path (12 of 12). A leftover process, a database lock, ident keep-alive and split request writes were each checked and ruled out. Worth one more look before launch, starting from the app under ordinary use rather than nine tests at once. +**Two more, found starting the app on this Mac's own engine** (27 September), both leaving the +container unhealthy with nothing on its port and no way back short of deleting the data: + +- **Any recreate of the container lost the database password.** The app mounts `xbot-data` at + `/var/lib/postgresql/data`; the password file lives one level up, so it went with the container + while the cluster kept the password. Every environment change recreates the container. Reproduced + on the old image with a clean stop and recreate; `postgres-init.sh` now sets a new password when a + cluster has none beside it, which also heals an install already in this state. +- **`migrate` raced Postgres after an unclean stop.** It started before crash recovery finished and + failed on `57P03`; 3 of 3 SIGKILL-and-start rounds broke the old image, 0 of 3 the fixed one. + `migrate.sh` now waits for `pg_isready`, up to 60s. + **Also close the last M2 item here:** point one agent at a second real vendor — Anthropic is already proven, so use OpenAI or Google — and confirm the reply comes from the vendor you picked. A model name the vendor does not have should come back as a named error, not a silent fall back to somebody diff --git a/engine/docker/s6/scripts/migrate.sh b/engine/docker/s6/scripts/migrate.sh index 6776b1e..27d167b 100755 --- a/engine/docker/s6/scripts/migrate.sh +++ b/engine/docker/s6/scripts/migrate.sh @@ -8,6 +8,19 @@ set -eu [ "${EMBEDDED_POSTGRES:-off}" = "on" ] || exit 0 cd /app/server +# `postgres` is a longrun with no readiness notification, so s6 starts this the moment the process +# exists, not when it accepts connections. After an unclean stop it replays WAL first and answers +# 57P03 "not yet accepting connections"; migrating then exits 1, and `api`, which depends on this, +# never starts — the container sits unhealthy with nothing on :3001. Wait for it, but not forever. +i=0 +until /usr/lib/postgresql/16/bin/pg_isready -q -h 127.0.0.1 -p 5432; do + i=$((i + 1)) + if [ "$i" -ge 120 ]; then + echo "migrate: postgres did not accept connections within 60s" >&2 + exit 1 + fi + sleep 0.5 +done # `scripts/migrate.ts`, not `drizzle-kit`. The CLI is a development dependency and needs esbuild to # read its TypeScript config, which `bun install --production` leaves out of this image: asked to # migrate here it exits 1 without printing why, and the container comes up against an empty database. diff --git a/engine/docker/s6/scripts/postgres-init.sh b/engine/docker/s6/scripts/postgres-init.sh index b40abf2..c5aa21f 100755 --- a/engine/docker/s6/scripts/postgres-init.sh +++ b/engine/docker/s6/scripts/postgres-init.sh @@ -62,6 +62,21 @@ if [ ! -s "$DATA/PG_VERSION" ]; then s6-setuidgid postgres "$BIN/pg_ctl" -D "$DATA" -w stop >/dev/null fi +# A cluster with no password file beside it. "Beside the data on the same volume" holds only when the +# volume is mounted at /var/lib/postgresql; mounted at $DATA — as the Mac app has always done, and +# moving it now would strand every existing cluster — the file lives in the container's own layer and +# is gone the first time the container is recreated, while the cluster keeps the password it no longer +# has. `migrate` then fails authentication, `api` never starts, and nothing fixes it short of deleting +# the data. So set a new one. Single-user mode needs no connection and so no password, and the +# statement arrives on stdin, never argv. The server is not running yet: `postgres` depends on this. +if [ -s "$DATA/PG_VERSION" ] && [ ! -s "$PW_FILE" ]; then + PW="$(od -An -N32 -tx1 /dev/urandom | tr -d ' \n')" + printf "ALTER ROLE openbot PASSWORD '%s';\n" "$PW" \ + | s6-setuidgid postgres "$BIN/postgres" --single -D "$DATA" postgres >/dev/null + ( umask 077; printf '%s' "$PW" > "$PW_FILE" ) + chown postgres:postgres "$PW_FILE" +fi + # Every boot, not only the first: hand the password-bearing URL to the services that connect over TCP # (`api` and `migrate`, both `with-contenv`). The password persists with the cluster; the container # environment is fresh each boot, so this has to run outside the first-init guard above. The file is From 96d9ce6e9416d627b16b096680b745a0967c2ab7 Mon Sep 17 00:00:00 2001 From: MasterYoav Date: Tue, 29 Sep 2026 07:59:07 +0300 Subject: [PATCH 2/3] Bundle SwiftPM resource bundles into XBot.app, or Bundle.module traps at launch. --- scripts/bundle-mac-app.sh | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/scripts/bundle-mac-app.sh b/scripts/bundle-mac-app.sh index f443a3f..474ff48 100755 --- a/scripts/bundle-mac-app.sh +++ b/scripts/bundle-mac-app.sh @@ -33,6 +33,12 @@ cp "${MAC}/Resources/Info.plist" "${APP}/Contents/Info.plist" chmod +x "${APP}/Contents/MacOS/XBot" +# SwiftPM's per-target resource bundles. `Bundle.module` traps when its bundle is missing, so an app +# without them dies at launch on the first view that loads a resource. +for bundle in "$(dirname "${BUILD}")"/*.bundle; do + [[ -d "${bundle}" ]] && rsync -a "${bundle}" "${APP}/Contents/Resources/" +done + # Sparkle ships as an embedded framework when linked through SwiftPM. SPARKLE_FW="$(find "${MAC}/.build" -path '*/Sparkle.framework' -type d 2>/dev/null | head -1)" if [[ -n "${SPARKLE_FW}" ]]; then From 8db17eca01abf1639d0a8fb17dbbf98ed595cc59 Mon Sep 17 00:00:00 2001 From: MasterYoav Date: Tue, 29 Sep 2026 07:59:07 +0300 Subject: [PATCH 3/3] Title bar drawn above the content on macOS 27; a bare swift run binary takes focus. --- apps/mac/Sources/XBotApp/QuitHandler.swift | 10 ++++++++++ .../Components/WindowChromeConfigurator.swift | 17 +++++++++++++++++ .../Sources/XBotUI/DesignSystem/Materials.swift | 5 ++++- apps/mac/Sources/XBotUI/MainWindow.swift | 5 +++++ 4 files changed, 36 insertions(+), 1 deletion(-) diff --git a/apps/mac/Sources/XBotApp/QuitHandler.swift b/apps/mac/Sources/XBotApp/QuitHandler.swift index 1ba6bc8..29256ed 100644 --- a/apps/mac/Sources/XBotApp/QuitHandler.swift +++ b/apps/mac/Sources/XBotApp/QuitHandler.swift @@ -11,6 +11,16 @@ final class QuitHandler: NSObject, NSApplicationDelegate { /// Set once, when the app builds its state. Nil in a build with no managed runtime. static var state: AppState? + /// A bare executable — `swift run`, or the debug binary started from a script — has no + /// Info.plist, and macOS can hand it the prohibited policy: the window draws and its field even + /// shows focus, but the app is never frontmost, so every keystroke goes to whatever is. The + /// bundled app never takes this branch. + func applicationWillFinishLaunching(_ notification: Notification) { + guard Bundle.main.bundleIdentifier == nil else { return } + NSApp.setActivationPolicy(.regular) + NSApp.activate(ignoringOtherApps: true) + } + func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply { guard let state = Self.state, state.hasManagedRuntime else { return .terminateNow } Task { @MainActor in diff --git a/apps/mac/Sources/XBotUI/Components/WindowChromeConfigurator.swift b/apps/mac/Sources/XBotUI/Components/WindowChromeConfigurator.swift index 1230dce..1fe7e40 100644 --- a/apps/mac/Sources/XBotUI/Components/WindowChromeConfigurator.swift +++ b/apps/mac/Sources/XBotUI/Components/WindowChromeConfigurator.swift @@ -73,6 +73,7 @@ public struct WindowChromeConfigurator: NSViewRepresentable { window.titlebarAppearsTransparent = true window.titleVisibility = .hidden window.styleMask.insert(.fullSizeContentView) + keepTitlebarAboveContent(in: window) switch style { case .main: @@ -98,6 +99,22 @@ public struct WindowChromeConfigurator: NSViewRepresentable { } } + /// The title bar above the SwiftUI content, where AppKit is meant to keep it. + /// + /// On macOS 27 the window opens with the hosting view stacked over `NSTitlebarContainerView`. + /// With a full-size content view the aurora then paints over the whole title bar: no traffic + /// lights, no toggles, no agent name — every view still laid out and not hidden, just covered. + /// Moving the container back to the top is all it takes, and it stays there once moved. + private func keepTitlebarAboveContent(in window: NSWindow) { + guard let frame = window.contentView?.superview, + let titlebar = frame.subviews.first(where: { + String(describing: type(of: $0)).contains("TitlebarContainer") + }), + frame.subviews.last !== titlebar + else { return } + frame.addSubview(titlebar, positioned: .above, relativeTo: nil) + } + private func stripToolbarItemBackgrounds(in view: NSView?) { guard let view else { return } let className = String(describing: type(of: view)) diff --git a/apps/mac/Sources/XBotUI/DesignSystem/Materials.swift b/apps/mac/Sources/XBotUI/DesignSystem/Materials.swift index 5198a6a..3105228 100644 --- a/apps/mac/Sources/XBotUI/DesignSystem/Materials.swift +++ b/apps/mac/Sources/XBotUI/DesignSystem/Materials.swift @@ -62,7 +62,10 @@ private struct FrostedGlassModifier: ViewModifier { if reduceTransparency { content.background(opaqueFallback) } else { - content.background(FrostedGlassBackground(material: material)) + // Up under the title bar, as the opaque fallback above already goes: a `Color` + // background ignores the safe area by default, a representable does not, so the rail + // and panel stopped 38pt short of the top only when transparency was on. + content.background(FrostedGlassBackground(material: material).ignoresSafeArea()) } } } diff --git a/apps/mac/Sources/XBotUI/MainWindow.swift b/apps/mac/Sources/XBotUI/MainWindow.swift index c9a3acd..b8fd725 100644 --- a/apps/mac/Sources/XBotUI/MainWindow.swift +++ b/apps/mac/Sources/XBotUI/MainWindow.swift @@ -51,6 +51,11 @@ public struct MainWindow: View { ToolbarItem(placement: .principal) { TitleBarAgentTitle() } + // Pushes the panel toggle to the trailing corner, over the panel it opens. Without it + // `.primaryAction` sits flush against the centred title. + if #available(macOS 26.0, *) { + ToolbarSpacer(.flexible) + } ToolbarItem(placement: .primaryAction) { sidebarToggle( isVisible: state.isPanelVisible,