From 70472dc945753c745de008d627035b549b150e31 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Fri, 17 Jul 2026 10:06:32 +0200 Subject: [PATCH 01/38] Always install the newest eligible desktop update (#2149) * Reapply "Always revalidate desktop updates before install" This reverts commit 623c05aa4d01e824d6381ba5d547a17834fa157f. * fix(desktop): make update revalidation safe on quit Updater-triggered quits now bypass normal quit handling, and manifest revalidation gives up after five seconds without allowing a late install. * fix(desktop): preserve fail-closed update installs Keep cached updates deferred when quit-time validation is offline, and report validation timeouts separately from superseded releases. * fix(desktop): preserve updater quit behavior Automatic installs remain silent without relaunching, updater handoff is bounded, and background preparation failures remain visible without pinning manifest checks. * fix(desktop): keep AppImage updates manual Preserve the AppImage safety exemption by skipping ordinary quit-time installation while retaining the explicit Update now path. * fix(desktop): serialize update preparation * fix(desktop): recognize macOS updater quit handoff --- docs/release.md | 4 +- .../desktop/src/daemon/quit-lifecycle.test.ts | 268 +++++++--- packages/desktop/src/daemon/quit-lifecycle.ts | 97 +++- .../src/features/app-update-service.test.ts | 461 +++++++++++++----- .../src/features/app-update-service.ts | 299 ++++++++---- .../desktop/src/features/auto-updater.test.ts | 14 +- packages/desktop/src/features/auto-updater.ts | 42 +- packages/desktop/src/main.ts | 52 +- 8 files changed, 909 insertions(+), 328 deletions(-) diff --git a/docs/release.md b/docs/release.md index 90dd263e3..8252898ca 100644 --- a/docs/release.md +++ b/docs/release.md @@ -169,13 +169,13 @@ This does **not** apply to fresh releases cut via `npm run release:patch` — th ### Releasing during an active rollout -If you ship N+1 while N is still ramping, N+1 starts a fresh rollout from its own publish timestamp. N's rollout effectively ends — the newer manifest supersedes it. +If you ship N+1 while N is still ramping, N+1 starts a fresh rollout from its own publish timestamp. N's rollout effectively ends — the newer manifest supersedes it. Rollout-aware clients revalidate the manifest for up to five seconds before installing a downloaded update on quit. If N+1 has replaced N but the client is not admitted to N+1 yet, it skips the downloaded N and waits rather than installing two updates in succession. If revalidation times out, the app exits without installing the cached update. If N+1 is a hotfix for a bug in N, dispatch `desktop-rollout.yml -f tag=v0.1. -f rollout_hours=0` after N+1 publishes so the users who already got N reach the fix fast. ### Limitations -- **No pause / kill switch.** Once a stable user is admitted, they will install the update on next quit (`autoInstallOnAppQuit = true`). To stop new admissions, ship a superseding release. To "recall" already-admitted users, ship a hotfix `+1` patch. +- **No pause / kill switch.** To stop new admissions, ship a superseding release. Clients revalidate on quit and will not install the superseded download, but a client that already completed installation cannot be recalled; ship a hotfix `+1` patch. - **No rollback.** `allowDowngrade = false`. Bad release = ship a hotfix. - **Bootstrap caveat.** Clients running a build older than the rollout feature ignore `rolloutHours` and admit immediately. Rollout protection only applies to clients running the rollout-aware version or later. - **Up to ~30 min automatic admission latency.** Renderer polls every 30 minutes, so a stable user may take up to that long to be evaluated against the rollout window. Clicking **Check** is manual and bypasses rollout admission. diff --git a/packages/desktop/src/daemon/quit-lifecycle.test.ts b/packages/desktop/src/daemon/quit-lifecycle.test.ts index d9af82b65..1d636bd95 100644 --- a/packages/desktop/src/daemon/quit-lifecycle.test.ts +++ b/packages/desktop/src/daemon/quit-lifecycle.test.ts @@ -1,8 +1,8 @@ -import { describe, expect, it, vi } from "vitest"; +import { describe, expect, it } from "vitest"; import { DEFAULT_DESKTOP_SETTINGS } from "../settings/desktop-settings"; import { - createBeforeQuitHandler, + createQuitLifecycle, shouldStopDesktopManagedDaemonOnQuit, stopDesktopManagedDaemonOnQuitIfNeeded, } from "./quit-lifecycle"; @@ -16,6 +16,18 @@ const SETTINGS_STOP_ON_QUIT = { }, }; +function deferred(): { promise: Promise; resolve(value: T): void } { + let resolve!: (value: T) => void; + const promise = new Promise((resolvePromise) => { + resolve = resolvePromise; + }); + return { promise, resolve }; +} + +function waitForQuitLifecycle(): Promise { + return new Promise((resolve) => setImmediate(resolve)); +} + describe("quit-lifecycle", () => { it("only stops when keepRunningAfterQuit is explicitly disabled", () => { expect(shouldStopDesktopManagedDaemonOnQuit(SETTINGS_STOP_ON_QUIT)).toBe(true); @@ -23,96 +35,238 @@ describe("quit-lifecycle", () => { }); it("short-circuits without inspecting the daemon when keep-running is on", async () => { - const isDesktopManagedDaemonRunning = vi.fn(() => true); - const stopDaemon = vi.fn(async () => undefined); - const showShutdownFeedback = vi.fn(); + const events: string[] = []; const stopped = await stopDesktopManagedDaemonOnQuitIfNeeded({ settingsStore: { get: async () => SETTINGS_KEEP_RUNNING }, - isDesktopManagedDaemonRunning, - stopDaemon, - showShutdownFeedback, + isDesktopManagedDaemonRunning: () => { + events.push("inspect"); + return true; + }, + stopDaemon: async () => { + events.push("stop"); + }, + showShutdownFeedback: () => { + events.push("feedback"); + }, }); expect(stopped).toBe(false); - expect(isDesktopManagedDaemonRunning).not.toHaveBeenCalled(); - expect(stopDaemon).not.toHaveBeenCalled(); - expect(showShutdownFeedback).not.toHaveBeenCalled(); + expect(events).toEqual([]); }); it("does not stop a manually started daemon on quit", async () => { - const stopDaemon = vi.fn(async () => undefined); - const showShutdownFeedback = vi.fn(); + const events: string[] = []; const stopped = await stopDesktopManagedDaemonOnQuitIfNeeded({ settingsStore: { get: async () => SETTINGS_STOP_ON_QUIT }, isDesktopManagedDaemonRunning: () => false, - stopDaemon, - showShutdownFeedback, + stopDaemon: async () => { + events.push("stop"); + }, + showShutdownFeedback: () => { + events.push("feedback"); + }, }); expect(stopped).toBe(false); - expect(stopDaemon).not.toHaveBeenCalled(); - expect(showShutdownFeedback).not.toHaveBeenCalled(); + expect(events).toEqual([]); }); it("shows feedback then stops a desktop-managed daemon", async () => { - const stopDaemon = vi.fn(async () => undefined); - const showShutdownFeedback = vi.fn(); + const events: string[] = []; const stopped = await stopDesktopManagedDaemonOnQuitIfNeeded({ settingsStore: { get: async () => SETTINGS_STOP_ON_QUIT }, isDesktopManagedDaemonRunning: () => true, - stopDaemon, - showShutdownFeedback, + stopDaemon: async () => { + events.push("stop"); + }, + showShutdownFeedback: () => { + events.push("feedback"); + }, }); expect(stopped).toBe(true); - expect(showShutdownFeedback).toHaveBeenCalledTimes(1); - expect(stopDaemon).toHaveBeenCalledTimes(1); - expect(showShutdownFeedback.mock.invocationCallOrder[0]).toBeLessThan( - stopDaemon.mock.invocationCallOrder[0], - ); + expect(events).toEqual(["feedback", "stop"]); }); - it("preventDefaults the first quit, runs the async stop decision, then exits hard", async () => { - let resolveStopDecision: (() => void) | null = null; - const app = { exit: vi.fn() }; - const closeTransportSessions = vi.fn(); - const onStopError = vi.fn(); - const preventDefault = vi.fn(); - const secondPreventDefault = vi.fn(); + it("revalidates updates after daemon shutdown before exiting", async () => { + const stopDecision = deferred(); + const updateDecision = deferred(); + const events: string[] = []; - const handleBeforeQuit = createBeforeQuitHandler({ - app, - closeTransportSessions, - stopDesktopManagedDaemonIfNeeded: vi.fn( - () => - new Promise((resolve) => { - resolveStopDecision = () => resolve(false); - }), - ), - onStopError, + const quitLifecycle = createQuitLifecycle({ + app: { + exit: (code) => { + events.push(`exit:${code}`); + }, + }, + closeTransportSessions: () => { + events.push("close-transports"); + }, + stopDesktopManagedDaemonIfNeeded: () => stopDecision.promise, + installAppUpdateOnQuit: () => updateDecision.promise, + createUpdateDeadlineSignal: () => new AbortController().signal, + onStopError: () => { + events.push("stop-error"); + }, + onUpdateError: () => { + events.push("update-error"); + }, }); - handleBeforeQuit({ preventDefault }); + quitLifecycle.handleBeforeQuit({ + preventDefault: () => { + events.push("prevent-default"); + }, + }); - expect(preventDefault).toHaveBeenCalledTimes(1); - expect(closeTransportSessions).toHaveBeenCalledTimes(1); - expect(app.exit).not.toHaveBeenCalled(); - expect(resolveStopDecision).not.toBeNull(); + expect(events).toEqual(["close-transports", "prevent-default"]); - resolveStopDecision?.(); - await Promise.resolve(); - await Promise.resolve(); + events.push("daemon-stopped"); + stopDecision.resolve(false); + await waitForQuitLifecycle(); - expect(app.exit).toHaveBeenCalledWith(0); - expect(onStopError).not.toHaveBeenCalled(); + expect(events).toEqual(["close-transports", "prevent-default", "daemon-stopped"]); - handleBeforeQuit({ preventDefault: secondPreventDefault }); + events.push("update-checked"); + updateDecision.resolve(false); + await waitForQuitLifecycle(); - expect(secondPreventDefault).not.toHaveBeenCalled(); - expect(closeTransportSessions).toHaveBeenCalledTimes(2); - expect(app.exit).toHaveBeenCalledTimes(1); + expect(events).toEqual([ + "close-transports", + "prevent-default", + "daemon-stopped", + "update-checked", + "exit:0", + ]); + + quitLifecycle.handleBeforeQuit({ + preventDefault: () => { + events.push("second-prevent-default"); + }, + }); + + expect(events.at(-1)).toBe("close-transports"); + expect(events).not.toContain("second-prevent-default"); + }); + + it("lets the updater own process exit when a validated update is installing", async () => { + const exits: number[] = []; + const quitLifecycle = createQuitLifecycle({ + app: { exit: (code) => exits.push(code) }, + closeTransportSessions: () => {}, + stopDesktopManagedDaemonIfNeeded: async () => false, + installAppUpdateOnQuit: async () => true, + createUpdateDeadlineSignal: () => new AbortController().signal, + onStopError: () => {}, + onUpdateError: () => {}, + }); + + quitLifecycle.handleBeforeQuit({ preventDefault: () => {} }); + await waitForQuitLifecycle(); + quitLifecycle.handleBeforeQuitForUpdate(); + await waitForQuitLifecycle(); + + expect(exits).toEqual([]); + }); + + it("recognizes a repeated quit as updater handoff", async () => { + const exits: number[] = []; + let preventedQuitCount = 0; + const quitLifecycle = createQuitLifecycle({ + app: { exit: (code) => exits.push(code) }, + closeTransportSessions: () => {}, + stopDesktopManagedDaemonIfNeeded: async () => false, + installAppUpdateOnQuit: async () => true, + createUpdateDeadlineSignal: () => new AbortController().signal, + onStopError: () => {}, + onUpdateError: () => {}, + }); + + quitLifecycle.handleBeforeQuit({ preventDefault: () => preventedQuitCount++ }); + await waitForQuitLifecycle(); + quitLifecycle.handleBeforeQuit({ preventDefault: () => preventedQuitCount++ }); + await waitForQuitLifecycle(); + + expect(preventedQuitCount).toBe(1); + expect(exits).toEqual([]); + }); + + it("exits when the updater does not take ownership before its deadline", async () => { + const revalidationDeadline = new AbortController(); + const handoffDeadline = new AbortController(); + let deadlineCount = 0; + const exits: number[] = []; + const quitLifecycle = createQuitLifecycle({ + app: { exit: (code) => exits.push(code) }, + closeTransportSessions: () => {}, + stopDesktopManagedDaemonIfNeeded: async () => false, + installAppUpdateOnQuit: async () => true, + createUpdateDeadlineSignal: () => + deadlineCount++ === 0 ? revalidationDeadline.signal : handoffDeadline.signal, + onStopError: () => {}, + onUpdateError: () => {}, + }); + + quitLifecycle.handleBeforeQuit({ preventDefault: () => {} }); + await waitForQuitLifecycle(); + handoffDeadline.abort(); + await waitForQuitLifecycle(); + + expect(exits).toEqual([0]); + }); + + it("does not intercept a quit started by a manual update", () => { + const events: string[] = []; + const quitLifecycle = createQuitLifecycle({ + app: { exit: (code) => events.push(`exit:${code}`) }, + closeTransportSessions: () => events.push("close-transports"), + stopDesktopManagedDaemonIfNeeded: async () => { + events.push("stop-daemon"); + return false; + }, + installAppUpdateOnQuit: async () => { + events.push("revalidate-update"); + return false; + }, + createUpdateDeadlineSignal: () => new AbortController().signal, + onStopError: () => events.push("stop-error"), + onUpdateError: () => events.push("update-error"), + }); + + quitLifecycle.handleBeforeQuitForUpdate(); + quitLifecycle.handleBeforeQuit({ + preventDefault: () => events.push("prevent-default"), + }); + + expect(events).toEqual(["close-transports"]); + }); + + it("exits when update revalidation reaches its deadline", async () => { + const deadline = new AbortController(); + const updateDecision = deferred(); + const exits: number[] = []; + const quitLifecycle = createQuitLifecycle({ + app: { exit: (code) => exits.push(code) }, + closeTransportSessions: () => {}, + stopDesktopManagedDaemonIfNeeded: async () => false, + installAppUpdateOnQuit: () => updateDecision.promise, + createUpdateDeadlineSignal: () => deadline.signal, + onStopError: () => {}, + onUpdateError: () => {}, + }); + + quitLifecycle.handleBeforeQuit({ preventDefault: () => {} }); + await waitForQuitLifecycle(); + deadline.abort(); + await waitForQuitLifecycle(); + + expect(exits).toEqual([0]); + + updateDecision.resolve(true); + await waitForQuitLifecycle(); + expect(exits).toEqual([0]); }); }); diff --git a/packages/desktop/src/daemon/quit-lifecycle.ts b/packages/desktop/src/daemon/quit-lifecycle.ts index 879f59778..1998985f5 100644 --- a/packages/desktop/src/daemon/quit-lifecycle.ts +++ b/packages/desktop/src/daemon/quit-lifecycle.ts @@ -14,6 +14,16 @@ interface BeforeQuitApp { exit(code: number): void; } +interface QuitLifecycle { + handleBeforeQuit(event: BeforeQuitEvent): void; + handleBeforeQuitForUpdate(): void; +} + +interface DeferredUpdateQuit { + promise: Promise; + resolve(): void; +} + export interface StopOnQuitDeps { settingsStore: Pick; isDesktopManagedDaemonRunning: () => boolean; @@ -42,36 +52,95 @@ export async function stopDesktopManagedDaemonOnQuitIfNeeded( return true; } -export function createBeforeQuitHandler({ +function waitForUpdateDeadline(signal: AbortSignal): Promise { + if (signal.aborted) { + return Promise.resolve(false); + } + + return new Promise((resolve) => { + signal.addEventListener("abort", () => resolve(false), { once: true }); + }); +} + +function createDeferredUpdateQuit(): DeferredUpdateQuit { + let resolvePromise!: (started: boolean) => void; + const promise = new Promise((resolve) => { + resolvePromise = resolve; + }); + return { promise, resolve: () => resolvePromise(true) }; +} + +export function createQuitLifecycle({ app, closeTransportSessions, stopDesktopManagedDaemonIfNeeded, + installAppUpdateOnQuit, + createUpdateDeadlineSignal, onStopError, + onUpdateError, }: { app: BeforeQuitApp; closeTransportSessions: () => void; stopDesktopManagedDaemonIfNeeded: () => Promise; + installAppUpdateOnQuit: (signal: AbortSignal) => Promise; + createUpdateDeadlineSignal: () => AbortSignal; onStopError: (error: unknown) => void; -}): (event: BeforeQuitEvent) => void { - // We always preventDefault on first quit so we can run the async stop - // decision, then call app.exit(0) — which bypasses Electron's - // close → window-all-closed → will-quit chain. The window-all-closed - // listener is a darwin no-op (macOS convention) and would otherwise - // veto a re-fired app.quit(). + onUpdateError: (error: unknown) => void; +}): QuitLifecycle { + // The first quit waits for daemon shutdown and update revalidation. A validated + // update re-fires app.quit(); otherwise app.exit(0) bypasses Electron's macOS + // window-all-closed handler, which would veto that second quit. let quitting = false; + let quittingForUpdate = false; + const updateQuit = createDeferredUpdateQuit(); - return (event) => { + function handleBeforeQuit(event: BeforeQuitEvent): void { closeTransportSessions(); - if (quitting) return; + if (quittingForUpdate) return; + if (quitting) { + // MacUpdater's no-relaunch path calls app.quit() without emitting + // before-quit-for-update. A second quit is equivalent handoff evidence. + updateQuit.resolve(); + return; + } quitting = true; event.preventDefault(); - void stopDesktopManagedDaemonIfNeeded() - .catch((error) => { + void (async () => { + try { + await stopDesktopManagedDaemonIfNeeded(); + } catch (error) { onStopError(error); - }) - .finally(() => { - app.exit(0); + } + + const signal = createUpdateDeadlineSignal(); + const updateInstallation = installAppUpdateOnQuit(signal).catch((error) => { + onUpdateError(error); + return false; }); + const installingUpdate = await Promise.race([ + updateInstallation, + waitForUpdateDeadline(signal), + ]); + if (installingUpdate) { + const handoffStarted = await Promise.race([ + updateQuit.promise, + waitForUpdateDeadline(createUpdateDeadlineSignal()), + ]); + if (handoffStarted) { + return; + } + } + + app.exit(0); + })(); + } + + return { + handleBeforeQuit, + handleBeforeQuitForUpdate() { + quittingForUpdate = true; + updateQuit.resolve(); + }, }; } diff --git a/packages/desktop/src/features/app-update-service.test.ts b/packages/desktop/src/features/app-update-service.test.ts index f243a7c28..1e8154602 100644 --- a/packages/desktop/src/features/app-update-service.test.ts +++ b/packages/desktop/src/features/app-update-service.test.ts @@ -17,7 +17,19 @@ class FakeAppUpdateRuntime implements AppUpdateRuntime { > = []; private gate: ((info: RuntimeUpdateInfo) => boolean | Promise) | null = null; private configuration: AppUpdateRuntimeConfiguration | null = null; + private downloadableUpdate: RuntimeUpdateInfo | null = null; + private downloadedUpdate: RuntimeUpdateInfo | null = null; + private activeDownload: { + info: RuntimeUpdateInfo; + promise: Promise; + resolve(): void; + reject(error: Error): void; + } | null = null; checkCount = 0; + downloadCallCount = 0; + downloadedVersions: string[] = []; + installedVersions: string[] = []; + installModes: Array<{ isSilent: boolean; isForceRunAfter: boolean }> = []; configure(input: AppUpdateRuntimeConfiguration): void { this.configuration = input; @@ -59,9 +71,42 @@ class FakeAppUpdateRuntime implements AppUpdateRuntime { } finishUpdateDownload(info: RuntimeUpdateInfo): void { + this.downloadedUpdate = info; + this.downloadedVersions.push(info.version); this.configuration?.onUpdateDownloaded(info); } + beginUpdateDownload(info: RuntimeUpdateInfo): { + resolve(): void; + reject(error: Error): void; + } { + this.downloadableUpdate = info; + this.prepareUpdate(info); + let resolvePromise!: () => void; + let rejectPromise!: (error: Error) => void; + const promise = new Promise((resolve, reject) => { + resolvePromise = resolve; + rejectPromise = reject; + }); + void promise.catch(() => undefined); + const activeDownload = { + info, + promise, + resolve: () => { + this.finishUpdateDownload(info); + this.activeDownload = null; + resolvePromise(); + }, + reject: (error: Error) => { + this.configuration?.onError(error); + this.activeDownload = null; + rejectPromise(error); + }, + }; + this.activeDownload = activeDownload; + return { resolve: activeDownload.resolve, reject: activeDownload.reject }; + } + async checkForUpdates(): Promise<{ isUpdateAvailable: boolean; updateInfo: RuntimeUpdateInfo; @@ -80,12 +125,27 @@ class FakeAppUpdateRuntime implements AppUpdateRuntime { } if (!result || !this.gate) return result; const admitted = await this.gate(result.updateInfo); - return { ...result, isUpdateAvailable: result.isUpdateAvailable && admitted }; + const isUpdateAvailable = result.isUpdateAvailable && admitted; + this.downloadableUpdate = isUpdateAvailable ? result.updateInfo : null; + return { ...result, isUpdateAvailable }; } - async downloadUpdate(): Promise {} + async downloadUpdate(): Promise { + this.downloadCallCount += 1; + if (this.activeDownload) { + return this.activeDownload.promise; + } + if (this.downloadableUpdate) { + this.finishUpdateDownload(this.downloadableUpdate); + } + } - quitAndInstall(): void {} + quitAndInstall(isSilent: boolean, isForceRunAfter: boolean): void { + if (this.downloadedUpdate) { + this.installedVersions.push(this.downloadedUpdate.version); + this.installModes.push({ isSilent, isForceRunAfter }); + } + } } function createService(input?: { now?: () => number; bucket?: () => Promise }) { @@ -148,6 +208,33 @@ describe("app update service", () => { }); }); + it("waits for an automatic poll before starting a manual rollout-bypassing check", async () => { + const { runtime, service } = createService(); + const automaticCheck = runtime.deferNextCheck(); + const automaticPending = service.checkForAppUpdate({ + currentVersion: "1.2.3", + releaseChannel: "stable", + intent: "automatic", + }); + runtime.nextCheck({ isUpdateAvailable: true, updateInfo: rolledOutUpdate }); + const manualPending = service.checkForAppUpdate({ + currentVersion: "1.2.3", + releaseChannel: "stable", + intent: "manual", + }); + + await Promise.resolve(); + expect(runtime.checkCount).toBe(1); + + automaticCheck.resolve({ isUpdateAvailable: false, updateInfo: rolledOutUpdate }); + await automaticPending; + const manualResult = await manualPending; + + expect(runtime.checkCount).toBe(2); + expect(manualResult.hasUpdate).toBe(true); + expect(manualResult.latestVersion).toBe("1.2.4"); + }); + it("performs a fresh manual check when an update is already cached", async () => { const { runtime, service } = createService({ bucket: async () => 0 }); runtime.nextCheck({ isUpdateAvailable: true, updateInfo: rolledOutUpdate }); @@ -179,6 +266,218 @@ describe("app update service", () => { }); }); + it("replaces a downloaded update when a newer release is admitted", async () => { + const { runtime, service } = createService({ bucket: async () => 0 }); + runtime.nextCheck({ isUpdateAvailable: true, updateInfo: rolledOutUpdate }); + + await service.checkForAppUpdate({ + currentVersion: "1.2.3", + releaseChannel: "stable", + intent: "automatic", + }); + runtime.finishUpdateDownload(rolledOutUpdate); + + const newerUpdate = { ...rolledOutUpdate, version: "1.2.5" }; + runtime.nextCheck({ isUpdateAvailable: true, updateInfo: newerUpdate }); + const result = await service.checkForAppUpdate({ + currentVersion: "1.2.3", + releaseChannel: "stable", + intent: "automatic", + }); + + expect(result).toEqual({ + hasUpdate: true, + readyToInstall: false, + currentVersion: "1.2.3", + latestVersion: "1.2.5", + body: null, + date: "2026-04-28T00:00:00.000Z", + errorMessage: null, + }); + }); + + it("installs the newest admitted release when quitting with an older download", async () => { + const { runtime, service } = createService({ bucket: async () => 0 }); + runtime.nextCheck({ isUpdateAvailable: true, updateInfo: rolledOutUpdate }); + + await service.checkForAppUpdate({ + currentVersion: "1.2.3", + releaseChannel: "stable", + intent: "automatic", + }); + runtime.finishUpdateDownload(rolledOutUpdate); + + const newerUpdate = { ...rolledOutUpdate, version: "1.2.5" }; + runtime.nextCheck({ isUpdateAvailable: true, updateInfo: newerUpdate }); + const installed = await service.installUpdateOnQuit({ + currentVersion: "1.2.3", + releaseChannel: "stable", + signal: new AbortController().signal, + }); + + expect(installed).toBe(true); + expect(runtime.installedVersions).toEqual(["1.2.5"]); + expect(runtime.installModes).toEqual([{ isSilent: true, isForceRunAfter: false }]); + }); + + it("does not install an older download while its replacement is still rolling out", async () => { + const now = Date.parse("2026-04-28T12:00:00.000Z"); + const { runtime, service } = createService({ now: () => now, bucket: async () => 0.4 }); + const olderUpdate = { + ...rolledOutUpdate, + releaseDate: "2026-04-27T00:00:00.000Z", + }; + runtime.nextCheck({ isUpdateAvailable: true, updateInfo: olderUpdate }); + + await service.checkForAppUpdate({ + currentVersion: "1.2.3", + releaseChannel: "stable", + intent: "automatic", + }); + runtime.finishUpdateDownload(olderUpdate); + + const newerUpdate = { + ...rolledOutUpdate, + version: "1.2.5", + releaseDate: "2026-04-28T12:00:00.000Z", + }; + runtime.nextCheck({ isUpdateAvailable: true, updateInfo: newerUpdate }); + const installed = await service.installUpdateOnQuit({ + currentVersion: "1.2.3", + releaseChannel: "stable", + signal: new AbortController().signal, + }); + + expect(installed).toBe(false); + expect(runtime.installedVersions).toEqual([]); + }); + + it("does not install after quit-time revalidation expires", async () => { + const { runtime, service } = createService({ bucket: async () => 0 }); + runtime.nextCheck({ isUpdateAvailable: true, updateInfo: rolledOutUpdate }); + + await service.checkForAppUpdate({ + currentVersion: "1.2.3", + releaseChannel: "stable", + intent: "automatic", + }); + runtime.finishUpdateDownload(rolledOutUpdate); + + const deadline = new AbortController(); + deadline.abort(); + runtime.nextCheck({ + isUpdateAvailable: true, + updateInfo: { ...rolledOutUpdate, version: "1.2.5" }, + }); + const installed = await service.installUpdateOnQuit({ + currentVersion: "1.2.3", + releaseChannel: "stable", + signal: deadline.signal, + }); + + expect(installed).toBe(false); + expect(runtime.installedVersions).toEqual([]); + }); + + it("does not install an unvalidated download when the quit-time check fails", async () => { + const { runtime, service } = createService({ bucket: async () => 0 }); + runtime.nextCheck({ isUpdateAvailable: true, updateInfo: rolledOutUpdate }); + + await service.checkForAppUpdate({ + currentVersion: "1.2.3", + releaseChannel: "stable", + intent: "automatic", + }); + runtime.finishUpdateDownload(rolledOutUpdate); + + runtime.failNextCheck(new Error("offline")); + const installed = await service.installUpdateOnQuit({ + currentVersion: "1.2.3", + releaseChannel: "stable", + signal: new AbortController().signal, + }); + + expect(installed).toBe(false); + expect(runtime.installedVersions).toEqual([]); + }); + + it("rechecks for the newest release before a manual install", async () => { + const { runtime, service } = createService({ bucket: async () => 0.99 }); + runtime.nextCheck({ isUpdateAvailable: true, updateInfo: rolledOutUpdate }); + + await service.checkForAppUpdate({ + currentVersion: "1.2.3", + releaseChannel: "stable", + intent: "manual", + }); + runtime.finishUpdateDownload(rolledOutUpdate); + + const newerUpdate = { ...rolledOutUpdate, version: "1.2.5" }; + runtime.nextCheck({ isUpdateAvailable: true, updateInfo: newerUpdate }); + const result = await service.downloadAndInstallUpdate({ + currentVersion: "1.2.3", + releaseChannel: "stable", + }); + + expect(result.installed).toBe(true); + expect(runtime.installedVersions).toEqual(["1.2.5"]); + expect(runtime.installModes).toEqual([{ isSilent: false, isForceRunAfter: true }]); + }); + + it("waits for a stale active download before downloading and installing the rechecked version", async () => { + const { runtime, service } = createService({ bucket: async () => 0 }); + runtime.nextCheck({ isUpdateAvailable: true, updateInfo: rolledOutUpdate }); + await service.checkForAppUpdate({ + currentVersion: "1.2.3", + releaseChannel: "stable", + intent: "automatic", + }); + const staleDownload = runtime.beginUpdateDownload(rolledOutUpdate); + + const newerUpdate = { ...rolledOutUpdate, version: "1.2.5" }; + runtime.nextCheck({ isUpdateAvailable: true, updateInfo: newerUpdate }); + const installPending = service.downloadAndInstallUpdate({ + currentVersion: "1.2.3", + releaseChannel: "stable", + }); + await Promise.resolve(); + expect(runtime.installedVersions).toEqual([]); + + staleDownload.resolve(); + const result = await installPending; + + expect(result.installed).toBe(true); + expect(runtime.downloadedVersions).toEqual(["1.2.4", "1.2.5"]); + expect(runtime.installedVersions).toEqual(["1.2.5"]); + }); + + it("installs the rechecked version when the stale active download fails", async () => { + const { runtime, service } = createService({ bucket: async () => 0 }); + runtime.nextCheck({ isUpdateAvailable: true, updateInfo: rolledOutUpdate }); + await service.checkForAppUpdate({ + currentVersion: "1.2.3", + releaseChannel: "stable", + intent: "automatic", + }); + const staleDownload = runtime.beginUpdateDownload(rolledOutUpdate); + + const newerUpdate = { ...rolledOutUpdate, version: "1.2.5" }; + runtime.nextCheck({ isUpdateAvailable: true, updateInfo: newerUpdate }); + const installPending = service.downloadAndInstallUpdate({ + currentVersion: "1.2.3", + releaseChannel: "stable", + }); + await new Promise((resolve) => setImmediate(resolve)); + expect(runtime.downloadCallCount).toBe(1); + + staleDownload.reject(new Error("old download failed")); + const result = await installPending; + + expect(result.installed).toBe(true); + expect(runtime.downloadedVersions).toEqual(["1.2.5"]); + expect(runtime.installedVersions).toEqual(["1.2.5"]); + }); + it("trusts the runtime availability decision before comparing versions", async () => { const { runtime, service } = createService({ bucket: async () => 0 }); runtime.nextCheck({ isUpdateAvailable: false, updateInfo: rolledOutUpdate }); @@ -322,28 +621,26 @@ describe("app update service", () => { }); }); - it("keeps preparation errors emitted before the update check rejects", async () => { - const { runtime, service } = createService(); - const deferredCheck = runtime.deferNextCheck(); - const pending = service.checkForAppUpdate({ + it("surfaces preparation errors without blocking newer releases", async () => { + const { runtime, service } = createService({ bucket: async () => 0 }); + runtime.nextCheck({ isUpdateAvailable: true, updateInfo: rolledOutUpdate }); + + await service.checkForAppUpdate({ currentVersion: "1.2.3", releaseChannel: "stable", - intent: "automatic", + intent: "manual", }); - runtime.prepareUpdate(rolledOutUpdate); runtime.failRuntime(new Error("sha512 checksum mismatch")); - deferredCheck.reject(new Error("sha512 checksum mismatch")); - const checkResult = await pending; - expect(checkResult.errorMessage).toBe("sha512 checksum mismatch"); - const automaticResult = await service.checkForAppUpdate({ + runtime.nextCheck({ isUpdateAvailable: true, updateInfo: rolledOutUpdate }); + const failedPreparation = await service.checkForAppUpdate({ currentVersion: "1.2.3", releaseChannel: "stable", intent: "automatic", }); - expect(automaticResult).toEqual({ + expect(failedPreparation).toEqual({ hasUpdate: true, readyToInstall: false, currentVersion: "1.2.3", @@ -352,19 +649,9 @@ describe("app update service", () => { date: "2026-04-28T00:00:00.000Z", errorMessage: "sha512 checksum mismatch", }); - }); - - it("returns runtime update errors after an update fails to prepare", async () => { - const { runtime, service } = createService(); - runtime.nextCheck({ isUpdateAvailable: true, updateInfo: rolledOutUpdate }); - - await service.checkForAppUpdate({ - currentVersion: "1.2.3", - releaseChannel: "stable", - intent: "manual", - }); - runtime.failRuntime(new Error("sha512 checksum mismatch")); + const newerUpdate = { ...rolledOutUpdate, version: "1.2.5" }; + runtime.nextCheck({ isUpdateAvailable: true, updateInfo: newerUpdate }); const result = await service.checkForAppUpdate({ currentVersion: "1.2.3", releaseChannel: "stable", @@ -375,13 +662,43 @@ describe("app update service", () => { hasUpdate: true, readyToInstall: false, currentVersion: "1.2.3", - latestVersion: "1.2.4", + latestVersion: "1.2.5", body: null, date: "2026-04-28T00:00:00.000Z", - errorMessage: "sha512 checksum mismatch", + errorMessage: null, }); }); + it("attributes a late preparation failure to the download that started it", async () => { + const { runtime, service } = createService({ bucket: async () => 0 }); + runtime.nextCheck({ isUpdateAvailable: true, updateInfo: rolledOutUpdate }); + await service.checkForAppUpdate({ + currentVersion: "1.2.3", + releaseChannel: "stable", + intent: "automatic", + }); + runtime.prepareUpdate(rolledOutUpdate); + + const newerUpdate = { ...rolledOutUpdate, version: "1.2.5" }; + runtime.nextCheck({ isUpdateAvailable: true, updateInfo: newerUpdate }); + await service.checkForAppUpdate({ + currentVersion: "1.2.3", + releaseChannel: "stable", + intent: "automatic", + }); + runtime.failRuntime(new Error("old download failed")); + + runtime.nextCheck({ isUpdateAvailable: true, updateInfo: newerUpdate }); + const result = await service.checkForAppUpdate({ + currentVersion: "1.2.3", + releaseChannel: "stable", + intent: "automatic", + }); + + expect(result.latestVersion).toBe("1.2.5"); + expect(result.errorMessage).toBeNull(); + }); + it("performs a fresh manual check after an update preparation error", async () => { const { runtime, service } = createService(); runtime.nextCheck({ isUpdateAvailable: true, updateInfo: rolledOutUpdate }); @@ -391,6 +708,7 @@ describe("app update service", () => { releaseChannel: "stable", intent: "manual", }); + runtime.prepareUpdate(rolledOutUpdate); runtime.failRuntime(new Error("sha512 checksum mismatch")); runtime.nextCheck({ @@ -445,91 +763,4 @@ describe("app update service", () => { errorMessage: null, }); }); - - it("returns runtime update errors to multiple automatic checks before a manual retry clears them", async () => { - const { runtime, service } = createService(); - runtime.nextCheck({ isUpdateAvailable: true, updateInfo: rolledOutUpdate }); - - await service.checkForAppUpdate({ - currentVersion: "1.2.3", - releaseChannel: "stable", - intent: "manual", - }); - runtime.failRuntime(new Error("sha512 checksum mismatch")); - - const firstAutomaticResult = await service.checkForAppUpdate({ - currentVersion: "1.2.3", - releaseChannel: "stable", - intent: "automatic", - }); - const secondAutomaticResult = await service.checkForAppUpdate({ - currentVersion: "1.2.3", - releaseChannel: "stable", - intent: "automatic", - }); - - expect(firstAutomaticResult).toEqual({ - hasUpdate: true, - readyToInstall: false, - currentVersion: "1.2.3", - latestVersion: "1.2.4", - body: null, - date: "2026-04-28T00:00:00.000Z", - errorMessage: "sha512 checksum mismatch", - }); - expect(secondAutomaticResult).toEqual(firstAutomaticResult); - - runtime.nextCheck(null); - const retryResult = await service.checkForAppUpdate({ - currentVersion: "1.2.3", - releaseChannel: "stable", - intent: "manual", - }); - - expect(runtime.checkCount).toBe(2); - expect(retryResult).toEqual({ - hasUpdate: false, - readyToInstall: false, - currentVersion: "1.2.3", - latestVersion: "1.2.3", - body: null, - date: null, - errorMessage: null, - }); - }); - - it("keeps runtime update errors visible after a manual retry fails", async () => { - const { runtime, service } = createService(); - runtime.nextCheck({ isUpdateAvailable: true, updateInfo: rolledOutUpdate }); - - await service.checkForAppUpdate({ - currentVersion: "1.2.3", - releaseChannel: "stable", - intent: "manual", - }); - runtime.failRuntime(new Error("sha512 checksum mismatch")); - - runtime.failNextCheck(new Error("network down")); - const retryResult = await service.checkForAppUpdate({ - currentVersion: "1.2.3", - releaseChannel: "stable", - intent: "manual", - }); - const automaticResult = await service.checkForAppUpdate({ - currentVersion: "1.2.3", - releaseChannel: "stable", - intent: "automatic", - }); - - expect(retryResult.errorMessage).toBe("network down"); - expect(automaticResult).toEqual({ - hasUpdate: true, - readyToInstall: false, - currentVersion: "1.2.3", - latestVersion: "1.2.4", - body: null, - date: "2026-04-28T00:00:00.000Z", - errorMessage: "sha512 checksum mismatch", - }); - }); }); diff --git a/packages/desktop/src/features/app-update-service.ts b/packages/desktop/src/features/app-update-service.ts index f62dd8167..d5dc513ef 100644 --- a/packages/desktop/src/features/app-update-service.ts +++ b/packages/desktop/src/features/app-update-service.ts @@ -62,6 +62,11 @@ export interface AppUpdateService { }, onBeforeQuit?: () => Promise, ): Promise; + installUpdateOnQuit(input: { + currentVersion: string; + releaseChannel: AppReleaseChannel; + signal: AbortSignal; + }): Promise; } export interface AppUpdateServiceDeps { @@ -96,10 +101,16 @@ function buildCheckResult(input: { async function performQuitAndInstall( runtime: AppUpdateRuntime, - onBeforeQuit?: () => Promise, + { + onBeforeQuit, + restart, + }: { + onBeforeQuit?: () => Promise; + restart: boolean; + }, ): Promise { if (onBeforeQuit) await onBeforeQuit(); - runtime.quitAndInstall(/* isSilent */ false, /* isForceRunAfter */ true); + runtime.quitAndInstall(/* isSilent */ !restart, /* isForceRunAfter */ restart); } function getErrorMessage(error: unknown): string { @@ -109,13 +120,21 @@ function getErrorMessage(error: unknown): string { return String(error); } +function buildDeferredInstallResult(currentVersion: string): AppUpdateInstallResult { + return { + installed: false, + version: currentVersion, + message: "Update validation timed out. The update will be installed later.", + }; +} + export function createAppUpdateService(deps: AppUpdateServiceDeps): AppUpdateService { let cachedUpdateInfo: RuntimeUpdateInfo | null = null; let downloadedUpdateVersion: string | null = null; - let downloading = false; let configuredReleaseChannel: AppReleaseChannel | null = null; - let runtimeErrorMessage: string | null = null; - let inFlightUpdateCheckCount = 0; + let preparationError: { version: string; message: string } | null = null; + let preparingUpdateVersion: string | null = null; + let checkQueue: Promise = Promise.resolve(); function isReadyToInstallVersion(version: string): boolean { return downloadedUpdateVersion === version; @@ -124,23 +143,8 @@ export function createAppUpdateService(deps: AppUpdateServiceDeps): AppUpdateSer function clearUpdateState(): void { cachedUpdateInfo = null; downloadedUpdateVersion = null; - downloading = false; - runtimeErrorMessage = null; - } - - function buildRuntimeErrorResult(currentVersion: string): AppUpdateCheckResult | null { - if (!runtimeErrorMessage) { - return null; - } - - const info = cachedUpdateInfo; - return buildCheckResult({ - currentVersion, - hasUpdate: info?.version !== undefined && info.version !== currentVersion, - readyToInstall: false, - info, - errorMessage: runtimeErrorMessage, - }); + preparationError = null; + preparingUpdateVersion = null; } function configureRuntime(releaseChannel: AppReleaseChannel, intent: AppUpdateCheckIntent): void { @@ -166,28 +170,47 @@ export function createAppUpdateService(deps: AppUpdateServiceDeps): AppUpdateSer const alreadyReady = downloadedUpdateVersion === info.version; cachedUpdateInfo = info; downloadedUpdateVersion = alreadyReady ? info.version : null; - downloading = !alreadyReady; - runtimeErrorMessage = null; + if (!alreadyReady && preparingUpdateVersion === null) { + preparingUpdateVersion = info.version; + } }, onUpdateDownloaded(info) { - cachedUpdateInfo = info; + // A superseded download can finish after a newer manifest check. Keep + // the validated manifest as the install target in that case. + cachedUpdateInfo ??= info; downloadedUpdateVersion = info.version; - downloading = false; - runtimeErrorMessage = null; + if (preparingUpdateVersion === info.version) { + preparingUpdateVersion = null; + } + if (preparationError?.version === info.version) { + preparationError = null; + } }, onUpdateNotAvailable() { clearUpdateState(); }, onError(error) { - downloading = false; - if (inFlightUpdateCheckCount === 0 || cachedUpdateInfo) { - runtimeErrorMessage = getErrorMessage(error); + if (preparingUpdateVersion) { + preparationError = { + version: preparingUpdateVersion, + message: getErrorMessage(error), + }; + preparingUpdateVersion = null; } deps.reportRuntimeError?.(error); }, }); } + function runCheckExclusively(check: () => Promise): Promise { + const result = checkQueue.then(check, check); + checkQueue = result.then( + () => undefined, + () => undefined, + ); + return result; + } + async function checkForAppUpdate({ currentVersion, releaseChannel, @@ -205,73 +228,56 @@ export function createAppUpdateService(deps: AppUpdateServiceDeps): AppUpdateSer }); } - configureRuntime(releaseChannel, intent); + return runCheckExclusively(async () => { + configureRuntime(releaseChannel, intent); - const runtimeErrorResult = buildRuntimeErrorResult(currentVersion); - if (runtimeErrorResult && intent === "automatic") { - return runtimeErrorResult; - } + try { + const result = await deps.runtime.checkForUpdates(); + if (!result || !result.updateInfo || !result.isUpdateAvailable) { + clearUpdateState(); + return buildCheckResult({ + currentVersion, + hasUpdate: false, + readyToInstall: false, + }); + } - const cachedVersion = cachedUpdateInfo?.version ?? null; - if ( - !runtimeErrorResult && - intent === "automatic" && - cachedVersion && - cachedVersion !== currentVersion - ) { - return buildCheckResult({ - currentVersion, - hasUpdate: true, - readyToInstall: isReadyToInstallVersion(cachedVersion), - info: cachedUpdateInfo, - }); - } + const info = result.updateInfo; + const latestVersion = info.version; + const hasUpdate = latestVersion !== currentVersion; + + if (hasUpdate) { + cachedUpdateInfo = info; + const errorMessage = + preparationError?.version === latestVersion ? preparationError.message : null; + if (!errorMessage) { + preparationError = null; + } + return buildCheckResult({ + currentVersion, + hasUpdate: true, + readyToInstall: isReadyToInstallVersion(latestVersion), + info, + errorMessage, + }); + } - try { - inFlightUpdateCheckCount += 1; - const result = await deps.runtime.checkForUpdates(); - if (!result || !result.updateInfo || !result.isUpdateAvailable) { clearUpdateState(); return buildCheckResult({ currentVersion, hasUpdate: false, readyToInstall: false, }); - } - - const info = result.updateInfo; - const latestVersion = info.version; - const hasUpdate = latestVersion !== currentVersion; - - if (hasUpdate) { - cachedUpdateInfo = info; - downloading = !isReadyToInstallVersion(latestVersion); - runtimeErrorMessage = null; + } catch (error) { + deps.reportCheckError?.(error); return buildCheckResult({ currentVersion, - hasUpdate: true, - readyToInstall: isReadyToInstallVersion(latestVersion), - info, + hasUpdate: false, + readyToInstall: false, + errorMessage: getErrorMessage(error), }); } - - clearUpdateState(); - return buildCheckResult({ - currentVersion, - hasUpdate: false, - readyToInstall: false, - }); - } catch (error) { - deps.reportCheckError?.(error); - return buildCheckResult({ - currentVersion, - hasUpdate: false, - readyToInstall: false, - errorMessage: getErrorMessage(error), - }); - } finally { - inFlightUpdateCheckCount -= 1; - } + }); } async function downloadAndInstallUpdate( @@ -292,6 +298,69 @@ export function createAppUpdateService(deps: AppUpdateServiceDeps): AppUpdateSer }; } + const check = await checkForAppUpdate({ + currentVersion, + releaseChannel, + intent: "manual", + }); + if (!check.hasUpdate) { + return { + installed: false, + version: currentVersion, + message: check.errorMessage ?? "No update available.", + }; + } + + return installCachedUpdate(currentVersion, { onBeforeQuit, restart: true }); + } + + async function ensureUpdateDownloaded( + readyVersion: string, + signal?: AbortSignal, + ): Promise<"ready" | "aborted" | "superseded"> { + while (!isReadyToInstallVersion(readyVersion)) { + if (signal?.aborted) return "aborted"; + if (cachedUpdateInfo?.version !== readyVersion) return "superseded"; + + const attemptedVersion: string = preparingUpdateVersion ?? readyVersion; + preparingUpdateVersion ??= readyVersion; + try { + await deps.runtime.downloadUpdate(); + } catch (error) { + if ( + attemptedVersion !== readyVersion && + cachedUpdateInfo?.version === readyVersion && + !signal?.aborted + ) { + continue; + } + throw error; + } + + // electron-updater can return an older, already-running download. Its + // event clears that version, then the next iteration starts the newly + // validated release instead of treating the stale artifact as ready. + if (attemptedVersion === readyVersion && !isReadyToInstallVersion(readyVersion)) { + downloadedUpdateVersion = readyVersion; + preparingUpdateVersion = null; + } + } + + return signal?.aborted ? "aborted" : "ready"; + } + + async function installCachedUpdate( + currentVersion: string, + { + onBeforeQuit, + signal, + restart, + }: { + onBeforeQuit?: () => Promise; + signal?: AbortSignal; + restart: boolean; + }, + ): Promise { if (!cachedUpdateInfo) { return { installed: false, @@ -300,11 +369,13 @@ export function createAppUpdateService(deps: AppUpdateServiceDeps): AppUpdateSer }; } - configureRuntime(releaseChannel, "manual"); - const readyVersion = cachedUpdateInfo.version; + if (signal?.aborted) { + return buildDeferredInstallResult(currentVersion); + } + if (isReadyToInstallVersion(readyVersion)) { - await performQuitAndInstall(deps.runtime, onBeforeQuit); + await performQuitAndInstall(deps.runtime, { onBeforeQuit, restart }); return { installed: true, version: readyVersion, @@ -312,21 +383,19 @@ export function createAppUpdateService(deps: AppUpdateServiceDeps): AppUpdateSer }; } - if (downloading) { - return { - installed: false, - version: currentVersion, - message: "Update is still being prepared. Try again in a moment.", - }; - } - - downloading = true; - try { - await deps.runtime.downloadUpdate(); - downloadedUpdateVersion = readyVersion; - downloading = false; - await performQuitAndInstall(deps.runtime, onBeforeQuit); + const preparation = await ensureUpdateDownloaded(readyVersion, signal); + if (preparation === "aborted") { + return buildDeferredInstallResult(currentVersion); + } + if (preparation === "superseded") { + return { + installed: false, + version: currentVersion, + message: "A newer update was found and will be installed later.", + }; + } + await performQuitAndInstall(deps.runtime, { onBeforeQuit, restart }); return { installed: true, @@ -334,7 +403,6 @@ export function createAppUpdateService(deps: AppUpdateServiceDeps): AppUpdateSer message: "Update downloaded. The app will restart shortly.", }; } catch (error) { - downloading = false; const message = error instanceof Error ? error.message : String(error); deps.reportInstallError?.(message); return { @@ -345,8 +413,35 @@ export function createAppUpdateService(deps: AppUpdateServiceDeps): AppUpdateSer } } + async function installUpdateOnQuit({ + currentVersion, + releaseChannel, + signal, + }: { + currentVersion: string; + releaseChannel: AppReleaseChannel; + signal: AbortSignal; + }): Promise { + if (!deps.isPackaged() || !downloadedUpdateVersion) { + return false; + } + + const check = await checkForAppUpdate({ + currentVersion, + releaseChannel, + intent: "automatic", + }); + if (signal.aborted || !check.hasUpdate) { + return false; + } + + const result = await installCachedUpdate(currentVersion, { signal, restart: false }); + return result.installed; + } + return { checkForAppUpdate, downloadAndInstallUpdate, + installUpdateOnQuit, }; } diff --git a/packages/desktop/src/features/auto-updater.test.ts b/packages/desktop/src/features/auto-updater.test.ts index 4de12d87d..3c28e61c1 100644 --- a/packages/desktop/src/features/auto-updater.test.ts +++ b/packages/desktop/src/features/auto-updater.test.ts @@ -19,15 +19,15 @@ import { resolveStagingUserId, rolloutManifestSchema, shouldAdmitToRollout, - shouldAutoInstallOnQuit, + shouldInstallAppUpdateOnQuit, } from "./auto-updater"; -describe("shouldAutoInstallOnQuit", () => { - it("auto-installs on quit everywhere except Linux AppImage", () => { - expect(shouldAutoInstallOnQuit({ platform: "linux", isAppImage: true })).toBe(false); - expect(shouldAutoInstallOnQuit({ platform: "linux", isAppImage: false })).toBe(true); - expect(shouldAutoInstallOnQuit({ platform: "darwin", isAppImage: false })).toBe(true); - expect(shouldAutoInstallOnQuit({ platform: "win32", isAppImage: false })).toBe(true); +describe("shouldInstallAppUpdateOnQuit", () => { + it("keeps Linux AppImage updates on the manual install path", () => { + expect(shouldInstallAppUpdateOnQuit({ platform: "linux", isAppImage: true })).toBe(false); + expect(shouldInstallAppUpdateOnQuit({ platform: "linux", isAppImage: false })).toBe(true); + expect(shouldInstallAppUpdateOnQuit({ platform: "darwin", isAppImage: false })).toBe(true); + expect(shouldInstallAppUpdateOnQuit({ platform: "win32", isAppImage: false })).toBe(true); }); }); diff --git a/packages/desktop/src/features/auto-updater.ts b/packages/desktop/src/features/auto-updater.ts index 876114585..c9b9a983b 100644 --- a/packages/desktop/src/features/auto-updater.ts +++ b/packages/desktop/src/features/auto-updater.ts @@ -76,18 +76,12 @@ export function getStagingUserId(): Promise { return cachedStagingUserIdPromise; } -// AppImages have no install step. electron-updater "installs" by unlinking the -// running file and mv-ing the downloaded one into place; on app quit it does this -// via a *blocking* execFileSync(newAppImage, { APPIMAGE_EXIT_AFTER_INSTALL: "true" }). -// That env var is only honored by AppImageLauncher, so without it the freshly -// launched process boots the full app and never exits — the quit hangs forever, -// with the old binary already deleted. We therefore install AppImages only on -// explicit quitAndInstall (the "Update now" button), which takes the non-blocking -// spawn path. Every other target keeps auto-install-on-quit, which works there. -export function shouldAutoInstallOnQuit(input: { +export function shouldInstallAppUpdateOnQuit(input: { platform: NodeJS.Platform; isAppImage: boolean; }): boolean { + // AppImage's no-relaunch install path blocks while launching the replacement + // binary, which can hang after the running file has already been replaced. return !(input.platform === "linux" && input.isAppImage); } @@ -97,10 +91,10 @@ class ElectronAppUpdateRuntime implements AppUpdateRuntime { configure(input: AppUpdateRuntimeConfiguration): void { autoUpdater.autoDownload = true; autoUpdater.autoRunAppAfterInstall = true; - autoUpdater.autoInstallOnAppQuit = shouldAutoInstallOnQuit({ - platform: process.platform, - isAppImage: Boolean(process.env.APPIMAGE), - }); + // Paseo revalidates the current manifest before explicitly installing on quit. + // Electron's built-in handler would install an older download without checking + // whether a newer release has superseded it. + autoUpdater.autoInstallOnAppQuit = false; autoUpdater.allowPrerelease = input.releaseChannel === "beta"; autoUpdater.channel = input.releaseChannel === "beta" ? "beta" : "latest"; autoUpdater.allowDowngrade = false; @@ -143,6 +137,7 @@ class ElectronAppUpdateRuntime implements AppUpdateRuntime { } quitAndInstall(isSilent: boolean, isForceRunAfter: boolean): void { + autoUpdater.autoRunAppAfterInstall = isForceRunAfter; autoUpdater.quitAndInstall(isSilent, isForceRunAfter); } } @@ -194,3 +189,24 @@ export async function downloadAndInstallUpdate( onBeforeQuit, ); } + +export async function installAppUpdateOnQuit({ + currentVersion, + releaseChannel, + signal, +}: { + currentVersion: string; + releaseChannel: AppReleaseChannel; + signal: AbortSignal; +}): Promise { + if ( + !shouldInstallAppUpdateOnQuit({ + platform: process.platform, + isAppImage: Boolean(process.env.APPIMAGE), + }) + ) { + return false; + } + + return appUpdateService.installUpdateOnQuit({ currentVersion, releaseChannel, signal }); +} diff --git a/packages/desktop/src/main.ts b/packages/desktop/src/main.ts index a75eb7106..ef4e8a82e 100644 --- a/packages/desktop/src/main.ts +++ b/packages/desktop/src/main.ts @@ -12,6 +12,7 @@ import { existsSync } from "node:fs"; import { execFileSync } from "node:child_process"; import { app, + autoUpdater as electronAutoUpdater, BrowserWindow, clipboard, Menu, @@ -81,19 +82,21 @@ import { stopDesktopDaemonViaCli, } from "./daemon/daemon-manager.js"; import { - createBeforeQuitHandler, + createQuitLifecycle, stopDesktopManagedDaemonOnQuitIfNeeded, } from "./daemon/quit-lifecycle.js"; import { runDesktopStartup } from "./desktop-startup.js"; import { autoUpdateInstalledSkills } from "./integrations/skills/index.js"; import { registerBrowserAutomationIpc } from "./features/browser-automation/ipc.js"; import { BrowserKeyboard } from "./features/browser-keyboard/index.js"; +import { installAppUpdateOnQuit } from "./features/auto-updater.js"; const DEV_SERVER_URL = process.env.EXPO_DEV_URL ?? "http://localhost:8081"; const APP_SCHEME = "paseo"; const PASEO_DEBUG = process.env.PASEO_DEBUG === "1"; const DISABLE_SINGLE_INSTANCE_LOCK = process.env.PASEO_DISABLE_SINGLE_INSTANCE_LOCK === "1"; const APP_NAME = process.env.PASEO_TEST_APP_NAME?.trim() || "Paseo"; +const UPDATE_QUIT_DEADLINE_MS = 5_000; const pendingBrowserWindowOpenRequests = new PendingBrowserWindowOpenRequests(); app.setName(APP_NAME); @@ -928,23 +931,36 @@ function showDaemonShutdownDialog(): void { } } -app.on( - "before-quit", - createBeforeQuitHandler({ - app, - closeTransportSessions: closeAllTransportSessions, - stopDesktopManagedDaemonIfNeeded: () => - stopDesktopManagedDaemonOnQuitIfNeeded({ - settingsStore: getDesktopSettingsStore(), - isDesktopManagedDaemonRunning: isDesktopManagedDaemonRunningSync, - stopDaemon: () => stopDesktopDaemonViaCli("quit"), - showShutdownFeedback: showDaemonShutdownDialog, - }), - onStopError: (error) => { - log.error("[desktop daemon] failed to stop managed daemon on quit", error); - }, - }), -); +const quitLifecycle = createQuitLifecycle({ + app, + closeTransportSessions: closeAllTransportSessions, + stopDesktopManagedDaemonIfNeeded: () => + stopDesktopManagedDaemonOnQuitIfNeeded({ + settingsStore: getDesktopSettingsStore(), + isDesktopManagedDaemonRunning: isDesktopManagedDaemonRunningSync, + stopDaemon: () => stopDesktopDaemonViaCli("quit"), + showShutdownFeedback: showDaemonShutdownDialog, + }), + installAppUpdateOnQuit: async (signal) => { + const settings = await getDesktopSettingsStore().get(); + return installAppUpdateOnQuit({ + currentVersion: app.getVersion(), + releaseChannel: settings.releaseChannel, + signal, + }); + }, + createUpdateDeadlineSignal: () => AbortSignal.timeout(UPDATE_QUIT_DEADLINE_MS), + onStopError: (error) => { + log.error("[desktop daemon] failed to stop managed daemon on quit", error); + }, + onUpdateError: (error) => { + log.error("[auto-updater] failed to validate downloaded update on quit", error); + }, +}); + +// electron-updater forwards this event through Electron's built-in autoUpdater. +electronAutoUpdater.on("before-quit-for-update", quitLifecycle.handleBeforeQuitForUpdate); +app.on("before-quit", quitLifecycle.handleBeforeQuit); app.on("window-all-closed", () => { if (process.platform !== "darwin") { From bce2c50b9e16831ab395e1ea313698ba4dfa0b22 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Fri, 17 Jul 2026 10:07:50 +0200 Subject: [PATCH 02/38] Keep terminal sizing reliable through focus changes (#2154) * fix(terminal): retry size claims until sent A focus resize could consume its once-per-focus latch before the renderer handed the size to the client. Keep the claim pending across visibility, connection, and renderer readiness changes, and only commit it after the resize is sent. * fix(terminal): keep disconnected claims retryable A retained runtime client can outlive its active connection. Require the connection at delivery time so a delayed resize callback cannot commit a claim that was never sent. * test(terminal): await PTY resize observation Prove the terminal starts at 80x24 while blurred, then probe through the daemon until its own reported size matches xterm after focus returns. This avoids racing a one-shot stty sample against the asynchronous resize claim. --- packages/app/e2e/terminal-stuck-size.spec.ts | 210 +++--------------- .../terminal-pane-focus-claim.test.ts | 134 +++++++++++ .../components/terminal-pane-focus-claim.ts | 73 ++++++ .../terminal-pane-focus-latch.test.ts | 92 -------- .../components/terminal-pane-focus-latch.ts | 33 --- packages/app/src/components/terminal-pane.tsx | 98 +++++--- .../terminal-emulator-runtime.browser.test.ts | 15 +- .../terminal-mount-claim.browser.test.ts | 159 ------------- 8 files changed, 320 insertions(+), 494 deletions(-) create mode 100644 packages/app/src/components/terminal-pane-focus-claim.test.ts create mode 100644 packages/app/src/components/terminal-pane-focus-claim.ts delete mode 100644 packages/app/src/components/terminal-pane-focus-latch.test.ts delete mode 100644 packages/app/src/components/terminal-pane-focus-latch.ts delete mode 100644 packages/app/src/terminal/runtime/terminal-mount-claim.browser.test.ts diff --git a/packages/app/e2e/terminal-stuck-size.spec.ts b/packages/app/e2e/terminal-stuck-size.spec.ts index ebdfd0110..9c498e44a 100644 --- a/packages/app/e2e/terminal-stuck-size.spec.ts +++ b/packages/app/e2e/terminal-stuck-size.spec.ts @@ -1,5 +1,6 @@ import { test, expect, type Page } from "./fixtures"; import { TerminalE2EHarness } from "./helpers/terminal-dsl"; +import { getTerminalBufferText } from "./helpers/terminal-perf"; import { buildHostWorkspaceRoute } from "../src/utils/host-routes"; import { getServerId } from "./helpers/server-id"; @@ -8,18 +9,14 @@ import { getServerId } from "./helpers/server-id"; * once focus returns. * * The PTY is only ever resized by an explicit client claim (`terminal_input` resize). A freshly - * mounted terminal produces exactly one claim, from terminal-pane's pane-focus reflow effect. If - * that claim is emitted while the app is not actively visible (window blurred / document hidden), - * `handleTerminalResize` drops it — and nothing used to re-send it, leaving the PTY at 80x24 - * while xterm renders the real pane size: vim/top squeezed into a corner until the user resizes - * the window. + * mounted terminal starts its claim from terminal-pane's pane-focus reflow effect. Previously, if + * that claim was emitted while the app was not actively visible, `handleTerminalResize` dropped + * it without a retry, leaving the PTY at 80x24 while xterm rendered the real pane size. * * The repro is the mundane user flow: with a workspace already showing a terminal, open another * one and switch to a different app while it spawns. We stage the blur deterministically by * stubbing `document.hasFocus()` (which `useAppVisible` consults on window focus/blur events) - * instead of racing real OS focus. The first terminal matters: `useAppVisible` only observes - * focus events while a consumer is mounted, so it is what makes the blur visible to the app — - * exactly as in the real flow. + * instead of racing real OS focus. * * The assertion reads the PTY's own opinion of its size (`stty size`) with input written * daemon-side — clicking or typing in the pane would fire the focus-claim path and mask the bug. @@ -58,31 +55,6 @@ async function readRenderedTerminalSize(page: Page): Promise { - return await page.evaluate(() => { - const terminal = ( - window as Window & { - __paseoTerminal?: { - buffer: { - active: { - length: number; - getLine(index: number): { translateToString(trim: boolean): string } | undefined; - }; - }; - }; - } - ).__paseoTerminal; - if (!terminal) { - return ""; - } - const lines: string[] = []; - for (let index = 0; index < terminal.buffer.active.length; index += 1) { - lines.push(terminal.buffer.active.getLine(index)?.translateToString(true) ?? ""); - } - return lines.join("\n"); - }); -} - async function createTerminalViaMenu(page: Page): Promise { await page.getByTestId("workspace-new-tab-menu-trigger").click(); await page.getByTestId("workspace-new-tab-menu-terminal").click(); @@ -112,116 +84,10 @@ function requireTerminalSize(size: RenderedTerminalSize | null): RenderedTermina return size; } -function claimsFor(frames: ResizeClaim[], terminalId: string): ResizeClaim[] { - return frames.filter((frame) => frame.terminalId === terminalId); -} - -function parseSttySize(bufferText: string): RenderedTerminalSize { - const match = /S=(\d+) (\d+)=/.exec(bufferText); - if (!match?.[1] || !match[2]) { - throw new Error(`stty size did not print in the terminal buffer:\n${bufferText}`); - } - return { rows: Number(match[1]), cols: Number(match[2]) }; -} - -interface ResizeClaim { - terminalId: string; - rows: number; - cols: number; -} - -/** - * Records every resize claim the app puts on the wire. Claims travel two ways: as a JSON - * `terminal_input` before the stream has a binary slot, and as a binary frame - * ([opcode 0x03][slot][JSON {rows, cols}]) once subscribed. The slot -> terminal mapping comes - * from subscribe_terminal_response on the receive side. - */ -function captureResizeClaims(page: Page): ResizeClaim[] { - const resizeFrames: ResizeClaim[] = []; - const slotTerminals = new Map(); - const unmappedBinaryResizes: Array<{ slot: number; rows: number; cols: number }> = []; - - const recordBinaryResize = (slot: number, size: { rows: number; cols: number }) => { - const terminalId = slotTerminals.get(slot); - if (terminalId) { - resizeFrames.push({ terminalId, ...size }); - } else { - unmappedBinaryResizes.push({ slot, ...size }); - } - }; - - const handleSentFrame = (frame: { payload: string | Buffer }) => { - const payload = frame.payload; - if (typeof payload !== "string") { - if (payload.length >= 2 && payload[0] === 0x03) { - try { - const parsed = JSON.parse(payload.subarray(2).toString("utf8")) as { - rows?: number; - cols?: number; - }; - if (parsed.rows !== undefined && parsed.cols !== undefined) { - recordBinaryResize(payload[1] ?? -1, { rows: parsed.rows, cols: parsed.cols }); - } - } catch { - // not a terminal resize frame — ignore - } - } - return; - } - if (!payload.includes("resize")) { - return; - } - try { - const outer = JSON.parse(payload) as { message?: Record }; - const message = outer.message ?? {}; - if (message.type !== "terminal_input") { - return; - } - const inner = message.message as { type?: string; rows?: number; cols?: number }; - if (inner?.type === "resize" && inner.rows !== undefined && inner.cols !== undefined) { - resizeFrames.push({ - terminalId: String(message.terminalId ?? ""), - rows: inner.rows, - cols: inner.cols, - }); - } - } catch { - // not JSON — ignore - } - }; - - const handleReceivedFrame = (frame: { payload: string | Buffer }) => { - const payload = frame.payload; - if (typeof payload !== "string" || !payload.includes("subscribe_terminal_response")) { - return; - } - try { - const outer = JSON.parse(payload) as { message?: Record }; - const message = outer.message ?? {}; - if (message.type !== "subscribe_terminal_response") { - return; - } - const responsePayload = (message.payload ?? {}) as { terminalId?: string; slot?: number }; - if ( - typeof responsePayload.terminalId === "string" && - typeof responsePayload.slot === "number" - ) { - slotTerminals.set(responsePayload.slot, responsePayload.terminalId); - for (const entry of unmappedBinaryResizes.splice(0)) { - recordBinaryResize(entry.slot, { rows: entry.rows, cols: entry.cols }); - } - } - } catch { - // not JSON — ignore - } - }; - - page.on("websocket", (ws) => { - ws.on("framesent", handleSentFrame); - ws.on("framereceived", handleReceivedFrame); - }); - - return resizeFrames; +function parseLatestSttySize(bufferText: string): RenderedTerminalSize | null { + const matches = [...bufferText.matchAll(/S\d+=(\d+) (\d+)=/g)]; + const match = matches.at(-1); + return match?.[1] && match[2] ? { rows: Number(match[1]), cols: Number(match[2]) } : null; } test.describe("terminal PTY size claim under lost window focus", () => { @@ -239,16 +105,14 @@ test.describe("terminal PTY size claim under lost window focus", () => { page, }) => { await page.setViewportSize({ width: 1280, height: 900 }); - const resizeFrames = captureResizeClaims(page); await page.goto(buildHostWorkspaceRoute(getServerId(), harness.workspaceId)); await expect(page.getByTestId("workspace-new-tab-menu-trigger")).toBeVisible({ timeout: 30_000, }); - // A terminal is already open — this also keeps useAppVisible's listeners mounted so the - // upcoming blur is actually observed by the app. Wait for the daemon to know about it rather - // than sleeping: if it were missing from this snapshot it would be misread as "new" below. + // Wait for the daemon to know about the first terminal rather than sleeping: if it were + // missing from this snapshot it would be misread as "new" below. await createTerminalViaMenu(page); await expect(harness.terminalSurface(page).first()).toBeVisible({ timeout: 30_000 }); const countTerminals = async () => (await listTerminalIds(harness)).length; @@ -265,14 +129,19 @@ test.describe("terminal PTY size claim under lost window focus", () => { await expect.poll(async () => (await findNewTerminalIds()).length, { timeout: 15_000 }).toBe(1); const newTerminalId = exactlyOne(await findNewTerminalIds(), "new terminal"); - // Let every mount-time refit run (the ladder goes out to 2s) while the window stays blurred. - // This one has to be a wait, not a poll: the assertion is that nothing happens. + // Keep the app blurred through the complete mount-time refit ladder, which runs out to 2s. await page.waitForTimeout(2_500); - // Nothing may reach the daemon while the app is hidden — handleTerminalResize drops claims - // that fire while !isAppVisible. Without this, deleting that gate would still leave the test - // green: the claim would simply arrive early instead of after focus returns. - expect(claimsFor(resizeFrames, newTerminalId)).toEqual([]); + await harness.client.subscribeTerminal(newTerminalId); + + // Confirm the PTY is still at its spawn size before focus returns. + harness.client.sendTerminalInput(newTerminalId, { + type: "input", + data: 'echo "S0=$(stty size)="\n', + }); + await expect + .poll(async () => getTerminalBufferText(page), { timeout: 15_000 }) + .toMatch(/S0=24 80=/); // ...and comes back. await setWindowFocused(page, true); @@ -282,29 +151,20 @@ test.describe("terminal PTY size claim under lost window focus", () => { // Sanity: the pane really rendered at a desktop size, not the PTY default. expect(rendered.cols).toBeGreaterThan(80); - // The claim must reach the wire once focus is back. Poll for it rather than sleeping: this - // is the behavior under test, so waiting a fixed duration would trade a real assertion for - // a timing bet. - const claimsForNewTerminal = () => claimsFor(resizeFrames, newTerminalId); - await expect - .poll(claimsForNewTerminal, { timeout: 15_000 }) - .toContainEqual({ terminalId: newTerminalId, rows: rendered.rows, cols: rendered.cols }); - - // And the PTY itself must agree. Ask it via the daemon, never via the page: focusing or + // The PTY itself must agree. Ask it via the daemon, never via the page: focusing or // typing in the pane triggers the focus-claim path and would mask the bug. - await harness.client.subscribeTerminal(newTerminalId); - harness.client.sendTerminalInput(newTerminalId, { - type: "input", - data: 'echo "S=$(stty size)="\n', - }); - + let probe = 1; await expect - .poll(async () => readTerminalBufferText(page), { timeout: 15_000 }) - .toMatch(/S=\d+ \d+=/); - - const ptySize = parseSttySize(await readTerminalBufferText(page)); - - // The PTY must match what xterm rendered — not the daemon's 80x24 spawn default. - expect(ptySize).toEqual({ rows: rendered.rows, cols: rendered.cols }); + .poll( + async () => { + harness.client.sendTerminalInput(newTerminalId, { + type: "input", + data: `echo "S${probe++}=$(stty size)="\n`, + }); + return parseLatestSttySize(await getTerminalBufferText(page)); + }, + { timeout: 15_000, intervals: [100, 250, 500] }, + ) + .toEqual({ rows: rendered.rows, cols: rendered.cols }); }); }); diff --git a/packages/app/src/components/terminal-pane-focus-claim.test.ts b/packages/app/src/components/terminal-pane-focus-claim.test.ts new file mode 100644 index 000000000..67e540f52 --- /dev/null +++ b/packages/app/src/components/terminal-pane-focus-claim.test.ts @@ -0,0 +1,134 @@ +import { describe, expect, it } from "vitest"; +import { + EMPTY_FOCUS_CLAIM_STATE, + canRequestFocusClaim, + reconcileFocusClaim, + settleFocusClaim, + type FocusClaimState, +} from "./terminal-pane-focus-claim"; + +function request( + state: FocusClaimState, + input: { key: string | null; canRequest: boolean }, +): FocusClaimState { + return reconcileFocusClaim(state, input).state; +} + +describe("terminal pane focus claim", () => { + it("waits for both the client and renderer before requesting a claim", () => { + const withoutClient = canRequestFocusClaim({ + isWorkspaceFocused: true, + isAppVisible: true, + isClientReady: false, + isConnected: true, + isRendererReady: true, + }); + const withoutRenderer = canRequestFocusClaim({ + isWorkspaceFocused: true, + isAppVisible: true, + isClientReady: true, + isConnected: true, + isRendererReady: false, + }); + + expect([withoutClient, withoutRenderer]).toEqual([false, false]); + }); + + it("does not deliver a requested claim after the host disconnects", () => { + const disconnected = canRequestFocusClaim({ + isWorkspaceFocused: true, + isAppVisible: true, + isClientReady: true, + isConnected: false, + isRendererReady: true, + }); + + expect(disconnected).toBe(false); + }); + + it("claims once per continuous pane-focus period after send", () => { + const firstRequest = reconcileFocusClaim(EMPTY_FOCUS_CLAIM_STATE, { + key: "ws:term-1", + canRequest: true, + }); + const sent = settleFocusClaim(firstRequest.state, { + key: "ws:term-1", + sent: true, + }); + const repeated = reconcileFocusClaim(sent, { + key: "ws:term-1", + canRequest: true, + }); + + expect(firstRequest.shouldRequest).toBe(true); + expect(repeated).toEqual({ + state: { claimedKey: "ws:term-1", requestedKey: null }, + shouldRequest: false, + }); + }); + + it("defers until the claim can be requested", () => { + const unavailable = reconcileFocusClaim(EMPTY_FOCUS_CLAIM_STATE, { + key: "ws:term-1", + canRequest: false, + }); + const available = reconcileFocusClaim(unavailable.state, { + key: "ws:term-1", + canRequest: true, + }); + + expect(unavailable.shouldRequest).toBe(false); + expect(available.shouldRequest).toBe(true); + }); + + it("retries when readiness changes before the requested claim lands", () => { + const requested = request(EMPTY_FOCUS_CLAIM_STATE, { + key: "ws:term-1", + canRequest: true, + }); + const hiddenBeforeDelivery = request(requested, { + key: "ws:term-1", + canRequest: false, + }); + const visibleAgain = reconcileFocusClaim(hiddenBeforeDelivery, { + key: "ws:term-1", + canRequest: true, + }); + + expect(visibleAgain).toEqual({ + state: { claimedKey: null, requestedKey: "ws:term-1" }, + shouldRequest: true, + }); + }); + + it("retries a requested claim that was not sent", () => { + const requested = request(EMPTY_FOCUS_CLAIM_STATE, { + key: "ws:term-1", + canRequest: true, + }); + const dropped = settleFocusClaim(requested, { + key: "ws:term-1", + sent: false, + }); + const retry = reconcileFocusClaim(dropped, { + key: "ws:term-1", + canRequest: true, + }); + + expect(retry.shouldRequest).toBe(true); + }); + + it("re-arms after pane blur or terminal change", () => { + const requested = request(EMPTY_FOCUS_CLAIM_STATE, { + key: "ws:term-1", + canRequest: true, + }); + const sent = settleFocusClaim(requested, { key: "ws:term-1", sent: true }); + const blurred = request(sent, { key: null, canRequest: true }); + const refocused = reconcileFocusClaim(blurred, { key: "ws:term-1", canRequest: true }); + const changed = reconcileFocusClaim(sent, { key: "ws:term-2", canRequest: true }); + + expect(refocused.shouldRequest).toBe(true); + expect(changed.shouldRequest).toBe(true); + }); +}); diff --git a/packages/app/src/components/terminal-pane-focus-claim.ts b/packages/app/src/components/terminal-pane-focus-claim.ts new file mode 100644 index 000000000..03f9ec2fa --- /dev/null +++ b/packages/app/src/components/terminal-pane-focus-claim.ts @@ -0,0 +1,73 @@ +export interface FocusClaimState { + claimedKey: string | null; + requestedKey: string | null; +} + +export interface FocusClaimStep { + state: FocusClaimState; + shouldRequest: boolean; +} + +interface FocusClaimReadiness { + isWorkspaceFocused: boolean; + isAppVisible: boolean; + isClientReady: boolean; + isConnected: boolean; + isRendererReady: boolean; +} + +export const EMPTY_FOCUS_CLAIM_STATE: FocusClaimState = { + claimedKey: null, + requestedKey: null, +}; + +export function canRequestFocusClaim(input: FocusClaimReadiness): boolean { + return ( + input.isWorkspaceFocused && + input.isAppVisible && + input.isClientReady && + input.isConnected && + input.isRendererReady + ); +} + +export function reconcileFocusClaim( + state: FocusClaimState, + input: { key: string | null; canRequest: boolean }, +): FocusClaimStep { + if (input.key === null) { + return { state: EMPTY_FOCUS_CLAIM_STATE, shouldRequest: false }; + } + if (state.claimedKey === input.key) { + return { + state: { claimedKey: input.key, requestedKey: null }, + shouldRequest: false, + }; + } + if (!input.canRequest) { + return { + state: { claimedKey: state.claimedKey, requestedKey: null }, + shouldRequest: false, + }; + } + if (state.requestedKey === input.key) { + return { state, shouldRequest: false }; + } + return { + state: { claimedKey: state.claimedKey, requestedKey: input.key }, + shouldRequest: true, + }; +} + +export function settleFocusClaim( + state: FocusClaimState, + input: { key: string; sent: boolean }, +): FocusClaimState { + if (state.requestedKey !== input.key) { + return state; + } + return { + claimedKey: input.sent ? input.key : state.claimedKey, + requestedKey: null, + }; +} diff --git a/packages/app/src/components/terminal-pane-focus-latch.test.ts b/packages/app/src/components/terminal-pane-focus-latch.test.ts deleted file mode 100644 index 41dd60de2..000000000 --- a/packages/app/src/components/terminal-pane-focus-latch.test.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { resolveFocusLatchStep, type FocusLatchStep } from "./terminal-pane-focus-latch"; - -/** - * Pins the ordering that caused "terminal stuck at 80x24": the latch must record that the action - * RAN, not that the effect ran. A pane that mounts before its workspace has focus (or while the - * app is hidden) must still fire once readiness arrives — burning the latch on the attempt - * permanently disarms the only resize claim a fresh terminal ever sends. - */ - -function run(steps: Array<{ key: string | null; canFire: boolean }>): FocusLatchStep[] { - const results: FocusLatchStep[] = []; - let latchedKey: string | null = null; - for (const step of steps) { - const result = resolveFocusLatchStep({ ...step, latchedKey }); - latchedKey = result.latchedKey; - results.push(result); - } - return results; -} - -describe("resolveFocusLatchStep", () => { - it("fires immediately when the pane is focused and ready at mount", () => { - const [step] = run([{ key: "ws:term-1", canFire: true }]); - expect(step).toEqual({ latchedKey: "ws:term-1", fire: true }); - }); - - it("defers instead of burning the latch when readiness arrives late", () => { - // The bug: pane focused while the workspace is not (or the app is hidden). The old code - // latched on the first pass and never fired again. - const steps = run([ - { key: "ws:term-1", canFire: false }, // mount: pane focused, workspace not focused yet - { key: "ws:term-1", canFire: true }, // workspace focus / visibility arrives - ]); - expect(steps[0]).toEqual({ latchedKey: null, fire: false }); - expect(steps[1]).toEqual({ latchedKey: "ws:term-1", fire: true }); - }); - - it("fires exactly once per continuous pane-focus period", () => { - const steps = run([ - { key: "ws:term-1", canFire: true }, - { key: "ws:term-1", canFire: true }, // unrelated re-render - { key: "ws:term-1", canFire: false }, // workspace blurs... - { key: "ws:term-1", canFire: true }, // ...and refocuses: not a new pane-focus period - ]); - expect(steps).toEqual([ - { latchedKey: "ws:term-1", fire: true }, - { latchedKey: "ws:term-1", fire: false }, - { latchedKey: "ws:term-1", fire: false }, - { latchedKey: "ws:term-1", fire: false }, - ]); - }); - - it("re-arms when the pane blurs and fires again on refocus", () => { - const steps = run([ - { key: "ws:term-1", canFire: true }, - { key: null, canFire: true }, // pane blurred / terminal gone - { key: "ws:term-1", canFire: true }, - ]); - expect(steps).toEqual([ - { latchedKey: "ws:term-1", fire: true }, - { latchedKey: null, fire: false }, - { latchedKey: "ws:term-1", fire: true }, - ]); - }); - - it("fires for a different terminal in the same pane", () => { - const steps = run([ - { key: "ws:term-1", canFire: true }, - { key: "ws:term-2", canFire: true }, - ]); - expect(steps).toEqual([ - { latchedKey: "ws:term-1", fire: true }, - { latchedKey: "ws:term-2", fire: true }, - ]); - }); - - it("stays deferred across repeated not-ready passes, then fires once", () => { - const steps = run([ - { key: "ws:term-1", canFire: false }, - { key: "ws:term-1", canFire: false }, - { key: "ws:term-1", canFire: true }, - { key: "ws:term-1", canFire: true }, - ]); - expect(steps).toEqual([ - { latchedKey: null, fire: false }, - { latchedKey: null, fire: false }, - { latchedKey: "ws:term-1", fire: true }, - { latchedKey: "ws:term-1", fire: false }, - ]); - }); -}); diff --git a/packages/app/src/components/terminal-pane-focus-latch.ts b/packages/app/src/components/terminal-pane-focus-latch.ts deleted file mode 100644 index 9d3d6196b..000000000 --- a/packages/app/src/components/terminal-pane-focus-latch.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Once-per-pane-focus latch for terminal-pane's focus/reflow effects. - * - * Each effect wants to fire exactly once per continuous pane-focus period per terminal, but only - * while the action can actually land (workspace focused, and for resize claims the app visible — - * `handleTerminalResize` drops claims emitted while hidden and nothing re-sends them). The latch - * must therefore record "the action ran", never "we tried": latching before the readiness gate - * permanently disarms the effect when a pane mounts before its workspace has focus, which is how - * a terminal ends up rendering full-size while its PTY stays at the daemon's 80x24 default. - */ -export interface FocusLatchStep { - latchedKey: string | null; - fire: boolean; -} - -export function resolveFocusLatchStep(input: { - /** Identity of the pane-focus period; null resets the latch (pane blurred, no terminal). */ - key: string | null; - latchedKey: string | null; - /** Whether the action can land right now; while false the latch stays untouched. */ - canFire: boolean; -}): FocusLatchStep { - if (input.key === null) { - return { latchedKey: null, fire: false }; - } - if (input.latchedKey === input.key) { - return { latchedKey: input.latchedKey, fire: false }; - } - if (!input.canFire) { - return { latchedKey: input.latchedKey, fire: false }; - } - return { latchedKey: input.key, fire: true }; -} diff --git a/packages/app/src/components/terminal-pane.tsx b/packages/app/src/components/terminal-pane.tsx index 52955c206..3eb607857 100644 --- a/packages/app/src/components/terminal-pane.tsx +++ b/packages/app/src/components/terminal-pane.tsx @@ -21,7 +21,12 @@ import { resolvePendingModifierDataInput, } from "@/utils/terminal-keys"; import { getWorkspaceTerminalSession } from "@/terminal/runtime/workspace-terminal-session"; -import { resolveFocusLatchStep } from "./terminal-pane-focus-latch"; +import { + EMPTY_FOCUS_CLAIM_STATE, + canRequestFocusClaim, + reconcileFocusClaim, + settleFocusClaim, +} from "./terminal-pane-focus-claim"; import { TerminalStreamController, type TerminalStreamControllerStatus, @@ -222,7 +227,7 @@ export function TerminalPane({ const pendingTerminalInputRef = useRef([]); const keyboardRefitTimeoutsRef = useRef>>([]); const lastAutoFocusKeyRef = useRef(null); - const lastPaneFocusResizeKeyRef = useRef(null); + const paneFocusResizeClaimRef = useRef(EMPTY_FOCUS_CLAIM_STATE); const initialSnapshot = workspaceTerminalSession.snapshots.get({ terminalId }); useEffect(() => { @@ -261,40 +266,48 @@ export function TerminalPane({ ); useEffect(() => { - // Latch only when the focus actually ran: latching before the workspace-focus gate would - // permanently disarm this effect for panes that mount before their workspace has focus. - const step = resolveFocusLatchStep({ - key: isMobile || !isPaneFocused || !terminalId ? null : `${scopeKey}:${terminalId}`, - latchedKey: lastAutoFocusKeyRef.current, - canFire: isWorkspaceFocused, - }); - lastAutoFocusKeyRef.current = step.latchedKey; - if (step.fire) { + if (isMobile || !isPaneFocused || !terminalId) { + lastAutoFocusKeyRef.current = null; + return; + } + if (!isWorkspaceFocused) { + return; + } + const focusKey = `${scopeKey}:${terminalId}`; + if (lastAutoFocusKeyRef.current !== focusKey) { + lastAutoFocusKeyRef.current = focusKey; requestTerminalFocus(); } }, [isMobile, isPaneFocused, isWorkspaceFocused, requestTerminalFocus, scopeKey, terminalId]); useEffect(() => { - // This reflow produces the one resize claim a freshly mounted terminal ever sends; defer it - // (without burning the latch) until the claim can land — handleTerminalResize drops claims - // while the workspace is unfocused or the app is hidden, and nothing re-sends a dropped one. - const step = resolveFocusLatchStep({ - key: !isPaneFocused || !terminalId ? null : `${scopeKey}:${terminalId}`, - latchedKey: lastPaneFocusResizeKeyRef.current, - canFire: isWorkspaceFocused && isAppVisible, + const canRequest = canRequestFocusClaim({ + isWorkspaceFocused, + isAppVisible, + isClientReady: client !== null, + isConnected, + isRendererReady: rendererReadyStreamKey === terminalStreamKey, }); - lastPaneFocusResizeKeyRef.current = step.latchedKey; - if (step.fire) { + const step = reconcileFocusClaim(paneFocusResizeClaimRef.current, { + key: !isPaneFocused || !terminalId ? null : `${scopeKey}:${terminalId}`, + canRequest, + }); + paneFocusResizeClaimRef.current = step.state; + if (step.shouldRequest) { lastSentTerminalSizeRef.current = null; requestTerminalReflow(); } }, [ + client, isAppVisible, + isConnected, isPaneFocused, isWorkspaceFocused, + rendererReadyStreamKey, requestTerminalReflow, scopeKey, terminalId, + terminalStreamKey, ]); const handleTerminalFocus = useCallback(() => { @@ -635,23 +648,40 @@ export function TerminalPane({ const normalizedCols = Math.floor(cols); const nextSize = { rows: normalizedRows, cols: normalizedCols }; measuredTerminalSizeRef.current = nextSize; - if (!input.shouldClaim || !client || !terminalId || !isWorkspaceFocused || !isAppVisible) { + if (!input.shouldClaim) { return; } - const previousSent = lastSentTerminalSizeRef.current; - if ( - previousSent && - previousSent.rows === normalizedRows && - previousSent.cols === normalizedCols - ) { - return; - } - lastSentTerminalSizeRef.current = nextSize; - client.sendTerminalInput(terminalId, { - type: "resize", - rows: normalizedRows, - cols: normalizedCols, + let sent = false; + const canSend = canRequestFocusClaim({ + isWorkspaceFocused, + isAppVisible, + isClientReady: client !== null, + isConnected, + isRendererReady: true, }); + if (client && terminalId && canSend) { + const previousSent = lastSentTerminalSizeRef.current; + if ( + !previousSent || + previousSent.rows !== normalizedRows || + previousSent.cols !== normalizedCols + ) { + lastSentTerminalSizeRef.current = nextSize; + client.sendTerminalInput(terminalId, { + type: "resize", + rows: normalizedRows, + cols: normalizedCols, + }); + } + sent = true; + } + const requestedKey = paneFocusResizeClaimRef.current.requestedKey; + if (requestedKey) { + paneFocusResizeClaimRef.current = settleFocusClaim(paneFocusResizeClaimRef.current, { + key: requestedKey, + sent, + }); + } }, ); diff --git a/packages/app/src/terminal/runtime/terminal-emulator-runtime.browser.test.ts b/packages/app/src/terminal/runtime/terminal-emulator-runtime.browser.test.ts index baa6ae350..1c191b1b1 100644 --- a/packages/app/src/terminal/runtime/terminal-emulator-runtime.browser.test.ts +++ b/packages/app/src/terminal/runtime/terminal-emulator-runtime.browser.test.ts @@ -66,6 +66,10 @@ async function waitFor(input: { predicate: () => boolean; timeoutMs?: number }): } } +function settleMountRefits(): Promise { + return new Promise((resolve) => setTimeout(resolve, 2_600)); +} + function createTerminalHost(input: { width: number; height: number; @@ -204,8 +208,17 @@ describe("terminal emulator runtime in a real browser", () => { const mounted = createTerminalHost({ width: 720, height: 360 }); await waitFor({ predicate: () => mounted.sizes.length > 0 }); + await settleMountRefits(); - expect(mounted.sizes[0]?.shouldClaim).toBe(false); + expect(mounted.sizes.length).toBeGreaterThan(1); + expect(mounted.sizes.filter((size) => size.shouldClaim)).toEqual([]); + + const settledSize = latestSize(mounted.sizes); + mounted.runtime.resize({ force: true, shouldClaim: true }); + + expect(mounted.sizes.filter((size) => size.shouldClaim)).toEqual([ + { ...settledSize, shouldClaim: true }, + ]); }); it("reports a larger PTY size when the terminal container grows", async () => { diff --git a/packages/app/src/terminal/runtime/terminal-mount-claim.browser.test.ts b/packages/app/src/terminal/runtime/terminal-mount-claim.browser.test.ts deleted file mode 100644 index ff3d36649..000000000 --- a/packages/app/src/terminal/runtime/terminal-mount-claim.browser.test.ts +++ /dev/null @@ -1,159 +0,0 @@ -import { page } from "@vitest/browser/context"; -import { afterEach, describe, expect, it } from "vitest"; -import { TerminalEmulatorRuntime } from "./terminal-emulator-runtime"; - -/** - * Pins the mechanism behind "terminal stuck at 80x24". - * - * The daemon spawns a PTY at 80x24 and only resizes it when the client sends a resize frame. - * `handleTerminalResize` in terminal-pane only forwards a frame when the emitted fit carries - * `shouldClaim: true`. - * - * These tests show that a freshly mounted terminal NEVER emits such a fit. Every mount-time - * refit (the mount fit, the retry ladder, fonts.ready, the WebGL swap, visibility restore) is - * emitted with `shouldClaim: false`. The ResizeObserver's first delivery IS `shouldClaim: true`, - * but it is emitted without `force`, so the runtime's own dedupe drops it: the size has not - * changed since the mount fit. - * - * So after a fresh mount, in a pane whose size never changes, the client sends the daemon - * NOTHING. The PTY keeps whatever size it was created with. The terminal's size then depends - * entirely on either the size carried at subscribe, or an explicit forced reflow - * (`requestTerminalReflow` -> `runtime.resize({force, shouldClaim})`) -- which is exactly the - * path terminal-pane's focus effects can permanently latch off. - */ - -interface EmittedSize { - rows: number; - cols: number; - shouldClaim: boolean; -} - -interface MountedTerminal { - root: HTMLDivElement; - runtime: TerminalEmulatorRuntime; - sizes: EmittedSize[]; -} - -const mounted: MountedTerminal[] = []; - -afterEach(() => { - for (const entry of mounted.splice(0)) { - entry.runtime.unmount(); - entry.root.remove(); - } -}); - -function mountTerminal(input: { width: number; height: number }): MountedTerminal { - const root = document.createElement("div"); - root.style.cssText = `width:${input.width}px;height:${input.height}px;position:fixed;left:0;top:0;overflow:hidden`; - const host = document.createElement("div"); - host.style.cssText = "width:100%;height:100%"; - root.appendChild(host); - document.body.appendChild(root); - - const sizes: EmittedSize[] = []; - const runtime = new TerminalEmulatorRuntime(); - runtime.setCallbacks({ - callbacks: { - onResize: (size) => { - sizes.push(size as EmittedSize); - }, - }, - }); - runtime.mount({ - root, - host, - initialSnapshot: null, - scrollback: 1_000, - theme: { background: "#0b0b0b", foreground: "#e6e6e6", cursor: "#e6e6e6" }, - }); - - const entry = { root, runtime, sizes }; - mounted.push(entry); - return entry; -} - -function nextFrame(): Promise { - return new Promise((resolve) => { - requestAnimationFrame(() => { - resolve(); - }); - }); -} - -async function waitFor(input: { predicate: () => boolean; timeoutMs?: number }): Promise { - const startedAt = performance.now(); - const timeoutMs = input.timeoutMs ?? 2_000; - - while (!input.predicate()) { - if (performance.now() - startedAt > timeoutMs) { - throw new Error("Timed out waiting for terminal browser condition"); - } - await nextFrame(); - } -} - -/** - * Outlast the runtime's whole refit ladder (0/16/48/120/250/500/1000/2000ms) plus fonts and the - * WebGL swap, so nothing is still in flight when we assert. Unlike every other wait in this file - * this one cannot be a predicate: the point is to prove that no claim EVER arrives, and there is - * no event that says "the mount is done emitting". - */ -function settleMountRefits(): Promise { - return new Promise((resolve) => setTimeout(resolve, 2_600)); -} - -describe("a freshly mounted terminal never claims its size", () => { - it("emits many fits on mount, and not one of them claims the PTY size", async () => { - await page.viewport(1400, 900); - const terminal = mountTerminal({ width: 900, height: 600 }); - - await settleMountRefits(); - - // It really did refit repeatedly while mounting... - expect(terminal.sizes.length).toBeGreaterThan(1); - // ...and it measured a real viewport, not the daemon's default. - expect(terminal.sizes.at(-1)?.cols).not.toBe(80); - - // ...but every single one of those fits declined to own the PTY size, so terminal-pane - // forwards NOTHING to the daemon. This is why a terminal can sit at 80x24 while its xterm - // renders perfectly at the real size. - const claims = terminal.sizes.filter((size) => size.shouldClaim); - expect(claims).toEqual([]); - }); - - it("only an explicit forced reflow claims — the path the focus latch can kill", async () => { - await page.viewport(1400, 900); - const terminal = mountTerminal({ width: 900, height: 600 }); - await settleMountRefits(); - expect(terminal.sizes.filter((size) => size.shouldClaim)).toEqual([]); - const settledSize = terminal.sizes.at(-1); - - // This is what requestTerminalReflow does. It is the ONLY thing that makes a - // stable-sized, freshly mounted terminal tell the daemon how big it is. - terminal.runtime.resize({ force: true, shouldClaim: true }); - - // It claims the size the terminal actually settled at, so the PTY ends up matching what the - // user sees rendered. - expect(terminal.sizes.filter((size) => size.shouldClaim)).toEqual([ - { rows: settledSize?.rows, cols: settledSize?.cols, shouldClaim: true }, - ]); - }); - - it("a genuine size change does claim — which is why resizing the window unsticks it", async () => { - await page.viewport(1400, 900); - const terminal = mountTerminal({ width: 900, height: 600 }); - await settleMountRefits(); - expect(terminal.sizes.filter((size) => size.shouldClaim)).toEqual([]); - - // The ResizeObserver fit IS shouldClaim:true; it is only suppressed while the size is - // unchanged. Change the box and it gets through -- the user-visible "resize the window and - // it fixes itself" behaviour. - terminal.root.style.width = "1300px"; - - await waitFor({ predicate: () => terminal.sizes.some((size) => size.shouldClaim) }); - const claims = terminal.sizes.filter((size) => size.shouldClaim); - // The claim carries the NEW width, not the size it mounted at. - expect(claims.at(-1)?.cols).toBeGreaterThan(terminal.sizes[0]?.cols ?? 0); - }); -}); From 557fc42c890b8badcb60249fd0b30a2396f2b112 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Fri, 17 Jul 2026 11:17:21 +0200 Subject: [PATCH 03/38] feat(server): include daemon version in logs (#2155) Attach the running release to every entry so log history can show when daemon upgrades took effect. --- packages/server/src/server/logger.test.ts | 19 +++++++++++++++++++ packages/server/src/server/logger.ts | 5 ++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/packages/server/src/server/logger.test.ts b/packages/server/src/server/logger.test.ts index 04a54b3ee..f7f42a7ef 100644 --- a/packages/server/src/server/logger.test.ts +++ b/packages/server/src/server/logger.test.ts @@ -5,6 +5,7 @@ import path from "node:path"; import { spawn } from "node:child_process"; import { fileURLToPath } from "node:url"; import { describe, expect, it } from "vitest"; +import { resolveDaemonVersion } from "./daemon-version.js"; import { resolveLogConfig } from "./logger.js"; import { loadConfig } from "./config.js"; import type { PersistedConfig } from "./persisted-config.js"; @@ -153,6 +154,24 @@ describe("loadConfig logger config", () => { }); describe("createRootLogger", () => { + it("includes the daemon version in child logger entries", async () => { + const paseoHome = await mkdtemp(path.join(tmpdir(), "paseo-logger-version-")); + + const { stdout } = await runLoggerFixture(` + const logger = createRootLogger(undefined, { paseoHome: ${JSON.stringify(paseoHome)} }); + logger.child({ name: "fixture" }).info("versioned child logger"); + logger.flush(); + `); + + expect(JSON.parse(stdout)).toMatchObject({ + pid: expect.any(Number), + hostname: expect.any(String), + daemonVersion: resolveDaemonVersion(), + name: "fixture", + msg: "versioned child logger", + }); + }); + it("writes JSON to stdout by default and does not initialize file logging", async () => { const paseoHome = await mkdtemp(path.join(tmpdir(), "paseo-logger-default-")); const missingLogDir = path.join(paseoHome, "logs"); diff --git a/packages/server/src/server/logger.ts b/packages/server/src/server/logger.ts index 5c1c0522b..118ce9bbe 100644 --- a/packages/server/src/server/logger.ts +++ b/packages/server/src/server/logger.ts @@ -2,6 +2,7 @@ import { mkdirSync } from "node:fs"; import path from "node:path"; import pino from "pino"; import pretty from "pino-pretty"; +import { resolveDaemonVersion } from "./daemon-version.js"; import type { PersistedConfig } from "./persisted-config.js"; import { resolvePaseoHome } from "./paseo-home.js"; @@ -180,13 +181,15 @@ export function createRootLogger( }) : pino.destination({ dest: config.file?.path ?? 1, sync: false }); - return pino( + const logger = pino( { level: config.file?.level ?? config.console.level, redact: { paths: REDACT_PATHS, remove: true }, }, stream, ); + + return logger.child({ daemonVersion: resolveDaemonVersion() }); } export function createChildLogger(parent: pino.Logger, name: string): pino.Logger { From 266d54463be65f01ef3e5e0d57505427d70e598e Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Fri, 17 Jul 2026 12:27:15 +0200 Subject: [PATCH 04/38] feat(app): highlight sidebar resize handles --- .../app/e2e/sidebar-resize-handle.spec.ts | 27 ++++++ .../app/src/components/explorer-sidebar.tsx | 21 ++--- packages/app/src/components/left-sidebar.tsx | 26 ++---- .../src/components/sidebar-resize-handle.tsx | 89 +++++++++++++++++++ 4 files changed, 130 insertions(+), 33 deletions(-) create mode 100644 packages/app/e2e/sidebar-resize-handle.spec.ts create mode 100644 packages/app/src/components/sidebar-resize-handle.tsx diff --git a/packages/app/e2e/sidebar-resize-handle.spec.ts b/packages/app/e2e/sidebar-resize-handle.spec.ts new file mode 100644 index 000000000..eda7f2c81 --- /dev/null +++ b/packages/app/e2e/sidebar-resize-handle.spec.ts @@ -0,0 +1,27 @@ +import { expect, test, type Page } from "./fixtures"; + +test.use({ viewport: { width: 1600, height: 900 } }); + +async function expectBorderHighlight(page: Page, testID: string) { + const handle = page.getByTestId(testID); + await expect(handle).toBeVisible(); + await expect(page.getByTestId(`${testID}-highlight`)).toHaveCount(0); + + await handle.hover(); + await expect(page.getByTestId(`${testID}-highlight`)).toHaveCount(0); + + const highlight = page.getByTestId(`${testID}-highlight`); + await expect(highlight).toBeVisible(); + await expect(highlight).toHaveCSS("width", "1px"); + await expect(highlight).not.toHaveCSS("background-color", "rgba(0, 0, 0, 0)"); +} + +test("both sidebar borders highlight on hover", async ({ page, withWorkspace }) => { + const workspace = await withWorkspace({ prefix: "sidebar-resize-handle-" }); + await workspace.navigateTo(); + + await expectBorderHighlight(page, "left-sidebar-resize-handle"); + + await page.getByTestId("workspace-explorer-toggle").first().click(); + await expectBorderHighlight(page, "explorer-sidebar-resize-handle"); +}); diff --git a/packages/app/src/components/explorer-sidebar.tsx b/packages/app/src/components/explorer-sidebar.tsx index 0af8a0398..8b6fa0e49 100644 --- a/packages/app/src/components/explorer-sidebar.tsx +++ b/packages/app/src/components/explorer-sidebar.tsx @@ -8,7 +8,7 @@ import { } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import Animated, { useAnimatedStyle, useSharedValue, runOnJS } from "react-native-reanimated"; -import { Gesture, GestureDetector } from "react-native-gesture-handler"; +import { Gesture } from "react-native-gesture-handler"; import { StyleSheet, useUnistyles } from "react-native-unistyles"; import { X } from "lucide-react-native"; import { useTranslation } from "react-i18next"; @@ -33,7 +33,7 @@ import { useKeyboardShiftStyle } from "@/hooks/use-keyboard-shift-style"; import { useHasOwnedWindowChromeObstruction, WindowChromeSafeArea } from "@/utils/desktop-window"; import { TitlebarDragRegion } from "@/components/desktop/titlebar-drag-region"; import { RetainedPanelActivity } from "@/components/retained-panel"; -import { isWeb } from "@/constants/platform"; +import { SidebarResizeHandle } from "@/components/sidebar-resize-handle"; import { buildWorkspaceAttachmentScopeKey } from "@/attachments/workspace-attachments-store"; import { resolveDesktopExplorerWidth } from "@/components/desktop-sidebar-layout"; @@ -212,9 +212,11 @@ export function ExplorerSidebar({ return ( - - - + ({ borderLeftColor: theme.colors.border, backgroundColor: theme.colors.surfaceSidebar, }, - resizeHandle: { - position: "absolute", - left: -5, - top: 0, - bottom: 0, - width: 10, - zIndex: 10, - }, sidebarContent: { flex: 1, minHeight: 0, @@ -524,4 +518,3 @@ const styles = StyleSheet.create((theme) => ({ })); const DESKTOP_SIDEBAR_BORDER_STYLE = [styles.desktopSidebarBorder, { flex: 1 }]; -const RESIZE_HANDLE_STYLE = [styles.resizeHandle, isWeb && ({ cursor: "col-resize" } as object)]; diff --git a/packages/app/src/components/left-sidebar.tsx b/packages/app/src/components/left-sidebar.tsx index 4dbac08be..0e36f9c78 100644 --- a/packages/app/src/components/left-sidebar.tsx +++ b/packages/app/src/components/left-sidebar.tsx @@ -20,7 +20,7 @@ import { View, type PressableStateCallbackType, } from "react-native"; -import { Gesture, GestureDetector } from "react-native-gesture-handler"; +import { Gesture } from "react-native-gesture-handler"; import Animated, { runOnJS, useAnimatedStyle, useSharedValue } from "react-native-reanimated"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { StyleSheet, useUnistyles } from "react-native-unistyles"; @@ -30,10 +30,10 @@ import { HostPicker } from "@/components/hosts/host-picker"; import { SidebarHeaderRow } from "@/components/sidebar/sidebar-header-row"; import { SidebarDisplayPreferencesMenu } from "@/components/sidebar/sidebar-display-preferences-menu"; import { SidebarHelpMenu } from "@/components/sidebar/sidebar-help-menu"; +import { SidebarResizeHandle } from "@/components/sidebar-resize-handle"; import { Shortcut } from "@/components/ui/shortcut"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { HEADER_INNER_HEIGHT, useIsCompactFormFactor } from "@/constants/layout"; -import { isWeb } from "@/constants/platform"; import { useOpenAddProject } from "@/hooks/use-open-add-project"; import { useShortcutKeys } from "@/hooks/use-shortcut-keys"; import { canCreateWorktreeForProjectKind } from "@/projects/host-projects"; @@ -765,11 +765,6 @@ function DesktopSidebar({ () => [styles.sidebarHeaderGroup, ownsTopLeft && styles.sidebarHeaderGroupBelowChrome], [ownsTopLeft], ); - const resizeHandleStyle = useMemo( - () => [styles.resizeHandle, isWeb && ({ cursor: "col-resize" } as object)], - [], - ); - return ( - {/* Resize handle - absolutely positioned over right border */} - - - + ); @@ -998,14 +994,6 @@ const styles = StyleSheet.create((theme) => ({ borderRightColor: theme.colors.border, backgroundColor: theme.colors.surfaceSidebar, }, - resizeHandle: { - position: "absolute", - right: -5, - top: 0, - bottom: 0, - width: 10, - zIndex: 10, - }, sidebarDragArea: { position: "relative", }, diff --git a/packages/app/src/components/sidebar-resize-handle.tsx b/packages/app/src/components/sidebar-resize-handle.tsx new file mode 100644 index 000000000..98f4e918b --- /dev/null +++ b/packages/app/src/components/sidebar-resize-handle.tsx @@ -0,0 +1,89 @@ +import { useCallback, useEffect, useRef, useState } from "react"; +import { Pressable, View } from "react-native"; +import { GestureDetector, type GestureType } from "react-native-gesture-handler"; +import { StyleSheet } from "react-native-unistyles"; +import { isWeb } from "@/constants/platform"; + +interface SidebarResizeHandleProps { + edge: "left" | "right"; + gesture: GestureType; + testID: string; +} + +const HIGHLIGHT_DELAY_MS = 100; + +const webResizeCursorStyle = isWeb + ? ({ + cursor: "col-resize", + } as object) + : null; + +export function SidebarResizeHandle({ edge, gesture, testID }: SidebarResizeHandleProps) { + const [highlighted, setHighlighted] = useState(false); + const highlightTimerRef = useRef | null>(null); + const hitAreaStyle = edge === "left" ? LEFT_HIT_AREA_STYLE : RIGHT_HIT_AREA_STYLE; + + const cancelHighlightTimer = useCallback(() => { + if (highlightTimerRef.current === null) return; + clearTimeout(highlightTimerRef.current); + highlightTimerRef.current = null; + }, []); + + const handleHoverIn = useCallback(() => { + cancelHighlightTimer(); + highlightTimerRef.current = setTimeout(() => { + highlightTimerRef.current = null; + setHighlighted(true); + }, HIGHLIGHT_DELAY_MS); + }, [cancelHighlightTimer]); + + const handleHoverOut = useCallback(() => { + cancelHighlightTimer(); + setHighlighted(false); + }, [cancelHighlightTimer]); + + useEffect(() => cancelHighlightTimer, [cancelHighlightTimer]); + + return ( + + + {highlighted ? ( + + ) : null} + + + ); +} + +const styles = StyleSheet.create((theme) => ({ + hitArea: { + position: "absolute", + top: 0, + bottom: 0, + width: 10, + zIndex: 10, + }, + leftEdge: { + left: -5, + }, + rightEdge: { + right: -5, + }, + highlight: { + position: "absolute", + top: 0, + bottom: 0, + left: 5, + width: 1, + backgroundColor: theme.colors.foreground, + opacity: 0.25, + }, +})); + +const LEFT_HIT_AREA_STYLE = [styles.hitArea, styles.leftEdge, webResizeCursorStyle]; +const RIGHT_HIT_AREA_STYLE = [styles.hitArea, styles.rightEdge, webResizeCursorStyle]; From a7cbf4f61ddd5adf78970e8c6584b6e8f5c78880 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Fri, 17 Jul 2026 13:00:47 +0200 Subject: [PATCH 05/38] fix(app): make sidebar reordering respond immediately Mouse input inherited the touch hold delay. Split activation by input so mouse dragging begins after deliberate movement while touch retains long-press arbitration. --- packages/app/e2e/sidebar-reorder.spec.ts | 71 ++++++ .../app/src/components/drag-reorder/index.ts | 5 +- .../drag-reorder/pointer-activation.test.ts | 21 +- .../drag-reorder/pointer-activation.ts | 31 ++- .../app/src/components/draggable-list.web.tsx | 25 +- .../src/components/sidebar-workspace-list.tsx | 222 +----------------- .../use-long-press-drag-interaction.ts | 5 +- 7 files changed, 125 insertions(+), 255 deletions(-) create mode 100644 packages/app/e2e/sidebar-reorder.spec.ts diff --git a/packages/app/e2e/sidebar-reorder.spec.ts b/packages/app/e2e/sidebar-reorder.spec.ts new file mode 100644 index 000000000..0979694f0 --- /dev/null +++ b/packages/app/e2e/sidebar-reorder.spec.ts @@ -0,0 +1,71 @@ +import type { Locator } from "@playwright/test"; +import { expect, test } from "./fixtures"; +import { gotoAppShell } from "./helpers/app"; +import { getServerId } from "./helpers/server-id"; +import { seedWorkspace } from "./helpers/seed-client"; +import { waitForSidebarHydration } from "./helpers/workspace-ui"; + +async function rowTestIds(rows: Locator) { + return rows.evaluateAll((elements) => + elements.map((element) => element.getAttribute("data-testid")), + ); +} + +async function visibleBoundingBox(row: Locator) { + const box = await row.boundingBox(); + if (!box) throw new Error("Expected a visible draggable row"); + return box; +} + +async function quickDragFirstRowAfterSecond(rows: Locator) { + await expect(rows).toHaveCount(2); + const before = await rowTestIds(rows); + const sourceBox = await visibleBoundingBox(rows.nth(0)); + const targetBox = await visibleBoundingBox(rows.nth(1)); + + const page = rows.page(); + const source = { x: sourceBox.x + sourceBox.width / 2, y: sourceBox.y + sourceBox.height / 2 }; + const target = { x: targetBox.x + targetBox.width / 2, y: targetBox.y + targetBox.height / 2 }; + + await page.mouse.move(source.x, source.y); + await page.mouse.down(); + await page.mouse.move(source.x, source.y + 7); + await page.mouse.move(target.x, target.y, { steps: 4 }); + await page.mouse.up(); + + await expect.poll(() => rowTestIds(rows)).toEqual([before[1], before[0]]); +} + +test("projects and workspaces reorder with an immediate mouse drag", async ({ page }) => { + const firstProject = await seedWorkspace({ repoPrefix: "sidebar-reorder-first-" }); + const secondProject = await seedWorkspace({ repoPrefix: "sidebar-reorder-second-" }); + + try { + const secondWorkspace = await firstProject.client.createWorkspace({ + source: { + kind: "directory", + path: firstProject.repoPath, + projectId: firstProject.projectId, + }, + title: "Second workspace", + }); + if (!secondWorkspace.workspace) { + throw new Error(secondWorkspace.error ?? "Failed to seed a second workspace"); + } + + await gotoAppShell(page); + await waitForSidebarHydration(page); + + await quickDragFirstRowAfterSecond(page.locator('[data-testid^="sidebar-project-row-"]')); + const firstWorkspaceTestId = `sidebar-workspace-row-${getServerId()}:${firstProject.workspaceId}`; + const secondWorkspaceTestId = `sidebar-workspace-row-${getServerId()}:${secondWorkspace.workspace.id}`; + await quickDragFirstRowAfterSecond( + page.locator( + `[data-testid="${firstWorkspaceTestId}"], [data-testid="${secondWorkspaceTestId}"]`, + ), + ); + } finally { + await firstProject.cleanup(); + await secondProject.cleanup(); + } +}); diff --git a/packages/app/src/components/drag-reorder/index.ts b/packages/app/src/components/drag-reorder/index.ts index 4016bd1e6..ba31937e4 100644 --- a/packages/app/src/components/drag-reorder/index.ts +++ b/packages/app/src/components/drag-reorder/index.ts @@ -1,8 +1,9 @@ export { reorderItemsOnDragEnd } from "./reorder-items"; export type { DragEndInput } from "./reorder-items"; export { - getPointerActivationConstraint, - type PointerActivationConfig, + getDragActivationConstraints, + type DragActivationConfig, + type DragActivationConstraints, type PointerActivationConstraint, } from "./pointer-activation"; export { diff --git a/packages/app/src/components/drag-reorder/pointer-activation.test.ts b/packages/app/src/components/drag-reorder/pointer-activation.test.ts index d1c6fa041..921dd79a2 100644 --- a/packages/app/src/components/drag-reorder/pointer-activation.test.ts +++ b/packages/app/src/components/drag-reorder/pointer-activation.test.ts @@ -1,14 +1,21 @@ import { describe, expect, it } from "vitest"; -import { getPointerActivationConstraint } from "./pointer-activation"; +import { getDragActivationConstraints } from "./pointer-activation"; -const config = { defaultDistance: 6, holdDelayMs: 250, holdTolerance: 8 }; +const config = { movementDistance: 6, touchHoldDelayMs: 180, touchHoldTolerance: 8 }; -describe("getPointerActivationConstraint", () => { - it("uses distance activation for default draggable rows", () => { - expect(getPointerActivationConstraint(false, config)).toEqual({ distance: 6 }); +describe("getDragActivationConstraints", () => { + it("starts mouse drags after deliberate pointer movement", () => { + expect(getDragActivationConstraints(true, config).mouse).toEqual({ distance: 6 }); }); - it("requires a held pointer before activating handle-based drags", () => { - expect(getPointerActivationConstraint(true, config)).toEqual({ delay: 250, tolerance: 8 }); + it("requires a short hold before starting touch drags", () => { + expect(getDragActivationConstraints(true, config).touch).toEqual({ + delay: 180, + tolerance: 8, + }); + }); + + it("starts ordinary touch rows after deliberate movement", () => { + expect(getDragActivationConstraints(false, config).touch).toEqual({ distance: 6 }); }); }); diff --git a/packages/app/src/components/drag-reorder/pointer-activation.ts b/packages/app/src/components/drag-reorder/pointer-activation.ts index cb68e2bda..9b929c8f7 100644 --- a/packages/app/src/components/drag-reorder/pointer-activation.ts +++ b/packages/app/src/components/drag-reorder/pointer-activation.ts @@ -2,18 +2,25 @@ export type PointerActivationConstraint = | { distance: number } | { delay: number; tolerance: number }; -export interface PointerActivationConfig { - defaultDistance: number; - holdDelayMs: number; - holdTolerance: number; +export interface DragActivationConfig { + movementDistance: number; + touchHoldDelayMs: number; + touchHoldTolerance: number; } -export function getPointerActivationConstraint( - useDragHandle: boolean, - config: PointerActivationConfig, -): PointerActivationConstraint { - if (useDragHandle) { - return { delay: config.holdDelayMs, tolerance: config.holdTolerance }; - } - return { distance: config.defaultDistance }; +export interface DragActivationConstraints { + mouse: PointerActivationConstraint; + touch: PointerActivationConstraint; +} + +export function getDragActivationConstraints( + useDragHandle: boolean, + config: DragActivationConfig, +): DragActivationConstraints { + const movement = { distance: config.movementDistance }; + const touch = useDragHandle + ? { delay: config.touchHoldDelayMs, tolerance: config.touchHoldTolerance } + : movement; + + return { mouse: movement, touch }; } diff --git a/packages/app/src/components/draggable-list.web.tsx b/packages/app/src/components/draggable-list.web.tsx index 3f16fc072..7c1e683e5 100644 --- a/packages/app/src/components/draggable-list.web.tsx +++ b/packages/app/src/components/draggable-list.web.tsx @@ -4,7 +4,8 @@ import { DndContext, closestCenter, KeyboardSensor, - PointerSensor, + MouseSensor, + TouchSensor, type Modifier, useSensor, useSensors, @@ -17,7 +18,7 @@ import { } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; import type { DraggableListProps, DraggableRenderItemInfo } from "./draggable-list.types"; -import { getPointerActivationConstraint, useDragReorderState } from "./drag-reorder"; +import { getDragActivationConstraints, useDragReorderState } from "./drag-reorder"; export type { DraggableListProps, DraggableRenderItemInfo }; @@ -27,10 +28,10 @@ const restrictToVerticalAxis: Modifier = ({ transform }) => ({ }); const DND_MODIFIERS = [restrictToVerticalAxis]; -const POINTER_ACTIVATION_CONFIG = { - defaultDistance: 6, - holdDelayMs: 250, - holdTolerance: 8, +const DRAG_ACTIVATION_CONFIG = { + movementDistance: 6, + touchHoldDelayMs: 180, + touchHoldTolerance: 8, }; interface SortableItemProps { @@ -145,14 +146,14 @@ export function DraggableList({ onDragEnd, onDragBegin, }); - const pointerActivationConstraint = getPointerActivationConstraint( - useDragHandle, - POINTER_ACTIVATION_CONFIG, - ); + const activationConstraints = getDragActivationConstraints(useDragHandle, DRAG_ACTIVATION_CONFIG); const sensors = useSensors( - useSensor(PointerSensor, { - activationConstraint: pointerActivationConstraint, + useSensor(MouseSensor, { + activationConstraint: activationConstraints.mouse, + }), + useSensor(TouchSensor, { + activationConstraint: activationConstraints.touch, }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates, diff --git a/packages/app/src/components/sidebar-workspace-list.tsx b/packages/app/src/components/sidebar-workspace-list.tsx index bad34715b..ffef8dfd0 100644 --- a/packages/app/src/components/sidebar-workspace-list.tsx +++ b/packages/app/src/components/sidebar-workspace-list.tsx @@ -2,15 +2,12 @@ import { View, Text, Pressable, - Platform, ActivityIndicator, - StatusBar, ScrollView, type GestureResponderEvent, type PressableStateCallbackType, type ViewStyle, } from "react-native"; -import * as Haptics from "expo-haptics"; import { useMutation } from "@tanstack/react-query"; import { ProjectIconView } from "@/components/project-icon-view"; import { AdaptiveRenameModal } from "@/components/rename-modal"; @@ -87,7 +84,6 @@ import { useToast } from "@/contexts/toast-context"; import { getForgePresentation, normalizeForge } from "@/git/forge"; import { toWorktreeArchiveRisk } from "@/git/worktree-archive-warning"; import { hasVisibleOrderChanged, mergeWithRemainder } from "@/utils/sidebar-reorder"; -import { decideLongPressMove } from "@/utils/sidebar-gesture-arbitration"; import { confirmDialog } from "@/utils/confirm-dialog"; import { projectIconPlaceholderLabelFromDisplayName } from "@/utils/project-display-name"; import { shouldRenderSyncedStatusLoader } from "@/utils/status-loader"; @@ -96,6 +92,7 @@ import type { SidebarStateBucket } from "@/utils/sidebar-agent-state"; import { SidebarStatusWorkspaceList } from "@/components/sidebar/sidebar-status-list"; import type { StatusGroup } from "@/hooks/sidebar-status-view-model"; import { SidebarWorkspaceMenu } from "@/components/sidebar/sidebar-workspace-menu"; +import { useLongPressDragInteraction } from "@/components/sidebar/use-long-press-drag-interaction"; import { PinnedSectionHeader } from "@/components/sidebar/pinned-section-header"; import { SidebarWorkspaceRowFrame, @@ -948,223 +945,6 @@ function NewWorkspaceGhostRow({ ); } -function useLongPressDragInteraction(input: { - drag: () => void; - menuController: ReturnType | null; -}) { - const didLongPressRef = useRef(false); - const dragArmedRef = useRef(false); - const dragActivatedRef = useRef(false); - const didStartDragRef = useRef(false); - const scrollIntentRef = useRef(false); - const menuOpenedRef = useRef(false); - const touchStartRef = useRef<{ x: number; y: number } | null>(null); - const touchCurrentRef = useRef<{ x: number; y: number } | null>(null); - const dragArmTimerRef = useRef | null>(null); - const contextMenuTimerRef = useRef | null>(null); - - const clearTimers = useCallback(() => { - if (dragArmTimerRef.current) { - clearTimeout(dragArmTimerRef.current); - dragArmTimerRef.current = null; - } - if (contextMenuTimerRef.current) { - clearTimeout(contextMenuTimerRef.current); - contextMenuTimerRef.current = null; - } - }, []); - - const openContextMenuAtStartPoint = useCallback(() => { - if (!input.menuController || !touchStartRef.current) { - return; - } - const statusBarHeight = Platform.OS === "android" ? (StatusBar.currentHeight ?? 0) : 0; - input.menuController.setAnchorRect({ - x: touchStartRef.current.x, - y: touchStartRef.current.y + statusBarHeight, - width: 0, - height: 0, - }); - input.menuController.setOpen(true); - menuOpenedRef.current = true; - didLongPressRef.current = true; - }, [input.menuController]); - - const handleLongPress = useCallback(() => { - // Manual timers own long-press behavior on mobile. - }, []); - - useEffect(() => { - return () => { - clearTimers(); - }; - }, [clearTimers]); - - const armTimers = useCallback(() => { - clearTimers(); - - const DRAG_ARM_DELAY_MS = 180; - const DRAG_ARM_STATIONARY_SLOP_PX = 4; - const CONTEXT_MENU_DELAY_MS = 450; - const CONTEXT_MENU_STATIONARY_SLOP_PX = 6; - - dragArmTimerRef.current = setTimeout(() => { - if (scrollIntentRef.current || didStartDragRef.current || menuOpenedRef.current) { - return; - } - const start = touchStartRef.current; - const current = touchCurrentRef.current ?? start; - if (!start || !current) { - return; - } - const dx = current.x - start.x; - const dy = current.y - start.y; - const distance = Math.sqrt(dx * dx + dy * dy); - if (distance > DRAG_ARM_STATIONARY_SLOP_PX) { - return; - } - dragArmedRef.current = true; - dragActivatedRef.current = true; - didLongPressRef.current = true; - void Haptics.selectionAsync().catch(() => {}); - input.drag(); - }, DRAG_ARM_DELAY_MS); - - if (!input.menuController || platformIsWeb) { - return; - } - - contextMenuTimerRef.current = setTimeout(() => { - if (scrollIntentRef.current || didStartDragRef.current || menuOpenedRef.current) { - return; - } - const start = touchStartRef.current; - const current = touchCurrentRef.current ?? start; - if (!start || !current) { - return; - } - const dx = current.x - start.x; - const dy = current.y - start.y; - const distance = Math.sqrt(dx * dx + dy * dy); - if (distance > CONTEXT_MENU_STATIONARY_SLOP_PX) { - return; - } - void Haptics.selectionAsync().catch(() => {}); - openContextMenuAtStartPoint(); - }, CONTEXT_MENU_DELAY_MS); - }, [clearTimers, input, openContextMenuAtStartPoint]); - - const handleDragIntent = useCallback( - (_details: { dx: number; dy: number; distance: number }) => { - if (!dragActivatedRef.current) { - return; - } - didStartDragRef.current = true; - didLongPressRef.current = true; - clearTimers(); - void Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium).catch(() => {}); - }, - [clearTimers], - ); - - const handleScrollIntent = useCallback( - (_details: { dx: number; dy: number; distance: number }) => { - scrollIntentRef.current = true; - didLongPressRef.current = true; - clearTimers(); - }, - [clearTimers], - ); - - const handleSwipeIntent = useCallback( - (_details: { dx: number; dy: number; distance: number }) => { - didLongPressRef.current = true; - clearTimers(); - }, - [clearTimers], - ); - - const handlePressIn = useCallback( - (event: GestureResponderEvent) => { - didLongPressRef.current = false; - dragArmedRef.current = false; - dragActivatedRef.current = false; - didStartDragRef.current = false; - scrollIntentRef.current = false; - menuOpenedRef.current = false; - touchStartRef.current = { - x: event.nativeEvent.pageX, - y: event.nativeEvent.pageY, - }; - touchCurrentRef.current = { - x: event.nativeEvent.pageX, - y: event.nativeEvent.pageY, - }; - armTimers(); - }, - [armTimers], - ); - - const handleTouchMove = useCallback( - (event: GestureResponderEvent) => { - const start = touchStartRef.current; - if (!start || didStartDragRef.current || menuOpenedRef.current) { - return; - } - - const touch = event?.nativeEvent?.touches?.[0] ?? event?.nativeEvent; - const x = touch?.pageX; - const y = touch?.pageY; - if (typeof x !== "number" || typeof y !== "number") { - return; - } - - const current = { x, y }; - touchCurrentRef.current = current; - const dx = current.x - start.x; - const dy = current.y - start.y; - const distance = Math.sqrt(dx * dx + dy * dy); - const decision = decideLongPressMove({ - dragArmed: dragArmedRef.current, - didStartDrag: didStartDragRef.current, - startPoint: start, - currentPoint: current, - }); - - if (decision === "vertical_scroll") { - handleScrollIntent({ dx, dy, distance }); - return; - } - - if (decision === "horizontal_swipe" || decision === "cancel_long_press") { - handleSwipeIntent({ dx, dy, distance }); - return; - } - - if (decision === "start_drag") { - handleDragIntent({ dx, dy, distance }); - } - }, - [handleDragIntent, handleScrollIntent, handleSwipeIntent], - ); - - const handlePressOut = useCallback(() => { - clearTimers(); - dragArmedRef.current = false; - dragActivatedRef.current = false; - touchStartRef.current = null; - touchCurrentRef.current = null; - }, [clearTimers]); - - return { - didLongPressRef, - handleLongPress, - handlePressIn, - handleTouchMove, - handlePressOut, - }; -} - function ProjectHeaderRow({ project, displayName, diff --git a/packages/app/src/components/sidebar/use-long-press-drag-interaction.ts b/packages/app/src/components/sidebar/use-long-press-drag-interaction.ts index 80f15d926..747f2b6bc 100644 --- a/packages/app/src/components/sidebar/use-long-press-drag-interaction.ts +++ b/packages/app/src/components/sidebar/use-long-press-drag-interaction.ts @@ -59,6 +59,9 @@ export function useLongPressDragInteraction(input: { const armTimers = useCallback(() => { clearTimers(); + if (platformIsWeb) { + return; + } const DRAG_ARM_DELAY_MS = 180; const DRAG_ARM_STATIONARY_SLOP_PX = 4; @@ -87,7 +90,7 @@ export function useLongPressDragInteraction(input: { input.drag(); }, DRAG_ARM_DELAY_MS); - if (!input.menuController || platformIsWeb) { + if (!input.menuController) { return; } From 388f1d426c01a9b492d084c207dfd99ff2bc33af Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Fri, 17 Jul 2026 13:02:09 +0200 Subject: [PATCH 06/38] Keep agent browser tabs connected across workspace switches (#2156) * fix(desktop): keep browser tabs connected across workspaces Electron can replace a guest WebContents when a retained browser tab is reparented. Re-register each attachment and keep background actionability checks running so agent browser tools retain the tab through workspace eviction.\n\nCover the full app, daemon, Electron, and MCP path in the existing desktop CI job. * fix(desktop): launch Electron E2E reliably on Linux CI Electron must receive --no-sandbox before the app starts because the hosted runner cannot use Electron's bundled SUID sandbox helper. Forward explicit dev-runner arguments and fail readiness waits as soon as a child exits. * fix(desktop): preserve active browser on repeated registration * fix(desktop): keep browser keyboard attachment idempotent * fix(desktop): wait for E2E bridge readiness * fix(desktop): close E2E logs after output drains --- .github/workflows/ci.yml | 21 +- docs/architecture.md | 2 +- docs/testing.md | 10 + .../components/browser-webview-resident.ts | 30 +- packages/desktop/package.json | 1 + .../scripts/browser-tab-bridge.e2e.mjs | 472 ++++++++++++++++++ packages/desktop/scripts/dev-runner.mjs | 3 +- .../browser-automation/actionability.ts | 9 +- .../features/browser-keyboard/index.test.ts | 51 +- .../src/features/browser-keyboard/index.ts | 7 + .../browser-webviews/registry.test.ts | 26 + .../src/features/browser-webviews/registry.ts | 8 + 12 files changed, 601 insertions(+), 39 deletions(-) create mode 100644 packages/desktop/scripts/browser-tab-bridge.e2e.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 234374fcd..bebab954a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -160,10 +160,29 @@ jobs: - name: Run desktop tests run: npm run test --workspace=@getpaseo/desktop + - name: Build app dependencies for desktop E2E + if: matrix.os == 'ubuntu-latest' + run: npm run build:app-deps + - name: Install virtual display - if: matrix.os == 'ubuntu-latest' && steps.desktop_changes.outputs.desktop == 'true' + if: matrix.os == 'ubuntu-latest' run: sudo apt-get update && sudo apt-get install -y xvfb xauth + - name: Run real Electron browser tab bridge E2E + if: matrix.os == 'ubuntu-latest' + run: npm run test:e2e:browser-tab-bridge --workspace=@getpaseo/desktop + env: + PASEO_TAB_BRIDGE_E2E_ARTIFACT_DIR: ${{ runner.temp }}/browser-tab-bridge-e2e + + - name: Upload browser tab bridge diagnostics + uses: actions/upload-artifact@v4 + if: failure() && matrix.os == 'ubuntu-latest' + with: + name: browser-tab-bridge-e2e + path: ${{ runner.temp }}/browser-tab-bridge-e2e + if-no-files-found: ignore + retention-days: 7 + - name: Build and smoke unpacked desktop app if: matrix.os == 'ubuntu-latest' && steps.desktop_changes.outputs.desktop == 'true' run: npm run build:desktop -- --publish never --linux --x64 --dir diff --git a/docs/architecture.md b/docs/architecture.md index 8ba52a3ca..5b0d8b89a 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -138,7 +138,7 @@ Electron wrapper for macOS, Linux, and Windows. > **Window-state v1 limitation:** only the _first_ window of a session restores and persists saved geometry (size/position/maximized). Windows opened via ⌘⇧N / second-instance / "Open in new window" open at the default size, OS-cascaded, and do not persist — this avoids every window stacking on the same restored bounds and fighting over the single window-state store. Lifting this needs per-window state keys. > -> **In-app browser profile.** Every browser guest uses one stable persistent Electron session, so cookies, authentication, cache, and site storage are shared across tabs, workspaces, and desktop windows and survive tab or app closure. Browser identity is independent of that storage partition: after `did-attach`, the renderer explicitly registers its browser id, workspace id, and guest `WebContents` id, and main accepts the registration only when that guest belongs to the calling renderer and the shared profile. Settings > General > Clear browser data is the sole profile-deletion path; it clears the shared session and reloads live guests without deleting saved tabs or URLs. +> **In-app browser profile.** Every browser guest uses one stable persistent Electron session, so cookies, authentication, cache, and site storage are shared across tabs, workspaces, and desktop windows and survive tab or app closure. Browser identity is independent of that storage partition: after every `did-attach`, the renderer explicitly registers its browser id, workspace id, and current guest `WebContents` id, and main accepts the registration only when that guest belongs to the calling renderer and the shared profile. Registration is intentionally repeated because reparenting a retained `` can replace its guest without replacing the DOM element. Settings > General > Clear browser data is the sole profile-deletion path; it clears the shared session and reloads live guests without deleting saved tabs or URLs. > > **In-app browser window opens.** Ordinary link opens, including Shift-clicked links, become Paseo workspace tabs. Script-created opens with popup features or a named window target and POST-backed opens remain secured Electron child windows in the shared browser profile, preserving `window.opener`, `postMessage`, named-window reuse, request bodies, and `window.close()` for OAuth, payment, and similar popup protocols. Unsupported URL schemes are denied before either path. > diff --git a/docs/testing.md b/docs/testing.md index 98e41d635..06a73d34b 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -124,6 +124,16 @@ PASEO_DESKTOP_SMOKE_ARTIFACT_DIR=/tmp/paseo-desktop-smoke \ npm run build:desktop -- --publish never --linux --x64 --dir ``` +### Browser tab bridge regression + +The desktop browser tab bridge E2E launches an isolated real daemon, Metro, and Electron app. It forces workspace LRU eviction to reparent the original tab and replace its guest `WebContents`, then makes one MCP call each for tab listing, snapshot, and click against that original browser id. A final MCP wait proves the real target page received the click. + +Run it locally with the same command owned by the Ubuntu leg of the existing `desktop-tests` CI check: + +```bash +npm run test:e2e:browser-tab-bridge --workspace=@getpaseo/desktop +``` + ## Test organization - Collocate tests with implementation: `thing.ts` + `thing.test.ts` diff --git a/packages/app/src/components/browser-webview-resident.ts b/packages/app/src/components/browser-webview-resident.ts index a97190018..92577f7da 100644 --- a/packages/app/src/components/browser-webview-resident.ts +++ b/packages/app/src/components/browser-webview-resident.ts @@ -54,22 +54,20 @@ function registerBrowserWhenAttached( identity: BrowserWebviewIdentity, browser: BrowserWebviewProfileHost, ): void { - webview.addEventListener( - "did-attach", - () => { - const webContentsId = webview.getWebContentsId(); - void browser - .registerAttachedBrowser({ - browserId: identity.browserId, - workspaceId: identity.workspaceId, - webContentsId, - }) - .catch((error) => { - console.error("[browser-webview] attached registration failed", error); - }); - }, - { once: true }, - ); + // Reparenting a webview can replace its guest WebContents without replacing + // this DOM element, so every attachment needs a fresh main-process registration. + webview.addEventListener("did-attach", () => { + const webContentsId = webview.getWebContentsId(); + void browser + .registerAttachedBrowser({ + browserId: identity.browserId, + workspaceId: identity.workspaceId, + webContentsId, + }) + .catch((error) => { + console.error("[browser-webview] attached registration failed", error); + }); + }); } function trimNonEmpty(value: string | null | undefined): string | null { diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 85e907a67..4173fae13 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -20,6 +20,7 @@ "capture-harness": "./capture-harness/run.sh", "dev": "./scripts/dev.sh", "dev:win": "powershell ./scripts/dev.ps1", + "test:e2e:browser-tab-bridge": "npm run build:main && node ./scripts/browser-tab-bridge.e2e.mjs", "verify:electron-cdp": "node ./scripts/verify-electron-cdp.mjs", "test": "vitest run", "typecheck": "tsgo --noEmit -p tsconfig.json" diff --git a/packages/desktop/scripts/browser-tab-bridge.e2e.mjs b/packages/desktop/scripts/browser-tab-bridge.e2e.mjs new file mode 100644 index 000000000..139a186e6 --- /dev/null +++ b/packages/desktop/scripts/browser-tab-bridge.e2e.mjs @@ -0,0 +1,472 @@ +#!/usr/bin/env node + +import { spawn } from "node:child_process"; +import fs from "node:fs"; +import { createServer } from "node:http"; +import net from "node:net"; +import os from "node:os"; +import path from "node:path"; +import process from "node:process"; +import { setTimeout as delay } from "node:timers/promises"; +import { fileURLToPath } from "node:url"; +import { experimental_createMCPClient } from "ai"; +import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; +import { chromium } from "playwright"; + +const scriptDir = path.dirname(fileURLToPath(import.meta.url)); +const desktopDir = path.resolve(scriptDir, ".."); +const rootDir = path.resolve(desktopDir, "../.."); +const devRunner = path.join(desktopDir, "scripts", "dev-runner.mjs"); +const workspaceIds = [ + "tab-bridge-original", + "tab-bridge-evict-one", + "tab-bridge-evict-two", + "tab-bridge-evict-three", +]; +const timeoutMs = 90_000; + +function assert(condition, message) { + if (!condition) throw new Error(message); +} + +async function reservePort() { + return await new Promise((resolve, reject) => { + const server = net.createServer(); + server.unref(); + server.once("error", reject); + server.listen(0, "127.0.0.1", () => { + const address = server.address(); + server.close((error) => { + if (error) return reject(error); + if (!address || typeof address === "string") { + return reject(new Error("Failed to reserve a local port")); + } + resolve(address.port); + }); + }); + }); +} + +async function waitForPort(port, label, processInfo) { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + if ( + processInfo && + (processInfo.child.exitCode !== null || processInfo.child.signalCode !== null) + ) { + throw new Error( + `${label} process exited before opening its port; see ${processInfo.logPath}`, + ); + } + const connected = await new Promise((resolve) => { + const socket = net.createConnection({ host: "127.0.0.1", port }); + socket.setTimeout(500); + socket.once("connect", () => { + socket.destroy(); + resolve(true); + }); + socket.once("timeout", () => { + socket.destroy(); + resolve(false); + }); + socket.once("error", () => resolve(false)); + }); + if (connected) return; + await delay(200); + } + throw new Error(`Timed out waiting for ${label} on port ${port}`); +} + +function writeJson(filePath, value) { + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`); +} + +function seedPaseoHome(paseoHome, listen, workspaceRoot) { + const timestamp = "2026-01-01T00:00:00.000Z"; + const projects = workspaceIds.map((workspaceId, index) => { + const cwd = path.join(workspaceRoot, `workspace-${index + 1}`); + fs.mkdirSync(cwd, { recursive: true }); + return { + projectId: `project-${workspaceId}`, + rootPath: cwd, + kind: "non_git", + displayName: `Tab bridge project ${index + 1}`, + customName: null, + createdAt: timestamp, + updatedAt: timestamp, + archivedAt: null, + }; + }); + const workspaces = workspaceIds.map((workspaceId, index) => ({ + workspaceId, + projectId: projects[index].projectId, + cwd: projects[index].rootPath, + kind: "directory", + displayName: `Tab bridge workspace ${index + 1}`, + title: `Tab bridge workspace ${index + 1}`, + branch: null, + baseBranch: null, + createdAt: timestamp, + updatedAt: timestamp, + archivedAt: null, + pinnedAt: null, + })); + + writeJson(path.join(paseoHome, "config.json"), { + version: 1, + daemon: { + listen, + relay: { enabled: false }, + mcp: { enabled: true, injectIntoAgents: false }, + browserTools: { enabled: true }, + cors: { allowedOrigins: ["*"] }, + }, + }); + writeJson(path.join(paseoHome, "projects", "projects.json"), projects); + writeJson(path.join(paseoHome, "projects", "workspaces.json"), workspaces); +} + +function spawnLogged(name, command, args, options, logDir) { + const logPath = path.join(logDir, `${name}.log`); + const log = fs.createWriteStream(logPath, { flags: "a" }); + const child = spawn(command, args, { + ...options, + detached: process.platform !== "win32", + stdio: ["ignore", "pipe", "pipe"], + }); + let openStreams = 2; + const closeLogStream = () => { + openStreams -= 1; + if (openStreams === 0) log.end(); + }; + child.stdout.pipe(log, { end: false }); + child.stderr.pipe(log, { end: false }); + child.stdout.once("end", closeLogStream); + child.stderr.once("end", closeLogStream); + return { child, logPath }; +} + +function stopProcess(child) { + if (!child?.pid || child.exitCode !== null) return; + try { + if (process.platform === "win32") child.kill("SIGTERM"); + else process.kill(-child.pid, "SIGTERM"); + } catch { + // The process may exit between the liveness check and signal delivery. + } +} + +async function waitForAppPage(browser, expoPort) { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + for (const context of browser.contexts()) { + for (const page of context.pages()) { + if (page.url().includes(`localhost:${expoPort}`)) return page; + } + } + await delay(250); + } + throw new Error("Timed out waiting for the real Electron app renderer"); +} + +async function waitForDesktopStatus(page) { + const deadline = Date.now() + timeoutMs; + let lastError; + while (Date.now() < deadline) { + try { + const status = await page.evaluate(async () => { + if (typeof window.paseoDesktop?.invoke !== "function") return null; + return await window.paseoDesktop.invoke("desktop_daemon_status"); + }); + if (typeof status?.serverId === "string") return status; + } catch (error) { + // Metro may replace the renderer execution context during its initial load. + lastError = error; + } + await delay(250); + } + throw new Error( + `Timed out waiting for the Electron desktop bridge${lastError ? `: ${String(lastError)}` : ""}`, + ); +} + +async function startTargetPage() { + const server = createServer((_request, response) => { + response.writeHead(200, { "content-type": "text/html; charset=utf-8" }); + response.end(` + + Tab bridge target + + + + `); + }); + await new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(0, "127.0.0.1", resolve); + }); + const address = server.address(); + assert(address && typeof address !== "string", "Target page did not bind a TCP port"); + return { server, url: `http://127.0.0.1:${address.port}/` }; +} + +async function closeServer(server) { + await new Promise((resolve) => server.close(resolve)); +} + +function mcpPayload(result, command) { + const payload = result.structuredContent; + assert( + payload && typeof payload === "object", + `${command} returned no structured payload: ${JSON.stringify(result)}`, + ); + assert(payload.ok === true, `${command} failed: ${JSON.stringify(payload)}`); + return payload.result; +} + +async function callBrowserTool(client, name, args = {}) { + return mcpPayload(await client.callTool({ name, args }), name); +} + +async function createCallerAgent(daemonPort) { + const transport = new StreamableHTTPClientTransport( + new URL(`http://127.0.0.1:${daemonPort}/mcp/agents`), + ); + const client = await experimental_createMCPClient({ transport }); + try { + const response = await client.callTool({ + name: "create_agent", + args: { + relationship: { kind: "detached" }, + workspace: { kind: "existing", workspaceId: workspaceIds[0] }, + title: "Browser tab bridge E2E caller", + provider: "mock/ten-second-stream", + settings: { modeId: "load-test" }, + initialPrompt: "Remain available while the browser bridge regression runs.", + background: true, + }, + }); + const result = response.structuredContent; + assert( + result && typeof result === "object", + `create_agent returned no structured payload: ${JSON.stringify(response)}`, + ); + assert(typeof result.agentId === "string", "create_agent returned no caller agent id"); + assert( + result.workspaceId === workspaceIds[0], + `MCP caller attached to unexpected workspace ${result.workspaceId}`, + ); + return result.agentId; + } finally { + await client.close(); + } +} + +async function readGuest(page, browserId) { + return await page.evaluate((id) => { + const webview = document.querySelector(`[data-paseo-browser-id="${id}"]`); + if (!(webview instanceof HTMLElement) || typeof webview.getWebContentsId !== "function") { + return null; + } + return { + webContentsId: webview.getWebContentsId(), + parentId: webview.parentElement?.id ?? null, + }; + }, browserId); +} + +async function runRegression({ page, client, serverId, targetUrl }) { + const originalWorkspaceId = workspaceIds[0]; + const originalWorkspaceRow = page.getByTestId( + `sidebar-workspace-row-${serverId}:${originalWorkspaceId}`, + ); + await originalWorkspaceRow.waitFor({ state: "visible", timeout: timeoutMs }); + await originalWorkspaceRow.click(); + + const created = await callBrowserTool(client, "browser_new_tab", { url: targetUrl }); + const browserId = created.browserId; + assert(typeof browserId === "string", "browser_new_tab returned no browserId"); + + const originalDeck = page.getByTestId(`workspace-deck-entry-${serverId}:${originalWorkspaceId}`); + await originalDeck.getByTestId(`workspace-tab-browser_${browserId}`).click(); + await page.waitForFunction( + (id) => { + const webview = document.querySelector(`[data-paseo-browser-id="${id}"]`); + return webview && webview.parentElement?.id !== "paseo-browser-resident-webviews"; + }, + browserId, + { timeout: timeoutMs }, + ); + const firstGuest = await readGuest(page, browserId); + assert(firstGuest, "Original browser guest was not attached to its workspace pane"); + + for (const workspaceId of workspaceIds.slice(1)) { + await page.getByTestId(`sidebar-workspace-row-${serverId}:${workspaceId}`).click(); + await page + .getByTestId(`workspace-deck-entry-${serverId}:${workspaceId}`) + .waitFor({ state: "visible" }); + } + + await page.waitForFunction( + ({ id, previousWebContentsId }) => { + const webview = document.querySelector(`[data-paseo-browser-id="${id}"]`); + return ( + webview?.parentElement?.id === "paseo-browser-resident-webviews" && + typeof webview.getWebContentsId === "function" && + webview.getWebContentsId() !== previousWebContentsId + ); + }, + { id: browserId, previousWebContentsId: firstGuest.webContentsId }, + { timeout: timeoutMs }, + ); + const replacementGuest = await readGuest(page, browserId); + assert(replacementGuest, "Replacement browser guest was not parked after workspace eviction"); + + const listed = await callBrowserTool(client, "browser_list_tabs"); + assert( + listed.tabs.some((tab) => tab.browserId === browserId), + "browser_list_tabs lost the original tab after guest replacement", + ); + + const snapshot = await callBrowserTool(client, "browser_snapshot", { browserId }); + const ref = snapshot.snapshot.match(/button "Bridge target" \[ref=(@e\d+)\]/)?.[1]; + assert(ref, `browser_snapshot did not expose the target button: ${snapshot.snapshot}`); + + const clicked = await callBrowserTool(client, "browser_click", { browserId, ref }); + assert( + clicked.browserId === browserId && clicked.ref === ref, + "browser_click targeted another tab", + ); + await callBrowserTool(client, "browser_wait", { + browserId, + text: "Clicked", + timeoutMs: 5_000, + }); + + return { + browserId, + originalWebContentsId: firstGuest.webContentsId, + replacementWebContentsId: replacementGuest.webContentsId, + list: "passed", + snapshot: "passed", + click: "passed", + }; +} + +async function main() { + const artifactDir = + process.env.PASEO_TAB_BRIDGE_E2E_ARTIFACT_DIR ?? + fs.mkdtempSync(path.join(os.tmpdir(), "paseo-tab-bridge-e2e-artifacts-")); + const runtimeDir = fs.mkdtempSync(path.join(os.tmpdir(), "paseo-tab-bridge-e2e-")); + fs.mkdirSync(artifactDir, { recursive: true }); + const paseoHome = path.join(runtimeDir, "paseo-home"); + const userData = path.join(runtimeDir, "electron-user-data"); + const workspaceRoot = path.join(runtimeDir, "workspaces"); + fs.mkdirSync(paseoHome, { recursive: true }); + + const [daemonPort, expoPort, cdpPort] = await Promise.all([ + reservePort(), + reservePort(), + reservePort(), + ]); + const listen = `127.0.0.1:${daemonPort}`; + seedPaseoHome(paseoHome, listen, workspaceRoot); + const target = await startTargetPage(); + const children = []; + let browser = null; + let client = null; + + try { + const commonEnv = { + ...process.env, + PASEO_HOME: paseoHome, + PASEO_LISTEN: listen, + PASEO_DAEMON_ENDPOINT: `localhost:${daemonPort}`, + PASEO_CORS_ORIGINS: "*", + PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD: "0", + PASEO_DICTATION_ENABLED: "0", + PASEO_VOICE_MODE_ENABLED: "0", + FORCE_COLOR: "0", + NO_COLOR: "1", + }; + const daemon = spawnLogged( + "daemon", + process.execPath, + ["--import", "tsx", path.join(rootDir, "packages/server/scripts/dev-runner.ts")], + { cwd: rootDir, env: { ...commonEnv, PASEO_NODE_ENV: "development" } }, + artifactDir, + ); + children.push(daemon.child); + await waitForPort(daemonPort, "daemon", daemon); + + const desktopArgs = [ + process.execPath, + devRunner, + ...(process.platform === "linux" ? ["--no-sandbox"] : []), + ]; + const desktopCommand = process.platform === "linux" ? "xvfb-run" : desktopArgs.shift(); + const desktopCommandArgs = + process.platform === "linux" + ? ["-a", "--server-args=-screen 0 1280x800x24", ...desktopArgs] + : desktopArgs; + const desktop = spawnLogged( + "desktop", + desktopCommand, + desktopCommandArgs, + { + cwd: rootDir, + env: { + ...commonEnv, + EXPO_PORT: String(expoPort), + EXPO_DEV_URL: `http://localhost:${expoPort}`, + PASEO_ELECTRON_REMOTE_DEBUGGING_PORT: String(cdpPort), + PASEO_ELECTRON_USER_DATA_DIR: userData, + PASEO_ELECTRON_FLAGS: `--remote-debugging-address=127.0.0.1 --remote-debugging-port=${cdpPort}`, + }, + }, + artifactDir, + ); + children.push(desktop.child); + await waitForPort(cdpPort, "Electron CDP", desktop); + + browser = await chromium.connectOverCDP(`http://127.0.0.1:${cdpPort}`); + const page = await waitForAppPage(browser, expoPort); + const status = await waitForDesktopStatus(page); + + const callerAgentId = await createCallerAgent(daemonPort); + const transport = new StreamableHTTPClientTransport( + new URL( + `http://127.0.0.1:${daemonPort}/mcp/agents?callerAgentId=${encodeURIComponent(callerAgentId)}`, + ), + ); + client = await experimental_createMCPClient({ transport }); + const report = await runRegression({ + page, + client, + serverId: status.serverId, + targetUrl: target.url, + }); + writeJson(path.join(artifactDir, "result.json"), report); + console.log( + `Browser tab bridge E2E passed: WebContents ${report.originalWebContentsId} -> ${report.replacementWebContentsId}; list, snapshot, click passed.`, + ); + } catch (error) { + console.error(`Browser tab bridge E2E failed. Artifacts: ${artifactDir}`); + console.error(error); + throw error; + } finally { + await client?.close().catch(() => undefined); + await browser?.close().catch(() => undefined); + for (const child of children.toReversed()) stopProcess(child); + await closeServer(target.server); + await delay(1_000); + try { + fs.rmSync(runtimeDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); + } catch (error) { + console.warn(`Failed to remove isolated E2E state ${runtimeDir}`, error); + } + } +} + +await main(); diff --git a/packages/desktop/scripts/dev-runner.mjs b/packages/desktop/scripts/dev-runner.mjs index 51ca14130..f2b16f822 100644 --- a/packages/desktop/scripts/dev-runner.mjs +++ b/packages/desktop/scripts/dev-runner.mjs @@ -19,6 +19,7 @@ if (!Number.isInteger(expoPort) || expoPort <= 0) { } const expoDevUrl = process.env.EXPO_DEV_URL || `http://localhost:${expoPort}`; +const electronArgs = process.argv.slice(2); const colorEnv = { FORCE_COLOR: process.env.FORCE_COLOR || "1", npm_config_color: process.env.npm_config_color || "always", @@ -175,7 +176,7 @@ try { } if (!stopping) { - spawnChild("electron", electron, [desktopDir], { + spawnChild("electron", electron, [...electronArgs, desktopDir], { detached: true, env: { ...process.env, diff --git a/packages/desktop/src/features/browser-automation/actionability.ts b/packages/desktop/src/features/browser-automation/actionability.ts index b5e48db76..0d2039904 100644 --- a/packages/desktop/src/features/browser-automation/actionability.ts +++ b/packages/desktop/src/features/browser-automation/actionability.ts @@ -100,8 +100,11 @@ function buildActionabilityScript(input: { const deadline = performance.now() + ${JSON.stringify(input.timeoutMs)}; const requiresEditable = ${JSON.stringify(input.editable)}; - const nextFrame = () => new Promise((resolve) => requestAnimationFrame(resolve)); const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + // Electron can suspend requestAnimationFrame while a guest is parked after + // workspace LRU eviction even though the guest remains CDP-controllable. + // Timed layout samples keep background browser automation live. + const waitForLayout = () => sleep(16); const nearlyEqual = (a, b) => Math.abs(a - b) < 0.25; const sameRect = (a, b) => nearlyEqual(a.x, b.x) && @@ -174,9 +177,9 @@ function buildActionabilityScript(input: { } element.scrollIntoView?.({ block: 'center', inline: 'center' }); - await nextFrame(); + await waitForLayout(); const firstRect = element.getBoundingClientRect(); - await nextFrame(); + await waitForLayout(); const secondRect = element.getBoundingClientRect(); if (!sameRect(firstRect, secondRect)) { detail = 'moving'; diff --git a/packages/desktop/src/features/browser-keyboard/index.test.ts b/packages/desktop/src/features/browser-keyboard/index.test.ts index 695750d21..2c792266b 100644 --- a/packages/desktop/src/features/browser-keyboard/index.test.ts +++ b/packages/desktop/src/features/browser-keyboard/index.test.ts @@ -21,11 +21,11 @@ class FakeBrowserContents { public readonly sent: SentMessage[] = []; private destroyed = false; private readonly destroyedListeners: Array<() => void> = []; - private domReadyListener: (() => void) | null = null; + private readonly domReadyListeners: Array<() => void> = []; private finishLoadListener: (() => void) | null = null; - private inputListener: - | ((event: { preventDefault(): void }, input: Electron.Input) => void) - | null = null; + private readonly inputListeners: Array< + (event: { preventDefault(): void }, input: Electron.Input) => void + > = []; public constructor(private readonly webContentsId: number) {} @@ -64,13 +64,12 @@ class FakeBrowserContents { return; } if (event === "dom-ready") { - this.domReadyListener = listener as () => void; + this.domReadyListeners.push(listener as () => void); return; } - this.inputListener = listener as ( - event: { preventDefault(): void }, - input: Electron.Input, - ) => void; + this.inputListeners.push( + listener as (event: { preventDefault(): void }, input: Electron.Input) => void, + ); } public send(channel: string, payload: unknown): void { @@ -105,19 +104,23 @@ class FakeBrowserContents { } public domReady(): void { - this.domReadyListener?.(); + for (const listener of this.domReadyListeners) { + listener(); + } } public input(input: Electron.Input): boolean { let wasPrevented = false; - this.inputListener?.( - { - preventDefault: () => { - wasPrevented = true; + for (const listener of this.inputListeners) { + listener( + { + preventDefault: () => { + wasPrevented = true; + }, }, - }, - input, - ); + input, + ); + } return wasPrevented; } } @@ -196,6 +199,20 @@ describe("BrowserKeyboard", () => { ]); }); + test("handles a reserved shortcut once when the same guest attaches again", () => { + const { attach } = createBrowserKeyboard(); + const guest = new FakeBrowserContents(53); + const host = new FakeBrowserContents(54); + attach({ browserId: "browser-a", contents: guest, hostContents: host }); + attach({ browserId: "browser-a", contents: guest, hostContents: host }); + const command = process.platform === "darwin" ? { meta: true } : { control: true }; + + const wasPrevented = guest.input(electronInput({ ...command, code: "KeyR", key: "r" })); + + expect(wasPrevented).toBe(true); + expect(guest.reloads).toEqual(["reload"]); + }); + test("republishes the latest shortcut policy when the next guest document is ready", () => { const { attach, keyboard } = createBrowserKeyboard(); const guest = new FakeBrowserContents(61); diff --git a/packages/desktop/src/features/browser-keyboard/index.ts b/packages/desktop/src/features/browser-keyboard/index.ts index 8084ddb97..0d90f019f 100644 --- a/packages/desktop/src/features/browser-keyboard/index.ts +++ b/packages/desktop/src/features/browser-keyboard/index.ts @@ -87,6 +87,13 @@ export class BrowserKeyboard { if (!registration || registration.hostWebContentsId !== input.hostContents.id) { return; } + const attachedGuest = this.attachedGuestsByWebContentsId.get(webContentsId); + if ( + attachedGuest?.contents === input.contents && + attachedGuest.hostContents === input.hostContents + ) { + return; + } const guest: BrowserKeyboardGuest = input; this.attachedGuestsByWebContentsId.set(webContentsId, guest); diff --git a/packages/desktop/src/features/browser-webviews/registry.test.ts b/packages/desktop/src/features/browser-webviews/registry.test.ts index 93b8c353b..5ef7a6cf5 100644 --- a/packages/desktop/src/features/browser-webviews/registry.test.ts +++ b/packages/desktop/src/features/browser-webviews/registry.test.ts @@ -33,6 +33,32 @@ describe("PaseoBrowserWebviewRegistry", () => { expect(registry.getActiveBrowserIdForHostWindow(101)).toBe("browser-a"); }); + it("keeps the active browser when the same guest registers again", () => { + const registry = new PaseoBrowserWebviewRegistry(); + + registry.registerWebContents({ + webContentsId: 1, + browserId: "browser-a", + hostWebContentsId: 101, + }); + registry.setWorkspaceActiveBrowser({ + hostWebContentsId: 101, + workspaceId: "workspace-a", + browserId: "browser-a", + }); + + registry.registerWebContents({ + webContentsId: 1, + browserId: "browser-a", + hostWebContentsId: 101, + }); + + expect(registry.getActiveBrowserIdForWorkspaceInHostWindow(101, "workspace-a")).toBe( + "browser-a", + ); + expect(registry.getWebContentsIdForBrowserInHostWindow(101, "browser-a")).toBe(1); + }); + it("ignores stale destroy events after a duplicate browserId moved", () => { const registry = new PaseoBrowserWebviewRegistry(); diff --git a/packages/desktop/src/features/browser-webviews/registry.ts b/packages/desktop/src/features/browser-webviews/registry.ts index 4e49fdf6e..4fb8381f3 100644 --- a/packages/desktop/src/features/browser-webviews/registry.ts +++ b/packages/desktop/src/features/browser-webviews/registry.ts @@ -21,6 +21,14 @@ export class PaseoBrowserWebviewRegistry { }): void { const hostBrowserKey = this.hostBrowserKey(input.hostWebContentsId, input.browserId); const replacedWebContentsId = this.webContentsIdsByHostAndBrowserId.get(hostBrowserKey); + const existingRegistration = this.registrationsByWebContentsId.get(input.webContentsId); + if ( + replacedWebContentsId === input.webContentsId && + existingRegistration?.browserId === input.browserId && + existingRegistration.hostWebContentsId === input.hostWebContentsId + ) { + return; + } if (replacedWebContentsId !== undefined && replacedWebContentsId !== input.webContentsId) { this.removeWebContents(replacedWebContentsId, { preserveActiveBrowser: true }); } From dfada2a556a5013899214dea75fad874723f084c Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Fri, 17 Jul 2026 14:03:43 +0200 Subject: [PATCH 07/38] fix(app): update provider icon --- packages/app/src/components/icons/omp-icon.tsx | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/packages/app/src/components/icons/omp-icon.tsx b/packages/app/src/components/icons/omp-icon.tsx index b674d1744..23780a5a8 100644 --- a/packages/app/src/components/icons/omp-icon.tsx +++ b/packages/app/src/components/icons/omp-icon.tsx @@ -1,4 +1,4 @@ -import Svg, { Circle, Rect } from "react-native-svg"; +import Svg, { Path } from "react-native-svg"; interface OmpIconProps { size?: number; @@ -6,17 +6,9 @@ interface OmpIconProps { } export function OmpIcon({ size = 16, color = "currentColor" }: OmpIconProps) { - // Ported from the Oh My Pi MIT-licensed assets/icon.svg mark. return ( - - - - - - - - - + + ); } From df2b7cab46f42538c3c7240da14a8bc426eb0354 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Fri, 17 Jul 2026 15:07:08 +0200 Subject: [PATCH 08/38] Document release version classification --- .agents/skills/release-beta/SKILL.md | 4 +- .agents/skills/release-stable/SKILL.md | 8 ++-- docs/release.md | 57 +++++++++++++++++++------- 3 files changed, 51 insertions(+), 18 deletions(-) diff --git a/.agents/skills/release-beta/SKILL.md b/.agents/skills/release-beta/SKILL.md index e763b97a7..fa8b72928 100644 --- a/.agents/skills/release-beta/SKILL.md +++ b/.agents/skills/release-beta/SKILL.md @@ -8,4 +8,6 @@ user-invocable: true Read `docs/release.md` in the Paseo repo and follow the **Beta flow** section end-to-end. Run the **Beta release** completion checklist at the bottom of that doc. -Key rules the doc enforces — each beta updates an in-place `CHANGELOG.md` entry (`## X.Y.Z-beta.N`) that gets overwritten at promotion (never leave a stale `-beta.N` heading behind), and betas publish npm only with the explicit `beta` dist-tag. +During preparation, classify the previous-stable-to-`HEAD` diff as patch or minor and show the target version and rationale to the user. Agents never select a major version autonomously. + +Each beta updates an in-place `CHANGELOG.md` entry (`## X.Y.Z-beta.N`) that gets overwritten at promotion, and npm publishes only on the explicit `beta` dist-tag. diff --git a/.agents/skills/release-stable/SKILL.md b/.agents/skills/release-stable/SKILL.md index f1dd8508e..9755e33e3 100644 --- a/.agents/skills/release-stable/SKILL.md +++ b/.agents/skills/release-stable/SKILL.md @@ -1,11 +1,13 @@ --- name: release-stable -description: Cut a stable release of Paseo (fresh patch or promote from beta). Use when the user says "release stable", "ship stable", "promote", "release:patch", "release:promote", or "/release-stable". +description: Cut a stable release of Paseo (fresh patch or minor, or promote from beta). Use when the user says "release stable", "ship stable", "promote", "release:patch", "release:minor", "release:promote", or "/release-stable". user-invocable: true --- # Release stable -Read `docs/release.md` in the Paseo repo and follow the **Standard release (patch)** flow if cutting fresh, or the **Beta flow** promotion step if promoting an existing beta. Run the **Stable release (or promotion)** completion checklist at the bottom of that doc. +Read `docs/release.md` in the Paseo repo and follow the **Standard release (stable)** flow if cutting fresh, or the **Beta flow** promotion step if promoting an existing beta. Run the **Stable release (or promotion)** completion checklist at the bottom of that doc. -The doc covers the changelog (required for stable), the pre-release sanity check (required for stable), and the post-release babysit pattern. Don't skip steps. +For a fresh release, classify the previous-stable-to-`HEAD` diff as patch or minor and show the target version and rationale to the user. Agents never select a major version autonomously. + +The doc covers the changelog, pre-release sanity check, and post-release babysit pattern. Don't skip steps. diff --git a/docs/release.md b/docs/release.md index 8252898ca..ed40fa278 100644 --- a/docs/release.md +++ b/docs/release.md @@ -12,6 +12,8 @@ A release has exactly two steps. The agent does the first, the user authorizes t - ACP provider catalog drift checked with `npm run acp:version-drift:check`; if stale package-runner pins are intentional, say so explicitly, otherwise run `npm run acp:version-drift:update` and commit the updated catalog +- classify the previous-stable-to-`HEAD` diff as patch or minor, then show the + target version and rationale to the user - draft the changelog, show it to the user, wait for review - run the pre-release sanity check, surface findings to the user - confirm CI is green @@ -36,16 +38,40 @@ There are two supported ways to ship from `main`: 1. **Direct stable release**: you are ready to ship the current `main` commit to everyone immediately. 2. **Beta flow**: release candidates on the `beta` channel. Betas carry an in-place changelog entry (beta users check it), publish npm only on the explicit `beta` dist-tag, and never move the website download target off the latest stable. -## Standard release (patch) +## Release version decision -Before running any stable patch release command: +Every fresh release starts by classifying the full previous-stable-to-`HEAD` +diff. The highest-impact change determines the version: + +- **Minor** — a user would experience the release as a significant upgrade. This + includes substantial new workflows, providers, forges, platforms, integrations, + or meaningful expansions of existing capabilities. Foundational internal work + also qualifies when it materially changes reliability, performance, + compatibility, deployment, or operation; diff size alone does not. +- **Patch** — fixes, polish, small enhancements, and reliability or performance + improvements within existing capabilities. Follow-up corrections to a minor + release are patches. + +The release agent selects patch or minor during preparation and presents the +target version with the changelog for approval. Agents never select a major +version autonomously. A major release requires an explicit user instruction and +approval; Paseo remains on major version zero until that deliberate decision. + +Version bumps are never used to retry a failed build. Retry the existing version +as described in **Fixing a failed release build**. + +## Standard release (stable) + +Before running any stable release command: - Make sure the intended release commit is already committed to `main` and the working tree is clean. - **Run `npm run format`, `npm run lint`, and `npm run typecheck` and commit any resulting changes BEFORE you start any `release:*` command.** `release:check` runs `npm install --workspaces --include-workspace-root` as part of `release:prepare`, which can mutate `package-lock.json` (e.g. churning `"dev": true` markers on optional deps). The next step, `version:all:*`, runs `npm version` which aborts when the working tree is dirty. If this happens mid-flight you have to commit the lockfile churn before retrying — and the pre-commit format hook will reject a lockfile-only commit because oxfmt internally skips `package-lock.json` while lefthook's glob still matches it. Avoid the whole mess by running format/lint/typecheck first, then `release:prepare` once on its own to absorb any lockfile churn into a normal commit, then start the release. -- Do not use `npm run release:patch` as a substitute for checking whether the current commit is actually ready. +- Do not use a release command as a substitute for checking whether the current commit is actually ready. ```bash +# Run exactly one, matching the approved decision: npm run release:patch +npm run release:minor ``` This bumps the version across all workspaces, runs checks, publishes to npm, and pushes the branch + tag. The tag push triggers `Desktop Release`, `Android APK Release`, `Docker`, and `Release Notes Sync` on GitHub Actions. EAS picks up the same tag via the EAS GitHub app and starts the iOS + Android store builds in parallel (see "Mobile builds (EAS)" below) — there is no `release-mobile.yml` in this repo. @@ -54,8 +80,6 @@ The Docker workflow builds images from the checked-out source tree on pull reque Relay deployment is manual-only while `relay.paseo.sh` bridges traffic to the Fly deployment. Releases and pushes to `main` do not deploy the Cloudflare relay worker. Deploy it explicitly with `gh workflow run deploy-relay.yml` only when the production bridge should change. -**Releases are always patch.** "Release paseo", "release stable", "ship stable", and similar always mean a patch bump from the previous stable. Never bump minor or major to trigger a build, ever — minor and major bumps are reserved for genuinely larger product cuts and require an explicit user instruction with the word "minor" or "major". If you find yourself reaching for `release:minor` to retrigger a failed build, you are doing the wrong thing — push a retry tag instead (see "Fixing a failed release build" below). - **Stable means stable.** If the user says "stable" or "ship stable", do not ask whether they want a beta first. They picked stable; treat it as a direct stable release. Only run the beta flow when the user explicitly says "beta". ## Manual step-by-step @@ -63,7 +87,9 @@ Relay deployment is manual-only while `relay.paseo.sh` bridges traffic to the Fl ```bash npm run typecheck # Verify the exact commit you intend to release npm run release:check # Typecheck, build, dry-run pack -npm run version:all:patch # Bump version, create commit + tag +# Run exactly one approved version command: +npm run version:all:patch +npm run version:all:minor npm run release:publish # Publish to npm npm run release:push # Push HEAD + tag (triggers CI workflows) ``` @@ -71,7 +97,8 @@ npm run release:push # Push HEAD + tag (triggers CI workflows) ## Beta flow ```bash -npm run release:beta:patch # Bump to X.Y.Z-beta.1, publish npm beta, push commit + tag +npm run release:beta:patch # Start the next patch beta line +npm run release:beta:minor # Start the next minor beta line # ... test desktop and APK prerelease assets from GitHub Releases ... npm run release:beta:next # Optional: cut X.Y.Z-beta.2, beta.3, ... npm run release:promote # Promote X.Y.Z-beta.N to stable X.Y.Z @@ -107,7 +134,7 @@ Updater clients only discover a release through those `.yml` manifests, so there ### Default behavior -`npm run release:patch` → tag push → 36h ramp. No extra action needed. +`npm run release:patch` or `npm run release:minor` → tag push → 36h ramp. No extra action needed. The `rollout_hours` input on `desktop-release.yml` is **only read on `workflow_dispatch`** — tag-push runs always default to 36. To get any other rollout duration on a fresh release, use the post-publish flip below. @@ -127,7 +154,7 @@ gh workflow run desktop-rollout.yml \ **Why this is gap-free:** `desktop-release.yml`'s `finalize-rollout` job and `desktop-rollout.yml` share the concurrency group `desktop-rollout-`. Dispatching `desktop-rollout.yml` while the tag-push pipeline is still running queues it safely behind `finalize-rollout`. The first public manifests already carry `rolloutHours=36`, then `desktop-rollout.yml` flips them to `rolloutHours=0` shortly afterward. The renderer polls every 30 minutes, so active stable users pick up the new manifest on their next check. -Run the dispatch right after `release:patch` returns. Don't wait for the tag-push CI to finish. +Run the dispatch right after `release:patch` or `release:minor` returns. Don't wait for the tag-push CI to finish. ### Adjusting an already-published release @@ -165,7 +192,7 @@ gh workflow run desktop-release.yml \ -f rollout_hours=6 ``` -This does **not** apply to fresh releases cut via `npm run release:patch` — that path always tag-pushes and stamps 36. For a fresh release with a custom ramp, cut normally and then dispatch `desktop-rollout.yml` (same pattern as the instant-admit flow above, with your chosen `rollout_hours`). +This does **not** apply to fresh releases cut via `npm run release:patch` or `npm run release:minor` — those paths always tag-push and stamp 36. For a fresh release with a custom ramp, cut normally and then dispatch `desktop-rollout.yml` (same pattern as the instant-admit flow above, with your chosen `rollout_hours`). ### Releasing during an active rollout @@ -463,7 +490,8 @@ Betas are checkpoints along the way; the entry is the single record for the jump - [ ] Working tree is clean and the intended commit is on `main` - [ ] Update the in-place beta entry in `CHANGELOG.md` (heading `## X.Y.Z-beta.N - YYYY-MM-DD`), review it against the changelog policy, get approval, and commit it before cutting the release -- [ ] `npm run release:beta:patch` (or `:next`) completes successfully +- [ ] The previous-stable-to-`HEAD` diff is classified as patch or minor, with the target version and rationale approved +- [ ] `npm run release:beta:patch`, `npm run release:beta:minor`, or `npm run release:beta:next` completes successfully - [ ] npm shows the version under the `beta` dist-tag, not `latest` - [ ] GitHub `Desktop Release` workflow for the `v*-beta.N` tag is green - [ ] GitHub `Android APK Release` workflow for the same tag is green @@ -472,11 +500,12 @@ Betas are checkpoints along the way; the entry is the single record for the jump ### Stable release (or promotion) - [ ] Run the pre-release sanity check (see above) and address any findings -- [ ] Ensure the intended release commit is already committed and the git worktree is clean before running any `release:*` patch/promote command -- [ ] Ensure local `npm run typecheck` passes on that exact commit before running any `release:*` patch/promote command +- [ ] The previous-stable-to-`HEAD` diff is classified as patch or minor, with the target version and rationale approved +- [ ] Ensure the intended release commit is already committed and the git worktree is clean before running any release command +- [ ] Ensure local `npm run typecheck` passes on that exact commit before running any release command - [ ] Update `CHANGELOG.md` with user-facing release notes (features, fixes — not refactors). When promoting from beta, overwrite the existing `## X.Y.Z-beta.N` heading in place (heading → `X.Y.Z`, date → promotion day) — do not add a new entry on top of the beta one - [ ] Verify the changelog heading follows strict `## X.Y.Z - YYYY-MM-DD` format -- [ ] `npm run release:patch` or `npm run release:promote` completes successfully +- [ ] `npm run release:patch`, `npm run release:minor`, or `npm run release:promote` completes successfully - [ ] GitHub `Desktop Release` workflow for the `v*` tag is green - [ ] GitHub `Android APK Release` workflow for the same tag is green - [ ] EAS `Release Mobile` workflow for the same tag is green From 293f55afc43200c69c9392b3b688be5580c658ab Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Fri, 17 Jul 2026 15:07:14 +0200 Subject: [PATCH 09/38] Update ACP provider catalog versions --- packages/app/src/data/acp-provider-catalog.ts | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/packages/app/src/data/acp-provider-catalog.ts b/packages/app/src/data/acp-provider-catalog.ts index b4bec91d2..a49a63589 100644 --- a/packages/app/src/data/acp-provider-catalog.ts +++ b/packages/app/src/data/acp-provider-catalog.ts @@ -59,10 +59,10 @@ const CATALOG_DATA = [ title: "Cline", description: "Autonomous coding agent CLI - capable of creating/editing files, running commands, using the browser, and more", - version: "3.0.39", + version: "3.0.44", iconId: "cline", installLink: "https://cline.bot/cli", - command: ["npx", "-y", "cline@3.0.39", "--acp"], + command: ["npx", "-y", "cline@3.0.44", "--acp"], }, { id: "codebuddy-code", @@ -122,10 +122,10 @@ const CATALOG_DATA = [ id: "deepagents", title: "DeepAgents", description: "Batteries-included AI coding and general purpose agent powered by LangChain.", - version: "0.1.17", + version: "0.1.19", iconId: "deepagents", installLink: "https://docs.langchain.com/oss/javascript/deepagents/overview", - command: ["npx", "-y", "deepagents-acp@0.1.17"], + command: ["npx", "-y", "deepagents-acp@0.1.19"], }, { id: "devin", @@ -140,29 +140,29 @@ const CATALOG_DATA = [ id: "dimcode", title: "DimCode", description: "A coding agent that puts leading models at your command.", - version: "0.2.27", + version: "0.2.31", iconId: "dimcode", installLink: "https://dimcode.dev/docs/acp.html", - command: ["npx", "-y", "dimcode@0.2.27", "acp"], + command: ["npx", "-y", "dimcode@0.2.31", "acp"], }, { id: "dirac", title: "Dirac", description: "Reduces API costs by more than 50%, produces better and faster work. Uses Hash anchored parallel edits, AST manipulation and a whole lot of neat optimizations. Fully Open Source.", - version: "0.4.15", + version: "0.4.17", iconId: "dirac", installLink: "https://dirac.run", - command: ["npx", "-y", "dirac-cli@0.4.15", "--acp"], + command: ["npx", "-y", "dirac-cli@0.4.17", "--acp"], }, { id: "factory-droid", title: "Factory Droid", description: "Factory Droid - AI coding agent powered by Factory AI", - version: "0.170.0", + version: "0.174.0", iconId: "factory-droid", installLink: "https://factory.ai/product/cli", - command: ["npx", "-y", "droid@0.170.0", "exec", "--output-format", "acp-daemon"], + command: ["npx", "-y", "droid@0.174.0", "exec", "--output-format", "acp-daemon"], env: { DROID_DISABLE_AUTO_UPDATE: "true", FACTORY_DROID_AUTO_UPDATE_ENABLED: "false", @@ -173,19 +173,19 @@ const CATALOG_DATA = [ id: "fast-agent", title: "fast-agent", description: "Code and build agents with comprehensive multi-provider support", - version: "0.9.7", + version: "0.9.14", iconId: "fast-agent", installLink: "https://fast-agent.ai/acp/", - command: ["uvx", "--from", "fast-agent-acp==0.9.7", "fast-agent-acp", "-x"], + command: ["uvx", "--from", "fast-agent-acp==0.9.14", "fast-agent-acp", "-x"], }, { id: "gemini", title: "Gemini CLI", description: "Google's official CLI for Gemini", - version: "0.50.0", + version: "0.51.0", iconId: "gemini", installLink: "https://geminicli.com", - command: ["npx", "-y", "@google/gemini-cli@0.50.0", "--acp"], + command: ["npx", "-y", "@google/gemini-cli@0.51.0", "--acp"], }, { id: "glm-acp-agent", @@ -284,10 +284,10 @@ const CATALOG_DATA = [ id: "nova", title: "Nova", description: "Nova by Compass AI - a fully-fledged software engineer at your command", - version: "1.1.25", + version: "1.1.27", iconId: "nova", installLink: "https://www.compassap.ai/portfolio/nova.html", - command: ["npx", "-y", "@compass-ai/nova@1.1.25", "acp"], + command: ["npx", "-y", "@compass-ai/nova@1.1.27", "acp"], }, { id: "poolside", @@ -302,19 +302,19 @@ const CATALOG_DATA = [ id: "qoder", title: "Qoder CLI", description: "AI coding assistant with agentic capabilities", - version: "1.0.43", + version: "1.0.48", iconId: "qoder", installLink: "https://qoder.com", - command: ["npx", "-y", "@qoder-ai/qodercli@1.0.43", "--acp"], + command: ["npx", "-y", "@qoder-ai/qodercli@1.0.48", "--acp"], }, { id: "qwen-code", title: "Qwen Code", description: "Alibaba's Qwen coding assistant", - version: "0.19.9", + version: "0.19.11", iconId: "qwen-code", installLink: "https://qwenlm.github.io/qwen-code-docs/en/users/overview", - command: ["npx", "-y", "@qwen-code/qwen-code@0.19.9", "--acp", "--experimental-skills"], + command: ["npx", "-y", "@qwen-code/qwen-code@0.19.11", "--acp", "--experimental-skills"], }, { id: "sigit", From c0f80e2477d1c3781381d3a69dab6b35365bcf4b Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Fri, 17 Jul 2026 15:07:21 +0200 Subject: [PATCH 10/38] Prepare 0.2.0 beta changelog --- CHANGELOG.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b9af2fc50..2bd2895dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,33 @@ # Changelog +## 0.2.0-beta.1 - 2026-07-17 + +### Added + +- Work with pull requests and merge requests from GitLab, Gitea, Forgejo, and Codeberg ([#1913](https://github.com/getpaseo/paseo/pull/1913) by [@nllptrx](https://github.com/nllptrx)) +- Use Oh My Pi as a native agent provider ([#2067](https://github.com/getpaseo/paseo/pull/2067) by [@ebg1223](https://github.com/ebg1223)) +- Browse branch commit history and open individual commit diffs from Changes ([#1534](https://github.com/getpaseo/paseo/pull/1534), [#2146](https://github.com/getpaseo/paseo/pull/2146) by [@adradr](https://github.com/adradr)) +- Open workspace files in more installed editors and file managers ([#2119](https://github.com/getpaseo/paseo/pull/2119)) +- Remove individual custom providers from Settings ([#1951](https://github.com/getpaseo/paseo/pull/1951)) + +### Improved + +- ACP provider catalog updated to the latest registry versions +- Workspace focus mode stays confined to the active workspace with a visible exit control ([#2151](https://github.com/getpaseo/paseo/pull/2151)) +- Desktop installs the newest available update instead of a cached older release ([#2149](https://github.com/getpaseo/paseo/pull/2149)) +- Remote daemon update failures now show specific recovery steps ([#2120](https://github.com/getpaseo/paseo/pull/2120)) +- Agent history errors now appear immediately instead of after a timeout ([#2124](https://github.com/getpaseo/paseo/pull/2124)) + +### Fixed + +- Terminal panes no longer remain at 80x24 after focus or visibility changes ([#2059](https://github.com/getpaseo/paseo/pull/2059), [#2154](https://github.com/getpaseo/paseo/pull/2154) by [@cleiter](https://github.com/cleiter)) +- Sign-in popups in the desktop browser now complete successfully ([#2137](https://github.com/getpaseo/paseo/pull/2137)) +- Browser typing and shortcuts no longer submit the active Paseo prompt ([#1982](https://github.com/getpaseo/paseo/pull/1982)) +- Agent browser tabs remain controllable after switching workspaces ([#2156](https://github.com/getpaseo/paseo/pull/2156)) +- Archived workspaces now show the correct Unarchive or Restore action ([#2002](https://github.com/getpaseo/paseo/pull/2002)) +- Archived sessions can be reimported into the current workspace ([#2123](https://github.com/getpaseo/paseo/pull/2123)) +- Browser shortcuts no longer appear where browser tabs are unavailable ([#2116](https://github.com/getpaseo/paseo/pull/2116) by [@jasonhnd](https://github.com/jasonhnd)) + ## 0.1.110 - 2026-07-16 ### Fixed From 0bec06c2db7d3ee071416cde80229eabd682b03e Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Fri, 17 Jul 2026 15:10:06 +0200 Subject: [PATCH 11/38] chore(release): cut 0.2.0-beta.1 --- package-lock.json | 42 ++++++++++++------------ package.json | 2 +- packages/app/package.json | 2 +- packages/cli/package.json | 8 ++--- packages/client/package.json | 6 ++-- packages/desktop/package.json | 2 +- packages/expo-two-way-audio/package.json | 2 +- packages/highlight/package.json | 2 +- packages/protocol/package.json | 2 +- packages/relay/package.json | 2 +- packages/server/package.json | 10 +++--- packages/website/package.json | 2 +- 12 files changed, 41 insertions(+), 41 deletions(-) diff --git a/package-lock.json b/package-lock.json index 297bdcbd1..3e04157f9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "paseo", - "version": "0.1.110", + "version": "0.2.0-beta.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "paseo", - "version": "0.1.110", + "version": "0.2.0-beta.1", "hasInstallScript": true, "license": "AGPL-3.0-or-later", "workspaces": [ @@ -35162,7 +35162,7 @@ }, "packages/app": { "name": "@getpaseo/app", - "version": "0.1.110", + "version": "0.2.0-beta.1", "dependencies": { "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", @@ -36181,12 +36181,12 @@ }, "packages/cli": { "name": "@getpaseo/cli", - "version": "0.1.110", + "version": "0.2.0-beta.1", "dependencies": { "@clack/prompts": "^1.0.0", - "@getpaseo/client": "0.1.110", - "@getpaseo/protocol": "0.1.110", - "@getpaseo/server": "0.1.110", + "@getpaseo/client": "0.2.0-beta.1", + "@getpaseo/protocol": "0.2.0-beta.1", + "@getpaseo/server": "0.2.0-beta.1", "chalk": "^5.3.0", "commander": "^12.0.0", "mime-types": "^2.1.35", @@ -36432,10 +36432,10 @@ }, "packages/client": { "name": "@getpaseo/client", - "version": "0.1.110", + "version": "0.2.0-beta.1", "dependencies": { - "@getpaseo/protocol": "0.1.110", - "@getpaseo/relay": "0.1.110", + "@getpaseo/protocol": "0.2.0-beta.1", + "@getpaseo/relay": "0.2.0-beta.1", "zod": "^4.4.3" }, "devDependencies": { @@ -36446,7 +36446,7 @@ }, "packages/desktop": { "name": "@getpaseo/desktop", - "version": "0.1.110", + "version": "0.2.0-beta.1", "license": "AGPL-3.0-or-later", "dependencies": { "@getpaseo/cli": "*", @@ -36689,7 +36689,7 @@ }, "packages/expo-two-way-audio": { "name": "@getpaseo/expo-two-way-audio", - "version": "0.1.110", + "version": "0.2.0-beta.1", "license": "MIT", "devDependencies": { "@types/jest": "^29.5.14", @@ -37585,7 +37585,7 @@ }, "packages/highlight": { "name": "@getpaseo/highlight", - "version": "0.1.110", + "version": "0.2.0-beta.1", "dependencies": { "@codemirror/language": "^6.12.3", "@codemirror/legacy-modes": "^6.5.3", @@ -37817,7 +37817,7 @@ }, "packages/protocol": { "name": "@getpaseo/protocol", - "version": "0.1.110", + "version": "0.2.0-beta.1", "dependencies": { "zod": "^4.4.3" }, @@ -37830,7 +37830,7 @@ }, "packages/relay": { "name": "@getpaseo/relay", - "version": "0.1.110", + "version": "0.2.0-beta.1", "dependencies": { "base64-js": "^1.5.1", "tweetnacl": "^1.0.3", @@ -38048,15 +38048,15 @@ }, "packages/server": { "name": "@getpaseo/server", - "version": "0.1.110", + "version": "0.2.0-beta.1", "dependencies": { "@agentclientprotocol/sdk": "^0.17.1", "@anthropic-ai/claude-agent-sdk": "^0.3.195", "@anthropic-ai/sdk": "^0.104.2", - "@getpaseo/client": "0.1.110", - "@getpaseo/highlight": "0.1.110", - "@getpaseo/protocol": "0.1.110", - "@getpaseo/relay": "0.1.110", + "@getpaseo/client": "0.2.0-beta.1", + "@getpaseo/highlight": "0.2.0-beta.1", + "@getpaseo/protocol": "0.2.0-beta.1", + "@getpaseo/relay": "0.2.0-beta.1", "@isaacs/ttlcache": "^2.1.4", "@modelcontextprotocol/sdk": "^1.20.1", "@opencode-ai/sdk": "1.14.46", @@ -38593,7 +38593,7 @@ }, "packages/website": { "name": "@getpaseo/website", - "version": "0.1.110", + "version": "0.2.0-beta.1", "dependencies": { "@cloudflare/vite-plugin": "^1.29.1", "@cloudflare/workers-types": "^4.20260317.1", diff --git a/package.json b/package.json index c77a52058..98fa77d80 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "paseo", - "version": "0.1.110", + "version": "0.2.0-beta.1", "private": true, "description": "Paseo: voice-controlled development environment for local AI coding agents", "keywords": [ diff --git a/packages/app/package.json b/packages/app/package.json index 5eb53ba0a..d173ff467 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/app", - "version": "0.1.110", + "version": "0.2.0-beta.1", "private": true, "main": "index.ts", "scripts": { diff --git a/packages/cli/package.json b/packages/cli/package.json index e4e158885..84a4e314a 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/cli", - "version": "0.1.110", + "version": "0.2.0-beta.1", "description": "Paseo CLI - control your AI coding agents from the command line", "bin": { "paseo": "bin/paseo" @@ -27,9 +27,9 @@ }, "dependencies": { "@clack/prompts": "^1.0.0", - "@getpaseo/client": "0.1.110", - "@getpaseo/protocol": "0.1.110", - "@getpaseo/server": "0.1.110", + "@getpaseo/client": "0.2.0-beta.1", + "@getpaseo/protocol": "0.2.0-beta.1", + "@getpaseo/server": "0.2.0-beta.1", "chalk": "^5.3.0", "commander": "^12.0.0", "mime-types": "^2.1.35", diff --git a/packages/client/package.json b/packages/client/package.json index 1bc2c5c20..f6485f634 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/client", - "version": "0.1.110", + "version": "0.2.0-beta.1", "description": "Paseo client SDK package", "files": [ "dist", @@ -35,8 +35,8 @@ "test": "vitest run" }, "dependencies": { - "@getpaseo/protocol": "0.1.110", - "@getpaseo/relay": "0.1.110", + "@getpaseo/protocol": "0.2.0-beta.1", + "@getpaseo/relay": "0.2.0-beta.1", "zod": "^4.4.3" }, "devDependencies": { diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 4173fae13..ebf9dfec2 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/desktop", - "version": "0.1.110", + "version": "0.2.0-beta.1", "private": true, "description": "Paseo desktop app (Electron wrapper)", "homepage": "https://paseo.sh", diff --git a/packages/expo-two-way-audio/package.json b/packages/expo-two-way-audio/package.json index 04bded4ad..ef9b00bf0 100644 --- a/packages/expo-two-way-audio/package.json +++ b/packages/expo-two-way-audio/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/expo-two-way-audio", - "version": "0.1.110", + "version": "0.2.0-beta.1", "description": "Native module for two way audio streaming", "keywords": [ "ExpoTwoWayAudio", diff --git a/packages/highlight/package.json b/packages/highlight/package.json index c5c00b134..ed86dd816 100644 --- a/packages/highlight/package.json +++ b/packages/highlight/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/highlight", - "version": "0.1.110", + "version": "0.2.0-beta.1", "files": [ "dist", "!dist/**/*.map" diff --git a/packages/protocol/package.json b/packages/protocol/package.json index 8627cba16..510625f6d 100644 --- a/packages/protocol/package.json +++ b/packages/protocol/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/protocol", - "version": "0.1.110", + "version": "0.2.0-beta.1", "description": "Paseo shared protocol schemas and wire types", "files": [ "dist", diff --git a/packages/relay/package.json b/packages/relay/package.json index 5877b3feb..1de1235e5 100644 --- a/packages/relay/package.json +++ b/packages/relay/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/relay", - "version": "0.1.110", + "version": "0.2.0-beta.1", "description": "Paseo relay for bridging daemon and client connections", "files": [ "dist", diff --git a/packages/server/package.json b/packages/server/package.json index c6f12ebca..408e012bc 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/server", - "version": "0.1.110", + "version": "0.2.0-beta.1", "description": "Paseo backend server", "files": [ "dist/server", @@ -67,10 +67,10 @@ "@agentclientprotocol/sdk": "^0.17.1", "@anthropic-ai/claude-agent-sdk": "^0.3.195", "@anthropic-ai/sdk": "^0.104.2", - "@getpaseo/client": "0.1.110", - "@getpaseo/highlight": "0.1.110", - "@getpaseo/protocol": "0.1.110", - "@getpaseo/relay": "0.1.110", + "@getpaseo/client": "0.2.0-beta.1", + "@getpaseo/highlight": "0.2.0-beta.1", + "@getpaseo/protocol": "0.2.0-beta.1", + "@getpaseo/relay": "0.2.0-beta.1", "@isaacs/ttlcache": "^2.1.4", "@modelcontextprotocol/sdk": "^1.20.1", "@opencode-ai/sdk": "1.14.46", diff --git a/packages/website/package.json b/packages/website/package.json index adfbf8fce..efbdd7abc 100644 --- a/packages/website/package.json +++ b/packages/website/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/website", - "version": "0.1.110", + "version": "0.2.0-beta.1", "private": true, "type": "module", "scripts": { From 6f753a142d5e276e4a7089ad17f76997ca44863f Mon Sep 17 00:00:00 2001 From: "paseo-ai[bot]" <266920839+paseo-ai[bot]@users.noreply.github.com> Date: Fri, 17 Jul 2026 13:18:56 +0000 Subject: [PATCH 12/38] fix: update lockfile signatures and Nix hash [skip ci] --- nix/npm-deps.hash | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nix/npm-deps.hash b/nix/npm-deps.hash index bb3948a0b..2b54e735b 100644 --- a/nix/npm-deps.hash +++ b/nix/npm-deps.hash @@ -1 +1 @@ -sha256-DL1LamUyFzJOkPYR7eeIefGhzP/mcWGO5oxld/Bt8n0= +sha256-ETZ7MRrBJ3ksGI1yK5oHvtLCDvxXXUcFH+W0myT1YOc= From 6c99efae52cccb5426f9c300fafaf50ab5447027 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Fri, 17 Jul 2026 15:48:06 +0200 Subject: [PATCH 13/38] fix(android): give release builds more memory --- docs/android.md | 2 ++ packages/app/eas.json | 1 + 2 files changed, 3 insertions(+) diff --git a/docs/android.md b/docs/android.md index ec75faa7c..69a31b5b5 100644 --- a/docs/android.md +++ b/docs/android.md @@ -122,6 +122,8 @@ The flag must be present for both prebuild and Gradle because Gradle starts Metr Keep the excluded npm packages installed. Normal builds use them, while the F-Droid profile removes only their Android native modules and config plugins. Paseo always applies `expo-gradle-jvmargs` with `-Xmx4096m` and `-XX:MaxMetaspaceSize=1024m` so local Expo prebuilds have enough Gradle heap whether they use precompiled AARs or source-built Expo modules. +The EAS `production-apk` profile uses the large Android resource class. Release builds compile the native ABIs and run Hermes bundling in the same Gradle invocation; the default worker can exhaust its remaining memory and kill Hermes with exit code 137 even when Gradle's own heap is correctly sized. + ### React version lockstep Keep `react` and `react-dom` pinned to the React version embedded by the current `react-native` release. React Native `0.81.x` embeds `react-native-renderer` `19.1.0`, so `packages/app` must use React `19.1.0`. Bumping React to a newer patch can build successfully but crash at JS startup on Android with `Incompatible React versions`, leaving the app on the native splash screen. diff --git a/packages/app/eas.json b/packages/app/eas.json index 805217b89..1550f441f 100644 --- a/packages/app/eas.json +++ b/packages/app/eas.json @@ -25,6 +25,7 @@ "extends": "production", "distribution": "internal", "android": { + "resourceClass": "large", "buildType": "apk", "gradleCommand": ":app:assembleRelease -x lint -x lintVitalAnalyzeRelease -x lintVitalRelease -x generateReleaseLintModel -x generateReleaseLintVitalModel" } From 1977d330edefb8d7c8c0f0673e911586d40a6262 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Fri, 17 Jul 2026 17:03:34 +0200 Subject: [PATCH 14/38] Reduce workspace, agent, and chat sync traffic (#2028) * feat(sync): keep live data scoped and current Only viewed chats receive live timeline rows, while directory state now uses one subscribed bootstrap followed by ordered deltas. Legacy clients and daemons retain their existing behavior through centralized compatibility gates. * fix(sync): preserve selective delivery boundaries Keep selective timeline capability app-owned, union viewed sets across shared sockets, and reconcile directory side effects from accepted state. Visibility and archive suppression now follow their existing authoritative boundaries. * test(app): align browser fixtures with runtime contracts * fix(sync): preserve mixed-client delivery guarantees * fix(sync): finish paged history before advancing * fix(sync): replay live deltas after refresh failures * test(server): model socket capability lookup * test(app): use platform shortcut for split-pane coverage * fix(app): keep hidden timelines dormant * test(app): normalize split-pane shortcut setup * fix(app): preserve sync state across retries * fix(app): preserve background sync state across races * fix directory bootstrap reconciliation edge cases * fix selective timeline compatibility races * fix superseded directory sync races * fix(sync): centralize directory replica ordering Own agent and workspace refresh transactions in HostRuntime so reconnect epochs, buffered updates, and lifecycle invalidation share one ordering boundary. Route timeline metadata and mixed-capability stream delivery through their authoritative runtime sources. * fix(app): keep timeline requests runtime-scoped * fix(app): close directory bootstrap races --- packages/app/e2e/directory-bootstrap.spec.ts | 19 + .../app/e2e/helpers/daemon-websocket-gate.ts | 114 ++ .../helpers/directory-bootstrap-scenario.ts | 155 +++ .../app/e2e/viewed-agent-timelines.spec.ts | 148 +++ .../app/e2e/workspace-model-restart.spec.ts | 2 +- .../workspace-navigation-regression.spec.ts | 57 +- .../rewind/use-rewind-agent-mutation.ts | 4 +- packages/app/src/contexts/session-context.tsx | 967 ++------------- .../session-resume-revalidation.test.ts | 35 + .../contexts/session-resume-revalidation.ts | 16 + ...workspace-directory-reconciliation.test.ts | 63 + .../workspace-directory-reconciliation.ts | 26 + .../diagnostics/app-diagnostic-report.test.ts | 1 + .../hooks/use-agent-initialization.test.ts | 101 +- .../app/src/hooks/use-agent-initialization.ts | 17 +- .../src/hooks/use-load-older-agent-history.ts | 8 +- packages/app/src/panels/agent-panel.tsx | 93 +- .../directory-sync/agent-replica.test.ts | 86 ++ .../runtime/directory-sync/agent-replica.ts | 121 ++ .../src/runtime/directory-sync/index.test.ts | 113 ++ .../app/src/runtime/directory-sync/index.ts | 416 +++++++ .../directory-sync/transaction.test.ts | 100 ++ .../src/runtime/directory-sync/transaction.ts | 70 ++ .../directory-sync/workspace-replica.test.ts | 57 + .../directory-sync/workspace-replica.ts | 93 ++ packages/app/src/runtime/host-runtime.test.ts | 1054 ++++++++++++++++- packages/app/src/runtime/host-runtime.ts | 336 +++--- .../workspace/visible-agent-ids.test.ts | 112 ++ .../screens/workspace/visible-agent-ids.ts | 27 + .../screens/workspace/workspace-screen.tsx | 28 +- packages/app/src/stores/session-store.ts | 41 +- .../fetch-agent-timeline-once.test.ts | 30 + .../src/timeline/fetch-agent-timeline-once.ts | 31 + .../src/timeline/session-stream-reducers.ts | 13 +- .../src/timeline/timeline-sync-plan.test.ts | 25 - .../app/src/timeline/timeline-sync-plan.ts | 13 - .../src/timeline/viewed-timeline-sync.test.ts | 448 +++++++ .../app/src/timeline/viewed-timeline-sync.ts | 321 +++++ .../agent-directory-reconciliation.test.ts | 240 ++++ .../utils/agent-directory-reconciliation.ts | 62 + .../src/utils/agent-directory-sync.test.ts | 165 ++- .../app/src/utils/agent-directory-sync.ts | 131 +- .../utils/agent-directory-update-policy.ts | 21 + .../src/workspace/legacy-daemon-workspaces.ts | 10 +- packages/client/src/daemon-client.test.ts | 93 +- packages/client/src/daemon-client.ts | 63 + packages/protocol/src/client-capabilities.ts | 4 + packages/protocol/src/messages.test.ts | 32 + packages/protocol/src/messages.ts | 41 + .../agent/providers/opencode-agent.test.ts | 8 +- .../selective-timeline-delivery.e2e.test.ts | 287 +++++ packages/server/src/server/session.test.ts | 219 ++++ packages/server/src/server/session.ts | 192 ++- .../src/server/session.workspaces.test.ts | 129 ++ .../websocket-server.notifications.test.ts | 2 + .../websocket-server.relay-reconnect.test.ts | 4 + .../server/src/server/websocket-server.ts | 62 +- 57 files changed, 5783 insertions(+), 1343 deletions(-) create mode 100644 packages/app/e2e/directory-bootstrap.spec.ts create mode 100644 packages/app/e2e/helpers/daemon-websocket-gate.ts create mode 100644 packages/app/e2e/helpers/directory-bootstrap-scenario.ts create mode 100644 packages/app/e2e/viewed-agent-timelines.spec.ts create mode 100644 packages/app/src/contexts/session-resume-revalidation.test.ts create mode 100644 packages/app/src/contexts/session-resume-revalidation.ts create mode 100644 packages/app/src/contexts/workspace-directory-reconciliation.test.ts create mode 100644 packages/app/src/contexts/workspace-directory-reconciliation.ts create mode 100644 packages/app/src/runtime/directory-sync/agent-replica.test.ts create mode 100644 packages/app/src/runtime/directory-sync/agent-replica.ts create mode 100644 packages/app/src/runtime/directory-sync/index.test.ts create mode 100644 packages/app/src/runtime/directory-sync/index.ts create mode 100644 packages/app/src/runtime/directory-sync/transaction.test.ts create mode 100644 packages/app/src/runtime/directory-sync/transaction.ts create mode 100644 packages/app/src/runtime/directory-sync/workspace-replica.test.ts create mode 100644 packages/app/src/runtime/directory-sync/workspace-replica.ts create mode 100644 packages/app/src/screens/workspace/visible-agent-ids.test.ts create mode 100644 packages/app/src/screens/workspace/visible-agent-ids.ts create mode 100644 packages/app/src/timeline/fetch-agent-timeline-once.test.ts create mode 100644 packages/app/src/timeline/fetch-agent-timeline-once.ts create mode 100644 packages/app/src/timeline/viewed-timeline-sync.test.ts create mode 100644 packages/app/src/timeline/viewed-timeline-sync.ts create mode 100644 packages/app/src/utils/agent-directory-reconciliation.test.ts create mode 100644 packages/app/src/utils/agent-directory-reconciliation.ts create mode 100644 packages/app/src/utils/agent-directory-update-policy.ts create mode 100644 packages/server/src/server/selective-timeline-delivery.e2e.test.ts diff --git a/packages/app/e2e/directory-bootstrap.spec.ts b/packages/app/e2e/directory-bootstrap.spec.ts new file mode 100644 index 000000000..ad42b991f --- /dev/null +++ b/packages/app/e2e/directory-bootstrap.spec.ts @@ -0,0 +1,19 @@ +import { test } from "./fixtures"; +import { DirectoryBootstrapScenario } from "./helpers/directory-bootstrap-scenario"; + +test.describe("Directory bootstrap correctness", () => { + test("connect, pushed deltas, and reconnect keep directories current without duplicate bootstraps", async ({ + page, + }) => { + test.setTimeout(180_000); + const scenario = await DirectoryBootstrapScenario.open(page); + try { + await scenario.expectDirectoryStarts(1); + await scenario.stayConnectedWithoutRefetchAndApplyDeltas(); + await scenario.disconnectMutateAndReconnect(); + await scenario.expectVisibleReconciliationAndNavigateAgent(); + } finally { + await scenario.cleanup(); + } + }); +}); diff --git a/packages/app/e2e/helpers/daemon-websocket-gate.ts b/packages/app/e2e/helpers/daemon-websocket-gate.ts new file mode 100644 index 000000000..a6c7f3867 --- /dev/null +++ b/packages/app/e2e/helpers/daemon-websocket-gate.ts @@ -0,0 +1,114 @@ +import type { Page, WebSocketRoute } from "@playwright/test"; +import { daemonWsRoutePattern } from "./daemon-port"; + +export interface DirectoryBootstrapCounts { + agents: number; + workspaces: number; +} + +export interface DirectoryRequestStartCounts { + subscribed: DirectoryBootstrapCounts; + unsubscribed: DirectoryBootstrapCounts; + total: DirectoryBootstrapCounts; +} + +interface ClientRequest { + type?: unknown; + subscribe?: unknown; + page?: { cursor?: unknown }; +} + +function readClientRequest(message: string | Buffer): ClientRequest | null { + if (typeof message !== "string") return null; + try { + const envelope = JSON.parse(message) as { + type?: unknown; + message?: ClientRequest; + }; + return envelope.type === "session" ? (envelope.message ?? null) : envelope; + } catch { + return null; + } +} + +function directoryForRequest(request: ClientRequest): keyof DirectoryBootstrapCounts | null { + if (request.page?.cursor) return null; + if (request.type === "fetch_agents_request") return "agents"; + if (request.type === "fetch_workspaces_request") return "workspaces"; + return null; +} + +export async function installDaemonWebSocketGate(page: Page) { + let acceptingConnections = true; + const activeSockets = new Set(); + const directoryStarts: DirectoryRequestStartCounts = { + subscribed: { agents: 0, workspaces: 0 }, + unsubscribed: { agents: 0, workspaces: 0 }, + total: { agents: 0, workspaces: 0 }, + }; + const clientRequestCounts = new Map(); + + await page.routeWebSocket(daemonWsRoutePattern(), (ws) => { + if (!acceptingConnections) { + void ws.close({ code: 1008, reason: "Blocked by reconnect test." }); + return; + } + + activeSockets.add(ws); + const server = ws.connectToServer(); + + ws.onMessage((message) => { + if (!acceptingConnections) return; + const request = readClientRequest(message); + if (typeof request?.type === "string") { + clientRequestCounts.set(request.type, (clientRequestCounts.get(request.type) ?? 0) + 1); + const directory = directoryForRequest(request); + if (directory) { + const subscription = request.subscribe === undefined ? "unsubscribed" : "subscribed"; + directoryStarts[subscription][directory] += 1; + directoryStarts.total[directory] += 1; + } + } + try { + server.send(message); + } catch { + activeSockets.delete(ws); + } + }); + + server.onMessage((message) => { + if (!acceptingConnections) return; + try { + ws.send(message); + } catch { + activeSockets.delete(ws); + } + }); + }); + + return { + async drop(): Promise { + acceptingConnections = false; + const sockets = Array.from(activeSockets); + activeSockets.clear(); + await Promise.all( + sockets.map((ws) => + ws.close({ code: 1008, reason: "Dropped by reconnect test." }).catch(() => undefined), + ), + ); + }, + restore(): void { + acceptingConnections = true; + }, + getDirectoryRequestStartCounts(): DirectoryRequestStartCounts { + return { + subscribed: { ...directoryStarts.subscribed }, + unsubscribed: { ...directoryStarts.unsubscribed }, + total: { ...directoryStarts.total }, + }; + }, + getClientRequestCount(type: string): number { + return clientRequestCounts.get(type) ?? 0; + }, + }; +} diff --git a/packages/app/e2e/helpers/directory-bootstrap-scenario.ts b/packages/app/e2e/helpers/directory-bootstrap-scenario.ts new file mode 100644 index 000000000..97192be9f --- /dev/null +++ b/packages/app/e2e/helpers/directory-bootstrap-scenario.ts @@ -0,0 +1,155 @@ +import { expect, type Page } from "@playwright/test"; +import { buildHostAgentDetailRoute } from "@/utils/host-routes"; +import { installDaemonWebSocketGate } from "./daemon-websocket-gate"; +import { seedWorkspace, type SeededWorkspace } from "./seed-client"; +import { getServerId } from "./server-id"; +import { waitForWorkspaceTabsVisible } from "./workspace-tabs"; +import { expectReconnectingToastGone, expectReconnectingToastVisible } from "./workspace-ui"; + +interface SeededDirectoryAgent { + id: string; + title: string; +} + +async function createRunningMockAgent( + workspace: SeededWorkspace, + title: string, +): Promise { + const agent = await workspace.client.createAgent({ + provider: "mock", + cwd: workspace.repoPath, + workspaceId: workspace.workspaceId, + title, + modeId: "load-test", + model: "five-minute-stream", + initialPrompt: `Keep ${title} running for directory synchronization.`, + }); + const running = await workspace.client.waitForAgentUpsert( + agent.id, + (snapshot) => snapshot.status === "running", + 30_000, + ); + expect(running.status).toBe("running"); + return { id: agent.id, title }; +} + +async function openCommandCenter(page: Page): Promise { + await page.getByRole("button", { name: "Open command center" }).click(); +} + +export class DirectoryBootstrapScenario { + private readonly workspaces: SeededWorkspace[] = []; + private disconnectedWorkspace: SeededWorkspace | null = null; + private disconnectedAgent: SeededDirectoryAgent | null = null; + + private constructor( + private readonly page: Page, + private readonly gate: Awaited>, + ) {} + + static async open(page: Page): Promise { + const gate = await installDaemonWebSocketGate(page); + const scenario = new DirectoryBootstrapScenario(page, gate); + const workspace = await scenario.seedWorkspace("directory-bootstrap-initial-"); + const agent = await createRunningMockAgent(workspace, "Initial directory agent"); + await page.goto(buildHostAgentDetailRoute(getServerId(), agent.id, workspace.workspaceId)); + await page.waitForURL( + (url) => url.pathname.includes("/workspace/") && !url.searchParams.has("open"), + ); + await waitForWorkspaceTabsVisible(page); + await expect(page.getByRole("button", { name: agent.title, exact: true })).toBeVisible(); + return scenario; + } + + async expectDirectoryStarts(expectedPerDirectory: number): Promise { + await expect + .poll(() => this.gate.getDirectoryRequestStartCounts()) + .toEqual({ + subscribed: { agents: expectedPerDirectory, workspaces: expectedPerDirectory }, + unsubscribed: { agents: 0, workspaces: 0 }, + total: { agents: expectedPerDirectory, workspaces: expectedPerDirectory }, + }); + } + + async stayConnectedWithoutRefetchAndApplyDeltas(): Promise { + const workspace = await this.seedWorkspace("directory-bootstrap-background-"); + const agent = await createRunningMockAgent(workspace, "Background directory agent"); + + const workspaceLink = this.page.getByText(workspace.projectDisplayName, { exact: true }); + await expect(workspaceLink).toHaveCount(1); + await expect(workspaceLink).toBeVisible(); + await openCommandCenter(this.page); + const agentLink = this.page.getByText(agent.title, { exact: true }); + await expect(agentLink).toHaveCount(1); + await expect(agentLink).toBeVisible(); + await this.page.keyboard.press("Escape"); + await this.expectDirectoryStarts(1); + } + + async disconnectMutateAndReconnect(): Promise { + await this.gate.drop(); + await expectReconnectingToastVisible(this.page); + + this.disconnectedWorkspace = await this.seedWorkspace("directory-bootstrap-reconnect-"); + this.disconnectedAgent = await createRunningMockAgent( + this.disconnectedWorkspace, + "Reconnected directory agent", + ); + await expect( + this.page.getByText(this.disconnectedWorkspace.projectDisplayName, { exact: true }), + ).toHaveCount(0); + await expect(this.page.getByText(this.disconnectedAgent.title, { exact: true })).toHaveCount(0); + + this.gate.restore(); + await expectReconnectingToastGone(this.page); + await this.expectDirectoryStarts(2); + } + + async expectVisibleReconciliationAndNavigateAgent(): Promise { + const workspace = this.requireDisconnectedWorkspace(); + const agent = this.requireDisconnectedAgent(); + const workspaceLink = this.page.getByText(workspace.projectDisplayName, { exact: true }); + await expect(workspaceLink).toHaveCount(1); + await expect(workspaceLink).toBeVisible(); + await openCommandCenter(this.page); + const agentLink = this.page.getByText(agent.title, { exact: true }); + await expect(agentLink).toHaveCount(1); + await expect(agentLink).toBeVisible(); + await agentLink.click(); + await expect(this.page).toHaveURL( + new RegExp( + `/workspace/${workspace.workspaceId}/agent/${agent.id}|/workspace/${workspace.workspaceId}`, + ), + ); + await expect(this.page.getByRole("button", { name: agent.title, exact: true })).toHaveAttribute( + "aria-selected", + "true", + ); + const pings = this.gate.getClientRequestCount("ping"); + await expect + .poll(() => this.gate.getClientRequestCount("ping"), { timeout: 30_000 }) + .toBeGreaterThan(pings); + await this.expectDirectoryStarts(2); + } + + async cleanup(): Promise { + this.gate.restore(); + await Promise.all(this.workspaces.map((workspace) => workspace.cleanup())); + } + + private async seedWorkspace(prefix: string): Promise { + const workspace = await seedWorkspace({ repoPrefix: prefix }); + this.workspaces.push(workspace); + return workspace; + } + + private requireDisconnectedWorkspace(): SeededWorkspace { + if (!this.disconnectedWorkspace) throw new Error("Reconnect workspace was not seeded."); + return this.disconnectedWorkspace; + } + + private requireDisconnectedAgent(): SeededDirectoryAgent { + if (!this.disconnectedAgent) throw new Error("Reconnect agent was not seeded."); + return this.disconnectedAgent; + } +} diff --git a/packages/app/e2e/viewed-agent-timelines.spec.ts b/packages/app/e2e/viewed-agent-timelines.spec.ts new file mode 100644 index 000000000..7e4c7d539 --- /dev/null +++ b/packages/app/e2e/viewed-agent-timelines.spec.ts @@ -0,0 +1,148 @@ +import { expect, type Page } from "@playwright/test"; +import { buildHostAgentDetailRoute } from "@/utils/host-routes"; +import { test } from "./fixtures"; +import { seedWorkspace, type SeedDaemonClient } from "./helpers/seed-client"; +import { getServerId } from "./helpers/server-id"; +import { waitForWorkspaceTabsVisible } from "./helpers/workspace-tabs"; +import { installDaemonWebSocketGate } from "./helpers/daemon-websocket-gate"; +import { + expectReconnectingToastGone, + expectReconnectingToastVisible, +} from "./helpers/workspace-ui"; + +interface ViewedTimelineScenario { + client: SeedDaemonClient; + workspaceId: string; + firstAgentId: string; + secondAgentId: string; + cleanup(): Promise; +} + +async function seedViewedTimelineScenario(): Promise { + const workspace = await seedWorkspace({ repoPrefix: "viewed-timelines-" }); + const createAgent = (title: string) => + workspace.client.createAgent({ + provider: "mock", + cwd: workspace.repoPath, + workspaceId: workspace.workspaceId, + title, + modeId: "load-test", + model: "ten-second-stream", + }); + const [firstAgent, secondAgent] = await Promise.all([ + createAgent("First viewed chat"), + createAgent("Second viewed chat"), + ]); + return { + client: workspace.client, + workspaceId: workspace.workspaceId, + firstAgentId: firstAgent.id, + secondAgentId: secondAgent.id, + cleanup: workspace.cleanup, + }; +} + +async function openAgent(page: Page, scenario: ViewedTimelineScenario, agentId: string) { + await page.goto(buildHostAgentDetailRoute(getServerId(), agentId, scenario.workspaceId)); + await page.waitForURL( + (url) => url.pathname.includes("/workspace/") && !url.searchParams.has("open"), + ); + await waitForWorkspaceTabsVisible(page); +} + +async function selectAgent(page: Page, title: string) { + await page.getByRole("button", { name: title, exact: true }).click(); +} + +async function enableMoveTabShortcut(page: Page) { + await page.addInitScript(() => { + Object.defineProperty(navigator, "platform", { get: () => "MacIntel" }); + }); +} + +async function moveActiveTabRight(page: Page) { + await page.keyboard.press("Meta+Alt+Shift+ArrowRight"); +} + +async function commitMessage(scenario: ViewedTimelineScenario, agentId: string, prompt: string) { + await scenario.client.sendAgentMessage(agentId, prompt); + const finish = await scenario.client.waitForFinish(agentId, 30_000); + expect(finish.status).toBe("idle"); +} + +test.describe("Viewed agent timelines", () => { + test("a hidden retained chat catches up when shown", async ({ page }) => { + const scenario = await seedViewedTimelineScenario(); + try { + await openAgent(page, scenario, scenario.firstAgentId); + await selectAgent(page, "Second viewed chat"); + await commitMessage( + scenario, + scenario.firstAgentId, + "Committed while the first chat is hidden.", + ); + await expect( + page.getByText("Committed while the first chat is hidden.", { exact: true }), + ).toHaveCount(0); + await selectAgent(page, "First viewed chat"); + await expect( + page.getByText("Committed while the first chat is hidden.", { exact: true }), + ).toBeVisible(); + } finally { + await scenario.cleanup(); + } + }); + + test("two visible split chats both stay current", async ({ page }) => { + const scenario = await seedViewedTimelineScenario(); + try { + await enableMoveTabShortcut(page); + await openAgent(page, scenario, scenario.firstAgentId); + await page.getByRole("button", { name: "Split pane right" }).click(); + await selectAgent(page, "Second viewed chat"); + await moveActiveTabRight(page); + await expect( + page.getByRole("button", { name: "First viewed chat", exact: true }), + ).toBeVisible(); + await expect( + page.getByRole("button", { name: "Second viewed chat", exact: true }), + ).toBeVisible(); + await expect(page.getByRole("textbox", { name: "Message agent..." })).toHaveCount(2); + await commitMessage(scenario, scenario.firstAgentId, "First visible pane update."); + await expect(page.getByText("First visible pane update.", { exact: true })).toBeVisible(); + await expect( + page.getByRole("button", { name: "Second viewed chat", exact: true }), + ).toBeVisible(); + } finally { + await scenario.cleanup(); + } + }); + + test("a visible chat catches up after reconnecting", async ({ page }) => { + const gate = await installDaemonWebSocketGate(page); + const scenario = await seedViewedTimelineScenario(); + try { + await openAgent(page, scenario, scenario.firstAgentId); + await expect(page.getByRole("button", { name: "First viewed chat" })).toHaveAttribute( + "aria-selected", + "true", + ); + await gate.drop(); + await expectReconnectingToastVisible(page); + await commitMessage(scenario, scenario.firstAgentId, "Committed while the chat reconnects."); + await expect( + page.getByText("Committed while the chat reconnects.", { exact: true }), + ).toHaveCount(0); + gate.restore(); + await expectReconnectingToastGone(page); + const recoveredMessage = page.getByText("Committed while the chat reconnects.", { + exact: true, + }); + await expect(recoveredMessage).toHaveCount(1); + await expect(recoveredMessage).toBeVisible(); + } finally { + gate.restore(); + await scenario.cleanup(); + } + }); +}); diff --git a/packages/app/e2e/workspace-model-restart.spec.ts b/packages/app/e2e/workspace-model-restart.spec.ts index 295234094..3f459bf46 100644 --- a/packages/app/e2e/workspace-model-restart.spec.ts +++ b/packages/app/e2e/workspace-model-restart.spec.ts @@ -20,7 +20,7 @@ import { import { waitForSidebarHydration } from "./helpers/workspace-ui"; import { getVisibleWorkspaceAgentTabIds } from "./helpers/workspace-tabs"; -const LEGACY_AGENT_ID = "legacy-cwd-only-agent"; +const LEGACY_AGENT_ID = "10000000-0000-4000-8000-000000000001"; const SERVER_ID = `srv_restart_${randomUUID().replace(/-/g, "").slice(0, 16)}`; interface RestartDaemonClient { diff --git a/packages/app/e2e/workspace-navigation-regression.spec.ts b/packages/app/e2e/workspace-navigation-regression.spec.ts index d1fccbdc3..62b18f6ee 100644 --- a/packages/app/e2e/workspace-navigation-regression.spec.ts +++ b/packages/app/e2e/workspace-navigation-regression.spec.ts @@ -1,5 +1,4 @@ import { buildHostAgentDetailRoute, buildHostWorkspaceRoute } from "@/utils/host-routes"; -import type { WebSocketRoute } from "@playwright/test"; import { expect, test, type Page } from "./fixtures"; import { gotoAppShell, openSettings } from "./helpers/app"; import { @@ -34,6 +33,7 @@ import { clickSettingsBackToWorkspace } from "./helpers/settings"; import { getServerId } from "./helpers/server-id"; import { injectDesktopBridge } from "./helpers/desktop-updates"; import { expectAppRoute } from "./helpers/route-assertions"; +import { installDaemonWebSocketGate } from "./helpers/daemon-websocket-gate"; const LOADING_WORKSPACE_TEXT_PATTERN = /Loading workspace/i; @@ -114,61 +114,6 @@ async function expectWorkspaceLocation( }); } -async function installDaemonWebSocketGate(page: Page) { - let acceptingConnections = true; - const activeSockets = new Set(); - - await page.routeWebSocket(daemonWsRoutePattern(), (ws) => { - if (!acceptingConnections) { - void ws.close({ code: 1008, reason: "Blocked by workspace reconnect regression test." }); - return; - } - - activeSockets.add(ws); - const server = ws.connectToServer(); - - ws.onMessage((message) => { - if (!acceptingConnections) { - return; - } - try { - server.send(message); - } catch { - activeSockets.delete(ws); - } - }); - - server.onMessage((message) => { - if (!acceptingConnections) { - return; - } - try { - ws.send(message); - } catch { - activeSockets.delete(ws); - } - }); - }); - - return { - async drop(): Promise { - acceptingConnections = false; - const sockets = Array.from(activeSockets); - activeSockets.clear(); - await Promise.all( - sockets.map((ws) => - ws - .close({ code: 1008, reason: "Dropped by workspace reconnect regression test." }) - .catch(() => undefined), - ), - ); - }, - restore(): void { - acceptingConnections = true; - }, - }; -} - test.describe("Workspace navigation regression", () => { test.describe.configure({ timeout: 240_000 }); diff --git a/packages/app/src/components/rewind/use-rewind-agent-mutation.ts b/packages/app/src/components/rewind/use-rewind-agent-mutation.ts index e684c4bf5..037f7c733 100644 --- a/packages/app/src/components/rewind/use-rewind-agent-mutation.ts +++ b/packages/app/src/components/rewind/use-rewind-agent-mutation.ts @@ -8,6 +8,7 @@ import { useRewindComposerRestore } from "./composer-restore"; import { useSessionStore } from "@/stores/session-store"; import { shouldRestoreComposerForRewindMode } from "./rewind-mode"; import { clearOptimisticUserMessages } from "@/types/stream"; +import { getHostRuntimeStore } from "@/runtime/host-runtime"; interface UseRewindAgentMutationInput { serverId?: string; @@ -47,7 +48,8 @@ export function useRewindAgentMutation(input: UseRewindAgentMutationInput): { .getState() .sessions[input.serverId]?.agentTimelineCursor.get(input.agentId) : undefined; - await input.client.fetchAgentTimeline(input.agentId, { + if (!input.serverId) throw new Error(t("common.errors.daemonClientUnavailable")); + await getHostRuntimeStore().fetchAgentTimeline(input.serverId, input.agentId, { direction: "tail", projection: "projected", ...(cursor ? { cursor: { epoch: cursor.epoch, seq: cursor.endSeq } } : {}), diff --git a/packages/app/src/contexts/session-context.tsx b/packages/app/src/contexts/session-context.tsx index b03702a81..31720b34d 100644 --- a/packages/app/src/contexts/session-context.tsx +++ b/packages/app/src/contexts/session-context.tsx @@ -4,9 +4,12 @@ import { AppState } from "react-native"; import { useQueryClient } from "@tanstack/react-query"; import { useTranslation } from "react-i18next"; import { useClientActivity } from "@/hooks/use-client-activity"; +import { useAppVisible } from "@/hooks/use-app-visible"; import { usePushTokenRegistration } from "@/hooks/use-push-token-registration"; -import { clearArchiveAgentPending } from "@/hooks/use-archive-agent"; -import { refreshAgentInitializationTimeout } from "@/hooks/use-agent-initialization"; +import { + createSetAgentInitializing, + refreshAgentInitializationTimeout, +} from "@/hooks/use-agent-initialization"; import { prefetchProvidersSnapshot } from "@/hooks/use-providers-snapshot"; import { generateMessageId, type StreamItem } from "@/types/stream"; import { @@ -16,12 +19,8 @@ import { type TimelineReducerSideEffect, } from "@/timeline/session-stream-reducers"; import { useCreateFlowStore } from "@/stores/create-flow-store"; -import { - isTimelineCatchUpComplete, - planResumeTimelineSync, - planTimelineCatchUpAfter, - planTimelineCatchUpFollowUp, -} from "@/timeline/timeline-sync-plan"; +import { isTimelineCatchUpComplete } from "@/timeline/timeline-sync-plan"; +import { createViewedTimelineSync, type ViewedTimelineSync } from "@/timeline/viewed-timeline-sync"; import type { AgentAttachment, SessionOutboundMessage } from "@getpaseo/protocol/messages"; import { parseServerInfoStatusPayload } from "@getpaseo/protocol/messages"; import { @@ -29,7 +28,6 @@ import { type AgentAttentionNotificationPayload, type NotificationPermissionRequest, } from "@getpaseo/protocol/agent-attention-notification"; -import type { AgentLifecycleStatus } from "@getpaseo/protocol/agent-lifecycle"; import type { DaemonClient } from "@getpaseo/client/internal/daemon-client"; import type { AgentSessionConfig } from "@getpaseo/protocol/agent-types"; import type { GitSetupOptions } from "@getpaseo/protocol/messages"; @@ -37,51 +35,27 @@ import type { AgentPermissionResponse } from "@getpaseo/protocol/agent-types"; import { getHostRuntimeStore, useHostRuntimeIsConnected } from "@/runtime/host-runtime"; import { useVoiceAudioEngineOptional, useVoiceRuntimeOptional } from "@/contexts/voice-context"; import type { AudioPlaybackSource } from "@/voice/audio-engine-types"; -import { - useSessionStore, - type Agent, - type MessageEntry, - type SessionState, - type WorkspaceDescriptor, - type EmptyProjectDescriptor, - normalizeWorkspaceDescriptor, - normalizeEmptyProjectDescriptor, -} from "@/stores/session-store"; -import { useDraftStore } from "@/stores/draft-store"; +import { useSessionStore, type MessageEntry, type SessionState } from "@/stores/session-store"; import { useWorkspaceSetupStore } from "@/stores/workspace-setup-store"; import { sendOsNotification } from "@/utils/os-notifications"; import { getIsAppActivelyVisible } from "@/utils/app-visibility"; import { getInitKey, getInitDeferred, + createInitDeferred, resolveInitDeferred, rejectInitDeferred, } from "@/utils/agent-initialization"; import { encodeImages } from "@/utils/encode-images"; -import { derivePendingPermissionKey, normalizeAgentSnapshot } from "@/utils/agent-snapshots"; -import { resolveProjectPlacement } from "@/utils/project-placement"; -import { buildDraftStoreKey } from "@/stores/draft-keys"; +import { derivePendingPermissionKey } from "@/utils/agent-snapshots"; import type { AttachmentMetadata } from "@/attachments/types"; -import { - resolveComposerAttachmentSubmitFormat, - splitComposerAttachmentsForSubmit, -} from "@/composer/attachments/submit"; -import { reconcilePreviousAgentStatuses } from "@/contexts/session-status-tracking"; import { patchWorkspaceScripts } from "@/contexts/session-workspace-scripts"; -import { - clearWorkspaceArchivePending, - shouldSuppressWorkspaceForLocalArchive, -} from "@/contexts/session-workspace-upserts"; -import { isNative } from "@/constants/platform"; import { useToast } from "@/contexts/toast-context"; import { toErrorMessage } from "@/utils/error-messages"; import { showProviderNoticeToast } from "@/utils/provider-notice-toast"; import { applyCheckoutStatusUpdateFromEvent } from "@/git/checkout-status-cache"; -import { - applyLegacyDaemonWorkspaceOwnership, - backfillLegacyDaemonWorkspaceDirectoryIfEmpty, -} from "@/workspace/legacy-daemon-workspaces"; import { useProviderSubagentStore } from "@/subagents/provider-store"; +import { revalidateSessionAfterResume } from "@/contexts/session-resume-revalidation"; // Re-export types from session-store and draft-store for backward compatibility export type { DraftInput } from "@/stores/draft-store"; @@ -96,25 +70,6 @@ export type { AgentFileExplorerState, } from "@/stores/session-store"; -const HISTORY_STALE_AFTER_MS = 60_000; -const AUTHORITATIVE_REVALIDATION_DEBOUNCE_MS = 300; - -function hasAgentUsageChanged( - incomingUsage: Agent["lastUsage"] | undefined, - currentUsage: Agent["lastUsage"] | undefined, -): boolean { - const keys: Array> = [ - "inputTokens", - "outputTokens", - "cachedInputTokens", - "totalCostUsd", - "contextWindowMaxTokens", - "contextWindowUsedTokens", - ]; - - return keys.some((key) => incomingUsage?.[key] !== currentUsage?.[key]); -} - type AudioOutputPayload = Extract["payload"]; interface BufferedAudioChunk { @@ -124,56 +79,6 @@ interface BufferedAudioChunk { id: string; } -interface WorkspaceHydrationSnapshot { - workspaces: Map; - emptyProjects: Map; -} - -async function fetchWorkspaceHydrationSnapshot(input: { - client: DaemonClient; - serverId: string; - subscribe: boolean; - isCancelled?: () => boolean; -}): Promise { - const workspaces = new Map(); - const emptyProjects = new Map(); - let cursor: string | null = null; - let includeSubscribe = input.subscribe; - - while (true) { - const payload = await input.client.fetchWorkspaces({ - sort: [{ key: "activity_at", direction: "desc" }], - ...(includeSubscribe ? { subscribe: {} } : {}), - page: cursor ? { limit: 200, cursor } : { limit: 200 }, - }); - if (input.isCancelled?.()) { - return null; - } - - for (const entry of payload.entries) { - const workspace = normalizeWorkspaceDescriptor(entry); - if (shouldSuppressWorkspaceForLocalArchive({ serverId: input.serverId, workspace })) { - continue; - } - workspaces.set(workspace.id, workspace); - } - - // Project parents with no active workspaces only ride on the first page. - for (const project of payload.emptyProjects ?? []) { - const descriptor = normalizeEmptyProjectDescriptor(project); - emptyProjects.set(descriptor.projectId, descriptor); - } - - if (!payload.pageInfo.hasMore || !payload.pageInfo.nextCursor) { - break; - } - cursor = payload.pageInfo.nextCursor; - includeSubscribe = false; - } - - return { workspaces, emptyProjects }; -} - function decodeBase64Chunk(base64: string): Uint8Array { return Buffer.from(base64, "base64"); } @@ -240,54 +145,11 @@ const getLatestPermissionRequest = ( return null; }; -type AgentUpdatePayload = Extract["payload"]; type WorkspaceSetupProgressPayload = Extract< SessionOutboundMessage, { type: "workspace_setup_progress" } >["payload"]; -const getAgentIdFromUpdate = (update: AgentUpdatePayload): string => - update.kind === "remove" ? update.agentId : update.agent.id; - -// --------------------------------------------------------------------------- -// Module-level pending agent updates buffer (scoped by serverId) -// --------------------------------------------------------------------------- -const pendingAgentUpdates = new Map(); - -function pendingKey(serverId: string, agentId: string): string { - return `${serverId}:${agentId}`; -} - -export function bufferPendingAgentUpdate( - serverId: string, - agentId: string, - update: AgentUpdatePayload, -): void { - pendingAgentUpdates.set(pendingKey(serverId, agentId), update); -} - -export function flushPendingAgentUpdate( - serverId: string, - agentId: string, -): AgentUpdatePayload | undefined { - const key = pendingKey(serverId, agentId); - const update = pendingAgentUpdates.get(key); - pendingAgentUpdates.delete(key); - return update; -} - -export function deletePendingAgentUpdate(serverId: string, agentId: string): void { - pendingAgentUpdates.delete(pendingKey(serverId, agentId)); -} - -export function clearPendingAgentUpdates(serverId: string): void { - for (const key of Array.from(pendingAgentUpdates.keys())) { - if (key.startsWith(`${serverId}:`)) { - pendingAgentUpdates.delete(key); - } - } -} - type SessionStoreActions = ReturnType; type SetInitializingAgents = SessionStoreActions["setInitializingAgents"]; type SetAgentStreamTail = SessionStoreActions["setAgentStreamTail"]; @@ -401,20 +263,12 @@ function applyTimelineStreamPatches(input: { function executeTimelineSideEffects(input: { sideEffects: TimelineReducerSideEffect[]; agentId: string; - serverId: string; - requestCanonicalCatchUp: (agentId: string, cursor: { epoch: string; endSeq: number }) => void; - applyAgentUpdatePayload: (payload: AgentUpdatePayload) => void; + recoverTimelineGap: (agentId: string, cursor: { epoch: string; endSeq: number }) => void; }): void { - const { sideEffects, agentId, serverId, requestCanonicalCatchUp, applyAgentUpdatePayload } = - input; + const { sideEffects, agentId, recoverTimelineGap } = input; for (const effect of sideEffects) { if (effect.type === "catch_up") { - requestCanonicalCatchUp(agentId, effect.cursor); - } else if (effect.type === "flush_pending_updates") { - const deferredUpdate = flushPendingAgentUpdate(serverId, agentId); - if (deferredUpdate) { - applyAgentUpdatePayload(deferredUpdate); - } + recoverTimelineGap(agentId, effect.cursor); } } } @@ -446,13 +300,16 @@ function finalizeTimelineApplication(input: { if (shouldMarkAuthoritativeHistoryApplied) { setAgentAuthoritativeHistoryApplied(serverId, agentId, true); useCreateFlowStore.getState().clearByAgent({ serverId, agentId }); + markAgentHistorySynchronized(serverId, agentId); + const session = useSessionStore.getState().sessions[serverId]; + const agent = session?.agents.get(agentId) ?? session?.agentDetails.get(agentId); + if (agent && agent.status !== "running") { + getHostRuntimeStore().drainQueuedAgentMessage(serverId, agentId); + } } if (result.initResolution === "resolve") { resolveInitDeferred(initKey); } - if (result.clearInitializing) { - markAgentHistorySynchronized(serverId, agentId); - } } function applyToolResultToMessages( @@ -540,24 +397,14 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider const setAgentAuthoritativeHistoryApplied = useSessionStore( (state) => state.setAgentAuthoritativeHistoryApplied, ); - const setHasHydratedAgents = useSessionStore((state) => state.setHasHydratedAgents); - const setHasHydratedWorkspaces = useSessionStore((state) => state.setHasHydratedWorkspaces); const setAgents = useSessionStore((state) => state.setAgents); const setWorkspaces = useSessionStore((state) => state.setWorkspaces); - const setEmptyProjects = useSessionStore((state) => state.setEmptyProjects); - const addEmptyProject = useSessionStore((state) => state.addEmptyProject); - const removeEmptyProject = useSessionStore((state) => state.removeEmptyProject); - const mergeWorkspaces = useSessionStore((state) => state.mergeWorkspaces); - const removeWorkspace = useSessionStore((state) => state.removeWorkspace); - const setAgentLastActivity = useSessionStore((state) => state.setAgentLastActivity); const flushAgentLastActivity = useSessionStore((state) => state.flushAgentLastActivity); const setPendingPermissions = useSessionStore((state) => state.setPendingPermissions); - const clearDraftInput = useDraftStore((state) => state.clearDraftInput); - const setQueuedMessages = useSessionStore((state) => state.setQueuedMessages); const updateSessionClient = useSessionStore((state) => state.updateSessionClient); const updateSessionServerInfo = useSessionStore((state) => state.updateSessionServerInfo); + const setViewedTimelineSync = useSessionStore((state) => state.setViewedTimelineSync); const upsertWorkspaceSetupProgress = useWorkspaceSetupStore((state) => state.upsertProgress); - const removeWorkspaceSetup = useWorkspaceSetupStore((state) => state.removeWorkspace); const clearWorkspaceSetupServer = useWorkspaceSetupStore((state) => state.clearServer); // Track focused agent for heartbeat @@ -567,28 +414,13 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider const focusedTerminalId = useSessionStore( (state) => state.sessions[serverId]?.focusedTerminalId ?? null, ); - const sessionAgents = useSessionStore((state) => state.sessions[serverId]?.agents); - - const previousAgentStatusRef = useRef>(new Map()); - const sendAgentMessageRef = useRef< - | (( - agentId: string, - message: string, - images?: AttachmentMetadata[], - attachments?: AgentAttachment[], - ) => Promise) - | null - >(null); const _sessionStateTimeoutRef = useRef | null>(null); const attentionNotifiedRef = useRef>(new Map()); const appStateRef = useRef(AppState.currentState); - const revalidationTimerRef = useRef | null>(null); - const revalidationInFlightRef = useRef | null>(null); - const revalidationQueuedRef = useRef(false); - const timelineCatchUpInFlightRef = useRef>(new Set()); - const wasConnectedRef = useRef(isConnected); + const viewedTimelineSyncRef = useRef(null); const audioOutputBuffersRef = useRef>(new Map()); const activeAudioGroupsRef = useRef>(new Set()); + const isAppVisible = useAppVisible(); useEffect(() => { const subscription = AppState.addEventListener("change", (nextState) => { @@ -601,290 +433,31 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider }, []); useEffect(() => { - previousAgentStatusRef.current = reconcilePreviousAgentStatuses( - previousAgentStatusRef.current, - sessionAgents, - ); - }, [sessionAgents]); + viewedTimelineSyncRef.current?.setActive(isAppVisible); + }, [isAppVisible]); - const hydrateWorkspaces = useCallback( - async (options?: { subscribe?: boolean; isCancelled?: () => boolean }) => { - if (!client || !isConnected) { - return; - } - - const snapshot = await fetchWorkspaceHydrationSnapshot({ - client, - serverId, - subscribe: options?.subscribe ?? false, - isCancelled: options?.isCancelled, - }); - if (!snapshot || options?.isCancelled?.()) { - return; - } - - const didBackfillLegacy = await backfillLegacyDaemonWorkspaceDirectoryIfEmpty({ - client, - serverId, - workspaces: snapshot.workspaces, - emptyProjects: snapshot.emptyProjects, - isCancelled: options?.isCancelled, - }); - if (didBackfillLegacy) { - return; - } - - setWorkspaces(serverId, snapshot.workspaces); - setEmptyProjects(serverId, snapshot.emptyProjects.values()); - setHasHydratedWorkspaces(serverId, true); - }, - [client, isConnected, serverId, setEmptyProjects, setHasHydratedWorkspaces, setWorkspaces], - ); - - const applyAuthoritativeAgentSnapshot = useCallback( - (agent: Agent) => { - setAgents(serverId, (prev) => { - const current = prev.get(agent.id); - if (current && agent.updatedAt.getTime() < current.updatedAt.getTime()) { - const hasUsageUpdate = hasAgentUsageChanged(agent.lastUsage, current.lastUsage); - if (hasUsageUpdate) { - const next = new Map(prev); - next.set(agent.id, { - ...current, - lastUsage: agent.lastUsage, - }); - return next; - } - return prev; - } - const next = new Map(prev); - next.set(agent.id, agent); - return next; - }); - - if (agent.archivedAt) { - clearArchiveAgentPending({ - queryClient, - serverId, - agentId: agent.id, - }); - } - - setAgentLastActivity(agent.id, agent.lastActivityAt); - - setPendingPermissions(serverId, (prev) => { - const existingKeysForAgent: string[] = []; - for (const [key, pending] of prev.entries()) { - if (pending.agentId === agent.id) { - existingKeysForAgent.push(key); - } - } - - const nextEntries = agent.pendingPermissions.map((request) => ({ - key: derivePendingPermissionKey(agent.id, request), - agentId: agent.id, - request, - })); - - let changed = existingKeysForAgent.length !== nextEntries.length; - if (!changed) { - const existingKeySet = new Set(existingKeysForAgent); - for (const entry of nextEntries) { - const existing = prev.get(entry.key); - if (!existingKeySet.has(entry.key) || !existing) { - changed = true; - break; - } - - const currentRequest = existing.request; - if ( - currentRequest.id !== entry.request.id || - currentRequest.kind !== entry.request.kind || - currentRequest.name !== entry.request.name || - currentRequest.title !== entry.request.title || - currentRequest.description !== entry.request.description - ) { - changed = true; - break; - } - } - } - - if (!changed) { - return prev; - } - - const next = new Map(prev); - for (const key of existingKeysForAgent) { - next.delete(key); - } - for (const entry of nextEntries) { - next.set(entry.key, entry); - } - return next; - }); - - const prevStatus = previousAgentStatusRef.current.get(agent.id); - if (prevStatus === "running" && agent.status !== "running") { - const session = useSessionStore.getState().sessions[serverId]; - const queue = session?.queuedMessages.get(agent.id); - if (queue && queue.length > 0) { - const [next, ...rest] = queue; - if (sendAgentMessageRef.current) { - const supportsForgeSearch = - useSessionStore.getState().sessions[serverId]?.serverInfo?.features?.forgeSearch === - true; - const wirePayload = splitComposerAttachmentsForSubmit(next.attachments, { - format: resolveComposerAttachmentSubmitFormat({ - supportsForgeAttachments: supportsForgeSearch, - }), - }); - void sendAgentMessageRef.current( - agent.id, - next.text, - wirePayload.images, - wirePayload.attachments, - ); - } - setQueuedMessages(serverId, (prev) => { - const updated = new Map(prev); - updated.set(agent.id, rest); - return updated; - }); - } - } - - previousAgentStatusRef.current.set(agent.id, agent.status); - }, - [ - queryClient, - serverId, - setAgentLastActivity, - setAgents, - setPendingPermissions, - setQueuedMessages, - ], - ); - - const runAuthoritativeRevalidation = useCallback(async () => { - await Promise.all([ - getHostRuntimeStore().refreshAgentDirectory({ serverId }), - hydrateWorkspaces(), - ]); - }, [hydrateWorkspaces, serverId]); - - const flushAuthoritativeRevalidation = useCallback(() => { - if (!client || !isConnected) { - return; - } - if (revalidationInFlightRef.current) { - revalidationQueuedRef.current = true; - return; - } - - const run = runAuthoritativeRevalidation() - .catch((error) => { - console.error("[Session] authoritative revalidation failed", { - serverId, - error, - }); - }) - .finally(() => { - if (revalidationInFlightRef.current === run) { - revalidationInFlightRef.current = null; - } - if (!revalidationQueuedRef.current) { - return; - } - revalidationQueuedRef.current = false; - if (revalidationTimerRef.current) { - clearTimeout(revalidationTimerRef.current); - } - revalidationTimerRef.current = setTimeout(() => { - revalidationTimerRef.current = null; - flushAuthoritativeRevalidation(); - }, AUTHORITATIVE_REVALIDATION_DEBOUNCE_MS); - }); - - revalidationInFlightRef.current = run; - }, [client, isConnected, runAuthoritativeRevalidation, serverId]); - - const scheduleAuthoritativeRevalidation = useCallback(() => { - if (!client || !isConnected) { - return; - } - - revalidationQueuedRef.current = true; - if (revalidationTimerRef.current) { - return; - } - revalidationTimerRef.current = setTimeout(() => { - revalidationTimerRef.current = null; - if (!revalidationQueuedRef.current) { - return; - } - revalidationQueuedRef.current = false; - flushAuthoritativeRevalidation(); - }, AUTHORITATIVE_REVALIDATION_DEBOUNCE_MS); - }, [client, flushAuthoritativeRevalidation, isConnected]); - - const requestCanonicalCatchUp = useCallback( + const recoverTimelineGap = useCallback( (agentId: string, cursor: { epoch: string; endSeq: number }) => { - const request = planTimelineCatchUpAfter({ epoch: cursor.epoch, seq: cursor.endSeq }); - const key = `${agentId}:${request.cursor.epoch}:${request.cursor.seq}`; - const inFlight = timelineCatchUpInFlightRef.current; - if (inFlight.has(key)) { - return; - } - inFlight.add(key); - void client - .fetchAgentTimeline(agentId, request) - .catch((error) => { - console.warn("[Session] failed to fetch canonical catch-up timeline", agentId, error); - }) - .finally(() => { - inFlight.delete(key); - }); + viewedTimelineSyncRef.current?.recoverGap(agentId, cursor); }, - [client], + [], ); const handleAppResumed = useCallback( (awayMs: number) => { - scheduleAuthoritativeRevalidation(); - - if (isNative) { - const session = useSessionStore.getState().sessions[serverId]; - const agentId = session?.focusedAgentId; - if (agentId) { - const plan = planResumeTimelineSync({ - cursor: session?.agentTimelineCursor.get(agentId), - }); - if (plan.direction === "after") { - requestCanonicalCatchUp(agentId, { - epoch: plan.cursor.epoch, - endSeq: plan.cursor.seq, - }); - } else { - void client.fetchAgentTimeline(agentId, plan).catch((error) => { - console.warn("[Session] failed to fetch tail timeline on resume", agentId, error); - }); - } - } - } - - if (awayMs < HISTORY_STALE_AFTER_MS) { - return; - } - bumpHistorySyncGeneration(serverId); + void revalidateSessionAfterResume({ + awayMs, + serverId, + bumpHistorySyncGeneration, + refreshDirectories: () => getHostRuntimeStore().refreshDirectories(serverId), + }).catch((error) => { + console.error("[SessionProvider] resume revalidation failed", { + serverId, + error: toErrorMessage(error), + }); + }); }, - [ - bumpHistorySyncGeneration, - client, - requestCanonicalCatchUp, - scheduleAuthoritativeRevalidation, - serverId, - ], + [bumpHistorySyncGeneration, serverId], ); // Client activity tracking (heartbeat, push token registration) @@ -944,11 +517,13 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider // Initialize session in store useEffect(() => { - initializeSession(serverId, client); + const generation = getHostRuntimeStore().getSnapshot(serverId)?.clientGeneration ?? 0; + initializeSession(serverId, client, generation); }, [serverId, client, initializeSession]); useEffect(() => { - updateSessionClient(serverId, client); + const generation = getHostRuntimeStore().getSnapshot(serverId)?.clientGeneration ?? 0; + updateSessionClient(serverId, client, generation); }, [serverId, client, updateSessionClient]); useEffect(() => { @@ -1024,112 +599,10 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider useEffect(() => { if (!isConnected) { flushAgentLastActivity(); - clearPendingAgentUpdates(serverId); setInitializingAgents(serverId, new Map()); } }, [flushAgentLastActivity, serverId, isConnected, setInitializingAgents]); - useEffect(() => { - if (!client || !isConnected) { - return () => {}; - } - - let cancelled = false; - void (async () => { - try { - await hydrateWorkspaces({ - subscribe: true, - isCancelled: () => cancelled, - }); - } catch (error) { - console.error("[Session] Failed to hydrate workspaces:", error); - } - })(); - - return () => { - cancelled = true; - }; - }, [client, hydrateWorkspaces, isConnected]); - - const applyAgentUpdatePayload = useCallback( - (update: AgentUpdatePayload) => { - if (update.kind === "remove") { - const agentId = update.agentId; - previousAgentStatusRef.current.delete(agentId); - deletePendingAgentUpdate(serverId, agentId); - clearArchiveAgentPending({ queryClient, serverId, agentId }); - - setAgents(serverId, (prev) => { - if (!prev.has(agentId)) { - return prev; - } - const next = new Map(prev); - next.delete(agentId); - return next; - }); - - setPendingPermissions(serverId, (prev) => { - if (prev.size === 0) { - return prev; - } - let changed = false; - const next = new Map(prev); - for (const [key, pending] of Array.from(next.entries())) { - if (pending.agentId === agentId) { - next.delete(key); - changed = true; - } - } - return changed ? next : prev; - }); - - setQueuedMessages(serverId, (prev) => { - if (!prev.has(agentId)) { - return prev; - } - const next = new Map(prev); - next.delete(agentId); - return next; - }); - - setAgentTimelineCursor(serverId, (prev) => { - if (!prev.has(agentId)) { - return prev; - } - const next = new Map(prev); - next.delete(agentId); - return next; - }); - setAgentAuthoritativeHistoryApplied(serverId, agentId, false); - return; - } - - const normalized = normalizeAgentSnapshot(update.agent, serverId); - const agent = applyLegacyDaemonWorkspaceOwnership({ - serverId, - agent: { - ...normalized, - projectPlacement: resolveProjectPlacement({ - projectPlacement: update.project, - cwd: normalized.cwd, - }), - }, - }); - - applyAuthoritativeAgentSnapshot(agent); - }, - [ - applyAuthoritativeAgentSnapshot, - queryClient, - serverId, - setAgentAuthoritativeHistoryApplied, - setAgents, - setAgentTimelineCursor, - setPendingPermissions, - setQueuedMessages, - ], - ); - const applyWorkspaceSetupProgress = useCallback( (payload: WorkspaceSetupProgressPayload) => { upsertWorkspaceSetupProgress({ serverId, payload }); @@ -1172,19 +645,6 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider return next; }); - if (payload.agent) { - const normalized = normalizeAgentSnapshot(payload.agent, serverId); - applyAuthoritativeAgentSnapshot( - applyLegacyDaemonWorkspaceOwnership({ - serverId, - agent: { - ...normalized, - projectPlacement: session?.agents.get(agentId)?.projectPlacement ?? null, - }, - }), - ); - } - // Call pure reducer const result = processTimelineResponse({ payload, @@ -1222,34 +682,9 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider executeTimelineSideEffects({ sideEffects: result.sideEffects, agentId, - serverId, - requestCanonicalCatchUp, - applyAgentUpdatePayload, + recoverTimelineGap, }); - const followUp = planTimelineCatchUpFollowUp({ - direction: payload.direction, - hasNewer: payload.hasNewer, - endCursor: payload.endCursor, - error: payload.error, - }); - if (followUp?.direction === "after") { - refreshAgentInitializationTimeout({ - key: initKey, - agentId, - setAgentInitializing: (id, initializing) => { - if (initializing) { - return; - } - clearAgentInitializingFlag(setInitializingAgents, serverId, id); - }, - }); - requestCanonicalCatchUp(agentId, { - epoch: followUp.cursor.epoch, - endSeq: followUp.cursor.seq, - }); - } - finalizeTimelineApplication({ result, agentId, @@ -1262,11 +697,9 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider }); }, [ - applyAuthoritativeAgentSnapshot, - applyAgentUpdatePayload, clearAgentStreamHead, markAgentHistorySynchronized, - requestCanonicalCatchUp, + recoverTimelineGap, serverId, setAgentAuthoritativeHistoryApplied, setAgentStreamHead, @@ -1278,54 +711,75 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider ); useEffect(() => { - if (isConnected) { - return; - } - clearPendingAgentUpdates(serverId); - }, [isConnected, serverId]); + const setAgentInitializing = createSetAgentInitializing(serverId, setInitializingAgents); + const sync = createViewedTimelineSync({ + setSubscription: (agentIds) => client.setAgentTimelineSubscription(agentIds), + readCursor: (agentId) => + useSessionStore.getState().sessions[serverId]?.agentTimelineCursor.get(agentId), + hasAuthoritativeHistory: (agentId) => + useSessionStore + .getState() + .sessions[serverId]?.agentAuthoritativeHistoryApplied.get(agentId) === true, + fetchPage: async (agentId, request) => { + const session = useSessionStore.getState().sessions[serverId]; + const initKey = getInitKey(serverId, agentId); + if (session?.agentAuthoritativeHistoryApplied.get(agentId) !== true) { + if (!getInitDeferred(initKey)) { + const deferred = createInitDeferred(initKey, request.direction ?? "tail"); + void deferred.promise.catch(() => undefined); + } + refreshAgentInitializationTimeout({ + key: initKey, + agentId, + setAgentInitializing, + }); + setAgentInitializing(agentId, true); + } + try { + const page = await getHostRuntimeStore().fetchAgentTimeline(serverId, agentId, request); + if (getInitDeferred(initKey)) { + refreshAgentInitializationTimeout({ key: initKey, agentId, setAgentInitializing }); + } + return page; + } catch (error) { + setAgentInitializing(agentId, false); + rejectInitDeferred(initKey, error instanceof Error ? error : new Error(String(error))); + throw error; + } + }, + reportError: (error) => { + console.warn("[Session] viewed timeline synchronization failed", { serverId, error }); + }, + scheduleRetry: (retry) => { + const timeout = setTimeout(retry, 1_000); + return () => clearTimeout(timeout); + }, + }); + viewedTimelineSyncRef.current = sync; + setViewedTimelineSync(serverId, sync); + sync.setActive(getIsAppActivelyVisible(appStateRef.current)); - useEffect(() => { - const wasConnected = wasConnectedRef.current; - wasConnectedRef.current = isConnected; - if (!wasConnected && isConnected) { - scheduleAuthoritativeRevalidation(); - } - }, [isConnected, scheduleAuthoritativeRevalidation]); - - useEffect(() => { return () => { - if (revalidationTimerRef.current) { - clearTimeout(revalidationTimerRef.current); + if (viewedTimelineSyncRef.current === sync) { + viewedTimelineSyncRef.current = null; } + setViewedTimelineSync(serverId, null); + sync.dispose(); }; - }, []); + }, [client, serverId, setInitializingAgents, setViewedTimelineSync]); + + useEffect(() => { + viewedTimelineSyncRef.current?.setConnected(isConnected); + }, [isConnected]); // Daemon message handlers - directly update Zustand store useEffect(() => { - const unsubAgentUpdate = client.on("agent_update", (message) => { - if (message.type !== "agent_update") return; - const update = message.payload; - const agentId = getAgentIdFromUpdate(update); - const initKey = getInitKey(serverId, agentId); - const session = useSessionStore.getState().sessions[serverId]; - const isSyncingHistory = - session?.initializingAgents.get(agentId) === true && Boolean(getInitDeferred(initKey)); - - if (isSyncingHistory) { - bufferPendingAgentUpdate(serverId, agentId, update); - return; - } - - deletePendingAgentUpdate(serverId, agentId); - applyAgentUpdatePayload(update); - }); - const agentStreamReducerQueue = createSessionAgentStreamReducerQueue({ serverId, setAgentStreamState, setAgentTimelineCursor, setAgents, - requestCanonicalCatchUp, + recoverTimelineGap, }); const unsubAgentStream = client.on("agent_stream", (message) => { @@ -1342,18 +796,6 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider voiceRuntime?.onTurnEvent(serverId, agentId, event.type); } - // Attention notification stays in React (not extractable to pure reducer) - if (event.type === "attention_required") { - if (event.shouldNotify) { - notifyAgentAttention({ - agentId, - reason: event.reason, - timestamp: event.timestamp, - notification: event.notification, - }); - } - } - agentStreamReducerQueue.enqueue(agentId, { event: streamEvent, seq, @@ -1366,6 +808,12 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider // on status changes, which is sufficient for sorting and display purposes. }); + const unsubAgentAttention = client.onAgentAttentionRequired((notification) => { + if (notification.shouldNotify) { + notifyAgentAttention(notification); + } + }); + const unsubAgentTimeline = client.on("fetch_agent_timeline_response", (message) => { if (message.type !== "fetch_agent_timeline_response") return; agentStreamReducerQueue.flushAgent(message.payload.agentId); @@ -1377,30 +825,6 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider useProviderSubagentStore.getState().applyUpdate(serverId, message.payload); }); - const unsubWorkspaceUpdate = client.on("workspace_update", (message) => { - if (message.type !== "workspace_update") return; - if (message.payload.kind === "remove") { - clearWorkspaceArchivePending({ - serverId, - workspaceId: message.payload.id, - }); - removeWorkspaceSetup({ serverId, workspaceId: message.payload.id }); - removeWorkspace(serverId, message.payload.id); - if (message.payload.emptyProject) { - addEmptyProject(serverId, normalizeEmptyProjectDescriptor(message.payload.emptyProject)); - } - if (message.payload.removedProjectId) { - removeEmptyProject(serverId, message.payload.removedProjectId); - } - return; - } - const workspace = normalizeWorkspaceDescriptor(message.payload.workspace); - if (shouldSuppressWorkspaceForLocalArchive({ serverId, workspace })) { - return; - } - mergeWorkspaces(serverId, [workspace]); - }); - const unsubScriptStatusUpdate = client.on("script_status_update", (message) => { if (message.type !== "script_status_update") return; setWorkspaces(serverId, (prev) => patchWorkspaceScripts(prev, message.payload)); @@ -1662,102 +1086,6 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider voiceRuntime?.onServerSpeechStateChanged(serverId, message.payload.isSpeaking); }); - const unsubAgentDeleted = client.on("agent_deleted", (message) => { - if (message.type !== "agent_deleted") { - return; - } - const { agentId } = message.payload; - deletePendingAgentUpdate(serverId, agentId); - clearArchiveAgentPending({ queryClient, serverId, agentId }); - - setAgents(serverId, (prev) => { - if (!prev.has(agentId)) { - return prev; - } - const next = new Map(prev); - next.delete(agentId); - return next; - }); - - // Remove from agentLastActivity slice (top-level) - useSessionStore.setState((state) => { - if (!state.agentLastActivity.has(agentId)) { - return state; - } - const nextActivity = new Map(state.agentLastActivity); - nextActivity.delete(agentId); - return { - ...state, - agentLastActivity: nextActivity, - }; - }); - - setAgentStreamTail(serverId, (prev) => { - if (!prev.has(agentId)) { - return prev; - } - const next = new Map(prev); - next.delete(agentId); - return next; - }); - clearAgentStreamHead(serverId, agentId); - setAgentTimelineCursor(serverId, (prev) => { - if (!prev.has(agentId)) { - return prev; - } - const next = new Map(prev); - next.delete(agentId); - return next; - }); - - // Remove draft input - clearDraftInput({ - draftKey: buildDraftStoreKey({ serverId, agentId }), - }); - - setPendingPermissions(serverId, (prev) => { - let changed = false; - const next = new Map(prev); - for (const [key, pending] of prev.entries()) { - if (pending.agentId === agentId) { - next.delete(key); - changed = true; - } - } - return changed ? next : prev; - }); - - setInitializingAgents(serverId, (prev) => { - if (!prev.has(agentId)) { - return prev; - } - const next = new Map(prev); - next.delete(agentId); - return next; - }); - }); - - const unsubAgentArchived = client.on("agent_archived", (message) => { - if (message.type !== "agent_archived") { - return; - } - const { agentId, archivedAt } = message.payload; - clearArchiveAgentPending({ queryClient, serverId, agentId }); - - setAgents(serverId, (prev) => { - const existing = prev.get(agentId); - if (!existing) { - return prev; - } - const next = new Map(prev); - next.set(agentId, { - ...existing, - archivedAt: new Date(archivedAt), - }); - return next; - }); - }); - const unsubTerminalAttention = client.on("terminal_attention_required", (message) => { if (message.type !== "terminal_attention_required") { return; @@ -1780,11 +1108,10 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider }); return () => { - unsubAgentUpdate(); unsubAgentStream(); unsubAgentTimeline(); unsubProviderSubagentUpdate(); - unsubWorkspaceUpdate(); + unsubAgentAttention(); unsubScriptStatusUpdate(); unsubCheckoutStatusUpdate(); unsubWorkspaceSetupProgress(); @@ -1797,8 +1124,6 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider unsubChunk(); unsubTranscription(); unsubVoiceInputState(); - unsubAgentDeleted(); - unsubAgentArchived(); unsubTerminalAttention(); agentStreamReducerQueue.dispose({ flush: true }); }; @@ -1817,18 +1142,9 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider setInitializingAgents, setAgents, setWorkspaces, - mergeWorkspaces, - removeWorkspace, - removeWorkspaceSetup, - addEmptyProject, - removeEmptyProject, - setAgentLastActivity, setPendingPermissions, - setHasHydratedAgents, - clearDraftInput, notifyAgentAttention, - requestCanonicalCatchUp, - applyAgentUpdatePayload, + recoverTimelineGap, applyWorkspaceSetupProgress, applyTimelineResponse, updateSessionServerInfo, @@ -1837,67 +1153,6 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider voiceAudioEngine, ]); - const sendAgentMessage = useCallback( - async ( - agentId: string, - message: string, - images?: AttachmentMetadata[], - attachments?: AgentAttachment[], - ) => { - const messageId = generateMessageId(); - const userMessage: StreamItem = { - kind: "user_message", - id: messageId, - text: message, - timestamp: new Date(), - optimistic: true, - ...(images && images.length > 0 ? { images } : {}), - ...(attachments && attachments.length > 0 ? { attachments } : {}), - }; - - // Append to head if streaming (keeps the user message with the current - // turn so late text_deltas still find the existing assistant_message). - // Otherwise append to tail. - const currentHead = useSessionStore - .getState() - .sessions[serverId]?.agentStreamHead?.get(agentId); - if (currentHead && currentHead.length > 0) { - setAgentStreamHead(serverId, (prev) => { - const head = prev.get(agentId) || []; - const updated = new Map(prev); - updated.set(agentId, [...head, userMessage]); - return updated; - }); - } else { - setAgentStreamTail(serverId, (prev) => { - const currentStream = prev.get(agentId) || []; - const updated = new Map(prev); - updated.set(agentId, [...currentStream, userMessage]); - return updated; - }); - } - - const imagesData = await encodeImages(images); - if (!client) { - console.warn("[Session] sendAgentMessage skipped: daemon unavailable"); - return; - } - void client - .sendAgentMessage(agentId, message, { - messageId, - ...(imagesData && imagesData.length > 0 ? { images: imagesData } : {}), - ...(attachments && attachments.length > 0 ? { attachments } : {}), - }) - .catch((error) => { - console.error("[Session] Failed to send agent message:", error); - }); - }, - [serverId, client, setAgentStreamTail, setAgentStreamHead], - ); - - // Keep the ref updated so the agent_update handler can call it - sendAgentMessageRef.current = sendAgentMessage; - const _cancelAgentRun = useCallback( (agentId: string) => { if (!client) { diff --git a/packages/app/src/contexts/session-resume-revalidation.test.ts b/packages/app/src/contexts/session-resume-revalidation.test.ts new file mode 100644 index 000000000..ebc90b68a --- /dev/null +++ b/packages/app/src/contexts/session-resume-revalidation.test.ts @@ -0,0 +1,35 @@ +import { describe, expect, it } from "vitest"; +import { + revalidateSessionAfterResume, + SESSION_STALE_AFTER_MS, +} from "./session-resume-revalidation"; + +describe("session resume revalidation", () => { + it("refreshes both directories and timeline history after a stale resume", async () => { + const calls: string[] = []; + + const revalidated = await revalidateSessionAfterResume({ + awayMs: SESSION_STALE_AFTER_MS, + serverId: "server", + bumpHistorySyncGeneration: (serverId) => calls.push(`history:${serverId}`), + refreshDirectories: async () => calls.push("directories"), + }); + + expect(revalidated).toBe(true); + expect(calls).toEqual(["history:server", "directories"]); + }); + + it("does nothing after a brief background interval", async () => { + const calls: string[] = []; + + const revalidated = await revalidateSessionAfterResume({ + awayMs: SESSION_STALE_AFTER_MS - 1, + serverId: "server", + bumpHistorySyncGeneration: () => calls.push("history"), + refreshDirectories: async () => calls.push("directories"), + }); + + expect(revalidated).toBe(false); + expect(calls).toEqual([]); + }); +}); diff --git a/packages/app/src/contexts/session-resume-revalidation.ts b/packages/app/src/contexts/session-resume-revalidation.ts new file mode 100644 index 000000000..8360316f5 --- /dev/null +++ b/packages/app/src/contexts/session-resume-revalidation.ts @@ -0,0 +1,16 @@ +export const SESSION_STALE_AFTER_MS = 60_000; + +export async function revalidateSessionAfterResume(input: { + awayMs: number; + serverId: string; + bumpHistorySyncGeneration: (serverId: string) => void; + refreshDirectories: () => Promise; +}): Promise { + if (input.awayMs < SESSION_STALE_AFTER_MS) { + return false; + } + + input.bumpHistorySyncGeneration(input.serverId); + await input.refreshDirectories(); + return true; +} diff --git a/packages/app/src/contexts/workspace-directory-reconciliation.test.ts b/packages/app/src/contexts/workspace-directory-reconciliation.test.ts new file mode 100644 index 000000000..851f784c1 --- /dev/null +++ b/packages/app/src/contexts/workspace-directory-reconciliation.test.ts @@ -0,0 +1,63 @@ +import { expect, it } from "vitest"; +import type { WorkspaceDescriptorPayload } from "@getpaseo/protocol/messages"; +import { normalizeWorkspaceDescriptor } from "@/stores/session-store"; +import { + clearWorkspaceArchivePending, + markWorkspaceArchivePending, +} from "./session-workspace-upserts"; +import { reconcileWorkspaceDirectory } from "./workspace-directory-reconciliation"; + +const SERVER_ID = "workspace-directory-reconciliation"; + +function workspace(id: string, title: string): WorkspaceDescriptorPayload { + return { + id, + projectId: "project", + projectDisplayName: "Project", + projectRootPath: "/repo", + workspaceDirectory: `/repo/${id}`, + projectKind: "git", + workspaceKind: "worktree", + name: id, + title, + status: "done", + activityAt: null, + statusEnteredAt: null, + archivingAt: null, + diffStat: null, + scripts: [], + }; +} + +it("keeps workspace upserts and removals received during later pages", () => { + const result = reconcileWorkspaceDirectory({ + serverId: SERVER_ID, + snapshot: new Map([ + ["updated", normalizeWorkspaceDescriptor(workspace("updated", "snapshot"))], + ["removed", normalizeWorkspaceDescriptor(workspace("removed", "snapshot"))], + ]), + deltas: [ + { kind: "upsert", workspace: workspace("updated", "live") }, + { kind: "remove", id: "removed" }, + ], + }); + + expect(Array.from(result.values()).map(({ id, title }) => [id, title])).toEqual([ + ["updated", "live"], + ]); +}); + +it("does not restore a locally archiving workspace from a buffered upsert", () => { + markWorkspaceArchivePending({ serverId: SERVER_ID, workspaceId: "archiving" }); + try { + const result = reconcileWorkspaceDirectory({ + serverId: SERVER_ID, + snapshot: new Map(), + deltas: [{ kind: "upsert", workspace: workspace("archiving", "live") }], + }); + + expect(result.has("archiving")).toBe(false); + } finally { + clearWorkspaceArchivePending({ serverId: SERVER_ID, workspaceId: "archiving" }); + } +}); diff --git a/packages/app/src/contexts/workspace-directory-reconciliation.ts b/packages/app/src/contexts/workspace-directory-reconciliation.ts new file mode 100644 index 000000000..4be9cdd9f --- /dev/null +++ b/packages/app/src/contexts/workspace-directory-reconciliation.ts @@ -0,0 +1,26 @@ +import type { SessionOutboundMessage } from "@getpaseo/protocol/messages"; +import { normalizeWorkspaceDescriptor, type WorkspaceDescriptor } from "@/stores/session-store"; +import { shouldSuppressWorkspaceForLocalArchive } from "./session-workspace-upserts"; + +type WorkspaceDelta = Extract["payload"]; + +export function reconcileWorkspaceDirectory(input: { + serverId: string; + snapshot: ReadonlyMap; + deltas: readonly WorkspaceDelta[]; +}): Map { + const workspaces = new Map(input.snapshot); + for (const delta of input.deltas) { + if (delta.kind === "remove") { + workspaces.delete(delta.id); + } else { + const workspace = normalizeWorkspaceDescriptor(delta.workspace); + if (shouldSuppressWorkspaceForLocalArchive({ serverId: input.serverId, workspace })) { + workspaces.delete(workspace.id); + } else { + workspaces.set(workspace.id, workspace); + } + } + } + return workspaces; +} diff --git a/packages/app/src/diagnostics/app-diagnostic-report.test.ts b/packages/app/src/diagnostics/app-diagnostic-report.test.ts index c01244d96..199ca22a6 100644 --- a/packages/app/src/diagnostics/app-diagnostic-report.test.ts +++ b/packages/app/src/diagnostics/app-diagnostic-report.test.ts @@ -79,6 +79,7 @@ describe("app diagnostics report", () => { ["relay:relay.secret.test:443", { status: "available", latencyMs: 8 }], ]), clientGeneration: 1, + connectionEpoch: 1, }; const report = formatHostRuntimeSection({ host, snapshot }); diff --git a/packages/app/src/hooks/use-agent-initialization.test.ts b/packages/app/src/hooks/use-agent-initialization.test.ts index 2ef638caf..52fb58e1e 100644 --- a/packages/app/src/hooks/use-agent-initialization.test.ts +++ b/packages/app/src/hooks/use-agent-initialization.test.ts @@ -1,6 +1,7 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { useSessionStore } from "@/stores/session-store"; import { TIMELINE_FETCH_PAGE_SIZE } from "@/timeline/timeline-fetch-policy"; +import type { HostRuntimeStore } from "@/runtime/host-runtime"; import { getInitDeferred, getInitKey, resolveInitDeferred } from "@/utils/agent-initialization"; import { createSetAgentInitializing, @@ -12,15 +13,28 @@ import { const serverId = "server-1"; const agentId = "agent-1"; -interface FakeDaemonClient { - fetchAgentTimeline: ReturnType; - refreshAgent: ReturnType; +class FakeDaemonClient { + readonly refreshedAgentIds: string[] = []; + + async refreshAgent(requestedAgentId: string): Promise { + this.refreshedAgentIds.push(requestedAgentId); + } } -function makeClient(): FakeDaemonClient { - return { - fetchAgentTimeline: vi.fn().mockResolvedValue(undefined), - refreshAgent: vi.fn().mockResolvedValue(undefined), +class FakeTimelineRuntime { + readonly requests: Array<{ + serverId: string; + agentId: string; + request: Parameters[2]; + }> = []; + + fetchAgentTimeline: HostRuntimeStore["fetchAgentTimeline"] = async ( + requestedServerId, + requestedAgentId, + request, + ) => { + this.requests.push({ serverId: requestedServerId, agentId: requestedAgentId, request }); + return undefined as never; }; } @@ -36,7 +50,8 @@ afterEach(() => { describe("ensureAgentIsInitialized", () => { it("requests bounded projected catch-up after the current cursor when authoritative history is loaded", () => { - const client = makeClient(); + const client = new FakeDaemonClient(); + const runtime = new FakeTimelineRuntime(); useSessionStore.getState().initializeSession(serverId, client as never); useSessionStore .getState() @@ -50,46 +65,63 @@ describe("ensureAgentIsInitialized", () => { serverId, agentId, client: client as never, + runtime, setAgentInitializing: bindSetAgentInitializing(), }); - expect(client.fetchAgentTimeline).toHaveBeenCalledWith(agentId, { - direction: "after", - cursor: { epoch: "epoch-1", seq: 42 }, - limit: TIMELINE_FETCH_PAGE_SIZE, - projection: "projected", - }); + expect(runtime.requests).toEqual([ + { + serverId, + agentId, + request: { + direction: "after", + cursor: { epoch: "epoch-1", seq: 42 }, + limit: TIMELINE_FETCH_PAGE_SIZE, + projection: "projected", + }, + }, + ]); expect(getInitDeferred(getInitKey(serverId, agentId))?.requestDirection).toBe("after"); }); it("requests a bounded projected tail when no authoritative cursor is available", () => { - const client = makeClient(); + const client = new FakeDaemonClient(); + const runtime = new FakeTimelineRuntime(); useSessionStore.getState().initializeSession(serverId, client as never); void ensureAgentIsInitialized({ serverId, agentId, client: client as never, + runtime, setAgentInitializing: bindSetAgentInitializing(), }); - expect(client.fetchAgentTimeline).toHaveBeenCalledWith(agentId, { - direction: "tail", - limit: TIMELINE_FETCH_PAGE_SIZE, - projection: "projected", - }); + expect(runtime.requests).toEqual([ + { + serverId, + agentId, + request: { + direction: "tail", + limit: TIMELINE_FETCH_PAGE_SIZE, + projection: "projected", + }, + }, + ]); expect(getInitDeferred(getInitKey(serverId, agentId))?.requestDirection).toBe("tail"); }); it("times out initialization after 65 seconds", async () => { vi.useFakeTimers(); - const client = makeClient(); + const client = new FakeDaemonClient(); + const runtime = new FakeTimelineRuntime(); useSessionStore.getState().initializeSession(serverId, client as never); const promise = ensureAgentIsInitialized({ serverId, agentId, client: client as never, + runtime, setAgentInitializing: bindSetAgentInitializing(), }); @@ -108,7 +140,8 @@ describe("ensureAgentIsInitialized", () => { it("refreshes the initialization timeout after paged catch-up progress", async () => { vi.useFakeTimers(); - const client = makeClient(); + const client = new FakeDaemonClient(); + const runtime = new FakeTimelineRuntime(); useSessionStore.getState().initializeSession(serverId, client as never); const setAgentInitializing = bindSetAgentInitializing(); const key = getInitKey(serverId, agentId); @@ -117,6 +150,7 @@ describe("ensureAgentIsInitialized", () => { serverId, agentId, client: client as never, + runtime, setAgentInitializing, }); @@ -141,20 +175,29 @@ describe("ensureAgentIsInitialized", () => { describe("refreshAgent", () => { it("fetches a bounded projected tail after refreshing the agent", async () => { - const client = makeClient(); + const client = new FakeDaemonClient(); + const runtime = new FakeTimelineRuntime(); useSessionStore.getState().initializeSession(serverId, client as never); await refreshAgent({ + serverId, agentId, client: client as never, + runtime, setAgentInitializing: bindSetAgentInitializing(), }); - expect(client.refreshAgent).toHaveBeenCalledWith(agentId); - expect(client.fetchAgentTimeline).toHaveBeenCalledWith(agentId, { - direction: "tail", - limit: TIMELINE_FETCH_PAGE_SIZE, - projection: "projected", - }); + expect(client.refreshedAgentIds).toEqual([agentId]); + expect(runtime.requests).toEqual([ + { + serverId, + agentId, + request: { + direction: "tail", + limit: TIMELINE_FETCH_PAGE_SIZE, + projection: "projected", + }, + }, + ]); }); }); diff --git a/packages/app/src/hooks/use-agent-initialization.ts b/packages/app/src/hooks/use-agent-initialization.ts index b9a76bc99..ae80d352e 100644 --- a/packages/app/src/hooks/use-agent-initialization.ts +++ b/packages/app/src/hooks/use-agent-initialization.ts @@ -10,6 +10,7 @@ import { rejectInitDeferred, refreshInitTimeout, } from "@/utils/agent-initialization"; +import { getHostRuntimeStore, type HostRuntimeStore } from "@/runtime/host-runtime"; import { planInitialAgentTimelineSync, planTimelineTailFetch } from "@/timeline/timeline-sync-plan"; import { i18n } from "@/i18n/i18next"; @@ -37,6 +38,7 @@ export interface EnsureAgentIsInitializedInput { serverId: string; agentId: string; client: Pick | null; + runtime: Pick; setAgentInitializing: SetAgentInitializing; hostDisconnectedMessage?: string; } @@ -68,7 +70,7 @@ export function ensureAgentIsInitialized(input: EnsureAgentIsInitializedInput): return deferred.promise; } - client.fetchAgentTimeline(agentId, timelineRequest).catch((error) => { + input.runtime.fetchAgentTimeline(serverId, agentId, timelineRequest).catch((error) => { setAgentInitializing(agentId, false); rejectInitDeferred(key, error instanceof Error ? error : new Error(String(error))); }); @@ -77,14 +79,16 @@ export function ensureAgentIsInitialized(input: EnsureAgentIsInitializedInput): } export interface RefreshAgentInput { + serverId: string; agentId: string; - client: Pick | null; + client: Pick | null; + runtime: Pick; setAgentInitializing: SetAgentInitializing; hostDisconnectedMessage?: string; } export async function refreshAgent(input: RefreshAgentInput): Promise { - const { agentId, client, setAgentInitializing } = input; + const { serverId, agentId, client, runtime, setAgentInitializing } = input; if (!client) { throw new Error(input.hostDisconnectedMessage ?? i18n.t("workspace.terminal.hostDisconnected")); } @@ -92,7 +96,7 @@ export async function refreshAgent(input: RefreshAgentInput): Promise { try { await client.refreshAgent(agentId); - await client.fetchAgentTimeline(agentId, planTimelineTailFetch()); + await runtime.fetchAgentTimeline(serverId, agentId, planTimelineTailFetch()); } catch (error) { setAgentInitializing(agentId, false); throw error; @@ -135,6 +139,7 @@ export function useAgentInitialization({ serverId, agentId, client, + runtime: getHostRuntimeStore(), setAgentInitializing, hostDisconnectedMessage: t("workspace.terminal.hostDisconnected"), }), @@ -144,12 +149,14 @@ export function useAgentInitialization({ const refreshAgentCallback = useCallback( (agentId: string): Promise => refreshAgent({ + serverId, agentId, client, + runtime: getHostRuntimeStore(), setAgentInitializing, hostDisconnectedMessage: t("workspace.terminal.hostDisconnected"), }), - [client, setAgentInitializing, t], + [client, serverId, setAgentInitializing, t], ); return { diff --git a/packages/app/src/hooks/use-load-older-agent-history.ts b/packages/app/src/hooks/use-load-older-agent-history.ts index 775f5f23a..c2cf7c02c 100644 --- a/packages/app/src/hooks/use-load-older-agent-history.ts +++ b/packages/app/src/hooks/use-load-older-agent-history.ts @@ -4,6 +4,7 @@ import type { ToastApi } from "@/components/toast-host"; import { i18n } from "@/i18n/i18next"; import { useSessionStore, type AgentTimelineCursorState } from "@/stores/session-store"; import { planTimelineOlderFetch } from "@/timeline/timeline-sync-plan"; +import { getHostRuntimeStore } from "@/runtime/host-runtime"; export interface LoadOlderAgentHistoryClient { fetchAgentTimeline: ( @@ -97,7 +98,12 @@ export function useLoadOlderAgentHistory({ const loadOlder = useCallback(() => { const session = useSessionStore.getState().sessions[serverId]; void loadOlderAgentHistory(agentId, { - client: (session?.client ?? null) as LoadOlderAgentHistoryClient | null, + client: session?.client + ? { + fetchAgentTimeline: (timelineAgentId, request) => + getHostRuntimeStore().fetchAgentTimeline(serverId, timelineAgentId, request), + } + : null, cursor: session?.agentTimelineCursor.get(agentId), hasOlder: session?.agentTimelineHasOlder.get(agentId) === true, isLoadingOlder: session?.agentTimelineOlderFetchInFlight.get(agentId) === true, diff --git a/packages/app/src/panels/agent-panel.tsx b/packages/app/src/panels/agent-panel.tsx index ba7b899a7..d4102d30c 100644 --- a/packages/app/src/panels/agent-panel.tsx +++ b/packages/app/src/panels/agent-panel.tsx @@ -13,6 +13,7 @@ import { useStoreWithEqualityFn } from "zustand/traditional"; import { AgentStreamView, type AgentStreamViewHandle } from "@/agent-stream/view"; import { ArchivedAgentCallout } from "@/components/archived-agent-callout"; import { FileDropZone } from "@/components/file-drop/file-drop-zone"; +import { useRetainedPanelActive } from "@/components/retained-panel"; import { Composer } from "@/composer"; import { AgentModeControl } from "@/composer/agent-controls/mode-control"; import { RewindComposerRestoreProvider } from "@/components/rewind/composer-restore"; @@ -26,7 +27,7 @@ import { import type { WorkspaceComposerAttachment } from "@/attachments/types"; import { useWorkspaceAttachmentScopeKey } from "@/attachments/workspace-attachments-store"; import { COMPACT_FORM_FACTOR_WIDTH, useIsCompactFormFactor } from "@/constants/layout"; -import { isNative, isWeb } from "@/constants/platform"; +import { isWeb } from "@/constants/platform"; import { useAgentAttentionClear } from "@/hooks/use-agent-attention-clear"; import { useAgentInitialization } from "@/hooks/use-agent-initialization"; import { useAgentInputDraft, type AgentInputDraft } from "@/composer/draft/input-draft"; @@ -40,10 +41,7 @@ import { import { useArchiveAgent } from "@/hooks/use-archive-agent"; import { useKeyboardShiftStyle } from "@/hooks/use-keyboard-shift-style"; import { useContainerWidthBelow } from "@/hooks/use-container-width"; -import { - clearHistorySyncErrorAfterSuccessfulSync, - reconcileMissingAgentStateWithPresentAgent, -} from "@/panels/agent-panel-load-state"; +import { reconcileMissingAgentStateWithPresentAgent } from "@/panels/agent-panel-load-state"; import { usePaneContext, usePaneFocus } from "@/panels/pane-context"; import type { PanelDescriptor, PanelRegistration } from "@/panels/panel-registry"; import { RenderProfile } from "@/utils/render-profiler"; @@ -709,6 +707,7 @@ function ChatAgentContent({ onOpenWorkspaceFile?: (request: WorkspaceFileOpenRequest) => void; }) { const { t } = useTranslation(); + const isPaneVisible = useRetainedPanelActive(); const { api: toastApi, toast: toastState, dismiss: dismissToast } = useToastHost(); const { isArchivingAgent } = useArchiveAgent(); const streamViewRef = useRef(null); @@ -782,44 +781,6 @@ function ChatAgentContent({ mode: "translate", }); - const handleHistorySyncFailure = useCallback( - ({ origin, error }: { origin: "focus" | "entry"; error: unknown }) => { - if (agentId) { - console.warn("[AgentPanel] history sync failed", { - origin, - agentId, - error, - }); - } - const message = toErrorMessage(error); - setMissingAgentState((previous) => { - if (previous.kind === "error" && previous.message === message) { - return previous; - } - return { kind: "error", message }; - }); - }, - [agentId], - ); - - const ensureInitializedWithSyncErrorHandling = useCallback( - (origin: "focus" | "entry") => { - if (!agentId) { - return; - } - ensureAgentIsInitialized(agentId) - .then(() => { - setMissingAgentState(clearHistorySyncErrorAfterSuccessfulSync); - return undefined; - }) - .catch((error) => { - handleHistorySyncFailure({ origin, error }); - return undefined; - }); - }, - [agentId, ensureAgentIsInitialized, handleHistorySyncFailure], - ); - useEffect(() => { if (connectionStatus === "online") { if (reconnectToastArmedRef.current) { @@ -840,13 +801,6 @@ function ChatAgentContent({ } }, [connectionStatus, dismissToast, toastApi, t]); - useEffect(() => { - if (!isPaneFocused || !agentId || !isConnected || !hasSession) { - return; - } - ensureInitializedWithSyncErrorHandling("focus"); - }, [agentId, ensureInitializedWithSyncErrorHandling, hasSession, isConnected, isPaneFocused]); - const isArchivingCurrentAgent = Boolean(agentId && isArchivingAgent({ serverId, agentId })); useEffect(() => { @@ -946,27 +900,6 @@ function ChatAgentContent({ streamViewRef.current?.scrollToBottom("message-sent"); }, [agentId]); - useEffect(() => { - if (!agentId) { - return; - } - if (!isConnected || !hasSession) { - return; - } - const shouldSyncOnEntry = needsAuthoritativeSync || isNative; - if (!shouldSyncOnEntry) { - return; - } - - ensureInitializedWithSyncErrorHandling("entry"); - }, [ - agentId, - ensureInitializedWithSyncErrorHandling, - hasSession, - isConnected, - needsAuthoritativeSync, - ]); - useEffect(() => { initAttemptTokenRef.current += 1; setMissingAgentState({ kind: "idle" }); @@ -976,16 +909,24 @@ function ChatAgentContent({ if (!agentId) { return; } - if (agentState.id) { - if (missingAgentState.kind === "resolving" || missingAgentState.kind === "not_found") { + if (agentState.id && hasAppliedAuthoritativeHistory) { + if ( + missingAgentState.kind === "resolving" || + missingAgentState.kind === "not_found" || + missingAgentState.kind === "error" + ) { setMissingAgentState(reconcileMissingAgentStateWithPresentAgent); } return; } - if (!isConnected || !hasSession) { + if (!isPaneVisible || !isConnected || !hasSession) { return; } - if (missingAgentState.kind === "resolving" || missingAgentState.kind === "not_found") { + if ( + missingAgentState.kind === "resolving" || + missingAgentState.kind === "not_found" || + missingAgentState.kind === "error" + ) { return; } @@ -1033,11 +974,13 @@ function ChatAgentContent({ }); }, [ agentState.id, + hasAppliedAuthoritativeHistory, agentId, client, ensureAgentIsInitialized, hasSession, isConnected, + isPaneVisible, missingAgentState.kind, serverId, ]); diff --git a/packages/app/src/runtime/directory-sync/agent-replica.test.ts b/packages/app/src/runtime/directory-sync/agent-replica.test.ts new file mode 100644 index 000000000..4456ecc0a --- /dev/null +++ b/packages/app/src/runtime/directory-sync/agent-replica.test.ts @@ -0,0 +1,86 @@ +import { describe, expect, it } from "vitest"; +import type { DaemonClient, FetchAgentsEntry } from "@getpaseo/client/internal/daemon-client"; +import type { AgentSnapshotPayload } from "@getpaseo/protocol/messages"; +import { useSessionStore } from "@/stores/session-store"; +import { AgentDirectoryReplica } from "./agent-replica"; + +function payload(title: string): AgentSnapshotPayload { + return { + id: "agent", + provider: "codex", + cwd: "/repo", + model: null, + createdAt: "2026-07-17T00:00:00.000Z", + updatedAt: "2026-07-17T00:01:00.000Z", + lastUserMessageAt: null, + status: "idle", + capabilities: { + supportsStreaming: true, + supportsSessionPersistence: true, + supportsDynamicModes: true, + supportsMcpServers: true, + supportsReasoningStream: true, + supportsToolInvocations: true, + }, + currentModeId: null, + availableModes: [], + pendingPermissions: [], + persistence: null, + title, + labels: {}, + }; +} + +function entry(agent: AgentSnapshotPayload): FetchAgentsEntry { + return { + agent, + project: { + projectKey: "/repo", + projectName: "repo", + checkout: { + cwd: "/repo", + isGit: false, + currentBranch: null, + remoteUrl: null, + worktreeRoot: null, + isPaseoOwnedWorktree: false, + mainRepoRoot: null, + }, + }, + }; +} + +describe("AgentDirectoryReplica", () => { + it("keeps membership authoritative across remove, stale timeline, and re-add", () => { + const serverId = "agent-replica"; + const store = useSessionStore.getState(); + store.initializeSession(serverId, null as unknown as DaemonClient); + const replica = new AgentDirectoryReplica(serverId, () => undefined); + replica.commitSnapshot([entry(payload("directory"))], []); + const directoryPlacement = useSessionStore + .getState() + .sessions[serverId]?.agents.get("agent")?.projectPlacement; + expect(directoryPlacement).toBeDefined(); + const staleToken = replica.captureTimeline("agent"); + + replica.remove("agent"); + expect(replica.submitTimelineAgent(staleToken, payload("stale"))).toBe(false); + expect(useSessionStore.getState().sessions[serverId]?.agents.has("agent")).toBe(false); + + replica.applyDelta({ + kind: "upsert", + agent: payload("re-added"), + project: entry(payload("x")).project, + }); + expect(replica.submitTimelineAgent(staleToken, payload("still stale"))).toBe(false); + const currentToken = replica.captureTimeline("agent"); + expect(replica.submitTimelineAgent(currentToken, payload("current"))).toBe(true); + expect(useSessionStore.getState().sessions[serverId]?.agents.get("agent")?.title).toBe( + "current", + ); + expect( + useSessionStore.getState().sessions[serverId]?.agents.get("agent")?.projectPlacement, + ).toEqual(directoryPlacement); + store.clearSession(serverId); + }); +}); diff --git a/packages/app/src/runtime/directory-sync/agent-replica.ts b/packages/app/src/runtime/directory-sync/agent-replica.ts new file mode 100644 index 000000000..62de843cd --- /dev/null +++ b/packages/app/src/runtime/directory-sync/agent-replica.ts @@ -0,0 +1,121 @@ +import type { FetchAgentsEntry } from "@getpaseo/client/internal/daemon-client"; +import type { AgentSnapshotPayload } from "@getpaseo/protocol/messages"; +import { clearArchiveAgentPending } from "@/hooks/use-archive-agent"; +import { queryClient } from "@/data/query-client"; +import { useSessionStore, type Agent } from "@/stores/session-store"; +import { normalizeAgentSnapshot } from "@/utils/agent-snapshots"; +import { + applyAgentDirectoryDelta, + type AgentDirectoryDelta, + removeAgentDirectoryReplica, + replaceAgentPendingPermissions, + replaceFetchedAgentDirectory, + upsertAgentReplica, +} from "@/utils/agent-directory-sync"; +import { reconcileAgentDirectory } from "@/utils/agent-directory-reconciliation"; +import { applyLegacyDaemonWorkspaceOwnership } from "@/workspace/legacy-daemon-workspaces"; + +export interface AgentLifecycleToken { + readonly agentId: string; + readonly version: number; +} + +export class AgentDirectoryReplica { + private readonly lifecycleVersions = new Map(); + private readonly members = new Set(); + + constructor( + private readonly serverId: string, + private readonly onStoppedRunning: (agentId: string) => void, + ) {} + + captureTimeline(agentId: string): AgentLifecycleToken { + return { agentId, version: this.lifecycleVersions.get(agentId) ?? 0 }; + } + + submitTimelineAgent(token: AgentLifecycleToken, payload: AgentSnapshotPayload): boolean { + if ( + !this.members.has(token.agentId) || + token.version !== (this.lifecycleVersions.get(token.agentId) ?? 0) + ) { + return false; + } + const existing = useSessionStore.getState().sessions[this.serverId]?.agents.get(token.agentId); + const timelineAgent = applyLegacyDaemonWorkspaceOwnership({ + serverId: this.serverId, + agent: normalizeAgentSnapshot(payload, this.serverId), + }); + const normalized: Agent = { + ...timelineAgent, + projectPlacement: timelineAgent.projectPlacement ?? existing?.projectPlacement, + }; + const accepted = upsertAgentReplica(this.serverId, normalized); + replaceAgentPendingPermissions(this.serverId, accepted); + useSessionStore.getState().setAgentLastActivity(accepted.id, accepted.lastActivityAt); + if (accepted.archivedAt) { + clearArchiveAgentPending({ queryClient, serverId: this.serverId, agentId: accepted.id }); + } + return true; + } + + applyDelta(delta: AgentDirectoryDelta): void { + const before = this.members.has(delta.kind === "remove" ? delta.agentId : delta.agent.id); + const result = applyAgentDirectoryDelta({ serverId: this.serverId, delta }); + if (delta.kind === "remove") { + this.members.delete(delta.agentId); + this.advance(delta.agentId); + } else { + this.members.add(delta.agent.id); + if (!before) this.advance(delta.agent.id); + } + if (result.stoppedRunning) this.onStoppedRunning(result.agentId); + } + + commitSnapshot( + entries: FetchAgentsEntry[], + deltas: readonly AgentDirectoryDelta[], + ): Map { + const previous = useSessionStore.getState().sessions[this.serverId]?.agents ?? new Map(); + const reconciled = reconcileAgentDirectory({ previous, snapshot: entries, deltas }); + const nextIds = new Set(reconciled.entries.map((entry) => entry.agent.id)); + for (const agentId of this.members) { + if (!nextIds.has(agentId)) this.advance(agentId); + } + for (const agentId of nextIds) { + if (!this.members.has(agentId)) this.advance(agentId); + } + for (const agentId of previous.keys()) { + if (!nextIds.has(agentId)) removeAgentDirectoryReplica(this.serverId, agentId); + } + this.members.clear(); + for (const agentId of nextIds) this.members.add(agentId); + const { agents } = replaceFetchedAgentDirectory({ + serverId: this.serverId, + entries: reconciled.entries, + }); + for (const agentId of reconciled.stoppedRunningAgentIds) this.onStoppedRunning(agentId); + return agents; + } + + archive(agentId: string, archivedAt: string): void { + this.advance(agentId); + useSessionStore.getState().setAgents(this.serverId, (current) => { + const agent = current.get(agentId); + if (!agent) return current; + const next = new Map(current); + next.set(agentId, { ...agent, archivedAt: new Date(archivedAt) }); + return next; + }); + clearArchiveAgentPending({ queryClient, serverId: this.serverId, agentId }); + } + + remove(agentId: string): void { + this.members.delete(agentId); + this.advance(agentId); + removeAgentDirectoryReplica(this.serverId, agentId); + } + + private advance(agentId: string): void { + this.lifecycleVersions.set(agentId, (this.lifecycleVersions.get(agentId) ?? 0) + 1); + } +} diff --git a/packages/app/src/runtime/directory-sync/index.test.ts b/packages/app/src/runtime/directory-sync/index.test.ts new file mode 100644 index 000000000..d610f04be --- /dev/null +++ b/packages/app/src/runtime/directory-sync/index.test.ts @@ -0,0 +1,113 @@ +import { afterEach, describe, expect, it } from "vitest"; +import type { DaemonClient } from "@getpaseo/client/internal/daemon-client"; +import { useSessionStore } from "@/stores/session-store"; +import { DirectoryRefreshSupersededError, DirectorySync } from "./index"; + +class FakeDirectoryClient { + fetchAgentsCalls = 0; + fetchWorkspacesCalls = 0; + + on(): () => void { + return () => undefined; + } + + async fetchAgents(): Promise>> { + this.fetchAgentsCalls += 1; + return { + requestId: "agents", + entries: [], + pageInfo: { hasMore: false, nextCursor: null, prevCursor: null }, + }; + } + + async fetchWorkspaces(): Promise>> { + this.fetchWorkspacesCalls += 1; + return { + requestId: "workspaces", + entries: [], + emptyProjects: [], + pageInfo: { hasMore: false, nextCursor: null, prevCursor: null }, + }; + } +} + +const serverIds = new Set(); + +function createDirectory(serverId: string): { + client: FakeDirectoryClient; + directory: DirectorySync; +} { + serverIds.add(serverId); + const client = new FakeDirectoryClient(); + const directory = new DirectorySync(serverId, { + drainQueuedAgentMessage: () => undefined, + markAgentLoading: () => undefined, + markAgentReady: () => undefined, + markAgentError: () => undefined, + }); + directory.connectionChanged({ + client: client as unknown as DaemonClient, + status: "online", + source: { clientGeneration: 1, connectionEpoch: 1 }, + }); + return { client, directory }; +} + +afterEach(() => { + for (const serverId of serverIds) useSessionStore.getState().clearSession(serverId); + serverIds.clear(); +}); + +describe("DirectorySync session readiness", () => { + it("waits for workspace capability metadata before choosing the workspace protocol", async () => { + const serverId = "workspace-metadata"; + const { client, directory } = createDirectory(serverId); + + const refresh = directory.refreshWorkspaces({ subscribe: true }); + await Promise.resolve(); + expect(client.fetchWorkspacesCalls).toBe(0); + + const store = useSessionStore.getState(); + store.initializeSession(serverId, client as unknown as DaemonClient, 1); + await Promise.resolve(); + expect(client.fetchWorkspacesCalls).toBe(0); + + store.updateSessionServerInfo(serverId, { + serverId, + hostname: null, + version: "test", + features: { workspaceMultiplicity: true }, + }); + await refresh; + + expect(client.fetchWorkspacesCalls).toBe(1); + expect(useSessionStore.getState().sessions[serverId]?.hasHydratedWorkspaces).toBe(true); + directory.dispose(); + }); + + it("rejects a session wait on disconnect so the reconnect can refresh", async () => { + const serverId = "session-wait-reconnect"; + const { client, directory } = createDirectory(serverId); + const staleRefresh = directory.refreshAgents(); + await Promise.resolve(); + + directory.connectionChanged({ + client: null, + status: "offline", + source: { clientGeneration: 1, connectionEpoch: 1 }, + }); + await expect(staleRefresh).rejects.toBeInstanceOf(DirectoryRefreshSupersededError); + + directory.connectionChanged({ + client: client as unknown as DaemonClient, + status: "online", + source: { clientGeneration: 1, connectionEpoch: 2 }, + }); + const currentRefresh = directory.refreshAgents(); + useSessionStore.getState().initializeSession(serverId, client as unknown as DaemonClient, 1); + await currentRefresh; + + expect(client.fetchAgentsCalls).toBe(1); + directory.dispose(); + }); +}); diff --git a/packages/app/src/runtime/directory-sync/index.ts b/packages/app/src/runtime/directory-sync/index.ts new file mode 100644 index 000000000..a181bcd0c --- /dev/null +++ b/packages/app/src/runtime/directory-sync/index.ts @@ -0,0 +1,416 @@ +import type { + DaemonClient, + FetchAgentsEntry, + FetchAgentsOptions, +} from "@getpaseo/client/internal/daemon-client"; +import { fetchAgentTimelineOnce } from "@/timeline/fetch-agent-timeline-once"; +import { + normalizeEmptyProjectDescriptor, + normalizeWorkspaceDescriptor, + useSessionStore, + type Agent, +} from "@/stores/session-store"; +import { + readLegacyDaemonWorkspaceDirectory, + buildLegacyWorkspaces, + shouldUseLegacyDaemonWorkspaceDirectory, + stampLegacyWorkspaceIds, +} from "@/workspace/legacy-daemon-workspaces"; +import type { AgentDirectoryDelta } from "@/utils/agent-directory-sync"; +import { AgentDirectoryReplica } from "./agent-replica"; +import { + WorkspaceDirectoryReplica, + type WorkspaceDirectoryDelta, + type WorkspaceDirectorySnapshot, +} from "./workspace-replica"; +import { + DirectoryTransactionOwner, + type DirectorySourceToken, + type DirectoryTransaction, +} from "./transaction"; + +const PAGE_LIMIT = 200; +const AGENT_SORT: NonNullable = [ + { key: "updated_at", direction: "desc" }, +]; + +interface AgentSnapshot { + entries: FetchAgentsEntry[]; + subscriptionId: string | null; + legacy: boolean; +} + +export interface DirectoryConnection { + client: DaemonClient | null; + status: "online" | "offline"; + source: DirectorySourceToken; +} + +export interface RefreshAgentDirectoryInput { + filter?: FetchAgentsOptions["filter"]; + subscribe?: FetchAgentsOptions["subscribe"]; + page?: FetchAgentsOptions["page"]; +} + +export interface RefreshAgentDirectoryResult { + agents: Map; + subscriptionId: string | null; +} + +export class DirectorySync { + private readonly agentTransactions = new DirectoryTransactionOwner< + AgentSnapshot, + AgentDirectoryDelta + >(); + private readonly workspaceTransactions = new DirectoryTransactionOwner< + WorkspaceDirectorySnapshot, + WorkspaceDirectoryDelta + >(); + private readonly agents: AgentDirectoryReplica; + private readonly workspaces: WorkspaceDirectoryReplica; + private connection: DirectoryConnection = { + client: null, + status: "offline", + source: { clientGeneration: 0, connectionEpoch: 0 }, + }; + private unsubscribe: (() => void) | null = null; + private readonly abortSessionWaits = new Set<() => void>(); + + constructor( + private readonly serverId: string, + private readonly callbacks: { + drainQueuedAgentMessage: (agentId: string) => void; + markAgentLoading: () => void; + markAgentReady: () => void; + markAgentError: (error: string) => void; + }, + ) { + this.agents = new AgentDirectoryReplica(serverId, callbacks.drainQueuedAgentMessage); + this.workspaces = new WorkspaceDirectoryReplica(serverId); + } + + connectionChanged(connection: DirectoryConnection): boolean { + const changed = + this.connection.client !== connection.client || + this.connection.source.clientGeneration !== connection.source.clientGeneration || + this.connection.source.connectionEpoch !== connection.source.connectionEpoch; + const wentOffline = this.connection.status === "online" && connection.status === "offline"; + if (!changed && !wentOffline) { + this.connection = connection; + return false; + } + this.flushAbortedTransactions(); + this.unsubscribe?.(); + this.unsubscribe = null; + this.connection = connection; + this.abortPendingSessionWaits(); + if (!connection.client || connection.status !== "online") return true; + const client = connection.client; + const source = connection.source; + const subscriptions = [ + client.on("agent_update", (message) => { + if (message.type !== "agent_update" || !this.isCurrent(client, source)) return; + if (!this.agentTransactions.record(source, message.payload)) + this.agents.applyDelta(message.payload); + }), + client.on("workspace_update", (message) => { + if (message.type !== "workspace_update" || !this.isCurrent(client, source)) return; + if (!this.workspaceTransactions.record(source, message.payload)) { + this.workspaces.applyDelta(message.payload); + } + }), + client.on("agent_deleted", (message) => { + if (message.type === "agent_deleted" && this.isCurrent(client, source)) { + this.agents.remove(message.payload.agentId); + } + }), + client.on("agent_archived", (message) => { + if (message.type === "agent_archived" && this.isCurrent(client, source)) { + this.agents.archive(message.payload.agentId, message.payload.archivedAt); + } + }), + ]; + this.unsubscribe = () => { + for (const unsubscribe of subscriptions) unsubscribe(); + }; + return true; + } + + dispose(): void { + this.flushAbortedTransactions(); + this.abortPendingSessionWaits(); + this.unsubscribe?.(); + this.unsubscribe = null; + } + + async fetchTimeline( + agentId: string, + request: Parameters[1], + ): Promise>> { + const { client } = this.requireOnline(); + const token = this.agents.captureTimeline(agentId); + const page = await fetchAgentTimelineOnce(client, agentId, request); + if (page.agent) this.agents.submitTimelineAgent(token, page.agent); + return page; + } + + async refreshAgents( + input: RefreshAgentDirectoryInput = {}, + ): Promise { + const { client, source } = this.requireOnline(); + const transaction = this.agentTransactions.begin(source, () => ({ + entries: [], + subscriptionId: null, + legacy: false, + })); + this.callbacks.markAgentLoading(); + try { + await this.waitForSession(client, source); + const session = useSessionStore.getState().sessions[this.serverId]; + if (!input.filter && shouldUseLegacyDaemonWorkspaceDirectory(session?.serverInfo)) { + const directory = await readLegacyDaemonWorkspaceDirectory({ + client, + subscribe: input.subscribe, + page: input.page, + }); + if ( + !directory || + !this.agentTransactions.isCurrent(transaction) || + !this.isCurrent(client, source) + ) { + throw new DirectoryRefreshSupersededError("legacy fetch no longer current"); + } + transaction.snapshot.entries.push(...stampLegacyWorkspaceIds(directory.entries)); + transaction.snapshot.subscriptionId = directory.subscriptionId; + transaction.snapshot.legacy = true; + } else { + await this.fetchAgents(client, source, transaction, input); + } + if (!this.isCurrent(client, source) || !this.hasMatchingSession(client, source)) { + throw new DirectoryRefreshSupersededError("agent completion no longer current"); + } + const completion = this.agentTransactions.complete(transaction); + if (completion.kind === "stale") { + throw new DirectoryRefreshSupersededError("agent completion was superseded"); + } + if (completion.snapshot.legacy) { + const store = useSessionStore.getState(); + store.setWorkspaces(this.serverId, buildLegacyWorkspaces(completion.snapshot.entries)); + store.setEmptyProjects(this.serverId, []); + store.setHasHydratedWorkspaces(this.serverId, true); + } + const deltas = completion.snapshot.legacy + ? completion.deltas.map((delta) => + delta.kind === "upsert" + ? { ...delta, agent: { ...delta.agent, workspaceId: delta.agent.cwd } } + : delta, + ) + : completion.deltas; + const agents = this.agents.commitSnapshot(completion.snapshot.entries, deltas); + this.callbacks.markAgentReady(); + return { agents, subscriptionId: completion.snapshot.subscriptionId }; + } catch (error) { + const deltas = this.agentTransactions.fail(transaction); + if (deltas) for (const delta of deltas) this.agents.applyDelta(delta); + if (!(error instanceof DirectoryRefreshSupersededError)) { + this.callbacks.markAgentError(error instanceof Error ? error.message : String(error)); + } + throw error; + } + } + + async refreshWorkspaces(input?: { subscribe?: boolean }): Promise { + const { client, source } = this.requireOnline(); + const transaction = this.workspaceTransactions.begin(source, () => ({ + workspaces: new Map(), + emptyProjects: new Map(), + })); + try { + await this.waitForSessionMetadata(client, source); + const serverInfo = useSessionStore.getState().sessions[this.serverId]?.serverInfo; + if (serverInfo?.features?.workspaceMultiplicity !== true) { + const deltas = this.workspaceTransactions.fail(transaction); + if (deltas) for (const delta of deltas) this.workspaces.applyDelta(delta); + return; + } + await this.fetchWorkspaceSnapshot(client, source, transaction, input?.subscribe === true); + if (!this.isCurrent(client, source) || !this.hasMatchingSession(client, source)) { + throw new DirectoryRefreshSupersededError("workspace completion no longer current"); + } + const completion = this.workspaceTransactions.complete(transaction); + if (completion.kind === "stale") { + throw new DirectoryRefreshSupersededError("workspace completion was superseded"); + } + this.workspaces.commitSnapshot(completion.snapshot, completion.deltas); + } catch (error) { + const deltas = this.workspaceTransactions.fail(transaction); + if (deltas) for (const delta of deltas) this.workspaces.applyDelta(delta); + throw error; + } + } + + private async fetchWorkspaceSnapshot( + client: DaemonClient, + source: DirectorySourceToken, + transaction: DirectoryTransaction, + initialSubscribe: boolean, + ): Promise { + let cursor: string | null = null; + let subscribe = initialSubscribe; + while (true) { + const payload = await client.fetchWorkspaces({ + sort: [{ key: "activity_at", direction: "desc" }], + ...(subscribe ? { subscribe: {} } : {}), + page: cursor ? { limit: PAGE_LIMIT, cursor } : { limit: PAGE_LIMIT }, + }); + this.assertWorkspaceTransactionCurrent(client, source, transaction); + for (const entry of payload.entries) { + const workspace = normalizeWorkspaceDescriptor(entry); + transaction.snapshot.workspaces.set(workspace.id, workspace); + } + for (const entry of payload.emptyProjects ?? []) { + const project = normalizeEmptyProjectDescriptor(entry); + transaction.snapshot.emptyProjects.set(project.projectId, project); + } + if (!payload.pageInfo.hasMore || !payload.pageInfo.nextCursor) return; + cursor = payload.pageInfo.nextCursor; + subscribe = false; + } + } + + async refreshAll(): Promise { + await Promise.all([this.refreshAgents(), this.refreshWorkspaces({ subscribe: true })]); + } + + private async fetchAgents( + client: DaemonClient, + source: DirectorySourceToken, + transaction: DirectoryTransaction, + input: RefreshAgentDirectoryInput, + ): Promise { + let cursor = input.page?.cursor ?? null; + let subscribe = input.subscribe; + while (true) { + const limit = input.page?.limit ?? PAGE_LIMIT; + const payload = await client.fetchAgents({ + ...(input.filter ? { filter: input.filter } : { scope: "active" as const }), + sort: AGENT_SORT, + ...(subscribe ? { subscribe } : {}), + page: cursor ? { limit, cursor } : { limit }, + }); + this.assertAgentTransactionCurrent(client, source, transaction); + transaction.snapshot.entries.push(...payload.entries); + transaction.snapshot.subscriptionId ??= payload.subscriptionId ?? null; + const pageInfo = payload.pageInfo as { + hasMore?: boolean; + hasMoreAfter?: boolean; + nextCursor?: string | null; + afterCursor?: string | null; + }; + const hasMore = pageInfo.hasMore ?? pageInfo.hasMoreAfter ?? false; + const nextCursor = pageInfo.nextCursor ?? pageInfo.afterCursor ?? null; + if (!hasMore || !nextCursor) break; + cursor = nextCursor; + subscribe = undefined; + } + } + + private assertAgentTransactionCurrent( + client: DaemonClient, + source: DirectorySourceToken, + transaction: DirectoryTransaction, + ): void { + if (!this.agentTransactions.isCurrent(transaction) || !this.isCurrent(client, source)) { + throw new DirectoryRefreshSupersededError("agent page no longer current"); + } + } + + private assertWorkspaceTransactionCurrent( + client: DaemonClient, + source: DirectorySourceToken, + transaction: DirectoryTransaction, + ): void { + if (!this.workspaceTransactions.isCurrent(transaction) || !this.isCurrent(client, source)) { + throw new DirectoryRefreshSupersededError("workspace fetch no longer current"); + } + } + + private requireOnline(): { client: DaemonClient; source: DirectorySourceToken } { + if (!this.connection.client || this.connection.status !== "online") { + throw new Error(`Host ${this.serverId} is not connected`); + } + return { client: this.connection.client, source: this.connection.source }; + } + + private isCurrent(client: DaemonClient, source: DirectorySourceToken): boolean { + return ( + this.connection.client === client && + this.connection.status === "online" && + this.connection.source.clientGeneration === source.clientGeneration && + this.connection.source.connectionEpoch === source.connectionEpoch + ); + } + + private async waitForSession(client: DaemonClient, source: DirectorySourceToken): Promise { + await this.waitForSessionState(client, source, () => this.hasMatchingSession(client, source)); + } + + private async waitForSessionMetadata( + client: DaemonClient, + source: DirectorySourceToken, + ): Promise { + await this.waitForSessionState(client, source, () => { + const session = useSessionStore.getState().sessions[this.serverId]; + return this.hasMatchingSession(client, source) && session?.serverInfo !== null; + }); + } + + private async waitForSessionState( + client: DaemonClient, + source: DirectorySourceToken, + matches: () => boolean, + ): Promise { + if (matches()) return; + await new Promise((resolve, reject) => { + let settled = false; + let unsubscribe: () => void = () => undefined; + const finish = (result: "ready" | "aborted") => { + if (settled) return; + settled = true; + unsubscribe(); + this.abortSessionWaits.delete(abort); + if (result === "ready") resolve(); + else reject(new DirectoryRefreshSupersededError("session wait no longer current")); + }; + const abort = () => finish("aborted"); + const check = () => { + if (matches()) { + finish("ready"); + } else if (!this.isCurrent(client, source)) { + finish("aborted"); + } + }; + this.abortSessionWaits.add(abort); + unsubscribe = useSessionStore.subscribe(check); + check(); + }); + } + + private hasMatchingSession(client: DaemonClient, source: DirectorySourceToken): boolean { + const session = useSessionStore.getState().sessions[this.serverId]; + return session?.client === client && session.clientGeneration === source.clientGeneration; + } + + private flushAbortedTransactions(): void { + for (const delta of this.agentTransactions.abort()) this.agents.applyDelta(delta); + for (const delta of this.workspaceTransactions.abort()) this.workspaces.applyDelta(delta); + } + + private abortPendingSessionWaits(): void { + for (const abort of this.abortSessionWaits) abort(); + } +} + +export class DirectoryRefreshSupersededError extends Error {} + +export type { DirectorySourceToken } from "./transaction"; diff --git a/packages/app/src/runtime/directory-sync/transaction.test.ts b/packages/app/src/runtime/directory-sync/transaction.test.ts new file mode 100644 index 000000000..bc8002704 --- /dev/null +++ b/packages/app/src/runtime/directory-sync/transaction.test.ts @@ -0,0 +1,100 @@ +import { describe, expect, it } from "vitest"; +import { DirectoryTransactionOwner, type DirectorySourceToken } from "./transaction"; + +const epoch = (connectionEpoch: number): DirectorySourceToken => ({ + clientGeneration: 1, + connectionEpoch, +}); + +type Event = "begin-new" | "first" | "second" | "finish-old" | "finish-new"; + +function permutations(events: Event[]): Event[][] { + if (events.length === 0) return [[]]; + return events.flatMap((event, index) => + permutations(events.filter((_, candidate) => candidate !== index)).map((tail) => + [event].concat(tail), + ), + ); +} + +describe("DirectoryTransactionOwner", () => { + it("applies every bounded two-refresh interleaving exactly once", () => { + const schedules = permutations([ + "begin-new", + "first", + "second", + "finish-old", + "finish-new", + ]).filter((events) => events.indexOf("begin-new") < events.indexOf("finish-new")); + expect(schedules).toHaveLength(60); + for (const events of schedules) { + const owner = new DirectoryTransactionOwner(); + const old = owner.begin(epoch(1), () => null); + let current = old; + const applied: string[] = []; + for (const event of events) { + if (event === "begin-new") current = owner.begin(epoch(1), () => null); + if (event === "first" || event === "second") { + if (!owner.record(epoch(1), event)) applied.push(event); + } + if (event === "finish-old") { + const result = owner.complete(old); + if (result.kind === "current") applied.push(...result.deltas); + } + if (event === "finish-new") { + const result = owner.complete(current); + if (result.kind === "current") applied.push(...result.deltas); + } + } + expect(applied.sort(), JSON.stringify(events)).toEqual(["first", "second"]); + } + }); + + it.each([ + ["success", false, false, ["delta"]], + ["failure", true, false, ["delta"]], + ["superseded success", false, true, ["delta"]], + ["superseded failure", true, true, ["delta"]], + ] as const)("preserves buffered deltas through %s", (_name, fail, supersede, expected) => { + const owner = new DirectoryTransactionOwner(); + const first = owner.begin(epoch(1), () => []); + owner.record(epoch(1), "delta"); + const current = supersede ? owner.begin(epoch(1), () => []) : first; + + let deltas: readonly string[] | null; + if (fail) { + deltas = owner.fail(current); + } else { + const completion = owner.complete(current); + deltas = completion.kind === "current" ? completion.deltas : null; + } + + expect(deltas).toEqual(expected); + if (supersede) expect(owner.complete(first)).toEqual({ kind: "stale" }); + }); + + it("makes a stale completion inert", () => { + const owner = new DirectoryTransactionOwner(); + const first = owner.begin(epoch(1), () => null); + const second = owner.begin(epoch(1), () => null); + expect(owner.complete(first)).toEqual({ kind: "stale" }); + expect(owner.isCurrent(second)).toBe(true); + }); + + it("isolates reconnect epochs", () => { + const owner = new DirectoryTransactionOwner(); + const disconnected = owner.begin(epoch(1), () => null); + owner.record(epoch(1), "old"); + + expect(owner.abort()).toEqual(["old"]); + const reconnected = owner.begin(epoch(2), () => null); + owner.record(epoch(2), "new"); + + expect(owner.complete(disconnected)).toEqual({ kind: "stale" }); + expect(owner.complete(reconnected)).toEqual({ + kind: "current", + snapshot: null, + deltas: ["new"], + }); + }); +}); diff --git a/packages/app/src/runtime/directory-sync/transaction.ts b/packages/app/src/runtime/directory-sync/transaction.ts new file mode 100644 index 000000000..15851491c --- /dev/null +++ b/packages/app/src/runtime/directory-sync/transaction.ts @@ -0,0 +1,70 @@ +export interface DirectorySourceToken { + clientGeneration: number; + connectionEpoch: number; +} + +export interface DirectoryTransaction { + readonly source: DirectorySourceToken; + readonly snapshot: TSnapshot; + readonly deltas: TDelta[]; +} + +export type DirectoryTransactionCompletion = + | { kind: "stale" } + | { kind: "current"; snapshot: TSnapshot; deltas: readonly TDelta[] }; + +function isSameSource(left: DirectorySourceToken, right: DirectorySourceToken): boolean { + return ( + left.clientGeneration === right.clientGeneration && + left.connectionEpoch === right.connectionEpoch + ); +} + +/** Owns the shared begin/record/supersede/complete lifecycle for a directory replica. */ +export class DirectoryTransactionOwner { + private current: DirectoryTransaction | null = null; + + begin( + source: DirectorySourceToken, + createSnapshot: () => TSnapshot, + ): DirectoryTransaction { + const transaction: DirectoryTransaction = { + source, + snapshot: createSnapshot(), + deltas: + this.current && isSameSource(this.current.source, source) ? [...this.current.deltas] : [], + }; + this.current = transaction; + return transaction; + } + + record(source: DirectorySourceToken, delta: TDelta): boolean { + if (!this.current || !isSameSource(this.current.source, source)) return false; + this.current.deltas.push(delta); + return true; + } + + isCurrent(transaction: DirectoryTransaction): boolean { + return this.current === transaction; + } + + complete( + transaction: DirectoryTransaction, + ): DirectoryTransactionCompletion { + if (this.current !== transaction) return { kind: "stale" }; + this.current = null; + return { kind: "current", snapshot: transaction.snapshot, deltas: transaction.deltas }; + } + + fail(transaction: DirectoryTransaction): readonly TDelta[] | null { + const completion = this.complete(transaction); + return completion.kind === "current" ? completion.deltas : null; + } + + abort(): readonly TDelta[] { + if (!this.current) return []; + const deltas = [...this.current.deltas]; + this.current = null; + return deltas; + } +} diff --git a/packages/app/src/runtime/directory-sync/workspace-replica.test.ts b/packages/app/src/runtime/directory-sync/workspace-replica.test.ts new file mode 100644 index 000000000..48b55c002 --- /dev/null +++ b/packages/app/src/runtime/directory-sync/workspace-replica.test.ts @@ -0,0 +1,57 @@ +import { expect, it } from "vitest"; +import type { DaemonClient } from "@getpaseo/client/internal/daemon-client"; +import type { WorkspaceDescriptorPayload } from "@getpaseo/protocol/messages"; +import { + normalizeEmptyProjectDescriptor, + normalizeWorkspaceDescriptor, + useSessionStore, +} from "@/stores/session-store"; +import { WorkspaceDirectoryReplica } from "./workspace-replica"; + +function workspace(id: string, projectId = "project"): WorkspaceDescriptorPayload { + return { + id, + projectId, + projectDisplayName: projectId, + projectRootPath: `/repo/${projectId}`, + workspaceDirectory: `/repo/${projectId}/${id}`, + projectKind: "git", + workspaceKind: "worktree", + name: id, + title: id, + status: "done", + activityAt: null, + statusEnteredAt: null, + archivingAt: null, + diffStat: null, + scripts: [], + }; +} + +it("commits workspace and project-parent state with filtered removals", () => { + const serverId = "workspace-replica"; + const store = useSessionStore.getState(); + store.initializeSession(serverId, null as unknown as DaemonClient); + const replica = new WorkspaceDirectoryReplica(serverId); + const empty = normalizeEmptyProjectDescriptor({ + projectId: "empty", + projectDisplayName: "Empty", + projectRootPath: "/repo/empty", + projectKind: "git", + }); + replica.commitSnapshot( + { + workspaces: new Map([ + ["kept", normalizeWorkspaceDescriptor(workspace("kept"))], + ["filtered", normalizeWorkspaceDescriptor(workspace("filtered", "filtered-project"))], + ]), + emptyProjects: new Map([[empty.projectId, empty]]), + }, + [{ kind: "remove", id: "filtered", removedProjectId: "filtered-project" }], + ); + + const session = useSessionStore.getState().sessions[serverId]; + expect(Array.from(session?.workspaces.keys() ?? [])).toEqual(["kept"]); + expect(Array.from(session?.emptyProjects.keys() ?? [])).toEqual(["empty"]); + store.clearSession(serverId); +}); diff --git a/packages/app/src/runtime/directory-sync/workspace-replica.ts b/packages/app/src/runtime/directory-sync/workspace-replica.ts new file mode 100644 index 000000000..f2b69c853 --- /dev/null +++ b/packages/app/src/runtime/directory-sync/workspace-replica.ts @@ -0,0 +1,93 @@ +import type { SessionOutboundMessage } from "@getpaseo/protocol/messages"; +import { + normalizeEmptyProjectDescriptor, + normalizeWorkspaceDescriptor, + useSessionStore, + type EmptyProjectDescriptor, + type WorkspaceDescriptor, +} from "@/stores/session-store"; +import { useWorkspaceSetupStore } from "@/stores/workspace-setup-store"; +import { + clearWorkspaceArchivePending, + shouldSuppressWorkspaceForLocalArchive, +} from "@/contexts/session-workspace-upserts"; + +export type WorkspaceDirectoryDelta = Extract< + SessionOutboundMessage, + { type: "workspace_update" } +>["payload"]; + +export interface WorkspaceDirectorySnapshot { + workspaces: Map; + emptyProjects: Map; +} + +export class WorkspaceDirectoryReplica { + constructor(private readonly serverId: string) {} + + applyDelta(delta: WorkspaceDirectoryDelta): void { + const state = this.reconcile(this.read(), [delta]); + this.commit(state, delta.kind === "remove" ? [delta.id] : []); + } + + commitSnapshot( + snapshot: WorkspaceDirectorySnapshot, + deltas: readonly WorkspaceDirectoryDelta[], + ): void { + const removedWorkspaceIds = deltas.flatMap((delta) => + delta.kind === "remove" ? [delta.id] : [], + ); + this.commit(this.reconcile(snapshot, deltas), removedWorkspaceIds); + } + + private read(): WorkspaceDirectorySnapshot { + const session = useSessionStore.getState().sessions[this.serverId]; + return { + workspaces: new Map(session?.workspaces), + emptyProjects: new Map(session?.emptyProjects), + }; + } + + private reconcile( + snapshot: WorkspaceDirectorySnapshot, + deltas: readonly WorkspaceDirectoryDelta[], + ): WorkspaceDirectorySnapshot { + const workspaces = new Map(snapshot.workspaces); + const emptyProjects = new Map(snapshot.emptyProjects); + for (const [workspaceId, workspace] of workspaces) { + if (shouldSuppressWorkspaceForLocalArchive({ serverId: this.serverId, workspace })) { + workspaces.delete(workspaceId); + } + } + for (const delta of deltas) { + if (delta.kind === "remove") { + workspaces.delete(delta.id); + if (delta.emptyProject) { + const project = normalizeEmptyProjectDescriptor(delta.emptyProject); + emptyProjects.set(project.projectId, project); + } + if (delta.removedProjectId) emptyProjects.delete(delta.removedProjectId); + continue; + } + const workspace = normalizeWorkspaceDescriptor(delta.workspace); + if (shouldSuppressWorkspaceForLocalArchive({ serverId: this.serverId, workspace })) { + workspaces.delete(workspace.id); + } else { + workspaces.set(workspace.id, workspace); + emptyProjects.delete(workspace.projectId); + } + } + return { workspaces, emptyProjects }; + } + + private commit(snapshot: WorkspaceDirectorySnapshot, removedWorkspaceIds: string[]): void { + const store = useSessionStore.getState(); + store.setWorkspaces(this.serverId, snapshot.workspaces); + store.setEmptyProjects(this.serverId, snapshot.emptyProjects.values()); + store.setHasHydratedWorkspaces(this.serverId, true); + for (const workspaceId of removedWorkspaceIds) { + clearWorkspaceArchivePending({ serverId: this.serverId, workspaceId }); + useWorkspaceSetupStore.getState().removeWorkspace({ serverId: this.serverId, workspaceId }); + } + } +} diff --git a/packages/app/src/runtime/host-runtime.test.ts b/packages/app/src/runtime/host-runtime.test.ts index f9424e195..4de541e34 100644 --- a/packages/app/src/runtime/host-runtime.test.ts +++ b/packages/app/src/runtime/host-runtime.test.ts @@ -1,8 +1,4 @@ import { afterEach, describe, expect, it, vi } from "vitest"; - -vi.hoisted(() => { - Object.defineProperty(globalThis, "__DEV__", { value: false, configurable: true }); -}); import type { DaemonClient, ConnectionState, @@ -10,8 +6,13 @@ import type { FetchAgentsOptions, } from "@getpaseo/client/internal/daemon-client"; import type { ConnectionOffer } from "@getpaseo/protocol/connection-offer"; +import type { SessionOutboundMessage } from "@getpaseo/protocol/messages"; +import type { AgentPermissionRequest } from "@getpaseo/protocol/agent-types"; import type { HostConnection, HostProfile } from "@/types/host-connection"; import { useSessionStore, type Agent } from "@/stores/session-store"; +import { normalizeAgentSnapshot } from "@/utils/agent-snapshots"; +import { isAgentArchiving, setAgentArchiving } from "@/hooks/use-archive-agent"; +import { queryClient } from "@/data/query-client"; import { HostRuntimeController, HostRuntimeStore, @@ -20,10 +21,6 @@ import { type HostRuntimeStorage, } from "./host-runtime"; -vi.mock("@/browser-automation/handler", () => ({ - mountBrowserAutomationDaemonClientHandler: vi.fn(() => () => undefined), -})); - class FakeDaemonClient { private state: ConnectionState = { status: "idle" }; private listeners = new Set<(status: ConnectionState) => void>(); @@ -33,7 +30,45 @@ class FakeDaemonClient { private latencyMeasurementsRequested: Array<{ timeoutMs?: number }> = []; public connectCalls = 0; public fetchAgentsCalls: FetchAgentsOptions[] = []; - public fetchAgentsResponses: Awaited>[] = []; + public fetchAgentsResponses: Array< + Awaited> | ReturnType + > = []; + public sentAgentMessages: Array> = []; + public sendAgentMessageFailures: Error[] = []; + public sendAgentMessageResponses: Promise[] = []; + private agentUpdateListeners = new Set< + (message: Extract) => void + >(); + private fetchWaiters = new Set<() => void>(); + private agentListenerWaiters = new Set<() => void>(); + private sentMessageWaiters = new Set<() => void>(); + + on( + type: "agent_update", + listener: (message: Extract) => void, + ): () => void { + if (type === "agent_update") this.agentUpdateListeners.add(listener); + for (const waiter of this.agentListenerWaiters) waiter(); + return () => this.agentUpdateListeners.delete(listener); + } + + async waitForAgentUpdates(): Promise { + if (this.agentUpdateListeners.size > 0) return; + await new Promise((resolve) => { + const waiter = () => { + if (this.agentUpdateListeners.size === 0) return; + this.agentListenerWaiters.delete(waiter); + resolve(); + }; + this.agentListenerWaiters.add(waiter); + }); + } + + agentUpdate(payload: Extract["payload"]): void { + for (const listener of this.agentUpdateListeners) { + listener({ type: "agent_update", payload }); + } + } async connect(): Promise { this.connectCalls += 1; @@ -44,6 +79,27 @@ class FakeDaemonClient { this.setConnectionState({ status: "disconnected", reason: "client_closed" }); } + async sendAgentMessage(...args: Parameters): Promise { + this.sentAgentMessages.push(args); + for (const waiter of this.sentMessageWaiters) waiter(); + const response = this.sendAgentMessageResponses.shift(); + if (response) await response; + const failure = this.sendAgentMessageFailures.shift(); + if (failure) throw failure; + } + + async waitForSentMessages(count: number): Promise { + if (this.sentAgentMessages.length >= count) return; + await new Promise((resolve) => { + const waiter = () => { + if (this.sentAgentMessages.length < count) return; + this.sentMessageWaiters.delete(waiter); + resolve(); + }; + this.sentMessageWaiters.add(waiter); + }); + } + ensureConnected(): void { if (this.state.status !== "connected") { this.setConnectionState({ status: "connected" }); @@ -70,9 +126,10 @@ class FakeDaemonClient { options?: FetchAgentsOptions, ): Promise>> { this.fetchAgentsCalls.push(options ?? {}); + for (const waiter of this.fetchWaiters) waiter(); const queued = this.fetchAgentsResponses.shift(); if (queued) { - return queued; + return await queued; } return makeFetchAgentsPayload({ entries: [], @@ -80,6 +137,18 @@ class FakeDaemonClient { }); } + async waitForFetches(count: number): Promise { + if (this.fetchAgentsCalls.length >= count) return; + await new Promise((resolve) => { + const waiter = () => { + if (this.fetchAgentsCalls.length < count) return; + this.fetchWaiters.delete(waiter); + resolve(); + }; + this.fetchWaiters.add(waiter); + }); + } + async ping(): Promise<{ rttMs: number }> { return { rttMs: 0 }; } @@ -160,6 +229,38 @@ function makeFetchAgentsPayload(input: { }; } +class Deferred { + readonly promise: Promise; + private resolvePromise!: (value: T) => void; + private rejectPromise!: (error: Error) => void; + + constructor() { + this.promise = new Promise((resolve, reject) => { + this.resolvePromise = resolve; + this.rejectPromise = reject; + }); + } + + resolve(value: T): void { + this.resolvePromise(value); + } + + reject(error: Error): void { + this.rejectPromise(error); + } +} + +async function waitForDirectoryReady(store: HostRuntimeStore, serverId: string): Promise { + if (store.getSnapshot(serverId)?.agentDirectoryStatus === "ready") return; + await new Promise((resolve) => { + const unsubscribe = store.subscribe(serverId, () => { + if (store.getSnapshot(serverId)?.agentDirectoryStatus !== "ready") return; + unsubscribe(); + resolve(); + }); + }); +} + function makeFetchAgentsEntry(input: { id: string; cwd: string; @@ -220,6 +321,14 @@ function makeFetchAgentsEntry(input: { }; } +function replicaAgent(snapshot: FetchAgentsEntry["agent"], serverId: string): Agent { + return { ...normalizeAgentSnapshot(snapshot, serverId), projectPlacement: null }; +} + +function agentPermission(id: string): AgentPermissionRequest { + return { id, provider: "codex", name: id, kind: "tool", title: id }; +} + function makeHost(input?: Partial): HostProfile { const direct: HostConnection = { id: "direct:lan:6767", @@ -524,7 +633,6 @@ describe("HostRuntimeController", () => { }); await controller.start({ autoProbe: false }); - const snapshot = controller.getSnapshot(); expect(snapshot.activeConnectionId).toBe("direct:lan:6767"); expect(snapshot.connectionStatus).toBe("online"); @@ -1005,6 +1113,7 @@ describe("HostRuntimeController", () => { }); await controller.start({ autoProbe: false }); + const firstEpoch = controller.getSnapshot().connectionEpoch; controller.markAgentDirectorySyncReady(); expect(controller.getSnapshot().agentDirectoryStatus).toBe("ready"); expect(controller.getSnapshot().hasEverLoadedAgentDirectory).toBe(true); @@ -1019,6 +1128,7 @@ describe("HostRuntimeController", () => { clients[0]?.setConnectionState({ status: "connected" }); expect(controller.getSnapshot().connectionStatus).toBe("online"); expect(controller.getSnapshot().agentDirectoryStatus).toBe("ready"); + expect(controller.getSnapshot().connectionEpoch).toBe(firstEpoch + 1); }); it("stores directory sync errors as non-blocking after a successful directory load", async () => { @@ -1322,13 +1432,11 @@ describe("HostRuntimeStore", () => { useSessionStore .getState() - .initializeSession(host.serverId, fakeClient as unknown as DaemonClient); + .initializeSession(host.serverId, fakeClient as unknown as DaemonClient, 1); store.syncHosts([host]); - const timeoutAt = Date.now() + 200; - while (fakeClient.fetchAgentsCalls.length === 0 && Date.now() < timeoutAt) { - await new Promise((resolve) => setTimeout(resolve, 0)); - } + await fakeClient.waitForFetches(1); + await waitForDirectoryReady(store, host.serverId); expect(fakeClient.fetchAgentsCalls).toHaveLength(1); expect(fakeClient.fetchAgentsCalls[0]).toEqual({ @@ -1342,11 +1450,18 @@ describe("HostRuntimeStore", () => { expect(snapshot?.agentDirectoryStatus).toBe("ready"); expect(snapshot?.hasEverLoadedAgentDirectory).toBe(true); + await store.refreshAgentDirectory({ serverId: host.serverId }); + expect(fakeClient.fetchAgentsCalls[1]).toEqual({ + scope: "active", + sort: [{ key: "updated_at", direction: "desc" }], + page: { limit: 200 }, + }); + store.syncHosts([]); useSessionStore.getState().clearSession(host.serverId); }); - it("bootstraps agent directory immediately when connection goes online (no session required)", async () => { + it("waits for the matching session replica before committing the connected client bootstrap", async () => { const host = makeHost({ serverId: "srv_no_session", connections: [ @@ -1373,10 +1488,14 @@ describe("HostRuntimeStore", () => { store.syncHosts([host]); - const timeoutAt = Date.now() + 200; - while (fakeClient.fetchAgentsCalls.length === 0 && Date.now() < timeoutAt) { - await new Promise((resolve) => setTimeout(resolve, 0)); - } + await Promise.resolve(); + expect(fakeClient.fetchAgentsCalls).toEqual([]); + useSessionStore + .getState() + .initializeSession(host.serverId, fakeClient as unknown as DaemonClient, 1); + + await fakeClient.waitForFetches(1); + await waitForDirectoryReady(store, host.serverId); expect(fakeClient.fetchAgentsCalls).toHaveLength(1); expect(fakeClient.fetchAgentsCalls[0]).toEqual({ @@ -1387,6 +1506,7 @@ describe("HostRuntimeStore", () => { }); store.syncHosts([]); + useSessionStore.getState().clearSession(host.serverId); }); it("bootstraps legacy daemons from unscoped agents and creates path-backed workspaces", async () => { @@ -1428,7 +1548,7 @@ describe("HostRuntimeStore", () => { }); const sessionStore = useSessionStore.getState(); - sessionStore.initializeSession(host.serverId, fakeClient as unknown as DaemonClient); + sessionStore.initializeSession(host.serverId, fakeClient as unknown as DaemonClient, 1); sessionStore.updateSessionServerInfo(host.serverId, { serverId: host.serverId, hostname: null, @@ -1436,14 +1556,8 @@ describe("HostRuntimeStore", () => { }); store.syncHosts([host]); - const timeoutAt = Date.now() + 300; - while ( - (fakeClient.fetchAgentsCalls.length === 0 || - !useSessionStore.getState().sessions[host.serverId]?.workspaces.has("/repo/legacy-app")) && - Date.now() < timeoutAt - ) { - await new Promise((resolve) => setTimeout(resolve, 0)); - } + await fakeClient.waitForFetches(1); + await waitForDirectoryReady(store, host.serverId); expect(fakeClient.fetchAgentsCalls).toEqual([ { @@ -1466,6 +1580,101 @@ describe("HostRuntimeStore", () => { useSessionStore.getState().clearSession(host.serverId); }); + it("drains legacy snapshot and buffered running transitions exactly once", async () => { + const host = makeHost({ + serverId: "srv_legacy_transitions", + connections: [{ id: "direct:lan:6767", type: "directTcp", endpoint: "lan:6767" }], + }); + const fakeClient = new FakeDaemonClient(); + fakeClient.setConnectionState({ status: "connected" }); + const pageTwo = new Deferred>>(); + const snapshotAgent = makeFetchAgentsEntry({ + id: "legacy-snapshot", + cwd: "/legacy/repo", + updatedAt: "2026-07-12T10:00:00.000Z", + }); + const bufferedAgent = makeFetchAgentsEntry({ + id: "legacy-buffered", + cwd: "/legacy/repo", + updatedAt: "2026-07-12T10:00:00.000Z", + }); + fakeClient.fetchAgentsResponses.push( + makeFetchAgentsPayload({ + entries: [ + { ...snapshotAgent, agent: { ...snapshotAgent.agent, status: "idle" } }, + { ...bufferedAgent, agent: { ...bufferedAgent.agent, status: "running" } }, + ], + hasMore: true, + nextCursor: "legacy-page-two", + }), + pageTwo.promise, + ); + const store = new HostRuntimeStore({ + deps: { + createClient: () => fakeClient as unknown as DaemonClient, + connectToDaemon: async () => ({ + client: fakeClient as unknown as DaemonClient, + serverId: host.serverId, + hostname: null, + }), + getClientId: async () => "cid_legacy_transitions", + }, + }); + const sessionStore = useSessionStore.getState(); + sessionStore.initializeSession(host.serverId, fakeClient as unknown as DaemonClient, 1); + sessionStore.updateSessionServerInfo(host.serverId, { + serverId: host.serverId, + hostname: null, + version: "0.1.96", + }); + sessionStore.setAgents( + host.serverId, + new Map([ + [ + "legacy-snapshot", + { ...replicaAgent(snapshotAgent.agent, host.serverId), status: "running" }, + ], + [ + "legacy-buffered", + { ...replicaAgent(bufferedAgent.agent, host.serverId), status: "running" }, + ], + ]), + ); + sessionStore.setQueuedMessages( + host.serverId, + new Map([ + ["legacy-snapshot", [{ id: "legacy-snapshot-message", text: "snapshot", attachments: [] }]], + ["legacy-buffered", [{ id: "legacy-buffered-message", text: "buffered", attachments: [] }]], + ]), + ); + store.syncHosts([host]); + await fakeClient.waitForFetches(2); + fakeClient.agentUpdate({ + kind: "upsert", + agent: { ...bufferedAgent.agent, status: "idle" }, + project: bufferedAgent.project, + }); + pageTwo.resolve(makeFetchAgentsPayload({ entries: [] })); + await waitForDirectoryReady(store, host.serverId); + await fakeClient.waitForSentMessages(2); + + expect(fakeClient.sentAgentMessages.map(([agentId, text]) => [agentId, text])).toEqual([ + ["legacy-snapshot", "snapshot"], + ["legacy-buffered", "buffered"], + ]); + expect( + Array.from(useSessionStore.getState().sessions[host.serverId]?.agents.values() ?? []).map( + ({ id, status, workspaceId }) => [id, status, workspaceId], + ), + ).toEqual([ + ["legacy-snapshot", "idle", "/legacy/repo"], + ["legacy-buffered", "idle", "/legacy/repo"], + ]); + + store.syncHosts([]); + useSessionStore.getState().clearSession(host.serverId); + }); + it("fetches all pages during bootstrap within the active agent scope", async () => { const host = makeHost({ serverId: "srv_paged", @@ -1521,13 +1730,11 @@ describe("HostRuntimeStore", () => { useSessionStore .getState() - .initializeSession(host.serverId, fakeClient as unknown as DaemonClient); + .initializeSession(host.serverId, fakeClient as unknown as DaemonClient, 1); store.syncHosts([host]); - const timeoutAt = Date.now() + 300; - while (fakeClient.fetchAgentsCalls.length < 2 && Date.now() < timeoutAt) { - await new Promise((resolve) => setTimeout(resolve, 0)); - } + await fakeClient.waitForFetches(2); + await waitForDirectoryReady(store, host.serverId); expect(fakeClient.fetchAgentsCalls).toHaveLength(2); expect(fakeClient.fetchAgentsCalls[0]).toEqual({ @@ -1542,16 +1749,9 @@ describe("HostRuntimeStore", () => { page: { limit: 200, cursor: "cursor-page-2" }, }); - let staleAgent = + const staleAgent = useSessionStore.getState().sessions[host.serverId]?.agents?.get("agent-stale-attention") ?? null; - const staleTimeoutAt = Date.now() + 300; - while (!staleAgent && Date.now() < staleTimeoutAt) { - await new Promise((resolve) => setTimeout(resolve, 0)); - staleAgent = - useSessionStore.getState().sessions[host.serverId]?.agents?.get("agent-stale-attention") ?? - null; - } expect(staleAgent?.requiresAttention).toBe(true); expect(staleAgent?.attentionReason).toBe("error"); @@ -1563,6 +1763,744 @@ describe("HostRuntimeStore", () => { useSessionStore.getState().clearSession(host.serverId); }); + it("replays agent updates received while a later bootstrap page is loading", async () => { + const host = makeHost({ + serverId: "srv_paged_delta", + connections: [{ id: "direct:lan:6767", type: "directTcp", endpoint: "lan:6767" }], + }); + const fakeClient = new FakeDaemonClient(); + fakeClient.setConnectionState({ status: "connected" }); + let finishPageTwo!: (payload: Awaited>) => void; + const pageTwo = new Promise>>((resolve) => { + finishPageTwo = resolve; + }); + const pageOneAgent = makeFetchAgentsEntry({ + id: "agent-a", + cwd: "/repo", + updatedAt: "2026-07-12T10:00:00.000Z", + title: "snapshot", + }); + const recoveredAgent = makeFetchAgentsEntry({ + id: "agent-recovered", + cwd: "/repo", + updatedAt: "2026-07-12T10:00:00.000Z", + title: "before refresh", + }); + fakeClient.fetchAgentsResponses.push( + makeFetchAgentsPayload({ + entries: [pageOneAgent], + hasMore: true, + nextCursor: "page-two", + subscriptionId: "app:srv_paged_delta", + }), + pageTwo, + ); + const store = new HostRuntimeStore({ + deps: { + createClient: () => fakeClient as unknown as DaemonClient, + connectToDaemon: async () => ({ + client: fakeClient as unknown as DaemonClient, + serverId: host.serverId, + hostname: null, + }), + getClientId: async () => "cid_paged_delta", + }, + }); + const sessionStore = useSessionStore.getState(); + sessionStore.initializeSession(host.serverId, fakeClient as unknown as DaemonClient, 1); + sessionStore.setAgents( + host.serverId, + new Map([[recoveredAgent.agent.id, replicaAgent(recoveredAgent.agent, host.serverId)]]), + ); + sessionStore.setAgentTimelineCursor( + host.serverId, + new Map([[recoveredAgent.agent.id, { epoch: "epoch", startSeq: 10, endSeq: 20 }]]), + ); + store.syncHosts([host]); + await fakeClient.waitForFetches(2); + + fakeClient.agentUpdate({ + kind: "upsert", + agent: { ...pageOneAgent.agent, title: "live" }, + project: pageOneAgent.project, + }); + fakeClient.agentUpdate({ + kind: "upsert", + agent: { ...recoveredAgent.agent, title: "recovered by delta" }, + project: recoveredAgent.project, + }); + finishPageTwo( + makeFetchAgentsPayload({ + entries: [ + makeFetchAgentsEntry({ + id: "agent-b", + cwd: "/repo", + updatedAt: "2026-07-12T09:00:00.000Z", + }), + ], + }), + ); + await waitForDirectoryReady(store, host.serverId); + + expect( + Array.from(useSessionStore.getState().sessions[host.serverId]?.agents.values() ?? []).map( + (agent) => [agent.id, agent.title], + ), + ).toEqual([ + ["agent-a", "live"], + ["agent-b", null], + ["agent-recovered", "recovered by delta"], + ]); + expect( + useSessionStore + .getState() + .sessions[host.serverId]?.agentTimelineCursor.get(recoveredAgent.agent.id), + ).toEqual({ epoch: "epoch", startSeq: 10, endSeq: 20 }); + + const agentB = makeFetchAgentsEntry({ + id: "agent-b", + cwd: "/repo", + updatedAt: "2026-07-12T11:00:00.000Z", + title: "immediate", + }); + fakeClient.agentUpdate({ kind: "upsert", agent: agentB.agent, project: agentB.project }); + expect(useSessionStore.getState().sessions[host.serverId]?.agents.get("agent-b")?.title).toBe( + "immediate", + ); + fakeClient.agentUpdate({ kind: "remove", agentId: "agent-b" }); + expect(useSessionStore.getState().sessions[host.serverId]?.agents.has("agent-b")).toBe(false); + + store.syncHosts([]); + useSessionStore.getState().clearSession(host.serverId); + }); + + it("buffers updates until the matching session generation exists", async () => { + const host = makeHost({ + serverId: "srv_pre_session", + connections: [{ id: "direct:lan:6767", type: "directTcp", endpoint: "lan:6767" }], + }); + const fakeClient = new FakeDaemonClient(); + fakeClient.setConnectionState({ status: "connected" }); + const snapshotEntry = makeFetchAgentsEntry({ + id: "agent-pre-session", + cwd: "/repo", + updatedAt: "2026-07-12T10:00:00.000Z", + title: "snapshot", + }); + fakeClient.fetchAgentsResponses.push(makeFetchAgentsPayload({ entries: [snapshotEntry] })); + const store = new HostRuntimeStore({ + deps: { + createClient: () => fakeClient as unknown as DaemonClient, + connectToDaemon: async () => ({ + client: fakeClient as unknown as DaemonClient, + serverId: host.serverId, + hostname: null, + }), + getClientId: async () => "cid_pre_session", + }, + }); + + store.syncHosts([host]); + await fakeClient.waitForAgentUpdates(); + fakeClient.agentUpdate({ + kind: "upsert", + agent: { ...snapshotEntry.agent, title: "before-session" }, + project: snapshotEntry.project, + }); + const generation = store.getSnapshot(host.serverId)?.clientGeneration ?? 0; + useSessionStore + .getState() + .initializeSession(host.serverId, fakeClient as unknown as DaemonClient, generation); + useSessionStore.getState().updateSessionServerInfo(host.serverId, { + serverId: host.serverId, + hostname: null, + version: "test", + }); + await fakeClient.waitForFetches(1); + await waitForDirectoryReady(store, host.serverId); + + expect( + useSessionStore.getState().sessions[host.serverId]?.agents.get("agent-pre-session")?.title, + ).toBe("before-session"); + + store.syncHosts([]); + useSessionStore.getState().clearSession(host.serverId); + }); + + it("restarts directory bootstrap when reconnect supersedes a pending session wait", async () => { + const host = makeHost({ + serverId: "srv_session_wait_reconnect", + connections: [{ id: "direct:lan:6767", type: "directTcp", endpoint: "lan:6767" }], + }); + const fakeClient = new FakeDaemonClient(); + fakeClient.setConnectionState({ status: "connected" }); + const store = new HostRuntimeStore({ + deps: { + createClient: () => fakeClient as unknown as DaemonClient, + connectToDaemon: async () => ({ + client: fakeClient as unknown as DaemonClient, + serverId: host.serverId, + hostname: null, + }), + getClientId: async () => "cid_session_wait_reconnect", + }, + }); + + store.syncHosts([host]); + await fakeClient.waitForAgentUpdates(); + fakeClient.setConnectionState({ status: "disconnected", reason: "network" }); + fakeClient.setConnectionState({ status: "connected" }); + + const generation = store.getSnapshot(host.serverId)?.clientGeneration ?? 0; + const sessionStore = useSessionStore.getState(); + sessionStore.initializeSession( + host.serverId, + fakeClient as unknown as DaemonClient, + generation, + ); + sessionStore.updateSessionServerInfo(host.serverId, { + serverId: host.serverId, + hostname: null, + version: "test", + }); + await fakeClient.waitForFetches(1); + await waitForDirectoryReady(store, host.serverId); + + expect(fakeClient.fetchAgentsCalls).toHaveLength(1); + store.syncHosts([]); + sessionStore.clearSession(host.serverId); + }); + + it("rejects a superseded refresh without overwriting the newer replica", async () => { + const host = makeHost({ + serverId: "srv_overlap", + connections: [{ id: "direct:lan:6767", type: "directTcp", endpoint: "lan:6767" }], + }); + const fakeClient = new FakeDaemonClient(); + fakeClient.setConnectionState({ status: "connected" }); + fakeClient.fetchAgentsResponses.push(makeFetchAgentsPayload({ entries: [] })); + const store = new HostRuntimeStore({ + deps: { + createClient: () => fakeClient as unknown as DaemonClient, + connectToDaemon: async () => ({ + client: fakeClient as unknown as DaemonClient, + serverId: host.serverId, + hostname: null, + }), + getClientId: async () => "cid_overlap", + }, + }); + useSessionStore + .getState() + .initializeSession(host.serverId, fakeClient as unknown as DaemonClient, 1); + store.syncHosts([host]); + await fakeClient.waitForFetches(1); + await waitForDirectoryReady(store, host.serverId); + const olderPage = new Deferred>>(); + fakeClient.fetchAgentsResponses.push(olderPage.promise); + const olderRefresh = store.refreshAgentDirectory({ serverId: host.serverId }); + await fakeClient.waitForFetches(2); + const newerEntry = makeFetchAgentsEntry({ + id: "newer", + cwd: "/repo", + updatedAt: "2026-07-12T11:00:00.000Z", + }); + fakeClient.fetchAgentsResponses.push(makeFetchAgentsPayload({ entries: [newerEntry] })); + + const newerRefresh = store.refreshAgentDirectory({ serverId: host.serverId }); + await fakeClient.waitForFetches(3); + await newerRefresh; + olderPage.resolve( + makeFetchAgentsPayload({ + entries: [ + makeFetchAgentsEntry({ + id: "older", + cwd: "/repo", + updatedAt: "2026-07-12T09:00:00.000Z", + }), + ], + }), + ); + await expect(olderRefresh).rejects.toThrow(); + + fakeClient.agentUpdate({ + kind: "upsert", + agent: { ...newerEntry.agent, title: "after cleanup" }, + project: newerEntry.project, + }); + + expect( + Array.from(useSessionStore.getState().sessions[host.serverId]?.agents.values() ?? []).map( + ({ id, title }) => [id, title], + ), + ).toEqual([["newer", "after cleanup"]]); + + store.syncHosts([]); + useSessionStore.getState().clearSession(host.serverId); + }); + + it("replays inherited deltas when a superseding refresh fails", async () => { + const host = makeHost({ + serverId: "srv_overlap_failure", + connections: [{ id: "direct:lan:6767", type: "directTcp", endpoint: "lan:6767" }], + }); + const fakeClient = new FakeDaemonClient(); + fakeClient.setConnectionState({ status: "connected" }); + fakeClient.fetchAgentsResponses.push(makeFetchAgentsPayload({ entries: [] })); + const store = new HostRuntimeStore({ + deps: { + createClient: () => fakeClient as unknown as DaemonClient, + connectToDaemon: async () => ({ + client: fakeClient as unknown as DaemonClient, + serverId: host.serverId, + hostname: null, + }), + getClientId: async () => "cid_overlap_failure", + }, + }); + useSessionStore + .getState() + .initializeSession(host.serverId, fakeClient as unknown as DaemonClient, 1); + store.syncHosts([host]); + await fakeClient.waitForFetches(1); + await waitForDirectoryReady(store, host.serverId); + + const olderPage = new Deferred>>(); + fakeClient.fetchAgentsResponses.push(olderPage.promise); + const olderRefresh = store.refreshAgentDirectory({ serverId: host.serverId }); + await fakeClient.waitForFetches(2); + const liveEntry = makeFetchAgentsEntry({ + id: "live-delta", + cwd: "/repo", + updatedAt: "2026-07-17T10:00:00.000Z", + title: "preserved", + }); + fakeClient.agentUpdate({ kind: "upsert", agent: liveEntry.agent, project: liveEntry.project }); + + const newerPage = new Deferred>>(); + fakeClient.fetchAgentsResponses.push(newerPage.promise); + const newerRefresh = store.refreshAgentDirectory({ serverId: host.serverId }); + await fakeClient.waitForFetches(3); + newerPage.reject(new Error("newer refresh failed")); + await expect(newerRefresh).rejects.toThrow("newer refresh failed"); + + expect( + useSessionStore.getState().sessions[host.serverId]?.agents.get("live-delta")?.title, + ).toBe("preserved"); + + olderPage.resolve(makeFetchAgentsPayload({ entries: [] })); + await expect(olderRefresh).rejects.toThrow(); + store.syncHosts([]); + useSessionStore.getState().clearSession(host.serverId); + }); + + it("rejects a refresh when the session generation changes before commit", async () => { + const host = makeHost({ + serverId: "srv_stale_generation", + connections: [{ id: "direct:lan:6767", type: "directTcp", endpoint: "lan:6767" }], + }); + const fakeClient = new FakeDaemonClient(); + fakeClient.setConnectionState({ status: "connected" }); + const existingEntry = makeFetchAgentsEntry({ + id: "existing", + cwd: "/repo", + updatedAt: "2026-07-12T10:00:00.000Z", + }); + fakeClient.fetchAgentsResponses.push(makeFetchAgentsPayload({ entries: [existingEntry] })); + const store = new HostRuntimeStore({ + deps: { + createClient: () => fakeClient as unknown as DaemonClient, + connectToDaemon: async () => ({ + client: fakeClient as unknown as DaemonClient, + serverId: host.serverId, + hostname: null, + }), + getClientId: async () => "cid_stale_generation", + }, + }); + const sessionStore = useSessionStore.getState(); + sessionStore.initializeSession(host.serverId, fakeClient as unknown as DaemonClient, 1); + store.syncHosts([host]); + await fakeClient.waitForFetches(1); + await waitForDirectoryReady(store, host.serverId); + const stalePage = new Deferred>>(); + fakeClient.fetchAgentsResponses.push(stalePage.promise); + + const refresh = store.refreshAgentDirectory({ serverId: host.serverId }); + await fakeClient.waitForFetches(2); + sessionStore.updateSessionClient(host.serverId, fakeClient as unknown as DaemonClient, 2); + stalePage.resolve( + makeFetchAgentsPayload({ + entries: [ + makeFetchAgentsEntry({ + id: "stale", + cwd: "/repo", + updatedAt: "2026-07-12T11:00:00.000Z", + }), + ], + }), + ); + + await expect(refresh).rejects.toThrow(); + expect( + Array.from(useSessionStore.getState().sessions[host.serverId]?.agents.keys() ?? []), + ).toEqual(["existing"]); + + store.syncHosts([]); + useSessionStore.getState().clearSession(host.serverId); + }); + + it("drains queued messages once for snapshot and buffered running transitions", async () => { + const host = makeHost({ + serverId: "srv_queued_transitions", + connections: [{ id: "direct:lan:6767", type: "directTcp", endpoint: "lan:6767" }], + }); + const fakeClient = new FakeDaemonClient(); + fakeClient.setConnectionState({ status: "connected" }); + const pageTwo = new Deferred>>(); + const snapshotAgent = makeFetchAgentsEntry({ + id: "snapshot-transition", + cwd: "/repo", + updatedAt: "2026-07-12T10:00:00.000Z", + }); + const bufferedAgent = makeFetchAgentsEntry({ + id: "buffered-transition", + cwd: "/repo", + updatedAt: "2026-07-12T10:00:00.000Z", + }); + fakeClient.fetchAgentsResponses.push( + makeFetchAgentsPayload({ + entries: [ + { ...snapshotAgent, agent: { ...snapshotAgent.agent, status: "idle" } }, + { ...bufferedAgent, agent: { ...bufferedAgent.agent, status: "running" } }, + ], + hasMore: true, + nextCursor: "page-two", + }), + pageTwo.promise, + ); + const store = new HostRuntimeStore({ + deps: { + createClient: () => fakeClient as unknown as DaemonClient, + connectToDaemon: async () => ({ + client: fakeClient as unknown as DaemonClient, + serverId: host.serverId, + hostname: null, + }), + getClientId: async () => "cid_queued_transitions", + }, + }); + const sessionStore = useSessionStore.getState(); + sessionStore.initializeSession(host.serverId, fakeClient as unknown as DaemonClient, 1); + sessionStore.setAgents( + host.serverId, + new Map([ + [ + "snapshot-transition", + { ...replicaAgent(snapshotAgent.agent, host.serverId), status: "running" }, + ], + [ + "buffered-transition", + { ...replicaAgent(bufferedAgent.agent, host.serverId), status: "running" }, + ], + ]), + ); + sessionStore.setQueuedMessages( + host.serverId, + new Map([ + [ + "snapshot-transition", + [{ id: "message-snapshot", text: "snapshot queued", attachments: [] }], + ], + [ + "buffered-transition", + [{ id: "message-buffered", text: "buffered queued", attachments: [] }], + ], + ]), + ); + store.syncHosts([host]); + await fakeClient.waitForFetches(2); + fakeClient.agentUpdate({ + kind: "upsert", + agent: { ...bufferedAgent.agent, status: "idle" }, + project: bufferedAgent.project, + }); + pageTwo.resolve(makeFetchAgentsPayload({ entries: [] })); + await waitForDirectoryReady(store, host.serverId); + await fakeClient.waitForSentMessages(2); + + expect(fakeClient.sentAgentMessages.map(([agentId, text]) => [agentId, text])).toEqual([ + ["snapshot-transition", "snapshot queued"], + ["buffered-transition", "buffered queued"], + ]); + expect( + Array.from(useSessionStore.getState().sessions[host.serverId]?.queuedMessages.values() ?? []), + ).toEqual([[], []]); + + store.syncHosts([]); + useSessionStore.getState().clearSession(host.serverId); + }); + + it("restores an automatically drained message when sending fails", async () => { + const host = makeHost({ serverId: "srv_failed_queue_drain" }); + const fakeClient = new FakeDaemonClient(); + fakeClient.sendAgentMessageFailures.push(new Error("connection lost")); + const store = new HostRuntimeStore({ + deps: { + createClient: () => fakeClient as unknown as DaemonClient, + connectToDaemon: async () => ({ + client: fakeClient as unknown as DaemonClient, + serverId: host.serverId, + hostname: null, + }), + getClientId: async () => "cid_failed_queue_drain", + }, + }); + const sessionStore = useSessionStore.getState(); + sessionStore.initializeSession(host.serverId, fakeClient as unknown as DaemonClient, 1); + sessionStore.setQueuedMessages( + host.serverId, + new Map([ + [ + "agent", + [ + { id: "first", text: "retry me", attachments: [] }, + { id: "second", text: "keep me behind", attachments: [] }, + ], + ], + ]), + ); + + store.drainQueuedAgentMessage(host.serverId, "agent"); + + await vi.waitFor(() => { + expect(fakeClient.sentAgentMessages).toHaveLength(1); + expect( + useSessionStore.getState().sessions[host.serverId]?.queuedMessages.get("agent"), + ).toEqual([ + { id: "first", text: "retry me", attachments: [] }, + { id: "second", text: "keep me behind", attachments: [] }, + ]); + }); + + useSessionStore.getState().clearSession(host.serverId); + }); + + it("serializes queued-message drains for the same agent", async () => { + const host = makeHost({ serverId: "srv_serialized_queue_drain" }); + const fakeClient = new FakeDaemonClient(); + const send = new Deferred(); + fakeClient.sendAgentMessageResponses.push(send.promise); + const store = new HostRuntimeStore({ + deps: { + createClient: () => fakeClient as unknown as DaemonClient, + connectToDaemon: async () => ({ + client: fakeClient as unknown as DaemonClient, + serverId: host.serverId, + hostname: null, + }), + getClientId: async () => "cid_serialized_queue_drain", + }, + }); + const sessionStore = useSessionStore.getState(); + sessionStore.initializeSession(host.serverId, fakeClient as unknown as DaemonClient, 1); + sessionStore.setQueuedMessages( + host.serverId, + new Map([["agent", [{ id: "first", text: "send once", attachments: [] }]]]), + ); + + store.drainQueuedAgentMessage(host.serverId, "agent"); + store.drainQueuedAgentMessage(host.serverId, "agent"); + await fakeClient.waitForSentMessages(1); + expect(fakeClient.sentAgentMessages).toHaveLength(1); + + send.resolve(); + await vi.waitFor(() => { + expect( + useSessionStore.getState().sessions[host.serverId]?.queuedMessages.get("agent"), + ).toEqual([]); + }); + useSessionStore.getState().clearSession(host.serverId); + }); + + it("uses legacy GitHub attachments when draining a queue for an old daemon", async () => { + const host = makeHost({ serverId: "srv_legacy_queue_attachment" }); + const fakeClient = new FakeDaemonClient(); + const store = new HostRuntimeStore({ + deps: { + createClient: () => fakeClient as unknown as DaemonClient, + connectToDaemon: async () => ({ + client: fakeClient as unknown as DaemonClient, + serverId: host.serverId, + hostname: null, + }), + getClientId: async () => "cid_legacy_queue_attachment", + }, + }); + const sessionStore = useSessionStore.getState(); + sessionStore.initializeSession(host.serverId, fakeClient as unknown as DaemonClient, 1); + sessionStore.updateSessionServerInfo(host.serverId, { + serverId: host.serverId, + hostname: null, + version: "0.1.105", + features: { forgeSearch: false }, + }); + sessionStore.setQueuedMessages( + host.serverId, + new Map([ + [ + "agent", + [ + { + id: "queued-legacy-attachment", + text: "review this", + attachments: [ + { + kind: "github_pr" as const, + item: { + kind: "change_request" as const, + number: 42, + title: "Compatibility fix", + url: "https://github.com/acme/repo/pull/42", + state: "open" as const, + body: "Details", + labels: [], + baseRefName: "main", + headRefName: "fix", + }, + }, + ], + }, + ], + ], + ]), + ); + + store.drainQueuedAgentMessage(host.serverId, "agent"); + await fakeClient.waitForSentMessages(1); + + expect(fakeClient.sentAgentMessages[0]?.[2]?.attachments).toEqual([ + { + type: "github_pr", + mimeType: "application/github-pr", + number: 42, + title: "Compatibility fix", + url: "https://github.com/acme/repo/pull/42", + body: "Details", + baseRefName: "main", + headRefName: "fix", + }, + ]); + sessionStore.clearSession(host.serverId); + }); + + it("applies buffered stale side effects from the accepted page agent", async () => { + const host = makeHost({ + serverId: "srv_buffered_stale_side_effects", + connections: [{ id: "direct:lan:6767", type: "directTcp", endpoint: "lan:6767" }], + }); + const fakeClient = new FakeDaemonClient(); + fakeClient.setConnectionState({ status: "connected" }); + const pageTwo = new Deferred>>(); + const base = makeFetchAgentsEntry({ + id: "stale-side-effects", + cwd: "/repo", + updatedAt: "2026-07-12T12:00:00.000Z", + title: "newer page", + }); + const pageAgent = { + ...base.agent, + status: "running" as const, + lastUsage: { inputTokens: 10, outputTokens: 5 }, + pendingPermissions: [agentPermission("current-permission")], + }; + fakeClient.fetchAgentsResponses.push( + makeFetchAgentsPayload({ + entries: [{ ...base, agent: pageAgent }], + hasMore: true, + nextCursor: "page-two", + }), + pageTwo.promise, + ); + const store = new HostRuntimeStore({ + deps: { + createClient: () => fakeClient as unknown as DaemonClient, + connectToDaemon: async () => ({ + client: fakeClient as unknown as DaemonClient, + serverId: host.serverId, + hostname: null, + }), + getClientId: async () => "cid_buffered_stale_side_effects", + }, + }); + const sessionStore = useSessionStore.getState(); + sessionStore.initializeSession(host.serverId, fakeClient as unknown as DaemonClient, 1); + sessionStore.setAgents( + host.serverId, + new Map([ + [ + pageAgent.id, + replicaAgent({ ...pageAgent, updatedAt: "2026-07-12T10:00:00.000Z" }, host.serverId), + ], + ]), + ); + sessionStore.setAgentLastActivity(pageAgent.id, new Date("2026-07-12T12:00:00.000Z")); + sessionStore.flushAgentLastActivity(); + setAgentArchiving({ + queryClient, + serverId: host.serverId, + agentId: pageAgent.id, + isArchiving: true, + }); + store.syncHosts([host]); + await fakeClient.waitForFetches(2); + fakeClient.agentUpdate({ + kind: "upsert", + agent: { + ...pageAgent, + status: "idle", + title: "stale live", + updatedAt: "2026-07-12T11:00:00.000Z", + lastUsage: { inputTokens: 20, outputTokens: 8 }, + pendingPermissions: [agentPermission("stale-permission")], + archivedAt: "2026-07-12T11:00:00.000Z", + }, + project: base.project, + }); + pageTwo.resolve(makeFetchAgentsPayload({ entries: [] })); + await waitForDirectoryReady(store, host.serverId); + sessionStore.flushAgentLastActivity(); + + const state = useSessionStore.getState(); + const agent = state.sessions[host.serverId]?.agents.get(pageAgent.id); + expect({ + title: agent?.title, + status: agent?.status, + usage: agent?.lastUsage, + permissions: Array.from(state.sessions[host.serverId]?.pendingPermissions.values() ?? []).map( + ({ request }) => request.id, + ), + archivePending: isAgentArchiving({ + queryClient, + serverId: host.serverId, + agentId: pageAgent.id, + }), + activity: state.agentLastActivity.get(pageAgent.id)?.toISOString(), + sentMessages: fakeClient.sentAgentMessages.length, + }).toEqual({ + title: "newer page", + status: "running", + usage: { inputTokens: 20, outputTokens: 8 }, + permissions: ["current-permission"], + archivePending: true, + activity: "2026-07-12T12:00:00.000Z", + sentMessages: 0, + }); + + store.syncHosts([]); + useSessionStore.getState().clearSession(host.serverId); + }); + it("re-subscribes agent directory updates after reconnect", async () => { const host = makeHost({ serverId: "srv_resubscribe", @@ -1590,13 +2528,16 @@ describe("HostRuntimeStore", () => { useSessionStore .getState() - .initializeSession(host.serverId, fakeClient as unknown as DaemonClient); + .initializeSession(host.serverId, fakeClient as unknown as DaemonClient, 1); store.syncHosts([host]); - const initialTimeoutAt = Date.now() + 200; - while (fakeClient.fetchAgentsCalls.length < 1 && Date.now() < initialTimeoutAt) { - await new Promise((resolve) => setTimeout(resolve, 0)); - } + await fakeClient.waitForFetches(1); + await waitForDirectoryReady(store, host.serverId); + + fakeClient.setConnectionState({ status: "connected" }); + await Promise.resolve(); + await Promise.resolve(); + expect(fakeClient.fetchAgentsCalls).toHaveLength(1); fakeClient.setConnectionState({ status: "disconnected", @@ -1604,10 +2545,8 @@ describe("HostRuntimeStore", () => { }); fakeClient.setConnectionState({ status: "connected" }); - const reconnectTimeoutAt = Date.now() + 200; - while (fakeClient.fetchAgentsCalls.length < 2 && Date.now() < reconnectTimeoutAt) { - await new Promise((resolve) => setTimeout(resolve, 0)); - } + await fakeClient.waitForFetches(2); + await waitForDirectoryReady(store, host.serverId); expect(fakeClient.fetchAgentsCalls).toEqual([ { @@ -1661,7 +2600,7 @@ describe("HostRuntimeStore", () => { useSessionStore .getState() - .initializeSession(host.serverId, fakeClient as unknown as DaemonClient); + .initializeSession(host.serverId, fakeClient as unknown as DaemonClient, 1); useSessionStore.getState().setAgents(host.serverId, () => { const stale = makeFetchAgentsEntry({ id: "agent-archived", @@ -1686,13 +2625,8 @@ describe("HostRuntimeStore", () => { store.syncHosts([host]); - const timeoutAt = Date.now() + 300; - while ( - useSessionStore.getState().sessions[host.serverId]?.agents.has("agent-archived") && - Date.now() < timeoutAt - ) { - await new Promise((resolve) => setTimeout(resolve, 0)); - } + await fakeClient.waitForFetches(1); + await waitForDirectoryReady(store, host.serverId); expect(useSessionStore.getState().sessions[host.serverId]?.agents.has("agent-archived")).toBe( false, diff --git a/packages/app/src/runtime/host-runtime.ts b/packages/app/src/runtime/host-runtime.ts index 283354231..85f4634ec 100644 --- a/packages/app/src/runtime/host-runtime.ts +++ b/packages/app/src/runtime/host-runtime.ts @@ -4,7 +4,6 @@ import equal from "fast-deep-equal/es6"; import { DaemonClient, type ConnectionState, - type FetchAgentsEntry, type FetchAgentsOptions, } from "@getpaseo/client/internal/daemon-client"; import { @@ -40,12 +39,7 @@ import { import { getDesktopHost } from "@/desktop/host"; import { CLIENT_CAPS } from "@getpaseo/protocol/client-capabilities"; import { BROWSER_AUTOMATION_COMMAND_NAMES } from "@getpaseo/protocol/browser-automation/rpc-schemas"; -import { replaceFetchedAgentDirectory } from "@/utils/agent-directory-sync"; import { useSessionStore } from "@/stores/session-store"; -import { - fetchLegacyDaemonWorkspaceDirectory, - shouldUseLegacyDaemonWorkspaceDirectory, -} from "@/workspace/legacy-daemon-workspaces"; import { invalidateCheckoutGitQueriesForServer } from "@/git/query-keys"; import { queryClient } from "@/data/query-client"; import { @@ -54,6 +48,13 @@ import { } from "@/data/push-router"; import { mountBrowserAutomationDaemonClientHandler } from "@/browser-automation/handler"; import { schedulesQueryBaseKey } from "@/schedules/aggregated-schedules"; +import { sendQueuedComposerMessageNow } from "@/composer/actions"; +import { + resolveComposerAttachmentSubmitFormat, + splitComposerAttachmentsForSubmit, +} from "@/composer/attachments/submit"; +import { encodeImages } from "@/utils/encode-images"; +import { DirectorySync, type RefreshAgentDirectoryResult } from "@/runtime/directory-sync"; export type HostRuntimeConnectionStatus = "idle" | "connecting" | "online" | "offline" | "error"; export type HostRegistryStatus = "loading" | "ready"; @@ -85,6 +86,7 @@ export interface HostRuntimeSnapshot { hasEverLoadedAgentDirectory: boolean; probeByConnectionId: Map; clientGeneration: number; + connectionEpoch: number; } type HostRuntimeSnapshotPatch = Partial>; @@ -176,90 +178,6 @@ const ADAPTIVE_SWITCH_CONSECUTIVE_PROBES = 3; const DEFAULT_AGENT_DIRECTORY_PAGE_LIMIT = 200; const CONFIGURED_OVERRIDE_BOOTSTRAP_RETRY_MS = 1_000; -const DEFAULT_AGENT_DIRECTORY_SORT: NonNullable = [ - { key: "updated_at", direction: "desc" }, -]; - -function readFetchAgentsHasMore( - pageInfo: Awaited>["pageInfo"], -): boolean { - const page = pageInfo as { - hasMore?: boolean; - hasMoreAfter?: boolean; - }; - if (typeof page.hasMore === "boolean") { - return page.hasMore; - } - if (typeof page.hasMoreAfter === "boolean") { - return page.hasMoreAfter; - } - return false; -} - -function readFetchAgentsNextCursor( - pageInfo: Awaited>["pageInfo"], -): string | null { - const page = pageInfo as { - nextCursor?: string | null; - afterCursor?: string | null; - }; - if (typeof page.nextCursor === "string" && page.nextCursor.length > 0) { - return page.nextCursor; - } - if (typeof page.afterCursor === "string" && page.afterCursor.length > 0) { - return page.afterCursor; - } - return null; -} - -interface AgentDirectoryFetchInput { - client: DaemonClient; - filter?: FetchAgentsOptions["filter"]; - subscribe?: FetchAgentsOptions["subscribe"]; - page?: FetchAgentsOptions["page"]; -} - -interface AgentDirectoryFetchResult { - entries: FetchAgentsEntry[]; - subscriptionId: string | null; -} - -async function fetchCurrentAgentDirectory( - input: AgentDirectoryFetchInput, -): Promise { - const pageLimit = input.page?.limit ?? DEFAULT_AGENT_DIRECTORY_PAGE_LIMIT; - let cursor = input.page?.cursor ?? null; - let includeSubscribe = true; - let subscriptionId: string | null = null; - const entries: FetchAgentsEntry[] = []; - - while (true) { - const payload = await input.client.fetchAgents({ - scope: input.filter ? undefined : "active", - ...(input.filter ? { filter: input.filter } : {}), - sort: DEFAULT_AGENT_DIRECTORY_SORT, - ...(includeSubscribe && input.subscribe ? { subscribe: input.subscribe } : {}), - page: cursor ? { limit: pageLimit, cursor } : { limit: pageLimit }, - }); - - entries.push(...payload.entries); - subscriptionId = subscriptionId ?? payload.subscriptionId ?? null; - includeSubscribe = false; - - if (!readFetchAgentsHasMore(payload.pageInfo)) { - break; - } - - const nextCursor = readFetchAgentsNextCursor(payload.pageInfo); - if (!nextCursor) { - break; - } - cursor = nextCursor; - } - - return { entries, subscriptionId }; -} - function toActiveConnection(connection: HostConnection): ActiveConnection { if (connection.type === "directSocket") { return { @@ -445,9 +363,15 @@ function nextConnectionMachineState(input: { function toSnapshotConnectionPatch( state: HostRuntimeConnectionMachineState, + connectionEpoch: number, ): Pick< HostRuntimeSnapshot, - "activeConnectionId" | "activeConnection" | "connectionStatus" | "lastError" | "lastOnlineAt" + | "activeConnectionId" + | "activeConnection" + | "connectionStatus" + | "lastError" + | "lastOnlineAt" + | "connectionEpoch" > { if (state.tag === "booting") { return { @@ -456,6 +380,7 @@ function toSnapshotConnectionPatch( connectionStatus: "connecting", lastError: null, lastOnlineAt: null, + connectionEpoch, }; } if (state.tag === "connecting") { @@ -465,6 +390,7 @@ function toSnapshotConnectionPatch( connectionStatus: "connecting", lastError: null, lastOnlineAt: null, + connectionEpoch, }; } if (state.tag === "online") { @@ -474,6 +400,7 @@ function toSnapshotConnectionPatch( connectionStatus: "online", lastError: null, lastOnlineAt: state.lastOnlineAt, + connectionEpoch, }; } if (state.tag === "offline") { @@ -483,6 +410,7 @@ function toSnapshotConnectionPatch( connectionStatus: "offline", lastError: null, lastOnlineAt: null, + connectionEpoch, }; } return { @@ -491,6 +419,7 @@ function toSnapshotConnectionPatch( connectionStatus: "error", lastError: state.message, lastOnlineAt: null, + connectionEpoch, }; } @@ -538,6 +467,10 @@ function createDefaultDeps(): HostRuntimeControllerDeps { }, } : undefined; + const appCapabilities = { + [CLIENT_CAPS.selectiveAgentTimeline]: true, + ...browserAutomationCapabilities, + }; return { createClient: ({ host, connection, clientId, runtimeGeneration }) => { @@ -548,7 +481,7 @@ function createDefaultDeps(): HostRuntimeControllerDeps { clientType: "mobile" as const, appVersion: resolveAppVersion() ?? undefined, runtimeGeneration, - ...(browserAutomationCapabilities ? { capabilities: browserAutomationCapabilities } : {}), + capabilities: appCapabilities, }; if (connection.type === "directSocket" || connection.type === "directPipe") { return new DaemonClient({ @@ -586,7 +519,7 @@ function createDefaultDeps(): HostRuntimeControllerDeps { connectToDaemon(connection, { ...(host.serverId ? { serverId: host.serverId } : {}), ...(timeoutMs !== undefined ? { timeoutMs } : {}), - ...(browserAutomationCapabilities ? { capabilities: browserAutomationCapabilities } : {}), + capabilities: appCapabilities, }), getClientId: () => getOrCreateClientId(), mountClientHandlers: ({ client, host }) => { @@ -614,6 +547,7 @@ export class HostRuntimeController { private deps: HostRuntimeControllerDeps; private onReconcileServerId: ((oldId: string, newId: string) => void) | null; private connectionMachineState: HostRuntimeConnectionMachineState; + private connectionEpoch = 0; private snapshot: HostRuntimeSnapshot; private listeners = new Set<() => void>(); private activeClient: DaemonClient | null = null; @@ -644,7 +578,7 @@ export class HostRuntimeController { }; this.snapshot = { serverId: this.host.serverId, - ...toSnapshotConnectionPatch(this.connectionMachineState), + ...toSnapshotConnectionPatch(this.connectionMachineState, this.connectionEpoch), client: null, agentDirectoryStatus: "idle", agentDirectoryError: null, @@ -712,7 +646,7 @@ export class HostRuntimeController { } this.applyConnectionEvent({ type: "stopped" }); this.updateSnapshot({ - ...toSnapshotConnectionPatch(this.connectionMachineState), + ...toSnapshotConnectionPatch(this.connectionMachineState, this.connectionEpoch), client: null, }); } @@ -776,7 +710,7 @@ export class HostRuntimeController { markStartupError(message: string): void { this.applyConnectionEvent({ type: "connect_failed", message }); this.updateSnapshot({ - ...toSnapshotConnectionPatch(this.connectionMachineState), + ...toSnapshotConnectionPatch(this.connectionMachineState, this.connectionEpoch), }); } @@ -809,7 +743,7 @@ export class HostRuntimeController { } this.applyConnectionEvent({ type: "no_connections" }); this.updateSnapshot({ - ...toSnapshotConnectionPatch(this.connectionMachineState), + ...toSnapshotConnectionPatch(this.connectionMachineState, this.connectionEpoch), probeByConnectionId: new Map(), }); return; @@ -1095,6 +1029,9 @@ export class HostRuntimeController { state: previousState, event, }); + if (previousState.tag !== "online" && nextState.tag === "online") { + this.connectionEpoch += 1; + } this.connectionMachineState = nextState; this.logConnectionTransition({ from: previousState.tag, @@ -1171,7 +1108,7 @@ export class HostRuntimeController { message: `Failed to resolve client id: ${message}`, }); this.updateSnapshot({ - ...toSnapshotConnectionPatch(this.connectionMachineState), + ...toSnapshotConnectionPatch(this.connectionMachineState, this.connectionEpoch), }); return null; } @@ -1269,7 +1206,7 @@ export class HostRuntimeController { this.snapshot = { ...this.snapshot, serverId: this.host.serverId, - ...toSnapshotConnectionPatch(this.connectionMachineState), + ...toSnapshotConnectionPatch(this.connectionMachineState, this.connectionEpoch), client, clientGeneration: nextGeneration, }; @@ -1287,7 +1224,7 @@ export class HostRuntimeController { lastError: client.lastError, }); const patch: HostRuntimeSnapshotPatch = { - ...toSnapshotConnectionPatch(this.connectionMachineState), + ...toSnapshotConnectionPatch(this.connectionMachineState, this.connectionEpoch), ...this.buildAgentDirectoryStatusPatch(), }; this.updateSnapshot(patch); @@ -1307,7 +1244,7 @@ export class HostRuntimeController { message, }); this.updateSnapshot({ - ...toSnapshotConnectionPatch(this.connectionMachineState), + ...toSnapshotConnectionPatch(this.connectionMachineState, this.connectionEpoch), }); } } @@ -1393,6 +1330,13 @@ function rekeyMap(map: Map, oldKey: string, newKey: string): void map.set(newKey, value); } +interface AgentDirectoryRefreshInput { + serverId: string; + filter?: FetchAgentsOptions["filter"]; + subscribe?: FetchAgentsOptions["subscribe"]; + page?: FetchAgentsOptions["page"]; +} + export class HostRuntimeStore { private controllers = new Map(); private serverListeners = new Map void>>(); @@ -1406,6 +1350,8 @@ export class HostRuntimeStore { private deps: HostRuntimeControllerDeps; private lastConnectionStatusByServer = new Map(); private agentDirectoryBootstrapInFlight = new Map>(); + private queuedAgentDrainInFlight = new Set(); + private directorySyncByServer = new Map(); private configuredOverrideBootstrapInFlight: Promise | null = null; private bootStarted = false; private storage: HostRuntimeStorage; @@ -1636,6 +1582,27 @@ export class HostRuntimeStore { rekeyMap(this.lastConnectionStatusByServer, oldServerId, newServerId); rekeyMap(this.agentDirectoryBootstrapInFlight, oldServerId, newServerId); + this.directorySyncByServer.get(oldServerId)?.dispose(); + this.directorySyncByServer.delete(oldServerId); + const directory = new DirectorySync(newServerId, { + drainQueuedAgentMessage: (agentId) => this.drainQueuedAgentMessage(newServerId, agentId), + markAgentLoading: () => controller.markAgentDirectorySyncLoading(), + markAgentReady: () => { + this.agentDirectoryBootstrapInFlight.delete(newServerId); + controller.markAgentDirectorySyncReady(); + }, + markAgentError: (error) => controller.markAgentDirectorySyncError(error), + }); + this.directorySyncByServer.set(newServerId, directory); + const snapshot = controller.getSnapshot(); + directory.connectionChanged({ + client: snapshot.client, + status: snapshot.connectionStatus === "online" ? "online" : "offline", + source: { + clientGeneration: snapshot.clientGeneration, + connectionEpoch: snapshot.connectionEpoch, + }, + }); const listeners = this.serverListeners.get(oldServerId); if (listeners) { @@ -1926,6 +1893,8 @@ export class HostRuntimeStore { this.controllers.delete(serverId); this.lastConnectionStatusByServer.delete(serverId); this.agentDirectoryBootstrapInFlight.delete(serverId); + this.directorySyncByServer.get(serverId)?.dispose(); + this.directorySyncByServer.delete(serverId); void controller.stop(); this.emit(serverId); } @@ -1948,6 +1917,19 @@ export class HostRuntimeStore { onReconcileServerId: (oldId, newId) => this.reconcileServerId(oldId, newId), }); this.controllers.set(host.serverId, controller); + this.directorySyncByServer.set( + host.serverId, + new DirectorySync(host.serverId, { + drainQueuedAgentMessage: (agentId) => + this.drainQueuedAgentMessage(host.serverId, agentId), + markAgentLoading: () => controller.markAgentDirectorySyncLoading(), + markAgentReady: () => { + this.agentDirectoryBootstrapInFlight.delete(host.serverId); + controller.markAgentDirectorySyncReady(); + }, + markAgentError: (error) => controller.markAgentDirectorySyncError(error), + }), + ); this.lastConnectionStatusByServer.set( host.serverId, controller.getSnapshot().connectionStatus, @@ -1980,6 +1962,15 @@ export class HostRuntimeStore { return; } const snapshot = controller.getSnapshot(); + const directorySourceChanged = + this.directorySyncByServer.get(serverId)?.connectionChanged({ + client: snapshot.client, + status: snapshot.connectionStatus === "online" ? "online" : "offline", + source: { + clientGeneration: snapshot.clientGeneration, + connectionEpoch: snapshot.connectionEpoch, + }, + }) ?? false; const previousStatus = this.lastConnectionStatusByServer.get(serverId); this.lastConnectionStatusByServer.set(serverId, snapshot.connectionStatus); const didTransitionOnline = @@ -2002,17 +1993,20 @@ export class HostRuntimeStore { if (!didTransitionOnline && snapshot.hasEverLoadedAgentDirectory) { return; } - if (this.agentDirectoryBootstrapInFlight.has(serverId)) { + if (this.agentDirectoryBootstrapInFlight.has(serverId) && !directorySourceChanged) { return; } const bootstrap = Promise.resolve() .then(() => - this.refreshAgentDirectory({ - serverId, - subscribe: { subscriptionId: `app:${serverId}` }, - page: { limit: DEFAULT_AGENT_DIRECTORY_PAGE_LIMIT }, - }), + Promise.all([ + this.refreshAgentDirectory({ + serverId, + subscribe: { subscriptionId: `app:${serverId}` }, + page: { limit: DEFAULT_AGENT_DIRECTORY_PAGE_LIMIT }, + }), + this.refreshWorkspaceDirectory({ serverId, subscribe: true }), + ]), ) .then(() => undefined) .catch((error) => { @@ -2031,6 +2025,55 @@ export class HostRuntimeStore { this.agentDirectoryBootstrapInFlight.set(serverId, bootstrap); } + drainQueuedAgentMessage(serverId: string, agentId: string): void { + const drainKey = `${serverId}:${agentId}`; + if (this.queuedAgentDrainInFlight.has(drainKey)) return; + const store = useSessionStore.getState(); + const session = store.sessions[serverId]; + const queue = session?.queuedMessages.get(agentId); + const client = session?.client; + if (!client || !queue?.length || session.initializingAgents.get(agentId) === true) { + return; + } + this.queuedAgentDrainInFlight.add(drainKey); + const next = queue[0]; + void sendQueuedComposerMessageNow({ + agentId, + messageId: next.id, + queue: { + read: (queuedAgentId) => + useSessionStore.getState().sessions[serverId]?.queuedMessages.get(queuedAgentId) ?? [], + write: (update) => useSessionStore.getState().setQueuedMessages(serverId, update), + }, + submitMessage: async ({ text, attachments }) => { + const supportsForgeAttachments = + useSessionStore.getState().sessions[serverId]?.serverInfo?.features?.forgeSearch === true; + const wirePayload = splitComposerAttachmentsForSubmit(attachments, { + format: resolveComposerAttachmentSubmitFormat({ supportsForgeAttachments }), + }); + const images = await encodeImages(wirePayload.images); + await client.sendAgentMessage(agentId, text, { + messageId: next.id, + ...(images && images.length > 0 ? { images } : {}), + attachments: wirePayload.attachments, + }); + }, + }) + .then((result) => { + if (result.status === "failed") { + console.error("[HostRuntime] failed to drain queued agent message", { + serverId, + agentId, + error: result.errorMessage, + }); + } + return result; + }) + .finally(() => { + this.queuedAgentDrainInFlight.delete(drainKey); + }); + } + getSnapshot(serverId: string): HostRuntimeSnapshot | null { return this.controllers.get(serverId)?.getSnapshot() ?? null; } @@ -2095,63 +2138,34 @@ export class HostRuntimeStore { ).then(() => undefined); } - async refreshAgentDirectory(input: { - serverId: string; - filter?: FetchAgentsOptions["filter"]; - subscribe?: FetchAgentsOptions["subscribe"]; - page?: FetchAgentsOptions["page"]; - }): Promise<{ - agents: ReturnType["agents"]; - subscriptionId: string | null; - }> { - const controller = this.controllers.get(input.serverId); - if (!controller) { - throw new Error(`Unknown host runtime for serverId ${input.serverId}`); - } - const snapshot = controller.getSnapshot(); - const client = controller.getClient(); - if (!client || snapshot.connectionStatus !== "online") { - throw new Error(`Host ${input.serverId} is not connected`); - } + async refreshAgentDirectory( + input: AgentDirectoryRefreshInput, + ): Promise { + const directory = this.directorySyncByServer.get(input.serverId); + if (!directory) throw new Error(`Unknown host runtime for serverId ${input.serverId}`); + return directory.refreshAgents(input); + } - controller.markAgentDirectorySyncLoading(); - try { - const session = useSessionStore.getState().sessions[input.serverId]; - if (!input.filter && shouldUseLegacyDaemonWorkspaceDirectory(session?.serverInfo)) { - const result = await fetchLegacyDaemonWorkspaceDirectory({ - client, - serverId: input.serverId, - subscribe: input.subscribe, - page: input.page, - }); - controller.markAgentDirectorySyncReady(); - return { - agents: result.agents, - subscriptionId: result.subscriptionId, - }; - } + async refreshWorkspaceDirectory(input: { serverId: string; subscribe?: boolean }): Promise { + const directory = this.directorySyncByServer.get(input.serverId); + if (!directory) throw new Error(`Unknown host runtime for serverId ${input.serverId}`); + await directory.refreshWorkspaces({ subscribe: input.subscribe }); + } - const directory = await fetchCurrentAgentDirectory({ - client, - filter: input.filter, - subscribe: input.subscribe, - page: input.page, - }); + async refreshDirectories(serverId: string): Promise { + const directory = this.directorySyncByServer.get(serverId); + if (!directory) throw new Error(`Unknown host runtime for serverId ${serverId}`); + await directory.refreshAll(); + } - const { agents } = replaceFetchedAgentDirectory({ - serverId: input.serverId, - entries: directory.entries, - }); - - controller.markAgentDirectorySyncReady(); - return { - agents, - subscriptionId: directory.subscriptionId, - }; - } catch (error) { - controller.markAgentDirectorySyncError(toErrorMessage(error)); - throw error; - } + fetchAgentTimeline( + serverId: string, + agentId: string, + request: Parameters[1], + ): Promise>> { + const directory = this.directorySyncByServer.get(serverId); + if (!directory) throw new Error(`Unknown host runtime for serverId ${serverId}`); + return directory.fetchTimeline(agentId, request); } refreshAllAgentDirectories(input?: { serverIds?: string[] }): void { diff --git a/packages/app/src/screens/workspace/visible-agent-ids.test.ts b/packages/app/src/screens/workspace/visible-agent-ids.test.ts new file mode 100644 index 000000000..3fbe56119 --- /dev/null +++ b/packages/app/src/screens/workspace/visible-agent-ids.test.ts @@ -0,0 +1,112 @@ +import { expect, test } from "vitest"; +import type { WorkspaceLayout } from "@/stores/workspace-layout-store"; +import type { WorkspaceTab } from "@/stores/workspace-tabs-store"; +import { selectVisibleAgentIds } from "./visible-agent-ids"; + +test("selects only the active agent tab in every visible pane", () => { + const layout: WorkspaceLayout = { + focusedPaneId: "left", + root: { + kind: "group", + group: { + id: "root", + direction: "horizontal", + sizes: [0.5, 0.5], + children: [ + { kind: "pane", pane: { id: "left", tabIds: ["a", "hidden"], focusedTabId: "a" } }, + { kind: "pane", pane: { id: "right", tabIds: ["b"], focusedTabId: "b" } }, + ], + }, + }, + }; + const tabs: WorkspaceTab[] = [ + { tabId: "a", target: { kind: "agent", agentId: "agent-a" }, createdAt: 1 }, + { tabId: "hidden", target: { kind: "agent", agentId: "agent-hidden" }, createdAt: 2 }, + { tabId: "b", target: { kind: "agent", agentId: "agent-b" }, createdAt: 3 }, + ]; + + expect( + selectVisibleAgentIds({ layout, tabs, routeFocused: true, focusedPaneOnly: false }), + ).toEqual(["agent-a", "agent-b"]); +}); + +test("route blur publishes no viewed agents", () => { + const layout: WorkspaceLayout = { + focusedPaneId: "main", + root: { kind: "pane", pane: { id: "main", tabIds: ["a"], focusedTabId: "a" } }, + }; + const tabs: WorkspaceTab[] = [ + { tabId: "a", target: { kind: "agent", agentId: "agent-a" }, createdAt: 1 }, + ]; + + expect( + selectVisibleAgentIds({ layout, tabs, routeFocused: false, focusedPaneOnly: false }), + ).toEqual([]); +}); + +test("compact and focus modes contribute only the focused pane", () => { + const layout: WorkspaceLayout = { + focusedPaneId: "right", + root: { + kind: "group", + group: { + id: "root", + direction: "horizontal", + sizes: [0.5, 0.5], + children: [ + { kind: "pane", pane: { id: "left", tabIds: ["a"], focusedTabId: "a" } }, + { kind: "pane", pane: { id: "right", tabIds: ["b"], focusedTabId: "b" } }, + ], + }, + }, + }; + const tabs: WorkspaceTab[] = [ + { tabId: "a", target: { kind: "agent", agentId: "agent-a" }, createdAt: 1 }, + { tabId: "b", target: { kind: "agent", agentId: "agent-b" }, createdAt: 2 }, + ]; + + expect( + selectVisibleAgentIds({ layout, tabs, routeFocused: true, focusedPaneOnly: true }), + ).toEqual(["agent-b"]); +}); + +test("pane retargeting replaces the viewed agent and duplicate panes collapse to one ID", () => { + const layout: WorkspaceLayout = { + focusedPaneId: "left", + root: { + kind: "group", + group: { + id: "root", + direction: "horizontal", + sizes: [0.5, 0.5], + children: [ + { kind: "pane", pane: { id: "left", tabIds: ["active"], focusedTabId: "active" } }, + { kind: "pane", pane: { id: "right", tabIds: ["duplicate"], focusedTabId: "duplicate" } }, + ], + }, + }, + }; + const duplicateTabs: WorkspaceTab[] = [ + { tabId: "active", target: { kind: "agent", agentId: "agent-a" }, createdAt: 1 }, + { tabId: "duplicate", target: { kind: "agent", agentId: "agent-a" }, createdAt: 2 }, + ]; + const retargetedTabs: WorkspaceTab[] = [ + { tabId: "active", target: { kind: "agent", agentId: "agent-b" }, createdAt: 1 }, + { tabId: "duplicate", target: { kind: "agent", agentId: "agent-a" }, createdAt: 2 }, + ]; + + expect({ + duplicate: selectVisibleAgentIds({ + layout, + tabs: duplicateTabs, + routeFocused: true, + focusedPaneOnly: false, + }), + retargeted: selectVisibleAgentIds({ + layout, + tabs: retargetedTabs, + routeFocused: true, + focusedPaneOnly: false, + }), + }).toEqual({ duplicate: ["agent-a"], retargeted: ["agent-a", "agent-b"] }); +}); diff --git a/packages/app/src/screens/workspace/visible-agent-ids.ts b/packages/app/src/screens/workspace/visible-agent-ids.ts new file mode 100644 index 000000000..cfaf4b3eb --- /dev/null +++ b/packages/app/src/screens/workspace/visible-agent-ids.ts @@ -0,0 +1,27 @@ +import { collectAllPanes, type WorkspaceLayout } from "@/stores/workspace-layout-store"; +import type { WorkspaceTab } from "@/stores/workspace-tabs-store"; +import { deriveWorkspacePaneState } from "./workspace-pane-state"; + +export function selectVisibleAgentIds(input: { + layout: WorkspaceLayout | null; + tabs: WorkspaceTab[]; + routeFocused: boolean; + focusedPaneOnly: boolean; +}): string[] { + if (!input.routeFocused || !input.layout) { + return []; + } + const panes = input.focusedPaneOnly + ? collectAllPanes(input.layout.root).filter((pane) => pane.id === input.layout?.focusedPaneId) + : collectAllPanes(input.layout.root); + + return [ + ...new Set( + panes.flatMap((pane) => { + const target = deriveWorkspacePaneState({ pane, tabs: input.tabs }).activeTab?.descriptor + .target; + return target?.kind === "agent" ? [target.agentId] : []; + }), + ), + ].sort(); +} diff --git a/packages/app/src/screens/workspace/workspace-screen.tsx b/packages/app/src/screens/workspace/workspace-screen.tsx index cc585e95a..3de632990 100644 --- a/packages/app/src/screens/workspace/workspace-screen.tsx +++ b/packages/app/src/screens/workspace/workspace-screen.tsx @@ -89,6 +89,7 @@ import { normalizeWorkspaceTabTarget, workspaceTabTargetsEqual, } from "@/workspace-tabs/identity"; +import { selectVisibleAgentIds } from "./visible-agent-ids"; import { getHostRuntimeStore, useHostRuntimeClient, @@ -2013,6 +2014,31 @@ function WorkspaceScreenContent({ }), [uiTabs, workspaceLayout], ); + const viewedTimelineSync = useSessionStore( + (state) => state.sessions[normalizedServerId]?.viewedTimelineSync ?? null, + ); + const visibleAgentIds = useMemo( + () => + selectVisibleAgentIds({ + layout: workspaceLayout, + tabs: uiTabs, + routeFocused: isRouteFocused, + focusedPaneOnly: isMobile || isFocusModeEnabled || !supportsDesktopPaneSplits(), + }), + [isFocusModeEnabled, isMobile, isRouteFocused, uiTabs, workspaceLayout], + ); + useEffect(() => { + if (!persistenceKey || !viewedTimelineSync) { + return; + } + viewedTimelineSync.replaceVisibleAgentIds(persistenceKey, visibleAgentIds); + }, [persistenceKey, viewedTimelineSync, visibleAgentIds]); + useEffect(() => { + if (!persistenceKey || !viewedTimelineSync) { + return; + } + return () => viewedTimelineSync.replaceVisibleAgentIds(persistenceKey, []); + }, [persistenceKey, viewedTimelineSync]); const setFocusedAgentId = useSessionStore((state) => state.setFocusedAgentId); const setFocusedTerminalId = useSessionStore((state) => state.setFocusedTerminalId); const focusedPaneAgentId = useMemo(() => { @@ -2740,7 +2766,7 @@ function WorkspaceScreenContent({ // dropped against the stale cursor. const sessionState = useSessionStore.getState().sessions[normalizedServerId]; const currentCursor = sessionState?.agentTimelineCursor.get(agentId); - await client.fetchAgentTimeline(agentId, { + await getHostRuntimeStore().fetchAgentTimeline(normalizedServerId, agentId, { direction: "tail", projection: "projected", ...(currentCursor diff --git a/packages/app/src/stores/session-store.ts b/packages/app/src/stores/session-store.ts index 480deaa87..ca2e25e5b 100644 --- a/packages/app/src/stores/session-store.ts +++ b/packages/app/src/stores/session-store.ts @@ -2,6 +2,7 @@ import equal from "fast-deep-equal"; import { create } from "zustand"; import { subscribeWithSelector } from "zustand/middleware"; import type { DaemonClient } from "@getpaseo/client/internal/daemon-client"; +import type { ViewedTimelineUiBridge } from "@/timeline/viewed-timeline-sync"; import type { AgentDirectoryEntry } from "@/types/agent-directory"; import { handoffCreatedAgentUserMessageToStream, @@ -325,6 +326,8 @@ export interface SessionState { // Daemon client (immutable reference) client: DaemonClient | null; + clientGeneration: number; + viewedTimelineSync: ViewedTimelineUiBridge | null; // Server metadata (from server_info handshake) serverInfo: DaemonServerInfo | null; @@ -390,10 +393,11 @@ interface SessionStoreState { // Action types interface SessionStoreActions { // Session management - initializeSession: (serverId: string, client: DaemonClient) => void; + initializeSession: (serverId: string, client: DaemonClient, clientGeneration?: number) => void; clearSession: (serverId: string) => void; getSession: (serverId: string) => SessionState | undefined; - updateSessionClient: (serverId: string, client: DaemonClient) => void; + updateSessionClient: (serverId: string, client: DaemonClient, clientGeneration?: number) => void; + setViewedTimelineSync: (serverId: string, sync: ViewedTimelineUiBridge | null) => void; updateSessionServerInfo: (serverId: string, info: DaemonServerInfo) => void; // Audio state @@ -531,10 +535,16 @@ type SessionStore = SessionStoreState & SessionStoreActions; const agentLastActivityCoalescer = createAgentLastActivityCoalescer(); // Helper to create initial session state -function createInitialSessionState(serverId: string, client: DaemonClient): SessionState { +function createInitialSessionState( + serverId: string, + client: DaemonClient, + clientGeneration = 0, +): SessionState { return { serverId, client, + clientGeneration, + viewedTimelineSync: null, serverInfo: null, hasHydratedAgents: false, hasHydratedWorkspaces: false, @@ -637,7 +647,7 @@ export const useSessionStore = create()( agentLastActivity: new Map(), // Session management - initializeSession: (serverId, client) => { + initializeSession: (serverId, client, clientGeneration) => { set((prev) => { if (prev.sessions[serverId]) { return prev; @@ -646,7 +656,7 @@ export const useSessionStore = create()( ...prev, sessions: { ...prev.sessions, - [serverId]: createInitialSessionState(serverId, client), + [serverId]: createInitialSessionState(serverId, client, clientGeneration), }, }; }); @@ -686,7 +696,7 @@ export const useSessionStore = create()( }); }, - updateSessionClient: (serverId, client) => { + updateSessionClient: (serverId, client, clientGeneration = 0) => { set((prev) => { const session = prev.sessions[serverId]; @@ -694,7 +704,7 @@ export const useSessionStore = create()( return prev; } - if (session.client === client) { + if (session.client === client && session.clientGeneration === clientGeneration) { return prev; } @@ -705,12 +715,29 @@ export const useSessionStore = create()( [serverId]: { ...session, client, + clientGeneration, }, }, }; }); }, + setViewedTimelineSync: (serverId, viewedTimelineSync) => { + set((prev) => { + const session = prev.sessions[serverId]; + if (!session || session.viewedTimelineSync === viewedTimelineSync) { + return prev; + } + return { + ...prev, + sessions: { + ...prev.sessions, + [serverId]: { ...session, viewedTimelineSync }, + }, + }; + }); + }, + updateSessionServerInfo: (serverId, info) => { set((prev) => { const session = prev.sessions[serverId]; diff --git a/packages/app/src/timeline/fetch-agent-timeline-once.test.ts b/packages/app/src/timeline/fetch-agent-timeline-once.test.ts new file mode 100644 index 000000000..1ae97e028 --- /dev/null +++ b/packages/app/src/timeline/fetch-agent-timeline-once.test.ts @@ -0,0 +1,30 @@ +import { expect, test } from "vitest"; +import type { DaemonClient } from "@getpaseo/client/internal/daemon-client"; +import { fetchAgentTimelineOnce } from "./fetch-agent-timeline-once"; + +type TimelinePage = Awaited>; + +test("concurrent identical timeline reads share one request", async () => { + let resolvePage: (page: TimelinePage) => void = () => {}; + const page = new Promise((resolve) => { + resolvePage = resolve; + }); + const requests: Array<{ agentId: string; direction: string }> = []; + const client = { + fetchAgentTimeline: async ( + agentId: string, + request: Parameters[1], + ) => { + requests.push({ agentId, direction: request?.direction ?? "tail" }); + return page; + }, + }; + const request = { direction: "tail" as const, limit: 100, projection: "projected" as const }; + + const first = fetchAgentTimelineOnce(client, "agent", request); + const second = fetchAgentTimelineOnce(client, "agent", request); + resolvePage({ hasNewer: false } as TimelinePage); + + await expect(Promise.all([first, second])).resolves.toHaveLength(2); + expect(requests).toEqual([{ agentId: "agent", direction: "tail" }]); +}); diff --git a/packages/app/src/timeline/fetch-agent-timeline-once.ts b/packages/app/src/timeline/fetch-agent-timeline-once.ts new file mode 100644 index 000000000..e0d67b489 --- /dev/null +++ b/packages/app/src/timeline/fetch-agent-timeline-once.ts @@ -0,0 +1,31 @@ +import type { DaemonClient } from "@getpaseo/client/internal/daemon-client"; + +type TimelineClient = Pick; +type TimelineRequest = Parameters[1]; +type TimelinePage = Awaited>; + +const inFlightByClient = new WeakMap>>(); + +export function fetchAgentTimelineOnce( + client: TimelineClient, + agentId: string, + request: TimelineRequest, +): Promise { + let inFlight = inFlightByClient.get(client); + if (!inFlight) { + inFlight = new Map(); + inFlightByClient.set(client, inFlight); + } + + const key = `${agentId}:${JSON.stringify(request)}`; + const existing = inFlight.get(key); + if (existing) return existing; + + const fetch = client.fetchAgentTimeline(agentId, request); + inFlight.set(key, fetch); + const clear = () => { + if (inFlight.get(key) === fetch) inFlight.delete(key); + }; + void fetch.then(clear, clear); + return fetch; +} diff --git a/packages/app/src/timeline/session-stream-reducers.ts b/packages/app/src/timeline/session-stream-reducers.ts index aaa0e21c0..c10f91c8b 100644 --- a/packages/app/src/timeline/session-stream-reducers.ts +++ b/packages/app/src/timeline/session-stream-reducers.ts @@ -1160,7 +1160,7 @@ export interface CreateSessionAgentStreamReducerQueueInput { state: (prev: Map) => Map, ) => void; setAgents: (serverId: string, state: (prev: Map) => Map) => void; - requestCanonicalCatchUp: (agentId: string, cursor: { epoch: string; endSeq: number }) => void; + recoverTimelineGap: (agentId: string, cursor: { epoch: string; endSeq: number }) => void; } function scheduleAgentStreamReducerFlush(callback: () => void): number { @@ -1174,13 +1174,8 @@ function cancelAgentStreamReducerFlush(id: number) { export function createSessionAgentStreamReducerQueue( input: CreateSessionAgentStreamReducerQueueInput, ): AgentStreamReducerQueue { - const { - serverId, - setAgentStreamState, - setAgentTimelineCursor, - setAgents, - requestCanonicalCatchUp, - } = input; + const { serverId, setAgentStreamState, setAgentTimelineCursor, setAgents, recoverTimelineGap } = + input; return createAgentStreamReducerQueue({ getSnapshot: (agentId) => { @@ -1258,7 +1253,7 @@ export function createSessionAgentStreamReducerQueue( handleSideEffects: (agentId, sideEffects) => { for (const effect of sideEffects) { if (effect.type === "catch_up") { - requestCanonicalCatchUp(agentId, effect.cursor); + recoverTimelineGap(agentId, effect.cursor); } } }, diff --git a/packages/app/src/timeline/timeline-sync-plan.test.ts b/packages/app/src/timeline/timeline-sync-plan.test.ts index 913f2501b..918c97357 100644 --- a/packages/app/src/timeline/timeline-sync-plan.test.ts +++ b/packages/app/src/timeline/timeline-sync-plan.test.ts @@ -4,7 +4,6 @@ import { isTimelineCatchUpComplete, planInitialAgentTimelineSync, planResumeTimelineSync, - planTimelineCatchUpFollowUp, planTimelineOlderFetch, } from "./timeline-sync-plan"; @@ -70,31 +69,7 @@ describe("timeline sync planning", () => { }); }); - test("catch-up keeps paging while the daemon reports newer rows", () => { - const plan = planTimelineCatchUpFollowUp({ - direction: "after", - hasNewer: true, - endCursor: { epoch: "epoch-1", seq: 200 }, - error: null, - }); - - expect(plan).toEqual({ - direction: "after", - cursor: { epoch: "epoch-1", seq: 200 }, - limit: TIMELINE_FETCH_PAGE_SIZE, - projection: "projected", - }); - }); - test("catch-up finishes when the daemon reports no newer rows", () => { - const plan = planTimelineCatchUpFollowUp({ - direction: "after", - hasNewer: false, - endCursor: { epoch: "epoch-1", seq: 200 }, - error: null, - }); - - expect(plan).toBeNull(); expect(isTimelineCatchUpComplete({ direction: "after", hasNewer: false, error: null })).toBe( true, ); diff --git a/packages/app/src/timeline/timeline-sync-plan.ts b/packages/app/src/timeline/timeline-sync-plan.ts index 43efec277..d1cd4b376 100644 --- a/packages/app/src/timeline/timeline-sync-plan.ts +++ b/packages/app/src/timeline/timeline-sync-plan.ts @@ -87,19 +87,6 @@ export function planTimelineOlderFetch(cursor: TimelineSyncCursor) { } as const; } -export function planTimelineCatchUpFollowUp(input: { - direction: "tail" | "before" | "after"; - hasNewer: boolean; - endCursor: TimelineSyncCursor | null; - error: string | null; -}): ProjectedTimelineAfterFetchPlan | null { - if (input.error || input.direction !== "after" || !input.hasNewer || !input.endCursor) { - return null; - } - - return planTimelineCatchUpAfter(input.endCursor); -} - export function isTimelineCatchUpComplete(input: { direction: "tail" | "before" | "after"; hasNewer: boolean; diff --git a/packages/app/src/timeline/viewed-timeline-sync.test.ts b/packages/app/src/timeline/viewed-timeline-sync.test.ts new file mode 100644 index 000000000..511650f62 --- /dev/null +++ b/packages/app/src/timeline/viewed-timeline-sync.test.ts @@ -0,0 +1,448 @@ +import { expect, test } from "vitest"; +import type { ProjectedTimelineForwardFetchPlan } from "./timeline-sync-plan"; +import { createViewedTimelineSync } from "./viewed-timeline-sync"; + +interface Deferred { + promise: Promise; + resolve(value: T): void; + reject(error: Error): void; +} + +function deferred(): Deferred { + let resolve!: (value: T) => void; + let reject!: (error: Error) => void; + const promise = new Promise((resolvePromise, rejectPromise) => { + resolve = resolvePromise; + reject = rejectPromise; + }); + return { promise, resolve, reject }; +} + +interface MembershipRequest { + agentIds: string[]; + succeed(): void; + fail(message: string): void; +} + +interface TimelineFetch { + agentId: string; + request: ProjectedTimelineForwardFetchPlan; + respond(input: { hasNewer: boolean; seq?: number }): void; + fail(message: string): void; +} + +class TimelineWorld { + readonly errors: string[] = []; + readonly sync = createViewedTimelineSync({ + setSubscription: async (agentIds) => { + const result = deferred(); + this.memberships.push({ + agentIds, + succeed: () => result.resolve(), + fail: (message) => result.reject(new Error(message)), + }); + this.releaseMembershipWaiter(); + return result.promise; + }, + readCursor: (agentId) => this.cursors.get(agentId), + hasAuthoritativeHistory: (agentId) => this.authoritativeHistory.has(agentId), + fetchPage: async (agentId, request) => { + const result = deferred<{ + hasNewer: boolean; + endCursor: { epoch: string; seq: number } | null; + }>(); + this.fetches.push({ + agentId, + request, + respond: ({ hasNewer, seq = 1 }) => + result.resolve({ + hasNewer, + endCursor: { epoch: `epoch-${agentId}`, seq }, + }), + fail: (message) => result.reject(new Error(message)), + }); + this.releaseFetchWaiters(); + return result.promise; + }, + reportError: (error) => { + this.errors.push(error instanceof Error ? error.message : String(error)); + const waiter = this.errorWaiters.shift(); + if (waiter) waiter(this.errors.at(-1) ?? ""); + }, + scheduleRetry: (retry) => { + this.retries.push(retry); + const waiter = this.retryWaiters.shift(); + if (waiter) waiter(this.retries.shift()!); + return () => { + const index = this.retries.indexOf(retry); + if (index >= 0) this.retries.splice(index, 1); + }; + }, + }); + + private readonly memberships: MembershipRequest[] = []; + private readonly membershipWaiters: Array<(request: MembershipRequest) => void> = []; + private readonly fetches: TimelineFetch[] = []; + private readonly fetchWaiters: Array<{ + agentId: string; + resolve(fetch: TimelineFetch): void; + }> = []; + private readonly cursors = new Map(); + private readonly authoritativeHistory = new Set(); + private readonly errorWaiters: Array<(message: string) => void> = []; + private readonly retries: Array<() => void> = []; + private readonly retryWaiters: Array<(retry: () => void) => void> = []; + + setCursor(agentId: string, endSeq: number): void { + this.cursors.set(agentId, { epoch: `epoch-${agentId}`, startSeq: 1, endSeq }); + this.authoritativeHistory.add(agentId); + } + + setLiveCursor(agentId: string, endSeq: number): void { + this.cursors.set(agentId, { epoch: `epoch-${agentId}`, startSeq: 1, endSeq }); + } + + nextMembership(): Promise { + const request = this.memberships.shift(); + if (request) return Promise.resolve(request); + return new Promise((resolve) => this.membershipWaiters.push(resolve)); + } + + nextFetch(agentId: string): Promise { + const index = this.fetches.findIndex((fetch) => fetch.agentId === agentId); + if (index >= 0) return Promise.resolve(this.fetches.splice(index, 1)[0]); + return new Promise((resolve) => this.fetchWaiters.push({ agentId, resolve })); + } + + expectNoPendingMembership(): void { + expect(this.memberships).toEqual([]); + } + + expectNoPendingFetch(): void { + expect(this.fetches).toEqual([]); + } + + nextError(): Promise { + const message = this.errors.at(-1); + if (message) return Promise.resolve(message); + return new Promise((resolve) => this.errorWaiters.push(resolve)); + } + + nextRetry(): Promise<() => void> { + const retry = this.retries.shift(); + if (retry) return Promise.resolve(retry); + return new Promise((resolve) => this.retryWaiters.push(resolve)); + } + + private releaseMembershipWaiter(): void { + const waiter = this.membershipWaiters.shift(); + if (!waiter) return; + const request = this.memberships.shift(); + if (request) waiter(request); + } + + private releaseFetchWaiters(): void { + for (let waiterIndex = this.fetchWaiters.length - 1; waiterIndex >= 0; waiterIndex -= 1) { + const waiter = this.fetchWaiters[waiterIndex]; + const index = this.fetches.findIndex((fetch) => fetch.agentId === waiter.agentId); + if (index < 0) continue; + this.fetchWaiters.splice(waiterIndex, 1); + waiter.resolve(this.fetches.splice(index, 1)[0]); + } + } +} + +test("uses a tail fetch when a live cursor is not authoritative", async () => { + const world = new TimelineWorld(); + world.setLiveCursor("agent-a", 9); + world.sync.setConnected(true); + world.sync.replaceVisibleAgentIds("workspace", ["agent-a"]); + const membership = await world.nextMembership(); + membership.succeed(); + + const fetch = await world.nextFetch("agent-a"); + expect(fetch.request).toEqual({ direction: "tail", limit: 100, projection: "projected" }); + fetch.respond({ hasNewer: false }); +}); + +test("unchanged visible-set publication does not cancel paged catch-up", async () => { + const world = new TimelineWorld(); + world.sync.setConnected(true); + world.sync.replaceVisibleAgentIds("workspace", ["agent-a"]); + const membership = await world.nextMembership(); + membership.succeed(); + const firstPage = await world.nextFetch("agent-a"); + + world.sync.replaceVisibleAgentIds("workspace", ["agent-a", "agent-a"]); + firstPage.respond({ hasNewer: true, seq: 5 }); + const secondPage = await world.nextFetch("agent-a"); + secondPage.respond({ hasNewer: false }); + + expect(secondPage.request).toEqual({ + direction: "after", + cursor: { epoch: "epoch-agent-a", seq: 5 }, + limit: 100, + projection: "projected", + }); + world.expectNoPendingMembership(); +}); + +test("all acknowledged agents begin catch-up independently", async () => { + const world = new TimelineWorld(); + world.sync.setConnected(true); + world.sync.replaceVisibleAgentIds("workspace", ["agent-b", "agent-a"]); + const membership = await world.nextMembership(); + membership.succeed(); + + const [agentA, agentB] = await Promise.all([ + world.nextFetch("agent-a"), + world.nextFetch("agent-b"), + ]); + agentA.respond({ hasNewer: false }); + agentB.respond({ hasNewer: false }); + + expect(membership.agentIds).toEqual(["agent-a", "agent-b"]); +}); + +test("membership changes during acknowledgement never catch up the stale set", async () => { + const world = new TimelineWorld(); + world.sync.setConnected(true); + world.sync.replaceVisibleAgentIds("workspace", ["agent-a"]); + const staleMembership = await world.nextMembership(); + + world.sync.replaceVisibleAgentIds("workspace", ["agent-b"]); + staleMembership.succeed(); + const currentMembership = await world.nextMembership(); + currentMembership.succeed(); + const agentB = await world.nextFetch("agent-b"); + agentB.respond({ hasNewer: false }); + + expect({ stale: staleMembership.agentIds, current: currentMembership.agentIds }).toEqual({ + stale: ["agent-a"], + current: ["agent-b"], + }); + world.expectNoPendingFetch(); +}); + +test("removing one agent during paging cancels only that agent", async () => { + const world = new TimelineWorld(); + world.sync.setConnected(true); + world.sync.replaceVisibleAgentIds("workspace", ["agent-a", "agent-b"]); + const initialMembership = await world.nextMembership(); + initialMembership.succeed(); + const [agentA, agentB] = await Promise.all([ + world.nextFetch("agent-a"), + world.nextFetch("agent-b"), + ]); + + world.sync.replaceVisibleAgentIds("workspace", ["agent-b"]); + const replacement = await world.nextMembership(); + agentA.respond({ hasNewer: true, seq: 4 }); + agentB.respond({ hasNewer: true, seq: 7 }); + const agentBNext = await world.nextFetch("agent-b"); + replacement.succeed(); + agentBNext.respond({ hasNewer: false }); + + expect(replacement.agentIds).toEqual(["agent-b"]); + world.expectNoPendingFetch(); +}); + +test("disconnect cancels paging and reconnect restores membership before fresh catch-up", async () => { + const world = new TimelineWorld(); + world.sync.setConnected(true); + world.sync.replaceVisibleAgentIds("workspace", ["agent-a"]); + const firstMembership = await world.nextMembership(); + firstMembership.succeed(); + const stalePage = await world.nextFetch("agent-a"); + + world.sync.setConnected(false); + stalePage.respond({ hasNewer: true, seq: 8 }); + world.sync.setConnected(true); + const restoredMembership = await world.nextMembership(); + restoredMembership.succeed(); + const restoredPage = await world.nextFetch("agent-a"); + restoredPage.respond({ hasNewer: false }); + + expect(restoredMembership.agentIds).toEqual(["agent-a"]); + world.expectNoPendingFetch(); +}); + +test("overlapping sources deduplicate membership and source removal preserves remaining views", async () => { + const world = new TimelineWorld(); + world.sync.replaceVisibleAgentIds("left-route", ["agent-a"]); + world.sync.replaceVisibleAgentIds("right-route", ["agent-a", "agent-b"]); + world.sync.setConnected(true); + const combined = await world.nextMembership(); + combined.succeed(); + const [agentA, agentB] = await Promise.all([ + world.nextFetch("agent-a"), + world.nextFetch("agent-b"), + ]); + agentA.respond({ hasNewer: false }); + agentB.respond({ hasNewer: false }); + + world.sync.replaceVisibleAgentIds("left-route", []); + world.expectNoPendingMembership(); + world.sync.replaceVisibleAgentIds("right-route", ["agent-b"]); + const remaining = await world.nextMembership(); + remaining.succeed(); + + expect({ combined: combined.agentIds, remaining: remaining.agentIds }).toEqual({ + combined: ["agent-a", "agent-b"], + remaining: ["agent-b"], + }); +}); + +test("a failed catch-up reports once and retries through the explicit retry policy", async () => { + const world = new TimelineWorld(); + world.sync.setConnected(true); + world.sync.replaceVisibleAgentIds("workspace", ["agent-a"]); + const membership = await world.nextMembership(); + membership.succeed(); + const failed = await world.nextFetch("agent-a"); + failed.fail("timeline unavailable"); + const [error, retryCatchUp] = await Promise.all([world.nextError(), world.nextRetry()]); + + retryCatchUp(); + const retry = await world.nextFetch("agent-a"); + retry.respond({ hasNewer: false }); + + expect({ error, retryDirection: retry.request.direction }).toEqual({ + error: "timeline unavailable", + retryDirection: "tail", + }); + world.expectNoPendingMembership(); +}); + +test("gap recovery supersedes completed catch-up and pages through the current tail", async () => { + const world = new TimelineWorld(); + world.sync.setConnected(true); + world.sync.replaceVisibleAgentIds("workspace", ["agent-a"]); + const membership = await world.nextMembership(); + membership.succeed(); + const initial = await world.nextFetch("agent-a"); + initial.respond({ hasNewer: false }); + + world.sync.recoverGap("agent-a", { epoch: "epoch-agent-a", endSeq: 10 }); + const gapPage = await world.nextFetch("agent-a"); + gapPage.respond({ hasNewer: true, seq: 15 }); + const finalPage = await world.nextFetch("agent-a"); + finalPage.respond({ hasNewer: false }); + + expect([gapPage.request, finalPage.request]).toEqual([ + { + direction: "after", + cursor: { epoch: "epoch-agent-a", seq: 10 }, + limit: 100, + projection: "projected", + }, + { + direction: "after", + cursor: { epoch: "epoch-agent-a", seq: 15 }, + limit: 100, + projection: "projected", + }, + ]); +}); + +test("repeated recovery for the same running gap reuses the in-flight fetch", async () => { + const world = new TimelineWorld(); + world.sync.setConnected(true); + world.sync.replaceVisibleAgentIds("workspace", ["agent-a"]); + const membership = await world.nextMembership(); + membership.succeed(); + const initial = await world.nextFetch("agent-a"); + initial.respond({ hasNewer: false }); + + const cursor = { epoch: "epoch-agent-a", endSeq: 10 }; + world.sync.recoverGap("agent-a", cursor); + const gapPage = await world.nextFetch("agent-a"); + world.sync.recoverGap("agent-a", cursor); + + world.expectNoPendingFetch(); + gapPage.respond({ hasNewer: false }); +}); + +test("membership failure autonomously retries without another visibility declaration", async () => { + const world = new TimelineWorld(); + world.sync.setConnected(true); + world.sync.replaceVisibleAgentIds("workspace", ["agent-a"]); + const failed = await world.nextMembership(); + failed.fail("subscription unavailable"); + const [error, retryMembership] = await Promise.all([world.nextError(), world.nextRetry()]); + + retryMembership(); + const retry = await world.nextMembership(); + retry.succeed(); + const catchUp = await world.nextFetch("agent-a"); + catchUp.respond({ hasNewer: false }); + + expect({ error, failed: failed.agentIds, retry: retry.agentIds }).toEqual({ + error: "subscription unavailable", + failed: ["agent-a"], + retry: ["agent-a"], + }); +}); + +test("background sends an empty set and foreground restores visible membership", async () => { + const world = new TimelineWorld(); + world.sync.setConnected(true); + world.sync.replaceVisibleAgentIds("workspace", ["agent-a"]); + const initial = await world.nextMembership(); + initial.succeed(); + const initialCatchUp = await world.nextFetch("agent-a"); + initialCatchUp.respond({ hasNewer: false }); + + world.sync.setActive(false); + const background = await world.nextMembership(); + background.succeed(); + world.sync.setActive(true); + const foreground = await world.nextMembership(); + foreground.succeed(); + const resumedCatchUp = await world.nextFetch("agent-a"); + resumedCatchUp.respond({ hasNewer: false }); + + expect({ background: background.agentIds, foreground: foreground.agentIds }).toEqual({ + background: [], + foreground: ["agent-a"], + }); +}); + +test("stale membership retry cannot overwrite a newer effective set", async () => { + const world = new TimelineWorld(); + world.sync.setConnected(true); + world.sync.replaceVisibleAgentIds("workspace", ["agent-a"]); + const failed = await world.nextMembership(); + failed.fail("subscription unavailable"); + const staleRetry = await world.nextRetry(); + + world.sync.replaceVisibleAgentIds("workspace", ["agent-b"]); + const current = await world.nextMembership(); + staleRetry(); + current.succeed(); + const catchUp = await world.nextFetch("agent-b"); + catchUp.respond({ hasNewer: false }); + + expect(current.agentIds).toEqual(["agent-b"]); + world.expectNoPendingMembership(); +}); + +test("membership retry cannot run while disconnected", async () => { + const world = new TimelineWorld(); + world.sync.setConnected(true); + world.sync.replaceVisibleAgentIds("workspace", ["agent-a"]); + const failed = await world.nextMembership(); + failed.fail("subscription unavailable"); + const disconnectedRetry = await world.nextRetry(); + + world.sync.setConnected(false); + disconnectedRetry(); + world.expectNoPendingMembership(); + world.sync.setConnected(true); + const restored = await world.nextMembership(); + restored.succeed(); + const catchUp = await world.nextFetch("agent-a"); + catchUp.respond({ hasNewer: false }); + + expect(restored.agentIds).toEqual(["agent-a"]); +}); diff --git a/packages/app/src/timeline/viewed-timeline-sync.ts b/packages/app/src/timeline/viewed-timeline-sync.ts new file mode 100644 index 000000000..f0f2c0870 --- /dev/null +++ b/packages/app/src/timeline/viewed-timeline-sync.ts @@ -0,0 +1,321 @@ +import type { AgentTimelineCursorState } from "@/stores/session-store"; +import { + planInitialAgentTimelineSync, + planResumeTimelineSync, + planTimelineCatchUpAfter, + type ProjectedTimelineForwardFetchPlan, +} from "./timeline-sync-plan"; + +interface TimelinePageResult { + hasNewer: boolean; + endCursor: { epoch: string; seq: number } | null; +} + +interface ViewedTimelineSyncPorts { + setSubscription(agentIds: string[]): Promise; + readCursor(agentId: string): AgentTimelineCursorState | undefined; + hasAuthoritativeHistory(agentId: string): boolean; + fetchPage( + agentId: string, + request: ProjectedTimelineForwardFetchPlan, + ): Promise; + reportError(error: unknown): void; + scheduleRetry(retry: () => void): () => void; +} + +export interface ViewedTimelineUiBridge { + replaceVisibleAgentIds(sourceId: string, agentIds: string[]): void; +} + +export interface ViewedTimelineSync extends ViewedTimelineUiBridge { + setActive(active: boolean): void; + setConnected(connected: boolean): void; + recoverGap(agentId: string, cursor: { epoch: string; endSeq: number }): void; + dispose(): void; +} + +type CatchUpStatus = "running" | "complete" | "error"; + +interface CatchUpState { + generation: number; + status: CatchUpStatus; + request?: ProjectedTimelineForwardFetchPlan; + cancelRetry?: () => void; +} + +function isSameCatchUpRequest( + left: ProjectedTimelineForwardFetchPlan | undefined, + right: ProjectedTimelineForwardFetchPlan | undefined, +): boolean { + if (!left || !right || left.direction !== right.direction) return false; + if (left.direction !== "after" || right.direction !== "after") return true; + return left.cursor.epoch === right.cursor.epoch && left.cursor.seq === right.cursor.seq; +} + +function shouldKeepCurrentCatchUp(input: { + current: CatchUpState | undefined; + request: ProjectedTimelineForwardFetchPlan | undefined; + supersede: boolean; +}): boolean { + if (!input.current) return false; + if (input.supersede) { + return ( + input.current.status === "running" && + isSameCatchUpRequest(input.current.request, input.request) + ); + } + return input.current.status === "running" || input.current.status === "complete"; +} + +function normalizeAgentIds(agentIds: string[]): string[] { + return [...new Set(agentIds)].filter(Boolean).sort(); +} + +function sameAgentIds(left: string[], right: string[]): boolean { + return left.length === right.length && left.every((agentId, index) => agentId === right[index]); +} + +export function createViewedTimelineSync(ports: ViewedTimelineSyncPorts): ViewedTimelineSync { + const sources = new Map(); + const catchUps = new Map(); + const catchUpGenerations = new Map(); + const pendingGaps = new Map(); + let active = true; + let connected = false; + let disposed = false; + let desired: string[] = []; + let acknowledged: string[] = []; + let membershipGeneration = 0; + let reconciling = false; + let reconcileRequested = false; + let membershipNeedsRetry = false; + let cancelMembershipRetry: (() => void) | null = null; + + const effectiveAgentIds = () => (active ? normalizeAgentIds([...sources.values()].flat()) : []); + + const isAcknowledged = (agentId: string) => acknowledged.includes(agentId); + const isDesired = (agentId: string) => desired.includes(agentId); + + const cancelCatchUp = (agentId: string) => { + catchUpGenerations.set(agentId, (catchUpGenerations.get(agentId) ?? 0) + 1); + catchUps.get(agentId)?.cancelRetry?.(); + catchUps.delete(agentId); + pendingGaps.delete(agentId); + }; + + const fetchUntilCurrent = async ( + agentId: string, + generation: number, + request: ProjectedTimelineForwardFetchPlan, + ): Promise => { + if ( + disposed || + !connected || + !isDesired(agentId) || + !isAcknowledged(agentId) || + catchUps.get(agentId)?.generation !== generation + ) { + return; + } + + try { + const page = await ports.fetchPage(agentId, request); + if ( + disposed || + !connected || + !isDesired(agentId) || + !isAcknowledged(agentId) || + catchUps.get(agentId)?.generation !== generation + ) { + return; + } + if (page.hasNewer && page.endCursor) { + await fetchUntilCurrent(agentId, generation, planTimelineCatchUpAfter(page.endCursor)); + return; + } + if (page.hasNewer) { + throw new Error(`Timeline page for ${agentId} hasNewer without an end cursor`); + } + catchUps.set(agentId, { generation, status: "complete" }); + } catch (error) { + if (catchUps.get(agentId)?.generation === generation) { + const cancelRetry = ports.scheduleRetry(() => { + const current = catchUps.get(agentId); + if (current?.generation !== generation || current.status !== "error") return; + startCatchUp(agentId); + }); + catchUps.set(agentId, { generation, status: "error", cancelRetry }); + ports.reportError(error); + } + } + }; + + const startCatchUp = ( + agentId: string, + options: { + request?: ProjectedTimelineForwardFetchPlan; + supersede?: boolean; + } = {}, + ) => { + const { request, supersede = false } = options; + if (!connected || !isDesired(agentId) || !isAcknowledged(agentId)) { + if (request) pendingGaps.set(agentId, request); + return; + } + const current = catchUps.get(agentId); + if (shouldKeepCurrentCatchUp({ current, request, supersede })) { + return; + } + current?.cancelRetry?.(); + const generation = (catchUpGenerations.get(agentId) ?? 0) + 1; + catchUpGenerations.set(agentId, generation); + catchUps.set(agentId, { generation, status: "running", request }); + pendingGaps.delete(agentId); + const cursor = ports.readCursor(agentId); + const nextRequest = + request ?? + (ports.hasAuthoritativeHistory(agentId) + ? planResumeTimelineSync({ cursor }) + : planInitialAgentTimelineSync({ cursor, hasAuthoritativeHistory: false })); + void fetchUntilCurrent(agentId, generation, nextRequest); + }; + + const startAcknowledgedCatchUps = () => { + for (const agentId of acknowledged) { + const gap = pendingGaps.get(agentId); + startCatchUp(agentId, { request: gap, supersede: Boolean(gap) }); + } + }; + + const reconcileLatestMembership = async (): Promise => { + if (disposed || !connected) return; + const generation = membershipGeneration; + const requested = desired; + if (!membershipNeedsRetry && sameAgentIds(requested, acknowledged)) return; + membershipNeedsRetry = false; + try { + await ports.setSubscription(requested); + } catch (error) { + membershipNeedsRetry = true; + cancelMembershipRetry?.(); + cancelMembershipRetry = ports.scheduleRetry(() => { + cancelMembershipRetry = null; + if ( + disposed || + !connected || + membershipGeneration !== generation || + !sameAgentIds(desired, requested) + ) { + return; + } + void reconcileMembership(); + }); + ports.reportError(error); + return; + } + cancelMembershipRetry?.(); + cancelMembershipRetry = null; + if (disposed || !connected) return; + acknowledged = requested; + if (generation !== membershipGeneration) { + await reconcileLatestMembership(); + return; + } + startAcknowledgedCatchUps(); + if (!sameAgentIds(desired, acknowledged)) await reconcileLatestMembership(); + }; + + const reconcileMembership = async () => { + if (reconciling) { + reconcileRequested = true; + return; + } + if (disposed || !connected) return; + reconciling = true; + try { + await reconcileLatestMembership(); + } finally { + reconciling = false; + if (reconcileRequested && !disposed && connected) { + reconcileRequested = false; + void reconcileMembership(); + } else if ( + !disposed && + connected && + !membershipNeedsRetry && + !sameAgentIds(desired, acknowledged) + ) { + void reconcileMembership(); + } + } + }; + + const retryFailedCatchUps = () => { + for (const agentId of acknowledged) { + if (catchUps.get(agentId)?.status === "error") startCatchUp(agentId); + } + }; + + const publishEffectiveMembership = () => { + const nextDesired = effectiveAgentIds(); + if (sameAgentIds(nextDesired, desired)) { + if (membershipNeedsRetry) void reconcileMembership(); + retryFailedCatchUps(); + return; + } + + for (const agentId of desired) { + if (!nextDesired.includes(agentId)) cancelCatchUp(agentId); + } + cancelMembershipRetry?.(); + cancelMembershipRetry = null; + desired = nextDesired; + membershipGeneration += 1; + void reconcileMembership(); + }; + + return { + replaceVisibleAgentIds(sourceId, agentIds) { + const normalized = normalizeAgentIds(agentIds); + if (normalized.length === 0) sources.delete(sourceId); + else sources.set(sourceId, normalized); + publishEffectiveMembership(); + }, + setActive(nextActive) { + if (active === nextActive) return; + active = nextActive; + publishEffectiveMembership(); + }, + setConnected(nextConnected) { + if (connected === nextConnected) return; + connected = nextConnected; + if (!connected) { + cancelMembershipRetry?.(); + cancelMembershipRetry = null; + acknowledged = []; + membershipGeneration += 1; + for (const agentId of desired) cancelCatchUp(agentId); + return; + } + membershipGeneration += 1; + void reconcileMembership(); + }, + recoverGap(agentId, cursor) { + if (!isDesired(agentId)) return; + startCatchUp(agentId, { + request: planTimelineCatchUpAfter({ epoch: cursor.epoch, seq: cursor.endSeq }), + supersede: true, + }); + }, + dispose() { + disposed = true; + cancelMembershipRetry?.(); + cancelMembershipRetry = null; + sources.clear(); + membershipGeneration += 1; + for (const agentId of desired) cancelCatchUp(agentId); + desired = []; + acknowledged = []; + }, + }; +} diff --git a/packages/app/src/utils/agent-directory-reconciliation.test.ts b/packages/app/src/utils/agent-directory-reconciliation.test.ts new file mode 100644 index 000000000..03387d010 --- /dev/null +++ b/packages/app/src/utils/agent-directory-reconciliation.test.ts @@ -0,0 +1,240 @@ +import { describe, expect, it } from "vitest"; +import type { FetchAgentsEntry } from "@getpaseo/client/internal/daemon-client"; +import type { AgentSnapshotPayload } from "@getpaseo/protocol/messages"; +import type { Agent } from "@/stores/session-store"; +import { reconcileAgentDirectory } from "./agent-directory-reconciliation"; + +function snapshot(id: string, status: AgentSnapshotPayload["status"]): AgentSnapshotPayload { + return { + id, + provider: "codex", + cwd: "/repo", + model: null, + createdAt: "2026-07-12T10:00:00.000Z", + updatedAt: "2026-07-12T10:00:00.000Z", + lastUserMessageAt: null, + status, + capabilities: { + supportsStreaming: true, + supportsSessionPersistence: true, + supportsDynamicModes: true, + supportsMcpServers: true, + supportsReasoningStream: true, + supportsToolInvocations: true, + }, + currentModeId: null, + availableModes: [], + pendingPermissions: [], + persistence: null, + title: null, + labels: {}, + }; +} + +function entry(id: string, status: AgentSnapshotPayload["status"]): FetchAgentsEntry { + return { + agent: snapshot(id, status), + project: { + projectKey: "/repo", + projectName: "repo", + checkout: { + cwd: "/repo", + isGit: false, + currentBranch: null, + remoteUrl: null, + worktreeRoot: null, + isPaseoOwnedWorktree: false, + mainRepoRoot: null, + }, + }, + }; +} + +function replica(id: string, status: Agent["status"]): Agent { + return { + ...snapshot(id, status), + serverId: "server", + createdAt: new Date("2026-07-12T10:00:00.000Z"), + updatedAt: new Date("2026-07-12T10:00:00.000Z"), + lastActivityAt: new Date("2026-07-12T10:00:00.000Z"), + lastUserMessageAt: null, + attentionTimestamp: null, + archivedAt: null, + parentAgentId: null, + }; +} + +describe("agent directory reconciliation", () => { + it("reports snapshot-only and buffered running transitions exactly once", () => { + const result = reconcileAgentDirectory({ + previous: new Map([ + ["snapshot", replica("snapshot", "running")], + ["buffered", replica("buffered", "running")], + ]), + snapshot: [entry("snapshot", "idle"), entry("buffered", "running")], + deltas: [ + { + kind: "upsert", + agent: snapshot("buffered", "idle"), + project: entry("buffered", "idle").project, + }, + { + kind: "upsert", + agent: snapshot("buffered", "idle"), + project: entry("buffered", "idle").project, + }, + ], + }); + + expect(result.stoppedRunningAgentIds).toEqual(["snapshot", "buffered"]); + expect(result.entries.map(({ agent }) => [agent.id, agent.status])).toEqual([ + ["snapshot", "idle"], + ["buffered", "idle"], + ]); + }); + + it("preserves ordered upserts and removals received after page one", () => { + const result = reconcileAgentDirectory({ + previous: new Map(), + snapshot: [entry("updated", "idle"), entry("removed", "idle")], + deltas: [ + { + kind: "upsert", + agent: { ...snapshot("updated", "idle"), title: "live" }, + project: entry("updated", "idle").project, + }, + { kind: "remove", agentId: "removed" }, + ], + }); + + expect(result.entries.map(({ agent }) => [agent.id, agent.title])).toEqual([ + ["updated", "live"], + ]); + }); + + it("keeps newer page metadata when a stale buffered upsert arrives", () => { + const snapshotEntry = entry("agent", "running"); + const staleProject = { + ...snapshotEntry.project, + projectName: "stale project", + }; + const result = reconcileAgentDirectory({ + previous: new Map([["agent", replica("agent", "running")]]), + snapshot: [ + { + ...entry("agent", "running"), + agent: { + ...snapshot("agent", "running"), + title: "newer page", + updatedAt: "2026-07-12T12:00:00.000Z", + }, + }, + ], + deltas: [ + { + kind: "upsert", + agent: { + ...snapshot("agent", "idle"), + title: "stale live", + updatedAt: "2026-07-12T11:00:00.000Z", + }, + project: staleProject, + }, + ], + }); + + expect({ + title: result.entries[0]?.agent.title, + status: result.entries[0]?.agent.status, + projectName: result.entries[0]?.project.projectName, + stopped: result.stoppedRunningAgentIds, + }).toEqual({ title: "newer page", status: "running", projectName: "repo", stopped: [] }); + }); + + it("clears a snapshot stop when a newer buffered upsert is running", () => { + const result = reconcileAgentDirectory({ + previous: new Map([["agent", replica("agent", "running")]]), + snapshot: [entry("agent", "idle")], + deltas: [ + { + kind: "upsert", + agent: { + ...snapshot("agent", "running"), + updatedAt: "2026-07-12T11:00:00.000Z", + }, + project: entry("agent", "running").project, + }, + ], + }); + + expect(result.entries[0]?.agent.status).toBe("running"); + expect(result.stoppedRunningAgentIds).toEqual([]); + }); + + it("accepts usage from a stale buffered upsert without regressing metadata", () => { + const result = reconcileAgentDirectory({ + previous: new Map(), + snapshot: [ + { + ...entry("agent", "idle"), + agent: { + ...snapshot("agent", "idle"), + title: "newer page", + updatedAt: "2026-07-12T12:00:00.000Z", + lastUsage: { inputTokens: 10, outputTokens: 5 }, + }, + }, + ], + deltas: [ + { + kind: "upsert", + agent: { + ...snapshot("agent", "running"), + title: "stale live", + updatedAt: "2026-07-12T11:00:00.000Z", + lastUsage: { inputTokens: 20, outputTokens: 8 }, + }, + project: entry("agent", "idle").project, + }, + ], + }); + + expect({ + title: result.entries[0]?.agent.title, + status: result.entries[0]?.agent.status, + usage: result.entries[0]?.agent.lastUsage, + }).toEqual({ + title: "newer page", + status: "idle", + usage: { inputTokens: 20, outputTokens: 8 }, + }); + }); + + it("preserves usage when a stale buffered upsert omits it", () => { + const result = reconcileAgentDirectory({ + previous: new Map(), + snapshot: [ + { + ...entry("agent", "idle"), + agent: { + ...snapshot("agent", "idle"), + updatedAt: "2026-07-12T12:00:00.000Z", + lastUsage: { inputTokens: 10, outputTokens: 5 }, + }, + }, + ], + deltas: [ + { + kind: "upsert", + agent: { + ...snapshot("agent", "running"), + updatedAt: "2026-07-12T11:00:00.000Z", + }, + project: entry("agent", "idle").project, + }, + ], + }); + + expect(result.entries[0]?.agent.lastUsage).toEqual({ inputTokens: 10, outputTokens: 5 }); + }); +}); diff --git a/packages/app/src/utils/agent-directory-reconciliation.ts b/packages/app/src/utils/agent-directory-reconciliation.ts new file mode 100644 index 000000000..86130f5d2 --- /dev/null +++ b/packages/app/src/utils/agent-directory-reconciliation.ts @@ -0,0 +1,62 @@ +import type { FetchAgentsEntry } from "@getpaseo/client/internal/daemon-client"; +import type { Agent } from "@/stores/session-store"; +import type { AgentDirectoryDelta } from "./agent-directory-sync"; +import { acceptAgentDirectoryUpdate } from "./agent-directory-update-policy"; + +export function reconcileAgentDirectory(input: { + previous: ReadonlyMap; + snapshot: FetchAgentsEntry[]; + deltas: readonly AgentDirectoryDelta[]; +}): { entries: FetchAgentsEntry[]; stoppedRunningAgentIds: string[] } { + const entries = new Map(input.snapshot.map((entry) => [entry.agent.id, entry])); + const statuses = new Map(Array.from(input.previous, ([id, agent]) => [id, agent.status])); + const stoppedRunningAgentIds = new Set(); + + for (const entry of input.snapshot) { + if (statuses.get(entry.agent.id) === "running" && entry.agent.status !== "running") { + stoppedRunningAgentIds.add(entry.agent.id); + } + statuses.set(entry.agent.id, entry.agent.status); + } + + for (const delta of input.deltas) { + if (delta.kind === "remove") { + entries.delete(delta.agentId); + statuses.delete(delta.agentId); + stoppedRunningAgentIds.delete(delta.agentId); + continue; + } + const previousEntry = entries.get(delta.agent.id); + const acceptedAgent = acceptAgentDirectoryUpdate(previousEntry?.agent, delta.agent); + if (acceptedAgent.status === "running") { + stoppedRunningAgentIds.delete(delta.agent.id); + } else if (statuses.get(delta.agent.id) === "running") { + stoppedRunningAgentIds.add(delta.agent.id); + } + statuses.set(delta.agent.id, acceptedAgent.status); + const previousProject = previousEntry?.project; + const acceptedProject = + acceptedAgent === delta.agent ? (delta.project ?? previousProject) : previousProject; + entries.set(delta.agent.id, { + agent: acceptedAgent, + project: acceptedProject ?? { + projectKey: delta.agent.cwd, + projectName: /[^/]+$/.exec(delta.agent.cwd)?.[0] ?? delta.agent.cwd, + checkout: { + cwd: delta.agent.cwd, + isGit: false, + currentBranch: null, + remoteUrl: null, + worktreeRoot: null, + isPaseoOwnedWorktree: false, + mainRepoRoot: null, + }, + }, + }); + } + + return { + entries: Array.from(entries.values()), + stoppedRunningAgentIds: Array.from(stoppedRunningAgentIds), + }; +} diff --git a/packages/app/src/utils/agent-directory-sync.test.ts b/packages/app/src/utils/agent-directory-sync.test.ts index d1c0d1df1..1ec16bde2 100644 --- a/packages/app/src/utils/agent-directory-sync.test.ts +++ b/packages/app/src/utils/agent-directory-sync.test.ts @@ -2,8 +2,12 @@ import { describe, expect, it } from "vitest"; import type { DaemonClient, FetchAgentsEntry } from "@getpaseo/client/internal/daemon-client"; import type { AgentSnapshotPayload } from "@getpaseo/protocol/messages"; import { PARENT_AGENT_ID_LABEL } from "@getpaseo/protocol/agent-labels"; +import type { AgentPermissionRequest } from "@getpaseo/protocol/agent-types"; import { useSessionStore } from "@/stores/session-store"; -import { replaceFetchedAgentDirectory } from "./agent-directory-sync"; +import { normalizeAgentSnapshot } from "@/utils/agent-snapshots"; +import { isAgentArchiving, setAgentArchiving } from "@/hooks/use-archive-agent"; +import { queryClient } from "@/data/query-client"; +import { applyAgentDirectoryDelta, replaceFetchedAgentDirectory } from "./agent-directory-sync"; function createAgentPayload( input: Partial> & { @@ -56,7 +60,28 @@ function createEntry(agent: AgentSnapshotPayload): FetchAgentsEntry { }; } +function permission(id: string): AgentPermissionRequest { + return { id, provider: "codex", name: id, kind: "tool", title: id }; +} + describe("replaceFetchedAgentDirectory", () => { + it("preserves timeline initialization while replacing directory state", () => { + const serverId = "server-initializing"; + const store = useSessionStore.getState(); + store.initializeSession(serverId, null as unknown as DaemonClient); + store.setInitializingAgents(serverId, new Map([["agent", true]])); + + replaceFetchedAgentDirectory({ + serverId, + entries: [createEntry(createAgentPayload({ id: "agent" }))], + }); + + expect(useSessionStore.getState().sessions[serverId]?.initializingAgents.get("agent")).toBe( + true, + ); + store.clearSession(serverId); + }); + it("re-derives parentAgentId every time an agent snapshot is ingested", () => { const serverId = "server-1"; const store = useSessionStore.getState(); @@ -92,4 +117,142 @@ describe("replaceFetchedAgentDirectory", () => { store.clearSession(serverId); }); + + it("removes every replica-owned artifact for a removed agent", () => { + const serverId = "server-removal"; + const agentId = "removed-agent"; + const store = useSessionStore.getState(); + store.initializeSession(serverId, null as unknown as DaemonClient); + const agent = { + ...normalizeAgentSnapshot(createAgentPayload({ id: agentId }), serverId), + projectPlacement: null, + }; + store.setAgents(serverId, new Map([[agentId, agent]])); + store.setAgentDetails(serverId, new Map([[agentId, agent]])); + store.setQueuedMessages( + serverId, + new Map([[agentId, [{ id: "queued", text: "next", attachments: [] }]]]), + ); + store.setAgentTimelineCursor( + serverId, + new Map([[agentId, { epoch: "epoch", startSeq: 1, endSeq: 2 }]]), + ); + store.setPendingPermissions( + serverId, + new Map([["permission", { key: "permission", agentId, request: null as never }]]), + ); + store.setInitializingAgents(serverId, new Map([[agentId, true]])); + setAgentArchiving({ queryClient, serverId, agentId, isArchiving: true }); + + applyAgentDirectoryDelta({ serverId, delta: { kind: "remove", agentId } }); + + const session = useSessionStore.getState().sessions[serverId]; + expect({ + agents: session?.agents.has(agentId), + details: session?.agentDetails.has(agentId), + queued: session?.queuedMessages.has(agentId), + cursor: session?.agentTimelineCursor.has(agentId), + permissions: session?.pendingPermissions.size, + initializing: session?.initializingAgents.has(agentId), + archivePending: isAgentArchiving({ queryClient, serverId, agentId }), + }).toEqual({ + agents: false, + details: false, + queued: false, + cursor: false, + permissions: 0, + initializing: false, + archivePending: false, + }); + + store.clearSession(serverId); + }); + + it("keeps newer metadata while accepting usage-only updates and legacy workspace ownership", () => { + const serverId = "server-usage"; + const agentId = "usage-agent"; + const store = useSessionStore.getState(); + store.initializeSession(serverId, null as unknown as DaemonClient); + store.setWorkspaces( + serverId, + new Map([ + [ + "legacy-workspace", + { + id: "legacy-workspace", + projectId: "project", + projectDisplayName: "Project", + projectRootPath: "/repo", + workspaceDirectory: "/repo", + projectKind: "git", + workspaceKind: "worktree", + name: "repo", + status: "done", + statusEnteredAt: null, + archivingAt: null, + diffStat: null, + scripts: [], + }, + ], + ]), + ); + const current = createAgentPayload({ + id: agentId, + title: "current", + status: "running", + updatedAt: "2026-07-12T11:00:00.000Z", + lastUsage: { inputTokens: 10, outputTokens: 5 }, + pendingPermissions: [permission("current-permission")], + }); + applyAgentDirectoryDelta({ + serverId, + delta: { kind: "upsert", agent: current, project: createEntry(current).project }, + }); + store.flushAgentLastActivity(); + setAgentArchiving({ queryClient, serverId, agentId, isArchiving: true }); + + const staleResult = applyAgentDirectoryDelta({ + serverId, + delta: { + kind: "upsert", + agent: { + ...current, + title: "stale", + status: "idle", + updatedAt: "2026-07-12T10:00:00.000Z", + lastUsage: { inputTokens: 20, outputTokens: 8 }, + pendingPermissions: [permission("stale-permission")], + archivedAt: "2026-07-12T10:00:00.000Z", + }, + project: createEntry(current).project, + }, + }); + store.flushAgentLastActivity(); + + const state = useSessionStore.getState(); + const agent = state.sessions[serverId]?.agents.get(agentId); + expect({ + title: agent?.title, + status: agent?.status, + usage: agent?.lastUsage, + workspaceId: agent?.workspaceId, + stoppedRunning: staleResult.stoppedRunning, + permissions: Array.from(state.sessions[serverId]?.pendingPermissions.values() ?? []).map( + ({ request }) => request.id, + ), + archivePending: isAgentArchiving({ queryClient, serverId, agentId }), + activity: state.agentLastActivity.get(agentId)?.toISOString(), + }).toEqual({ + title: "current", + status: "running", + usage: { inputTokens: 20, outputTokens: 8 }, + workspaceId: "legacy-workspace", + stoppedRunning: false, + permissions: ["current-permission"], + archivePending: true, + activity: "2026-07-12T11:00:00.000Z", + }); + + store.clearSession(serverId); + }); }); diff --git a/packages/app/src/utils/agent-directory-sync.ts b/packages/app/src/utils/agent-directory-sync.ts index 684aaf82b..24e14c420 100644 --- a/packages/app/src/utils/agent-directory-sync.ts +++ b/packages/app/src/utils/agent-directory-sync.ts @@ -2,8 +2,132 @@ import type { FetchAgentsEntry } from "@getpaseo/client/internal/daemon-client"; import { type Agent, useSessionStore } from "@/stores/session-store"; import { derivePendingPermissionKey, normalizeAgentSnapshot } from "@/utils/agent-snapshots"; import { resolveProjectPlacement } from "@/utils/project-placement"; +import type { SessionOutboundMessage } from "@getpaseo/protocol/messages"; +import { clearArchiveAgentPending } from "@/hooks/use-archive-agent"; +import { queryClient } from "@/data/query-client"; +import { acceptAgentDirectoryUpdate } from "@/utils/agent-directory-update-policy"; +import { buildDraftStoreKey } from "@/stores/draft-keys"; +import { useDraftStore } from "@/stores/draft-store"; +import { getInitDeferred, getInitKey, rejectInitDeferred } from "@/utils/agent-initialization"; type AgentDirectoryFetchEntry = FetchAgentsEntry; +export type AgentDirectoryDelta = Extract< + SessionOutboundMessage, + { type: "agent_update" } +>["payload"]; + +export function applyAgentDirectoryDelta(input: { serverId: string; delta: AgentDirectoryDelta }): { + agentId: string; + stoppedRunning: boolean; +} { + if (input.delta.kind === "remove") { + removeAgentDirectoryReplica(input.serverId, input.delta.agentId); + return { agentId: input.delta.agentId, stoppedRunning: false }; + } + return upsertAgentDirectoryReplica(input.serverId, input.delta); +} + +type AgentUpsertDelta = Extract; + +function upsertAgentDirectoryReplica( + serverId: string, + delta: AgentUpsertDelta, +): { agentId: string; stoppedRunning: boolean } { + const normalized = normalizeAgentSnapshot(delta.agent, serverId); + const session = useSessionStore.getState().sessions[serverId]; + const previousAgent = + session?.agents.get(normalized.id) ?? session?.agentDetails.get(normalized.id); + const legacyWorkspaceId = + previousAgent?.workspaceId ?? + Array.from(session?.workspaces.values() ?? []).find( + (workspace) => + session?.serverInfo?.features?.workspaceMultiplicity !== true && + workspace.workspaceDirectory === normalized.cwd, + )?.id; + const agent: Agent = { + ...normalized, + workspaceId: normalized.workspaceId ?? legacyWorkspaceId, + projectPlacement: + resolveProjectPlacement({ projectPlacement: delta.project, cwd: normalized.cwd }) ?? + previousAgent?.projectPlacement, + }; + const acceptedAgent = upsertAgentReplica(serverId, agent); + if (acceptedAgent.archivedAt) { + clearArchiveAgentPending({ queryClient, serverId, agentId: acceptedAgent.id }); + } + replaceAgentPendingPermissions(serverId, acceptedAgent); + useSessionStore.getState().setAgentLastActivity(acceptedAgent.id, acceptedAgent.lastActivityAt); + return { + agentId: acceptedAgent.id, + stoppedRunning: previousAgent?.status === "running" && acceptedAgent.status !== "running", + }; +} + +export function upsertAgentReplica(serverId: string, agent: Agent): Agent { + let acceptedAgent = agent; + useSessionStore.getState().setAgents(serverId, (current) => { + const currentAgent = current.get(agent.id); + acceptedAgent = acceptAgentDirectoryUpdate(currentAgent, agent); + if (acceptedAgent === currentAgent) return current; + const next = new Map(current); + next.set(agent.id, acceptedAgent); + return next; + }); + return acceptedAgent; +} + +export function replaceAgentPendingPermissions(serverId: string, agent: Agent): void { + const pendingPermissions = new Map( + useSessionStore.getState().sessions[serverId]?.pendingPermissions, + ); + for (const [key, pending] of pendingPermissions) { + if (pending.agentId === agent.id) pendingPermissions.delete(key); + } + for (const request of agent.pendingPermissions) { + const key = derivePendingPermissionKey(agent.id, request); + pendingPermissions.set(key, { key, agentId: agent.id, request }); + } + useSessionStore.getState().setPendingPermissions(serverId, pendingPermissions); +} + +export function removeAgentDirectoryReplica(serverId: string, agentId: string): void { + const store = useSessionStore.getState(); + clearArchiveAgentPending({ queryClient, serverId, agentId }); + const removeKey = (current: Map): Map => { + if (!current.has(agentId)) return current; + const next = new Map(current); + next.delete(agentId); + return next; + }; + store.setAgents(serverId, removeKey); + store.setAgentDetails(serverId, removeKey); + store.setQueuedMessages(serverId, removeKey); + store.setAgentTimelineCursor(serverId, removeKey); + store.setInitializingAgents(serverId, removeKey); + store.setPendingPermissions(serverId, (current) => { + const next = new Map(current); + for (const [key, pending] of next) { + if (pending.agentId === agentId) next.delete(key); + } + return next.size === current.size ? current : next; + }); + store.setAgentAuthoritativeHistoryApplied(serverId, agentId, false); + store.setAgentStreamTail(serverId, removeKey); + store.clearAgentStreamHead(serverId, agentId); + useSessionStore.setState((state) => { + if (!state.agentLastActivity.has(agentId)) return state; + const agentLastActivity = new Map(state.agentLastActivity); + agentLastActivity.delete(agentId); + return { ...state, agentLastActivity }; + }); + useDraftStore.getState().clearDraftInput({ + draftKey: buildDraftStoreKey({ serverId, agentId }), + }); + const initKey = getInitKey(serverId, agentId); + if (getInitDeferred(initKey)) { + rejectInitDeferred(initKey, new Error("Agent was removed during initialization")); + } +} interface PendingPermissionEntry { key: string; @@ -49,6 +173,12 @@ export function replaceFetchedAgentDirectory(input: { const { agents: fetchedAgents, pendingPermissions } = buildAgentDirectoryState(input); const store = useSessionStore.getState(); + for (const agent of fetchedAgents.values()) { + if (agent.archivedAt) { + clearArchiveAgentPending({ queryClient, serverId: input.serverId, agentId: agent.id }); + } + } + store.setAgents(input.serverId, fetchedAgents); store.setAgentDetails(input.serverId, (prev) => { let next: Map | null = null; @@ -69,7 +199,6 @@ export function replaceFetchedAgentDirectory(input: { store.setAgentLastActivityBatch(lastActivityByAgentId); store.setPendingPermissions(input.serverId, new Map(pendingPermissions)); - store.setInitializingAgents(input.serverId, new Map()); store.setHasHydratedAgents(input.serverId, true); return { agents: fetchedAgents }; } diff --git a/packages/app/src/utils/agent-directory-update-policy.ts b/packages/app/src/utils/agent-directory-update-policy.ts new file mode 100644 index 000000000..4d14f101b --- /dev/null +++ b/packages/app/src/utils/agent-directory-update-policy.ts @@ -0,0 +1,21 @@ +import equal from "fast-deep-equal"; +import type { AgentUsage } from "@getpaseo/protocol/agent-types"; + +interface AgentUpdateValue { + updatedAt: Date | string; + lastUsage?: AgentUsage; +} + +function timestamp(value: Date | string): number { + return value instanceof Date ? value.getTime() : Date.parse(value); +} + +export function acceptAgentDirectoryUpdate( + current: T | undefined, + incoming: T, +): T { + if (!current || timestamp(incoming.updatedAt) >= timestamp(current.updatedAt)) return incoming; + if (incoming.lastUsage === undefined) return current; + if (equal(incoming.lastUsage, current.lastUsage)) return current; + return { ...current, lastUsage: incoming.lastUsage }; +} diff --git a/packages/app/src/workspace/legacy-daemon-workspaces.ts b/packages/app/src/workspace/legacy-daemon-workspaces.ts index 23de643b3..820e37368 100644 --- a/packages/app/src/workspace/legacy-daemon-workspaces.ts +++ b/packages/app/src/workspace/legacy-daemon-workspaces.ts @@ -121,7 +121,7 @@ export async function backfillLegacyDaemonWorkspaceDirectoryIfEmpty( return true; } -async function readLegacyDaemonWorkspaceDirectory(input: { +export async function readLegacyDaemonWorkspaceDirectory(input: { client: Pick; subscribe?: FetchAgentsOptions["subscribe"]; page?: FetchAgentsOptions["page"]; @@ -193,7 +193,7 @@ export function applyLegacyDaemonWorkspaceOwnership(input: { }; } -function replaceLegacyDaemonWorkspaceDirectory(input: { +export function replaceLegacyDaemonWorkspaceDirectory(input: { serverId: string; entries: FetchAgentsEntry[]; }): LegacyDaemonWorkspaceSnapshot { @@ -242,7 +242,7 @@ function readFetchAgentsNextCursor( return null; } -function stampLegacyWorkspaceIds(entries: FetchAgentsEntry[]): FetchAgentsEntry[] { +export function stampLegacyWorkspaceIds(entries: FetchAgentsEntry[]): FetchAgentsEntry[] { return entries.map((entry) => { const workspaceId = resolveLegacyWorkspaceId(entry); return { @@ -255,7 +255,9 @@ function stampLegacyWorkspaceIds(entries: FetchAgentsEntry[]): FetchAgentsEntry[ }); } -function buildLegacyWorkspaces(entries: FetchAgentsEntry[]): Map { +export function buildLegacyWorkspaces( + entries: FetchAgentsEntry[], +): Map { const workspaces = new Map(); for (const entry of entries) { const workspaceId = entry.agent.workspaceId ?? resolveLegacyWorkspaceId(entry); diff --git a/packages/client/src/daemon-client.test.ts b/packages/client/src/daemon-client.test.ts index 3b3183862..ad41bddda 100644 --- a/packages/client/src/daemon-client.test.ts +++ b/packages/client/src/daemon-client.test.ts @@ -76,7 +76,7 @@ function createMockTransport() { return { transport, sent, - triggerOpen: (options?: { preserveSent?: boolean }) => { + triggerOpen: (options?: { preserveSent?: boolean; features?: Record }) => { onOpen(); if (!options?.preserveSent) { // Ignore HELLO handshake payloads in assertions. @@ -92,6 +92,7 @@ function createMockTransport() { serverId: `srv_test_${serverInfoOrdinal++}`, hostname: null, version: null, + ...(options?.features ? { features: options.features } : {}), }, }, }), @@ -185,6 +186,7 @@ test("does not infer browser automation capabilities from Electron runtime", asy }) .parse(JSON.parse(assertStr(mock.sent[0]))); expect(hello.capabilities[CLIENT_CAPS.browserHost]).toBeUndefined(); + expect(hello.capabilities[CLIENT_CAPS.selectiveAgentTimeline]).toBeUndefined(); }); test("advertises consumer-provided browser automation capabilities", async () => { @@ -219,6 +221,95 @@ test("advertises consumer-provided browser automation capabilities", async () => }); }); +test("sets the complete viewed timeline subscription only when the daemon supports it", async () => { + const supportedTransport = createMockTransport(); + const supportedClient = new DaemonClient({ + url: "ws://test", + clientId: "timeline_supported", + transportFactory: () => supportedTransport.transport, + reconnect: { enabled: false }, + }); + const legacyTransport = createMockTransport(); + const legacyClient = new DaemonClient({ + url: "ws://test", + clientId: "timeline_legacy", + transportFactory: () => legacyTransport.transport, + reconnect: { enabled: false }, + }); + clients.push(supportedClient, legacyClient); + + const supportedConnect = supportedClient.connect(); + supportedTransport.triggerOpen({ features: { selectiveAgentTimeline: true } }); + await supportedConnect; + const legacyConnect = legacyClient.connect(); + legacyTransport.triggerOpen(); + await legacyConnect; + + expect(supportedClient.getLastServerInfoMessage()?.features).toEqual({ + selectiveAgentTimeline: true, + }); + + const setPromise = supportedClient.setAgentTimelineSubscription(["agent-b", "agent-a"]); + await Promise.resolve(); + const request = parseSentFrame(supportedTransport.sent[0]); + supportedTransport.triggerMessage( + wrapSessionMessage({ + type: "agent.timeline.set_subscription.response", + payload: { + requestId: request.requestId, + agentIds: ["agent-a", "agent-b"], + }, + }), + ); + await setPromise; + await legacyClient.setAgentTimelineSubscription(["agent-a"]); + + expect({ request, legacyFrames: legacyTransport.sent }).toEqual({ + request: { + type: "agent.timeline.set_subscription.request", + requestId: expect.any(String), + agentIds: ["agent-a", "agent-b"], + }, + legacyFrames: [], + }); +}); + +test("normalizes legacy and dedicated agent attention notifications", async () => { + const mock = createMockTransport(); + const client = new DaemonClient({ + url: "ws://test", + clientId: "attention_normalization", + transportFactory: () => mock.transport, + reconnect: { enabled: false }, + }); + clients.push(client); + const connect = client.connect(); + mock.triggerOpen(); + await connect; + const notifications: unknown[] = []; + client.onAgentAttentionRequired((notification) => notifications.push(notification)); + const payload = { + agentId: "agent-a", + reason: "finished", + timestamp: "2026-07-12T00:00:00.000Z", + shouldNotify: true, + } as const; + + mock.triggerMessage( + wrapSessionMessage({ + type: "agent_stream", + payload: { + agentId: payload.agentId, + timestamp: payload.timestamp, + event: { type: "attention_required", provider: "codex", ...payload }, + }, + }), + ); + mock.triggerMessage(wrapSessionMessage({ type: "agent_attention_required", payload })); + + expect(notifications).toEqual([payload, payload]); +}); + const noopLogger: Logger = { debug: () => {}, info: () => {}, diff --git a/packages/client/src/daemon-client.ts b/packages/client/src/daemon-client.ts index 33a801b35..91bd350a3 100644 --- a/packages/client/src/daemon-client.ts +++ b/packages/client/src/daemon-client.ts @@ -1,5 +1,6 @@ import type { z } from "zod"; import { CLIENT_CAPS, type ClientCapability } from "@getpaseo/protocol/client-capabilities"; +import type { AgentAttentionNotificationPayload } from "@getpaseo/protocol/agent-attention-notification"; import { AgentCreateFailedStatusPayloadSchema, AgentCreatedStatusPayloadSchema, @@ -317,6 +318,14 @@ export interface SendMessageOptions { attachments?: SendAgentMessageRequest["attachments"]; } +export interface AgentAttentionRequiredNotification { + agentId: string; + reason: "finished" | "error" | "permission"; + timestamp: string; + shouldNotify: boolean; + notification?: AgentAttentionNotificationPayload; +} + type AgentConfigOverrides = Partial>; export interface CreateAgentRequestOptions extends AgentConfigOverrides { @@ -1478,6 +1487,31 @@ export class DaemonClient { }; } + onAgentAttentionRequired( + handler: (notification: AgentAttentionRequiredNotification) => void, + ): () => void { + const unsubscribeLegacy = this.on("agent_stream", (message) => { + if (message.payload.event.type !== "attention_required") { + return; + } + const event = message.payload.event; + handler({ + agentId: message.payload.agentId, + reason: event.reason, + timestamp: event.timestamp, + shouldNotify: event.shouldNotify, + ...(event.notification ? { notification: event.notification } : {}), + }); + }); + const unsubscribeDedicated = this.on("agent_attention_required", (message) => { + handler(message.payload); + }); + return () => { + unsubscribeLegacy(); + unsubscribeDedicated(); + }; + } + // ============================================================================ // Core Send Helpers // ============================================================================ @@ -2687,6 +2721,35 @@ export class DaemonClient { return payload; } + async setAgentTimelineSubscription(agentIds: string[]): Promise { + // COMPAT(selectiveAgentTimeline): added in v0.1.106. Old daemons keep their + // legacy global stream and do not understand this RPC. Remove after + // 2027-01-12 once the supported daemon floor is >= v0.1.106. + if (!this.lastServerInfoMessage?.features?.selectiveAgentTimeline) { + return; + } + + const requestId = this.createRequestId(); + const normalizedAgentIds = [...new Set(agentIds)].sort(); + const message = SessionInboundMessageSchema.parse({ + type: "agent.timeline.set_subscription.request", + agentIds: normalizedAgentIds, + requestId, + }); + + await this.sendRequest({ + requestId, + message, + options: { skipQueue: true }, + select: (response) => { + if (response.type !== "agent.timeline.set_subscription.response") { + return null; + } + return response.payload.requestId === requestId ? response.payload : null; + }, + }); + } + async buildAgentForkContext( agentId: string, options: AgentForkContextOptions = {}, diff --git a/packages/protocol/src/client-capabilities.ts b/packages/protocol/src/client-capabilities.ts index 63e01a5e4..dcf428ffd 100644 --- a/packages/protocol/src/client-capabilities.ts +++ b/packages/protocol/src/client-capabilities.ts @@ -1,4 +1,8 @@ export const CLIENT_CAPS = { + // COMPAT(selectiveAgentTimeline): added in v0.1.106. Capable clients receive + // agent streams only for their explicit viewed set. Remove after 2027-01-12 + // once the supported client floor is >= v0.1.106. + selectiveAgentTimeline: "selective_agent_timeline", reasoningMergeEnum: "reasoning_merge_enum", // COMPAT(customModeIcons): added in v0.1.84. Old clients pin AgentModeIcon to // a closed enum and crash rendering unknown values; daemon downgrades icons diff --git a/packages/protocol/src/messages.test.ts b/packages/protocol/src/messages.test.ts index dec27d458..29dd7d550 100644 --- a/packages/protocol/src/messages.test.ts +++ b/packages/protocol/src/messages.test.ts @@ -437,3 +437,35 @@ describe("daemon update messages", () => { }); }); }); + +describe("viewed timeline subscription messages", () => { + test("parses a complete viewed-agent set and its acknowledgement", () => { + const request = SessionInboundMessageSchema.parse({ + type: "agent.timeline.set_subscription.request", + agentIds: ["agent-a", "agent-b"], + requestId: "timeline-subscription-1", + }); + const response = SessionOutboundMessageSchema.parse({ + type: "agent.timeline.set_subscription.response", + payload: { + agentIds: ["agent-a", "agent-b"], + requestId: "timeline-subscription-1", + }, + }); + + expect({ request, response }).toEqual({ + request: { + type: "agent.timeline.set_subscription.request", + agentIds: ["agent-a", "agent-b"], + requestId: "timeline-subscription-1", + }, + response: { + type: "agent.timeline.set_subscription.response", + payload: { + agentIds: ["agent-a", "agent-b"], + requestId: "timeline-subscription-1", + }, + }, + }); + }); +}); diff --git a/packages/protocol/src/messages.ts b/packages/protocol/src/messages.ts index cf01fc15d..5d3320589 100644 --- a/packages/protocol/src/messages.ts +++ b/packages/protocol/src/messages.ts @@ -1369,6 +1369,12 @@ export const ProviderSubagentTimelineRequestMessageSchema = z.object({ limit: z.number().int().nonnegative().optional(), }); +export const SetAgentTimelineSubscriptionRequestMessageSchema = z.object({ + type: z.literal("agent.timeline.set_subscription.request"), + agentIds: z.array(z.string()), + requestId: z.string(), +}); + export const AgentForkContextRequestMessageSchema = z.object({ type: z.literal("agent.fork_context.request"), agentId: z.string(), @@ -2359,6 +2365,7 @@ export const SessionInboundMessageSchema = z.discriminatedUnion("type", [ FetchAgentTimelineRequestMessageSchema, ProviderSubagentListRequestMessageSchema, ProviderSubagentTimelineRequestMessageSchema, + SetAgentTimelineSubscriptionRequestMessageSchema, AgentForkContextRequestMessageSchema, SetAgentModeRequestMessageSchema, SetAgentModelRequestMessageSchema, @@ -2678,6 +2685,8 @@ export const ServerInfoStatusPayloadSchema = z // Daemon advertises pluggable non-GitHub forge support (the forge registry); // the client gates non-GitHub setup UI on it. forgeProviders: z.boolean().optional(), + // COMPAT(selectiveAgentTimeline): added in v0.1.106, remove after 2027-01-12. + selectiveAgentTimeline: z.boolean().optional(), }) .optional(), }) @@ -3425,6 +3434,35 @@ export const ProviderSubagentUpdateMessageSchema = z.object({ ]), }); +export const SetAgentTimelineSubscriptionResponseMessageSchema = z.object({ + type: z.literal("agent.timeline.set_subscription.response"), + payload: z.object({ + agentIds: z.array(z.string()), + requestId: z.string(), + }), +}); + +export const AgentAttentionRequiredMessageSchema = z.object({ + type: z.literal("agent_attention_required"), + payload: z.object({ + agentId: z.string(), + reason: z.enum(["finished", "error", "permission"]), + timestamp: z.string(), + shouldNotify: z.boolean(), + notification: z + .object({ + title: z.string(), + body: z.string(), + data: z.object({ + serverId: z.string(), + agentId: z.string(), + reason: z.enum(["finished", "error", "permission"]), + }), + }) + .optional(), + }), +}); + export const AgentForkContextResponseMessageSchema = z.object({ type: z.literal("agent.fork_context.response"), payload: z.object({ @@ -4832,6 +4870,8 @@ export const SessionOutboundMessageSchema = z.discriminatedUnion("type", [ ProviderSubagentListResponseMessageSchema, ProviderSubagentTimelineResponseMessageSchema, ProviderSubagentUpdateMessageSchema, + SetAgentTimelineSubscriptionResponseMessageSchema, + AgentAttentionRequiredMessageSchema, AgentForkContextResponseMessageSchema, CancelAgentResponseMessageSchema, ClearAgentAttentionResponseMessageSchema, @@ -5365,6 +5405,7 @@ export const WSHelloMessageSchema = z.object({ voice: z.boolean().optional(), pushNotifications: z.boolean().optional(), [CLIENT_CAPS.reasoningMergeEnum]: z.boolean().optional(), + [CLIENT_CAPS.selectiveAgentTimeline]: z.boolean().optional(), [CLIENT_CAPS.customModeIcons]: z.boolean().optional(), [CLIENT_CAPS.terminalReflowableSnapshot]: z.boolean().optional(), [CLIENT_CAPS.providerSubagents]: z.boolean().optional(), diff --git a/packages/server/src/server/agent/providers/opencode-agent.test.ts b/packages/server/src/server/agent/providers/opencode-agent.test.ts index 2cccfcbee..829422ea7 100644 --- a/packages/server/src/server/agent/providers/opencode-agent.test.ts +++ b/packages/server/src/server/agent/providers/opencode-agent.test.ts @@ -3033,11 +3033,11 @@ describe("OpenCode provider subagent contract", () => { event: { type: "timeline", id: "ses_child_with_history", - item: { + item: expect.objectContaining({ type: "assistant_message", text: "Persisted child result.", messageId: "msg_child_history", - }, + }), timestamp: "1970-01-01T00:00:02.000Z", }, }), @@ -3111,11 +3111,11 @@ describe("OpenCode provider subagent contract", () => { event: expect.objectContaining({ type: "timeline", id: "ses_child_hydrating", - item: { + item: expect.objectContaining({ type: "assistant_message", text: "Hydration did not lose this.", messageId: "msg_child_hydrating", - }, + }), }), }), ), diff --git a/packages/server/src/server/selective-timeline-delivery.e2e.test.ts b/packages/server/src/server/selective-timeline-delivery.e2e.test.ts new file mode 100644 index 000000000..8933def32 --- /dev/null +++ b/packages/server/src/server/selective-timeline-delivery.e2e.test.ts @@ -0,0 +1,287 @@ +import { afterEach, beforeEach, expect, test } from "vitest"; +import { CLIENT_CAPS } from "@getpaseo/protocol/client-capabilities"; +import type { SessionOutboundMessage } from "@getpaseo/protocol/messages"; +import { DaemonClient } from "./test-utils/daemon-client.js"; +import { createTestPaseoDaemon, type TestPaseoDaemon } from "./test-utils/paseo-daemon.js"; + +interface MessageWaiter { + predicate(message: SessionOutboundMessage): boolean; + resolve(message: SessionOutboundMessage): void; + reject(error: Error): void; + timeout: ReturnType; +} + +class ConnectedClient { + readonly messages: SessionOutboundMessage[] = []; + private readonly waiters: MessageWaiter[] = []; + private readonly unsubscribe: () => void; + + constructor(readonly client: DaemonClient) { + this.unsubscribe = client.subscribeRawMessages((message) => { + this.messages.push(message); + for (let waiterIndex = this.waiters.length - 1; waiterIndex >= 0; waiterIndex -= 1) { + const waiter = this.waiters[waiterIndex]; + if (!waiter.predicate(message)) continue; + clearTimeout(waiter.timeout); + this.waiters.splice(waiterIndex, 1); + waiter.resolve(message); + } + }); + } + + clear(): void { + this.messages.length = 0; + } + + next( + predicate: (message: SessionOutboundMessage) => boolean, + description: string, + ): Promise { + const existing = this.messages.find(predicate); + if (existing) return Promise.resolve(existing); + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + const index = this.waiters.findIndex((waiter) => waiter.resolve === resolve); + if (index >= 0) this.waiters.splice(index, 1); + reject(new Error(`Timed out waiting for ${description}`)); + }, 5_000); + this.waiters.push({ predicate, resolve, reject, timeout }); + }); + } + + hasTimeline(agentId: string): boolean { + return this.messages.some( + (message) => message.type === "agent_stream" && message.payload.agentId === agentId, + ); + } + + async barrier(label: string): Promise { + await this.client.ping({ requestId: `barrier-${label}` }); + } + + close(): void { + this.unsubscribe(); + for (const waiter of this.waiters) { + clearTimeout(waiter.timeout); + waiter.reject(new Error("Client boundary closed")); + } + this.waiters.length = 0; + } +} + +function isAgentStream(agentId: string) { + return (message: SessionOutboundMessage): boolean => + message.type === "agent_stream" && message.payload.agentId === agentId; +} + +function isDedicatedAttention(agentId: string) { + return (message: SessionOutboundMessage): boolean => + message.type === "agent_attention_required" && message.payload.agentId === agentId; +} + +function isLegacyAttention(agentId: string) { + return (message: SessionOutboundMessage): boolean => + message.type === "agent_stream" && + message.payload.agentId === agentId && + message.payload.event.type === "attention_required"; +} + +function dedicatedAttentionResult(message: SessionOutboundMessage, timelineLeaked: boolean) { + if (message.type !== "agent_attention_required") { + throw new Error(`Expected agent_attention_required, received ${message.type}`); + } + return { + type: message.type, + shouldNotify: message.payload.shouldNotify, + timelineLeaked, + }; +} + +function legacyAttentionResult(message: SessionOutboundMessage) { + if (message.type !== "agent_stream" || message.payload.event.type !== "attention_required") { + throw new Error(`Expected legacy attention_required agent_stream, received ${message.type}`); + } + return { + type: message.type, + eventType: message.payload.event.type, + agentId: message.payload.agentId, + }; +} + +let daemon: TestPaseoDaemon; +const clients: ConnectedClient[] = []; + +beforeEach(async () => { + daemon = await createTestPaseoDaemon(); +}); + +afterEach(async () => { + for (const connected of clients) { + connected.close(); + await connected.client.close().catch(() => undefined); + } + clients.length = 0; + await daemon.close(); +}, 30_000); + +async function connect(input: { clientId: string; selective: boolean }): Promise { + const client = new DaemonClient({ + url: `ws://127.0.0.1:${daemon.port}/ws`, + clientId: input.clientId, + capabilities: { [CLIENT_CAPS.selectiveAgentTimeline]: input.selective }, + reconnect: { enabled: false }, + }); + await client.connect(); + const connected = new ConnectedClient(client); + clients.push(connected); + return connected; +} + +test("subscription acknowledgements stay on the requesting socket of a retained session", async () => { + const legacy = await connect({ clientId: "shared-client", selective: false }); + const capable = await connect({ clientId: "shared-client", selective: true }); + legacy.clear(); + capable.clear(); + + await capable.client.setAgentTimelineSubscription(["agent-a"]); + await capable.barrier("targeted-subscription-ack"); + + expect( + legacy.messages.some((message) => message.type === "agent.timeline.set_subscription.response"), + ).toBe(false); +}); + +test("real WebSocket sessions enforce selective delivery, retained resets, downgrade, and dedicated attention", async () => { + const legacy = await connect({ clientId: "legacy-client", selective: false }); + let capable = await connect({ clientId: "capable-client", selective: true }); + const agents = await Promise.all( + ["A", "B", "C"].map((title) => + legacy.client.createAgent({ + provider: "codex", + cwd: "/tmp", + title: `Selective ${title}`, + modeId: "full-access", + }), + ), + ); + const [agentA, agentB, agentC] = agents; + legacy.clear(); + capable.clear(); + + await daemon.daemon.agentManager.emitLiveTimelineItem(agentC.id, { + type: "assistant_message", + text: "before membership", + }); + await legacy.next(isAgentStream(agentC.id), "legacy global delivery before membership"); + await capable.barrier("before-membership"); + expect(capable.hasTimeline(agentC.id)).toBe(false); + + await capable.client.setAgentTimelineSubscription([agentA.id, agentB.id]); + legacy.clear(); + capable.clear(); + await daemon.daemon.agentManager.emitLiveTimelineItem(agentA.id, { + type: "assistant_message", + text: "viewed A", + }); + await daemon.daemon.agentManager.emitLiveTimelineItem(agentB.id, { + type: "assistant_message", + text: "viewed B", + }); + await daemon.daemon.agentManager.emitLiveTimelineItem(agentC.id, { + type: "assistant_message", + text: "unviewed C", + }); + await Promise.all([ + capable.next(isAgentStream(agentA.id), "capable A delivery"), + capable.next(isAgentStream(agentB.id), "capable B delivery"), + legacy.next(isAgentStream(agentC.id), "legacy C delivery"), + ]); + await capable.barrier("unviewed-c"); + expect(capable.hasTimeline(agentC.id)).toBe(false); + + await capable.client.setAgentTimelineSubscription([agentB.id]); + legacy.clear(); + capable.clear(); + await daemon.daemon.agentManager.emitLiveTimelineItem(agentA.id, { + type: "assistant_message", + text: "removed A", + }); + await daemon.daemon.agentManager.emitLiveTimelineItem(agentB.id, { + type: "assistant_message", + text: "retained B", + }); + await Promise.all([ + legacy.next(isAgentStream(agentA.id), "legacy removed A delivery"), + capable.next(isAgentStream(agentB.id), "capable retained B delivery"), + ]); + await capable.barrier("removed-a"); + expect(capable.hasTimeline(agentA.id)).toBe(false); + + capable.close(); + await capable.client.close(); + clients.splice(clients.indexOf(capable), 1); + capable = await connect({ clientId: "capable-client", selective: true }); + legacy.clear(); + capable.clear(); + await daemon.daemon.agentManager.emitLiveTimelineItem(agentB.id, { + type: "assistant_message", + text: "after capable resume", + }); + await legacy.next(isAgentStream(agentB.id), "legacy delivery after capable resume"); + await capable.barrier("resumed-membership-reset"); + expect(capable.hasTimeline(agentB.id)).toBe(false); + + capable.client.sendHeartbeat({ + deviceType: "mobile", + focusedAgentId: null, + lastActivityAt: new Date().toISOString(), + appVisible: true, + }); + legacy.clear(); + capable.clear(); + const attention = capable.next( + isDedicatedAttention(agentC.id), + "capable dedicated attention notification", + ); + const legacyAttention = legacy.next( + isLegacyAttention(agentC.id), + "legacy attention stream notification", + ); + await legacy.client.sendMessage(agentC.id, "finish attention boundary test"); + const [attentionMessage, legacyAttentionMessage] = await Promise.all([ + attention, + legacyAttention, + ]); + await capable.barrier("attention-delivery"); + expect({ + capable: dedicatedAttentionResult(attentionMessage, capable.hasTimeline(agentC.id)), + legacy: legacyAttentionResult(legacyAttentionMessage), + }).toEqual({ + capable: { + type: "agent_attention_required", + shouldNotify: true, + timelineLeaked: false, + }, + legacy: { + type: "agent_stream", + eventType: "attention_required", + agentId: agentC.id, + }, + }); + + capable.close(); + await capable.client.close(); + clients.splice(clients.indexOf(capable), 1); + const downgraded = await connect({ clientId: "capable-client", selective: false }); + downgraded.clear(); + await daemon.daemon.agentManager.emitLiveTimelineItem(agentA.id, { + type: "assistant_message", + text: "after downgrade", + }); + const downgradedDelivery = await downgraded.next( + isAgentStream(agentA.id), + "legacy global delivery after capability downgrade", + ); + + expect(downgradedDelivery.type).toBe("agent_stream"); +}, 30_000); diff --git a/packages/server/src/server/session.test.ts b/packages/server/src/server/session.test.ts index c0fcff4bb..9f05447e8 100644 --- a/packages/server/src/server/session.test.ts +++ b/packages/server/src/server/session.test.ts @@ -10,6 +10,7 @@ import { assertPullRequestAutoMergeEnableReady, } from "../services/github-service.js"; import { PARENT_AGENT_ID_LABEL } from "@getpaseo/protocol/agent-labels"; +import { CLIENT_CAPS } from "@getpaseo/protocol/client-capabilities"; import type { WorkspaceDescriptorPayload } from "@getpaseo/protocol/messages"; import { decodeFileTransferFrame, @@ -21,6 +22,7 @@ import { Session } from "./session.js"; import { DownloadTokenStore } from "./file-download/token-store.js"; import { StructuredAgentFallbackError } from "./agent/agent-response-loop.js"; import type { StoredAgentRecord } from "./agent/agent-storage.js"; +import type { AgentManagerEvent } from "./agent/agent-manager.js"; import type { ProviderSnapshotManager } from "./agent/provider-snapshot-manager.js"; import type { SessionOptions } from "./session.js"; import type { SessionInboundMessage, SessionOutboundMessage } from "./messages.js"; @@ -309,6 +311,7 @@ interface SessionForTestOptions { daemonRuntimeConfig?: SessionOptions["daemonRuntimeConfig"]; downloadTokenStore?: SessionOptions["downloadTokenStore"]; messages?: unknown[]; + targetedMessages?: Array<{ source: object; message: SessionOutboundMessage }>; binaryMessages?: Uint8Array[]; } @@ -346,6 +349,12 @@ function createSessionForTest(options: SessionForTestOptions = {}): Session { return new Session({ clientId: "test-client", onMessage: (message) => messages.push(message), + ...(options.targetedMessages + ? { + onMessageToSource: (source: object, message: SessionOutboundMessage) => + options.targetedMessages?.push({ source, message }), + } + : {}), onBinaryMessage: createBinaryMessageHandler(options.binaryMessages), logger, downloadTokenStore: options.downloadTokenStore ?? asDownloadTokenStore(), @@ -4554,6 +4563,216 @@ describe("chat/schedule/loop dispatch routing (behavior preservation)", () => { }); }); +test("replaces a capable session's complete viewed timeline set", async () => { + const messages: SessionOutboundMessage[] = []; + const session = createSessionForTest({ messages }); + session.updateClientCapabilities({ selective_agent_timeline: true }); + + await session.handleMessage({ + type: "agent.timeline.set_subscription.request", + agentIds: ["agent-b", "agent-a", "agent-a"], + requestId: "timeline-subscription-1", + }); + + expect(messages).toEqual([ + { + type: "agent.timeline.set_subscription.response", + payload: { + agentIds: ["agent-a", "agent-b"], + requestId: "timeline-subscription-1", + }, + }, + ]); +}); + +test("acknowledges a timeline subscription only to its socket source", async () => { + const messages: SessionOutboundMessage[] = []; + const targetedMessages: Array<{ source: object; message: SessionOutboundMessage }> = []; + const session = createSessionForTest({ messages, targetedMessages }); + const capableSocket = {}; + session.updateClientCapabilities({ selective_agent_timeline: true }, capableSocket); + + await session.handleMessage( + { + type: "agent.timeline.set_subscription.request", + agentIds: ["agent-a"], + requestId: "timeline-subscription-targeted", + }, + capableSocket, + ); + + expect(messages).toEqual([]); + expect(targetedMessages).toEqual([ + { + source: capableSocket, + message: { + type: "agent.timeline.set_subscription.response", + payload: { + agentIds: ["agent-a"], + requestId: "timeline-subscription-targeted", + }, + }, + }, + ]); +}); + +test("unions viewed timelines across socket sources and removes detached sources", async () => { + const messages: SessionOutboundMessage[] = []; + const agentEventListeners: Array<(event: AgentManagerEvent) => void> = []; + const session = createSessionForTest({ + messages, + agentManager: { + subscribe: vi.fn((listener: (event: AgentManagerEvent) => void) => { + agentEventListeners.push(listener); + return () => {}; + }), + }, + }); + session.updateClientCapabilities({ selective_agent_timeline: true }); + const firstSocket = {}; + const secondSocket = {}; + session.updateClientCapabilities({ selective_agent_timeline: true }, firstSocket); + session.updateClientCapabilities({ selective_agent_timeline: true }, secondSocket); + + await session.handleMessage( + { + type: "agent.timeline.set_subscription.request", + agentIds: ["agent-a"], + requestId: "timeline-subscription-a", + }, + firstSocket, + ); + await session.handleMessage( + { + type: "agent.timeline.set_subscription.request", + agentIds: ["agent-b"], + requestId: "timeline-subscription-b", + }, + secondSocket, + ); + messages.length = 0; + + if (agentEventListeners.length === 0) throw new Error("Agent event listener was not installed"); + const forward = (event: AgentManagerEvent) => { + for (const listener of agentEventListeners) listener(event); + }; + forward({ + type: "agent_stream", + agentId: "agent-a", + event: { + type: "timeline", + provider: "mock", + item: { type: "assistant_message", messageId: "message-a", text: "A" }, + }, + }); + forward({ + type: "agent_stream", + agentId: "agent-b", + event: { + type: "timeline", + provider: "mock", + item: { type: "assistant_message", messageId: "message-b", text: "B" }, + }, + }); + expect(messages.filter((message) => message.type === "agent_stream")).toHaveLength(2); + + const legacySocket = {}; + session.updateClientCapabilities(null, legacySocket); + expect(session.supportsForSource(CLIENT_CAPS.selectiveAgentTimeline, legacySocket)).toBe(false); + expect(session.supportsForSource(CLIENT_CAPS.selectiveAgentTimeline, firstSocket)).toBe(true); + messages.length = 0; + forward({ + type: "agent_stream", + agentId: "agent-not-viewed", + event: { + type: "timeline", + provider: "mock", + item: { type: "assistant_message", messageId: "message-legacy", text: "legacy" }, + }, + }); + expect(messages.some((message) => message.type === "agent_stream")).toBe(true); + + session.clearAgentTimelineSubscription(legacySocket); + + session.clearAgentTimelineSubscription(firstSocket); + messages.length = 0; + forward({ + type: "agent_stream", + agentId: "agent-a", + event: { + type: "timeline", + provider: "mock", + item: { type: "assistant_message", messageId: "message-a-2", text: "detached A" }, + }, + }); + forward({ + type: "agent_stream", + agentId: "agent-b", + event: { + type: "timeline", + provider: "mock", + item: { type: "assistant_message", messageId: "message-b-2", text: "retained B" }, + }, + }); + expect( + messages.flatMap((message) => + message.type === "agent_stream" ? [message.payload.agentId] : [], + ), + ).toEqual(["agent-b"]); +}); + +test("keeps selective delivery scoped per socket when a retained session also has a legacy socket", async () => { + const messages: SessionOutboundMessage[] = []; + const targetedMessages: Array<{ source: object; message: SessionOutboundMessage }> = []; + const agentEventListeners: Array<(event: AgentManagerEvent) => void> = []; + const session = createSessionForTest({ + messages, + targetedMessages, + agentManager: { + subscribe: vi.fn((listener: (event: AgentManagerEvent) => void) => { + agentEventListeners.push(listener); + return () => {}; + }), + }, + }); + const legacySocket = {}; + const selectiveSocket = {}; + session.updateClientCapabilities(null, legacySocket); + session.updateClientCapabilities({ selective_agent_timeline: true }, selectiveSocket); + await session.handleMessage( + { + type: "agent.timeline.set_subscription.request", + agentIds: ["viewed-agent"], + requestId: "timeline-subscription-selective", + }, + selectiveSocket, + ); + targetedMessages.length = 0; + + const listener = agentEventListeners[0]; + if (!listener) throw new Error("Agent event listener was not installed"); + listener({ + type: "agent_stream", + agentId: "not-viewed-agent", + event: { + type: "timeline", + provider: "mock", + item: { type: "assistant_message", messageId: "message-global", text: "global" }, + }, + }); + + expect(messages).toEqual([]); + expect(targetedMessages).toEqual([ + { + source: legacySocket, + message: expect.objectContaining({ + type: "agent_stream", + payload: expect.objectContaining({ agentId: "not-viewed-agent" }), + }), + }, + ]); +}); + describe("agent config setters", () => { test("set_agent_mode_request: success emits accepted response carrying the notice", async () => { const messages: SessionOutboundMessage[] = []; diff --git a/packages/server/src/server/session.ts b/packages/server/src/server/session.ts index 2416c77e3..ee010eb29 100644 --- a/packages/server/src/server/session.ts +++ b/packages/server/src/server/session.ts @@ -429,6 +429,7 @@ export interface SessionOptions { appVersion?: string | null; clientCapabilities?: Record | null; onMessage: (msg: SessionOutboundMessage) => void; + onMessageToSource?: (source: object, msg: SessionOutboundMessage) => void; onBinaryMessage?: (frame: Uint8Array) => void; getTransportBufferedAmount?: () => number | null; onLifecycleIntent?: (intent: SessionLifecycleIntent) => void; @@ -558,6 +559,9 @@ export class Session { private clientCapabilities: ReadonlySet; private readonly sessionId: string; private readonly onMessage: (msg: SessionOutboundMessage) => void; + private readonly onMessageToSource: + | ((source: object, msg: SessionOutboundMessage) => void) + | null; private readonly onBinaryMessage: ((frame: Uint8Array) => void) | null; private readonly getTransportBufferedAmount: () => number | null; private readonly onLifecycleIntent: ((intent: SessionLifecycleIntent) => void) | null; @@ -583,6 +587,10 @@ export class Session { private readonly daemonConfigStore: DaemonConfigStore; private readonly pushTokenStore: PushTokenStore; private unsubscribeAgentEvents: (() => void) | null = null; + private viewedTimelineAgentIds = new Set(); + private readonly viewedTimelineAgentIdsBySource = new Map>(); + private readonly selectiveTimelineCapabilityBySource = new Map(); + private readonly defaultTimelineSubscriptionSource = {}; private unsubscribeTerminalWorkspaceContributionEvents: (() => void) | null = null; private readonly agentUpdates: AgentUpdatesService; private workspaceUpdatesSubscription: WorkspaceUpdatesSubscriptionState | null = null; @@ -625,6 +633,7 @@ export class Session { appVersion, clientCapabilities, onMessage, + onMessageToSource, onBinaryMessage, getTransportBufferedAmount, onLifecycleIntent, @@ -675,6 +684,7 @@ export class Session { this.clientCapabilities = parseClientCapabilities(clientCapabilities); this.sessionId = uuidv4(); this.onMessage = onMessage; + this.onMessageToSource = onMessageToSource ?? null; this.onBinaryMessage = onBinaryMessage ?? null; this.getTransportBufferedAmount = getTransportBufferedAmount ?? (() => 0); this.onLifecycleIntent = onLifecycleIntent ?? null; @@ -963,14 +973,118 @@ export class Session { } } - updateClientCapabilities(capabilities: Record | null): void { + updateClientCapabilities(capabilities: Record | null, source?: object): void { this.clientCapabilities = parseClientCapabilities(capabilities); + if (source) { + this.selectiveTimelineCapabilityBySource.set( + source, + this.supports(CLIENT_CAPS.selectiveAgentTimeline), + ); + } + if (!source && !this.supports(CLIENT_CAPS.selectiveAgentTimeline)) { + this.viewedTimelineAgentIdsBySource.clear(); + this.viewedTimelineAgentIds.clear(); + } + } + + clearAgentTimelineSubscription(source: object): void { + this.selectiveTimelineCapabilityBySource.delete(source); + if (this.viewedTimelineAgentIdsBySource.delete(source)) { + this.rebuildViewedTimelineAgentIds(); + } + } + + private replaceAgentTimelineSubscription(source: object | undefined, agentIds: string[]): void { + const subscriptionSource = source ?? this.defaultTimelineSubscriptionSource; + if (agentIds.length === 0) this.viewedTimelineAgentIdsBySource.delete(subscriptionSource); + else this.viewedTimelineAgentIdsBySource.set(subscriptionSource, new Set(agentIds)); + this.rebuildViewedTimelineAgentIds(); + } + + private rebuildViewedTimelineAgentIds(): void { + const viewedAgentIds = new Set(); + for (const agentIds of this.viewedTimelineAgentIdsBySource.values()) { + for (const agentId of agentIds) viewedAgentIds.add(agentId); + } + this.viewedTimelineAgentIds = viewedAgentIds; + } + + private usesSelectiveTimelineDelivery(): boolean { + if (this.selectiveTimelineCapabilityBySource.size === 0) { + return this.supports(CLIENT_CAPS.selectiveAgentTimeline); + } + for (const capable of this.selectiveTimelineCapabilityBySource.values()) { + if (!capable) return false; + } + return true; + } + + private forwardAgentStream( + event: Extract, + serializedEvent: Extract["payload"]["event"], + ): void { + if (this.selectiveTimelineCapabilityBySource.size === 0 || !this.onMessageToSource) { + if (this.usesSelectiveTimelineDelivery() && serializedEvent.type === "attention_required") { + this.emit({ + type: "agent_attention_required", + payload: { + agentId: event.agentId, + reason: serializedEvent.reason, + timestamp: serializedEvent.timestamp, + shouldNotify: serializedEvent.shouldNotify, + ...(serializedEvent.notification ? { notification: serializedEvent.notification } : {}), + }, + }); + } else if ( + !this.usesSelectiveTimelineDelivery() || + this.viewedTimelineAgentIds.has(event.agentId) + ) { + this.emit({ + type: "agent_stream", + payload: this.buildAgentStreamPayload(event, serializedEvent), + }); + } + return; + } + + for (const [source, supportsSelectiveDelivery] of this.selectiveTimelineCapabilityBySource) { + if (supportsSelectiveDelivery && serializedEvent.type === "attention_required") { + this.onMessageToSource(source, { + type: "agent_attention_required", + payload: { + agentId: event.agentId, + reason: serializedEvent.reason, + timestamp: serializedEvent.timestamp, + shouldNotify: serializedEvent.shouldNotify, + ...(serializedEvent.notification ? { notification: serializedEvent.notification } : {}), + }, + }); + continue; + } + if ( + supportsSelectiveDelivery && + !this.viewedTimelineAgentIdsBySource.get(source)?.has(event.agentId) + ) { + continue; + } + this.onMessageToSource(source, { + type: "agent_stream", + payload: this.buildAgentStreamPayload(event, serializedEvent), + }); + } } supports(capability: ClientCapability): boolean { return this.clientCapabilities.has(capability); } + supportsForSource(capability: ClientCapability, source: object): boolean { + if (capability === CLIENT_CAPS.selectiveAgentTimeline) { + return this.selectiveTimelineCapabilityBySource.get(source) ?? this.supports(capability); + } + return this.supports(capability); + } + async syncWorkspaceGitObserverForWorkspace(workspace: PersistedWorkspaceRecord): Promise { await this.workspaceGitObserver.syncObserverForWorkspace(workspace); } @@ -979,6 +1093,16 @@ export class Session { await this.emitWorkspaceUpdatesForWorkspaceIds([workspaceId], { skipReconcile: true }); } + private async emitCreatedWorkspaceUpdate(workspace: WorkspaceDescriptorPayload): Promise { + if (this.workspaceUpdatesSubscription) { + await this.emitWorkspaceUpdateForWorkspaceId(workspace.id); + return; + } + // COMPAT(workspaceCreateCausalUpdate): added in v0.1.106, remove after 2027-01-12. + // Older clients create before subscribing and require the causal update beside the response. + this.emit({ type: "workspace_update", payload: { kind: "upsert", workspace } }); + } + async archiveWorkspaceRecordForExternalMutation(workspaceId: string): Promise { await this.archiveWorkspaceRecord(workspaceId); } @@ -1271,10 +1395,7 @@ export class Session { "agent.session.forward_stream", ); - this.emit({ - type: "agent_stream", - payload: this.buildAgentStreamPayload(event, serializedEvent), - }); + this.forwardAgentStream(event, serializedEvent); if (event.event.type === "permission_requested") { this.emit({ @@ -1377,7 +1498,7 @@ export class Session { /** * Main entry point for processing session messages */ - public async handleMessage(msg: SessionInboundMessage): Promise { + public async handleMessage(msg: SessionInboundMessage, source?: object): Promise { this.inflightRequests++; if (this.inflightRequests > this.peakInflightRequests) { this.peakInflightRequests = this.inflightRequests; @@ -1391,7 +1512,7 @@ export class Session { "agent.session.inbound", ); try { - await this.dispatchInboundMessage(msg); + await this.dispatchInboundMessage(msg, source); } catch (error) { const err = error instanceof Error ? error : new Error(String(error)); this.sessionLogger.error({ err }, "Error handling message"); @@ -1429,12 +1550,12 @@ export class Session { } } - private async dispatchInboundMessage(msg: SessionInboundMessage): Promise { + private async dispatchInboundMessage(msg: SessionInboundMessage, source?: object): Promise { const promise = this.dispatchVoiceAndControlMessage(msg) ?? this.dispatchAgentRewindMessage(msg) ?? this.dispatchAgentRelationshipMessage(msg) ?? - this.dispatchAgentTimelineMessage(msg) ?? + this.dispatchAgentTimelineMessage(msg, source) ?? this.dispatchAgentLifecycleMessage(msg) ?? this.dispatchAgentConfigMessage(msg) ?? this.dispatchCheckoutMessage(msg) ?? @@ -1516,7 +1637,10 @@ export class Session { } } - private dispatchAgentTimelineMessage(msg: SessionInboundMessage): Promise | undefined { + private dispatchAgentTimelineMessage( + msg: SessionInboundMessage, + source?: object, + ): Promise | undefined { switch (msg.type) { case "fetch_agent_timeline_request": return this.handleFetchAgentTimelineRequest(msg); @@ -1524,6 +1648,24 @@ export class Session { return this.handleProviderSubagentListRequest(msg); case "agent.provider_subagents.timeline.get.request": return this.handleProviderSubagentTimelineRequest(msg); + case "agent.timeline.set_subscription.request": { + const agentIds = [...new Set(msg.agentIds)].sort(); + if ( + source + ? (this.selectiveTimelineCapabilityBySource.get(source) ?? + this.supports(CLIENT_CAPS.selectiveAgentTimeline)) + : this.supports(CLIENT_CAPS.selectiveAgentTimeline) + ) { + this.replaceAgentTimelineSubscription(source, agentIds); + } + const response: SessionOutboundMessage = { + type: "agent.timeline.set_subscription.response", + payload: { agentIds, requestId: msg.requestId }, + }; + if (source && this.onMessageToSource) this.onMessageToSource(source, response); + else this.emit(response); + return undefined; + } case "agent.fork_context.request": return this.handleAgentForkContextRequest(msg); default: @@ -3967,6 +4109,8 @@ export class Session { continue; } } + const workspaceId = payload.kind === "upsert" ? payload.workspace.id : payload.id; + subscription.lastEmittedByWorkspaceId.set(workspaceId, payload); this.emit({ type: "workspace_update", payload, @@ -4259,6 +4403,9 @@ export class Session { this.workspaceGitObserver.recordDescriptorState(workspaceId, nextWorkspace); if (!nextWorkspace) { + if (workspace && !subscription.lastEmittedByWorkspaceId.has(workspaceId)) { + continue; + } subscription.lastEmittedByWorkspaceId.delete(workspaceId); this.bufferOrEmitWorkspaceUpdate( subscription, @@ -4515,6 +4662,7 @@ export class Session { "fetch_workspaces_response_ready", ); const snapshot = this.buildBootstrapSnapshot(payload.entries); + this.seedWorkspaceSubscriptionSnapshot(subscriptionId, request.filter, payload.entries); this.emit({ type: "fetch_workspaces_response", @@ -4575,6 +4723,23 @@ export class Session { return { snapshotByWorkspaceId }; } + private seedWorkspaceSubscriptionSnapshot( + subscriptionId: string | null, + filter: FetchWorkspacesRequestFilter | undefined, + entries: FetchWorkspacesResponseEntry[], + ): void { + const subscription = this.workspaceUpdatesSubscription; + if (!subscription) return; + if (subscriptionId && subscription.subscriptionId !== subscriptionId) return; + if (!subscriptionId && !equal(subscription.filter, filter)) return; + for (const entry of entries) { + subscription.lastEmittedByWorkspaceId.set(entry.id, { + kind: "upsert", + workspace: entry, + }); + } + } + private async registerWorkspaceForImportedAgent( workspace: PersistedWorkspaceRecord, ): Promise { @@ -4661,7 +4826,7 @@ export class Session { error: null, }, }); - await this.emitWorkspaceUpdateForWorkspaceId(workspace.workspaceId); + await this.emitCreatedWorkspaceUpdate(descriptor); void this.workspaceGitService .getSnapshot(workspace.cwd, { force: true, includeForge: true, reason: "open_project" }) .catch((error) => { @@ -4746,10 +4911,7 @@ export class Session { error: null, }, }); - this.emit({ - type: "workspace_update", - payload: { kind: "upsert", workspace: descriptor }, - }); + await this.emitCreatedWorkspaceUpdate(descriptor); } private async resolveWorktreeSourceCwd(input: { diff --git a/packages/server/src/server/session.workspaces.test.ts b/packages/server/src/server/session.workspaces.test.ts index 1b23c3bbf..99ae1169f 100644 --- a/packages/server/src/server/session.workspaces.test.ts +++ b/packages/server/src/server/session.workspaces.test.ts @@ -6568,6 +6568,64 @@ test("subscribed fetch_workspaces includes git enrichment in the initial snapsho ); }); +test("a workspace leaving a filtered subscription after bootstrap emits a removal", async () => { + const emitted: SessionOutboundMessage[] = []; + const session = asTestSession( + createSessionForWorkspaceTests({ onMessage: (message) => emitted.push(message) }), + ); + const descriptor = { + id: "ws-buffered", + projectId: "proj-buffered", + projectDisplayName: "repo", + projectRootPath: REPO_CWD, + workspaceDirectory: REPO_CWD, + projectKind: "git" as const, + workspaceKind: "local_checkout" as const, + name: "repo work", + status: "done" as const, + activityAt: null, + diffStat: null, + }; + let currentDescriptor: typeof descriptor | null = descriptor; + let finishListing: (result: ListFetchResult) => void = () => {}; + const listing = new Promise((resolve) => { + finishListing = resolve; + }); + session.listFetchWorkspacesEntries = async () => listing; + session.buildWorkspaceDescriptorMap = async () => + new Map(currentDescriptor ? [[currentDescriptor.id, currentDescriptor]] : []); + session.reconcileAndEmitWorkspaceUpdates = async () => undefined; + + const bootstrap = session.handleMessage({ + type: "fetch_workspaces_request", + requestId: "req-buffered-filter", + filter: { query: "repo" }, + subscribe: { subscriptionId: "sub-buffered-filter" }, + }); + await session.emitWorkspaceUpdatesForWorkspaceIds([descriptor.id], { skipReconcile: true }); + finishListing({ + entries: [], + emptyProjects: [], + pageInfo: { nextCursor: null, prevCursor: null, hasMore: false }, + }); + await bootstrap; + + expect(filterByType(emitted, "workspace_update")).toEqual([ + { type: "workspace_update", payload: { kind: "upsert", workspace: descriptor } }, + ]); + + emitted.length = 0; + currentDescriptor = { ...descriptor, name: "other work" }; + await session.emitWorkspaceUpdatesForWorkspaceIds([descriptor.id], { skipReconcile: true }); + + expect(filterByType(emitted, "workspace_update")).toEqual([ + { + type: "workspace_update", + payload: { kind: "remove", id: descriptor.id }, + }, + ]); +}); + test("project.rename.request stores customName and emits an updated workspace descriptor", async () => { const emitted: SessionOutboundMessage[] = []; const session = asTestSession( @@ -7827,4 +7885,75 @@ test("workspace.create.response persists the first prompt as the initial title", expect(workspaceId).toBeDefined(); const persisted = await session.workspaceRegistry.get(workspaceId as string); expect(persisted?.title).toBe("Add retries to the payments flow"); + expect(filterByType(emitted, "workspace_update")).toHaveLength(1); +}); + +test("workspace create emits through a matching workspace subscription", async () => { + const emitted: SessionOutboundMessage[] = []; + const workspaces = new Map>(); + const session = createSessionForWorkspaceTests({ + onMessage: (message) => emitted.push(message), + workspaceRegistry: { + initialize: async () => {}, + existsOnDisk: async () => true, + list: async () => Array.from(workspaces.values()), + get: async (workspaceId) => workspaces.get(workspaceId) ?? null, + upsert: async (workspace) => { + workspaces.set(workspace.workspaceId, workspace); + }, + archive: async () => {}, + remove: async () => {}, + }, + }); + session.listAgentPayloads = async () => []; + + await session.handleMessage({ + type: "fetch_workspaces_request", + requestId: "req-subscribe-create-match", + filter: { query: "repo" }, + subscribe: { subscriptionId: "sub-create-match" }, + }); + emitted.length = 0; + await session.handleMessage({ + type: "workspace.create.request", + requestId: "req-create-match", + source: { kind: "directory", path: REPO_CWD }, + }); + + expect(filterByType(emitted, "workspace_update")).toHaveLength(1); +}); + +test("workspace create stays out of a non-matching workspace subscription", async () => { + const emitted: SessionOutboundMessage[] = []; + const workspaces = new Map>(); + const session = createSessionForWorkspaceTests({ + onMessage: (message) => emitted.push(message), + workspaceRegistry: { + initialize: async () => {}, + existsOnDisk: async () => true, + list: async () => Array.from(workspaces.values()), + get: async (workspaceId) => workspaces.get(workspaceId) ?? null, + upsert: async (workspace) => { + workspaces.set(workspace.workspaceId, workspace); + }, + archive: async () => {}, + remove: async () => {}, + }, + }); + session.listAgentPayloads = async () => []; + + await session.handleMessage({ + type: "fetch_workspaces_request", + requestId: "req-subscribe-create-filtered", + filter: { query: "definitely-not-this-workspace" }, + subscribe: { subscriptionId: "sub-create-filtered" }, + }); + emitted.length = 0; + await session.handleMessage({ + type: "workspace.create.request", + requestId: "req-create-filtered", + source: { kind: "directory", path: REPO_CWD }, + }); + + expect(filterByType(emitted, "workspace_update")).toEqual([]); }); diff --git a/packages/server/src/server/websocket-server.notifications.test.ts b/packages/server/src/server/websocket-server.notifications.test.ts index e0eba9362..0772e7bd5 100644 --- a/packages/server/src/server/websocket-server.notifications.test.ts +++ b/packages/server/src/server/websocket-server.notifications.test.ts @@ -173,6 +173,8 @@ function createSessionWithActivity( ) { return { getClientActivity: vi.fn(() => activity), + supports: () => false, + supportsForSource: () => false, }; } diff --git a/packages/server/src/server/websocket-server.relay-reconnect.test.ts b/packages/server/src/server/websocket-server.relay-reconnect.test.ts index be49b8dae..cd077f037 100644 --- a/packages/server/src/server/websocket-server.relay-reconnect.test.ts +++ b/packages/server/src/server/websocket-server.relay-reconnect.test.ts @@ -52,6 +52,10 @@ const sessionMock = vi.hoisted(() => { handleMessage = vi.fn(async () => {}); handleBinaryFrame = vi.fn((_frame: unknown) => {}); supports = vi.fn((capability: string) => this.args.clientCapabilities?.[capability] === true); + updateClientCapabilities = vi.fn((capabilities: Record | null) => { + this.args.clientCapabilities = capabilities; + }); + clearAgentTimelineSubscription = vi.fn(); getClientActivity = vi.fn(() => null); getSessionId = vi.fn(() => "mock-session-id"); resetPeakInflight = vi.fn(() => {}); diff --git a/packages/server/src/server/websocket-server.ts b/packages/server/src/server/websocket-server.ts index cbe2a13af..62b6b03f4 100644 --- a/packages/server/src/server/websocket-server.ts +++ b/packages/server/src/server/websocket-server.ts @@ -988,6 +988,12 @@ export class VoiceAssistantWebSocketServer { } this.sendToConnection(connection, wrapSessionMessage(msg)); }, + onMessageToSource: (source, msg) => { + if (!connection || !connection.sockets.has(source as WebSocketLike)) { + return; + } + this.sendToClient(source as WebSocketLike, wrapSessionMessage(msg)); + }, onBinaryMessage: (frame) => { if (!connection) { return; @@ -1093,6 +1099,7 @@ export class VoiceAssistantWebSocketServer { sockets: new Set([ws]), externalDisconnectCleanupTimeout: null, }; + session.updateClientCapabilities(clientCapabilities, ws); return connection; } @@ -1163,12 +1170,15 @@ export class VoiceAssistantWebSocketServer { existing.session.updateAppVersion(newAppVersion); } const newClientCapabilities = message.capabilities ?? null; + // COMPAT(selectiveAgentTimeline): added in v0.1.106. Every capable resumed + // hello resets membership before server_info so stale retained-session + // state cannot leak. Remove after 2027-01-12. + existing.session.updateClientCapabilities(newClientCapabilities, ws); if ( JSON.stringify(existing.clientCapabilities ?? null) !== JSON.stringify(newClientCapabilities ?? null) ) { existing.clientCapabilities = newClientCapabilities; - existing.session.updateClientCapabilities(newClientCapabilities); this.syncBrowserToolsClientRegistration(existing); } existing.sockets.add(ws); @@ -1281,6 +1291,8 @@ export class VoiceAssistantWebSocketServer { importSessionWorkspaceTarget: true, // COMPAT(forgeProviders): added in v0.1.106, drop the gate when daemon floor >= v0.1.106. forgeProviders: true, + // COMPAT(selectiveAgentTimeline): added in v0.1.106, remove after 2027-01-12. + selectiveAgentTimeline: true, }, }; } @@ -1390,6 +1402,7 @@ export class VoiceAssistantWebSocketServer { this.sessions.delete(ws); connection.sockets.delete(ws); + connection.session.clearAgentTimelineSubscription(ws); this.socketIdentities.delete(ws); if (connection.sockets.size === 0) { @@ -1758,7 +1771,7 @@ export class VoiceAssistantWebSocketServer { } const startMs = performance.now(); - await activeConnection.session.handleMessage(message.message); + await activeConnection.session.handleMessage(message.message, ws); const durationMs = performance.now() - startMs; this.recordRequestLatency(message.message.type, durationMs); @@ -2009,21 +2022,36 @@ export class VoiceAssistantWebSocketServer { for (const [clientIndex, { ws }] of clientEntries.entries()) { const shouldNotify = clientIndex === plan.inAppRecipientIndex; const timestamp = new Date().toISOString(); - const message = wrapSessionMessage({ - type: "agent_stream", - payload: { - agentId: params.agentId, - event: { - type: "attention_required", - provider: params.provider, - reason: params.reason, - timestamp, - shouldNotify, - notification, - }, - timestamp, - }, - }); + const connection = this.sessions.get(ws); + const attentionPayload = { + agentId: params.agentId, + reason: params.reason, + timestamp, + shouldNotify, + notification, + }; + const message = wrapSessionMessage( + connection?.session.supportsForSource(CLIENT_CAPS.selectiveAgentTimeline, ws) + ? { + type: "agent_attention_required", + payload: attentionPayload, + } + : { + type: "agent_stream", + payload: { + agentId: params.agentId, + event: { + type: "attention_required", + provider: params.provider, + reason: params.reason, + timestamp, + shouldNotify, + notification, + }, + timestamp, + }, + }, + ); this.sendToClient(ws, message); } From 39cb3dbb9c8784f9f25fce54fc429b848076fe0e Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Fri, 17 Jul 2026 19:30:17 +0200 Subject: [PATCH 15/38] Treat every added folder as an independent project (#2098) * fix(projects): detect when projects become Git repositories Project identity was coupled to Git placement, while non-Git roots were dropped by session-scoped observation. Keep identity tied to the selected root and observe Git transitions daemon-wide so empty projects update without rehoming workspaces. * fix(projects): preserve metadata across Git read failures Refresh archived workspace facts before persistence and treat only confirmed non-repositories as non-Git so transient failures cannot rewrite stored project metadata. * fix(projects): close project update races * fix(projects): refresh checkout metadata when reopening folders Persist worktree ownership separately from workspace kind and gate exact-root project creation on stable host identity. Refresh active and archived records so missed Git transitions cannot return stale checkout descriptors. * test(projects): accept Windows aliases for repaired roots * test(server): retry transient Windows cleanup locks * fix(projects): propagate project metadata refreshes Project-only Git transitions now fan out workspace updates for legacy clients. Explicit project creation refreshes stored kind, and equivalent cwd spellings reuse existing workspace records. * fix(projects): refresh worktree source project kind * fix(projects): preserve exact-folder runtime isolation * fix(projects): preserve exact cwd across worktree lifecycle * fix(projects): harden exact-folder worktree flows * fix(projects): derive exact cwd from matched path identity * fix(projects): preserve nested worktree lifecycle * refactor(projects): centralize workspace placement * test(e2e): verify isolated server ports * fix(projects): preserve placement reshape compatibility * fix(projects): validate created worktree placement * fix(projects): preserve placement through workspace lifecycle Archive and recovery were rediscovering or guessing checkout roots instead of consuming the persisted workspace placement. Make the record authoritative for exact cwd, backing worktree, and source repository, and classify stale paths before Git reconciliation. * fix(worktrees): preserve source checkout root Convert Git's common administrative directory back to the source checkout root when legacy archive placement is reconstructed. * fix(worktrees): compare filesystem identities Resolve discovered worktrees and teardown locations through the shared realpath-aware matcher so Windows short and long path spellings cannot split placement identity. * fix(worktrees): centralize path containment Route worktree ownership, listing, resolution, and deletion through the realpath-aware containment primitive so Windows path aliases cannot be filtered by an earlier raw prefix check. * fix(worktrees): remove duplicate ownership discovery * test(e2e): isolate daemon restart ownership * fix(worktrees): make lifecycle operations transactional * fix(workspaces): share git watches by cwd * test(worktrees): make teardown proof portable * fix(sync): integrate project updates with directory owner * fix(workspaces): close reconciliation edge cases * refactor(sync): remove duplicate project reconciliation Keep workspace and project deltas behind the host directory transaction, with owner-boundary coverage for snapshot replay and queued full reconciliation. * fix(sync): buffer project updates before hydration Start the workspace transaction with the online connection epoch so project broadcasts cannot publish a partial directory before the authoritative snapshot commits. --- docs/architecture.md | 10 +- docs/data-model.md | 75 +- docs/glossary.md | 8 +- docs/testing.md | 1 + .../app/e2e/empty-project-persists.spec.ts | 13 +- packages/app/e2e/global-setup.ts | 95 ++- .../app/e2e/helpers/daemon-client-loader.ts | 7 +- packages/app/e2e/helpers/daemon-restart.ts | 159 ---- .../app/e2e/helpers/isolated-host-daemon.ts | 71 +- packages/app/e2e/helpers/new-workspace.ts | 5 +- packages/app/e2e/helpers/seed-client.ts | 3 +- packages/app/e2e/projects-settings.spec.ts | 2 +- packages/app/e2e/sidebar-workspace.spec.ts | 18 +- .../worktree-restore-after-restart.spec.ts | 49 +- packages/app/e2e/worktree-restore.spec.ts | 2 +- .../app/src/add-project-flow/model.test.ts | 8 + packages/app/src/add-project-flow/options.ts | 19 +- .../app/src/components/add-project-flow.tsx | 13 +- packages/app/src/e2e-metro-readiness.test.ts | 65 ++ packages/app/src/hooks/use-open-project.ts | 3 +- .../src/runtime/directory-sync/index.test.ts | 150 +++- .../app/src/runtime/directory-sync/index.ts | 10 + .../directory-sync/workspace-replica.test.ts | 88 +++ .../directory-sync/workspace-replica.ts | 42 +- packages/app/src/stores/session-store.test.ts | 1 + packages/app/src/stores/session-store.ts | 81 +++ packages/client/src/daemon-client.test.ts | 27 + packages/client/src/daemon-client.ts | 7 + packages/protocol/src/client-capabilities.ts | 2 + packages/protocol/src/messages.ts | 12 + .../agent/create-agent-lifecycle-dispatch.ts | 105 ++- .../server/agent/create-agent/create.test.ts | 6 +- .../src/server/agent/create-agent/create.ts | 2 +- .../src/server/agent/mcp-server.test.ts | 70 +- .../archive-if-safe.test.ts | 2 - .../auto-archive-on-merge/archive-if-safe.ts | 2 - .../bootstrap-provider-availability.test.ts | 18 +- packages/server/src/server/bootstrap.test.ts | 56 ++ packages/server/src/server/bootstrap.ts | 95 ++- .../empty-project-persists.e2e.test.ts | 3 +- ...ject-worktree-reclassification.e2e.test.ts | 10 +- .../project-becomes-git.e2e.test.ts | 123 ++++ .../server/src/server/loop-service.test.ts | 13 +- .../src/server/paseo-worktree-service.test.ts | 475 ++++++++++-- .../src/server/paseo-worktree-service.ts | 374 +++------- .../src/server/schedule/service.test.ts | 62 +- .../server/src/server/schedule/service.ts | 18 +- ...ate-agent-worktree-autoarchive.e2e.test.ts | 121 +++- packages/server/src/server/session.test.ts | 86 ++- packages/server/src/server/session.ts | 271 +++---- .../session.workspace-git-watch.test.ts | 13 +- ...on.workspace-resolution-invariants.test.ts | 62 +- .../src/server/session.workspaces.test.ts | 666 ++++++++++++++--- .../workspace-git-observer-service.test.ts | 79 +- .../workspace-git-observer-service.ts | 133 ++-- .../workspace-provisioning-service.test.ts | 281 +++++++- .../workspace-provisioning-service.ts | 429 +++++------ .../workspace-recovery-service.test.ts | 237 +++++- .../workspace-recovery-service.ts | 185 ++++- .../workspace-scripts-service.test.ts | 217 +++++- .../workspace-scripts-service.ts | 101 ++- .../test-utils/workspace-git-service-stub.ts | 21 +- .../websocket-server.relay-reconnect.test.ts | 14 + .../server/src/server/websocket-server.ts | 33 +- .../server/src/server/wire-compat.test.ts | 101 ++- .../server/workspace-archive-service.test.ts | 241 ++++++- .../src/server/workspace-archive-service.ts | 246 +++++-- .../server/src/server/workspace-auto-name.ts | 9 +- .../server/src/server/workspace-directory.ts | 11 + .../src/server/workspace-git-metadata.test.ts | 48 +- .../src/server/workspace-git-metadata.ts | 62 +- .../workspace-git-service.primitive.test.ts | 30 +- .../src/server/workspace-git-service.ts | 67 +- ...rkspace-reconciliation-observation.test.ts | 584 +++++++++++++++ .../workspace-reconciliation-service.test.ts | 681 ++++++++++++++++-- .../workspace-reconciliation-service.ts | 586 +++++++++------ .../workspace-registry-bootstrap-legacy.ts | 120 +++ .../workspace-registry-bootstrap.test.ts | 208 +++++- .../server/workspace-registry-bootstrap.ts | 19 +- .../server/workspace-registry-model.test.ts | 330 ++++----- .../src/server/workspace-registry-model.ts | 360 ++++----- .../src/server/workspace-registry.test.ts | 260 ++++++- .../server/src/server/workspace-registry.ts | 158 +++- .../server/src/server/worktree-bootstrap.ts | 11 +- .../src/server/worktree-session.test.ts | 150 +++- .../server/src/server/worktree-session.ts | 12 +- .../server/src/server/worktree/commands.ts | 38 +- .../server/src/utils/checkout-git.test.ts | 7 + packages/server/src/utils/checkout-git.ts | 62 +- packages/server/src/utils/path.test.ts | 29 +- packages/server/src/utils/path.ts | 42 +- packages/server/src/utils/worktree.test.ts | 84 ++- packages/server/src/utils/worktree.ts | 203 +++--- 93 files changed, 7426 insertions(+), 2762 deletions(-) delete mode 100644 packages/app/e2e/helpers/daemon-restart.ts create mode 100644 packages/app/src/e2e-metro-readiness.test.ts create mode 100644 packages/server/src/server/bootstrap.test.ts create mode 100644 packages/server/src/server/daemon-e2e/project-becomes-git.e2e.test.ts create mode 100644 packages/server/src/server/workspace-reconciliation-observation.test.ts create mode 100644 packages/server/src/server/workspace-registry-bootstrap-legacy.ts diff --git a/docs/architecture.md b/docs/architecture.md index 5b0d8b89a..405602def 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -54,6 +54,14 @@ The heart of Paseo. A Node.js process that: All paths are under `packages/server/src/`. +Project identity is daemon-global rather than session-owned. After registry bootstrap, the daemon's +project Git observer keeps one non-recursive watch on each lexically equivalent active project root +and listens only for the root `.git` entry, with a slow rescan as a missed-event fallback. It runs +for empty projects and without connected clients, then fans metadata changes through the WebSocket +server to capability-aware sessions. It deliberately does not use the broad recursive working-tree +watcher or the per-session Git observer: those are checkout/status mechanisms and intentionally do +not retain non-Git directories. + **Key modules:** | Module | Responsibility | @@ -89,7 +97,7 @@ code imports from `@getpaseo/client`. Cross-platform React Native app that connects to one or more daemons. -- Expo Router navigation (`/h/[serverId]/workspace/[workspaceId]`, `/h/[serverId]/agent/[agentId]`, etc.). The `workspaceId` URL segment is an opaque workspace id (path-shaped today and opaque-encoded for routing), not a directly meaningful filesystem path. +- Expo Router navigation (`/h/[serverId]/workspace/[workspaceId]`, `/h/[serverId]/agent/[agentId]`, etc.). The `workspaceId` URL segment is an opaque workspace id, not a directly meaningful filesystem path. - `HostRuntimeController` manages saved host connections, reconnection, and per-host runtime state - `SessionContext` wraps the daemon client for the active session - Composer UI and submit/draft behavior live in `packages/app/src/composer/`; screens and panels should integrate it from there instead of dropping composer internals into `components/`, `hooks/`, or `screens/workspace/` diff --git a/docs/data-model.md b/docs/data-model.md index 71050c594..b2ff26ba4 100644 --- a/docs/data-model.md +++ b/docs/data-model.md @@ -1,5 +1,29 @@ # Data Model +## Project identity + +Projects are allocated for the exact root selected by the caller, normalized lexically with `path.resolve` (never `realpath`). New project IDs are opaque `prj_<16 hex>` values. Existing remote-shaped or path-shaped IDs are retained as readable compatibility records and are never rekeyed. An active exact root is idempotent; archived-only matches do not resurrect an old project. Workspace `projectId` is stable membership: reconciliation may update git-derived kind and branch metadata, but never rehomes a workspace or changes a project's root, ID, or default name. + +`kind` is mutable metadata, not identity. Workspace reconciliation watches active project roots and +updates only a project's `kind` and `updatedAt` when `.git` appears or disappears, preserving its +ID, root path, names, and workspace foreign keys. Attached workspaces are independently refreshed +from their own cwd, so an explicit project root never implies a workspace checkout. Empty projects +are observed too. + +The workspace registry model defines placement once: initial directory/worktree construction, +mutable reconciliation fields, and the persisted-to-wire checkout projection. Its update policy +preserves `displayName` and `baseBranch`. `WorkspaceProvisioningService` owns the corresponding +registry writes, so directory opens, agent imports, and worktree creation all enter through that +service instead of constructing records independently. The workspace record is then the durable +placement authority: `cwd` is the exact execution directory, while `worktreeRoot` is the backing +checkout root. They intentionally differ for an exact subproject inside a worktree. Archive, +restore, branch auto-name, and descriptor flows consume those persisted facts rather than +rediscovering ownership from a directory that may already be gone. Reconciliation may refresh +mutable placement facts, but never changes `projectId`, `cwd`, `displayName`, or `baseBranch`. +Workspace archive runs lifecycle teardown from the exact `cwd` but removes only the backing +`worktreeRoot` after its last active reference disappears. Worktree recovery recreates that backing +checkout from `mainRepoRoot`, then restores the relative path from `worktreeRoot` to `cwd`. + Paseo uses **file-based JSON persistence** instead of a traditional database. All data is validated at runtime with Zod schemas. Most stores write atomically (write to temp file, then rename); a few still use plain `writeFile` — see each section. There is no schema-versioning/migration framework — schemas rely on optional fields with defaults for forward compatibility, with a small amount of inline normalization in `persisted-config.ts` for legacy provider/speech entries. All server-side stores live under `$PASEO_HOME` (defaults to `~/.paseo`). @@ -422,19 +446,22 @@ Array of project records. | Field | Type | Description | | ------------- | --------------------------- | -------------------------------------------------------------------------------- | -| `projectId` | `string` | Primary key | -| `rootPath` | `string` | Filesystem root of the project | -| `kind` | `"git" \| "non_git"` | | -| `displayName` | `string` | | +| `projectId` | `string` | Primary key; new records use opaque `prj_<16 hex>` IDs | +| `rootPath` | `string` | Exact lexically normalized selected root; never realpathed | +| `kind` | `"git" \| "non_git"` | Mutable Git observation about `rootPath`, never a membership key | +| `displayName` | `string` | Selected-root basename, stable across remote and Git changes | | `customName` | `string \| null` | User-set override layered over `displayName`. Null means "use the derived name". | | `createdAt` | `string` (ISO 8601) | | | `updatedAt` | `string` (ISO 8601) | | | `archivedAt` | `string \| null` (ISO 8601) | Soft-delete timestamp; required nullable | -Active git projects are unique by normalized `rootPath`. Startup reconciliation repairs older bad -states by moving workspaces from duplicate path-keyed projects onto the canonical project, -preferring remote-keyed project IDs such as `remote:github.com/owner/repo`, then archiving the -emptied duplicate. +Active exact roots are idempotent using lexical platform-equivalence semantics. Existing legacy +remote-shaped and path-shaped IDs remain readable, including duplicate roots; reconciliation never +merges them, transfers names, archives them, or moves workspace foreign keys. An explicit +workspace `projectId` is authoritative when it names an active project, regardless of cwd +containment. Archived-only exact-root records are not resurrected by explicit add/open; a fresh +opaque project is allocated instead. Agent restore is separate and restores the agent's existing +workspace together with its owning project. --- @@ -444,21 +471,25 @@ emptied duplicate. Array of workspace records. A workspace is a specific working directory within a project. -| Field | Type | Description | -| ------------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `workspaceId` | `string` | Opaque stable identifier (`wks_`), generated independently of the directory. MUST NOT be treated as a path; compare by exact equality. Use the `cwd` field for directory access. | -| `projectId` | `string` | FK to Project.projectId | -| `cwd` | `string` | Filesystem path | -| `kind` | `"local_checkout" \| "worktree" \| "directory"` | | -| `displayName` | `string` | The human name (the generated/derived title). Decoupled from `branch` by construction. | -| `title` | `string \| null` | User-set name override layered over `displayName`. Null means "use `displayName`". | -| `branch` | `string \| null` | The worktree's git branch. Separate from `displayName`/`title`; only worktree workspaces set it. A branch rename writes this and never the name. | -| `createdAt` | `string` (ISO 8601) | | -| `updatedAt` | `string` (ISO 8601) | | -| `archivedAt` | `string \| null` (ISO 8601) | Soft-delete; required nullable | -| `pinnedAt` | `string \| null` (ISO 8601) | Pinned-to-top-of-sidebar timestamp; null means "not pinned" | +| Field | Type | Description | +| ---------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `workspaceId` | `string` | Opaque stable identifier (`wks_`), generated independently of the directory. MUST NOT be treated as a path; compare by exact equality. Use the `cwd` field for directory access. | +| `projectId` | `string` | FK to Project.projectId; the workspace's stable project membership | +| `cwd` | `string` | Exact execution directory selected for agents, files, scripts, and setup | +| `kind` | `"local_checkout" \| "worktree" \| "directory"` | Mutable checkout classification | +| `displayName` | `string` | The human name (the generated/derived title). Decoupled from `branch` by construction. | +| `title` | `string \| null` | User-set name override layered over `displayName`. Null means "use `displayName`". | +| `branch` | `string \| null` | The current Git branch for git-backed workspaces. Separate from `displayName`/`title`; a background branch refresh never rewrites the name. | +| `worktreeRoot` | `string \| null` | Backing checkout/worktree root. May differ from `cwd` for exact subprojects and remains persisted after the worktree is deleted so restore can reproduce the placement. | +| `baseBranch` | `string \| null` | Normalized branch the Paseo worktree was created from; null for directories, local checkouts, and checkout-branch worktrees | +| `isPaseoOwnedWorktree` | `boolean` | Whether Paseo owns and may remove/recreate the backing `worktreeRoot` | +| `mainRepoRoot` | `string \| null` | Main repository root for worktree checkouts, independent of both exact `cwd` and backing `worktreeRoot` | +| `createdAt` | `string` (ISO 8601) | | +| `updatedAt` | `string` (ISO 8601) | | +| `archivedAt` | `string \| null` (ISO 8601) | Soft-delete; required nullable | +| `pinnedAt` | `string \| null` (ISO 8601) | Pinned-to-top-of-sidebar timestamp; null means "not pinned" | -> **Opaque-ID invariant:** `workspaceId` is opaque identity, never a filesystem path. Filesystem and git operations take `cwd`/`workspaceDirectory` only — never the id. Path-derived grouping keys (e.g. `deriveWorkspaceDirectoryKey`, used at bootstrap to group agents into a workspace) are directory keys, not workspace identity, and must not be persisted or compared as ids. +> **Opaque-ID invariant:** `workspaceId` is opaque identity, never a filesystem path. Filesystem and git operations take `cwd`/`workspaceDirectory` only — never the id. A compatibility-only first-materialization bootstrap still groups pre-registry agent records by path and Git remote so existing installs retain their legacy records. That grouping never runs against a live registry, and its keys are not runtime project or workspace identity. `projectId` is still a real FK: workspace records should have a matching project record. Read-only history surfaces tolerate transient orphaned workspaces by omitting those rows so one bad FK cannot diff --git a/docs/glossary.md b/docs/glossary.md index cdbdd0565..fc8878f74 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -2,22 +2,22 @@ Authoritative terminology. UI label wins. Don't invent synonyms; use what's here. -- **Project** — Logical grouping of workspaces sharing a git remote (or main repo root). UI: "Project" / "Add project". Code: `ProjectSummary` (`packages/app/src/utils/projects.ts:22`), `projectKey` (`packages/server/src/server/workspace-registry-model.ts:16`). Forbidden: "Repo", "Repository" as UI label. +- **Project** — A stable, exact selected-root record. New IDs are opaque `prj_<16 hex>` values; older remote-shaped and path-shaped IDs remain readable compatibility records. Git facts can update mutable kind metadata but never project identity, root, or default display name. UI: "Project" / "Add project". Forbidden: "Repo", "Repository" as UI label. - **Workspace** — One concrete `cwd` on one daemon, with git state; belongs to exactly one project. Its `id` is opaque workspace identity; its `cwd` is the filesystem directory. UI: "Workspace". Code: `WorkspaceDescriptorPayload` (`packages/protocol/src/messages.ts:2178`). Don't confuse with: Branch (one branch can back many workspaces via worktrees). Forbidden: "Folder", "Directory" as UI label. - **Archive workspace** — Removes one workspace from active use and archives everything it owns. UI and app shortcuts always say "Archive workspace", regardless of backing. The daemon leaves ordinary directories intact and removes a Paseo-owned worktree only when no active workspace still references it. CLI/MCP **archive worktree** is a separate lower-level operation that archives every workspace on that worktree. -- **Workspace kind** — `"directory" | "local_checkout" | "worktree"`. The git-derived, persisted property of a workspace, used across its lifetime (archive safety, sidebar, grouping). Derived from the cwd's git reality (`deriveWorkspaceKind`, `packages/server/src/server/workspace-registry-model.ts:158`), not stored from a user choice. Code: `PersistedWorkspaceKind` (`packages/server/src/server/workspace-registry-model.ts:8`). Don't confuse with **Isolation** (the create-time intent). +- **Workspace kind** — `"directory" | "local_checkout" | "worktree"`. The git-derived, persisted property of a workspace, used across its lifetime (archive safety, sidebar, grouping). Derived from the cwd's git reality by `deriveWorkspaceKind` in `workspace-registry-model.ts`, not stored from a user choice. Don't confuse with **Isolation** (the create-time intent). - **Isolation** — Create-time choice for a new workspace: reuse the existing checkout (**Local**) or cut a dedicated git worktree (**New worktree**). A transient setup input, also remembered as a create-form preference; it is not a workspace property. UI: "Isolation" control on the New Workspace screen. Code: `isolation` (`"local" | "worktree"`), `useWorkspaceIsolation` (`packages/app/src/screens/new-workspace-screen.tsx`); persisted as `FormPreferences.isolation` (`packages/app/src/create-agent-preferences/preferences.ts`). Distinct from **Workspace kind**, which is the git-derived property the intent produces (Local → `local_checkout` or `directory` by git-ness; New worktree → `worktree`). On the wire it is the create request's `source.kind` (`directory | worktree`, `packages/protocol/src/messages.ts:1693`). - **Agent** — See **Agent session**. UI still says "Agent" / "New Agent" in places, but moving toward **Agent session** as the canonical term. Code: `AgentSnapshotPayload` (`packages/protocol/src/messages.ts:608`). Forbidden: "Task", "Job", "Run". - **Daemon** — Local Paseo server process; identified by `serverId`. UI: "Daemon" (system contexts only). Code: `serverId` in `ServerInfoStatusPayloadSchema` (`packages/protocol/src/messages.ts:1936`), `DaemonClient` (`packages/client/src/daemon-client.ts`). - **Host** — Client-side connection profile pointing at a daemon; bundles one or more `HostConnection`s. UI: "Host" / "Add host" / "Switch host". Code: `HostProfile` (`packages/app/src/types/host-connection.ts:37`). Forbidden: "Connection" (means `HostConnection`, not host). - **Project host entry** — One row in a project for a single (project, daemon) pair, aggregating that daemon's workspaces in the project. Internal. Code: `ProjectHostEntry` (`packages/app/src/utils/projects.ts:11`). Don't introduce "Checkout" as a synonym. -- **Placement** — One workspace's relationship to its project (projectKey, projectName, git checkout snapshot). Internal. Code: `ProjectPlacementPayload` (`packages/protocol/src/messages.ts:2113`). +- **Placement** — One workspace's stable foreign-key relationship to its project plus its git checkout snapshot. Internal. An explicit creation `projectId` is authoritative when active. - **Branch** — Plain git branch. UI: "Switch branch". Code: `currentBranch` in `WorkspaceGitRuntimePayloadSchema` (`packages/protocol/src/messages.ts:2136`); `BranchSwitcher` (`packages/app/src/components/branch-switcher.tsx`). - **Forge** — Git hosting service behind Paseo's change-request features: GitHub, GitLab, Gitea, Forgejo, or a future registered adapter. Code: `ForgeService`, `forge-registry`, `forge-resolver`. Use `forge` for internal abstraction and registry IDs; use concrete forge names only when a behavior or RPC is forge-specific. - **Change request** — Forge-neutral term for a proposed branch-to-branch code change. UI normally renders the forge noun instead: GitHub/Gitea/Forgejo "PR", GitLab "MR". Code: `forge_change_request` attachments, `checkoutSource: { kind: "change_request" }`, and PR/MR status payloads. - **MR** — GitLab merge request. UI label for GitLab change requests only; do not use MR for GitHub/Gitea/Forgejo. - **Worktree** — Paseo-managed git worktree (`~/.paseo/worktrees/{name}`); also a `workspaceKind` value. UI: CLI + `paseo.json` keys (`worktree.setup`, `worktree.teardown`) only. Code: `ProjectCheckoutLiteGitPaseoPayload` (`packages/protocol/src/messages.ts:2092`); CLI `paseo worktree` (`packages/cli/src/commands/worktree/index.ts:8`). Forbidden: "Checkout" as a synonym. -- **Repository / Remote** — Internal git inputs (`remoteUrl`, `mainRepoRoot`) used to derive `projectKey`. No UI label. +- **Repository / Remote** — Internal Git observations. They may affect mutable kind/branch metadata but never project identity, root, display name, or workspace membership. No UI label. - **Directory-backed surface** — A right-sidebar surface whose content is determined by the workspace's `cwd`, so two workspaces on the same directory see identical content: git diff/status, forge change-request info, file preview/explorer contents. Keyed by `(serverId, cwd)`, never `workspaceId`. See [architecture.md](architecture.md#right-sidebar-boundary-directory-backed-vs-workspace-owned). - **Workspace-owned state** — Per-workspace state that never leaks to a same-`cwd` sibling: tabs, agents, terminals, panes, title, plus review drafts, diff-mode overrides, composer attachments, and file-explorer open/expand state. Keyed by `workspaceId` (`cwd` only as a fallback for old payloads). See [architecture.md](architecture.md#right-sidebar-boundary-directory-backed-vs-workspace-owned). - **Workspace status bucket** — Aggregate activity signal for a workspace row. Same-`cwd` workspaces intentionally share agent and terminal status buckets, while tab, agent, and terminal visibility remains scoped by `workspaceId`. diff --git a/docs/testing.md b/docs/testing.md index 06a73d34b..876971979 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -177,6 +177,7 @@ Test suites in this repo are heavy. Running them in bulk freezes the machine, es - Never run the full Playwright E2E suite locally — defer whole-suite verification to CI. Targeted Playwright specs are allowed when you changed or need to prove that specific flow. - App Playwright specs share one isolated daemon per run. Helpers that create projects or workspaces must remove the daemon project record during cleanup, not only delete the temp directory. Agent helpers must pass the intended `workspaceId` through to agent creation; never infer ownership from `cwd`. - CI can shard app Playwright across multiple jobs; each shard still owns a full isolated daemon/relay/Metro stack from global setup. Helpers that restart the daemon must preserve the global setup environment, including disabled speech/local-model settings, so a restart does not change the tested surface or start background downloads. +- Global setup starts Metro before Wrangler, assigns Wrangler explicit distinct relay and inspector ports, and accepts Metro as ready only when `/status` returns `packager-status:running`. A generic TCP listener is not sufficient readiness evidence. ## Agent authentication in tests diff --git a/packages/app/e2e/empty-project-persists.spec.ts b/packages/app/e2e/empty-project-persists.spec.ts index 2344fd597..c915d809f 100644 --- a/packages/app/e2e/empty-project-persists.spec.ts +++ b/packages/app/e2e/empty-project-persists.spec.ts @@ -219,15 +219,20 @@ test.describe("Project remove", () => { const readded = await workspace.client.addProject(workspace.repoPath); expect(readded.error).toBeNull(); + expect(readded.project).not.toBeNull(); + const readdedProjectId = readded.project?.projectId ?? ""; + expect(readdedProjectId).not.toBe(workspace.projectId); expect(readded.project?.projectDisplayName).toBe(workspace.projectDisplayName); await page.reload(); await waitForSidebarHydration(page); - await expect(projectRow).toBeVisible({ timeout: 30_000 }); - await expect(projectRow).toContainText(workspace.projectDisplayName); - await expect(projectRow).not.toContainText(workspace.repoPath); + await expect(projectRow).toHaveCount(0, { timeout: 30_000 }); + const readdedProjectRow = page.getByTestId(`sidebar-project-row-${readdedProjectId}`); + await expect(readdedProjectRow).toBeVisible({ timeout: 30_000 }); + await expect(readdedProjectRow).toContainText(workspace.projectDisplayName); + await expect(readdedProjectRow).not.toContainText(workspace.repoPath); await expect( - page.getByTestId(`sidebar-project-new-workspace-row-${workspace.projectId}`), + page.getByTestId(`sidebar-project-new-workspace-row-${readdedProjectId}`), ).toBeVisible({ timeout: 30_000 }); } finally { await workspace.cleanup(); diff --git a/packages/app/e2e/global-setup.ts b/packages/app/e2e/global-setup.ts index c64b58072..b71da5ba0 100644 --- a/packages/app/e2e/global-setup.ts +++ b/packages/app/e2e/global-setup.ts @@ -14,7 +14,7 @@ import { withDisabledE2ESpeechEnv } from "./helpers/speech-env"; const wranglerCliPath = path.resolve(__dirname, "../node_modules/wrangler/bin/wrangler.js"); -interface WaitForServerOptions { +export interface WaitForServerOptions { host?: string; timeoutMs?: number; label: string; @@ -22,6 +22,8 @@ interface WaitForServerOptions { getRecentOutput?: () => string; } +type ServerProbe = (host: string, port: number) => Promise; + async function getAvailablePort(): Promise { return new Promise((resolve, reject) => { const server = net.createServer(); @@ -75,7 +77,25 @@ function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } -async function waitForServer(port: number, options: WaitForServerOptions): Promise { +async function connectToServer(host: string, port: number): Promise { + await new Promise((resolve, reject) => { + const socket = net.connect(port, host, () => { + socket.end(); + resolve(); + }); + socket.setTimeout(1000, () => { + socket.destroy(); + reject(new Error(`Connection timed out to ${host}:${port}`)); + }); + socket.on("error", reject); + }); +} + +async function waitForServer( + port: number, + options: WaitForServerOptions, + probe: ServerProbe = connectToServer, +): Promise { const { host = "127.0.0.1", timeoutMs = 15000, label, childProcess, getRecentOutput } = options; const start = Date.now(); let lastConnectionError: unknown = null; @@ -89,17 +109,7 @@ async function waitForServer(port: number, options: WaitForServerOptions): Promi } try { - await new Promise((resolve, reject) => { - const socket = net.connect(port, host, () => { - socket.end(); - resolve(); - }); - socket.setTimeout(1000, () => { - socket.destroy(); - reject(new Error(`Connection timed out to ${host}:${port}`)); - }); - socket.on("error", reject); - }); + await probe(host, port); return; } catch (error) { lastConnectionError = error; @@ -116,6 +126,22 @@ async function waitForServer(port: number, options: WaitForServerOptions): Promi ); } +async function probeMetro(host: string, port: number): Promise { + const response = await fetch(`http://${host}:${port}/status`, { + signal: AbortSignal.timeout(1000), + }); + const body = (await response.text()).trim(); + if (response.status !== 200 || body !== "packager-status:running") { + throw new Error( + `Expected Metro status on ${host}:${port}, received HTTP ${response.status}: ${JSON.stringify(body.slice(0, 200))}`, + ); + } +} + +export async function waitForMetro(port: number, options: WaitForServerOptions): Promise { + await waitForServer(port, options, probeMetro); +} + function parseRelayStartupFailure(line: string): string | null { const clean = stripAnsi(line); if (/Address already in use/i.test(clean)) { @@ -556,13 +582,19 @@ async function getAvailablePortExcluding(excludedPorts: Set): Promise): Promise { +interface RelayPorts { + relayPort: number; + inspectorPort: number; +} + +async function startRelay(excludedPorts: Set): Promise { const relayDir = path.resolve(__dirname, "..", "..", "relay"); const maxRelayStartupAttempts = 5; let lastRelayStartupError: unknown = null; for (let attempt = 1; attempt <= maxRelayStartupAttempts; attempt += 1) { const relayPort = await getAvailablePortExcluding(excludedPorts); + const inspectorPort = await getAvailablePortExcluding(new Set([...excludedPorts, relayPort])); const buffer = createLineBuffer(); const state: RelayStreamState = { failureLine: null, readyForSelectedPort: false }; @@ -576,6 +608,10 @@ async function startRelay(excludedPorts: Set): Promise { "127.0.0.1", "--port", String(relayPort), + "--inspector-ip", + "127.0.0.1", + "--inspector-port", + String(inspectorPort), "--live-reload=false", "--show-interactive-dev-session=false", ], @@ -590,7 +626,7 @@ async function startRelay(excludedPorts: Set): Promise { try { await awaitRelayReady(relayProcess, relayPort, state, buffer); - return relayPort; + return { relayPort, inspectorPort }; } catch (error) { lastRelayStartupError = error; await stopProcess(relayProcess); @@ -767,12 +803,19 @@ export default async function globalSetup() { await logSpeechHarnessConfig(); try { - const relayPort = await startRelay(new Set([port, metroPort])); metroProcess = startMetro({ metroPort, daemonPort: port, buffer: metroLineBuffer, }); + await waitForMetro(metroPort, { + label: "Metro web server", + timeoutMs: 120000, + childProcess: metroProcess, + getRecentOutput: metroLineBuffer.dump, + }); + + const { relayPort, inspectorPort } = await startRelay(new Set([port, metroPort])); daemonProcess = startDaemon({ port, relayPort, @@ -783,19 +826,11 @@ export default async function globalSetup() { buffer: daemonLineBuffer, }); - await Promise.all([ - waitForServer(port, { - label: "Paseo daemon", - childProcess: daemonProcess, - getRecentOutput: daemonLineBuffer.dump, - }), - waitForServer(metroPort, { - label: "Metro web server", - timeoutMs: 120000, - childProcess: metroProcess, - getRecentOutput: metroLineBuffer.dump, - }), - ]); + await waitForServer(port, { + label: "Paseo daemon", + childProcess: daemonProcess, + getRecentOutput: daemonLineBuffer.dump, + }); const offer = await waitForPairingOfferFromDaemon({ port, @@ -809,7 +844,7 @@ export default async function globalSetup() { process.env.E2E_PASEO_HOME = paseoHome; process.env.E2E_EDITOR_RECORD_PATH = editorRecordPath; console.log( - `[e2e] Test daemon started on port ${port}, Metro on port ${metroPort}, home: ${paseoHome}`, + `[e2e] Test daemon started on port ${port}, Metro on port ${metroPort}, relay on port ${relayPort}, relay inspector on port ${inspectorPort}, home: ${paseoHome}`, ); return async () => { diff --git a/packages/app/e2e/helpers/daemon-client-loader.ts b/packages/app/e2e/helpers/daemon-client-loader.ts index 3a2a1b7ce..b9a6e7b22 100644 --- a/packages/app/e2e/helpers/daemon-client-loader.ts +++ b/packages/app/e2e/helpers/daemon-client-loader.ts @@ -26,13 +26,14 @@ interface E2EDaemonClientConfig { webSocketFactory?: NodeWebSocketFactory; } -function resolveDaemonWsUrl(): string { - return `ws://127.0.0.1:${getE2EDaemonPort()}/ws`; +function resolveDaemonWsUrl(port?: number): string { + return `ws://127.0.0.1:${port ?? getE2EDaemonPort()}/ws`; } export interface ConnectDaemonClientOptions { clientIdPrefix: string; appVersion?: string; + port?: number; } /** @@ -45,7 +46,7 @@ export async function connectDaemonClient { const DaemonClient = await loadDaemonClientConstructor(); const client = new DaemonClient({ - url: resolveDaemonWsUrl(), + url: resolveDaemonWsUrl(options.port), clientId: `${options.clientIdPrefix}-${randomUUID()}`, clientType: "cli", appVersion: options.appVersion ?? loadAppVersion(), diff --git a/packages/app/e2e/helpers/daemon-restart.ts b/packages/app/e2e/helpers/daemon-restart.ts deleted file mode 100644 index 5490d449f..000000000 --- a/packages/app/e2e/helpers/daemon-restart.ts +++ /dev/null @@ -1,159 +0,0 @@ -import { spawn, type ChildProcess } from "node:child_process"; -import { createRequire } from "node:module"; -import { readFile } from "node:fs/promises"; -import net from "node:net"; -import path from "node:path"; - -import { getE2EDaemonPort } from "./daemon-port"; -import { withDisabledE2ESpeechEnv } from "./speech-env"; - -/** - * Restarts the isolated E2E daemon against the SAME PASEO_HOME and SAME port so - * persisted state reloads and existing clients can reconnect. This exercises the - * post-restart rehydration path (the daemon rebuilding workspace/agent links - * from disk), which is where the worktree-branch regression lives. - * - * The daemon is owned by Playwright's `globalSetup`, which keeps its child - * handle in module scope we can't reach from a spec. Instead we drive it the - * same way an operator would: read the supervisor PID from - * `$PASEO_HOME/paseo.pid`, SIGTERM it (the supervisor forwards the signal to its - * worker and releases the lock), wait for the port to free, then re-spawn the - * supervisor with the identical environment globalSetup used. The relay and - * Metro processes are untouched, so we reuse their already-published ports. - * - * This NEVER targets the developer daemon: the port comes from - * `getE2EDaemonPort()`, which refuses 6767, and PASEO_HOME is the isolated E2E - * home globalSetup created. - */ - -function getEnvOrThrow(name: string): string { - const value = process.env[name]; - if (!value) { - throw new Error(`${name} is not set (expected from Playwright globalSetup).`); - } - return value; -} - -async function readSupervisorPid(paseoHome: string): Promise { - const pidPath = path.join(paseoHome, "paseo.pid"); - const content = await readFile(pidPath, "utf8"); - const parsed = JSON.parse(content) as { pid?: unknown }; - if (typeof parsed.pid !== "number") { - throw new Error(`Malformed PID lock at ${pidPath}: ${content}`); - } - return parsed.pid; -} - -function isPidRunning(pid: number): boolean { - try { - process.kill(pid, 0); - return true; - } catch { - return false; - } -} - -function isPortListening(port: number, host = "127.0.0.1"): Promise { - return new Promise((resolve) => { - const socket = net.connect(port, host, () => { - socket.end(); - resolve(true); - }); - socket.setTimeout(1000, () => { - socket.destroy(); - resolve(false); - }); - socket.on("error", () => resolve(false)); - }); -} - -async function waitUntil( - predicate: () => Promise | boolean, - options: { timeoutMs: number; label: string }, -): Promise { - const deadline = Date.now() + options.timeoutMs; - while (Date.now() < deadline) { - if (await predicate()) { - return; - } - await new Promise((resolve) => setTimeout(resolve, 100)); - } - throw new Error(`Timed out after ${options.timeoutMs}ms waiting for ${options.label}.`); -} - -function spawnSupervisor(args: { - paseoHome: string; - port: string; - relayPort: string; - metroPort: string; - editorRecordPath: string; -}): ChildProcess { - const serverDir = path.resolve(__dirname, "../../../..", "packages/server"); - // Run the supervisor through the resolved tsx CLI under the current node - // binary. Spawning the `node_modules/.bin/tsx` shim directly is unreliable - // inside the Playwright worker (the shim is a .mjs symlink, not an executable), - // so resolve the CLI module and load it with node. - const tsxCli = createRequire(path.join(serverDir, "package.json")).resolve("tsx/cli"); - const env = withDisabledE2ESpeechEnv({ - ...process.env, - PASEO_HOME: args.paseoHome, - PASEO_E2E_EDITOR_RECORD_PATH: args.editorRecordPath, - PASEO_SERVER_ID: "srv_e2e_test_daemon", - PASEO_LISTEN: `0.0.0.0:${args.port}`, - PASEO_RELAY_ENDPOINT: `127.0.0.1:${args.relayPort}`, - PASEO_CORS_ORIGINS: `http://localhost:${args.metroPort}`, - PASEO_NODE_ENV: "development", - NODE_ENV: "development", - }); - - const child = spawn(process.execPath, [tsxCli, "scripts/supervisor-entrypoint.ts", "--dev"], { - cwd: serverDir, - env, - stdio: ["ignore", "pipe", "pipe"], - detached: false, - }); - - child.stdout?.on("data", (data: Buffer) => { - for (const line of data.toString().split("\n")) { - if (line.trim()) console.log(`[daemon:restart] ${line.trim()}`); - } - }); - child.stderr?.on("data", (data: Buffer) => { - for (const line of data.toString().split("\n")) { - if (line.trim()) console.error(`[daemon:restart] ${line.trim()}`); - } - }); - - // Detach our handles so the spawned supervisor outlives this spec process and - // is reaped by globalSetup's cleanup (the original process tree), not us. - child.unref(); - return child; -} - -export async function restartTestDaemon(): Promise { - const port = getE2EDaemonPort(); - const paseoHome = getEnvOrThrow("E2E_PASEO_HOME"); - const relayPort = getEnvOrThrow("E2E_RELAY_PORT"); - const metroPort = getEnvOrThrow("E2E_METRO_PORT"); - const editorRecordPath = - process.env.E2E_EDITOR_RECORD_PATH ?? path.join(paseoHome, "editor-open-records.jsonl"); - - const pid = await readSupervisorPid(paseoHome); - process.kill(pid, "SIGTERM"); - - await waitUntil(() => !isPidRunning(pid), { - timeoutMs: 15_000, - label: `supervisor PID ${pid} to exit`, - }); - await waitUntil(async () => !(await isPortListening(Number(port))), { - timeoutMs: 15_000, - label: `port ${port} to free`, - }); - - spawnSupervisor({ paseoHome, port, relayPort, metroPort, editorRecordPath }); - - await waitUntil(async () => isPortListening(Number(port)), { - timeoutMs: 30_000, - label: `restarted daemon to listen on port ${port}`, - }); -} diff --git a/packages/app/e2e/helpers/isolated-host-daemon.ts b/packages/app/e2e/helpers/isolated-host-daemon.ts index d98d40e4f..1d2d6a0db 100644 --- a/packages/app/e2e/helpers/isolated-host-daemon.ts +++ b/packages/app/e2e/helpers/isolated-host-daemon.ts @@ -9,6 +9,7 @@ import { withDisabledE2ESpeechEnv } from "./speech-env"; export interface IsolatedHostDaemon { serverId: string; port: number; + restart(): Promise; close(): Promise; } @@ -85,43 +86,61 @@ export async function startIsolatedHostDaemon(serverId: string): Promise => { + const child = spawn(tsxBin, ["scripts/supervisor-entrypoint.ts", "--dev"], { + cwd: serverDir, + env: withDisabledE2ESpeechEnv({ + ...process.env, + PASEO_HOME: paseoHome, + PASEO_SERVER_ID: serverId, + PASEO_LISTEN: `127.0.0.1:${port}`, + PASEO_CORS_ORIGINS: `http://localhost:${metroPort}`, + PASEO_RELAY_ENABLED: "0", + PASEO_NODE_ENV: "development", + NODE_ENV: "development", + }), + stdio: ["ignore", "ignore", "pipe"], + detached: false, + }); - let stderr = ""; - child.stderr?.on("data", (chunk: Buffer) => { - stderr += chunk.toString("utf8"); - stderr = stderr.split("\n").slice(-40).join("\n"); - }); + let stderr = ""; + child.stderr?.on("data", (chunk: Buffer) => { + stderr += chunk.toString("utf8"); + stderr = stderr.split("\n").slice(-40).join("\n"); + }); + try { + await waitForServer(port, child); + return child; + } catch (error) { + await stopProcess(child); + throw new Error( + `${error instanceof Error ? error.message : String(error)}\nDaemon stderr:\n${stderr}`, + { cause: error }, + ); + } + }; + + let child: ChildProcess; try { - await waitForServer(port, child); + child = await spawnDaemon(); } catch (error) { - await stopProcess(child); await rm(paseoHome, { recursive: true, force: true }); - throw new Error( - `${error instanceof Error ? error.message : String(error)}\nDaemon stderr:\n${stderr}`, - { cause: error }, - ); + throw error; } + let closed = false; return { serverId, port, + restart: async () => { + if (closed) throw new Error(`Cannot restart closed isolated daemon ${serverId}`); + await stopProcess(child); + child = await spawnDaemon(); + }, close: async () => { + if (closed) return; + closed = true; await stopProcess(child); await rm(paseoHome, { recursive: true, force: true }); }, diff --git a/packages/app/e2e/helpers/new-workspace.ts b/packages/app/e2e/helpers/new-workspace.ts index 13c267ab3..e8ef24e32 100644 --- a/packages/app/e2e/helpers/new-workspace.ts +++ b/packages/app/e2e/helpers/new-workspace.ts @@ -89,9 +89,12 @@ function parseWorkspaceIdFromPageUrl(page: Page, serverId: string): string | nul return decodeWorkspaceIdFromPathSegment(match[1]); } -export async function connectNewWorkspaceDaemonClient(): Promise { +export async function connectNewWorkspaceDaemonClient(options?: { + port?: number; +}): Promise { return connectDaemonClient({ clientIdPrefix: "app-e2e-new-workspace", + port: options?.port, }); } diff --git a/packages/app/e2e/helpers/seed-client.ts b/packages/app/e2e/helpers/seed-client.ts index 6a04bbd51..2b243022f 100644 --- a/packages/app/e2e/helpers/seed-client.ts +++ b/packages/app/e2e/helpers/seed-client.ts @@ -158,10 +158,11 @@ export interface SeedDaemonClient { killTerminal(terminalId: string): Promise<{ error: string | null }>; } -export async function connectSeedClient(): Promise { +export async function connectSeedClient(options?: { port?: number }): Promise { return connectDaemonClient({ clientIdPrefix: "seed", appVersion: loadAppVersion(), + port: options?.port, }); } diff --git a/packages/app/e2e/projects-settings.spec.ts b/packages/app/e2e/projects-settings.spec.ts index 1669586b5..6330b8cef 100644 --- a/packages/app/e2e/projects-settings.spec.ts +++ b/packages/app/e2e/projects-settings.spec.ts @@ -208,7 +208,7 @@ test.describe("Projects settings", () => { page, gitlabRemoteProject, }) => { - expect(gitlabRemoteProject.name).toBe("acme/app"); + expect(gitlabRemoteProject.name).toBe(path.basename(gitlabRemoteProject.path)); await openProjects(page); await openProjectSettings(page, gitlabRemoteProject.name); await editWorktreeSetup(page, updatedSetup); diff --git a/packages/app/e2e/sidebar-workspace.spec.ts b/packages/app/e2e/sidebar-workspace.spec.ts index d23085852..be68e73a7 100644 --- a/packages/app/e2e/sidebar-workspace.spec.ts +++ b/packages/app/e2e/sidebar-workspace.spec.ts @@ -46,24 +46,25 @@ async function waitForSidebarWorkspace(page: import("@playwright/test").Page, wo } test.describe("Sidebar workspace list", () => { - test("project with GitHub remote shows owner/repo name in sidebar", async ({ page }) => { + test("project with GitHub remote shows its selected folder name in sidebar", async ({ page }) => { const workspace = await seedWorkspace({ repoPrefix: "sidebar-remote-", repo: { withRemote: true, originUrl: GITHUB_REMOTE_URL }, }); try { + const projectName = path.basename(workspace.repoPath); await gotoAppShell(page); - await waitForSidebarProject(page, "test-owner/test-repo"); + await waitForSidebarProject(page, projectName); await waitForSidebarWorkspace(page, workspace.workspaceId); const projectRow = page .locator('[data-testid^="sidebar-project-row-"]') - .filter({ hasText: "test-owner/test-repo" }) + .filter({ hasText: projectName }) .first(); await expect(projectRow).toBeVisible({ timeout: 30_000 }); - await expect(projectRow).not.toContainText(path.basename(workspace.repoPath)); + await expect(projectRow).not.toContainText("test-owner/test-repo"); } finally { await workspace.cleanup(); } @@ -96,21 +97,24 @@ test.describe("Sidebar workspace list", () => { } }); - test("workspace header shows correct title and subtitle", async ({ page }) => { + test("workspace header uses the selected folder name instead of its GitHub remote", async ({ + page, + }) => { const workspace = await seedWorkspace({ repoPrefix: "sidebar-header-", repo: { withRemote: true, originUrl: GITHUB_REMOTE_URL }, }); try { + const projectName = path.basename(workspace.repoPath); await gotoAppShell(page); - await waitForSidebarProject(page, "test-owner/test-repo"); + await waitForSidebarProject(page, projectName); await waitForSidebarWorkspace(page, workspace.workspaceId); await openWorkspaceFromSidebar(page, workspace.workspaceId); await expectWorkspaceHeader(page, { title: workspace.workspaceName, - subtitle: "test-owner/test-repo", + subtitle: projectName, }); } finally { await workspace.cleanup(); diff --git a/packages/app/e2e/worktree-restore-after-restart.spec.ts b/packages/app/e2e/worktree-restore-after-restart.spec.ts index 4ef4c3dce..a11df5daf 100644 --- a/packages/app/e2e/worktree-restore-after-restart.spec.ts +++ b/packages/app/e2e/worktree-restore-after-restart.spec.ts @@ -1,9 +1,10 @@ import { randomUUID } from "node:crypto"; import { existsSync } from "node:fs"; -import { expect, test } from "./fixtures"; +import { expect, test, type Page } from "@playwright/test"; import { gotoAppShell } from "./helpers/app"; import { createIdleAgent, expectSessionRowArchived, openSessions } from "./helpers/archive-tab"; -import { restartTestDaemon } from "./helpers/daemon-restart"; +import { buildCreateAgentPreferences, buildSeededHost } from "./helpers/daemon-registry"; +import { startIsolatedHostDaemon, type IsolatedHostDaemon } from "./helpers/isolated-host-daemon"; import { archiveWorkspaceFromDaemon, connectNewWorkspaceDaemonClient, @@ -11,11 +12,12 @@ import { openProjectViaDaemon, } from "./helpers/new-workspace"; import { connectSeedClient } from "./helpers/seed-client"; -import { getServerId } from "./helpers/server-id"; import { createTempGitRepo } from "./helpers/workspace"; import { waitForSidebarHydration } from "./helpers/workspace-ui"; test.describe("Worktree restore after daemon restart", () => { + const serverId = `srv_worktree_restart_${randomUUID().replaceAll("-", "").slice(0, 12)}`; + let daemon: IsolatedHostDaemon; let client: Awaited>; let worktreeClient: Awaited>; let tempRepo: { path: string; cleanup: () => Promise }; @@ -25,8 +27,9 @@ test.describe("Worktree restore after daemon restart", () => { test.describe.configure({ retries: 0, timeout: 180_000 }); test.beforeEach(async () => { - client = await connectSeedClient(); - worktreeClient = await connectNewWorkspaceDaemonClient(); + daemon = await startIsolatedHostDaemon(serverId); + client = await connectSeedClient({ port: daemon.port }); + worktreeClient = await connectNewWorkspaceDaemonClient({ port: daemon.port }); tempRepo = await createTempGitRepo("wt-restart-"); }); @@ -42,13 +45,33 @@ test.describe("Worktree restore after daemon restart", () => { await client?.close().catch(() => undefined); await worktreeClient?.close().catch(() => undefined); await tempRepo?.cleanup().catch(() => undefined); + await daemon?.close().catch(() => undefined); }); + async function seedBrowser(page: Page) { + const nowIso = new Date().toISOString(); + await page.addInitScript( + ({ host, preferences }) => { + localStorage.setItem("@paseo:e2e", "1"); + localStorage.setItem("@paseo:daemon-registry", JSON.stringify([host])); + localStorage.removeItem("@paseo:settings"); + localStorage.setItem("@paseo:create-agent-preferences", JSON.stringify(preferences)); + }, + { + host: buildSeededHost({ + serverId, + endpoint: `127.0.0.1:${daemon.port}`, + label: "restart daemon", + nowIso, + }), + preferences: buildCreateAgentPreferences(serverId), + }, + ); + } + test("after archiving a worktree and restarting the daemon, History shows the worktree branch (not main) before any restore", async ({ page, }) => { - const serverId = getServerId(); - // A paseo worktree is cut on its own branch named after the slug, and the // worktree workspace is displayed under the same name. These are the values // the History table cells must show after restore — never "main". @@ -76,14 +99,16 @@ test.describe("Worktree restore after daemon restart", () => { .poll(() => existsSync(worktree.workspaceDirectory), { timeout: 30_000 }) .toBe(false); - // Bounce the isolated test daemon on the SAME home and port so it rebuilds - // all workspace/agent links from persisted state. Then reconnect both clients. + // Restart this spec's daemon on the same home and port so it rebuilds all + // workspace/agent links from persisted state without replacing the shared + // Playwright daemon owned by global setup. await client.close().catch(() => undefined); await worktreeClient.close().catch(() => undefined); - await restartTestDaemon(); - client = await connectSeedClient(); - worktreeClient = await connectNewWorkspaceDaemonClient(); + await daemon.restart(); + client = await connectSeedClient({ port: daemon.port }); + worktreeClient = await connectNewWorkspaceDaemonClient({ port: daemon.port }); + await seedBrowser(page); await gotoAppShell(page); await waitForSidebarHydration(page); await openSessions(page); diff --git a/packages/app/e2e/worktree-restore.spec.ts b/packages/app/e2e/worktree-restore.spec.ts index 4497b568e..480bdbad4 100644 --- a/packages/app/e2e/worktree-restore.spec.ts +++ b/packages/app/e2e/worktree-restore.spec.ts @@ -249,7 +249,7 @@ test.describe("Worktree restore", () => { try { await page.getByTestId("workspace-recovery-action").click(); await expect(page.getByTestId("workspace-recovery-error")).toHaveText( - "The project directory needed to restore this worktree no longer exists.", + "The source repository needed to restore this worktree no longer exists.", ); await expect(page.getByTestId("workspace-recovery-action")).toHaveText("Retry"); expect(existsSync(worktree.workspaceDirectory)).toBe(false); diff --git a/packages/app/src/add-project-flow/model.test.ts b/packages/app/src/add-project-flow/model.test.ts index 8319771c3..ff496f53c 100644 --- a/packages/app/src/add-project-flow/model.test.ts +++ b/packages/app/src/add-project-flow/model.test.ts @@ -16,6 +16,7 @@ import { type AddProjectHost, } from "./model"; import { + addProjectMethodEmptyText, buildAddProjectMethods, buildCloneLocationOptions, buildManualGithubRepositoryChoices, @@ -110,6 +111,13 @@ describe("Add Project navigation", () => { }); describe("Add Project options", () => { + it("hides every mutating method when the host lacks stable project identity", () => { + const outdatedHost = { ...HOST, canAddProject: false }; + + expect(buildAddProjectMethods(outdatedHost)).toEqual([]); + expect(addProjectMethodEmptyText(outdatedHost)).toBe("Update the host to use Add Project."); + }); + it("keeps host-upgrade methods discoverable while hiding local-only Browse", () => { expect( buildAddProjectMethods({ diff --git a/packages/app/src/add-project-flow/options.ts b/packages/app/src/add-project-flow/options.ts index 0c968cc35..1c9fec5bf 100644 --- a/packages/app/src/add-project-flow/options.ts +++ b/packages/app/src/add-project-flow/options.ts @@ -34,14 +34,13 @@ export function filterAddProjectHosts(hosts: AddProjectHost[], query: string): A } export function buildAddProjectMethods(host: AddProjectHost): AddProjectMethodOption[] { + if (!host.canAddProject) return []; const options: AddProjectMethodOption[] = []; - if (host.canAddProject) { - options.push({ - id: "directory-search", - label: "Search for directory", - description: `Find a directory on ${host.label}`, - }); - } + options.push({ + id: "directory-search", + label: "Search for directory", + description: `Find a directory on ${host.label}`, + }); if (host.canBrowse) { options.push({ id: "browse", @@ -66,6 +65,12 @@ export function buildAddProjectMethods(host: AddProjectHost): AddProjectMethodOp return options; } +export function addProjectMethodEmptyText(host: AddProjectHost | null): string { + return host?.canAddProject === false + ? "Update the host to use Add Project." + : "No matching options"; +} + function githubMethodDescription(host: AddProjectHost): string { if (!host.canCloneGithubRepositories) { return "Update this host to clone GitHub repositories"; diff --git a/packages/app/src/components/add-project-flow.tsx b/packages/app/src/components/add-project-flow.tsx index 36aa29d97..cfdda3d86 100644 --- a/packages/app/src/components/add-project-flow.tsx +++ b/packages/app/src/components/add-project-flow.tsx @@ -45,6 +45,7 @@ import { } from "@/add-project-flow/model"; import { buildAddProjectMethods, + addProjectMethodEmptyText, buildCloneLocationOptions, buildManualGithubRepositoryChoices, buildSuggestedParentDirectories, @@ -161,9 +162,10 @@ function progressText(page: AddProjectPage): string { return "Adding project..."; } -function emptyText(page: AddProjectPage): string { +function emptyText(page: AddProjectPage, host: AddProjectHost | null): string { if (page.kind === "host") return "No connected hosts"; if (page.kind === "github-search") return "Enter a GitHub URL or owner/repo"; + if (page.kind === "method") return addProjectMethodEmptyText(host); return "No matching options"; } @@ -297,6 +299,8 @@ export function AddProjectFlow({ request, onClose }: AddProjectFlowProps) { const hostIds = useMemo(() => hosts.map((host) => host.serverId), [hosts]); const connectionStatuses = useHostRuntimeConnectionStatuses(hostIds); const projectAddByHost = useHostFeatureMap(hostIds, "projectAdd"); + // COMPAT(stableProjectIdentity): added in v0.1.109, remove gate after 2027-01-15. + const stableProjectIdentityByHost = useHostFeatureMap(hostIds, "stableProjectIdentity"); // COMPAT(projectGithubClone): added in v0.1.108, remove gate after 2027-01-15. const githubCloneByHost = useHostFeatureMap(hostIds, "projectGithubClone"); // COMPAT(workspaceGithubRepositorySearch): added in v0.1.108, remove gate after 2027-01-15. @@ -308,7 +312,9 @@ export function AddProjectFlow({ request, onClose }: AddProjectFlowProps) { () => hosts.flatMap((host) => { if (connectionStatuses.get(host.serverId) !== "online") return []; - const canAddProject = projectAddByHost.get(host.serverId) === true; + const canAddProject = + projectAddByHost.get(host.serverId) === true && + stableProjectIdentityByHost.get(host.serverId) === true; return [ { serverId: host.serverId, @@ -329,6 +335,7 @@ export function AddProjectFlow({ request, onClose }: AddProjectFlowProps) { hosts, localServerId, projectAddByHost, + stableProjectIdentityByHost, ], ); const [state, setState] = useState(() => @@ -893,7 +900,7 @@ export function AddProjectFlow({ request, onClose }: AddProjectFlowProps) { rows.length === 0 && page.kind !== "new-directory-name" ? ( - {emptyText(page)} + {emptyText(page, host ?? null)} ) : null} diff --git a/packages/app/src/e2e-metro-readiness.test.ts b/packages/app/src/e2e-metro-readiness.test.ts new file mode 100644 index 000000000..0ba5df9e3 --- /dev/null +++ b/packages/app/src/e2e-metro-readiness.test.ts @@ -0,0 +1,65 @@ +import { createServer, type Server } from "node:http"; +import { afterEach, expect, test } from "vitest"; + +import { waitForMetro } from "../e2e/global-setup"; + +class MetroPort { + private response = { status: 500, body: "fallback" }; + + private constructor( + readonly port: number, + private readonly server: Server, + ) {} + + static async listen(): Promise { + let endpoint!: MetroPort; + const server = createServer((_request, response) => { + response.writeHead(endpoint.response.status, { "content-type": "text/plain" }); + response.end(endpoint.response.body); + }); + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); + const address = server.address(); + if (!address || typeof address === "string") { + server.close(); + throw new Error("Failed to listen for Metro readiness test"); + } + endpoint = new MetroPort(address.port, server); + return endpoint; + } + + serveMetro(): void { + this.response = { status: 200, body: "packager-status:running" }; + } + + async close(): Promise { + await new Promise((resolve, reject) => { + this.server.close((error) => { + if (error) { + reject(error); + return; + } + resolve(); + }); + }); + } +} + +let endpoint: MetroPort | null = null; + +afterEach(async () => { + await endpoint?.close(); + endpoint = null; +}); + +test("Metro readiness rejects another HTTP listener on the selected port", async () => { + endpoint = await MetroPort.listen(); + + await expect(waitForMetro(endpoint.port, { label: "Metro", timeoutMs: 150 })).rejects.toThrow( + "Expected Metro status", + ); + + endpoint.serveMetro(); + await expect(waitForMetro(endpoint.port, { label: "Metro", timeoutMs: 150 })).resolves.toBe( + undefined, + ); +}); diff --git a/packages/app/src/hooks/use-open-project.ts b/packages/app/src/hooks/use-open-project.ts index 29f571413..98bd07295 100644 --- a/packages/app/src/hooks/use-open-project.ts +++ b/packages/app/src/hooks/use-open-project.ts @@ -16,7 +16,8 @@ export function useOpenProject( const isConnected = useHostRuntimeIsConnected(normalizedServerId); const canAddProject = useSessionStore((state) => normalizedServerId - ? state.sessions[normalizedServerId]?.serverInfo?.features?.projectAdd === true + ? state.sessions[normalizedServerId]?.serverInfo?.features?.projectAdd === true && + state.sessions[normalizedServerId]?.serverInfo?.features?.stableProjectIdentity === true : false, ); const addEmptyProject = useSessionStore((state) => state.addEmptyProject); diff --git a/packages/app/src/runtime/directory-sync/index.test.ts b/packages/app/src/runtime/directory-sync/index.test.ts index d610f04be..727967713 100644 --- a/packages/app/src/runtime/directory-sync/index.test.ts +++ b/packages/app/src/runtime/directory-sync/index.test.ts @@ -1,14 +1,43 @@ import { afterEach, describe, expect, it } from "vitest"; import type { DaemonClient } from "@getpaseo/client/internal/daemon-client"; +import type { SessionOutboundMessage } from "@getpaseo/protocol/messages"; import { useSessionStore } from "@/stores/session-store"; import { DirectoryRefreshSupersededError, DirectorySync } from "./index"; +type WorkspaceFetchResult = Awaited>; + class FakeDirectoryClient { fetchAgentsCalls = 0; fetchWorkspacesCalls = 0; + private pendingWorkspaceFetch: Promise | null = null; + private readonly handlers = new Map< + SessionOutboundMessage["type"], + Set<(message: SessionOutboundMessage) => void> + >(); - on(): () => void { - return () => undefined; + on( + type: TType, + handler: (message: Extract) => void, + ): () => void { + const handlers = this.handlers.get(type) ?? new Set(); + const registered = handler as unknown as (message: SessionOutboundMessage) => void; + handlers.add(registered); + this.handlers.set(type, handlers); + return () => handlers.delete(registered); + } + + emit( + message: Extract, + ): void { + for (const handler of this.handlers.get(message.type) ?? []) handler(message); + } + + holdWorkspaceFetch(): (result: WorkspaceFetchResult) => void { + let complete!: (result: WorkspaceFetchResult) => void; + this.pendingWorkspaceFetch = new Promise((resolve) => { + complete = resolve; + }); + return complete; } async fetchAgents(): Promise>> { @@ -20,8 +49,13 @@ class FakeDirectoryClient { }; } - async fetchWorkspaces(): Promise>> { + async fetchWorkspaces(): Promise { this.fetchWorkspacesCalls += 1; + if (this.pendingWorkspaceFetch) { + const pending = this.pendingWorkspaceFetch; + this.pendingWorkspaceFetch = null; + return pending; + } return { requestId: "workspaces", entries: [], @@ -110,4 +144,114 @@ describe("DirectorySync session readiness", () => { expect(client.fetchAgentsCalls).toBe(1); directory.dispose(); }); + + it("buffers workspace and project updates in the same hydration transaction", async () => { + const serverId = "workspace-project-transaction"; + const { client, directory } = createDirectory(serverId); + const store = useSessionStore.getState(); + store.initializeSession(serverId, client as unknown as DaemonClient, 1); + store.updateSessionServerInfo(serverId, { + serverId, + hostname: null, + version: "test", + features: { workspaceMultiplicity: true }, + }); + const completeFetch = client.holdWorkspaceFetch(); + + const refresh = directory.refreshWorkspaces({ subscribe: true }); + await Promise.resolve(); + client.emit({ + type: "workspace_update", + payload: { + kind: "remove", + id: "removed-workspace", + emptyProject: { + projectId: "workspace-project", + projectDisplayName: "Project from workspace update", + projectRootPath: "/repo/workspace-project", + projectKind: "git", + }, + }, + }); + client.emit({ + type: "project.update", + payload: { + kind: "upsert", + project: { + projectId: "snapshot-project", + projectDisplayName: "Renamed during hydration", + projectRootPath: "/moved/snapshot-project", + projectKind: "directory", + }, + }, + }); + completeFetch({ + requestId: "workspaces", + entries: [], + emptyProjects: [ + { + projectId: "snapshot-project", + projectDisplayName: "Stale snapshot project", + projectRootPath: "/repo/snapshot-project", + projectKind: "git", + }, + ], + pageInfo: { hasMore: false, nextCursor: null, prevCursor: null }, + }); + await refresh; + + const emptyProjects = useSessionStore.getState().sessions[serverId]?.emptyProjects; + expect(Array.from(emptyProjects?.keys() ?? [])).toEqual([ + "snapshot-project", + "workspace-project", + ]); + expect(emptyProjects?.get("snapshot-project")).toMatchObject({ + projectDisplayName: "Renamed during hydration", + projectRootPath: "/moved/snapshot-project", + projectKind: "directory", + }); + expect(emptyProjects?.get("workspace-project")).toMatchObject({ + projectDisplayName: "Project from workspace update", + }); + directory.dispose(); + }); + + it("buffers project updates from the online epoch before workspace hydration starts", async () => { + const serverId = "project-before-workspace-hydration"; + const { client, directory } = createDirectory(serverId); + const store = useSessionStore.getState(); + store.initializeSession(serverId, client as unknown as DaemonClient, 1); + store.updateSessionServerInfo(serverId, { + serverId, + hostname: null, + version: "test", + features: { workspaceMultiplicity: true }, + }); + + client.emit({ + type: "project.update", + payload: { + kind: "upsert", + project: { + projectId: "early-project", + projectDisplayName: "Early project", + projectRootPath: "/repo/early-project", + projectKind: "git", + }, + }, + }); + + expect(useSessionStore.getState().sessions[serverId]?.hasHydratedWorkspaces).toBe(false); + + await directory.refreshWorkspaces({ subscribe: true }); + + expect(useSessionStore.getState().sessions[serverId]?.hasHydratedWorkspaces).toBe(true); + expect( + useSessionStore.getState().sessions[serverId]?.emptyProjects.get("early-project"), + ).toMatchObject({ + projectDisplayName: "Early project", + projectRootPath: "/repo/early-project", + }); + directory.dispose(); + }); }); diff --git a/packages/app/src/runtime/directory-sync/index.ts b/packages/app/src/runtime/directory-sync/index.ts index a181bcd0c..317eabf6a 100644 --- a/packages/app/src/runtime/directory-sync/index.ts +++ b/packages/app/src/runtime/directory-sync/index.ts @@ -107,6 +107,10 @@ export class DirectorySync { if (!connection.client || connection.status !== "online") return true; const client = connection.client; const source = connection.source; + this.workspaceTransactions.begin(source, () => ({ + workspaces: new Map(), + emptyProjects: new Map(), + })); const subscriptions = [ client.on("agent_update", (message) => { if (message.type !== "agent_update" || !this.isCurrent(client, source)) return; @@ -119,6 +123,12 @@ export class DirectorySync { this.workspaces.applyDelta(message.payload); } }), + client.on("project.update", (message) => { + if (message.type !== "project.update" || !this.isCurrent(client, source)) return; + if (!this.workspaceTransactions.record(source, message.payload)) { + this.workspaces.applyDelta(message.payload); + } + }), client.on("agent_deleted", (message) => { if (message.type === "agent_deleted" && this.isCurrent(client, source)) { this.agents.remove(message.payload.agentId); diff --git a/packages/app/src/runtime/directory-sync/workspace-replica.test.ts b/packages/app/src/runtime/directory-sync/workspace-replica.test.ts index 48b55c002..f72b78102 100644 --- a/packages/app/src/runtime/directory-sync/workspace-replica.test.ts +++ b/packages/app/src/runtime/directory-sync/workspace-replica.test.ts @@ -55,3 +55,91 @@ it("commits workspace and project-parent state with filtered removals", () => { expect(Array.from(session?.emptyProjects.keys() ?? [])).toEqual(["empty"]); store.clearSession(serverId); }); + +it("commits the authoritative snapshot before buffered project updates", () => { + const serverId = "project-update-replica"; + const store = useSessionStore.getState(); + store.initializeSession(serverId, null as unknown as DaemonClient); + const replica = new WorkspaceDirectoryReplica(serverId); + const attachedMain = normalizeWorkspaceDescriptor(workspace("attached-main", "attached")); + const attachedFeature = normalizeWorkspaceDescriptor(workspace("attached-feature", "attached")); + const removed = normalizeWorkspaceDescriptor(workspace("removed", "removed")); + const unrelated = normalizeWorkspaceDescriptor(workspace("unrelated", "unrelated")); + const staleAttachedProject = normalizeEmptyProjectDescriptor({ + projectId: "attached", + projectDisplayName: "Stale attached project", + projectRootPath: "/repo/attached", + projectKind: "git", + }); + const removedProject = normalizeEmptyProjectDescriptor({ + projectId: "removed", + projectDisplayName: "Removed project", + projectRootPath: "/repo/removed", + projectKind: "git", + }); + const unchangedEmptyProject = normalizeEmptyProjectDescriptor({ + projectId: "unchanged-empty", + projectDisplayName: "Unchanged empty project", + projectRootPath: "/repo/unchanged-empty", + projectKind: "git", + }); + + replica.commitSnapshot( + { + workspaces: new Map([ + [attachedMain.id, attachedMain], + [attachedFeature.id, attachedFeature], + [removed.id, removed], + [unrelated.id, unrelated], + ]), + emptyProjects: new Map([ + [staleAttachedProject.projectId, staleAttachedProject], + [removedProject.projectId, removedProject], + [unchangedEmptyProject.projectId, unchangedEmptyProject], + ]), + }, + [ + { + kind: "upsert", + project: { + projectId: "attached", + projectDisplayName: "Renamed attached project", + projectCustomName: "Personal name", + projectRootPath: "/moved/attached", + projectKind: "directory", + }, + }, + { + kind: "upsert", + project: { + projectId: "new-empty", + projectDisplayName: "New empty project", + projectRootPath: "/repo/new-empty", + projectKind: "directory", + }, + }, + { kind: "remove", projectId: "removed" }, + ], + ); + + const session = useSessionStore.getState().sessions[serverId]; + expect(session?.workspaces.get(attachedMain.id)).toMatchObject({ + projectDisplayName: "Renamed attached project", + projectCustomName: "Personal name", + projectRootPath: "/moved/attached", + projectKind: "directory", + }); + expect(session?.workspaces.get(attachedFeature.id)).toMatchObject({ + projectDisplayName: "Renamed attached project", + projectRootPath: "/moved/attached", + }); + expect(session?.workspaces.has(removed.id)).toBe(false); + expect(session?.workspaces.get(unrelated.id)).toBe(unrelated); + expect(Array.from(session?.emptyProjects.keys() ?? [])).toEqual(["unchanged-empty", "new-empty"]); + expect(session?.emptyProjects.get("unchanged-empty")).toBe(unchangedEmptyProject); + expect(session?.emptyProjects.get("new-empty")).toMatchObject({ + projectDisplayName: "New empty project", + projectRootPath: "/repo/new-empty", + }); + store.clearSession(serverId); +}); diff --git a/packages/app/src/runtime/directory-sync/workspace-replica.ts b/packages/app/src/runtime/directory-sync/workspace-replica.ts index f2b69c853..e78a2b557 100644 --- a/packages/app/src/runtime/directory-sync/workspace-replica.ts +++ b/packages/app/src/runtime/directory-sync/workspace-replica.ts @@ -14,20 +14,50 @@ import { export type WorkspaceDirectoryDelta = Extract< SessionOutboundMessage, - { type: "workspace_update" } + { type: "workspace_update" | "project.update" } >["payload"]; +type ProjectDirectoryDelta = Extract["payload"]; export interface WorkspaceDirectorySnapshot { workspaces: Map; emptyProjects: Map; } +function applyProjectDelta( + snapshot: WorkspaceDirectorySnapshot, + delta: ProjectDirectoryDelta, +): void { + if (delta.kind === "remove") { + snapshot.emptyProjects.delete(delta.projectId); + for (const [workspaceId, workspace] of snapshot.workspaces) { + if (workspace.projectId === delta.projectId) snapshot.workspaces.delete(workspaceId); + } + return; + } + + const project = normalizeEmptyProjectDescriptor(delta.project); + let hasAttachedWorkspace = false; + for (const [workspaceId, workspace] of snapshot.workspaces) { + if (workspace.projectId !== project.projectId) continue; + hasAttachedWorkspace = true; + snapshot.workspaces.set(workspaceId, { + ...workspace, + projectDisplayName: project.projectDisplayName, + projectCustomName: project.projectCustomName, + projectRootPath: project.projectRootPath, + projectKind: project.projectKind, + }); + } + if (hasAttachedWorkspace) snapshot.emptyProjects.delete(project.projectId); + else snapshot.emptyProjects.set(project.projectId, project); +} + export class WorkspaceDirectoryReplica { constructor(private readonly serverId: string) {} applyDelta(delta: WorkspaceDirectoryDelta): void { const state = this.reconcile(this.read(), [delta]); - this.commit(state, delta.kind === "remove" ? [delta.id] : []); + this.commit(state, delta.kind === "remove" && "id" in delta ? [delta.id] : []); } commitSnapshot( @@ -35,9 +65,10 @@ export class WorkspaceDirectoryReplica { deltas: readonly WorkspaceDirectoryDelta[], ): void { const removedWorkspaceIds = deltas.flatMap((delta) => - delta.kind === "remove" ? [delta.id] : [], + delta.kind === "remove" && "id" in delta ? [delta.id] : [], ); this.commit(this.reconcile(snapshot, deltas), removedWorkspaceIds); + useSessionStore.getState().setHasHydratedWorkspaces(this.serverId, true); } private read(): WorkspaceDirectorySnapshot { @@ -60,6 +91,10 @@ export class WorkspaceDirectoryReplica { } } for (const delta of deltas) { + if ("projectId" in delta || "project" in delta) { + applyProjectDelta({ workspaces, emptyProjects }, delta); + continue; + } if (delta.kind === "remove") { workspaces.delete(delta.id); if (delta.emptyProject) { @@ -84,7 +119,6 @@ export class WorkspaceDirectoryReplica { const store = useSessionStore.getState(); store.setWorkspaces(this.serverId, snapshot.workspaces); store.setEmptyProjects(this.serverId, snapshot.emptyProjects.values()); - store.setHasHydratedWorkspaces(this.serverId, true); for (const workspaceId of removedWorkspaceIds) { clearWorkspaceArchivePending({ serverId: this.serverId, workspaceId }); useWorkspaceSetupStore.getState().removeWorkspace({ serverId: this.serverId, workspaceId }); diff --git a/packages/app/src/stores/session-store.test.ts b/packages/app/src/stores/session-store.test.ts index 4353c4ea8..1588fd114 100644 --- a/packages/app/src/stores/session-store.test.ts +++ b/packages/app/src/stores/session-store.test.ts @@ -17,6 +17,7 @@ function createWorkspace( id: input.id, projectId: input.projectId ?? "project-1", projectDisplayName: input.projectDisplayName ?? "Project 1", + projectCustomName: input.projectCustomName ?? null, projectRootPath: input.projectRootPath ?? "/repo", workspaceDirectory: input.workspaceDirectory ?? "/repo", projectKind: input.projectKind ?? "git", diff --git a/packages/app/src/stores/session-store.ts b/packages/app/src/stores/session-store.ts index ca2e25e5b..baa5d26ba 100644 --- a/packages/app/src/stores/session-store.ts +++ b/packages/app/src/stores/session-store.ts @@ -320,6 +320,8 @@ export interface AgentTimelineCursorState { endSeq: number; } +export type WorkspaceRestoreStatus = "restoring" | "failed" | "needs-host-upgrade"; + // Per-session state export interface SessionState { serverId: string; @@ -368,6 +370,9 @@ export interface SessionState { // Project parents with no active workspaces, keyed by projectId. The // `emptyProjects` name is the existing protocol/store projection. emptyProjects: Map; + // Transient restore state for archived workspaces, keyed by normalized + // workspaceId. Cleared in mergeWorkspaces when the descriptor lands. + restoringWorkspaces: Map; // Permissions pendingPermissions: Map; @@ -489,6 +494,13 @@ interface SessionStoreActions { setEmptyProjects: (serverId: string, emptyProjects: Iterable) => void; addEmptyProject: (serverId: string, emptyProject: EmptyProjectDescriptor) => void; removeEmptyProject: (serverId: string, projectId: string) => void; + setWorkspaceRestoreStatus: ( + serverId: string, + workspaceId: string, + status: WorkspaceRestoreStatus, + ) => void; + clearWorkspaceRestoreStatus: (serverId: string, workspaceId: string) => void; + // Agent activity timestamps setAgentLastActivity: (agentId: string, timestamp: Date) => void; setAgentLastActivityBatch: ( @@ -567,6 +579,7 @@ function createInitialSessionState( agentDetails: new Map(), workspaces: new Map(), emptyProjects: new Map(), + restoringWorkspaces: new Map(), pendingPermissions: new Map(), fileExplorer: new Map(), queuedMessages: new Map(), @@ -1347,6 +1360,54 @@ export const useSessionStore = create()( }); }, + setWorkspaceRestoreStatus: (serverId, workspaceId, status) => { + set((prev) => { + const session = prev.sessions[serverId]; + if (!session) { + return prev; + } + if (session.restoringWorkspaces.get(workspaceId) === status) { + return prev; + } + // A late dir-gone timeout must not override a successful restore: + // only mark failed while still restoring and the descriptor is absent. + if ( + status === "failed" && + (session.restoringWorkspaces.get(workspaceId) !== "restoring" || + session.workspaces.has(workspaceId)) + ) { + return prev; + } + const next = new Map(session.restoringWorkspaces); + next.set(workspaceId, status); + return { + ...prev, + sessions: { + ...prev.sessions, + [serverId]: { ...session, restoringWorkspaces: next }, + }, + }; + }); + }, + + clearWorkspaceRestoreStatus: (serverId, workspaceId) => { + set((prev) => { + const session = prev.sessions[serverId]; + if (!session || !session.restoringWorkspaces.has(workspaceId)) { + return prev; + } + const next = new Map(session.restoringWorkspaces); + next.delete(workspaceId); + return { + ...prev, + sessions: { + ...prev.sessions, + [serverId]: { ...session, restoringWorkspaces: next }, + }, + }; + }); + }, + mergeWorkspaces: (serverId, workspaces) => { const nextEntries = Array.from(workspaces); set((prev) => { @@ -1360,10 +1421,18 @@ export const useSessionStore = create()( // empty: prune any stale empty descriptor so it stops governing the // project's rendered metadata. const nextEmptyProjects = new Map(session.emptyProjects); + // A descriptor arriving is the success signal for a pending restore: + // clear it at the source so every entry point converges to "ready". + let nextRestoring: Map | null = null; for (const workspace of nextEntries) { if (nextEmptyProjects.delete(workspace.projectId)) { changed = true; } + if (session.restoringWorkspaces.has(workspace.id)) { + nextRestoring ??= new Map(session.restoringWorkspaces); + nextRestoring.delete(workspace.id); + changed = true; + } const existing = next.get(workspace.id); const nextWorkspace = preserveWorkspaceDescriptorIdentity(workspace, existing); if (existing === nextWorkspace) { @@ -1383,6 +1452,7 @@ export const useSessionStore = create()( ...session, workspaces: next, emptyProjects: nextEmptyProjects, + restoringWorkspaces: nextRestoring ?? session.restoringWorkspaces, }, }, }; @@ -1601,3 +1671,14 @@ export const useSessionStore = create()( }; }), ); + +export function useWorkspaceRestoreStatus( + serverId: string | null, + workspaceId: string | null, +): WorkspaceRestoreStatus | null { + return useSessionStore((state) => + serverId && workspaceId + ? (state.sessions[serverId]?.restoringWorkspaces.get(workspaceId) ?? null) + : null, + ); +} diff --git a/packages/client/src/daemon-client.test.ts b/packages/client/src/daemon-client.test.ts index ad41bddda..723ffd91b 100644 --- a/packages/client/src/daemon-client.test.ts +++ b/packages/client/src/daemon-client.test.ts @@ -646,6 +646,7 @@ test("advertises client capabilities in hello", async () => { protocolVersion: 1, capabilities: { custom_mode_icons: true, + project_updates: true, provider_subagents: true, reasoning_merge_enum: true, terminal_reflowable_snapshot: true, @@ -657,6 +658,32 @@ test("advertises client capabilities in hello", async () => { }); }); +test("allows callers to disable default client capabilities", async () => { + const mock = createMockTransport(); + const client = new DaemonClient({ + url: "ws://test", + clientId: "clsk_capability_override_test", + reconnect: { enabled: false }, + transportFactory: () => mock.transport, + capabilities: { + [CLIENT_CAPS.projectUpdates]: false, + }, + }); + clients.push(client); + + const connectPromise = client.connect(); + mock.triggerOpen({ preserveSent: true }); + await connectPromise; + + const hello = z + .object({ + type: z.literal("hello"), + capabilities: z.record(z.unknown()), + }) + .parse(JSON.parse(assertStr(mock.sent[0]))); + expect(hello.capabilities[CLIENT_CAPS.projectUpdates]).toBe(false); +}); + test("sends new-agent run options when creating schedules", async () => { const logger = createMockLogger(); const mock = createMockTransport(); diff --git a/packages/client/src/daemon-client.ts b/packages/client/src/daemon-client.ts index 91bd350a3..f7731112a 100644 --- a/packages/client/src/daemon-client.ts +++ b/packages/client/src/daemon-client.ts @@ -249,6 +249,10 @@ export type DaemonEvent = workspaceId: string; payload: Extract["payload"]; } + | { + type: "project.update"; + payload: Extract["payload"]; + } | { type: "workspace_setup_progress"; workspaceId: string; @@ -5111,6 +5115,7 @@ export class DaemonClient { [CLIENT_CAPS.reasoningMergeEnum]: true, [CLIENT_CAPS.terminalReflowableSnapshot]: true, [CLIENT_CAPS.providerSubagents]: true, + [CLIENT_CAPS.projectUpdates]: true, ...this.config.capabilities, }, ...(this.config.appVersion ? { appVersion: this.config.appVersion } : {}), @@ -5581,6 +5586,8 @@ export class DaemonClient { workspaceId: msg.payload.kind === "upsert" ? msg.payload.workspace.id : msg.payload.id, payload: msg.payload, }; + case "project.update": + return { type: "project.update", payload: msg.payload }; case "workspace_setup_progress": return { type: "workspace_setup_progress", diff --git a/packages/protocol/src/client-capabilities.ts b/packages/protocol/src/client-capabilities.ts index dcf428ffd..f9ccbeec9 100644 --- a/packages/protocol/src/client-capabilities.ts +++ b/packages/protocol/src/client-capabilities.ts @@ -18,6 +18,8 @@ export const CLIENT_CAPS = { // COMPAT(providerSubagents): added in v0.1.107. The daemon emits provider-owned // child descriptors and timelines only to clients that understand the new messages. providerSubagents: "provider_subagents", + // COMPAT(projectUpdates): added in v0.1.109, remove gate after 2027-01-15. + projectUpdates: "project_updates", browserHost: "browser_host", } as const; diff --git a/packages/protocol/src/messages.ts b/packages/protocol/src/messages.ts index 5d3320589..9918fb533 100644 --- a/packages/protocol/src/messages.ts +++ b/packages/protocol/src/messages.ts @@ -2687,6 +2687,8 @@ export const ServerInfoStatusPayloadSchema = z forgeProviders: z.boolean().optional(), // COMPAT(selectiveAgentTimeline): added in v0.1.106, remove after 2027-01-12. selectiveAgentTimeline: z.boolean().optional(), + // COMPAT(stableProjectIdentity): added in v0.1.109, remove gate after 2027-01-15. + stableProjectIdentity: z.boolean().optional(), }) .optional(), }) @@ -3135,6 +3137,14 @@ export const WorkspaceUpdateMessageSchema = z.object({ ]), }); +export const ProjectUpdateMessageSchema = z.object({ + type: z.literal("project.update"), + payload: z.discriminatedUnion("kind", [ + z.object({ kind: z.literal("upsert"), project: WorkspaceProjectDescriptorPayloadSchema }), + z.object({ kind: z.literal("remove"), projectId: z.string() }), + ]), +}); + export const ScriptStatusUpdateMessageSchema = z.object({ type: z.literal("script_status_update"), payload: z.object({ @@ -4847,6 +4857,7 @@ export const SessionOutboundMessageSchema = z.discriminatedUnion("type", [ ArtifactMessageSchema, AgentUpdateMessageSchema, WorkspaceUpdateMessageSchema, + ProjectUpdateMessageSchema, ScriptStatusUpdateMessageSchema, WorkspaceSetupProgressMessageSchema, WorkspaceSetupStatusResponseMessageSchema, @@ -5409,6 +5420,7 @@ export const WSHelloMessageSchema = z.object({ [CLIENT_CAPS.customModeIcons]: z.boolean().optional(), [CLIENT_CAPS.terminalReflowableSnapshot]: z.boolean().optional(), [CLIENT_CAPS.providerSubagents]: z.boolean().optional(), + [CLIENT_CAPS.projectUpdates]: z.boolean().optional(), [CLIENT_CAPS.browserHost]: BrowserAutomationHostCapabilitySchema.optional(), }) .passthrough() diff --git a/packages/server/src/server/agent/create-agent-lifecycle-dispatch.ts b/packages/server/src/server/agent/create-agent-lifecycle-dispatch.ts index 31f77500d..25f616922 100644 --- a/packages/server/src/server/agent/create-agent-lifecycle-dispatch.ts +++ b/packages/server/src/server/agent/create-agent-lifecycle-dispatch.ts @@ -3,11 +3,7 @@ import type pino from "pino"; import type { ForgeService } from "../../services/forge-service.js"; import { isPaseoOwnedWorktreeCwd } from "../../utils/worktree.js"; -import { - archiveByScope, - type ActiveWorkspaceRef, - resolveWorkspaceIdAtPath, -} from "../workspace-archive-service.js"; +import { archiveByScope, type ActiveWorkspaceRef } from "../workspace-archive-service.js"; import type { CreatePaseoWorktreeWorkflowFn, CreatePaseoWorktreeWorkflowResult, @@ -42,6 +38,10 @@ interface CreateAgentLifecycleDispatchDependencies { logger: pino.Logger; } +type AutoArchiveTarget = + | { kind: "agent-only" } + | { kind: "created-worktree"; result: CreatePaseoWorktreeWorkflowResult }; + export class CreateAgentLifecycleDispatch { private readonly autoArchiveAgentIds = new Set(); @@ -72,10 +72,10 @@ export class CreateAgentLifecycleDispatch { return; } - this.registerAutoArchiveOnTerminalState(input.agentId, { - worktreePath: input.createdWorktree?.worktree.worktreePath ?? null, - repoRoot: input.createdWorktree?.repoRoot ?? null, - }); + this.registerAutoArchiveOnTerminalState( + input.agentId, + toAutoArchiveTarget(input.createdWorktree), + ); } async cleanupCreatedWorktreeAfterFailedAgentCreate(input: { @@ -89,8 +89,7 @@ export class CreateAgentLifecycleDispatch { await this.archiveAutoCreatedWorktree({ agentId: null, - worktreePath: createdWorktree.worktree.worktreePath, - repoRoot: createdWorktree.repoRoot, + createdWorktree, }).catch((archiveError) => { this.dependencies.logger.warn( { @@ -143,10 +142,7 @@ export class CreateAgentLifecycleDispatch { } } - private registerAutoArchiveOnTerminalState( - agentId: string, - options: { worktreePath: string | null; repoRoot: string | null }, - ): void { + private registerAutoArchiveOnTerminalState(agentId: string, target: AutoArchiveTarget): void { const unsubscribe = this.dependencies.agentManager.subscribe( (event) => { if (event.type !== "agent_stream") { @@ -160,27 +156,23 @@ export class CreateAgentLifecycleDispatch { return; } unsubscribe(); - void this.autoArchiveAgentOnce(agentId, options); + void this.autoArchiveAgentOnce(agentId, target); }, { agentId, replayState: false }, ); } - private async autoArchiveAgentOnce( - agentId: string, - options: { worktreePath: string | null; repoRoot: string | null }, - ): Promise { + private async autoArchiveAgentOnce(agentId: string, target: AutoArchiveTarget): Promise { if (this.autoArchiveAgentIds.has(agentId)) { return; } this.autoArchiveAgentIds.add(agentId); try { - if (options.worktreePath) { + if (target.kind === "created-worktree") { await this.archiveAutoCreatedWorktree({ agentId, - worktreePath: options.worktreePath, - repoRoot: options.repoRoot, + createdWorktree: target.result, }); return; } @@ -193,10 +185,11 @@ export class CreateAgentLifecycleDispatch { private async archiveAutoCreatedWorktree(options: { agentId: string | null; - worktreePath: string; - repoRoot: string | null; + createdWorktree: CreatePaseoWorktreeWorkflowResult; }): Promise { - const ownership = await isPaseoOwnedWorktreeCwd(options.worktreePath, { + const { createdWorktree } = options; + const worktreePath = createdWorktree.worktree.worktreePath; + const ownership = await isPaseoOwnedWorktreeCwd(worktreePath, { paseoHome: this.dependencies.paseoHome, worktreesRoot: this.dependencies.worktreesRoot, }); @@ -204,49 +197,39 @@ export class CreateAgentLifecycleDispatch { throw new Error("Auto-created worktree is not a Paseo-owned worktree"); } - const workspaceId = await resolveWorkspaceIdAtPath( + await archiveByScope( { + paseoHome: this.dependencies.paseoHome, + paseoWorktreesBaseRoot: this.dependencies.worktreesRoot, + github: this.dependencies.github, + workspaceGitService: this.dependencies.workspaceGitService, + agentManager: this.dependencies.agentManager, + agentStorage: this.dependencies.agentStorage, findWorkspaceIdForCwd: this.dependencies.findWorkspaceIdForCwd, listActiveWorkspaces: this.dependencies.listActiveWorkspaces, + archiveWorkspaceRecord: this.dependencies.archiveWorkspaceRecord, + emitWorkspaceUpdatesForWorkspaceIds: this.dependencies.emitWorkspaceUpdatesForWorkspaceIds, + markWorkspaceArchiving: this.dependencies.markWorkspaceArchiving, + clearWorkspaceArchiving: this.dependencies.clearWorkspaceArchiving, + killTerminalsForWorkspace: this.dependencies.killTerminalsForWorkspace, + sessionLogger: this.dependencies.logger, + }, + { + scope: { kind: "workspace", workspaceId: createdWorktree.workspace.workspaceId }, + requestId: randomUUID(), }, - options.worktreePath, ); - if (!workspaceId) { - this.dependencies.logger.warn( - { worktreePath: options.worktreePath }, - "Could not resolve workspace for auto-archive; skipping", - ); - } else { - await archiveByScope( - { - paseoHome: this.dependencies.paseoHome, - paseoWorktreesBaseRoot: this.dependencies.worktreesRoot, - github: this.dependencies.github, - workspaceGitService: this.dependencies.workspaceGitService, - agentManager: this.dependencies.agentManager, - agentStorage: this.dependencies.agentStorage, - findWorkspaceIdForCwd: this.dependencies.findWorkspaceIdForCwd, - listActiveWorkspaces: this.dependencies.listActiveWorkspaces, - archiveWorkspaceRecord: this.dependencies.archiveWorkspaceRecord, - emitWorkspaceUpdatesForWorkspaceIds: - this.dependencies.emitWorkspaceUpdatesForWorkspaceIds, - markWorkspaceArchiving: this.dependencies.markWorkspaceArchiving, - clearWorkspaceArchiving: this.dependencies.clearWorkspaceArchiving, - killTerminalsForWorkspace: this.dependencies.killTerminalsForWorkspace, - sessionLogger: this.dependencies.logger, - }, - { - scope: { kind: "workspace", workspaceId }, - repoRoot: options.repoRoot ?? ownership.repoRoot ?? null, - paseoWorktreesBaseRoot: this.dependencies.worktreesRoot, - requestId: randomUUID(), - }, - ); - } - if (options.agentId) { this.dependencies.emitAgentRemove(options.agentId); } } } + +function toAutoArchiveTarget( + createdWorktree: CreatePaseoWorktreeWorkflowResult | null, +): AutoArchiveTarget { + return createdWorktree + ? { kind: "created-worktree", result: createdWorktree } + : { kind: "agent-only" }; +} diff --git a/packages/server/src/server/agent/create-agent/create.test.ts b/packages/server/src/server/agent/create-agent/create.test.ts index 5b51fe521..458ed1ccf 100644 --- a/packages/server/src/server/agent/create-agent/create.test.ts +++ b/packages/server/src/server/agent/create-agent/create.test.ts @@ -27,12 +27,13 @@ function createRealAgentManager(storage: AgentStorage): AgentManager { // worktree service). function fakeWorktreeCreator(args: { repoRoot: string; createdWorkspaceId: string }) { const worktreePath = join(args.repoRoot, "worktree"); - mkdirSync(worktreePath, { recursive: true }); + const workspaceCwd = join(worktreePath, "packages", "app"); + mkdirSync(workspaceCwd, { recursive: true }); return async (): Promise => ({ worktree: { worktreePath }, intent: {}, - workspace: { workspaceId: args.createdWorkspaceId }, + workspace: { workspaceId: args.createdWorkspaceId, cwd: workspaceCwd }, repoRoot: args.repoRoot, created: true, setupContinuation: { kind: "agent" as const, startAfterAgentCreate: () => {} }, @@ -321,6 +322,7 @@ test("mcp create stamps the new worktree's workspaceId, not the parent's", async const storedChild = await storage.get(child.id); expect(storedChild?.workspaceId).toBe("ws-new-worktree"); + expect(child.cwd).toBe(join(workdir, "worktree", "packages", "app")); } finally { rmSync(workdir, { recursive: true, force: true }); } diff --git a/packages/server/src/server/agent/create-agent/create.ts b/packages/server/src/server/agent/create-agent/create.ts index 7954ec1d4..ea20d3ffe 100644 --- a/packages/server/src/server/agent/create-agent/create.ts +++ b/packages/server/src/server/agent/create-agent/create.ts @@ -573,7 +573,7 @@ async function resolveMcpCwd(params: { }, }); return { - resolvedCwd: createdWorktree.worktree.worktreePath, + resolvedCwd: createdWorktree.workspace.cwd, setupContinuation: createdWorktree.setupContinuation, createdWorkspaceId: createdWorktree.workspace.workspaceId, }; diff --git a/packages/server/src/server/agent/mcp-server.test.ts b/packages/server/src/server/agent/mcp-server.test.ts index 594a1794a..66f71f829 100644 --- a/packages/server/src/server/agent/mcp-server.test.ts +++ b/packages/server/src/server/agent/mcp-server.test.ts @@ -22,9 +22,12 @@ import { AgentSnapshotPayloadSchema, } from "@getpaseo/protocol/messages"; import { + createPersistedProjectRecord, createPersistedWorkspaceRecord, type PersistedProjectRecord, type PersistedWorkspaceRecord, + type ProjectRegistry, + type WorkspaceRegistry, } from "../workspace-registry.js"; import type { CreateScheduleInput, @@ -46,11 +49,13 @@ import { WorkspaceAutoName } from "../workspace-auto-name.js"; import { createGitMutationService } from "../session/git-mutation/git-mutation-service.js"; import type { GeneratedWorkspaceName } from "../worktree-branch-name-generator.js"; import type { ForgeService } from "../../services/forge-service.js"; +import { areEquivalentPaths } from "../../utils/path.js"; import type { TerminalManager } from "../../terminal/terminal-manager.js"; import { PARENT_AGENT_ID_LABEL } from "@getpaseo/protocol/agent-labels"; import type { BrowserToolsBroker, BrowserToolsExecuteInput } from "../browser-tools/broker.js"; import type { BrowserToolsResponsePayload } from "../browser-tools/errors.js"; import { readPaseoWorktreeMetadata } from "../../utils/worktree-metadata.js"; +import { createWorkspaceProvisioningService } from "../session/workspace-provisioning/workspace-provisioning-service.js"; const REPO_CWD = resolvePath("/tmp/repo"); const TARGET_CWD = resolvePath("/tmp/target"); @@ -670,13 +675,68 @@ function createPaseoWorktreeForMcpTest(options: { paseoHome: options.paseoHome, deps: { forgeOverrides: { github } }, }); - const workspaceRegistry = { + const projectRegistry: ProjectRegistry = { + initialize: async () => {}, + existsOnDisk: async () => true, + list: async () => Array.from(projects.values()), + get: async (projectId) => projects.get(projectId) ?? null, + getOrCreateActiveByRoot: async (allocation) => { + const existing = Array.from(projects.values()).find( + (project) => + areEquivalentPaths(project.rootPath, allocation.rootPath) && !project.archivedAt, + ); + if (existing) return existing; + const project = createPersistedProjectRecord({ + projectId: `prj_test_${projects.size + 1}`, + rootPath: allocation.rootPath, + kind: allocation.kind, + displayName: allocation.displayName, + createdAt: allocation.timestamp, + updatedAt: allocation.timestamp, + }); + projects.set(project.projectId, project); + return project; + }, + upsert: async (record) => { + projects.set(record.projectId, record); + }, + archive: async (projectId, archivedAt) => { + const project = projects.get(projectId); + if (project) projects.set(projectId, { ...project, archivedAt }); + }, + remove: async (projectId) => { + projects.delete(projectId); + }, + }; + const workspaceRegistry: WorkspaceRegistry = { + initialize: async () => {}, + existsOnDisk: async () => true, get: async (workspaceId: string) => workspaces.get(workspaceId) ?? null, list: async () => Array.from(workspaces.values()), + update: async (workspaceId, updater) => { + const workspace = workspaces.get(workspaceId); + if (!workspace) return null; + const updated = updater(workspace); + workspaces.set(workspaceId, updated); + return updated; + }, upsert: async (record: PersistedWorkspaceRecord) => { workspaces.set(record.workspaceId, record); }, + archive: async (workspaceId, archivedAt) => { + const workspace = workspaces.get(workspaceId); + if (workspace) workspaces.set(workspaceId, { ...workspace, archivedAt }); + }, + remove: async (workspaceId) => { + workspaces.delete(workspaceId); + }, }; + const workspaceProvisioning = createWorkspaceProvisioningService({ + projectRegistry, + workspaceRegistry, + workspaceGitService, + logger: createTestLogger(), + }); const workspaceAutoName = new WorkspaceAutoName({ agentManager: buildAgentManagerSpies() as unknown as AgentManager, workspaceRegistry, @@ -710,14 +770,8 @@ function createPaseoWorktreeForMcpTest(options: { ...(workflowOptions?.resolveDefaultBranch ? { resolveDefaultBranch: workflowOptions.resolveDefaultBranch } : {}), - projectRegistry: { - get: async (projectId) => projects.get(projectId) ?? null, - upsert: async (record) => { - projects.set(record.projectId, record); - }, - }, - workspaceRegistry, workspaceGitService, + workspaceProvisioning, }), warmWorkspaceGitData: async () => {}, autoNameWorkspaceBranchForFirstAgent: (autoNameInput) => diff --git a/packages/server/src/server/auto-archive-on-merge/archive-if-safe.test.ts b/packages/server/src/server/auto-archive-on-merge/archive-if-safe.test.ts index 6a6f5d7db..9f83c1ce4 100644 --- a/packages/server/src/server/auto-archive-on-merge/archive-if-safe.test.ts +++ b/packages/server/src/server/auto-archive-on-merge/archive-if-safe.test.ts @@ -486,8 +486,6 @@ describe("archiveIfSafe", () => { }), { scope: { kind: "workspace", workspaceId: "ws-auto-archive" }, - repoRoot: "/tmp/repo", - paseoWorktreesBaseRoot: undefined, requestId: "auto-archive-on-merge", }, ); diff --git a/packages/server/src/server/auto-archive-on-merge/archive-if-safe.ts b/packages/server/src/server/auto-archive-on-merge/archive-if-safe.ts index bd6f8d720..126f615bc 100644 --- a/packages/server/src/server/auto-archive-on-merge/archive-if-safe.ts +++ b/packages/server/src/server/auto-archive-on-merge/archive-if-safe.ts @@ -138,8 +138,6 @@ export async function archiveIfSafe(input: { }, { scope: { kind: "workspace", workspaceId }, - repoRoot: ownership.repoRoot ?? null, - paseoWorktreesBaseRoot: options.paseoWorktreesBaseRoot, requestId: "auto-archive-on-merge", }, ); diff --git a/packages/server/src/server/bootstrap-provider-availability.test.ts b/packages/server/src/server/bootstrap-provider-availability.test.ts index 552441639..be431e0dc 100644 --- a/packages/server/src/server/bootstrap-provider-availability.test.ts +++ b/packages/server/src/server/bootstrap-provider-availability.test.ts @@ -1,3 +1,4 @@ +import { execFileSync } from "node:child_process"; import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; @@ -18,7 +19,9 @@ describe("bootstrap provider availability", () => { process.env.PATH = originalEnv.PATH; process.env.PATHEXT = originalEnv.PATHEXT; await Promise.all( - tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })), + tempRoots + .splice(0) + .map((root) => rm(root, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 })), ); }); @@ -26,12 +29,13 @@ describe("bootstrap provider availability", () => { const { createPaseoDaemon } = await import("./bootstrap.js"); const root = await mkdtemp(path.join(os.tmpdir(), "paseo-bootstrap-provider-")); tempRoots.push(root); - const binDir = await mkdtemp(path.join(os.tmpdir(), "paseo-bootstrap-provider-bin-")); - tempRoots.push(binDir); - process.env.PATH = binDir; - if (process.platform === "win32") { - process.env.PATHEXT = ".CMD"; - } + const gitPath = execFileSync(process.platform === "win32" ? "where" : "which", ["git"], { + encoding: "utf8", + }) + .split(/\r?\n/)[0] + .trim(); + process.env.PATH = path.dirname(gitPath); + expect(execFileSync("git", ["--version"], { encoding: "utf8" })).toMatch(/git version/i); const paseoHome = path.join(root, ".paseo"); const staticDir = path.join(root, "static"); const agentStoragePath = path.join(paseoHome, "agents"); diff --git a/packages/server/src/server/bootstrap.test.ts b/packages/server/src/server/bootstrap.test.ts new file mode 100644 index 000000000..6f705ec55 --- /dev/null +++ b/packages/server/src/server/bootstrap.test.ts @@ -0,0 +1,56 @@ +import { expect, test } from "vitest"; + +import { fanOutReconciledWorkspaceUpdates } from "./bootstrap.js"; + +test("reconciliation emits workspace updates when observer sync fails", async () => { + const emittedWorkspaceIds: string[][] = []; + const syncFailure = new Error("workspace observer unavailable"); + + await fanOutReconciledWorkspaceUpdates({ + sessions: [ + { + syncWorkspaceGitObserversForExternalWorkspaceIds: async () => { + throw syncFailure; + }, + emitWorkspaceUpdatesForExternalWorkspaceIds: async (workspaceIds) => { + emittedWorkspaceIds.push(Array.from(workspaceIds)); + }, + }, + ], + workspaceIds: ["ws-reclassified"], + logger: { warn: () => {} }, + }); + + expect(emittedWorkspaceIds).toEqual([["ws-reclassified"]]); +}); + +test("reconciliation isolates workspace update failures between sessions", async () => { + const emittedWorkspaceIds: string[][] = []; + const warnings: unknown[] = []; + + await fanOutReconciledWorkspaceUpdates({ + sessions: [ + { + syncWorkspaceGitObserversForExternalWorkspaceIds: async () => {}, + emitWorkspaceUpdatesForExternalWorkspaceIds: async () => { + throw new Error("session closed"); + }, + }, + { + syncWorkspaceGitObserversForExternalWorkspaceIds: async () => {}, + emitWorkspaceUpdatesForExternalWorkspaceIds: async (workspaceIds) => { + emittedWorkspaceIds.push(Array.from(workspaceIds)); + }, + }, + ], + workspaceIds: ["ws-reclassified"], + logger: { + warn: (context) => { + warnings.push(context); + }, + }, + }); + + expect(emittedWorkspaceIds).toEqual([["ws-reclassified"]]); + expect(warnings).toHaveLength(1); +}); diff --git a/packages/server/src/server/bootstrap.ts b/packages/server/src/server/bootstrap.ts index a1ad98dbd..135f0efc4 100644 --- a/packages/server/src/server/bootstrap.ts +++ b/packages/server/src/server/bootstrap.ts @@ -89,12 +89,42 @@ function formatListenTarget(listenTarget: ListenTarget | null): string | null { return listenTarget.path; } +export async function fanOutReconciledWorkspaceUpdates(input: { + sessions: Iterable<{ + syncWorkspaceGitObserversForExternalWorkspaceIds(workspaceIds: Iterable): Promise; + emitWorkspaceUpdatesForExternalWorkspaceIds( + workspaceIds: Iterable, + options: { skipReconcile: boolean }, + ): Promise; + }>; + workspaceIds: readonly string[]; + logger: Pick; +}): Promise { + await Promise.all( + Array.from(input.sessions, async (session) => { + try { + await session.syncWorkspaceGitObserversForExternalWorkspaceIds(input.workspaceIds); + } catch (error) { + input.logger.warn( + { err: error }, + "Failed to sync workspace Git observers after reconciliation", + ); + } + try { + await session.emitWorkspaceUpdatesForExternalWorkspaceIds(input.workspaceIds, { + skipReconcile: true, + }); + } catch (error) { + input.logger.warn({ err: error }, "Failed to emit workspace updates after reconciliation"); + } + }), + ); +} + import { VoiceAssistantWebSocketServer } from "./websocket-server.js"; import { createGitHubService } from "../services/github-service.js"; -import { - createPaseoWorktree as createRegisteredPaseoWorktree, - createLocalCheckoutWorkspace, -} from "./paseo-worktree-service.js"; +import { createPaseoWorktree as createRegisteredPaseoWorktree } from "./paseo-worktree-service.js"; +import { createWorkspaceProvisioningService } from "./session/workspace-provisioning/workspace-provisioning-service.js"; import { createPaseoWorktreeWorkflow } from "./worktree-session.js"; import { DownloadTokenStore } from "./file-download/token-store.js"; import type { OpenAiSpeechProviderConfig } from "./speech/providers/openai/config.js"; @@ -744,6 +774,12 @@ export async function createPaseoDaemon( forgeOverrides: { github }, }, }); + const workspaceProvisioning = createWorkspaceProvisioningService({ + projectRegistry, + workspaceRegistry, + workspaceGitService, + logger, + }); const providerSnapshotLogger = logger.child({ module: "provider-snapshot-manager" }); const providerSnapshotManager = new ProviderSnapshotManager({ logger: providerSnapshotLogger, @@ -788,21 +824,19 @@ export async function createPaseoDaemon( workspaceRegistry, logger, workspaceGitService, + onProjectUpdate: (update) => wsServer?.publishProjectUpdate(update), + onWorkspacesChanged: async (workspaceIds) => { + await fanOutReconciledWorkspaceUpdates({ + sessions: wsServer?.listActiveSessions() ?? [], + workspaceIds, + logger, + }); + }, + }); + await workspaceReconciliation.start(); + void workspaceReconciliation.runOnce().catch((error) => { + logger.warn({ err: error }, "Initial workspace reconciliation failed"); }); - void (async () => { - try { - const result = await workspaceReconciliation.runOnce(); - logger.info( - { - elapsed: elapsed(), - changeCount: result.changesApplied.length, - }, - "Workspace registries reconciled", - ); - } catch (error) { - logger.error({ err: error }, "Background workspace reconciliation failed"); - } - })(); await chatService.initialize(); logger.info({ elapsed: elapsed() }, "Chat service initialized"); const checkoutDiffManager = new CheckoutDiffManager({ @@ -833,9 +867,9 @@ export async function createPaseoDaemon( cwd: string, firstAgentContext?: FirstAgentContext, ): Promise => { - const workspace = await createLocalCheckoutWorkspace( - { cwd, title: resolveFirstAgentPromptTitle(firstAgentContext) }, - { projectRegistry, workspaceRegistry, workspaceGitService }, + const workspace = await workspaceProvisioning.createWorkspaceForDirectory( + cwd, + resolveFirstAgentPromptTitle(firstAgentContext), ); if (firstAgentContext) { workspaceAutoName.scheduleForDirectory({ @@ -854,6 +888,9 @@ export async function createPaseoDaemon( workspaceId: workspace.workspaceId, cwd: workspace.cwd, kind: workspace.kind, + worktreeRoot: workspace.worktreeRoot, + isPaseoOwnedWorktree: workspace.isPaseoOwnedWorktree, + mainRepoRoot: workspace.mainRepoRoot, })); }; const markWorkspaceArchivingExternal = (workspaceIds: Iterable, archivingAt: string) => { @@ -942,9 +979,8 @@ export async function createPaseoDaemon( resolveDefaultBranch: workflowOptions.resolveDefaultBranch, } : {}), - projectRegistry, - workspaceRegistry, workspaceGitService, + workspaceProvisioning, }); }, warmWorkspaceGitData: async (workspace) => { @@ -1003,9 +1039,9 @@ export async function createPaseoDaemon( cwd: string; firstAgentContext: FirstAgentContext; }) => { - const workspace = await createLocalCheckoutWorkspace( - { cwd: input.cwd, title: resolveFirstAgentPromptTitle(input.firstAgentContext) }, - { projectRegistry, workspaceRegistry, workspaceGitService }, + const workspace = await workspaceProvisioning.createWorkspaceForDirectory( + input.cwd, + resolveFirstAgentPromptTitle(input.firstAgentContext), ); workspaceAutoName.scheduleForDirectory({ workspaceId: workspace.workspaceId, @@ -1026,7 +1062,7 @@ export async function createPaseoDaemon( await emitWorkspaceUpdatesExternal([result.workspace.workspaceId]); return result; }; - const archiveScheduleWorkspaceExternal = async (workspaceId: string, repoRoot: string) => { + const archiveScheduleWorkspaceExternal = async (workspaceId: string) => { await archiveByScope( { paseoHome: config.paseoHome, @@ -1053,8 +1089,6 @@ export async function createPaseoDaemon( }, { scope: { kind: "workspace", workspaceId }, - repoRoot, - paseoWorktreesBaseRoot: config.worktreesRoot, requestId: "schedule-run-finish", }, ); @@ -1065,7 +1099,7 @@ export async function createPaseoDaemon( agentManager, agentStorage, createAgent, - createLocalCheckoutWorkspace: createScheduleLocalWorkspaceExternal, + createDirectoryWorkspace: createScheduleLocalWorkspaceExternal, createPaseoWorktreeWorkspace: createSchedulePaseoWorktreeExternal, archiveWorkspace: archiveScheduleWorkspaceExternal, }); @@ -1444,6 +1478,7 @@ export async function createPaseoDaemon( }; const stop = async () => { + workspaceReconciliation.dispose(); scriptHealthMonitor.stop(); // Freeze both ingress and registration before taking the agent closure snapshot. wsServer?.prepareForShutdown(); diff --git a/packages/server/src/server/daemon-e2e/empty-project-persists.e2e.test.ts b/packages/server/src/server/daemon-e2e/empty-project-persists.e2e.test.ts index 87f092cd6..1c5fed39e 100644 --- a/packages/server/src/server/daemon-e2e/empty-project-persists.e2e.test.ts +++ b/packages/server/src/server/daemon-e2e/empty-project-persists.e2e.test.ts @@ -62,10 +62,10 @@ test("project.add creates a project without creating a workspace", async () => { expect(added.project).not.toBeNull(); const project = added.project!; expect(project).toMatchObject({ - projectId: repoRoot, projectRootPath: repoRoot, projectKind: "git", }); + expect(project.projectId).toMatch(/^prj_[0-9a-f]{16}$/); const workspaces = await client.fetchWorkspaces({ filter: { projectId: project.projectId }, @@ -73,7 +73,6 @@ test("project.add creates a project without creating a workspace", async () => { expect(workspaces.entries).toEqual([]); expect(workspaces.emptyProjects).toEqual([ expect.objectContaining({ - projectId: repoRoot, projectRootPath: repoRoot, projectKind: "git", }), diff --git a/packages/server/src/server/daemon-e2e/open-project-worktree-reclassification.e2e.test.ts b/packages/server/src/server/daemon-e2e/open-project-worktree-reclassification.e2e.test.ts index 503707e71..ab49ecf9d 100644 --- a/packages/server/src/server/daemon-e2e/open-project-worktree-reclassification.e2e.test.ts +++ b/packages/server/src/server/daemon-e2e/open-project-worktree-reclassification.e2e.test.ts @@ -29,7 +29,7 @@ afterEach(async () => { cleanupPaths.clear(); }); -test("openProject reclassifies an existing directory workspace into its parent git project", async () => { +test("openProject preserves a worktree's exact-root project without rehoming it", async () => { const previousSupervised = process.env.PASEO_SUPERVISED; process.env.PASEO_SUPERVISED = "0"; try { @@ -113,17 +113,13 @@ test("openProject reclassifies an existing directory workspace into its parent g const persistedWorkspaces = await readRegistry(workspacesPath); expect(response.error).toBeNull(); - expect(response.workspace?.projectId).toBe(repoRoot); - expect(response.workspace?.workspaceKind).toBe("worktree"); + expect(response.workspace?.projectId).toBe(worktreeRoot); expect(persistedProjects.find((project) => project.projectId === repoRoot)?.rootPath).toBe( repoRoot, ); expect( persistedWorkspaces.find((workspace) => workspace.workspaceId === worktreeRoot)?.projectId, - ).toBe(repoRoot); - expect( - persistedWorkspaces.find((workspace) => workspace.workspaceId === worktreeRoot)?.kind, - ).toBe("worktree"); + ).toBe(worktreeRoot); } finally { process.env.PASEO_SUPERVISED = previousSupervised; } diff --git a/packages/server/src/server/daemon-e2e/project-becomes-git.e2e.test.ts b/packages/server/src/server/daemon-e2e/project-becomes-git.e2e.test.ts new file mode 100644 index 000000000..e7843f78a --- /dev/null +++ b/packages/server/src/server/daemon-e2e/project-becomes-git.e2e.test.ts @@ -0,0 +1,123 @@ +import { execFile as execFileCallback } from "node:child_process"; +import { mkdtempSync, realpathSync } from "node:fs"; +import { readFile, rm } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { promisify } from "node:util"; +import { afterEach, expect, test } from "vitest"; + +import { withTimeout } from "../../utils/promise-timeout.js"; +import { DaemonClient, type DaemonEvent } from "../test-utils/daemon-client.js"; +import { createTestPaseoDaemon, type TestPaseoDaemon } from "../test-utils/paseo-daemon.js"; +import { type PersistedProjectRecord } from "../workspace-registry.js"; + +const cleanupPaths = new Set(); +const cleanupDaemons = new Set(); +const cleanupClients = new Set(); +const cleanupListeners = new Set<() => void>(); +const execFile = promisify(execFileCallback); + +type ProjectUpdatePayload = Extract["payload"]; + +function waitForProjectUpdate( + client: DaemonClient, + predicate: (payload: ProjectUpdatePayload) => boolean, +): Promise { + return new Promise((resolve) => { + const unsubscribe = client.on("project.update", (message) => { + if (!predicate(message.payload)) return; + cleanupListeners.delete(unsubscribe); + unsubscribe(); + resolve(message.payload); + }); + cleanupListeners.add(unsubscribe); + }); +} + +afterEach(async () => { + for (const unsubscribe of cleanupListeners) unsubscribe(); + cleanupListeners.clear(); + await Promise.all(Array.from(cleanupClients, (client) => client.close().catch(() => undefined))); + cleanupClients.clear(); + await Promise.all(Array.from(cleanupDaemons, (daemon) => daemon.close().catch(() => undefined))); + cleanupDaemons.clear(); + await Promise.all( + Array.from(cleanupPaths, (target) => rm(target, { recursive: true, force: true })), + ); + cleanupPaths.clear(); +}); + +test("an empty project becomes Git without changing its identity or creating a workspace", async () => { + const projectRoot = realpathSync( + mkdtempSync(path.join(os.tmpdir(), "paseo-project-becomes-git-")), + ); + const paseoHomeRoot = realpathSync( + mkdtempSync(path.join(os.tmpdir(), "paseo-project-becomes-git-home-")), + ); + cleanupPaths.add(projectRoot); + cleanupPaths.add(paseoHomeRoot); + + const daemon = await createTestPaseoDaemon({ paseoHomeRoot, cleanup: false }); + cleanupDaemons.add(daemon); + const client = new DaemonClient({ url: `ws://127.0.0.1:${daemon.port}/ws` }); + cleanupClients.add(client); + await client.connect(); + await client.fetchAgents({ subscribe: { subscriptionId: "project-becomes-git" } }); + + const added = await client.addProject(projectRoot); + + expect(added).toEqual({ + requestId: expect.any(String), + project: { + projectId: expect.stringMatching(/^prj_[0-9a-f]{16}$/), + projectDisplayName: path.basename(projectRoot), + projectCustomName: null, + projectRootPath: projectRoot, + projectKind: "non_git", + }, + error: null, + }); + const project = added.project!; + const beforeGitInit = await client.fetchWorkspaces({ filter: { projectId: project.projectId } }); + expect(beforeGitInit).toMatchObject({ entries: [], emptyProjects: [project] }); + + const gitProjectUpdate = waitForProjectUpdate( + client, + (payload) => + payload.kind === "upsert" && + payload.project.projectId === project.projectId && + payload.project.projectRootPath === projectRoot && + payload.project.projectKind === "git", + ); + await execFile("git", ["init", "-b", "main"], { cwd: projectRoot }); + const update = await withTimeout({ + promise: gitProjectUpdate, + timeoutMs: 10_000, + label: "project.update after git init", + }); + + expect(update).toEqual({ + kind: "upsert", + project: { ...project, projectKind: "git" }, + }); + + const afterGitInit = await client.fetchWorkspaces({ filter: { projectId: project.projectId } }); + expect(afterGitInit).toMatchObject({ + entries: [], + emptyProjects: [{ ...project, projectKind: "git" }], + }); + + const persistedProjects = JSON.parse( + await readFile(path.join(daemon.paseoHome, "projects", "projects.json"), "utf8"), + ) as PersistedProjectRecord[]; + expect(persistedProjects).toContainEqual({ + projectId: project.projectId, + rootPath: projectRoot, + kind: "git", + displayName: project.projectDisplayName, + customName: null, + createdAt: expect.any(String), + updatedAt: expect.any(String), + archivedAt: null, + }); +}, 30_000); diff --git a/packages/server/src/server/loop-service.test.ts b/packages/server/src/server/loop-service.test.ts index 37280535e..917a04f1e 100644 --- a/packages/server/src/server/loop-service.test.ts +++ b/packages/server/src/server/loop-service.test.ts @@ -32,7 +32,7 @@ import { AgentStorage } from "./agent/agent-storage.js"; import { AgentManager } from "./agent/agent-manager.js"; import { createAgentCommand } from "./agent/create-agent/create.js"; import type { ProviderSnapshotManager } from "./agent/provider-snapshot-manager.js"; -import { createLocalCheckoutWorkspace } from "./paseo-worktree-service.js"; +import { createWorkspaceProvisioningService } from "./session/workspace-provisioning/workspace-provisioning-service.js"; import { createNoopWorkspaceGitService } from "./test-utils/workspace-git-service-stub.js"; import { FileBackedProjectRegistry, FileBackedWorkspaceRegistry } from "./workspace-registry.js"; import { LoopService } from "./loop-service.js"; @@ -108,12 +108,17 @@ async function createRegistryBackedWorkspaceEnsure(rootDir: string): Promise<{ await workspaceRegistry.initialize(); await projectRegistry.initialize(); const workspaceGitService = createNoopWorkspaceGitService(); + const workspaceProvisioning = createWorkspaceProvisioningService({ + projectRegistry, + workspaceRegistry, + workspaceGitService, + }); return { workspaceRegistry, ensureWorkspaceForCreate: async (cwd, firstAgentContext) => { - const workspace = await createLocalCheckoutWorkspace( - { cwd, title: firstAgentContext?.prompt ?? null }, - { projectRegistry, workspaceRegistry, workspaceGitService }, + const workspace = await workspaceProvisioning.createWorkspaceForDirectory( + cwd, + firstAgentContext?.prompt ?? null, ); return workspace.workspaceId; }, diff --git a/packages/server/src/server/paseo-worktree-service.test.ts b/packages/server/src/server/paseo-worktree-service.test.ts index 527e4e7d1..bd30a28a1 100644 --- a/packages/server/src/server/paseo-worktree-service.test.ts +++ b/packages/server/src/server/paseo-worktree-service.test.ts @@ -1,5 +1,5 @@ import { execFileSync } from "node:child_process"; -import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; import { afterEach, expect, test, vi } from "vitest"; @@ -10,17 +10,19 @@ import type { PersistedProjectRecord, PersistedWorkspaceRecord, ProjectRegistry, + WorkspaceRegistry, } from "./workspace-registry.js"; +import { createWorkspaceProvisioningService } from "./session/workspace-provisioning/workspace-provisioning-service.js"; +import { createTestLogger } from "../test-utils/test-logger.js"; import { attemptFirstAgentBranchAutoName, - createLocalCheckoutWorkspace, createPaseoWorktree, type CreatePaseoWorktreeDeps, } from "./paseo-worktree-service.js"; import { readPaseoWorktreeMetadata } from "../utils/worktree-metadata.js"; -import { createWorktree } from "../utils/worktree.js"; +import { createWorktree, getPaseoWorktreesRoot } from "../utils/worktree.js"; import { isPlatform } from "../test-utils/platform.js"; -import { existsSync } from "node:fs"; +import { areEquivalentPaths, createRealpathAwarePathMatcher } from "../utils/path.js"; const cleanupPaths: string[] = []; @@ -69,10 +71,307 @@ test("creates a worktree and registers it in the source workspace project withou expect(result.workspace.displayName).toBe("feature-one"); expect(result.workspace.baseBranch).toBe("main"); expect(deps.workspaceGitService.getSnapshot).not.toHaveBeenCalled(); - expect(events).toEqual([ - "project:remote:github.com/acme/repo", - `workspace:${result.workspace.workspaceId}`, - ]); + expect(deps.projects.get(sourceProject.projectId)).toEqual(sourceProject); + expect(events).toEqual([`workspace:${result.workspace.workspaceId}`]); +}); + +test("refreshes a source project that became Git while creating a worktree", async () => { + const { repoDir, tempDir } = createGitRepo(); + cleanupPaths.push(tempDir); + const deps = createDeps(); + const sourceProject = { + ...createPersistedProjectRecordForTest({ + projectId: "prj_existing-source", + rootPath: repoDir, + displayName: "Repository", + }), + kind: "non_git" as const, + customName: "My project", + }; + const sourceWorkspace = createPersistedWorkspaceRecordForTest({ + workspaceId: "ws-main-checkout", + projectId: sourceProject.projectId, + cwd: repoDir, + kind: "local_checkout", + displayName: "main", + }); + deps.projects.set(sourceProject.projectId, sourceProject); + deps.workspaces.set(sourceWorkspace.workspaceId, sourceWorkspace); + + const result = await createPaseoWorktree( + { + cwd: repoDir, + worktreeSlug: "project-became-git", + runSetup: false, + paseoHome: path.join(tempDir, ".paseo"), + }, + deps, + ); + + expect(result.workspace.projectId).toBe(sourceProject.projectId); + expect(deps.projects.get(sourceProject.projectId)).toMatchObject({ + projectId: sourceProject.projectId, + rootPath: repoDir, + kind: "git", + displayName: "Repository", + customName: "My project", + archivedAt: null, + }); +}); + +test("repairs a legacy source workspace whose project record is missing", async () => { + const { repoDir, tempDir } = createGitRepo(); + cleanupPaths.push(tempDir); + const deps = createDeps(); + const sourceWorkspace = createPersistedWorkspaceRecordForTest({ + workspaceId: "ws-missing-project", + projectId: "project-missing", + cwd: repoDir, + kind: "local_checkout", + displayName: "main", + }); + deps.workspaces.set(sourceWorkspace.workspaceId, sourceWorkspace); + + const result = await createPaseoWorktree( + { + cwd: repoDir, + worktreeSlug: "repaired-source", + runSetup: false, + paseoHome: path.join(tempDir, ".paseo"), + }, + deps, + ); + + expect(result.workspace.projectId).toMatch(/^prj_[0-9a-f]{16}$/); + expect(result.workspace.projectId).not.toBe(sourceWorkspace.projectId); + const repairedProject = deps.projects.get(result.workspace.projectId); + expect(repairedProject).toMatchObject({ + projectId: result.workspace.projectId, + kind: "git", + archivedAt: null, + }); + expect(createRealpathAwarePathMatcher(repoDir)(repairedProject?.rootPath ?? "")).toBe(true); +}); + +test("uses an equivalent source workspace path when creating a worktree", async () => { + const { repoDir, tempDir } = createGitRepo(); + cleanupPaths.push(tempDir); + const sourceDir = path.join(repoDir, "app"); + mkdirSync(sourceDir); + writeFileSync(path.join(repoDir, "app", ".gitkeep"), ""); + commitAll(repoDir, "add app"); + const deps = createDeps(); + const sourceProject = createPersistedProjectRecordForTest({ + projectId: "prj_source-folder", + rootPath: sourceDir, + displayName: "app", + }); + const sourceWorkspace = createPersistedWorkspaceRecordForTest({ + workspaceId: "ws-source-folder", + projectId: sourceProject.projectId, + cwd: `${sourceDir}${path.sep}`, + kind: "local_checkout", + displayName: "app", + }); + deps.projects.set(sourceProject.projectId, sourceProject); + deps.workspaces.set(sourceWorkspace.workspaceId, sourceWorkspace); + + const result = await createPaseoWorktree( + { + cwd: sourceDir, + worktreeSlug: "equivalent-source", + runSetup: false, + paseoHome: path.join(tempDir, ".paseo"), + }, + deps, + ); + + expect(result.workspace.projectId).toBe(sourceProject.projectId); +}); + +test("creates a worktree workspace at the selected project subdirectory", async () => { + const { repoDir, tempDir } = createGitRepo(); + cleanupPaths.push(tempDir); + const sourceDir = path.join(repoDir, "packages", "app"); + mkdirSync(sourceDir, { recursive: true }); + writeFileSync(path.join(sourceDir, "package.json"), "{}\n"); + commitAll(repoDir, "add subproject"); + const deps = createDeps(); + const project = createPersistedProjectRecordForTest({ + projectId: "prj_selected-subdirectory", + rootPath: sourceDir, + displayName: "app", + }); + deps.projects.set(project.projectId, project); + + const result = await createPaseoWorktree( + { + cwd: sourceDir, + projectId: project.projectId, + worktreeSlug: "selected-subdirectory", + runSetup: false, + paseoHome: path.join(tempDir, ".paseo"), + }, + deps, + ); + + expect(result.workspace).toMatchObject({ + projectId: project.projectId, + cwd: path.join(result.worktree.worktreePath, "packages", "app"), + worktreeRoot: result.worktree.worktreePath, + kind: "worktree", + }); +}); + +test("seeds an uncommitted exact-project config into the mapped worktree directory", async () => { + const { repoDir, tempDir } = createGitRepo(); + cleanupPaths.push(tempDir); + const sourceDir = path.join(repoDir, "packages", "app"); + mkdirSync(sourceDir, { recursive: true }); + writeFileSync(path.join(sourceDir, "package.json"), "{}\n"); + commitAll(repoDir, "add subproject"); + const config = JSON.stringify({ worktree: { setup: ["npm install"] } }); + writeFileSync(path.join(sourceDir, "paseo.json"), config); + + const result = await createPaseoWorktree( + { + cwd: sourceDir, + worktreeSlug: "seed-nested-config", + runSetup: false, + paseoHome: path.join(tempDir, ".paseo"), + }, + createDeps(), + ); + + expect(readFileSync(path.join(result.workspace.cwd, "paseo.json"), "utf8")).toBe(config); + expect(existsSync(path.join(result.worktree.worktreePath, "paseo.json"))).toBe(false); +}); + +test("removes a new worktree when its ref does not contain the selected project directory", async () => { + const { repoDir, tempDir } = createGitRepo(); + cleanupPaths.push(tempDir); + execFileSync("git", ["branch", "without-subproject"], { cwd: repoDir, stdio: "pipe" }); + const sourceDir = path.join(repoDir, "packages", "app"); + mkdirSync(sourceDir, { recursive: true }); + writeFileSync(path.join(sourceDir, "package.json"), "{}\n"); + commitAll(repoDir, "add subproject"); + const deps = createDeps(); + const paseoHome = path.join(tempDir, ".paseo"); + const worktreePath = path.join( + await getPaseoWorktreesRoot(repoDir, paseoHome), + "missing-subproject", + ); + + await expect( + createPaseoWorktree( + { + cwd: sourceDir, + action: "checkout", + refName: "without-subproject", + worktreeSlug: "missing-subproject", + runSetup: false, + paseoHome, + }, + deps, + ), + ).rejects.toThrow("Selected project directory is missing from the worktree"); + + expect(deps.workspaces.size).toBe(0); + expect(existsSync(worktreePath)).toBe(false); + expect( + execFileSync("git", ["worktree", "list", "--porcelain"], { cwd: repoDir, stdio: "pipe" }) + .toString() + .includes("missing-subproject"), + ).toBe(false); +}); + +test("removes a new worktree when workspace persistence fails", async () => { + const { repoDir, tempDir } = createGitRepo(); + cleanupPaths.push(tempDir); + const paseoHome = path.join(tempDir, ".paseo"); + const worktreePath = path.join( + await getPaseoWorktreesRoot(repoDir, paseoHome), + "persistence-failure", + ); + + await expect( + createPaseoWorktree( + { + cwd: repoDir, + projectId: "missing-project", + worktreeSlug: "persistence-failure", + runSetup: false, + paseoHome, + }, + createDeps(), + ), + ).rejects.toThrow("Unknown project: missing-project"); + + expect(existsSync(worktreePath)).toBe(false); + expect( + execFileSync("git", ["worktree", "list", "--porcelain"], { cwd: repoDir, stdio: "pipe" }) + .toString() + .includes("persistence-failure"), + ).toBe(false); +}); + +test("maps a nested cwd from an existing Paseo worktree into the next worktree", async () => { + const { repoDir, tempDir } = createGitRepo(); + cleanupPaths.push(tempDir); + const paseoHome = path.join(tempDir, ".paseo"); + const projectDir = path.join(repoDir, "packages", "app"); + mkdirSync(projectDir, { recursive: true }); + writeFileSync(path.join(projectDir, "package.json"), "{}\n"); + commitAll(repoDir, "add subproject"); + const deps = createDeps(); + const source = await createPaseoWorktree( + { + cwd: repoDir, + worktreeSlug: "source-worktree", + runSetup: false, + paseoHome, + }, + deps, + ); + const sourceCwd = path.join(source.worktree.worktreePath, "packages", "app"); + + const created = await createPaseoWorktree( + { + cwd: sourceCwd, + worktreeSlug: "nested-worktree", + runSetup: false, + paseoHome, + }, + deps, + ); + + expect(created.workspace.cwd).toBe(path.join(created.worktree.worktreePath, "packages", "app")); + expect(deps.workspaces.get(created.workspace.workspaceId)).toEqual(created.workspace); +}); + +test("rejects source checkout planning before creating a worktree", async () => { + const { repoDir, tempDir } = createGitRepo(); + cleanupPaths.push(tempDir); + const paseoHome = path.join(tempDir, ".paseo"); + const deps = createDeps(); + deps.workspaceGitService.getCheckout = async () => { + throw new Error("source checkout unavailable"); + }; + + await expect( + createPaseoWorktree( + { + cwd: repoDir, + worktreeSlug: "must-not-create", + runSetup: false, + paseoHome, + }, + deps, + ), + ).rejects.toThrow("source checkout unavailable"); + + expect(existsSync(path.join(paseoHome, "worktrees"))).toBe(false); + expect(Array.from(deps.workspaces.values())).toEqual([]); }); test("registers a new worktree in the existing root project after the main checkout workspace is removed", async () => { @@ -109,6 +408,35 @@ test("registers a new worktree in the existing root project after the main check expect(Array.from(deps.projects.keys()).sort()).toEqual(["remote:github.com/acme/repo"]); }); +test("an explicit project FK remains unchanged when its worktree comes from another checkout", async () => { + const { repoDir, tempDir } = createGitRepo(); + cleanupPaths.push(tempDir); + const deps = createDeps(); + const project = { + ...createPersistedProjectRecordForTest({ + projectId: "prj_explicitproject", + rootPath: path.join(tempDir, "unrelated"), + displayName: "unrelated", + }), + kind: "non_git" as const, + }; + deps.projects.set(project.projectId, project); + + const result = await createPaseoWorktree( + { + cwd: repoDir, + projectId: project.projectId, + worktreeSlug: "attached-worktree", + runSetup: false, + paseoHome: path.join(tempDir, ".paseo"), + }, + deps, + ); + + expect(result.workspace.projectId).toBe(project.projectId); + expect(deps.projects.get(project.projectId)).toEqual(project); +}); + // POSIX-only: Windows git worktree paths need separate canonicalization coverage. test.skipIf(isPlatform("win32"))( "reuses an existing worktree and still upserts the workspace", @@ -152,19 +480,6 @@ test.skipIf(isPlatform("win32"))( }, ); -test("creates a distinct local checkout workspace for the same cwd on every call", async () => { - const { repoDir, tempDir } = createGitRepo(); - cleanupPaths.push(tempDir); - const deps = createDeps(); - - const first = await createLocalCheckoutWorkspace({ cwd: repoDir }, deps); - const second = await createLocalCheckoutWorkspace({ cwd: repoDir }, deps); - - expect(first.cwd).toBe(second.cwd); - expect(first.workspaceId).not.toBe(second.workspaceId); - expect(deps.workspaces.size).toBe(2); -}); - test("renames an eligible unnamed branch-off worktree once on first agent context", async () => { const { repoDir, tempDir } = createGitRepo(); cleanupPaths.push(tempDir); @@ -679,7 +994,6 @@ test.skipIf(isPlatform("win32"))( ); interface TestDeps extends CreatePaseoWorktreeDeps { - projectRegistry: Pick; projects: Map; workspaces: Map; } @@ -692,28 +1006,73 @@ function createDeps(options?: { const events = options?.events ?? []; const projects = options?.projects ?? new Map(); const workspaces = options?.workspaces ?? new Map(); + const projectRegistry: ProjectRegistry = { + initialize: async () => {}, + existsOnDisk: async () => true, + list: async () => Array.from(projects.values()), + get: async (projectId) => projects.get(projectId) ?? null, + getOrCreateActiveByRoot: async (input) => { + const existing = Array.from(projects.values()).find( + (project) => !project.archivedAt && areEquivalentPaths(project.rootPath, input.rootPath), + ); + if (existing) return existing; + const project = createPersistedProjectRecordForTest({ + projectId: `prj_${projects.size.toString().padStart(16, "0")}`, + rootPath: input.rootPath, + displayName: input.displayName, + }); + projects.set(project.projectId, project); + return project; + }, + upsert: async (project) => { + projects.set(project.projectId, project); + }, + archive: async (projectId, archivedAt) => { + const project = projects.get(projectId); + if (project) projects.set(projectId, { ...project, archivedAt }); + }, + remove: async (projectId) => { + projects.delete(projectId); + }, + }; + const workspaceRegistry: WorkspaceRegistry = { + initialize: async () => {}, + existsOnDisk: async () => true, + list: async () => Array.from(workspaces.values()), + get: async (workspaceId) => workspaces.get(workspaceId) ?? null, + update: async (workspaceId, updater) => { + const workspace = workspaces.get(workspaceId); + if (!workspace) return null; + const updated = updater(workspace); + workspaces.set(workspaceId, updated); + return updated; + }, + upsert: async (record) => { + events.push(`workspace:${record.workspaceId}`); + workspaces.set(record.workspaceId, record); + }, + archive: async (workspaceId, archivedAt) => { + const workspace = workspaces.get(workspaceId); + if (workspace) workspaces.set(workspaceId, { ...workspace, archivedAt }); + }, + remove: async (workspaceId) => { + workspaces.delete(workspaceId); + }, + }; + const workspaceGitService = createWorkspaceGitServiceStub(); + const workspaceProvisioning = createWorkspaceProvisioningService({ + projectRegistry, + workspaceRegistry, + workspaceGitService, + logger: createTestLogger(), + }); return { github: createGitHubServiceStub(), projects, workspaces, - projectRegistry: { - get: async (projectId) => projects.get(projectId) ?? null, - list: async () => Array.from(projects.values()), - upsert: async (record) => { - events.push(`project:${record.projectId}`); - projects.set(record.projectId, record); - }, - }, - workspaceRegistry: { - get: async (workspaceId) => workspaces.get(workspaceId) ?? null, - list: async () => Array.from(workspaces.values()), - upsert: async (record) => { - events.push(`workspace:${record.workspaceId}`); - workspaces.set(record.workspaceId, record); - }, - }, - workspaceGitService: createWorkspaceGitServiceStub(), + workspaceGitService, + workspaceProvisioning, }; } @@ -808,15 +1167,30 @@ function createWorkspaceGitServiceStub(): WorkspaceGitService { unsubscribe: () => {}, }), peekSnapshot: (cwd) => createWorkspaceGitSnapshot(cwd), - getCheckout: async (cwd) => ({ - cwd, - isGit: false, - currentBranch: null, - remoteUrl: null, - worktreeRoot: null, - isPaseoOwnedWorktree: false, - mainRepoRoot: null, - }), + getCheckout: async (cwd) => { + try { + const snapshot = createWorkspaceGitSnapshot(cwd); + return { + cwd, + isGit: snapshot.git.isGit, + currentBranch: snapshot.git.currentBranch, + remoteUrl: snapshot.git.remoteUrl, + worktreeRoot: snapshot.git.repoRoot, + isPaseoOwnedWorktree: snapshot.git.isPaseoOwnedWorktree, + mainRepoRoot: snapshot.git.mainRepoRoot, + }; + } catch { + return { + cwd, + isGit: false, + currentBranch: null, + remoteUrl: null, + worktreeRoot: null, + isPaseoOwnedWorktree: false, + mainRepoRoot: null, + }; + } + }, getSnapshot: async (cwd) => createWorkspaceGitSnapshot(cwd), resolveForge: async () => null, resolveRepoRoot: async (cwd) => { @@ -902,6 +1276,11 @@ function createGitRepo(): { tempDir: string; repoDir: string } { return { tempDir, repoDir }; } +function commitAll(repoDir: string, message: string): void { + execFileSync("git", ["add", "."], { cwd: repoDir, stdio: "pipe" }); + execFileSync("git", ["commit", "-m", message], { cwd: repoDir, stdio: "pipe" }); +} + function createGitHubPrRemoteRepo(): { tempDir: string; repoDir: string } { const { tempDir, repoDir } = createGitRepo(); execFileSync("git", ["checkout", "-b", "pr-123"], { cwd: repoDir, stdio: "pipe" }); diff --git a/packages/server/src/server/paseo-worktree-service.ts b/packages/server/src/server/paseo-worktree-service.ts index debc91463..43a8570d9 100644 --- a/packages/server/src/server/paseo-worktree-service.ts +++ b/packages/server/src/server/paseo-worktree-service.ts @@ -1,23 +1,22 @@ -import type { WorkspaceGitService } from "./workspace-git-service.js"; +import { stat } from "node:fs/promises"; import { resolve } from "node:path"; -import { - type PersistedWorkspaceRecord, - type ProjectRegistry, - type WorkspaceRegistry, - createPersistedProjectRecord, - createPersistedWorkspaceRecord, -} from "./workspace-registry.js"; -import { - classifyDirectoryForProjectMembership, - deriveProjectGroupingName, - generateWorkspaceId, -} from "./workspace-registry-model.js"; + +import type { WorkspaceGitService } from "./workspace-git-service.js"; +import { getRealpathAwareRelativePath } from "../utils/path.js"; +import type { PersistedWorkspaceRecord } from "./workspace-registry.js"; +import type { WorkspaceProvisioningService } from "./session/workspace-provisioning/workspace-provisioning-service.js"; import { createWorktreeCore, type CreateWorktreeCoreDeps, type CreateWorktreeCoreInput, } from "./worktree-core.js"; -import { validateBranchSlug, type WorktreeConfig } from "../utils/worktree.js"; +import { + mapWorkspaceRelativeCwdToWorktree, + rollbackCreatedPaseoWorktree, + seedPaseoConfigFile, + validateBranchSlug, + type WorktreeConfig, +} from "../utils/worktree.js"; import { getCurrentBranch, localBranchExists, renameCurrentBranch } from "../utils/checkout-git.js"; import { markPaseoWorktreeFirstAgentBranchAutoNameAttempted, @@ -56,36 +55,89 @@ export interface AttemptFirstAgentBranchAutoNameResult { } export interface CreatePaseoWorktreeDeps extends CreateWorktreeCoreDeps { - projectRegistry: Pick; - workspaceRegistry: Pick; workspaceGitService: WorkspaceGitService; + workspaceProvisioning: Pick; } export async function createPaseoWorktree( input: CreatePaseoWorktreeInput, deps: CreatePaseoWorktreeDeps, ): Promise { + const workspaceCwdPlan = await planWorkspaceCwdForWorktree(input.cwd, deps.workspaceGitService); const createdWorktree = await createWorktreeCore(input, deps); - maybeMarkFirstAgentBranchAutoNameEligible({ createdWorktree }); - const workspace = await upsertWorkspaceForWorktree({ - inputCwd: input.cwd, - projectId: input.projectId, - repoRoot: createdWorktree.repoRoot, - worktree: createdWorktree.worktree, - baseBranch: resolveIntentBaseBranch(createdWorktree.intent), - title: resolveFirstAgentPromptTitle(input.firstAgentContext), - deps, - }); + try { + maybeMarkFirstAgentBranchAutoNameEligible({ createdWorktree }); + const workspaceCwd = mapWorkspaceRelativeCwdToWorktree({ + relativeWorkspaceCwd: workspaceCwdPlan.relativeWorkspaceCwd, + targetWorktreePath: createdWorktree.worktree.worktreePath, + }); + if (!(await isDirectory(workspaceCwd))) { + throw new Error(`Selected project directory is missing from the worktree: ${workspaceCwd}`); + } - deps.github.invalidate({ cwd: createdWorktree.worktree.worktreePath }); + if (createdWorktree.created) { + await seedPaseoConfigFile({ + sourceCwd: workspaceCwdPlan.inputCwd, + targetCwd: workspaceCwd, + }); + } + const workspace = await deps.workspaceProvisioning.createWorkspaceForWorktree({ + sourceCwd: workspaceCwdPlan.inputCwd, + projectId: input.projectId, + repoRoot: createdWorktree.repoRoot, + cwd: workspaceCwd, + worktreeRoot: createdWorktree.worktree.worktreePath, + branch: createdWorktree.worktree.branchName || null, + baseBranch: resolveIntentBaseBranch(createdWorktree.intent), + title: resolveFirstAgentPromptTitle(input.firstAgentContext), + }); - return { - worktree: createdWorktree.worktree, - intent: createdWorktree.intent, - workspace, - repoRoot: createdWorktree.repoRoot, - created: createdWorktree.created, - }; + deps.github.invalidate({ cwd: createdWorktree.worktree.worktreePath }); + + return { + worktree: createdWorktree.worktree, + intent: createdWorktree.intent, + workspace, + repoRoot: createdWorktree.repoRoot, + created: createdWorktree.created, + }; + } catch (error) { + if (!createdWorktree.created) { + throw error; + } + return rollbackCreatedPaseoWorktree( + { + cwd: createdWorktree.repoRoot, + worktreePath: createdWorktree.worktree.worktreePath, + ...(input.runSetup === false ? { teardownCwds: [] } : {}), + paseoHome: input.paseoHome, + worktreesBaseRoot: input.worktreesRoot, + }, + error, + ); + } +} + +async function isDirectory(targetPath: string): Promise { + try { + return (await stat(targetPath)).isDirectory(); + } catch { + return false; + } +} + +async function planWorkspaceCwdForWorktree( + inputCwd: string, + workspaceGitService: Pick, +): Promise<{ inputCwd: string; relativeWorkspaceCwd: string }> { + const normalizedInputCwd = resolve(inputCwd); + const sourceCheckout = await workspaceGitService.getCheckout(normalizedInputCwd); + const sourceWorktreePath = sourceCheckout.worktreeRoot ?? normalizedInputCwd; + const relativeWorkspaceCwd = getRealpathAwareRelativePath(sourceWorktreePath, normalizedInputCwd); + if (relativeWorkspaceCwd === null) { + throw new Error(`Workspace cwd is outside its source worktree: ${normalizedInputCwd}`); + } + return { inputCwd: normalizedInputCwd, relativeWorkspaceCwd }; } export async function attemptFirstAgentBranchAutoName(options: { @@ -213,257 +265,3 @@ function resolveIntentBaseBranch(intent: WorktreeCreationIntent): string | null return null; } } - -async function upsertWorkspaceForWorktree(options: { - inputCwd: string; - projectId?: string; - repoRoot: string; - worktree: WorktreeConfig; - baseBranch?: string | null; - title?: string | null; - deps: Pick< - CreatePaseoWorktreeDeps, - "projectRegistry" | "workspaceRegistry" | "workspaceGitService" - >; -}): Promise { - const normalizedCwd = resolve(options.worktree.worktreePath); - const normalizedInputCwd = resolve(options.inputCwd); - const normalizedRepoRoot = resolve(options.repoRoot); - // Creation never deduplicates by directory: a worktree directory may back - // more than one workspace. We still resolve the source project from the - // originating checkout, but always mint a fresh workspace record. - const sourceProject = await resolveSourceProjectForWorktree({ - inputCwd: normalizedInputCwd, - projectId: options.projectId, - repoRoot: normalizedRepoRoot, - existingWorkspace: null, - deps: options.deps, - }); - const workspaceId = generateWorkspaceId(); - const now = new Date().toISOString(); - - await options.deps.projectRegistry.upsert( - createPersistedProjectRecord({ - projectId: sourceProject.projectId, - rootPath: sourceProject.rootPath, - kind: sourceProject.kind, - displayName: sourceProject.displayName, - customName: sourceProject.customName, - createdAt: sourceProject.createdAt ?? now, - updatedAt: now, - archivedAt: null, - }), - ); - - const workspace = createPersistedWorkspaceRecord({ - workspaceId, - projectId: sourceProject.projectId, - cwd: normalizedCwd, - kind: "worktree", - displayName: options.worktree.branchName || normalizedCwd, - branch: options.worktree.branchName || null, - baseBranch: options.baseBranch ?? null, - title: options.title ?? null, - createdAt: now, - updatedAt: now, - archivedAt: null, - }); - - await options.deps.workspaceRegistry.upsert(workspace); - return (await options.deps.workspaceRegistry.get(workspace.workspaceId)) ?? workspace; -} - -export interface CreateLocalCheckoutWorkspaceDeps { - projectRegistry: Pick; - workspaceRegistry: Pick; - workspaceGitService: Pick; -} - -// Always create a NEW workspace record backed by the existing directory `cwd`. -// Never reuses a same-cwd record: a directory may back any number of -// workspaces. Used by explicit user creation. -export async function createLocalCheckoutWorkspace( - options: { cwd: string; title?: string | null }, - deps: CreateLocalCheckoutWorkspaceDeps, -): Promise { - const normalizedCwd = resolve(options.cwd); - const checkout = await deps.workspaceGitService.getCheckout(normalizedCwd); - const membership = classifyDirectoryForProjectMembership({ cwd: normalizedCwd, checkout }); - const now = new Date().toISOString(); - const projectRecord = await resolveProjectRecordForMembership({ - membership, - timestamp: now, - projectRegistry: deps.projectRegistry, - }); - await deps.projectRegistry.upsert(projectRecord); - - const trimmedTitle = options.title?.trim(); - // Persist the live git branch into the dedicated `branch` field so - // buildWorkspaceCheckout reports the real branch for directory/local_checkout - // workspaces too (it reads workspace.branch). Same source deriveWorkspaceDisplayName - // reads. HEAD/detached resolves to null — there is no branch to report. - const currentBranch = checkout.currentBranch?.trim() ?? null; - const branch = currentBranch && currentBranch.toUpperCase() !== "HEAD" ? currentBranch : null; - const workspace = createPersistedWorkspaceRecord({ - workspaceId: generateWorkspaceId(), - projectId: projectRecord.projectId, - cwd: normalizedCwd, - kind: membership.workspaceKind, - displayName: membership.workspaceDisplayName, - branch, - title: trimmedTitle ? trimmedTitle : null, - createdAt: now, - updatedAt: now, - }); - await deps.workspaceRegistry.upsert(workspace); - return workspace; -} - -async function resolveProjectRecordForMembership(options: { - membership: ReturnType; - timestamp: string; - projectRegistry: Pick; -}) { - const rootPath = options.membership.projectRootPath; - const projects = await options.projectRegistry.list(); - const existingProject = - projects.find((project) => !project.archivedAt && project.rootPath === rootPath) ?? - projects.find((project) => project.rootPath === rootPath) ?? - null; - - if (!existingProject) { - return createPersistedProjectRecord({ - projectId: options.membership.projectKey, - rootPath, - kind: options.membership.projectKind, - displayName: options.membership.projectName, - createdAt: options.timestamp, - updatedAt: options.timestamp, - }); - } - - return { - ...existingProject, - rootPath, - kind: options.membership.projectKind, - archivedAt: null, - updatedAt: options.timestamp, - }; -} - -interface SourceProjectForWorktree { - projectId: string; - rootPath: string; - kind: "git"; - displayName: string; - customName: string | null; - createdAt: string | null; -} - -function sourceProjectFromRecord(record: { - projectId: string; - rootPath: string; - displayName: string; - customName?: string | null; - createdAt?: string | null; -}): SourceProjectForWorktree { - return { - projectId: record.projectId, - rootPath: record.rootPath, - kind: "git", - displayName: record.displayName, - customName: record.customName ?? null, - createdAt: record.createdAt ?? null, - }; -} - -async function resolveExplicitProjectForWorktree(options: { - projectId: string; - projectRegistry: Pick; -}): Promise { - const project = await options.projectRegistry.get(options.projectId); - if (!project || project.archivedAt) { - throw new Error(`Project not found for worktree: ${options.projectId}`); - } - return sourceProjectFromRecord(project); -} - -async function resolveWorkspaceProjectForWorktree(options: { - sourceWorkspace: PersistedWorkspaceRecord; - repoRoot: string; - projectRegistry: Pick; -}): Promise { - const sourceProject = await options.projectRegistry.get(options.sourceWorkspace.projectId); - return sourceProjectFromRecord({ - projectId: options.sourceWorkspace.projectId, - rootPath: sourceProject?.rootPath ?? options.repoRoot, - displayName: - sourceProject?.displayName ?? deriveProjectGroupingName(options.sourceWorkspace.projectId), - customName: sourceProject?.customName ?? null, - createdAt: sourceProject?.createdAt ?? null, - }); -} - -async function resolveFallbackProjectForWorktree(options: { - repoRoot: string; - projectRegistry: Pick; -}): Promise { - const existingFallbackProject = await options.projectRegistry.get(options.repoRoot); - return sourceProjectFromRecord({ - projectId: options.repoRoot, - rootPath: existingFallbackProject?.rootPath ?? options.repoRoot, - displayName: - existingFallbackProject?.displayName ?? deriveProjectGroupingName(options.repoRoot), - customName: existingFallbackProject?.customName ?? null, - createdAt: existingFallbackProject?.createdAt ?? null, - }); -} - -async function resolveSourceProjectForWorktree(options: { - inputCwd: string; - projectId?: string; - repoRoot: string; - existingWorkspace: PersistedWorkspaceRecord | null; - deps: Pick; -}): Promise { - if (options.projectId) { - return resolveExplicitProjectForWorktree({ - projectId: options.projectId, - projectRegistry: options.deps.projectRegistry, - }); - } - - const sourceWorkspace = - options.existingWorkspace ?? - (await findWorkspaceForSource({ - inputCwd: options.inputCwd, - repoRoot: options.repoRoot, - workspaceRegistry: options.deps.workspaceRegistry, - })); - - if (sourceWorkspace) { - return resolveWorkspaceProjectForWorktree({ - sourceWorkspace, - repoRoot: options.repoRoot, - projectRegistry: options.deps.projectRegistry, - }); - } - - return resolveFallbackProjectForWorktree({ - repoRoot: options.repoRoot, - projectRegistry: options.deps.projectRegistry, - }); -} - -async function findWorkspaceForSource(options: { - inputCwd: string; - repoRoot: string; - workspaceRegistry: Pick; -}): Promise { - const workspaces = await options.workspaceRegistry.list(); - return ( - workspaces.find((workspace) => workspace.cwd === options.inputCwd && !workspace.archivedAt) ?? - workspaces.find((workspace) => workspace.cwd === options.repoRoot && !workspace.archivedAt) ?? - null - ); -} diff --git a/packages/server/src/server/schedule/service.test.ts b/packages/server/src/server/schedule/service.test.ts index e61d28286..a7fad87be 100644 --- a/packages/server/src/server/schedule/service.test.ts +++ b/packages/server/src/server/schedule/service.test.ts @@ -23,7 +23,7 @@ import type { import { createTestAgentClients } from "../test-utils/fake-agent-client.js"; import { createTestLogger } from "../../test-utils/test-logger.js"; import type { ProviderSnapshotManager } from "../agent/provider-snapshot-manager.js"; -import { createLocalCheckoutWorkspace } from "../paseo-worktree-service.js"; +import { createWorkspaceProvisioningService } from "../session/workspace-provisioning/workspace-provisioning-service.js"; import { resolveWorkspaceIdForPath } from "../resolve-workspace-id-for-path.js"; import { createNoopWorkspaceGitService } from "../test-utils/workspace-git-service-stub.js"; import { @@ -66,15 +66,12 @@ let workspaceArchiveInProgress = false; type TestScheduleServiceOptions = Omit< ScheduleServiceOptions, - | "createAgent" - | "createLocalCheckoutWorkspace" - | "createPaseoWorktreeWorkspace" - | "archiveWorkspace" + "createAgent" | "createDirectoryWorkspace" | "createPaseoWorktreeWorkspace" | "archiveWorkspace" > & { agentManager: AgentManager; providerSnapshotManager: Pick; createAgent?: ScheduleServiceOptions["createAgent"]; - createLocalCheckoutWorkspace?: ScheduleServiceOptions["createLocalCheckoutWorkspace"]; + createDirectoryWorkspace?: ScheduleServiceOptions["createDirectoryWorkspace"]; createPaseoWorktreeWorkspace?: ScheduleServiceOptions["createPaseoWorktreeWorkspace"]; archiveWorkspace?: ScheduleServiceOptions["archiveWorkspace"]; }; @@ -83,7 +80,7 @@ function createScheduleService(options: TestScheduleServiceOptions): ScheduleSer let workspaceCounter = 0; const workspaces = new Map(); const workspaceGitService = createNoopWorkspaceGitService(); - const createDefaultWorkspace: ScheduleServiceOptions["createLocalCheckoutWorkspace"] = async ( + const createDefaultWorkspace: ScheduleServiceOptions["createDirectoryWorkspace"] = async ( input, ) => { const timestamp = new Date().toISOString(); @@ -141,7 +138,6 @@ function createScheduleService(options: TestScheduleServiceOptions): ScheduleSer }, { scope: { kind: "workspace", workspaceId }, - repoRoot: null, requestId: "schedule-service-test", }, ); @@ -163,7 +159,7 @@ function createScheduleService(options: TestScheduleServiceOptions): ScheduleSer }, input, )), - createLocalCheckoutWorkspace: options.createLocalCheckoutWorkspace ?? createDefaultWorkspace, + createDirectoryWorkspace: options.createDirectoryWorkspace ?? createDefaultWorkspace, createPaseoWorktreeWorkspace: options.createPaseoWorktreeWorkspace ?? (async (input) => { @@ -182,7 +178,7 @@ function createScheduleService(options: TestScheduleServiceOptions): ScheduleSer async function createRegistryBackedScheduleWorkspaceDeps(rootDir: string): Promise<{ workspaceRegistry: FileBackedWorkspaceRegistry; - createLocalCheckoutWorkspace: ScheduleServiceOptions["createLocalCheckoutWorkspace"]; + createDirectoryWorkspace: ScheduleServiceOptions["createDirectoryWorkspace"]; createArchiveWorkspace: (input: { agentManager: AgentManager; agentStorage: AgentStorage; @@ -200,12 +196,17 @@ async function createRegistryBackedScheduleWorkspaceDeps(rootDir: string): Promi await workspaceRegistry.initialize(); await projectRegistry.initialize(); const workspaceGitService = createNoopWorkspaceGitService(); + const workspaceProvisioning = createWorkspaceProvisioningService({ + projectRegistry, + workspaceRegistry, + workspaceGitService, + }); return { workspaceRegistry, - createLocalCheckoutWorkspace: async (input) => { - return createLocalCheckoutWorkspace( - { cwd: input.cwd, title: input.firstAgentContext.prompt }, - { projectRegistry, workspaceRegistry, workspaceGitService }, + createDirectoryWorkspace: async (input) => { + return workspaceProvisioning.createWorkspaceForDirectory( + input.cwd, + input.firstAgentContext.prompt, ); }, createArchiveWorkspace: @@ -240,7 +241,6 @@ async function createRegistryBackedScheduleWorkspaceDeps(rootDir: string): Promi }, { scope: { kind: "workspace", workspaceId }, - repoRoot: null, requestId: "schedule-service-test", }, ); @@ -493,7 +493,7 @@ describe("ScheduleService", () => { }); test("new-agent schedule records create no workspace until run time", async () => { - const { workspaceRegistry, createLocalCheckoutWorkspace: createScheduleLocalWorkspace } = + const { workspaceRegistry, createDirectoryWorkspace: createScheduleDirectoryWorkspace } = await createRegistryBackedScheduleWorkspaceDeps(tempDir); const service = createScheduleService({ paseoHome: tempDir, @@ -501,7 +501,7 @@ describe("ScheduleService", () => { agentManager: new AgentManager({ logger: createTestLogger() }), agentStorage, providerSnapshotManager: NO_UNATTENDED_SCHEDULE_POLICY, - createLocalCheckoutWorkspace: createScheduleLocalWorkspace, + createDirectoryWorkspace: createScheduleDirectoryWorkspace, now: () => now, runner: async () => ({ agentId: null, output: "ok" }), }); @@ -531,7 +531,7 @@ describe("ScheduleService", () => { test("archiveOnFinish=false local runs create one active workspace per run", async () => { const { workspaceRegistry, - createLocalCheckoutWorkspace: createScheduleLocalWorkspace, + createDirectoryWorkspace: createScheduleDirectoryWorkspace, createArchiveWorkspace, } = await createRegistryBackedScheduleWorkspaceDeps(tempDir); const manager = new AgentManager({ @@ -545,7 +545,7 @@ describe("ScheduleService", () => { agentManager: manager, agentStorage, providerSnapshotManager: NO_UNATTENDED_SCHEDULE_POLICY, - createLocalCheckoutWorkspace: createScheduleLocalWorkspace, + createDirectoryWorkspace: createScheduleDirectoryWorkspace, archiveWorkspace: createArchiveWorkspace({ agentManager: manager, agentStorage, @@ -599,7 +599,7 @@ describe("ScheduleService", () => { test("archiveOnFinish=true archives the run workspace through workspace archive", async () => { const { workspaceRegistry, - createLocalCheckoutWorkspace: createScheduleLocalWorkspace, + createDirectoryWorkspace: createScheduleDirectoryWorkspace, createArchiveWorkspace, } = await createRegistryBackedScheduleWorkspaceDeps(tempDir); const manager = new AgentManager({ @@ -620,7 +620,7 @@ describe("ScheduleService", () => { agentManager: manager, agentStorage, providerSnapshotManager: NO_UNATTENDED_SCHEDULE_POLICY, - createLocalCheckoutWorkspace: createScheduleLocalWorkspace, + createDirectoryWorkspace: createScheduleDirectoryWorkspace, archiveWorkspace: createArchiveWorkspace({ agentManager: manager, agentStorage, @@ -663,7 +663,7 @@ describe("ScheduleService", () => { test("archives the run workspace when scheduled agent creation fails before archive opt-out can preserve an agent", async () => { const { workspaceRegistry, - createLocalCheckoutWorkspace: createScheduleLocalWorkspace, + createDirectoryWorkspace: createScheduleDirectoryWorkspace, createArchiveWorkspace, } = await createRegistryBackedScheduleWorkspaceDeps(tempDir); const manager = new AgentManager({ @@ -678,7 +678,7 @@ describe("ScheduleService", () => { agentManager: manager, agentStorage, providerSnapshotManager: NO_UNATTENDED_SCHEDULE_POLICY, - createLocalCheckoutWorkspace: createScheduleLocalWorkspace, + createDirectoryWorkspace: createScheduleDirectoryWorkspace, archiveWorkspace: createArchiveWorkspace({ agentManager: manager, agentStorage, @@ -1904,7 +1904,7 @@ describe("ScheduleService", () => { ], })); - const archiveCalls: Array<{ workspaceId: string; repoRoot: string }> = []; + const archiveCalls: string[] = []; now = new Date("2026-01-01T00:10:00.000Z"); const service2 = createScheduleService({ paseoHome: tempDir, @@ -1914,13 +1914,13 @@ describe("ScheduleService", () => { providerSnapshotManager: NO_UNATTENDED_SCHEDULE_POLICY, now: () => now, runner: async () => ({ agentId: null, output: "ok" }), - archiveWorkspace: async (archivedWorkspaceId, repoRoot) => { - archiveCalls.push({ workspaceId: archivedWorkspaceId, repoRoot }); + archiveWorkspace: async (archivedWorkspaceId) => { + archiveCalls.push(archivedWorkspaceId); }, }); await service2.start(); - expect(archiveCalls).toEqual([{ workspaceId, repoRoot: tempDir }]); + expect(archiveCalls).toEqual([workspaceId]); const inspected = await service2.inspect(created.id); expect(inspected.runs[0]).toMatchObject({ status: "failed", @@ -1972,7 +1972,7 @@ describe("ScheduleService", () => { ], })); - const archiveCalls: Array<{ workspaceId: string; repoRoot: string }> = []; + const archiveCalls: string[] = []; now = new Date("2026-01-01T00:10:00.000Z"); const service2 = createScheduleService({ paseoHome: tempDir, @@ -1982,13 +1982,13 @@ describe("ScheduleService", () => { providerSnapshotManager: NO_UNATTENDED_SCHEDULE_POLICY, now: () => now, runner: async () => ({ agentId: null, output: "ok" }), - archiveWorkspace: async (archivedWorkspaceId, repoRoot) => { - archiveCalls.push({ workspaceId: archivedWorkspaceId, repoRoot }); + archiveWorkspace: async (archivedWorkspaceId) => { + archiveCalls.push(archivedWorkspaceId); }, }); await service2.start(); - expect(archiveCalls).toEqual([{ workspaceId, repoRoot: tempDir }]); + expect(archiveCalls).toEqual([workspaceId]); const inspected = await service2.inspect(created.id); expect(inspected.runs[0]).toMatchObject({ status: "failed", diff --git a/packages/server/src/server/schedule/service.ts b/packages/server/src/server/schedule/service.ts index bc5d63b9e..d55ecb464 100644 --- a/packages/server/src/server/schedule/service.ts +++ b/packages/server/src/server/schedule/service.ts @@ -218,13 +218,13 @@ export interface ScheduleServiceOptions { agentManager: ScheduleAgentManager; agentStorage: AgentStorage; createAgent: BoundCreateAgentCommand; - createLocalCheckoutWorkspace: ( + createDirectoryWorkspace: ( input: ScheduleWorkspaceCreateInput, ) => Promise; createPaseoWorktreeWorkspace: ( input: ScheduleWorkspaceCreateInput, ) => Promise; - archiveWorkspace: (workspaceId: string, repoRoot: string) => Promise; + archiveWorkspace: (workspaceId: string) => Promise; now?: () => Date; runner?: (schedule: StoredSchedule, runId: string) => Promise; } @@ -235,13 +235,13 @@ export class ScheduleService { private readonly agentManager: ScheduleAgentManager; private readonly agentStorage: AgentStorage; private readonly createAgent: BoundCreateAgentCommand; - private readonly createLocalCheckoutWorkspace: ( + private readonly createDirectoryWorkspace: ( input: ScheduleWorkspaceCreateInput, ) => Promise; private readonly createPaseoWorktreeWorkspace: ( input: ScheduleWorkspaceCreateInput, ) => Promise; - private readonly archiveWorkspace: (workspaceId: string, repoRoot: string) => Promise; + private readonly archiveWorkspace: (workspaceId: string) => Promise; private readonly now: () => Date; private readonly runner: ( schedule: StoredSchedule, @@ -256,7 +256,7 @@ export class ScheduleService { this.agentManager = options.agentManager; this.agentStorage = options.agentStorage; this.createAgent = options.createAgent; - this.createLocalCheckoutWorkspace = options.createLocalCheckoutWorkspace; + this.createDirectoryWorkspace = options.createDirectoryWorkspace; this.createPaseoWorktreeWorkspace = options.createPaseoWorktreeWorkspace; this.archiveWorkspace = options.archiveWorkspace; this.now = options.now ?? (() => new Date()); @@ -579,7 +579,6 @@ export class ScheduleService { private async recoverInterruptedSchedule(scheduleId: string, now: Date): Promise { const interruptedWorkspaces: Array<{ workspaceId: string; - repoRoot: string; agentId: string | null; runId: string; }> = []; @@ -601,7 +600,6 @@ export class ScheduleService { ) { interruptedWorkspaces.push({ workspaceId: runningRun.workspaceId, - repoRoot: updated.target.config.cwd, agentId: runningRun.agentId, runId: runningRun.id, }); @@ -639,7 +637,7 @@ export class ScheduleService { return; } try { - await this.archiveWorkspace(interruptedWorkspace.workspaceId, interruptedWorkspace.repoRoot); + await this.archiveWorkspace(interruptedWorkspace.workspaceId); } catch (error) { this.logger.warn( { @@ -930,7 +928,7 @@ export class ScheduleService { shouldArchiveScheduleRunWorkspace({ agentId, archiveOnFinish: config.archiveOnFinish }) ) { try { - await this.archiveWorkspace(workspace.workspaceId, config.cwd); + await this.archiveWorkspace(workspace.workspaceId); } catch (error) { this.logger.warn( { @@ -954,7 +952,7 @@ export class ScheduleService { const firstAgentContext = { prompt }; switch (config.isolation ?? "local") { case "local": - return this.createLocalCheckoutWorkspace({ cwd: config.cwd, firstAgentContext }); + return this.createDirectoryWorkspace({ cwd: config.cwd, firstAgentContext }); case "worktree": return (await this.createPaseoWorktreeWorkspace({ cwd: config.cwd, firstAgentContext })) .workspace; diff --git a/packages/server/src/server/session.create-agent-worktree-autoarchive.e2e.test.ts b/packages/server/src/server/session.create-agent-worktree-autoarchive.e2e.test.ts index c76827e95..8507a935e 100644 --- a/packages/server/src/server/session.create-agent-worktree-autoarchive.e2e.test.ts +++ b/packages/server/src/server/session.create-agent-worktree-autoarchive.e2e.test.ts @@ -1,5 +1,5 @@ import { execFileSync } from "node:child_process"; -import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; import { afterEach, beforeEach, expect, test } from "vitest"; @@ -8,6 +8,7 @@ import { getFullAccessConfig } from "./daemon-e2e/agent-configs.js"; import { createDaemonTestContext, type DaemonTestContext } from "./test-utils/index.js"; import type { CreateAgentOptions } from "./test-utils/index.js"; import type { CreateAgentWorktreeTarget } from "./messages.js"; +import { createRealpathAwarePathMatcher } from "../utils/path.js"; let ctx: DaemonTestContext; const tempRoots: string[] = []; @@ -42,6 +43,18 @@ function createGitRepo(): string { return repoDir; } +function createGitRepoWithNestedDirectory(): string { + const repoDir = createGitRepo(); + mkdirSync(path.join(repoDir, "packages", "app"), { recursive: true }); + writeFileSync(path.join(repoDir, "packages", "app", ".gitkeep"), ""); + execFileSync("git", ["add", "."], { cwd: repoDir, stdio: "pipe" }); + execFileSync("git", ["-c", "commit.gpgsign=false", "commit", "-m", "add nested app"], { + cwd: repoDir, + stdio: "pipe", + }); + return repoDir; +} + async function expectAgentAbsentFromActiveList(agentId: string): Promise { await expect .poll( @@ -84,8 +97,9 @@ async function expectWorktreeListEmpty(repoDir: string): Promise { async function createAgentInBranchOffWorktree(options?: { autoArchive?: boolean; branchName?: string; + repoDir?: string; }): Promise<{ repoDir: string; agentId: string; worktreePath: string }> { - const repoDir = createGitRepo(); + const repoDir = options?.repoDir ?? createGitRepo(); const branchName = options?.branchName ?? `agent-lifecycle-${Date.now()}`; const created = await ctx.client.createAgent({ config: { @@ -142,6 +156,109 @@ test("create_agent_request creates a worktree and auto-archives both after the f await expect.poll(() => existsSync(created.cwd), { timeout: 10000, interval: 100 }).toBe(false); }, 30000); +test("create_agent_request auto-archives a nested workspace from an existing Paseo worktree", async () => { + const repoDir = createGitRepoWithNestedDirectory(); + const source = await createAgentInBranchOffWorktree({ branchName: "nested-source", repoDir }); + await ctx.client.waitForFinish(source.agentId, 10000); + const nestedCwd = path.join(source.worktreePath, "packages", "app"); + + const created = await ctx.client.createAgent({ + config: { + ...getFullAccessConfig("codex"), + cwd: nestedCwd, + }, + worktree: { + mode: "branch-off", + newBranch: "nested-auto-archive", + base: "main", + }, + autoArchive: true, + initialPrompt: "Say done.", + }); + + const createdWorktreeRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], { + cwd: created.cwd, + stdio: "pipe", + }) + .toString() + .trim(); + expect( + createRealpathAwarePathMatcher(path.join(createdWorktreeRoot, "packages", "app"))(created.cwd), + ).toBe(true); + await ctx.client.waitForFinish(created.id, 10000); + + await expectAgentAbsentFromActiveList(created.id); + await expect + .poll( + async () => { + const workspaces = await ctx.client.fetchWorkspaces(); + const matchesCreatedWorkspace = createRealpathAwarePathMatcher(created.cwd); + return workspaces.entries.some((workspace) => + matchesCreatedWorkspace(workspace.workspaceDirectory), + ); + }, + { timeout: 10000, interval: 100 }, + ) + .toBe(false); + await expect.poll(() => existsSync(created.cwd), { timeout: 10000, interval: 100 }).toBe(false); + expect(existsSync(source.worktreePath)).toBe(true); + + await ctx.client.archivePaseoWorktree({ worktreePath: source.worktreePath }); +}, 30000); + +test("failed nested worktree creation cleans up the created workspace and backing directory", async () => { + const repoDir = createGitRepoWithNestedDirectory(); + const source = await createAgentInBranchOffWorktree({ + branchName: "nested-failure-source", + repoDir, + }); + await ctx.client.waitForFinish(source.agentId, 10000); + const nestedCwd = path.join(source.worktreePath, "packages", "app"); + + await expect( + ctx.client.createAgent({ + config: { provider: "unknown-provider", cwd: nestedCwd }, + worktree: { + mode: "branch-off", + newBranch: "nested-failure-cleanup", + base: "main", + }, + initialPrompt: "This agent cannot be created.", + }), + ).rejects.toThrow(); + + await expect + .poll( + async () => { + const listed = await ctx.client.getPaseoWorktreeList({ cwd: source.repoDir }); + return ( + listed.worktrees.length === 1 && + createRealpathAwarePathMatcher(source.worktreePath)( + listed.worktrees[0]?.worktreePath ?? "", + ) + ); + }, + { timeout: 10000, interval: 100 }, + ) + .toBe(true); + await expect + .poll( + async () => { + const workspaces = await ctx.client.fetchWorkspaces(); + return ( + workspaces.entries.length === 1 && + createRealpathAwarePathMatcher(source.worktreePath)( + workspaces.entries[0]?.workspaceDirectory ?? "", + ) + ); + }, + { timeout: 10000, interval: 100 }, + ) + .toBe(true); + + await ctx.client.archivePaseoWorktree({ worktreePath: source.worktreePath }); +}, 30000); + test("create_agent_request with autoArchive archives only the agent when no worktree was created", async () => { const repoDir = createGitRepo(); const created = await ctx.client.createAgent({ diff --git a/packages/server/src/server/session.test.ts b/packages/server/src/server/session.test.ts index 9f05447e8..683d2821e 100644 --- a/packages/server/src/server/session.test.ts +++ b/packages/server/src/server/session.test.ts @@ -24,6 +24,7 @@ import { StructuredAgentFallbackError } from "./agent/agent-response-loop.js"; import type { StoredAgentRecord } from "./agent/agent-storage.js"; import type { AgentManagerEvent } from "./agent/agent-manager.js"; import type { ProviderSnapshotManager } from "./agent/provider-snapshot-manager.js"; +import { createPersistedProjectRecord } from "./workspace-registry.js"; import type { SessionOptions } from "./session.js"; import type { SessionInboundMessage, SessionOutboundMessage } from "./messages.js"; import { @@ -294,6 +295,7 @@ interface SessionForTestOptions { resolveRepoRoot?: ReturnType; resolveForge?: ReturnType; getWorkspaceGitMetadata?: ReturnType; + getProjectSlug?: ReturnType; }; workspaceRegistry?: { get: ReturnType }; projectRegistry?: Partial; @@ -342,6 +344,7 @@ function createSessionForTest(options: SessionForTestOptions = {}): Session { // Mirror production: invalidateForge resolves the forge and busts the // adapter's cache. The resolved forge here is github, so delegate to it. invalidateForge: vi.fn((cwd: string) => github.invalidate({ cwd })), + getProjectSlug: vi.fn(), ...options.workspaceGitService, }; const messages = options.messages ?? []; @@ -369,14 +372,16 @@ function createSessionForTest(options: SessionForTestOptions = {}): Session { list: vi.fn().mockResolvedValue([]), ...options.agentStorage, }), - projectRegistry: options.projectRegistry ?? { + projectRegistry: { list: vi.fn().mockResolvedValue([]), get: vi.fn(), + getOrCreateActiveByRoot: vi.fn(), upsert: vi.fn(), archive: vi.fn(), remove: vi.fn(), initialize: vi.fn(), existsOnDisk: vi.fn(), + ...options.projectRegistry, }, workspaceRegistry: options.workspaceRegistry ?? { get: vi.fn(), @@ -525,12 +530,20 @@ describe("project command-center RPCs", () => { const parentDirectory = realpathSync(mkdtempSync(join(tmpdir(), "paseo-project-session-"))); const directoryPath = join(parentDirectory, "new-project"); const messages: SessionOutboundMessage[] = []; - const projectUpsert = vi.fn().mockResolvedValue(undefined); + const projectAllocation = vi.fn(async (input) => + createPersistedProjectRecord({ + projectId: "prj_created_directory", + rootPath: input.rootPath, + kind: input.kind, + displayName: input.displayName, + createdAt: input.timestamp, + updatedAt: input.timestamp, + }), + ); const session = createSessionForTest({ messages, projectRegistry: { - list: vi.fn().mockResolvedValue([]), - upsert: projectUpsert, + getOrCreateActiveByRoot: projectAllocation, }, workspaceGitService: { getCheckout: vi.fn(async (cwd: string) => ({ @@ -554,7 +567,12 @@ describe("project command-center RPCs", () => { }); expect(existsSync(directoryPath)).toBe(true); - expect(projectUpsert).toHaveBeenCalledOnce(); + expect(projectAllocation).toHaveBeenCalledWith({ + rootPath: directoryPath, + kind: "non_git", + displayName: "new-project", + timestamp: expect.any(String), + }); expect(messages).toEqual([ { type: "project.create_directory.response", @@ -562,7 +580,7 @@ describe("project command-center RPCs", () => { requestId: "req-create-directory", directoryPath, project: { - projectId: directoryPath, + projectId: "prj_created_directory", projectDisplayName: "new-project", projectCustomName: null, projectRootPath: directoryPath, @@ -585,8 +603,7 @@ describe("project command-center RPCs", () => { const session = createSessionForTest({ messages, projectRegistry: { - list: vi.fn().mockResolvedValue([]), - upsert: vi.fn().mockRejectedValue(new Error("registry unavailable")), + getOrCreateActiveByRoot: vi.fn().mockRejectedValue(new Error("registry unavailable")), }, workspaceGitService: { getCheckout: vi.fn(async (cwd: string) => ({ @@ -4041,17 +4058,17 @@ describe("session paseo worktree creation handling", () => { }); describe("session workspace script handling", () => { - test("passes service-owned git metadata into workspace script spawning", async () => { + test("passes the project slug and cached branch into workspace script spawning", async () => { const messages: unknown[] = []; - const workspaceGitService = { - peekSnapshot: vi.fn(() => null), - getWorkspaceGitMetadata: vi.fn().mockResolvedValue({ - projectKind: "git", - projectDisplayName: "getpaseo/paseo", - workspaceDisplayName: "feature/service-scripts", - projectSlug: "paseo", + const snapshot = createWorkspaceGitSnapshot("/tmp/repo", { + git: { currentBranch: "feature/service-scripts", - }), + remoteUrl: "https://github.com/getpaseo/paseo.git", + }, + }); + const workspaceGitService = { + peekSnapshot: vi.fn(() => snapshot), + getProjectSlug: vi.fn().mockResolvedValue("paseo"), }; const workspaceRegistry = { get: vi.fn().mockResolvedValue({ @@ -4084,8 +4101,6 @@ describe("session workspace script handling", () => { requestId: "request-script", }); - expect(workspaceGitService.getWorkspaceGitMetadata).toHaveBeenCalledTimes(1); - expect(workspaceGitService.getWorkspaceGitMetadata).toHaveBeenCalledWith("/tmp/repo"); expect(spawnMocks.spawnWorkspaceScript).toHaveBeenCalledWith( expect.objectContaining({ repoRoot: "/tmp/repo", @@ -4773,6 +4788,39 @@ test("keeps selective delivery scoped per socket when a retained session also ha ]); }); +test("sends project updates only to capable sockets in a retained session", () => { + const messages: SessionOutboundMessage[] = []; + const targetedMessages: Array<{ source: object; message: SessionOutboundMessage }> = []; + const session = createSessionForTest({ messages, targetedMessages }); + const legacySocket = {}; + const capableSocket = {}; + session.updateClientCapabilities(null, legacySocket); + session.updateClientCapabilities({ [CLIENT_CAPS.projectUpdates]: true }, capableSocket); + + session.emitProjectUpdate({ + kind: "upsert", + project: createPersistedProjectRecord({ + projectId: "project-capable-socket", + rootPath: "/tmp/project-capable-socket", + kind: "git", + displayName: "project-capable-socket", + createdAt: "2026-07-17T00:00:00.000Z", + updatedAt: "2026-07-17T00:00:00.000Z", + }), + }); + + expect(messages).toEqual([]); + expect(targetedMessages).toEqual([ + { + source: capableSocket, + message: expect.objectContaining({ + type: "project.update", + payload: expect.objectContaining({ kind: "upsert" }), + }), + }, + ]); +}); + describe("agent config setters", () => { test("set_agent_mode_request: success emits accepted response carrying the notice", async () => { const messages: SessionOutboundMessage[] = []; diff --git a/packages/server/src/server/session.ts b/packages/server/src/server/session.ts index ee010eb29..70fbb9b13 100644 --- a/packages/server/src/server/session.ts +++ b/packages/server/src/server/session.ts @@ -1,7 +1,7 @@ import equal from "fast-deep-equal"; import { v4 as uuidv4 } from "uuid"; import { lstat, mkdir, mkdtemp, rename, rm, stat } from "node:fs/promises"; -import { basename, normalize, resolve, sep } from "path"; +import { resolve, sep } from "path"; import { homedir } from "node:os"; import { CLIENT_CAPS, type ClientCapability } from "@getpaseo/protocol/client-capabilities"; import { @@ -61,6 +61,7 @@ import { getErrorMessage, getErrorMessageOr } from "@getpaseo/protocol/error-uti import { getAgentStatusPriority } from "@getpaseo/protocol/agent-state-bucket"; import { getParentAgentIdFromLabels } from "@getpaseo/protocol/agent-labels"; import type { WorkspaceGitRuntimeSnapshot, WorkspaceGitService } from "./workspace-git-service.js"; +import type { ProjectUpdate } from "./workspace-reconciliation-service.js"; import { CLIENT_SHUTDOWN_RPC_REASON, normalizeClientRestartRpcReason, @@ -119,6 +120,7 @@ import { } from "./agent/import-sessions.js"; import { checkoutLiteFromGitSnapshot, + checkoutFromPersistedWorkspacePlacement, deriveWorkspaceDisplayName, } from "./workspace-registry-model.js"; import { resolveWorkspaceIdForPath } from "./resolve-workspace-id-for-path.js"; @@ -165,6 +167,7 @@ import { } from "./session/git-mutation/git-mutation-service.js"; import { createWorkspaceProvisioningService, + WorkspaceProvisioningError, type WorkspaceProvisioningService, } from "./session/workspace-provisioning/workspace-provisioning-service.js"; import { @@ -199,13 +202,13 @@ import type { ForgeService } from "../services/forge-service.js"; import type { ProviderUsageService } from "../services/quota-fetcher/service.js"; import { summarizeFetchWorkspacesEntries, + workspaceIdsForProjects, workspaceIdsOnCheckout, WorkspaceDirectory, type WorkspaceUpdatesFilter, } from "./workspace-directory.js"; import { shouldEmitPendingBootstrapUpdate } from "./workspace-bootstrap-dedupe.js"; import { - createLocalCheckoutWorkspace, createPaseoWorktree, type CreatePaseoWorktreeInput, type CreatePaseoWorktreeResult, @@ -222,17 +225,12 @@ import { handleWorkspaceSetupStatusRequest as handleWorkspaceSetupStatusRequestMessage, } from "./worktree-session.js"; import { archiveByScope, type ActiveWorkspaceRef } from "./workspace-archive-service.js"; -import { - WorktreeRequestError, - toWorktreeRequestError, - toWorktreeWireError, -} from "./worktree-errors.js"; +import { WorktreeRequestError, toWorktreeWireError } from "./worktree-errors.js"; import { parseGitRemoteLocation } from "@getpaseo/protocol/git-remote"; import { createProjectDirectory, ProjectDirectoryRequestError, } from "./project-directory-service.js"; -import { type WorktreeConfig, createWorktree } from "../utils/worktree.js"; import { runGitCommand } from "../utils/run-git-command.js"; import { CreateAgentLifecycleDispatch } from "./agent/create-agent-lifecycle-dispatch.js"; @@ -259,49 +257,6 @@ function resolveSubscriptionId( return uuidv4(); } -function buildWorkspaceCheckout( - workspace: PersistedWorkspaceRecord, - project: PersistedProjectRecord, - // The persisted `branch` field is the source of truth, but it is null for - // records created before branch was lifted to its own field (no migrations, - // per data-model.md) and for any path that didn't backfill it. Fall back to - // the live git branch so checkout.currentBranch never regresses to null. - fallbackBranch?: string | null, -): ProjectPlacementPayload["checkout"] { - if (project.kind !== "git") { - return { - cwd: workspace.cwd, - isGit: false, - currentBranch: null, - remoteUrl: null, - worktreeRoot: null, - isPaseoOwnedWorktree: false, - mainRepoRoot: null, - }; - } - const currentBranch = workspace.branch ?? fallbackBranch ?? null; - if (workspace.kind === "worktree") { - return { - cwd: workspace.cwd, - isGit: true, - currentBranch, - remoteUrl: null, - worktreeRoot: workspace.cwd, - isPaseoOwnedWorktree: true, - mainRepoRoot: project.rootPath, - }; - } - return { - cwd: workspace.cwd, - isGit: true, - currentBranch, - remoteUrl: null, - worktreeRoot: workspace.cwd, - isPaseoOwnedWorktree: false, - mainRepoRoot: null, - }; -} - function isAppVersionAtLeast(appVersion: string | null, minVersion: string): boolean { if (!appVersion) return false; // Strip prerelease suffix: "0.1.45-beta.4" -> "0.1.45" @@ -589,7 +544,7 @@ export class Session { private unsubscribeAgentEvents: (() => void) | null = null; private viewedTimelineAgentIds = new Set(); private readonly viewedTimelineAgentIdsBySource = new Map>(); - private readonly selectiveTimelineCapabilityBySource = new Map(); + private readonly clientCapabilitiesBySource = new Map>(); private readonly defaultTimelineSubscriptionSource = {}; private unsubscribeTerminalWorkspaceContributionEvents: (() => void) | null = null; private readonly agentUpdates: AgentUpdatesService; @@ -727,10 +682,11 @@ export class Session { logger: this.sessionLogger, }); this.workspaceRecovery = createWorkspaceRecoveryService({ + paseoHome: this.paseoHome, + worktreesRoot: this.worktreesRoot, getWorkspace: (workspaceId) => this.workspaceRegistry.get(workspaceId), getProject: (projectId) => this.projectRegistry.get(projectId), isDirectory: (path) => this.filesystem.isDirectory(path), - recreateWorktree: (workspace) => this.recreateArchivedWorktree(workspace), unarchiveWorkspace: async (workspace) => { await this.workspaceProvisioning.ensureWorkspaceRecordUnarchived(workspace); }, @@ -907,6 +863,7 @@ export class Session { scriptRuntimeStore: this.scriptRuntimeStore, terminalManager: this.terminalManager, workspaceRegistry: this.workspaceRegistry, + projectRegistry: this.projectRegistry, workspaceGitService: this.workspaceGitService, getDaemonTcpPort: this.getDaemonTcpPort, getDaemonTcpHost: this.getDaemonTcpHost, @@ -976,10 +933,7 @@ export class Session { updateClientCapabilities(capabilities: Record | null, source?: object): void { this.clientCapabilities = parseClientCapabilities(capabilities); if (source) { - this.selectiveTimelineCapabilityBySource.set( - source, - this.supports(CLIENT_CAPS.selectiveAgentTimeline), - ); + this.clientCapabilitiesBySource.set(source, this.clientCapabilities); } if (!source && !this.supports(CLIENT_CAPS.selectiveAgentTimeline)) { this.viewedTimelineAgentIdsBySource.clear(); @@ -988,7 +942,7 @@ export class Session { } clearAgentTimelineSubscription(source: object): void { - this.selectiveTimelineCapabilityBySource.delete(source); + this.clientCapabilitiesBySource.delete(source); if (this.viewedTimelineAgentIdsBySource.delete(source)) { this.rebuildViewedTimelineAgentIds(); } @@ -1010,11 +964,11 @@ export class Session { } private usesSelectiveTimelineDelivery(): boolean { - if (this.selectiveTimelineCapabilityBySource.size === 0) { + if (this.clientCapabilitiesBySource.size === 0) { return this.supports(CLIENT_CAPS.selectiveAgentTimeline); } - for (const capable of this.selectiveTimelineCapabilityBySource.values()) { - if (!capable) return false; + for (const capabilities of this.clientCapabilitiesBySource.values()) { + if (!capabilities.has(CLIENT_CAPS.selectiveAgentTimeline)) return false; } return true; } @@ -1023,7 +977,7 @@ export class Session { event: Extract, serializedEvent: Extract["payload"]["event"], ): void { - if (this.selectiveTimelineCapabilityBySource.size === 0 || !this.onMessageToSource) { + if (this.clientCapabilitiesBySource.size === 0 || !this.onMessageToSource) { if (this.usesSelectiveTimelineDelivery() && serializedEvent.type === "attention_required") { this.emit({ type: "agent_attention_required", @@ -1047,7 +1001,8 @@ export class Session { return; } - for (const [source, supportsSelectiveDelivery] of this.selectiveTimelineCapabilityBySource) { + for (const [source, capabilities] of this.clientCapabilitiesBySource) { + const supportsSelectiveDelivery = capabilities.has(CLIENT_CAPS.selectiveAgentTimeline); if (supportsSelectiveDelivery && serializedEvent.type === "attention_required") { this.onMessageToSource(source, { type: "agent_attention_required", @@ -1079,10 +1034,28 @@ export class Session { } supportsForSource(capability: ClientCapability, source: object): boolean { - if (capability === CLIENT_CAPS.selectiveAgentTimeline) { - return this.selectiveTimelineCapabilityBySource.get(source) ?? this.supports(capability); + return ( + this.clientCapabilitiesBySource.get(source)?.has(capability) ?? this.supports(capability) + ); + } + + emitProjectUpdate(update: ProjectUpdate): void { + const message: SessionOutboundMessage = { + type: "project.update", + payload: + update.kind === "upsert" + ? { kind: "upsert", project: this.buildProjectDescriptor(update.project) } + : update, + }; + if (this.clientCapabilitiesBySource.size === 0 || !this.onMessageToSource) { + if (this.supports(CLIENT_CAPS.projectUpdates)) this.emit(message); + return; + } + for (const [source, capabilities] of this.clientCapabilitiesBySource) { + if (capabilities.has(CLIENT_CAPS.projectUpdates)) { + this.onMessageToSource(source, message); + } } - return this.supports(capability); } async syncWorkspaceGitObserverForWorkspace(workspace: PersistedWorkspaceRecord): Promise { @@ -1118,8 +1091,24 @@ export class Session { this.clearWorkspaceArchiving(workspaceIds); } - async emitWorkspaceUpdatesForExternalWorkspaceIds(workspaceIds: Iterable): Promise { - await this.emitWorkspaceUpdatesForWorkspaceIds(workspaceIds); + async emitWorkspaceUpdatesForExternalWorkspaceIds( + workspaceIds: Iterable, + options?: { skipReconcile?: boolean }, + ): Promise { + await this.emitWorkspaceUpdatesForWorkspaceIds(workspaceIds, options); + } + + async syncWorkspaceGitObserversForExternalWorkspaceIds( + workspaceIds: Iterable, + ): Promise { + await Promise.all( + Array.from(new Set(workspaceIds)).map(async (workspaceId) => { + const workspace = await this.workspaceRegistry.get(workspaceId); + if (workspace && !workspace.archivedAt) { + await this.workspaceGitObserver.syncObserverForWorkspace(workspace); + } + }), + ); } async warmWorkspaceGitDataForWorkspace(workspace: PersistedWorkspaceRecord): Promise { @@ -1473,9 +1462,15 @@ export class Session { if (!project) { throw new Error(`Project not found for workspace ${workspace.workspaceId}`); } - const liveBranch = - this.workspaceGitService.peekSnapshot(workspace.cwd)?.git.currentBranch ?? null; - const checkout = buildWorkspaceCheckout(workspace, project, liveBranch); + const snapshot = this.workspaceGitService.peekSnapshot(workspace.cwd); + const checkout = checkoutFromPersistedWorkspacePlacement({ + workspace, + // COMPAT(workspacePlacementBackfill): added in v0.1.107, remove after 2027-01-15. + // Legacy records can lack branch and worktreeRoot because persisted registries + // are not migrated in place. + fallbackBranch: snapshot?.git.currentBranch ?? null, + fallbackWorktreeRoot: snapshot?.git.repoRoot, + }); return { projectKey: project.projectId, projectName: resolveProjectDisplayName(project), @@ -1652,8 +1647,7 @@ export class Session { const agentIds = [...new Set(msg.agentIds)].sort(); if ( source - ? (this.selectiveTimelineCapabilityBySource.get(source) ?? - this.supports(CLIENT_CAPS.selectiveAgentTimeline)) + ? this.supportsForSource(CLIENT_CAPS.selectiveAgentTimeline, source) : this.supports(CLIENT_CAPS.selectiveAgentTimeline) ) { this.replaceAgentTimelineSubscription(source, agentIds); @@ -2756,7 +2750,7 @@ export class Session { }); createdWorktreeForCleanup = createdWorktree; const createAgentConfig: AgentSessionConfig = createdWorktree - ? { ...config, cwd: createdWorktree.worktree.worktreePath } + ? { ...config, cwd: createdWorktree.workspace.cwd } : config; const workspaceId = await this.workspaceProvisioning.resolveOrCreateWorkspaceIdForCreateAgent( { @@ -3891,7 +3885,7 @@ export class Session { statusEnteredAt: null, activityAt: null, diffStat, - scripts: this.buildWorkspaceScriptPayloadSnapshot(workspace.workspaceId, workspace.cwd), + scripts: this.buildWorkspaceScriptPayloadSnapshot(workspace, resolvedProjectRecord), ...(resolvedProjectRecord ? { project: await this.buildProjectPlacementForWorkspace(workspace, resolvedProjectRecord), @@ -3967,7 +3961,7 @@ export class Session { projectCustomName: projectRecord?.customName ?? null, projectRootPath: projectRecord?.rootPath ?? result.repoRoot, workspaceDirectory: result.workspace.cwd, - projectKind: "git", + projectKind: projectRecord?.kind ?? "git", workspaceKind: result.workspace.kind, name: resolveWorkspaceName({ title: result.workspace.title, @@ -3999,7 +3993,7 @@ export class Session { projectRecord?: PersistedProjectRecord | null; includeGitData: boolean; }): Promise { - if (input.includeGitData && input.projectRecord?.kind === "git") { + if (input.includeGitData && input.workspace.kind !== "directory") { return this.describeWorkspaceRecordWithGitData(input.workspace, input.projectRecord); } return this.describeWorkspaceRecord(input.workspace, input.projectRecord); @@ -4130,65 +4124,6 @@ export class Session { }; } - private async recreateArchivedWorktree(workspace: PersistedWorkspaceRecord): Promise { - const branch = workspace.branch; - if (!branch) { - throw new WorktreeRequestError({ - code: "unknown", - message: `Workspace ${workspace.workspaceId} has no branch to restore`, - }); - } - const project = await this.projectRegistry.get(workspace.projectId); - if (!project) { - throw new WorktreeRequestError({ - code: "unknown", - message: `Project ${workspace.projectId} not found for workspace ${workspace.workspaceId}`, - }); - } - const projectRootExists = await this.filesystem - .isDirectory(project.rootPath) - .catch(() => false); - if (!projectRootExists) { - throw new WorktreeRequestError({ - code: "unknown", - message: `Project root is missing for ${workspace.projectId}: ${project.rootPath}`, - }); - } - - // Archiving through the default path (scope "workspace", worktreePath only) - // resolves repoRoot=null, so deletePaseoWorktree's `git worktree remove`/ - // `prune` is skipped and the admin registration survives — pinning the - // branch as "already checked out". Prune here frees any stale registration - // whose working dir is missing (a no-op for live worktrees) so the recreate - // below succeeds regardless of how the worktree was archived. - try { - await runGitCommand(["worktree", "prune"], { cwd: project.rootPath, timeout: 30_000 }); - } catch { - // not critical; git will prune lazily - } - - let result: WorktreeConfig; - try { - result = await createWorktree({ - cwd: project.rootPath, - worktreeSlug: basename(workspace.cwd), - source: { kind: "checkout-branch", branchName: branch }, - runSetup: false, - paseoHome: this.paseoHome, - worktreesRoot: this.worktreesRoot, - }); - } catch (error) { - throw toWorktreeRequestError(error); - } - - if (normalize(result.worktreePath) !== normalize(workspace.cwd)) { - throw new WorktreeRequestError({ - code: "unknown", - message: `Recreated worktree diverged from ${workspace.cwd}: ${result.worktreePath}`, - }); - } - } - private async restoreWorkspaceAndEmit(workspaceId: string): Promise { await this.workspaceRecovery.restore(workspaceId); const workspace = await this.workspaceRegistry.get(workspaceId); @@ -4237,9 +4172,8 @@ export class Session { ...(options?.resolveDefaultBranch ? { resolveDefaultBranch: options.resolveDefaultBranch } : {}), - projectRegistry: this.projectRegistry, - workspaceRegistry: this.workspaceRegistry, workspaceGitService: this.workspaceGitService, + workspaceProvisioning: this.workspaceProvisioning, }); void Promise.all([ this.gitMutation.notifyGitMutation(input.cwd, "create-worktree"), @@ -4261,6 +4195,9 @@ export class Session { workspaceId: workspace.workspaceId, cwd: workspace.cwd, kind: workspace.kind, + worktreeRoot: workspace.worktreeRoot, + isPaseoOwnedWorktree: workspace.isPaseoOwnedWorktree, + mainRepoRoot: workspace.mainRepoRoot, })); } @@ -4292,21 +4229,12 @@ export class Session { ); } - await this.teardownArchivedWorkspace({ - workspaceId: existingWorkspace.workspaceId, - cwd: existingWorkspace.cwd, - }); + await this.teardownArchivedWorkspace(existingWorkspace.workspaceId); } - // Git watch and subscription state is keyed by directory; the script runtime - // store is keyed by the opaque workspace id. Each cleanup uses its own key so an - // opaque id is never resolved as a filesystem path. - private async teardownArchivedWorkspace(input: { - workspaceId: string; - cwd: string; - }): Promise { - this.workspaceGitObserver.removeForCwd(input.cwd); - this.scriptRuntimeStore?.removeForWorkspace(input.workspaceId); + private async teardownArchivedWorkspace(workspaceId: string): Promise { + this.workspaceGitObserver.removeForWorkspaceId(workspaceId); + this.scriptRuntimeStore?.removeForWorkspace(workspaceId); } private async reconcileAndEmitWorkspaceUpdates(): Promise { @@ -4341,16 +4269,12 @@ export class Session { result.changesApplied.map(async (change) => { switch (change.kind) { case "workspace_archived": - await this.teardownArchivedWorkspace({ - workspaceId: change.workspaceId, - cwd: change.directory, - }); + await this.teardownArchivedWorkspace(change.workspaceId); changedWorkspaceIds.add(change.workspaceId); break; case "workspace_updated": changedWorkspaceIds.add(change.workspaceId); break; - case "project_archived": case "project_updated": changedProjectIds.add(change.projectId); break; @@ -4359,10 +4283,11 @@ export class Session { ); if (changedProjectIds.size > 0) { - for (const workspace of await this.workspaceRegistry.list()) { - if (changedProjectIds.has(workspace.projectId)) { - changedWorkspaceIds.add(workspace.workspaceId); - } + for (const workspaceId of workspaceIdsForProjects( + await this.workspaceRegistry.list(), + changedProjectIds, + )) { + changedWorkspaceIds.add(workspaceId); } } @@ -4770,6 +4695,7 @@ export class Session { { err: error, sourceKind: request.source.kind, requestId: request.requestId }, "Failed to create workspace", ); + const errorCode = error instanceof WorkspaceProvisioningError ? error.code : undefined; this.emit({ type: "workspace.create.response", payload: { @@ -4777,6 +4703,7 @@ export class Session { workspace: null, setupTerminalId: null, error: message, + errorCode, }, }); } @@ -4807,13 +4734,10 @@ export class Session { const explicitTitle = request.title?.trim() || null; const promptTitle = resolveFirstAgentPromptTitle(request.firstAgentContext); - const workspace = await createLocalCheckoutWorkspace( - { cwd, title: explicitTitle ?? promptTitle }, - { - projectRegistry: this.projectRegistry, - workspaceRegistry: this.workspaceRegistry, - workspaceGitService: this.workspaceGitService, - }, + const workspace = await this.workspaceProvisioning.createWorkspaceForDirectory( + cwd, + explicitTitle ?? promptTitle, + request.source.projectId, ); await this.syncWorkspaceGitObserverForWorkspace(workspace); const descriptor = await this.describeWorkspaceRecord(workspace); @@ -5287,10 +5211,10 @@ export class Session { // Named accessor: the workspace descriptor builder and the git-watch test both read a workspace's // scripts snapshot through here; the workspace-scripts module owns the payload assembly. private buildWorkspaceScriptPayloadSnapshot( - workspaceId: string, - workspaceDirectory: string, + workspace: PersistedWorkspaceRecord, + project: PersistedProjectRecord | null, ): WorkspaceDescriptorPayload["scripts"] { - return this.workspaceScripts.buildSnapshot(workspaceId, workspaceDirectory); + return this.workspaceScripts.buildSnapshot(workspace, project); } private handleStartWorkspaceScriptRequest(request: StartWorkspaceScriptRequest): Promise { @@ -5401,11 +5325,6 @@ export class Session { throw new Error(`Workspace not found: ${request.workspaceId}`); } - const gitSnapshot = await this.workspaceGitService - .getSnapshot(existing.cwd) - .catch(() => null); - const repoRoot = gitSnapshot?.git?.repoRoot ?? null; - await archiveByScope( { paseoHome: this.paseoHome, @@ -5428,8 +5347,6 @@ export class Session { }, { scope: { kind: "workspace", workspaceId: existing.workspaceId }, - repoRoot, - paseoWorktreesBaseRoot: this.worktreesRoot, requestId: request.requestId, }, ); diff --git a/packages/server/src/server/session.workspace-git-watch.test.ts b/packages/server/src/server/session.workspace-git-watch.test.ts index 8460584fd..64e4baa3c 100644 --- a/packages/server/src/server/session.workspace-git-watch.test.ts +++ b/packages/server/src/server/session.workspace-git-watch.test.ts @@ -472,7 +472,6 @@ describe("workspace git watch targets", () => { onBranchChanged: handleBranchChange, }, ); - const sessionAny = session as unknown as SessionInternals; seedGitWorkspace({ projects, workspaces, @@ -482,7 +481,7 @@ describe("workspace git watch targets", () => { name: "old-branch", }); - syncGitObserver(session, "/tmp/repo", "ws-10"); + await session.syncWorkspaceGitObserversForExternalWorkspaceIds(["ws-10"]); subscriptions[0]?.listener( createWorkspaceRuntimeSnapshot("/tmp/repo", { @@ -498,16 +497,6 @@ describe("workspace git watch targets", () => { scriptName: "app", }), ]); - expect(sessionAny.buildWorkspaceScriptPayloadSnapshot("ws-10", "/tmp/repo")).toEqual([ - expect.objectContaining({ - scriptName: "app", - hostname: "app--new-branch--paseo.localhost", - localProxyUrl: "http://app--new-branch--paseo.localhost:6767", - publicProxyUrl: null, - proxyUrl: "http://app--new-branch--paseo.localhost:6767", - }), - ]); - await session.cleanup(); }); diff --git a/packages/server/src/server/session.workspace-resolution-invariants.test.ts b/packages/server/src/server/session.workspace-resolution-invariants.test.ts index f730a74cd..42dcd593b 100644 --- a/packages/server/src/server/session.workspace-resolution-invariants.test.ts +++ b/packages/server/src/server/session.workspace-resolution-invariants.test.ts @@ -112,6 +112,22 @@ function createHarness(input: { existsOnDisk: async () => true, list: async () => Array.from(projects.values()), get: async (id: string) => projects.get(id) ?? null, + getOrCreateActiveByRoot: async (allocation) => { + const existing = Array.from(projects.values()).find( + (project) => !project.archivedAt && project.rootPath === allocation.rootPath, + ); + if (existing) return existing; + const project = createPersistedProjectRecord({ + projectId: `prj_${projects.size.toString().padStart(16, "0")}`, + rootPath: allocation.rootPath, + kind: allocation.kind, + displayName: allocation.displayName, + createdAt: allocation.timestamp, + updatedAt: allocation.timestamp, + }); + projects.set(project.projectId, project); + return project; + }, upsert: async (record: PersistedProjectRecord) => { projects.set(record.projectId, record); }, @@ -310,10 +326,9 @@ test("S3: re-open active workspace by exact path returns the same record", async }); // ───────────────────────────────────────────────────────────────────────────── -// S4. Open a subdir of an active git workspace: canonicalizes UP to the repo -// root, returns the existing workspace. (Per "always go to the nearest git".) +// S4. Every selected path is an exact lexical root, even inside a Git checkout. // ───────────────────────────────────────────────────────────────────────────── -test("S4: open subdir of active git workspace returns the repo-root workspace", async () => { +test("S4: open subdir of active git workspace creates an independent exact-root workspace", async () => { const h = createHarness({ workspaces: [gitWorkspace(FOO)], projects: [gitProject(FOO)], @@ -321,8 +336,9 @@ test("S4: open subdir of active git workspace returns the repo-root workspace", }); await openProject(h.session, FOO_SUB); const resp = getOpenResponse(h.emitted, "req-1"); - expect(resp?.workspace?.id).toBe(workspaceByCwd(h.workspaces, FOO)?.workspaceId); - expect(h.workspaces.size).toBe(1); + expect(resp?.workspace?.workspaceDirectory).toBe(FOO_SUB); + expect(resp?.workspace?.projectId).not.toBe(workspaceByCwd(h.workspaces, FOO)?.projectId); + expect(h.workspaces.size).toBe(2); }); // ───────────────────────────────────────────────────────────────────────────── @@ -343,10 +359,10 @@ test("S5: open subdir of active non-git directory creates a SEPARATE workspace", }); // ───────────────────────────────────────────────────────────────────────────── -// S6. Open the EXACT path of an archived git workspace: this IS explicit user -// intent to re-open what they archived. Unarchive is correct here. +// S6. Explicit project opening allocates a fresh identity when only archived +// records exist. Agent restore is the separate path that restores ownership. // ───────────────────────────────────────────────────────────────────────────── -test("S6: re-opening an archived git workspace by exact path UNARCHIVES it", async () => { +test("S6: re-opening an archived git workspace by exact path creates a fresh project and workspace", async () => { const archivedAt = "2026-04-22T13:08:05.400Z"; const h = createHarness({ workspaces: [gitWorkspace(TOOLBOX, archivedAt)], @@ -354,8 +370,12 @@ test("S6: re-opening an archived git workspace by exact path UNARCHIVES it", asy gitRoots: [TOOLBOX], }); await openProject(h.session, TOOLBOX); - expect(workspaceByCwd(h.workspaces, TOOLBOX)?.archivedAt).toBeNull(); - expect(h.projects.get(TOOLBOX)?.archivedAt).toBeNull(); + const fresh = Array.from(h.workspaces.values()).find( + (workspace) => workspace.cwd === TOOLBOX && !workspace.archivedAt, + ); + expect(fresh?.workspaceId).not.toBe("ws-toolbox"); + expect(fresh?.projectId).toMatch(/^prj_[0-9a-f]{16}$/); + expect(h.projects.get(TOOLBOX)?.archivedAt).toBe(archivedAt); }); // ───────────────────────────────────────────────────────────────────────────── @@ -433,11 +453,10 @@ test("S10: opening a git repo nested inside an archived non-git directory create }); // ───────────────────────────────────────────────────────────────────────────── -// S11. Archive then re-add round-trip (project-level): opening the exact path -// of an archived project unarchives both the project and its workspace, -// reusing the same path-derived ids. +// S11. Archive then re-add produces a fresh opaque identity; it never reuses +// archived compatibility IDs. // ───────────────────────────────────────────────────────────────────────────── -test("S11: re-opening an archived project by exact path unarchives project + workspace and reuses ids", async () => { +test("S11: re-opening an archived project by exact path keeps archived records and allocates fresh ids", async () => { const archivedAt = "2026-04-22T13:08:05.400Z"; const h = createHarness({ workspaces: [gitWorkspace(TOOLBOX, archivedAt)], @@ -447,12 +466,11 @@ test("S11: re-opening an archived project by exact path unarchives project + wor await openProject(h.session, TOOLBOX); const resp = getOpenResponse(h.emitted, "req-1"); expect(resp?.error).toBeNull(); - expect(resp?.workspace?.id).toBe(workspaceByCwd(h.workspaces, TOOLBOX)?.workspaceId); - expect(resp?.workspace?.projectId).toBe(TOOLBOX); - expect(h.workspaces.size).toBe(1); - expect(h.projects.size).toBe(1); - expect(workspaceByCwd(h.workspaces, TOOLBOX)?.archivedAt).toBeNull(); - expect(h.projects.get(TOOLBOX)?.archivedAt).toBeNull(); + expect(resp?.workspace?.id).not.toBe("ws-toolbox"); + expect(resp?.workspace?.projectId).toMatch(/^prj_[0-9a-f]{16}$/); + expect(h.workspaces.size).toBe(2); + expect(h.projects.size).toBe(2); + expect(h.projects.get(TOOLBOX)?.archivedAt).toBe(archivedAt); }); // ───────────────────────────────────────────────────────────────────────────── @@ -482,7 +500,7 @@ test("S12: resolveWorkspaceIdForPath does not return archived ancestor via prefi // being explicit. To get git features back the user unarchives the parent // (S6/S11). // ───────────────────────────────────────────────────────────────────────────── -test("S13: subfolder of an archived git repo opens as a directory workspace", async () => { +test("S13: subfolder of an archived git repo opens as its own git-backed workspace", async () => { const archivedAt = "2026-04-22T13:08:05.400Z"; const h = createHarness({ workspaces: [gitWorkspace(TOOLBOX, archivedAt)], @@ -492,5 +510,5 @@ test("S13: subfolder of an archived git repo opens as a directory workspace", as await openProject(h.session, TOOLBOX_FLOMO); const resp = getOpenResponse(h.emitted, "req-1"); expect(resp?.error).toBeNull(); - expect(resp?.workspace?.workspaceKind).toBe("directory"); + expect(resp?.workspace?.workspaceKind).toBe("local_checkout"); }); diff --git a/packages/server/src/server/session.workspaces.test.ts b/packages/server/src/server/session.workspaces.test.ts index 99ae1169f..05ba4d591 100644 --- a/packages/server/src/server/session.workspaces.test.ts +++ b/packages/server/src/server/session.workspaces.test.ts @@ -33,12 +33,12 @@ import type { AgentStreamEvent, } from "./agent/agent-sdk-types.js"; import { createWorktree } from "../utils/worktree.js"; +import { createRealpathAwarePathMatcher } from "../utils/path.js"; import { readPaseoWorktreeMetadata, writePaseoWorktreeFirstAgentBranchAutoNameMetadata, writePaseoWorktreeMetadata, } from "../utils/worktree-metadata.js"; -import { WorktreeRequestError } from "./worktree-errors.js"; import type { WorkspaceGitRuntimeSnapshot } from "./workspace-git-service.js"; import type { GeneratedWorkspaceName } from "./worktree-branch-name-generator.js"; import { WorkspaceAutoName } from "./workspace-auto-name.js"; @@ -128,7 +128,6 @@ interface SessionTestAccess { agentUpdates: AgentUpdatesService; workspaceUpdatesSubscription: unknown; interruptAgentIfRunning(agentId: string): unknown; - recreateArchivedWorktree(workspace: PersistedWorkspaceRecord): Promise; reconcileActiveWorkspaceRecords(...args: unknown[]): Promise>; reconcileWorkspaceRecord(workspaceId: string): Promise<{ changed: boolean; @@ -154,6 +153,10 @@ interface SessionTestAccess { clearWorkspaceArchiving(workspaceIds: Iterable): void; emitWorkspaceUpdateForCwd(...args: unknown[]): Promise; emitWorkspaceUpdatesForWorkspaceIds(...args: unknown[]): Promise; + emitWorkspaceUpdatesForExternalWorkspaceIds( + workspaceIds: Iterable, + options?: { skipReconcile?: boolean }, + ): Promise; emit(message: unknown): void; onMessage(message: unknown): void; paseoHome: string; @@ -652,6 +655,15 @@ function createSessionForWorkspaceTests( existsOnDisk: async () => true, list: async () => [], get: async () => null, + getOrCreateActiveByRoot: async (input) => + createPersistedProjectRecord({ + projectId: "prj_0000000000000000", + rootPath: input.rootPath, + kind: input.kind, + displayName: input.displayName, + createdAt: input.timestamp, + updatedAt: input.timestamp, + }), upsert: async () => {}, archive: async () => {}, remove: async () => {}, @@ -844,7 +856,6 @@ test("create_agent_request keeps requested child cwd when grouped under an exist terminalManager: null, }), ); - await session.handleMessage({ type: "create_agent_request", requestId: "req-create-child", @@ -857,7 +868,7 @@ test("create_agent_request keeps requested child cwd when grouped under an exist await expect( session.buildProjectPlacementForWorkspaceId(createdAgent!.workspaceId!), ).resolves.toMatchObject({ - projectKey: parent, + projectKey: expect.stringMatching(/^prj_[0-9a-f]{16}$/), checkout: { cwd: child }, }); expect(findByType(emitted, "status")?.payload).toMatchObject({ @@ -869,6 +880,163 @@ test("create_agent_request keeps requested child cwd when grouped under an exist } }); +test("create_agent_request launches from an exact subdirectory in a created worktree", async () => { + const workdir = mkdtempSync(path.join(tmpdir(), "paseo-create-agent-worktree-cwd-")); + try { + const parent = path.join(workdir, "parent"); + const child = path.join(parent, "packages", "app"); + mkdirSync(child, { recursive: true }); + execFileSync("git", ["init", "-b", "main"], { cwd: parent, stdio: "pipe" }); + execFileSync("git", ["config", "user.email", "test@getpaseo.local"], { + cwd: parent, + stdio: "pipe", + }); + execFileSync("git", ["config", "user.name", "Paseo Test"], { cwd: parent, stdio: "pipe" }); + writeFileSync(path.join(child, "README.md"), "app\n"); + execFileSync("git", ["add", "."], { cwd: parent, stdio: "pipe" }); + execFileSync("git", ["commit", "-m", "initial"], { cwd: parent, stdio: "pipe" }); + + const logger = { + child: () => logger, + trace: vi.fn(), + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + }; + const agentStorage = new AgentStorage(path.join(workdir, "agents"), asSessionLogger(logger)); + const agentManager = new AgentManager({ + clients: { codex: new CreateAgentTestClient() }, + registry: agentStorage, + logger: asSessionLogger(logger), + idFactory: () => "00000000-0000-4000-8000-000000000552", + }); + const projectRegistry = new FileBackedProjectRegistry( + path.join(workdir, "projects.json"), + asSessionLogger(logger), + ); + const workspaceRegistry = new FileBackedWorkspaceRegistry( + path.join(workdir, "workspaces.json"), + asSessionLogger(logger), + ); + const workspaceGitService = createNoopWorkspaceGitService({ + getCheckout: async (cwd: string) => ({ + cwd, + isGit: true, + currentBranch: "main", + remoteUrl: null, + worktreeRoot: parent, + isPaseoOwnedWorktree: false, + mainRepoRoot: null, + }), + resolveRepoRoot: async () => parent, + resolveDefaultBranch: async () => "main", + }); + await projectRegistry.upsert( + createPersistedProjectRecord({ + projectId: "proj-parent", + rootPath: parent, + kind: "git", + displayName: "parent", + createdAt: "2026-05-07T00:00:00.000Z", + updatedAt: "2026-05-07T00:00:00.000Z", + }), + ); + await workspaceRegistry.upsert( + createPersistedWorkspaceRecord({ + workspaceId: "ws-parent", + projectId: "proj-parent", + cwd: parent, + kind: "local_checkout", + displayName: "parent", + createdAt: "2026-05-07T00:00:00.000Z", + updatedAt: "2026-05-07T00:00:00.000Z", + }), + ); + + const emitted: SessionOutboundMessage[] = []; + const session = new Session({ + clientId: "test-client", + appVersion: null, + onMessage: (message) => emitted.push(message), + logger: asSessionLogger(logger), + downloadTokenStore: asDownloadTokenStore(), + pushTokenStore: asPushTokenStore(), + paseoHome: path.join(workdir, "paseo-home"), + agentManager, + agentStorage, + projectRegistry, + workspaceRegistry, + chatService: asChatService(), + scheduleService: asScheduleService(), + loopService: asLoopService(), + checkoutDiffManager: asCheckoutDiffManager({ + subscribe: async () => ({ + initial: { cwd: child, files: [], error: null }, + unsubscribe: () => {}, + }), + scheduleRefreshForCwd: () => {}, + onWorkspaceStateMayHaveChanged: () => {}, + getMetrics: () => ({ + checkoutDiffTargetCount: 0, + checkoutDiffSubscriptionCount: 0, + checkoutDiffWatcherCount: 0, + checkoutDiffFallbackRefreshTargetCount: 0, + }), + dispose: () => {}, + }), + workspaceGitService, + workspaceAutoName: new WorkspaceAutoName({ + agentManager, + workspaceRegistry, + workspaceGitService, + providerSnapshotManager: createProviderSnapshotManagerStub().manager, + readDaemonConfig: () => ({ metadataGeneration: { providers: [] } }), + gitMutation: { notifyGitMutation: async () => {} }, + emitWorkspaceUpdateForCwd: async () => {}, + emitWorkspaceUpdateForWorkspaceId: async () => {}, + logger: asSessionLogger(logger), + }), + daemonConfigStore: asDaemonConfigStore({ + get: () => ({ mcp: { injectIntoAgents: false }, providers: {} }), + onChange: () => () => {}, + }), + mcpBaseUrl: null, + stt: null, + tts: null, + providerSnapshotManager: createProviderSnapshotManagerStub().manager, + terminalManager: null, + }); + + await session.handleMessage({ + type: "create_agent_request", + requestId: "req-create-worktree-child", + config: { provider: "codex", cwd: child }, + attachments: [], + worktree: { mode: "branch-off", newBranch: "feature/created-worktree" }, + }); + + const [createdAgent] = agentManager.listAgents(); + const createdWorktreeRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], { + cwd: createdAgent!.cwd, + stdio: "pipe", + }) + .toString() + .trim(); + expect( + createRealpathAwarePathMatcher(path.join(createdWorktreeRoot, "packages", "app"))( + createdAgent?.cwd ?? "", + ), + ).toBe(true); + expect(findByType(emitted, "status")?.payload).toMatchObject({ + status: "agent_created", + agent: { cwd: createdAgent?.cwd }, + }); + } finally { + rmSync(workdir, { recursive: true, force: true }); + } +}); + test("create_agent_request does not title an existing workspace from the agent prompt", async () => { vi.useFakeTimers(); const workdir = mkdtempSync(path.join(tmpdir(), "paseo-create-agent-existing-title-")); @@ -2106,6 +2274,93 @@ test("non-git workspace uses deterministic directory name and no unknown branch expect(result.entries[0]?.name).not.toBe("Unknown branch"); }); +test("workspace placements preserve checkout facts independently from the project", async () => { + const session = createSessionForWorkspaceTests(); + const manualWorktree = createPersistedWorkspaceRecord({ + workspaceId: "ws-manual-worktree", + projectId: "proj-manual-worktree", + cwd: "/tmp/manual-worktree", + kind: "worktree", + displayName: "manual", + isPaseoOwnedWorktree: false, + mainRepoRoot: "/tmp/main-repo", + createdAt: "2026-03-01T12:00:00.000Z", + updatedAt: "2026-03-01T12:00:00.000Z", + }); + const explicitDirectory = createPersistedWorkspaceRecord({ + workspaceId: "ws-explicit-directory", + projectId: "proj-manual-worktree", + cwd: "/tmp/plain-directory", + kind: "directory", + displayName: "plain", + createdAt: "2026-03-01T12:00:00.000Z", + updatedAt: "2026-03-01T12:00:00.000Z", + }); + const paseoSubdirectory = createPersistedWorkspaceRecord({ + workspaceId: "ws-paseo-subdirectory", + projectId: "proj-manual-worktree", + cwd: "/tmp/paseo-worktree/packages/app", + kind: "worktree", + displayName: "app", + isPaseoOwnedWorktree: true, + mainRepoRoot: "/tmp/main-repo", + createdAt: "2026-03-01T12:00:00.000Z", + updatedAt: "2026-03-01T12:00:00.000Z", + }); + const project = createPersistedProjectRecord({ + projectId: "proj-manual-worktree", + rootPath: "/tmp/main-repo", + kind: "git", + displayName: "main", + createdAt: "2026-03-01T12:00:00.000Z", + updatedAt: "2026-03-01T12:00:00.000Z", + }); + session.workspaceRegistry.get = async (workspaceId: string) => + [manualWorktree, explicitDirectory, paseoSubdirectory].find( + (workspace) => workspace.workspaceId === workspaceId, + ) ?? null; + session.projectRegistry.get = async () => project; + session.workspaceGitService.peekSnapshot = (cwd: string) => + cwd === paseoSubdirectory.cwd + ? createWorkspaceRuntimeSnapshot(cwd, { + git: { repoRoot: "/tmp/paseo-worktree" }, + }) + : null; + + await expect( + session.buildProjectPlacementForWorkspaceId(manualWorktree.workspaceId), + ).resolves.toEqual( + expect.objectContaining({ + checkout: expect.objectContaining({ + isGit: true, + isPaseoOwnedWorktree: false, + mainRepoRoot: "/tmp/main-repo", + }), + }), + ); + await expect( + session.buildProjectPlacementForWorkspaceId(explicitDirectory.workspaceId), + ).resolves.toEqual( + expect.objectContaining({ + checkout: expect.objectContaining({ + isGit: false, + isPaseoOwnedWorktree: false, + mainRepoRoot: null, + }), + }), + ); + await expect( + session.buildProjectPlacementForWorkspaceId(paseoSubdirectory.workspaceId), + ).resolves.toEqual( + expect.objectContaining({ + checkout: expect.objectContaining({ + cwd: paseoSubdirectory.cwd, + worktreeRoot: "/tmp/paseo-worktree", + }), + }), + ); +}); + test("active-scoped fetch_agents includes only unarchived agents in active workspaces", async () => { const session = createSessionForWorkspaceTests(); const archivedAt = "2026-03-02T12:00:00.000Z"; @@ -3345,7 +3600,7 @@ test("project.remove.request removes an already-empty project", async () => { ]); }); -test("create paseo worktree request returns a registered workspace descriptor", async () => { +test("create paseo worktree response preserves an explicit non-Git project", async () => { const emitted: SessionOutboundMessage[] = []; const createdAt = "2026-05-12T12:00:00.000Z"; vi.setSystemTime(new Date(createdAt)); @@ -3408,7 +3663,15 @@ test("create paseo worktree request returns a registered workspace descriptor", ); const workspaces = new Map(); - const projects = new Map(); + const explicitProject = createPersistedProjectRecord({ + projectId: "prj_explicit_non_git", + rootPath: path.join(tempDir, "selected-project"), + kind: "non_git", + displayName: "Selected project", + createdAt, + updatedAt: createdAt, + }); + const projects = new Map([[explicitProject.projectId, explicitProject]]); session.paseoHome = paseoHome; session.workspaceRegistry.get = async (lookupWorkspaceId: string) => workspaces.get(lookupWorkspaceId) ?? null; @@ -3420,6 +3683,22 @@ test("create paseo worktree request returns a registered workspace descriptor", }; session.projectRegistry.get = async (projectId: string) => projects.get(projectId) ?? null; session.projectRegistry.list = async () => Array.from(projects.values()); + session.projectRegistry.getOrCreateActiveByRoot = async (input) => { + const existing = Array.from(projects.values()).find( + (project) => !project.archivedAt && project.rootPath === input.rootPath, + ); + if (existing) return existing; + const project = createPersistedProjectRecord({ + projectId: `prj_${projects.size.toString().padStart(16, "0")}`, + rootPath: input.rootPath, + kind: input.kind, + displayName: input.displayName, + createdAt: input.timestamp, + updatedAt: input.timestamp, + }); + projects.set(project.projectId, project); + return project; + }; session.projectRegistry.upsert = async ( record: ReturnType, ) => { @@ -3432,6 +3711,7 @@ test("create paseo worktree request returns a registered workspace descriptor", await session.handleCreatePaseoWorktreeRequest({ type: "create_paseo_worktree_request", cwd: repoDir, + projectId: explicitProject.projectId, worktreeSlug: "worktree-123", requestId: "req-worktree", }); @@ -3444,8 +3724,10 @@ test("create paseo worktree request returns a registered workspace descriptor", expect(response?.payload.error).toBeNull(); expect(response?.payload.workspace).toMatchObject({ - projectDisplayName: "repo", - projectKind: "git", + projectId: explicitProject.projectId, + projectDisplayName: explicitProject.displayName, + projectRootPath: explicitProject.rootPath, + projectKind: "non_git", workspaceKind: "worktree", name: "worktree-123", status: "done", @@ -3454,7 +3736,7 @@ test("create paseo worktree request returns a registered workspace descriptor", expect(response?.payload.workspace?.id).toMatch(/^wks_[0-9a-f]{16}$/); expect(response?.payload.workspace?.workspaceDirectory).toContain(path.join("worktree-123")); expect(workspaces.has(response?.payload.workspace?.id ?? "")).toBe(true); - expect(projects.has(response?.payload.workspace?.projectId ?? "")).toBe(true); + expect(projects.get(explicitProject.projectId)).toEqual(explicitProject); }); test("workspace update fanout for multiple cwd values is deduplicated", async () => { @@ -3547,6 +3829,18 @@ test("open_project_request registers a workspace before any agent exists", async if (isSessionOutboundMessage(message)) emitted.push(message); }; session.projectRegistry.get = async (projectId: string) => projects.get(projectId) ?? null; + session.projectRegistry.getOrCreateActiveByRoot = async (allocation) => { + const project = createPersistedProjectRecord({ + projectId: "prj_githubruntime", + rootPath: allocation.rootPath, + kind: allocation.kind, + displayName: allocation.displayName, + createdAt: allocation.timestamp, + updatedAt: allocation.timestamp, + }); + projects.set(project.projectId, project); + return project; + }; session.projectRegistry.upsert = async ( record: ReturnType, ) => { @@ -3860,6 +4154,18 @@ test("open_project_request emits a workspace_update with githubRuntime once the if (isSessionOutboundMessage(message)) emitted.push(message); }; session.projectRegistry.get = async (projectId: string) => projects.get(projectId) ?? null; + session.projectRegistry.getOrCreateActiveByRoot = async (allocation) => { + const project = createPersistedProjectRecord({ + projectId: "prj_githubruntime", + rootPath: allocation.rootPath, + kind: allocation.kind, + displayName: allocation.displayName, + createdAt: allocation.timestamp, + updatedAt: allocation.timestamp, + }); + projects.set(project.projectId, project); + return project; + }; session.projectRegistry.upsert = async ( record: ReturnType, ) => { @@ -4085,7 +4391,6 @@ test("open_project_request reclassifies an archived directory workspace when git "orchestrate", "desktop-daemon-settings", ); - const remoteProjectId = "remote:github.com/getpaseo/paseo"; const archivedAt = "2026-04-24T09:48:36.168Z"; const workspaceId = "ws-desktop-daemon-settings"; @@ -4163,12 +4468,9 @@ test("open_project_request reclassifies an archived directory workspace when git const response = findByType(emitted, "open_project_response"); expect(response?.payload.error).toBeNull(); - expect(response?.payload.workspace?.projectId).toBe(remoteProjectId); - expect(response?.payload.workspace?.workspaceKind).toBe("worktree"); - expect(projects.get(remoteProjectId)?.kind).toBe("git"); - expect(workspaces.get(workspaceId)?.projectId).toBe(remoteProjectId); - expect(workspaces.get(workspaceId)?.kind).toBe("worktree"); - expect(workspaces.get(workspaceId)?.displayName).toBe("feature/desktop-daemon-settings"); + expect(response?.payload.workspace?.projectId).toMatch(/^prj_[0-9a-f]{16}$/); + expect(projects.get(cwd)?.archivedAt).toBe(archivedAt); + expect(workspaces.get(workspaceId)?.archivedAt).toBe(archivedAt); }); test("open_project_request reclassifies an active directory workspace when git metadata becomes available", async () => { @@ -4282,14 +4584,11 @@ test("open_project_request reclassifies an active directory workspace when git m const response = findByType(emitted, "open_project_response"); expect(response?.payload.error).toBeNull(); - expect(response?.payload.workspace?.projectId).toBe(repoRoot); - expect(response?.payload.workspace?.workspaceKind).toBe("worktree"); - expect(workspaces.get(workspaceId)?.projectId).toBe(repoRoot); - expect(workspaces.get(workspaceId)?.kind).toBe("worktree"); - expect(workspaces.get(workspaceId)?.displayName).toBe("feature/desktop-daemon-settings"); + expect(response?.payload.workspace?.projectId).toBe(cwd); + expect(workspaces.get(workspaceId)?.projectId).toBe(cwd); }); -test("open_project_request groups a plain git worktree under an existing repo project", async () => { +test("open_project_request gives a plain git worktree its own exact-root project", async () => { const emitted: SessionOutboundMessage[] = []; const session = createSessionForWorkspaceTests(); const projects = new Map>(); @@ -4375,16 +4674,14 @@ test("open_project_request groups a plain git worktree under an existing repo pr const response = findByType(emitted, "open_project_response"); expect(response?.payload.error).toBeNull(); - expect(response?.payload.workspace?.projectId).toBe(repoRoot); - expect(response?.payload.workspace?.workspaceKind).toBe("worktree"); + expect(response?.payload.workspace?.projectId).toMatch(/^prj_[0-9a-f]{16}$/); const worktreeWorkspace = Array.from(workspaces.values()).find( (workspace) => workspace.cwd === cwd, ); - expect(worktreeWorkspace?.projectId).toBe(repoRoot); - expect(worktreeWorkspace?.kind).toBe("worktree"); + expect(worktreeWorkspace?.projectId).toMatch(/^prj_[0-9a-f]{16}$/); }); -test("open_project_request unarchives an existing archived workspace and project", async () => { +test("open_project_request keeps archived records and allocates a fresh workspace", async () => { const emitted: SessionOutboundMessage[] = []; const session = createSessionForWorkspaceTests(); const projects = new Map>(); @@ -4443,14 +4740,15 @@ test("open_project_request unarchives an existing archived workspace and project requestId: "req-open-unarchive", }); - expect(workspaces.get(workspaceId)?.archivedAt).toBeNull(); - expect(projects.get(cwd)?.archivedAt).toBeNull(); + expect(workspaces.get(workspaceId)?.archivedAt).not.toBeNull(); + expect(projects.get(cwd)?.archivedAt).not.toBeNull(); const response = findByType(emitted, "open_project_response"); expect(response?.payload.error).toBeNull(); - expect(response?.payload.workspace?.id).toBe(workspaceId); + expect(response?.payload.workspace?.id).not.toBe(workspaceId); + expect(response?.payload.workspace?.projectId).toMatch(/^prj_[0-9a-f]{16}$/); }); -test("open_project_request recreates a missing project record when unarchiving its workspace", async () => { +test("open_project_request does not repurpose an orphaned archived workspace", async () => { const emitted: SessionOutboundMessage[] = []; const session = createSessionForWorkspaceTests(); const projects = new Map>(); @@ -4497,18 +4795,12 @@ test("open_project_request recreates a missing project record when unarchiving i requestId: "req-open-removed-project", }); - expect(projects.get(cwd)).toEqual( - expect.objectContaining({ - projectId: cwd, - displayName: "repo", - archivedAt: null, - }), - ); - expect(workspaces.get(workspaceId)?.archivedAt).toBeNull(); + expect(projects.get(cwd)).toBeUndefined(); + expect(workspaces.get(workspaceId)?.archivedAt).not.toBeNull(); const response = findByType(emitted, "open_project_response"); expect(response?.payload.error).toBeNull(); - expect(response?.payload.workspace?.id).toBe(workspaceId); - expect(response?.payload.workspace?.projectDisplayName).toBe("repo"); + expect(response?.payload.workspace?.id).not.toBe(workspaceId); + expect(response?.payload.workspace?.projectId).toMatch(/^prj_[0-9a-f]{16}$/); }); test("workspace recovery stays accepted when git observer warming fails", async () => { @@ -5099,68 +5391,6 @@ test("legacy refresh_agent_request restores a real deleted worktree", async () = rmSync(tempDir, { recursive: true, force: true }); }); -test("recreateArchivedWorktree throws a typed WorktreeRequestError when the project root is missing", async () => { - const { tempDir, repoDir } = createRecreateWorktreeRepo(); - const branch = "feature/keep"; - execFileSync("git", ["branch", branch], { cwd: repoDir, stdio: "pipe" }); - - const worktreesRoot = path.join(tempDir, "worktrees"); - const paseoHome = path.join(tempDir, "paseo-home"); - const created = await createWorktree({ - cwd: repoDir, - worktreeSlug: "keep", - source: { kind: "checkout-branch", branchName: branch }, - runSetup: false, - paseoHome, - worktreesRoot, - }); - const worktreePath = realpathSync(created.worktreePath); - - const session = createSessionForWorkspaceTests({ paseoHome, worktreesRoot }); - const projects = new Map>(); - const workspaces = new Map>(); - const workspaceId = "ws-missing-root"; - const projectId = repoDir; - const missingRoot = path.join(tempDir, "does-not-exist"); - projects.set( - projectId, - createPersistedProjectRecord({ - projectId, - rootPath: missingRoot, - kind: "git", - displayName: "worktree-project", - createdAt: "2026-03-01T12:00:00.000Z", - updatedAt: "2026-03-10T00:00:00.000Z", - archivedAt: "2026-03-10T00:00:00.000Z", - }), - ); - const workspaceRecord = createPersistedWorkspaceRecord({ - workspaceId, - projectId, - cwd: worktreePath, - kind: "worktree", - branch, - displayName: branch, - createdAt: "2026-03-01T12:00:00.000Z", - updatedAt: "2026-03-10T00:00:00.000Z", - archivedAt: "2026-03-10T00:00:00.000Z", - }); - workspaces.set(workspaceId, workspaceRecord); - - session.projectRegistry.get = async (id: string) => projects.get(id) ?? null; - session.workspaceRegistry.get = async (id: string) => workspaces.get(id) ?? null; - session.filesystem.isDirectory = async (target: string) => - existsSync(target) && statSync(target).isDirectory(); - - await expect(session.recreateArchivedWorktree(workspaceRecord)).rejects.toBeInstanceOf( - WorktreeRequestError, - ); - // Guard fires before createWorktree, so archivedAt is untouched. - expect(workspaces.get(workspaceId)?.archivedAt).toBe("2026-03-10T00:00:00.000Z"); - - rmSync(tempDir, { recursive: true, force: true }); -}); - test.skip("open_project_request collapses a git subdirectory onto the repo root workspace", async () => { const emitted: SessionOutboundMessage[] = []; const session = createSessionForWorkspaceTests(); @@ -5747,6 +5977,40 @@ test("listWorkspaceDescriptorsSnapshot keeps git workspaces on the baseline desc expect(descriptors).toEqual([baselineDescriptor]); }); +test("lists Git runtime for a checkout explicitly owned by a non-Git project", async () => { + const session = createSessionForWorkspaceTests(); + const project = createPersistedProjectRecord({ + projectId: "proj-explicit-directory", + rootPath: "/tmp/explicit-directory", + kind: "non_git", + displayName: "directory project", + createdAt: "2026-03-01T12:00:00.000Z", + updatedAt: "2026-03-01T12:00:00.000Z", + }); + const workspace = createPersistedWorkspaceRecord({ + workspaceId: "ws-explicit-checkout", + projectId: project.projectId, + cwd: REPO_CWD, + kind: "local_checkout", + displayName: "main", + createdAt: "2026-03-01T12:00:00.000Z", + updatedAt: "2026-03-01T12:00:00.000Z", + }); + session.listAgentPayloads = async () => []; + session.projectRegistry.list = async () => [project]; + session.workspaceRegistry.list = async () => [workspace]; + session.workspaceGitService.peekSnapshot = () => createWorkspaceRuntimeSnapshot(REPO_CWD); + + const descriptors = Array.from( + (await session.buildWorkspaceDescriptorMap({ includeGitData: true })).values(), + ) as Array<{ gitRuntime?: { currentBranch: string | null }; githubRuntime?: unknown }>; + + expect(descriptors[0]).toMatchObject({ + gitRuntime: { currentBranch: "main" }, + githubRuntime: expect.any(Object), + }); +}); + test("buildWorkspaceDescriptorMap computes statusEnteredAt from runtime agent fields", async () => { const setupSession = () => { const session = createSessionForWorkspaceTests(); @@ -6188,6 +6452,90 @@ test("emitWorkspaceUpdatesForWorkspaceIds includes archiving state and dedupes u ]); }); +test("external workspace updates emit one deduplicated batch without reconciling", async () => { + const emitted: SessionOutboundMessage[] = []; + const session = createSessionForWorkspaceTests(); + const project = createPersistedProjectRecord({ + projectId: "proj-observer-batch", + rootPath: "/tmp/observer-batch", + kind: "non_git", + displayName: "observer-batch", + createdAt: "2026-07-15T00:00:00.000Z", + updatedAt: "2026-07-15T00:00:00.000Z", + }); + const main = createPersistedWorkspaceRecord({ + workspaceId: "ws-observer-main", + projectId: project.projectId, + cwd: "/tmp/observer-batch/main", + kind: "directory", + displayName: "main", + createdAt: "2026-07-15T00:00:00.000Z", + updatedAt: "2026-07-15T00:00:00.000Z", + }); + const feature = createPersistedWorkspaceRecord({ + workspaceId: "ws-observer-feature", + projectId: project.projectId, + cwd: "/tmp/observer-batch/feature", + kind: "directory", + displayName: "feature", + createdAt: "2026-07-15T00:00:00.000Z", + updatedAt: "2026-07-15T00:00:00.000Z", + }); + const snapshotReads = { projects: 0, workspaces: 0 }; + session.projectRegistry.list = async () => { + snapshotReads.projects += 1; + return [project]; + }; + session.workspaceRegistry.list = async () => { + snapshotReads.workspaces += 1; + return [main, feature]; + }; + session.listAgentPayloads = async () => []; + session.workspaceUpdatesSubscription = { + subscriptionId: "sub-observer-batch", + filter: undefined, + isBootstrapping: false, + pendingUpdatesByWorkspaceId: new Map(), + lastEmittedByWorkspaceId: new Map(), + }; + session.onMessage = (message) => { + if (isSessionOutboundMessage(message)) emitted.push(message); + }; + + await session.emitWorkspaceUpdatesForExternalWorkspaceIds( + [main.workspaceId, feature.workspaceId, main.workspaceId], + { skipReconcile: true }, + ); + + expect(filterByType(emitted, "workspace_update")).toEqual([ + { + type: "workspace_update", + payload: { + kind: "upsert", + workspace: expect.objectContaining({ + id: main.workspaceId, + projectId: project.projectId, + workspaceDirectory: main.cwd, + name: main.displayName, + }), + }, + }, + { + type: "workspace_update", + payload: { + kind: "upsert", + workspace: expect.objectContaining({ + id: feature.workspaceId, + projectId: project.projectId, + workspaceDirectory: feature.cwd, + name: feature.displayName, + }), + }, + }, + ]); + expect(snapshotReads).toEqual({ projects: 1, workspaces: 1 }); +}); + test("fetch_workspaces_response reads runtime fields from passive workspace git service snapshots", async () => { const emitted: SessionOutboundMessage[] = []; const runtimeSnapshot = createWorkspaceRuntimeSnapshot(REPO_CWD, { @@ -7497,6 +7845,18 @@ test("workspace.create worktree source checks out a GitHub PR from githubPrNumbe existsOnDisk: async () => true, list: async () => Array.from(projects.values()), get: async (projectId: string) => projects.get(projectId) ?? null, + getOrCreateActiveByRoot: async (allocation) => { + const project = createPersistedProjectRecord({ + projectId: "prj_worktreefixture", + rootPath: allocation.rootPath, + kind: allocation.kind, + displayName: allocation.displayName, + createdAt: allocation.timestamp, + updatedAt: allocation.timestamp, + }); + projects.set(project.projectId, project); + return project; + }, upsert: async (record) => { projects.set(record.projectId, record); }, @@ -7691,7 +8051,7 @@ test("workspace auto-name replaces the unchanged prompt title", async () => { } }); -test("workspace auto-name applies title once when branch auto-name is rejected", async () => { +test("workspace auto-name uses the backing root for a nested worktree", async () => { vi.useFakeTimers(); const tempDir = realpathSync(mkdtempSync(path.join(tmpdir(), "workspace-auto-name-rejected-"))); const repoDir = path.join(tempDir, "repo"); @@ -7711,15 +8071,19 @@ test("workspace auto-name applies title once when branch auto-name is rejected", writePaseoWorktreeFirstAgentBranchAutoNameMetadata(repoDir, { placeholderBranchName: "placeholder-branch", }); + const workspaceCwd = path.join(repoDir, "packages", "app"); + mkdirSync(workspaceCwd, { recursive: true }); const workspace = createPersistedWorkspaceRecord({ workspaceId: "ws-rejected-branch-title", projectId: "proj-rejected-branch-title", - cwd: repoDir, + cwd: workspaceCwd, kind: "worktree", displayName: "Fix checkout title", title: "Fix checkout title", branch: "placeholder-branch", + worktreeRoot: repoDir, + isPaseoOwnedWorktree: true, createdAt: "2026-03-01T12:00:00.000Z", updatedAt: "2026-03-01T12:00:00.000Z", }); @@ -7779,7 +8143,7 @@ test("workspace auto-name applies title once when branch auto-name is rejected", }, }); expect(gitMutations).toEqual([]); - expect(emittedCwds).toEqual([repoDir]); + expect(emittedCwds).toEqual([workspaceCwd]); } finally { vi.useRealTimers(); rmSync(tempDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); @@ -7957,3 +8321,93 @@ test("workspace create stays out of a non-matching workspace subscription", asyn expect(filterByType(emitted, "workspace_update")).toEqual([]); }); + +test("workspace.create.request attaches a directory workspace to its explicit active project", async () => { + const emitted: SessionOutboundMessage[] = []; + const projects = new Map([ + [ + "prj_explicit", + createPersistedProjectRecord({ + projectId: "prj_explicit", + rootPath: path.join(REPO_CWD, "unrelated"), + kind: "non_git", + displayName: "unrelated", + createdAt: "2026-03-01T00:00:00.000Z", + updatedAt: "2026-03-01T00:00:00.000Z", + }), + ], + ]); + const workspaces = new Map(); + const session = createSessionForWorkspaceTests({ onMessage: (message) => emitted.push(message) }); + session.projectRegistry.get = async (projectId: string) => projects.get(projectId) ?? null; + session.workspaceRegistry.upsert = async (record: unknown) => { + const workspace = record as PersistedWorkspaceRecord; + workspaces.set(workspace.workspaceId, workspace); + }; + session.workspaceRegistry.get = async (workspaceId: string) => + workspaces.get(workspaceId) ?? null; + + await session.handleMessage({ + type: "workspace.create.request", + requestId: "req-explicit-project", + source: { kind: "directory", path: REPO_CWD, projectId: "prj_explicit" }, + }); + + const response = findByType(emitted, "workspace.create.response"); + expect(response?.payload).toMatchObject({ + requestId: "req-explicit-project", + error: null, + workspace: { projectId: "prj_explicit" }, + }); + const workspaceId = response?.payload.workspace?.id; + expect(workspaceId).toEqual(expect.any(String)); + expect(workspaces.get(workspaceId as string)).toMatchObject({ + cwd: REPO_CWD, + projectId: "prj_explicit", + }); +}); + +test("workspace.create.request reports an unknown explicit project", async () => { + const emitted: SessionOutboundMessage[] = []; + const session = createSessionForWorkspaceTests({ onMessage: (message) => emitted.push(message) }); + + await session.handleMessage({ + type: "workspace.create.request", + requestId: "req-unknown-project", + source: { kind: "directory", path: REPO_CWD, projectId: "prj_missing" }, + }); + + expect(findByType(emitted, "workspace.create.response")?.payload).toMatchObject({ + requestId: "req-unknown-project", + workspace: null, + errorCode: "unknown_project", + }); +}); + +test("workspace.create.request reports an archived explicit project", async () => { + const emitted: SessionOutboundMessage[] = []; + const archivedProject = createPersistedProjectRecord({ + projectId: "prj_archived", + rootPath: path.join(REPO_CWD, "unrelated"), + kind: "non_git", + displayName: "unrelated", + createdAt: "2026-03-01T00:00:00.000Z", + updatedAt: "2026-03-01T00:00:00.000Z", + archivedAt: "2026-03-02T00:00:00.000Z", + }); + const session = createSessionForWorkspaceTests({ onMessage: (message) => emitted.push(message) }); + session.projectRegistry.get = async (projectId: string) => + projectId === archivedProject.projectId ? archivedProject : null; + + await session.handleMessage({ + type: "workspace.create.request", + requestId: "req-archived-project", + source: { kind: "directory", path: REPO_CWD, projectId: "prj_archived" }, + }); + + expect(findByType(emitted, "workspace.create.response")?.payload).toMatchObject({ + requestId: "req-archived-project", + workspace: null, + errorCode: "archived_project", + }); +}); diff --git a/packages/server/src/server/session/workspace-git-observer/workspace-git-observer-service.test.ts b/packages/server/src/server/session/workspace-git-observer/workspace-git-observer-service.test.ts index 08d37068e..b86131401 100644 --- a/packages/server/src/server/session/workspace-git-observer/workspace-git-observer-service.test.ts +++ b/packages/server/src/server/session/workspace-git-observer/workspace-git-observer-service.test.ts @@ -24,6 +24,7 @@ function makeDescriptor(overrides: { id: string; workspaceDirectory: string; projectKind?: string; + workspaceKind?: "directory" | "local_checkout" | "worktree"; name?: string | null; diffStat?: { additions: number; deletions: number } | null; }): WorkspaceDescriptorPayload { @@ -31,6 +32,7 @@ function makeDescriptor(overrides: { id: overrides.id, workspaceDirectory: overrides.workspaceDirectory, projectKind: overrides.projectKind ?? "git", + workspaceKind: overrides.workspaceKind ?? "local_checkout", name: overrides.name ?? null, diffStat: overrides.diffStat ?? null, } as unknown as WorkspaceDescriptorPayload; @@ -135,11 +137,24 @@ describe("syncObservers", () => { test("does not register a non-git workspace", () => { const h = buildHarness(); h.service.syncObservers([ - makeDescriptor({ id: "ws1", workspaceDirectory: WS1, projectKind: "directory" }), + makeDescriptor({ + id: "ws1", + workspaceDirectory: WS1, + projectKind: "directory", + workspaceKind: "directory", + }), ]); expect(h.registerCalls).toEqual([]); }); + test("registers a Git workspace even when its owning project is non-Git", () => { + const h = buildHarness(); + h.service.syncObservers([ + makeDescriptor({ id: "ws1", workspaceDirectory: WS1, projectKind: "non_git" }), + ]); + expect(h.registerCalls).toEqual([WS1]); + }); + test("is idempotent — re-syncing the same git workspace does not re-register", () => { const h = buildHarness(); const descriptor = makeDescriptor({ id: "ws1", workspaceDirectory: WS1 }); @@ -148,14 +163,46 @@ describe("syncObservers", () => { expect(h.registerCalls).toEqual([WS1]); }); + test("shares one cwd subscription across distinct workspace identities", () => { + const h = buildHarness(); + h.service.syncObservers([ + makeDescriptor({ id: "ws1", workspaceDirectory: WS1 }), + makeDescriptor({ id: "ws2", workspaceDirectory: WS1 }), + ]); + expect(h.registerCalls).toEqual([WS1]); + }); + test("tears down the subscription when a git workspace becomes non-git", () => { const h = buildHarness(); h.service.syncObservers([makeDescriptor({ id: "ws1", workspaceDirectory: WS1 })]); h.service.syncObservers([ - makeDescriptor({ id: "ws1", workspaceDirectory: WS1, projectKind: "directory" }), + makeDescriptor({ + id: "ws1", + workspaceDirectory: WS1, + projectKind: "directory", + workspaceKind: "directory", + }), ]); expect(h.unsubscribeCalls).toEqual([WS1]); }); + + test("keeps a shared subscription when one sibling becomes non-git", () => { + const h = buildHarness(); + h.service.syncObservers([ + makeDescriptor({ id: "ws1", workspaceDirectory: WS1 }), + makeDescriptor({ id: "ws2", workspaceDirectory: WS1 }), + ]); + h.service.syncObservers([ + makeDescriptor({ + id: "ws1", + workspaceDirectory: WS1, + workspaceKind: "directory", + }), + ]); + expect(h.unsubscribeCalls).toEqual([]); + h.emitSnapshot(WS1, "feature"); + expect(h.branchChanges).toEqual([["ws2", null, "feature"]]); + }); }); describe("git snapshot listener", () => { @@ -202,6 +249,13 @@ describe("shouldSkipUpdate", () => { expect(h.service.shouldSkipUpdate("ws1", a)).toBe(true); expect(h.service.shouldSkipUpdate("ws1", b)).toBe(false); }); + + test("starts from the descriptor state recorded during observer sync", () => { + const h = buildHarness(); + const descriptor = makeDescriptor({ id: "ws1", workspaceDirectory: WS1, name: "main" }); + h.service.syncObservers([descriptor]); + expect(h.service.shouldSkipUpdate("ws1", descriptor)).toBe(true); + }); }); describe("recordDescriptorState", () => { @@ -239,10 +293,25 @@ describe("teardown", () => { expect(h.unsubscribeCalls).toEqual([]); }); - test("removeForCwd unsubscribes and stops the observer", () => { + test("keeps a shared cwd subscription until its last workspace is removed", () => { const h = buildHarness(); - h.service.syncObservers([makeDescriptor({ id: "ws1", workspaceDirectory: WS1 })]); - h.service.removeForCwd(WS1); + h.service.syncObservers([ + makeDescriptor({ id: "ws1", workspaceDirectory: WS1, name: "main" }), + makeDescriptor({ id: "ws2", workspaceDirectory: WS1, name: "main" }), + ]); + + h.emitSnapshot(WS1, "feature"); + h.service.removeForWorkspaceId("ws1"); + expect(h.unsubscribeCalls).toEqual([]); + + h.emitSnapshot(WS1, "next"); + expect(h.branchChanges).toEqual([ + ["ws1", "main", "feature"], + ["ws2", "main", "feature"], + ["ws2", "feature", "next"], + ]); + + h.service.removeForWorkspaceId("ws2"); expect(h.unsubscribeCalls).toEqual([WS1]); expect(() => h.emitSnapshot(WS1, "x")).toThrow(); }); diff --git a/packages/server/src/server/session/workspace-git-observer/workspace-git-observer-service.ts b/packages/server/src/server/session/workspace-git-observer/workspace-git-observer-service.ts index 75aac14de..9a26f8bc4 100644 --- a/packages/server/src/server/session/workspace-git-observer/workspace-git-observer-service.ts +++ b/packages/server/src/server/session/workspace-git-observer/workspace-git-observer-service.ts @@ -10,8 +10,11 @@ import type { PersistedWorkspaceRecord } from "../../workspace-registry.js"; const WORKSPACE_GIT_WATCH_REMOVED_STATE_KEY = "__removed__"; interface WorkspaceGitWatchTarget { + workspaceIds: Set; +} + +interface WorkspaceGitWatchState { cwd: string; - workspaceId: string; latestDescriptorStateKey: string | null; lastBranchName: string | null; } @@ -20,8 +23,9 @@ interface WorkspaceGitWatchTarget { * Observes a workspace's git state on disk (via WorkspaceGitService) and drives the * live update fan-out: branch-change notifications, workspace-card refreshes, and * checkout status updates. It owns the per-cwd watch targets and the WorkspaceGitService - * subscription handles, so the registration / dedupe / teardown lifecycle lives in one - * module instead of being smeared across the client session. + * subscription handles. Filesystem subscriptions are keyed by cwd while descriptor and + * branch state remain keyed by workspace id, so same-directory workspace records share one + * watch without sharing identity or teardown lifetime. * * Branch changes reach `onBranchChanged` from two paths that share `lastBranchName`: the * on-disk snapshot listener (handleBranchSnapshot) and the workspace-emit loop @@ -37,7 +41,6 @@ export interface WorkspaceGitObserverService { recordDescriptorState(workspaceId: string, workspace: WorkspaceDescriptorPayload | null): void; handleBranchSnapshot(cwd: string, branchName: string | null): void; removeForWorkspaceId(workspaceId: string): void; - removeForCwd(cwd: string): void; dispose(): void; } @@ -67,6 +70,7 @@ export function createWorkspaceGitObserverService(deps: { } = deps; const watchTargets = new Map(); + const workspaceStates = new Map(); const subscriptions = new Map void>(); function descriptorStateKey(workspace: WorkspaceDescriptorPayload | null): string { @@ -79,71 +83,93 @@ export function createWorkspaceGitObserverService(deps: { ]); } - function resolveTargetByWorkspaceId(workspaceId: string): WorkspaceGitWatchTarget | null { - for (const target of watchTargets.values()) { - if (target.workspaceId === workspaceId) { - return target; - } - } - return null; - } - function rememberDescriptorState( workspaceId: string, workspace: WorkspaceDescriptorPayload | null, ): void { - const target = resolveTargetByWorkspaceId(workspaceId); - if (!target) { + const state = workspaceStates.get(workspaceId); + if (!state) { return; } - target.latestDescriptorStateKey = descriptorStateKey(workspace); - target.lastBranchName = workspace?.name ?? null; + state.latestDescriptorStateKey = descriptorStateKey(workspace); + state.lastBranchName = workspace?.name ?? null; } function removeForCwd(cwd: string): void { const normalizedCwd = resolve(cwd); + const target = watchTargets.get(normalizedCwd); + for (const workspaceId of target?.workspaceIds ?? []) { + workspaceStates.delete(workspaceId); + } watchTargets.delete(normalizedCwd); subscriptions.get(normalizedCwd)?.(); subscriptions.delete(normalizedCwd); } + function removeForWorkspaceId(workspaceId: string): void { + const state = workspaceStates.get(workspaceId); + if (!state) { + return; + } + workspaceStates.delete(workspaceId); + const target = watchTargets.get(state.cwd); + target?.workspaceIds.delete(workspaceId); + if (target?.workspaceIds.size === 0) { + removeForCwd(state.cwd); + } + } + function handleBranchSnapshot(cwd: string, branchName: string | null): void { const target = watchTargets.get(resolve(cwd)); if (!target) { return; } - const previousBranchName = target.lastBranchName; - if (branchName === previousBranchName) { - return; + for (const workspaceId of target.workspaceIds) { + const state = workspaceStates.get(workspaceId); + if (!state) { + continue; + } + const previousBranchName = state.lastBranchName; + if (branchName === previousBranchName) { + continue; + } + state.lastBranchName = branchName; + onBranchChanged?.(workspaceId, previousBranchName, branchName); } - - target.lastBranchName = branchName; - onBranchChanged?.(target.workspaceId, previousBranchName, branchName); } function syncObserver(cwd: string, options: { isGit: boolean; workspaceId: string }): void { const normalizedCwd = resolve(cwd); + const currentState = workspaceStates.get(options.workspaceId); + if (currentState && currentState.cwd !== normalizedCwd) { + removeForWorkspaceId(options.workspaceId); + } if (!options.isGit) { - removeForCwd(normalizedCwd); + removeForWorkspaceId(options.workspaceId); return; } + const target = watchTargets.get(normalizedCwd) ?? { + workspaceIds: new Set(), + }; + watchTargets.set(normalizedCwd, target); + target.workspaceIds.add(options.workspaceId); + if (!workspaceStates.has(options.workspaceId)) { + workspaceStates.set(options.workspaceId, { + cwd: normalizedCwd, + latestDescriptorStateKey: null, + lastBranchName: null, + }); + } + if (subscriptions.has(normalizedCwd)) { return; } - const target: WorkspaceGitWatchTarget = { - cwd: normalizedCwd, - workspaceId: options.workspaceId, - latestDescriptorStateKey: null, - lastBranchName: null, - }; - watchTargets.set(normalizedCwd, target); - - const subscription = workspaceGitService.registerWorkspace( - { cwd: normalizedCwd }, - (snapshot) => { + let subscription: ReturnType; + try { + subscription = workspaceGitService.registerWorkspace({ cwd: normalizedCwd }, (snapshot) => { handleBranchSnapshot(normalizedCwd, snapshot.git.currentBranch ?? null); void emitWorkspaceUpdateForCwd(normalizedCwd).catch((error) => { logger.warn( @@ -152,18 +178,21 @@ export function createWorkspaceGitObserverService(deps: { ); }); emitStatusUpdate(normalizedCwd, snapshot); - }, - ); + }); + } catch (error) { + removeForWorkspaceId(options.workspaceId); + throw error; + } subscriptions.set(normalizedCwd, subscription.unsubscribe); } function syncObservers(workspaces: Iterable): void { for (const workspace of workspaces) { syncObserver(workspace.workspaceDirectory, { - isGit: workspace.projectKind === "git", + isGit: workspace.workspaceKind !== "directory", workspaceId: workspace.id, }); - rememberDescriptorState(workspace.workspaceDirectory, workspace); + rememberDescriptorState(workspace.id, workspace); } } @@ -182,24 +211,24 @@ export function createWorkspaceGitObserverService(deps: { }, shouldSkipUpdate(workspaceId, workspace) { - const target = resolveTargetByWorkspaceId(workspaceId); - if (!target) { + const state = workspaceStates.get(workspaceId); + if (!state) { return false; } const nextStateKey = descriptorStateKey(workspace); - if (target.latestDescriptorStateKey === nextStateKey) { + if (state.latestDescriptorStateKey === nextStateKey) { return true; } - target.latestDescriptorStateKey = nextStateKey; + state.latestDescriptorStateKey = nextStateKey; return false; }, recordDescriptorState(workspaceId, nextWorkspace) { - const target = resolveTargetByWorkspaceId(workspaceId); - if (target && onBranchChanged) { + const state = workspaceStates.get(workspaceId); + if (state && onBranchChanged) { const newBranchName = nextWorkspace?.name ?? null; - if (newBranchName !== target.lastBranchName) { - onBranchChanged(workspaceId, target.lastBranchName, newBranchName); + if (newBranchName !== state.lastBranchName) { + onBranchChanged(workspaceId, state.lastBranchName, newBranchName); } } rememberDescriptorState(workspaceId, nextWorkspace); @@ -207,14 +236,7 @@ export function createWorkspaceGitObserverService(deps: { handleBranchSnapshot, - removeForWorkspaceId(workspaceId) { - const target = resolveTargetByWorkspaceId(workspaceId); - if (target) { - removeForCwd(target.cwd); - } - }, - - removeForCwd, + removeForWorkspaceId, dispose() { for (const unsubscribe of subscriptions.values()) { @@ -222,6 +244,7 @@ export function createWorkspaceGitObserverService(deps: { } subscriptions.clear(); watchTargets.clear(); + workspaceStates.clear(); }, }; } diff --git a/packages/server/src/server/session/workspace-provisioning/workspace-provisioning-service.test.ts b/packages/server/src/server/session/workspace-provisioning/workspace-provisioning-service.test.ts index 91349f238..698e2e451 100644 --- a/packages/server/src/server/session/workspace-provisioning/workspace-provisioning-service.test.ts +++ b/packages/server/src/server/session/workspace-provisioning/workspace-provisioning-service.test.ts @@ -10,10 +10,13 @@ import { FileBackedProjectRegistry, FileBackedWorkspaceRegistry, type PersistedProjectRecord, + createPersistedWorkspaceRecord, + type WorkspaceRegistry, } from "../../workspace-registry.js"; import type { CreatePaseoWorktreeWorkflowResult } from "../../worktree-session.js"; import { createWorkspaceProvisioningService, + WorkspaceProvisioningError, type WorkspaceProvisioningService, } from "./workspace-provisioning-service.js"; @@ -27,6 +30,8 @@ const directorySymlinkType = process.platform === "win32" ? "junction" : "dir"; let tmpDir: string; let gitRoots: Set; +let gitBranches: Map; +let checkoutFailure: Error | null; let workspaceRegistry: FileBackedWorkspaceRegistry; let projectRegistry: FileBackedProjectRegistry; let provisioning: WorkspaceProvisioningService; @@ -35,6 +40,7 @@ function gitService() { return createNoopWorkspaceGitService({ peekSnapshot: () => null, getCheckout: async (cwd: string) => { + if (checkoutFailure) throw checkoutFailure; let worktreeRoot: string | null = null; for (const root of gitRoots) { if ( @@ -47,7 +53,7 @@ function gitService() { return { cwd, isGit: worktreeRoot !== null, - currentBranch: worktreeRoot ? "main" : null, + currentBranch: worktreeRoot ? (gitBranches.get(worktreeRoot) ?? "main") : null, remoteUrl: null, worktreeRoot, isPaseoOwnedWorktree: false, @@ -60,6 +66,8 @@ function gitService() { beforeEach(async () => { tmpDir = mkdtempSync(path.join(os.tmpdir(), "workspace-provisioning-")); gitRoots = new Set(); + gitBranches = new Map(); + checkoutFailure = null; workspaceRegistry = new FileBackedWorkspaceRegistry( path.join(tmpDir, "projects", "workspaces.json"), logger, @@ -112,6 +120,72 @@ test("re-opening an active workspace by exact path returns the same record witho expect(await workspaceRegistry.list()).toHaveLength(1); }); +test("re-opening Windows-equivalent workspace cwd spellings reuses the active and archived record", async () => { + const cwd = path.join(tmpDir, "workspace"); + const created = await provisioning.findOrCreateWorkspaceForDirectory(cwd); + await workspaceRegistry.upsert({ ...created, cwd: `${cwd}${path.sep}` }); + + const active = await provisioning.findOrCreateWorkspaceForDirectory(cwd); + expect(active.workspaceId).toBe(created.workspaceId); + expect(await workspaceRegistry.list()).toHaveLength(1); + + await workspaceRegistry.archive(created.workspaceId, ARCHIVED_AT); + const reopened = await provisioning.findOrCreateWorkspaceForDirectory(cwd); + expect(reopened).toMatchObject({ workspaceId: created.workspaceId, archivedAt: null }); + expect(await workspaceRegistry.list()).toHaveLength(1); +}); + +test("re-opening refreshes mutable checkout metadata without renaming the workspace", async () => { + const repo = path.join(tmpDir, "repo"); + const first = await provisioning.findOrCreateWorkspaceForDirectory(repo); + await workspaceRegistry.upsert({ ...first, title: "Pinned work" }); + gitRoots.add(repo); + gitBranches.set(repo, "feature/refresh"); + + const refreshed = await provisioning.findOrCreateWorkspaceForDirectory(repo); + + expect(refreshed).toMatchObject({ + workspaceId: first.workspaceId, + kind: "local_checkout", + branch: "feature/refresh", + displayName: first.displayName, + title: "Pinned work", + isPaseoOwnedWorktree: false, + mainRepoRoot: null, + }); + expect(await workspaceRegistry.get(first.workspaceId)).toEqual(refreshed); + expect((await projectRegistry.get(first.projectId))?.kind).toBe("git"); +}); + +test("persists manual worktree ownership separately from its workspace kind", async () => { + const cwd = path.join(tmpDir, "manual-worktree"); + const mainRepoRoot = path.join(tmpDir, "main-repo"); + const manualWorktreeProvisioning = createWorkspaceProvisioningService({ + workspaceRegistry, + projectRegistry, + workspaceGitService: createNoopWorkspaceGitService({ + peekSnapshot: () => null, + getCheckout: async () => ({ + cwd, + isGit: true, + currentBranch: "feature/manual", + remoteUrl: null, + worktreeRoot: cwd, + isPaseoOwnedWorktree: false, + mainRepoRoot, + }), + }), + }); + + const workspace = await manualWorktreeProvisioning.findOrCreateWorkspaceForDirectory(cwd); + + expect(workspace).toMatchObject({ + kind: "worktree", + isPaseoOwnedWorktree: false, + mainRepoRoot, + }); +}); + test("re-opening an archived workspace by its exact path unarchives it and keeps the id", async () => { const repo = path.join(tmpDir, "repo"); gitRoots.add(repo); @@ -124,6 +198,110 @@ test("re-opening an archived workspace by its exact path unarchives it and keeps expect(reopened.archivedAt).toBeNull(); }); +test("reopening archived exact-root records restores the fresh Git project", async () => { + const cwd = path.join(tmpDir, "repo"); + const project = await projectRegistry.getOrCreateActiveByRoot({ + rootPath: cwd, + kind: "non_git", + displayName: "repo", + timestamp: ARCHIVED_AT, + }); + const workspace = createPersistedWorkspaceRecord({ + workspaceId: "ws-archived-root", + projectId: project.projectId, + cwd, + kind: "directory", + displayName: "repo", + createdAt: ARCHIVED_AT, + updatedAt: ARCHIVED_AT, + archivedAt: ARCHIVED_AT, + }); + await workspaceRegistry.upsert(workspace); + await projectRegistry.archive(project.projectId, ARCHIVED_AT); + const archivedProvisioning = createWorkspaceProvisioningService({ + workspaceRegistry, + projectRegistry, + workspaceGitService: createNoopWorkspaceGitService({ + peekSnapshot: () => null, + getCheckout: async () => ({ + cwd, + isGit: true, + currentBranch: "main", + remoteUrl: null, + worktreeRoot: cwd, + isPaseoOwnedWorktree: false, + mainRepoRoot: null, + }), + }), + }); + + const reopened = await archivedProvisioning.ensureWorkspaceRecordUnarchived(workspace); + + expect(reopened).toMatchObject({ + workspaceId: workspace.workspaceId, + kind: "local_checkout", + archivedAt: null, + }); + expect(await projectRegistry.get(project.projectId)).toMatchObject({ + kind: "git", + archivedAt: null, + }); +}); + +test("uses one workspace snapshot when reopening an archived workspace", async () => { + const repo = path.join(tmpDir, "repo"); + gitRoots.add(repo); + const created = await provisioning.findOrCreateWorkspaceForDirectory(repo); + await workspaceRegistry.archive(created.workspaceId, ARCHIVED_AT); + + const archived = (await workspaceRegistry.list()).filter( + (workspace) => workspace.workspaceId === created.workspaceId, + ); + let reads = 0; + const snapshotRegistry: WorkspaceRegistry = { + initialize: () => workspaceRegistry.initialize(), + existsOnDisk: () => workspaceRegistry.existsOnDisk(), + list: async () => (reads++ === 0 ? archived : []), + get: (workspaceId) => workspaceRegistry.get(workspaceId), + upsert: (workspace) => workspaceRegistry.upsert(workspace), + archive: (workspaceId, archivedAt) => workspaceRegistry.archive(workspaceId, archivedAt), + remove: (workspaceId) => workspaceRegistry.remove(workspaceId), + }; + const snapshotProvisioning = createWorkspaceProvisioningService({ + workspaceRegistry: snapshotRegistry, + projectRegistry, + workspaceGitService: gitService(), + }); + + const reopened = await snapshotProvisioning.findOrCreateWorkspaceForDirectory(repo); + + expect(reopened).toMatchObject({ workspaceId: created.workspaceId, archivedAt: null }); + expect(await workspaceRegistry.list()).toHaveLength(1); +}); + +test("reopening an archived workspace refreshes placement without renaming it", async () => { + const repo = path.join(tmpDir, "repo"); + gitRoots.add(repo); + const created = await provisioning.findOrCreateWorkspaceForDirectory(repo); + await workspaceRegistry.upsert({ ...created, title: "Pinned archived work" }); + await workspaceRegistry.archive(created.workspaceId, ARCHIVED_AT); + gitRoots.delete(repo); + + const reopened = await provisioning.findOrCreateWorkspaceForDirectory(repo); + + expect(reopened).toMatchObject({ + workspaceId: created.workspaceId, + projectId: created.projectId, + title: "Pinned archived work", + kind: "directory", + branch: null, + displayName: created.displayName, + archivedAt: null, + }); + expect(reopened.updatedAt).toEqual(expect.any(String)); + expect(await workspaceRegistry.get(created.workspaceId)).toEqual(reopened); +}); + test("opening a subpath of an archived git workspace mints a fresh workspace at the exact subpath", async () => { const repo = path.join(tmpDir, "repo"); gitRoots.add(repo); @@ -138,7 +316,7 @@ test("opening a subpath of an archived git workspace mints a fresh workspace at expect((await workspaceRegistry.get(canonical.workspaceId))?.archivedAt).toBe(ARCHIVED_AT); }); -test("ensureWorkspaceRecordUnarchived clears archivedAt on the workspace and its project", async () => { +test("ensureWorkspaceRecordUnarchived restores the owning archived project with the workspace", async () => { const repo = path.join(tmpDir, "repo"); gitRoots.add(repo); const created = await provisioning.findOrCreateWorkspaceForDirectory(repo); @@ -154,6 +332,24 @@ test("ensureWorkspaceRecordUnarchived clears archivedAt on the workspace and its expect((await projectRegistry.get(created.projectId))?.archivedAt).toBeNull(); }); +test("does not unarchive either record when checkout refresh fails", async () => { + const repo = path.join(tmpDir, "repo"); + gitRoots.add(repo); + const created = await provisioning.findOrCreateWorkspaceForDirectory(repo); + await projectRegistry.archive(created.projectId, ARCHIVED_AT); + await workspaceRegistry.archive(created.workspaceId, ARCHIVED_AT); + const archivedProject = await projectRegistry.get(created.projectId); + const archivedWorkspace = await workspaceRegistry.get(created.workspaceId); + checkoutFailure = new Error("Git read failed"); + + await expect(provisioning.ensureWorkspaceRecordUnarchived(archivedWorkspace!)).rejects.toThrow( + "Git read failed", + ); + + expect(await projectRegistry.get(created.projectId)).toEqual(archivedProject); + expect(await workspaceRegistry.get(created.workspaceId)).toEqual(archivedWorkspace); +}); + test("resolveOrCreateWorkspaceIdForCreateAgent returns a created worktree's id without touching the registry", async () => { // The branch only reads workspace.workspaceId off the worktree result. const createdWorktree = { @@ -207,15 +403,90 @@ test("createWorkspaceForDirectory always mints a fresh workspace even when one a expect(await workspaceRegistry.list()).toHaveLength(2); }); -test("findOrCreateProjectForDirectory reuses the active project for the same root", async () => { +test("directory creation persists the live branch and a trimmed title", async () => { + const repo = path.join(tmpDir, "repo"); + gitRoots.add(repo); + const workspace = await provisioning.createWorkspaceForDirectory(repo, " Focused work "); + expect(workspace).toMatchObject({ branch: "main", title: "Focused work" }); +}); + +test("createWorkspaceForDirectory honors an explicit active project without cwd containment", async () => { + const project = await projectRegistry.getOrCreateActiveByRoot({ + rootPath: path.join(tmpDir, "elsewhere"), + kind: "non_git", + displayName: "elsewhere", + timestamp: "2026-03-01T00:00:00.000Z", + }); + const workspace = await provisioning.createWorkspaceForDirectory( + path.join(tmpDir, "directory"), + null, + project.projectId, + ); + expect(workspace.projectId).toBe(project.projectId); +}); + +test("createWorkspaceForDirectory refreshes an explicit project's stale Git kind", async () => { + const rootPath = path.join(tmpDir, "repo"); + gitRoots.add(rootPath); + const project = await projectRegistry.getOrCreateActiveByRoot({ + rootPath, + kind: "non_git", + displayName: "Saved project name", + timestamp: ARCHIVED_AT, + }); + await projectRegistry.upsert({ ...project, customName: "Pinned project name" }); + + const workspace = await provisioning.createWorkspaceForDirectory( + rootPath, + null, + project.projectId, + ); + + expect(workspace.projectId).toBe(project.projectId); + expect(await projectRegistry.get(project.projectId)).toMatchObject({ + projectId: project.projectId, + rootPath, + kind: "git", + displayName: "Saved project name", + customName: "Pinned project name", + }); +}); + +test("createWorkspaceForDirectory classifies unknown and archived explicit projects", async () => { + await expect( + provisioning.createWorkspaceForDirectory(path.join(tmpDir, "directory"), null, "missing"), + ).rejects.toMatchObject({ + code: "unknown_project", + } satisfies Partial); + const project = await projectRegistry.getOrCreateActiveByRoot({ + rootPath: path.join(tmpDir, "archived"), + kind: "non_git", + displayName: "archived", + timestamp: "2026-03-01T00:00:00.000Z", + }); + await projectRegistry.archive(project.projectId, "2026-03-02T00:00:00.000Z"); + await expect( + provisioning.createWorkspaceForDirectory( + path.join(tmpDir, "directory"), + null, + project.projectId, + ), + ).rejects.toMatchObject({ + code: "archived_project", + } satisfies Partial); +}); + +test("findOrCreateProjectForDirectory keeps nested selected roots independent", async () => { const repo = path.join(tmpDir, "repo"); gitRoots.add(repo); const first = await provisioning.findOrCreateProjectForDirectory(repo); const second = await provisioning.findOrCreateProjectForDirectory(path.join(repo, "sub")); - expect(second.projectId).toBe(first.projectId); - expect(await projectRegistry.list()).toHaveLength(1); + expect(second.projectId).not.toBe(first.projectId); + expect(first.rootPath).toBe(repo); + expect(second.rootPath).toBe(path.join(repo, "sub")); + expect(await projectRegistry.list()).toHaveLength(2); }); test("runInImportWorkspace uses an active requested workspace without creating another", async () => { diff --git a/packages/server/src/server/session/workspace-provisioning/workspace-provisioning-service.ts b/packages/server/src/server/session/workspace-provisioning/workspace-provisioning-service.ts index 15d910aaf..103033803 100644 --- a/packages/server/src/server/session/workspace-provisioning/workspace-provisioning-service.ts +++ b/packages/server/src/server/session/workspace-provisioning/workspace-provisioning-service.ts @@ -1,12 +1,11 @@ -import { resolve } from "node:path"; +import { basename, resolve } from "node:path"; import type { Logger } from "pino"; import { - checkoutLiteFromGitSnapshot, - classifyDirectoryForProjectMembership, generateWorkspaceId, + initialWorkspacePlacement, + reconcileWorkspacePlacement, } from "../../workspace-registry-model.js"; import { - createPersistedProjectRecord, createPersistedWorkspaceRecord, type PersistedProjectRecord, type PersistedWorkspaceRecord, @@ -15,20 +14,8 @@ import { } from "../../workspace-registry.js"; import type { WorkspaceGitService } from "../../workspace-git-service.js"; import type { CreatePaseoWorktreeWorkflowResult } from "../../worktree-session.js"; -import { createRealpathAwarePathMatcher } from "../../../utils/path.js"; +import { areEquivalentPaths, createRealpathAwarePathMatcher } from "../../../utils/path.js"; -/** - * Resolves which workspace and project records a directory belongs to, creating, - * reclassifying, or unarchiving them as needed. Every path that needs a workspace - * for a cwd — opening a project, importing an agent, creating an agent, restoring - * an archived worktree — funnels through this one module, so the - * classify → resolve-project → persist → unarchive sequence (and the - * archived-reopen-at-a-different-path and reclassify-vs-unarchive special cases) - * lives in a single place instead of being smeared across the session. - * - * Read-only path resolution (no create/persist) lives in resolve-workspace-id-for-path.ts; - * this module owns the create-and-persist side. - */ export interface ResolveOrCreateWorkspaceIdInput { createdWorktree: CreatePaseoWorktreeWorkflowResult | null; requestedWorkspaceId?: string; @@ -46,6 +33,17 @@ export interface ImportWorkspaceResult { createdWorkspace: PersistedWorkspaceRecord | null; } +export interface CreateWorktreeWorkspaceInput { + sourceCwd: string; + projectId?: string; + repoRoot: string; + cwd: string; + worktreeRoot: string; + branch: string | null; + baseBranch: string | null; + title: string | null; +} + export interface WorkspaceProvisioningService { runInImportWorkspace( input: ImportWorkspaceInput, @@ -56,6 +54,10 @@ export interface WorkspaceProvisioningService { createWorkspaceForDirectory( cwd: string, title?: string | null, + projectId?: string, + ): Promise; + createWorkspaceForWorktree( + input: CreateWorktreeWorkspaceInput, ): Promise; findOrCreateProjectForDirectory(cwd: string): Promise; ensureWorkspaceRecordUnarchived( @@ -63,6 +65,22 @@ export interface WorkspaceProvisioningService { ): Promise; } +export type WorkspaceProvisioningErrorCode = "unknown_project" | "archived_project"; + +export class WorkspaceProvisioningError extends Error { + constructor( + readonly code: WorkspaceProvisioningErrorCode, + projectId: string, + ) { + super( + code === "unknown_project" + ? `Unknown project: ${projectId}` + : `Archived project: ${projectId}`, + ); + this.name = "WorkspaceProvisioningError"; + } +} + export function createWorkspaceProvisioningService(deps: { workspaceRegistry: WorkspaceRegistry; projectRegistry: ProjectRegistry; @@ -134,224 +152,220 @@ export function createWorkspaceProvisioningService(deps: { } } - async function resolveWorkspaceDirectory( - cwd: string, - options?: { refreshGit?: boolean }, - ): Promise { - const normalizedCwd = resolve(cwd); - if (options?.refreshGit === false) { - const snapshot = workspaceGitService.peekSnapshot(normalizedCwd); - return resolve(snapshot?.git.repoRoot ?? normalizedCwd); - } - - const checkout = await workspaceGitService.getCheckout(normalizedCwd); - return resolve(checkout.worktreeRoot ?? normalizedCwd); - } - - async function findExactWorkspaceByDirectory( - cwd: string, - options?: { refreshGit?: boolean }, - ): Promise { - const normalizedCwd = await resolveWorkspaceDirectory(cwd, options); - const workspaces = await workspaceRegistry.list(); - return workspaces.find((workspace) => workspace.cwd === normalizedCwd) ?? null; - } - - async function resolveProjectRecordForPlacement(input: { - membership: ReturnType; - timestamp: string; - }): Promise { - const rootPath = input.membership.projectRootPath; - const kind = input.membership.projectKind; - const projects = await projectRegistry.list(); - const existingProject = - projects.find((project) => !project.archivedAt && project.rootPath === rootPath) ?? - projects.find((project) => project.rootPath === rootPath) ?? - null; - - if (!existingProject) { - return createPersistedProjectRecord({ - projectId: input.membership.projectKey, - rootPath, - kind, - displayName: input.membership.projectName, - createdAt: input.timestamp, - updatedAt: input.timestamp, - }); - } - - return { - ...existingProject, - rootPath, - kind, - archivedAt: null, - updatedAt: input.timestamp, - }; - } - - async function reclassifyOrUnarchiveWorkspaceForDirectory(input: { - workspace: PersistedWorkspaceRecord; - project: PersistedProjectRecord | null; - cwd: string; - }): Promise { - const checkout = await workspaceGitService.getCheckout(input.cwd); - const membership = classifyDirectoryForProjectMembership({ cwd: input.cwd, checkout }); + async function findOrCreateProjectForDirectory(cwd: string): Promise { + const rootPath = resolve(cwd); + const checkout = await workspaceGitService.getCheckout(rootPath); const timestamp = new Date().toISOString(); - const projectRecord = await resolveProjectRecordForPlacement({ - membership, + return projectRegistry.getOrCreateActiveByRoot({ + rootPath, + kind: checkout.isGit ? "git" : "non_git", + displayName: basename(rootPath) || rootPath, timestamp, }); - const projectId = projectRecord.projectId; - const kind = membership.workspaceKind; - const displayName = membership.workspaceDisplayName; + } - if ( - input.workspace.projectId === projectId && - input.workspace.kind === kind && - input.workspace.displayName === displayName - ) { - if (!input.project) { - await projectRegistry.upsert(projectRecord); - } - return ensureWorkspaceRecordUnarchived(input.workspace); + async function requireActiveProject(projectId: string): Promise { + const project = await projectRegistry.get(projectId); + if (!project) throw new WorkspaceProvisioningError("unknown_project", projectId); + if (project.archivedAt) throw new WorkspaceProvisioningError("archived_project", projectId); + return project; + } + + async function createWorkspaceForDirectory( + cwd: string, + title?: string | null, + projectId?: string, + ): Promise { + const normalizedCwd = resolve(cwd); + const checkout = await workspaceGitService.getCheckout(normalizedCwd); + const project = projectId + ? await refreshProjectKind(await requireActiveProject(projectId), normalizedCwd, checkout) + : // COMPAT(workspaceCreateMissingProjectId): added in v0.1.107, remove after 2027-01-15. + await findOrCreateProjectForDirectory(normalizedCwd); + const timestamp = new Date().toISOString(); + const workspace = createPersistedWorkspaceRecord({ + workspaceId: generateWorkspaceId(), + projectId: project.projectId, + ...initialWorkspacePlacement({ source: "checkout", cwd: normalizedCwd, checkout }), + title: title?.trim() || null, + createdAt: timestamp, + updatedAt: timestamp, + }); + await workspaceRegistry.upsert(workspace); + return workspace; + } + + async function createWorkspaceForWorktree( + input: CreateWorktreeWorkspaceInput, + ): Promise { + const sourceCwd = resolve(input.sourceCwd); + const repoRoot = resolve(input.repoRoot); + const cwd = resolve(input.cwd); + const worktreeRoot = resolve(input.worktreeRoot); + const project = await resolveSourceProjectForWorktree({ + sourceCwd, + projectId: input.projectId, + repoRoot, + }); + const timestamp = new Date().toISOString(); + const workspace = createPersistedWorkspaceRecord({ + workspaceId: generateWorkspaceId(), + projectId: project.projectId, + ...initialWorkspacePlacement({ + source: "created_worktree", + cwd, + worktreeRoot, + branch: input.branch, + baseBranch: input.baseBranch, + mainRepoRoot: repoRoot, + }), + title: input.title, + createdAt: timestamp, + updatedAt: timestamp, + }); + await workspaceRegistry.upsert(workspace); + return workspace; + } + + async function resolveSourceProjectForWorktree(input: { + sourceCwd: string; + projectId?: string; + repoRoot: string; + }): Promise { + if (input.projectId) { + return refreshProjectKind(await requireActiveProject(input.projectId)); } - await projectRegistry.upsert(projectRecord); + const workspaces = await workspaceRegistry.list(); + const sourceWorkspace = + workspaces.find( + (workspace) => !workspace.archivedAt && areEquivalentPaths(workspace.cwd, input.sourceCwd), + ) ?? + workspaces.find( + (workspace) => !workspace.archivedAt && areEquivalentPaths(workspace.cwd, input.repoRoot), + ); + if (sourceWorkspace) { + const project = await projectRegistry.get(sourceWorkspace.projectId); + if (project) return refreshProjectKind(project); + // COMPAT(worktreeMissingSourceProject): added in v0.1.107, remove after 2027-01-15. + // Orphaned legacy workspace FKs fall through to exact-root allocation. + } - const nextWorkspace = { - ...input.workspace, - projectId, - cwd: input.cwd, - kind, - displayName, - archivedAt: null, - updatedAt: timestamp, - }; - await workspaceRegistry.upsert(nextWorkspace); - return nextWorkspace; + const project = await projectRegistry.getOrCreateActiveByRoot({ + rootPath: input.repoRoot, + kind: "git", + displayName: basename(input.repoRoot) || input.repoRoot, + timestamp: new Date().toISOString(), + }); + return refreshProjectKind(project); } async function findOrCreateWorkspaceForDirectory(cwd: string): Promise { - const inputCwd = resolve(cwd); - const normalizedCwd = await resolveWorkspaceDirectory(cwd); - const existingWorkspace = await findExactWorkspaceByDirectory(normalizedCwd, { - refreshGit: false, - }); - if (existingWorkspace) { - if (existingWorkspace.archivedAt && inputCwd !== normalizedCwd) { - const timestamp = new Date().toISOString(); - const checkout = checkoutLiteFromGitSnapshot(inputCwd, { - isGit: false, - currentBranch: null, - remoteUrl: null, - repoRoot: null, - isPaseoOwnedWorktree: false, - mainRepoRoot: null, - }); - const membership = classifyDirectoryForProjectMembership({ cwd: inputCwd, checkout }); - const projectRecord = await resolveProjectRecordForPlacement({ - membership, - timestamp, - }); - await projectRegistry.upsert(projectRecord); - const workspaceRecord = createPersistedWorkspaceRecord({ - workspaceId: generateWorkspaceId(), - projectId: projectRecord.projectId, - cwd: inputCwd, - kind: membership.workspaceKind, - displayName: membership.workspaceDisplayName, - createdAt: timestamp, - updatedAt: timestamp, - }); - await workspaceRegistry.upsert(workspaceRecord); - return workspaceRecord; - } - return reclassifyOrUnarchiveWorkspaceForDirectory({ - workspace: existingWorkspace, - project: await projectRegistry.get(existingWorkspace.projectId), - cwd: normalizedCwd, - }); + const normalizedCwd = resolve(cwd); + const workspaces = await workspaceRegistry.list(); + const active = workspaces + .filter( + (workspace) => !workspace.archivedAt && areEquivalentPaths(workspace.cwd, normalizedCwd), + ) + .sort( + (left, right) => + Date.parse(left.createdAt) - Date.parse(right.createdAt) || + left.workspaceId.localeCompare(right.workspaceId), + )[0]; + if (active) return refreshWorkspaceRecord(active); + const archived = workspaces + .filter( + (workspace) => workspace.archivedAt && areEquivalentPaths(workspace.cwd, normalizedCwd), + ) + .sort( + (left, right) => + Date.parse(left.createdAt) - Date.parse(right.createdAt) || + left.workspaceId.localeCompare(right.workspaceId), + )[0]; + if (archived) { + const project = await projectRegistry.get(archived.projectId); + if (project && !project.archivedAt) return ensureWorkspaceRecordUnarchived(archived); } - return createWorkspaceForDirectory(normalizedCwd); } async function resolveOrCreateWorkspaceIdForCreateAgent( input: ResolveOrCreateWorkspaceIdInput, ): Promise { - if (input.createdWorktree) { - return input.createdWorktree.workspace.workspaceId; - } - - if (input.requestedWorkspaceId) { - return input.requestedWorkspaceId; - } - + if (input.createdWorktree) return input.createdWorktree.workspace.workspaceId; + if (input.requestedWorkspaceId) return input.requestedWorkspaceId; return (await createWorkspaceForDirectory(input.cwd, input.initialTitle)).workspaceId; } - async function createWorkspaceForDirectory( - cwd: string, - title?: string | null, - ): Promise { - const checkout = await workspaceGitService.getCheckout(cwd); - const membership = classifyDirectoryForProjectMembership({ cwd, checkout }); - const timestamp = new Date().toISOString(); - - const projectRecord = await resolveProjectRecordForPlacement({ - membership, - timestamp, - }); - await projectRegistry.upsert(projectRecord); - - const workspaceRecord = createPersistedWorkspaceRecord({ - workspaceId: generateWorkspaceId(), - projectId: projectRecord.projectId, - cwd, - kind: membership.workspaceKind, - displayName: membership.workspaceDisplayName, - title: title ?? null, - createdAt: timestamp, - updatedAt: timestamp, - }); - await workspaceRegistry.upsert(workspaceRecord); - return workspaceRecord; - } - - async function findOrCreateProjectForDirectory(cwd: string): Promise { - const normalizedCwd = resolve(cwd); - const checkout = await workspaceGitService.getCheckout(normalizedCwd); - const membership = classifyDirectoryForProjectMembership({ cwd: normalizedCwd, checkout }); - const projectRecord = await resolveProjectRecordForPlacement({ - membership, - timestamp: new Date().toISOString(), - }); - await projectRegistry.upsert(projectRecord); - return projectRecord; - } - async function ensureWorkspaceRecordUnarchived( workspace: PersistedWorkspaceRecord, ): Promise { const project = await projectRegistry.get(workspace.projectId); - if (!workspace.archivedAt && (!project || !project.archivedAt)) { - return workspace; - } - + if (!project) throw new Error(`Unknown project: ${workspace.projectId}`); const timestamp = new Date().toISOString(); - let unarchivedWorkspace = workspace; - if (workspace.archivedAt) { - unarchivedWorkspace = { ...workspace, archivedAt: null, updatedAt: timestamp }; - await workspaceRegistry.upsert(unarchivedWorkspace); - } - if (project?.archivedAt) { - await projectRegistry.upsert({ - ...project, - archivedAt: null, + const checkout = + workspace.archivedAt || project.archivedAt + ? await workspaceGitService.getCheckout(workspace.cwd) + : null; + let next: PersistedWorkspaceRecord | null = null; + if (workspace.archivedAt && checkout) { + const placementUpdate = reconcileWorkspacePlacement({ + workspace, + checkout, updatedAt: timestamp, }); + next = { + ...(placementUpdate?.workspace ?? workspace), + archivedAt: null, + updatedAt: timestamp, + }; } - return unarchivedWorkspace; + if (checkout && (project.archivedAt || workspace.archivedAt)) { + const projectCheckout = areEquivalentPaths(project.rootPath, workspace.cwd) + ? checkout + : await workspaceGitService.getCheckout(project.rootPath); + const kind = projectCheckout.isGit ? "git" : "non_git"; + if (project.archivedAt || project.kind !== kind) { + await projectRegistry.upsert({ ...project, kind, archivedAt: null, updatedAt: timestamp }); + } + } + if (!next) return workspace; + await workspaceRegistry.upsert(next); + return next; + } + + async function refreshWorkspaceRecord( + workspace: PersistedWorkspaceRecord, + ): Promise { + const checkout = await workspaceGitService.getCheckout(workspace.cwd); + const project = await projectRegistry.get(workspace.projectId); + if (project && !project.archivedAt) { + await refreshProjectKind(project, workspace.cwd, checkout); + } + const update = reconcileWorkspacePlacement({ + workspace, + checkout, + updatedAt: new Date().toISOString(), + }); + if (!update) return workspace; + await workspaceRegistry.upsert(update.workspace); + return update.workspace; + } + + async function refreshProjectKind( + project: PersistedProjectRecord, + workspaceCwd?: string, + workspaceCheckout?: Awaited>, + ): Promise { + const projectCheckout = + workspaceCwd && workspaceCheckout && areEquivalentPaths(project.rootPath, workspaceCwd) + ? workspaceCheckout + : await workspaceGitService.getCheckout(project.rootPath); + const kind: PersistedProjectRecord["kind"] = projectCheckout.isGit ? "git" : "non_git"; + if (project.kind === kind) return project; + const refreshed = { ...project, kind, updatedAt: new Date().toISOString() }; + await projectRegistry.upsert(refreshed); + return refreshed; } return { @@ -359,6 +373,7 @@ export function createWorkspaceProvisioningService(deps: { findOrCreateWorkspaceForDirectory, resolveOrCreateWorkspaceIdForCreateAgent, createWorkspaceForDirectory, + createWorkspaceForWorktree, findOrCreateProjectForDirectory, ensureWorkspaceRecordUnarchived, }; diff --git a/packages/server/src/server/session/workspace-recovery/workspace-recovery-service.test.ts b/packages/server/src/server/session/workspace-recovery/workspace-recovery-service.test.ts index 2d04d9fc9..91631a563 100644 --- a/packages/server/src/server/session/workspace-recovery/workspace-recovery-service.test.ts +++ b/packages/server/src/server/session/workspace-recovery/workspace-recovery-service.test.ts @@ -1,4 +1,19 @@ -import { describe, expect, test } from "vitest"; +import { execFileSync } from "node:child_process"; +import { + existsSync, + mkdirSync, + mkdtempSync, + realpathSync, + rmSync, + statSync, + writeFileSync, +} from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +import { afterEach, describe, expect, test } from "vitest"; + +import { createWorktree } from "../../../utils/worktree.js"; import { createPersistedProjectRecord, createPersistedWorkspaceRecord, @@ -8,15 +23,23 @@ import { import { createWorkspaceRecoveryService } from "./workspace-recovery-service.js"; const NOW = "2026-07-11T10:12:30.752Z"; +const tempDirectories: string[] = []; -function createProject(): PersistedProjectRecord { +afterEach(() => { + for (const directory of tempDirectories.splice(0)) { + rmSync(directory, { recursive: true, force: true }); + } +}); + +function createProject(overrides: Partial = {}): PersistedProjectRecord { return createPersistedProjectRecord({ - projectId: "/repo", - rootPath: "/repo", + projectId: "/project", + rootPath: "/project", kind: "git", - displayName: "repo", + displayName: "project", createdAt: NOW, updatedAt: NOW, + ...overrides, }); } @@ -25,12 +48,15 @@ function createWorkspace( ): PersistedWorkspaceRecord { return createPersistedWorkspaceRecord({ workspaceId: "wks_15a1b5630ebaab33", - projectId: "/repo", + projectId: "/project", cwd: "/worktrees/trigger-1525443412986298439", kind: "worktree", displayName: "diagnose-repro-tdd", - title: "Codex TDD reproduction", + title: "TDD reproduction", branch: "diagnose-repro-tdd", + worktreeRoot: "/worktrees/trigger-1525443412986298439", + isPaseoOwnedWorktree: true, + mainRepoRoot: "/repo", createdAt: NOW, updatedAt: NOW, archivedAt: NOW, @@ -42,55 +68,53 @@ function createHarness(input?: { workspace?: PersistedWorkspaceRecord | null; project?: PersistedProjectRecord | null; directories?: string[]; - recreate?: (workspace: PersistedWorkspaceRecord) => Promise; + paseoHome?: string; + worktreesRoot?: string; }) { const workspace = input?.workspace === undefined ? createWorkspace() : input.workspace; const project = input?.project === undefined ? createProject() : input.project; const directories = new Set(input?.directories ?? ["/repo"]); const unarchived: string[] = []; - const recreated: string[] = []; const service = createWorkspaceRecoveryService({ + paseoHome: input?.paseoHome ?? "/paseo-home", + worktreesRoot: input?.worktreesRoot ?? "/worktrees", getWorkspace: async (workspaceId) => workspace?.workspaceId === workspaceId ? workspace : null, getProject: async (projectId) => (project?.projectId === projectId ? project : null), isDirectory: async (path) => directories.has(path), - recreateWorktree: async (record) => { - recreated.push(record.workspaceId); - await input?.recreate?.(record); - }, unarchiveWorkspace: async (record) => { unarchived.push(record.workspaceId); }, }); - return { service, recreated, unarchived }; + return { service, unarchived }; } describe("workspace recovery", () => { - test("authoritatively describes the archived missing worktree from the failed cloud run", async () => { - const { service, recreated, unarchived } = createHarness(); + test("describes a missing archived worktree from persisted placement", async () => { + const { service, unarchived } = createHarness(); await expect(service.inspect("wks_15a1b5630ebaab33")).resolves.toEqual({ kind: "recoverable", workspaceId: "wks_15a1b5630ebaab33", - workspaceName: "Codex TDD reproduction", + workspaceName: "TDD reproduction", action: "restore", branch: "diagnose-repro-tdd", }); - expect(recreated).toEqual([]); expect(unarchived).toEqual([]); }); - test("describes an archived workspace whose directory remains as unarchivable", async () => { + test("unarchives an archived workspace whose exact directory remains", async () => { const workspace = createWorkspace({ kind: "directory", branch: null }); - const { service } = createHarness({ + const { service, unarchived } = createHarness({ workspace, - directories: ["/repo", workspace.cwd], + directories: [workspace.cwd], }); - await expect(service.inspect(workspace.workspaceId)).resolves.toMatchObject({ - kind: "recoverable", + await expect(service.restore(workspace.workspaceId)).resolves.toEqual({ + workspaceId: workspace.workspaceId, action: "unarchive", }); + expect(unarchived).toEqual([workspace.workspaceId]); }); test("does not offer recovery for a missing non-worktree directory", async () => { @@ -105,27 +129,160 @@ describe("workspace recovery", () => { }); }); - test("keeps the workspace archived when recreation fails so restore can be retried", async () => { - let attempts = 0; - const { service, recreated, unarchived } = createHarness({ - recreate: async () => { - attempts += 1; - if (attempts === 1) { - throw new Error("git branch diagnose-repro-tdd is unavailable"); - } + test("uses the persisted source repository instead of the owning project to restore an exact subdirectory", async () => { + const { tempDir, repoDir } = createGitRepository(); + const branch = "feature/mixed-project"; + const sourceSubdirectory = join(repoDir, "packages", "app"); + mkdirSync(sourceSubdirectory, { recursive: true }); + writeFileSync(join(sourceSubdirectory, "README.md"), "app\n"); + execFileSync("git", ["add", "."], { cwd: repoDir, stdio: "pipe" }); + execFileSync("git", ["commit", "-m", "add app"], { cwd: repoDir, stdio: "pipe" }); + execFileSync("git", ["branch", branch], { cwd: repoDir, stdio: "pipe" }); + + const paseoHome = join(tempDir, "paseo-home"); + const worktreesRoot = join(tempDir, "worktrees"); + const created = await createWorktree({ + cwd: repoDir, + worktreeSlug: "mixed-project", + source: { kind: "checkout-branch", branchName: branch }, + runSetup: false, + paseoHome, + worktreesRoot, + }); + const worktreeRoot = realpathSync(created.worktreePath); + const workspaceCwd = join(worktreeRoot, "packages", "app"); + rmSync(worktreeRoot, { recursive: true, force: true }); + execFileSync("git", ["worktree", "prune"], { cwd: repoDir, stdio: "pipe" }); + + const projectRoot = join(tempDir, "explicit-non-git-project"); + mkdirSync(projectRoot); + const project = createProject({ + projectId: "explicit-non-git-project", + rootPath: projectRoot, + kind: "non_git", + }); + const workspace = createWorkspace({ + workspaceId: "ws-mixed-project-recreate", + projectId: project.projectId, + cwd: workspaceCwd, + branch, + worktreeRoot, + mainRepoRoot: repoDir, + }); + const unarchived: string[] = []; + const service = createWorkspaceRecoveryService({ + paseoHome, + worktreesRoot, + getWorkspace: async (workspaceId) => + workspaceId === workspace.workspaceId ? workspace : null, + getProject: async (projectId) => (projectId === project.projectId ? project : null), + isDirectory: async (path) => existsSync(path) && statSync(path).isDirectory(), + unarchiveWorkspace: async (record) => { + unarchived.push(record.workspaceId); }, }); - await expect(service.restore("wks_15a1b5630ebaab33")).rejects.toThrow( - "git branch diagnose-repro-tdd is unavailable", - ); - expect(unarchived).toEqual([]); - - await expect(service.restore("wks_15a1b5630ebaab33")).resolves.toEqual({ - workspaceId: "wks_15a1b5630ebaab33", + await expect(service.restore(workspace.workspaceId)).resolves.toEqual({ + workspaceId: workspace.workspaceId, action: "restore", }); - expect(recreated).toEqual(["wks_15a1b5630ebaab33", "wks_15a1b5630ebaab33"]); - expect(unarchived).toEqual(["wks_15a1b5630ebaab33"]); + expect(existsSync(worktreeRoot)).toBe(true); + expect(existsSync(workspaceCwd)).toBe(true); + expect(unarchived).toEqual([workspace.workspaceId]); + }); + + test("keeps an exact-subdirectory workspace archived when its branch lacks that directory", async () => { + const { tempDir, repoDir } = createGitRepository(); + const branch = "feature/without-subproject"; + execFileSync("git", ["branch", branch], { cwd: repoDir, stdio: "pipe" }); + const paseoHome = join(tempDir, "paseo-home"); + const worktreesRoot = join(tempDir, "worktrees"); + const created = await createWorktree({ + cwd: repoDir, + worktreeSlug: "without-subproject", + source: { kind: "checkout-branch", branchName: branch }, + runSetup: false, + paseoHome, + worktreesRoot, + }); + const worktreeRoot = realpathSync(created.worktreePath); + const workspaceCwd = join(worktreeRoot, "packages", "app"); + rmSync(worktreeRoot, { recursive: true, force: true }); + execFileSync("git", ["worktree", "prune"], { cwd: repoDir, stdio: "pipe" }); + + const project = createProject({ rootPath: repoDir }); + const workspace = createWorkspace({ + workspaceId: "ws-missing-restored-subdirectory", + cwd: workspaceCwd, + branch, + worktreeRoot, + mainRepoRoot: repoDir, + }); + const unarchived: string[] = []; + const service = createWorkspaceRecoveryService({ + paseoHome, + worktreesRoot, + getWorkspace: async (workspaceId) => + workspaceId === workspace.workspaceId ? workspace : null, + getProject: async (projectId) => (projectId === project.projectId ? project : null), + isDirectory: async (targetPath) => + existsSync(targetPath) && statSync(targetPath).isDirectory(), + unarchiveWorkspace: async (record) => { + unarchived.push(record.workspaceId); + }, + }); + + await expect(service.restore(workspace.workspaceId)).rejects.toThrow( + "Selected project directory is missing from the restored worktree", + ); + expect(unarchived).toEqual([]); + expect(existsSync(worktreeRoot)).toBe(false); + expect( + execFileSync("git", ["worktree", "list", "--porcelain"], { + cwd: repoDir, + stdio: "pipe", + }) + .toString() + .includes("without-subproject"), + ).toBe(false); + }); + + test("keeps the workspace archived when its persisted source repository is missing", async () => { + const workspace = createWorkspace({ mainRepoRoot: "/missing-source" }); + const { service, unarchived } = createHarness({ + workspace, + directories: ["/project"], + }); + + await expect(service.inspect(workspace.workspaceId)).resolves.toEqual({ + kind: "unavailable", + workspaceId: workspace.workspaceId, + reason: "project_directory_missing", + message: "The source repository needed to restore this worktree no longer exists.", + }); + await expect(service.restore(workspace.workspaceId)).rejects.toThrow( + "The source repository needed to restore this worktree no longer exists.", + ); + expect(unarchived).toEqual([]); }); }); + +function createGitRepository(): { tempDir: string; repoDir: string } { + const tempDir = mkdtempSync(join(tmpdir(), "paseo-workspace-recovery-")); + tempDirectories.push(tempDir); + const repoDir = join(tempDir, "repo"); + mkdirSync(repoDir); + execFileSync("git", ["init", "-b", "main"], { cwd: repoDir, stdio: "pipe" }); + execFileSync("git", ["config", "user.email", "test@example.com"], { + cwd: repoDir, + stdio: "pipe", + }); + execFileSync("git", ["config", "user.name", "Paseo Test"], { + cwd: repoDir, + stdio: "pipe", + }); + writeFileSync(join(repoDir, "README.md"), "initial\n"); + execFileSync("git", ["add", "."], { cwd: repoDir, stdio: "pipe" }); + execFileSync("git", ["commit", "-m", "initial"], { cwd: repoDir, stdio: "pipe" }); + return { tempDir, repoDir }; +} diff --git a/packages/server/src/server/session/workspace-recovery/workspace-recovery-service.ts b/packages/server/src/server/session/workspace-recovery/workspace-recovery-service.ts index 31a5f0415..1d98a9128 100644 --- a/packages/server/src/server/session/workspace-recovery/workspace-recovery-service.ts +++ b/packages/server/src/server/session/workspace-recovery/workspace-recovery-service.ts @@ -1,3 +1,14 @@ +import { basename } from "node:path"; + +import { createRealpathAwarePathMatcher } from "../../../utils/path.js"; +import { runGitCommand } from "../../../utils/run-git-command.js"; +import { + createWorktree, + isPaseoOwnedWorktreeCwd, + mapWorkspaceCwdToWorktree, + rollbackCreatedPaseoWorktree, +} from "../../../utils/worktree.js"; +import { WorktreeRequestError, toWorktreeRequestError } from "../../worktree-errors.js"; import { resolveWorkspaceDisplayName, type PersistedProjectRecord, @@ -32,14 +43,32 @@ export interface WorkspaceRecoveryService { restore(workspaceId: string): Promise<{ workspaceId: string; action: WorkspaceRecoveryAction }>; } +type RecoveryPlan = + | { + kind: "unarchive"; + state: Extract; + workspace: PersistedWorkspaceRecord; + } + | { + kind: "restore"; + state: Extract; + workspace: PersistedWorkspaceRecord; + sourceRepoRoot: string; + }; + +type UnavailableRecoveryState = Extract; + export function createWorkspaceRecoveryService(deps: { + paseoHome: string; + worktreesRoot?: string; getWorkspace: (workspaceId: string) => Promise; getProject: (projectId: string) => Promise; isDirectory: (path: string) => Promise; - recreateWorktree: (workspace: PersistedWorkspaceRecord) => Promise; unarchiveWorkspace: (workspace: PersistedWorkspaceRecord) => Promise; }): WorkspaceRecoveryService { - async function inspect(workspaceId: string): Promise { + async function resolveRecovery( + workspaceId: string, + ): Promise { const workspace = await deps.getWorkspace(workspaceId); if (!workspace) { return { @@ -69,13 +98,7 @@ export function createWorkspaceRecoveryService(deps: { } if (await deps.isDirectory(workspace.cwd)) { - return { - kind: "recoverable", - workspaceId, - workspaceName: resolveWorkspaceDisplayName(workspace), - action: "unarchive", - branch: workspace.branch, - }; + return createRecoveryPlan({ action: "unarchive", workspace }); } if (workspace.kind !== "worktree") { @@ -94,42 +117,148 @@ export function createWorkspaceRecoveryService(deps: { message: "The archived worktree has no branch recorded, so it cannot be restored.", }; } - if (!(await deps.isDirectory(project.rootPath))) { + + // COMPAT(worktreeRestoreMissingMainRepoRoot): records created before v0.1.110 + // lack placement ownership; remove the project-root fallback after 2027-01-17. + const sourceRepoRoot = workspace.mainRepoRoot ?? project.rootPath; + if (!(await deps.isDirectory(sourceRepoRoot))) { return { kind: "unavailable", workspaceId, reason: "project_directory_missing", - message: "The project directory needed to restore this worktree no longer exists.", + message: "The source repository needed to restore this worktree no longer exists.", }; } - return { - kind: "recoverable", - workspaceId, - workspaceName: resolveWorkspaceDisplayName(workspace), - action: "restore", - branch: workspace.branch, - }; + return createRecoveryPlan({ action: "restore", workspace, sourceRepoRoot }); + } + + async function inspect(workspaceId: string): Promise { + const resolved = await resolveRecovery(workspaceId); + return resolved.kind === "unavailable" ? resolved : resolved.state; } async function restore( workspaceId: string, ): Promise<{ workspaceId: string; action: WorkspaceRecoveryAction }> { - const state = await inspect(workspaceId); - if (state.kind === "unavailable") { - throw new Error(state.message); + const resolved = await resolveRecovery(workspaceId); + if (resolved.kind === "unavailable") { + throw new Error(resolved.message); } - const workspace = await deps.getWorkspace(workspaceId); - if (!workspace?.archivedAt) { - throw new Error("The archived workspace changed before it could be recovered."); + if (resolved.kind === "restore") { + await recreateArchivedWorktree(resolved.workspace, resolved.sourceRepoRoot); } - if (state.action === "restore") { - await deps.recreateWorktree(workspace); + await deps.unarchiveWorkspace(resolved.workspace); + return { workspaceId, action: resolved.kind }; + } + + async function recreateArchivedWorktree( + workspace: PersistedWorkspaceRecord, + sourceRepoRoot: string, + ): Promise { + const branch = workspace.branch; + if (!branch) { + throw new WorktreeRequestError({ + code: "unknown", + message: `Workspace ${workspace.workspaceId} has no branch to restore`, + }); + } + + try { + await runGitCommand(["worktree", "prune"], { cwd: sourceRepoRoot, timeout: 30_000 }); + } catch { + // A stale worktree registration is not guaranteed; creation reports any real conflict. + } + + let previousWorktreePath = workspace.worktreeRoot; + if (!previousWorktreePath) { + // COMPAT(worktreeRestoreMissingWorktreeRoot): records created before v0.1.110 + // lack durable backing placement; remove filesystem discovery after 2027-01-17. + const ownership = await isPaseoOwnedWorktreeCwd(workspace.cwd, { + paseoHome: deps.paseoHome, + worktreesRoot: deps.worktreesRoot, + }); + previousWorktreePath = ownership.allowed + ? (ownership.worktreePath ?? workspace.cwd) + : workspace.cwd; + } + + let recreatedWorktreePath: string; + try { + const result = await createWorktree({ + cwd: sourceRepoRoot, + worktreeSlug: basename(previousWorktreePath), + source: { kind: "checkout-branch", branchName: branch }, + runSetup: false, + paseoHome: deps.paseoHome, + worktreesRoot: deps.worktreesRoot, + }); + recreatedWorktreePath = result.worktreePath; + } catch (error) { + throw toWorktreeRequestError(error); + } + + try { + const recreatedWorkspacePath = mapWorkspaceCwdToWorktree({ + sourceWorktreePath: previousWorktreePath, + workspaceCwd: workspace.cwd, + targetWorktreePath: recreatedWorktreePath, + }); + if (!createRealpathAwarePathMatcher(workspace.cwd)(recreatedWorkspacePath)) { + throw new WorktreeRequestError({ + code: "unknown", + message: `Recreated worktree diverged from ${workspace.cwd}: ${recreatedWorkspacePath}`, + }); + } + if (!(await deps.isDirectory(recreatedWorkspacePath))) { + throw new WorktreeRequestError({ + code: "unknown", + message: `Selected project directory is missing from the restored worktree: ${recreatedWorkspacePath}`, + }); + } + } catch (error) { + return rollbackCreatedPaseoWorktree( + { + cwd: sourceRepoRoot, + worktreePath: recreatedWorktreePath, + teardownCwds: [], + paseoHome: deps.paseoHome, + worktreesBaseRoot: deps.worktreesRoot, + }, + error, + ); } - await deps.unarchiveWorkspace(workspace); - return { workspaceId, action: state.action }; } return { inspect, restore }; } + +function createRecoveryPlan( + input: + | { action: "unarchive"; workspace: PersistedWorkspaceRecord } + | { action: "restore"; workspace: PersistedWorkspaceRecord; sourceRepoRoot: string }, +): RecoveryPlan { + const state = { + kind: "recoverable" as const, + workspaceId: input.workspace.workspaceId, + workspaceName: resolveWorkspaceDisplayName(input.workspace), + branch: input.workspace.branch, + }; + if (input.action === "restore") { + return { + kind: input.action, + state: { ...state, action: input.action }, + workspace: input.workspace, + sourceRepoRoot: input.sourceRepoRoot, + }; + } + return { + kind: input.action, + state: { + ...state, + action: input.action, + }, + workspace: input.workspace, + }; +} diff --git a/packages/server/src/server/session/workspace-scripts/workspace-scripts-service.test.ts b/packages/server/src/server/session/workspace-scripts/workspace-scripts-service.test.ts index fbb0b78c9..137b83ac1 100644 --- a/packages/server/src/server/session/workspace-scripts/workspace-scripts-service.test.ts +++ b/packages/server/src/server/session/workspace-scripts/workspace-scripts-service.test.ts @@ -1,4 +1,4 @@ -import { mkdtempSync, rmSync } from "node:fs"; +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { pino } from "pino"; @@ -6,34 +6,29 @@ import { afterEach, describe, expect, test } from "vitest"; import type { SessionOutboundMessage, StartWorkspaceScriptRequest } from "../../messages.js"; import { createServiceProxySubsystem, type ServiceProxySubsystem } from "../../service-proxy.js"; import type { TerminalManager } from "../../../terminal/terminal-manager.js"; -import type { PersistedWorkspaceRecord, WorkspaceRegistry } from "../../workspace-registry.js"; -import type { WorkspaceGitMetadata } from "../../workspace-git-metadata.js"; +import type { + PersistedProjectRecord, + PersistedWorkspaceRecord, + ProjectRegistry, + WorkspaceRegistry, +} from "../../workspace-registry.js"; +import { createNoGitWorkspaceRuntimeSnapshot } from "../../test-utils/workspace-git-service-stub.js"; import { WorkspaceScriptRuntimeStore } from "../../workspace-script-runtime-store.js"; import type { SpawnWorkspaceScriptOptions, WorktreeScriptResult, } from "../../worktree-bootstrap.js"; +import type { WorkspaceGitService } from "../../workspace-git-service.js"; import { createWorkspaceScriptsService } from "./workspace-scripts-service.js"; +import { deriveProjectServiceSlug } from "../../workspace-git-metadata.js"; -// The production module reads only WorkspaceGitService.{peekSnapshot,getWorkspaceGitMetadata}, +// The production module reads only WorkspaceGitService.{peekSnapshot,getProjectSlug}, // WorkspaceRegistry.get, and forwards the launcher + opaque managers to the injected // spawnWorkspaceScript port. The fakes below implement exactly that slice; the service proxy and // runtime store are the real in-memory implementations, and spawning is injected so no process runs. const logger = pino({ level: "silent" }); -const gitMetadata: WorkspaceGitMetadata = { - projectKind: "git", - projectDisplayName: "repo", - workspaceDisplayName: "repo", - gitRemote: null, - isWorktree: false, - projectSlug: "paseo", - repoRoot: "/tmp/repo", - currentBranch: "feature/scripts", - remoteUrl: null, -}; - function fakeWorkspaceRegistry( record: PersistedWorkspaceRecord | null, ): Pick { @@ -44,13 +39,28 @@ function fakeWorkspaceRegistry( }; } -function fakeGitService(metadata: WorkspaceGitMetadata = gitMetadata) { +function fakeProjectRegistry(record: PersistedProjectRecord | null): Pick { + return { + async get() { + return record; + }, + }; +} + +function fakeGitService() { + const snapshot = createNoGitWorkspaceRuntimeSnapshot("/tmp/repo"); + snapshot.git = { + ...snapshot.git, + isGit: true, + repoRoot: "/tmp/repo", + currentBranch: "feature/scripts", + remoteUrl: "https://github.com/getpaseo/paseo.git", + hasRemote: true, + }; + return { peekSnapshot() { - return null; - }, - async getWorkspaceGitMetadata() { - return metadata; + return snapshot; }, }; } @@ -64,7 +74,9 @@ interface BuildOptions { scriptRuntimeStore?: WorkspaceScriptRuntimeStore | null; terminalManager?: TerminalManager | null; workspace?: PersistedWorkspaceRecord | null; + project?: PersistedProjectRecord | null; spawnThrows?: string; + gitService?: Pick; } function buildService(options: BuildOptions = {}) { @@ -87,7 +99,8 @@ function buildService(options: BuildOptions = {}) { terminalManager: options.terminalManager === undefined ? availableTerminalManager : options.terminalManager, workspaceRegistry: fakeWorkspaceRegistry(workspace), - workspaceGitService: fakeGitService(), + projectRegistry: fakeProjectRegistry(options.project ?? null), + workspaceGitService: options.gitService ?? fakeGitService(), getDaemonTcpPort: () => 6767, getDaemonTcpHost: () => "127.0.0.1", serviceProxyPublicBaseUrl: null, @@ -130,28 +143,77 @@ afterEach(() => { }); describe("buildSnapshot", () => { - test("returns no scripts when the service proxy is unavailable", () => { + test("returns no scripts when the service proxy is unavailable", async () => { const { service } = buildService({ serviceProxy: null }); - expect(service.buildSnapshot("ws-1", "/tmp/repo")).toEqual([]); + expect( + service.buildSnapshot({ workspaceId: "ws-1", cwd: "/tmp/repo" } as PersistedWorkspaceRecord), + ).toEqual([]); }); - test("returns no scripts when the runtime store is unavailable", () => { + test("returns no scripts when the runtime store is unavailable", async () => { const { service } = buildService({ scriptRuntimeStore: null }); - expect(service.buildSnapshot("ws-1", "/tmp/repo")).toEqual([]); + expect( + service.buildSnapshot({ workspaceId: "ws-1", cwd: "/tmp/repo" } as PersistedWorkspaceRecord), + ).toEqual([]); }); - test("returns no scripts for a workspace without a paseo.json", () => { + test("returns no scripts for a workspace without a paseo.json", async () => { const dir = mkdtempSync(join(tmpdir(), "workspace-scripts-")); tempDirs.push(dir); const { service } = buildService(); - expect(service.buildSnapshot("ws-1", dir)).toEqual([]); + expect( + service.buildSnapshot({ workspaceId: "ws-1", cwd: dir } as PersistedWorkspaceRecord), + ).toEqual([]); + }); + + test("projects service hostnames without a Git snapshot", async () => { + const directory = mkdtempSync(join(tmpdir(), "workspace-scripts-")); + tempDirs.push(directory); + writeFileSync( + join(directory, "paseo.json"), + JSON.stringify({ scripts: { app: { type: "service", command: "npm run app", port: 3000 } } }), + ); + const project = { + projectId: "prj_no_snapshot", + rootPath: directory, + kind: "git", + displayName: "app", + customName: null, + createdAt: "2026-01-01T00:00:00.000Z", + updatedAt: "2026-01-01T00:00:00.000Z", + archivedAt: null, + } as PersistedProjectRecord; + const workspace = { + workspaceId: "ws-no-snapshot", + projectId: project.projectId, + cwd: directory, + branch: "feature/persisted", + } as PersistedWorkspaceRecord; + const serviceProxy = createServiceProxySubsystem({ logger }); + const { service, spawnCalls } = buildService({ + workspace, + project, + serviceProxy, + gitService: { peekSnapshot: () => undefined }, + }); + + expect(service.buildSnapshot(workspace, project)[0]?.hostname).toBe( + serviceProxy.projectWorkspaceService({ + projectSlug: deriveProjectServiceSlug(project), + branchName: workspace.branch, + scriptName: "app", + daemonPort: 6767, + }).hostname, + ); + await service.start({ ...request, workspaceId: workspace.workspaceId }); + expect(spawnCalls[0]?.branchName).toBe(workspace.branch); }); }); describe("emitStatusUpdate", () => { - test("emits one script_status_update carrying the snapshot", () => { + test("emits one script_status_update carrying the snapshot", async () => { const { service, emitted } = buildService(); - service.emitStatusUpdate("ws-1", "/tmp/repo"); + await service.emitStatusUpdate("ws-1", "/tmp/repo"); expect(emitted).toEqual([ { type: "script_status_update", payload: { workspaceId: "ws-1", scripts: [] } }, ]); @@ -225,6 +287,101 @@ describe("start", () => { }); }); + test("uses the exact project root for a service hostname", async () => { + const workspace = { + workspaceId: "ws-app", + projectId: "prj-app", + cwd: "/repo/apps/app", + } as PersistedWorkspaceRecord; + const project = { + projectId: "prj-app", + rootPath: "/repo/apps/app", + kind: "git", + displayName: "app", + customName: null, + createdAt: "2026-01-01T00:00:00.000Z", + updatedAt: "2026-01-01T00:00:00.000Z", + archivedAt: null, + } as PersistedProjectRecord; + const { service, spawnCalls } = buildService({ workspace, project }); + + await service.start({ ...request, workspaceId: workspace.workspaceId }); + + expect(spawnCalls[0]).toMatchObject({ + projectSlug: deriveProjectServiceSlug(project), + }); + }); + + test("keeps same-named service projects distinct", async () => { + const projectA = { + projectId: "prj-app-a", + rootPath: "/repo-a/app", + kind: "git", + displayName: "app", + customName: null, + createdAt: "2026-01-01T00:00:00.000Z", + updatedAt: "2026-01-01T00:00:00.000Z", + archivedAt: null, + } as PersistedProjectRecord; + const projectB = { ...projectA, projectId: "prj-app-b", rootPath: "/repo-b/app" }; + const workspaceA = { + workspaceId: "ws-app-a", + projectId: projectA.projectId, + cwd: projectA.rootPath, + } as PersistedWorkspaceRecord; + const workspaceB = { + workspaceId: "ws-app-b", + projectId: projectB.projectId, + cwd: projectB.rootPath, + } as PersistedWorkspaceRecord; + const first = buildService({ workspace: workspaceA, project: projectA }); + const second = buildService({ workspace: workspaceB, project: projectB }); + + await first.service.start({ ...request, workspaceId: workspaceA.workspaceId }); + await second.service.start({ ...request, workspaceId: workspaceB.workspaceId }); + + expect(first.spawnCalls[0]?.projectSlug).not.toBe(second.spawnCalls[0]?.projectSlug); + }); + + test("predicts the same service hostname that start registers", async () => { + const directory = mkdtempSync(join(tmpdir(), "workspace-scripts-")); + tempDirs.push(directory); + writeFileSync( + join(directory, "paseo.json"), + JSON.stringify({ scripts: { app: { type: "service", command: "npm run app", port: 3000 } } }), + ); + const project = { + projectId: "prj_hostname", + rootPath: directory, + kind: "git", + displayName: "app", + customName: null, + createdAt: "2026-01-01T00:00:00.000Z", + updatedAt: "2026-01-01T00:00:00.000Z", + archivedAt: null, + } as PersistedProjectRecord; + const workspace = { + workspaceId: "ws-hostname", + projectId: project.projectId, + cwd: directory, + } as PersistedWorkspaceRecord; + const serviceProxy = createServiceProxySubsystem({ logger }); + const { service, spawnCalls } = buildService({ workspace, project, serviceProxy }); + + const snapshot = service.buildSnapshot(workspace, project); + await service.start({ ...request, workspaceId: workspace.workspaceId }); + + const started = spawnCalls[0]!; + expect(snapshot[0]?.hostname).toBe( + serviceProxy.projectWorkspaceService({ + projectSlug: started.projectSlug, + branchName: started.branchName, + scriptName: started.scriptName, + daemonPort: started.daemonPort, + }).hostname, + ); + }); + test("reports the launcher error when spawning fails", async () => { const { service, emitted } = buildService({ spawnThrows: "boom" }); await service.start(request); diff --git a/packages/server/src/server/session/workspace-scripts/workspace-scripts-service.ts b/packages/server/src/server/session/workspace-scripts/workspace-scripts-service.ts index 044b23c5d..a17c34b0b 100644 --- a/packages/server/src/server/session/workspace-scripts/workspace-scripts-service.ts +++ b/packages/server/src/server/session/workspace-scripts/workspace-scripts-service.ts @@ -9,7 +9,12 @@ import type { ServiceProxySubsystem } from "../../service-proxy.js"; import type { WorkspaceScriptRuntimeStore } from "../../workspace-script-runtime-store.js"; import type { ScriptHealthState } from "../../script-health-monitor.js"; import type { WorkspaceGitService } from "../../workspace-git-service.js"; -import type { WorkspaceRegistry } from "../../workspace-registry.js"; +import type { + PersistedProjectRecord, + PersistedWorkspaceRecord, + ProjectRegistry, + WorkspaceRegistry, +} from "../../workspace-registry.js"; import type { SpawnWorkspaceScriptOptions, WorktreeScriptResult, @@ -18,15 +23,10 @@ import { buildWorkspaceScriptPayloads, readPaseoConfigForProjection, } from "../../script-status-projection.js"; -import { deriveProjectSlug } from "../../workspace-git-metadata.js"; +import { deriveProjectServiceSlug, deriveProjectSlug } from "../../workspace-git-metadata.js"; type WorkspaceScriptsPayload = WorkspaceDescriptorPayload["scripts"]; -interface WorkspaceScriptGitMetadata { - projectSlug: string; - currentBranch: string | null; -} - /** * The service-proxy-backed scripts a workspace exposes: build the scripts payload * snapshot, emit a script_status_update to clients, and start a script. @@ -37,21 +37,22 @@ interface WorkspaceScriptGitMetadata { * that assembly and guard across the session. */ export interface WorkspaceScriptsService { - buildSnapshot(workspaceId: string, workspaceDirectory: string): WorkspaceScriptsPayload; - emitStatusUpdate(workspaceId: string, workspaceDirectory: string): void; + buildSnapshot( + workspace: PersistedWorkspaceRecord, + project?: PersistedProjectRecord | null, + ): WorkspaceScriptsPayload; + emitStatusUpdate(workspaceId: string, workspaceDirectory: string): Promise; start(request: StartWorkspaceScriptRequest): Promise; } -type WorkspaceScriptsGitSource = Pick< - WorkspaceGitService, - "peekSnapshot" | "getWorkspaceGitMetadata" ->; +type WorkspaceScriptsGitSource = Pick; export function createWorkspaceScriptsService(deps: { serviceProxy: ServiceProxySubsystem | null; scriptRuntimeStore: WorkspaceScriptRuntimeStore | null; terminalManager: TerminalManager | null; workspaceRegistry: Pick; + projectRegistry: Pick; workspaceGitService: WorkspaceScriptsGitSource; getDaemonTcpPort: (() => number | null) | null; getDaemonTcpHost: (() => string | null) | null; @@ -66,6 +67,7 @@ export function createWorkspaceScriptsService(deps: { scriptRuntimeStore, terminalManager, workspaceRegistry, + projectRegistry, workspaceGitService, getDaemonTcpPort, getDaemonTcpHost, @@ -76,45 +78,60 @@ export function createWorkspaceScriptsService(deps: { spawnWorkspaceScript, } = deps; - function resolveGitMetadata(workspaceDirectory: string): WorkspaceScriptGitMetadata | undefined { - const snapshot = workspaceGitService.peekSnapshot(workspaceDirectory); - if (!snapshot) { - return undefined; + function resolveGitMetadata( + workspace: PersistedWorkspaceRecord, + project: { projectId: string; rootPath: string } | null, + ) { + const snapshot = workspaceGitService.peekSnapshot(workspace.cwd); + const currentBranch = snapshot?.git.currentBranch ?? workspace.branch ?? null; + if (project) { + return { + projectSlug: deriveProjectServiceSlug(project), + currentBranch, + }; } + if (!snapshot) return undefined; return { projectSlug: deriveProjectSlug( - workspaceDirectory, + workspace.cwd, snapshot.git.isGit ? snapshot.git.remoteUrl : null, ), - currentBranch: snapshot.git.currentBranch, + currentBranch, }; } - function buildSnapshot(workspaceId: string, workspaceDirectory: string): WorkspaceScriptsPayload { + function buildSnapshot( + workspace: PersistedWorkspaceRecord, + project: PersistedProjectRecord | null = null, + ): WorkspaceScriptsPayload { if (!serviceProxy || !scriptRuntimeStore) { return []; } return buildWorkspaceScriptPayloads({ - workspaceId, - workspaceDirectory, - paseoConfig: readPaseoConfigForProjection(workspaceDirectory, logger), + workspaceId: workspace.workspaceId, + workspaceDirectory: workspace.cwd, + paseoConfig: readPaseoConfigForProjection(workspace.cwd, logger), serviceProxy, runtimeStore: scriptRuntimeStore, daemonPort: getDaemonTcpPort?.() ?? null, serviceProxyPublicBaseUrl, - gitMetadata: resolveGitMetadata(workspaceDirectory), + gitMetadata: resolveGitMetadata(workspace, project), resolveHealth: resolveScriptHealth ?? undefined, }); } - function emitStatusUpdate(workspaceId: string, workspaceDirectory: string): void { - emit({ - type: "script_status_update", - payload: { - workspaceId, - scripts: buildSnapshot(workspaceId, workspaceDirectory), - }, - }); + async function emitStatusUpdate(workspaceId: string, _workspaceDirectory: string): Promise { + try { + const workspace = await workspaceRegistry.get(workspaceId); + if (!workspace) return; + const project = await projectRegistry.get(workspace.projectId); + emit({ + type: "script_status_update", + payload: { workspaceId, scripts: buildSnapshot(workspace, project) }, + }); + } catch (error) { + logger.warn({ err: error, workspaceId }, "Failed to project workspace script status"); + } } async function start(request: StartWorkspaceScriptRequest): Promise { @@ -127,13 +144,23 @@ export function createWorkspaceScriptsService(deps: { if (!workspace) { throw new Error(`Workspace not found: ${request.workspaceId}`); } - const gitMetadata = await workspaceGitService.getWorkspaceGitMetadata(workspace.cwd); + const project = await projectRegistry.get(workspace.projectId); + const projectSlug = project + ? deriveProjectServiceSlug(project) + : deriveProjectSlug( + workspace.cwd, + workspaceGitService.peekSnapshot(workspace.cwd)?.git.remoteUrl ?? null, + ); + const branchName = + workspaceGitService.peekSnapshot(workspace.cwd)?.git.currentBranch ?? + workspace.branch ?? + null; const serviceResult = await spawnWorkspaceScript({ repoRoot: workspace.cwd, workspaceId: workspace.workspaceId, - projectSlug: gitMetadata.projectSlug, - branchName: gitMetadata.currentBranch, + projectSlug, + branchName, scriptName: request.scriptName, daemonPort: getDaemonTcpPort?.() ?? null, daemonListenHost: getDaemonTcpHost?.() ?? null, @@ -143,11 +170,11 @@ export function createWorkspaceScriptsService(deps: { terminalManager, logger, onLifecycleChanged: () => { - emitStatusUpdate(workspace.workspaceId, workspace.cwd); + void emitStatusUpdate(workspace.workspaceId, workspace.cwd); }, }); - emitStatusUpdate(workspace.workspaceId, workspace.cwd); + void emitStatusUpdate(workspace.workspaceId, workspace.cwd); emit({ type: "start_workspace_script_response", payload: { diff --git a/packages/server/src/server/test-utils/workspace-git-service-stub.ts b/packages/server/src/server/test-utils/workspace-git-service-stub.ts index 91390e4fc..20f5d2ebb 100644 --- a/packages/server/src/server/test-utils/workspace-git-service-stub.ts +++ b/packages/server/src/server/test-utils/workspace-git-service-stub.ts @@ -1,9 +1,5 @@ -import { basename } from "node:path"; import type { CheckoutDiffResult } from "../../utils/checkout-git.js"; -import { - buildWorkspaceGitMetadataFromSnapshot, - type WorkspaceGitMetadata, -} from "../workspace-git-metadata.js"; +import { deriveProjectSlug } from "../workspace-git-metadata.js"; import type { WorkspaceGitRuntimeSnapshot, WorkspaceGitService } from "../workspace-git-service.js"; export function createNoGitWorkspaceRuntimeSnapshot(cwd: string): WorkspaceGitRuntimeSnapshot { @@ -39,6 +35,9 @@ export function createNoopWorkspaceGitService( registerWorkspace: () => ({ unsubscribe: () => {}, }), + onSnapshotUpdated: () => ({ + unsubscribe: () => {}, + }), peekSnapshot: () => null, getCheckout: async (cwd: string) => ({ cwd, @@ -56,17 +55,9 @@ export function createNoopWorkspaceGitService( suggestBranchesForCwd: async () => [], listStashes: async () => [], listWorktrees: async () => [], - getWorkspaceGitMetadata: async (cwd: string, options): Promise => { + getProjectSlug: async (cwd: string) => { const snapshot = createNoGitWorkspaceRuntimeSnapshot(cwd); - return buildWorkspaceGitMetadataFromSnapshot({ - cwd, - directoryName: options?.directoryName ?? basename(cwd), - isGit: snapshot.git.isGit, - repoRoot: snapshot.git.repoRoot, - mainRepoRoot: snapshot.git.mainRepoRoot, - currentBranch: snapshot.git.currentBranch, - remoteUrl: snapshot.git.remoteUrl, - }); + return deriveProjectSlug(cwd, snapshot.git.isGit ? snapshot.git.remoteUrl : null); }, resolveForge: async () => null, resolveRepoRoot: async (cwd: string) => cwd, diff --git a/packages/server/src/server/websocket-server.relay-reconnect.test.ts b/packages/server/src/server/websocket-server.relay-reconnect.test.ts index cd077f037..6a6cef196 100644 --- a/packages/server/src/server/websocket-server.relay-reconnect.test.ts +++ b/packages/server/src/server/websocket-server.relay-reconnect.test.ts @@ -921,6 +921,20 @@ describe("relay external socket reconnect behavior", () => { await server.close(); }); + test("advertises stable project identity in initial server_info", async () => { + const server = createServer(); + const socket = new MockSocket(); + + const serverInfo = await attachRelayAndHello({ + server, + socket, + clientId: "cid-stable-project-identity", + }); + + expect(serverInfo.features?.stableProjectIdentity).toBe(true); + await server.close(); + }); + test("includes voice capabilities in initial server_info when speech readiness exists", async () => { const speechReadiness = createReadySpeechReadinessSnapshot(); const server = createServer({ speechReadiness }); diff --git a/packages/server/src/server/websocket-server.ts b/packages/server/src/server/websocket-server.ts index 62b6b03f4..649b54152 100644 --- a/packages/server/src/server/websocket-server.ts +++ b/packages/server/src/server/websocket-server.ts @@ -1,6 +1,6 @@ import { WebSocket, WebSocketServer } from "ws"; import type { IncomingMessage, Server as HTTPServer } from "http"; -import { basename, join } from "path"; +import { join } from "path"; import { hostname as getHostname } from "node:os"; import { randomUUID } from "node:crypto"; import { monitorEventLoopDelay } from "node:perf_hooks"; @@ -10,6 +10,7 @@ import type { DownloadTokenStore } from "./file-download/token-store.js"; import type { TerminalManager } from "../terminal/terminal-manager.js"; import type pino from "pino"; import type { ProjectRegistry, WorkspaceRegistry } from "./workspace-registry.js"; +import type { ProjectUpdate } from "./workspace-reconciliation-service.js"; import type { FileBackedChatService } from "./chat/chat-service.js"; import type { LoopService } from "./loop-service.js"; import type { ScheduleService } from "./schedule/service.js"; @@ -35,7 +36,7 @@ import type { AgentProvider } from "./agent/agent-sdk-types.js"; import { ProviderSnapshotManager } from "./agent/provider-snapshot-manager.js"; import type { WorkspaceGitRuntimeSnapshot, WorkspaceGitService } from "./workspace-git-service.js"; import type { WorkspaceAutoName } from "./workspace-auto-name.js"; -import { buildWorkspaceGitMetadataFromSnapshot } from "./workspace-git-metadata.js"; +import { deriveProjectSlug } from "./workspace-git-metadata.js"; import { PushTokenStore } from "./push/token-store.js"; import { createPushNotificationSender, type PushNotificationSender } from "./push/notifications.js"; import type { ScriptHealthState } from "./script-health-monitor.js"; @@ -190,17 +191,9 @@ function createFallbackWorkspaceGitService(): WorkspaceGitService { suggestBranchesForCwd: async () => [], listStashes: async () => [], listWorktrees: async () => [], - getWorkspaceGitMetadata: async (cwd: string, options) => { + getProjectSlug: async (cwd: string) => { const snapshot = createFallbackWorkspaceGitSnapshot(cwd); - return buildWorkspaceGitMetadataFromSnapshot({ - cwd, - directoryName: options?.directoryName ?? basename(cwd), - isGit: snapshot.git.isGit, - repoRoot: snapshot.git.repoRoot, - mainRepoRoot: snapshot.git.mainRepoRoot, - currentBranch: snapshot.git.currentBranch, - remoteUrl: snapshot.git.remoteUrl, - }); + return deriveProjectSlug(cwd, snapshot.git.isGit ? snapshot.git.remoteUrl : null); }, resolveRepoRoot: async (cwd: string) => cwd, resolveDefaultBranch: async () => "main", @@ -223,6 +216,16 @@ function createNoopProjectRegistry(): ProjectRegistry { existsOnDisk: async () => true, list: async () => [], get: async () => null, + getOrCreateActiveByRoot: async (input) => ({ + projectId: "prj_noop", + rootPath: input.rootPath, + kind: input.kind, + displayName: input.displayName, + customName: null, + createdAt: input.timestamp, + updatedAt: input.timestamp, + archivedAt: null, + }), upsert: async () => {}, archive: async () => {}, remove: async () => {}, @@ -767,6 +770,10 @@ export class VoiceAssistantWebSocketServer { ); } + public publishProjectUpdate(update: ProjectUpdate): void { + for (const session of this.listActiveSessions()) session.emitProjectUpdate(update); + } + public publishSpeechReadiness(readiness: SpeechReadinessSnapshot | null): void { this.updateServerCapabilities(buildServerCapabilities({ readiness })); } @@ -1293,6 +1300,8 @@ export class VoiceAssistantWebSocketServer { forgeProviders: true, // COMPAT(selectiveAgentTimeline): added in v0.1.106, remove after 2027-01-12. selectiveAgentTimeline: true, + // COMPAT(stableProjectIdentity): added in v0.1.109, remove gate after 2027-01-15. + stableProjectIdentity: true, }, }; } diff --git a/packages/server/src/server/wire-compat.test.ts b/packages/server/src/server/wire-compat.test.ts index 55a8fceb3..3b131cec8 100644 --- a/packages/server/src/server/wire-compat.test.ts +++ b/packages/server/src/server/wire-compat.test.ts @@ -7,13 +7,17 @@ import { AgentSnapshotPayloadSchema, AgentTimelineItemPayloadSchema, FetchAgentTimelineResponseMessageSchema, + ServerInfoStatusPayloadSchema, SessionInboundMessageSchema, + SessionOutboundMessageSchema, type SessionOutboundMessage, + WSHelloMessageSchema, } from "@getpaseo/protocol/messages"; import { Session, type SessionOptions } from "./session.js"; import { createProviderSnapshotManagerStub } from "./test-utils/session-stubs.js"; import type { AgentTimelineRow } from "./agent/agent-manager.js"; import { handleCreatePaseoWorktreeRequest } from "./worktree-session.js"; +import { createPersistedProjectRecord } from "./workspace-registry.js"; const LegacyTimelineEntryPayloadSchema = z.object({ provider: z.enum(["claude", "codex", "opencode"]), @@ -288,8 +292,8 @@ function createSessionForWireCompatTest(options?: { async resolveRepoRemoteUrl() { return null; }, - async getWorkspaceGitMetadata() { - return null; + async getProjectSlug() { + return "project"; }, } as unknown as SessionOptions["workspaceGitService"], daemonConfigStore: @@ -326,6 +330,99 @@ async function emitTimelineResponse( } describe("wire compatibility", () => { + test("sends project updates only to clients that declare support", () => { + const project = createPersistedProjectRecord({ + projectId: "project-1", + rootPath: "/tmp/project", + kind: "git", + displayName: "project", + customName: "Favorite project", + createdAt: "2026-07-15T00:00:00.000Z", + updatedAt: "2026-07-15T00:00:00.000Z", + }); + const legacyMessages: SessionOutboundMessage[] = []; + const capableMessages: SessionOutboundMessage[] = []; + const legacy = createSessionForWireCompatTest({ messages: legacyMessages }); + const capable = createSessionForWireCompatTest({ + clientCapabilities: { [CLIENT_CAPS.projectUpdates]: true }, + messages: capableMessages, + }); + + legacy.emitProjectUpdate({ kind: "upsert", project }); + legacy.emitProjectUpdate({ kind: "remove", projectId: project.projectId }); + capable.emitProjectUpdate({ kind: "upsert", project }); + capable.emitProjectUpdate({ kind: "remove", projectId: project.projectId }); + + expect(legacyMessages).toEqual([]); + expect(capableMessages.map((message) => SessionOutboundMessageSchema.parse(message))).toEqual([ + { + type: "project.update", + payload: { + kind: "upsert", + project: { + projectId: "project-1", + projectDisplayName: "Favorite project", + projectCustomName: "Favorite project", + projectRootPath: "/tmp/project", + projectKind: "git", + }, + }, + }, + { + type: "project.update", + payload: { kind: "remove", projectId: "project-1" }, + }, + ]); + }); + + test("hello parses with and without the project update capability", () => { + const legacy = WSHelloMessageSchema.parse({ + type: "hello", + clientId: "legacy-client", + clientType: "mobile", + protocolVersion: 1, + }); + const capable = WSHelloMessageSchema.parse({ + type: "hello", + clientId: "capable-client", + clientType: "mobile", + protocolVersion: 1, + capabilities: { [CLIENT_CAPS.projectUpdates]: true }, + }); + + expect([legacy, capable]).toEqual([ + { + type: "hello", + clientId: "legacy-client", + clientType: "mobile", + protocolVersion: 1, + }, + { + type: "hello", + clientId: "capable-client", + clientType: "mobile", + protocolVersion: 1, + capabilities: { project_updates: true }, + }, + ]); + }); + + test("server info accepts legacy feature payloads without stable project identity", () => { + const parsed = ServerInfoStatusPayloadSchema.parse({ + status: "server_info", + serverId: "legacy-server", + features: { workspaceGithubClone: true }, + }); + + expect(parsed).toEqual({ + status: "server_info", + serverId: "legacy-server", + hostname: null, + version: null, + features: {}, + }); + }); + test("assistant timeline message ids are optional on the wire", () => { expect( AgentTimelineItemPayloadSchema.parse({ diff --git a/packages/server/src/server/workspace-archive-service.test.ts b/packages/server/src/server/workspace-archive-service.test.ts index 958e8b221..4adbadb60 100644 --- a/packages/server/src/server/workspace-archive-service.test.ts +++ b/packages/server/src/server/workspace-archive-service.test.ts @@ -1,11 +1,12 @@ import { execFileSync } from "node:child_process"; -import { existsSync, mkdirSync, mkdtempSync, rmSync } from "node:fs"; +import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; import pino, { type Logger } from "pino"; import { afterEach, describe, expect, test, vi } from "vitest"; import type { ForgeService } from "../services/forge-service.js"; +import { createRealpathAwarePathMatcher } from "../utils/path.js"; import { createWorktree, type WorktreeConfig } from "../utils/worktree.js"; import type { ManagedAgent } from "./agent/agent-manager.js"; import type { AgentStorage, StoredAgentRecord } from "./agent/agent-storage.js"; @@ -205,7 +206,6 @@ describe("archiveByScope", () => { }), { scope: { kind: "workspace", workspaceId }, - repoRoot: repoDir, requestId: "req-last-ref-workspace", }, ); @@ -217,8 +217,23 @@ describe("archiveByScope", () => { expect(existsSync(worktree.worktreePath)).toBe(false); }); - test("workspace scope keeps the directory when a sibling workspace still references it", async () => { + test("workspace scope runs teardown while keeping a directory referenced by a sibling", async () => { const { tempDir, repoDir } = createGitRepo(); + writeFileSync( + path.join(repoDir, "paseo.json"), + JSON.stringify({ + worktree: { + teardown: [ + "node -e \"require('fs').writeFileSync(process.env.PASEO_SOURCE_CHECKOUT_PATH + '/shared-teardown.log', 'ok')\"", + ], + }, + }), + ); + execFileSync("git", ["add", "."], { cwd: repoDir, stdio: "pipe" }); + execFileSync("git", ["-c", "commit.gpgsign=false", "commit", "-m", "shared teardown"], { + cwd: repoDir, + stdio: "pipe", + }); const paseoHome = path.join(tempDir, ".paseo"); const worktree = await createPaseoOwnedWorktree(repoDir, paseoHome, "sibling-workspace"); const workspaceA = "ws-sibling-a"; @@ -234,7 +249,6 @@ describe("archiveByScope", () => { }), { scope: { kind: "workspace", workspaceId: workspaceA }, - repoRoot: repoDir, requestId: "req-sibling-workspace", }, ); @@ -244,34 +258,228 @@ describe("archiveByScope", () => { removedDirectory: false, }); expect(existsSync(worktree.worktreePath)).toBe(true); + expect(readFileSync(path.join(repoDir, "shared-teardown.log"), "utf8")).toBe("ok"); }); - test("worktree scope archives every workspace on the directory and removes it", async () => { + test("workspace scope keeps a worktree for an active workspace in a subdirectory", async () => { const { tempDir, repoDir } = createGitRepo(); const paseoHome = path.join(tempDir, ".paseo"); - const worktree = await createPaseoOwnedWorktree(repoDir, paseoHome, "worktree-scope"); - const workspaceA = "ws-worktree-a"; - const workspaceB = "ws-worktree-b"; + const worktree = await createPaseoOwnedWorktree(repoDir, paseoHome, "subdirectory-sibling"); + const sourceWorkspaceId = "ws-subdirectory-source"; + const siblingWorkspaceId = "ws-subdirectory-sibling"; + const siblingDirectory = path.join(worktree.worktreePath, "packages", "app"); + mkdirSync(siblingDirectory, { recursive: true }); const result = await archiveByScope( createArchiveDeps({ paseoHome, activeWorkspaces: [ - { workspaceId: workspaceA, cwd: worktree.worktreePath, kind: "worktree" }, - { workspaceId: workspaceB, cwd: worktree.worktreePath, kind: "local_checkout" }, + { + workspaceId: sourceWorkspaceId, + cwd: worktree.worktreePath, + kind: "worktree", + worktreeRoot: worktree.worktreePath, + isPaseoOwnedWorktree: true, + }, + { + workspaceId: siblingWorkspaceId, + cwd: siblingDirectory, + kind: "worktree", + worktreeRoot: worktree.worktreePath, + isPaseoOwnedWorktree: true, + }, + ], + }), + { + scope: { kind: "workspace", workspaceId: sourceWorkspaceId }, + requestId: "req-subdirectory-sibling", + }, + ); + + assertArchiveResult(result, { + archivedWorkspaceIds: [sourceWorkspaceId], + removedDirectory: false, + }); + expect(existsSync(worktree.worktreePath)).toBe(true); + }); + + test("archiving a subdirectory workspace keeps its active worktree root", async () => { + const { tempDir, repoDir } = createGitRepo(); + const paseoHome = path.join(tempDir, ".paseo"); + const worktree = await createPaseoOwnedWorktree(repoDir, paseoHome, "subdirectory-target"); + const rootWorkspaceId = "ws-subdirectory-root"; + const subdirectoryWorkspaceId = "ws-subdirectory-target"; + const subdirectory = path.join(worktree.worktreePath, "packages", "app"); + mkdirSync(subdirectory, { recursive: true }); + + const result = await archiveByScope( + createArchiveDeps({ + paseoHome, + activeWorkspaces: [ + { + workspaceId: rootWorkspaceId, + cwd: worktree.worktreePath, + kind: "worktree", + worktreeRoot: worktree.worktreePath, + isPaseoOwnedWorktree: true, + }, + { + workspaceId: subdirectoryWorkspaceId, + cwd: subdirectory, + kind: "worktree", + worktreeRoot: worktree.worktreePath, + isPaseoOwnedWorktree: true, + }, + ], + }), + { + scope: { kind: "workspace", workspaceId: subdirectoryWorkspaceId }, + requestId: "req-subdirectory-target", + }, + ); + + assertArchiveResult(result, { + archivedWorkspaceIds: [subdirectoryWorkspaceId], + removedDirectory: false, + }); + expect(existsSync(worktree.worktreePath)).toBe(true); + }); + + test("workspace scope runs teardown from the exact nested workspace before deleting its worktree", async () => { + const { tempDir, repoDir } = createGitRepo(); + const nestedRelative = path.join("packages", "app"); + const sourceNested = path.join(repoDir, nestedRelative); + mkdirSync(sourceNested, { recursive: true }); + writeFileSync( + path.join(sourceNested, "paseo.json"), + JSON.stringify({ + worktree: { + teardown: [ + "node -e \"require('fs').writeFileSync(process.env.PASEO_SOURCE_CHECKOUT_PATH + '/nested-teardown.log', process.cwd())\"", + ], + }, + }), + ); + execFileSync("git", ["add", "."], { cwd: repoDir, stdio: "pipe" }); + execFileSync("git", ["-c", "commit.gpgsign=false", "commit", "-m", "nested teardown"], { + cwd: repoDir, + stdio: "pipe", + }); + + const paseoHome = path.join(tempDir, ".paseo"); + const worktree = await createPaseoOwnedWorktree(repoDir, paseoHome, "nested-teardown"); + const workspaceCwd = path.join(worktree.worktreePath, nestedRelative); + const matchesWorkspaceCwd = createRealpathAwarePathMatcher(workspaceCwd); + const workspaceId = "ws-nested-teardown"; + + const result = await archiveByScope( + createArchiveDeps({ + paseoHome, + activeWorkspaces: [ + { + workspaceId, + cwd: workspaceCwd, + kind: "worktree", + worktreeRoot: worktree.worktreePath, + isPaseoOwnedWorktree: true, + mainRepoRoot: repoDir, + }, + ], + }), + { + scope: { kind: "workspace", workspaceId }, + requestId: "req-nested-teardown", + }, + ); + + assertArchiveResult(result, { + archivedWorkspaceIds: [workspaceId], + removedDirectory: true, + }); + expect(existsSync(worktree.worktreePath)).toBe(false); + expect( + matchesWorkspaceCwd(readFileSync(path.join(repoDir, "nested-teardown.log"), "utf8")), + ).toBe(true); + }); + + test("worktree scope archives root and subdirectory workspaces before removing the backing worktree", async () => { + const { tempDir, repoDir } = createGitRepo(); + const nestedRelative = path.join("packages", "app"); + const sourceNested = path.join(repoDir, nestedRelative); + mkdirSync(sourceNested, { recursive: true }); + writeFileSync( + path.join(repoDir, "paseo.json"), + JSON.stringify({ + worktree: { + teardown: [ + "node -e \"const fs=require('fs');const out=process.env.PASEO_SOURCE_CHECKOUT_PATH+'/root-scope-teardown.log';if(fs.existsSync(out))process.exit(2);fs.writeFileSync(out,'ok')\"", + ], + }, + }), + ); + writeFileSync( + path.join(sourceNested, "paseo.json"), + JSON.stringify({ + worktree: { + teardown: [ + "node -e \"require('fs').writeFileSync(process.env.PASEO_SOURCE_CHECKOUT_PATH+'/nested-scope-teardown.log','ok')\"", + ], + }, + }), + ); + execFileSync("git", ["add", "."], { cwd: repoDir, stdio: "pipe" }); + execFileSync("git", ["-c", "commit.gpgsign=false", "commit", "-m", "scope teardown"], { + cwd: repoDir, + stdio: "pipe", + }); + const paseoHome = path.join(tempDir, ".paseo"); + const worktree = await createPaseoOwnedWorktree(repoDir, paseoHome, "worktree-scope"); + const workspaceA = "ws-worktree-a"; + const workspaceB = "ws-worktree-b"; + const workspaceC = "ws-worktree-subdirectory"; + const subdirectory = path.join(worktree.worktreePath, nestedRelative); + + const result = await archiveByScope( + createArchiveDeps({ + paseoHome, + activeWorkspaces: [ + { + workspaceId: workspaceA, + cwd: worktree.worktreePath, + kind: "worktree", + worktreeRoot: worktree.worktreePath, + isPaseoOwnedWorktree: true, + }, + { + workspaceId: workspaceB, + cwd: worktree.worktreePath, + kind: "worktree", + worktreeRoot: worktree.worktreePath, + isPaseoOwnedWorktree: true, + }, + { + workspaceId: workspaceC, + cwd: subdirectory, + kind: "worktree", + worktreeRoot: worktree.worktreePath, + isPaseoOwnedWorktree: true, + }, ], }), { scope: { kind: "worktree", targetPath: worktree.worktreePath }, - repoRoot: repoDir, requestId: "req-worktree-scope", }, ); - expect(result.archivedWorkspaceIds).toEqual(expect.arrayContaining([workspaceA, workspaceB])); - expect(result.archivedWorkspaceIds).toHaveLength(2); + expect(result.archivedWorkspaceIds).toEqual( + expect.arrayContaining([workspaceA, workspaceB, workspaceC]), + ); + expect(result.archivedWorkspaceIds).toHaveLength(3); expect(result.removedDirectory).toBe(true); expect(existsSync(worktree.worktreePath)).toBe(false); + expect(readFileSync(path.join(repoDir, "root-scope-teardown.log"), "utf8")).toBe("ok"); + expect(readFileSync(path.join(repoDir, "nested-scope-teardown.log"), "utf8")).toBe("ok"); }); test("workspace scope never removes a non-Paseo-owned directory", async () => { @@ -286,7 +494,6 @@ describe("archiveByScope", () => { }), { scope: { kind: "workspace", workspaceId }, - repoRoot: null, requestId: "req-local-checkout", }, ); @@ -322,7 +529,6 @@ describe("archiveByScope", () => { const result = await archiveByScope(deps, { scope: { kind: "worktree", targetPath: worktree.worktreePath }, - repoRoot: repoDir, requestId: "req-partial-failure", }); @@ -347,7 +553,6 @@ describe("archiveByScope", () => { const result = await archiveByScope(deps, { scope: { kind: "workspace", workspaceId: "ws-does-not-exist" }, - repoRoot: null, requestId: "req-unknown-workspace", }); @@ -372,7 +577,6 @@ describe("archiveByScope", () => { }), { scope: { kind: "worktree", targetPath: worktree.worktreePath }, - repoRoot: repoDir, requestId: "req-zero-records", }, ); @@ -452,7 +656,6 @@ describe("archiveByScope", () => { await archiveByScope(deps, { scope: { kind: "workspace", workspaceId }, - repoRoot: repoDir, requestId: "req-lifecycle", }); @@ -506,7 +709,6 @@ describe("archiveByScope", () => { const result = await archiveByScope(deps, { scope: { kind: "workspace", workspaceId: targetWorkspaceId }, - repoRoot: repoDir, requestId: "req-snapshot-scope", }); @@ -540,7 +742,6 @@ describe("archiveByScope", () => { }), { scope: { kind: "worktree", targetPath: worktree.worktreePath }, - repoRoot: repoDir, requestId: "req-worktree-scope-n3", }, ); diff --git a/packages/server/src/server/workspace-archive-service.ts b/packages/server/src/server/workspace-archive-service.ts index 0988f43d8..2d9a33fe2 100644 --- a/packages/server/src/server/workspace-archive-service.ts +++ b/packages/server/src/server/workspace-archive-service.ts @@ -9,22 +9,21 @@ import type { ForgeService } from "../services/forge-service.js"; import { deletePaseoWorktree, isPaseoOwnedWorktreeCwd, - resolvePaseoWorktreeRootForCwd, + runWorktreeTeardownCommands, WorktreeTeardownError, } from "../utils/worktree.js"; import type { TerminalManager } from "../terminal/terminal-manager.js"; import type { PersistedWorkspaceRecord, WorkspaceRegistry } from "./workspace-registry.js"; +import { createRealpathAwarePathMatcher } from "../utils/path.js"; -export interface ActiveWorkspaceRef { - workspaceId: string; - cwd: string; - kind?: "local_checkout" | "worktree" | "directory"; -} +export type ActiveWorkspaceRef = Pick< + PersistedWorkspaceRecord, + "workspaceId" | "cwd" | "kind" | "worktreeRoot" | "isPaseoOwnedWorktree" | "mainRepoRoot" +>; export interface ArchiveDependencies { paseoHome?: string; - // Base directory that may hold worktrees across repositories. Used as a fallback - // when the request does not supply a per-repo root. + // Base directory that may hold worktrees across repositories. paseoWorktreesBaseRoot?: string; github: ForgeService; workspaceGitService: Pick; @@ -65,22 +64,29 @@ export interface ArchiveResult { export interface ArchiveByScopeRequest { scope: ArchiveScope; - repoRoot: string | null; - // Per-repository worktree root, used to remove the actual directory. - repoWorktreesRoot?: string; - // Base directory that may hold worktrees across repositories; falls back to the - // dependency's base root for ownership checks and path resolution. - paseoWorktreesBaseRoot?: string; requestId: string; } +interface BackingDirectory { + path: string; + isPaseoOwnedWorktree: boolean; + mainRepoRoot: string | null; + paseoWorktreesRoot: string | null; +} + +interface ArchiveTarget { + backing: BackingDirectory | null; + teardownTargets: Array<{ workspaceId: string | null; cwd: string }>; + workspaceIds: string[]; +} + export async function resolveWorkspaceIdAtPath( dependencies: Pick, targetPath: string, ): Promise { - const targetDir = resolve(targetPath); + const matchesTarget = createRealpathAwarePathMatcher(targetPath); const activeWorkspaces = await dependencies.listActiveWorkspaces(); - const exactMatches = activeWorkspaces.filter((workspace) => resolve(workspace.cwd) === targetDir); + const exactMatches = activeWorkspaces.filter((workspace) => matchesTarget(workspace.cwd)); const worktreeMatch = exactMatches.find((workspace) => workspace.kind === "worktree"); if (worktreeMatch) { return worktreeMatch.workspaceId; @@ -88,18 +94,15 @@ export async function resolveWorkspaceIdAtPath( return dependencies.findWorkspaceIdForCwd(targetPath); } -// THE single archive entry. Resolves the in-scope record set, tears each down +// Resolves the in-scope record set, tears each down // (agents + terminals + record), then removes the backing directory iff it is // Paseo-owned AND no active workspace still references it. export async function archiveByScope( dependencies: ArchiveDependencies, request: ArchiveByScopeRequest, ): Promise { - const { targetDir, targetWorkspaceIds } = await resolveArchiveTargets( - dependencies, - request.scope, - request.paseoWorktreesBaseRoot, - ); + const target = await resolveArchiveTarget(dependencies, request.scope); + const targetWorkspaceIds = target.workspaceIds; if (targetWorkspaceIds.length > 0) { dependencies.markWorkspaceArchiving(targetWorkspaceIds, new Date().toISOString()); @@ -118,25 +121,25 @@ export async function archiveByScope( request.requestId, ); - if (request.repoRoot) { + if (target.backing?.mainRepoRoot) { try { - await dependencies.workspaceGitService.getSnapshot(request.repoRoot, { + await dependencies.workspaceGitService.getSnapshot(target.backing.mainRepoRoot, { force: true, reason: "archive-worktree", }); } catch (error) { dependencies.sessionLogger?.warn( - { err: error, cwd: request.repoRoot, requestId: request.requestId }, + { err: error, cwd: target.backing.mainRepoRoot, requestId: request.requestId }, "Failed to force-refresh workspace git snapshot after archiving", ); } } - if (targetDir !== null) { + if (target.backing !== null) { removedDirectory = await maybeRemoveDirectory( dependencies, request, - targetDir, + target, archivedWorkspaceIds, ); } @@ -154,11 +157,10 @@ export async function archiveByScope( } } -async function resolveArchiveTargets( +async function resolveArchiveTarget( dependencies: ArchiveDependencies, scope: ArchiveScope, - paseoWorktreesBaseRoot?: string, -): Promise<{ targetDir: string | null; targetWorkspaceIds: string[] }> { +): Promise { const activeWorkspaces = await dependencies.listActiveWorkspaces(); if (scope.kind === "workspace") { @@ -169,24 +171,89 @@ async function resolveArchiveTargets( { workspaceId }, "Workspace not found for archive-by-scope; skipping", ); - return { targetDir: null, targetWorkspaceIds: [] }; + return { backing: null, teardownTargets: [], workspaceIds: [] }; } - return { targetDir: resolve(record.cwd), targetWorkspaceIds: [workspaceId] }; + return { + backing: await resolveWorkspaceBackingDirectory(record, dependencies), + teardownTargets: [{ workspaceId, cwd: record.cwd }], + workspaceIds: [workspaceId], + }; } - let targetPath = scope.targetPath; - const resolvedWorktree = await resolvePaseoWorktreeRootForCwd(targetPath, { - paseoHome: dependencies.paseoHome, - worktreesRoot: paseoWorktreesBaseRoot ?? dependencies.paseoWorktreesBaseRoot, - }); - if (resolvedWorktree) { - targetPath = resolvedWorktree.worktreePath; + const backing = await resolveBackingDirectory(scope.targetPath, dependencies); + const matchesBackingDirectory = createRealpathAwarePathMatcher(backing.path); + const targetWorkspaces = ( + await Promise.all( + activeWorkspaces.map(async (workspace) => { + const backingDirectory = await resolveWorkspaceBackingDirectory(workspace, dependencies); + return matchesBackingDirectory(backingDirectory.path) ? workspace : null; + }), + ) + ).filter((workspace): workspace is ActiveWorkspaceRef => workspace !== null); + const persistedMainRepoRoot = targetWorkspaces.find( + (workspace) => workspace.mainRepoRoot, + )?.mainRepoRoot; + return { + backing: { + ...backing, + mainRepoRoot: persistedMainRepoRoot ?? backing.mainRepoRoot, + }, + teardownTargets: + targetWorkspaces.length > 0 + ? targetWorkspaces.map((workspace) => ({ + workspaceId: workspace.workspaceId, + cwd: workspace.cwd, + })) + : [{ workspaceId: null, cwd: scope.targetPath }], + workspaceIds: targetWorkspaces.map((workspace) => workspace.workspaceId), + }; +} + +async function resolveWorkspaceBackingDirectory( + workspace: ActiveWorkspaceRef, + dependencies: Pick, +): Promise { + if (workspace.isPaseoOwnedWorktree && workspace.worktreeRoot && workspace.mainRepoRoot) { + return { + path: resolve(workspace.worktreeRoot), + isPaseoOwnedWorktree: true, + mainRepoRoot: workspace.mainRepoRoot, + paseoWorktreesRoot: null, + }; } - const targetDir = resolve(targetPath); - const targetWorkspaceIds = activeWorkspaces - .filter((workspace) => resolve(workspace.cwd) === targetDir) - .map((workspace) => workspace.workspaceId); - return { targetDir, targetWorkspaceIds }; + if (workspace.kind !== "worktree") { + return { + path: resolve(workspace.cwd), + isPaseoOwnedWorktree: false, + mainRepoRoot: workspace.mainRepoRoot ?? null, + paseoWorktreesRoot: null, + }; + } + + // COMPAT(archiveMissingWorkspacePlacement): worktree records created before v0.1.110 + // lack durable backing ownership; remove filesystem discovery after 2027-01-17. + const backing = await resolveBackingDirectory( + workspace.worktreeRoot ?? workspace.cwd, + dependencies, + ); + return { ...backing, mainRepoRoot: workspace.mainRepoRoot ?? backing.mainRepoRoot }; +} + +async function resolveBackingDirectory( + cwd: string, + dependencies: Pick, +): Promise { + const options = { + paseoHome: dependencies.paseoHome, + worktreesRoot: dependencies.paseoWorktreesBaseRoot, + }; + const ownership = await isPaseoOwnedWorktreeCwd(cwd, options); + return { + path: resolve(ownership.allowed && ownership.worktreePath ? ownership.worktreePath : cwd), + isPaseoOwnedWorktree: ownership.allowed, + mainRepoRoot: ownership.repoRoot ?? null, + paseoWorktreesRoot: ownership.worktreeRoot ?? null, + }; } async function archiveTargetRecords( @@ -224,37 +291,72 @@ async function archiveTargetRecords( async function maybeRemoveDirectory( dependencies: ArchiveDependencies, - request: Omit, - targetDir: string, + request: Pick, + target: ArchiveTarget, archivedWorkspaceIds: string[], ): Promise { - const ownership = await isPaseoOwnedWorktreeCwd(targetDir, { - paseoHome: dependencies.paseoHome, - worktreesRoot: request.paseoWorktreesBaseRoot ?? dependencies.paseoWorktreesBaseRoot, - }); - if (!ownership.allowed) { + const backing = target.backing; + if (!backing?.isPaseoOwnedWorktree) { return false; } + const archivedWorkspaceIdSet = new Set(archivedWorkspaceIds); + const teardownCwds = uniqueFilesystemPaths( + target.teardownTargets + .filter( + (teardownTarget) => + teardownTarget.workspaceId === null || + archivedWorkspaceIdSet.has(teardownTarget.workspaceId), + ) + .map((teardownTarget) => teardownTarget.cwd), + ); + + try { + for (const teardownCwd of teardownCwds) { + await runWorktreeTeardownCommands({ + worktreePath: backing.path, + teardownCwd, + repoRootPath: backing.mainRepoRoot ?? undefined, + }); + } + } catch (error) { + if (error instanceof WorktreeTeardownError) { + dependencies.sessionLogger?.warn( + { err: error, targetPath: backing.path, requestId: request.requestId }, + "Worktree teardown failed during archive; workspace already archived", + ); + return false; + } + throw error; + } + const remainingActive = await dependencies.listActiveWorkspaces(); - if (!isDirectoryUnreferenced(remainingActive, targetDir, new Set(archivedWorkspaceIds))) { + if ( + !(await isDirectoryUnreferenced( + remainingActive, + backing.path, + new Set(archivedWorkspaceIds), + dependencies, + )) + ) { return false; } try { await deletePaseoWorktree({ - cwd: request.repoRoot, - worktreePath: targetDir, - worktreesRoot: request.repoWorktreesRoot ?? ownership.worktreeRoot, + cwd: backing.mainRepoRoot, + worktreePath: backing.path, + teardownCwds: [], + worktreesRoot: backing.paseoWorktreesRoot ?? undefined, paseoHome: dependencies.paseoHome, - worktreesBaseRoot: request.paseoWorktreesBaseRoot ?? dependencies.paseoWorktreesBaseRoot, + worktreesBaseRoot: dependencies.paseoWorktreesBaseRoot, }); - dependencies.github.invalidate({ cwd: targetDir }); + dependencies.github.invalidate({ cwd: backing.path }); return true; } catch (error) { if (error instanceof WorktreeTeardownError) { dependencies.sessionLogger?.warn( - { err: error, targetPath: targetDir, requestId: request.requestId }, + { err: error, targetPath: backing.path, requestId: request.requestId }, "Worktree disk removal failed during archive; workspace already archived", ); return false; @@ -263,6 +365,16 @@ async function maybeRemoveDirectory( } } +function uniqueFilesystemPaths(paths: string[]): string[] { + const unique: string[] = []; + for (const candidate of paths) { + if (!unique.some((existing) => createRealpathAwarePathMatcher(existing)(candidate))) { + unique.push(candidate); + } + } + return unique; +} + export type ArchiveWorkspaceContentsDependencies = Pick< ArchiveDependencies, "agentManager" | "agentStorage" | "killTerminalsForWorkspace" | "sessionLogger" @@ -323,19 +435,23 @@ export async function archiveWorkspaceContents( return archivedAgents; } -// EXACTLY one last-reference predicate in the module. True when, after archiving +// True when, after archiving // the in-scope records, no active workspace still points at targetDir. Derived // from records each call — no stored counter. -function isDirectoryUnreferenced( +async function isDirectoryUnreferenced( activeWorkspaces: ActiveWorkspaceRef[], targetDir: string, archivedWorkspaceIds: ReadonlySet, -): boolean { + dependencies: Pick, +): Promise { const target = resolve(targetDir); - return !activeWorkspaces.some( - (workspace) => - !archivedWorkspaceIds.has(workspace.workspaceId) && resolve(workspace.cwd) === target, - ); + const matchesTarget = createRealpathAwarePathMatcher(target); + for (const workspace of activeWorkspaces) { + if (archivedWorkspaceIds.has(workspace.workspaceId)) continue; + const backingDirectory = await resolveWorkspaceBackingDirectory(workspace, dependencies); + if (matchesTarget(backingDirectory.path)) return false; + } + return true; } export async function killTerminalsForWorkspace( diff --git a/packages/server/src/server/workspace-auto-name.ts b/packages/server/src/server/workspace-auto-name.ts index 2bf573620..558f2fac7 100644 --- a/packages/server/src/server/workspace-auto-name.ts +++ b/packages/server/src/server/workspace-auto-name.ts @@ -108,13 +108,14 @@ export class WorkspaceAutoName { firstAgentContext: FirstAgentContext; currentSelection: CurrentSelection; }): Promise { + const worktreeRoot = input.workspace.worktreeRoot ?? input.workspace.cwd; let generated: GeneratedWorkspaceName | null = null; const result: AttemptFirstAgentBranchAutoNameResult = await attemptFirstAgentBranchAutoName({ - cwd: input.workspace.cwd, + cwd: worktreeRoot, firstAgentContext: input.firstAgentContext, - generateBranchNameFromContext: ({ cwd, firstAgentContext }) => { + generateBranchNameFromContext: ({ firstAgentContext }) => { return this.generateFromContext({ - cwd, + cwd: input.workspace.cwd, firstAgentContext, currentSelection: input.currentSelection, }).then((nextGenerated) => { @@ -146,7 +147,7 @@ export class WorkspaceAutoName { promptTitle: resolveFirstAgentPromptTitle(input.firstAgentContext), }); if (result.renamed) { - await this.gitMutation.notifyGitMutation(input.workspace.cwd, "rename-branch"); + await this.gitMutation.notifyGitMutation(worktreeRoot, "rename-branch"); } await this.emitWorkspaceUpdateForCwd(input.workspace.cwd); } diff --git a/packages/server/src/server/workspace-directory.ts b/packages/server/src/server/workspace-directory.ts index 6e2193227..0e85f0dac 100644 --- a/packages/server/src/server/workspace-directory.ts +++ b/packages/server/src/server/workspace-directory.ts @@ -130,6 +130,17 @@ export function workspaceIdsOnCheckout( .map((workspace) => workspace.workspaceId); } +export function workspaceIdsForProjects( + workspaces: Iterable, + projectIds: ReadonlySet, +): string[] { + const workspaceIds = new Set(); + for (const workspace of workspaces) { + if (projectIds.has(workspace.projectId)) workspaceIds.add(workspace.workspaceId); + } + return Array.from(workspaceIds); +} + export class WorkspaceDirectory { private readonly archivingByWorkspaceId = new Map(); /** diff --git a/packages/server/src/server/workspace-git-metadata.test.ts b/packages/server/src/server/workspace-git-metadata.test.ts index d1a824b41..441246420 100644 --- a/packages/server/src/server/workspace-git-metadata.test.ts +++ b/packages/server/src/server/workspace-git-metadata.test.ts @@ -6,7 +6,7 @@ import path from "node:path"; import { afterEach, beforeEach, describe, expect, test } from "vitest"; import { - buildWorkspaceGitMetadataFromSnapshot, + deriveProjectServiceSlug, deriveProjectSlug, parseGitHubRepoNameFromRemote, } from "./workspace-git-metadata.js"; @@ -171,46 +171,12 @@ describe("deriveProjectSlug", () => { }); }); -describe("buildWorkspaceGitMetadataFromSnapshot", () => { - test("uses owner/repo as the display name for GitHub remotes", () => { - const result = buildWorkspaceGitMetadataFromSnapshot({ - cwd: "/repos/some-dir", - directoryName: "some-dir", - isGit: true, - repoRoot: "/repos/some-dir", - mainRepoRoot: null, - currentBranch: "main", - remoteUrl: "git@github.com:acme/widgets.git", - }); +describe("deriveProjectServiceSlug", () => { + test("keeps same-basename exact projects distinct and stable", () => { + const first = { projectId: "prj_aaaaaaaaaaaaaaaa", rootPath: "/repo-a/app" }; + const second = { projectId: "prj_bbbbbbbbbbbbbbbb", rootPath: "/repo-b/app" }; - expect(result.projectDisplayName).toBe("acme/widgets"); - }); - - test("uses owner/repo as the display name for non-GitHub remotes", () => { - const result = buildWorkspaceGitMetadataFromSnapshot({ - cwd: "/repos/random-name", - directoryName: "random-name", - isGit: true, - repoRoot: "/repos/random-name", - mainRepoRoot: null, - currentBranch: "main", - remoteUrl: "git@gitlab.com:acme/app.git", - }); - - expect(result.projectDisplayName).toBe("acme/app"); - }); - - test("falls back to the directory name when there is no remote", () => { - const result = buildWorkspaceGitMetadataFromSnapshot({ - cwd: "/repos/local-only", - directoryName: "local-only", - isGit: true, - repoRoot: "/repos/local-only", - mainRepoRoot: null, - currentBranch: "main", - remoteUrl: null, - }); - - expect(result.projectDisplayName).toBe("local-only"); + expect(deriveProjectServiceSlug(first)).toBe(deriveProjectServiceSlug(first)); + expect(deriveProjectServiceSlug(first)).not.toBe(deriveProjectServiceSlug(second)); }); }); diff --git a/packages/server/src/server/workspace-git-metadata.ts b/packages/server/src/server/workspace-git-metadata.ts index a2d53a380..dd38679f9 100644 --- a/packages/server/src/server/workspace-git-metadata.ts +++ b/packages/server/src/server/workspace-git-metadata.ts @@ -1,19 +1,7 @@ import { basename } from "path"; +import { createHash } from "node:crypto"; import { parseGitHubRemoteUrl } from "@getpaseo/protocol/git-remote"; import { slugify } from "../utils/worktree.js"; -import { deriveProjectGroupingKey, deriveProjectGroupingName } from "./workspace-registry-model.js"; - -export interface WorkspaceGitMetadata { - projectKind: "git" | "directory"; - projectDisplayName: string; - workspaceDisplayName: string; - gitRemote: string | null; - isWorktree: boolean; - projectSlug: string; - repoRoot: string | null; - currentBranch: string | null; - remoteUrl: string | null; -} export function parseGitHubRepoFromRemote(remoteUrl: string): string | null { return parseGitHubRemoteUrl(remoteUrl)?.repo ?? null; @@ -34,49 +22,7 @@ export function deriveProjectSlug(cwd: string, remoteUrl: string | null = null): return slugify(sourceName) || "untitled"; } -export function buildWorkspaceGitMetadataFromSnapshot(input: { - cwd: string; - directoryName: string; - isGit: boolean; - repoRoot: string | null; - mainRepoRoot: string | null; - currentBranch: string | null; - remoteUrl: string | null; -}): WorkspaceGitMetadata { - if (!input.isGit) { - return { - projectKind: "directory", - projectDisplayName: input.directoryName, - workspaceDisplayName: input.directoryName, - gitRemote: null, - isWorktree: false, - projectSlug: deriveProjectSlug(input.cwd), - repoRoot: null, - currentBranch: null, - remoteUrl: null, - }; - } - - const isWorktree = - input.mainRepoRoot !== null && input.repoRoot !== null && input.mainRepoRoot !== input.repoRoot; - const projectKey = deriveProjectGroupingKey({ - cwd: input.repoRoot ?? input.cwd, - remoteUrl: input.remoteUrl, - mainRepoRoot: input.mainRepoRoot, - }); - const projectDisplayName = projectKey.startsWith("remote:") - ? deriveProjectGroupingName(projectKey) - : input.directoryName; - - return { - projectKind: "git", - projectDisplayName, - workspaceDisplayName: input.currentBranch ?? input.directoryName, - gitRemote: input.remoteUrl, - isWorktree, - projectSlug: deriveProjectSlug(input.cwd, input.remoteUrl), - repoRoot: input.repoRoot, - currentBranch: input.currentBranch, - remoteUrl: input.remoteUrl, - }; +export function deriveProjectServiceSlug(project: { projectId: string; rootPath: string }): string { + const identity = createHash("sha256").update(project.projectId).digest("hex").slice(0, 8); + return `${deriveProjectSlug(project.rootPath)}-${identity}`; } diff --git a/packages/server/src/server/workspace-git-service.primitive.test.ts b/packages/server/src/server/workspace-git-service.primitive.test.ts index f3be87aa1..eb76955a3 100644 --- a/packages/server/src/server/workspace-git-service.primitive.test.ts +++ b/packages/server/src/server/workspace-git-service.primitive.test.ts @@ -390,6 +390,18 @@ describe("WorkspaceGitServiceImpl primitive refresh entrypoint", () => { vi.useRealTimers(); }); + test("getCheckout surfaces an unexpected Git read failure", async () => { + const service = createService({ + getCheckoutStatus: vi.fn(async () => { + throw new Error("Git read failed"); + }), + }); + + await expect(service.getCheckout(REPO_CWD)).rejects.toThrow("Git read failed"); + + service.dispose(); + }); + test("getSnapshot returns the current snapshot without shelling out", async () => { let nowMs = Date.parse("2026-04-12T00:00:00.000Z"); const getCheckoutStatus = vi.fn(async (cwd: string) => createCheckoutStatus(cwd)); @@ -1735,7 +1747,7 @@ describe("WorkspaceGitServiceImpl D2 read methods", () => { service.dispose(); }); - test("getWorkspaceGitMetadata derives reconciliation metadata from the snapshot cache", async () => { + test("getProjectSlug derives the slug from the snapshot cache", async () => { let nowMs = 0; const getCheckoutStatus = vi.fn(async (cwd: string) => createCheckoutStatus(cwd, { @@ -1749,22 +1761,10 @@ describe("WorkspaceGitServiceImpl D2 read methods", () => { now: () => new Date(nowMs), }); - await expect( - service.getWorkspaceGitMetadata(REPO_CWD, { directoryName: "Local Repo" }), - ).resolves.toEqual({ - projectKind: "git", - projectDisplayName: "getpaseo/paseo", - workspaceDisplayName: "feature/service-metadata", - gitRemote: "https://github.com/getpaseo/paseo.git", - isWorktree: false, - projectSlug: "paseo", - repoRoot: REPO_CWD, - currentBranch: "feature/service-metadata", - remoteUrl: "https://github.com/getpaseo/paseo.git", - }); + await expect(service.getProjectSlug(REPO_CWD)).resolves.toBe("paseo"); nowMs = 1_000; - await service.getWorkspaceGitMetadata(join(REPO_CWD, "."), { directoryName: "Local Repo" }); + await service.getProjectSlug(join(REPO_CWD, ".")); expect(getCheckoutStatus).toHaveBeenCalledTimes(1); service.dispose(); diff --git a/packages/server/src/server/workspace-git-service.ts b/packages/server/src/server/workspace-git-service.ts index 24edaf90c..a0c7818c5 100644 --- a/packages/server/src/server/workspace-git-service.ts +++ b/packages/server/src/server/workspace-git-service.ts @@ -1,6 +1,6 @@ import { watch, type FSWatcher } from "node:fs"; import { readFile, readdir } from "node:fs/promises"; -import { basename, join, resolve } from "node:path"; +import { join, resolve } from "node:path"; import { LRUCache } from "lru-cache"; import pLimit from "p-limit"; import type pino from "pino"; @@ -41,10 +41,7 @@ import { parseGitRevParsePath } from "../utils/git-rev-parse-path.js"; import { runGitCommand } from "../utils/run-git-command.js"; import { listPaseoWorktrees, type PaseoWorktreeInfo } from "../utils/worktree.js"; import { READ_ONLY_GIT_ENV } from "./checkout-git-utils.js"; -import { - buildWorkspaceGitMetadataFromSnapshot, - type WorkspaceGitMetadata, -} from "./workspace-git-metadata.js"; +import { deriveProjectSlug } from "./workspace-git-metadata.js"; import { checkoutLiteFromGitSnapshot } from "./workspace-registry-model.js"; const WORKSPACE_GIT_WATCH_DEBOUNCE_MS = 1_000; @@ -162,10 +159,7 @@ export interface WorkspaceGitService { cwdOrRepoRoot: string, options?: WorkspaceGitReadOptions, ): Promise; - getWorkspaceGitMetadata( - cwd: string, - options?: WorkspaceGitReadOptions & { directoryName?: string }, - ): Promise; + getProjectSlug(cwd: string, options?: WorkspaceGitReadOptions): Promise; resolveRepoRoot(cwd: string, options?: WorkspaceGitReadOptions): Promise; resolveDefaultBranch(cwdOrRepoRoot: string, options?: WorkspaceGitReadOptions): Promise; resolveRepoRemoteUrl(cwd: string, options?: WorkspaceGitReadOptions): Promise; @@ -473,31 +467,12 @@ export class WorkspaceGitServiceImpl implements WorkspaceGitService { async getCheckout(cwd: string): Promise { const normalizedCwd = resolve(cwd); - try { - const status = await this.deps.getCheckoutStatus(normalizedCwd, { - paseoHome: this.paseoHome, - worktreesRoot: this.worktreesRoot, - logger: this.logger, - }); - if (!status.isGit) { - return checkoutLiteFromGitSnapshot(normalizedCwd, { - isGit: false, - currentBranch: null, - remoteUrl: null, - repoRoot: null, - isPaseoOwnedWorktree: false, - mainRepoRoot: null, - }); - } - return checkoutLiteFromGitSnapshot(normalizedCwd, { - isGit: true, - currentBranch: status.currentBranch, - remoteUrl: status.remoteUrl, - repoRoot: status.repoRoot, - isPaseoOwnedWorktree: status.isPaseoOwnedWorktree, - mainRepoRoot: status.mainRepoRoot, - }); - } catch { + const status = await this.deps.getCheckoutStatus(normalizedCwd, { + paseoHome: this.paseoHome, + worktreesRoot: this.worktreesRoot, + logger: this.logger, + }); + if (!status.isGit) { return checkoutLiteFromGitSnapshot(normalizedCwd, { isGit: false, currentBranch: null, @@ -507,6 +482,14 @@ export class WorkspaceGitServiceImpl implements WorkspaceGitService { mainRepoRoot: null, }); } + return checkoutLiteFromGitSnapshot(normalizedCwd, { + isGit: true, + currentBranch: status.currentBranch, + remoteUrl: status.remoteUrl, + repoRoot: status.repoRoot, + isPaseoOwnedWorktree: status.isPaseoOwnedWorktree, + mainRepoRoot: status.mainRepoRoot, + }); } peekSnapshot(cwd: string): WorkspaceGitRuntimeSnapshot | null { @@ -654,21 +637,9 @@ export class WorkspaceGitServiceImpl implements WorkspaceGitService { }); } - async getWorkspaceGitMetadata( - cwd: string, - options?: WorkspaceGitReadOptions & { directoryName?: string }, - ): Promise { + async getProjectSlug(cwd: string, options?: WorkspaceGitReadOptions): Promise { const snapshot = await this.getSnapshot(cwd, options); - const directoryName = options?.directoryName ?? basename(cwd) ?? cwd; - return buildWorkspaceGitMetadataFromSnapshot({ - cwd: resolve(cwd), - directoryName, - isGit: snapshot.git.isGit, - repoRoot: snapshot.git.repoRoot, - mainRepoRoot: snapshot.git.mainRepoRoot, - currentBranch: snapshot.git.currentBranch, - remoteUrl: snapshot.git.remoteUrl, - }); + return deriveProjectSlug(resolve(cwd), snapshot.git.isGit ? snapshot.git.remoteUrl : null); } async resolveRepoRemoteUrl( diff --git a/packages/server/src/server/workspace-reconciliation-observation.test.ts b/packages/server/src/server/workspace-reconciliation-observation.test.ts new file mode 100644 index 000000000..a5fa20c97 --- /dev/null +++ b/packages/server/src/server/workspace-reconciliation-observation.test.ts @@ -0,0 +1,584 @@ +import { mkdirSync, mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import type { ProjectCheckoutLitePayload } from "@getpaseo/protocol/messages"; +import { afterEach, describe, expect, test } from "vitest"; + +import { createTestLogger } from "../test-utils/test-logger.js"; +import { areEquivalentPaths } from "../utils/path.js"; +import { + createPersistedProjectRecord, + createPersistedWorkspaceRecord, + FileBackedProjectRegistry, + FileBackedWorkspaceRegistry, + type PersistedWorkspaceRecord, +} from "./workspace-registry.js"; +import { + type ProjectRootWatch, + type ProjectUpdate, + type ReconciliationClock, + type ReconciliationTimer, + WorkspaceReconciliationService, +} from "./workspace-reconciliation-service.js"; + +const TIMESTAMP = "2026-07-15T00:00:00.000Z"; +const DEBOUNCE_MS = 10; +const RESCAN_INTERVAL_MS = 50; +const cleanupPaths: string[] = []; + +afterEach(() => { + for (const target of cleanupPaths.splice(0)) rmSync(target, { recursive: true, force: true }); +}); + +interface ProjectSpec { + id: string; + root: string; + workspaces?: Array<{ id: string; cwd: string }>; + archived?: boolean; +} + +interface Gate { + started: Promise; + release(): void; +} + +function createGate(): Gate & { arrive(): Promise } { + let markStarted!: () => void; + let release!: () => void; + const started = new Promise((resolve) => (markStarted = resolve)); + const released = new Promise((resolve) => (release = resolve)); + return { started, release, arrive: async () => (markStarted(), released) }; +} + +class ObservedProjectRegistry extends FileBackedProjectRegistry { + private nextRead: ReturnType | null = null; + + holdNextRead(): Gate { + return (this.nextRead = createGate()); + } + + override async list() { + if (this.nextRead) { + const pending = this.nextRead; + this.nextRead = null; + await pending.arrive(); + } + return super.list(); + } +} + +class ObservedWorkspaceRegistry extends FileBackedWorkspaceRegistry { + private nextRead: ReturnType | null = null; + private nextError: Error | null = null; + + holdNextRead(): Gate { + return (this.nextRead = createGate()); + } + + failNextRead(error: Error): void { + this.nextError = error; + } + + override async list() { + if (this.nextRead) { + const pending = this.nextRead; + this.nextRead = null; + await pending.arrive(); + } + if (this.nextError) { + const error = this.nextError; + this.nextError = null; + throw error; + } + return super.list(); + } +} + +interface TestTimer extends ReconciliationTimer { + callback: () => void | Promise; + dueAt: number; + intervalMs: number | null; +} + +class TestClock implements ReconciliationClock { + private now = 0; + private readonly timers = new Set(); + + get pendingCount(): number { + return this.timers.size; + } + + setTimeout(callback: () => void | Promise, delayMs: number): TestTimer { + return this.add(callback, delayMs, null); + } + + clearTimeout(timer: TestTimer): void { + this.timers.delete(timer); + } + + setInterval(callback: () => void | Promise, delayMs: number): TestTimer { + return this.add(callback, delayMs, delayMs); + } + + clearInterval(timer: TestTimer): void { + this.timers.delete(timer); + } + + async advanceBy(elapsedMs: number): Promise { + const target = this.now + elapsedMs; + for (;;) { + const next = [...this.timers] + .filter((timer) => timer.dueAt <= target) + .sort((left, right) => left.dueAt - right.dueAt)[0]; + if (!next) break; + this.now = next.dueAt; + if (next.intervalMs === null) this.timers.delete(next); + else next.dueAt += next.intervalMs; + await next.callback(); + } + this.now = target; + } + + private add( + callback: () => void | Promise, + delayMs: number, + intervalMs: number | null, + ): TestTimer { + const timer = { callback, dueAt: this.now + delayMs, intervalMs, unref: () => undefined }; + this.timers.add(timer); + return timer; + } +} + +interface RootWatch { + rootPath: string; + onChange: (event: string, filename: string | Buffer | null) => void; + onError: (error: Error) => void; + closed: boolean; +} + +/** One public behavioral seam for the complete observed-placement lifecycle. */ +class ObservedPlacements { + private readonly home = mkdtempSync(path.join(tmpdir(), "observed-placement-")); + private readonly projects: ObservedProjectRegistry; + private readonly workspaces: ObservedWorkspaceRegistry; + private readonly clock = new TestClock(); + private readonly watches: RootWatch[] = []; + private readonly checkoutByCwd = new Map(); + private readonly rootByProjectId = new Map(); + private readonly failedWatchRoots = new Set(); + private readonly projectEvents: ProjectUpdate[] = []; + private readonly workspaceEvents: string[][] = []; + private readonly workspaceEventWaiters: Array<() => void> = []; + private readonly lifecycleEvents: string[] = []; + private readonly service: WorkspaceReconciliationService; + private started = false; + private checkoutReadCount = 0; + + constructor(private readonly specs: ProjectSpec[]) { + cleanupPaths.push(this.home); + const logger = createTestLogger(); + this.projects = new ObservedProjectRegistry(path.join(this.home, "projects.json"), logger); + this.workspaces = new ObservedWorkspaceRegistry( + path.join(this.home, "workspaces.json"), + logger, + ); + const watchProjectRoot: ProjectRootWatch = (rootPath, _options, onChange, onError) => { + if (this.failedWatchRoots.delete(rootPath)) throw new Error("root unavailable"); + const watch = { rootPath, onChange, onError, closed: false }; + this.watches.push(watch); + this.lifecycleEvents.push(`watch installed:${path.relative(this.home, rootPath)}`); + return { close: () => (watch.closed = true) }; + }; + this.service = new WorkspaceReconciliationService({ + projectRegistry: this.projects, + workspaceRegistry: this.workspaces, + workspaceGitService: { getCheckout: async (cwd) => this.readCheckout(cwd) }, + logger, + watchProjectRoot, + clock: this.clock, + debounceMs: DEBOUNCE_MS, + rescanIntervalMs: RESCAN_INTERVAL_MS, + onProjectUpdate: (update) => { + this.projectEvents.push(update); + this.lifecycleEvents.push( + update.kind === "upsert" + ? `project published:upsert:${update.project.projectId}` + : `project published:remove:${update.projectId}`, + ); + }, + onWorkspacesChanged: async (workspaceIds) => { + this.workspaceEvents.push(workspaceIds); + this.workspaceEventWaiters.shift()?.(); + }, + }); + } + + async start(): Promise { + if (!this.started) { + for (const spec of this.specs) await this.seed(spec); + this.started = true; + } + await this.service.start(); + } + + async add(spec: ProjectSpec): Promise { + await this.seed(spec); + this.lifecycleEvents.push(`registry mutation resolved:${spec.id}`); + } + + async archive(projectId: string): Promise { + await this.projects.archive(projectId, TIMESTAMP); + } + + async remove(projectId: string): Promise { + await this.projects.remove(projectId); + } + + failNextWatch(root: string): void { + this.failedWatchRoots.add(this.absolute(root)); + } + + watcherFailed(root: string): void { + this.activeWatch(root)?.onError(new Error("watch failed")); + } + + change(root: string, filename: string | null): void { + this.activeWatch(root)?.onChange("rename", filename); + } + + makeProjectGit(projectId: string, branch = "main"): void { + const rootPath = this.rootByProjectId.get(projectId); + if (!rootPath) throw new Error(`Unknown project: ${projectId}`); + this.checkoutByCwd.set(rootPath, { + cwd: rootPath, + isGit: true, + currentBranch: branch, + remoteUrl: null, + worktreeRoot: rootPath, + isPaseoOwnedWorktree: false, + mainRepoRoot: null, + }); + } + + failNextReconciliation(error: Error): void { + this.workspaces.failNextRead(error); + } + + holdNextRegistryRead(): Gate { + return this.projects.holdNextRead(); + } + + holdNextReconciliation(): Gate { + return this.workspaces.holdNextRead(); + } + + async advanceBy(elapsedMs: number): Promise { + await this.clock.advanceBy(elapsedMs); + } + + dispose(): void { + this.service.dispose(); + } + + watchedRoots(): string[] { + return this.watches + .filter((watch) => !watch.closed) + .map((watch) => path.relative(this.home, watch.rootPath)); + } + + closedRoots(): string[] { + return this.watches + .filter((watch) => watch.closed) + .map((watch) => path.relative(this.home, watch.rootPath)); + } + + async placement(workspaceId: string): Promise { + return this.workspaces.get(workspaceId); + } + + async deleteWorkspaceDirectory(workspaceId: string): Promise { + const workspace = await this.workspaces.get(workspaceId); + if (!workspace) throw new Error(`Unknown workspace: ${workspaceId}`); + rmSync(workspace.cwd, { recursive: true, force: true }); + } + + get projectUpdates(): ProjectUpdate[] { + return [...this.projectEvents]; + } + + get workspaceBatches(): string[][] { + return this.workspaceEvents.map((batch) => [...batch]); + } + + waitForWorkspaceBatch(): Promise { + return new Promise((resolve) => this.workspaceEventWaiters.push(resolve)); + } + + get lifecycle(): string[] { + return [...this.lifecycleEvents]; + } + + get gitReads(): number { + return this.checkoutReadCount; + } + + get pendingTimers(): number { + return this.clock.pendingCount; + } + + private async seed(spec: ProjectSpec): Promise { + const rootPath = this.absolute(spec.root); + mkdirSync(rootPath, { recursive: true }); + this.rootByProjectId.set(spec.id, rootPath); + await this.projects.upsert( + createPersistedProjectRecord({ + projectId: spec.id, + rootPath, + kind: "non_git", + displayName: spec.id, + createdAt: TIMESTAMP, + updatedAt: TIMESTAMP, + archivedAt: spec.archived ? TIMESTAMP : null, + }), + ); + for (const workspace of spec.workspaces ?? []) { + const cwd = this.absolute(workspace.cwd); + mkdirSync(cwd, { recursive: true }); + await this.workspaces.upsert( + createPersistedWorkspaceRecord({ + workspaceId: workspace.id, + projectId: spec.id, + cwd, + kind: "directory", + displayName: `Durable ${workspace.id}`, + createdAt: TIMESTAMP, + updatedAt: TIMESTAMP, + }), + ); + } + } + + private async readCheckout(cwd: string): Promise { + this.checkoutReadCount += 1; + const configured = [...this.checkoutByCwd.entries()].find(([root]) => + areEquivalentPaths(root, cwd), + )?.[1]; + return ( + configured ?? { + cwd, + isGit: false, + currentBranch: null, + remoteUrl: null, + worktreeRoot: null, + isPaseoOwnedWorktree: false, + mainRepoRoot: null, + } + ); + } + + private activeWatch(root: string): RootWatch | undefined { + const target = this.absolute(root); + return this.watches.find( + (watch) => !watch.closed && areEquivalentPaths(watch.rootPath, target), + ); + } + + private absolute(relative: string): string { + return path.join(this.home, relative); + } +} + +describe("observed workspace placement", () => { + test("installs and publishes a new project before add resolves without Git feedback", async () => { + const observed = new ObservedPlacements([]); + await observed.start(); + + await observed.add({ id: "project-new", root: "new" }); + await observed.advanceBy(DEBOUNCE_MS); + + expect(observed.watchedRoots()).toEqual(["new"]); + expect(observed.projectUpdates).toEqual([ + { kind: "upsert", project: expect.objectContaining({ projectId: "project-new" }) }, + ]); + expect(observed.lifecycle).toEqual([ + "watch installed:new", + "project published:upsert:project-new", + "registry mutation resolved:project-new", + ]); + expect(observed.gitReads).toBe(0); + observed.dispose(); + }); + + test("deduplicates active root watches and tears them down on archive and remove", async () => { + const observed = new ObservedPlacements([ + { id: "project-one", root: "repo" }, + { id: "project-duplicate", root: "repo" }, + { id: "project-remove", root: "other" }, + { id: "project-archived", root: "archived", archived: true }, + ]); + + await observed.start(); + await observed.start(); + expect(observed.watchedRoots()).toEqual(["repo", "other"]); + + await observed.archive("project-one"); + expect(observed.watchedRoots()).toEqual(["repo", "other"]); + await observed.remove("project-duplicate"); + await observed.remove("project-remove"); + + expect(observed.watchedRoots()).toEqual([]); + expect(observed.closedRoots()).toEqual(["repo", "other"]); + expect(observed.projectUpdates).toEqual([ + { kind: "remove", projectId: "project-one" }, + { kind: "remove", projectId: "project-duplicate" }, + { kind: "remove", projectId: "project-remove" }, + ]); + observed.dispose(); + }); + + test("filters unrelated files and coalesces Git change bursts", async () => { + const observed = new ObservedPlacements([{ id: "project-one", root: "repo" }]); + await observed.start(); + + observed.change("repo", "README.md"); + await observed.advanceBy(DEBOUNCE_MS); + expect(observed.gitReads).toBe(0); + + observed.change("repo", ".git"); + observed.change("repo", ".git"); + observed.change("repo", null); + await observed.advanceBy(DEBOUNCE_MS); + expect(observed.gitReads).toBe(1); + observed.dispose(); + }); + + test("recovers errored and temporarily unavailable watchers on the periodic pass", async () => { + const observed = new ObservedPlacements([ + { id: "project-errored", root: "errored" }, + { id: "project-unavailable", root: "unavailable" }, + ]); + observed.failNextWatch("unavailable"); + await observed.start(); + expect(observed.watchedRoots()).toEqual(["errored"]); + + observed.watcherFailed("errored"); + expect(observed.watchedRoots()).toEqual([]); + await observed.advanceBy(RESCAN_INTERVAL_MS); + + expect(observed.watchedRoots()).toEqual(["errored", "unavailable"]); + expect(observed.closedRoots()).toEqual(["errored"]); + observed.dispose(); + }); + + test("archives missing workspace directories on the periodic pass", async () => { + const observed = new ObservedPlacements([ + { id: "project-one", root: "repo", workspaces: [{ id: "workspace-one", cwd: "repo" }] }, + ]); + await observed.start(); + await observed.deleteWorkspaceDirectory("workspace-one"); + + await observed.advanceBy(RESCAN_INTERVAL_MS); + + expect((await observed.placement("workspace-one"))?.archivedAt).toEqual(expect.any(String)); + expect(observed.workspaceBatches).toEqual([["workspace-one"]]); + observed.dispose(); + }); + + test("preserves a periodic full pass queued behind metadata reconciliation", async () => { + const observed = new ObservedPlacements([ + { id: "project-one", root: "repo", workspaces: [{ id: "workspace-one", cwd: "repo" }] }, + ]); + await observed.start(); + const metadataRead = observed.holdNextReconciliation(); + observed.change("repo", ".git"); + const metadataPass = observed.advanceBy(DEBOUNCE_MS); + await metadataRead.started; + await observed.deleteWorkspaceDirectory("workspace-one"); + const workspaceBatch = observed.waitForWorkspaceBatch(); + + await observed.advanceBy(RESCAN_INTERVAL_MS); + metadataRead.release(); + await metadataPass; + await workspaceBatch; + + expect((await observed.placement("workspace-one"))?.archivedAt).toEqual(expect.any(String)); + expect(observed.workspaceBatches).toEqual([["workspace-one"]]); + observed.dispose(); + }); + + test("contains a failed reconciliation and converges on the next change", async () => { + const observed = new ObservedPlacements([ + { id: "project-one", root: "repo", workspaces: [{ id: "workspace-one", cwd: "repo" }] }, + ]); + await observed.start(); + observed.makeProjectGit("project-one", "main"); + observed.failNextReconciliation(new Error("registry unavailable")); + + observed.change("repo", ".git"); + await observed.advanceBy(DEBOUNCE_MS); + expect((await observed.placement("workspace-one"))?.kind).toBe("directory"); + + observed.change("repo", ".git"); + await observed.advanceBy(DEBOUNCE_MS); + expect(await observed.placement("workspace-one")).toMatchObject({ + kind: "local_checkout", + branch: "main", + displayName: "Durable workspace-one", + }); + observed.dispose(); + }); + + test("deduplicates direct placement and project-derived workspace fanout", async () => { + const observed = new ObservedPlacements([ + { + id: "project-one", + root: "repo", + workspaces: [ + { id: "workspace-one", cwd: "repo" }, + { id: "workspace-two", cwd: "repo/feature" }, + ], + }, + ]); + await observed.start(); + observed.makeProjectGit("project-one", "main"); + + observed.change("repo", ".git"); + await observed.advanceBy(DEBOUNCE_MS); + + expect(observed.projectUpdates).toHaveLength(1); + expect(observed.workspaceBatches).toEqual([["workspace-one", "workspace-two"]]); + expect(new Set(observed.workspaceBatches[0]).size).toBe(2); + observed.dispose(); + }); + + test("disposal suppresses in-flight mutations and reconciliation fanout", async () => { + const mutation = new ObservedPlacements([{ id: "project-one", root: "repo" }]); + await mutation.start(); + const registryRead = mutation.holdNextRegistryRead(); + const adding = mutation.add({ id: "project-late", root: "late" }); + await registryRead.started; + mutation.dispose(); + registryRead.release(); + await adding; + expect(mutation.projectUpdates).toEqual([]); + expect(mutation.watchedRoots()).toEqual([]); + + const reconciliation = new ObservedPlacements([ + { id: "project-one", root: "repo", workspaces: [{ id: "workspace-one", cwd: "repo" }] }, + ]); + await reconciliation.start(); + reconciliation.makeProjectGit("project-one"); + const workspaceRead = reconciliation.holdNextReconciliation(); + reconciliation.change("repo", ".git"); + const advancing = reconciliation.advanceBy(DEBOUNCE_MS); + await workspaceRead.started; + reconciliation.dispose(); + workspaceRead.release(); + await advancing; + + expect(reconciliation.workspaceBatches).toEqual([]); + expect(reconciliation.watchedRoots()).toEqual([]); + expect(reconciliation.pendingTimers).toBe(0); + }); +}); diff --git a/packages/server/src/server/workspace-reconciliation-service.test.ts b/packages/server/src/server/workspace-reconciliation-service.test.ts index 556b0ccad..4cdebd624 100644 --- a/packages/server/src/server/workspace-reconciliation-service.test.ts +++ b/packages/server/src/server/workspace-reconciliation-service.test.ts @@ -2,8 +2,9 @@ import { execFileSync } from "node:child_process"; import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; +import type { ProjectCheckoutLitePayload } from "@getpaseo/protocol/messages"; import type pino from "pino"; -import { describe, expect, test, vi, afterEach } from "vitest"; +import { afterEach, describe, expect, test } from "vitest"; import { createPersistedProjectRecord, createPersistedWorkspaceRecord, @@ -14,7 +15,10 @@ import type { ProjectRegistry, WorkspaceRegistry, } from "./workspace-registry.js"; -import { WorkspaceReconciliationService } from "./workspace-reconciliation-service.js"; +import { + type ReconciliationChange, + WorkspaceReconciliationService, +} from "./workspace-reconciliation-service.js"; function createTestRegistries() { const projects = new Map(); @@ -25,6 +29,22 @@ function createTestRegistries() { existsOnDisk: async () => true, list: async () => Array.from(projects.values()), get: async (id: string) => projects.get(id) ?? null, + getOrCreateActiveByRoot: async (input) => { + const existing = Array.from(projects.values()).find( + (project) => !project.archivedAt && project.rootPath === input.rootPath, + ); + if (existing) return existing; + const record = createPersistedProjectRecord({ + projectId: `prj_${projects.size}`, + rootPath: input.rootPath, + kind: input.kind, + displayName: input.displayName, + createdAt: input.timestamp, + updatedAt: input.timestamp, + }); + projects.set(record.projectId, record); + return record; + }, upsert: async (record: PersistedProjectRecord) => { projects.set(record.projectId, record); }, @@ -64,11 +84,11 @@ function createTestRegistries() { function createTestLogger() { const logger = { child: () => logger, - trace: vi.fn(), - debug: vi.fn(), - info: vi.fn(), - warn: vi.fn(), - error: vi.fn(), + trace: () => undefined, + debug: () => undefined, + info: () => undefined, + warn: () => undefined, + error: () => undefined, }; return logger as unknown as pino.Logger; } @@ -106,35 +126,62 @@ function createWorkspaceGitServiceStub( >, ) { return { - getWorkspaceGitMetadata: vi.fn(async (cwd: string, options?: { directoryName?: string }) => { + getCheckout: async (cwd: string) => { const metadata = metadataByCwd[cwd]; - const directoryName = options?.directoryName ?? path.basename(cwd); if (!metadata) { return { - projectKind: "directory" as const, - projectDisplayName: directoryName, - workspaceDisplayName: directoryName, - gitRemote: null, - isWorktree: false, - projectSlug: "untitled", - repoRoot: null, + cwd, + isGit: false as const, currentBranch: null, remoteUrl: null, + worktreeRoot: null, + isPaseoOwnedWorktree: false, + mainRepoRoot: null, }; } return { - gitRemote: metadata.gitRemote ?? null, - isWorktree: false, - projectSlug: "repo", - repoRoot: cwd, - currentBranch: metadata.workspaceDisplayName, + cwd, + isGit: metadata.projectKind === "git", + currentBranch: metadata.currentBranch ?? metadata.workspaceDisplayName, remoteUrl: metadata.gitRemote ?? null, - ...metadata, + worktreeRoot: null, + isPaseoOwnedWorktree: false, + mainRepoRoot: null, }; - }), + }, }; } +function createCheckout( + cwd: string, + overrides: Partial = {}, +): ProjectCheckoutLitePayload { + return { + cwd, + isGit: false, + currentBranch: null, + remoteUrl: null, + worktreeRoot: null, + isPaseoOwnedWorktree: false, + mainRepoRoot: null, + ...overrides, + }; +} + +class TestCheckouts { + readonly reads: string[] = []; + private readonly checkouts = new Map(); + + set(cwd: string, checkout: ProjectCheckoutLitePayload): void { + this.checkouts.set(cwd, checkout); + } + + async getCheckout(cwd: string): Promise { + this.reads.push(cwd); + return this.checkouts.get(cwd) ?? createCheckout(cwd); + } +} + function initGitRepoInDir(dir: string): void { execFileSync("git", ["init", "-b", "main"], { cwd: dir, stdio: "ignore" }); execFileSync("git", ["config", "user.email", "test@test.com"], { cwd: dir, stdio: "ignore" }); @@ -169,6 +216,262 @@ describe("WorkspaceReconciliationService", () => { tempDirs.length = 0; }); + test("metadata reconciliation leaves missing workspaces active while a full pass archives them", async () => { + const projectRoot = realpathSync(mkdtempSync(path.join(tmpdir(), "reconcile-metadata-only-"))); + const missingWorkspace = path.join(projectRoot, "missing-workspace"); + tempDirs.push(projectRoot); + const { projects, workspaces, projectRegistry, workspaceRegistry } = createTestRegistries(); + + projects.set( + "p1", + createPersistedProjectRecord({ + projectId: "p1", + rootPath: projectRoot, + kind: "non_git", + displayName: "metadata-only", + createdAt: timestamp, + updatedAt: timestamp, + }), + ); + workspaces.set( + "w1", + createPersistedWorkspaceRecord({ + workspaceId: "w1", + projectId: "p1", + cwd: missingWorkspace, + kind: "directory", + displayName: "missing-workspace", + createdAt: timestamp, + updatedAt: timestamp, + }), + ); + const service = new WorkspaceReconciliationService({ + projectRegistry, + workspaceRegistry, + logger: createTestLogger(), + }); + + const metadataResult = await service.reconcileGitMetadata(); + + expect(metadataResult.changesApplied).toEqual([]); + expect(workspaces.get("w1")?.archivedAt).toBeNull(); + + const fullResult = await service.runOnce(); + + expect(fullResult.changesApplied).toEqual([ + { + kind: "workspace_archived", + workspaceId: "w1", + directory: missingWorkspace, + reason: "directory_missing", + }, + ]); + expect(workspaces.get("w1")?.archivedAt).toEqual(expect.any(String)); + }); + + test("reads fresh checkout facts on every metadata pass", async () => { + const projectRoot = realpathSync(mkdtempSync(path.join(tmpdir(), "reconcile-fresh-git-"))); + tempDirs.push(projectRoot); + const { projects, projectRegistry, workspaceRegistry } = createTestRegistries(); + const git = new TestCheckouts(); + git.set(projectRoot, createCheckout(projectRoot)); + projects.set( + "p1", + createPersistedProjectRecord({ + projectId: "p1", + rootPath: projectRoot, + kind: "non_git", + displayName: "fresh-git", + createdAt: timestamp, + updatedAt: timestamp, + }), + ); + const service = new WorkspaceReconciliationService({ + projectRegistry, + workspaceRegistry, + logger: createTestLogger(), + workspaceGitService: git, + }); + + const beforeGitInit = await service.reconcileGitMetadata(); + git.set( + projectRoot, + createCheckout(projectRoot, { + isGit: true, + currentBranch: "main", + worktreeRoot: projectRoot, + }), + ); + const afterGitInit = await service.reconcileGitMetadata(); + + expect(beforeGitInit.changesApplied).toEqual([]); + expect(afterGitInit.changesApplied).toEqual([ + { + kind: "project_updated", + projectId: "p1", + directory: projectRoot, + fields: { kind: "git" }, + }, + ]); + expect(git.reads).toEqual([projectRoot, projectRoot]); + expect(projects.get("p1")?.kind).toBe("git"); + }); + + test("deduplicates equivalent project and workspace paths across legacy duplicate projects", async () => { + const projectRoot = realpathSync(mkdtempSync(path.join(tmpdir(), "reconcile-global-root-"))); + const workspaceRoot = realpathSync( + mkdtempSync(path.join(tmpdir(), "reconcile-global-workspace-")), + ); + tempDirs.push(projectRoot, workspaceRoot); + const equivalentProjectRoot = `${projectRoot}${path.sep}.`; + const equivalentWorkspaceRoot = `${workspaceRoot}${path.sep}.`; + const { projects, workspaces, projectRegistry, workspaceRegistry } = createTestRegistries(); + const git = new TestCheckouts(); + const projectCheckout = createCheckout(projectRoot, { + isGit: true, + currentBranch: "main", + worktreeRoot: projectRoot, + }); + const workspaceCheckout = createCheckout(workspaceRoot, { + isGit: true, + currentBranch: "topic", + worktreeRoot: workspaceRoot, + }); + git.set(projectRoot, projectCheckout); + git.set(equivalentProjectRoot, projectCheckout); + git.set(workspaceRoot, workspaceCheckout); + git.set(equivalentWorkspaceRoot, workspaceCheckout); + + for (const [projectId, rootPath] of [ + ["p1", projectRoot], + ["p2", equivalentProjectRoot], + ] as const) { + projects.set( + projectId, + createPersistedProjectRecord({ + projectId, + rootPath, + kind: "git", + displayName: projectId, + createdAt: timestamp, + updatedAt: timestamp, + }), + ); + } + for (const [workspaceId, projectId, cwd] of [ + ["w1", "p1", workspaceRoot], + ["w2", "p2", equivalentWorkspaceRoot], + ] as const) { + workspaces.set( + workspaceId, + createPersistedWorkspaceRecord({ + workspaceId, + projectId, + cwd, + kind: "local_checkout", + displayName: workspaceId, + branch: "topic", + worktreeRoot: workspaceRoot, + createdAt: timestamp, + updatedAt: timestamp, + }), + ); + } + const service = new WorkspaceReconciliationService({ + projectRegistry, + workspaceRegistry, + logger: createTestLogger(), + workspaceGitService: git, + }); + + const result = await service.reconcileGitMetadata(); + + expect(result.changesApplied).toEqual([]); + expect(git.reads).toEqual([projectRoot, workspaceRoot]); + }); + + test("updates mutable Git facts without changing project or workspace identity", async () => { + const projectRoot = realpathSync(mkdtempSync(path.join(tmpdir(), "reconcile-stable-project-"))); + const workspaceRoot = realpathSync( + mkdtempSync(path.join(tmpdir(), "reconcile-explicit-workspace-")), + ); + tempDirs.push(projectRoot, workspaceRoot); + const { projects, workspaces, projectRegistry, workspaceRegistry } = createTestRegistries(); + const originalProject = createPersistedProjectRecord({ + projectId: "p1", + rootPath: projectRoot, + kind: "non_git", + displayName: "Stable project name", + customName: "Pinned project name", + createdAt: timestamp, + updatedAt: timestamp, + }); + const originalWorkspace = createPersistedWorkspaceRecord({ + workspaceId: "w1", + projectId: "p1", + cwd: workspaceRoot, + kind: "local_checkout", + displayName: "Stable workspace name", + title: "Pinned workspace name", + branch: "stale-branch", + baseBranch: "main", + createdAt: timestamp, + updatedAt: timestamp, + }); + projects.set(originalProject.projectId, originalProject); + workspaces.set(originalWorkspace.workspaceId, originalWorkspace); + const git = new TestCheckouts(); + git.set( + projectRoot, + createCheckout(projectRoot, { + isGit: true, + currentBranch: "main", + worktreeRoot: projectRoot, + }), + ); + git.set(workspaceRoot, createCheckout(workspaceRoot)); + const service = new WorkspaceReconciliationService({ + projectRegistry, + workspaceRegistry, + logger: createTestLogger(), + workspaceGitService: git, + }); + + const result = await service.reconcileGitMetadata(); + + expect(result.changesApplied).toEqual( + expect.arrayContaining([ + { + kind: "project_updated", + projectId: "p1", + directory: projectRoot, + fields: { kind: "git" }, + }, + { + kind: "workspace_updated", + workspaceId: "w1", + directory: workspaceRoot, + fields: { + branch: null, + kind: "directory", + }, + }, + ]), + ); + expect(result.changesApplied).toHaveLength(2); + expect(projects.get("p1")).toEqual({ + ...originalProject, + kind: "git", + updatedAt: expect.any(String), + }); + expect(workspaces.get("w1")).toEqual({ + ...originalWorkspace, + kind: "directory", + branch: null, + updatedAt: expect.any(String), + }); + }); + test("archives workspaces whose directories no longer exist", async () => { const { projects, workspaces, projectRegistry, workspaceRegistry } = createTestRegistries(); @@ -213,17 +516,15 @@ describe("WorkspaceReconciliationService", () => { test("keeps a project active after all its workspaces are archived", async () => { const { projects, workspaces, projectRegistry, workspaceRegistry } = createTestRegistries(); - projects.set( - "p1", - createPersistedProjectRecord({ - projectId: "p1", - rootPath: "/tmp/does-not-exist-reconcile-orphan", - kind: "non_git", - displayName: "orphan", - createdAt: timestamp, - updatedAt: timestamp, - }), - ); + const project = createPersistedProjectRecord({ + projectId: "p1", + rootPath: "/tmp/does-not-exist-reconcile-orphan", + kind: "non_git", + displayName: "orphan", + createdAt: timestamp, + updatedAt: timestamp, + }); + projects.set(project.projectId, project); workspaces.set( "w1", createPersistedWorkspaceRecord({ @@ -245,9 +546,32 @@ describe("WorkspaceReconciliationService", () => { const result = await service.runOnce(); - const projChange = result.changesApplied.find((c) => c.kind === "project_archived"); - expect(projChange).toBeUndefined(); - expect(projects.get("p1")!.archivedAt).toBeFalsy(); + expect(result.changesApplied).toEqual([ + { + kind: "workspace_archived", + workspaceId: "w1", + directory: "/tmp/does-not-exist-reconcile-orphan", + reason: "directory_missing", + }, + ]); + expect(workspaces.get("w1")).toEqual({ + workspaceId: "w1", + projectId: "p1", + cwd: "/tmp/does-not-exist-reconcile-orphan", + kind: "directory", + displayName: "orphan", + title: null, + pinnedAt: null, + branch: null, + worktreeRoot: null, + baseBranch: null, + isPaseoOwnedWorktree: false, + mainRepoRoot: null, + createdAt: timestamp, + updatedAt: expect.any(String), + archivedAt: expect.any(String), + }); + expect(projects.get("p1")).toEqual(project); }); test("updates project kind when a directory becomes a git repo", async () => { @@ -357,7 +681,7 @@ describe("WorkspaceReconciliationService", () => { expect(workspaces.get("w1")!.kind).toBe("local_checkout"); }); - test("moves workspaces from a path-keyed duplicate project to the existing remote-keyed project", async () => { + test("keeps legacy duplicate projects and workspace membership intact", async () => { const repoDir = createTempGitRepo("reconcile-duplicate-project-"); tempDirs.push(repoDir); const canonicalWorktreeDir = path.join(repoDir, ".paseo", "worktrees", "focused-bat"); @@ -442,33 +766,35 @@ describe("WorkspaceReconciliationService", () => { const result = await service.runOnce(); - expect(result.changesApplied).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - kind: "workspace_updated", - workspaceId: "gigantic-blowfish", - fields: { projectId: "remote:github.com/blank-dot-page/editor" }, - }), - expect.objectContaining({ - kind: "project_updated", - projectId: "remote:github.com/blank-dot-page/editor", - fields: { customName: "Editor" }, - }), - expect.objectContaining({ - kind: "project_archived", - projectId: repoDir, - reason: "merged_duplicate", - }), - ]), - ); - expect(workspaces.get("gigantic-blowfish")!.projectId).toBe( - "remote:github.com/blank-dot-page/editor", - ); - expect(projects.get("remote:github.com/blank-dot-page/editor")!.customName).toBe("Editor"); - expect(projects.get(repoDir)!.archivedAt).toBeTruthy(); + expect(result.changesApplied.map((change) => change.kind).sort()).toEqual([ + "workspace_updated", + "workspace_updated", + ]); + expect(projects.get("remote:github.com/blank-dot-page/editor")).toMatchObject({ + projectId: "remote:github.com/blank-dot-page/editor", + rootPath: repoDir, + displayName: "blank-dot-page/editor", + customName: null, + archivedAt: null, + }); + expect(projects.get(repoDir)).toMatchObject({ + projectId: repoDir, + rootPath: repoDir, + displayName: "editor", + customName: "Editor", + archivedAt: null, + }); + expect(workspaces.get("focused-bat")).toMatchObject({ + projectId: "remote:github.com/blank-dot-page/editor", + archivedAt: null, + }); + expect(workspaces.get("gigantic-blowfish")).toMatchObject({ + projectId: repoDir, + archivedAt: null, + }); }); - test("updates project display name when git remote changes", async () => { + test("keeps project display name stable when git remote changes", async () => { const dir = createTempGitRepo("reconcile-remote-"); tempDirs.push(dir); @@ -520,12 +846,11 @@ describe("WorkspaceReconciliationService", () => { const result = await service.runOnce(); - const projUpdate = result.changesApplied.find((c) => c.kind === "project_updated"); - expect(projUpdate).toBeDefined(); - expect(projects.get("p1")!.displayName).toBe("new-owner/new-repo"); + expect(result.changesApplied.find((c) => c.kind === "project_updated")).toBeUndefined(); + expect(projects.get("p1")!.displayName).toBe("old-owner/old-repo"); }); - test("preserves customName even when the derived displayName changes", async () => { + test("keeps custom and default names stable when the remote changes", async () => { const dir = createTempGitRepo("reconcile-customname-"); tempDirs.push(dir); @@ -577,10 +902,151 @@ describe("WorkspaceReconciliationService", () => { await service.runOnce(); - expect(projects.get("p1")!.displayName).toBe("new-owner/new-repo"); + expect(projects.get("p1")!.displayName).toBe("old-owner/old-repo"); expect(projects.get("p1")!.customName).toBe("My Fork"); }); + test("keeps persisted Git metadata when a workspace checkout read fails", async () => { + const projectRoot = mkdtempSync(path.join(tmpdir(), "reconcile-checkout-read-project-")); + const workspaceRoot = path.join(projectRoot, "workspace"); + mkdirSync(workspaceRoot); + tempDirs.push(projectRoot); + const { projects, workspaces, projectRegistry, workspaceRegistry } = createTestRegistries(); + const project = createPersistedProjectRecord({ + projectId: "p1", + rootPath: projectRoot, + kind: "non_git", + displayName: "project", + createdAt: timestamp, + updatedAt: timestamp, + }); + const workspace = createPersistedWorkspaceRecord({ + workspaceId: "w1", + projectId: project.projectId, + cwd: workspaceRoot, + kind: "local_checkout", + displayName: "workspace", + branch: "feature", + createdAt: timestamp, + updatedAt: timestamp, + }); + projects.set(project.projectId, project); + workspaces.set(workspace.workspaceId, workspace); + const service = new WorkspaceReconciliationService({ + projectRegistry, + workspaceRegistry, + logger: createTestLogger(), + workspaceGitService: { + getCheckout: async (cwd) => { + if (cwd === workspaceRoot) throw new Error("Git read failed"); + return createCheckout(cwd, { isGit: true, currentBranch: "main", worktreeRoot: cwd }); + }, + }, + }); + + const result = await service.reconcileGitMetadata(); + + expect(result.changesApplied).toEqual([]); + expect(projects.get(project.projectId)).toEqual(project); + expect(workspaces.get(workspace.workspaceId)).toEqual(workspace); + }); + + test("archives non-directory workspaces without blocking sibling reconciliation", async () => { + const projectRoot = mkdtempSync(path.join(tmpdir(), "reconcile-file-workspace-")); + const replacedWorkspace = path.join(projectRoot, "replaced-workspace"); + const siblingWorkspace = path.join(projectRoot, "sibling-workspace"); + writeFileSync(replacedWorkspace, "not a directory\n"); + mkdirSync(siblingWorkspace); + tempDirs.push(projectRoot); + + const { projects, workspaces, projectRegistry, workspaceRegistry } = createTestRegistries(); + projects.set( + "p1", + createPersistedProjectRecord({ + projectId: "p1", + rootPath: projectRoot, + kind: "non_git", + displayName: "project", + createdAt: timestamp, + updatedAt: timestamp, + }), + ); + workspaces.set( + "replaced", + createPersistedWorkspaceRecord({ + workspaceId: "replaced", + projectId: "p1", + cwd: replacedWorkspace, + kind: "directory", + displayName: "replaced-workspace", + createdAt: timestamp, + updatedAt: timestamp, + }), + ); + workspaces.set( + "sibling", + createPersistedWorkspaceRecord({ + workspaceId: "sibling", + projectId: "p1", + cwd: siblingWorkspace, + kind: "directory", + displayName: "sibling-workspace", + createdAt: timestamp, + updatedAt: timestamp, + }), + ); + + const service = new WorkspaceReconciliationService({ + projectRegistry, + workspaceRegistry, + logger: createTestLogger(), + workspaceGitService: { + getCheckout: async (cwd) => { + if (cwd === replacedWorkspace) { + throw new Error("Git cannot use a regular file as cwd"); + } + return createCheckout(cwd, { + isGit: true, + currentBranch: cwd === siblingWorkspace ? "feature/sibling" : "main", + worktreeRoot: cwd, + }); + }, + }, + }); + + const result = await service.runOnce(); + + expect(result.changesApplied).toEqual([ + { + kind: "workspace_archived", + workspaceId: "replaced", + directory: replacedWorkspace, + reason: "directory_missing", + }, + { + kind: "project_updated", + projectId: "p1", + directory: projectRoot, + fields: { kind: "git" }, + }, + { + kind: "workspace_updated", + workspaceId: "sibling", + directory: siblingWorkspace, + fields: { + kind: "local_checkout", + branch: "feature/sibling", + worktreeRoot: siblingWorkspace, + }, + }, + ]); + expect(workspaces.get("replaced")?.archivedAt).toEqual(expect.any(String)); + expect(workspaces.get("sibling")).toMatchObject({ + kind: "local_checkout", + branch: "feature/sibling", + }); + }); + test("updates workspace branch metadata without clobbering the workspace name", async () => { const dir = createTempGitRepo("reconcile-branch-"); tempDirs.push(dir); @@ -707,18 +1173,24 @@ describe("WorkspaceReconciliationService", () => { }), ); - const onChanges = vi.fn(); + const reportedChanges: ReconciliationChange[] = []; const service = new WorkspaceReconciliationService({ projectRegistry, workspaceRegistry, logger: createTestLogger(), - onChanges, + onChanges: (changes) => reportedChanges.push(...changes), }); await service.runOnce(); - expect(onChanges).toHaveBeenCalledTimes(1); - expect(onChanges.mock.calls[0][0].length).toBeGreaterThan(0); + expect(reportedChanges).toEqual([ + { + kind: "workspace_archived", + workspaceId: "w1", + directory: "/tmp/does-not-exist-callback-test", + reason: "directory_missing", + }, + ]); }); test("logs reconciliation changes with affected paths and reasons", async () => { @@ -791,4 +1263,69 @@ describe("WorkspaceReconciliationService", () => { expect(infoRecords).toEqual([]); }); + + test("backfills persisted worktree ownership from the current checkout", async () => { + const rootPath = realpathSync(mkdtempSync(path.join(tmpdir(), "reconcile-worktree-owner-"))); + tempDirs.push(rootPath); + const { projects, workspaces, projectRegistry, workspaceRegistry } = createTestRegistries(); + const checkouts = new TestCheckouts(); + checkouts.set( + rootPath, + createCheckout(rootPath, { + isGit: true, + worktreeRoot: rootPath, + isPaseoOwnedWorktree: true, + mainRepoRoot: "/tmp/main-repo", + }), + ); + projects.set( + "p1", + createPersistedProjectRecord({ + projectId: "p1", + rootPath, + kind: "git", + displayName: "worktree", + createdAt: timestamp, + updatedAt: timestamp, + }), + ); + workspaces.set( + "w1", + createPersistedWorkspaceRecord({ + workspaceId: "w1", + projectId: "p1", + cwd: rootPath, + kind: "worktree", + displayName: "worktree", + createdAt: timestamp, + updatedAt: timestamp, + }), + ); + const service = new WorkspaceReconciliationService({ + projectRegistry, + workspaceRegistry, + workspaceGitService: checkouts, + logger: createTestLogger(), + }); + + const result = await service.reconcileGitMetadata(); + + expect(result.changesApplied).toEqual([ + { + kind: "workspace_updated", + workspaceId: "w1", + directory: rootPath, + fields: { + worktreeRoot: rootPath, + isPaseoOwnedWorktree: true, + mainRepoRoot: "/tmp/main-repo", + }, + }, + ]); + expect(workspaces.get("w1")).toMatchObject({ + worktreeRoot: rootPath, + isPaseoOwnedWorktree: true, + mainRepoRoot: "/tmp/main-repo", + }); + }); }); diff --git a/packages/server/src/server/workspace-reconciliation-service.ts b/packages/server/src/server/workspace-reconciliation-service.ts index acf1cd650..28ca9e219 100644 --- a/packages/server/src/server/workspace-reconciliation-service.ts +++ b/packages/server/src/server/workspace-reconciliation-service.ts @@ -1,5 +1,5 @@ -import { existsSync } from "node:fs"; -import { resolve } from "node:path"; +import { statSync, watch as watchPath } from "node:fs"; +import type { ProjectCheckoutLitePayload } from "@getpaseo/protocol/messages"; import type pino from "pino"; import type { ProjectRegistry, @@ -8,49 +8,71 @@ import type { PersistedWorkspaceRecord, } from "./workspace-registry.js"; import type { WorkspaceGitService } from "./workspace-git-service.js"; +import { areEquivalentPaths } from "../utils/path.js"; +import { + deriveProjectKind, + reconcileWorkspacePlacement, + type MutableWorkspacePlacement, +} from "./workspace-registry-model.js"; +import { workspaceIdsForProjects } from "./workspace-directory.js"; -const DEFAULT_RECONCILE_INTERVAL_MS = 60_000; +const DEFAULT_RESCAN_INTERVAL_MS = 5 * 60_000; +const DEFAULT_DEBOUNCE_MS = 100; -function deriveWorkspaceKindFromMetadata(metadata: { - projectKind: "git" | "directory"; - isWorktree: boolean; -}): PersistedWorkspaceRecord["kind"] { - if (metadata.projectKind !== "git") return "directory"; - if (metadata.isWorktree) return "worktree"; - return "local_checkout"; +export type ProjectUpdate = + | { kind: "upsert"; project: PersistedProjectRecord } + | { kind: "remove"; projectId: string }; + +interface ProjectRootWatcher { + close(): void; } -function chooseCanonicalProject(projects: PersistedProjectRecord[]): PersistedProjectRecord { - return [...projects].sort((left, right) => { - const leftRemote = left.projectId.startsWith("remote:"); - const rightRemote = right.projectId.startsWith("remote:"); - if (leftRemote !== rightRemote) { - return leftRemote ? -1 : 1; - } - const createdAt = Date.parse(left.createdAt) - Date.parse(right.createdAt); - if (createdAt !== 0) { - return createdAt; - } - return left.projectId.localeCompare(right.projectId); - })[0]!; +export interface ProjectRootWatch { + ( + rootPath: string, + options: { recursive: false }, + onChange: (event: string, filename: string | Buffer | null) => void, + onError: (error: Error) => void, + ): ProjectRootWatcher; } +export interface ReconciliationTimer { + unref?(): void; +} + +export interface ReconciliationClock { + setTimeout(callback: () => void | Promise, delayMs: number): ReconciliationTimer; + clearTimeout(timer: ReconciliationTimer): void; + setInterval(callback: () => void | Promise, delayMs: number): ReconciliationTimer; + clearInterval(timer: ReconciliationTimer): void; +} + +const systemClock: ReconciliationClock = { + setTimeout: (callback, delayMs) => setTimeout(() => void callback(), delayMs), + clearTimeout: (timer) => clearTimeout(timer as ReturnType), + setInterval: (callback, delayMs) => setInterval(() => void callback(), delayMs), + clearInterval: (timer) => clearInterval(timer as ReturnType), +}; + +const watchProjectRoot: ProjectRootWatch = (rootPath, options, onChange, onError) => { + const watcher = watchPath(rootPath, options, onChange); + watcher.on("error", onError); + return watcher; +}; + export type ReconciliationChange = | { kind: "workspace_archived"; workspaceId: string; directory: string; reason: string } - | { kind: "project_archived"; projectId: string; directory: string; reason: string } | { kind: "project_updated"; projectId: string; directory: string; - fields: Partial< - Pick - >; + fields: Partial>; } | { kind: "workspace_updated"; workspaceId: string; directory: string; - fields: Partial>; + fields: Partial; }; export interface ReconciliationResult { @@ -62,62 +84,130 @@ export interface WorkspaceReconciliationServiceOptions { projectRegistry: ProjectRegistry; workspaceRegistry: WorkspaceRegistry; logger: pino.Logger; - intervalMs?: number; onChanges?: (changes: ReconciliationChange[]) => void; - workspaceGitService?: Pick; + workspaceGitService?: Pick; + onProjectUpdate?: (update: ProjectUpdate) => void; + onWorkspacesChanged?: (workspaceIds: string[]) => Promise; + watchProjectRoot?: ProjectRootWatch; + clock?: ReconciliationClock; + rescanIntervalMs?: number; + debounceMs?: number; } +interface ProjectReconciliationInput { + project: PersistedProjectRecord; + siblings: PersistedWorkspaceRecord[]; + currentGit: ProjectCheckoutLitePayload; + readCheckout: (cwd: string) => Promise; + changes: ReconciliationChange[]; +} + +interface CachedCheckoutRead { + cwd: string; + checkout: Promise; +} + +type DirectoryState = "directory" | "missing" | "unreadable"; + export class WorkspaceReconciliationService { private readonly projectRegistry: ProjectRegistry; private readonly workspaceRegistry: WorkspaceRegistry; private readonly logger: pino.Logger; - private readonly intervalMs: number; private readonly onChanges: ((changes: ReconciliationChange[]) => void) | null; - private readonly workspaceGitService: Pick | null; - private timer: ReturnType | null = null; - private running = false; + private readonly workspaceGitService: Pick | null; + private readonly onProjectUpdate: ((update: ProjectUpdate) => void) | null; + private readonly onWorkspacesChanged: ((workspaceIds: string[]) => Promise) | null; + private readonly watchProjectRoot: ProjectRootWatch; + private readonly clock: ReconciliationClock; + private readonly rescanIntervalMs: number; + private readonly debounceMs: number; + private readonly watchers: Array<{ rootPath: string; watcher: ProjectRootWatcher }> = []; + private unsubscribeRegistry: (() => void) | null = null; + private rescanTimer: ReconciliationTimer | null = null; + private debounceTimer: ReconciliationTimer | null = null; + private disposed = false; + private started = false; + private reconciling = false; + private reconcileQueuedMode: "metadata" | "full" | null = null; constructor(options: WorkspaceReconciliationServiceOptions) { this.projectRegistry = options.projectRegistry; this.workspaceRegistry = options.workspaceRegistry; this.logger = options.logger.child({ module: "workspace-reconciliation" }); - this.intervalMs = options.intervalMs ?? DEFAULT_RECONCILE_INTERVAL_MS; this.onChanges = options.onChanges ?? null; this.workspaceGitService = options.workspaceGitService ?? null; + this.onProjectUpdate = options.onProjectUpdate ?? null; + this.onWorkspacesChanged = options.onWorkspacesChanged ?? null; + this.watchProjectRoot = options.watchProjectRoot ?? watchProjectRoot; + this.clock = options.clock ?? systemClock; + this.rescanIntervalMs = options.rescanIntervalMs ?? DEFAULT_RESCAN_INTERVAL_MS; + this.debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS; } - start(): void { - if (this.timer) return; - this.logger.info({ intervalMs: this.intervalMs }, "Starting workspace reconciliation service"); - this.timer = setInterval(() => void this.runSafe(), this.intervalMs); - // Run once immediately on start - void this.runSafe(); + async start(): Promise { + if (this.started) return; + this.started = true; + this.unsubscribeRegistry = + this.projectRegistry.subscribeToMutations?.(async (mutation) => { + try { + // Project creation does not resolve until its root watch is installed, + // closing the git-init race for newly added empty projects. + await this.syncProjectRootWatches(); + if (this.disposed) return; + if (mutation.kind === "upsert" && mutation.project && !mutation.project.archivedAt) { + this.onProjectUpdate?.({ kind: "upsert", project: mutation.project }); + } else { + this.onProjectUpdate?.({ kind: "remove", projectId: mutation.projectId }); + } + } catch (error) { + this.logger.warn({ err: error }, "Project reconciliation mutation handling failed"); + } + }) ?? null; + await this.syncProjectRootWatches(); + this.rescanTimer = this.clock.setInterval( + () => this.reconcileObservedGitMetadata("full"), + this.rescanIntervalMs, + ); + this.rescanTimer.unref?.(); } - stop(): void { - if (this.timer) { - clearInterval(this.timer); - this.timer = null; + dispose(): void { + this.disposed = true; + this.unsubscribeRegistry?.(); + this.unsubscribeRegistry = null; + if (this.rescanTimer) this.clock.clearInterval(this.rescanTimer); + if (this.debounceTimer) this.clock.clearTimeout(this.debounceTimer); + for (const { watcher } of this.watchers) watcher.close(); + this.watchers.length = 0; + } + + /** Reconciles mutable Git facts only; never archives missing records. */ + async reconcileGitMetadata(): Promise { + const start = Date.now(); + const changes: ReconciliationChange[] = []; + const [projects, workspaces] = await Promise.all([ + this.projectRegistry.list(), + this.workspaceRegistry.list(), + ]); + const workspacesByProject = new Map(); + for (const workspace of workspaces) { + if (workspace.archivedAt || this.inspectDirectory(workspace.cwd) !== "directory") continue; + const siblings = workspacesByProject.get(workspace.projectId) ?? []; + siblings.push(workspace); + workspacesByProject.set(workspace.projectId, siblings); } + await this.reconcileGitMetadataForProjects( + projects.filter( + (project) => !project.archivedAt && this.inspectDirectory(project.rootPath) === "directory", + ), + workspacesByProject, + changes, + ); + if (changes.length > 0) this.onChanges?.(changes); + return { changesApplied: changes, durationMs: Date.now() - start }; } async runOnce(): Promise { - return this.reconcile(); - } - - private async runSafe(): Promise { - if (this.running) return; - this.running = true; - try { - await this.reconcile(); - } catch (error) { - this.logger.error({ err: error }, "Reconciliation pass failed"); - } finally { - this.running = false; - } - } - - private async reconcile(): Promise { const start = Date.now(); const changes: ReconciliationChange[] = []; @@ -126,16 +216,23 @@ export class WorkspaceReconciliationService { const activeProjects = allProjects.filter((p) => !p.archivedAt); const activeWorkspaces = allWorkspaces.filter((w) => !w.archivedAt); + const workspaceDirectoryStates = activeWorkspaces.map((workspace) => ({ + workspace, + state: this.inspectDirectory(workspace.cwd), + })); const workspacesByProject = new Map(); - for (const workspace of activeWorkspaces) { + for (const { workspace, state } of workspaceDirectoryStates) { + if (state !== "directory") continue; const list = workspacesByProject.get(workspace.projectId) ?? []; list.push(workspace); workspacesByProject.set(workspace.projectId, list); } // 1. Archive workspaces whose directories no longer exist - const missingWorkspaces = activeWorkspaces.filter((workspace) => !existsSync(workspace.cwd)); + const missingWorkspaces = workspaceDirectoryStates + .filter(({ state }) => state === "missing") + .map(({ workspace }) => workspace); await Promise.all( missingWorkspaces.map(async (workspace) => { const timestamp = new Date().toISOString(); @@ -156,29 +253,13 @@ export class WorkspaceReconciliationService { }), ); - // 2. Merge duplicate active project records that point at the same repo root. - await this.mergeDuplicateProjectsByRoot(activeProjects, workspacesByProject, changes); - - // 3. Reconcile git metadata for active projects whose directories still exist. + // 2. Reconcile mutable git metadata without changing identity or membership. // Projects persist until explicitly removed, even when they currently have // zero active workspaces, so they still reconcile their own metadata. - // Skip projects archived earlier in this pass (e.g. merged duplicates) so we - // don't resurrect them by upserting a stale, non-archived copy. - const archivedProjectIds = new Set( - changes - .filter((change) => change.kind === "project_archived") - .map((change) => change.projectId), - ); - const projectsToReconcile = activeProjects.filter((project) => { - if (project.archivedAt) return false; - if (archivedProjectIds.has(project.projectId)) return false; - if (!existsSync(project.rootPath)) return false; - return true; - }); - await Promise.all( - projectsToReconcile.map((project) => - this.reconcileProject(project, workspacesByProject.get(project.projectId) ?? [], changes), - ), + await this.reconcileGitMetadataForProjects( + activeProjects.filter((project) => this.inspectDirectory(project.rootPath) === "directory"), + workspacesByProject, + changes, ); if (changes.length > 0 && this.onChanges) { @@ -188,136 +269,72 @@ export class WorkspaceReconciliationService { const result = { changesApplied: changes, durationMs: Date.now() - start }; if (changes.length > 0) { this.logger.info( - { - changeCount: changes.length, - durationMs: result.durationMs, - changes, - }, + { changeCount: changes.length, durationMs: result.durationMs, changes }, "Workspace reconciliation applied changes", ); } - return result; } - private async mergeDuplicateProjectsByRoot( - activeProjects: PersistedProjectRecord[], + private async reconcileGitMetadataForProjects( + projectsToReconcile: PersistedProjectRecord[], workspacesByProject: Map, changes: ReconciliationChange[], ): Promise { - const projectsByRoot = new Map(); - for (const project of activeProjects) { - if (project.kind !== "git") { - continue; - } - const rootKey = resolve(project.rootPath); - const group = projectsByRoot.get(rootKey) ?? []; - group.push(project); - projectsByRoot.set(rootKey, group); - } - - for (const duplicates of projectsByRoot.values()) { - if (duplicates.length < 2) { - continue; - } - const canonical = chooseCanonicalProject(duplicates); - const duplicateProjects = duplicates.filter( - (project) => project.projectId !== canonical.projectId, + const checkoutReads: CachedCheckoutRead[] = []; + const readCheckout = (cwd: string): Promise => { + const existing = checkoutReads.find((read) => areEquivalentPaths(read.cwd, cwd)); + if (existing) return existing.checkout; + const checkout = this.readCheckout(cwd); + checkoutReads.push({ cwd, checkout }); + return checkout; + }; + const roots: Array<{ rootPath: string; projects: PersistedProjectRecord[] }> = []; + for (const project of projectsToReconcile) { + const root = roots.find((candidate) => + areEquivalentPaths(candidate.rootPath, project.rootPath), ); - await this.mergeDuplicateProjectCustomName(canonical, duplicateProjects, changes); - await Promise.all( - duplicateProjects.flatMap((project) => - (workspacesByProject.get(project.projectId) ?? []).map(async (workspace) => { - const timestamp = new Date().toISOString(); - const updatedWorkspace = { - ...workspace, - projectId: canonical.projectId, - updatedAt: timestamp, - }; - await this.workspaceRegistry.upsert(updatedWorkspace); - changes.push({ - kind: "workspace_updated", - workspaceId: workspace.workspaceId, - directory: workspace.cwd, - fields: { - projectId: canonical.projectId, - }, - }); - - const canonicalSiblings = workspacesByProject.get(canonical.projectId) ?? []; - canonicalSiblings.push(updatedWorkspace); - workspacesByProject.set(canonical.projectId, canonicalSiblings); - }), - ), - ); - - for (const project of duplicateProjects) { - workspacesByProject.set(project.projectId, []); - const timestamp = new Date().toISOString(); - await this.projectRegistry.archive(project.projectId, timestamp); - changes.push({ - kind: "project_archived", - projectId: project.projectId, - directory: project.rootPath, - reason: "merged_duplicate", - }); - } + if (root) root.projects.push(project); + else roots.push({ rootPath: project.rootPath, projects: [project] }); } + await Promise.all( + roots.map(async ({ rootPath, projects }) => { + try { + const rootGit = await readCheckout(rootPath); + await Promise.all( + projects.map((project) => + this.reconcileProject({ + project, + siblings: workspacesByProject.get(project.projectId) ?? [], + currentGit: rootGit, + readCheckout, + changes, + }), + ), + ); + } catch (error) { + this.logger.warn( + { err: error, rootPath }, + "Skipped workspace reconciliation after Git read failed", + ); + } + }), + ); } - private async mergeDuplicateProjectCustomName( - canonical: PersistedProjectRecord, - duplicateProjects: PersistedProjectRecord[], - changes: ReconciliationChange[], - ): Promise { - if (canonical.customName) { - return; - } - const customName = duplicateProjects.find((project) => project.customName)?.customName ?? null; - if (!customName) { - return; - } - - const timestamp = new Date().toISOString(); - await this.projectRegistry.upsert({ - ...canonical, - customName, - updatedAt: timestamp, - }); - canonical.customName = customName; - changes.push({ - kind: "project_updated", - projectId: canonical.projectId, - directory: canonical.rootPath, - fields: { customName }, - }); - } - - private async reconcileProject( - project: PersistedProjectRecord, - siblings: PersistedWorkspaceRecord[], - changes: ReconciliationChange[], - ): Promise { - const directoryName = project.rootPath.split(/[\\/]/).findLast(Boolean) ?? project.rootPath; - const currentGit = await this.readWorkspaceGitMetadata(project.rootPath, directoryName); - - const projectUpdates: Partial< - Pick - > = {}; - - const mappedKind = currentGit.projectKind === "git" ? "git" : "non_git"; + private async reconcileProject(input: ProjectReconciliationInput): Promise { + const { project, siblings, currentGit, readCheckout, changes } = input; + const workspaceCheckouts = await Promise.all( + siblings.map(async (workspace) => ({ + workspace, + checkout: await readCheckout(workspace.cwd), + })), + ); + const projectUpdates: Partial> = {}; + const mappedKind = deriveProjectKind(currentGit); if (project.kind !== mappedKind) { projectUpdates.kind = mappedKind; - projectUpdates.displayName = currentGit.projectDisplayName; - } - - if ( - project.kind === "git" && - currentGit.projectKind === "git" && - project.displayName !== currentGit.projectDisplayName - ) { - projectUpdates.displayName = currentGit.projectDisplayName; } if (Object.keys(projectUpdates).length > 0) { @@ -335,58 +352,163 @@ export class WorkspaceReconciliationService { }); } - const existingSiblings = siblings.filter((workspace) => existsSync(workspace.cwd)); await Promise.all( - existingSiblings.map(async (workspace) => { - const wsDirName = workspace.cwd.split(/[\\/]/).findLast(Boolean) ?? workspace.cwd; - const wsGit = await this.readWorkspaceGitMetadata(workspace.cwd, wsDirName); - - const expectedKind = deriveWorkspaceKindFromMetadata(wsGit); - - const workspaceUpdates: Partial> = {}; - - if (wsGit.projectKind === "git" && workspace.branch !== wsGit.currentBranch) { - workspaceUpdates.branch = wsGit.currentBranch; - } - - if (workspace.kind !== expectedKind) { - workspaceUpdates.kind = expectedKind; - } - - if (Object.keys(workspaceUpdates).length === 0) { - return; - } - + workspaceCheckouts.map(async ({ workspace, checkout: wsGit }) => { const timestamp = new Date().toISOString(); - await this.workspaceRegistry.upsert({ - ...workspace, - ...workspaceUpdates, + const update = reconcileWorkspacePlacement({ + workspace, + checkout: wsGit, updatedAt: timestamp, }); + if (!update) return; + + await this.workspaceRegistry.upsert(update.workspace); changes.push({ kind: "workspace_updated", workspaceId: workspace.workspaceId, directory: workspace.cwd, - fields: workspaceUpdates, + fields: update.fields, }); }), ); } - private async readWorkspaceGitMetadata(cwd: string, directoryName: string) { + private async syncProjectRootWatches(): Promise { + if (this.disposed) return; + const projects = await this.projectRegistry.list(); + if (this.disposed) return; + const activeProjects = projects.filter((project) => !project.archivedAt); + + for (let index = this.watchers.length - 1; index >= 0; index -= 1) { + const target = this.watchers[index]!; + const stillActive = activeProjects.some((project) => + areEquivalentPaths(project.rootPath, target.rootPath), + ); + if (stillActive) continue; + target.watcher.close(); + this.watchers.splice(index, 1); + } + + for (const project of activeProjects) { + const alreadyWatching = this.watchers.some((target) => + areEquivalentPaths(target.rootPath, project.rootPath), + ); + if (alreadyWatching) continue; + try { + let watcher: ProjectRootWatcher; + watcher = this.watchProjectRoot( + project.rootPath, + { recursive: false }, + (_event, filename) => { + if (filename === null || filename.toString() === ".git") { + this.scheduleObservedReconciliation(); + } + }, + (error) => { + watcher.close(); + const index = this.watchers.findIndex((target) => target.watcher === watcher); + if (index >= 0) this.watchers.splice(index, 1); + this.logger.warn( + { err: error, rootPath: project.rootPath }, + "Project root watch failed", + ); + }, + ); + this.watchers.push({ rootPath: project.rootPath, watcher }); + } catch (error) { + // The periodic reconciliation is the convergence path for roots that + // are temporarily missing or unwatchable. + this.logger.debug( + { err: error, rootPath: project.rootPath }, + "Project root is not watchable yet", + ); + } + } + } + + private scheduleObservedReconciliation(): void { + if (this.disposed || this.debounceTimer) return; + this.debounceTimer = this.clock.setTimeout(() => { + this.debounceTimer = null; + return this.reconcileObservedGitMetadata(); + }, this.debounceMs); + } + + private async reconcileObservedGitMetadata( + mode: "metadata" | "full" = "metadata", + ): Promise { + if (this.disposed) return; + if (this.reconciling) { + if (mode === "full" || this.reconcileQueuedMode === null) { + this.reconcileQueuedMode = mode; + } + return; + } + this.reconciling = true; + try { + await this.syncProjectRootWatches(); + const result = mode === "full" ? await this.runOnce() : await this.reconcileGitMetadata(); + const workspaceIds = new Set(); + const projectIds = new Set(); + for (const change of result.changesApplied) { + if (change.kind === "workspace_updated" || change.kind === "workspace_archived") { + workspaceIds.add(change.workspaceId); + } + if (change.kind === "project_updated") projectIds.add(change.projectId); + } + if (projectIds.size > 0) { + const workspaces = await this.workspaceRegistry.list(); + for (const workspaceId of workspaceIdsForProjects(workspaces, projectIds)) { + workspaceIds.add(workspaceId); + } + } + if (!this.disposed && workspaceIds.size > 0) { + await this.onWorkspacesChanged?.(Array.from(workspaceIds)); + } + } catch (error) { + if (!this.disposed) { + this.logger.warn({ err: error }, "Workspace reconciliation failed"); + } + } finally { + this.reconciling = false; + if (this.reconcileQueuedMode) { + const queuedMode = this.reconcileQueuedMode; + this.reconcileQueuedMode = null; + void this.reconcileObservedGitMetadata(queuedMode); + } + } + } + + private async readCheckout(cwd: string): Promise { if (!this.workspaceGitService) { return { - projectKind: "directory" as const, - projectDisplayName: directoryName, - workspaceDisplayName: directoryName, - gitRemote: null, - isWorktree: false, - projectSlug: "untitled", - repoRoot: null, + cwd, + isGit: false as const, currentBranch: null, remoteUrl: null, + worktreeRoot: null, + isPaseoOwnedWorktree: false as const, + mainRepoRoot: null, }; } - return this.workspaceGitService.getWorkspaceGitMetadata(cwd, { directoryName }); + return this.workspaceGitService.getCheckout(cwd); + } + + private inspectDirectory(targetPath: string): DirectoryState { + try { + return statSync(targetPath).isDirectory() ? "directory" : "missing"; + } catch (error) { + if (isMissingPathError(error)) return "missing"; + this.logger.warn( + { err: error, targetPath }, + "Skipped workspace reconciliation after directory inspection failed", + ); + return "unreadable"; + } } } + +function isMissingPathError(error: unknown): boolean { + if (typeof error !== "object" || error === null || !("code" in error)) return false; + return error.code === "ENOENT" || error.code === "ENOTDIR"; +} diff --git a/packages/server/src/server/workspace-registry-bootstrap-legacy.ts b/packages/server/src/server/workspace-registry-bootstrap-legacy.ts new file mode 100644 index 000000000..2a712e6a5 --- /dev/null +++ b/packages/server/src/server/workspace-registry-bootstrap-legacy.ts @@ -0,0 +1,120 @@ +import { resolve } from "node:path"; + +import type { ProjectCheckoutLitePayload } from "@getpaseo/protocol/messages"; + +import { parseGitRevParsePath } from "../utils/git-rev-parse-path.js"; +import { + deriveProjectKind, + deriveWorkspaceDisplayName, + deriveWorkspaceKind, + type PersistedProjectKind, + type PersistedWorkspaceKind, +} from "./workspace-registry-model.js"; + +// COMPAT(legacyRegistryBootstrap): added in v0.1.109 on 2026-07-15; remove after +// 2027-01-15, once every supported install has materialized its registry files. +interface DirectoryProjectMembership { + cwd: string; + checkout: ProjectCheckoutLitePayload; + workspaceDirectoryKey: string; + workspaceKind: PersistedWorkspaceKind; + workspaceDisplayName: string; + projectKey: string; + projectName: string; + projectRootPath: string; + projectKind: PersistedProjectKind; +} + +export function classifyDirectoryForProjectMembership(input: { + cwd: string; + checkout: ProjectCheckoutLitePayload; +}): DirectoryProjectMembership { + const cwd = resolve(input.cwd); + const checkout: ProjectCheckoutLitePayload = { ...input.checkout, cwd }; + const projectKey = deriveProjectGroupingKey({ + cwd: checkout.worktreeRoot ?? cwd, + remoteUrl: checkout.remoteUrl, + mainRepoRoot: checkout.mainRepoRoot, + }); + + return { + cwd, + checkout, + workspaceDirectoryKey: deriveWorkspaceDirectoryKey(cwd, checkout), + workspaceKind: deriveWorkspaceKind(checkout), + workspaceDisplayName: deriveWorkspaceDisplayName({ cwd, checkout }), + projectKey, + projectName: deriveProjectGroupingName(projectKey), + projectRootPath: deriveProjectRootPath({ cwd, checkout }), + projectKind: deriveProjectKind(checkout), + }; +} + +function deriveWorkspaceDirectoryKey(cwd: string, checkout: ProjectCheckoutLitePayload): string { + const worktreeRoot = checkout.worktreeRoot ? parseGitRevParsePath(checkout.worktreeRoot) : null; + return worktreeRoot ?? resolve(cwd); +} + +function deriveRemoteProjectKey(remoteUrl: string | null): string | null { + if (!remoteUrl) return null; + + const trimmed = remoteUrl.trim(); + if (!trimmed) return null; + + let host: string | null = null; + let remotePath: string | null = null; + const scpLike = trimmed.match(/^[^@]+@([^:]+):(.+)$/); + if (scpLike) { + host = scpLike[1] ?? null; + remotePath = scpLike[2] ?? null; + } else if (trimmed.includes("://")) { + try { + const parsed = new URL(trimmed); + host = parsed.hostname || null; + remotePath = parsed.pathname ? parsed.pathname.replace(/^\/+/, "") : null; + } catch { + return null; + } + } + + if (!host || !remotePath) return null; + + let cleanedPath = remotePath.trim().replace(/^\/+/, "").replace(/\/+$/, ""); + if (cleanedPath.endsWith(".git")) cleanedPath = cleanedPath.slice(0, -4); + if (!cleanedPath.includes("/")) return null; + + return `remote:${host.toLowerCase()}/${cleanedPath}`; +} + +function deriveProjectGroupingKey(options: { + cwd: string; + remoteUrl: string | null; + mainRepoRoot: string | null; +}): string { + const remoteKey = deriveRemoteProjectKey(options.remoteUrl); + if (remoteKey) return remoteKey; + + const mainRepoRoot = options.mainRepoRoot?.trim(); + return mainRepoRoot || options.cwd; +} + +function deriveProjectGroupingName(projectKey: string): string { + if (projectKey.startsWith("remote:")) { + const pathSegments = projectKey.slice("remote:".length).split("/").filter(Boolean).slice(1); + if (pathSegments.length >= 2) return pathSegments.slice(-2).join("/"); + if (pathSegments.length === 1) return pathSegments[0]; + return projectKey; + } + + const segments = projectKey.split(/[\\/]/).filter(Boolean); + return segments[segments.length - 1] || projectKey; +} + +function deriveProjectRootPath(input: { + cwd: string; + checkout: ProjectCheckoutLitePayload; +}): string { + return input.checkout.isGit && input.checkout.mainRepoRoot + ? input.checkout.mainRepoRoot + : input.cwd; +} diff --git a/packages/server/src/server/workspace-registry-bootstrap.test.ts b/packages/server/src/server/workspace-registry-bootstrap.test.ts index e97b83112..56ccf4d0b 100644 --- a/packages/server/src/server/workspace-registry-bootstrap.test.ts +++ b/packages/server/src/server/workspace-registry-bootstrap.test.ts @@ -1,6 +1,6 @@ import os from "node:os"; import path from "node:path"; -import { mkdtempSync, rmSync } from "node:fs"; +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { afterEach, beforeEach, describe, expect, test } from "vitest"; @@ -11,8 +11,10 @@ import type { WorkspaceGitService } from "./workspace-git-service.js"; import { FileBackedProjectRegistry, FileBackedWorkspaceRegistry } from "./workspace-registry.js"; import { bootstrapWorkspaceRegistries } from "./workspace-registry-bootstrap.js"; -const NON_GIT_PROJECT = path.resolve("/tmp/non-git-project"); -const ARCHIVED_PROJECT = path.resolve("/tmp/archived-project"); +let NON_GIT_PROJECT: string; +let ARCHIVED_PROJECT: string; +let GIT_PROJECT: string; +let GIT_WORKTREE: string; describe("bootstrapWorkspaceRegistries", () => { let tmpDir: string; @@ -25,6 +27,10 @@ describe("bootstrapWorkspaceRegistries", () => { beforeEach(() => { tmpDir = mkdtempSync(path.join(os.tmpdir(), "workspace-bootstrap-")); + NON_GIT_PROJECT = path.join(tmpDir, "non-git-project"); + ARCHIVED_PROJECT = path.join(tmpDir, "archived-project"); + GIT_PROJECT = path.join(tmpDir, "legacy-git-project"); + GIT_WORKTREE = path.join(tmpDir, "legacy-git-project-feature"); paseoHome = path.join(tmpDir, ".paseo"); agentStorage = new AgentStorage(path.join(paseoHome, "agents"), logger); projectRegistry = new FileBackedProjectRegistry( @@ -36,12 +42,133 @@ describe("bootstrapWorkspaceRegistries", () => { logger, ); workspaceGitService = createNoopWorkspaceGitService(); + for (const directory of [NON_GIT_PROJECT, ARCHIVED_PROJECT, GIT_PROJECT, GIT_WORKTREE]) { + mkdirSync(directory, { recursive: true }); + } }); afterEach(() => { rmSync(tmpDir, { recursive: true, force: true }); }); + test("skips a legacy agent whose directory no longer exists", async () => { + const missingDirectory = path.join(tmpDir, "missing-project"); + const getCheckout = async () => { + throw new Error("Git must not inspect a missing directory"); + }; + workspaceGitService = { ...createNoopWorkspaceGitService(), getCheckout }; + await agentStorage.initialize(); + await agentStorage.upsert({ + id: "agent-missing-directory", + provider: "codex", + cwd: missingDirectory, + createdAt: "2026-03-01T00:00:00.000Z", + updatedAt: "2026-03-01T00:00:00.000Z", + lastActivityAt: null, + lastUserMessageAt: null, + title: null, + labels: {}, + lastStatus: "idle", + lastModeId: null, + config: null, + runtimeInfo: { provider: "codex", sessionId: null }, + persistence: null, + archivedAt: null, + }); + + await bootstrapWorkspaceRegistries({ + paseoHome, + agentStorage, + projectRegistry, + workspaceRegistry, + workspaceGitService, + logger, + }); + + expect(await projectRegistry.list()).toEqual([]); + expect(await workspaceRegistry.list()).toEqual([]); + }); + + test("skips a legacy agent whose cwd is a file", async () => { + const cwd = path.join(tmpDir, "not-a-directory"); + writeFileSync(cwd, "not a directory"); + workspaceGitService = { + ...createNoopWorkspaceGitService(), + getCheckout: async () => { + throw new Error("Git must not inspect a file"); + }, + }; + await agentStorage.initialize(); + await agentStorage.upsert({ + id: "agent-file-cwd", + provider: "codex", + cwd, + createdAt: "2026-03-01T00:00:00.000Z", + updatedAt: "2026-03-01T00:00:00.000Z", + lastActivityAt: null, + lastUserMessageAt: null, + title: null, + labels: {}, + lastStatus: "idle", + lastModeId: null, + config: null, + runtimeInfo: { provider: "codex", sessionId: null }, + persistence: null, + archivedAt: null, + }); + + await bootstrapWorkspaceRegistries({ + paseoHome, + agentStorage, + projectRegistry, + workspaceRegistry, + workspaceGitService, + logger, + }); + + expect(await projectRegistry.list()).toEqual([]); + expect(await workspaceRegistry.list()).toEqual([]); + }); + + test("propagates a Git failure for an existing legacy directory", async () => { + const gitFailure = new Error("Git is unavailable"); + workspaceGitService = { + ...createNoopWorkspaceGitService(), + getCheckout: async () => { + throw gitFailure; + }, + }; + await agentStorage.initialize(); + await agentStorage.upsert({ + id: "agent-existing-directory", + provider: "codex", + cwd: NON_GIT_PROJECT, + createdAt: "2026-03-01T00:00:00.000Z", + updatedAt: "2026-03-01T00:00:00.000Z", + lastActivityAt: null, + lastUserMessageAt: null, + title: null, + labels: {}, + lastStatus: "idle", + lastModeId: null, + config: null, + runtimeInfo: { provider: "codex", sessionId: null }, + persistence: null, + archivedAt: null, + }); + + await expect( + bootstrapWorkspaceRegistries({ + paseoHome, + agentStorage, + projectRegistry, + workspaceRegistry, + workspaceGitService, + logger, + }), + ).rejects.toBe(gitFailure); + }); + test("materializes workspace registries from non-archived agent records", async () => { await agentStorage.initialize(); await agentStorage.upsert({ @@ -175,6 +302,81 @@ describe("bootstrapWorkspaceRegistries", () => { expect((await workspaceRegistry.list())[0]?.workspaceId).toBe("ws-existing"); }); + test("materializes legacy remote worktrees into one readable project", async () => { + workspaceGitService = createNoopWorkspaceGitService({ + getCheckout: async (cwd) => ({ + cwd, + isGit: true, + currentBranch: cwd === GIT_PROJECT ? "main" : "feature/plain", + remoteUrl: "git@github.com:acme/legacy-project.git", + worktreeRoot: cwd, + isPaseoOwnedWorktree: false, + mainRepoRoot: cwd === GIT_PROJECT ? null : GIT_PROJECT, + }), + }); + await agentStorage.initialize(); + for (const [id, cwd] of [ + ["main-agent", GIT_PROJECT], + ["worktree-agent", GIT_WORKTREE], + ]) { + await agentStorage.upsert({ + id, + provider: "codex", + cwd, + createdAt: "2026-03-01T00:00:00.000Z", + updatedAt: "2026-03-02T00:00:00.000Z", + lastActivityAt: "2026-03-02T00:00:00.000Z", + lastUserMessageAt: null, + title: null, + labels: {}, + lastStatus: "idle", + lastModeId: null, + config: null, + runtimeInfo: { provider: "codex", sessionId: null }, + persistence: null, + archivedAt: null, + }); + } + + await bootstrapWorkspaceRegistries({ + paseoHome, + agentStorage, + projectRegistry, + workspaceRegistry, + workspaceGitService, + logger, + }); + + const projects = await projectRegistry.list(); + expect(projects).toHaveLength(1); + expect(projects[0]).toMatchObject({ + projectId: "remote:github.com/acme/legacy-project", + rootPath: GIT_PROJECT, + kind: "git", + displayName: "acme/legacy-project", + }); + + const workspaces = await workspaceRegistry.list(); + expect( + workspaces + .map(({ projectId, cwd, kind, displayName }) => ({ projectId, cwd, kind, displayName })) + .sort((left, right) => left.cwd.localeCompare(right.cwd)), + ).toEqual([ + { + projectId: "remote:github.com/acme/legacy-project", + cwd: GIT_PROJECT, + kind: "local_checkout", + displayName: "main", + }, + { + projectId: "remote:github.com/acme/legacy-project", + cwd: GIT_WORKTREE, + kind: "worktree", + displayName: "feature/plain", + }, + ]); + }); + test("migrates cwd-only agents to the oldest existing same-cwd workspace", async () => { await projectRegistry.initialize(); await workspaceRegistry.initialize(); diff --git a/packages/server/src/server/workspace-registry-bootstrap.ts b/packages/server/src/server/workspace-registry-bootstrap.ts index c673059e3..8a75d5323 100644 --- a/packages/server/src/server/workspace-registry-bootstrap.ts +++ b/packages/server/src/server/workspace-registry-bootstrap.ts @@ -1,13 +1,12 @@ import path from "node:path"; +import { statSync } from "node:fs"; import type { Logger } from "pino"; import type { StoredAgentRecord } from "./agent/agent-storage.js"; import type { AgentStorage } from "./agent/agent-storage.js"; -import { - classifyDirectoryForProjectMembership, - generateWorkspaceId, -} from "./workspace-registry-model.js"; +import { classifyDirectoryForProjectMembership } from "./workspace-registry-bootstrap-legacy.js"; +import { generateWorkspaceId } from "./workspace-registry-model.js"; import { backfillWorkspaceIdForLegacyAgents } from "./migrations/backfill-workspace-id.migration.js"; import type { WorkspaceGitService } from "./workspace-git-service.js"; import { @@ -72,7 +71,17 @@ export async function bootstrapWorkspaceRegistries(options: { ]), ); const records = await options.agentStorage.list(); - const activeRecords = records.filter((record) => !record.archivedAt); + // A legacy agent can outlive its working directory. Reconciliation treats a + // missing directory as absent rather than asking Git about it; bootstrap must + // do the same before materializing its first workspace record. + const activeRecords = records.filter((record) => { + if (record.archivedAt) return false; + try { + return statSync(record.cwd).isDirectory(); + } catch { + return false; + } + }); const recordsByDirectoryKey = new Map< string, { diff --git a/packages/server/src/server/workspace-registry-model.test.ts b/packages/server/src/server/workspace-registry-model.test.ts index 115d8048a..1f51a0c4a 100644 --- a/packages/server/src/server/workspace-registry-model.test.ts +++ b/packages/server/src/server/workspace-registry-model.test.ts @@ -1,226 +1,30 @@ import { describe, expect, test } from "vitest"; -import { basename, isAbsolute, resolve } from "node:path"; +import { isAbsolute } from "node:path"; import { - classifyDirectoryForProjectMembership, - deriveProjectGroupingName, - deriveProjectRootPath, - deriveWorkspaceDirectoryKey, + checkoutFromPersistedWorkspacePlacement, deriveWorkspaceKind, - detectStaleWorkspaces, generateWorkspaceId, + generateProjectId, + initialWorkspacePlacement, + reconcileWorkspacePlacement, } from "./workspace-registry-model.js"; import { createPersistedWorkspaceRecord } from "./workspace-registry.js"; -function createWorkspaceRecord( - cwd: string, - workspaceId: string, - overrides?: { createdAt?: string; archivedAt?: string }, -) { - return createPersistedWorkspaceRecord({ - workspaceId, - projectId: workspaceId, - cwd, - kind: "directory", - displayName: basename(cwd) || cwd, - createdAt: overrides?.createdAt ?? "2026-03-01T00:00:00.000Z", - updatedAt: overrides?.createdAt ?? "2026-03-01T00:00:00.000Z", - archivedAt: overrides?.archivedAt ?? null, - }); -} - -describe("deriveProjectGroupingName", () => { - test("returns owner/repo for a github remote project key", () => { - expect(deriveProjectGroupingName("remote:github.com/acme/app")).toBe("acme/app"); +describe("opaque registry ids", () => { + test("generates opaque project ids", () => { + expect(generateProjectId()).toMatch(/^prj_[0-9a-f]{16}$/); }); - test("returns owner/repo for a gitlab remote project key", () => { - expect(deriveProjectGroupingName("remote:gitlab.com/acme/app")).toBe("acme/app"); - }); - - test("returns last two segments for a self-hosted remote project key", () => { - expect(deriveProjectGroupingName("remote:git.acme.internal/platform/api")).toBe("platform/api"); - }); - - test("returns last two segments for a deeply-nested remote project key", () => { - expect(deriveProjectGroupingName("remote:gitlab.com/group/sub/app")).toBe("sub/app"); - }); - - test("returns the lone path segment when only one segment follows the host", () => { - expect(deriveProjectGroupingName("remote:github.com/solo")).toBe("solo"); - }); - - test("returns the trailing path segment for a non-remote project key", () => { - expect(deriveProjectGroupingName("/repo/local")).toBe("local"); - }); - - test("returns the project key itself when no segments are present", () => { - expect(deriveProjectGroupingName("")).toBe(""); - }); -}); - -describe("detectStaleWorkspaces", () => { - test("returns workspace ids whose directories no longer exist", async () => { - const checkedDirectories: string[] = []; - const existingDirectories = new Set(["/tmp/existing"]); - - const staleWorkspaceIds = await detectStaleWorkspaces({ - activeWorkspaces: [ - createWorkspaceRecord("/tmp/existing", "ws-existing"), - createWorkspaceRecord("/tmp/missing", "ws-missing"), - ], - checkDirectoryExists: async (cwd) => { - checkedDirectories.push(cwd); - return existingDirectories.has(cwd); - }, - }); - - expect(Array.from(staleWorkspaceIds)).toEqual(["ws-missing"]); - expect(checkedDirectories).toEqual(["/tmp/existing", "/tmp/missing"]); - }); - - test("keeps workspaces whose directories exist even when all agents are archived", async () => { - const staleWorkspaceIds = await detectStaleWorkspaces({ - activeWorkspaces: [ - createWorkspaceRecord("/tmp/repo", "ws-repo"), - createWorkspaceRecord("/tmp/other", "ws-other"), - ], - checkDirectoryExists: async () => true, - }); - - expect(Array.from(staleWorkspaceIds)).toEqual([]); - }); - - test("keeps workspaces with no agents when directory exists", async () => { - const staleWorkspaceIds = await detectStaleWorkspaces({ - activeWorkspaces: [ - createWorkspaceRecord("/tmp/active", "ws-active"), - createWorkspaceRecord("/tmp/no-agents", "ws-no-agents"), - ], - checkDirectoryExists: async () => true, - }); - - expect(Array.from(staleWorkspaceIds)).toEqual([]); - }); -}); - -describe("deriveWorkspaceDirectoryKey", () => { - test("uses git worktree root when available", () => { - expect( - deriveWorkspaceDirectoryKey("/tmp/repo/packages/app", { - cwd: "/tmp/repo/packages/app", - isGit: true, - currentBranch: "main", - remoteUrl: "https://github.com/acme/repo.git", - worktreeRoot: "/tmp/repo", - isPaseoOwnedWorktree: false, - mainRepoRoot: null, - }), - ).toBe("/tmp/repo"); - }); - - test("falls back to normalized cwd when git worktree root contains multiple lines", () => { - const cwd = String.raw`E:\project\node-ai`; - - expect( - deriveWorkspaceDirectoryKey(cwd, { - cwd, - isGit: true, - currentBranch: "main", - remoteUrl: null, - worktreeRoot: `--path-format=absolute\n${cwd}`, - isPaseoOwnedWorktree: false, - mainRepoRoot: null, - }), - ).toBe(resolve(cwd)); - }); - - test("falls back to normalized cwd for non-git directories", () => { - const cwd = "/tmp/repo/../repo/scratch"; - - expect( - deriveWorkspaceDirectoryKey(cwd, { - cwd, - isGit: false, - currentBranch: null, - remoteUrl: null, - worktreeRoot: null, - isPaseoOwnedWorktree: false, - mainRepoRoot: null, - }), - ).toBe(resolve("/tmp/repo/scratch")); - }); -}); - -describe("opaque workspace id versus directory key", () => { test("generates opaque workspace ids that are not filesystem paths", () => { const workspaceId = generateWorkspaceId(); expect(workspaceId).toMatch(/^wks_[0-9a-f]+$/); expect(isAbsolute(workspaceId)).toBe(false); }); - - test("derives a path-shaped directory key that is never an opaque workspace id", () => { - const directoryKey = deriveWorkspaceDirectoryKey("/tmp/repo/scratch", { - cwd: "/tmp/repo/scratch", - isGit: false, - currentBranch: null, - remoteUrl: null, - worktreeRoot: null, - isPaseoOwnedWorktree: false, - mainRepoRoot: null, - }); - - expect(directoryKey).toBe(resolve("/tmp/repo/scratch")); - expect(directoryKey.startsWith("wks_")).toBe(false); - }); }); -describe("git worktree grouping", () => { - test("classifies plain git worktrees for project membership from git facts", () => { - const membership = classifyDirectoryForProjectMembership({ - cwd: "/tmp/repo-feature", - checkout: { - cwd: "/tmp/repo-feature", - isGit: true, - currentBranch: "feature/plain", - remoteUrl: "https://github.com/acme/repo.git", - worktreeRoot: "/tmp/repo-feature", - isPaseoOwnedWorktree: false, - mainRepoRoot: "/tmp/repo", - }, - }); - - expect(membership).toMatchObject({ - // Path-derived directory key, distinct from the opaque workspace id (generated separately). - cwd: resolve("/tmp/repo-feature"), - workspaceDirectoryKey: "/tmp/repo-feature", - workspaceKind: "worktree", - workspaceDisplayName: "feature/plain", - projectKey: "remote:github.com/acme/repo", - projectName: "acme/repo", - projectRootPath: "/tmp/repo", - projectKind: "git", - }); - }); - - test("uses mainRepoRoot as the project root for plain git worktrees", () => { - expect( - deriveProjectRootPath({ - cwd: "/tmp/repo-feature", - checkout: { - cwd: "/tmp/repo-feature", - isGit: true, - currentBranch: "feature/plain", - remoteUrl: "https://github.com/acme/repo.git", - worktreeRoot: "/tmp/repo-feature", - isPaseoOwnedWorktree: false, - mainRepoRoot: "/tmp/repo", - }, - }), - ).toBe("/tmp/repo"); - }); - +describe("workspace kind", () => { test("classifies plain git worktrees as workspaces of kind worktree", () => { expect( deriveWorkspaceKind({ @@ -235,3 +39,119 @@ describe("git worktree grouping", () => { ).toBe("worktree"); }); }); + +describe("workspace placement", () => { + test("defines checkout and created-worktree placement completely", () => { + expect( + initialWorkspacePlacement({ + source: "checkout", + cwd: "/repo", + checkout: { + cwd: "/repo", + isGit: true, + currentBranch: " main ", + remoteUrl: null, + worktreeRoot: "/repo", + isPaseoOwnedWorktree: false, + mainRepoRoot: null, + }, + }), + ).toEqual({ + cwd: "/repo", + kind: "local_checkout", + displayName: "main", + branch: "main", + worktreeRoot: "/repo", + baseBranch: null, + isPaseoOwnedWorktree: false, + mainRepoRoot: null, + }); + expect( + initialWorkspacePlacement({ + source: "created_worktree", + cwd: "/repo-feature/app", + worktreeRoot: "/repo-feature", + branch: "feature/placement", + baseBranch: "main", + mainRepoRoot: "/repo", + }), + ).toEqual({ + cwd: "/repo-feature/app", + kind: "worktree", + displayName: "feature/placement", + branch: "feature/placement", + worktreeRoot: "/repo-feature", + baseBranch: "main", + isPaseoOwnedWorktree: true, + mainRepoRoot: "/repo", + }); + }); + + test("updates live placement while preserving its durable name and base branch", () => { + const workspace = createPersistedWorkspaceRecord({ + workspaceId: "workspace-one", + projectId: "project-one", + cwd: "/repo-feature", + kind: "worktree", + displayName: "Keep this name", + branch: "old-branch", + worktreeRoot: "/old-root", + baseBranch: "release", + isPaseoOwnedWorktree: true, + mainRepoRoot: "/repo", + createdAt: "2026-03-01T00:00:00.000Z", + updatedAt: "2026-03-01T00:00:00.000Z", + }); + + const update = reconcileWorkspacePlacement({ + workspace, + checkout: { + cwd: workspace.cwd, + isGit: true, + currentBranch: "renamed-branch", + remoteUrl: null, + worktreeRoot: "/repo-feature", + isPaseoOwnedWorktree: false, + mainRepoRoot: "/repo", + }, + updatedAt: "2026-03-02T00:00:00.000Z", + }); + + expect(update?.fields).toEqual({ + branch: "renamed-branch", + worktreeRoot: "/repo-feature", + isPaseoOwnedWorktree: false, + }); + expect(update?.workspace).toMatchObject({ + displayName: "Keep this name", + baseBranch: "release", + branch: "renamed-branch", + }); + }); + + test("projects persisted placement to the wire checkout", () => { + const workspace = createPersistedWorkspaceRecord({ + workspaceId: "workspace-one", + projectId: "project-one", + cwd: "/repo-feature/app", + kind: "worktree", + displayName: "feature", + branch: "feature", + worktreeRoot: "/repo-feature", + isPaseoOwnedWorktree: true, + mainRepoRoot: "/repo", + createdAt: "2026-03-01T00:00:00.000Z", + updatedAt: "2026-03-01T00:00:00.000Z", + }); + + expect(checkoutFromPersistedWorkspacePlacement({ workspace })).toEqual({ + cwd: "/repo-feature/app", + isGit: true, + currentBranch: "feature", + remoteUrl: null, + worktreeRoot: "/repo-feature", + isPaseoOwnedWorktree: true, + mainRepoRoot: "/repo", + }); + }); +}); diff --git a/packages/server/src/server/workspace-registry-model.ts b/packages/server/src/server/workspace-registry-model.ts index 08af1ad27..acc4d4953 100644 --- a/packages/server/src/server/workspace-registry-model.ts +++ b/packages/server/src/server/workspace-registry-model.ts @@ -1,154 +1,20 @@ import { randomBytes } from "node:crypto"; -import { resolve } from "node:path"; import type { ProjectCheckoutLitePayload, ProjectPlacementPayload, } from "@getpaseo/protocol/messages"; -import { parseGitRevParsePath } from "../utils/git-rev-parse-path.js"; import type { PersistedWorkspaceRecord } from "./workspace-registry.js"; export type PersistedProjectKind = "git" | "non_git"; export type PersistedWorkspaceKind = "local_checkout" | "worktree" | "directory"; -export interface DirectoryProjectMembership { - cwd: string; - checkout: ProjectCheckoutLitePayload; - workspaceDirectoryKey: string; - workspaceKind: PersistedWorkspaceKind; - workspaceDisplayName: string; - projectKey: string; - projectName: string; - projectRootPath: string; - projectKind: PersistedProjectKind; -} - -export interface DetectStaleWorkspacesInput { - activeWorkspaces: PersistedWorkspaceRecord[]; - checkDirectoryExists: (cwd: string) => Promise; -} - export function generateWorkspaceId(): string { return `wks_${randomBytes(8).toString("hex")}`; } -// Path-derived grouping key for a workspace directory. This is NOT the opaque -// workspace identity (see generateWorkspaceId); never persist or compare it as one. -export function deriveWorkspaceDirectoryKey( - cwd: string, - checkout: ProjectCheckoutLitePayload, -): string { - const worktreeRoot = checkout.worktreeRoot ? parseGitRevParsePath(checkout.worktreeRoot) : null; - return worktreeRoot ?? resolve(cwd); -} - -function deriveRemoteProjectKey(remoteUrl: string | null): string | null { - if (!remoteUrl) { - return null; - } - - const trimmed = remoteUrl.trim(); - if (!trimmed) { - return null; - } - - let host: string | null = null; - let remotePath: string | null = null; - - const scpLike = trimmed.match(/^[^@]+@([^:]+):(.+)$/); - if (scpLike) { - host = scpLike[1] ?? null; - remotePath = scpLike[2] ?? null; - } else if (trimmed.includes("://")) { - try { - const parsed = new URL(trimmed); - host = parsed.hostname || null; - remotePath = parsed.pathname ? parsed.pathname.replace(/^\/+/, "") : null; - } catch { - return null; - } - } - - if (!host || !remotePath) { - return null; - } - - let cleanedPath = remotePath.trim().replace(/^\/+/, "").replace(/\/+$/, ""); - if (cleanedPath.endsWith(".git")) { - cleanedPath = cleanedPath.slice(0, -4); - } - if (!cleanedPath.includes("/")) { - return null; - } - - const cleanedHost = host.toLowerCase(); - if (cleanedHost === "github.com") { - return `remote:github.com/${cleanedPath}`; - } - - return `remote:${cleanedHost}/${cleanedPath}`; -} - -export function deriveProjectGroupingKey(options: { - cwd: string; - remoteUrl: string | null; - mainRepoRoot: string | null; -}): string { - const remoteKey = deriveRemoteProjectKey(options.remoteUrl); - if (remoteKey) { - return remoteKey; - } - - const mainRepoRoot = options.mainRepoRoot?.trim(); - if (mainRepoRoot) { - return mainRepoRoot; - } - - return options.cwd; -} - -export function deriveProjectGroupingName(projectKey: string): string { - if (projectKey.startsWith("remote:")) { - const remainder = projectKey.slice("remote:".length); - const pathSegments = remainder.split("/").filter(Boolean).slice(1); - if (pathSegments.length >= 2) { - return pathSegments.slice(-2).join("/"); - } - if (pathSegments.length === 1) { - return pathSegments[0]; - } - return projectKey; - } - - const segments = projectKey.split(/[\\/]/).filter(Boolean); - return segments[segments.length - 1] || projectKey; -} - -function deriveWorkspaceDirectoryName(cwd: string): string { - const normalized = cwd.replace(/\\/g, "/"); - const segments = normalized.split("/").filter(Boolean); - return segments[segments.length - 1] ?? cwd; -} - -export function deriveWorkspaceDisplayName(input: { - cwd: string; - checkout: ProjectCheckoutLitePayload; -}): string { - const branch = input.checkout.currentBranch?.trim() ?? null; - if (branch && branch.toUpperCase() !== "HEAD") { - return branch; - } - return deriveWorkspaceDirectoryName(input.cwd); -} - -export function deriveProjectRootPath(input: { - cwd: string; - checkout: ProjectCheckoutLitePayload; -}): string { - if (input.checkout.isGit && input.checkout.mainRepoRoot) { - return input.checkout.mainRepoRoot; - } - return input.cwd; +export function generateProjectId(): string { + return `prj_${randomBytes(8).toString("hex")}`; } export function deriveProjectKind(checkout: ProjectCheckoutLitePayload): PersistedProjectKind { @@ -162,6 +28,161 @@ export function deriveWorkspaceKind(checkout: ProjectCheckoutLitePayload): Persi return checkout.mainRepoRoot ? "worktree" : "local_checkout"; } +export function deriveWorkspaceDisplayName(input: { + cwd: string; + checkout: ProjectCheckoutLitePayload; +}): string { + const branch = input.checkout.currentBranch?.trim() ?? null; + if (branch && branch.toUpperCase() !== "HEAD") return branch; + + const segments = input.cwd.replace(/\\/g, "/").split("/").filter(Boolean); + return segments[segments.length - 1] ?? input.cwd; +} + +export type PersistedWorkspacePlacement = Pick< + PersistedWorkspaceRecord, + | "cwd" + | "kind" + | "displayName" + | "branch" + | "worktreeRoot" + | "baseBranch" + | "isPaseoOwnedWorktree" + | "mainRepoRoot" +>; + +export type MutableWorkspacePlacement = Pick< + PersistedWorkspaceRecord, + "kind" | "branch" | "worktreeRoot" | "isPaseoOwnedWorktree" | "mainRepoRoot" +>; + +export type InitialWorkspacePlacementInput = + | { + source: "checkout"; + cwd: string; + checkout: ProjectCheckoutLitePayload; + } + | { + source: "created_worktree"; + cwd: string; + worktreeRoot: string; + branch: string | null; + baseBranch: string | null; + mainRepoRoot: string; + }; + +export interface WorkspacePlacementUpdate { + workspace: PersistedWorkspaceRecord; + fields: Partial; +} + +/** Defines the complete persisted placement for every new workspace. */ +export function initialWorkspacePlacement( + input: InitialWorkspacePlacementInput, +): PersistedWorkspacePlacement { + if (input.source === "created_worktree") { + return { + cwd: input.cwd, + kind: "worktree", + displayName: input.branch || input.cwd, + branch: input.branch, + worktreeRoot: input.worktreeRoot, + baseBranch: input.baseBranch, + isPaseoOwnedWorktree: true, + mainRepoRoot: input.mainRepoRoot, + }; + } + + const branch = normalizeBranch(input.checkout.currentBranch); + return { + cwd: input.cwd, + kind: deriveWorkspaceKind(input.checkout), + displayName: deriveWorkspaceDisplayName(input), + branch, + worktreeRoot: input.checkout.isGit ? (input.checkout.worktreeRoot ?? input.cwd) : null, + baseBranch: null, + isPaseoOwnedWorktree: input.checkout.isGit && input.checkout.isPaseoOwnedWorktree, + mainRepoRoot: input.checkout.isGit ? input.checkout.mainRepoRoot : null, + }; +} + +/** + * Applies live placement facts without rewriting the workspace's durable name + * or its creation-time base branch. + */ +export function reconcileWorkspacePlacement(input: { + workspace: PersistedWorkspaceRecord; + checkout: ProjectCheckoutLitePayload; + updatedAt: string; +}): WorkspacePlacementUpdate | null { + const observed = initialWorkspacePlacement({ + source: "checkout", + cwd: input.workspace.cwd, + checkout: input.checkout, + }); + const fields: Partial = {}; + if (input.workspace.kind !== observed.kind) fields.kind = observed.kind; + if (input.workspace.branch !== observed.branch) fields.branch = observed.branch; + if (input.workspace.worktreeRoot !== observed.worktreeRoot) + fields.worktreeRoot = observed.worktreeRoot; + if (input.workspace.isPaseoOwnedWorktree !== observed.isPaseoOwnedWorktree) + fields.isPaseoOwnedWorktree = observed.isPaseoOwnedWorktree; + if (input.workspace.mainRepoRoot !== observed.mainRepoRoot) + fields.mainRepoRoot = observed.mainRepoRoot; + + if (Object.keys(fields).length === 0) return null; + return { + workspace: { ...input.workspace, ...fields, updatedAt: input.updatedAt }, + fields, + }; +} + +/** Projects persisted placement onto the checkout shape sent over the wire. */ +export function checkoutFromPersistedWorkspacePlacement(input: { + workspace: PersistedWorkspaceRecord; + fallbackBranch?: string | null; + fallbackWorktreeRoot?: string | null; +}): ProjectPlacementPayload["checkout"] { + const { workspace } = input; + if (workspace.kind === "directory") { + return { + cwd: workspace.cwd, + isGit: false, + currentBranch: null, + remoteUrl: null, + worktreeRoot: null, + isPaseoOwnedWorktree: false, + mainRepoRoot: null, + }; + } + + const checkout = { + cwd: workspace.cwd, + currentBranch: workspace.branch ?? input.fallbackBranch ?? null, + remoteUrl: null, + worktreeRoot: workspace.worktreeRoot ?? input.fallbackWorktreeRoot ?? workspace.cwd, + }; + if (workspace.isPaseoOwnedWorktree && workspace.mainRepoRoot) { + return { + ...checkout, + isGit: true, + isPaseoOwnedWorktree: true, + mainRepoRoot: workspace.mainRepoRoot, + }; + } + return { + ...checkout, + isGit: true, + isPaseoOwnedWorktree: false, + mainRepoRoot: workspace.mainRepoRoot ?? null, + }; +} + +function normalizeBranch(branch: string | null | undefined): string | null { + const normalized = branch?.trim() ?? null; + return normalized && normalized.toUpperCase() !== "HEAD" ? normalized : null; +} + export function checkoutLiteFromGitSnapshot( cwd: string, git: { @@ -205,70 +226,3 @@ export function checkoutLiteFromGitSnapshot( mainRepoRoot: git.mainRepoRoot, }; } - -export async function detectStaleWorkspaces( - input: DetectStaleWorkspacesInput, -): Promise> { - const staleWorkspaceIds = new Set(); - - const existenceChecks = await Promise.all( - input.activeWorkspaces.map(async (workspace) => ({ - workspace, - exists: await input.checkDirectoryExists(workspace.cwd), - })), - ); - for (const { workspace, exists } of existenceChecks) { - if (!exists) { - staleWorkspaceIds.add(workspace.workspaceId); - } - } - - return staleWorkspaceIds; -} - -export function buildProjectPlacementForCwd(input: { - cwd: string; - checkout: ProjectCheckoutLitePayload; -}): ProjectPlacementPayload { - const membership = classifyDirectoryForProjectMembership(input); - return { - projectKey: membership.projectKey, - projectName: membership.projectName, - checkout: membership.checkout, - }; -} - -export function classifyDirectoryForProjectMembership(input: { - cwd: string; - checkout: ProjectCheckoutLitePayload; -}): DirectoryProjectMembership { - const normalizedCwd = resolve(input.cwd); - const checkout: ProjectCheckoutLitePayload = { - ...input.checkout, - cwd: normalizedCwd, - }; - - const projectKey = deriveProjectGroupingKey({ - cwd: checkout.worktreeRoot ?? normalizedCwd, - remoteUrl: checkout.remoteUrl, - mainRepoRoot: checkout.mainRepoRoot, - }); - - return { - cwd: normalizedCwd, - checkout, - workspaceDirectoryKey: deriveWorkspaceDirectoryKey(normalizedCwd, checkout), - workspaceKind: deriveWorkspaceKind(checkout), - workspaceDisplayName: deriveWorkspaceDisplayName({ - cwd: normalizedCwd, - checkout, - }), - projectKey, - projectName: deriveProjectGroupingName(projectKey), - projectRootPath: deriveProjectRootPath({ - cwd: normalizedCwd, - checkout, - }), - projectKind: deriveProjectKind(checkout), - }; -} diff --git a/packages/server/src/server/workspace-registry.test.ts b/packages/server/src/server/workspace-registry.test.ts index 0d14cea39..7c5f13ef2 100644 --- a/packages/server/src/server/workspace-registry.test.ts +++ b/packages/server/src/server/workspace-registry.test.ts @@ -1,6 +1,6 @@ import os from "node:os"; import path from "node:path"; -import { mkdtempSync, rmSync } from "node:fs"; +import { mkdirSync, mkdtempSync, rmSync, symlinkSync } from "node:fs"; import { beforeEach, afterEach, describe, expect, test } from "vitest"; @@ -99,43 +99,228 @@ describe("workspace registries", () => { expect(await projectRegistry.list()).toEqual([]); }); - test("PIN: two checkouts of the same git remote collapse into a single project record", async () => { - // Reproduces the situation in #987: two directories that share a git remote - // both derive the same projectKey/displayName. Because the registry is keyed - // by projectId, the second upsert overwrites the first — so the registry can - // only ever hold one record per remote, and there is no way to distinguish - // the two checkouts in the UI. + test("publishes only project mutations that change the persisted lifecycle", async () => { await projectRegistry.initialize(); + const mutations: Array<{ + kind: "upsert" | "archive" | "remove"; + projectId: string; + project: ReturnType | null; + }> = []; + const unsubscribe = projectRegistry.subscribeToMutations((mutation) => { + mutations.push(mutation); + }); + const active = createPersistedProjectRecord({ + projectId: "project-one", + rootPath: "/tmp/project-one", + kind: "non_git", + displayName: "project-one", + createdAt: "2026-03-01T00:00:00.000Z", + updatedAt: "2026-03-01T00:00:00.000Z", + }); + const archived = { + ...active, + updatedAt: "2026-03-02T00:00:00.000Z", + archivedAt: "2026-03-02T00:00:00.000Z", + }; - const remoteKey = "remote:github.com/acme/repo"; + await projectRegistry.upsert(active); + await projectRegistry.archive(active.projectId, archived.archivedAt); + await projectRegistry.archive(active.projectId, "2026-03-03T00:00:00.000Z"); + await projectRegistry.archive("project-unknown", "2026-03-03T00:00:00.000Z"); + await projectRegistry.remove(active.projectId); + await projectRegistry.remove(active.projectId); + await projectRegistry.remove("project-unknown"); + expect(mutations).toEqual([ + { kind: "upsert", projectId: active.projectId, project: active }, + { kind: "archive", projectId: active.projectId, project: archived }, + { kind: "remove", projectId: active.projectId, project: null }, + ]); + unsubscribe(); + }); + + test("atomically allocates one opaque project for concurrent exact-root adds", async () => { + await projectRegistry.initialize(); + const rootPath = path.join(tmpDir, "same-root"); + const projects = await Promise.all( + Array.from({ length: 20 }, () => + projectRegistry.getOrCreateActiveByRoot({ + rootPath, + kind: "non_git", + displayName: "same-root", + timestamp: "2026-03-01T00:00:00.000Z", + }), + ), + ); + + expect(new Set(projects.map((project) => project.projectId))).toEqual( + new Set([projects[0]!.projectId]), + ); + expect(projects[0]!.projectId).toMatch(/^prj_[0-9a-f]{16}$/); + expect(await projectRegistry.list()).toHaveLength(1); + }); + + test("keeps readable legacy IDs alongside newly allocated opaque IDs", async () => { + await projectRegistry.initialize(); await projectRegistry.upsert( createPersistedProjectRecord({ - projectId: remoteKey, - rootPath: "/home/me/work/repo", + projectId: "remote:github.com/acme/repo", + rootPath: "/tmp/legacy", kind: "git", - displayName: "acme/repo", + displayName: "repo", + createdAt: "2026-03-01T00:00:00.000Z", + updatedAt: "2026-03-01T00:00:00.000Z", + }), + ); + const opaque = await projectRegistry.getOrCreateActiveByRoot({ + rootPath: "/tmp/new", + kind: "non_git", + displayName: "new", + timestamp: "2026-03-01T00:00:00.000Z", + }); + expect((await projectRegistry.get("remote:github.com/acme/repo"))?.rootPath).toBe( + "/tmp/legacy", + ); + expect(opaque.projectId).toMatch(/^prj_[0-9a-f]{16}$/); + }); + + test("allocates a fresh opaque ID when only an archived exact root exists", async () => { + await projectRegistry.initialize(); + const rootPath = path.join(tmpDir, "archived-root"); + const archived = createPersistedProjectRecord({ + projectId: "prj_archived", + rootPath, + kind: "non_git", + displayName: "archived-root", + createdAt: "2026-03-01T00:00:00.000Z", + updatedAt: "2026-03-01T00:00:00.000Z", + archivedAt: "2026-03-02T00:00:00.000Z", + }); + await projectRegistry.upsert(archived); + + const created = await projectRegistry.getOrCreateActiveByRoot({ + rootPath, + kind: "non_git", + displayName: "archived-root", + timestamp: "2026-03-03T00:00:00.000Z", + }); + + expect(created).toMatchObject({ rootPath, archivedAt: null }); + expect(created.projectId).not.toBe(archived.projectId); + expect(await projectRegistry.get(archived.projectId)).toEqual(archived); + }); + + test("refreshes the oldest active legacy duplicate kind without rewriting its identity", async () => { + await projectRegistry.initialize(); + const rootPath = path.join(tmpDir, "legacy-root"); + const oldest = createPersistedProjectRecord({ + projectId: "remote:oldest", + rootPath, + kind: "git", + displayName: "oldest", + createdAt: "2026-03-01T00:00:00.000Z", + updatedAt: "2026-03-01T00:00:00.000Z", + }); + const duplicate = createPersistedProjectRecord({ + projectId: "remote:duplicate", + rootPath, + kind: "git", + displayName: "duplicate", + createdAt: "2026-03-02T00:00:00.000Z", + updatedAt: "2026-03-02T00:00:00.000Z", + }); + await projectRegistry.upsert(oldest); + await projectRegistry.upsert(duplicate); + + await expect( + projectRegistry.getOrCreateActiveByRoot({ + rootPath, + kind: "non_git", + displayName: "new-name", + timestamp: "2026-03-03T00:00:00.000Z", + }), + ).resolves.toEqual({ + ...oldest, + kind: "non_git", + updatedAt: "2026-03-03T00:00:00.000Z", + }); + expect(await projectRegistry.list()).toEqual([ + { ...oldest, kind: "non_git", updatedAt: "2026-03-03T00:00:00.000Z" }, + duplicate, + ]); + }); + + test("reuses an active project for Windows lexical-equivalent root spellings", async () => { + await projectRegistry.initialize(); + const first = await projectRegistry.getOrCreateActiveByRoot({ + rootPath: "C:\\Users\\Paseo\\Repo", + kind: "git", + displayName: "Repo", + timestamp: "2026-03-01T00:00:00.000Z", + }); + const second = await projectRegistry.getOrCreateActiveByRoot({ + rootPath: "c:/users/paseo/repo/.", + kind: "git", + displayName: "Repo", + timestamp: "2026-03-02T00:00:00.000Z", + }); + + expect(second).toEqual(first); + expect(await projectRegistry.list()).toEqual([first]); + }); + + test("keeps lexical and symlink root spellings distinct without realpath", async () => { + await projectRegistry.initialize(); + const target = path.join(tmpDir, "target"); + const link = path.join(tmpDir, "link"); + mkdirSync(target); + symlinkSync(target, link, process.platform === "win32" ? "junction" : "dir"); + + const targetProject = await projectRegistry.getOrCreateActiveByRoot({ + rootPath: target, + kind: "non_git", + displayName: "target", + timestamp: "2026-03-01T00:00:00.000Z", + }); + const linkProject = await projectRegistry.getOrCreateActiveByRoot({ + rootPath: link, + kind: "non_git", + displayName: "link", + timestamp: "2026-03-02T00:00:00.000Z", + }); + + expect(linkProject.projectId).not.toBe(targetProject.projectId); + expect(await projectRegistry.list()).toEqual([targetProject, linkProject]); + }); + + test("retries a generated project ID collision", async () => { + const generatedIds = ["prj_collision", "prj_fresh"]; + projectRegistry = new FileBackedProjectRegistry( + path.join(tmpDir, "projects", "projects.json"), + logger, + { projectIdFactory: () => generatedIds.shift() ?? "prj_unexpected" }, + ); + await projectRegistry.initialize(); + await projectRegistry.upsert( + createPersistedProjectRecord({ + projectId: "prj_collision", + rootPath: path.join(tmpDir, "existing"), + kind: "non_git", + displayName: "existing", createdAt: "2026-03-01T00:00:00.000Z", updatedAt: "2026-03-01T00:00:00.000Z", }), ); - await projectRegistry.upsert( - createPersistedProjectRecord({ - projectId: remoteKey, - rootPath: "/home/me/scratch/repo", - kind: "git", - displayName: "acme/repo", - createdAt: "2026-03-01T00:00:00.000Z", - updatedAt: "2026-03-02T00:00:00.000Z", - }), - ); + const created = await projectRegistry.getOrCreateActiveByRoot({ + rootPath: path.join(tmpDir, "new"), + kind: "non_git", + displayName: "new", + timestamp: "2026-03-02T00:00:00.000Z", + }); - const all = await projectRegistry.list(); - expect(all).toHaveLength(1); - expect(all[0]?.displayName).toBe("acme/repo"); - // Second upsert wins — the first rootPath is lost. - expect(all[0]?.rootPath).toBe("/home/me/scratch/repo"); + expect(created.projectId).toBe("prj_fresh"); + expect(await projectRegistry.list()).toHaveLength(2); }); test("project record schema accepts records without customName (legacy on-disk records)", async () => { @@ -212,6 +397,29 @@ describe("workspace registries", () => { expect(await workspaceRegistry.list()).toEqual([]); }); + test("refreshes workspace archive timestamps when an archive is repeated", async () => { + await workspaceRegistry.initialize(); + await workspaceRegistry.upsert( + createPersistedWorkspaceRecord({ + workspaceId: "workspace-one", + projectId: "project-one", + cwd: "/tmp/repo", + kind: "local_checkout", + displayName: "main", + createdAt: "2026-03-01T00:00:00.000Z", + updatedAt: "2026-03-01T00:00:00.000Z", + }), + ); + + await workspaceRegistry.archive("workspace-one", "2026-03-02T00:00:00.000Z"); + await workspaceRegistry.archive("workspace-one", "2026-03-03T00:00:00.000Z"); + + expect(await workspaceRegistry.get("workspace-one")).toMatchObject({ + archivedAt: "2026-03-03T00:00:00.000Z", + updatedAt: "2026-03-03T00:00:00.000Z", + }); + }); + test("composes concurrent workspace field updates without losing either change", async () => { await workspaceRegistry.initialize(); await workspaceRegistry.upsert( diff --git a/packages/server/src/server/workspace-registry.ts b/packages/server/src/server/workspace-registry.ts index bddf33734..f219d9158 100644 --- a/packages/server/src/server/workspace-registry.ts +++ b/packages/server/src/server/workspace-registry.ts @@ -4,7 +4,12 @@ import type { Logger } from "pino"; import { z } from "zod"; import { writeJsonFileAtomic } from "./atomic-file.js"; -import type { PersistedProjectKind, PersistedWorkspaceKind } from "./workspace-registry-model.js"; +import { areEquivalentPaths } from "../utils/path.js"; +import { + generateProjectId, + type PersistedProjectKind, + type PersistedWorkspaceKind, +} from "./workspace-registry-model.js"; const PersistedProjectRecordSchema = z.object({ projectId: z.string(), @@ -45,6 +50,11 @@ const PersistedWorkspaceRecordSchema = z.object({ .nullable() .optional() .transform((value) => value ?? null), + // Exact checkout/worktree root backing cwd. This differs from cwd when the + // selected project is a subdirectory inside a repository. Persist it so + // archive and recovery do not need the directory to still exist in order to + // recover placement. + worktreeRoot: z.string().nullable().default(null), // The base branch the worktree was created from (normalized like worktree.json's // baseRefName). Only worktree workspaces carry a base branch; checkout-branch // worktrees and directory/local_checkout workspaces leave it null. @@ -53,6 +63,8 @@ const PersistedWorkspaceRecordSchema = z.object({ .nullable() .optional() .transform((value) => value ?? null), + isPaseoOwnedWorktree: z.boolean().default(false), + mainRepoRoot: z.string().nullable().default(null), createdAt: z.string(), updatedAt: z.string(), archivedAt: z.string().nullable(), @@ -71,9 +83,23 @@ export interface ProjectRegistry { existsOnDisk(): Promise; list(): Promise; get(projectId: string): Promise; + getOrCreateActiveByRoot(input: { + rootPath: string; + kind: PersistedProjectKind; + displayName: string; + timestamp: string; + }): Promise; upsert(record: PersistedProjectRecord): Promise; archive(projectId: string, archivedAt: string): Promise; remove(projectId: string): Promise; + /** Central lifecycle seam for daemon-global project observers. */ + subscribeToMutations?( + listener: (mutation: { + kind: "upsert" | "archive" | "remove"; + projectId: string; + project: PersistedProjectRecord | null; + }) => void | Promise, + ): () => void; } export interface WorkspaceRegistry { @@ -162,24 +188,43 @@ class FileBackedRegistry { async archive(id: string, archivedAt: string): Promise { await this.load(); const existing = this.cache.get(id); - if (!existing) { - return; + if (!existing) return; + await this.persistArchive(existing, archivedAt); + } + + protected async archiveIfActive(id: string, archivedAt: string): Promise { + await this.load(); + const existing = this.cache.get(id); + if (!existing || existing.archivedAt) { + return null; } + return this.persistArchive(existing, archivedAt); + } + + private async persistArchive(existing: TRecord, archivedAt: string): Promise { const next = this.schema.parse({ ...existing, updatedAt: archivedAt, archivedAt, }); - this.cache.set(id, next); + this.cache.set(this.getId(next), next); await this.enqueuePersist(); + return next; } async remove(id: string): Promise { + await this.removeIfPresent(id); + } + + protected async removeIfPresent(id: string): Promise { await this.load(); - if (!this.cache.delete(id)) { - return; + const existing = this.cache.get(id); + if (!existing) { + return null; } + this.cache.delete(id); await this.enqueuePersist(); + return existing; } private async load(): Promise { @@ -219,7 +264,17 @@ export class FileBackedProjectRegistry extends FileBackedRegistry implements ProjectRegistry { - constructor(filePath: string, logger: Logger) { + private allocationQueue: Promise = Promise.resolve(); + private readonly projectIdFactory: () => string; + private readonly mutationListeners = new Set< + (mutation: { + kind: "upsert" | "archive" | "remove"; + projectId: string; + project: PersistedProjectRecord | null; + }) => void | Promise + >(); + + constructor(filePath: string, logger: Logger, options?: { projectIdFactory?: () => string }) { super({ filePath, logger, @@ -227,6 +282,89 @@ export class FileBackedProjectRegistry getId: (record) => record.projectId, component: "projects", }); + this.projectIdFactory = options?.projectIdFactory ?? generateProjectId; + } + + async getOrCreateActiveByRoot(input: { + rootPath: string; + kind: PersistedProjectKind; + displayName: string; + timestamp: string; + }): Promise { + const previous = this.allocationQueue; + let release!: () => void; + this.allocationQueue = new Promise((resolve) => (release = resolve)); + await previous; + try { + const active = (await this.list()) + .filter( + (project) => !project.archivedAt && areEquivalentPaths(project.rootPath, input.rootPath), + ) + .sort( + (left, right) => + Date.parse(left.createdAt) - Date.parse(right.createdAt) || + left.projectId.localeCompare(right.projectId), + )[0]; + if (active) { + if (active.kind === input.kind) return active; + const refreshed = { ...active, kind: input.kind, updatedAt: input.timestamp }; + await this.upsert(refreshed); + return refreshed; + } + + for (;;) { + const projectId = this.projectIdFactory(); + if (await this.get(projectId)) continue; + const record = createPersistedProjectRecord({ + projectId, + rootPath: input.rootPath, + kind: input.kind, + displayName: input.displayName, + createdAt: input.timestamp, + updatedAt: input.timestamp, + }); + await this.upsert(record); + return record; + } + } finally { + release(); + } + } + + subscribeToMutations( + listener: (mutation: { + kind: "upsert" | "archive" | "remove"; + projectId: string; + project: PersistedProjectRecord | null; + }) => void | Promise, + ): () => void { + this.mutationListeners.add(listener); + return () => this.mutationListeners.delete(listener); + } + + override async upsert(record: PersistedProjectRecord): Promise { + await super.upsert(record); + await this.notifyMutation({ kind: "upsert", projectId: record.projectId, project: record }); + } + + override async archive(projectId: string, archivedAt: string): Promise { + const project = await this.archiveIfActive(projectId, archivedAt); + if (!project) return; + await this.notifyMutation({ kind: "archive", projectId, project }); + } + + override async remove(projectId: string): Promise { + const project = await this.removeIfPresent(projectId); + if (!project) return; + await this.notifyMutation({ kind: "remove", projectId, project: null }); + } + + private async notifyMutation(mutation: { + kind: "upsert" | "archive" | "remove"; + projectId: string; + project: PersistedProjectRecord | null; + }): Promise { + await Promise.all([...this.mutationListeners].map((listener) => listener(mutation))); } } @@ -274,7 +412,10 @@ export function createPersistedWorkspaceRecord(input: { displayName: string; title?: string | null; branch?: string | null; + worktreeRoot?: string | null; baseBranch?: string | null; + isPaseoOwnedWorktree?: boolean; + mainRepoRoot?: string | null; createdAt: string; updatedAt: string; archivedAt?: string | null; @@ -284,7 +425,10 @@ export function createPersistedWorkspaceRecord(input: { ...input, title: input.title ?? null, branch: input.branch ?? null, + worktreeRoot: input.worktreeRoot ?? null, baseBranch: input.baseBranch ?? null, + isPaseoOwnedWorktree: input.isPaseoOwnedWorktree ?? false, + mainRepoRoot: input.mainRepoRoot ?? null, archivedAt: input.archivedAt ?? null, pinnedAt: input.pinnedAt ?? null, }); diff --git a/packages/server/src/server/worktree-bootstrap.ts b/packages/server/src/server/worktree-bootstrap.ts index 8cb880675..895ab2337 100644 --- a/packages/server/src/server/worktree-bootstrap.ts +++ b/packages/server/src/server/worktree-bootstrap.ts @@ -44,6 +44,7 @@ export interface RunAsyncWorktreeBootstrapOptions { // workspaceId-scoped archive tear these terminals down. workspaceId: string; worktree: WorktreeConfig; + workspaceCwd?: string; shouldBootstrap?: boolean; terminalManager: TerminalManager | null; appendTimelineItem: (item: AgentTimelineItem) => Promise; @@ -503,7 +504,8 @@ async function runWorktreeTerminalBootstrap( options: RunAsyncWorktreeBootstrapOptions, runtimeEnv: WorktreeRuntimeEnv, ): Promise { - const terminalSpecs = getWorktreeTerminalSpecs(options.worktree.worktreePath); + const workspaceCwd = options.workspaceCwd ?? options.worktree.worktreePath; + const terminalSpecs = getWorktreeTerminalSpecs(workspaceCwd); if (terminalSpecs.length === 0) { return; } @@ -540,7 +542,7 @@ async function runWorktreeTerminalBootstrap( terminalSpecs.map(async (spec): Promise => { try { const terminal = await terminalManager.createTerminal({ - cwd: options.worktree.worktreePath, + cwd: workspaceCwd, name: spec.name, env: runtimeEnv, workspaceId: options.workspaceId, @@ -597,6 +599,7 @@ export async function runAsyncWorktreeBootstrap( let runtimeEnv: WorktreeRuntimeEnv | null = null; const emitLiveTimelineItem = options.emitLiveTimelineItem; const progressAccumulator = createWorktreeSetupProgressAccumulator(); + const workspaceCwd = options.workspaceCwd ?? options.worktree.worktreePath; let liveEmitQueue = Promise.resolve(); const queueLiveRunningEmit = () => { @@ -632,12 +635,12 @@ export async function runAsyncWorktreeBootstrap( branchName: options.worktree.branchName, }); options.terminalManager?.registerCwdEnv({ - cwd: options.worktree.worktreePath, + cwd: workspaceCwd, env: runtimeEnv, }); setupResults = await runWorktreeSetupCommands({ - worktreePath: options.worktree.worktreePath, + worktreePath: workspaceCwd, branchName: options.worktree.branchName, cleanupOnFailure: false, runtimeEnv, diff --git a/packages/server/src/server/worktree-session.test.ts b/packages/server/src/server/worktree-session.test.ts index 6c8eacb02..a5de5baed 100644 --- a/packages/server/src/server/worktree-session.test.ts +++ b/packages/server/src/server/worktree-session.test.ts @@ -32,8 +32,15 @@ import { import type { TerminalManager } from "../terminal/terminal-manager.js"; import type { TerminalSession } from "../terminal/terminal.js"; import type { AgentStorage, StoredAgentRecord } from "./agent/agent-storage.js"; -import type { PersistedProjectRecord, PersistedWorkspaceRecord } from "./workspace-registry.js"; +import { + createPersistedProjectRecord, + type PersistedProjectRecord, + type PersistedWorkspaceRecord, + type ProjectRegistry, + type WorkspaceRegistry, +} from "./workspace-registry.js"; import type { ForgeService } from "../services/forge-service.js"; +import { areEquivalentPaths } from "../utils/path.js"; import { createPaseoWorktree as createPaseoWorktreeService, type CreatePaseoWorktreeFn, @@ -41,6 +48,7 @@ import { import { WorkspaceGitServiceImpl } from "./workspace-git-service.js"; import type { WorkspaceGitService } from "./workspace-git-service.js"; import { isPlatform } from "../test-utils/platform.js"; +import { createWorkspaceProvisioningService } from "./session/workspace-provisioning/workspace-provisioning-service.js"; interface LegacyCreateWorktreeTestOptions { branchName: string; @@ -294,6 +302,70 @@ function createPaseoWorktreeForTest(options: { forgeOverrides: { github: createGitHubServiceStub() }, }, }); + const projectRegistry: ProjectRegistry = { + initialize: async () => {}, + existsOnDisk: async () => true, + list: async () => Array.from(projects.values()), + get: async (projectId) => projects.get(projectId) ?? null, + getOrCreateActiveByRoot: async (allocation) => { + const existing = Array.from(projects.values()).find( + (project) => + areEquivalentPaths(project.rootPath, allocation.rootPath) && !project.archivedAt, + ); + if (existing) return existing; + const project = createPersistedProjectRecord({ + projectId: `prj_test_${projects.size + 1}`, + rootPath: allocation.rootPath, + kind: allocation.kind, + displayName: allocation.displayName, + createdAt: allocation.timestamp, + updatedAt: allocation.timestamp, + }); + projects.set(project.projectId, project); + return project; + }, + upsert: async (record) => { + options.events?.push(`project:${record.projectId}`); + projects.set(record.projectId, record); + }, + archive: async (projectId, archivedAt) => { + const project = projects.get(projectId); + if (project) projects.set(projectId, { ...project, archivedAt }); + }, + remove: async (projectId) => { + projects.delete(projectId); + }, + }; + const workspaceRegistry: WorkspaceRegistry = { + initialize: async () => {}, + existsOnDisk: async () => true, + list: async () => Array.from(workspaces.values()), + get: async (workspaceId) => workspaces.get(workspaceId) ?? null, + update: async (workspaceId, updater) => { + const workspace = workspaces.get(workspaceId); + if (!workspace) return null; + const updated = updater(workspace); + workspaces.set(workspaceId, updated); + return updated; + }, + upsert: async (record) => { + options.events?.push(`workspace:${record.workspaceId}`); + workspaces.set(record.workspaceId, record); + }, + archive: async (workspaceId, archivedAt) => { + const workspace = workspaces.get(workspaceId); + if (workspace) workspaces.set(workspaceId, { ...workspace, archivedAt }); + }, + remove: async (workspaceId) => { + workspaces.delete(workspaceId); + }, + }; + const workspaceProvisioning = createWorkspaceProvisioningService({ + projectRegistry, + workspaceRegistry, + workspaceGitService, + logger: createLogger(), + }); return (input, serviceOptions) => { return createPaseoWorktreeService(input, { @@ -301,22 +373,8 @@ function createPaseoWorktreeForTest(options: { ...(serviceOptions?.resolveDefaultBranch ? { resolveDefaultBranch: serviceOptions.resolveDefaultBranch } : {}), - projectRegistry: { - get: async (projectId) => projects.get(projectId) ?? null, - upsert: async (record) => { - options.events?.push(`project:${record.projectId}`); - projects.set(record.projectId, record); - }, - }, - workspaceRegistry: { - get: async (workspaceId) => workspaces.get(workspaceId) ?? null, - list: async () => Array.from(workspaces.values()), - upsert: async (record) => { - options.events?.push(`workspace:${record.workspaceId}`); - workspaces.set(record.workspaceId, record); - }, - }, workspaceGitService, + workspaceProvisioning, }); }; } @@ -548,6 +606,62 @@ describe("runWorktreeSetupInBackground", () => { } }); + test("runs setup from an exact workspace subdirectory", async () => { + const { tempDir, repoDir } = createGitRepo(); + cleanupPaths.push(tempDir); + const sourceWorkspaceCwd = path.join(repoDir, "packages", "app"); + mkdirSync(sourceWorkspaceCwd, { recursive: true }); + writeFileSync( + path.join(sourceWorkspaceCwd, "paseo.json"), + JSON.stringify({ + worktree: { + setup: ["pwd > setup-cwd.txt"], + }, + }), + ); + execFileSync("git", ["add", "."], { cwd: repoDir, stdio: "pipe" }); + execFileSync("git", ["-c", "commit.gpgsign=false", "commit", "-m", "add app setup"], { + cwd: repoDir, + stdio: "pipe", + }); + + const paseoHome = path.join(tempDir, ".paseo"); + const createdWorktree = await createLegacyWorktreeForTest({ + branchName: "feature-subdirectory-setup", + cwd: repoDir, + baseBranch: "main", + worktreeSlug: "feature-subdirectory-setup", + runSetup: false, + paseoHome, + }); + const workspaceCwd = path.join(createdWorktree.worktreePath, "packages", "app"); + + await runWorktreeSetupInBackground( + { + paseoHome, + emitWorkspaceUpdateForWorkspaceId: async () => {}, + cacheWorkspaceSetupSnapshot: () => {}, + emit: () => {}, + sessionLogger: createLogger(), + terminalManager: null, + archiveWorkspaceRecord: async () => {}, + }, + { + requestCwd: sourceWorkspaceCwd, + repoRoot: repoDir, + workspaceId: "ws-subdirectory-setup", + worktree: createdWorktree, + shouldBootstrap: true, + slug: "feature-subdirectory-setup", + worktreePath: createdWorktree.worktreePath, + workspaceCwd, + }, + ); + + expect(existsSync(path.join(workspaceCwd, "setup-cwd.txt"))).toBe(true); + expect(existsSync(path.join(createdWorktree.worktreePath, "setup-cwd.txt"))).toBe(false); + }); + test("emits running then completed snapshots for no-setup workspaces without auto-starting scripts", async () => { const { tempDir, repoDir } = createGitRepo({ paseoConfig: { @@ -1325,7 +1439,7 @@ describe("handleCreatePaseoWorktreeRequest", () => { workspace: { workspaceId: "ws-fix-attached-pr-context", projectId: "/tmp/repo", - cwd: "/tmp/worktrees/fix-attached-pr-context", + cwd: "/tmp/worktrees/fix-attached-pr-context/packages/app", kind: "worktree" as const, displayName: "fix-attached-pr-context", createdAt: "2026-04-30T00:00:00.000Z", @@ -1382,7 +1496,7 @@ describe("handleCreatePaseoWorktreeRequest", () => { }), expect.anything(), ); - expect(result.sessionConfig.cwd).toBe("/tmp/worktrees/fix-attached-pr-context"); + expect(result.sessionConfig.cwd).toBe("/tmp/worktrees/fix-attached-pr-context/packages/app"); }); test("buildAgentSessionConfig invalidates GitHub cache after branch setup mutations", async () => { diff --git a/packages/server/src/server/worktree-session.ts b/packages/server/src/server/worktree-session.ts index db296c8fe..5573a945f 100644 --- a/packages/server/src/server/worktree-session.ts +++ b/packages/server/src/server/worktree-session.ts @@ -244,7 +244,7 @@ export async function buildAgentSessionConfig( ), }, ); - cwd = createdWorktree.worktree.worktreePath; + cwd = createdWorktree.workspace.cwd; setupContinuation = createdWorktree.setupContinuation; createdWorkspaceId = createdWorktree.workspace.workspaceId; } else if (normalized.createNewBranch) { @@ -627,6 +627,7 @@ export async function createPaseoWorktreeWorkflow( shouldBootstrap: createdWorktree.created, slug, worktreePath: createdWorktree.worktree.worktreePath, + workspaceCwd: workspace.cwd, }); } }, 0); @@ -641,6 +642,7 @@ export async function createPaseoWorktreeWorkflow( agentId, workspaceId: workspace.workspaceId, worktree: createdWorktree.worktree, + workspaceCwd: workspace.cwd, shouldBootstrap: createdWorktree.created, terminalManager: setupContinuation.terminalManager, appendTimelineItem: (item) => setupContinuation.appendTimelineItem({ agentId, item }), @@ -683,6 +685,7 @@ export async function runWorktreeSetupInBackground( shouldBootstrap: boolean; slug: string; worktreePath: string; + workspaceCwd?: string; }, ): Promise { let worktree: WorktreeConfig = options.worktree; @@ -721,7 +724,8 @@ export async function runWorktreeSetupInBackground( if (!options.shouldBootstrap) { emitSetupProgress("completed", null); } else { - const setupCommands = getWorktreeSetupCommands(worktree.worktreePath); + const workspaceCwd = options.workspaceCwd ?? worktree.worktreePath; + const setupCommands = getWorktreeSetupCommands(workspaceCwd); if (setupCommands.length === 0) { setupStarted = true; emitSetupProgress("completed", null); @@ -732,12 +736,12 @@ export async function runWorktreeSetupInBackground( repoRootPath: options.repoRoot, }); dependencies.terminalManager?.registerCwdEnv({ - cwd: worktree.worktreePath, + cwd: workspaceCwd, env: runtimeEnv, }); setupStarted = true; setupResults = await runWorktreeSetupCommands({ - worktreePath: worktree.worktreePath, + worktreePath: workspaceCwd, branchName: worktree.branchName, cleanupOnFailure: false, repoRootPath: options.repoRoot, diff --git a/packages/server/src/server/worktree/commands.ts b/packages/server/src/server/worktree/commands.ts index 3fa7f601a..cedc5013c 100644 --- a/packages/server/src/server/worktree/commands.ts +++ b/packages/server/src/server/worktree/commands.ts @@ -122,15 +122,14 @@ export async function archiveCommand( dependencies: ArchiveCommandDependencies, input: ArchiveCommandInput, ): Promise { - const resolvedTarget = await resolveArchiveTarget(dependencies, input); + const targetPath = await resolveArchiveTarget(dependencies, input); const scope = input.scope ?? "workspace"; + const ownership = await isPaseoOwnedWorktreeCwd(targetPath, { + paseoHome: dependencies.paseoHome, + worktreesRoot: dependencies.paseoWorktreesBaseRoot, + }); if (scope === "worktree") { - const ownership = await isPaseoOwnedWorktreeCwd(resolvedTarget.targetPath, { - paseoHome: dependencies.paseoHome, - worktreesRoot: dependencies.paseoWorktreesBaseRoot, - }); - if (!ownership.allowed) { return { ok: false, @@ -141,10 +140,7 @@ export async function archiveCommand( } const result = await archiveByScope(dependencies, { - scope: { kind: "worktree", targetPath: resolvedTarget.targetPath }, - repoRoot: ownership.repoRoot ?? resolvedTarget.repoRoot ?? null, - repoWorktreesRoot: ownership.worktreeRoot, - paseoWorktreesBaseRoot: dependencies.paseoWorktreesBaseRoot, + scope: { kind: "worktree", targetPath }, requestId: input.requestId, }); @@ -155,11 +151,11 @@ export async function archiveCommand( } const workspaceId = - input.workspaceId ?? (await resolveWorkspaceIdAtPath(dependencies, resolvedTarget.targetPath)); + input.workspaceId ?? (await resolveWorkspaceIdAtPath(dependencies, targetPath)); if (!workspaceId) { dependencies.sessionLogger?.warn( - { targetPath: resolvedTarget.targetPath }, + { targetPath }, "Could not resolve workspace for archive; skipping", ); return { @@ -170,8 +166,6 @@ export async function archiveCommand( const result = await archiveByScope(dependencies, { scope: { kind: "workspace", workspaceId }, - repoRoot: resolvedTarget.repoRoot, - paseoWorktreesBaseRoot: dependencies.paseoWorktreesBaseRoot, requestId: input.requestId, }); @@ -181,28 +175,20 @@ export async function archiveCommand( }; } -interface ResolvedArchiveTarget { - targetPath: string; - repoRoot: string | null; -} - async function resolveArchiveTarget( dependencies: ArchiveCommandDependencies, input: ArchiveCommandInput, -): Promise { +): Promise { const repoRoot = input.repoRoot ?? null; if (input.worktreePath) { - return { targetPath: input.worktreePath, repoRoot }; + return input.worktreePath; } if (input.worktreeSlug) { if (!repoRoot) { throw new Error("repoRoot is required when worktreeSlug is supplied"); } - return { - targetPath: await resolveWorktreeSlugPath(dependencies, repoRoot, input.worktreeSlug), - repoRoot, - }; + return resolveWorktreeSlugPath(dependencies, repoRoot, input.worktreeSlug); } if (repoRoot && input.branchName) { @@ -211,7 +197,7 @@ async function resolveArchiveTarget( if (!match) { throw new Error(`Paseo worktree not found for branch ${input.branchName}`); } - return { targetPath: match.path, repoRoot }; + return match.path; } throw new Error("worktreePath, worktreeSlug, or repoRoot+branchName is required"); diff --git a/packages/server/src/utils/checkout-git.test.ts b/packages/server/src/utils/checkout-git.test.ts index 6d1b2f238..78369c64a 100644 --- a/packages/server/src/utils/checkout-git.test.ts +++ b/packages/server/src/utils/checkout-git.test.ts @@ -307,6 +307,13 @@ describe("checkout git utilities", () => { }); }); + it("reports a real non-git directory as non-git", async () => { + const nonGitDir = join(tempDir, "not-git-status"); + mkdirSync(nonGitDir, { recursive: true }); + + await expect(getCheckoutStatus(nonGitDir)).resolves.toEqual({ isGit: false }); + }); + it("returns null for getCurrentBranch in a repo with no commits", async () => { const emptyRepo = join(tempDir, "empty-repo"); mkdirSync(emptyRepo, { recursive: true }); diff --git a/packages/server/src/utils/checkout-git.ts b/packages/server/src/utils/checkout-git.ts index c680efcc5..46a35f66e 100644 --- a/packages/server/src/utils/checkout-git.ts +++ b/packages/server/src/utils/checkout-git.ts @@ -22,6 +22,7 @@ import { isPaseoOwnedWorktreeCwd, resolvePaseoWorktreesBaseRoot } from "./worktr import { type PaseoWorktreeMetadata, readPaseoWorktreeMetadata } from "./worktree-metadata.js"; const READ_ONLY_GIT_ENV = { GIT_OPTIONAL_LOCKS: "0", + LC_ALL: "C", } as const; /** @@ -804,11 +805,11 @@ export type CheckoutSnapshotFacts = pullRequestLookupTarget: PullRequestStatusLookupTarget | null; }; -function isGitError(error: unknown): boolean { +function isNotGitRepositoryError(error: unknown): boolean { if (!(error instanceof Error)) { return false; } - return /not a git repository/i.test(error.message) || /git repository/i.test(error.message); + return /not a git repository \(or any of the parent directories\): \.git/i.test(error.message); } async function requireGitRepo(cwd: string): Promise { @@ -865,8 +866,11 @@ async function getWorktreeRoot(cwd: string, context?: CheckoutContext): Promise< logger: context?.logger, }); return parseGitRevParsePath(stdout); - } catch { - return null; + } catch (error) { + if (isNotGitRepositoryError(error)) { + return null; + } + throw error; } } @@ -1533,35 +1537,29 @@ async function inspectCheckoutContext( cwd: string, context?: CheckoutContext, ): Promise { - try { - const root = await getWorktreeRoot(cwd, context); - if (!root) { - return null; - } - - const [currentBranch, remoteUrl, absoluteGitDir, gitCommonDir, paseoWorktree] = - await Promise.all([ - getCurrentBranch(cwd), - getOriginRemoteUrl(cwd), - resolveAbsoluteGitDir(cwd), - resolveGitCommonDir(cwd), - getPaseoWorktreeForCwd(cwd, context, root), - ]); - - return { - worktreeRoot: root, - currentBranch, - remoteUrl, - absoluteGitDir, - gitCommonDir, - paseoWorktree, - }; - } catch (error) { - if (isGitError(error)) { - return null; - } - throw error; + const root = await getWorktreeRoot(cwd, context); + if (!root) { + return null; } + + const [currentBranch, remoteUrl, absoluteGitDir, gitCommonDir, paseoWorktree] = await Promise.all( + [ + getCurrentBranch(cwd), + getOriginRemoteUrl(cwd), + resolveAbsoluteGitDir(cwd), + resolveGitCommonDir(cwd), + getPaseoWorktreeForCwd(cwd, context, root), + ], + ); + + return { + worktreeRoot: root, + currentBranch, + remoteUrl, + absoluteGitDir, + gitCommonDir, + paseoWorktree, + }; } function buildPullRequestLookupTargetFromBranchConfig( diff --git a/packages/server/src/utils/path.test.ts b/packages/server/src/utils/path.test.ts index d84a1bf6a..fbdf509f5 100644 --- a/packages/server/src/utils/path.test.ts +++ b/packages/server/src/utils/path.test.ts @@ -1,6 +1,14 @@ +import { mkdtempSync, mkdirSync, rmSync, symlinkSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; import { describe, expect, test } from "vitest"; -import { areEquivalentPaths, createPathEquivalenceMatcher, isPathInsideRoot } from "./path.js"; +import { + areEquivalentPaths, + createPathEquivalenceMatcher, + getRealpathAwareRelativePath, + isPathInsideRoot, +} from "./path.js"; describe("path equivalence", () => { test.each([ @@ -33,4 +41,23 @@ describe("path equivalence", () => { false, ); }); + + test.skipIf(process.platform === "win32")( + "derives the contained suffix from a realpath-equivalent root", + () => { + const tempDir = mkdtempSync(join(tmpdir(), "paseo-path-")); + try { + const realRoot = join(tempDir, "real-root"); + const nestedPath = join(realRoot, "packages", "app"); + const aliasRoot = join(tempDir, "root-alias"); + mkdirSync(nestedPath, { recursive: true }); + symlinkSync(realRoot, aliasRoot, "dir"); + + expect(getRealpathAwareRelativePath(aliasRoot, nestedPath)).toBe(join("packages", "app")); + expect(getRealpathAwareRelativePath(aliasRoot, tempDir)).toBeNull(); + } finally { + rmSync(tempDir, { recursive: true, force: true }); + } + }, + ); }); diff --git a/packages/server/src/utils/path.ts b/packages/server/src/utils/path.ts index f4d87a92a..150bb184d 100644 --- a/packages/server/src/utils/path.ts +++ b/packages/server/src/utils/path.ts @@ -59,22 +59,44 @@ export function createRealpathAwarePathMatcher(target: string): (candidate: stri } export function isPathInsideRoot(root: string, candidate: string): boolean { + return getRelativePathInsideRoot(root, candidate) !== null; +} + +/** + * Returns the candidate's relative suffix when it is inside root. + * + * The suffix is derived from the same lexical path pair used to prove + * containment. Callers that map an existing filesystem path into a new root + * must keep those two operations coupled. + */ +export function getRealpathAwareRelativePath(root: string, candidate: string): string | null { + const rootVariants = collectPathVariants(root); + const candidateVariants = collectPathVariants(candidate); + + for (const rootVariant of rootVariants) { + for (const candidateVariant of candidateVariants) { + const relativePath = getRelativePathInsideRoot(rootVariant, candidateVariant); + if (relativePath !== null) return relativePath; + } + } + + return null; +} + +export function isRealpathInsideRoot(root: string, candidate: string): boolean { + return getRealpathAwareRelativePath(root, candidate) !== null; +} + +function getRelativePathInsideRoot(root: string, candidate: string): string | null { const compareAsWindows = shouldCompareAsWindows(root, candidate); const platformPath = compareAsWindows ? nodePath.win32 : nodePath.posix; const normalizedRoot = normalizePathForComparison(root, compareAsWindows); const normalizedCandidate = normalizePathForComparison(candidate, compareAsWindows); const relative = platformPath.relative(normalizedRoot, normalizedCandidate); - return relative === "" || (!relative.startsWith("..") && !platformPath.isAbsolute(relative)); -} - -export function isRealpathInsideRoot(root: string, candidate: string): boolean { - const rootVariants = collectPathVariants(root); - const candidateVariants = collectPathVariants(candidate); - - return rootVariants.some((rootVariant) => - candidateVariants.some((candidateVariant) => isPathInsideRoot(rootVariant, candidateVariant)), - ); + return relative === "" || (!relative.startsWith("..") && !platformPath.isAbsolute(relative)) + ? relative + : null; } function collectPathVariants(value: string): string[] { diff --git a/packages/server/src/utils/worktree.test.ts b/packages/server/src/utils/worktree.test.ts index 0200c5a1c..4769bed7d 100644 --- a/packages/server/src/utils/worktree.test.ts +++ b/packages/server/src/utils/worktree.test.ts @@ -4,14 +4,24 @@ import { deriveWorktreeProjectHash, deletePaseoWorktree, isPaseoOwnedWorktreeCwd, + mapWorkspaceCwdToWorktree, slugify, type CreateWorktreeOptions, type WorktreeConfig, } from "./worktree"; import { execFileSync } from "child_process"; -import { mkdtempSync, mkdirSync, rmSync, existsSync, realpathSync, writeFileSync } from "fs"; +import { + mkdtempSync, + mkdirSync, + rmSync, + existsSync, + realpathSync, + symlinkSync, + writeFileSync, +} from "fs"; import { join } from "path"; import { tmpdir } from "os"; +import { createRealpathAwarePathMatcher } from "./path"; interface LegacyCreateWorktreeTestOptions { branchName: string; @@ -86,6 +96,12 @@ describe("paseo worktree manager", () => { const ownership = await isPaseoOwnedWorktreeCwd(created.worktreePath, { paseoHome }); expect(ownership.allowed).toBe(true); + await expect( + isPaseoOwnedWorktreeCwd(join(created.worktreePath, "packages", "app"), { paseoHome }), + ).resolves.toMatchObject({ + allowed: true, + worktreePath: created.worktreePath, + }); }); it("rejects paths that are not under the paseo worktrees root", async () => { @@ -97,6 +113,72 @@ describe("paseo worktree manager", () => { expect(ownership.allowed).toBe(false); }); + it("reports the source checkout root separately from Git's common directory", async () => { + const created = await createLegacyWorktreeForTest({ + branchName: "placement-root-branch", + cwd: repoDir, + baseBranch: "main", + worktreeSlug: "placement-root", + paseoHome, + }); + + const ownership = await isPaseoOwnedWorktreeCwd(created.worktreePath, { paseoHome }); + + expect(ownership.allowed).toBe(true); + expect(createRealpathAwarePathMatcher(repoDir)(ownership.repoRoot ?? "")).toBe(true); + expect(createRealpathAwarePathMatcher(created.worktreePath)(ownership.worktreePath ?? "")).toBe( + true, + ); + }); + + it("maps only root-contained workspace paths into a replacement worktree", () => { + const sourceWorktreePath = join(tempDir, "source-worktree"); + const targetWorktreePath = join(tempDir, "target-worktree"); + const nestedWorkspaceCwd = join(sourceWorktreePath, "packages", "app"); + + expect( + mapWorkspaceCwdToWorktree({ + sourceWorktreePath, + workspaceCwd: sourceWorktreePath, + targetWorktreePath, + }), + ).toBe(targetWorktreePath); + expect( + mapWorkspaceCwdToWorktree({ + sourceWorktreePath, + workspaceCwd: nestedWorkspaceCwd, + targetWorktreePath, + }), + ).toBe(join(targetWorktreePath, "packages", "app")); + expect(() => + mapWorkspaceCwdToWorktree({ + sourceWorktreePath, + workspaceCwd: join(tempDir, "outside-worktree"), + targetWorktreePath, + }), + ).toThrow("outside its source worktree"); + }); + + it.skipIf(process.platform === "win32")( + "maps a realpath-equivalent source workspace into the matching target subdirectory", + () => { + const sourceWorktreePath = join(tempDir, "source-worktree"); + const workspaceCwd = join(sourceWorktreePath, "packages", "app"); + const sourceAlias = join(tempDir, "source-alias"); + const targetWorktreePath = join(tempDir, "target-worktree"); + mkdirSync(workspaceCwd, { recursive: true }); + symlinkSync(sourceWorktreePath, sourceAlias, "dir"); + + expect( + mapWorkspaceCwdToWorktree({ + sourceWorktreePath: sourceAlias, + workspaceCwd, + targetWorktreePath, + }), + ).toBe(join(targetWorktreePath, "packages", "app")); + }, + ); + it("rejects the worktrees root itself and the per-repo hash dir", async () => { const projectHash = await deriveWorktreeProjectHash(repoDir); const worktreesRoot = join(paseoHome, "worktrees"); diff --git a/packages/server/src/utils/worktree.ts b/packages/server/src/utils/worktree.ts index 4b6f24e87..64d5ae2c7 100644 --- a/packages/server/src/utils/worktree.ts +++ b/packages/server/src/utils/worktree.ts @@ -35,7 +35,7 @@ import { resolvePaseoHome } from "../server/paseo-home.js"; import { createExternalProcessEnv } from "../server/paseo-env.js"; import { parseGitRevParsePath, resolveGitRevParsePath } from "./git-rev-parse-path.js"; import { validateBranchSlug } from "@getpaseo/protocol/branch-slug"; -import { expandTilde } from "./path.js"; +import { expandTilde, getRealpathAwareRelativePath, isPathInsideRoot } from "./path.js"; export { slugify, validateBranchSlug } from "@getpaseo/protocol/branch-slug"; @@ -724,11 +724,15 @@ export async function resolveWorktreeRuntimeEnv(options: { export async function runWorktreeTeardownCommands(options: { worktreePath: string; + teardownCwd?: string; branchName?: string; repoRootPath?: string; }): Promise { - // Read paseo.json from the worktree (it will have the same content as the source repo) - const teardownCommands = getWorktreeTeardownCommands(options.worktreePath); + const teardownCwd = options.teardownCwd ?? options.worktreePath; + if (getRealpathAwareRelativePath(options.worktreePath, teardownCwd) === null) { + throw new Error(`Worktree teardown cwd is outside the worktree: ${teardownCwd}`); + } + const teardownCommands = getWorktreeTeardownCommands(teardownCwd); if (teardownCommands.length === 0) { return []; } @@ -756,7 +760,7 @@ export async function runWorktreeTeardownCommands(options: { const results: WorktreeTeardownCommandResult[] = []; for (const cmd of teardownCommands) { const result = await execSetupCommand(cmd, { - cwd: options.worktreePath, + cwd: teardownCwd, env: teardownEnv, }); results.push(result); @@ -772,6 +776,23 @@ export async function runWorktreeTeardownCommands(options: { return results; } +export async function seedPaseoConfigFile(options: { + sourceCwd: string; + targetCwd: string; +}): Promise { + const sourceConfigPath = join(options.sourceCwd, "paseo.json"); + const targetConfigPath = join(options.targetCwd, "paseo.json"); + try { + await stat(targetConfigPath); + return; + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; + } + await copyFile(sourceConfigPath, targetConfigPath).catch((error) => { + if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; + }); +} + /** * Get the git common directory (shared across worktrees) for a given cwd. * This is where refs, objects, etc. are stored. @@ -845,6 +866,36 @@ export async function computeWorktreePath( return join(projectWorktreesRoot, slug); } +export function mapWorkspaceCwdToWorktree(input: { + sourceWorktreePath: string; + workspaceCwd: string; + targetWorktreePath: string; +}): string { + const relativeWorkspaceCwd = getRealpathAwareRelativePath( + input.sourceWorktreePath, + input.workspaceCwd, + ); + if (relativeWorkspaceCwd === null) { + throw new Error(`Workspace cwd is outside its source worktree: ${input.workspaceCwd}`); + } + + return mapWorkspaceRelativeCwdToWorktree({ + relativeWorkspaceCwd, + targetWorktreePath: input.targetWorktreePath, + }); +} + +export function mapWorkspaceRelativeCwdToWorktree(input: { + relativeWorkspaceCwd: string; + targetWorktreePath: string; +}): string { + const mappedCwd = resolve(input.targetWorktreePath, input.relativeWorkspaceCwd); + if (!isPathInsideRoot(input.targetWorktreePath, mappedCwd)) { + throw new Error(`Workspace cwd escapes its target worktree: ${input.relativeWorkspaceCwd}`); + } + return mappedCwd; +} + function normalizePathForOwnership(input: string): string { try { return realpathSync(input); @@ -878,13 +929,13 @@ export async function isPaseoOwnedWorktreeCwd( } const worktreesBaseRoot = resolvePaseoWorktreesBaseRoot(options); - const paseoWorktreesPrefix = normalizePathForOwnership(worktreesBaseRoot) + sep; + const relativePath = getRealpathAwareRelativePath(worktreesBaseRoot, resolvedCwd); // Ownership is defined by the path living under //[/...]. // The / prefix is Paseo-private — nothing else writes there — so the // path shape alone is sufficient proof of ownership, even when git has already // forgotten about the worktree. - if (!resolvedCwd.startsWith(paseoWorktreesPrefix)) { + if (relativePath === null) { return { allowed: false, ...(repoRoot !== undefined ? { repoRoot } : {}), @@ -892,8 +943,7 @@ export async function isPaseoOwnedWorktreeCwd( }; } - const relative = resolvedCwd.slice(paseoWorktreesPrefix.length); - const parts = relative.split(sep).filter((part) => part.length > 0); + const parts = relativePath.split(sep).filter((part) => part.length > 0); if (parts.length < 2) { return { allowed: false, @@ -907,7 +957,7 @@ export async function isPaseoOwnedWorktreeCwd( allowed: true, ...(repoRoot !== undefined ? { repoRoot } : {}), worktreeRoot: worktreesRoot, - worktreePath: resolvedCwd, + worktreePath: join(worktreesRoot, parts[1]), }; } @@ -977,10 +1027,9 @@ export async function listPaseoWorktrees({ envOverlay: READ_ONLY_GIT_ENV, }); - const rootPrefix = normalizePathForOwnership(projectWorktreesRoot) + sep; return parseWorktreeList(stdout) .map((entry) => Object.assign({}, entry, { path: normalizePathForOwnership(entry.path) })) - .filter((entry) => entry.path.startsWith(rootPrefix)) + .filter((entry) => getRealpathAwareRelativePath(projectWorktreesRoot, entry.path) !== null) .map((entry) => Object.assign({}, entry, { createdAt: resolveWorktreeCreatedAtIso(entry.path) }), ); @@ -1018,76 +1067,25 @@ export async function resolveExistingWorktreeForSlug({ }; } -export async function resolvePaseoWorktreeRootForCwd( - cwd: string, - options?: WorktreeRootOptions, -): Promise<{ repoRoot: string; worktreeRoot: string; worktreePath: string } | null> { - let gitCommonDir: string; - try { - gitCommonDir = await getGitCommonDir(cwd); - } catch { - return null; - } - - const worktreesRoot = await getPaseoWorktreesRoot( - cwd, - options?.paseoHome, - options?.worktreesRoot, - ); - const resolvedRoot = normalizePathForOwnership(worktreesRoot) + sep; - - let worktreeRoot: string | null = null; - try { - const { stdout } = await runGitCommand(["rev-parse", "--show-toplevel"], { - cwd, - envOverlay: READ_ONLY_GIT_ENV, - }); - worktreeRoot = parseGitRevParsePath(stdout); - } catch { - worktreeRoot = null; - } - - if (!worktreeRoot) { - return null; - } - - const resolvedWorktreeRoot = normalizePathForOwnership(worktreeRoot); - if (!resolvedWorktreeRoot.startsWith(resolvedRoot)) { - return null; - } - - const knownWorktrees = await listPaseoWorktrees({ - cwd, - paseoHome: options?.paseoHome, - worktreesRoot: options?.worktreesRoot, - }); - const match = knownWorktrees.find((entry) => entry.path === resolvedWorktreeRoot); - if (!match) { - return null; - } - - return { - repoRoot: gitCommonDir, - worktreeRoot: worktreesRoot, - worktreePath: match.path, - }; +export interface DeletePaseoWorktreeOptions { + cwd: string | null; + worktreePath?: string; + teardownCwds?: string[]; + worktreeSlug?: string; + worktreesRoot?: string; + paseoHome?: string; + worktreesBaseRoot?: string; } export async function deletePaseoWorktree({ cwd, worktreePath, + teardownCwds, worktreeSlug, worktreesRoot, paseoHome, worktreesBaseRoot, -}: { - cwd: string | null; - worktreePath?: string; - worktreeSlug?: string; - worktreesRoot?: string; - paseoHome?: string; - worktreesBaseRoot?: string; -}): Promise { +}: DeletePaseoWorktreeOptions): Promise { if (!worktreePath && !worktreeSlug) { throw new Error("worktreePath or worktreeSlug is required"); } @@ -1104,25 +1102,30 @@ export async function deletePaseoWorktree({ throw new Error("cwd or worktreesRoot is required to delete a Paseo worktree"); } - const resolvedRoot = normalizePathForOwnership(resolvedWorktreesRoot) + sep; const requestedPath = worktreePath ?? join(resolvedWorktreesRoot, worktreeSlug!); const resolvedRequested = normalizePathForOwnership(requestedPath); + const ownership = await isPaseoOwnedWorktreeCwd(requestedPath, { + paseoHome, + worktreesRoot: worktreesBaseRoot, + }); const resolvedWorktree = - ( - await resolvePaseoWorktreeRootForCwd(requestedPath, { - paseoHome, - worktreesRoot: worktreesBaseRoot, - }) - )?.worktreePath ?? resolvedRequested; + ownership.allowed && ownership.worktreePath ? ownership.worktreePath : resolvedRequested; - if (!resolvedWorktree.startsWith(resolvedRoot)) { + const relativeWorktreePath = getRealpathAwareRelativePath( + resolvedWorktreesRoot, + resolvedWorktree, + ); + if (relativeWorktreePath === null || relativeWorktreePath === "") { throw new Error("Refusing to delete non-Paseo worktree"); } if (await pathExists(resolvedWorktree)) { - await runWorktreeTeardownCommands({ - worktreePath: resolvedWorktree, - }); + for (const teardownCwd of teardownCwds ?? [resolvedWorktree]) { + await runWorktreeTeardownCommands({ + worktreePath: resolvedWorktree, + teardownCwd, + }); + } } if (cwd) { @@ -1150,6 +1153,27 @@ export async function deletePaseoWorktree({ } } +export async function rollbackCreatedPaseoWorktree( + options: DeletePaseoWorktreeOptions, + cause: unknown, +): Promise { + let cleanupError: unknown; + try { + await deletePaseoWorktree(options); + } catch (error) { + cleanupError = error; + } + if (cleanupError) { + const failure = new Error( + `${cause instanceof Error ? cause.message : "Worktree workflow failed"}; rollback also failed: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`, + { cause }, + ); + Object.assign(failure, { cleanupError }); + throw failure; + } + throw cause; +} + async function pathExists(path: string): Promise { try { await stat(path); @@ -1243,18 +1267,7 @@ export const createWorktree = async ({ : {}), }); - // If paseo.json exists in the main repo but wasn't checked into the worktree - // (e.g. uncommitted on first-time setup), seed the worktree with it so setup - // commands and scripts pick up the user's intended config. - const mainConfigPath = join(cwd, "paseo.json"); - const worktreeConfigPath = join(worktreePath, "paseo.json"); - try { - await stat(worktreeConfigPath); - } catch { - await copyFile(mainConfigPath, worktreeConfigPath).catch((err) => { - if ((err as NodeJS.ErrnoException).code !== "ENOENT") throw err; - }); - } + await seedPaseoConfigFile({ sourceCwd: cwd, targetCwd: worktreePath }); if (runSetup) { await runWorktreeSetupCommands({ From a414f8ea8572412cae498bcb62cac9429cc13bdf Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Fri, 17 Jul 2026 20:20:57 +0200 Subject: [PATCH 16/38] Connect your Paseo daemon to Hub (#2035) * feat(hub): connect daemons to Paseo Hub Make Hub an explicit daemon-owned relationship with local-only management and scoped access to Hub-owned executions. * fix(hub): harden relationship boundaries * fix(hub): harden relationship lifecycle * fix(hub): isolate CLI test entrypoint * fix(hub): run CLI tests from workspace source * fix(hub): settle failed relationship connections * fix(hub): resume interrupted owned turns Provider session rehydration does not continue foreground work lost during daemon shutdown. Persist narrowly scoped Hub execution intent and replay only an interrupted running initial turn. * fix(hub): harden relationship lifecycle Keep optional Hub authority from blocking daemon startup, revoke ambiguous enrollments durably, and close owned agents when their relationship no longer exists. Reject remote CLI connect targets before transmitting enrollment authority. * fix(hub): stop replaying interrupted turns Daemon restart cannot safely guarantee prompt idempotency across providers. Persist the normal closed session state while retaining Hub relationship, execution, and agent identity. * fix(hub): fail creates when prompts cannot start * fix: make Hub lifecycle cleanup deterministic * fix(hub): preserve fresh enrollment authority * fix(hub): contain enrollment retry failures * fix(hub): reject invalid socket transport URLs * fix(hub): bind socket transport to Hub authority * fix(hub): close relationship lifecycle gaps * test(hub): stabilize lifecycle coverage on Windows * fix(hub): close execution authority races * fix(hub): return relationship command errors * fix(app): preserve workspace navigation compatibility * fix(hub): correct relationship trust boundaries Authenticated daemon sessions own relationship management regardless of transport. The separate Hub session remains operation-allowlisted, rejects malformed execution inputs, and uses bounded outbound handshakes. * refactor(hub): authorize execution through sessions * fix(hub): validate persisted origins * fix(hub): retain local execution grants * fix(hub): enforce session scope boundaries Make session authority explicit and mutable without adding scope negotiation. Fence persisted Hub scopes and retire in-flight execution authority during cleanup and re-enrollment. * fix(server): preserve main session compatibility --- docs/architecture.md | 5 + docs/hub.md | 72 + packages/cli/src/cli.ts | 2 + packages/cli/src/commands/hub/index.ts | 104 ++ packages/client/src/daemon-client.test.ts | 19 + packages/client/src/daemon-client.ts | 34 + packages/protocol/src/messages.hub.test.ts | 210 +++ packages/protocol/src/messages.ts | 143 ++ .../server/src/server/agent/agent-loading.ts | 1 + .../server/src/server/agent/agent-manager.ts | 21 +- .../server/src/server/agent/agent-owner.ts | 16 + .../src/server/agent/agent-projections.ts | 1 + .../server/src/server/agent/agent-storage.ts | 41 +- .../create-agent-lifecycle-dispatch.test.ts | 47 + .../agent/create-agent-lifecycle-dispatch.ts | 82 +- .../server/agent/create-agent/create.test.ts | 52 + .../src/server/agent/create-agent/create.ts | 35 +- packages/server/src/server/bootstrap.ts | 88 +- .../src/server/daemon-client.e2e.test.ts | 1 + .../src/server/hub/daemon-executions.test.ts | 152 ++ .../src/server/hub/daemon-executions.ts | 297 ++++ .../server/hub/execution-controller.test.ts | 86 + .../src/server/hub/execution-controller.ts | 129 ++ .../hub/execution-session.websocket.test.ts | 145 ++ .../hub/relationship-controller.test.ts | 806 ++++++++++ .../src/server/hub/relationship-controller.ts | 556 +++++++ .../server/hub/relationship-remote.test.ts | 551 +++++++ .../src/server/hub/relationship-remote.ts | 187 +++ .../src/server/hub/relationship-retry.test.ts | 26 + .../src/server/hub/relationship-retry.ts | 16 + .../hub/test-utils/relationship-harness.ts | 1409 +++++++++++++++++ .../server/src/server/persistence-hooks.ts | 2 + packages/server/src/server/session.test.ts | 83 +- packages/server/src/server/session.ts | 83 + .../session.workspace-git-watch.test.ts | 1 + ...on.workspace-resolution-invariants.test.ts | 1 + .../src/server/session.workspaces.test.ts | 9 + .../session/daemon/daemon-session.test.ts | 57 + .../server/session/daemon/daemon-session.ts | 54 + .../snapshot-mutation-ownership.test.ts | 1 + .../server/test-utils/fake-agent-client.ts | 7 + .../src/server/test-utils/hub-cli-entry.ts | 4 + .../websocket-server.notifications.test.ts | 1 + ...cket-server.terminal-notifications.test.ts | 1 + .../server/src/server/websocket-server.ts | 240 ++- .../server/src/server/wire-compat.test.ts | 1 + 46 files changed, 5799 insertions(+), 80 deletions(-) create mode 100644 docs/hub.md create mode 100644 packages/cli/src/commands/hub/index.ts create mode 100644 packages/protocol/src/messages.hub.test.ts create mode 100644 packages/server/src/server/agent/agent-owner.ts create mode 100644 packages/server/src/server/agent/create-agent-lifecycle-dispatch.test.ts create mode 100644 packages/server/src/server/hub/daemon-executions.test.ts create mode 100644 packages/server/src/server/hub/daemon-executions.ts create mode 100644 packages/server/src/server/hub/execution-controller.test.ts create mode 100644 packages/server/src/server/hub/execution-controller.ts create mode 100644 packages/server/src/server/hub/execution-session.websocket.test.ts create mode 100644 packages/server/src/server/hub/relationship-controller.test.ts create mode 100644 packages/server/src/server/hub/relationship-controller.ts create mode 100644 packages/server/src/server/hub/relationship-remote.test.ts create mode 100644 packages/server/src/server/hub/relationship-remote.ts create mode 100644 packages/server/src/server/hub/relationship-retry.test.ts create mode 100644 packages/server/src/server/hub/relationship-retry.ts create mode 100644 packages/server/src/server/hub/test-utils/relationship-harness.ts create mode 100644 packages/server/src/server/test-utils/hub-cli-entry.ts diff --git a/docs/architecture.md b/docs/architecture.md index 405602def..9d9af4bb7 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -134,6 +134,11 @@ Enables remote access when the daemon is behind a firewall. See [SECURITY.md](../SECURITY.md) for the full threat model. +### Paseo Hub + +The optional Hub relationship is daemon-outbound and does not use the relay. Its connection, +authorization, ownership, persistence, and lifecycle contract is documented in [hub.md](hub.md). + ### `packages/desktop` — Desktop app (Electron) Electron wrapper for macOS, Linux, and Windows. diff --git a/docs/hub.md b/docs/hub.md new file mode 100644 index 000000000..4a6d736eb --- /dev/null +++ b/docs/hub.md @@ -0,0 +1,72 @@ +# Paseo Hub relationship + +Paseo Hub is an explicit opt-in connection from one Paseo daemon to one Hub. Running a daemon does +not register it with a Hub. The relationship begins only when a user runs +`paseo hub connect --token ` from the daemon machine. + +## Connection and authority + +The daemon enrolls over HTTP(S), then opens and maintains a direct outbound WebSocket to the Hub. +The Hub never discovers or acquires the daemon through Paseo's relay. The relay remains an optional +encrypted path for normal Paseo clients and has no role in Hub enrollment, authentication, dispatch, +or reconnects. + +The daemon persists a relationship ID and private connection credential before enrollment. The +relationship is independent of its current transport, so a future transport can replace the direct +WebSocket without pairing again. The current foundation supports one Hub relationship per daemon. + +Normal authenticated daemon sessions may run the `hub.management.daemon.connect`, +`hub.management.daemon.get_status`, and `hub.management.daemon.disconnect` RPCs. Hub connections +receive only `hub.execution.*` authority, so execution credentials cannot manage the relationship. + +## Session grants and execution ownership + +Trusted clients and the Hub use the same `Session` implementation. The connection boundary supplies +grants: trusted clients receive `*`, while an enrolled Hub connection receives its persisted +`hub.execution.*` grant. One matcher handles exact RPC names and trailing namespace wildcards for +both inbound requests and outbound messages. A denied request returns the ordinary `rpc_error` +shape. + +The Hub connection still has a narrow lifecycle boundary: it has no trusted-client hello/resume, +browser, binary, retained-session, or broadcast state. Its outbound execution events include only +agents owned by that daemon identity, so unrelated local agents remain outside the Hub surface. + +Each Hub create carries an execution ID. The daemon stores that ID with the agent's relationship +owner before acknowledging creation. Duplicate or replayed creates for the same daemon and +execution resolve to the same durable agent. After a lost response, reconnect, or daemon restart, +the Hub retries `hub.execution.agent.create.request` with the same execution ID. The idempotent +response returns the existing agent and its current state; there is no separate reconciliation RPC. +Transient stream frames are not durably replayed. + +Daemon restart preserves the Hub relationship and owned execution identity, but interrupts any +active turn. The daemon persists that agent as `closed`; an idempotent create retry returns the same +daemon, execution, and agent identity with that terminal state. Paseo never stores or automatically +replays the original prompt. A duplicate create returns the existing agent without starting another +turn. + +Hub creates use the same agent creation path as trusted clients. They may select any existing +worktree target shape and may request `autoArchive`. Worktree creation and terminal auto-archive use +the shared workspace-aware lifecycle policy; Hub does not have a second launch or cleanup path. + +## Disconnect and revocation + +Normal socket loss reconnects the active relationship with bounded exponential backoff and jitter. +Daemon restart loads the same relationship and credential and reconnects without another enrollment +ceremony. + +Hub authentication rejection or close code `4403` permanently revokes the local relationship. The +daemon deletes its credential, stops reconnecting, and retains only the relationship ID, Hub origin, +scopes, and a sanitized reason for status reporting. + +`paseo hub disconnect` disables socket reconnect before requesting remote revocation. If the Hub is +offline, the daemon persists `disconnecting` and retries revocation across daemon restarts without +opening a Hub socket. This also covers an enrollment whose request may have succeeded but whose +response was lost. `--force` removes local authority immediately and warns that remote revocation may +still be pending. + +## Cross-repository compatibility + +The consumer implementation lives in Paseo Cloud. Cloud owns its copy of the Hub wire schemas and +has no Paseo runtime or build dependency. Cross-repository end-to-end verification separately builds +a Paseo source checkout and exercises the real daemon, CLI, direct WebSocket, Cloud service, and +Postgres. That compatibility fixture is not a package dependency or fallback implementation. diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index 9c98310ba..318252802 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -9,6 +9,7 @@ import { createScheduleCommand } from "./commands/schedule/index.js"; import { createSpeechCommand } from "./commands/speech/index.js"; import { createTerminalCommand } from "./commands/terminal/index.js"; import { createWorktreeCommand } from "./commands/worktree/index.js"; +import { createHubCommand } from "./commands/hub/index.js"; import { createHooksCommand } from "./commands/hooks.js"; import { startCommand as daemonStartCommand } from "./commands/daemon/start.js"; import { runStatusCommand as runDaemonStatusCommand } from "./commands/daemon/status.js"; @@ -160,6 +161,7 @@ export function createCli(): Command { // Daemon commands program.addCommand(createDaemonCommand()); + program.addCommand(createHubCommand()); // Chat commands program.addCommand(createChatCommand()); diff --git a/packages/cli/src/commands/hub/index.ts b/packages/cli/src/commands/hub/index.ts new file mode 100644 index 000000000..fce9f1c65 --- /dev/null +++ b/packages/cli/src/commands/hub/index.ts @@ -0,0 +1,104 @@ +import { Command } from "commander"; +import { withOutput, type ListResult, type OutputSchema } from "../../output/index.js"; +import { addJsonAndDaemonHostOptions } from "../../utils/command-options.js"; +import { connectToDaemon } from "../../utils/client.js"; + +interface HubRow { + state: string; + daemonId: string | null; + hub: string | null; + scopes: string; + connectedAt: string | null; + error: string | null; + warning?: string; +} + +const schema: OutputSchema = { + idField: "state", + columns: [ + { header: "STATE", field: "state" }, + { header: "HUB", field: "hub" }, + { header: "DAEMON", field: "daemonId" }, + { header: "SCOPES", field: "scopes" }, + { header: "CONNECTED", field: "connectedAt" }, + { header: "ERROR", field: "error" }, + { header: "WARNING", field: "warning" }, + ], +}; + +function result( + status: { + state: string; + daemonId: string | null; + hubOrigin: string | null; + scopes: string[]; + connectedAt: string | null; + lastError: string | null; + }, + warning?: string, +): ListResult { + return { + type: "list", + data: [ + { + state: status.state, + daemonId: status.daemonId, + hub: status.hubOrigin, + scopes: status.scopes.join(", "), + connectedAt: status.connectedAt, + error: status.lastError, + warning, + }, + ], + schema, + }; +} + +async function withClient( + host: string | undefined, + action: (client: Awaited>) => Promise, +): Promise { + const client = await connectToDaemon({ host }); + try { + return await action(client); + } finally { + await client.close().catch(() => undefined); + } +} + +export function createHubCommand(): Command { + const hub = new Command("hub").description("Manage this daemon's Paseo Hub relationship"); + addJsonAndDaemonHostOptions( + hub.command("connect").argument("").requiredOption("--token "), + ).action( + withOutput(async (...args) => { + const url = args[0] as string; + const options = args.at(-2) as { token: string; host?: string }; + return withClient(options.host, async (client) => + result((await client.connectHub(url, options.token)).status), + ); + }), + ); + addJsonAndDaemonHostOptions(hub.command("status")).action( + withOutput(async (...args) => { + const options = args.at(-2) as { host?: string }; + return withClient(options.host, async (client) => + result((await client.getHubStatus()).status), + ); + }), + ); + addJsonAndDaemonHostOptions( + hub + .command("disconnect") + .option("--force", "Remove local authority even if the Hub is offline"), + ).action( + withOutput(async (...args) => { + const options = args.at(-2) as { host?: string; force?: boolean }; + return withClient(options.host, async (client) => { + const response = await client.disconnectHub(options.force ?? false); + return result(response.status, response.warning); + }); + }), + ); + return hub; +} diff --git a/packages/client/src/daemon-client.test.ts b/packages/client/src/daemon-client.test.ts index 723ffd91b..37a766d23 100644 --- a/packages/client/src/daemon-client.test.ts +++ b/packages/client/src/daemon-client.test.ts @@ -221,6 +221,25 @@ test("advertises consumer-provided browser automation capabilities", async () => }); }); +test("Hub management requires daemon support before dispatching requests", async () => { + const mock = createMockTransport(); + const client = new DaemonClient({ + url: "ws://test", + clientId: "hub_feature_gate_unit_test", + transportFactory: () => mock.transport, + reconnect: { enabled: false }, + }); + clients.push(client); + const connecting = client.connect(); + mock.triggerOpen(); + await connecting; + + await expect(client.getHubStatus()).rejects.toThrow( + "Update the host to use Hub relationship management.", + ); + expect(mock.sent).toEqual([]); +}); + test("sets the complete viewed timeline subscription only when the daemon supports it", async () => { const supportedTransport = createMockTransport(); const supportedClient = new DaemonClient({ diff --git a/packages/client/src/daemon-client.ts b/packages/client/src/daemon-client.ts index f7731112a..6eaa0b155 100644 --- a/packages/client/src/daemon-client.ts +++ b/packages/client/src/daemon-client.ts @@ -4269,6 +4269,33 @@ export class DaemonClient { }); } + async connectHub(hubUrl: string, token: string, requestId?: string) { + this.requireHubRelationshipSupport(); + return this.sendCorrelatedSessionRequest({ + requestId, + message: { type: "hub.management.daemon.connect.request", hubUrl, token }, + responseType: "hub.management.daemon.connect.response", + }); + } + + async getHubStatus(requestId?: string) { + this.requireHubRelationshipSupport(); + return this.sendCorrelatedSessionRequest({ + requestId, + message: { type: "hub.management.daemon.get_status.request" }, + responseType: "hub.management.daemon.get_status.response", + }); + } + + async disconnectHub(force = false, requestId?: string) { + this.requireHubRelationshipSupport(); + return this.sendCorrelatedSessionRequest({ + requestId, + message: { type: "hub.management.daemon.disconnect.request", force }, + responseType: "hub.management.daemon.disconnect.response", + }); + } + async getDaemonPairingOffer( options?: DaemonPairingOfferOptions, ): Promise { @@ -5089,6 +5116,13 @@ export class DaemonClient { return this.lastServerInfoMessage; } + private requireHubRelationshipSupport(): void { + // COMPAT(hubRelationship): added in v0.1.X, drop the gate when floor >= v0.1.X. + if (this.lastServerInfoMessage?.features?.hubRelationship !== true) { + throw new Error("Update the host to use Hub relationship management."); + } + } + private resolveTransportUrlForAttempt(): string { return this.config.url; } diff --git a/packages/protocol/src/messages.hub.test.ts b/packages/protocol/src/messages.hub.test.ts new file mode 100644 index 000000000..ae63b0dc6 --- /dev/null +++ b/packages/protocol/src/messages.hub.test.ts @@ -0,0 +1,210 @@ +import { describe, expect, test } from "vitest"; +import { z } from "zod"; + +import { + HubMessageCorrelationError, + SessionInboundMessageSchema, + SessionOutboundMessageSchema, + parseHubExecutionOutboundMessage, +} from "./messages.js"; + +const agent = { + id: "agent-1", + provider: "codex", + cwd: "/workspace", + model: null, + createdAt: "2026-07-13T00:00:00.000Z", + updatedAt: "2026-07-13T00:00:00.000Z", + lastUserMessageAt: null, + status: "idle", + capabilities: { + supportsStreaming: true, + supportsSessionPersistence: true, + supportsDynamicModes: true, + supportsMcpServers: false, + supportsReasoningStream: true, + supportsToolInvocations: true, + supportsRewindConversation: false, + supportsRewindFiles: false, + supportsRewindBoth: false, + }, + currentModeId: null, + availableModes: [], + pendingPermissions: [], + persistence: null, + title: null, + labels: {}, +}; + +// Frozen at the Hub create request shape shipped before worktree and autoArchive. +const PreviousHubAgentCreateRequestSchema = z.object({ + type: z.literal("hub.execution.agent.create.request"), + requestId: z.string(), + executionId: z.string(), + provider: z.string(), + cwd: z.string(), + workspaceId: z.string().optional(), + prompt: z.string(), + model: z.string().optional(), + modeId: z.string().optional(), + thinkingOptionId: z.string().optional(), + featureValues: z.record(z.string(), z.unknown()).optional(), + env: z.record(z.string(), z.string()).optional(), +}); + +describe("Hub session protocol", () => { + test("accepts the Hub execution create request", () => { + const message = { + type: "hub.execution.agent.create.request", + requestId: "request-1", + executionId: "execution-1", + provider: "codex", + cwd: "/workspace", + prompt: "Implement the requested change", + modeId: "code", + }; + + expect(SessionInboundMessageSchema.parse(message)).toEqual(message); + }); + + test.each([ + undefined, + { mode: "branch-off", newBranch: "hub-work", base: "main" }, + { mode: "checkout-branch", branch: "existing-work" }, + { mode: "checkout-pr", prNumber: 42 }, + ])("accepts Hub create worktree target %#", (worktree) => { + const message = { + type: "hub.execution.agent.create.request", + requestId: "hub-worktree", + executionId: "execution-worktree", + provider: "codex", + cwd: "/repo", + prompt: "Work in the requested target", + ...(worktree ? { worktree, autoArchive: true } : {}), + }; + + expect(SessionInboundMessageSchema.parse(message)).toEqual(message); + }); + + test("the previous Hub create parser ignores additive worktree and auto-archive fields", () => { + const newRequest = { + type: "hub.execution.agent.create.request" as const, + requestId: "hub-worktree", + executionId: "execution-worktree", + provider: "codex", + cwd: "/repo", + prompt: "Work in the requested target", + worktree: { mode: "branch-off", newBranch: "hub-work", base: "main" }, + autoArchive: true, + }; + + expect(PreviousHubAgentCreateRequestSchema.parse(newRequest)).toEqual({ + type: "hub.execution.agent.create.request", + requestId: "hub-worktree", + executionId: "execution-worktree", + provider: "codex", + cwd: "/repo", + prompt: "Work in the requested target", + }); + }); + + test.each([ + { + type: "hub.execution.agent.create.response", + payload: { + requestId: "request-1", + executionId: "execution-1", + agentId: "agent-1", + agent, + success: true, + error: null, + }, + }, + { + type: "hub.execution.agent.update", + payload: { executionId: "execution-1", agentId: "agent-1", agent }, + }, + { + type: "hub.execution.agent.stream", + payload: { + executionId: "execution-1", + agentId: "agent-1", + event: { type: "turn_started", provider: "codex" }, + }, + }, + ])("accepts outbound variant $type", (message) => { + expect(SessionOutboundMessageSchema.parse(message)).toEqual(message); + expect(parseHubExecutionOutboundMessage(message)).toEqual(message); + }); + + test("rejects a Hub update whose correlated agent ids disagree", () => { + const malformed = { + type: "hub.execution.agent.update", + payload: { executionId: "execution-1", agentId: "agent-2", agent }, + }; + + expect(SessionOutboundMessageSchema.safeParse(malformed).success).toBe(true); + expect(() => parseHubExecutionOutboundMessage(malformed)).toThrow(HubMessageCorrelationError); + }); + + test.each([ + { + type: "hub.management.daemon.connect.request", + requestId: "r1", + hubUrl: "https://hub.example", + token: "token", + }, + { type: "hub.management.daemon.get_status.request", requestId: "r2" }, + { type: "hub.management.daemon.disconnect.request", requestId: "r3", force: true }, + ])("accepts trusted management request $type", (message) => { + expect(SessionInboundMessageSchema.parse(message)).toEqual(message); + }); + + test.each([ + { + type: "hub.management.daemon.connect.response", + payload: { + requestId: "r1", + status: { + state: "connected", + daemonId: "daemon-1", + hubOrigin: "https://hub.example", + scopes: ["hub.execution.*"], + connectedAt: "2026-07-13T00:00:00.000Z", + lastError: null, + }, + }, + }, + { + type: "hub.management.daemon.get_status.response", + payload: { + requestId: "r2", + status: { + state: "not_connected", + daemonId: null, + hubOrigin: null, + scopes: [], + connectedAt: null, + lastError: null, + }, + }, + }, + { + type: "hub.management.daemon.disconnect.response", + payload: { + requestId: "r3", + status: { + state: "disconnecting", + daemonId: "daemon-1", + hubOrigin: "https://hub.example", + scopes: ["hub.execution.*"], + connectedAt: null, + lastError: "offline", + }, + warning: "pending", + }, + }, + ])("accepts trusted management response $type", (message) => { + expect(SessionOutboundMessageSchema.parse(message)).toEqual(message); + }); +}); diff --git a/packages/protocol/src/messages.ts b/packages/protocol/src/messages.ts index 9918fb533..48e9f106d 100644 --- a/packages/protocol/src/messages.ts +++ b/packages/protocol/src/messages.ts @@ -1135,6 +1135,22 @@ export const DaemonGetPairingOfferRequestSchema = z.object({ requestId: z.string(), }); +export const HubManagementDaemonConnectRequestSchema = z.object({ + type: z.literal("hub.management.daemon.connect.request"), + requestId: z.string(), + hubUrl: z.string(), + token: z.string(), +}); +export const HubManagementDaemonGetStatusRequestSchema = z.object({ + type: z.literal("hub.management.daemon.get_status.request"), + requestId: z.string(), +}); +export const HubManagementDaemonDisconnectRequestSchema = z.object({ + type: z.literal("hub.management.daemon.disconnect.request"), + requestId: z.string(), + force: z.boolean().optional(), +}); + export const DiagnosticsRequestSchema = z.object({ type: z.literal("diagnostics.request"), requestId: z.string(), @@ -2312,7 +2328,27 @@ export const CaptureTerminalRequestSchema = z.object({ requestId: z.string(), }); +export const HubExecutionAgentCreateRequestSchema = z.object({ + type: z.literal("hub.execution.agent.create.request"), + requestId: z.string(), + executionId: z.string(), + provider: z.string(), + cwd: z.string(), + prompt: z.string(), + workspaceId: z.string().optional(), + model: z.string().optional(), + modeId: z.string().optional(), + thinkingOptionId: z.string().optional(), + featureValues: z.record(z.string(), z.unknown()).optional(), + env: z.record(z.string(), z.string()).optional(), + worktree: CreateAgentWorktreeTargetSchema.optional(), + autoArchive: z.boolean().optional(), +}); + +export type HubExecutionAgentCreateRequest = z.infer; + export const SessionInboundMessageSchema = z.discriminatedUnion("type", [ + HubExecutionAgentCreateRequestSchema, BrowserAutomationExecuteResponseSchema, VoiceAudioChunkMessageSchema, AbortRequestMessageSchema, @@ -2337,6 +2373,9 @@ export const SessionInboundMessageSchema = z.discriminatedUnion("type", [ WaitForFinishRequestSchema, DaemonGetStatusRequestSchema, DaemonGetPairingOfferRequestSchema, + HubManagementDaemonConnectRequestSchema, + HubManagementDaemonGetStatusRequestSchema, + HubManagementDaemonDisconnectRequestSchema, DiagnosticsRequestSchema, GetDaemonConfigRequestMessageSchema, SetDaemonConfigRequestMessageSchema, @@ -2669,6 +2708,8 @@ export const ServerInfoStatusPayloadSchema = z providerSubagents: z.boolean().optional(), // COMPAT(workspacePinning): added in v0.1.107, remove gate after 2027-01-12. workspacePinning: z.boolean().optional(), + // COMPAT(hubRelationship): added in v0.1.X, drop the gate when floor >= v0.1.X. + hubRelationship: z.boolean().optional(), // COMPAT(projectGithubClone): added in v0.1.108, remove gate after 2027-01-15. projectGithubClone: z.boolean().optional(), // COMPAT(workspaceGithubRepositorySearch): added in v0.1.108, remove gate after 2027-01-15. @@ -3598,6 +3639,38 @@ export const DaemonGetStatusResponseSchema = z.object({ .passthrough(), }); +export const HubRelationshipStatusSchema = z.object({ + state: z.enum([ + "not_connected", + "connecting", + "connected", + "reconnecting", + "disconnecting", + "revoked", + ]), + daemonId: z.string().nullable(), + hubOrigin: z.string().nullable(), + scopes: z.array(z.string()), + connectedAt: z.string().nullable(), + lastError: z.string().nullable(), +}); +export const HubManagementDaemonConnectResponseSchema = z.object({ + type: z.literal("hub.management.daemon.connect.response"), + payload: z.object({ requestId: z.string(), status: HubRelationshipStatusSchema }), +}); +export const HubManagementDaemonGetStatusResponseSchema = z.object({ + type: z.literal("hub.management.daemon.get_status.response"), + payload: z.object({ requestId: z.string(), status: HubRelationshipStatusSchema }), +}); +export const HubManagementDaemonDisconnectResponseSchema = z.object({ + type: z.literal("hub.management.daemon.disconnect.response"), + payload: z.object({ + requestId: z.string(), + status: HubRelationshipStatusSchema, + warning: z.string().optional(), + }), +}); + export const DaemonGetPairingOfferResponseSchema = z.object({ type: z.literal("daemon.get_pairing_offer.response"), payload: z @@ -4837,9 +4910,76 @@ export const DaemonUpdateProgressMessageSchema = z.object({ }), }); +export const HubExecutionAgentCreateResponseSchema = z.object({ + type: z.literal("hub.execution.agent.create.response"), + payload: z.object({ + requestId: z.string(), + executionId: z.string(), + agentId: z.string().nullable(), + agent: AgentSnapshotPayloadSchema.nullable(), + success: z.boolean(), + error: z.string().nullable(), + }), +}); + +export const HubExecutionAgentUpdateSchema = z.object({ + type: z.literal("hub.execution.agent.update"), + payload: z.object({ + executionId: z.string(), + agentId: z.string(), + agent: AgentSnapshotPayloadSchema, + }), +}); + +export const HubExecutionAgentStreamSchema = z.object({ + type: z.literal("hub.execution.agent.stream"), + payload: z.object({ + executionId: z.string(), + agentId: z.string(), + event: AgentStreamEventPayloadSchema, + }), +}); + +export type HubExecutionAgentCreateResponse = z.infer; +export type HubExecutionAgentUpdate = z.infer; +export type HubExecutionAgentStream = z.infer; + +export const HubExecutionOutboundMessageSchema = z.discriminatedUnion("type", [ + HubExecutionAgentCreateResponseSchema, + HubExecutionAgentUpdateSchema, + HubExecutionAgentStreamSchema, +]); + +export type HubExecutionOutboundMessage = z.infer; + +export class HubMessageCorrelationError extends Error { + constructor(messageType: HubExecutionOutboundMessage["type"]) { + super(`Hub message ${messageType} has mismatched agent correlation`); + this.name = "HubMessageCorrelationError"; + } +} + +export function parseHubExecutionOutboundMessage(value: unknown): HubExecutionOutboundMessage { + const message = HubExecutionOutboundMessageSchema.parse(value); + const payload = message.payload; + if ( + "agent" in payload && + payload.agent !== null && + "agentId" in payload && + payload.agentId !== null && + payload.agent.id !== payload.agentId + ) { + throw new HubMessageCorrelationError(message.type); + } + return message; +} + export type DaemonUpdateProgressMessage = z.infer; export const SessionOutboundMessageSchema = z.discriminatedUnion("type", [ + HubExecutionAgentCreateResponseSchema, + HubExecutionAgentUpdateSchema, + HubExecutionAgentStreamSchema, BrowserAutomationExecuteRequestSchema, ActivityLogMessageSchema, AssistantChunkMessageSchema, @@ -4892,6 +5032,9 @@ export const SessionOutboundMessageSchema = z.discriminatedUnion("type", [ SetVoiceModeResponseMessageSchema, DaemonGetStatusResponseSchema, DaemonGetPairingOfferResponseSchema, + HubManagementDaemonConnectResponseSchema, + HubManagementDaemonGetStatusResponseSchema, + HubManagementDaemonDisconnectResponseSchema, DiagnosticsResponseSchema, GetDaemonConfigResponseMessageSchema, SetDaemonConfigResponseMessageSchema, diff --git a/packages/server/src/server/agent/agent-loading.ts b/packages/server/src/server/agent/agent-loading.ts index dfe06fc43..c3a67af2c 100644 --- a/packages/server/src/server/agent/agent-loading.ts +++ b/packages/server/src/server/agent/agent-loading.ts @@ -75,6 +75,7 @@ export async function ensureAgentLoaded( snapshot = await deps.agentManager.createAgent(config, agentId, { labels: record.labels, workspaceId: record.workspaceId, + owner: record.owner, }); deps.logger.info({ agentId, provider: record.provider }, "Agent created from stored config"); } diff --git a/packages/server/src/server/agent/agent-manager.ts b/packages/server/src/server/agent/agent-manager.ts index 73f51346e..158cdc811 100644 --- a/packages/server/src/server/agent/agent-manager.ts +++ b/packages/server/src/server/agent/agent-manager.ts @@ -44,6 +44,7 @@ import { } from "./agent-sdk-types.js"; import { buildArchivedAgentRecord, type ArchivedStoredAgentRecord } from "./agent-archive.js"; import type { StoredAgentRecord, AgentStorage } from "./agent-storage.js"; +import type { AgentOwner } from "./agent-owner.js"; import { InMemoryAgentTimelineStore, type SeedAgentTimelineOptions, @@ -232,6 +233,7 @@ export interface CreateAgentOptions { initialTitle?: string | null; // undefined is an explicit decision: the agent never appears in the sidebar. workspaceId: string | undefined; + owner?: AgentOwner; } export interface AgentManagerOptions { @@ -306,6 +308,7 @@ interface ManagedAgentBase { * Null/undefined for legacy agents created before ownership stamping. */ workspaceId?: string; + owner?: AgentOwner; capabilities: AgentCapabilityFlags; config: AgentSessionConfig; runtimeInfo?: AgentRuntimeInfo; @@ -780,6 +783,10 @@ export class AgentManager { }; } + subscriptionCount(): number { + return this.subscribers.size; + } + listAgents(): ManagedAgent[] { return Array.from(this.agents.values()) .filter((agent) => !agent.internal) @@ -1010,6 +1017,7 @@ export class AgentManager { labels: options.labels, initialTitle: options.initialTitle, workspaceId: options.workspaceId, + owner: options.owner, }); } @@ -1033,6 +1041,7 @@ export class AgentManager { lastUserMessageAt?: Date | null; labels?: Record; workspaceId?: string; + owner?: AgentOwner; }, ): Promise { return this.trackAgentRegistrationOperation( @@ -1050,6 +1059,7 @@ export class AgentManager { lastUserMessageAt?: Date | null; labels?: Record; workspaceId?: string; + owner?: AgentOwner; }, ): Promise { this.assertAcceptingAgentRegistrations(); @@ -1229,6 +1239,7 @@ export class AgentManager { return this.registerSession(session, storedConfig, agentId, { labels: existing.labels, workspaceId: existing.workspaceId, + owner: existing.owner, createdAt: existing.createdAt, updatedAt: existing.updatedAt, lastUserMessageAt: existing.lastUserMessageAt, @@ -1298,7 +1309,7 @@ export class AgentManager { } } - async closeAgent(agentId: string): Promise { + async closeAgent(agentId: string, options: { persistClosedState?: boolean } = {}): Promise { const agent = this.requireAgent(agentId); this.logger.trace( { @@ -1318,7 +1329,9 @@ export class AgentManager { for (const event of this.providerSubagents.deleteParent(agentId)) { this.dispatch({ type: "provider_subagent", event }); } - await this.persistSnapshot(closedAgent); + if (options.persistClosedState !== false) { + await this.persistSnapshot(closedAgent); + } this.emitClosedAgent(closedAgent, { persist: false }); this.logger.trace( { @@ -1420,6 +1433,7 @@ export class AgentManager { provider: record.provider, cwd: record.cwd, workspaceId: record.workspaceId, + owner: record.owner, session: null, capabilities: STORED_AGENT_CAPABILITIES, config: buildStoredAgentConfig(record), @@ -2574,6 +2588,7 @@ export class AgentManager { initialTitle?: string | null; publishWhenReady?: boolean; workspaceId?: string; + owner?: AgentOwner; }, ): Promise { let registered = false; @@ -2709,6 +2724,7 @@ export class AgentManager { attention?: AttentionState; persistence?: AgentPersistenceHandle; workspaceId?: string; + owner?: AgentOwner; } | undefined; }): ActiveManagedAgent { @@ -2718,6 +2734,7 @@ export class AgentManager { provider: config.provider, cwd: config.cwd, workspaceId: options?.workspaceId, + owner: options?.owner, session, capabilities: session.capabilities, config, diff --git a/packages/server/src/server/agent/agent-owner.ts b/packages/server/src/server/agent/agent-owner.ts new file mode 100644 index 000000000..7dd9e9442 --- /dev/null +++ b/packages/server/src/server/agent/agent-owner.ts @@ -0,0 +1,16 @@ +import { z } from "zod"; + +export const AgentOwnerSchema = z.discriminatedUnion("kind", [ + z.object({ + kind: z.literal("daemon"), + daemonId: z.string(), + executionId: z.string(), + }), +]); + +export type AgentOwner = z.infer; +export type DaemonAgentOwner = Extract; + +export function daemonExecutionKey(owner: DaemonAgentOwner): string { + return `${owner.daemonId}\0${owner.executionId}`; +} diff --git a/packages/server/src/server/agent/agent-projections.ts b/packages/server/src/server/agent/agent-projections.ts index 6adee4bd5..a01c67b29 100644 --- a/packages/server/src/server/agent/agent-projections.ts +++ b/packages/server/src/server/agent/agent-projections.ts @@ -93,6 +93,7 @@ export function toStoredAgentRecord( ? agent.attention.attentionTimestamp.toISOString() : null, internal: options?.internal, + owner: agent.owner, } satisfies StoredAgentRecord; } diff --git a/packages/server/src/server/agent/agent-storage.ts b/packages/server/src/server/agent/agent-storage.ts index 069865f66..5e504cee6 100644 --- a/packages/server/src/server/agent/agent-storage.ts +++ b/packages/server/src/server/agent/agent-storage.ts @@ -1,4 +1,4 @@ -import { promises as fs } from "node:fs"; +import { promises as fs, type Dirent } from "node:fs"; import path from "node:path"; import { z } from "zod"; import type { Logger } from "pino"; @@ -8,6 +8,7 @@ import { AgentFeatureSchema, AgentStatusSchema } from "../messages.js"; import { toStoredAgentRecord } from "./agent-projections.js"; import type { ManagedAgent } from "./agent-manager.js"; import type { AgentSessionConfig } from "./agent-sdk-types.js"; +import { AgentOwnerSchema, daemonExecutionKey, type DaemonAgentOwner } from "./agent-owner.js"; const SERIALIZABLE_CONFIG_SCHEMA = z .object({ @@ -64,6 +65,7 @@ const STORED_AGENT_SCHEMA = z.object({ attentionTimestamp: z.string().nullable().optional(), internal: z.boolean().optional(), archivedAt: z.string().nullable().optional(), + owner: AgentOwnerSchema.optional(), }); export type SerializableAgentConfig = Pick< @@ -88,6 +90,8 @@ export class AgentStorage { private pathsById: Map> = new Map(); private pendingWrites: Map> = new Map(); private deleting: Set = new Set(); + private daemonAgentIdsByExecution: Map = new Map(); + private daemonExecutionKeysByAgentId: Map = new Map(); private loaded = false; private baseDir: string; private loadPromise: Promise | null = null; @@ -112,6 +116,12 @@ export class AgentStorage { return this.cache.get(agentId) ?? null; } + async findByDaemonExecution(owner: DaemonAgentOwner): Promise { + await this.load(); + const agentId = this.daemonAgentIdsByExecution.get(daemonExecutionKey(owner)); + return agentId ? (this.cache.get(agentId) ?? null) : null; + } + async upsert(record: StoredAgentRecord): Promise { await this.load(); await this.queueRecordWrite(record); @@ -157,6 +167,7 @@ export class AgentStorage { } this.cache.set(agentId, record); + this.indexOwner(record); this.pathById.set(agentId, nextPath); } @@ -186,6 +197,7 @@ export class AgentStorage { ); this.cache.delete(agentId); + this.removeOwnerIndex(agentId); this.pathById.delete(agentId); this.pathsById.delete(agentId); } @@ -248,6 +260,8 @@ export class AgentStorage { this.cache.clear(); this.pathById.clear(); this.pathsById.clear(); + this.daemonAgentIdsByExecution.clear(); + this.daemonExecutionKeysByAgentId.clear(); try { const records = await this.scanDisk(); @@ -266,7 +280,7 @@ export class AgentStorage { private async scanDisk(): Promise { const records: StoredAgentRecord[] = []; - let entries: Array = []; + let entries: Dirent[] = []; try { entries = await fs.readdir(this.baseDir, { withFileTypes: true }); } catch (error) { @@ -310,6 +324,7 @@ export class AgentStorage { const { record, filePath } = item; records.push(record); this.cache.set(record.id, record); + this.indexOwner(record); this.pathById.set(record.id, filePath); this.addIndexedPath(record.id, filePath); } @@ -350,6 +365,28 @@ export class AgentStorage { } } + private indexOwner(record: StoredAgentRecord): void { + this.removeOwnerIndex(record.id); + if (record.owner?.kind === "daemon") { + const key = daemonExecutionKey(record.owner); + const previousAgentId = this.daemonAgentIdsByExecution.get(key); + if (previousAgentId && previousAgentId !== record.id) { + this.daemonExecutionKeysByAgentId.delete(previousAgentId); + } + this.daemonAgentIdsByExecution.set(key, record.id); + this.daemonExecutionKeysByAgentId.set(record.id, key); + } + } + + private removeOwnerIndex(agentId: string): void { + const key = this.daemonExecutionKeysByAgentId.get(agentId); + if (!key) return; + if (this.daemonAgentIdsByExecution.get(key) === agentId) { + this.daemonAgentIdsByExecution.delete(key); + } + this.daemonExecutionKeysByAgentId.delete(agentId); + } + private async waitForPendingWrite(agentId: string): Promise { await (this.pendingWrites.get(agentId) ?? Promise.resolve()).catch(() => undefined); } diff --git a/packages/server/src/server/agent/create-agent-lifecycle-dispatch.test.ts b/packages/server/src/server/agent/create-agent-lifecycle-dispatch.test.ts new file mode 100644 index 000000000..bdb903043 --- /dev/null +++ b/packages/server/src/server/agent/create-agent-lifecycle-dispatch.test.ts @@ -0,0 +1,47 @@ +import { expect, test } from "vitest"; + +import type { AgentManagerEvent, AgentSubscriber } from "./agent-manager.js"; +import { registerAgentAutoArchive } from "./create-agent-lifecycle-dispatch.js"; + +class AgentLifecycleEvents { + private readonly listeners = new Set(); + + subscribe(listener: AgentSubscriber): () => void { + this.listeners.add(listener); + return () => this.listeners.delete(listener); + } + + completeTurn(agentId: string): void { + const event: AgentManagerEvent = { + type: "agent_stream", + agentId, + event: { type: "turn_completed", provider: "codex" }, + }; + for (const listener of this.listeners) listener(event); + } + + listenerCount(): number { + return this.listeners.size; + } +} + +test("auto-archive self-releases once and later cancellation waits harmlessly", async () => { + const agentId = "4a7e2521-286d-4ad5-af35-e091c55302e3"; + const agents = new AgentLifecycleEvents(); + let archiveCount = 0; + const registration = registerAgentAutoArchive({ + agentManager: agents, + agentId, + archive: async () => { + archiveCount += 1; + }, + }); + + agents.completeTurn(agentId); + await registration.cancel(); + await registration.cancel(); + agents.completeTurn(agentId); + + expect(archiveCount).toBe(1); + expect(agents.listenerCount()).toBe(0); +}); diff --git a/packages/server/src/server/agent/create-agent-lifecycle-dispatch.ts b/packages/server/src/server/agent/create-agent-lifecycle-dispatch.ts index 25f616922..e4c29c958 100644 --- a/packages/server/src/server/agent/create-agent-lifecycle-dispatch.ts +++ b/packages/server/src/server/agent/create-agent-lifecycle-dispatch.ts @@ -14,7 +14,7 @@ import type { FirstAgentContext, SessionOutboundMessage, } from "../messages.js"; -import type { AgentManager } from "./agent-manager.js"; +import type { AgentManager, AgentSubscriber, SubscribeOptions } from "./agent-manager.js"; import type { AgentStorage } from "./agent-storage.js"; interface CreateAgentLifecycleDispatchDependencies { @@ -38,6 +38,16 @@ interface CreateAgentLifecycleDispatchDependencies { logger: pino.Logger; } +export interface LifecycleRegistration { + cancel(): Promise; +} + +interface AgentLifecycleEvents { + subscribe(callback: AgentSubscriber, options?: SubscribeOptions): () => void; +} + +const inactiveRegistration: LifecycleRegistration = { cancel: async () => undefined }; + type AutoArchiveTarget = | { kind: "agent-only" } | { kind: "created-worktree"; result: CreatePaseoWorktreeWorkflowResult }; @@ -67,12 +77,12 @@ export class CreateAgentLifecycleDispatch { autoArchive: boolean | undefined; agentId: string; createdWorktree: CreatePaseoWorktreeWorkflowResult | null; - }): void { + }): LifecycleRegistration { if (input.autoArchive !== true) { - return; + return inactiveRegistration; } - this.registerAutoArchiveOnTerminalState( + return this.registerAutoArchiveOnTerminalState( input.agentId, toAutoArchiveTarget(input.createdWorktree), ); @@ -142,24 +152,15 @@ export class CreateAgentLifecycleDispatch { } } - private registerAutoArchiveOnTerminalState(agentId: string, target: AutoArchiveTarget): void { - const unsubscribe = this.dependencies.agentManager.subscribe( - (event) => { - if (event.type !== "agent_stream") { - return; - } - if ( - event.event.type !== "turn_completed" && - event.event.type !== "turn_failed" && - event.event.type !== "turn_canceled" - ) { - return; - } - unsubscribe(); - void this.autoArchiveAgentOnce(agentId, target); - }, - { agentId, replayState: false }, - ); + private registerAutoArchiveOnTerminalState( + agentId: string, + target: AutoArchiveTarget, + ): LifecycleRegistration { + return registerAgentAutoArchive({ + agentManager: this.dependencies.agentManager, + agentId, + archive: () => this.autoArchiveAgentOnce(agentId, target), + }); } private async autoArchiveAgentOnce(agentId: string, target: AutoArchiveTarget): Promise { @@ -226,6 +227,43 @@ export class CreateAgentLifecycleDispatch { } } +export function registerAgentAutoArchive(input: { + agentManager: AgentLifecycleEvents; + agentId: string; + archive: () => Promise; +}): LifecycleRegistration { + let unsubscribe: (() => void) | null = null; + let archiveTask: Promise | null = null; + const release = () => { + if (!unsubscribe) return; + const subscribed = unsubscribe; + unsubscribe = null; + subscribed(); + }; + const registration: LifecycleRegistration = { + async cancel() { + release(); + await archiveTask; + }, + }; + unsubscribe = input.agentManager.subscribe( + (event) => { + if (event.type !== "agent_stream") return; + if ( + event.event.type !== "turn_completed" && + event.event.type !== "turn_failed" && + event.event.type !== "turn_canceled" + ) { + return; + } + release(); + archiveTask = input.archive(); + }, + { agentId: input.agentId, replayState: false }, + ); + return registration; +} + function toAutoArchiveTarget( createdWorktree: CreatePaseoWorktreeWorkflowResult | null, ): AutoArchiveTarget { diff --git a/packages/server/src/server/agent/create-agent/create.test.ts b/packages/server/src/server/agent/create-agent/create.test.ts index 458ed1ccf..e5c90efc2 100644 --- a/packages/server/src/server/agent/create-agent/create.test.ts +++ b/packages/server/src/server/agent/create-agent/create.test.ts @@ -328,6 +328,58 @@ test("mcp create stamps the new worktree's workspaceId, not the parent's", async } }); +test("mcp create exposes the created worktree before dispatching the initial prompt", async () => { + const workdir = mkdtempSync(join(tmpdir(), "create-agent-worktree-callback-test-")); + const storage = new AgentStorage(join(workdir, "agents"), logger); + const agentManager = createRealAgentManager(storage); + const createdWorktree = await fakeWorktreeCreator({ + repoRoot: workdir, + createdWorkspaceId: "ws-created-worktree", + })(); + let observed: + | { + createdWorktree: CreatePaseoWorktreeWorkflowResult | null; + lifecycle: ManagedAgent["lifecycle"] | null; + } + | undefined; + + try { + await createAgentCommand( + { + agentManager, + agentStorage: storage, + logger, + providerSnapshotManager: { + async resolveCreateConfig() { + return {}; + }, + }, + createPaseoWorktree: async () => createdWorktree, + }, + { + kind: "mcp", + provider: "codex", + cwd: workdir, + title: "worktree callback", + initialPrompt: "Say done.", + background: true, + notifyOnFinish: false, + worktree: { worktreeName: "feature", baseBranch: "main" }, + onCreated: ({ agentId, createdWorktree: callbackWorktree }) => { + observed = { + createdWorktree: callbackWorktree, + lifecycle: agentManager.getAgent(agentId)?.lifecycle ?? null, + }; + }, + }, + ); + + expect(observed).toEqual({ createdWorktree, lifecycle: "idle" }); + } finally { + rmSync(workdir, { recursive: true, force: true }); + } +}); + test("session create keeps the prompt title after the initial prompt settles", async () => { const workdir = mkdtempSync(join(tmpdir(), "create-agent-title-test-")); const storage = new AgentStorage(join(workdir, "agents"), logger); diff --git a/packages/server/src/server/agent/create-agent/create.ts b/packages/server/src/server/agent/create-agent/create.ts index ea20d3ffe..539f324d2 100644 --- a/packages/server/src/server/agent/create-agent/create.ts +++ b/packages/server/src/server/agent/create-agent/create.ts @@ -15,6 +15,7 @@ import type { AgentAttachment, FirstAgentContext, GitSetupOptions } from "../../ import type { AgentManager, CreateAgentOptions, ManagedAgent } from "../agent-manager.js"; import type { AgentPromptInput, AgentRunOptions, AgentSessionConfig } from "../agent-sdk-types.js"; import type { AgentStorage } from "../agent-storage.js"; +import type { AgentOwner } from "../agent-owner.js"; import type { ProviderSnapshotManager } from "../provider-snapshot-manager.js"; import { setupFinishNotification, startCreatedAgentInitialPrompt } from "../agent-prompt.js"; import { resolveCreateAgentTitles } from "../create-agent-title.js"; @@ -41,7 +42,7 @@ export interface CreateAgentCommandDependencies { paseoHome?: string; worktreesRoot?: string; terminalManager?: TerminalManager | null; - providerSnapshotManager: ProviderSnapshotManager; + providerSnapshotManager: Pick; createPaseoWorktree?: CreatePaseoWorktreeWorkflowFn; // Mints a fresh directory workspace for a cwd and returns its id. ensureWorkspaceForCreate?: EnsureWorkspaceForCreate; @@ -93,6 +94,13 @@ export interface CreateAgentFromMcpInput { notifyOnFinish: boolean; internal?: boolean; detached?: boolean; + owner?: AgentOwner; + env?: Record; + onCreated?: (created: { + agentId: string; + createdWorktree: CreatePaseoWorktreeWorkflowResult | null; + }) => void; + onWorktreeCreated?: (createdWorktree: CreatePaseoWorktreeWorkflowResult) => void; callerAgentId?: string; callerContext?: { lockedCwd?: string; @@ -118,6 +126,7 @@ export interface CreateAgentCommandResult { background: boolean; initialPromptStarted: boolean; initialPromptError: unknown | null; + createdWorktree?: CreatePaseoWorktreeWorkflowResult; } export type BoundCreateAgentCommand = ( @@ -158,6 +167,7 @@ interface ResolvedCreateAgent { background: boolean; promptFailure: CreateAgentPromptFailureMode; promptLogger?: Logger; + createdWorktree?: CreatePaseoWorktreeWorkflowResult; } export async function createAgentCommand( @@ -182,6 +192,9 @@ export async function createAgentCommand( let liveSnapshot = snapshot; let initialPromptStarted = false; let initialPromptError: unknown | null = null; + if (input.kind === "mcp") { + input.onCreated?.({ agentId: snapshot.id, createdWorktree: resolved.createdWorktree ?? null }); + } if (resolved.prompt !== undefined) { const sendResult = await sendInitialPrompt(dependencies, resolved, snapshot); initialPromptStarted = sendResult.started; @@ -205,6 +218,7 @@ export async function createAgentCommand( background: resolved.background, initialPromptStarted, initialPromptError, + ...(resolved.createdWorktree ? { createdWorktree: resolved.createdWorktree } : {}), }; } @@ -294,12 +308,14 @@ async function resolveMcpCreateAgent( ? requireParentAgent(dependencies.agentManager, input.callerAgentId) : null; const cwd = resolveMcpInitialCwd(input, parentAgent); - const { resolvedCwd, setupContinuation, createdWorkspaceId } = await resolveMcpCwd({ - dependencies, - cwd, - worktree: input.worktree, - initialPrompt: input.initialPrompt ?? "", - }); + const { resolvedCwd, setupContinuation, createdWorkspaceId, createdWorktree } = + await resolveMcpCwd({ + dependencies, + cwd, + worktree: input.worktree, + initialPrompt: input.initialPrompt ?? "", + }); + if (createdWorktree) input.onWorktreeCreated?.(createdWorktree); const workspaceId = await resolveMcpWorkspaceId({ dependencies, @@ -338,9 +354,12 @@ async function resolveMcpCreateAgent( createOptions: { ...(labels ? { labels } : {}), workspaceId: requireResolvedWorkspaceId(workspaceId), + owner: input.owner, + env: input.env, }, prompt: trimmedPrompt ? trimmedPrompt : undefined, setupContinuation, + createdWorktree, background: input.background, promptFailure: input.promptFailure ?? "log", }; @@ -518,6 +537,7 @@ async function resolveMcpCwd(params: { resolvedCwd: string; setupContinuation?: AgentWorktreeSetupContinuation; createdWorkspaceId?: string; + createdWorktree?: CreatePaseoWorktreeWorkflowResult; }> { const { dependencies, worktree } = params; if (!worktree) { @@ -576,6 +596,7 @@ async function resolveMcpCwd(params: { resolvedCwd: createdWorktree.workspace.cwd, setupContinuation: createdWorktree.setupContinuation, createdWorkspaceId: createdWorktree.workspace.workspaceId, + createdWorktree, }; } diff --git a/packages/server/src/server/bootstrap.ts b/packages/server/src/server/bootstrap.ts index 135f0efc4..0157f81e2 100644 --- a/packages/server/src/server/bootstrap.ts +++ b/packages/server/src/server/bootstrap.ts @@ -202,6 +202,18 @@ import { createAgentCommand, type CreateAgentCommandDependencies, } from "./agent/create-agent/create.js"; +import { archiveAgentCommand } from "./agent/lifecycle-command.js"; +import { CreateAgentLifecycleDispatch } from "./agent/create-agent-lifecycle-dispatch.js"; +import { + HubRelationshipController, + type HubRelationshipClock, + type HubRelationshipRetryPolicy, +} from "./hub/relationship-controller.js"; +import { + DirectHubRelationshipRemote, + type HubRelationshipRemote, +} from "./hub/relationship-remote.js"; +import { DaemonExecutions } from "./hub/daemon-executions.js"; const MAX_MCP_DEBUG_BATCH_ITEMS = 10; const REDACTED_LOG_VALUE = "[redacted]"; @@ -434,6 +446,13 @@ export interface PaseoDaemon { getListenTarget(): ListenTarget | null; } +export interface PaseoDaemonDependencies { + hubRelationshipRemote?: HubRelationshipRemote; + hubRelationshipClock?: HubRelationshipClock; + hubRelationshipRetryPolicy?: HubRelationshipRetryPolicy; + createHubDaemonId?: () => string; +} + function createBootstrapManagedProcessRegistry( config: Pick, logger: Logger, @@ -511,6 +530,7 @@ function createInitialMutableDaemonConfig(config: PaseoDaemonConfig): MutableDae export async function createPaseoDaemon( config: PaseoDaemonConfig, rootLogger: Logger, + dependencies: PaseoDaemonDependencies = {}, ): Promise { const logger = rootLogger.child({ module: "bootstrap" }); const bootstrapStart = performance.now(); @@ -577,7 +597,7 @@ export async function createPaseoDaemon( serviceProxy, onChange: createScriptStatusEmitter({ sessions: () => - wsServer?.listActiveSessions().map((session) => ({ + wsServer?.listTrustedSessions().map((session) => ({ emit: (message) => session.emitServerMessage(message), })) ?? [], serviceProxy, @@ -827,7 +847,7 @@ export async function createPaseoDaemon( onProjectUpdate: (update) => wsServer?.publishProjectUpdate(update), onWorkspacesChanged: async (workspaceIds) => { await fanOutReconciledWorkspaceUpdates({ - sessions: wsServer?.listActiveSessions() ?? [], + sessions: wsServer?.listTrustedSessions() ?? [], workspaceIds, logger, }); @@ -845,7 +865,7 @@ export async function createPaseoDaemon( workspaceGitService, }); const archiveWorkspaceRecordExternal = async (workspaceId: string) => { - const sessions = wsServer?.listActiveSessions() ?? []; + const sessions = wsServer?.listTrustedSessions() ?? []; if (sessions.length > 0) { await Promise.all( sessions.map((session) => session.archiveWorkspaceRecordForExternalMutation(workspaceId)), @@ -895,20 +915,20 @@ export async function createPaseoDaemon( }; const markWorkspaceArchivingExternal = (workspaceIds: Iterable, archivingAt: string) => { const workspaceIdList = Array.from(workspaceIds); - for (const session of wsServer?.listActiveSessions() ?? []) { + for (const session of wsServer?.listTrustedSessions() ?? []) { session.markWorkspaceArchivingForExternalMutation(workspaceIdList, archivingAt); } }; const clearWorkspaceArchivingExternal = (workspaceIds: Iterable) => { const workspaceIdList = Array.from(workspaceIds); - for (const session of wsServer?.listActiveSessions() ?? []) { + for (const session of wsServer?.listTrustedSessions() ?? []) { session.clearWorkspaceArchivingForExternalMutation(workspaceIdList); } }; const emitWorkspaceUpdatesExternal = async (workspaceIds: Iterable) => { const workspaceIdList = Array.from(workspaceIds); await Promise.all( - (wsServer?.listActiveSessions() ?? []).map((session) => + (wsServer?.listTrustedSessions() ?? []).map((session) => session.emitWorkspaceUpdatesForExternalWorkspaceIds(workspaceIdList), ), ); @@ -986,7 +1006,7 @@ export async function createPaseoDaemon( warmWorkspaceGitData: async (workspace) => { await Promise.all( wsServer - ?.listActiveSessions() + ?.listTrustedSessions() .map((session) => session.warmWorkspaceGitDataForWorkspace(workspace)) ?? [], ); }, @@ -1025,6 +1045,57 @@ export async function createPaseoDaemon( }; const createAgent = (input: Parameters[1]) => createAgentCommand(createAgentCommandDependencies, input); + const hubAgentLifecycle = new CreateAgentLifecycleDispatch({ + paseoHome: config.paseoHome, + worktreesRoot: config.worktreesRoot, + agentManager, + agentStorage, + github, + workspaceGitService, + createPaseoWorktreeWorkflow: createPaseoWorktreeForTools, + archiveAgentForClose: (agentId) => + archiveAgentCommand({ agentManager, agentStorage, logger }, agentId), + findWorkspaceIdForCwd: findWorkspaceIdForCwdExternal, + listActiveWorkspaces: listActiveWorkspacesExternal, + archiveWorkspaceRecord: archiveWorkspaceRecordExternal, + emit: emitExternalSessionMessage, + emitAgentRemove: () => undefined, + emitWorkspaceUpdatesForWorkspaceIds: emitWorkspaceUpdatesExternal, + markWorkspaceArchiving: markWorkspaceArchivingExternal, + clearWorkspaceArchiving: clearWorkspaceArchivingExternal, + killTerminalsForWorkspace: (workspaceId) => + killTerminalsForWorkspace({ terminalManager, sessionLogger: logger }, workspaceId), + logger, + }); + const hubRelationships = new HubRelationshipController({ + paseoHome: config.paseoHome, + serverId, + daemonPublicKey: daemonKeyPair.publicKeyB64, + logger, + remote: dependencies.hubRelationshipRemote ?? new DirectHubRelationshipRemote(), + clock: dependencies.hubRelationshipClock, + retryPolicy: dependencies.hubRelationshipRetryPolicy, + createDaemonId: dependencies.createHubDaemonId, + attachSocket: async (socket, options) => { + if (!wsServer) throw new Error("WebSocket server is not running"); + await wsServer.attachHubSocket(socket, options); + }, + createExecutionAgents: (daemonId) => + new DaemonExecutions({ + daemonId, + agentManager, + agentStorage, + createAgent, + registerAutoArchive: ({ agentId, createdWorktree }) => + hubAgentLifecycle.registerAutoArchiveIfRequested({ + autoArchive: true, + agentId, + createdWorktree, + }), + cleanupFailedCreate: (input) => + hubAgentLifecycle.cleanupCreatedWorktreeAfterFailedAgentCreate(input), + }), + }); const loopService = new LoopService({ paseoHome: config.paseoHome, @@ -1417,7 +1488,9 @@ export async function createPaseoDaemon( }, serviceProxyPublicBaseUrl, browserToolsBroker, + hubRelationships, ); + await hubRelationships.start(); if (relayEnabled) { const offer = await createConnectionOfferV2({ @@ -1478,6 +1551,7 @@ export async function createPaseoDaemon( }; const stop = async () => { + await hubRelationships.stop(); workspaceReconciliation.dispose(); scriptHealthMonitor.stop(); // Freeze both ingress and registration before taking the agent closure snapshot. diff --git a/packages/server/src/server/daemon-client.e2e.test.ts b/packages/server/src/server/daemon-client.e2e.test.ts index 0065d9e5e..44c4b51be 100644 --- a/packages/server/src/server/daemon-client.e2e.test.ts +++ b/packages/server/src/server/daemon-client.e2e.test.ts @@ -1006,6 +1006,7 @@ test("receives server_info on websocket connect", async () => { expect(serverInfo).not.toBeNull(); expect(serverInfo?.serverId.length).toBeGreaterThan(0); expect(serverInfo?.features?.["terminal-restore-modes"]).toBe(true); + expect(serverInfo?.features?.hubRelationship).toBe(true); expect(serverInfo?.features?.commitsList).toBe(true); expect(serverInfo?.desktopManaged).toBe(false); expect(serverInfo?.features?.daemonSelfUpdate).toBe(true); diff --git a/packages/server/src/server/hub/daemon-executions.test.ts b/packages/server/src/server/hub/daemon-executions.test.ts new file mode 100644 index 000000000..363f341d0 --- /dev/null +++ b/packages/server/src/server/hub/daemon-executions.test.ts @@ -0,0 +1,152 @@ +import { afterEach, expect, test } from "vitest"; +import { HubRelationshipHarness } from "./test-utils/relationship-harness.js"; + +let relationship: HubRelationshipHarness | null = null; + +afterEach(async () => { + await relationship?.close(); + relationship = null; +}); + +async function launchRelationship(): Promise { + const launched = await HubRelationshipHarness.start(); + await launched.beginConnect().result; + launched.connectLatestSocket(); + relationship = launched; + return launched; +} + +test("sequential replay after reconstruction keeps one durable owned agent", async () => { + const hub = await launchRelationship(); + const created = await hub.createOwnedConcurrently(); + + const reconstructed = await hub.reconstructAndReplay(); + + expect(reconstructed.replay.agent.id).toBe(created.first.agentId); + expect(reconstructed.replay.agent.status).toBe("closed"); + expect(reconstructed.durableAgentCount).toBe(1); +}); + +test("removing a daemon-owned agent removes its execution association", async () => { + const hub = await launchRelationship(); + const created = await hub.createOwnedConcurrently(); + + const removed = await hub.removeOwnedAgent(created.first.agentId); + + expect(removed.durableAgentCount).toBe(0); +}); + +test("a failed Hub create removes its auto-created worktree", async () => { + const hub = await launchRelationship(); + hub.beginOwnedCreate("failed-worktree-create", "failed-worktree-execution", { + modeId: "missing-mode", + worktree: { mode: "branch-off", newBranch: "failed-hub-create" }, + }); + + const response = await hub.ownedCreateResult("failed-worktree-create"); + + expect(response).toMatchObject({ + type: "hub.execution.agent.create.response", + payload: { success: false, executionId: "failed-worktree-execution" }, + }); + expect(await hub.listedWorktrees()).toHaveLength(1); + expect(await hub.durableOwnedAgentIds()).toEqual([]); +}); + +test("failed Hub auto-archive creates release their lifecycle subscriptions", async () => { + const hub = await launchRelationship(); + const subscriptionBaseline = hub.agentSubscriptionCount(); + + hub.failProviderPromptStart(); + hub.beginOwnedCreate("failed-prompt-create-1", "failed-prompt-execution-1", { + autoArchive: true, + worktree: { mode: "branch-off", newBranch: "failed-prompt-1" }, + }); + const first = await hub.ownedCreateResult("failed-prompt-create-1"); + + expect(first).toMatchObject({ + type: "hub.execution.agent.create.response", + payload: { success: false, executionId: "failed-prompt-execution-1" }, + }); + expect(hub.activeOwnedAgentIds()).toEqual([]); + expect(await hub.durableOwnedAgentIds()).toEqual([]); + expect(await hub.listedWorktrees()).toHaveLength(1); + expect(hub.agentSubscriptionCount()).toBe(subscriptionBaseline); + + hub.failProviderPromptStart(); + hub.beginOwnedCreate("failed-prompt-create-2", "failed-prompt-execution-2", { + autoArchive: true, + worktree: { mode: "branch-off", newBranch: "failed-prompt-2" }, + }); + const second = await hub.ownedCreateResult("failed-prompt-create-2"); + + expect(second).toMatchObject({ + type: "hub.execution.agent.create.response", + payload: { success: false, executionId: "failed-prompt-execution-2" }, + }); + expect(hub.activeOwnedAgentIds()).toEqual([]); + expect(await hub.durableOwnedAgentIds()).toEqual([]); + expect(await hub.listedWorktrees()).toHaveLength(1); + expect(hub.agentSubscriptionCount()).toBe(subscriptionBaseline); +}); + +test("failed Hub create cleans durable state when provider close rejects", async () => { + const hub = await launchRelationship(); + hub.failProviderPromptStart(); + hub.failNextProviderSessionClose(); + hub.beginOwnedCreate("failed-close-create", "failed-close-execution", { + worktree: { mode: "branch-off", newBranch: "failed-close-worktree" }, + }); + + const response = await hub.ownedCreateResult("failed-close-create"); + + expect(response).toMatchObject({ + type: "hub.execution.agent.create.response", + payload: { success: false, executionId: "failed-close-execution" }, + }); + expect(hub.activeOwnedAgentIds()).toEqual([]); + expect(await hub.durableOwnedAgentIds()).toEqual([]); + expect(await hub.listedWorktrees()).toHaveLength(1); +}); + +test("Hub checkout uses the requested branch ref", async () => { + const hub = await launchRelationship(); + await hub.createBranch("existing-hub-branch"); + hub.beginOwnedCreate("checkout-create", "checkout-execution", { + worktree: { mode: "checkout-branch", branch: "existing-hub-branch" }, + }); + + const response = await hub.ownedCreateResult("checkout-create"); + + expect(response).toMatchObject({ + type: "hub.execution.agent.create.response", + payload: { success: true, executionId: "checkout-execution" }, + }); + expect(await hub.currentBranch(hub.latestCreatedCwd()!)).toBe("existing-hub-branch"); +}); + +test("failed create never archives a reused worktree", async () => { + const hub = await launchRelationship(); + hub.beginOwnedCreate("original-create", "original-execution", { + worktree: { mode: "branch-off", newBranch: "shared-hub-worktree" }, + }); + const original = await hub.ownedCreateResult("original-create"); + const worktreeCwd = original.payload.agent?.cwd; + expect(worktreeCwd).toEqual(expect.any(String)); + await hub.ownedTurnCompletion(original.payload.agentId!); + + const failedPrompt = "Fail the reused worktree create"; + hub.failProviderPromptStart(failedPrompt); + hub.beginOwnedCreate("reused-create", "reused-execution", { + autoArchive: true, + prompt: failedPrompt, + worktree: { mode: "branch-off", newBranch: "shared-hub-worktree" }, + }); + const reused = await hub.ownedCreateResult("reused-create"); + + expect(reused).toMatchObject({ + type: "hub.execution.agent.create.response", + payload: { success: false, executionId: "reused-execution" }, + }); + expect(await hub.worktreeState(worktreeCwd!)).toEqual({ exists: true, listed: true }); +}); diff --git a/packages/server/src/server/hub/daemon-executions.ts b/packages/server/src/server/hub/daemon-executions.ts new file mode 100644 index 000000000..83b4d0693 --- /dev/null +++ b/packages/server/src/server/hub/daemon-executions.ts @@ -0,0 +1,297 @@ +import type { + AgentSnapshotPayload, + AgentStreamEventPayload, + CreateAgentWorktreeTarget, +} from "@getpaseo/protocol/messages"; + +import type { AgentManager, AgentManagerEvent, ManagedAgent } from "../agent/agent-manager.js"; +import type { AgentStorage, StoredAgentRecord } from "../agent/agent-storage.js"; +import type { LifecycleRegistration } from "../agent/create-agent-lifecycle-dispatch.js"; +import type { BoundCreateAgentCommand } from "../agent/create-agent/create.js"; +import type { CreatePaseoWorktreeWorkflowResult } from "../worktree-session.js"; +import { buildStoredAgentPayload } from "../agent/agent-projections.js"; +import { serializeAgentSnapshot, serializeAgentStreamEvent } from "../messages.js"; +import { daemonExecutionKey, type DaemonAgentOwner } from "../agent/agent-owner.js"; + +export interface HubExecutionAgentCreateInput { + executionId: string; + provider: string; + cwd: string; + workspaceId?: string; + prompt: string; + model?: string; + modeId?: string; + thinkingOptionId?: string; + featureValues?: Record; + env?: Record; + worktree?: CreateAgentWorktreeTarget; + autoArchive?: boolean; +} + +export interface OwnedAgentSnapshot { + executionId: string; + agent: AgentSnapshotPayload; +} + +export type OwnedAgentEvent = + | { type: "update"; executionId: string; agent: AgentSnapshotPayload } + | { + type: "stream"; + executionId: string; + agentId: string; + event: AgentStreamEventPayload; + }; + +interface DaemonExecutionsOptions { + daemonId: string; + agentManager: AgentManager; + agentStorage: AgentStorage; + createAgent: BoundCreateAgentCommand; + registerAutoArchive?: (input: { + agentId: string; + createdWorktree: CreatePaseoWorktreeWorkflowResult | null; + }) => LifecycleRegistration; + cleanupFailedCreate?: (input: { + createdWorktree: CreatePaseoWorktreeWorkflowResult | null; + createdAgentId: string | null; + }) => Promise; +} + +export interface HubExecutionAgents { + create(input: HubExecutionAgentCreateInput): Promise; + subscribe(listener: (event: OwnedAgentEvent) => void): () => void; + invalidateAuthority(): Promise; +} + +export class DaemonExecutions implements HubExecutionAgents { + private readonly daemonId: string; + private readonly agentManager: AgentManager; + private readonly agentStorage: AgentStorage; + private readonly createAgentCommand: BoundCreateAgentCommand; + private readonly pendingCreates = new Map>(); + private authorityGeneration = 0; + private authorityActive = true; + private readonly registerAutoArchive: (input: { + agentId: string; + createdWorktree: CreatePaseoWorktreeWorkflowResult | null; + }) => LifecycleRegistration; + private readonly cleanupFailedCreate: NonNullable; + + constructor(options: DaemonExecutionsOptions) { + this.daemonId = options.daemonId; + this.agentManager = options.agentManager; + this.agentStorage = options.agentStorage; + this.createAgentCommand = options.createAgent; + this.registerAutoArchive = + options.registerAutoArchive ?? (() => ({ cancel: async () => undefined })); + this.cleanupFailedCreate = options.cleanupFailedCreate ?? (async () => undefined); + } + + create(input: HubExecutionAgentCreateInput): Promise { + if (!this.authorityActive) { + return Promise.reject(new Error("Hub relationship authority is no longer active")); + } + const owner = this.owner(input.executionId); + const key = daemonExecutionKey(owner); + const pending = this.pendingCreates.get(key); + if (pending) { + return pending; + } + + const authorityGeneration = this.authorityGeneration; + const create = this.createOrResolve(owner, input, authorityGeneration).finally(() => { + if (this.pendingCreates.get(key) === create) { + this.pendingCreates.delete(key); + } + }); + this.pendingCreates.set(key, create); + return create; + } + + async invalidateAuthority(): Promise { + this.authorityActive = false; + this.authorityGeneration++; + await Promise.allSettled(this.pendingCreates.values()); + } + + subscribe(listener: (event: OwnedAgentEvent) => void): () => void { + return this.agentManager.subscribe( + (event) => { + const owned = this.projectEvent(event); + if (owned) { + listener(owned); + } + }, + { replayState: true }, + ); + } + + private async createOrResolve( + owner: DaemonAgentOwner, + input: HubExecutionAgentCreateInput, + authorityGeneration: number, + ): Promise { + const existing = await this.agentStorage.findByDaemonExecution(owner); + if (existing) { + this.requireAuthority(authorityGeneration); + return this.resolveRecord(existing); + } + this.requireAuthority(authorityGeneration); + + let createdWorktree: CreatePaseoWorktreeWorkflowResult | null = null; + let createdAgentId: string | null = null; + let autoArchiveRegistration: LifecycleRegistration = { cancel: async () => undefined }; + let result: Awaited>; + try { + result = await this.createAgentCommand({ + kind: "mcp", + provider: input.model ? `${input.provider}/${input.model}` : input.provider, + title: input.prompt, + initialPrompt: input.prompt, + promptFailure: "throw", + cwd: input.cwd, + workspaceId: input.workspaceId, + mode: input.modeId, + thinking: input.thinkingOptionId, + features: input.featureValues, + env: input.env, + worktree: toCreateAgentWorktree(input.worktree), + background: true, + notifyOnFinish: false, + owner, + onWorktreeCreated: (worktree) => { + createdWorktree = worktree; + }, + onCreated: (created) => { + createdAgentId = created.agentId; + if (input.autoArchive === true) { + autoArchiveRegistration = this.registerAutoArchive({ + ...created, + createdWorktree: ownedCreatedWorktree(created.createdWorktree), + }); + } + }, + }); + this.requireAuthority(authorityGeneration); + } catch (error) { + try { + await autoArchiveRegistration.cancel(); + if (createdAgentId && this.agentManager.getAgent(createdAgentId)) { + await this.agentManager.closeAgent(createdAgentId); + } + } finally { + try { + await this.cleanupFailedCreate({ + createdWorktree: ownedCreatedWorktree(createdWorktree), + createdAgentId: null, + }); + } finally { + if (createdAgentId) { + await this.agentStorage.remove(createdAgentId); + } + } + } + throw error; + } + + return { + executionId: owner.executionId, + agent: serializeAgentSnapshot(result.liveSnapshot), + }; + } + + private resolveRecord(record: StoredAgentRecord): OwnedAgentSnapshot { + return this.projectRecord(record); + } + + private requireAuthority(authorityGeneration: number): void { + if (!this.authorityActive || authorityGeneration !== this.authorityGeneration) { + throw new Error("Hub relationship authority ended during agent creation"); + } + } + + private projectRecord(record: StoredAgentRecord): OwnedAgentSnapshot { + const owner = this.requireOwner(record); + const live = this.agentManager.getAgent(record.id); + return { + executionId: owner.executionId, + agent: live + ? serializeAgentSnapshot(live) + : { + ...buildStoredAgentPayload(record, this.agentManager.getRegisteredProviderIds()), + status: "closed", + }, + }; + } + + private projectEvent(event: AgentManagerEvent): OwnedAgentEvent | null { + if (event.type === "agent_state") { + return this.projectAgentState(event.agent); + } + if (event.type !== "agent_stream") { + return null; + } + const agent = this.agentManager.getAgent(event.agentId); + if (!this.isOwned(agent)) { + return null; + } + const serialized = serializeAgentStreamEvent(event.event); + if (!serialized) { + return null; + } + return { + type: "stream", + executionId: agent.owner.executionId, + agentId: agent.id, + event: serialized, + }; + } + + private projectAgentState(agent: ManagedAgent): OwnedAgentEvent | null { + if (!this.isOwned(agent)) { + return null; + } + return { + type: "update", + executionId: agent.owner.executionId, + agent: serializeAgentSnapshot(agent), + }; + } + + private isOwned(agent: ManagedAgent | null): agent is ManagedAgent & { owner: DaemonAgentOwner } { + return agent?.owner?.kind === "daemon" && agent.owner.daemonId === this.daemonId; + } + + private owner(executionId: string): DaemonAgentOwner { + return { kind: "daemon", daemonId: this.daemonId, executionId }; + } + + private requireOwner(record: StoredAgentRecord): DaemonAgentOwner { + const owner = record.owner; + if (owner?.kind !== "daemon" || owner.daemonId !== this.daemonId) { + throw new Error(`Agent ${record.id} is not owned by daemon ${this.daemonId}`); + } + return owner; + } +} + +function ownedCreatedWorktree( + worktree: CreatePaseoWorktreeWorkflowResult | null, +): CreatePaseoWorktreeWorkflowResult | null { + return worktree?.created === true ? worktree : null; +} + +function toCreateAgentWorktree(target: CreateAgentWorktreeTarget | undefined) { + if (!target) return undefined; + if (target.mode === "branch-off") { + return { + worktreeName: target.newBranch, + baseBranch: target.base, + action: "branch-off" as const, + }; + } + if (target.mode === "checkout-branch") { + return { refName: target.branch, action: "checkout" as const }; + } + return { githubPrNumber: target.prNumber, action: "checkout" as const }; +} diff --git a/packages/server/src/server/hub/execution-controller.test.ts b/packages/server/src/server/hub/execution-controller.test.ts new file mode 100644 index 000000000..585b63d51 --- /dev/null +++ b/packages/server/src/server/hub/execution-controller.test.ts @@ -0,0 +1,86 @@ +import { describe, expect, test } from "vitest"; +import type { + AgentSnapshotPayload, + HubExecutionAgentCreateRequest, + SessionOutboundMessage, +} from "@getpaseo/protocol/messages"; + +import type { + HubExecutionAgents, + OwnedAgentEvent, + OwnedAgentSnapshot, +} from "./daemon-executions.js"; +import { HubExecutionController } from "./execution-controller.js"; + +interface Deferred { + promise: Promise; + resolve(value: T): void; + reject(error: Error): void; +} + +function deferred(): Deferred { + let resolve!: (value: T) => void; + let reject!: (error: Error) => void; + const promise = new Promise((accept, decline) => { + resolve = accept; + reject = decline; + }); + return { promise, resolve, reject }; +} + +class ControlledHubExecutionAgents implements HubExecutionAgents { + private readonly createObserved = deferred(); + private readonly createGate = deferred(); + + create(): Promise { + this.createObserved.resolve(); + return this.createGate.promise; + } + + subscribe(_listener: (event: OwnedAgentEvent) => void): () => void { + return () => undefined; + } + + async invalidateAuthority(): Promise {} + + async creationStarted(): Promise { + await this.createObserved.promise; + } + + finishCreate(): void { + this.createGate.resolve({ + executionId: "execution-shutdown", + agent: { + id: "agent-shutdown", + status: "running", + } as AgentSnapshotPayload, + }); + } +} + +describe("HubExecutionController", () => { + test("cleanup fences in-flight creates before the dead session can receive a response", async () => { + const agents = new ControlledHubExecutionAgents(); + const messages: SessionOutboundMessage[] = []; + const controller = new HubExecutionController({ + agents, + send: (message) => messages.push(message), + }); + + const create = controller.createAgent({ + type: "hub.execution.agent.create.request", + requestId: "shutdown-create", + executionId: "execution-shutdown", + provider: "codex", + cwd: "/tmp/paseo", + prompt: "sleep 30", + } satisfies HubExecutionAgentCreateRequest); + await agents.creationStarted(); + + const cleanup = controller.cleanup(); + agents.finishCreate(); + await Promise.all([create, cleanup]); + + expect(messages).toEqual([]); + }); +}); diff --git a/packages/server/src/server/hub/execution-controller.ts b/packages/server/src/server/hub/execution-controller.ts new file mode 100644 index 000000000..c9b93155e --- /dev/null +++ b/packages/server/src/server/hub/execution-controller.ts @@ -0,0 +1,129 @@ +import { isAbsolute } from "node:path"; +import type { + HubExecutionAgentCreateRequest, + SessionOutboundMessage, +} from "@getpaseo/protocol/messages"; + +import type { HubExecutionAgents, OwnedAgentEvent } from "./daemon-executions.js"; + +interface HubExecutionControllerOptions { + agents: HubExecutionAgents; + send: (message: SessionOutboundMessage) => void; +} + +export class HubExecutionController { + private readonly agents: HubExecutionAgents; + private readonly send: (message: SessionOutboundMessage) => void; + private readonly unsubscribe: () => void; + private readonly pendingCreates = new Set>(); + private cleanupPromise: Promise | null = null; + private closed = false; + + constructor(options: HubExecutionControllerOptions) { + this.agents = options.agents; + this.send = options.send; + this.unsubscribe = this.agents.subscribe((event) => this.sendOwnedEvent(event)); + } + + cleanup(): Promise { + this.cleanupPromise ??= this.cleanupOnce(); + return this.cleanupPromise; + } + + private async cleanupOnce(): Promise { + this.closed = true; + this.unsubscribe(); + await Promise.allSettled(this.pendingCreates); + } + + async createAgent(message: HubExecutionAgentCreateRequest): Promise { + if (this.closed) return; + const create = this.createAgentWithResponse(message); + this.pendingCreates.add(create); + try { + await create; + } finally { + this.pendingCreates.delete(create); + } + } + + private async createAgentWithResponse(message: HubExecutionAgentCreateRequest): Promise { + try { + requireNonBlankHubAgentField("executionId", message.executionId); + requireNonBlankHubAgentField("prompt", message.prompt); + requireNonBlankHubAgentField("cwd", message.cwd); + if (!isAbsolute(message.cwd)) throw new Error("Hub agent cwd must be absolute"); + const result = await this.agents.create({ + executionId: message.executionId, + provider: message.provider, + cwd: message.cwd, + workspaceId: message.workspaceId, + prompt: message.prompt, + model: message.model, + modeId: message.modeId, + thinkingOptionId: message.thinkingOptionId, + featureValues: message.featureValues, + env: message.env, + worktree: message.worktree, + autoArchive: message.autoArchive, + }); + if (this.closed) return; + this.send({ + type: "hub.execution.agent.create.response", + payload: { + requestId: message.requestId, + executionId: message.executionId, + agentId: result.agent.id, + agent: result.agent, + success: true, + error: null, + }, + }); + } catch (error) { + if (this.closed) return; + this.send({ + type: "hub.execution.agent.create.response", + payload: { + requestId: message.requestId, + executionId: message.executionId, + agentId: null, + agent: null, + success: false, + error: error instanceof Error ? error.message : String(error), + }, + }); + } + } + + private sendOwnedEvent(event: OwnedAgentEvent): void { + if (this.closed) return; + if (event.type === "update") { + this.send({ + type: "hub.execution.agent.update", + payload: { + executionId: event.executionId, + agentId: event.agent.id, + agent: event.agent, + }, + }); + return; + } + this.send({ + type: "hub.execution.agent.stream", + payload: { + executionId: event.executionId, + agentId: event.agentId, + event: event.event, + }, + }); + } +} + +function requireNonBlankHubAgentField( + field: "executionId" | "prompt" | "cwd", + value: string, +): void { + if (value.trim().length === 0) { + throw new Error(`Hub agent ${field} cannot be blank`); + } +} diff --git a/packages/server/src/server/hub/execution-session.websocket.test.ts b/packages/server/src/server/hub/execution-session.websocket.test.ts new file mode 100644 index 000000000..caeccd6c6 --- /dev/null +++ b/packages/server/src/server/hub/execution-session.websocket.test.ts @@ -0,0 +1,145 @@ +import { afterEach, expect, test } from "vitest"; +import { + HubRelationshipHarness, + SetupFailingArchiveWatchFiles, +} from "./test-utils/relationship-harness.js"; + +let relationship: HubRelationshipHarness | null = null; + +afterEach(async () => { + await relationship?.close(); + relationship = null; +}); + +async function launchRelationship(): Promise { + const launched = await HubRelationshipHarness.start(); + await launched.beginConnect().result; + launched.connectLatestSocket(); + relationship = launched; + return launched; +} + +test("Hub retries one durable daemon execution across concurrency and reconstruction", async () => { + const hub = await launchRelationship(); + + const created = await hub.createOwnedConcurrently(); + const update = await hub.ownedUpdate(created.first.agentId); + const stream = await hub.ownedStream(created.first.agentId); + const reconstructed = await hub.reconstructAndReplay(); + + expect(created.duplicate.agentId).toBe(created.first.agentId); + expect(update).toMatchObject({ + executionId: "execution-1", + agentId: created.first.agentId, + agent: { id: created.first.agentId }, + }); + expect(stream).toMatchObject({ executionId: "execution-1", agentId: created.first.agentId }); + expect(reconstructed.replay.agent.id).toBe(created.first.agentId); + expect(reconstructed.durableAgentCount).toBe(1); +}); + +test("Hub denies trusted steering and browser dispatch", async () => { + const hub = await launchRelationship(); + const localAgentId = await hub.createUnrelatedLocalAgent(); + + const steeringDenial = await hub.deniedSteering(localAgentId); + const browserDenial = await hub.deniedBrowserDispatch(); + + expect(steeringDenial).toEqual({ + requestId: "denied-steer", + requestType: "send_agent_message_request", + error: "Session is not authorized for send_agent_message_request", + code: "access_denied", + }); + expect(browserDenial).toEqual({ + requestId: "browser-1", + requestType: "browser.automation.execute.response", + error: "Session is not authorized for browser.automation.execute.response", + code: "access_denied", + }); + expect(hub.observedAgentIds()).not.toContain(localAgentId); + expect(hub.observedTrustedLifecycleMessages()).toEqual([]); +}); + +test("Hub sockets reject trusted hello and capabilities", async () => { + const hub = await launchRelationship(); + + expect(hub.probeTrustedHello()).toBe(4002); +}); + +test("Hub sockets reject trusted binary frames", async () => { + const hub = await launchRelationship(); + + expect(hub.probeBinaryFrame()).toBe(4002); +}); + +test("Hub does not receive trusted broadcasts", async () => { + const hub = await launchRelationship(); + + const trustedBroadcasts = await hub.trustedBroadcastCount(); + const trustedStatus = await hub.trustedDaemonStatus(); + + expect(trustedBroadcasts).toBe(0); + expect(trustedStatus).toMatchObject({ pid: process.pid, relay: { enabled: false } }); + expect(hub.observedTrustedLifecycleMessages()).toEqual([]); +}); + +test("Hub reconnects without retaining trusted session state", async () => { + const hub = await launchRelationship(); + const created = await hub.createOwnedConcurrently(); + + const reconnected = await hub.reconnectAndRetry(); + + expect(reconnected).toMatchObject({ + executionId: "execution-1", + agentId: created.first.agentId, + }); + expect(hub.observedTrustedLifecycleMessages()).toEqual([]); +}); + +test("Hub create forwards worktree and auto-archive through the existing create path", async () => { + const hub = await launchRelationship(); + hub.beginOwnedCreate("worktree-create", "execution-worktree", { + worktree: { mode: "branch-off", newBranch: "hub-created-worktree", base: "main" }, + autoArchive: true, + prompt: "sleep 30", + modeId: "always-ask", + }); + const worktreeCreated = await hub.ownedCreateResult("worktree-create"); + const worktreeCwd = hub.latestCreatedCwd(); + const permission = await hub.ownedPermissionRequest(worktreeCreated.payload.agentId!); + const duringRun = await hub.worktreeState(worktreeCwd!); + const archiveCompletion = hub.waitForOwnedArchiveCompletion(worktreeCreated.payload.agentId!); + await hub.allowOwnedPermission(worktreeCreated.payload.agentId!, permission.id); + await hub.ownedTurnCompletion(worktreeCreated.payload.agentId!); + const archive = await archiveCompletion; + const afterArchive = await hub.worktreeState(worktreeCwd!); + + expect(worktreeCreated).toMatchObject({ + type: "hub.execution.agent.create.response", + payload: { success: true, agent: { cwd: worktreeCwd } }, + }); + expect(worktreeCwd).not.toBe(hub.repoRoot()); + expect(duringRun).toEqual({ exists: true, listed: true }); + expect(afterArchive).toEqual({ exists: false, listed: false }); + expect(archive).toEqual({ + agentArchivedAt: expect.any(String), + workspaceArchivedAt: expect.any(String), + }); +}, 20_000); + +test("archive observation closes its first watcher when the second watcher cannot start", async () => { + const watchFiles = new SetupFailingArchiveWatchFiles(2); + const hub = await HubRelationshipHarness.start(watchFiles); + relationship = hub; + await hub.beginConnect().result; + hub.connectLatestSocket(); + hub.beginOwnedCreate("watch-setup-create", "watch-setup-execution"); + const created = await hub.ownedCreateResult("watch-setup-create"); + + await expect(hub.waitForOwnedArchiveCompletion(created.payload.agentId!)).rejects.toThrow( + "Cannot watch", + ); + + expect(watchFiles.activeDirectories()).toEqual([]); +}); diff --git a/packages/server/src/server/hub/relationship-controller.test.ts b/packages/server/src/server/hub/relationship-controller.test.ts new file mode 100644 index 000000000..d7bbe1f68 --- /dev/null +++ b/packages/server/src/server/hub/relationship-controller.test.ts @@ -0,0 +1,806 @@ +import { createHash } from "node:crypto"; +import { platform } from "node:os"; +import { afterEach, describe, expect, test } from "vitest"; +import { HubRelationshipHarness } from "./test-utils/relationship-harness.js"; + +async function captureUnhandledRejections(action: () => Promise): Promise { + const rejections: unknown[] = []; + const capture = (reason: unknown) => rejections.push(reason); + process.on("unhandledRejection", capture); + try { + await action(); + await new Promise((resolve) => setImmediate(resolve)); + } finally { + process.off("unhandledRejection", capture); + } + return rejections; +} + +describe("Hub relationship", () => { + let relationship: HubRelationshipHarness | null = null; + + afterEach(async () => { + await relationship?.close(); + relationship = null; + }); + + test("the CLI connects, reports status, and disconnects through the daemon", async () => { + relationship = await HubRelationshipHarness.start(); + const connected = await relationship.beginConnect().result; + relationship.connectLatestSocket(); + + const status = await relationship.status(); + const enrollment = relationship.enrollmentAttempts()[0]; + const secret = relationship.relationshipFile()?.credential?.secret; + const disconnected = await relationship.disconnect(); + + expect(connected.state).toBe("connecting"); + expect(status.state).toBe("connected"); + expect(relationship.loggableValues(status)).not.toContain(secret); + expect(relationship.loggableValues(status)).not.toContain(enrollment.credentialVerifier); + expect(relationship.loggableValues(status)).not.toContain(enrollment.token); + expect(relationship.loggableValues(status)).not.toContain(enrollment.idempotencyKey); + expect(disconnected.state).toBe("not_connected"); + }, 30_000); + + test("Hub URLs cannot persist embedded credentials", async () => { + relationship = await HubRelationshipHarness.start(); + + await expect( + relationship.beginConnect("ceremony-token", "https://user:password@hub.example").result, + ).rejects.toThrow(); + + expect(relationship.relationshipFile()).toBeNull(); + expect(relationship.enrollmentAttempts()).toEqual([]); + }); + + test("an authenticated external socket can manage the Hub relationship", async () => { + relationship = await HubRelationshipHarness.start(); + + const responses = await relationship.manageRelationshipFromExternalSocket(); + + expect(responses.map((response) => response.type)).toEqual([ + "hub.management.daemon.connect.response", + "hub.management.daemon.get_status.response", + "hub.management.daemon.disconnect.response", + ]); + expect(relationship.enrollmentAttempts()).toHaveLength(1); + expect(relationship.relationshipFile()).toBeNull(); + }); + + test("an authenticated browser socket can manage the Hub relationship", async () => { + relationship = await HubRelationshipHarness.start(); + + const response = await relationship.connectFromBrowserSocket(); + + expect(response.type).toBe("hub.management.daemon.connect.response"); + expect(relationship.enrollmentAttempts()).toHaveLength(1); + expect(relationship.relationshipFile()?.state).toBe("active"); + }); + + test("persists private generated authority before enrollment and active before dialing", async () => { + relationship = await HubRelationshipHarness.start(); + const privateFileMode = platform() === "win32" ? 0o666 : 0o600; + relationship.holdEnrollment(); + const connecting = relationship.beginConnect("one-time-token"); + + const enrollment = await relationship.enrollmentBegins(); + const pending = relationship.enrollmentInvocation(); + + expect(pending).toMatchObject({ + record: { + state: "pending", + relationship: { + daemonId: enrollment.daemonId, + idempotencyKey: enrollment.idempotencyKey, + }, + credential: { secret: expect.any(String) }, + enrollment: { token: "one-time-token" }, + identity: { serverId: expect.any(String), daemonPublicKey: expect.any(String) }, + }, + }); + expect(pending.mode).toBe(privateFileMode); + const secret = pending.record.credential?.secret; + expect(secret).toEqual(expect.any(String)); + expect(enrollment.credentialVerifier).toBe( + createHash("sha256") + .update(secret ?? "") + .digest("base64url"), + ); + relationship.completeEnrollment(); + await connecting.result; + await relationship.socketDialed(); + expect(relationship.socketInvocation()).toMatchObject({ + mode: privateFileMode, + record: { state: "active", relationship: { daemonId: enrollment.daemonId } }, + }); + }); + + test("Hub enrollment cannot widen the daemon's locally granted scopes", async () => { + relationship = await HubRelationshipHarness.start(); + relationship.returnEnrollmentScopes(["hub.execution.*", "*"]); + + await relationship.beginConnect().result; + relationship.connectLatestSocket(); + const responses = relationship.sendHubRequestOnLatest({ + type: "daemon.get_status.request", + requestId: "scope-escalation", + }); + + expect(relationship.relationshipFile()?.relationship.scopes).toEqual(["hub.execution.*"]); + expect(responses).toContainEqual({ + type: "rpc_error", + payload: { + requestId: "scope-escalation", + requestType: "daemon.get_status.request", + error: "Session is not authorized for daemon.get_status.request", + code: "access_denied", + }, + }); + }); + + test("a lost enrollment response reuses the exact ceremony", async () => { + relationship = await HubRelationshipHarness.start(); + relationship.holdEnrollment(); + const connecting = relationship.beginConnect("same-token"); + await relationship.enrollmentBegins(); + relationship.loseEnrollmentResponse(); + await connecting.result; + + await relationship.retry(); + const attempts = relationship.enrollmentAttempts(); + + expect(attempts).toHaveLength(2); + expect(attempts[1]).toEqual(attempts[0]); + expect(relationship.enrolledRelationships()).toBe(1); + }); + + test("a fresh token replaces the token for a pending enrollment ceremony", async () => { + relationship = await HubRelationshipHarness.start(); + relationship.holdEnrollment(); + const firstConnect = relationship.beginConnect("expired-token"); + const firstResult = expect(firstConnect.result).resolves.toMatchObject({ + state: "reconnecting", + }); + await relationship.enrollmentBegins(); + relationship.loseEnrollmentResponse(); + await firstResult; + + relationship.holdEnrollment(); + const secondConnect = relationship.beginConnect("fresh-token"); + const secondResult = expect(secondConnect.result).resolves.toMatchObject({ + state: "connecting", + }); + const retried = await relationship.enrollmentBegins(); + + expect(retried.token).toBe("fresh-token"); + expect(relationship.relationshipFile()?.enrollment?.token).toBe("fresh-token"); + relationship.completeEnrollment(); + await secondResult; + }); + + test("a stale enrollment rejection cannot discard a fresh pending ceremony", async () => { + relationship = await HubRelationshipHarness.start(); + relationship.holdEnrollment(); + const expiredConnect = relationship.beginConnect("expired-token"); + await relationship.enrollmentBegins(); + + relationship.holdEnrollment(); + const freshConnect = relationship.beginConnect("fresh-token"); + const freshEnrollment = await relationship.enrollmentBegins(); + relationship.rejectEnrollment(0, 403); + await expiredConnect.result; + + expect(relationship.relationshipFile()?.enrollment?.token).toBe("fresh-token"); + expect(relationship.pendingRelationshipRetries()).toBe(0); + + relationship.completeEnrollment(); + await freshConnect.result; + + expect(freshEnrollment.token).toBe("fresh-token"); + expect(relationship.relationshipFile()?.state).toBe("active"); + expect(relationship.enrollmentAttempts()).toHaveLength(2); + expect(relationship.pendingRelationshipRetries()).toBe(0); + }); + + test("a rejected pending enrollment is discarded without blocking daemon restart", async () => { + relationship = await HubRelationshipHarness.start(); + relationship.holdEnrollment(); + const connecting = relationship.beginConnect("expired-token"); + await relationship.enrollmentBegins(); + relationship.loseEnrollmentResponse(); + await connecting.result; + relationship.rejectNextEnrollment(401); + + await relationship.restartDaemon(); + + expect(await relationship.status()).toMatchObject({ state: "not_connected" }); + expect(relationship.relationshipFile()).toBeNull(); + }); + + test("a scheduled enrollment rejection is contained after removing pending authority", async () => { + relationship = await HubRelationshipHarness.start(); + relationship.holdEnrollment(); + const connecting = relationship.beginConnect("expired-token"); + await relationship.enrollmentBegins(); + relationship.loseEnrollmentResponse(); + await connecting.result; + relationship.rejectNextEnrollment(403); + + const unhandledRejections = await captureUnhandledRejections(() => relationship!.retry()); + const status = await relationship.status(); + + expect(status).toMatchObject({ state: "not_connected" }); + expect(relationship.relationshipFile()).toBeNull(); + expect(relationship.pendingRelationshipRetries()).toBe(0); + expect(unhandledRejections).toEqual([]); + }); + + test.each(["{not-json", JSON.stringify({ version: 1, state: "unknown" })])( + "an invalid relationship file is quarantined without blocking daemon startup", + async (contents) => { + relationship = await HubRelationshipHarness.start(); + await relationship.corruptRelationshipFile(contents); + + await relationship.startStoppedDaemon(); + + expect(await relationship.status()).toMatchObject({ state: "not_connected" }); + expect(relationship.relationshipFile()).toBeNull(); + expect(await relationship.quarantinedRelationshipFiles()).toHaveLength(1); + }, + ); + + test.each([ + ["a non-HTTP scheme", "ftp://hub.test"], + ["embedded credentials", "https://user:password@hub.test"], + ["a query", "https://hub.test?token=secret"], + ["a fragment", "https://hub.test#secret"], + ])("a persisted Hub origin with %s is quarantined before startup", async (_, hubOrigin) => { + relationship = await HubRelationshipHarness.start(); + await relationship.corruptRelationshipFile( + JSON.stringify({ + version: 1, + state: "pending", + relationship: { + daemonId: "daemon-1", + idempotencyKey: "ceremony-1", + hubOrigin, + createdAt: "2026-07-13T00:00:00.000Z", + scopes: ["hub.execution.*"], + }, + credential: { secret: "credential" }, + enrollment: { token: "enrollment-token" }, + identity: { serverId: "server-1", daemonPublicKey: "public-key" }, + }), + ); + + await relationship.startStoppedDaemon(); + + expect(await relationship.status()).toMatchObject({ state: "not_connected" }); + expect(relationship.relationshipFile()).toBeNull(); + expect(await relationship.quarantinedRelationshipFiles()).toHaveLength(1); + expect(relationship.enrollmentAttempts()).toEqual([]); + }); + + test("a persisted Hub relationship cannot widen its local execution scope", async () => { + relationship = await HubRelationshipHarness.start(); + await relationship.beginConnect().result; + const persisted = relationship.relationshipFile(); + expect(persisted).not.toBeNull(); + persisted!.relationship.scopes = ["*"]; + await relationship.corruptRelationshipFile(JSON.stringify(persisted)); + const socketAttemptsBeforeRestart = relationship.socketAttempts(); + + await relationship.startStoppedDaemon(); + + expect(await relationship.status()).toMatchObject({ state: "not_connected" }); + expect(relationship.relationshipFile()).toBeNull(); + expect(await relationship.quarantinedRelationshipFiles()).toHaveLength(1); + expect(relationship.socketAttempts()).toBe(socketAttemptsBeforeRestart); + }); + + test("a persisted non-WebSocket transport is quarantined before daemon startup", async () => { + relationship = await HubRelationshipHarness.start(); + await relationship.corruptRelationshipFile( + JSON.stringify({ + version: 1, + state: "active", + relationship: { + id: "relationship-1", + idempotencyKey: "ceremony-1", + hubOrigin: "https://hub.test", + createdAt: "2026-07-13T00:00:00.000Z", + scopes: ["hub.execution.*"], + }, + credential: { secret: "credential" }, + transport: { kind: "direct_websocket", webSocketUrl: "ftp://hub.test/daemon" }, + }), + ); + + await relationship.startStoppedDaemon(); + + expect(await relationship.status()).toMatchObject({ state: "not_connected" }); + expect(relationship.relationshipFile()).toBeNull(); + expect(await relationship.quarantinedRelationshipFiles()).toHaveLength(1); + expect(relationship.socketAttempts()).toBe(0); + }); + + test("a persisted WebSocket transport with a fragment is quarantined before startup", async () => { + relationship = await HubRelationshipHarness.start(); + await relationship.corruptRelationshipFile( + JSON.stringify({ + version: 1, + state: "active", + relationship: { + id: "relationship-1", + idempotencyKey: "ceremony-1", + hubOrigin: "https://hub.test", + createdAt: "2026-07-13T00:00:00.000Z", + scopes: ["hub.execution.*"], + }, + credential: { secret: "credential" }, + transport: { + kind: "direct_websocket", + webSocketUrl: "wss://hub.test/daemon#fragment", + }, + }), + ); + + await relationship.startStoppedDaemon(); + + expect(await relationship.status()).toMatchObject({ state: "not_connected" }); + expect(relationship.relationshipFile()).toBeNull(); + expect(await relationship.quarantinedRelationshipFiles()).toHaveLength(1); + expect(relationship.socketAttempts()).toBe(0); + }); + + test("disconnect revokes an ambiguous pending enrollment before removing local authority", async () => { + relationship = await HubRelationshipHarness.start(); + relationship.holdEnrollment(); + const connecting = relationship.beginConnect("one-time-token"); + const enrollment = await relationship.enrollmentBegins(); + const credential = relationship.relationshipFile()?.credential?.secret; + relationship.loseEnrollmentResponse(); + await connecting.result; + relationship.failRevocations(2); + + const disconnected = await relationship.disconnect(); + expect(disconnected.state).toBe("disconnecting"); + expect(relationship.relationshipFile()?.state).toBe("disconnecting"); + + await relationship.restartDaemon(); + await relationship.retry(); + + expect(relationship.revocationAttempts()).toBe(3); + expect(relationship.latestRevocation()).toEqual( + expect.objectContaining({ + daemonId: enrollment.daemonId, + hubOrigin: enrollment.hubOrigin, + credential, + }), + ); + expect(relationship.relationshipFile()).toBeNull(); + }); + + test("disconnect revokes only after an in-flight enrollment settles", async () => { + relationship = await HubRelationshipHarness.start(); + relationship.holdEnrollment(); + const connecting = relationship.beginConnect("one-time-token"); + const enrollment = await relationship.enrollmentBegins(); + + const disconnecting = relationship.beginDisconnect(); + await relationship.relationshipStateBecomes("disconnecting"); + expect(relationship.revocationAttempts()).toBe(0); + + relationship.completeEnrollment(); + await connecting.result; + const disconnected = await disconnecting.result; + + expect(disconnected.state).toBe("not_connected"); + expect(relationship.latestRevocation()?.daemonId).toBe(enrollment.daemonId); + expect(relationship.revocationAttempts()).toBe(1); + expect(relationship.socketAttempts()).toBe(0); + expect(relationship.relationshipFile()).toBeNull(); + }); + + test("daemon restart reconnects the same durable relationship", async () => { + relationship = await HubRelationshipHarness.start(); + await relationship.beginConnect().result; + const id = relationship.relationshipFile()?.relationship.daemonId; + relationship.connectLatestSocket(); + await relationship.socketDialed(); + + await relationship.restartDaemon(); + await relationship.socketDialed(); + + expect(relationship.relationshipFile()?.relationship.daemonId).toBe(id); + expect(relationship.socketAttempts()).toBe(2); + }); + + test("daemon restart closes an interrupted owned turn without replaying its prompt", async () => { + relationship = await HubRelationshipHarness.start(); + await relationship.beginConnect().result; + await relationship.socketDialed(); + relationship.connectLatestSocket(); + const daemonId = relationship.relationshipFile()?.relationship.daemonId; + const prompt = "sleep 30"; + relationship.beginOwnedCreate("running-create", "execution-running", { + prompt, + modeId: "full-access", + }); + const created = await relationship.ownedCreateResult("running-create"); + const running = await relationship.ownedRunningUpdate(created.payload.agentId!); + + await relationship.restartDaemon(); + await relationship.socketDialed(); + relationship.connectLatestSocket(); + relationship.beginOwnedCreate("running-duplicate", "execution-running", { prompt }); + const duplicate = await relationship.ownedCreateResult("running-duplicate"); + expect(duplicate).toMatchObject({ + payload: { + success: true, + executionId: "execution-running", + agentId: created.payload.agentId, + agent: { id: created.payload.agentId, status: "closed" }, + }, + }); + const durableAgentIds = await relationship.durableOwnedAgentIds(); + + expect(running).toMatchObject({ + executionId: "execution-running", + agentId: created.payload.agentId, + agent: { id: created.payload.agentId, status: "running" }, + }); + expect(relationship.relationshipFile()?.relationship.daemonId).toBe(daemonId); + expect(durableAgentIds).toEqual([created.payload.agentId]); + expect(relationship.providerCreations()).toBe(1); + expect(relationship.providerResumes()).toBe(0); + expect(relationship.providerPromptTexts()).toEqual([prompt]); + expect(relationship.latestOwnedTurnCompletions(created.payload.agentId!)).toBe(0); + }); + + test("an owned execution does not persist a daemon-restart prompt intent", async () => { + relationship = await HubRelationshipHarness.start(); + await relationship.beginConnect().result; + relationship.connectLatestSocket(); + relationship.beginOwnedCreate("intent-create", "execution-intent", { prompt: "sleep 30" }); + await relationship.ownedCreateResult("intent-create"); + + expect(await relationship.hubExecutionIntentFiles()).toEqual([]); + }); + + test("an ordinary duplicate create does not replay a completed owned turn", async () => { + relationship = await HubRelationshipHarness.start(); + await relationship.beginConnect().result; + relationship.connectLatestSocket(); + const prompt = "respond with exactly: completed once"; + relationship.beginOwnedCreate("completed-create", "execution-completed", { prompt }); + const created = await relationship.ownedCreateResult("completed-create"); + await relationship.ownedTurnCompletion(created.payload.agentId!); + + relationship.beginOwnedCreate("completed-duplicate", "execution-completed", { prompt }); + const duplicate = await relationship.ownedCreateResult("completed-duplicate"); + + expect(duplicate).toMatchObject({ + type: "hub.execution.agent.create.response", + payload: { + success: true, + executionId: "execution-completed", + agentId: created.payload.agentId, + agent: { status: "idle" }, + }, + }); + expect(relationship.providerPromptTexts()).toEqual([prompt]); + expect(await relationship.durableOwnedAgentIds()).toEqual([created.payload.agentId]); + expect(relationship.latestOwnedTurnCompletions(created.payload.agentId!)).toBe(1); + }); + + test("daemon restart closes a completed owned session without replaying its prompt", async () => { + relationship = await HubRelationshipHarness.start(); + await relationship.beginConnect().result; + relationship.connectLatestSocket(); + const prompt = "respond with exactly: already completed"; + relationship.beginOwnedCreate("idle-create", "execution-idle", { prompt }); + const created = await relationship.ownedCreateResult("idle-create"); + await relationship.ownedTurnCompletion(created.payload.agentId!); + + await relationship.restartDaemon(); + await relationship.socketDialed(); + relationship.connectLatestSocket(); + relationship.beginOwnedCreate("idle-retry", "execution-idle", { prompt }); + const retried = await relationship.ownedCreateResult("idle-retry"); + + expect(retried).toMatchObject({ + payload: { + executionId: "execution-idle", + agentId: created.payload.agentId, + agent: { id: created.payload.agentId, status: "closed" }, + }, + }); + expect(relationship.providerPromptTexts()).toEqual([prompt]); + expect(relationship.providerResumes()).toBe(0); + expect(relationship.latestOwnedTurnCompletions(created.payload.agentId!)).toBe(0); + }); + + test("daemon restart closes a failed owned session without replaying its prompt", async () => { + relationship = await HubRelationshipHarness.start(); + await relationship.beginConnect().result; + relationship.connectLatestSocket(); + const prompt = "emit a turn failure"; + relationship.beginOwnedCreate("failed-create", "execution-failed", { prompt }); + const created = await relationship.ownedCreateResult("failed-create"); + const failed = await relationship.ownedTurnFailure(created.payload.agentId!); + + await relationship.restartDaemon(); + await relationship.socketDialed(); + relationship.connectLatestSocket(); + relationship.beginOwnedCreate("failed-retry", "execution-failed", { prompt }); + const retried = await relationship.ownedCreateResult("failed-retry"); + + expect(failed).toMatchObject({ + executionId: "execution-failed", + agentId: created.payload.agentId, + event: { type: "turn_failed" }, + }); + expect(retried).toMatchObject({ + payload: { + executionId: "execution-failed", + agentId: created.payload.agentId, + agent: { id: created.payload.agentId, status: "closed" }, + }, + }); + expect(relationship.providerPromptTexts()).toEqual([prompt]); + expect(relationship.providerResumes()).toBe(0); + expect(relationship.latestOwnedTurnCompletions(created.payload.agentId!)).toBe(0); + }); + + test("Hub revocation leaves no execution intent artifacts", async () => { + relationship = await HubRelationshipHarness.start(); + await relationship.beginConnect().result; + relationship.connectLatestSocket(); + relationship.beginOwnedCreate("revoked-create", "execution-revoked", { prompt: "sleep 30" }); + await relationship.ownedCreateResult("revoked-create"); + + relationship.rejectRelationship(401); + + expect(await relationship.hubExecutionIntentFiles()).toEqual([]); + }); + + test("stale socket generations cannot replace or unregister the current socket", async () => { + relationship = await HubRelationshipHarness.start(); + await relationship.beginConnect().result; + relationship.closeSocket(0, 1006); + await relationship.retry(); + relationship.connectSocket(1); + + relationship.connectSocket(0); + relationship.closeSocket(0, 1000); + const messages = relationship.sendHubRequestOnLatest({ + type: "daemon.get_status.request", + requestId: "still-current", + }); + + expect(messages).toContainEqual({ + type: "rpc_error", + payload: { + requestId: "still-current", + requestType: "daemon.get_status.request", + error: "Session is not authorized for daemon.get_status.request", + code: "access_denied", + }, + }); + expect(relationship.socketAttempts()).toBe(2); + }); + + test("replayed create across socket generations shares one pending durable execution", async () => { + relationship = await HubRelationshipHarness.start(); + await relationship.beginConnect().result; + relationship.connectLatestSocket(); + relationship.holdAgentCreation(); + relationship.beginOwnedCreate("first-create"); + await relationship.agentCreationAttempts(1); + + relationship.closeLatestSocket(1006); + await relationship.retry(); + relationship.connectLatestSocket(); + relationship.beginOwnedCreate("replayed-create"); + relationship.finishAgentCreation(); + + const replayed = await relationship.ownedCreateResult("replayed-create"); + const durableAgentIds = await relationship.durableOwnedAgentIds(); + + expect(relationship.socketDeliveredResponse(0, "first-create")).toBe(false); + expect(replayed).toMatchObject({ + type: "hub.execution.agent.create.response", + payload: { + success: true, + executionId: "execution-race", + agentId: durableAgentIds[0], + }, + }); + expect(relationship.providerCreations()).toBe(1); + expect(durableAgentIds).toHaveLength(1); + }); + + test("idempotent retry waits for a pending create across socket generations", async () => { + relationship = await HubRelationshipHarness.start(); + await relationship.beginConnect().result; + relationship.connectLatestSocket(); + relationship.holdAgentCreation(); + relationship.beginOwnedCreate("pending-create", "pending-execution"); + await relationship.agentCreationAttempts(1); + + relationship.closeLatestSocket(1006); + await relationship.retry(); + relationship.connectLatestSocket(); + relationship.beginOwnedCreate("pending-retry", "pending-execution"); + relationship.finishAgentCreation(); + + await expect(relationship.ownedCreateResult("pending-retry")).resolves.toMatchObject({ + payload: { + executionId: "pending-execution", + agentId: expect.any(String), + agent: { id: expect.any(String) }, + }, + }); + expect(relationship.providerCreations()).toBe(1); + expect(await relationship.durableOwnedAgentIds()).toHaveLength(1); + }); + + test("force disconnect fences a pending create before removing authority", async () => { + relationship = await HubRelationshipHarness.start(); + await relationship.beginConnect().result; + relationship.connectLatestSocket(); + relationship.holdAgentCreation(); + relationship.beginOwnedCreate("cancelled-create", "cancelled-execution", { + worktree: { mode: "branch-off", newBranch: "cancelled-hub-create" }, + }); + await relationship.agentCreationAttempts(1); + + const disconnecting = relationship.beginDisconnect(true); + await relationship.relationshipStateBecomes(null); + relationship.finishAgentCreation(); + await disconnecting.result; + + expect(relationship.activeOwnedAgentIds()).toEqual([]); + expect(await relationship.durableOwnedAgentIds()).toEqual([]); + expect(await relationship.listedWorktrees()).toHaveLength(1); + }); + + test("re-enrollment gives the same daemon fresh execution authority", async () => { + relationship = await HubRelationshipHarness.start(); + await relationship.beginConnect().result; + relationship.connectLatestSocket(); + relationship.beginOwnedCreate("first-create", "first-execution"); + const first = await relationship.ownedCreateResult("first-create"); + const daemonId = relationship.relationshipFile()?.relationship.daemonId; + + await relationship.disconnect(); + await relationship.beginConnect("fresh-token").result; + relationship.connectLatestSocket(); + relationship.beginOwnedCreate("second-create", "second-execution"); + const second = await relationship.ownedCreateResult("second-create"); + + expect(relationship.relationshipFile()?.relationship.daemonId).toBe(daemonId); + expect(first).toMatchObject({ payload: { success: true, executionId: "first-execution" } }); + expect(second).toMatchObject({ payload: { success: true, executionId: "second-execution" } }); + expect(relationship.providerCreations()).toBe(2); + }); + + test("daemon shutdown fences a pending create before closing owned agents", async () => { + relationship = await HubRelationshipHarness.start(); + await relationship.beginConnect().result; + relationship.connectLatestSocket(); + relationship.holdAgentCreation(); + relationship.beginOwnedCreate("shutdown-create", "execution-shutdown", { prompt: "sleep 30" }); + await relationship.agentCreationAttempts(1); + + const shutdown = relationship.shutdownDaemon(); + relationship.finishAgentCreation(); + await shutdown; + + expect(relationship.socketDeliveredResponse(0, "shutdown-create")).toBe(false); + expect(await relationship.durableOwnedAgentIdsOnDisk()).toEqual([]); + }); + + test.each([ + [4403, "Hub revoked this relationship"], + [401, "Hub rejected socket authentication (401)"], + [403, "Hub rejected socket authentication (403)"], + ] as const)("authentication rejection %s revokes permanently", async (code, reason) => { + relationship = await HubRelationshipHarness.start(); + await relationship.beginConnect("private-token").result; + const enrollment = relationship.enrollmentAttempts()[0]; + const secret = relationship.relationshipFile()?.credential?.secret; + relationship.rejectRelationship(code); + + await relationship.restartDaemon(); + const status = await relationship.status(); + const persisted = relationship.relationshipFile(); + + expect(status).toMatchObject({ + state: "revoked", + daemonId: enrollment.daemonId, + hub: "https://hub.test", + scopes: "hub.execution.*", + error: reason, + }); + expect(persisted?.state).toBe("revoked"); + expect(persisted?.relationship).toMatchObject({ + daemonId: enrollment.daemonId, + hubOrigin: "https://hub.test", + scopes: ["hub.execution.*"], + }); + expect(persisted?.reason).toBe(reason); + expect(persisted).not.toHaveProperty("credential"); + expect(persisted).not.toHaveProperty("relationship.idempotencyKey"); + expect(relationship.socketAttempts()).toBe(1); + const loggable = relationship.loggableValues(status); + const reconstructed = JSON.stringify({ status, persisted }); + expect(reconstructed).not.toContain(secret); + expect(reconstructed).not.toContain(enrollment.credentialVerifier); + expect(reconstructed).not.toContain(enrollment.token); + expect(reconstructed).not.toContain(enrollment.idempotencyKey); + expect(loggable).not.toContain(secret); + expect(loggable).not.toContain(enrollment.credentialVerifier); + expect(loggable).not.toContain(enrollment.token); + expect(loggable).not.toContain(enrollment.idempotencyKey); + }); + + test("offline disconnect retries across runtime and restart without opening a socket", async () => { + relationship = await HubRelationshipHarness.start(); + await relationship.beginConnect().result; + relationship.connectLatestSocket(); + relationship.failRevocations(3); + + const disconnecting = await relationship.disconnect(); + await relationship.retry(); + await relationship.restartDaemon(); + await relationship.retry(); + + expect(disconnecting.state).toBe("disconnecting"); + expect(relationship.revocationAttempts()).toBe(4); + expect(relationship.socketAttempts()).toBe(1); + expect(relationship.relationshipFile()).toBeNull(); + }); + + test("successful disconnect clears a transient revocation error", async () => { + relationship = await HubRelationshipHarness.start(); + await relationship.beginConnect().result; + relationship.failRevocations(1); + + const disconnecting = await relationship.disconnect(); + await relationship.retry(); + + expect(disconnecting).toMatchObject({ state: "disconnecting", error: expect.any(String) }); + expect(await relationship.status()).toMatchObject({ state: "not_connected", error: null }); + }); + + test("force disconnect removes local authority and reports the remote warning", async () => { + relationship = await HubRelationshipHarness.start(); + await relationship.beginConnect().result; + relationship.failRevocations(1); + await relationship.disconnect(); + + const forced = await relationship.disconnect(true); + + expect(forced.state).toBe("not_connected"); + expect(forced.warning).toContain("remote revocation"); + expect(relationship.relationshipFile()).toBeNull(); + }); + + test("disconnect leaves no intent artifacts and shutdown closes the owned agent", async () => { + relationship = await HubRelationshipHarness.start(); + await relationship.beginConnect().result; + relationship.connectLatestSocket(); + relationship.beginOwnedCreate("orphan-create", "execution-orphan", { prompt: "sleep 30" }); + const created = await relationship.ownedCreateResult("orphan-create"); + expect(created).toMatchObject({ payload: { agent: { status: "running" } } }); + + await relationship.disconnect(true); + expect(await relationship.hubExecutionIntentFiles()).toEqual([]); + await relationship.restartDaemon(); + + expect(await relationship.storedOwnedStatus(created.payload.agentId!)).toBe("closed"); + }); +}); diff --git a/packages/server/src/server/hub/relationship-controller.ts b/packages/server/src/server/hub/relationship-controller.ts new file mode 100644 index 000000000..457a5b701 --- /dev/null +++ b/packages/server/src/server/hub/relationship-controller.ts @@ -0,0 +1,556 @@ +import { createHash, randomBytes, randomUUID } from "node:crypto"; +import { existsSync, readFileSync, renameSync, rmSync } from "node:fs"; +import path from "node:path"; +import type pino from "pino"; +import { z } from "zod"; +import { ensurePrivateFile, writePrivateFileAtomicSync } from "../private-files.js"; +import type { WebSocketLike } from "../websocket-server.js"; +import type { HubExecutionAgents } from "./daemon-executions.js"; +import type { + HubRelationshipRemote, + HubSocketConnection, + HubSocketEvents, +} from "./relationship-remote.js"; +import { HubEnrollmentRejectedError } from "./relationship-remote.js"; +import { BoundedExponentialHubRetryPolicy } from "./relationship-retry.js"; + +const FILE_NAME = "hub-relationship.json"; +const HUB_EXECUTION_SCOPE = "hub.execution.*"; +const SCOPES = [HUB_EXECUTION_SCOPE] as const; +const HubOriginSchema = z + .string() + .url() + .superRefine((value, context) => { + try { + normalizeHubUrl(value); + } catch (error) { + context.addIssue({ + code: "custom", + message: error instanceof Error ? error.message : "Invalid Hub URL", + }); + } + }); + +const RelationshipSchema = z.object({ + daemonId: z.string().min(1), + idempotencyKey: z.string().min(1), + hubOrigin: HubOriginSchema, + createdAt: z.string(), + scopes: z.tuple([z.literal(HUB_EXECUTION_SCOPE)]), +}); +const SanitizedRelationshipSchema = RelationshipSchema.omit({ idempotencyKey: true }); +const CredentialSchema = z.object({ secret: z.string().min(1) }); +const TransportSchema = z.object({ + kind: z.literal("direct_websocket"), + webSocketUrl: z + .string() + .url() + .refine((value) => ["ws:", "wss:"].includes(new URL(value).protocol)) + .refine((value) => new URL(value).hash === ""), +}); +const PendingSchema = z.object({ + version: z.literal(1), + state: z.literal("pending"), + relationship: RelationshipSchema, + credential: CredentialSchema, + enrollment: z.object({ token: z.string().min(1) }), + identity: z.object({ serverId: z.string().min(1), daemonPublicKey: z.string().min(1) }), +}); +const ActiveSchema = z.object({ + version: z.literal(1), + state: z.literal("active"), + relationship: RelationshipSchema, + credential: CredentialSchema, + transport: TransportSchema, +}); +const DisconnectingSchema = z.object({ + version: z.literal(1), + state: z.literal("disconnecting"), + relationship: RelationshipSchema, + credential: CredentialSchema, + transport: TransportSchema.optional(), +}); +const RevokedSchema = z.object({ + version: z.literal(1), + state: z.literal("revoked"), + relationship: SanitizedRelationshipSchema, + transport: TransportSchema.optional(), + reason: z.string().optional(), +}); +const RecordSchema = z + .discriminatedUnion("state", [PendingSchema, ActiveSchema, DisconnectingSchema, RevokedSchema]) + .superRefine((record, context) => { + if (!("transport" in record) || !record.transport) return; + const hub = new URL(record.relationship.hubOrigin); + const socket = new URL(record.transport.webSocketUrl); + const expectedProtocol = hub.protocol === "https:" ? "wss:" : "ws:"; + if (socket.protocol === expectedProtocol && socket.host === hub.host) return; + context.addIssue({ + code: "custom", + path: ["transport", "webSocketUrl"], + message: "Hub WebSocket URL must match the Hub origin", + }); + }); +type PendingRecord = z.infer; +type ActiveRecord = z.infer; +type DisconnectingRecord = z.infer; +type RevokedRecord = z.infer; +type HubRelationshipRecord = z.infer; + +export type HubConnectionState = + | "not_connected" + | "connecting" + | "connected" + | "reconnecting" + | "disconnecting" + | "revoked"; + +export interface HubRelationshipStatus { + state: HubConnectionState; + daemonId: string | null; + hubOrigin: string | null; + scopes: string[]; + connectedAt: string | null; + lastError: string | null; +} + +export interface HubRelationshipManagement { + connect(input: { hubUrl: string; token: string }): Promise; + status(): HubRelationshipStatus; + disconnect(input: { + force: boolean; + }): Promise<{ status: HubRelationshipStatus; warning?: string }>; +} + +export interface ScheduledRelationshipTask { + cancel(): void; +} + +export interface HubRelationshipClock { + now(): Date; + schedule(delayMs: number, task: () => void): ScheduledRelationshipTask; +} + +export interface HubRelationshipRetryPolicy { + delay(attempt: number): number; +} + +export interface HubRelationshipControllerOptions { + paseoHome: string; + serverId: string; + daemonPublicKey: string; + logger: pino.Logger; + remote: HubRelationshipRemote; + clock?: HubRelationshipClock; + retryPolicy?: HubRelationshipRetryPolicy; + createDaemonId?: () => string; + attachSocket: ( + socket: WebSocketLike, + options: { daemonId: string; scopes: readonly string[]; agents: HubExecutionAgents }, + ) => Promise; + createExecutionAgents: (daemonId: string) => HubExecutionAgents; +} + +const systemClock: HubRelationshipClock = { + now: () => new Date(), + schedule(delayMs, task) { + const timer = setTimeout(task, delayMs); + timer.unref?.(); + return { cancel: () => clearTimeout(timer) }; + }, +}; + +function normalizeHubUrl(value: string): string { + const url = new URL(value); + if (url.protocol !== "http:" && url.protocol !== "https:") { + throw new Error("Hub URL must use HTTP or HTTPS"); + } + if (url.username || url.password) { + throw new Error("Hub URL cannot include credentials"); + } + if (url.search || url.hash) { + throw new Error("Hub URL cannot include a query or fragment"); + } + url.pathname = url.pathname.replace(/\/$/, ""); + return url.toString().replace(/\/$/, ""); +} + +export class HubRelationshipController implements HubRelationshipManagement { + private readonly filePath: string; + private readonly clock: HubRelationshipClock; + private readonly retryPolicy: HubRelationshipRetryPolicy; + private record: HubRelationshipRecord | null; + private state: HubConnectionState = "not_connected"; + private connectedAt: string | null = null; + private lastError: string | null = null; + private socket: HubSocketConnection | null = null; + private retry: ScheduledRelationshipTask | null = null; + private generation = 0; + private enrollmentGeneration = 0; + private retryAttempt = 0; + private readonly inFlightEnrollments = new Set>(); + private executionAgents: { daemonId: string; value: HubExecutionAgents } | null = null; + + constructor(private readonly options: HubRelationshipControllerOptions) { + this.filePath = path.join(options.paseoHome, FILE_NAME); + this.clock = options.clock ?? systemClock; + this.retryPolicy = options.retryPolicy ?? new BoundedExponentialHubRetryPolicy(); + this.record = this.load(); + if (this.record?.state === "revoked") { + this.state = "revoked"; + this.lastError = this.record.reason ?? null; + } else if (this.record?.state === "disconnecting") this.state = "disconnecting"; + else if (this.record) this.state = "connecting"; + } + + async start(): Promise { + if (this.record?.state === "active") this.openSocket(this.record, false); + if (this.record?.state === "pending") { + const enrollmentGeneration = this.beginEnrollmentAttempt(); + try { + await this.tryEnrollment(this.record, enrollmentGeneration); + } catch (error) { + if (!(error instanceof HubEnrollmentRejectedError)) throw error; + this.options.logger.warn( + { statusCode: error.statusCode }, + "Discarded rejected pending Hub enrollment during startup", + ); + } + } + if (this.record?.state === "disconnecting") await this.tryRevocation(this.record); + } + + async stop(): Promise { + const pendingExecutionCleanup = this.retireExecutionAgents(); + this.cancelLifecycle(); + this.socket?.close(); + this.socket = null; + await pendingExecutionCleanup; + } + + status(): HubRelationshipStatus { + return { + state: this.state, + daemonId: this.record?.relationship.daemonId ?? null, + hubOrigin: this.record?.relationship.hubOrigin ?? null, + scopes: this.record?.relationship.scopes.slice() ?? [], + connectedAt: this.connectedAt, + lastError: this.lastError, + }; + } + + async connect(input: { hubUrl: string; token: string }): Promise { + if (this.record?.state === "pending") { + if (normalizeHubUrl(input.hubUrl) !== this.record.relationship.hubOrigin) { + throw new Error("A pending Hub enrollment already exists for a different Hub"); + } + this.record = { ...this.record, enrollment: { token: input.token } }; + const enrollmentGeneration = this.beginEnrollmentAttempt(); + this.persist(this.record); + this.state = "connecting"; + this.lastError = null; + await this.tryEnrollment(this.record, enrollmentGeneration); + return this.status(); + } + if (this.record && this.record.state !== "revoked") { + throw new Error("This daemon already has a Hub relationship"); + } + const pending: PendingRecord = { + version: 1, + state: "pending", + relationship: { + daemonId: this.options.createDaemonId?.() ?? randomUUID(), + idempotencyKey: randomUUID(), + hubOrigin: normalizeHubUrl(input.hubUrl), + createdAt: this.clock.now().toISOString(), + scopes: [...SCOPES], + }, + credential: { secret: randomBytes(32).toString("base64url") }, + enrollment: { token: input.token }, + identity: { serverId: this.options.serverId, daemonPublicKey: this.options.daemonPublicKey }, + }; + this.persist(pending); + this.record = pending; + this.state = "connecting"; + this.lastError = null; + await this.tryEnrollment(pending, this.beginEnrollmentAttempt()); + return this.status(); + } + + async disconnect(input: { + force: boolean; + }): Promise<{ status: HubRelationshipStatus; warning?: string }> { + const waitForEnrollment = this.record?.state === "pending"; + const pendingCreateCleanup = this.retireExecutionAgents(); + this.cancelLifecycle(); + this.socket?.close(); + this.socket = null; + if (!this.record || this.record.state === "revoked") { + this.remove(); + await pendingCreateCleanup; + return { status: this.status() }; + } + if (input.force) { + this.remove(); + await pendingCreateCleanup; + return { + status: this.status(), + warning: "Local Hub credential removed; remote revocation may remain pending.", + }; + } + const disconnecting: DisconnectingRecord = { + version: 1, + state: "disconnecting", + relationship: this.record.relationship, + credential: this.record.credential, + ...(this.record.state === "active" ? { transport: this.record.transport } : {}), + }; + this.persist(disconnecting); + this.record = disconnecting; + this.state = "disconnecting"; + if (waitForEnrollment) { + await Promise.all(this.inFlightEnrollments); + } + await this.tryRevocation(disconnecting); + await pendingCreateCleanup; + return { status: this.status() }; + } + + private async tryEnrollment(pending: PendingRecord, enrollmentGeneration: number): Promise { + if (enrollmentGeneration !== this.enrollmentGeneration) return; + const verifier = createHash("sha256").update(pending.credential.secret).digest("base64url"); + const request = this.options.remote.enroll({ + daemonId: pending.relationship.daemonId, + idempotencyKey: pending.relationship.idempotencyKey, + hubOrigin: pending.relationship.hubOrigin, + token: pending.enrollment.token, + serverId: pending.identity.serverId, + daemonPublicKey: pending.identity.daemonPublicKey, + credentialVerifier: verifier, + scopes: pending.relationship.scopes, + }); + const settled = request.then( + () => undefined, + () => undefined, + ); + this.inFlightEnrollments.add(settled); + try { + const enrollment = await request; + if (enrollmentGeneration !== this.enrollmentGeneration) return; + if ( + enrollment.daemonId !== pending.relationship.daemonId || + !enrollment.scopes.includes(HUB_EXECUTION_SCOPE) + ) { + throw new Error("Hub enrollment response did not match the pending relationship"); + } + const active: ActiveRecord = { + version: 1, + state: "active", + relationship: pending.relationship, + credential: pending.credential, + transport: { kind: "direct_websocket", webSocketUrl: enrollment.webSocketUrl }, + }; + this.persist(active); + this.record = active; + this.retry = null; + this.retryAttempt = 0; + this.openSocket(active, false); + } catch (error) { + if (enrollmentGeneration !== this.enrollmentGeneration) return; + if (error instanceof HubEnrollmentRejectedError) { + this.remove(); + throw error; + } + this.lastError = error instanceof Error ? error.message : String(error); + this.scheduleEnrollment(pending, enrollmentGeneration); + } finally { + this.inFlightEnrollments.delete(settled); + } + } + + private openSocket(record: ActiveRecord, reconnecting: boolean): void { + const generation = ++this.generation; + this.state = reconnecting ? "reconnecting" : "connecting"; + const events: HubSocketEvents = { + connected: (socket) => this.socketConnected(generation, record, socket), + rejected: (statusCode) => this.socketRejected(generation, statusCode), + closed: (code) => this.socketClosed(generation, record, code), + failed: (error) => this.socketFailed(generation, record, error), + }; + this.socket = this.options.remote.openSocket( + { + daemonId: record.relationship.daemonId, + webSocketUrl: record.transport.webSocketUrl, + credential: record.credential.secret, + }, + events, + ); + } + + private socketConnected(generation: number, record: ActiveRecord, socket: WebSocketLike): void { + if (generation !== this.generation) { + socket.close(); + return; + } + this.retryAttempt = 0; + this.state = "connected"; + this.connectedAt = this.clock.now().toISOString(); + this.lastError = null; + void this.options.attachSocket(socket, { + daemonId: record.relationship.daemonId, + scopes: record.relationship.scopes, + agents: this.executionAgentsFor(record.relationship.daemonId), + }); + } + + private executionAgentsFor(daemonId: string): HubExecutionAgents { + if (this.executionAgents?.daemonId === daemonId) return this.executionAgents.value; + const value = this.options.createExecutionAgents(daemonId); + this.executionAgents = { daemonId, value }; + return value; + } + + private retireExecutionAgents(): Promise { + const executionAgents = this.executionAgents; + this.executionAgents = null; + return executionAgents?.value.invalidateAuthority() ?? Promise.resolve(); + } + + private socketRejected(generation: number, statusCode: 401 | 403): void { + if (generation !== this.generation) return; + this.revoke(`Hub rejected socket authentication (${statusCode})`); + } + + private socketClosed(generation: number, record: ActiveRecord, code: number): void { + if (generation !== this.generation) return; + if (code === 4403) { + this.revoke("Hub revoked this relationship"); + return; + } + if (this.record?.state === "active") this.scheduleSocket(record); + } + + private socketFailed(generation: number, record: ActiveRecord, error: Error): void { + if (generation !== this.generation) return; + this.lastError = error.message; + if (this.record?.state === "active") this.scheduleSocket(record); + } + + private scheduleSocket(record: ActiveRecord): void { + this.state = "reconnecting"; + this.schedule(() => this.openSocket(record, true)); + } + + private scheduleEnrollment(record: PendingRecord, enrollmentGeneration: number): void { + if (enrollmentGeneration !== this.enrollmentGeneration) return; + this.state = "reconnecting"; + this.retry?.cancel(); + const delay = this.retryPolicy.delay(this.retryAttempt++); + this.retry = this.clock.schedule(delay, () => { + if (enrollmentGeneration !== this.enrollmentGeneration) return; + void this.tryEnrollment(record, enrollmentGeneration).catch((error: unknown) => { + if (error instanceof HubEnrollmentRejectedError) return; + this.options.logger.error({ err: error }, "Scheduled Hub enrollment retry failed"); + }); + }); + } + + private async tryRevocation(record: DisconnectingRecord): Promise { + const generation = this.generation; + try { + await this.options.remote.revoke({ + daemonId: record.relationship.daemonId, + hubOrigin: record.relationship.hubOrigin, + credential: record.credential.secret, + }); + if (generation !== this.generation) return; + this.remove(); + } catch (error) { + if (generation !== this.generation) return; + this.lastError = error instanceof Error ? error.message : String(error); + this.state = "disconnecting"; + this.schedule(() => void this.tryRevocation(record)); + } + } + + private schedule(task: () => void): void { + this.retry?.cancel(); + const generation = this.generation; + const delay = this.retryPolicy.delay(this.retryAttempt++); + this.retry = this.clock.schedule(delay, () => { + if (generation === this.generation) task(); + }); + } + + private revoke(reason: string): void { + void this.retireExecutionAgents(); + this.cancelLifecycle(); + if (!this.record) return; + const revoked: RevokedRecord = { + version: 1, + state: "revoked", + relationship: { + daemonId: this.record.relationship.daemonId, + hubOrigin: this.record.relationship.hubOrigin, + createdAt: this.record.relationship.createdAt, + scopes: this.record.relationship.scopes, + }, + transport: "transport" in this.record ? this.record.transport : undefined, + reason, + }; + this.persist(revoked); + this.record = revoked; + this.state = "revoked"; + this.lastError = reason; + } + + private cancelLifecycle(): void { + ++this.generation; + ++this.enrollmentGeneration; + this.retry?.cancel(); + this.retry = null; + } + + private beginEnrollmentAttempt(): number { + this.retry?.cancel(); + this.retry = null; + this.retryAttempt = 0; + return ++this.enrollmentGeneration; + } + + private persist(record: HubRelationshipRecord): void { + writePrivateFileAtomicSync(this.filePath, `${JSON.stringify(record, null, 2)}\n`); + } + + private remove(): void { + void this.retireExecutionAgents(); + this.cancelLifecycle(); + rmSync(this.filePath, { force: true }); + this.record = null; + this.state = "not_connected"; + this.connectedAt = null; + this.lastError = null; + } + + private load(): HubRelationshipRecord | null { + if (!existsSync(this.filePath)) return null; + let record: HubRelationshipRecord; + try { + record = RecordSchema.parse(JSON.parse(readFileSync(this.filePath, "utf8"))); + } catch (error) { + const quarantinePath = path.join( + path.dirname(this.filePath), + `hub-relationship.invalid-${this.clock.now().getTime()}-${randomUUID()}.json`, + ); + renameSync(this.filePath, quarantinePath); + ensurePrivateFile(quarantinePath); + this.options.logger.error( + { err: error, quarantinePath }, + "Quarantined invalid Hub relationship authority", + ); + return null; + } + ensurePrivateFile(this.filePath); + return record; + } +} diff --git a/packages/server/src/server/hub/relationship-remote.test.ts b/packages/server/src/server/hub/relationship-remote.test.ts new file mode 100644 index 000000000..e4950adab --- /dev/null +++ b/packages/server/src/server/hub/relationship-remote.test.ts @@ -0,0 +1,551 @@ +import { createServer } from "node:http"; +import { mkdtemp, rm } from "node:fs/promises"; +import type { AddressInfo, Socket } from "node:net"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import pino from "pino"; +import { afterEach, expect, test } from "vitest"; +import { WebSocket } from "ws"; +import type { HubExecutionAgents } from "./daemon-executions.js"; +import { + HubRelationshipController, + type HubRelationshipClock, + type HubRelationshipRetryPolicy, + type ScheduledRelationshipTask, +} from "./relationship-controller.js"; +import { + DirectHubRelationshipRemote, + HubEnrollmentRejectedError, + type HubSocketConnection, + type HubSocketEvents, +} from "./relationship-remote.js"; + +const openServers: ReturnType[] = []; +const openUpgradeHubs: UpgradeRejectingHub[] = []; +const openPaseoHomes: string[] = []; + +afterEach(async () => { + for (const hub of openUpgradeHubs.splice(0)) hub.destroyConnections(); + await Promise.all(openServers.splice(0).map((server) => closeServer(server))); + await Promise.all(openPaseoHomes.splice(0).map((home) => rm(home, { recursive: true }))); +}); + +test.each([401, 403, 404])( + "revocation status %s clears already-invalid local authority", + async (status) => { + const hubOrigin = await startHubReturning(status); + const remote = new DirectHubRelationshipRemote(); + + await expect( + remote.revoke({ daemonId: "daemon-1", hubOrigin, credential: "invalid" }), + ).resolves.toBeUndefined(); + }, +); + +test("transient revocation failures remain retryable", async () => { + const hubOrigin = await startHubReturning(503); + const remote = new DirectHubRelationshipRemote(); + + await expect( + remote.revoke({ daemonId: "daemon-1", hubOrigin, credential: "credential" }), + ).rejects.toThrow("Hub revocation failed (503)"); +}); + +test.each([408, 429])("transient enrollment status %s remains retryable", async (status) => { + const hubOrigin = await startHubReturning(status); + const remote = new DirectHubRelationshipRemote(); + + const error = await remote + .enroll({ + daemonId: "daemon-1", + idempotencyKey: "ceremony-1", + hubOrigin, + token: "token", + serverId: "server-1", + daemonPublicKey: "public-key", + credentialVerifier: "verifier", + scopes: ["hub.execution.*"], + }) + .catch((caught: unknown) => caught); + + expect(error).toBeInstanceOf(Error); + expect(error).not.toBeInstanceOf(HubEnrollmentRejectedError); + expect((error as Error).message).toBe(`Hub enrollment failed (${status})`); +}); + +test("enrollment rejects a transport URL that cannot open a WebSocket", async () => { + const hubOrigin = await startEnrollmentHub("ftp://hub.test/daemon"); + const remote = new DirectHubRelationshipRemote(); + + await expect( + remote.enroll({ + daemonId: "daemon-1", + idempotencyKey: "ceremony-1", + hubOrigin, + token: "token", + serverId: "server-1", + daemonPublicKey: "public-key", + credentialVerifier: "verifier", + scopes: ["hub.execution.*"], + }), + ).rejects.toThrow("Hub WebSocket URL must use ws or wss"); +}); + +test("enrollment rejects a WebSocket URL with a fragment", async () => { + const hubOrigin = await startEnrollmentHub("ws://hub.test/daemon#fragment"); + const remote = new DirectHubRelationshipRemote(); + + await expect( + remote.enroll({ + daemonId: "daemon-1", + idempotencyKey: "ceremony-1", + hubOrigin, + token: "token", + serverId: "server-1", + daemonPublicKey: "public-key", + credentialVerifier: "verifier", + scopes: ["hub.execution.*"], + }), + ).rejects.toThrow("Hub WebSocket URL cannot include a fragment"); +}); + +test("enrollment rejects a WebSocket outside the enrolled Hub authority", async () => { + const hubOrigin = await startEnrollmentHub("ws://other-hub.test/daemon"); + const remote = new DirectHubRelationshipRemote(); + + await expect( + remote.enroll({ + daemonId: "daemon-1", + idempotencyKey: "ceremony-1", + hubOrigin, + token: "token", + serverId: "server-1", + daemonPublicKey: "public-key", + credentialVerifier: "verifier", + scopes: ["hub.execution.*"], + }), + ).rejects.toThrow("Hub WebSocket URL must match the Hub origin"); +}); + +test.each(["enrollment", "revocation"])("%s HTTP calls are bounded", async (operation) => { + const hubOrigin = await startStalledHub(); + const remote = new DirectHubRelationshipRemote({ requestTimeoutMs: 25 }); + + const request = + operation === "enrollment" + ? remote.enroll({ + daemonId: "daemon-1", + idempotencyKey: "ceremony-1", + hubOrigin, + token: "token", + serverId: "server-1", + daemonPublicKey: "public-key", + credentialVerifier: "verifier", + scopes: ["hub.execution.*"], + }) + : remote.revoke({ + daemonId: "daemon-1", + hubOrigin, + credential: "credential", + }); + + await expect(request).rejects.toThrow("Hub request timed out"); +}); + +test.each([401, 403] as const)( + "socket authentication status %s is terminal and releases the failed upgrade", + async (status) => { + const hub = await UpgradeRejectingHub.start([status]); + const outcomes = new SocketOutcomes(); + const remote = new DirectHubRelationshipRemote(); + + const socket = remote.openSocket( + { + daemonId: "daemon-1", + webSocketUrl: hub.webSocketUrl, + credential: "invalid", + }, + outcomes.events, + ) as HubSocketConnection & WebSocket; + + expect(await outcomes.next()).toEqual({ type: "rejected", status }); + await hub.expectAttemptReleased(1); + expect(await outcomes.afterCleanup()).toEqual([{ type: "rejected", status }]); + expect(socket.readyState).toBe(WebSocket.CLOSED); + expect(hub.openConnections()).toBe(0); + }, +); + +test("a transient socket response releases the failed upgrade and reports one connection loss", async () => { + const hub = await UpgradeRejectingHub.start([503]); + const outcomes = new SocketOutcomes(); + const remote = new DirectHubRelationshipRemote(); + + const socket = remote.openSocket( + { + daemonId: "daemon-1", + webSocketUrl: hub.webSocketUrl, + credential: "credential", + }, + outcomes.events, + ) as HubSocketConnection & WebSocket; + + expect(await outcomes.next()).toEqual({ type: "closed", code: 1006 }); + await hub.expectAttemptReleased(1); + expect(await outcomes.afterCleanup()).toEqual([{ type: "closed", code: 1006 }]); + expect(socket.readyState).toBe(WebSocket.CLOSED); + expect(hub.openConnections()).toBe(0); +}); + +test("a stalled socket opening handshake times out and releases the connection", async () => { + const hub = await UpgradeRejectingHub.start(["stall"]); + const outcomes = new SocketOutcomes(); + const remote = new DirectHubRelationshipRemote({ requestTimeoutMs: 25 }); + + const socket = remote.openSocket( + { + daemonId: "daemon-1", + webSocketUrl: hub.webSocketUrl, + credential: "credential", + }, + outcomes.events, + ) as HubSocketConnection & WebSocket; + + expect(await outcomes.next()).toEqual({ + type: "failed", + message: "Opening handshake has timed out", + }); + expect(await outcomes.afterCleanup()).toEqual([ + { type: "failed", message: "Opening handshake has timed out" }, + ]); + expect(socket.readyState).toBe(WebSocket.CLOSED); +}); + +test.each([401, 403] as const)( + "controller treats socket authentication status %s as terminal without redialing", + async (status) => { + const hub = await UpgradeRejectingHub.start([status]); + const clock = new ManualRelationshipClock(); + const controller = await connectController(hub, clock); + + await hub.expectAttemptReleased(1); + + expect(controller.status()).toMatchObject({ + state: "revoked", + lastError: `Hub rejected socket authentication (${status})`, + }); + expect(hub.attemptCount()).toBe(1); + expect(clock.pendingTasks()).toBe(0); + expect(hub.openConnections()).toBe(0); + }, +); + +test("controller redials after a transient socket response and releases both failed upgrades", async () => { + const hub = await UpgradeRejectingHub.start([503, 401]); + const clock = new ManualRelationshipClock(); + const controller = await connectController(hub, clock); + + await hub.expectAttemptReleased(1); + expect(controller.status().state).toBe("reconnecting"); + expect(clock.pendingTasks()).toBe(1); + + clock.runNext(); + await hub.expectAttemptReleased(2); + + expect(hub.attemptCount()).toBe(2); + expect(controller.status().state).toBe("revoked"); + expect(clock.pendingTasks()).toBe(0); + expect(hub.openConnections()).toBe(0); +}); + +test("controller redials once after a failed upgrade without also handling its close", async () => { + const hub = await UpgradeRejectingHub.start([0, 401]); + const clock = new ManualRelationshipClock(); + const controller = await connectController(hub, clock); + + await hub.expectAttemptReleased(1); + + expect(controller.status().state).toBe("reconnecting"); + expect(clock.scheduledAttempts()).toEqual([0]); + expect(clock.pendingTasks()).toBe(1); + + clock.runNext(); + await hub.expectAttemptReleased(2); + + expect(hub.attemptCount()).toBe(2); + expect(controller.status().state).toBe("revoked"); + expect(clock.pendingTasks()).toBe(0); +}); + +async function startHubReturning(status: number): Promise { + const server = createServer((_request, response) => { + response.writeHead(status).end(); + }); + openServers.push(server); + await new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(0, "127.0.0.1", resolve); + }); + const address = server.address() as AddressInfo; + return `http://127.0.0.1:${address.port}`; +} + +async function startEnrollmentHub(webSocketUrl: string): Promise { + const server = createServer(async (request, response) => { + let body = ""; + for await (const chunk of request) body += chunk; + const enrollment = JSON.parse(body) as { daemonId: string }; + response.writeHead(200, { "content-type": "application/json" }).end( + JSON.stringify({ + daemonId: enrollment.daemonId, + scopes: ["hub.execution.*"], + webSocketUrl, + }), + ); + }); + openServers.push(server); + await new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(0, "127.0.0.1", resolve); + }); + const address = server.address() as AddressInfo; + return `http://127.0.0.1:${address.port}`; +} + +async function startStalledHub(): Promise { + const server = createServer(() => undefined); + openServers.push(server); + await new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(0, "127.0.0.1", resolve); + }); + const address = server.address() as AddressInfo; + return `http://127.0.0.1:${address.port}`; +} + +function closeServer(server: ReturnType): Promise { + return new Promise((resolve, reject) => { + server.close((error) => { + if (error) { + reject(error); + return; + } + resolve(); + }); + }); +} + +type SocketOutcome = + | { type: "connected" } + | { type: "rejected"; status: 401 | 403 } + | { type: "closed"; code: number } + | { type: "failed"; message: string }; + +interface Deferred { + promise: Promise; + resolve(value: T): void; +} + +function deferred(): Deferred { + let resolve!: (value: T) => void; + const promise = new Promise((accept) => { + resolve = accept; + }); + return { promise, resolve }; +} + +class SocketOutcomes { + private readonly outcomes: SocketOutcome[] = []; + private nextOutcome = deferred(); + + readonly events: HubSocketEvents = { + connected: () => this.record({ type: "connected" }), + rejected: (status) => this.record({ type: "rejected", status }), + closed: (code) => this.record({ type: "closed", code }), + failed: (error) => this.record({ type: "failed", message: error.message }), + }; + + next(): Promise { + return withDeadline(this.nextOutcome.promise, "Hub socket produced no outcome"); + } + + async afterCleanup(): Promise { + await new Promise((resolve) => setImmediate(resolve)); + return this.outcomes.slice(); + } + + private record(outcome: SocketOutcome): void { + this.outcomes.push(outcome); + this.nextOutcome.resolve(outcome); + this.nextOutcome = deferred(); + } +} + +class UpgradeRejectingHub { + private readonly server = createServer(async (request, response) => { + let body = ""; + for await (const chunk of request) body += chunk; + const enrollment = JSON.parse(body) as { daemonId: string }; + response.writeHead(200, { "content-type": "application/json" }).end( + JSON.stringify({ + daemonId: enrollment.daemonId, + scopes: ["hub.execution.*"], + webSocketUrl: this.webSocketUrl, + }), + ); + }); + private readonly sockets = new Set(); + private readonly releasedAttempts = new Map>(); + private attemptObserved = deferred(); + private attempts = 0; + + private constructor(private readonly statuses: Array) { + this.server.on("upgrade", (_request, socket) => { + const attempt = ++this.attempts; + this.attemptObserved.resolve(); + this.attemptObserved = deferred(); + const released = deferred(); + this.releasedAttempts.set(attempt, released); + this.sockets.add(socket); + socket.once("close", () => { + this.sockets.delete(socket); + released.resolve(); + }); + const status = this.statuses[attempt - 1] ?? this.statuses.at(-1) ?? 503; + if (status === "stall") { + socket.resume(); + return; + } + if (status === 0) { + socket.destroy(); + return; + } + socket.end( + `HTTP/1.1 ${status} Rejected\r\nContent-Length: 4\r\nConnection: close\r\n\r\ndeny`, + ); + }); + } + + static async start(statuses: Array): Promise { + const hub = new UpgradeRejectingHub(statuses); + openUpgradeHubs.push(hub); + openServers.push(hub.server); + await new Promise((resolve, reject) => { + hub.server.once("error", reject); + hub.server.listen(0, "127.0.0.1", resolve); + }); + return hub; + } + + get webSocketUrl(): string { + const address = this.server.address() as AddressInfo; + return `ws://127.0.0.1:${address.port}/daemon`; + } + + get origin(): string { + const address = this.server.address() as AddressInfo; + return `http://127.0.0.1:${address.port}`; + } + + attemptCount(): number { + return this.attempts; + } + + openConnections(): number { + return this.sockets.size; + } + + destroyConnections(): void { + for (const socket of this.sockets) socket.destroy(); + } + + async expectAttemptReleased(attempt: number): Promise { + while (this.attempts < attempt) { + await withDeadline( + this.attemptObserved.promise, + `Hub did not receive socket attempt ${attempt}`, + ); + } + const released = this.releasedAttempts.get(attempt); + if (!released) throw new Error(`Hub did not receive socket attempt ${attempt}`); + await withDeadline(released.promise, `Hub socket attempt ${attempt} remained open`); + await new Promise((resolve) => setImmediate(resolve)); + } +} + +class ManualRelationshipClock implements HubRelationshipClock, HubRelationshipRetryPolicy { + private readonly tasks: Array<{ cancelled: boolean; task: () => void }> = []; + private readonly attempts: number[] = []; + + now(): Date { + return new Date("2026-07-13T00:00:00.000Z"); + } + + delay(attempt: number): number { + this.attempts.push(attempt); + return 0; + } + + schedule(_delayMs: number, task: () => void): ScheduledRelationshipTask { + const scheduled = { cancelled: false, task }; + this.tasks.push(scheduled); + return { cancel: () => (scheduled.cancelled = true) }; + } + + pendingTasks(): number { + return this.tasks.filter((task) => !task.cancelled).length; + } + + scheduledAttempts(): number[] { + return this.attempts.slice(); + } + + runNext(): void { + const scheduled = this.tasks.shift(); + if (!scheduled || scheduled.cancelled) throw new Error("No Hub reconnect is scheduled"); + scheduled.task(); + } +} + +const unusedExecutionAgents: HubExecutionAgents = { + create: async () => { + throw new Error("Unexpected Hub agent create"); + }, + subscribe: () => () => undefined, + invalidateAuthority: async () => undefined, +}; + +async function connectController( + hub: UpgradeRejectingHub, + clock: ManualRelationshipClock, +): Promise { + const paseoHome = await mkdtemp(path.join(tmpdir(), "paseo-hub-socket-")); + openPaseoHomes.push(paseoHome); + const controller = new HubRelationshipController({ + paseoHome, + serverId: "server-1", + daemonPublicKey: "daemon-public-key", + logger: pino({ level: "silent" }), + remote: new DirectHubRelationshipRemote(), + clock, + retryPolicy: clock, + attachSocket: async () => undefined, + createExecutionAgents: () => unusedExecutionAgents, + }); + await controller.connect({ hubUrl: hub.origin, token: "enrollment-token" }); + return controller; +} + +async function withDeadline(promise: Promise, message: string): Promise { + let timer: ReturnType | undefined; + try { + return await Promise.race([ + promise, + new Promise((_resolve, reject) => { + timer = setTimeout(() => reject(new Error(message)), 2_000); + }), + ]); + } finally { + if (timer) clearTimeout(timer); + } +} diff --git a/packages/server/src/server/hub/relationship-remote.ts b/packages/server/src/server/hub/relationship-remote.ts new file mode 100644 index 000000000..82172a411 --- /dev/null +++ b/packages/server/src/server/hub/relationship-remote.ts @@ -0,0 +1,187 @@ +import { WebSocket } from "ws"; +import { z } from "zod"; +import type { WebSocketLike } from "../websocket-server.js"; + +export interface HubEnrollment { + daemonId: string; + idempotencyKey: string; + hubOrigin: string; + token: string; + serverId: string; + daemonPublicKey: string; + credentialVerifier: string; + scopes: string[]; +} + +export interface HubEnrollmentResult { + daemonId: string; + scopes: string[]; + webSocketUrl: string; +} + +export interface HubRevocation { + daemonId: string; + hubOrigin: string; + credential: string; +} + +export interface HubSocketCredentials { + daemonId: string; + webSocketUrl: string; + credential: string; +} + +export interface HubSocketEvents { + connected(socket: WebSocketLike): void; + rejected(statusCode: 401 | 403): void; + closed(code: number): void; + failed(error: Error): void; +} + +export interface HubSocketConnection { + close(): void; +} + +export interface HubRelationshipRemote { + enroll(input: HubEnrollment): Promise; + revoke(input: HubRevocation): Promise; + openSocket(input: HubSocketCredentials, events: HubSocketEvents): HubSocketConnection; +} + +export class HubEnrollmentRejectedError extends Error { + constructor(readonly statusCode: number) { + super(`Hub enrollment failed (${statusCode})`); + this.name = "HubEnrollmentRejectedError"; + } +} + +const EnrollmentResultSchema = z.object({ + daemonId: z.string(), + scopes: z.array(z.string()), + webSocketUrl: z + .string() + .url() + .refine((value) => ["ws:", "wss:"].includes(new URL(value).protocol), { + message: "Hub WebSocket URL must use ws or wss", + }) + .refine((value) => new URL(value).hash === "", { + message: "Hub WebSocket URL cannot include a fragment", + }), +}); + +function ensureWebSocketMatchesHubOrigin(hubOrigin: string, webSocketUrl: string): void { + const hub = new URL(hubOrigin); + const socket = new URL(webSocketUrl); + const expectedProtocol = hub.protocol === "https:" ? "wss:" : "ws:"; + if (socket.protocol !== expectedProtocol || socket.host !== hub.host) { + throw new Error("Hub WebSocket URL must match the Hub origin"); + } +} + +export class DirectHubRelationshipRemote implements HubRelationshipRemote { + private readonly requestTimeoutMs: number; + + constructor(options: { requestTimeoutMs?: number } = {}) { + this.requestTimeoutMs = options.requestTimeoutMs ?? 15_000; + } + + async enroll(input: HubEnrollment): Promise { + return this.withRequestTimeout(async (signal) => { + const response = await fetch(`${input.hubOrigin}/api/daemons/enroll`, { + method: "POST", + headers: { + "content-type": "application/json", + authorization: `Bearer ${input.token}`, + }, + body: JSON.stringify({ + daemonId: input.daemonId, + idempotencyKey: input.idempotencyKey, + serverId: input.serverId, + daemonPublicKey: input.daemonPublicKey, + credentialVerifier: input.credentialVerifier, + scopes: input.scopes, + }), + signal, + }); + if (!response.ok) { + if (response.status === 401 || response.status === 403) { + throw new HubEnrollmentRejectedError(response.status); + } + throw new Error(`Hub enrollment failed (${response.status})`); + } + const enrollment = EnrollmentResultSchema.parse(await response.json()); + ensureWebSocketMatchesHubOrigin(input.hubOrigin, enrollment.webSocketUrl); + return enrollment; + }); + } + + async revoke(input: HubRevocation): Promise { + await this.withRequestTimeout(async (signal) => { + const response = await fetch( + `${input.hubOrigin}/api/daemons/${encodeURIComponent(input.daemonId)}`, + { + method: "DELETE", + headers: { authorization: `Bearer ${input.credential}` }, + signal, + }, + ); + if (!response.ok && ![401, 403, 404].includes(response.status)) { + throw new Error(`Hub revocation failed (${response.status})`); + } + }); + } + + openSocket(input: HubSocketCredentials, events: HubSocketEvents): HubSocketConnection { + const socket = new WebSocket(input.webSocketUrl, { + handshakeTimeout: this.requestTimeoutMs, + headers: { + authorization: `Bearer ${input.credential}`, + "x-paseo-daemon-id": input.daemonId, + }, + }); + let settled = false; + socket.once("open", () => { + if (!settled) events.connected(socket as WebSocketLike); + }); + socket.once("unexpected-response", (_request, response) => { + if (settled) { + response.destroy(); + return; + } + settled = true; + response.destroy(); + socket.terminate(); + if (response.statusCode === 401 || response.statusCode === 403) { + events.rejected(response.statusCode); + return; + } + events.closed(1006); + }); + socket.once("close", (code) => { + if (settled) return; + settled = true; + events.closed(code); + }); + socket.once("error", (error) => { + if (settled) return; + settled = true; + socket.terminate(); + events.failed(error); + }); + return socket; + } + + private async withRequestTimeout(operation: (signal: AbortSignal) => Promise): Promise { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), this.requestTimeoutMs); + timeout.unref?.(); + try { + return await operation(controller.signal); + } catch (error) { + if (controller.signal.aborted) throw new Error("Hub request timed out", { cause: error }); + throw error; + } finally { + clearTimeout(timeout); + } + } +} diff --git a/packages/server/src/server/hub/relationship-retry.test.ts b/packages/server/src/server/hub/relationship-retry.test.ts new file mode 100644 index 000000000..8331df1d5 --- /dev/null +++ b/packages/server/src/server/hub/relationship-retry.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, test } from "vitest"; +import { BoundedExponentialHubRetryPolicy } from "./relationship-retry.js"; + +describe("Hub relationship retry policy", () => { + test("keeps jitter within the lower and upper bounds", () => { + const lower = new BoundedExponentialHubRetryPolicy(() => 0); + const upper = new BoundedExponentialHubRetryPolicy(() => 1); + + expect(lower.delay(0)).toBe(375); + expect(upper.delay(0)).toBe(625); + }); + + test("grows exponentially and caps the base delay", () => { + const retry = new BoundedExponentialHubRetryPolicy(() => 0.5); + + expect([0, 1, 2, 3, 4, 5, 6, 20].map((attempt) => retry.delay(attempt))).toEqual([ + 500, 1_000, 2_000, 4_000, 8_000, 16_000, 30_000, 30_000, + ]); + }); + + test("never schedules a zero-delay tight loop", () => { + const retry = new BoundedExponentialHubRetryPolicy(() => 0); + + expect([0, 1, 2].map((attempt) => retry.delay(attempt))).toEqual([375, 750, 1_500]); + }); +}); diff --git a/packages/server/src/server/hub/relationship-retry.ts b/packages/server/src/server/hub/relationship-retry.ts new file mode 100644 index 000000000..005aff89f --- /dev/null +++ b/packages/server/src/server/hub/relationship-retry.ts @@ -0,0 +1,16 @@ +import type { HubRelationshipRetryPolicy } from "./relationship-controller.js"; + +const INITIAL_DELAY_MS = 500; +const MAX_BASE_DELAY_MS = 30_000; +const MIN_JITTER_FACTOR = 0.75; +const JITTER_RANGE = 0.5; + +export class BoundedExponentialHubRetryPolicy implements HubRelationshipRetryPolicy { + constructor(private readonly random: () => number = Math.random) {} + + delay(attempt: number): number { + const base = Math.min(MAX_BASE_DELAY_MS, INITIAL_DELAY_MS * 2 ** attempt); + const jitter = MIN_JITTER_FACTOR + this.random() * JITTER_RANGE; + return Math.max(1, Math.round(base * jitter)); + } +} diff --git a/packages/server/src/server/hub/test-utils/relationship-harness.ts b/packages/server/src/server/hub/test-utils/relationship-harness.ts new file mode 100644 index 000000000..6603bff87 --- /dev/null +++ b/packages/server/src/server/hub/test-utils/relationship-harness.ts @@ -0,0 +1,1409 @@ +import { execFile } from "node:child_process"; +import { execFileSync } from "node:child_process"; +import { EventEmitter } from "node:events"; +import { existsSync, readFileSync, realpathSync, statSync, watch } from "node:fs"; +import { mkdir, mkdtemp, readdir, rm, writeFile } from "node:fs/promises"; +import { networkInterfaces, platform, tmpdir } from "node:os"; +import path from "node:path"; +import { promisify } from "node:util"; +import { Writable } from "node:stream"; +import pino from "pino"; +import { WebSocket } from "ws"; +import type { + AgentSnapshotPayload, + HubExecutionAgentCreateResponse, + HubExecutionAgentStream, + HubExecutionAgentUpdate, + RpcErrorMessage, + CreateAgentWorktreeTarget, + SessionOutboundMessage, +} from "../../messages.js"; +import { createPaseoDaemon, type PaseoDaemon, type PaseoDaemonConfig } from "../../bootstrap.js"; +import type { WebSocketLike } from "../../websocket-server.js"; +import type { + AgentClient, + AgentCreateSessionOptions, + AgentLaunchContext, + AgentPromptInput, + AgentPersistenceHandle, + AgentSession, + AgentSessionConfig, + FetchCatalogOptions, + ProviderCatalog, +} from "../../agent/agent-sdk-types.js"; +import { createTestAgentClients } from "../../test-utils/fake-agent-client.js"; +import { DaemonClient } from "../../test-utils/daemon-client.js"; +import { AgentStorage } from "../../agent/agent-storage.js"; +import { AgentManager } from "../../agent/agent-manager.js"; +import { DaemonExecutions } from "../daemon-executions.js"; +import { + createAgentCommand, + type CreateAgentCommandDependencies, +} from "../../agent/create-agent/create.js"; +import type { + HubRelationshipClock, + HubRelationshipRetryPolicy, + ScheduledRelationshipTask, +} from "../relationship-controller.js"; +import type { + HubEnrollment, + HubEnrollmentResult, + HubRelationshipRemote, + HubRevocation, + HubSocketConnection, + HubSocketCredentials, + HubSocketEvents, +} from "../relationship-remote.js"; +import { HubEnrollmentRejectedError } from "../relationship-remote.js"; + +const execFileAsync = promisify(execFile); +const HUB_ORIGIN = "https://hub.test"; +const SOCKET_URL = "wss://hub.test/daemon"; +const REPOSITORY_ROOT = path.resolve(import.meta.dirname, "../../../../../.."); + +export interface ArchiveWatcher { + close(): void; + onError(listener: (error: Error) => void): void; +} + +export interface ArchiveWatchFiles { + watchDirectory(path: string, onChange: () => void): ArchiveWatcher; +} + +const nodeArchiveWatchFiles: ArchiveWatchFiles = { + watchDirectory(directory, onChange) { + const watcher = watch(directory, onChange); + return { + close: () => watcher.close(), + onError: (listener) => watcher.on("error", listener), + }; + }, +}; + +export class SetupFailingArchiveWatchFiles implements ArchiveWatchFiles { + private attempts = 0; + private readonly openDirectories = new Set(); + + constructor(private readonly failOnAttempt: number) {} + + watchDirectory(directory: string): ArchiveWatcher { + this.attempts++; + if (this.attempts === this.failOnAttempt) { + throw new Error(`Cannot watch ${directory}`); + } + this.openDirectories.add(directory); + let open = true; + return { + close: () => { + if (!open) return; + open = false; + this.openDirectories.delete(directory); + }, + onError: () => {}, + }; + } + + activeDirectories(): string[] { + return [...this.openDirectories]; + } +} + +interface PersistedRelationship { + version: number; + state: string; + reason?: string; + relationship: { + daemonId: string; + idempotencyKey?: string; + hubOrigin: string; + scopes: string[]; + }; + credential?: { secret: string }; + enrollment?: { token: string }; + identity?: { serverId: string; daemonPublicKey: string }; +} + +export interface RelationshipInvocationSnapshot { + mode: number; + record: PersistedRelationship; +} + +interface Deferred { + promise: Promise; + resolve(value: T): void; + reject(error: Error): void; +} + +function deferred(): Deferred { + let resolve!: (value: T) => void; + let reject!: (error: Error) => void; + const promise = new Promise((accept, decline) => { + resolve = accept; + reject = decline; + }); + return { promise, resolve, reject }; +} + +class TestRelationshipClock implements HubRelationshipClock, HubRelationshipRetryPolicy { + private current = new Date("2026-07-13T00:00:00.000Z"); + private tasks: Array<{ cancelled: boolean; task: () => void }> = []; + + now(): Date { + return this.current; + } + + delay(attempt: number): number { + return Math.min(8_000, 1_000 * 2 ** attempt); + } + + schedule(_delayMs: number, task: () => void): ScheduledRelationshipTask { + const scheduled = { cancelled: false, task }; + this.tasks.push(scheduled); + return { cancel: () => (scheduled.cancelled = true) }; + } + + async runNext(): Promise { + while (true) { + const scheduled = this.tasks.shift(); + if (!scheduled) throw new Error("No relationship retry is scheduled"); + if (!scheduled.cancelled) { + scheduled.task(); + await Promise.resolve(); + return; + } + } + } + + pendingTasks(): number { + return this.tasks.filter((task) => !task.cancelled).length; + } +} + +class MemoryHubSocket extends EventEmitter implements WebSocketLike, HubSocketConnection { + readyState = 1; + bufferedAmount = 0; + sent: SessionOutboundMessage[] = []; + closed = false; + closeCode: number | null = null; + private messageObserved = deferred(); + + send(data: string | Uint8Array | ArrayBuffer): void { + if (typeof data !== "string") return; + const frame = JSON.parse(data) as { type: "session"; message: SessionOutboundMessage }; + this.sent.push(frame.message); + this.messageObserved.resolve(); + } + + close(code = 1000): void { + this.closed = true; + this.readyState = 3; + this.closeCode = code; + this.emit("close", code); + } + + receive(message: unknown): void { + this.emit("message", JSON.stringify({ type: "session", message }), false); + } + + receiveEnvelope(message: unknown): void { + this.emit("message", JSON.stringify(message), false); + } + + receiveBinary(): void { + this.emit("message", new Uint8Array([1, 2, 3]), true); + } + + async messageFor(requestId: string): Promise { + while (true) { + const message = this.sent.find( + (candidate) => + "payload" in candidate && + "requestId" in candidate.payload && + candidate.payload.requestId === requestId, + ); + if (message) return message; + await this.messageObserved.promise; + this.messageObserved = deferred(); + } + } + + async messageMatching( + predicate: (message: SessionOutboundMessage) => message is TMessage, + ): Promise { + while (true) { + const message = this.sent.find(predicate); + if (message) return message; + await this.messageObserved.promise; + this.messageObserved = deferred(); + } + } +} + +class ControlledAgentClient implements AgentClient { + readonly provider; + readonly capabilities; + private gate: Deferred | null = null; + private creationObserved = deferred(); + creations = 0; + resumes = 0; + createdConfigs: AgentSessionConfig[] = []; + + constructor(private readonly client: AgentClient) { + this.provider = client.provider; + this.capabilities = client.capabilities; + } + + holdCreation(): void { + this.gate = deferred(); + } + + async creationAt(count: number): Promise { + while (this.creations < count) { + await this.creationObserved.promise; + this.creationObserved = deferred(); + } + } + + finishCreation(): void { + if (!this.gate) throw new Error("Agent creation is not held"); + this.gate.resolve(); + this.gate = null; + } + + async createSession( + config: AgentSessionConfig, + launchContext?: AgentLaunchContext, + options?: AgentCreateSessionOptions, + ): Promise { + this.creations++; + this.createdConfigs.push({ ...config }); + this.creationObserved.resolve(); + await this.gate?.promise; + return this.client.createSession(config, launchContext, options); + } + + resumeSession( + handle: AgentPersistenceHandle, + overrides?: Partial, + launchContext?: AgentLaunchContext, + ): Promise { + this.resumes++; + return this.client.resumeSession(handle, overrides, launchContext); + } + + fetchCatalog(options: FetchCatalogOptions): Promise { + return this.client.fetchCatalog(options); + } + + isAvailable(): Promise { + return this.client.isAvailable(); + } +} + +interface SocketAttempt { + input: HubSocketCredentials; + events: HubSocketEvents; + socket: MemoryHubSocket; +} + +class InMemoryHubRelationships implements HubRelationshipRemote { + enrollments: HubEnrollment[] = []; + revocations: HubRevocation[] = []; + sockets: SocketAttempt[] = []; + private readonly enrollmentGates: Array> = []; + private readonly heldEnrollments: Array<{ + input: HubEnrollment; + gate: Deferred; + }> = []; + private enrollmentObserved = deferred(); + private socketObserved = deferred(); + private enrollmentRejection: 401 | 403 | null = null; + private enrollmentScopes = ["hub.execution.*"]; + private revokeFailures = 0; + private readonly relationships = new Set(); + readonly enrollmentSnapshots: RelationshipInvocationSnapshot[] = []; + readonly socketSnapshots: RelationshipInvocationSnapshot[] = []; + + constructor(private readonly captureRelationship: () => RelationshipInvocationSnapshot) {} + + holdEnrollment(): void { + this.enrollmentGates.push(deferred()); + } + + rejectNextEnrollment(statusCode: 401 | 403): void { + this.enrollmentRejection = statusCode; + } + + returnEnrollmentScopes(scopes: string[]): void { + this.enrollmentScopes = scopes.slice(); + } + + async enroll(input: HubEnrollment): Promise { + this.enrollmentSnapshots.push(this.captureRelationship()); + this.enrollments.push({ ...input, scopes: input.scopes.slice() }); + this.relationships.add(input.idempotencyKey); + this.enrollmentObserved.resolve(); + if (this.enrollmentRejection) { + const statusCode = this.enrollmentRejection; + this.enrollmentRejection = null; + throw new HubEnrollmentRejectedError(statusCode); + } + const gate = this.enrollmentGates.shift(); + if (gate) { + this.heldEnrollments.push({ input, gate }); + return gate.promise; + } + return this.enrollmentResult(input); + } + + completeEnrollment(): void { + const held = this.heldEnrollments.at(-1); + if (!held) throw new Error("No enrollment is waiting"); + held.gate.resolve(this.enrollmentResult(held.input)); + this.heldEnrollments.splice(this.heldEnrollments.indexOf(held), 1); + } + + loseEnrollmentResponse(): void { + const held = this.heldEnrollments.at(-1); + if (!held) throw new Error("No enrollment is waiting"); + held.gate.reject(new Error("Enrollment response was lost")); + this.heldEnrollments.splice(this.heldEnrollments.indexOf(held), 1); + } + + rejectEnrollment(index: number, statusCode: 401 | 403): void { + const held = this.heldEnrollments[index]; + if (!held) throw new Error(`Enrollment ${index} is not waiting`); + held.gate.reject(new HubEnrollmentRejectedError(statusCode)); + this.heldEnrollments.splice(index, 1); + } + + async enrollmentAt(index: number): Promise { + while (!this.enrollments[index]) { + await this.enrollmentObserved.promise; + this.enrollmentObserved = deferred(); + } + return this.enrollments[index]; + } + + failRevocations(count: number): void { + this.revokeFailures = count; + } + + async revoke(input: HubRevocation): Promise { + this.revocations.push({ ...input }); + if (this.revokeFailures > 0) { + this.revokeFailures--; + throw new Error("Hub is offline"); + } + } + + openSocket(input: HubSocketCredentials, events: HubSocketEvents): HubSocketConnection { + this.socketSnapshots.push(this.captureRelationship()); + const attempt = { input: { ...input }, events, socket: new MemoryHubSocket() }; + this.sockets.push(attempt); + this.socketObserved.resolve(); + return attempt.socket; + } + + async socketAt(index: number): Promise { + while (!this.sockets[index]) { + await this.socketObserved.promise; + this.socketObserved = deferred(); + } + return this.sockets[index]; + } + + relationshipCount(): number { + return this.relationships.size; + } + + private enrollmentResult(input: HubEnrollment): HubEnrollmentResult { + return { + daemonId: input.daemonId, + scopes: this.enrollmentScopes.slice(), + webSocketUrl: SOCKET_URL, + }; + } +} + +interface CliProcess { + result: Promise>; +} + +type AcceptedCreate = Omit< + HubExecutionAgentCreateResponse["payload"], + "agentId" | "agent" | "success" +> & { + agentId: string; + agent: AgentSnapshotPayload; + success: true; +}; + +const providerCatalog = { + async resolveCreateConfig(input) { + return { modeId: input.requestedMode, featureValues: input.featureValues }; + }, +} satisfies CreateAgentCommandDependencies["providerSnapshotManager"]; + +export class HubRelationshipHarness { + private readonly clock = new TestRelationshipClock(); + private readonly remote = new InMemoryHubRelationships(() => this.captureRelationship()); + private daemon: PaseoDaemon | null = null; + private config!: PaseoDaemonConfig; + private root = ""; + private paseoHome = ""; + private host = ""; + private readonly logs: string[] = []; + private readonly providerPrompts: AgentPromptInput[] = []; + private readonly cliProcesses = new Set>(); + private readonly claimedCliSockets = new Set(); + private readonly promptsToFail = new Set(); + private failNextSessionClose = false; + private observedEnrollments = 0; + private observedSockets = 0; + private readonly codex = new ControlledAgentClient( + createTestAgentClients({ + closeSession: async () => { + if (!this.failNextSessionClose) return; + this.failNextSessionClose = false; + throw new Error("Requested provider session close failure"); + }, + onStartTurn: (prompt) => { + const promptText = typeof prompt === "string" ? prompt : JSON.stringify(prompt); + if (this.promptsToFail.delete(promptText)) { + throw new Error("Requested provider prompt startup failure"); + } + this.providerPrompts.push(prompt); + }, + }).codex, + ); + + private constructor(private readonly archiveWatchFiles: ArchiveWatchFiles) {} + + static async start( + archiveWatchFiles: ArchiveWatchFiles = nodeArchiveWatchFiles, + ): Promise { + const harness = new HubRelationshipHarness(archiveWatchFiles); + await harness.createHome(); + await harness.startDaemon(); + return harness; + } + + holdEnrollment(): void { + this.remote.holdEnrollment(); + } + + beginConnect(token = "ceremony-token", hubUrl = HUB_ORIGIN): CliProcess { + return { result: this.runCli(["hub", "connect", hubUrl, "--token", token]) }; + } + + async status(): Promise> { + return this.runCli(["hub", "status"]); + } + + async disconnect(force = false): Promise> { + return this.runCli(["hub", "disconnect", ...(force ? ["--force"] : [])]); + } + + beginDisconnect(force = false): CliProcess { + return { result: this.runCli(["hub", "disconnect", ...(force ? ["--force"] : [])]) }; + } + + async relationshipStateBecomes(expected: string | null): Promise { + const observed = deferred(); + const watcher = watch(this.paseoHome, () => { + if ((this.relationshipFile()?.state ?? null) === expected) observed.resolve(); + }); + if ((this.relationshipFile()?.state ?? null) === expected) observed.resolve(); + try { + await observed.promise; + } finally { + watcher.close(); + } + } + + async manageRelationshipFromExternalSocket(): Promise { + const address = Object.values(networkInterfaces()) + .flat() + .find((candidate) => candidate?.family === "IPv4" && !candidate.internal)?.address; + if (!address) + throw new Error("No non-loopback IPv4 address is available for the external test"); + const target = this.daemon?.getListenTarget(); + if (!target || target.type !== "tcp") throw new Error("Daemon did not bind TCP"); + const socket = await this.openClaimedCliSocket(`ws://${address}:${target.port}/ws`); + const messages = [ + { + type: "hub.management.daemon.connect.request", + requestId: "external-hub-connect", + hubUrl: HUB_ORIGIN, + token: "external-token", + }, + { type: "hub.management.daemon.get_status.request", requestId: "external-hub-status" }, + { + type: "hub.management.daemon.disconnect.request", + requestId: "external-hub-disconnect", + force: false, + }, + ]; + const responses: SessionOutboundMessage[] = []; + for (const message of messages) { + socket.send(JSON.stringify({ type: "session", message })); + responses.push((await this.nextSocketEnvelope(socket)).message); + } + await this.closeClaimedCliSocket(socket); + return responses; + } + + async connectFromBrowserSocket(): Promise { + const target = this.daemon?.getListenTarget(); + if (!target || target.type !== "tcp") throw new Error("Daemon did not bind TCP"); + const socket = await this.openClaimedCliSocket(`ws://127.0.0.1:${target.port}/ws`, { + origin: `http://127.0.0.1:${target.port}`, + }); + socket.send( + JSON.stringify({ + type: "session", + message: { + type: "hub.management.daemon.connect.request", + requestId: "browser-hub-connect", + hubUrl: HUB_ORIGIN, + token: "browser-token", + }, + }), + ); + const response = (await this.nextSocketEnvelope(socket)).message; + await this.closeClaimedCliSocket(socket); + return response; + } + + async enrollmentBegins(): Promise { + return this.remote.enrollmentAt(this.observedEnrollments++); + } + + completeEnrollment(): void { + this.remote.completeEnrollment(); + } + + returnEnrollmentScopes(scopes: string[]): void { + this.remote.returnEnrollmentScopes(scopes); + } + + loseEnrollmentResponse(): void { + this.remote.loseEnrollmentResponse(); + } + + rejectEnrollment(index: number, statusCode: 401 | 403): void { + this.remote.rejectEnrollment(index, statusCode); + } + + pendingRelationshipRetries(): number { + return this.clock.pendingTasks(); + } + + rejectNextEnrollment(statusCode: 401 | 403): void { + this.remote.rejectNextEnrollment(statusCode); + } + + failRevocations(count: number): void { + this.remote.failRevocations(count); + } + + failProviderPromptStart(prompt = "Create through the Hub"): void { + this.promptsToFail.add(prompt); + } + + failNextProviderSessionClose(): void { + this.failNextSessionClose = true; + } + + async socketDialed(): Promise { + await this.remote.socketAt(this.observedSockets++); + } + + connectLatestSocket(): void { + const socket = this.latestSocket(); + socket.events.connected(socket.socket); + } + + rejectLatestSocket(statusCode: 401 | 403): void { + this.latestSocket().events.rejected(statusCode); + } + + rejectRelationship(code: 4403 | 401 | 403): void { + if (code === 4403) { + this.closeLatestSocket(code); + return; + } + this.rejectLatestSocket(code); + } + + closeLatestSocket(code: number): void { + const socket = this.latestSocket(); + socket.events.closed(code); + socket.socket.close(code); + } + + connectSocket(index: number): void { + const socket = this.remote.sockets[index]; + if (!socket) throw new Error(`Socket ${index} does not exist`); + socket.events.connected(socket.socket); + } + + closeSocket(index: number, code: number): void { + const socket = this.remote.sockets[index]; + if (!socket) throw new Error(`Socket ${index} does not exist`); + socket.events.closed(code); + socket.socket.close(code); + } + + sendHubRequestOnLatest(message: unknown): SessionOutboundMessage[] { + const socket = this.latestSocket().socket; + socket.receive(message); + return socket.sent.slice(); + } + + holdAgentCreation(): void { + this.codex.holdCreation(); + } + + beginOwnedCreate( + requestId: string, + executionId = "execution-race", + options: { + worktree?: CreateAgentWorktreeTarget; + autoArchive?: boolean; + prompt?: string; + modeId?: string; + } = {}, + ): void { + const { prompt = "Create through the Hub", ...requestOptions } = options; + this.latestSocket().socket.receive({ + type: "hub.execution.agent.create.request", + requestId, + executionId, + provider: "codex", + cwd: this.root, + workspaceId: "hub-workspace", + prompt, + ...requestOptions, + }); + } + + async agentCreationAttempts(count: number): Promise { + await this.codex.creationAt(count); + } + + finishAgentCreation(): void { + this.codex.finishCreation(); + } + + async ownedCreateResult(requestId: string): Promise { + return this.latestSocket().socket.messageFor(requestId); + } + + async durableOwnedAgentIds(): Promise { + return (await this.daemon!.agentStorage.list()) + .filter((record) => record.owner?.kind === "daemon") + .map((record) => record.id); + } + + async durableOwnedAgentIdsOnDisk(): Promise { + const storage = new AgentStorage( + path.join(this.paseoHome, "agents"), + pino({ level: "silent" }), + ); + return (await storage.list()) + .filter((record) => record.owner?.kind === "daemon") + .map((record) => record.id); + } + + activeOwnedAgentIds(): string[] { + return this.daemon!.agentManager.listAgents() + .filter((agent) => agent.owner?.kind === "daemon") + .map((agent) => agent.id); + } + + agentSubscriptionCount(): number { + return this.daemon!.agentManager.subscriptionCount(); + } + + async hubExecutionIntentFiles(): Promise { + const directory = path.join(this.paseoHome, "hub-executions"); + return existsSync(directory) ? readdir(directory) : []; + } + + socketDeliveredResponse(socketIndex: number, requestId: string): boolean { + const socket = this.remote.sockets[socketIndex]; + if (!socket) throw new Error(`Socket ${socketIndex} does not exist`); + return socket.socket.sent.some( + (message) => + "payload" in message && + "requestId" in message.payload && + message.payload.requestId === requestId, + ); + } + + providerCreations(): number { + return this.codex.creations; + } + + providerResumes(): number { + return this.codex.resumes; + } + + providerPromptTexts(): string[] { + return this.providerPrompts.map((prompt) => + typeof prompt === "string" ? prompt : JSON.stringify(prompt), + ); + } + + latestCreatedCwd(): string | null { + return this.codex.createdConfigs.at(-1)?.cwd ?? null; + } + + repoRoot(): string { + return this.root; + } + + async waitForOwnedArchiveCompletion( + agentId: string, + ): Promise<{ agentArchivedAt: string; workspaceArchivedAt: string }> { + const created = await this.daemon!.agentStorage.get(agentId); + if (!created) throw new Error(`Owned agent ${agentId} does not exist`); + if (!created.workspaceId) throw new Error(`Owned agent ${agentId} has no workspace`); + return new Promise<{ agentArchivedAt: string; workspaceArchivedAt: string }>( + (resolve, reject) => { + const watchers: ArchiveWatcher[] = []; + let settled = false; + const timeout = setTimeout( + () => finish(new Error(`Timed out waiting for owned agent ${agentId} to archive`)), + 15_000, + ); + timeout.unref?.(); + const closeWatchers = () => { + clearTimeout(timeout); + for (const watcher of watchers) watcher.close(); + }; + const finish = ( + result: Error | { agentArchivedAt: string; workspaceArchivedAt: string }, + ) => { + if (settled) return; + settled = true; + closeWatchers(); + if (result instanceof Error) reject(result); + else resolve(result); + }; + const observeCompletion = async () => { + try { + const archived = await this.daemon!.agentStorage.get(agentId); + const workspaceArchivedAt = this.workspaceArchivedAt(created.workspaceId!); + if (!archived?.archivedAt || !workspaceArchivedAt || existsSync(created.cwd)) return; + finish({ agentArchivedAt: archived.archivedAt, workspaceArchivedAt }); + } catch (error) { + finish(error instanceof Error ? error : new Error(String(error))); + } + }; + try { + const projectsWatcher = this.archiveWatchFiles.watchDirectory( + path.join(this.paseoHome, "projects"), + observeCompletion, + ); + watchers.push(projectsWatcher); + projectsWatcher.onError(finish); + const worktreeWatcher = this.archiveWatchFiles.watchDirectory( + path.dirname(created.cwd), + observeCompletion, + ); + watchers.push(worktreeWatcher); + worktreeWatcher.onError(finish); + void observeCompletion(); + } catch (error) { + finish(error instanceof Error ? error : new Error(String(error))); + } + }, + ); + } + + private workspaceArchivedAt(workspaceId: string): string | null { + const records = JSON.parse( + readFileSync(path.join(this.paseoHome, "projects", "workspaces.json"), "utf8"), + ) as Array<{ workspaceId: string; archivedAt?: string | null }>; + return records.find((workspace) => workspace.workspaceId === workspaceId)?.archivedAt ?? null; + } + + async worktreeState(worktreePath: string): Promise<{ exists: boolean; listed: boolean }> { + const { stdout } = await execFileAsync("git", [ + "-C", + this.root, + "worktree", + "list", + "--porcelain", + ]); + const listedPaths = stdout + .split("\n") + .filter((line) => line.startsWith("worktree ")) + .map((line) => this.comparablePath(line.slice("worktree ".length))); + return { + exists: existsSync(worktreePath), + listed: listedPaths.includes(this.comparablePath(worktreePath)), + }; + } + + async createBranch(branch: string): Promise { + await execFileAsync("git", ["-C", this.root, "branch", branch]); + } + + async currentBranch(cwd: string): Promise { + const { stdout } = await execFileAsync("git", ["-C", cwd, "branch", "--show-current"]); + return stdout.trim(); + } + + async ownedPermissionRequest(agentId: string) { + const message = await this.latestSocket().socket.messageMatching( + (candidate): candidate is HubExecutionAgentStream => + candidate.type === "hub.execution.agent.stream" && + candidate.payload.agentId === agentId && + candidate.payload.event.type === "permission_requested", + ); + if (message.payload.event.type !== "permission_requested") { + throw new Error(`Owned agent ${agentId} did not request permission`); + } + return message.payload.event.request; + } + + async allowOwnedPermission(agentId: string, requestId: string): Promise { + await this.daemon!.agentManager.respondToPermission(agentId, requestId, { behavior: "allow" }); + } + + async listedWorktrees(): Promise { + const { stdout } = await execFileAsync("git", [ + "-C", + this.root, + "worktree", + "list", + "--porcelain", + ]); + return stdout + .split("\n") + .filter((line) => line.startsWith("worktree ")) + .map((line) => line.slice("worktree ".length)); + } + + async createOwnedConcurrently(executionId = "execution-1"): Promise<{ + first: AcceptedCreate; + duplicate: AcceptedCreate; + }> { + this.beginOwnedCreate("create-1", executionId); + this.beginOwnedCreate("create-2", executionId); + const first = await this.acceptedCreate("create-1"); + const duplicate = await this.acceptedCreate("create-2"); + return { first, duplicate }; + } + + async ownedUpdate(agentId: string): Promise { + const message = await this.latestSocket().socket.messageMatching( + (candidate): candidate is HubExecutionAgentUpdate => + candidate.type === "hub.execution.agent.update" && candidate.payload.agentId === agentId, + ); + return message.payload; + } + + async ownedRunningUpdate(agentId: string): Promise { + const message = await this.latestSocket().socket.messageMatching( + (candidate): candidate is HubExecutionAgentUpdate => + candidate.type === "hub.execution.agent.update" && + candidate.payload.agentId === agentId && + candidate.payload.agent.status === "running", + ); + return message.payload; + } + + async ownedTurnCompletion(agentId: string): Promise { + const socket = this.latestSocket().socket; + const completed = await socket.messageMatching( + (candidate): candidate is HubExecutionAgentStream => + candidate.type === "hub.execution.agent.stream" && + candidate.payload.agentId === agentId && + candidate.payload.event.type === "turn_completed", + ); + return completed.payload; + } + + async ownedTurnFailure(agentId: string): Promise { + const failed = await this.latestSocket().socket.messageMatching( + (candidate): candidate is HubExecutionAgentStream => + candidate.type === "hub.execution.agent.stream" && + candidate.payload.agentId === agentId && + candidate.payload.event.type === "turn_failed", + ); + return failed.payload; + } + + latestOwnedTurnCompletions(agentId: string): number { + return this.latestSocket().socket.sent.filter( + (message) => + message.type === "hub.execution.agent.stream" && + message.payload.agentId === agentId && + message.payload.event.type === "turn_completed", + ).length; + } + + async ownedStream(agentId: string): Promise { + const message = await this.latestSocket().socket.messageMatching( + (candidate): candidate is HubExecutionAgentStream => + candidate.type === "hub.execution.agent.stream" && candidate.payload.agentId === agentId, + ); + return message.payload; + } + + async createUnrelatedLocalAgent(): Promise { + const agent = await this.daemon!.agentManager.createAgent( + { provider: "codex", cwd: this.root }, + undefined, + { workspaceId: "local-workspace" }, + ); + return agent.id; + } + + async deniedSteering(agentId: string): Promise { + const requestId = "denied-steer"; + this.latestSocket().socket.receive({ + type: "send_agent_message_request", + requestId, + agentId, + text: "This must not run", + }); + return ((await this.latestSocket().socket.messageFor(requestId)) as RpcErrorMessage).payload; + } + + async deniedBrowserDispatch(): Promise { + const requestId = "browser-1"; + this.latestSocket().socket.receive({ + type: "browser.automation.execute.response", + payload: { + requestId, + ok: false, + error: { code: "browser_denied", message: "denied", retryable: false }, + }, + }); + return ((await this.latestSocket().socket.messageFor(requestId)) as RpcErrorMessage).payload; + } + + observedAgentIds(): string[] { + return this.remote.sockets.flatMap(({ socket }) => + socket.sent.flatMap((message) => { + if (message.type === "hub.execution.agent.update") return [message.payload.agentId]; + if (message.type === "hub.execution.agent.stream") return [message.payload.agentId]; + return []; + }), + ); + } + + observedTrustedLifecycleMessages(): string[] { + return this.remote.sockets.flatMap(({ socket }) => + socket.sent + .filter((message) => message.type === "status") + .map((message) => message.payload.status), + ); + } + + probeTrustedHello(): number | null { + const socket = this.latestSocket().socket; + socket.receiveEnvelope({ + type: "hello", + clientId: "hub-must-not-resume", + clientType: "browser", + protocolVersion: 1, + capabilities: { voice: true, pushNotifications: true }, + }); + return socket.closeCode; + } + + probeBinaryFrame(): number | null { + const socket = this.latestSocket().socket; + socket.receiveBinary(); + return socket.closeCode; + } + + async trustedBroadcastCount(): Promise { + const before = this.daemonConfigBroadcasts(); + const client = await this.trustedClient(); + try { + await client.patchDaemonConfig({ appendSystemPrompt: "hub-broadcast-isolation" }); + this.beginOwnedCreate("broadcast-barrier", "broadcast-barrier"); + await this.acceptedCreate("broadcast-barrier"); + return this.daemonConfigBroadcasts() - before; + } finally { + await client.close(); + } + } + + async trustedDaemonStatus() { + const client = await this.trustedClient(); + try { + return await client.getDaemonStatus(); + } finally { + await client.close(); + } + } + + async reconnectAndRetry(executionId = "execution-1") { + this.closeLatestSocket(1006); + await this.retry(); + this.connectLatestSocket(); + this.beginOwnedCreate("reconnect-retry", executionId); + return this.acceptedCreate("reconnect-retry"); + } + + async reconstructAndReplay(executionId = "execution-1") { + const storage = new AgentStorage( + path.join(this.paseoHome, "agents"), + pino({ level: "silent" }), + ); + const manager = new AgentManager({ + clients: createTestAgentClients(), + registry: storage, + logger: pino({ level: "silent" }), + }); + const executions = this.executionsForReconstruction(manager, storage); + const replay = await executions.create(this.ownedCreateInput(executionId)); + const durableAgentCount = (await storage.list()).filter( + (record) => record.owner?.kind === "daemon", + ).length; + return { replay, durableAgentCount }; + } + + async removeOwnedAgent(agentId: string) { + await this.daemon!.agentStorage.remove(agentId); + const storage = new AgentStorage( + path.join(this.paseoHome, "agents"), + pino({ level: "silent" }), + ); + return { + durableAgentCount: (await storage.list()).filter((record) => record.owner?.kind === "daemon") + .length, + }; + } + + socketAttempts(): number { + return this.remote.sockets.length; + } + + revocationAttempts(): number { + return this.remote.revocations.length; + } + + latestRevocation(): HubRevocation | null { + return this.remote.revocations.at(-1) ?? null; + } + + enrollmentAttempts(): HubEnrollment[] { + return this.remote.enrollments.map((input) => ({ ...input, scopes: input.scopes.slice() })); + } + + enrollmentInvocation(index = 0): RelationshipInvocationSnapshot { + const snapshot = this.remote.enrollmentSnapshots[index]; + if (!snapshot) throw new Error(`Enrollment invocation ${index} does not exist`); + return snapshot; + } + + socketInvocation(index = 0): RelationshipInvocationSnapshot { + const snapshot = this.remote.socketSnapshots[index]; + if (!snapshot) throw new Error(`Socket invocation ${index} does not exist`); + return snapshot; + } + + enrolledRelationships(): number { + return this.remote.relationshipCount(); + } + + loggableValues(status: Record): string { + return JSON.stringify({ status, logs: this.logs }); + } + + relationshipFile(): PersistedRelationship | null { + const file = path.join(this.paseoHome, "hub-relationship.json"); + if (!existsSync(file)) return null; + return JSON.parse(readFileSync(file, "utf8")) as PersistedRelationship; + } + + relationshipFileMode(): number { + return statSync(path.join(this.paseoHome, "hub-relationship.json")).mode & 0o777; + } + + async corruptRelationshipFile(contents = "{not-json"): Promise { + await this.stopDaemon(); + await writeFile(path.join(this.paseoHome, "hub-relationship.json"), contents, "utf8"); + } + + async quarantinedRelationshipFiles(): Promise { + return (await readdir(this.paseoHome)).filter((file) => + file.startsWith("hub-relationship.invalid-"), + ); + } + + async startStoppedDaemon(): Promise { + await this.startDaemon(); + } + + async storedOwnedStatus(agentId: string): Promise { + return (await this.daemon!.agentStorage.get(agentId))?.lastStatus ?? null; + } + + private captureRelationship(): RelationshipInvocationSnapshot { + const record = this.relationshipFile(); + if (!record) throw new Error("Relationship authority was not persisted before invocation"); + return { mode: this.relationshipFileMode(), record }; + } + + async retry(): Promise { + await this.clock.runNext(); + } + + async restartDaemon(): Promise { + await this.stopDaemon(); + await this.startDaemon(); + } + + async shutdownDaemon(): Promise { + await this.stopDaemon(); + } + + async close(): Promise { + await this.stopDaemon(); + await Promise.allSettled(this.cliProcesses); + await Promise.all( + [...this.claimedCliSockets].map((socket) => this.closeClaimedCliSocket(socket)), + ); + await this.removeRoot(); + } + + private async createHome(): Promise { + this.root = await mkdtemp(path.join(tmpdir(), "paseo-hub-relationship-")); + this.paseoHome = path.join(this.root, ".paseo"); + const staticDir = path.join(this.root, "static"); + await Promise.all([mkdir(this.paseoHome, { recursive: true }), mkdir(staticDir)]); + execFileSync("git", ["init", "-b", "main", this.root], { stdio: "ignore" }); + execFileSync("git", ["-C", this.root, "config", "user.email", "hub@test.invalid"]); + execFileSync("git", ["-C", this.root, "config", "user.name", "Hub Test"]); + execFileSync("git", ["-C", this.root, "commit", "--allow-empty", "-m", "initial"], { + stdio: "ignore", + }); + this.config = { + listen: "0.0.0.0:0", + paseoHome: this.paseoHome, + corsAllowedOrigins: [], + hostnames: true, + mcpEnabled: false, + staticDir, + mcpDebug: false, + agentClients: { + ...createTestAgentClients(), + codex: this.codex, + }, + agentStoragePath: path.join(this.paseoHome, "agents"), + relayEnabled: false, + relayEndpoint: "relay.paseo.sh:443", + appBaseUrl: "https://app.paseo.sh", + }; + } + + private async startDaemon(): Promise { + const destination = new Writable({ + write: (chunk, _encoding, done) => { + this.logs.push(String(chunk)); + done(); + }, + }); + this.daemon = await createPaseoDaemon(this.config, pino({ level: "trace" }, destination), { + hubRelationshipRemote: this.remote, + hubRelationshipClock: this.clock, + hubRelationshipRetryPolicy: this.clock, + createHubDaemonId: () => "daemon-test", + }); + await this.daemon.start(); + const target = this.daemon.getListenTarget(); + if (!target || target.type !== "tcp") throw new Error("Daemon did not bind TCP"); + this.host = `127.0.0.1:${target.port}`; + } + + private async stopDaemon(): Promise { + await this.daemon?.stop(); + this.daemon = null; + } + + private runCli(args: string[]): Promise> { + return this.trackCli(this.executeCli(args)); + } + + private async executeCli(args: string[]): Promise> { + const entrypoint = path.join(import.meta.dirname, "../../test-utils/hub-cli-entry.ts"); + const { stdout } = await execFileAsync( + process.execPath, + [ + "--conditions=source", + "--import", + "tsx", + entrypoint, + ...args, + "--host", + this.host, + "--json", + ], + { cwd: REPOSITORY_ROOT, env: { ...process.env, NO_COLOR: "1" } }, + ); + const parsed = JSON.parse(stdout) as unknown; + if (Array.isArray(parsed)) return parsed[0] as Record; + return parsed as Record; + } + + private trackCli(process: Promise): Promise { + this.cliProcesses.add(process); + void process.then( + () => this.cliProcesses.delete(process), + () => this.cliProcesses.delete(process), + ); + return process; + } + + private nextSocketEnvelope(socket: WebSocket): Promise<{ message: SessionOutboundMessage }> { + return new Promise((resolve, reject) => { + socket.once("message", (data) => { + try { + resolve(JSON.parse(data.toString()) as { message: SessionOutboundMessage }); + } catch (error) { + reject(error); + } + }); + socket.once("error", reject); + }); + } + + private async openClaimedCliSocket( + url: string, + options: { origin?: string } = {}, + ): Promise { + const socket = new WebSocket(url, options); + this.claimedCliSockets.add(socket); + await new Promise((resolve, reject) => { + socket.once("open", resolve); + socket.once("error", reject); + }); + socket.send( + JSON.stringify({ + type: "hello", + clientId: "external-claimed-cli", + clientType: "cli", + protocolVersion: 1, + }), + ); + await this.nextSocketEnvelope(socket); + return socket; + } + + private async closeClaimedCliSocket(socket: WebSocket): Promise { + this.claimedCliSockets.delete(socket); + if (socket.readyState === WebSocket.CLOSED) return; + const closed = new Promise((resolve) => socket.once("close", () => resolve())); + socket.terminate(); + await closed; + } + + private comparablePath(value: string): string { + const resolved = path.resolve(value); + let canonical = resolved; + try { + canonical = realpathSync.native(resolved); + } catch { + // An archived worktree no longer exists, so compare its normalized target path. + } + const normalized = path.normalize(canonical); + return platform() === "win32" ? normalized.toLowerCase() : normalized; + } + + private async removeRoot(): Promise { + let retryableCode: string | null = null; + if (platform() === "win32") retryableCode = "EBUSY"; + if (platform() === "darwin") retryableCode = "ENOTEMPTY"; + const attempts = retryableCode ? 10 : 1; + for (let attempt = 1; attempt <= attempts; attempt++) { + try { + await rm(this.root, { recursive: true, force: true }); + return; + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== retryableCode || attempt === attempts) { + throw error; + } + await new Promise((resolve) => setTimeout(resolve, 100 * attempt)); + } + } + } + + private latestSocket(): SocketAttempt { + const socket = this.remote.sockets.at(-1); + if (!socket) throw new Error("No Hub socket has been opened"); + return socket; + } + + private async acceptedCreate(requestId: string): Promise { + const message = (await this.latestSocket().socket.messageFor( + requestId, + )) as HubExecutionAgentCreateResponse; + if (!message.payload.success || !message.payload.agentId || !message.payload.agent) { + throw new Error(message.payload.error ?? "Hub agent creation failed"); + } + return { + ...message.payload, + success: true, + agentId: message.payload.agentId, + agent: message.payload.agent, + }; + } + + private ownedCreateInput(executionId: string) { + return { + executionId, + provider: "codex", + cwd: this.root, + workspaceId: "hub-workspace", + prompt: "Create through the Hub", + }; + } + + private executionsForReconstruction(manager: AgentManager, storage: AgentStorage) { + return new DaemonExecutions({ + daemonId: this.relationshipFile()!.relationship.daemonId, + agentManager: manager, + agentStorage: storage, + createAgent: (input) => + createAgentCommand( + { + agentManager: manager, + agentStorage: storage, + logger: pino({ level: "silent" }), + providerSnapshotManager: providerCatalog, + }, + input, + ), + }); + } + + private async trustedClient(): Promise { + const client = new DaemonClient({ + url: `ws://${this.host}/ws`, + appVersion: "0.1.106", + }); + await client.connect(); + await client.fetchAgents({ subscribe: { subscriptionId: "hub-relationship-trusted" } }); + return client; + } + + private daemonConfigBroadcasts(): number { + return this.remote.sockets + .flatMap(({ socket }) => socket.sent) + .filter( + (message) => + message.type === "status" && message.payload.status === "daemon_config_changed", + ).length; + } +} diff --git a/packages/server/src/server/persistence-hooks.ts b/packages/server/src/server/persistence-hooks.ts index 5fc511372..ba220eefa 100644 --- a/packages/server/src/server/persistence-hooks.ts +++ b/packages/server/src/server/persistence-hooks.ts @@ -110,6 +110,7 @@ export function extractTimestamps(record: StoredAgentRecord): { lastUserMessageAt: Date | null; labels?: Record; workspaceId?: string; + owner?: StoredAgentRecord["owner"]; } { return { createdAt: new Date(record.createdAt), @@ -117,6 +118,7 @@ export function extractTimestamps(record: StoredAgentRecord): { lastUserMessageAt: record.lastUserMessageAt ? new Date(record.lastUserMessageAt) : null, labels: record.labels, workspaceId: record.workspaceId, + owner: record.owner, }; } diff --git a/packages/server/src/server/session.test.ts b/packages/server/src/server/session.test.ts index 683d2821e..4db2bc7a6 100644 --- a/packages/server/src/server/session.test.ts +++ b/packages/server/src/server/session.test.ts @@ -18,7 +18,7 @@ import { FileTransferOpcode, type FileTransferFrame, } from "@getpaseo/protocol/binary-frames/index"; -import { Session } from "./session.js"; +import { isSessionRpcAllowed, Session } from "./session.js"; import { DownloadTokenStore } from "./file-download/token-store.js"; import { StructuredAgentFallbackError } from "./agent/agent-response-loop.js"; import type { StoredAgentRecord } from "./agent/agent-storage.js"; @@ -278,6 +278,7 @@ vi.mock("./worktree-bootstrap.js", async (importOriginal) => { }); interface SessionForTestOptions { + scopes?: readonly string[]; agentManager?: { [K in keyof SessionOptions["agentManager"]]?: unknown }; agentStorage?: { [K in keyof SessionOptions["agentStorage"]]?: unknown }; github?: Partial; @@ -349,7 +350,7 @@ function createSessionForTest(options: SessionForTestOptions = {}): Session { }; const messages = options.messages ?? []; - return new Session({ + const sessionOptions: SessionOptions = { clientId: "test-client", onMessage: (message) => messages.push(message), ...(options.targetedMessages @@ -413,9 +414,85 @@ function createSessionForTest(options: SessionForTestOptions = {}): Session { serverId: options.serverId, daemonVersion: options.daemonVersion, daemonRuntimeConfig: options.daemonRuntimeConfig, - }); + scopes: options.scopes ?? ["*"], + }; + return new Session(sessionOptions); } +describe("session authorization scopes", () => { + test("rejects an RPC outside an exact grant with the generic RPC error", async () => { + const messages: SessionOutboundMessage[] = []; + const session = createSessionForTest({ + scopes: ["hub.execution.agent.create.request"], + messages, + }); + + await session.handleMessage({ type: "ping", requestId: "restricted-ping", clientSentAt: 42 }); + + expect(messages).toEqual([ + { + type: "rpc_error", + payload: { + requestId: "restricted-ping", + requestType: "ping", + error: "Session is not authorized for ping", + code: "access_denied", + }, + }, + ]); + }); + + test.each([ + ["*", "ping"], + ["hub.execution.*", "hub.execution.agent.create.request"], + ["hub.execution.agent.create.request", "hub.execution.agent.create.request"], + ])("scope %s authorizes %s", (scope, requestType) => { + expect(isSessionRpcAllowed([scope], requestType)).toBe(true); + }); + + test.each([ + ["hub.execution.*", "hub.management.daemon.get_status.request"], + ["hub.execution.agent.create.request", "hub.execution.agent.update"], + ["hub.execution.*", "hub.executions.agent.create.request"], + ])("scope %s rejects %s", (scope, requestType) => { + expect(isSessionRpcAllowed([scope], requestType)).toBe(false); + }); + + test("replaces a session's scopes without reconstructing the session", async () => { + const messages: SessionOutboundMessage[] = []; + const session = createSessionForTest({ scopes: ["hub.execution.*"], messages }); + + await session.handleMessage({ + type: "ping", + requestId: "before-scope-change", + clientSentAt: 1, + }); + session.setScopes(["*"]); + await session.handleMessage({ type: "ping", requestId: "after-scope-change", clientSentAt: 2 }); + + expect(messages).toEqual([ + { + type: "rpc_error", + payload: { + requestId: "before-scope-change", + requestType: "ping", + error: "Session is not authorized for ping", + code: "access_denied", + }, + }, + { + type: "pong", + payload: { + requestId: "after-scope-change", + clientSentAt: 2, + serverReceivedAt: expect.any(Number), + serverSentAt: expect.any(Number), + }, + }, + ]); + }); +}); + describe("project command-center RPCs", () => { test("returns normalized repositories from the host GitHub service", async () => { const messages: SessionOutboundMessage[] = []; diff --git a/packages/server/src/server/session.ts b/packages/server/src/server/session.ts index 70fbb9b13..8476973c4 100644 --- a/packages/server/src/server/session.ts +++ b/packages/server/src/server/session.ts @@ -152,6 +152,9 @@ import { AgentConfigSession } from "./session/agent-config/agent-config-session. import { ProjectConfigSession } from "./session/project-config/project-config-session.js"; import { DaemonSession, type DaemonRuntimeConfig } from "./session/daemon/daemon-session.js"; import type { DaemonWebSocketRuntimeDiagnosticSnapshot } from "./session/daemon/diagnostics.js"; +import type { HubRelationshipManagement } from "./hub/relationship-controller.js"; +import { HubExecutionController } from "./hub/execution-controller.js"; +import type { HubExecutionAgents } from "./hub/daemon-executions.js"; import { DownloadTokenStore } from "./file-download/token-store.js"; import { PushTokenStore } from "./push/token-store.js"; import { @@ -381,6 +384,7 @@ type AgentMcpTransportFactory = () => Promise; export interface SessionOptions { clientId: string; + scopes: readonly string[]; appVersion?: string | null; clientCapabilities?: Record | null; onMessage: (msg: SessionOutboundMessage) => void; @@ -418,6 +422,8 @@ export interface SessionOptions { terminalManager: TerminalManager | null; providerSnapshotManager: ProviderSnapshotManager; providerUsageService: ProviderUsageService; + hubExecutionAgents?: HubExecutionAgents; + hubRelationships?: HubRelationshipManagement; serviceProxy?: ServiceProxySubsystem; scriptRuntimeStore?: WorkspaceScriptRuntimeStore; workspaceSetupSnapshots?: Map; @@ -481,6 +487,34 @@ function parseClientCapabilities( return new Set(result); } +export function isSessionRpcAllowed(scopes: readonly string[], rpcName: string): boolean { + return scopes.some((scope) => { + if (scope === "*" || scope === rpcName) { + return true; + } + if (!scope.endsWith(".*")) { + return false; + } + return rpcName.startsWith(scope.slice(0, -1)); + }); +} + +function sessionRequestId(message: SessionInboundMessage): string | null { + if ("requestId" in message && typeof message.requestId === "string") { + return message.requestId; + } + if ( + "payload" in message && + typeof message.payload === "object" && + message.payload !== null && + "requestId" in message.payload && + typeof message.payload.requestId === "string" + ) { + return message.payload.requestId; + } + return null; +} + interface AgentTimelineProjectionSelection { timeline: AgentTimelineFetchResult; entries: TimelineProjectionEntry[]; @@ -510,6 +544,7 @@ function describeRegistryTransition(record: ArchivedRecordSnapshot | null): Regi */ export class Session { private readonly clientId: string; + private scopes: readonly string[]; private appVersion: string | null; private clientCapabilities: ReadonlySet; private readonly sessionId: string; @@ -579,12 +614,14 @@ export class Session { private readonly agentConfigSession: AgentConfigSession; private readonly projectConfigSession: ProjectConfigSession; private readonly daemonSession: DaemonSession; + private readonly hubExecutionController: HubExecutionController | null; private readonly workspaceScripts: WorkspaceScriptsService; private readonly createAgentLifecycleDispatch: CreateAgentLifecycleDispatch; constructor(options: SessionOptions) { const { clientId, + scopes, appVersion, clientCapabilities, onMessage, @@ -635,6 +672,7 @@ export class Session { getWebSocketRuntimeMetrics, } = options; this.clientId = clientId; + this.scopes = [...scopes]; this.appVersion = appVersion ?? null; this.clientCapabilities = parseClientCapabilities(clientCapabilities); this.sessionId = uuidv4(); @@ -800,7 +838,14 @@ export class Session { listProjects: () => this.projectRegistry.list(), listWorkspaces: () => this.workspaceRegistry.list(), logger: this.sessionLogger, + hubRelationships: options.hubRelationships, }); + this.hubExecutionController = options.hubExecutionAgents + ? new HubExecutionController({ + agents: options.hubExecutionAgents, + send: (message) => this.emit(message), + }) + : null; this.daemonConfigStore = daemonConfigStore; this.terminalManager = terminalManager; this.terminalController = new TerminalSessionController({ @@ -1506,6 +1551,21 @@ export class Session { }, "agent.session.inbound", ); + if (!isSessionRpcAllowed(this.scopes, msg.type)) { + const requestId = sessionRequestId(msg); + if (requestId) { + this.emit({ + type: "rpc_error", + payload: { + requestId, + requestType: msg.type, + error: `Session is not authorized for ${msg.type}`, + code: "access_denied", + }, + }); + } + return; + } try { await this.dispatchInboundMessage(msg, source); } catch (error) { @@ -1545,12 +1605,17 @@ export class Session { } } + public setScopes(scopes: readonly string[]): void { + this.scopes = [...scopes]; + } + private async dispatchInboundMessage(msg: SessionInboundMessage, source?: object): Promise { const promise = this.dispatchVoiceAndControlMessage(msg) ?? this.dispatchAgentRewindMessage(msg) ?? this.dispatchAgentRelationshipMessage(msg) ?? this.dispatchAgentTimelineMessage(msg, source) ?? + this.dispatchHubExecutionMessage(msg) ?? this.dispatchAgentLifecycleMessage(msg) ?? this.dispatchAgentConfigMessage(msg) ?? this.dispatchCheckoutMessage(msg) ?? @@ -1667,6 +1732,12 @@ export class Session { } } + private dispatchHubExecutionMessage(msg: SessionInboundMessage): Promise | undefined { + return msg.type === "hub.execution.agent.create.request" + ? this.hubExecutionController?.createAgent(msg) + : undefined; + } + private dispatchAgentLifecycleMessage(msg: SessionInboundMessage): Promise | undefined { switch (msg.type) { case "fetch_agents_request": @@ -1730,6 +1801,10 @@ export class Session { return this.daemonSession.handleGetStatusRequest(msg); case "daemon.get_pairing_offer.request": return this.daemonSession.handleGetPairingOfferRequest(msg); + case "hub.management.daemon.connect.request": + case "hub.management.daemon.get_status.request": + case "hub.management.daemon.disconnect.request": + return this.daemonSession.handleHubRelationshipRequest(msg); case "diagnostics.request": return this.daemonSession.handleDiagnosticsRequest(msg); case "daemon.update.request": @@ -2731,6 +2806,10 @@ export class Session { let createdWorktreeForCleanup: CreatePaseoWorktreeWorkflowResult | null = null; let createdAgentId: string | null = null; try { + const requestedCwd = resolve(config.cwd); + if (!(await this.filesystem.isDirectory(requestedCwd))) { + throw new Error(`Working directory does not exist or is not a directory: ${requestedCwd}`); + } const trimmedPrompt = initialPrompt?.trim(); const { provisionalTitle } = resolveCreateAgentTitles({ configTitle: config.title, @@ -6108,6 +6187,9 @@ export class Session { * Emit a message to the client */ private emit(msg: SessionOutboundMessage): void { + if (msg.type !== "rpc_error" && !isSessionRpcAllowed(this.scopes, msg.type)) { + return; + } // JSON.stringify(msg) is only computed when trace is enabled — it runs for // every outbound message otherwise, and trace is disabled by default. // Optional-chained because test logger stubs don't implement isLevelEnabled. @@ -6145,6 +6227,7 @@ export class Session { this.unsubscribeAgentEvents = null; } this.agentUpdates.dispose(); + await this.hubExecutionController?.cleanup(); if (this.unsubscribeTerminalWorkspaceContributionEvents) { this.unsubscribeTerminalWorkspaceContributionEvents(); this.unsubscribeTerminalWorkspaceContributionEvents = null; diff --git a/packages/server/src/server/session.workspace-git-watch.test.ts b/packages/server/src/server/session.workspace-git-watch.test.ts index 64e4baa3c..06daa4726 100644 --- a/packages/server/src/server/session.workspace-git-watch.test.ts +++ b/packages/server/src/server/session.workspace-git-watch.test.ts @@ -182,6 +182,7 @@ function createSessionForWorkspaceGitWatchTests(options?: { const session = new Session({ clientId: "test-client", + scopes: ["*"], onMessage: (message) => emitted.push(message as { type: string; payload: unknown }), logger: createStub(logger), downloadTokenStore: createStub({}), diff --git a/packages/server/src/server/session.workspace-resolution-invariants.test.ts b/packages/server/src/server/session.workspace-resolution-invariants.test.ts index 42dcd593b..277320e54 100644 --- a/packages/server/src/server/session.workspace-resolution-invariants.test.ts +++ b/packages/server/src/server/session.workspace-resolution-invariants.test.ts @@ -88,6 +88,7 @@ function createHarness(input: { const session = new Session({ clientId: "test", + scopes: ["*"], appVersion: null, onMessage: (m) => emitted.push(m), logger: createStub(logger), diff --git a/packages/server/src/server/session.workspaces.test.ts b/packages/server/src/server/session.workspaces.test.ts index 05ba4d591..cd2b94e17 100644 --- a/packages/server/src/server/session.workspaces.test.ts +++ b/packages/server/src/server/session.workspaces.test.ts @@ -615,6 +615,7 @@ function createSessionForWorkspaceTests( const session = asTestSession( new Session({ clientId: "test-client", + scopes: ["*"], appVersion: options.appVersion ?? null, onMessage: options.onMessage ?? vi.fn(), onWorkspaceRecovered: options.onWorkspaceRecovered, @@ -815,6 +816,7 @@ test("create_agent_request keeps requested child cwd when grouped under an exist const session = asTestSession( new Session({ clientId: "test-client", + scopes: ["*"], appVersion: null, onMessage: (message) => emitted.push(message), logger: asSessionLogger(logger), @@ -957,6 +959,7 @@ test("create_agent_request launches from an exact subdirectory in a created work const emitted: SessionOutboundMessage[] = []; const session = new Session({ clientId: "test-client", + scopes: ["*"], appVersion: null, onMessage: (message) => emitted.push(message), logger: asSessionLogger(logger), @@ -1095,6 +1098,7 @@ test("create_agent_request does not title an existing workspace from the agent p const session = asTestSession( new Session({ clientId: "test-client", + scopes: ["*"], appVersion: null, onMessage: vi.fn(), logger: asSessionLogger(logger), @@ -1418,6 +1422,7 @@ test("archive emits an authoritative agent_update upsert for subscribed clients" const session = asTestSession( new Session({ clientId: "test-client", + scopes: ["*"], onMessage: (message) => emitted.push(message), logger: asSessionLogger(logger), downloadTokenStore: asDownloadTokenStore(), @@ -1784,6 +1789,7 @@ test("close_items_request archives agents and kills terminals in one batch", asy const session = asTestSession( new Session({ clientId: "test-client", + scopes: ["*"], onMessage: (message) => emitted.push(message), logger: asSessionLogger(sessionLogger), downloadTokenStore: asDownloadTokenStore(), @@ -1953,6 +1959,7 @@ test("close_items_request archives stored agents that are not currently loaded", const session = asTestSession( new Session({ clientId: "test-client", + scopes: ["*"], onMessage: (message) => emitted.push(message), logger: asSessionLogger(sessionLogger), downloadTokenStore: asDownloadTokenStore(), @@ -2113,6 +2120,7 @@ test("close_items_request continues after an archive failure", async () => { const session = asTestSession( new Session({ clientId: "test-client", + scopes: ["*"], onMessage: (message) => emitted.push(message), logger: asSessionLogger(sessionLogger), downloadTokenStore: asDownloadTokenStore(), @@ -3212,6 +3220,7 @@ test("workspace update stream keeps persisted workspace visible after agents sto const session = asTestSession( new Session({ clientId: "test-client", + scopes: ["*"], onMessage: (message) => emitted.push(message), logger: asSessionLogger(logger), downloadTokenStore: asDownloadTokenStore(), diff --git a/packages/server/src/server/session/daemon/daemon-session.test.ts b/packages/server/src/server/session/daemon/daemon-session.test.ts index 9417a17ed..661439bad 100644 --- a/packages/server/src/server/session/daemon/daemon-session.test.ts +++ b/packages/server/src/server/session/daemon/daemon-session.test.ts @@ -10,6 +10,7 @@ import { } from "./daemon-session.js"; import type { DaemonWebSocketRuntimeDiagnosticSnapshot } from "./diagnostics.js"; import type { ProviderAvailability } from "../../agent/agent-manager.js"; +import type { HubRelationshipManagement } from "../../hub/relationship-controller.js"; import type { SessionOutboundMessage } from "../../messages.js"; const tempDirs: string[] = []; @@ -40,6 +41,7 @@ function makeSubsystem(overrides: { daemonRuntimeConfig?: DaemonRuntimeConfig; listProviderAvailability?: () => Promise; getWebSocketRuntimeMetrics?: () => DaemonWebSocketRuntimeDiagnosticSnapshot | null; + hubRelationships?: HubRelationshipManagement; }) { const emitted: SessionOutboundMessage[] = []; const restartIntents: Parameters[0][] = []; @@ -60,12 +62,67 @@ function makeSubsystem(overrides: { listWorkspaces: async () => [], listProviderAvailability: overrides.listProviderAvailability ?? (async () => []), getWebSocketRuntimeMetrics: overrides.getWebSocketRuntimeMetrics, + hubRelationships: overrides.hubRelationships, logger: pino({ level: "silent" }), }); return { subsystem, emitted, paseoHome, restartIntents }; } describe("DaemonSession", () => { + test("Hub relationship command failures return correlated RPC errors", async () => { + const { subsystem, emitted } = makeSubsystem({ + hubRelationships: { + connect: async () => { + throw new Error("Hub rejected enrollment (401)"); + }, + status: () => ({ + state: "not_connected", + daemonId: null, + hubOrigin: null, + scopes: [], + connectedAt: null, + lastError: null, + }), + disconnect: async () => { + throw new Error("Hub revocation failed (503)"); + }, + }, + }); + + await subsystem.handleHubRelationshipRequest({ + type: "hub.management.daemon.connect.request", + requestId: "connect-1", + hubUrl: "https://hub.test", + token: "token", + }); + await subsystem.handleHubRelationshipRequest({ + type: "hub.management.daemon.disconnect.request", + requestId: "disconnect-1", + force: false, + }); + + expect(emitted).toEqual([ + { + type: "rpc_error", + payload: { + requestId: "connect-1", + requestType: "hub.management.daemon.connect.request", + error: "Hub rejected enrollment (401)", + code: "handler_error", + }, + }, + { + type: "rpc_error", + payload: { + requestId: "disconnect-1", + requestType: "hub.management.daemon.disconnect.request", + error: "Hub revocation failed (503)", + code: "handler_error", + }, + }, + ]); + }); + test("status reports identity, runtime config, and providers with errors normalized to null", async () => { const { subsystem, emitted } = makeSubsystem({ serverId: "srv-1", diff --git a/packages/server/src/server/session/daemon/daemon-session.ts b/packages/server/src/server/session/daemon/daemon-session.ts index beb525118..7142ce8f8 100644 --- a/packages/server/src/server/session/daemon/daemon-session.ts +++ b/packages/server/src/server/session/daemon/daemon-session.ts @@ -10,6 +10,7 @@ import { import { DaemonSelfUpdateSessionController } from "./daemon-self-update-session-controller.js"; import type { ManagedAgent } from "../../agent/agent-manager.js"; import type { PersistedProjectRecord, PersistedWorkspaceRecord } from "../../workspace-registry.js"; +import type { HubRelationshipManagement } from "../../hub/relationship-controller.js"; export interface DaemonRuntimeConfig { listen: string | null; @@ -48,6 +49,7 @@ export interface DaemonSessionOptions { listProviderAvailability: () => Promise; getWebSocketRuntimeMetrics?: () => DaemonWebSocketRuntimeDiagnosticSnapshot | null; logger: pino.Logger; + hubRelationships?: HubRelationshipManagement; } /** @@ -71,6 +73,7 @@ export class DaemonSession { private readonly getWebSocketRuntimeMetrics: () => DaemonWebSocketRuntimeDiagnosticSnapshot | null; private readonly logger: pino.Logger; private readonly selfUpdate: DaemonSelfUpdateSessionController; + private readonly hubRelationships: HubRelationshipManagement | null; constructor(options: DaemonSessionOptions) { this.host = options.host; @@ -85,6 +88,7 @@ export class DaemonSession { this.listProviderAvailability = options.listProviderAvailability; this.getWebSocketRuntimeMetrics = options.getWebSocketRuntimeMetrics ?? (() => null); this.logger = options.logger; + this.hubRelationships = options.hubRelationships ?? null; this.selfUpdate = new DaemonSelfUpdateSessionController({ clientId: this.clientId, daemonVersion: this.daemonVersion ?? null, @@ -95,6 +99,56 @@ export class DaemonSession { }); } + async handleHubRelationshipRequest( + msg: Extract< + SessionInboundMessage, + { + type: + | "hub.management.daemon.connect.request" + | "hub.management.daemon.get_status.request" + | "hub.management.daemon.disconnect.request"; + } + >, + ): Promise { + try { + if (!this.hubRelationships) throw new Error("Hub relationship management is unavailable"); + if (msg.type === "hub.management.daemon.connect.request") { + const status = await this.hubRelationships.connect({ + hubUrl: msg.hubUrl, + token: msg.token, + }); + this.host.emit({ + type: "hub.management.daemon.connect.response", + payload: { requestId: msg.requestId, status }, + }); + return; + } + if (msg.type === "hub.management.daemon.disconnect.request") { + const result = await this.hubRelationships.disconnect({ force: msg.force ?? false }); + this.host.emit({ + type: "hub.management.daemon.disconnect.response", + payload: { requestId: msg.requestId, ...result }, + }); + return; + } + this.host.emit({ + type: "hub.management.daemon.get_status.response", + payload: { requestId: msg.requestId, status: this.hubRelationships.status() }, + }); + } catch (error) { + this.logger.error({ err: error }, "Failed to handle Hub relationship request"); + this.host.emit({ + type: "rpc_error", + payload: { + requestId: msg.requestId, + requestType: msg.type, + error: error instanceof Error ? error.message : String(error), + code: "handler_error", + }, + }); + } + } + async handleGetStatusRequest( msg: Extract, ): Promise { diff --git a/packages/server/src/server/snapshot-mutation-ownership.test.ts b/packages/server/src/server/snapshot-mutation-ownership.test.ts index 0f23c2a40..87308a3de 100644 --- a/packages/server/src/server/snapshot-mutation-ownership.test.ts +++ b/packages/server/src/server/snapshot-mutation-ownership.test.ts @@ -99,6 +99,7 @@ describe("snapshot mutation ownership boundary", () => { const session = asInternals( new Session({ clientId: "test-client", + scopes: ["*"], onMessage, logger: createStub(logger), downloadTokenStore: createStub({}), diff --git a/packages/server/src/server/test-utils/fake-agent-client.ts b/packages/server/src/server/test-utils/fake-agent-client.ts index 6468f9fb1..e025634b3 100644 --- a/packages/server/src/server/test-utils/fake-agent-client.ts +++ b/packages/server/src/server/test-utils/fake-agent-client.ts @@ -57,10 +57,12 @@ interface FakeAgentSessionOptions { sessionId?: string; memoryMarker?: string | null; closeSession?: () => Promise; + onStartTurn?: (prompt: AgentPromptInput) => void; } export interface TestAgentClientOptions { closeSession?: () => Promise; + onStartTurn?: (prompt: AgentPromptInput) => void; } function createDeferred(): Deferred { @@ -329,6 +331,7 @@ class FakeAgentSession implements AgentSession { private activeForegroundTurnId: string | null = null; private readonly closeSession: (() => Promise) | undefined; + private readonly onStartTurn: ((prompt: AgentPromptInput) => void) | undefined; constructor(options: FakeAgentSessionOptions) { this.providerName = options.providerName; @@ -336,6 +339,7 @@ class FakeAgentSession implements AgentSession { this.id = options.sessionId ?? randomUUID(); this.memoryMarker = options.memoryMarker ?? null; this.closeSession = options.closeSession; + this.onStartTurn = options.onStartTurn; this.historyPath = path.join( tmpdir(), "paseo-fake-provider-history", @@ -426,6 +430,7 @@ class FakeAgentSession implements AgentSession { const turnId = `fake-turn-${this.nextTurnOrdinal++}`; this.activeForegroundTurnId = turnId; + this.onStartTurn?.(prompt); void this.emitTurnEvents(prompt); @@ -1180,6 +1185,7 @@ class FakeAgentClient implements AgentClient { providerName: this.provider, config: { ...config }, closeSession: this.options.closeSession, + onStartTurn: this.options.onStartTurn, }); } @@ -1203,6 +1209,7 @@ class FakeAgentClient implements AgentClient { sessionId: handle.sessionId, memoryMarker: typeof marker === "string" ? marker : null, closeSession: this.options.closeSession, + onStartTurn: this.options.onStartTurn, }); } diff --git a/packages/server/src/server/test-utils/hub-cli-entry.ts b/packages/server/src/server/test-utils/hub-cli-entry.ts new file mode 100644 index 000000000..3e64439c3 --- /dev/null +++ b/packages/server/src/server/test-utils/hub-cli-entry.ts @@ -0,0 +1,4 @@ +import { createHubCommand } from "../../../../cli/src/commands/hub/index.js"; + +const argv = [process.argv[0] ?? "node", process.argv[1] ?? "paseo", ...process.argv.slice(3)]; +await createHubCommand().parseAsync(argv, { from: "node" }); diff --git a/packages/server/src/server/websocket-server.notifications.test.ts b/packages/server/src/server/websocket-server.notifications.test.ts index 0772e7bd5..fb517efe5 100644 --- a/packages/server/src/server/websocket-server.notifications.test.ts +++ b/packages/server/src/server/websocket-server.notifications.test.ts @@ -190,6 +190,7 @@ function connectClient( ) { const ws = createOpenSocket(); asInternals(server).sessions.set(ws, { + kind: "trusted", session: createSessionWithActivity(activity), clientId: "client-test", appVersion: null, diff --git a/packages/server/src/server/websocket-server.terminal-notifications.test.ts b/packages/server/src/server/websocket-server.terminal-notifications.test.ts index 3862f5b4f..7d3cba294 100644 --- a/packages/server/src/server/websocket-server.terminal-notifications.test.ts +++ b/packages/server/src/server/websocket-server.terminal-notifications.test.ts @@ -198,6 +198,7 @@ function createOpenSocket() { function connectClient(server: VoiceAssistantWebSocketServer) { const ws = createOpenSocket(); asInternals<{ sessions: Map }>(server).sessions.set(ws, { + kind: "trusted", session: { getClientActivity: vi.fn(() => null), }, diff --git a/packages/server/src/server/websocket-server.ts b/packages/server/src/server/websocket-server.ts index 649b54152..73e6f80d1 100644 --- a/packages/server/src/server/websocket-server.ts +++ b/packages/server/src/server/websocket-server.ts @@ -18,6 +18,7 @@ import type { CheckoutDiffManager, CheckoutDiffMetrics } from "./checkout-diff-m import type { DaemonConfigStore, MutableDaemonConfig } from "./daemon-config-store.js"; import { type ServerInfoStatusPayload, + type SessionOutboundMessage, type WorkspaceSetupSnapshot, type WSHelloMessage, type WSInboundMessage, @@ -32,6 +33,8 @@ import type { TerminalActivity } from "@getpaseo/protocol/terminal-activity"; import type { HostnamesConfig } from "./hostnames.js"; import { isHostnameAllowed } from "./hostnames.js"; import { Session, type SessionLifecycleIntent, type SessionRuntimeMetrics } from "./session.js"; +import type { HubRelationshipManagement } from "./hub/relationship-controller.js"; +import type { HubExecutionAgents } from "./hub/daemon-executions.js"; import type { AgentProvider } from "./agent/agent-sdk-types.js"; import { ProviderSnapshotManager } from "./agent/provider-snapshot-manager.js"; import type { WorkspaceGitRuntimeSnapshot, WorkspaceGitService } from "./workspace-git-service.js"; @@ -98,6 +101,8 @@ interface PendingConnection { interface WebSocketConnectionIdentity { connectionId: string; transport: "direct" | "relay"; + peer: "loopback" | "local_ipc" | "external"; + browserOrigin: boolean; host?: string; origin?: string; userAgent?: string; @@ -330,7 +335,7 @@ function getBrowserHostCapability( return parsed.success ? parsed.data : null; } -interface WebSocketLike { +export interface WebSocketLike { readyState: number; bufferedAmount?: number; send: (data: string | Uint8Array | ArrayBuffer) => void; @@ -339,7 +344,8 @@ interface WebSocketLike { once: (event: "close" | "error", listener: (...args: unknown[]) => void) => void; } -interface SessionConnection { +interface TrustedSessionConnection { + kind: "trusted"; session: Session; clientId: string; appVersion: string | null; @@ -349,11 +355,46 @@ interface SessionConnection { externalDisconnectCleanupTimeout: ReturnType | null; } +interface HubConnection { + kind: "hub"; + session: Session; + daemonId: string; + connectionLogger: pino.Logger; + socket: WebSocketLike; +} + +type SessionConnection = TrustedSessionConnection | HubConnection; + +type TrustedLifecycleKey = + | "clientId" + | "appVersion" + | "clientCapabilities" + | "sockets" + | "externalDisconnectCleanupTimeout"; +type HubLifecycleOverlap = Extract; +const HUB_HAS_NO_TRUSTED_LIFECYCLE_STATE: HubLifecycleOverlap extends never ? true : never = true; +void HUB_HAS_NO_TRUSTED_LIFECYCLE_STATE; + interface BrowserToolsRegistration { capabilitySignature: string; unregister: () => void; } +interface SocketSessionOptions { + clientId: string; + appVersion: string | null; + clientCapabilities: Record | null; + scopes: readonly string[]; + connectionLogger: pino.Logger; + onMessage: (message: SessionOutboundMessage) => void; + onMessageToSource?: (source: object, message: SessionOutboundMessage) => void; + onBinaryMessage?: (frame: Uint8Array) => void; + getTransportBufferedAmount?: () => number | null; + onLifecycleIntent?: (intent: SessionLifecycleIntent) => void; + hubExecutionAgents?: HubExecutionAgents; + hubRelationships?: HubRelationshipManagement; +} + const SLOW_REQUEST_THRESHOLD_MS = 500; const EXTERNAL_SESSION_DISCONNECT_GRACE_MS = 90_000; const HELLO_TIMEOUT_MS = 15_000; @@ -409,7 +450,7 @@ export class VoiceAssistantWebSocketServer { private readonly pendingConnections: Map = new Map(); private readonly sessions: Map = new Map(); private readonly socketIdentities: Map = new Map(); - private readonly externalSessionsByKey: Map = new Map(); + private readonly externalSessionsByKey: Map = new Map(); private readonly serverId: string; private readonly daemonVersion: string; private readonly daemonRuntimeConfig: DaemonRuntimeConfig | undefined; @@ -460,6 +501,7 @@ export class VoiceAssistantWebSocketServer { private readonly providerUsageService: ProviderUsageService; private unsubscribeTerminalActivity: (() => void) | null = null; private readonly browserToolsBroker: BrowserToolsBroker | null; + private readonly hubRelationships: HubRelationshipManagement | null; private readonly browserToolsRegistrations = new Map(); private acceptingConnections = true; @@ -506,6 +548,7 @@ export class VoiceAssistantWebSocketServer { daemonRuntimeConfig?: DaemonRuntimeConfig, serviceProxyPublicBaseUrl?: string | null, browserToolsBroker?: BrowserToolsBroker | null, + hubRelationships?: HubRelationshipManagement | null, ) { this.logger = logger.child({ module: "websocket-server" }); this.serverId = serverId; @@ -515,6 +558,7 @@ export class VoiceAssistantWebSocketServer { this.daemonVersion = daemonVersion.trim(); this.daemonRuntimeConfig = daemonRuntimeConfig; this.browserToolsBroker = browserToolsBroker ?? null; + this.hubRelationships = hubRelationships ?? null; this.agentManager = agentManager; this.agentStorage = agentStorage; this.projectRegistry = projectRegistry ?? createNoopProjectRegistry(); @@ -751,7 +795,10 @@ export class VoiceAssistantWebSocketServer { public broadcast(message: WSOutboundMessage): void { const payload = JSON.stringify(message); - for (const ws of this.sessions.keys()) { + for (const [ws, connection] of this.sessions) { + if (connection.kind !== "trusted") { + continue; + } // WebSocket.OPEN = 1 if (ws.readyState === 1) { ws.send(payload); @@ -760,18 +807,20 @@ export class VoiceAssistantWebSocketServer { } } - public listActiveSessions(): Session[] { + public listTrustedSessions(): Session[] { return Array.from( new Set( - [...this.sessions.values(), ...this.externalSessionsByKey.values()].map( - (connection) => connection.session, - ), + [...this.sessions.values(), ...this.externalSessionsByKey.values()] + .filter( + (connection): connection is TrustedSessionConnection => connection.kind === "trusted", + ) + .map((connection) => connection.session), ), ); } public publishProjectUpdate(update: ProjectUpdate): void { - for (const session of this.listActiveSessions()) session.emitProjectUpdate(update); + for (const session of this.listTrustedSessions()) session.emitProjectUpdate(update); } public publishSpeechReadiness(readiness: SpeechReadinessSnapshot | null): void { @@ -797,6 +846,44 @@ export class VoiceAssistantWebSocketServer { await this.attachSocket(ws, undefined, metadata); } + public async attachHubSocket( + ws: WebSocketLike, + options: { + daemonId: string; + scopes: readonly string[]; + agents: HubExecutionAgents; + }, + ): Promise { + if (!this.acceptingConnections) { + ws.close(WS_CLOSE_SERVER_SHUTDOWN, "Server shutting down"); + return; + } + + const connectionLogger = this.logger.child({ + connectionKind: "hub", + daemonId: options.daemonId, + }); + const session = this.createSocketSession({ + clientId: `hub:${options.daemonId}`, + appVersion: null, + clientCapabilities: null, + scopes: options.scopes, + connectionLogger, + onMessage: (message) => this.sendToClient(ws, wrapSessionMessage(message)), + hubExecutionAgents: options.agents, + }); + const connection: HubConnection = { + kind: "hub", + session, + daemonId: options.daemonId, + connectionLogger, + socket: ws, + }; + this.sessions.set(ws, connection); + this.bindSocketHandlers(ws); + connectionLogger.info("Hub session attached"); + } + public prepareForShutdown(): void { this.acceptingConnections = false; } @@ -832,13 +919,14 @@ export class VoiceAssistantWebSocketServer { const cleanupPromises: Promise[] = []; for (const connection of uniqueConnections) { - if (connection.externalDisconnectCleanupTimeout) { + if (connection.kind === "trusted" && connection.externalDisconnectCleanupTimeout) { clearTimeout(connection.externalDisconnectCleanupTimeout); connection.externalDisconnectCleanupTimeout = null; } - cleanupPromises.push(connection.session.cleanup()); - for (const ws of connection.sockets) { + cleanupPromises.push(Promise.resolve(connection.session.cleanup())); + const sockets = connection.kind === "trusted" ? connection.sockets : [connection.socket]; + for (const ws of sockets) { cleanupPromises.push( new Promise((resolve) => { // WebSocket.CLOSED = 3 @@ -908,12 +996,16 @@ export class VoiceAssistantWebSocketServer { } private sendToConnection(connection: SessionConnection, message: WSOutboundMessage): void { - for (const ws of connection.sockets) { + const sockets = connection.kind === "trusted" ? connection.sockets : [connection.socket]; + for (const ws of sockets) { this.sendToClient(ws, message); } } private sendBinaryToConnection(connection: SessionConnection, frame: Uint8Array): void { + if (connection.kind !== "trusted") { + return; + } for (const ws of connection.sockets) { this.sendBinaryToClient(ws, frame); } @@ -981,14 +1073,16 @@ export class VoiceAssistantWebSocketServer { appVersion: string | null; clientCapabilities: Record | null; connectionLogger: pino.Logger; - }): SessionConnection { + }): TrustedSessionConnection { const { ws, clientId, appVersion, clientCapabilities, connectionLogger } = params; - let connection: SessionConnection | null = null; + let connection: TrustedSessionConnection | null = null; - const session = new Session({ + const session = this.createSocketSession({ clientId, appVersion, clientCapabilities, + scopes: ["*"], + connectionLogger, onMessage: (msg) => { if (!connection) { return; @@ -1026,14 +1120,42 @@ export class VoiceAssistantWebSocketServer { onLifecycleIntent: (intent) => { this.onLifecycleIntent?.(intent); }, + hubRelationships: this.hubRelationships ?? undefined, + }); + + connection = { + kind: "trusted", + session, + clientId, + appVersion, + clientCapabilities, + connectionLogger, + sockets: new Set([ws]), + externalDisconnectCleanupTimeout: null, + }; + session.updateClientCapabilities(clientCapabilities, ws); + return connection; + } + + private createSocketSession(options: SocketSessionOptions): Session { + return new Session({ + clientId: options.clientId, + appVersion: options.appVersion, + clientCapabilities: options.clientCapabilities, + scopes: options.scopes, + onMessage: options.onMessage, + onMessageToSource: options.onMessageToSource, + onBinaryMessage: options.onBinaryMessage, + getTransportBufferedAmount: options.getTransportBufferedAmount, + onLifecycleIntent: options.onLifecycleIntent, + logger: options.connectionLogger.child({ module: "session" }), onWorkspaceRecovered: async (workspace) => { await Promise.all( - this.listActiveSessions().map((activeSession) => + this.listTrustedSessions().map((activeSession) => activeSession.refreshRecoveredWorkspaceForExternalMutation(workspace), ), ); }, - logger: connectionLogger.child({ module: "session" }), downloadTokenStore: this.downloadTokenStore, pushTokenStore: this.pushTokenStore, paseoHome: this.paseoHome, @@ -1057,6 +1179,8 @@ export class VoiceAssistantWebSocketServer { terminalManager: this.terminalManager, providerSnapshotManager: this.providerSnapshotManager, providerUsageService: this.providerUsageService, + hubExecutionAgents: options.hubExecutionAgents, + hubRelationships: options.hubRelationships, serviceProxy: this.serviceProxy ?? undefined, scriptRuntimeStore: this.scriptRuntimeStore ?? undefined, workspaceSetupSnapshots: this.workspaceSetupSnapshots, @@ -1096,18 +1220,6 @@ export class VoiceAssistantWebSocketServer { daemonRuntimeConfig: this.daemonRuntimeConfig, getWebSocketRuntimeMetrics: () => this.lastRuntimeMetricsSnapshot, }); - - connection = { - session, - clientId, - appVersion, - clientCapabilities, - connectionLogger, - sockets: new Set([ws]), - externalDisconnectCleanupTimeout: null, - }; - session.updateClientCapabilities(clientCapabilities, ws); - return connection; } private clearPendingConnection(ws: WebSocketLike): PendingConnection | null { @@ -1284,6 +1396,8 @@ export class VoiceAssistantWebSocketServer { providerSubagents: true, // COMPAT(workspacePinning): added in v0.1.107, remove gate after 2027-01-12. workspacePinning: true, + // COMPAT(hubRelationship): added in v0.1.X, drop the gate when floor >= v0.1.X. + hubRelationship: true, // COMPAT(projectGithubClone): added in v0.1.108, remove gate after 2027-01-15. projectGithubClone: true, // COMPAT(workspaceGithubRepositorySearch): added in v0.1.108, remove gate after 2027-01-15. @@ -1410,6 +1524,15 @@ export class VoiceAssistantWebSocketServer { } this.sessions.delete(ws); + if (connection.kind === "hub") { + this.socketIdentities.delete(ws); + connection.connectionLogger.info( + { code: details.code, reason: stringifyCloseReason(details.reason) }, + "Hub session disconnected", + ); + connection.session.cleanup(); + return; + } connection.sockets.delete(ws); connection.session.clearAgentTimelineSubscription(ws); this.socketIdentities.delete(ws); @@ -1459,7 +1582,7 @@ export class VoiceAssistantWebSocketServer { } private async cleanupConnection( - connection: SessionConnection, + connection: TrustedSessionConnection, logMessage: string, ): Promise { this.incrementRuntimeCounter("sessionCleanup"); @@ -1486,7 +1609,7 @@ export class VoiceAssistantWebSocketServer { await connection.session.cleanup(); } - private syncBrowserToolsClientRegistration(connection: SessionConnection): void { + private syncBrowserToolsClientRegistration(connection: TrustedSessionConnection): void { if (!this.browserToolsBroker) { return; } @@ -1562,7 +1685,7 @@ export class VoiceAssistantWebSocketServer { log.warn( { - clientId: activeConnection?.clientId, + clientId: activeConnection?.kind === "trusted" ? activeConnection.clientId : undefined, requestId: requestInfo?.requestId, requestType: requestInfo?.requestType, error: parsedMessage.error.message, @@ -1627,6 +1750,11 @@ export class VoiceAssistantWebSocketServer { } return true; } + if (activeConnection.kind === "hub") { + log.warn("Rejected binary frame on Hub session"); + ws.close(WS_CLOSE_INVALID_HELLO, "Binary frames are not supported on Hub sessions"); + return true; + } void Promise.resolve(activeConnection.session.handleBinaryFrame(decodedFrame)).catch( (error: unknown) => { this.handleRawMessageError({ @@ -1766,15 +1894,26 @@ export class VoiceAssistantWebSocketServer { const controlRpc = getControlRpcLogInfo(message.message); if (controlRpc) { const identity = this.socketIdentities.get(ws); + let connectionFields: Record; + if (identity) { + connectionFields = toConnectionLogFields(identity); + } else if (activeConnection.kind === "trusted") { + connectionFields = { clientId: activeConnection.clientId }; + } else { + connectionFields = { daemonId: activeConnection.daemonId }; + } activeConnection.connectionLogger.warn( { - ...(identity ? toConnectionLogFields(identity) : { clientId: activeConnection.clientId }), + ...connectionFields, ...controlRpc, }, "ws_control_rpc_received", ); } - if (message.message.type === "browser.automation.execute.response") { + if ( + activeConnection.kind === "trusted" && + message.message.type === "browser.automation.execute.response" + ) { this.browserToolsBroker?.receiveResponse(message.message as BrowserAutomationExecuteResponse); return; } @@ -1784,7 +1923,7 @@ export class VoiceAssistantWebSocketServer { const durationMs = performance.now() - startMs; this.recordRequestLatency(message.message.type, durationMs); - if (durationMs >= SLOW_REQUEST_THRESHOLD_MS) { + if (durationMs >= SLOW_REQUEST_THRESHOLD_MS && activeConnection.kind === "trusted") { activeConnection.connectionLogger.warn( { requestType: message.message.type, @@ -1896,7 +2035,9 @@ export class VoiceAssistantWebSocketServer { } private collectSessionRuntimeMetrics(): WebSocketRuntimeMetrics { - const uniqueConnections = new Set(this.externalSessionsByKey.values()); + const uniqueConnections = new Set( + this.externalSessionsByKey.values(), + ); let terminalDirectorySubscriptionCount = 0; let terminalSubscriptionCount = 0; let inflightRequests = 0; @@ -1997,6 +2138,9 @@ export class VoiceAssistantWebSocketServer { }> = []; for (const [ws, connection] of this.sessions) { + if (connection.kind !== "trusted") { + continue; + } clientEntries.push({ ws, state: this.getClientActivityState(connection.session), @@ -2079,6 +2223,9 @@ export class VoiceAssistantWebSocketServer { }> = []; for (const [ws, connection] of this.sessions) { + if (connection.kind !== "trusted") { + continue; + } clientEntries.push({ ws, state: this.getClientActivityState(connection.session), @@ -2153,6 +2300,8 @@ function createWebSocketConnectionIdentity( return { connectionId: `conn_${randomUUID().replaceAll("-", "")}`, transport: metadata?.transport === "relay" ? "relay" : "direct", + peer: resolveConnectionPeer(requestMetadata, metadata), + browserOrigin: requestMetadata.origin !== undefined, ...(requestMetadata.host ? { host: requestMetadata.host } : {}), ...(requestMetadata.origin ? { origin: requestMetadata.origin } : {}), ...(requestMetadata.userAgent ? { userAgent: requestMetadata.userAgent } : {}), @@ -2165,6 +2314,7 @@ function toConnectionLogFields(identity: WebSocketConnectionIdentity): Record messages.push(message), logger: pino({ level: "silent" }), From a1de743ef67dde4fe7c48d045a3714f65dfa5e90 Mon Sep 17 00:00:00 2001 From: Slava Goltser Date: Fri, 17 Jul 2026 15:07:44 -0400 Subject: [PATCH 17/38] fix(app): align thinking section scroll layout with other detail sections (#1884) * feat(app): make thinking sections scrollable when expanded * fix(app): align thinking section scroll layout with other detail sections --- .../app/src/components/tool-call-details.tsx | 31 +++++++++---------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/packages/app/src/components/tool-call-details.tsx b/packages/app/src/components/tool-call-details.tsx index b3e6cd463..9025f6a23 100644 --- a/packages/app/src/components/tool-call-details.tsx +++ b/packages/app/src/components/tool-call-details.tsx @@ -490,12 +490,19 @@ function FetchDetailSection({ url, result, ds }: FetchDetailProps) { ); } -function PlainTextSection({ text }: { text: string }) { +function ScrollablePlainTextSection({ text, ds }: { text: string; ds: DetailStyles }) { return ( - - - {text} - + + + + {text} + + ); } @@ -576,13 +583,7 @@ function buildUnknownSections(detail: UnknownDetail, ds: DetailStyles, t: TFunct typeof detail.input === "string" && detail.output === null ? detail.input : null; if (plainInputText !== null) { - return [ - - - {plainInputText} - - , - ]; + return []; } const sectionsFromTopLevel = [ @@ -698,7 +699,7 @@ function buildDetailSections( } if (detail.type === "plain_text") { if (!detail.text) return []; - return []; + return []; } if (detail.type === "unknown") { return buildUnknownSections(detail, ds, t); @@ -807,10 +808,6 @@ const styles = StyleSheet.create((theme) => { flex: 1, minHeight: 0, }, - plainTextSection: { - gap: theme.spacing[2], - padding: theme.spacing[3], - }, plainText: { fontFamily: theme.fontFamily.ui, fontSize: theme.fontSize.base, From b4a5b6a3ff94af056e30231b19cbdacbe0566c8f Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Sat, 18 Jul 2026 11:49:11 +0200 Subject: [PATCH 18/38] Add non-Git projects across filesystem mounts (#2187) * fix(projects): allow non-git folders across mounts Treat failed Git worktree discovery as non-Git so ordinary directories remain addable. Preserve the underlying diagnostic as a structured warning instead of failing project creation. * fix(projects): preserve discovery warnings Propagate checkout context through the remaining worktree discovery calls so fail-open classification retains its structured warning. --- .../server/src/utils/checkout-git.test.ts | 34 +++++++++++++++++++ packages/server/src/utils/checkout-git.ts | 23 +++++-------- 2 files changed, 43 insertions(+), 14 deletions(-) diff --git a/packages/server/src/utils/checkout-git.test.ts b/packages/server/src/utils/checkout-git.test.ts index 78369c64a..cf07bfc97 100644 --- a/packages/server/src/utils/checkout-git.test.ts +++ b/packages/server/src/utils/checkout-git.test.ts @@ -8,10 +8,12 @@ import { readFileSync, realpathSync, mkdirSync, + statSync, } from "fs"; import { join } from "path"; import { win32 } from "node:path"; import { tmpdir } from "os"; +import pino from "pino"; import { __resetCheckoutShortstatCacheForTests, __resetPullRequestStatusCacheForTests, @@ -314,6 +316,38 @@ describe("checkout git utilities", () => { await expect(getCheckoutStatus(nonGitDir)).resolves.toEqual({ isGit: false }); }); + it.runIf( + process.platform !== "win32" && + existsSync("/dev/shm") && + statSync("/dev").dev !== statSync("/dev/shm").dev, + )("warns and reports a non-git directory at a filesystem boundary as non-git", async () => { + const nonGitDir = realpathSync.native(mkdtempSync("/dev/shm/checkout-git-boundary-test-")); + const records: unknown[] = []; + const logger = pino( + { level: "warn" }, + { + write(line: string) { + records.push(JSON.parse(line)); + }, + }, + ); + try { + await expect(getCheckoutStatus(nonGitDir, { logger })).resolves.toEqual({ isGit: false }); + expect(records).toEqual([ + expect.objectContaining({ + level: 40, + cwd: nonGitDir, + msg: "Git worktree discovery failed; treating directory as non-Git", + err: expect.objectContaining({ + message: expect.stringContaining("Stopping at filesystem boundary"), + }), + }), + ]); + } finally { + rmSync(nonGitDir, { recursive: true, force: true }); + } + }); + it("returns null for getCurrentBranch in a repo with no commits", async () => { const emptyRepo = join(tempDir, "empty-repo"); mkdirSync(emptyRepo, { recursive: true }); diff --git a/packages/server/src/utils/checkout-git.ts b/packages/server/src/utils/checkout-git.ts index 46a35f66e..331d2413f 100644 --- a/packages/server/src/utils/checkout-git.ts +++ b/packages/server/src/utils/checkout-git.ts @@ -780,7 +780,7 @@ export interface MergeFromBaseOptions { export interface CheckoutContext { paseoHome?: string; worktreesRoot?: string; - logger?: Pick; + logger?: Pick; facts?: CheckoutSnapshotFacts | null; } @@ -805,13 +805,6 @@ export type CheckoutSnapshotFacts = pullRequestLookupTarget: PullRequestStatusLookupTarget | null; }; -function isNotGitRepositoryError(error: unknown): boolean { - if (!(error instanceof Error)) { - return false; - } - return /not a git repository \(or any of the parent directories\): \.git/i.test(error.message); -} - async function requireGitRepo(cwd: string): Promise { try { await runGitCommand(["rev-parse", "--git-dir"], { cwd, envOverlay: READ_ONLY_GIT_ENV }); @@ -867,10 +860,12 @@ async function getWorktreeRoot(cwd: string, context?: CheckoutContext): Promise< }); return parseGitRevParsePath(stdout); } catch (error) { - if (isNotGitRepositoryError(error)) { - return null; - } - throw error; + // Git discovery is fail-open: keep the directory usable as non-Git and retain the diagnostic. + context?.logger?.warn( + { err: error, cwd }, + "Git worktree discovery failed; treating directory as non-Git", + ); + return null; } } @@ -1035,7 +1030,7 @@ async function getPaseoWorktreeForCwd( return { isPaseoOwnedWorktree: true, - worktreeRoot: knownWorktreeRoot ?? (await getWorktreeRoot(cwd)) ?? cwd, + worktreeRoot: knownWorktreeRoot ?? (await getWorktreeRoot(cwd, context)) ?? cwd, }; } @@ -2972,7 +2967,7 @@ export async function mergeToBase( } let normalizedBaseRef = baseRef; normalizedBaseRef = normalizeLocalBranchRefName(normalizedBaseRef); - const currentWorktreeRoot = (await getWorktreeRoot(cwd)) ?? cwd; + const currentWorktreeRoot = (await getWorktreeRoot(cwd, context)) ?? cwd; if (normalizedBaseRef === currentBranch) { return currentWorktreeRoot; } From 745e8afe45f04374e26e85e69aa47bc52a11b390 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Sat, 18 Jul 2026 11:54:49 +0200 Subject: [PATCH 19/38] Make keyboard shortcuts searchable (#2160) * feat(app): make keyboard shortcuts searchable * test(app): cover shortcut search empty state * fix(app): index shortcut modifier aliases --- packages/app/e2e/sidebar-help.spec.ts | 26 ++++++ .../components/keyboard-shortcuts-dialog.tsx | 83 ++++++++++++++++++- packages/app/src/i18n/resources/ar.ts | 1 + packages/app/src/i18n/resources/en.ts | 1 + packages/app/src/i18n/resources/es.ts | 1 + packages/app/src/i18n/resources/fr.ts | 1 + packages/app/src/i18n/resources/ja.ts | 1 + packages/app/src/i18n/resources/pt-BR.ts | 1 + packages/app/src/i18n/resources/ru.ts | 1 + packages/app/src/i18n/resources/zh-CN.ts | 1 + 10 files changed, 113 insertions(+), 4 deletions(-) diff --git a/packages/app/e2e/sidebar-help.spec.ts b/packages/app/e2e/sidebar-help.spec.ts index abf6919f1..129251516 100644 --- a/packages/app/e2e/sidebar-help.spec.ts +++ b/packages/app/e2e/sidebar-help.spec.ts @@ -81,6 +81,32 @@ test("opens support and release destinations", async ({ page }) => { await expectExternalPage(page, "sidebar-help-changelog", CHANGELOG_DESTINATION); }); +test("searches keyboard shortcuts from the sidebar help menu", async ({ page }) => { + await page.addInitScript(() => { + Object.defineProperty(navigator, "platform", { get: () => "MacIntel" }); + }); + await gotoAppShell(page); + await openHelpMenu(page); + await page.getByTestId("sidebar-help-shortcuts").click(); + + const dialog = page.getByTestId("keyboard-shortcuts-dialog"); + const search = page.getByPlaceholder("Search shortcuts"); + + await search.fill("command+n"); + await expect(dialog.getByText("New workspace", { exact: true })).toBeVisible(); + + await search.fill("interrupt"); + + await expect(dialog.getByText("Interrupt agent", { exact: true })).toBeVisible(); + await expect(dialog.getByText("New workspace", { exact: true })).toHaveCount(0); + + await search.fill("no matching shortcut"); + await expect(dialog.getByText("No results found", { exact: true })).toBeVisible(); + + await search.fill(""); + await expect(dialog.getByText("New workspace", { exact: true })).toBeVisible(); +}); + test("keeps diagnostics available from Settings after globalizing the sheet", async ({ page }) => { await gotoAppShell(page); await openSettings(page); diff --git a/packages/app/src/components/keyboard-shortcuts-dialog.tsx b/packages/app/src/components/keyboard-shortcuts-dialog.tsx index ea90e6ed1..770f1193d 100644 --- a/packages/app/src/components/keyboard-shortcuts-dialog.tsx +++ b/packages/app/src/components/keyboard-shortcuts-dialog.tsx @@ -1,4 +1,4 @@ -import { useCallback, useMemo } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { Text, View } from "react-native"; import { StyleSheet } from "react-native-unistyles"; @@ -6,25 +6,91 @@ import { getIsElectronRuntime } from "@/constants/layout"; import { AdaptiveModalSheet, type SheetHeader } from "@/components/adaptive-modal-sheet"; import { Shortcut } from "@/components/ui/shortcut"; import { useKeyboardShortcutsStore } from "@/stores/keyboard-shortcuts-store"; +import { formatShortcut } from "@/utils/format-shortcut"; import { getShortcutOs } from "@/utils/shortcut-platform"; import { buildKeyboardShortcutHelpSections } from "@/keyboard/keyboard-shortcuts"; const SNAP_POINTS: string[] = ["70%", "92%"]; +function shortcutSearchAliases(keys: string[], shortcutOs: "mac" | "non-mac"): string { + const aliases = keys.map((key) => { + if (shortcutOs === "mac") { + if (key === "mod" || key === "meta") return ["cmd", "command"]; + if (key === "alt") return ["alt", "option"]; + } else { + if (key === "mod" || key === "ctrl") return ["ctrl", "control"]; + if (key === "meta") return ["win", "windows"]; + } + return [key]; + }); + const combinations = aliases.reduce( + (prefixes, choices) => + prefixes.flatMap((prefix) => choices.map((choice) => [...prefix, choice])), + [[]], + ); + return combinations + .flatMap((combination) => [combination.join(" "), combination.join("+")]) + .join(" "); +} + export function KeyboardShortcutsDialog() { const { t } = useTranslation(); const open = useKeyboardShortcutsStore((s) => s.shortcutsDialogOpen); const setOpen = useKeyboardShortcutsStore((s) => s.setShortcutsDialogOpen); + const [query, setQuery] = useState(""); - const isMac = getShortcutOs() === "mac"; + const shortcutOs = getShortcutOs(); + const isMac = shortcutOs === "mac"; const isDesktopApp = getIsElectronRuntime(); const sections = useMemo( () => buildKeyboardShortcutHelpSections({ isMac, isDesktop: isDesktopApp }), [isDesktopApp, isMac], ); + const visibleSections = useMemo(() => { + const normalizedQuery = query.trim().toLocaleLowerCase(); + if (!normalizedQuery) return sections; + + return sections.flatMap((section) => { + const sectionTitle = t(section.titleKey); + if (sectionTitle.toLocaleLowerCase().includes(normalizedQuery)) { + return [section]; + } + + const rows = section.rows.filter((row) => { + const searchText = [ + t(row.labelKey), + row.noteKey ? t(row.noteKey) : row.note, + row.keys.join(" "), + formatShortcut(row.keys, shortcutOs), + shortcutSearchAliases(row.keys, shortcutOs), + ] + .filter(Boolean) + .join(" ") + .toLocaleLowerCase(); + return searchText.includes(normalizedQuery); + }); + + return rows.length > 0 ? [{ ...section, rows }] : []; + }); + }, [query, sections, shortcutOs, t]); + + useEffect(() => { + if (!open) setQuery(""); + }, [open]); const handleClose = useCallback(() => setOpen(false), [setOpen]); - const header = useMemo(() => ({ title: t("settings.shortcuts.dialogTitle") }), [t]); + const header = useMemo( + () => ({ + title: t("settings.shortcuts.dialogTitle"), + search: { + onChange: setQuery, + resetKey: Number(open), + placeholder: t("settings.shortcuts.searchPlaceholder"), + autoFocus: true, + }, + }), + [open, t], + ); return ( - {sections.map((section) => ( + {visibleSections.map((section) => ( {t(section.titleKey)} @@ -53,6 +119,9 @@ export function KeyboardShortcutsDialog() { ))} + {visibleSections.length === 0 ? ( + {t("common.empty.noResults")} + ) : null} ); @@ -102,4 +171,10 @@ const styles = StyleSheet.create((theme) => ({ rowShortcut: { alignSelf: "flex-start", }, + empty: { + paddingVertical: theme.spacing[6], + textAlign: "center", + fontSize: theme.fontSize.sm, + color: theme.colors.foregroundMuted, + }, })); diff --git a/packages/app/src/i18n/resources/ar.ts b/packages/app/src/i18n/resources/ar.ts index 38379dd5a..3062b5e9c 100644 --- a/packages/app/src/i18n/resources/ar.ts +++ b/packages/app/src/i18n/resources/ar.ts @@ -1713,6 +1713,7 @@ export const ar: TranslationResources = { }, shortcuts: { dialogTitle: "الاختصارات", + searchPlaceholder: "البحث في الاختصارات", unavailableOnMobile: "اختصارات لوحة المفاتيح متاحة فقط على سطح المكتب", capturePrompt: "اضغط على الاختصار...", actions: { diff --git a/packages/app/src/i18n/resources/en.ts b/packages/app/src/i18n/resources/en.ts index 641176146..d04201091 100644 --- a/packages/app/src/i18n/resources/en.ts +++ b/packages/app/src/i18n/resources/en.ts @@ -1724,6 +1724,7 @@ export const en = { }, shortcuts: { dialogTitle: "Shortcuts", + searchPlaceholder: "Search shortcuts", unavailableOnMobile: "Keyboard shortcuts are only available on desktop", capturePrompt: "Press shortcut...", actions: { diff --git a/packages/app/src/i18n/resources/es.ts b/packages/app/src/i18n/resources/es.ts index 3b1ebb85c..58221329b 100644 --- a/packages/app/src/i18n/resources/es.ts +++ b/packages/app/src/i18n/resources/es.ts @@ -1761,6 +1761,7 @@ export const es: TranslationResources = { }, shortcuts: { dialogTitle: "Atajos", + searchPlaceholder: "Buscar atajos", unavailableOnMobile: "Los atajos de teclado solo están disponibles en el escritorio", capturePrompt: "Presione el acceso directo...", actions: { diff --git a/packages/app/src/i18n/resources/fr.ts b/packages/app/src/i18n/resources/fr.ts index 5525f1be5..9083dc98c 100644 --- a/packages/app/src/i18n/resources/fr.ts +++ b/packages/app/src/i18n/resources/fr.ts @@ -1762,6 +1762,7 @@ export const fr: TranslationResources = { }, shortcuts: { dialogTitle: "Raccourcis", + searchPlaceholder: "Rechercher des raccourcis", unavailableOnMobile: "Les raccourcis clavier ne sont disponibles que sur le bureau", capturePrompt: "Appuyez sur le raccourci...", actions: { diff --git a/packages/app/src/i18n/resources/ja.ts b/packages/app/src/i18n/resources/ja.ts index 29000af6a..ff5c83e9b 100644 --- a/packages/app/src/i18n/resources/ja.ts +++ b/packages/app/src/i18n/resources/ja.ts @@ -1731,6 +1731,7 @@ export const ja: TranslationResources = { }, shortcuts: { dialogTitle: "ショートカット", + searchPlaceholder: "ショートカットを検索", unavailableOnMobile: "キーボードショートカットはデスクトップでのみ利用できます", capturePrompt: "ショートカットを押してください...", actions: { diff --git a/packages/app/src/i18n/resources/pt-BR.ts b/packages/app/src/i18n/resources/pt-BR.ts index f3db413ac..3d5f1f20b 100644 --- a/packages/app/src/i18n/resources/pt-BR.ts +++ b/packages/app/src/i18n/resources/pt-BR.ts @@ -1745,6 +1745,7 @@ export const ptBR: TranslationResources = { }, shortcuts: { dialogTitle: "Atalhos", + searchPlaceholder: "Pesquisar atalhos", unavailableOnMobile: "Atalhos de teclado estão disponíveis apenas no desktop", capturePrompt: "Pressione o atalho...", actions: { diff --git a/packages/app/src/i18n/resources/ru.ts b/packages/app/src/i18n/resources/ru.ts index e5cda739e..57f147682 100644 --- a/packages/app/src/i18n/resources/ru.ts +++ b/packages/app/src/i18n/resources/ru.ts @@ -1752,6 +1752,7 @@ export const ru: TranslationResources = { }, shortcuts: { dialogTitle: "Ярлыки", + searchPlaceholder: "Поиск сочетаний клавиш", unavailableOnMobile: "Сочетания клавиш доступны только на рабочем столе.", capturePrompt: "Нажмите ярлык...", actions: { diff --git a/packages/app/src/i18n/resources/zh-CN.ts b/packages/app/src/i18n/resources/zh-CN.ts index 65b8c970c..2feb4684a 100644 --- a/packages/app/src/i18n/resources/zh-CN.ts +++ b/packages/app/src/i18n/resources/zh-CN.ts @@ -1694,6 +1694,7 @@ export const zhCN: TranslationResources = { }, shortcuts: { dialogTitle: "快捷键", + searchPlaceholder: "搜索快捷键", unavailableOnMobile: "键盘快捷键仅在桌面端可用", capturePrompt: "按下快捷键...", actions: { From d9abac0f8e4675c808377d24610a345cd9ca1978 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Sat, 18 Jul 2026 12:43:14 +0200 Subject: [PATCH 20/38] chore(server): update Claude Agent SDK (#2189) --- package-lock.json | 88 ++++++++++++++++++------------------ packages/server/package.json | 2 +- 2 files changed, 45 insertions(+), 45 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3e04157f9..a146cb6f5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38051,7 +38051,7 @@ "version": "0.2.0-beta.1", "dependencies": { "@agentclientprotocol/sdk": "^0.17.1", - "@anthropic-ai/claude-agent-sdk": "^0.3.195", + "@anthropic-ai/claude-agent-sdk": "^0.3.214", "@anthropic-ai/sdk": "^0.104.2", "@getpaseo/client": "0.2.0-beta.1", "@getpaseo/highlight": "0.2.0-beta.1", @@ -38100,22 +38100,22 @@ } }, "packages/server/node_modules/@anthropic-ai/claude-agent-sdk": { - "version": "0.3.195", - "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.3.195.tgz", - "integrity": "sha512-FVmXu9pvOMbuBKWrF8YsYQdQ/upOpv5rS8lFAnFO5jbyXT/2hN7kEPd2vd2GJpaMvNcO/KptyQUK5AxjjTz3+w==", + "version": "0.3.214", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.3.214.tgz", + "integrity": "sha512-wt5ImwhU+p259Zt4K/Q9v5xVi6ruxYO5+KFICyJxnjs/QFEClAeSRqhcXx1J8jgGfatPW1faw09hkm66680UjA==", "license": "SEE LICENSE IN README.md", "engines": { "node": ">=18.0.0" }, "optionalDependencies": { - "@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.195", - "@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.195", - "@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.195", - "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.195", - "@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.195", - "@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.195", - "@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.195", - "@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.195" + "@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.214", + "@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.214", + "@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.214", + "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.214", + "@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.214", + "@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.214", + "@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.214", + "@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.214" }, "peerDependencies": { "@anthropic-ai/sdk": ">=0.93.0", @@ -38123,10 +38123,10 @@ "zod": "^4.0.0" } }, - "packages/server/node_modules/@anthropic-ai/claude-agent-sdk-darwin-arm64": { - "version": "0.3.195", - "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-arm64/-/claude-agent-sdk-darwin-arm64-0.3.195.tgz", - "integrity": "sha512-WIMM/8HRCLsTDHFTIwQvvE8WCA/oaMJtdQxsP7iNyfzIGwXbuOyU95V8vYIhZfaO2yaSpbBRncunq4CtR5H4ng==", + "packages/server/node_modules/@anthropic-ai/claude-agent-sdk/node_modules/@anthropic-ai/claude-agent-sdk-darwin-arm64": { + "version": "0.3.214", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-arm64/-/claude-agent-sdk-darwin-arm64-0.3.214.tgz", + "integrity": "sha512-vAAOeVtlXs3p7MpFVfvfu9ja32eaKtB+MDZnPxCbdzxJk5nofCHlNsHa1UJwXHOsP9lOnFYNIccYztsZ4DNaJA==", "cpu": [ "arm64" ], @@ -38136,10 +38136,10 @@ "darwin" ] }, - "packages/server/node_modules/@anthropic-ai/claude-agent-sdk-darwin-x64": { - "version": "0.3.195", - "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-x64/-/claude-agent-sdk-darwin-x64-0.3.195.tgz", - "integrity": "sha512-RY7DB+4LXosE0MJ+XELmakfPrDN1YX4lkk9CTDm28jGCVcESRz9kAEqbyaiC48dZcmN9V1NCLutzINGdcr1TBg==", + "packages/server/node_modules/@anthropic-ai/claude-agent-sdk/node_modules/@anthropic-ai/claude-agent-sdk-darwin-x64": { + "version": "0.3.214", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-x64/-/claude-agent-sdk-darwin-x64-0.3.214.tgz", + "integrity": "sha512-NTbV8U2yucxCWqEiDC7L0MUehNmd8x3Op8OGzLZRhMF3xmQBy2DxpXiNZgSwwKWNDC3HdgKmJM5ZBbT7XrF/0g==", "cpu": [ "x64" ], @@ -38149,10 +38149,10 @@ "darwin" ] }, - "packages/server/node_modules/@anthropic-ai/claude-agent-sdk-linux-arm64": { - "version": "0.3.195", - "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64/-/claude-agent-sdk-linux-arm64-0.3.195.tgz", - "integrity": "sha512-JuIq5Fnz/F1snl0aqi1gcuRZqPWoPNrL9dJ0DuievCxKkO8hnEz/Mmn5Zos7x1X8HE//ZnEvmQXoEQEZXonJew==", + "packages/server/node_modules/@anthropic-ai/claude-agent-sdk/node_modules/@anthropic-ai/claude-agent-sdk-linux-arm64": { + "version": "0.3.214", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64/-/claude-agent-sdk-linux-arm64-0.3.214.tgz", + "integrity": "sha512-KBCf+BlusG0ZcgvpjjHwv1kh+6WiR8vJbPjpR2udwkrtcQgKLN+24l+FeaQEzO2TL+ExCmM1KC4tNPvnPpy+tw==", "cpu": [ "arm64" ], @@ -38162,10 +38162,10 @@ "linux" ] }, - "packages/server/node_modules/@anthropic-ai/claude-agent-sdk-linux-arm64-musl": { - "version": "0.3.195", - "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64-musl/-/claude-agent-sdk-linux-arm64-musl-0.3.195.tgz", - "integrity": "sha512-ZmyBA/AFzhgutcxb7dbhCm6GTjJytwNYXTxJoKE2B3A409WCYccjMqeji6vCMNxyyfylglGo5D8dVMIxW9aoug==", + "packages/server/node_modules/@anthropic-ai/claude-agent-sdk/node_modules/@anthropic-ai/claude-agent-sdk-linux-arm64-musl": { + "version": "0.3.214", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64-musl/-/claude-agent-sdk-linux-arm64-musl-0.3.214.tgz", + "integrity": "sha512-i06wQRsmevE7spY1ryYfs+NP+xdZ1FwAyTjDaF0k/xG+cgtzZYghTFUemdYF3GVwgWgpcava9GiCFDT3DoHI6w==", "cpu": [ "arm64" ], @@ -38175,10 +38175,10 @@ "linux" ] }, - "packages/server/node_modules/@anthropic-ai/claude-agent-sdk-linux-x64": { - "version": "0.3.195", - "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64/-/claude-agent-sdk-linux-x64-0.3.195.tgz", - "integrity": "sha512-s1lNi1cL93luoqsItH+fNO4KpIhdkvnVhWGGQUQ/8ftwa2gfmcIQnOg1hG8Ks+KzeD3UUQ8L9YEVHVADnFI/9A==", + "packages/server/node_modules/@anthropic-ai/claude-agent-sdk/node_modules/@anthropic-ai/claude-agent-sdk-linux-x64": { + "version": "0.3.214", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64/-/claude-agent-sdk-linux-x64-0.3.214.tgz", + "integrity": "sha512-vqadSceJkBKHaTUszxI35uTuECGWtsHWK/mOwIJ4b9DUtYYySz6EJEk4gHg4ccutisJ/oRVCpFXHIKFb+osrKw==", "cpu": [ "x64" ], @@ -38188,10 +38188,10 @@ "linux" ] }, - "packages/server/node_modules/@anthropic-ai/claude-agent-sdk-linux-x64-musl": { - "version": "0.3.195", - "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64-musl/-/claude-agent-sdk-linux-x64-musl-0.3.195.tgz", - "integrity": "sha512-nf8Q/LauB+ZOC6QDjxNhbsvwUtYjKYnaWJLTYFwhkmsLujePnety1AtT/1ubaUoq5AM1j297DhMlYTasa79OUA==", + "packages/server/node_modules/@anthropic-ai/claude-agent-sdk/node_modules/@anthropic-ai/claude-agent-sdk-linux-x64-musl": { + "version": "0.3.214", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64-musl/-/claude-agent-sdk-linux-x64-musl-0.3.214.tgz", + "integrity": "sha512-948RstHDhs0E69h+2dKYWIAL1kcwEme4zvtgNUwP8XpKXzWOhrTKmd6MAokHn25zwfuSx5d+HE0XHfFH6R4hLg==", "cpu": [ "x64" ], @@ -38201,10 +38201,10 @@ "linux" ] }, - "packages/server/node_modules/@anthropic-ai/claude-agent-sdk-win32-arm64": { - "version": "0.3.195", - "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-arm64/-/claude-agent-sdk-win32-arm64-0.3.195.tgz", - "integrity": "sha512-hbkDE+xPIZzRWm+D+BKrH9uJH6USIZdDIlsyrIlGi3JFHoieYoA1vdUNyldSS9+F3ZqQtfPjr2Qy08IVB6akYA==", + "packages/server/node_modules/@anthropic-ai/claude-agent-sdk/node_modules/@anthropic-ai/claude-agent-sdk-win32-arm64": { + "version": "0.3.214", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-arm64/-/claude-agent-sdk-win32-arm64-0.3.214.tgz", + "integrity": "sha512-CEKlPFCPv+ee79utQMEDSsgeo2f27ulhNHpjrKIt1jXz5G04J7lAWN/QwvPDv9XaLBkWpyfqZWiPXlRcwuaO/w==", "cpu": [ "arm64" ], @@ -38214,10 +38214,10 @@ "win32" ] }, - "packages/server/node_modules/@anthropic-ai/claude-agent-sdk-win32-x64": { - "version": "0.3.195", - "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-x64/-/claude-agent-sdk-win32-x64-0.3.195.tgz", - "integrity": "sha512-av0piEB3X1Dzhpr8A+DqHVZ9y8s1jpn8enzwX0TKKUPBn5IqLTWC7wD6v66aoUgu4f+g4ThZirmDZA6shyPEZQ==", + "packages/server/node_modules/@anthropic-ai/claude-agent-sdk/node_modules/@anthropic-ai/claude-agent-sdk-win32-x64": { + "version": "0.3.214", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-x64/-/claude-agent-sdk-win32-x64-0.3.214.tgz", + "integrity": "sha512-cJMJfFoR9IWBZWnTtt9PnEm89hGOsZjoDNjOCEOF3+x+g/ANIXAjMJTcaQYv2HKh6MylWZ0AkxBASI+twkukaQ==", "cpu": [ "x64" ], diff --git a/packages/server/package.json b/packages/server/package.json index 408e012bc..456e78d1b 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -65,7 +65,7 @@ }, "dependencies": { "@agentclientprotocol/sdk": "^0.17.1", - "@anthropic-ai/claude-agent-sdk": "^0.3.195", + "@anthropic-ai/claude-agent-sdk": "^0.3.214", "@anthropic-ai/sdk": "^0.104.2", "@getpaseo/client": "0.2.0-beta.1", "@getpaseo/highlight": "0.2.0-beta.1", From 99da5736db11b51bd6493604e53444aff9c4e317 Mon Sep 17 00:00:00 2001 From: "paseo-ai[bot]" <266920839+paseo-ai[bot]@users.noreply.github.com> Date: Sat, 18 Jul 2026 10:51:30 +0000 Subject: [PATCH 21/38] fix: update lockfile signatures and Nix hash [skip ci] --- nix/npm-deps.hash | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nix/npm-deps.hash b/nix/npm-deps.hash index 2b54e735b..7d52fd51c 100644 --- a/nix/npm-deps.hash +++ b/nix/npm-deps.hash @@ -1 +1 @@ -sha256-ETZ7MRrBJ3ksGI1yK5oHvtLCDvxXXUcFH+W0myT1YOc= +sha256-MjAAaU+zU3m5fdE9rQrr4aA3eTDZztIXJQVXKbj73rY= From 98f6611362e39cbd351cb6456cc642bd0dd79f27 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Sat, 18 Jul 2026 13:49:32 +0200 Subject: [PATCH 22/38] Fix duplicated and out-of-order agent chat messages (#2185) * fix(app): keep agent timelines ordered during catch-up Projected catch-up pages can overlap live rows and arrive after optimistic prompts. Reconcile canonical projections before the existing stream reducer, and retain selective subscriptions briefly across quick view switches. * refactor(app): make optimistic catch-up consumption explicit * fix(app): preserve catch-up message order * fix(app): isolate delayed catch-up history * fix(app): anchor catch-up before live turns * fix(app): stage complete catch-up history * fix(app): keep timeline arrays Hermes-safe * refactor(app): preserve prompt positions during catch-up --- docs/timeline-sync.md | 30 + packages/app/src/contexts/session-context.tsx | 33 +- .../timeline/session-stream-reducers.test.ts | 622 +++++++++++++++++- .../src/timeline/session-stream-reducers.ts | 416 +++++++++++- .../src/timeline/viewed-timeline-sync.test.ts | 195 +++++- .../app/src/timeline/viewed-timeline-sync.ts | 96 ++- 6 files changed, 1325 insertions(+), 67 deletions(-) diff --git a/docs/timeline-sync.md b/docs/timeline-sync.md index 020ee0ed4..dfd56a46a 100644 --- a/docs/timeline-sync.md +++ b/docs/timeline-sync.md @@ -51,9 +51,39 @@ When a client resumes with a known cursor, it catches up after that cursor to co When a client resumes without a cursor, it fetches the latest tail page. +## Selective and legacy delivery + +The app chooses one delivery policy from `server_info.features.selectiveAgentTimeline`: + +- Selective daemons receive the union of agents visible in every pane. Additions subscribe and + catch up immediately. Removals stay subscribed for a short grace period so quick tab and pane + switches do not repeatedly unsubscribe and catch up. Backgrounding, disconnecting, and disposal + clear that grace immediately. +- Legacy daemons keep globally streaming agent timelines. Visibility still triggers the existing + authoritative catch-up, but the app does not issue selective-subscription RPCs. + +This policy is owned by `viewed-timeline-sync.ts`; downstream reducers do not branch on daemon +version. + +## Projected pages reconcile with live presentation + +A projected page is canonical state, not a sequence of live deltas. One projected item can overlap +rows already received live—for example, a tool call retained at its original display position while +its completion advances `seqEnd`, followed by a merged assistant message. The app uses +`sourceSeqRanges` to replace overlapping assistant and reasoning projections before applying the +remaining page through the existing stream reducer. It must not append full projected text to a +live prefix. + +Optimistic user prompts are presentation state rather than canonical history. Incremental catch-up +temporarily separates them, applies canonical entries, lets canonical user rows reconcile through +the existing optimistic-message rules, then restores any unmatched prompts after the caught-up +history. This keeps late history before a newly submitted prompt without duplicating an +acknowledged prompt. + ## Relevant code - Server live stream forwarding: `packages/server/src/server/session.ts` - App sync planning: `packages/app/src/timeline/timeline-sync-plan.ts` +- App viewed-agent synchronization: `packages/app/src/timeline/viewed-timeline-sync.ts` - App stream/timeline reducer: `packages/app/src/timeline/session-stream-reducers.ts` - Session wiring: `packages/app/src/contexts/session-context.tsx` diff --git a/packages/app/src/contexts/session-context.tsx b/packages/app/src/contexts/session-context.tsx index 31720b34d..abde80934 100644 --- a/packages/app/src/contexts/session-context.tsx +++ b/packages/app/src/contexts/session-context.tsx @@ -20,7 +20,11 @@ import { } from "@/timeline/session-stream-reducers"; import { useCreateFlowStore } from "@/stores/create-flow-store"; import { isTimelineCatchUpComplete } from "@/timeline/timeline-sync-plan"; -import { createViewedTimelineSync, type ViewedTimelineSync } from "@/timeline/viewed-timeline-sync"; +import { + createViewedTimelineSync, + type TimelineDeliveryMode, + type ViewedTimelineSync, +} from "@/timeline/viewed-timeline-sync"; import type { AgentAttachment, SessionOutboundMessage } from "@getpaseo/protocol/messages"; import { parseServerInfoStatusPayload } from "@getpaseo/protocol/messages"; import { @@ -79,6 +83,11 @@ interface BufferedAudioChunk { id: string; } +// COMPAT(selectiveAgentTimeline): added in v0.1.106, remove after 2027-01-12. +function getTimelineDeliveryMode(selectiveAgentTimeline?: boolean): TimelineDeliveryMode { + return selectiveAgentTimeline ? "selective" : "legacy"; +} + function decodeBase64Chunk(base64: string): Uint8Array { return Buffer.from(base64, "base64"); } @@ -712,7 +721,11 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider useEffect(() => { const setAgentInitializing = createSetAgentInitializing(serverId, setInitializingAgents); + const initialDeliveryMode = getTimelineDeliveryMode( + client.getLastServerInfoMessage()?.features?.selectiveAgentTimeline, + ); const sync = createViewedTimelineSync({ + initialDeliveryMode, setSubscription: (agentIds) => client.setAgentTimelineSubscription(agentIds), readCursor: (agentId) => useSessionStore.getState().sessions[serverId]?.agentTimelineCursor.get(agentId), @@ -723,7 +736,8 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider fetchPage: async (agentId, request) => { const session = useSessionStore.getState().sessions[serverId]; const initKey = getInitKey(serverId, agentId); - if (session?.agentAuthoritativeHistoryApplied.get(agentId) !== true) { + const shouldInitialize = session?.agentAuthoritativeHistoryApplied.get(agentId) !== true; + if (shouldInitialize) { if (!getInitDeferred(initKey)) { const deferred = createInitDeferred(initKey, request.direction ?? "tail"); void deferred.promise.catch(() => undefined); @@ -737,21 +751,23 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider } try { const page = await getHostRuntimeStore().fetchAgentTimeline(serverId, agentId, request); - if (getInitDeferred(initKey)) { + if (shouldInitialize && getInitDeferred(initKey)) { refreshAgentInitializationTimeout({ key: initKey, agentId, setAgentInitializing }); } return page; } catch (error) { - setAgentInitializing(agentId, false); - rejectInitDeferred(initKey, error instanceof Error ? error : new Error(String(error))); + if (shouldInitialize) { + setAgentInitializing(agentId, false); + rejectInitDeferred(initKey, error instanceof Error ? error : new Error(String(error))); + } throw error; } }, reportError: (error) => { console.warn("[Session] viewed timeline synchronization failed", { serverId, error }); }, - scheduleRetry: (retry) => { - const timeout = setTimeout(retry, 1_000); + schedule: (task, delayMs) => { + const timeout = setTimeout(task, delayMs); return () => clearTimeout(timeout); }, }); @@ -855,6 +871,9 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider if (message.type !== "status") return; const serverInfo = parseServerInfoStatusPayload(message.payload); if (serverInfo) { + viewedTimelineSyncRef.current?.setDeliveryMode( + getTimelineDeliveryMode(serverInfo.features?.selectiveAgentTimeline), + ); updateSessionServerInfo(serverId, { serverId: serverInfo.serverId, hostname: serverInfo.hostname, diff --git a/packages/app/src/timeline/session-stream-reducers.test.ts b/packages/app/src/timeline/session-stream-reducers.test.ts index e0026371c..05048e2d5 100644 --- a/packages/app/src/timeline/session-stream-reducers.test.ts +++ b/packages/app/src/timeline/session-stream-reducers.test.ts @@ -107,7 +107,10 @@ function makeStreamReducerEvent( }; } -function makeAssistantItem(text: string, id = `assistant-${text.length}`): StreamItem { +function makeAssistantItem( + text: string, + id = `assistant-${text.length}`, +): Extract { return { kind: "assistant_message", id, @@ -556,7 +559,7 @@ describe("processTimelineResponse", () => { item: { type: "user_message", text: "sent while catching up", - messageId: "canonical-after", + messageId: "optimistic-after", }, }, ], @@ -565,10 +568,125 @@ describe("processTimelineResponse", () => { const userMessages = result.tail.filter((item) => item.kind === "user_message"); expect(userMessages).toHaveLength(1); - expect(userMessages[0]?.id).toBe("canonical-after"); + expect(userMessages[0]?.id).toBe("optimistic-after"); expect(userMessages[0]?.optimistic).toBeUndefined(); }); + it("reconciles multiple optimistic user messages in canonical order", () => { + const result = processTimelineResponse({ + ...baseTimelineInput, + currentTail: [ + makeOptimisticUserMessage("first prompt", "optimistic-first"), + makeOptimisticUserMessage("second prompt", "optimistic-second"), + ], + currentCursor: { epoch: "epoch-1", startSeq: 1, endSeq: 1 }, + payload: { + ...baseTimelineInput.payload, + epoch: "epoch-1", + startCursor: { seq: 2 }, + endCursor: { seq: 3 }, + entries: [ + { + ...makeTimelineEntry(2, "first prompt", "user_message"), + item: { type: "user_message", text: "first prompt", messageId: "optimistic-first" }, + }, + { + ...makeTimelineEntry(3, "second prompt", "user_message"), + item: { + type: "user_message", + text: "second prompt", + messageId: "optimistic-second", + }, + }, + ], + }, + }); + + expect( + result.tail + .filter((item) => item.kind === "user_message") + .map((item) => ({ id: item.id, text: item.text, optimistic: item.optimistic })), + ).toEqual([ + { id: "optimistic-first", text: "first prompt", optimistic: undefined }, + { id: "optimistic-second", text: "second prompt", optimistic: undefined }, + ]); + }); + + it("keeps a tail optimistic prompt before a reconciled live assistant head", () => { + const prompt = makeOptimisticUserMessage("new prompt", "optimistic-new-prompt"); + + const result = processTimelineResponse({ + ...baseTimelineInput, + currentTail: [prompt], + currentHead: [ + { + ...makeAssistantItem("Hel", "answer-1"), + messageId: "answer-1", + }, + ], + currentCursor: { epoch: "epoch-1", startSeq: 1, endSeq: 2 }, + payload: { + ...baseTimelineInput.payload, + direction: "after", + epoch: "epoch-1", + startCursor: { seq: 3 }, + endCursor: { seq: 3 }, + entries: [ + { + ...makeTimelineEntry(2, "Hello", "assistant_message", 3), + sourceSeqRanges: [{ startSeq: 2, endSeq: 3 }], + item: { + type: "assistant_message", + text: "Hello", + messageId: "answer-1", + }, + }, + ], + }, + }); + + expect( + [...result.tail, ...result.head] + .filter((item) => item.kind === "assistant_message" || item.kind === "user_message") + .map((item) => item.text), + ).toEqual(["new prompt", "Hello"]); + }); + + it("keeps a tail optimistic prompt before a live head flushed by catch-up", () => { + const prompt = makeOptimisticUserMessage("new prompt", "optimistic-new-prompt"); + + const result = processTimelineResponse({ + ...baseTimelineInput, + currentTail: [prompt], + currentHead: [ + { + ...makeAssistantItem("Live response", "answer-1"), + messageId: "answer-1", + }, + ], + currentCursor: { epoch: "epoch-1", startSeq: 1, endSeq: 2 }, + payload: { + ...baseTimelineInput.payload, + direction: "after", + epoch: "epoch-1", + startCursor: { seq: 3 }, + endCursor: { seq: 3 }, + entries: [ + makeToolCallTimelineEntry(3, "call-1", "running", { + type: "read", + filePath: "/tmp/example.ts", + }), + ], + }, + }); + + expect( + [...result.tail, ...result.head] + .filter((item) => item.kind === "assistant_message" || item.kind === "user_message") + .map((item) => item.text), + ).toEqual(["new prompt", "Live response"]); + }); + it("keeps an active assistant head live when an incremental fetch accepts same-turn assistant text", () => { const existingCursor: TimelineCursor = { epoch: "epoch-1", @@ -597,6 +715,504 @@ describe("processTimelineResponse", () => { }); }); + it("does not replay an assistant prefix when catch-up completes an earlier tool call", () => { + const live = processAgentStreamEvents({ + events: [ + makeStreamReducerEvent(makeToolCallTimelineEvent("call-1"), 1), + makeStreamReducerEvent(makeAssistantTimelineEvent("Hel", "answer-1"), 2), + ], + currentTail: [], + currentHead: [], + currentCursor: undefined, + currentAgent: null, + }); + + const result = processTimelineResponse({ + ...baseTimelineInput, + currentTail: live.tail, + currentHead: live.head, + currentCursor: live.cursor ?? undefined, + payload: { + ...baseTimelineInput.payload, + epoch: "epoch-1", + startCursor: { seq: 3 }, + endCursor: { seq: 4 }, + entries: [ + { + ...makeToolCallTimelineEntry(1, "call-1", "completed", { + type: "read", + filePath: "/tmp/example.ts", + }), + seqEnd: 4, + sourceSeqRanges: [ + { startSeq: 1, endSeq: 1 }, + { startSeq: 4, endSeq: 4 }, + ], + }, + { + ...makeTimelineEntry(2, "Hello", "assistant_message", 3), + sourceSeqRanges: [{ startSeq: 2, endSeq: 3 }], + item: { + type: "assistant_message", + text: "Hello", + messageId: "answer-1", + }, + }, + ], + }, + }); + + expect(getAssistantTexts([...result.tail, ...result.head])).toEqual(["Hello"]); + }); + + it("reconciles an identified projection with an overlapping anonymous live prefix", () => { + const result = processTimelineResponse({ + ...baseTimelineInput, + currentHead: [makeAssistantItem("Hel")], + currentCursor: { epoch: "epoch-1", startSeq: 1, endSeq: 2 }, + payload: { + ...baseTimelineInput.payload, + epoch: "epoch-1", + startCursor: { seq: 3 }, + endCursor: { seq: 3 }, + entries: [ + { + ...makeTimelineEntry(2, "Hello", "assistant_message", 3), + sourceSeqRanges: [{ startSeq: 2, endSeq: 3 }], + item: { + type: "assistant_message", + text: "Hello", + messageId: "answer-1", + }, + }, + ], + }, + }); + + const assistants = [...result.tail, ...result.head].filter( + (item) => item.kind === "assistant_message", + ); + expect(assistants).toHaveLength(1); + expect(assistants[0]).toMatchObject({ text: "Hello", messageId: "answer-1" }); + }); + + it("replaces every promoted assistant block when reconciling a projected message", () => { + const live = processAgentStreamEvents({ + events: [makeStreamReducerEvent(makeAssistantTimelineEvent("First paragraph.\n\nSec"), 2)], + currentTail: [], + currentHead: [], + currentCursor: { epoch: "epoch-1", startSeq: 1, endSeq: 1 }, + currentAgent: null, + }); + expect(getAssistantTexts(live.tail)).toHaveLength(1); + expect(getAssistantTexts(live.head)).toHaveLength(1); + + const result = processTimelineResponse({ + ...baseTimelineInput, + currentTail: live.tail, + currentHead: live.head, + currentCursor: live.cursor ?? undefined, + payload: { + ...baseTimelineInput.payload, + direction: "after", + epoch: "epoch-1", + startCursor: { seq: 3 }, + endCursor: { seq: 3 }, + entries: [ + { + ...makeTimelineEntry( + 2, + "First paragraph.\n\nSecond paragraph.", + "assistant_message", + 3, + ), + sourceSeqRanges: [{ startSeq: 2, endSeq: 3 }], + item: { + type: "assistant_message", + text: "First paragraph.\n\nSecond paragraph.", + }, + }, + ], + }, + }); + + expect(getAssistantTexts([...result.tail, ...result.head])).toEqual([ + "First paragraph.\n\nSecond paragraph.", + ]); + }); + + it("does not replay a reasoning prefix when catch-up completes an earlier tool call", () => { + const live = processAgentStreamEvents({ + events: [ + makeStreamReducerEvent(makeToolCallTimelineEvent("call-1"), 1), + makeStreamReducerEvent(makeTimelineEvent("Thi", "reasoning"), 2), + ], + currentTail: [], + currentHead: [], + currentCursor: undefined, + currentAgent: null, + }); + + const result = processTimelineResponse({ + ...baseTimelineInput, + currentTail: live.tail, + currentHead: live.head, + currentCursor: live.cursor ?? undefined, + payload: { + ...baseTimelineInput.payload, + epoch: "epoch-1", + startCursor: { seq: 3 }, + endCursor: { seq: 4 }, + entries: [ + { + ...makeToolCallTimelineEntry(1, "call-1", "completed", { + type: "read", + filePath: "/tmp/example.ts", + }), + seqEnd: 4, + sourceSeqRanges: [ + { startSeq: 1, endSeq: 1 }, + { startSeq: 4, endSeq: 4 }, + ], + }, + { + ...makeTimelineEntry(2, "Thinking", "reasoning", 3), + sourceSeqRanges: [{ startSeq: 2, endSeq: 3 }], + }, + ], + }, + }); + + const thoughts = [...result.tail, ...result.head].filter((item) => item.kind === "thought"); + expect(thoughts).toHaveLength(1); + expect(thoughts[0]?.text).toBe("Thinking"); + }); + + it("keeps delayed catch-up history before a newly submitted prompt", () => { + const prompt = makeOptimisticUserMessage("New prompt", "new-prompt"); + + const result = processTimelineResponse({ + ...baseTimelineInput, + currentTail: [makeAssistantItem("Earlier answer", "earlier-answer"), prompt], + currentHead: [], + currentCursor: { epoch: "epoch-1", startSeq: 1, endSeq: 2 }, + payload: { + ...baseTimelineInput.payload, + epoch: "epoch-1", + startCursor: { seq: 3 }, + endCursor: { seq: 3 }, + entries: [makeTimelineEntry(3, "Missed answer")], + }, + }); + + expect( + [...result.tail, ...result.head] + .filter((item) => item.kind === "assistant_message" || item.kind === "user_message") + .map((item) => item.text), + ).toEqual(["Earlier answer", "Missed answer", "New prompt"]); + }); + + it("keeps delayed catch-up history between a live head and its unmatched head prompt", () => { + const prompt = makeOptimisticUserMessage("New prompt", "new-prompt"); + + const result = processTimelineResponse({ + ...baseTimelineInput, + currentTail: [], + currentHead: [makeAssistantItem("Live answer", "live-answer"), prompt], + currentCursor: { epoch: "epoch-1", startSeq: 1, endSeq: 2 }, + payload: { + ...baseTimelineInput.payload, + epoch: "epoch-1", + startCursor: { seq: 3 }, + endCursor: { seq: 3 }, + entries: [ + makeToolCallTimelineEntry(3, "missed-call", "completed", { + type: "read", + filePath: "/tmp/missed.ts", + }), + ], + }, + }); + + expect([...result.tail, ...result.head].map((item) => item.kind)).toEqual([ + "assistant_message", + "tool_call", + "user_message", + ]); + }); + + it("keeps delayed catch-up history between a live head and its acknowledged head prompt", () => { + const prompt = makeOptimisticUserMessage("New prompt", "new-prompt"); + + const result = processTimelineResponse({ + ...baseTimelineInput, + currentTail: [], + currentHead: [makeAssistantItem("Live answer", "live-answer"), prompt], + currentCursor: { epoch: "epoch-1", startSeq: 1, endSeq: 2 }, + payload: { + ...baseTimelineInput.payload, + epoch: "epoch-1", + startCursor: { seq: 3 }, + endCursor: { seq: 4 }, + entries: [ + makeToolCallTimelineEntry(3, "missed-call", "completed", { + type: "read", + filePath: "/tmp/missed.ts", + }), + { + ...makeTimelineEntry(4, "New prompt", "user_message"), + item: { + type: "user_message", + text: "New prompt", + messageId: "new-prompt", + }, + }, + ], + }, + }); + + expect([...result.tail, ...result.head].map((item) => item.kind)).toEqual([ + "assistant_message", + "tool_call", + "user_message", + ]); + expect( + [...result.tail, ...result.head] + .filter((item) => item.kind === "user_message") + .map((item) => item.optimistic), + ).toEqual([undefined]); + }); + + it("keeps unrelated delayed history before the prompt and its live response", () => { + const prompt = makeOptimisticUserMessage("New prompt", "new-prompt"); + + const result = processTimelineResponse({ + ...baseTimelineInput, + currentTail: [makeAssistantItem("Earlier answer", "earlier-answer"), prompt], + currentHead: [ + { + ...makeAssistantItem("Live response", "live-response"), + messageId: "live-response", + }, + ], + currentCursor: { epoch: "epoch-1", startSeq: 1, endSeq: 2 }, + payload: { + ...baseTimelineInput.payload, + epoch: "epoch-1", + startCursor: { seq: 3 }, + endCursor: { seq: 4 }, + entries: [ + makeTimelineEntry(3, "Missed answer"), + { + ...makeTimelineEntry(4, "Remote prompt", "user_message"), + item: { + type: "user_message", + text: "Remote prompt", + messageId: "remote-prompt", + }, + }, + ], + }, + }); + + expect( + [...result.tail, ...result.head] + .filter((item) => item.kind === "assistant_message" || item.kind === "user_message") + .map((item) => item.text), + ).toEqual(["Earlier answer", "Missed answer", "Remote prompt", "New prompt", "Live response"]); + }); + + it("keeps delayed history before a prompt whose live answer has promoted blocks", () => { + const prompt = makeOptimisticUserMessage("New prompt", "new-prompt"); + const live = processAgentStreamEvents({ + events: [ + makeStreamReducerEvent( + makeAssistantTimelineEvent("First paragraph.\n\nSecond paragraph", "live-response"), + 2, + ), + ], + currentTail: [prompt], + currentHead: [], + currentCursor: { epoch: "epoch-1", startSeq: 1, endSeq: 1 }, + currentAgent: null, + }); + + const result = processTimelineResponse({ + ...baseTimelineInput, + currentTail: live.tail, + currentHead: live.head, + currentCursor: live.cursor ?? undefined, + payload: { + ...baseTimelineInput.payload, + epoch: "epoch-1", + startCursor: { seq: 3 }, + endCursor: { seq: 3 }, + entries: [makeTimelineEntry(3, "Missed answer")], + }, + }); + + expect( + [...result.tail, ...result.head] + .filter((item) => item.kind === "assistant_message" || item.kind === "user_message") + .map((item) => item.text), + ).toEqual(["Missed answer", "New prompt", "First paragraph.", "Second paragraph"]); + }); + + it("keeps delayed tool history before a prompt whose live answer has promoted blocks", () => { + const prompt = makeOptimisticUserMessage("New prompt", "new-prompt"); + const live = processAgentStreamEvents({ + events: [ + makeStreamReducerEvent( + makeAssistantTimelineEvent("First paragraph.\n\nSecond paragraph", "live-response"), + 2, + ), + ], + currentTail: [prompt], + currentHead: [], + currentCursor: { epoch: "epoch-1", startSeq: 1, endSeq: 1 }, + currentAgent: null, + }); + + const result = processTimelineResponse({ + ...baseTimelineInput, + currentTail: live.tail, + currentHead: live.head, + currentCursor: live.cursor ?? undefined, + payload: { + ...baseTimelineInput.payload, + epoch: "epoch-1", + startCursor: { seq: 3 }, + endCursor: { seq: 3 }, + entries: [ + makeToolCallTimelineEntry(3, "missed-call", "completed", { + type: "read", + filePath: "/tmp/missed.ts", + }), + ], + }, + }); + + expect([...result.tail, ...result.head].map((item) => item.kind)).toEqual([ + "tool_call", + "user_message", + "assistant_message", + "assistant_message", + ]); + }); + + it("matches a local optimistic prompt after an unrelated remote user row", () => { + const prompt = makeOptimisticUserMessage("Local prompt", "local-prompt"); + + const result = processTimelineResponse({ + ...baseTimelineInput, + currentTail: [prompt], + currentCursor: { epoch: "epoch-1", startSeq: 1, endSeq: 1 }, + payload: { + ...baseTimelineInput.payload, + epoch: "epoch-1", + startCursor: { seq: 2 }, + endCursor: { seq: 3 }, + entries: [ + { + ...makeTimelineEntry(2, "Remote prompt", "user_message"), + item: { + type: "user_message", + text: "Remote prompt", + messageId: "remote-prompt", + }, + }, + { + ...makeTimelineEntry(3, "Local prompt", "user_message"), + item: { + type: "user_message", + text: "Local prompt", + messageId: "local-prompt", + }, + }, + ], + }, + }); + + expect( + result.tail + .filter((item) => item.kind === "user_message") + .map((item) => ({ text: item.text, optimistic: item.optimistic })), + ).toEqual([ + { text: "Remote prompt", optimistic: undefined }, + { text: "Local prompt", optimistic: undefined }, + ]); + }); + + it("keeps an unmatched optimistic prompt when catch-up contains only a remote user row", () => { + const prompt = makeOptimisticUserMessage("Local prompt", "local-prompt"); + + const result = processTimelineResponse({ + ...baseTimelineInput, + currentTail: [prompt], + currentCursor: { epoch: "epoch-1", startSeq: 1, endSeq: 1 }, + payload: { + ...baseTimelineInput.payload, + epoch: "epoch-1", + startCursor: { seq: 2 }, + endCursor: { seq: 2 }, + entries: [ + { + ...makeTimelineEntry(2, "Remote prompt", "user_message"), + item: { + type: "user_message", + text: "Remote prompt", + messageId: "remote-prompt", + }, + }, + ], + }, + }); + + expect( + result.tail + .filter((item) => item.kind === "user_message") + .map((item) => ({ text: item.text, optimistic: item.optimistic })), + ).toEqual([ + { text: "Remote prompt", optimistic: undefined }, + { text: "Local prompt", optimistic: true }, + ]); + }); + + it("does not match equal prompt text when canonical message ids differ", () => { + const prompt = makeOptimisticUserMessage("continue", "local-prompt"); + + const result = processTimelineResponse({ + ...baseTimelineInput, + currentTail: [prompt], + currentCursor: { epoch: "epoch-1", startSeq: 1, endSeq: 1 }, + payload: { + ...baseTimelineInput.payload, + epoch: "epoch-1", + startCursor: { seq: 2 }, + endCursor: { seq: 2 }, + entries: [ + { + ...makeTimelineEntry(2, "continue", "user_message"), + item: { + type: "user_message", + text: "continue", + messageId: "remote-prompt", + }, + }, + ], + }, + }); + + expect( + result.tail + .filter((item) => item.kind === "user_message") + .map((item) => ({ id: item.id, optimistic: item.optimistic })), + ).toEqual([ + { id: "remote-prompt", optimistic: undefined }, + { id: "local-prompt", optimistic: true }, + ]); + }); + it("hydrates a fetched in-progress tool call as one item and streams the next update on top", () => { const fetched = processTimelineResponse({ ...baseTimelineInput, diff --git a/packages/app/src/timeline/session-stream-reducers.ts b/packages/app/src/timeline/session-stream-reducers.ts index c10f91c8b..854832762 100644 --- a/packages/app/src/timeline/session-stream-reducers.ts +++ b/packages/app/src/timeline/session-stream-reducers.ts @@ -2,9 +2,10 @@ import type { AgentStreamEventPayload } from "@getpaseo/protocol/messages"; import type { AgentLifecycleStatus } from "@getpaseo/protocol/agent-lifecycle"; import type { Agent } from "@/stores/session-store"; import { useSessionStore } from "@/stores/session-store"; -import type { StreamItem, UserMessageItem } from "@/types/stream"; +import type { AssistantMessageItem, StreamItem, UserMessageItem } from "@/types/stream"; import { applyStreamEvent, + flushHeadToTail, hydrateStreamState, isAgentToolCallItem, mergeAgentToolCallItem, @@ -544,6 +545,378 @@ function replaceLiveAssistantWithProjectedText(params: { return next; } +interface OptimisticUserMessagePosition { + item: UserMessageItem; + placement: "tail" | "head"; + index: number; +} + +function findOptimisticUserMessage(params: { + tail: StreamItem[]; + head: StreamItem[]; +}): OptimisticUserMessagePosition | null { + const tailIndex = params.tail.findIndex( + (item) => item.kind === "user_message" && item.optimistic, + ); + const tailItem = params.tail[tailIndex]; + if (tailItem?.kind === "user_message") { + return { item: tailItem, placement: "tail", index: tailIndex }; + } + + const headIndex = params.head.findIndex( + (item) => item.kind === "user_message" && item.optimistic, + ); + const headItem = params.head[headIndex]; + return headItem?.kind === "user_message" + ? { item: headItem, placement: "head", index: headIndex } + : null; +} + +function replaceOptimisticUserMessage(params: { + tail: StreamItem[]; + head: StreamItem[]; + position: OptimisticUserMessagePosition; + precedingItems: StreamItem[]; + replacement: StreamItem[]; +}): { tail: StreamItem[]; head: StreamItem[] } { + const { position } = params; + const items = [...params.precedingItems, ...params.replacement]; + if (position.placement === "tail") { + return { + tail: [ + ...params.tail.slice(0, position.index), + ...items, + ...params.tail.slice(position.index + 1), + ], + head: params.head, + }; + } + return { + tail: params.tail, + head: [ + ...params.head.slice(0, position.index), + ...items, + ...params.head.slice(position.index + 1), + ], + }; +} + +function reconcileOverlappingProjectedAssistant(params: { + tail: StreamItem[]; + head: StreamItem[]; + unit: TimelineUnit; + epoch: string; + currentEndSeq: number; +}): { tail: StreamItem[]; head: StreamItem[]; reconciled: boolean } { + const { unit } = params; + if ( + unit.event.type !== "timeline" || + unit.event.item.type !== "assistant_message" || + !unit.sourceSeqRanges.some( + (range) => range.startSeq <= params.currentEndSeq && range.endSeq > params.currentEndSeq, + ) + ) { + return { tail: params.tail, head: params.head, reconciled: false }; + } + + const projectedText = unit.event.item.text; + const projectedMessageId = unit.event.item.messageId; + const matches = (item: StreamItem) => { + if (item.kind !== "assistant_message") return false; + if (projectedMessageId && item.messageId) return item.messageId === projectedMessageId; + return projectedText.startsWith(item.text); + }; + const findMatch = (items: StreamItem[]) => { + const index = items.findLastIndex(matches); + const current = items[index]; + return current?.kind === "assistant_message" ? { current, index } : null; + }; + + const headMatch = findMatch(params.head); + const tailMatch = headMatch ? null : findMatch(params.tail); + const match = headMatch ?? tailMatch; + if (!match) { + return { tail: params.tail, head: params.head, reconciled: false }; + } + + const blockGroupId = match.current.blockGroupId; + const messageId = projectedMessageId ?? match.current.messageId; + const replacement: AssistantMessageItem = { + kind: "assistant_message", + id: blockGroupId ?? match.current.id, + ...(messageId !== undefined ? { messageId } : {}), + text: projectedText, + timestamp: unit.timestamp, + timelineCursor: { epoch: params.epoch, seq: unit.seqEnd }, + }; + const belongsToBlockGroup = (item: StreamItem) => + blockGroupId !== undefined && + item.kind === "assistant_message" && + item.blockGroupId === blockGroupId; + const removeBlockGroup = (items: StreamItem[]) => + blockGroupId !== undefined ? items.filter((item) => !belongsToBlockGroup(item)) : items; + const replaceMatch = (items: StreamItem[], index: number) => { + if (!blockGroupId) { + const next = [...items]; + next[index] = replacement; + return next; + } + const next: StreamItem[] = []; + let inserted = false; + for (const item of items) { + if (!belongsToBlockGroup(item)) { + next.push(item); + } else if (!inserted) { + next.push(replacement); + inserted = true; + } + } + return next; + }; + + if (headMatch) { + return { + tail: removeBlockGroup(params.tail), + head: replaceMatch(params.head, headMatch.index), + reconciled: true, + }; + } + return { + tail: replaceMatch(params.tail, match.index), + head: removeBlockGroup(params.head), + reconciled: true, + }; +} + +function reconcileOverlappingProjectedReasoning(params: { + tail: StreamItem[]; + head: StreamItem[]; + unit: TimelineUnit; + currentEndSeq: number; +}): { tail: StreamItem[]; head: StreamItem[]; reconciled: boolean } { + const { unit } = params; + if ( + unit.event.type !== "timeline" || + unit.event.item.type !== "reasoning" || + !unit.sourceSeqRanges.some( + (range) => range.startSeq <= params.currentEndSeq && range.endSeq > params.currentEndSeq, + ) + ) { + return { tail: params.tail, head: params.head, reconciled: false }; + } + + const projectedText = unit.event.item.text; + const replaceIn = (items: StreamItem[]): StreamItem[] | null => { + const index = items.findLastIndex( + (item) => item.kind === "thought" && projectedText.startsWith(item.text), + ); + const current = items[index]; + if (!current || current.kind !== "thought") return null; + const next = [...items]; + next[index] = { + ...current, + text: projectedText, + timestamp: unit.timestamp, + status: "loading", + }; + return next; + }; + + const nextHead = replaceIn(params.head); + if (nextHead) return { tail: params.tail, head: nextHead, reconciled: true }; + const nextTail = replaceIn(params.tail); + return nextTail + ? { tail: nextTail, head: params.head, reconciled: true } + : { tail: params.tail, head: params.head, reconciled: false }; +} + +function reconcileOverlappingProjectedStreamItems(params: { + tail: StreamItem[]; + head: StreamItem[]; + units: TimelineUnit[]; + epoch: string; + currentEndSeq: number | undefined; +}): { tail: StreamItem[]; head: StreamItem[]; reconciledUnits: Set } { + let tail = params.tail; + let head = params.head; + const reconciledUnits = new Set(); + if (params.currentEndSeq === undefined) return { tail, head, reconciledUnits }; + + for (const unit of params.units) { + let reconciled = reconcileOverlappingProjectedAssistant({ + tail, + head, + unit, + epoch: params.epoch, + currentEndSeq: params.currentEndSeq, + }); + if (!reconciled.reconciled) { + reconciled = reconcileOverlappingProjectedReasoning({ + tail, + head, + unit, + currentEndSeq: params.currentEndSeq, + }); + } + tail = reconciled.tail; + head = reconciled.head; + if (reconciled.reconciled) reconciledUnits.add(unit); + } + return { tail, head, reconciledUnits }; +} + +function matchesOptimisticUserMessage(params: { + unit: TimelineUnit; + optimistic: UserMessageItem; +}): boolean { + const { event } = params.unit; + if (event.type !== "timeline" || event.item.type !== "user_message") { + return false; + } + if (event.item.messageId !== undefined) { + return event.item.messageId === params.optimistic.id; + } + return event.item.text.length > 0 && event.item.text === params.optimistic.text; +} + +function acknowledgeOptimisticUserMessage(params: { + tail: StreamItem[]; + head: StreamItem[]; + unit: TimelineUnit; + epoch: string; + optimistic: OptimisticUserMessagePosition; + precedingItems: StreamItem[]; +}): { tail: StreamItem[]; head: StreamItem[] } { + const { event, timestamp, seqEnd } = params.unit; + const timelineCursor = { epoch: params.epoch, seq: seqEnd }; + const acknowledged = reduceStreamUpdate([params.optimistic.item], event, timestamp, { + source: "canonical", + timelineCursor, + }); + return replaceOptimisticUserMessage({ + tail: params.tail, + head: params.head, + position: params.optimistic, + precedingItems: params.precedingItems, + replacement: acknowledged, + }); +} + +function applyCanonicalForwardUnit(params: { + tail: StreamItem[]; + head: StreamItem[]; + unit: TimelineUnit; + epoch: string; +}): { tail: StreamItem[]; head: StreamItem[] } { + const { event, timestamp, seqEnd } = params.unit; + const timelineCursor = { epoch: params.epoch, seq: seqEnd }; + if (params.head.length === 0) { + return { + tail: reduceStreamUpdate(params.tail, event, timestamp, { + source: "canonical", + timelineCursor, + }), + head: params.head, + }; + } + const replacedHead = replaceLiveAssistantWithProjectedText({ + head: params.head, + event, + timestamp, + timelineCursor, + }); + if (replacedHead) return { tail: params.tail, head: replacedHead }; + + const applied = applyStreamEvent({ + tail: params.tail, + head: params.head, + event, + timestamp, + source: "canonical", + timelineCursor, + }); + return { tail: applied.tail, head: applied.head }; +} + +function applyAcceptedForwardTimelineUnits(params: { + units: TimelineUnit[]; + epoch: string; + currentTail: StreamItem[]; + currentHead: StreamItem[]; + currentEndSeq: number | undefined; +}): { tail: StreamItem[]; head: StreamItem[] } { + const reconciled = reconcileOverlappingProjectedStreamItems({ + tail: params.currentTail, + head: params.currentHead, + units: params.units, + epoch: params.epoch, + currentEndSeq: params.currentEndSeq, + }); + let tail = reconciled.tail; + let head = reconciled.head; + let delayedHistoryTail: StreamItem[] = []; + let delayedHistoryHead: StreamItem[] = []; + + for (const unit of params.units) { + if (reconciled.reconciledUnits.has(unit)) continue; + const nextOptimistic = findOptimisticUserMessage({ tail, head }); + if (nextOptimistic && matchesOptimisticUserMessage({ unit, optimistic: nextOptimistic.item })) { + const precedingItems = flushHeadToTail(delayedHistoryTail, delayedHistoryHead); + delayedHistoryTail = []; + delayedHistoryHead = []; + const applied = acknowledgeOptimisticUserMessage({ + tail, + head, + unit, + epoch: params.epoch, + optimistic: nextOptimistic, + precedingItems, + }); + tail = applied.tail; + head = applied.head; + continue; + } + if (nextOptimistic) { + if ( + unit.event.type === "timeline" && + (unit.event.item.type === "assistant_message" || unit.event.item.type === "reasoning") + ) { + delayedHistoryHead = reduceStreamUpdate(delayedHistoryHead, unit.event, unit.timestamp, { + source: "canonical", + timelineCursor: { epoch: params.epoch, seq: unit.seqEnd }, + }); + continue; + } + const applied = applyStreamEvent({ + tail: delayedHistoryTail, + head: delayedHistoryHead, + event: unit.event, + timestamp: unit.timestamp, + source: "canonical", + timelineCursor: { epoch: params.epoch, seq: unit.seqEnd }, + }); + delayedHistoryTail = applied.tail; + delayedHistoryHead = applied.head; + continue; + } + const applied = applyCanonicalForwardUnit({ tail, head, unit, epoch: params.epoch }); + tail = applied.tail; + head = applied.head; + } + + const remainingOptimistic = findOptimisticUserMessage({ tail, head }); + if (!remainingOptimistic) return { tail, head }; + const precedingItems = flushHeadToTail(delayedHistoryTail, delayedHistoryHead); + if (precedingItems.length === 0) return { tail, head }; + return replaceOptimisticUserMessage({ + tail, + head, + position: remainingOptimistic, + precedingItems, + replacement: [remainingOptimistic.item], + }); +} + function applyTimelineIncrementalPath(args: { timelineUnits: TimelineUnit[]; payload: ProcessTimelineResponseInput["payload"]; @@ -586,39 +959,16 @@ function applyTimelineIncrementalPath(args: { { source: "canonical" }, ); nextTail = mergePrependedCanonicalTail(olderTail, currentTail); - } else if (currentHead.length > 0) { - for (const { event, timestamp, seqEnd } of acceptedUnits) { - const timelineCursor = { epoch: payload.epoch, seq: seqEnd }; - const replacedHead = replaceLiveAssistantWithProjectedText({ - head: nextHead, - event, - timestamp, - timelineCursor, - }); - if (replacedHead) { - nextHead = replacedHead; - continue; - } - const applied = applyStreamEvent({ - tail: nextTail, - head: nextHead, - event, - timestamp, - source: "canonical", - timelineCursor, - }); - nextTail = applied.tail; - nextHead = applied.head; - } } else { - nextTail = acceptedUnits.reduce( - (state, { event, timestamp, seqEnd }) => - reduceStreamUpdate(state, event, timestamp, { - source: "canonical", - timelineCursor: { epoch: payload.epoch, seq: seqEnd }, - }), - currentTail, - ); + const applied = applyAcceptedForwardTimelineUnits({ + units: acceptedUnits, + epoch: payload.epoch, + currentTail: nextTail, + currentHead: nextHead, + currentEndSeq: currentCursor?.endSeq, + }); + nextTail = applied.tail; + nextHead = applied.head; } } diff --git a/packages/app/src/timeline/viewed-timeline-sync.test.ts b/packages/app/src/timeline/viewed-timeline-sync.test.ts index 511650f62..2e7fa6dd1 100644 --- a/packages/app/src/timeline/viewed-timeline-sync.test.ts +++ b/packages/app/src/timeline/viewed-timeline-sync.test.ts @@ -1,6 +1,9 @@ import { expect, test } from "vitest"; import type { ProjectedTimelineForwardFetchPlan } from "./timeline-sync-plan"; -import { createViewedTimelineSync } from "./viewed-timeline-sync"; +import { + createViewedTimelineSync, + VIEWED_TIMELINE_UNSUBSCRIBE_GRACE_MS, +} from "./viewed-timeline-sync"; interface Deferred { promise: Promise; @@ -34,6 +37,7 @@ interface TimelineFetch { class TimelineWorld { readonly errors: string[] = []; readonly sync = createViewedTimelineSync({ + initialDeliveryMode: "selective", setSubscription: async (agentIds) => { const result = deferred(); this.memberships.push({ @@ -69,13 +73,17 @@ class TimelineWorld { const waiter = this.errorWaiters.shift(); if (waiter) waiter(this.errors.at(-1) ?? ""); }, - scheduleRetry: (retry) => { - this.retries.push(retry); + schedule: (task, delayMs) => { + const scheduled = { task, delayMs }; + this.scheduled.push(scheduled); const waiter = this.retryWaiters.shift(); - if (waiter) waiter(this.retries.shift()!); + if (waiter && delayMs === 1_000) { + this.scheduled.splice(this.scheduled.indexOf(scheduled), 1); + waiter(task); + } return () => { - const index = this.retries.indexOf(retry); - if (index >= 0) this.retries.splice(index, 1); + const index = this.scheduled.indexOf(scheduled); + if (index >= 0) this.scheduled.splice(index, 1); }; }, }); @@ -90,7 +98,7 @@ class TimelineWorld { private readonly cursors = new Map(); private readonly authoritativeHistory = new Set(); private readonly errorWaiters: Array<(message: string) => void> = []; - private readonly retries: Array<() => void> = []; + private readonly scheduled: Array<{ task: () => void; delayMs: number }> = []; private readonly retryWaiters: Array<(retry: () => void) => void> = []; setCursor(agentId: string, endSeq: number): void { @@ -129,11 +137,25 @@ class TimelineWorld { } nextRetry(): Promise<() => void> { - const retry = this.retries.shift(); - if (retry) return Promise.resolve(retry); + const index = this.scheduled.findIndex((entry) => entry.delayMs === 1_000); + if (index >= 0) return Promise.resolve(this.scheduled.splice(index, 1)[0].task); return new Promise((resolve) => this.retryWaiters.push(resolve)); } + runUnsubscribeGrace(): void { + const index = this.scheduled.findIndex( + (entry) => entry.delayMs === VIEWED_TIMELINE_UNSUBSCRIBE_GRACE_MS, + ); + expect(index).toBeGreaterThanOrEqual(0); + this.scheduled.splice(index, 1)[0].task(); + } + + expectNoPendingUnsubscribe(): void { + expect( + this.scheduled.filter((entry) => entry.delayMs === VIEWED_TIMELINE_UNSUBSCRIBE_GRACE_MS), + ).toEqual([]); + } + private releaseMembershipWaiter(): void { const waiter = this.membershipWaiters.shift(); if (!waiter) return; @@ -211,6 +233,7 @@ test("membership changes during acknowledgement never catch up the stale set", a const staleMembership = await world.nextMembership(); world.sync.replaceVisibleAgentIds("workspace", ["agent-b"]); + world.runUnsubscribeGrace(); staleMembership.succeed(); const currentMembership = await world.nextMembership(); currentMembership.succeed(); @@ -236,6 +259,7 @@ test("removing one agent during paging cancels only that agent", async () => { ]); world.sync.replaceVisibleAgentIds("workspace", ["agent-b"]); + world.runUnsubscribeGrace(); const replacement = await world.nextMembership(); agentA.respond({ hasNewer: true, seq: 4 }); agentB.respond({ hasNewer: true, seq: 7 }); @@ -283,7 +307,9 @@ test("overlapping sources deduplicate membership and source removal preserves re world.sync.replaceVisibleAgentIds("left-route", []); world.expectNoPendingMembership(); + world.expectNoPendingUnsubscribe(); world.sync.replaceVisibleAgentIds("right-route", ["agent-b"]); + world.runUnsubscribeGrace(); const remaining = await world.nextMembership(); remaining.succeed(); @@ -417,6 +443,7 @@ test("stale membership retry cannot overwrite a newer effective set", async () = const staleRetry = await world.nextRetry(); world.sync.replaceVisibleAgentIds("workspace", ["agent-b"]); + world.runUnsubscribeGrace(); const current = await world.nextMembership(); staleRetry(); current.succeed(); @@ -446,3 +473,153 @@ test("membership retry cannot run while disconnected", async () => { expect(restored.agentIds).toEqual(["agent-a"]); }); + +test("quickly returning to an agent cancels its pending unsubscribe without another catch-up", async () => { + const world = new TimelineWorld(); + world.sync.setConnected(true); + world.sync.replaceVisibleAgentIds("workspace", ["agent-a"]); + const membership = await world.nextMembership(); + membership.succeed(); + const initialCatchUp = await world.nextFetch("agent-a"); + initialCatchUp.respond({ hasNewer: false }); + + world.sync.replaceVisibleAgentIds("workspace", []); + world.expectNoPendingMembership(); + world.sync.replaceVisibleAgentIds("workspace", ["agent-a"]); + + world.expectNoPendingUnsubscribe(); + world.expectNoPendingMembership(); + world.expectNoPendingFetch(); +}); + +test("unsubscribe grace expiry removes the agent exactly once", async () => { + const world = new TimelineWorld(); + world.sync.setConnected(true); + world.sync.replaceVisibleAgentIds("workspace", ["agent-a"]); + const membership = await world.nextMembership(); + membership.succeed(); + const catchUp = await world.nextFetch("agent-a"); + catchUp.respond({ hasNewer: false }); + + world.sync.replaceVisibleAgentIds("workspace", []); + world.runUnsubscribeGrace(); + const unsubscribe = await world.nextMembership(); + unsubscribe.succeed(); + + expect(unsubscribe.agentIds).toEqual([]); + world.expectNoPendingUnsubscribe(); + world.expectNoPendingMembership(); +}); + +test("a new visible agent subscribes immediately while the previous agent lingers", async () => { + const world = new TimelineWorld(); + world.sync.setConnected(true); + world.sync.replaceVisibleAgentIds("workspace", ["agent-a"]); + const initialMembership = await world.nextMembership(); + initialMembership.succeed(); + const initialCatchUp = await world.nextFetch("agent-a"); + initialCatchUp.respond({ hasNewer: false }); + + world.sync.replaceVisibleAgentIds("workspace", ["agent-b"]); + const expandedMembership = await world.nextMembership(); + expandedMembership.succeed(); + const agentBCatchUp = await world.nextFetch("agent-b"); + agentBCatchUp.respond({ hasNewer: false }); + + expect(expandedMembership.agentIds).toEqual(["agent-a", "agent-b"]); + world.runUnsubscribeGrace(); + const settledMembership = await world.nextMembership(); + settledMembership.succeed(); + expect(settledMembership.agentIds).toEqual(["agent-b"]); +}); + +test("backgrounding clears pending unsubscribe grace and membership immediately", async () => { + const world = new TimelineWorld(); + world.sync.setConnected(true); + world.sync.replaceVisibleAgentIds("workspace", ["agent-a"]); + const membership = await world.nextMembership(); + membership.succeed(); + const catchUp = await world.nextFetch("agent-a"); + catchUp.respond({ hasNewer: false }); + + world.sync.replaceVisibleAgentIds("workspace", []); + world.sync.setActive(false); + const unsubscribe = await world.nextMembership(); + unsubscribe.succeed(); + + expect(unsubscribe.agentIds).toEqual([]); + world.expectNoPendingUnsubscribe(); +}); + +test("disconnecting cancels pending unsubscribe grace without publishing on the closed socket", async () => { + const world = new TimelineWorld(); + world.sync.setConnected(true); + world.sync.replaceVisibleAgentIds("workspace", ["agent-a"]); + const membership = await world.nextMembership(); + membership.succeed(); + const catchUp = await world.nextFetch("agent-a"); + catchUp.respond({ hasNewer: false }); + + world.sync.replaceVisibleAgentIds("workspace", []); + world.sync.setConnected(false); + + world.expectNoPendingUnsubscribe(); + world.expectNoPendingMembership(); +}); + +test("disposing cancels pending unsubscribe grace", async () => { + const world = new TimelineWorld(); + world.sync.setConnected(true); + world.sync.replaceVisibleAgentIds("workspace", ["agent-a"]); + const membership = await world.nextMembership(); + membership.succeed(); + const catchUp = await world.nextFetch("agent-a"); + catchUp.respond({ hasNewer: false }); + + world.sync.replaceVisibleAgentIds("workspace", []); + world.sync.dispose(); + + world.expectNoPendingUnsubscribe(); + world.expectNoPendingMembership(); +}); + +test("legacy delivery skips subscription RPCs while retaining visibility catch-up and gap recovery", async () => { + const world = new TimelineWorld(); + world.sync.setDeliveryMode("legacy"); + world.sync.replaceVisibleAgentIds("workspace", ["agent-a"]); + world.sync.setConnected(true); + + world.expectNoPendingMembership(); + const initial = await world.nextFetch("agent-a"); + initial.respond({ hasNewer: false }); + + world.sync.recoverGap("agent-a", { epoch: "epoch-agent-a", endSeq: 10 }); + const recovery = await world.nextFetch("agent-a"); + recovery.respond({ hasNewer: false }); + + expect(recovery.request).toEqual({ + direction: "after", + cursor: { epoch: "epoch-agent-a", seq: 10 }, + limit: 100, + projection: "projected", + }); +}); + +test("switching from legacy to selective delivery publishes membership and catches up once", async () => { + const world = new TimelineWorld(); + world.sync.setDeliveryMode("legacy"); + world.sync.replaceVisibleAgentIds("workspace", ["agent-a"]); + world.sync.setConnected(true); + const legacyCatchUp = await world.nextFetch("agent-a"); + legacyCatchUp.respond({ hasNewer: false }); + + world.sync.setDeliveryMode("selective"); + const membership = await world.nextMembership(); + membership.succeed(); + const catchUp = await world.nextFetch("agent-a"); + catchUp.respond({ hasNewer: false }); + + expect(membership.agentIds).toEqual(["agent-a"]); + world.expectNoPendingMembership(); + world.expectNoPendingFetch(); +}); diff --git a/packages/app/src/timeline/viewed-timeline-sync.ts b/packages/app/src/timeline/viewed-timeline-sync.ts index f0f2c0870..4aa8a26dd 100644 --- a/packages/app/src/timeline/viewed-timeline-sync.ts +++ b/packages/app/src/timeline/viewed-timeline-sync.ts @@ -12,6 +12,7 @@ interface TimelinePageResult { } interface ViewedTimelineSyncPorts { + initialDeliveryMode: TimelineDeliveryMode; setSubscription(agentIds: string[]): Promise; readCursor(agentId: string): AgentTimelineCursorState | undefined; hasAuthoritativeHistory(agentId: string): boolean; @@ -20,9 +21,11 @@ interface ViewedTimelineSyncPorts { request: ProjectedTimelineForwardFetchPlan, ): Promise; reportError(error: unknown): void; - scheduleRetry(retry: () => void): () => void; + schedule(task: () => void, delayMs: number): () => void; } +export type TimelineDeliveryMode = "legacy" | "selective"; + export interface ViewedTimelineUiBridge { replaceVisibleAgentIds(sourceId: string, agentIds: string[]): void; } @@ -30,10 +33,14 @@ export interface ViewedTimelineUiBridge { export interface ViewedTimelineSync extends ViewedTimelineUiBridge { setActive(active: boolean): void; setConnected(connected: boolean): void; + setDeliveryMode(mode: TimelineDeliveryMode): void; recoverGap(agentId: string, cursor: { epoch: string; endSeq: number }): void; dispose(): void; } +const RETRY_DELAY_MS = 1_000; +export const VIEWED_TIMELINE_UNSUBSCRIBE_GRACE_MS = 5_000; + type CatchUpStatus = "running" | "complete" | "error"; interface CatchUpState { @@ -80,8 +87,10 @@ export function createViewedTimelineSync(ports: ViewedTimelineSyncPorts): Viewed const catchUps = new Map(); const catchUpGenerations = new Map(); const pendingGaps = new Map(); + const lingeringRemovals = new Map void>(); let active = true; let connected = false; + let deliveryMode = ports.initialDeliveryMode; let disposed = false; let desired: string[] = []; let acknowledged: string[] = []; @@ -91,7 +100,9 @@ export function createViewedTimelineSync(ports: ViewedTimelineSyncPorts): Viewed let membershipNeedsRetry = false; let cancelMembershipRetry: (() => void) | null = null; - const effectiveAgentIds = () => (active ? normalizeAgentIds([...sources.values()].flat()) : []); + const visibleAgentIds = () => (active ? normalizeAgentIds([...sources.values()].flat()) : []); + const effectiveAgentIds = () => + normalizeAgentIds([...visibleAgentIds(), ...lingeringRemovals.keys()]); const isAcknowledged = (agentId: string) => acknowledged.includes(agentId); const isDesired = (agentId: string) => desired.includes(agentId); @@ -139,11 +150,11 @@ export function createViewedTimelineSync(ports: ViewedTimelineSyncPorts): Viewed catchUps.set(agentId, { generation, status: "complete" }); } catch (error) { if (catchUps.get(agentId)?.generation === generation) { - const cancelRetry = ports.scheduleRetry(() => { + const cancelRetry = ports.schedule(() => { const current = catchUps.get(agentId); if (current?.generation !== generation || current.status !== "error") return; startCatchUp(agentId); - }); + }, RETRY_DELAY_MS); catchUps.set(agentId, { generation, status: "error", cancelRetry }); ports.reportError(error); } @@ -188,7 +199,7 @@ export function createViewedTimelineSync(ports: ViewedTimelineSyncPorts): Viewed }; const reconcileLatestMembership = async (): Promise => { - if (disposed || !connected) return; + if (disposed || !connected || deliveryMode !== "selective") return; const generation = membershipGeneration; const requested = desired; if (!membershipNeedsRetry && sameAgentIds(requested, acknowledged)) return; @@ -198,7 +209,7 @@ export function createViewedTimelineSync(ports: ViewedTimelineSyncPorts): Viewed } catch (error) { membershipNeedsRetry = true; cancelMembershipRetry?.(); - cancelMembershipRetry = ports.scheduleRetry(() => { + cancelMembershipRetry = ports.schedule(() => { cancelMembershipRetry = null; if ( disposed || @@ -209,13 +220,13 @@ export function createViewedTimelineSync(ports: ViewedTimelineSyncPorts): Viewed return; } void reconcileMembership(); - }); + }, RETRY_DELAY_MS); ports.reportError(error); return; } cancelMembershipRetry?.(); cancelMembershipRetry = null; - if (disposed || !connected) return; + if (disposed || !connected || deliveryMode !== "selective") return; acknowledged = requested; if (generation !== membershipGeneration) { await reconcileLatestMembership(); @@ -236,12 +247,13 @@ export function createViewedTimelineSync(ports: ViewedTimelineSyncPorts): Viewed await reconcileLatestMembership(); } finally { reconciling = false; - if (reconcileRequested && !disposed && connected) { + if (reconcileRequested && !disposed && connected && deliveryMode === "selective") { reconcileRequested = false; void reconcileMembership(); } else if ( !disposed && connected && + deliveryMode === "selective" && !membershipNeedsRetry && !sameAgentIds(desired, acknowledged) ) { @@ -256,10 +268,9 @@ export function createViewedTimelineSync(ports: ViewedTimelineSyncPorts): Viewed } }; - const publishEffectiveMembership = () => { - const nextDesired = effectiveAgentIds(); + const commitDesiredMembership = (nextDesired: string[]) => { if (sameAgentIds(nextDesired, desired)) { - if (membershipNeedsRetry) void reconcileMembership(); + if (deliveryMode === "selective" && membershipNeedsRetry) void reconcileMembership(); retryFailedCatchUps(); return; } @@ -271,25 +282,60 @@ export function createViewedTimelineSync(ports: ViewedTimelineSyncPorts): Viewed cancelMembershipRetry = null; desired = nextDesired; membershipGeneration += 1; + if (deliveryMode === "legacy") { + acknowledged = connected ? desired : []; + if (connected) startAcknowledgedCatchUps(); + return; + } void reconcileMembership(); }; + const clearLingeringRemovals = () => { + for (const cancel of lingeringRemovals.values()) cancel(); + lingeringRemovals.clear(); + }; + + const publishVisibleMembership = (allowGrace: boolean) => { + const visible = visibleAgentIds(); + for (const agentId of visible) { + lingeringRemovals.get(agentId)?.(); + lingeringRemovals.delete(agentId); + } + + if (allowGrace && connected && active && deliveryMode === "selective") { + for (const agentId of desired) { + if (visible.includes(agentId) || lingeringRemovals.has(agentId)) continue; + const cancel = ports.schedule(() => { + lingeringRemovals.delete(agentId); + commitDesiredMembership(effectiveAgentIds()); + }, VIEWED_TIMELINE_UNSUBSCRIBE_GRACE_MS); + lingeringRemovals.set(agentId, cancel); + } + } else { + clearLingeringRemovals(); + } + + commitDesiredMembership(effectiveAgentIds()); + }; + return { replaceVisibleAgentIds(sourceId, agentIds) { const normalized = normalizeAgentIds(agentIds); if (normalized.length === 0) sources.delete(sourceId); else sources.set(sourceId, normalized); - publishEffectiveMembership(); + publishVisibleMembership(true); }, setActive(nextActive) { if (active === nextActive) return; active = nextActive; - publishEffectiveMembership(); + publishVisibleMembership(false); }, setConnected(nextConnected) { if (connected === nextConnected) return; connected = nextConnected; if (!connected) { + clearLingeringRemovals(); + commitDesiredMembership(visibleAgentIds()); cancelMembershipRetry?.(); cancelMembershipRetry = null; acknowledged = []; @@ -298,7 +344,26 @@ export function createViewedTimelineSync(ports: ViewedTimelineSyncPorts): Viewed return; } membershipGeneration += 1; - void reconcileMembership(); + if (deliveryMode === "legacy") { + acknowledged = desired; + startAcknowledgedCatchUps(); + } else { + void reconcileMembership(); + } + }, + setDeliveryMode(nextMode) { + if (deliveryMode === nextMode) return; + deliveryMode = nextMode; + clearLingeringRemovals(); + cancelMembershipRetry?.(); + cancelMembershipRetry = null; + membershipNeedsRetry = false; + membershipGeneration += 1; + for (const agentId of desired) cancelCatchUp(agentId); + desired = visibleAgentIds(); + acknowledged = deliveryMode === "legacy" && connected ? desired : []; + if (deliveryMode === "selective" && connected) void reconcileMembership(); + else if (connected) startAcknowledgedCatchUps(); }, recoverGap(agentId, cursor) { if (!isDesired(agentId)) return; @@ -309,6 +374,7 @@ export function createViewedTimelineSync(ports: ViewedTimelineSyncPorts): Viewed }, dispose() { disposed = true; + clearLingeringRemovals(); cancelMembershipRetry?.(); cancelMembershipRetry = null; sources.clear(); From ffe76a7e5702d60d32c9c770dab9e7985cc1917a Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Sat, 18 Jul 2026 14:04:48 +0200 Subject: [PATCH 23/38] Make workspace, agent, and schedule automation consistent (#2186) * feat(workspaces): align agent and schedule automation Make workspace identity the shared placement boundary for MCP and CLI, while caller identity determines parentage. Keep heartbeats minimal and cron-based without changing legacy rolling interval semantics. * test(cli): expect canonical schedule cadence * fix(automation): preserve workspace and schedule compatibility * fix(automation): preserve compatibility edges * fix(workspaces): align MCP lifecycle resolution * fix(workspaces): preserve creation intent * fix(workspaces): preserve branch and schedule identity --- docs/agent-lifecycle.md | 29 +- docs/architecture.md | 32 +- docs/data-model.md | 6 +- docs/development.md | 2 + docs/glossary.md | 6 +- docs/product.md | 2 +- .../schedules/schedule-form-sheet.tsx | 49 +- .../src/components/schedules/schedule-row.tsx | 121 ++- .../components/schedules/schedules-table.tsx | 5 +- .../src/schedules/schedule-form-model.test.ts | 51 +- .../app/src/schedules/schedule-form-model.ts | 22 +- .../app/src/utils/schedule-format.test.ts | 6 + packages/app/src/utils/schedule-format.ts | 6 +- packages/cli/src/cli-surface.test.ts | 24 + packages/cli/src/cli.ts | 10 +- packages/cli/src/commands/agent/detach.ts | 58 ++ packages/cli/src/commands/agent/index.ts | 8 + packages/cli/src/commands/agent/run.test.ts | 75 +- packages/cli/src/commands/agent/run.ts | 243 +++-- packages/cli/src/commands/heartbeat/index.ts | 180 ++++ packages/cli/src/commands/schedule/index.ts | 11 +- packages/cli/src/commands/schedule/inspect.ts | 3 + packages/cli/src/commands/schedule/logs.ts | 2 + packages/cli/src/commands/schedule/ls.ts | 4 +- packages/cli/src/commands/schedule/pause.ts | 2 + packages/cli/src/commands/schedule/resume.ts | 2 + .../cli/src/commands/schedule/run-once.ts | 2 + .../cli/src/commands/schedule/shared.test.ts | 59 +- packages/cli/src/commands/schedule/shared.ts | 53 +- packages/cli/src/commands/schedule/update.ts | 2 + .../cli/src/commands/workspace/archive.ts | 52 + .../cli/src/commands/workspace/create.test.ts | 125 +++ packages/cli/src/commands/workspace/create.ts | 161 ++++ packages/cli/src/commands/workspace/index.ts | 43 + packages/cli/src/commands/workspace/ls.ts | 32 + packages/cli/src/commands/workspace/shared.ts | 31 + packages/cli/src/utils/duration.ts | 17 +- packages/cli/tests/05-agent-run.test.ts | 17 +- packages/cli/tests/14-worktree.test.ts | 8 +- packages/cli/tests/31-loop-schedule.test.ts | 27 +- packages/client/src/daemon-client.test.ts | 8 +- packages/client/src/daemon-client.ts | 34 +- packages/client/src/index.ts | 2 +- packages/protocol/src/messages.ts | 9 +- .../protocol/src/messages.workspaces.test.ts | 19 + .../protocol/src/schedule/cadence.test.ts | 21 + packages/protocol/src/schedule/cadence.ts | 27 + .../src/server/agent/agent-prompt.test.ts | 49 +- .../server/src/server/agent/agent-prompt.ts | 14 +- .../src/server/agent/create-agent/create.ts | 85 +- .../server/agent/create-agent/intent.test.ts | 74 ++ .../src/server/agent/create-agent/intent.ts | 64 ++ .../src/server/agent/mcp-server.test.ts | 503 +++++++--- .../src/server/agent/tools/paseo-tools.ts | 893 ++++++++++++------ packages/server/src/server/bootstrap.ts | 10 + .../cli-run-workspace-precedence.e2e.test.ts | 46 +- .../src/server/paseo-worktree-service.test.ts | 2 + .../src/server/paseo-worktree-service.ts | 3 +- packages/server/src/server/schedule/cron.ts | 2 + packages/server/src/server/session.ts | 142 +-- .../src/server/workspace-archive-service.ts | 13 + ...rkspace-create-worktree-source.e2e.test.ts | 12 +- .../server/src/server/workspace-source.ts | 25 + skills/paseo-handoff/SKILL.md | 16 +- skills/paseo-loop/SKILL.md | 2 + skills/paseo/SKILL.md | 52 +- 66 files changed, 2791 insertions(+), 924 deletions(-) create mode 100644 packages/cli/src/cli-surface.test.ts create mode 100644 packages/cli/src/commands/agent/detach.ts create mode 100644 packages/cli/src/commands/heartbeat/index.ts create mode 100644 packages/cli/src/commands/workspace/archive.ts create mode 100644 packages/cli/src/commands/workspace/create.test.ts create mode 100644 packages/cli/src/commands/workspace/create.ts create mode 100644 packages/cli/src/commands/workspace/index.ts create mode 100644 packages/cli/src/commands/workspace/ls.ts create mode 100644 packages/cli/src/commands/workspace/shared.ts create mode 100644 packages/protocol/src/schedule/cadence.test.ts create mode 100644 packages/protocol/src/schedule/cadence.ts create mode 100644 packages/server/src/server/agent/create-agent/intent.test.ts create mode 100644 packages/server/src/server/agent/create-agent/intent.ts create mode 100644 packages/server/src/server/workspace-source.ts diff --git a/docs/agent-lifecycle.md b/docs/agent-lifecycle.md index b87b700ff..2be3c3b55 100644 --- a/docs/agent-lifecycle.md +++ b/docs/agent-lifecycle.md @@ -18,21 +18,14 @@ Cancellation changes lifecycle state only after the provider acknowledges the in ## Relationships -Agents can launch other agents via the agent-scoped `create_agent` MCP tool. Agent-scoped creation is always asynchronous. `relationship` and `workspace` are separate decisions: - -- `relationship` decides whether the new agent belongs under the caller. -- `workspace` decides where the new agent lives and whether a new workspace/worktree is created. - -`relationship: { kind: "subagent" }` stamps the created agent with `paseo.parent-agent-id`, pointing back at the creating agent. The client surfaces that as `agent.parentAgentId`. This requires an agent-scoped MCP session. - -`relationship: { kind: "detached" }` creates a sibling/root agent (e.g. handoffs, fire-and-forget delegations). The daemon may still use the creating agent for cwd/config inheritance, but it does not write `paseo.parent-agent-id`. +Agents can launch other agents via the agent-scoped `create_agent` MCP tool. Agent-scoped creation is always asynchronous and always stamps `paseo.parent-agent-id`, pointing back at the caller. Omit `workspaceId` to use the caller's workspace, or pass an existing workspace ID returned by `create_workspace`. Placement never changes parentage. - **Subagents** — exist as part of the creating agent's work, appear in that agent's subagent track, and are archived with it. -- **Detached agents** — stand on their own, do not appear in the creating agent's subagent track, and are not archived with it. +- **Detached agents** — stand on their own after an explicit detach transition, do not appear in the former parent's subagent track, and are not archived with it. -`workspace: { kind: "current" }` uses the caller's workspace and can optionally override the runtime cwd. It requires an agent-scoped MCP session. `workspace: { kind: "create", source: { kind: "directory" | "worktree", ... } }` creates a new workspace for the new agent; worktree creation goes through the Paseo worktree workflow and stamps the agent with that fresh workspace id. +Runtime ownership is resolved from explicit workspace ID and caller context, never from `cwd`. Workspace creation is a separate operation with `local | worktree` isolation; agent creation only selects an existing workspace. -Users can also detach an existing subagent from the subagents track. Detach removes the `paseo.parent-agent-id` label only: it does not stop, archive, move, or restart the agent. The agent keeps its current `cwd` and `workspaceId`, leaves the former parent's track, and behaves like a root agent for tab close, workspace activity, and future parent archive. +Users can also detach an existing subagent from the subagents track. Detach is deliberately a manual lifecycle gesture, not an agent-facing MCP tool. It removes the `paseo.parent-agent-id` label only: it does not stop, archive, move, or restart the agent. The agent keeps its current `cwd` and `workspaceId`, leaves the former parent's track, and behaves like a root agent for tab close, workspace activity, and future parent archive. `notifyOnFinish` defaults to `true` for agent-scoped creation and background prompt follow-ups because most delegated work needs to report back to the creating agent. Set it to `false` only for truly fire-and-forget agents or prompts. @@ -46,7 +39,7 @@ The provider still owns the underlying runtime. Paseo keeps an agent record so t Archive is a **soft delete**: the agent record stays on disk with `archivedAt` set, the runtime is closed, and the agent disappears from active lists. Archive is **global** — it lives on the server and propagates to every connected client. -`create_agent_request` can opt an agent into `autoArchive`. In that mode the daemon archives the agent after the first terminal turn event (`turn_completed`, `turn_failed`, or `turn_canceled`). If the same request created a Paseo worktree through its `worktree` field, auto-archive archives that worktree too, which removes the agent records inside the worktree. +`create_agent_request` can opt an agent into `autoArchive`. In that mode the daemon archives the agent after the first terminal turn event (`turn_completed`, `turn_failed`, or `turn_canceled`). When the agent owns an isolated workspace, auto-archive archives that workspace too; the managed worktree is removed when its final workspace reference is gone. Archiving runs through `AgentManager.archiveAgent` (`packages/server/src/server/agent/agent-manager.ts`): @@ -139,11 +132,11 @@ $PASEO_HOME/agents/{cwd-with-dashes}/{agent-id}.json Each agent is a single JSON file. Fields relevant to this doc: -| Field | Type | Meaning | -| --------------------------------- | ------------- | -------------------------------------------------------------------------------------------- | -| `id` | `string` | Stable identifier | -| `archivedAt` | `string?` | Soft-delete timestamp (ISO 8601) | -| `labels["paseo.parent-agent-id"]` | `string?` | Parent agent ID, set automatically by `create_agent` when `relationship.kind === "subagent"` | -| `lastStatus` | `AgentStatus` | `initializing` / `idle` / `running` / `error` / `closed` | +| Field | Type | Meaning | +| --------------------------------- | ------------- | ---------------------------------------------------------------------------------- | +| `id` | `string` | Stable identifier | +| `archivedAt` | `string?` | Soft-delete timestamp (ISO 8601) | +| `labels["paseo.parent-agent-id"]` | `string?` | Parent agent ID, set automatically for agent-scoped creation and removed by detach | +| `lastStatus` | `AgentStatus` | `initializing` / `idle` / `running` / `error` / `closed` | See [`docs/data-model.md`](./data-model.md) for the full agent record. diff --git a/docs/architecture.md b/docs/architecture.md index 9d9af4bb7..b4ab8ef21 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -64,20 +64,20 @@ not retain non-Git directories. **Key modules:** -| Module | Responsibility | -| ------------------------------- | ---------------------------------------------------------------------------- | -| `server/bootstrap.ts` | Daemon initialization: HTTP server, WS server, agent manager, storage, relay | -| `server/websocket-server.ts` | WebSocket connection management, hello handshake, binary frame routing | -| `server/session.ts` | Per-client session state, timeline subscriptions, terminal operations | -| `server/agent/agent-manager.ts` | Agent lifecycle state machine, timeline tracking, subscriber management | -| `server/agent/agent-storage.ts` | File-backed JSON persistence at `$PASEO_HOME/agents/` | -| `server/agent/tools/` | Transport-neutral Paseo tool catalog for subagents, permissions, worktrees | -| `server/agent/mcp-server.ts` | Thin MCP adapter that registers the Paseo tool catalog with the MCP SDK | -| `server/agent/providers/` | Provider adapters (see "Agent providers" below) | -| `server/relay-transport.ts` | Outbound relay connection with E2E encryption | -| `server/schedule/` | Cron-based scheduled agents | -| `server/loop-service.ts` | Looping agent runs that retry until an exit condition | -| `server/chat/` | Chat rooms for agent-to-agent and human-to-agent messaging | +| Module | Responsibility | +| ------------------------------- | ----------------------------------------------------------------------------- | +| `server/bootstrap.ts` | Daemon initialization: HTTP server, WS server, agent manager, storage, relay | +| `server/websocket-server.ts` | WebSocket connection management, hello handshake, binary frame routing | +| `server/session.ts` | Per-client session state, timeline subscriptions, terminal operations | +| `server/agent/agent-manager.ts` | Agent lifecycle state machine, timeline tracking, subscriber management | +| `server/agent/agent-storage.ts` | File-backed JSON persistence at `$PASEO_HOME/agents/` | +| `server/agent/tools/` | Transport-neutral catalog for workspaces, agents, permissions, and automation | +| `server/agent/mcp-server.ts` | Thin MCP adapter that registers the Paseo tool catalog with the MCP SDK | +| `server/agent/providers/` | Provider adapters (see "Agent providers" below) | +| `server/relay-transport.ts` | Outbound relay connection with E2E encryption | +| `server/schedule/` | Cron-based scheduled agents | +| `server/loop-service.ts` | Looping agent runs that retry until an exit condition | +| `server/chat/` | Chat rooms for agent-to-agent and human-to-agent messaging | ### `packages/protocol` — Wire schemas and shared protocol types @@ -115,9 +115,11 @@ Commander.js CLI with Docker-style commands. Common agent operations are also ex - `paseo terminal ls/create/capture/send-keys/kill` - `paseo loop run/ls/inspect/logs/stop` - `paseo schedule create/ls/inspect/update/pause/resume/run-once/logs/delete` +- `paseo heartbeat create/update/delete` +- `paseo workspace create/ls/archive` - `paseo permit allow/deny/ls` - `paseo provider ls/models` -- `paseo worktree create/ls/archive` +- hidden legacy `paseo worktree create/ls/archive` compatibility alias - `paseo speech …` Communicates with the daemon via the same WebSocket protocol as the app. diff --git a/docs/data-model.md b/docs/data-model.md index b2ff26ba4..0d34d49a6 100644 --- a/docs/data-model.md +++ b/docs/data-model.md @@ -82,7 +82,7 @@ Each agent is stored as a separate JSON file, grouped by project directory. | `lastActivityAt` | `string?` (ISO 8601) | Last activity timestamp | | `lastUserMessageAt` | `string?` (ISO 8601) | Last user message timestamp | | `title` | `string?` | User-visible title | -| `labels` | `Record` | Key-value labels (default `{}`). `paseo.parent-agent-id` is set automatically for `create_agent` subagent relationships — see [agent-lifecycle.md](./agent-lifecycle.md) | +| `labels` | `Record` | Key-value labels (default `{}`). `paseo.parent-agent-id` is set automatically for agent-scoped creation and removed by detach — see [agent-lifecycle.md](./agent-lifecycle.md) | | `lastStatus` | `AgentStatus` | One of: `"initializing"`, `"idle"`, `"running"`, `"error"`, `"closed"` | | `lastModeId` | `string?` | Last active mode ID | | `config` | `SerializableConfig?` | Agent session configuration (see below) | @@ -289,8 +289,8 @@ One file per schedule. ID is 8 hex characters. ### Nested: ScheduleCadence (discriminated union on `type`) -- `{ type: "every", everyMs: number }` — interval in milliseconds -- `{ type: "cron", expression: string, timezone?: string }` — cron expression; absent `timezone` means UTC, present `timezone` is an IANA time zone used for local wall-clock recurrence +- `{ type: "cron", expression: string, timezone?: string }` — canonical cadence for new writes; absent `timezone` means UTC +- `{ type: "every", everyMs: number }` — legacy rolling interval, still readable and executable during the compatibility window ### Nested: ScheduleTarget (discriminated union on `type`) diff --git a/docs/development.md b/docs/development.md index d303f3c22..c36de231f 100644 --- a/docs/development.md +++ b/docs/development.md @@ -374,6 +374,8 @@ install. Use `npm run cli` to run the in-repo CLI from source (`npx tsx packages/cli/src/index.ts`). The script wraps the CLI with `scripts/dev-home.sh`, so it automatically uses this checkout's `.dev/paseo-home` and dev daemon endpoint unless you pass an explicit override. The globally installed `paseo` binary on macOS is a symlink into the installed Paseo desktop app, not this checkout — use it to drive the desktop's built-in daemon, but use `npm run cli` when you want to talk to the CLI you are editing. +Canonical automation uses `paseo workspace create/ls/archive`, `paseo heartbeat create/update/delete`, and the full `paseo schedule` group. MCP heartbeat automation is intentionally smaller: create and delete only. Detach remains an explicit user lifecycle action rather than an agent tool. `paseo run --isolation local|worktree` composes workspace creation with agent creation. The old `paseo worktree` and `paseo run --worktree` forms are hidden compatibility aliases. + ```bash npm run cli -- ls -a -g # List all agents globally npm run cli -- ls -a -g --json # Same, as JSON diff --git a/docs/glossary.md b/docs/glossary.md index fc8878f74..e2345e704 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -4,7 +4,7 @@ Authoritative terminology. UI label wins. Don't invent synonyms; use what's here - **Project** — A stable, exact selected-root record. New IDs are opaque `prj_<16 hex>` values; older remote-shaped and path-shaped IDs remain readable compatibility records. Git facts can update mutable kind metadata but never project identity, root, or default display name. UI: "Project" / "Add project". Forbidden: "Repo", "Repository" as UI label. - **Workspace** — One concrete `cwd` on one daemon, with git state; belongs to exactly one project. Its `id` is opaque workspace identity; its `cwd` is the filesystem directory. UI: "Workspace". Code: `WorkspaceDescriptorPayload` (`packages/protocol/src/messages.ts:2178`). Don't confuse with: Branch (one branch can back many workspaces via worktrees). Forbidden: "Folder", "Directory" as UI label. -- **Archive workspace** — Removes one workspace from active use and archives everything it owns. UI and app shortcuts always say "Archive workspace", regardless of backing. The daemon leaves ordinary directories intact and removes a Paseo-owned worktree only when no active workspace still references it. CLI/MCP **archive worktree** is a separate lower-level operation that archives every workspace on that worktree. +- **Archive workspace** — Removes one workspace from active use and archives everything it owns. UI, CLI, and MCP always say "Archive workspace", regardless of backing. The daemon leaves ordinary directories intact and removes a Paseo-owned worktree only when no active workspace still references it. - **Workspace kind** — `"directory" | "local_checkout" | "worktree"`. The git-derived, persisted property of a workspace, used across its lifetime (archive safety, sidebar, grouping). Derived from the cwd's git reality by `deriveWorkspaceKind` in `workspace-registry-model.ts`, not stored from a user choice. Don't confuse with **Isolation** (the create-time intent). - **Isolation** — Create-time choice for a new workspace: reuse the existing checkout (**Local**) or cut a dedicated git worktree (**New worktree**). A transient setup input, also remembered as a create-form preference; it is not a workspace property. UI: "Isolation" control on the New Workspace screen. Code: `isolation` (`"local" | "worktree"`), `useWorkspaceIsolation` (`packages/app/src/screens/new-workspace-screen.tsx`); persisted as `FormPreferences.isolation` (`packages/app/src/create-agent-preferences/preferences.ts`). Distinct from **Workspace kind**, which is the git-derived property the intent produces (Local → `local_checkout` or `directory` by git-ness; New worktree → `worktree`). On the wire it is the create request's `source.kind` (`directory | worktree`, `packages/protocol/src/messages.ts:1693`). - **Agent** — See **Agent session**. UI still says "Agent" / "New Agent" in places, but moving toward **Agent session** as the canonical term. Code: `AgentSnapshotPayload` (`packages/protocol/src/messages.ts:608`). Forbidden: "Task", "Job", "Run". @@ -16,7 +16,7 @@ Authoritative terminology. UI label wins. Don't invent synonyms; use what's here - **Forge** — Git hosting service behind Paseo's change-request features: GitHub, GitLab, Gitea, Forgejo, or a future registered adapter. Code: `ForgeService`, `forge-registry`, `forge-resolver`. Use `forge` for internal abstraction and registry IDs; use concrete forge names only when a behavior or RPC is forge-specific. - **Change request** — Forge-neutral term for a proposed branch-to-branch code change. UI normally renders the forge noun instead: GitHub/Gitea/Forgejo "PR", GitLab "MR". Code: `forge_change_request` attachments, `checkoutSource: { kind: "change_request" }`, and PR/MR status payloads. - **MR** — GitLab merge request. UI label for GitLab change requests only; do not use MR for GitHub/Gitea/Forgejo. -- **Worktree** — Paseo-managed git worktree (`~/.paseo/worktrees/{name}`); also a `workspaceKind` value. UI: CLI + `paseo.json` keys (`worktree.setup`, `worktree.teardown`) only. Code: `ProjectCheckoutLiteGitPaseoPayload` (`packages/protocol/src/messages.ts:2092`); CLI `paseo worktree` (`packages/cli/src/commands/worktree/index.ts:8`). Forbidden: "Checkout" as a synonym. +- **Worktree** — Paseo-managed git worktree (`~/.paseo/worktrees/{name}`); also a `workspaceKind` value. User-facing creation treats it as the `worktree` workspace isolation choice. Code and `paseo.json` retain worktree terminology for git lifecycle implementation. Forbidden: "Checkout" as a product synonym. - **Repository / Remote** — Internal Git observations. They may affect mutable kind/branch metadata but never project identity, root, display name, or workspace membership. No UI label. - **Directory-backed surface** — A right-sidebar surface whose content is determined by the workspace's `cwd`, so two workspaces on the same directory see identical content: git diff/status, forge change-request info, file preview/explorer contents. Keyed by `(serverId, cwd)`, never `workspaceId`. See [architecture.md](architecture.md#right-sidebar-boundary-directory-backed-vs-workspace-owned). - **Workspace-owned state** — Per-workspace state that never leaks to a same-`cwd` sibling: tabs, agents, terminals, panes, title, plus review drafts, diff-mode overrides, composer attachments, and file-explorer open/expand state. Keyed by `workspaceId` (`cwd` only as a fallback for old payloads). See [architecture.md](architecture.md#right-sidebar-boundary-directory-backed-vs-workspace-owned). @@ -29,7 +29,7 @@ Authoritative terminology. UI label wins. Don't invent synonyms; use what's here - **Tab** — UI surface representing one session inside a workspace. Not a conceptual unit; use **Agent session** when talking about the model. Code: `WorkspaceTabDescriptor` (`packages/app/src/screens/workspace/workspace-tabs-types.ts`). - **Terminal** — Workspace-scoped PTY shell streamed over the binary mux channel. UI: "Terminal". Code: `TerminalStreamFrame` (`packages/protocol/src/terminal-stream-protocol.ts`). - **Schedule** — Cron-style trigger that creates new agents. UI: CLI/MCP (`paseo schedule`, `create_schedule`). Don't confuse with: Heartbeat (cron prompt back into the same agent) or Loop (iterative re-execution of one agent). -- **Heartbeat** — Cron-style prompt sent back into the same agent/conversation. MCP: `create_heartbeat`. Use for reminders and babysitting where the status should return inline. +- **Heartbeat** — Ephemeral cron prompt sent back into the same agent/conversation. Agent surfaces expose create, update cron, and delete only. Use for reminders and babysitting where status should return inline. - **Mode** — Provider-specific operational mode (plan, default, full-access, …). UI: icon-only. Code: `modeId` in `AgentSessionConfig` (`packages/protocol/src/messages.ts:257`). - **Attachment** — External or local context bound to an agent prompt: forge issue/change request, review context, uploaded file, text, or image. UI: "Attach issue or PR/MR". Code: `AgentAttachment` (`packages/protocol/src/messages.ts:782`). - **Composer** — The whole prompt surface for sending work to an agent. Code: `Composer` (`packages/app/src/composer/index.tsx`). Don't call this "message input" except for the text-entry subcomponent. diff --git a/docs/product.md b/docs/product.md index fbfc4cfa0..2eeeb0b84 100644 --- a/docs/product.md +++ b/docs/product.md @@ -73,7 +73,7 @@ Anyone who builds software: - Built-in providers: Claude Code (Agent SDK), Codex (app-server), GitHub Copilot (ACP), OpenCode, Pi, OMP - One-click ACP provider catalog: CodeWhale, Cursor, Hermes, Qwen Coder, Kimi Code, and others — plus custom ACP providers - Voice mode: dictate prompts or talk through problems hands-free -- MCP server exposes the daemon to other agents (create_agent, send_agent_prompt, schedules, terminals, worktrees, workspace renaming) +- MCP server exposes the daemon to other agents (workspaces, create/detach agent, schedules, heartbeats, terminals, workspace renaming) - Scheduled agents (cron-style triggers) via app, CLI, and MCP - Frequent releases (multiple per week) - Community contributions across packaging, providers, and bug fixes diff --git a/packages/app/src/components/schedules/schedule-form-sheet.tsx b/packages/app/src/components/schedules/schedule-form-sheet.tsx index 721ceb225..febfbf6ca 100644 --- a/packages/app/src/components/schedules/schedule-form-sheet.tsx +++ b/packages/app/src/components/schedules/schedule-form-sheet.tsx @@ -13,7 +13,7 @@ import { Text, View } from "react-native"; import { Brain, Folder, GitBranch } from "lucide-react-native"; import { StyleSheet } from "react-native-unistyles"; import type { AgentProvider } from "@getpaseo/protocol/agent-types"; -import type { ScheduleSummary } from "@getpaseo/protocol/schedule/types"; +import type { ScheduleCadence, ScheduleSummary } from "@getpaseo/protocol/schedule/types"; import { useStoreWithEqualityFn } from "zustand/traditional"; import { AdaptiveModalSheet, type SheetHeader } from "@/components/adaptive-modal-sheet"; import { ComboboxItem } from "@/components/ui/combobox"; @@ -71,6 +71,15 @@ function parseMaxRuns(raw: string): number | null { return Number.isFinite(parsed) && parsed > 0 ? parsed : null; } +function requireCronCadence( + cadence: Extract | undefined, +): Extract { + if (!cadence) { + throw new Error("Choose a cron cadence before creating this schedule"); + } + return cadence; +} + function resolveCreateServerId(input: { mode: "create" | "edit"; serverId: string | null | undefined; @@ -306,18 +315,15 @@ function OpenScheduleFormSheet({ ]); const submitAgentTarget = useCallback(async (): Promise => { - if (!schedule) { + if (!schedule || !state.submitCadence) { return false; } await updateSchedule({ id: schedule.id, - name: state.name.trim() || null, - prompt: state.prompt.trim(), cadence: state.submitCadence, - maxRuns: parseMaxRuns(state.maxRuns), }); return true; - }, [schedule, state.maxRuns, state.name, state.prompt, state.submitCadence, updateSchedule]); + }, [schedule, state.submitCadence, updateSchedule]); const submitNewAgent = useCallback(async (): Promise => { const provider = state.selectedProvider; @@ -333,7 +339,7 @@ function OpenScheduleFormSheet({ id: schedule.id, name: state.name.trim() || null, prompt: state.prompt.trim(), - cadence: state.submitCadence, + ...(state.submitCadence ? { cadence: state.submitCadence } : {}), newAgentConfig: { provider, model: state.selectedModel || null, @@ -353,7 +359,7 @@ function OpenScheduleFormSheet({ await createSchedule({ prompt: state.prompt.trim(), name: state.name.trim() || undefined, - cadence: state.submitCadence, + cadence: requireCronCadence(state.submitCadence), target: { type: "new-agent", config: { @@ -394,10 +400,12 @@ function OpenScheduleFormSheet({ void handleSubmit(); }, [handleSubmit]); - const header = useMemo( - () => ({ title: mode === "edit" ? "Edit schedule" : "New schedule" }), - [mode], - ); + const header = useMemo(() => { + if (mode !== "edit") { + return { title: "New schedule" }; + } + return { title: schedule?.target.type === "agent" ? "Edit heartbeat" : "Edit schedule" }; + }, [mode, schedule?.target.type]); const footer = useMemo( () => ( @@ -466,6 +474,21 @@ function ScheduleFormFields({ cadenceError, mutationServerId, }: ScheduleFormFieldsProps): ReactElement { + if (state.targetKind === "agent") { + return ( + <> + + + {state.submitError ? {state.submitError} : null} + + ); + } + return ( <> @@ -502,7 +525,7 @@ function ScheduleFormFields({ model={model} state={state} providerSnapshot={providerSnapshot} - agentTargetLabel={agentTargetLabel} + agentTargetLabel={null} controlSize={controlSize} mutationServerId={mutationServerId} /> diff --git a/packages/app/src/components/schedules/schedule-row.tsx b/packages/app/src/components/schedules/schedule-row.tsx index a5b6b0c4e..faf491a35 100644 --- a/packages/app/src/components/schedules/schedule-row.tsx +++ b/packages/app/src/components/schedules/schedule-row.tsx @@ -16,7 +16,12 @@ import { useIsCompactFormFactor } from "@/constants/layout"; import { settingsStyles } from "@/styles/settings"; import type { Theme } from "@/styles/theme"; import type { ScheduleDerivedState } from "@/schedules/schedule-derivation"; -import { formatCadence, formatNextRun, resolveScheduleTitle } from "@/utils/schedule-format"; +import { + formatCadence, + formatNextRun, + resolveScheduleTitle, + scheduleProductName, +} from "@/utils/schedule-format"; import { formatTimeAgo } from "@/utils/time"; import type { ScheduleSummary } from "@getpaseo/protocol/schedule/types"; @@ -153,9 +158,10 @@ export function ScheduleRow({ const handlePointerLeave = useCallback(() => setIsHovered(false), []); const title = resolveScheduleTitle(schedule); + const productName = scheduleProductName(schedule); const badge = stateBadge(state); const meta = buildMeta(schedule, state, serverName, singleHost ?? false); - const canRun = state === "active" || state === "paused"; + const canRun = schedule.target.type === "new-agent" && (state === "active" || state === "paused"); const rowStyle = useCallback( ({ pressed }: PressableStateCallbackType) => [ @@ -178,7 +184,7 @@ export function ScheduleRow({ style={rowStyle} onPress={onEdit} accessibilityRole="button" - accessibilityLabel={`Edit schedule ${title}`} + accessibilityLabel={`Edit ${productName.toLowerCase()} ${title}`} testID={`schedule-row-${schedule.id}`} > @@ -222,6 +228,66 @@ const resumeLeading = ; const deleteLeading = ; +function ScheduleExecutionMenuItems({ + schedule, + canRun, + pending, + onPause, + onResume, + onRunNow, +}: Pick & { + canRun: boolean; +}): ReactElement | null { + if (schedule.target.type === "agent") { + return null; + } + + let cadenceAction: ReactElement; + if (schedule.status === "paused") { + cadenceAction = ( + + Resume schedule + + ); + } else { + cadenceAction = ( + + Pause schedule + + ); + } + + return ( + <> + {cadenceAction} + + Run now + + + ); +} + function renderKebabTriggerIcon({ hovered }: { hovered?: boolean }): ReactElement { return ( & { canRun: boolean; }): ReactElement { + const productName = scheduleProductName(schedule); + const productNameLower = productName.toLowerCase(); return ( {renderKebabTriggerIcon} @@ -263,41 +331,16 @@ function ScheduleKebabMenu({ onSelect={onEdit} testID={`schedule-menu-edit-${schedule.id}`} > - Edit schedule - - {schedule.status === "paused" ? ( - - Resume schedule - - ) : ( - - Pause schedule - - )} - - Run now + Edit {productNameLower} + - Delete schedule + Delete {productNameLower} diff --git a/packages/app/src/components/schedules/schedules-table.tsx b/packages/app/src/components/schedules/schedules-table.tsx index 037bc1f10..c591b03d4 100644 --- a/packages/app/src/components/schedules/schedules-table.tsx +++ b/packages/app/src/components/schedules/schedules-table.tsx @@ -7,7 +7,7 @@ import type { AggregatedSchedule } from "@/hooks/use-schedules"; import type { ScheduleDerivedState } from "@/schedules/schedule-derivation"; import { settingsStyles } from "@/styles/settings"; import { confirmDialog } from "@/utils/confirm-dialog"; -import { resolveScheduleTitle } from "@/utils/schedule-format"; +import { resolveScheduleTitle, scheduleProductName } from "@/utils/schedule-format"; /** A schedule plus the client-derived fields the row renders. */ export interface ScheduleRowView { @@ -113,8 +113,9 @@ function SchedulesTableRow({ const handleDelete = useCallback(() => { void (async () => { + const productName = scheduleProductName(schedule); const confirmed = await confirmDialog({ - title: "Delete schedule", + title: `Delete ${productName.toLowerCase()}`, message: `Delete "${resolveScheduleTitle(schedule)}"? This cannot be undone.`, confirmLabel: "Delete", destructive: true, diff --git a/packages/app/src/schedules/schedule-form-model.test.ts b/packages/app/src/schedules/schedule-form-model.test.ts index ef490e857..5a50cd33a 100644 --- a/packages/app/src/schedules/schedule-form-model.test.ts +++ b/packages/app/src/schedules/schedule-form-model.test.ts @@ -113,6 +113,26 @@ function scheduleOnHost(input: { }; } +function heartbeatOnHost(cadence: ScheduleSummary["cadence"]): TestSchedule { + return { + id: "heartbeat-host-a", + serverId: "host-a", + serverName: "Host A", + name: "Babysit", + prompt: "Check status", + cadence, + target: { type: "agent", agentId: "agent-1" }, + status: "active", + createdAt: "2026-07-01T00:00:00.000Z", + updatedAt: "2026-07-01T00:00:00.000Z", + nextRunAt: "2026-07-02T00:00:00.000Z", + lastRunAt: null, + pausedAt: null, + expiresAt: null, + maxRuns: null, + }; +} + function providerSnapshot(models: AgentModelDefinition[]): { entries: ProviderSnapshotEntry[] } { return { entries: [ @@ -389,7 +409,7 @@ describe("schedule form model", () => { }); }); - it("normalizes interval cadences to cron cadences when opening the form", () => { + it("displays a representable legacy interval without submitting a cadence change", () => { const form = open({ mode: "edit", schedule: scheduleOnHost({ @@ -412,9 +432,12 @@ describe("schedule form model", () => { expression: "* * * * *", timezone: "Europe/Madrid", }); + form.setName("Renamed without touching cadence"); + + expect(form.getState().submitCadence).toBeUndefined(); }); - it("preserves an edited schedule's interval cadence until the cadence changes", () => { + it("does not rewrite an unrepresentable legacy interval until cadence changes", () => { const originalCadence = { type: "every" as const, everyMs: 90 * 60_000 }; const form = open({ mode: "edit", @@ -441,7 +464,7 @@ describe("schedule form model", () => { form.setName("Renamed without touching cadence"); - expect(form.getState().submitCadence).toEqual(originalCadence); + expect(form.getState().submitCadence).toBeUndefined(); form.setCadence({ type: "cron", @@ -456,6 +479,28 @@ describe("schedule form model", () => { }); }); + it("requires a cron choice before updating a legacy heartbeat", () => { + const form = open({ + mode: "edit", + schedule: heartbeatOnHost({ type: "every", everyMs: 90 * 60_000 }), + defaults: { + serverId: null, + projectTargets: PROJECT_TARGETS, + preferences: {}, + timezone: "Europe/Madrid", + }, + }); + + expect(form.getState()).toMatchObject({ targetKind: "agent", canSubmit: false }); + + form.setCadence({ type: "cron", expression: "0 9 * * *", timezone: "Europe/Madrid" }); + + expect(form.getState()).toMatchObject({ + submitCadence: { type: "cron", expression: "0 9 * * *", timezone: "Europe/Madrid" }, + canSubmit: true, + }); + }); + it("clears provider selection while resolving a different project", () => { const form = open({ mode: "create", diff --git a/packages/app/src/schedules/schedule-form-model.ts b/packages/app/src/schedules/schedule-form-model.ts index 39d4b66b3..33928ea72 100644 --- a/packages/app/src/schedules/schedule-form-model.ts +++ b/packages/app/src/schedules/schedule-form-model.ts @@ -77,6 +77,7 @@ export interface ScheduleFormProjectOption { } export type ScheduleFormTargetKind = "agent" | "new-agent"; +type CronCadence = Extract; type ProviderResolutionStatus = "idle" | "pending" | "complete"; export interface ScheduleFormState { @@ -86,7 +87,7 @@ export interface ScheduleFormState { prompt: string; maxRuns: string; cadence: ScheduleCadence; - submitCadence: ScheduleCadence; + submitCadence: CronCadence | undefined; hosts: ScheduleFormHost[]; projectOptions: ScheduleFormProjectOption[]; selectedServerId: string | null; @@ -436,13 +437,10 @@ function formatInitialMaxRuns(schedule: ScheduleFormSnapshot["schedule"]): strin } function resolveInitialSubmitCadence( - snapshot: ScheduleFormSnapshot, - initialCadence: ScheduleCadence, -): ScheduleCadence { - if (snapshot.mode === "edit" && snapshot.schedule) { - return snapshot.schedule.cadence; - } - return initialCadence; + schedule: ScheduleFormSnapshot["schedule"], + initialCadence: CronCadence, +): CronCadence | undefined { + return schedule ? undefined : initialCadence; } function resolveInitialIsolation(input: { @@ -547,12 +545,12 @@ function resolveDisclosure(state: ScheduleFormState): ScheduleDisclosureState { } function resolveCanSubmit(state: ScheduleFormState): boolean { + if (state.targetKind === "agent") { + return state.submitCadence !== undefined; + } if (state.prompt.trim().length === 0) { return false; } - if (state.targetKind === "agent") { - return true; - } const hasWorkingDir = state.workingDir.trim().length > 0; const hasMatchedProject = state.selectedProjectOptionId.trim().length > 0; if (state.mode === "create" && !hasMatchedProject) { @@ -657,7 +655,7 @@ function buildInitialState(snapshot: ScheduleFormSnapshot): ScheduleFormState { prompt: snapshot.schedule?.prompt ?? "", maxRuns: formatInitialMaxRuns(snapshot.schedule), cadence: initialCadence, - submitCadence: resolveInitialSubmitCadence(snapshot, initialCadence), + submitCadence: resolveInitialSubmitCadence(snapshot.schedule, initialCadence), hosts: [...snapshot.hosts], projectOptions: buildProjectOptions(snapshot.defaults.projectTargets, selectedServerId), selectedServerId, diff --git a/packages/app/src/utils/schedule-format.test.ts b/packages/app/src/utils/schedule-format.test.ts index 8715aa03c..529c3d913 100644 --- a/packages/app/src/utils/schedule-format.test.ts +++ b/packages/app/src/utils/schedule-format.test.ts @@ -6,6 +6,7 @@ import { formatCadence, formatNextRun, isNewAgentSchedule, + scheduleProductName, partsToEveryMs, resolveScheduleTitle, validateCron, @@ -54,6 +55,11 @@ describe("schedule title helpers", () => { expect(isNewAgentSchedule(createSchedule({ targetType: "agent" }))).toBe(false); }); + it("labels engine records by product meaning", () => { + expect(scheduleProductName(createSchedule({ targetType: "new-agent" }))).toBe("Schedule"); + expect(scheduleProductName(createSchedule({ targetType: "agent" }))).toBe("Heartbeat"); + }); + it("resolves display titles by name, config title, prompt, then fallback", () => { expect( resolveScheduleTitle(createSchedule({ name: "Morning run", title: "Config title" })), diff --git a/packages/app/src/utils/schedule-format.ts b/packages/app/src/utils/schedule-format.ts index 2fa21ce4e..8a239768f 100644 --- a/packages/app/src/utils/schedule-format.ts +++ b/packages/app/src/utils/schedule-format.ts @@ -28,6 +28,10 @@ export function isNewAgentSchedule(schedule: ScheduleSummary): boolean { return schedule.target.type === "new-agent"; } +export function scheduleProductName(schedule: ScheduleSummary): "Heartbeat" | "Schedule" { + return schedule.target.type === "agent" ? "Heartbeat" : "Schedule"; +} + export function resolveScheduleTitle(schedule: ScheduleSummary): string { const name = schedule.name?.trim(); if (name) { @@ -43,7 +47,7 @@ export function resolveScheduleTitle(schedule: ScheduleSummary): string { .split("\n") .map((line) => line.trim()) .find((line) => line.length > 0); - return firstPromptLine || "Untitled schedule"; + return firstPromptLine || `Untitled ${scheduleProductName(schedule).toLowerCase()}`; } function pluralize(value: number, noun: string): string { diff --git a/packages/cli/src/cli-surface.test.ts b/packages/cli/src/cli-surface.test.ts new file mode 100644 index 000000000..fa772f85c --- /dev/null +++ b/packages/cli/src/cli-surface.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, it } from "vitest"; +import { createCli } from "./cli.js"; + +describe("canonical CLI surface", () => { + it("shows workspace and heartbeat commands while hiding worktree compatibility", () => { + const cli = createCli(); + const help = cli.helpInformation(); + expect(help).toContain("workspace"); + expect(help).toContain("heartbeat"); + expect(help).not.toContain("worktree"); + }); + + it("hides legacy run worktree syntax", () => { + const run = createCli().commands.find((command) => command.name() === "run"); + expect(run?.helpInformation()).toContain("--isolation "); + expect(run?.helpInformation()).not.toContain("--worktree "); + }); + + it("uses background for execution and reserves detach for ownership", () => { + const run = createCli().commands.find((command) => command.name() === "run"); + expect(run?.helpInformation()).toContain("--background"); + expect(run?.helpInformation()).not.toContain("--detach"); + }); +}); diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index 318252802..9124fc6c3 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -9,6 +9,8 @@ import { createScheduleCommand } from "./commands/schedule/index.js"; import { createSpeechCommand } from "./commands/speech/index.js"; import { createTerminalCommand } from "./commands/terminal/index.js"; import { createWorktreeCommand } from "./commands/worktree/index.js"; +import { createWorkspaceCommand } from "./commands/workspace/index.js"; +import { createHeartbeatCommand } from "./commands/heartbeat/index.js"; import { createHubCommand } from "./commands/hub/index.js"; import { createHooksCommand } from "./commands/hooks.js"; import { startCommand as daemonStartCommand } from "./commands/daemon/start.js"; @@ -174,6 +176,7 @@ export function createCli(): Command { // Schedule commands program.addCommand(createScheduleCommand()); + program.addCommand(createHeartbeatCommand()); // Permission commands program.addCommand(createPermitCommand()); @@ -184,8 +187,11 @@ export function createCli(): Command { // Speech model commands program.addCommand(createSpeechCommand()); - // Worktree commands - program.addCommand(createWorktreeCommand()); + // Workspace commands + program.addCommand(createWorkspaceCommand()); + // COMPAT(worktreeCli): legacy command alias added before workspace was the product unit. + // Added in v0.2.0; remove after 2027-01-17. + program.addCommand(createWorktreeCommand(), { hidden: true }); return program; } diff --git a/packages/cli/src/commands/agent/detach.ts b/packages/cli/src/commands/agent/detach.ts new file mode 100644 index 000000000..5de8ae8c9 --- /dev/null +++ b/packages/cli/src/commands/agent/detach.ts @@ -0,0 +1,58 @@ +import type { Command } from "commander"; +import type { DaemonClient } from "@getpaseo/client/internal/daemon-client"; +import { connectToDaemon, getDaemonHost, resolveAgentId } from "../../utils/client.js"; +import type { + CommandError, + CommandOptions, + OutputSchema, + SingleResult, +} from "../../output/index.js"; + +interface AgentDetachResult { + agentId: string; + status: "detached"; +} + +const detachSchema: OutputSchema = { + idField: "agentId", + columns: [ + { header: "AGENT ID", field: "agentId" }, + { header: "STATUS", field: "status" }, + ], +}; + +export async function runDetachCommand( + agentIdArg: string, + options: CommandOptions, + _command: Command, +): Promise> { + const host = getDaemonHost({ host: options.host }); + let client: DaemonClient; + try { + client = await connectToDaemon({ host: options.host }); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw { + code: "DAEMON_NOT_RUNNING", + message: `Cannot connect to daemon at ${host}: ${message}`, + } satisfies CommandError; + } + + try { + const payload = await client.fetchAgents({ filter: { includeArchived: true } }); + const agentId = resolveAgentId( + agentIdArg, + payload.entries.map((entry) => entry.agent), + ); + if (!agentId) { + throw { + code: "AGENT_NOT_FOUND", + message: `Agent not found: ${agentIdArg}`, + } satisfies CommandError; + } + await client.detachAgent(agentId); + return { type: "single", data: { agentId, status: "detached" }, schema: detachSchema }; + } finally { + await client.close().catch(() => undefined); + } +} diff --git a/packages/cli/src/commands/agent/index.ts b/packages/cli/src/commands/agent/index.ts index 9c592d2ae..37d8f6949 100644 --- a/packages/cli/src/commands/agent/index.ts +++ b/packages/cli/src/commands/agent/index.ts @@ -13,6 +13,7 @@ import { addAttachOptions, runAttachCommand } from "./attach.js"; import { addReloadOptions, runReloadCommand } from "./reload.js"; import { addImportOptions, runImportCommand } from "./import.js"; import { runUpdateCommand } from "./update.js"; +import { runDetachCommand } from "./detach.js"; import { withOutput } from "../../output/index.js"; import { addDaemonHostOption, @@ -76,6 +77,13 @@ export function createAgentCommand(): Command { withOutput(runReloadCommand), ); + addJsonAndDaemonHostOptions( + agent + .command("detach") + .description("Make a subagent independent without stopping or moving it") + .argument("", "Agent ID, prefix, or name"), + ).action(withOutput(runDetachCommand)); + addJsonAndDaemonHostOptions( agent .command("update") diff --git a/packages/cli/src/commands/agent/run.test.ts b/packages/cli/src/commands/agent/run.test.ts index 65ea370ec..59081c99d 100644 --- a/packages/cli/src/commands/agent/run.test.ts +++ b/packages/cli/src/commands/agent/run.test.ts @@ -1,5 +1,52 @@ -import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { runRunCommand, type AgentRunOptions } from "./run"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { + resolveExistingRunWorkspace, + resolveRunCallerAgentId, + runRunCommand, + type AgentRunOptions, +} from "./run"; + +describe("managed agent caller context", () => { + it("propagates a trimmed PASEO_AGENT_ID", () => { + expect(resolveRunCallerAgentId({ PASEO_AGENT_ID: " parent-agent " })).toBe("parent-agent"); + }); + + it("omits blank caller ids", () => { + expect(resolveRunCallerAgentId({ PASEO_AGENT_ID: " " })).toBeUndefined(); + }); +}); + +describe("existing run workspace resolution", () => { + it("queries the daemon for an exact workspace id and uses its directory", async () => { + const fetchWorkspaces = vi.fn().mockResolvedValue({ + entries: [{ id: "workspace-2", workspaceDirectory: "/workspace/two" }], + pageInfo: { nextCursor: null }, + }); + + await expect(resolveExistingRunWorkspace({ fetchWorkspaces }, "workspace-2")).resolves.toEqual({ + id: "workspace-2", + cwd: "/workspace/two", + }); + expect(fetchWorkspaces).toHaveBeenCalledWith({ + filter: { query: "workspace-2" }, + page: { limit: 200 }, + }); + }); + + it("rejects a workspace id absent from daemon state", async () => { + const fetchWorkspaces = vi.fn().mockResolvedValue({ + entries: [], + pageInfo: { nextCursor: null }, + }); + + await expect(resolveExistingRunWorkspace({ fetchWorkspaces }, "missing")).rejects.toMatchObject( + { + code: "WORKSPACE_NOT_FOUND", + message: "Workspace not found: missing", + }, + ); + }); +}); // validateRunOptions runs before the CLI ever connects to a daemon, so these // invalid combinations reject without one running. @@ -25,27 +72,23 @@ describe("runRunCommand option validation", () => { }); } - it("rejects --worktree combined with --workspace", async () => { + it("rejects --isolation combined with --workspace", async () => { await expectInvalidOptions( - { worktree: "feat", workspace: "ws-1" }, - /--worktree and --workspace cannot be combined/, + { isolation: "worktree", workspace: "ws-1" }, + /--isolation and --workspace cannot be combined/, ); }); - it("rejects --worktree combined with an ambient PASEO_WORKSPACE_ID", async () => { - process.env.PASEO_WORKSPACE_ID = "ws-ambient"; - await expectInvalidOptions( - { worktree: "feat" }, - /--worktree cannot be combined with an ambient PASEO_WORKSPACE_ID/, - ); - }); - - it("allows a bare --worktree through validation when no workspace is selected", async () => { - // A bare --worktree with no --workspace and no ambient PASEO_WORKSPACE_ID + it("allows explicit worktree isolation through validation", async () => { + // Explicit isolation with no --workspace // must clear validation. It still fails later (provider resolution), which // is enough to prove the new guard did not reject it. await expect( - runRunCommand("do something", { worktree: "feat", provider: undefined }, {} as never), + runRunCommand("do something", { isolation: "worktree", provider: undefined }, {} as never), ).rejects.not.toMatchObject({ code: "INVALID_OPTIONS" }); }); + + it("rejects unknown workspace isolation", async () => { + await expectInvalidOptions({ isolation: "container" }, /Unsupported workspace isolation/); + }); }); diff --git a/packages/cli/src/commands/agent/run.ts b/packages/cli/src/commands/agent/run.ts index 60f30abc3..d283a6d10 100644 --- a/packages/cli/src/commands/agent/run.ts +++ b/packages/cli/src/commands/agent/run.ts @@ -18,55 +18,61 @@ import { resolveProviderAndModel } from "../../utils/provider-model.js"; export { resolveProviderAndModel } from "../../utils/provider-model.js"; export function addRunOptions(cmd: Command): Command { - return cmd - .description("Create and start an agent with a task") - .argument("", "The task/prompt for the agent") - .option("-d, --detach", "Run in background (detached)") - .option("--title ", "Assign a title to the agent") - .addOption(new Option("--name ", "Hidden alias for --title").hideHelp()) - .option( - "--provider ", - "Agent provider, or provider/model (e.g. codex or codex/gpt-5.4)", - ) - .option( - "--model ", - "Model to use (e.g., claude-sonnet-4-20250514, claude-3-5-haiku-20241022)", - ) - .option("--thinking ", "Thinking option ID to use for this run") - .option("--mode ", "Provider-specific mode (e.g., plan, default, bypass)") - .option("--worktree ", "Create agent in a new git worktree") - .option("--base ", "Base branch for worktree (default: current branch)") - .option( - "--workspace ", - "Run in an existing workspace (default: a new workspace is created per run; falls back to $PASEO_WORKSPACE_ID)", - ) - .option( - "--image ", - "Attach image(s) to the initial prompt (can be used multiple times)", - collectMultiple, - [], - ) - .option("--cwd ", "Working directory (default: current)") - .option( - "--env ", - "Set environment variable(s) for the agent process (can be used multiple times)", - collectMultiple, - [], - ) - .option( - "--label ", - "Add label(s) to the agent (can be used multiple times)", - collectMultiple, - [], - ) - .option( - "--wait-timeout ", - "Maximum time to wait for agent to finish (e.g., 30s, 5m, 1h). Default: no limit", - ) - .option( - "--output-schema ", - "Output JSON matching the provided schema file path or inline JSON schema", - ); + return ( + cmd + .description("Create and start an agent with a task") + .argument("", "The task/prompt for the agent") + .option("-d, --background", "Run in background") + // COMPAT(detachRunFlag): --detach used to mean background execution, not + // ownership transfer. Added in v0.2.0; remove after 2027-01-17. + .addOption(new Option("--detach", "Legacy alias for --background").hideHelp()) + .option("--title ", "Assign a title to the agent") + .addOption(new Option("--name <name>", "Hidden alias for --title").hideHelp()) + .option( + "--provider <provider>", + "Agent provider, or provider/model (e.g. codex or codex/gpt-5.4)", + ) + .option( + "--model <model>", + "Model to use (e.g., claude-sonnet-4-20250514, claude-3-5-haiku-20241022)", + ) + .option("--thinking <id>", "Thinking option ID to use for this run") + .option("--mode <mode>", "Provider-specific mode (e.g., plan, default, bypass)") + .option("--isolation <local|worktree>", "Create a new workspace with this isolation") + .addOption(new Option("--worktree <name>", "Legacy workspace isolation alias").hideHelp()) + .option("--base <branch>", "Base branch for an isolated workspace") + .option( + "--workspace <id>", + "Run in an existing workspace (default: a new workspace is created per run; falls back to $PASEO_WORKSPACE_ID)", + ) + .option( + "--image <path>", + "Attach image(s) to the initial prompt (can be used multiple times)", + collectMultiple, + [], + ) + .option("--cwd <path>", "Working directory (default: current)") + .option( + "--env <key=value>", + "Set environment variable(s) for the agent process (can be used multiple times)", + collectMultiple, + [], + ) + .option( + "--label <key=value>", + "Add label(s) to the agent (can be used multiple times)", + collectMultiple, + [], + ) + .option( + "--wait-timeout <duration>", + "Maximum time to wait for agent to finish (e.g., 30s, 5m, 1h). Default: no limit", + ) + .option( + "--output-schema <schema>", + "Output JSON matching the provided schema file path or inline JSON schema", + ) + ); } /** Result type for agent run command */ @@ -91,6 +97,7 @@ export const agentRunSchema: OutputSchema<AgentRunResult> = { }; export interface AgentRunOptions extends CommandOptions { + background?: boolean; detach?: boolean; title?: string; name?: string; @@ -98,6 +105,7 @@ export interface AgentRunOptions extends CommandOptions { model?: string; thinking?: string; mode?: string; + isolation?: string; worktree?: string; base?: string; workspace?: string; @@ -270,40 +278,54 @@ function validateRunOptions(prompt: string, options: AgentRunOptions, outputSche } satisfies CommandError; } - if (options.base && !options.worktree) { + const createsIsolatedWorkspace = options.isolation === "worktree" || Boolean(options.worktree); + if (options.isolation && options.isolation !== "local" && options.isolation !== "worktree") { throw { code: "INVALID_OPTIONS", - message: "--base can only be used with --worktree", - details: "Usage: paseo agent run --worktree <name> --base <branch> <prompt>", + message: `Unsupported workspace isolation: ${options.isolation}`, + details: "Use --isolation local or --isolation worktree", } satisfies CommandError; } + if (options.base && !createsIsolatedWorkspace) { + throw { + code: "INVALID_OPTIONS", + message: "--base can only be used with --isolation worktree", + details: "Usage: paseo agent run --isolation worktree --base <branch> <prompt>", + } satisfies CommandError; + } + + if (options.isolation && options.workspace) { + throw { + code: "INVALID_OPTIONS", + message: "--isolation and --workspace cannot be combined", + details: "Select an existing workspace or create a new one with an isolation choice", + } satisfies CommandError; + } + + // COMPAT(worktreeRunFlag): --worktree implies a new worktree-isolated workspace. + // Added in v0.2.0; remove after 2027-01-17. if (options.worktree && options.workspace) { throw { code: "INVALID_OPTIONS", message: "--worktree and --workspace cannot be combined", - details: "--workspace runs in an existing workspace; --worktree mints a new one", + details: "Use --isolation worktree instead of the legacy --worktree flag", } satisfies CommandError; } - if (options.worktree && !options.workspace && process.env.PASEO_WORKSPACE_ID) { + if (outputSchema && runsInBackground(options)) { throw { code: "INVALID_OPTIONS", - message: "--worktree cannot be combined with an ambient PASEO_WORKSPACE_ID", - details: - "PASEO_WORKSPACE_ID selects an existing workspace; --worktree mints a new one. Unset PASEO_WORKSPACE_ID to use --worktree.", - } satisfies CommandError; - } - - if (outputSchema && options.detach) { - throw { - code: "INVALID_OPTIONS", - message: "--output-schema cannot be used with --detach", + message: "--output-schema cannot be used with --background", details: "Structured output requires waiting for the agent to finish", } satisfies CommandError; } } +function runsInBackground(options: Pick<AgentRunOptions, "background" | "detach">): boolean { + return Boolean(options.background || options.detach); +} + function parseWaitTimeoutOption(waitTimeout: string | undefined): number { if (!waitTimeout) return 0; try { @@ -409,45 +431,79 @@ async function connectToDaemonOrThrow( // runs in. The CLI resolves one before creating any agent, so no run leans on // createAgent's legacy cwd->workspace fallback. interface RunWorkspace { - id: string; + id?: string; cwd: string; } +export interface RunWorkspaceLookupClient { + fetchWorkspaces(options: { filter: { query: string }; page: { limit: number } }): Promise<{ + entries: Array<{ id: string; workspaceDirectory: string }>; + pageInfo: { nextCursor: string | null }; + }>; +} + +export async function resolveExistingRunWorkspace( + client: RunWorkspaceLookupClient, + workspaceId: string, +): Promise<RunWorkspace> { + const result = await client.fetchWorkspaces({ + filter: { query: workspaceId }, + page: { limit: 200 }, + }); + const workspace = result.entries.find((entry) => entry.id === workspaceId); + if (workspace) { + return { id: workspace.id, cwd: workspace.workspaceDirectory }; + } + + throw { + code: "WORKSPACE_NOT_FOUND", + message: `Workspace not found: ${workspaceId}`, + } satisfies CommandError; +} + // Workspace policy for `paseo run`. Precedence: // 1. --workspace <id> -> run in that existing workspace -// 2. $PASEO_WORKSPACE_ID -> exported by workspace terminals -// 3. --worktree <name> -> mint a new worktree-backed workspace -// 4. bare run -> mint a new local-backed workspace for cwd -// --worktree is rejected alongside both --workspace and an ambient -// $PASEO_WORKSPACE_ID (validateRunOptions), so worktree resolution here never -// races an existing-workspace selection. +// 2. $PASEO_AGENT_ID -> daemon resolves the caller's workspace +// 3. $PASEO_WORKSPACE_ID -> exported by workspace terminals +// 4. --isolation <kind> -> mint a new workspace with explicit isolation +// 5. bare run -> mint a new local-backed workspace for cwd async function resolveRunWorkspace( client: ConnectedDaemonClient, options: AgentRunOptions, cwd: string, ): Promise<RunWorkspace> { - // An explicit --worktree mints its own workspace; --workspace and an ambient - // PASEO_WORKSPACE_ID are both rejected alongside --worktree upstream. - const explicit = options.worktree - ? undefined - : options.workspace?.trim() || process.env.PASEO_WORKSPACE_ID?.trim(); + const requestedIsolation = options.isolation ?? (options.worktree ? "worktree" : undefined); + const explicit = requestedIsolation ? undefined : options.workspace?.trim(); if (explicit) { console.error(`Using workspace ${explicit}`); - return { id: explicit, cwd }; + return resolveExistingRunWorkspace(client, explicit); + } + + if (!requestedIsolation && resolveRunCallerAgentId()) { + return { cwd }; + } + + const ambientWorkspaceId = requestedIsolation + ? undefined + : process.env.PASEO_WORKSPACE_ID?.trim(); + if (ambientWorkspaceId) { + console.error(`Using workspace ${ambientWorkspaceId}`); + return resolveExistingRunWorkspace(client, ambientWorkspaceId); } // TODO: thread the run `prompt` as firstAgentContext so workspace-level // title/branch generation picks up the task description (U8/U6 deferred). - const result = options.worktree - ? await client.createWorkspace({ - source: { - kind: "worktree", - cwd, - worktreeSlug: options.worktree, - baseBranch: options.base, - }, - }) - : await client.createWorkspace({ source: { kind: "directory", path: cwd } }); + const result = + requestedIsolation === "worktree" + ? await client.createWorkspace({ + source: { + kind: "worktree", + cwd, + worktreeSlug: options.worktree, + baseBranch: options.base, + }, + }) + : await client.createWorkspace({ source: { kind: "directory", path: cwd } }); if (!result.workspace) { throw { @@ -503,6 +559,7 @@ export async function runRunCommand( const workspace = await resolveRunWorkspace(client, options, cwd); const workspaceId = workspace.id; + const callerAgentId = resolveRunCallerAgentId(); const runCwd = workspace.cwd; if (outputSchema) { @@ -514,6 +571,7 @@ export async function runRunCommand( provider: resolvedProviderModel.provider, cwd: runCwd, workspaceId, + callerAgentId, title: resolvedTitle, modeId: options.mode, model: resolvedProviderModel.model, @@ -584,6 +642,7 @@ export async function runRunCommand( provider: resolvedProviderModel.provider, cwd: runCwd, workspaceId, + callerAgentId, title: resolvedTitle, modeId: options.mode, model: resolvedProviderModel.model, @@ -594,8 +653,8 @@ export async function runRunCommand( labels: Object.keys(labels).length > 0 ? labels : undefined, }); - // Default run behavior is foreground: wait for completion unless --detach is set. - if (!options.detach) { + // Default run behavior is foreground: wait for completion unless background execution is set. + if (!runsInBackground(options)) { const state = await client.waitForFinish(agent.id, waitTimeoutMs); await client.close(); @@ -631,3 +690,9 @@ export async function runRunCommand( throw error; } } + +export function resolveRunCallerAgentId( + env: { PASEO_AGENT_ID?: string } = process.env, +): string | undefined { + return env.PASEO_AGENT_ID?.trim() || undefined; +} diff --git a/packages/cli/src/commands/heartbeat/index.ts b/packages/cli/src/commands/heartbeat/index.ts new file mode 100644 index 000000000..7ded358e8 --- /dev/null +++ b/packages/cli/src/commands/heartbeat/index.ts @@ -0,0 +1,180 @@ +import { Command } from "commander"; +import type { CommandOptions, OutputSchema, SingleResult } from "../../output/index.js"; +import { withOutput } from "../../output/index.js"; +import { addJsonAndDaemonHostOptions } from "../../utils/command-options.js"; +import { parseDuration } from "../../utils/duration.js"; +import { + connectScheduleClient, + toScheduleCommandError, + toScheduleRow, + type ScheduleRow, +} from "../schedule/shared.js"; +import { scheduleSchema } from "../schedule/schema.js"; + +interface HeartbeatOptions extends CommandOptions { + cron?: string; + timezone?: string; + name?: string; + maxRuns?: string; + expiresIn?: string; +} + +interface HeartbeatDeleteRow { + id: string; + status: "deleted"; +} + +const heartbeatDeleteSchema: OutputSchema<HeartbeatDeleteRow> = { + idField: "id", + columns: [ + { header: "ID", field: "id" }, + { header: "STATUS", field: "status" }, + ], +}; + +function requireCallerAgentId(): string { + const agentId = process.env.PASEO_AGENT_ID?.trim(); + if (!agentId) { + throw new Error("Heartbeat commands must run inside a Paseo agent"); + } + return agentId; +} + +async function requireOwnedHeartbeat( + client: Awaited<ReturnType<typeof connectScheduleClient>>["client"], + id: string, + agentId: string, +): Promise<void> { + const payload = await client.scheduleInspect({ id }); + if (payload.error || !payload.schedule) { + throw new Error(payload.error ?? `Heartbeat not found: ${id}`); + } + if (payload.schedule.target.type !== "agent" || payload.schedule.target.agentId !== agentId) { + throw new Error(`Heartbeat ${id} does not belong to agent ${agentId}`); + } +} + +async function runCreateHeartbeat( + prompt: string, + options: HeartbeatOptions, + _command: Command, +): Promise<SingleResult<ScheduleRow>> { + const agentId = requireCallerAgentId(); + const cron = options.cron?.trim(); + if (!cron) { + throw new Error("--cron is required"); + } + const { client } = await connectScheduleClient(options.host); + try { + const maxRuns = options.maxRuns ? Number.parseInt(options.maxRuns, 10) : undefined; + if (maxRuns !== undefined && (!Number.isSafeInteger(maxRuns) || maxRuns <= 0)) { + throw new Error("--max-runs must be a positive integer"); + } + const payload = await client.scheduleCreate({ + prompt: prompt.trim(), + cadence: { + type: "cron", + expression: cron, + ...(options.timezone?.trim() ? { timezone: options.timezone.trim() } : {}), + }, + target: { type: "agent", agentId }, + ...(options.name?.trim() ? { name: options.name.trim() } : {}), + ...(maxRuns ? { maxRuns } : {}), + ...(options.expiresIn + ? { expiresAt: new Date(Date.now() + parseDuration(options.expiresIn)).toISOString() } + : {}), + }); + if (payload.error || !payload.schedule) { + throw new Error(payload.error ?? "Heartbeat creation failed"); + } + return { type: "single", data: toScheduleRow(payload.schedule), schema: scheduleSchema }; + } catch (error) { + throw toScheduleCommandError("HEARTBEAT_CREATE_FAILED", "create heartbeat", error); + } finally { + await client.close().catch(() => undefined); + } +} + +async function runUpdateHeartbeat( + id: string, + options: HeartbeatOptions, + _command: Command, +): Promise<SingleResult<ScheduleRow>> { + const agentId = requireCallerAgentId(); + const cron = options.cron?.trim(); + if (!cron) { + throw new Error("--cron is required"); + } + const { client } = await connectScheduleClient(options.host); + try { + await requireOwnedHeartbeat(client, id, agentId); + const payload = await client.scheduleUpdate({ + id, + cadence: { + type: "cron", + expression: cron, + ...(options.timezone?.trim() ? { timezone: options.timezone.trim() } : {}), + }, + }); + if (payload.error || !payload.schedule) { + throw new Error(payload.error ?? `Heartbeat update failed: ${id}`); + } + return { type: "single", data: toScheduleRow(payload.schedule), schema: scheduleSchema }; + } catch (error) { + throw toScheduleCommandError("HEARTBEAT_UPDATE_FAILED", "update heartbeat", error); + } finally { + await client.close().catch(() => undefined); + } +} + +async function runDeleteHeartbeat( + id: string, + options: HeartbeatOptions, + _command: Command, +): Promise<SingleResult<HeartbeatDeleteRow>> { + const agentId = requireCallerAgentId(); + const { client } = await connectScheduleClient(options.host); + try { + await requireOwnedHeartbeat(client, id, agentId); + const payload = await client.scheduleDelete({ id }); + if (payload.error) { + throw new Error(payload.error); + } + return { + type: "single", + data: { id: payload.scheduleId, status: "deleted" }, + schema: heartbeatDeleteSchema, + }; + } catch (error) { + throw toScheduleCommandError("HEARTBEAT_DELETE_FAILED", "delete heartbeat", error); + } finally { + await client.close().catch(() => undefined); + } +} + +export function createHeartbeatCommand(): Command { + const heartbeat = new Command("heartbeat").description("Manage this agent's heartbeats"); + addJsonAndDaemonHostOptions( + heartbeat + .command("create") + .description("Create a recurring prompt for this agent") + .argument("<prompt>", "Prompt to send") + .requiredOption("--cron <expr>", "Five-field cron cadence") + .option("--timezone <iana>", "IANA time zone") + .option("--name <name>", "Heartbeat name") + .option("--max-runs <n>", "Maximum number of runs") + .option("--expires-in <duration>", "Time to live"), + ).action(withOutput(runCreateHeartbeat)); + addJsonAndDaemonHostOptions( + heartbeat + .command("update") + .description("Change a heartbeat cron cadence") + .argument("<id>", "Heartbeat ID") + .requiredOption("--cron <expr>", "Five-field cron cadence") + .option("--timezone <iana>", "IANA time zone"), + ).action(withOutput(runUpdateHeartbeat)); + addJsonAndDaemonHostOptions( + heartbeat.command("delete").description("Delete a heartbeat").argument("<id>", "Heartbeat ID"), + ).action(withOutput(runDeleteHeartbeat)); + return heartbeat; +} diff --git a/packages/cli/src/commands/schedule/index.ts b/packages/cli/src/commands/schedule/index.ts index 5087db18a..f72a59b19 100644 --- a/packages/cli/src/commands/schedule/index.ts +++ b/packages/cli/src/commands/schedule/index.ts @@ -1,4 +1,4 @@ -import { Command } from "commander"; +import { Command, Option } from "commander"; import { withOutput } from "../../output/index.js"; import { addJsonAndDaemonHostOptions } from "../../utils/command-options.js"; import { runCreateCommand } from "./create.js"; @@ -19,11 +19,11 @@ export function createScheduleCommand(): Command { .command("create") .description("Create a schedule") .argument("<prompt>", "Prompt to run on the schedule") - .option("--every <duration>", "Fixed interval cadence (for example: 5m, 1h)") + .option("--every <duration>", "Cron-compatible cadence preset (for example: 5m, 1h)") .option("--cron <expr>", "Cron cadence expression") .option("--timezone <iana>", "IANA time zone for cron cadence (default: UTC)") .option("--name <name>", "Optional schedule name") - .option("--target <self|new-agent|agent-id>", "Run target") + .addOption(new Option("--target <target>", "Legacy schedule target").hideHelp()) .option( "--provider <provider>", "Agent provider, or provider/model (e.g. codex or codex/gpt-5.4)", @@ -33,8 +33,7 @@ export function createScheduleCommand(): Command { "Provider-specific mode (e.g. claude bypassPermissions, opencode build)", ) .option("--cwd <path>", "Working directory (default: current; required with --host)") - .option("--run-now", "Fire one immediate run on creation (only with --cron)") - .option("--no-run-now", "Wait the full interval before the first run (only with --every)") + .option("--run-now", "Fire one immediate run on creation") .option("--max-runs <n>", "Maximum number of runs") .option("--expires-in <duration>", "Time to live for the schedule"), ).action(withOutput(runCreateCommand)); @@ -81,7 +80,7 @@ export function createScheduleCommand(): Command { .command("update") .description("Update an existing schedule in place") .argument("<id>", "Schedule ID") - .option("--every <duration>", "Switch to fixed interval cadence (for example: 5m, 1h)") + .option("--every <duration>", "Cron-compatible cadence preset (for example: 5m, 1h)") .option("--cron <expr>", "Switch to cron cadence expression") .option("--timezone <iana>", "IANA time zone for cron cadence (requires --cron)") .option("--name <name>", "Rename the schedule (empty string clears the name)") diff --git a/packages/cli/src/commands/schedule/inspect.ts b/packages/cli/src/commands/schedule/inspect.ts index d9044382f..d020f2408 100644 --- a/packages/cli/src/commands/schedule/inspect.ts +++ b/packages/cli/src/commands/schedule/inspect.ts @@ -22,6 +22,9 @@ export async function runInspectCommand( if (payload.error || !payload.schedule) { throw new Error(payload.error ?? `Schedule not found: ${id}`); } + if (payload.schedule.target.type !== "new-agent") { + throw new Error(`Schedule not found: ${id}`); + } const rows = createScheduleInspectRows(payload.schedule); return { type: "list", diff --git a/packages/cli/src/commands/schedule/logs.ts b/packages/cli/src/commands/schedule/logs.ts index b411822d9..c9565b5c0 100644 --- a/packages/cli/src/commands/schedule/logs.ts +++ b/packages/cli/src/commands/schedule/logs.ts @@ -3,6 +3,7 @@ import type { ListResult } from "../../output/index.js"; import { scheduleLogSchema, toScheduleLogRow, type ScheduleLogRow } from "./schema.js"; import { connectScheduleClient, + requireNewAgentSchedule, toScheduleCommandError, type ScheduleCommandOptions, } from "./shared.js"; @@ -14,6 +15,7 @@ export async function runLogsCommand( ): Promise<ListResult<ScheduleLogRow>> { const { client } = await connectScheduleClient(options.host); try { + await requireNewAgentSchedule(client, id); const payload = await client.scheduleLogs({ id }); if (payload.error) { throw new Error(payload.error); diff --git a/packages/cli/src/commands/schedule/ls.ts b/packages/cli/src/commands/schedule/ls.ts index da2c0dfbc..501c86653 100644 --- a/packages/cli/src/commands/schedule/ls.ts +++ b/packages/cli/src/commands/schedule/ls.ts @@ -21,7 +21,9 @@ export async function runLsCommand( } return { type: "list", - data: payload.schedules.map(toScheduleRow), + data: payload.schedules + .filter((schedule) => schedule.target.type === "new-agent") + .map(toScheduleRow), schema: scheduleSchema, }; } catch (error) { diff --git a/packages/cli/src/commands/schedule/pause.ts b/packages/cli/src/commands/schedule/pause.ts index 97e5914a5..ed8e12108 100644 --- a/packages/cli/src/commands/schedule/pause.ts +++ b/packages/cli/src/commands/schedule/pause.ts @@ -3,6 +3,7 @@ import type { SingleResult } from "../../output/index.js"; import { scheduleSchema } from "./schema.js"; import { connectScheduleClient, + requireNewAgentSchedule, toScheduleCommandError, toScheduleRow, type ScheduleCommandOptions, @@ -16,6 +17,7 @@ export async function runPauseCommand( ): Promise<SingleResult<ScheduleRow>> { const { client } = await connectScheduleClient(options.host); try { + await requireNewAgentSchedule(client, id); const payload = await client.schedulePause({ id }); if (payload.error || !payload.schedule) { throw new Error(payload.error ?? `Failed to pause schedule: ${id}`); diff --git a/packages/cli/src/commands/schedule/resume.ts b/packages/cli/src/commands/schedule/resume.ts index 88d4853dc..efac2d634 100644 --- a/packages/cli/src/commands/schedule/resume.ts +++ b/packages/cli/src/commands/schedule/resume.ts @@ -3,6 +3,7 @@ import type { SingleResult } from "../../output/index.js"; import { scheduleSchema } from "./schema.js"; import { connectScheduleClient, + requireNewAgentSchedule, toScheduleCommandError, toScheduleRow, type ScheduleCommandOptions, @@ -16,6 +17,7 @@ export async function runResumeCommand( ): Promise<SingleResult<ScheduleRow>> { const { client } = await connectScheduleClient(options.host); try { + await requireNewAgentSchedule(client, id); const payload = await client.scheduleResume({ id }); if (payload.error || !payload.schedule) { throw new Error(payload.error ?? `Failed to resume schedule: ${id}`); diff --git a/packages/cli/src/commands/schedule/run-once.ts b/packages/cli/src/commands/schedule/run-once.ts index 705e9548f..2818afc5d 100644 --- a/packages/cli/src/commands/schedule/run-once.ts +++ b/packages/cli/src/commands/schedule/run-once.ts @@ -3,6 +3,7 @@ import type { SingleResult } from "../../output/index.js"; import { scheduleSchema } from "./schema.js"; import { connectScheduleClient, + requireNewAgentSchedule, toScheduleCommandError, toScheduleRow, type ScheduleCommandOptions, @@ -16,6 +17,7 @@ export async function runRunOnceCommand( ): Promise<SingleResult<ScheduleRow>> { const { client } = await connectScheduleClient(options.host); try { + await requireNewAgentSchedule(client, id); const payload = await client.scheduleRunOnce({ id }); if (payload.error || !payload.schedule) { throw new Error(payload.error ?? `Failed to run schedule once: ${id}`); diff --git a/packages/cli/src/commands/schedule/shared.test.ts b/packages/cli/src/commands/schedule/shared.test.ts index e483e71aa..d1684e7ac 100644 --- a/packages/cli/src/commands/schedule/shared.test.ts +++ b/packages/cli/src/commands/schedule/shared.test.ts @@ -1,6 +1,10 @@ import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { parseScheduleCreateInput, parseScheduleUpdateInput } from "./shared.js"; +import { + compileEveryPresetToCron, + parseScheduleCreateInput, + parseScheduleUpdateInput, +} from "./shared.js"; const baseOptions = { prompt: "do the thing", @@ -68,13 +72,9 @@ describe("parseScheduleCreateInput cwd/host validation", () => { }); describe("parseScheduleCreateInput first-run timing", () => { - test("--every with no run-now flag fires immediately on creation", () => { + test("--every compiles to cron and waits for the next slot", () => { const input = parseScheduleCreateInput(baseOptions); - expect(input.runOnCreate).toBe(true); - }); - - test("--every with --no-run-now waits the interval", () => { - const input = parseScheduleCreateInput({ ...baseOptions, runNow: false }); + expect(input.cadence).toEqual({ type: "cron", expression: "*/5 * * * *" }); expect(input.runOnCreate).toBe(false); }); @@ -101,22 +101,9 @@ describe("parseScheduleCreateInput first-run timing", () => { }); }); - test("--every with --run-now is rejected as redundant", () => { - expect(() => parseScheduleCreateInput({ ...baseOptions, runNow: true })).toThrow( - expect.objectContaining({ - code: "REDUNDANT_RUN_NOW", - message: expect.stringContaining("--run-now is redundant with --every"), - }), - ); - }); - - test("--cron with --no-run-now is rejected as redundant", () => { - expect(() => parseScheduleCreateInput({ ...baseCron, runNow: false })).toThrow( - expect.objectContaining({ - code: "REDUNDANT_NO_RUN_NOW", - message: expect.stringContaining("--no-run-now is redundant with --cron"), - }), - ); + test("--run-now is orthogonal to a compiled preset", () => { + const input = parseScheduleCreateInput({ ...baseOptions, runNow: true }); + expect(input.runOnCreate).toBe(true); }); test("--timezone without --cron is rejected", () => { @@ -159,10 +146,10 @@ describe("parseScheduleUpdateInput", () => { ); }); - test("parses --every cadence", () => { + test("compiles --every cadence to cron", () => { expect(parseScheduleUpdateInput({ id: "abc", every: "5m" })).toEqual({ id: "abc", - cadence: { type: "every", everyMs: 5 * 60_000 }, + cadence: { type: "cron", expression: "*/5 * * * *" }, }); }); @@ -271,3 +258,25 @@ describe("parseScheduleUpdateInput", () => { ).toThrow(expect.objectContaining({ code: "CONFLICTING_EXPIRES" })); }); }); + +describe("compileEveryPresetToCron", () => { + test.each([ + ["1m", "*/1 * * * *"], + ["15m", "*/15 * * * *"], + ["1h", "0 * * * *"], + ["6h", "0 */6 * * *"], + ["1d", "0 0 * * *"], + ])("compiles %s", (value, expected) => { + expect(compileEveryPresetToCron(value)).toBe(expected); + }); + + test.each(["30s", "7m", "5h", "2d"])("rejects rolling interval %s", (value) => { + expect(() => compileEveryPresetToCron(value)).toThrow( + expect.objectContaining({ code: "UNREPRESENTABLE_CADENCE" }), + ); + }); + + test.each(["15minutes", "junk15m", "1h-nope"])("rejects malformed preset %s", (value) => { + expect(() => compileEveryPresetToCron(value)).toThrow("Invalid duration format"); + }); +}); diff --git a/packages/cli/src/commands/schedule/shared.ts b/packages/cli/src/commands/schedule/shared.ts index 643037c06..f70677db4 100644 --- a/packages/cli/src/commands/schedule/shared.ts +++ b/packages/cli/src/commands/schedule/shared.ts @@ -12,6 +12,7 @@ import type { } from "./types.js"; import { parseDuration } from "../../utils/duration.js"; import { resolveProviderAndModel } from "../../utils/provider-model.js"; +import { everyMsToFiveFieldCron } from "@getpaseo/protocol/schedule/cadence"; export interface ScheduleCommandOptions extends CommandOptions { host?: string; @@ -47,6 +48,16 @@ export function toScheduleCommandError(code: string, action: string, error: unkn }; } +export async function requireNewAgentSchedule( + client: ScheduleDaemonClient, + id: string, +): Promise<void> { + const payload = await client.scheduleInspect({ id }); + if (payload.error || !payload.schedule || payload.schedule.target.type !== "new-agent") { + throw new Error(payload.error ?? `Schedule not found: ${id}`); + } +} + export function formatCadence(cadence: ScheduleCadence): string { if (cadence.type === "cron") { const timezoneSuffix = cadence.timezone ? ` (${cadence.timezone})` : ""; @@ -92,12 +103,7 @@ function resolveScheduleTarget(args: { createNewAgentTarget: () => ScheduleTarget; }): ScheduleTarget { const { targetValue, hasExplicitNewAgentOption, createNewAgentTarget } = args; - const currentAgentId = process.env.PASEO_AGENT_ID?.trim(); - if (!targetValue) { - if (currentAgentId && !hasExplicitNewAgentOption) { - return { type: "self", agentId: currentAgentId }; - } return createNewAgentTarget(); } @@ -114,6 +120,9 @@ function resolveScheduleTarget(args: { } if (targetValue === "self") { + // COMPAT(scheduleSelfTarget): heartbeat creation moved to `paseo heartbeat create`. + // Added in v0.2.0; remove after 2027-01-17. + const currentAgentId = process.env.PASEO_AGENT_ID?.trim(); if (!currentAgentId) { throw { code: "INVALID_TARGET", @@ -211,23 +220,9 @@ export function parseScheduleCreateInput(options: { function resolveRunOnCreate( runNow: boolean | undefined, - cadenceType: ScheduleCadence["type"], + _cadenceType: ScheduleCadence["type"], ): boolean { - if (runNow === true && cadenceType === "every") { - throw { - code: "REDUNDANT_RUN_NOW", - message: "--run-now is redundant with --every (interval schedules already fire on creation)", - details: "Drop --run-now, or use --no-run-now to wait the full interval before the first run", - } satisfies CommandError; - } - if (runNow === false && cadenceType === "cron") { - throw { - code: "REDUNDANT_NO_RUN_NOW", - message: "--no-run-now is redundant with --cron (cron schedules never fire on creation)", - details: "Drop --no-run-now, or use --run-now to fire one immediate run on creation", - } satisfies CommandError; - } - return runNow ?? cadenceType === "every"; + return runNow ?? false; } export interface ScheduleUpdateOptionsInput { @@ -307,7 +302,7 @@ function parseCadenceFromFlags( } satisfies CommandError; } if (every !== undefined) { - return { type: "every", everyMs: parseDuration(every) }; + return { type: "cron", expression: compileEveryPresetToCron(every) }; } if (cron !== undefined) { return { @@ -319,6 +314,20 @@ function parseCadenceFromFlags( return undefined; } +export function compileEveryPresetToCron(value: string): string { + const durationMs = parseDuration(value); + const cron = everyMsToFiveFieldCron(durationMs); + if (cron) { + return cron; + } + + throw { + code: "UNREPRESENTABLE_CADENCE", + message: `${value} cannot be represented faithfully by five-field cron`, + details: "Use --cron for calendar schedules", + } satisfies CommandError; +} + function parseTimeZoneFlag(timeZone: string | undefined): string | undefined { if (timeZone === undefined) { return undefined; diff --git a/packages/cli/src/commands/schedule/update.ts b/packages/cli/src/commands/schedule/update.ts index 5b62590fd..30873853c 100644 --- a/packages/cli/src/commands/schedule/update.ts +++ b/packages/cli/src/commands/schedule/update.ts @@ -8,6 +8,7 @@ import { import { connectScheduleClient, parseScheduleUpdateInput, + requireNewAgentSchedule, toScheduleCommandError, type ScheduleCommandOptions, } from "./shared.js"; @@ -51,6 +52,7 @@ export async function runUpdateCommand( }); const { client } = await connectScheduleClient(options.host); try { + await requireNewAgentSchedule(client, id); const payload = await client.scheduleUpdate(input); if (payload.error || !payload.schedule) { throw new Error(payload.error ?? `Failed to update schedule: ${id}`); diff --git a/packages/cli/src/commands/workspace/archive.ts b/packages/cli/src/commands/workspace/archive.ts new file mode 100644 index 000000000..958d0451d --- /dev/null +++ b/packages/cli/src/commands/workspace/archive.ts @@ -0,0 +1,52 @@ +import type { Command } from "commander"; +import { connectToDaemon, getDaemonHost } from "../../utils/client.js"; +import type { CommandError, OutputSchema, SingleResult } from "../../output/index.js"; + +interface WorkspaceArchiveResult { + workspaceId: string; + status: "archived"; + archivedAt: string; +} + +const workspaceArchiveSchema: OutputSchema<WorkspaceArchiveResult> = { + idField: "workspaceId", + columns: [ + { header: "WORKSPACE ID", field: "workspaceId", width: 20 }, + { header: "STATUS", field: "status", width: 10 }, + { header: "ARCHIVED AT", field: "archivedAt", width: 26 }, + ], +}; + +export async function runArchiveCommand( + workspaceId: string, + options: { host?: string }, + _command: Command, +): Promise<SingleResult<WorkspaceArchiveResult>> { + const host = getDaemonHost({ host: options.host }); + const client = await connectToDaemon({ host: options.host }).catch((error: unknown) => { + const message = error instanceof Error ? error.message : String(error); + throw { + code: "DAEMON_NOT_RUNNING", + message: `Cannot connect to daemon at ${host}: ${message}`, + } satisfies CommandError; + }); + try { + const payload = await client.archiveWorkspace(workspaceId); + if (payload.error) { + throw new Error(payload.error); + } + if (!payload.archivedAt) { + throw new Error("Workspace archive did not return an archive timestamp"); + } + return { + type: "single", + data: { workspaceId, status: "archived", archivedAt: payload.archivedAt }, + schema: workspaceArchiveSchema, + }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw { code: "WORKSPACE_ARCHIVE_FAILED", message } satisfies CommandError; + } finally { + await client.close().catch(() => undefined); + } +} diff --git a/packages/cli/src/commands/workspace/create.test.ts b/packages/cli/src/commands/workspace/create.test.ts new file mode 100644 index 000000000..62bfcdc6f --- /dev/null +++ b/packages/cli/src/commands/workspace/create.test.ts @@ -0,0 +1,125 @@ +import { describe, expect, it } from "vitest"; +import { buildWorkspaceSource } from "./create.js"; + +describe("workspace create source", () => { + it("maps local isolation to a directory workspace", () => { + expect( + buildWorkspaceSource({ isolation: "local", path: "/tmp/project", project: "project-1" }), + ).toEqual({ kind: "directory", path: "/tmp/project", projectId: "project-1" }); + }); + + it("keeps branch names separate from managed worktree slugs", () => { + expect( + buildWorkspaceSource({ + isolation: "worktree", + path: "/tmp/project", + mode: "branch-off", + newBranch: "feature/auth", + worktreeSlug: "feature-auth", + base: "main", + }), + ).toEqual({ + kind: "worktree", + cwd: "/tmp/project", + action: "branch-off", + branchName: "feature/auth", + worktreeSlug: "feature-auth", + baseBranch: "main", + }); + }); + + it("uses a project as the worktree source without capturing the ambient directory", () => { + expect( + buildWorkspaceSource({ + isolation: "worktree", + project: "project-1", + mode: "branch-off", + newBranch: "fix-x", + }), + ).toEqual({ + kind: "worktree", + projectId: "project-1", + action: "branch-off", + branchName: "fix-x", + }); + }); + + it("checks out an existing branch into a worktree workspace", () => { + expect( + buildWorkspaceSource({ + isolation: "worktree", + path: "/tmp/project", + mode: "checkout-branch", + branch: "existing-work", + worktreeSlug: "existing-work-copy", + }), + ).toEqual({ + kind: "worktree", + cwd: "/tmp/project", + action: "checkout", + refName: "existing-work", + worktreeSlug: "existing-work-copy", + }); + }); + + it("checks out a pull request into a worktree workspace", () => { + expect( + buildWorkspaceSource({ + isolation: "worktree", + path: "/tmp/project", + mode: "checkout-pr", + prNumber: "42", + forge: "gitlab", + }), + ).toEqual({ + kind: "worktree", + cwd: "/tmp/project", + action: "checkout", + checkoutSource: { + kind: "change_request", + forge: "gitlab", + number: 42, + }, + }); + }); + + it("lets the source checkout select the forge when it is omitted", () => { + expect( + buildWorkspaceSource({ + isolation: "worktree", + path: "/tmp/project", + mode: "checkout-pr", + prNumber: "42", + }), + ).toEqual({ + kind: "worktree", + cwd: "/tmp/project", + action: "checkout", + checkoutSource: { + kind: "change_request", + number: 42, + }, + }); + }); + + it("requires the mode-specific checkout target", () => { + expect(() => buildWorkspaceSource({ isolation: "worktree", mode: "checkout-branch" })).toThrow( + "--branch is required", + ); + expect(() => buildWorkspaceSource({ isolation: "worktree", mode: "checkout-pr" })).toThrow( + "--pr-number is required", + ); + }); + + it("rejects worktree options for local isolation", () => { + expect(() => buildWorkspaceSource({ isolation: "local", mode: "branch-off" })).toThrow( + "Worktree options require --isolation worktree", + ); + }); + + it("rejects unknown isolation", () => { + expect(() => buildWorkspaceSource({ isolation: "container" })).toThrow( + "Unsupported workspace isolation", + ); + }); +}); diff --git a/packages/cli/src/commands/workspace/create.ts b/packages/cli/src/commands/workspace/create.ts new file mode 100644 index 000000000..078eefb06 --- /dev/null +++ b/packages/cli/src/commands/workspace/create.ts @@ -0,0 +1,161 @@ +import type { Command } from "commander"; +import { connectToDaemon, getDaemonHost } from "../../utils/client.js"; +import type { CommandError, CommandOptions, SingleResult } from "../../output/index.js"; +import { toWorkspaceRow, workspaceSchema, type WorkspaceRow } from "./shared.js"; + +export interface WorkspaceCreateOptions extends CommandOptions { + isolation?: string; + path?: string; + project?: string; + title?: string; + mode?: string; + worktreeSlug?: string; + newBranch?: string; + base?: string; + branch?: string; + prNumber?: string; + forge?: string; +} + +interface WorktreeSourceBase { + kind: "worktree"; + cwd?: string; + projectId?: string; + worktreeSlug?: string; +} + +function assertOptionsAbsent(values: unknown[], message: string): void { + if (values.some((value) => value !== undefined)) { + throw new Error(message); + } +} + +function buildLocalWorkspaceSource(options: WorkspaceCreateOptions, path: string) { + assertOptionsAbsent( + [ + options.mode, + options.worktreeSlug, + options.newBranch, + options.base, + options.branch, + options.prNumber, + options.forge, + ], + "Worktree options require --isolation worktree", + ); + return { + kind: "directory" as const, + path, + ...(options.project ? { projectId: options.project } : {}), + }; +} + +function buildBranchOffSource(options: WorkspaceCreateOptions, source: WorktreeSourceBase) { + assertOptionsAbsent( + [options.branch, options.prNumber, options.forge], + "--branch, --pr-number, and --forge require a checkout mode", + ); + return { + ...source, + action: "branch-off" as const, + ...(options.newBranch ? { branchName: options.newBranch } : {}), + ...(options.base ? { baseBranch: options.base } : {}), + }; +} + +function buildBranchCheckoutSource(options: WorkspaceCreateOptions, source: WorktreeSourceBase) { + if (!options.branch) { + throw new Error("--branch is required for --mode checkout-branch"); + } + assertOptionsAbsent( + [options.newBranch, options.base, options.prNumber, options.forge], + "--new-branch, --base, --pr-number, and --forge are not valid for --mode checkout-branch", + ); + return { ...source, action: "checkout" as const, refName: options.branch }; +} + +function buildPullRequestCheckoutSource( + options: WorkspaceCreateOptions, + source: WorktreeSourceBase, +) { + if (options.prNumber === undefined || options.prNumber === "") { + throw new Error("--pr-number is required for --mode checkout-pr"); + } + const prNumber = Number(options.prNumber); + if (!Number.isInteger(prNumber) || prNumber <= 0) { + throw new Error("--pr-number must be a positive integer"); + } + assertOptionsAbsent( + [options.newBranch, options.base, options.branch], + "--new-branch, --base, and --branch are not valid for --mode checkout-pr", + ); + return { + ...source, + action: "checkout" as const, + checkoutSource: { + kind: "change_request" as const, + ...(options.forge ? { forge: options.forge } : {}), + number: prNumber, + }, + }; +} + +function buildWorktreeWorkspaceSource(options: WorkspaceCreateOptions, path: string | undefined) { + const source: WorktreeSourceBase = { + kind: "worktree", + ...(path ? { cwd: path } : {}), + ...(options.project ? { projectId: options.project } : {}), + ...(options.worktreeSlug ? { worktreeSlug: options.worktreeSlug } : {}), + }; + switch (options.mode ?? "branch-off") { + case "branch-off": + return buildBranchOffSource(options, source); + case "checkout-branch": + return buildBranchCheckoutSource(options, source); + case "checkout-pr": + return buildPullRequestCheckoutSource(options, source); + default: + throw new Error(`Unsupported worktree mode: ${String(options.mode)}`); + } +} + +export function buildWorkspaceSource(options: WorkspaceCreateOptions) { + if (options.isolation === "local") { + return buildLocalWorkspaceSource(options, options.path ?? process.cwd()); + } + if (options.isolation === "worktree") { + const sourcePath = options.path ?? (options.project ? undefined : process.cwd()); + return buildWorktreeWorkspaceSource(options, sourcePath); + } + throw new Error(`Unsupported workspace isolation: ${String(options.isolation)}`); +} + +export async function runCreateCommand( + options: WorkspaceCreateOptions, + _command: Command, +): Promise<SingleResult<WorkspaceRow>> { + const host = getDaemonHost({ host: options.host }); + const client = await connectToDaemon({ host: options.host }).catch((error: unknown) => { + const message = error instanceof Error ? error.message : String(error); + throw { + code: "DAEMON_NOT_RUNNING", + message: `Cannot connect to daemon at ${host}: ${message}`, + } satisfies CommandError; + }); + + try { + const payload = await client.createWorkspace({ + source: buildWorkspaceSource(options), + ...(options.title ? { title: options.title } : {}), + }); + if (!payload.workspace) { + throw new Error(payload.error ?? "Workspace creation failed"); + } + return { type: "single", data: toWorkspaceRow(payload.workspace), schema: workspaceSchema }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw { code: "WORKSPACE_CREATE_FAILED", message } satisfies CommandError; + } finally { + await client.close().catch(() => undefined); + } +} diff --git a/packages/cli/src/commands/workspace/index.ts b/packages/cli/src/commands/workspace/index.ts new file mode 100644 index 000000000..27d7131dc --- /dev/null +++ b/packages/cli/src/commands/workspace/index.ts @@ -0,0 +1,43 @@ +import { Command } from "commander"; +import { withOutput } from "../../output/index.js"; +import { addJsonAndDaemonHostOptions } from "../../utils/command-options.js"; +import { runArchiveCommand } from "./archive.js"; +import { runCreateCommand } from "./create.js"; +import { runLsCommand } from "./ls.js"; + +export function createWorkspaceCommand(): Command { + const workspace = new Command("workspace").description("Manage workspaces"); + + addJsonAndDaemonHostOptions( + workspace + .command("create") + .description("Create a workspace") + .requiredOption("--isolation <local|worktree>", "Workspace isolation") + .option("--path <path>", "Local directory or source checkout (default: current)") + .option("--project <id>", "Existing project id") + .option("--title <title>", "Workspace title") + .option( + "--mode <mode>", + "Worktree mode: branch-off, checkout-branch, or checkout-pr (default: branch-off)", + ) + .option("--worktree-slug <slug>", "Managed worktree path slug") + .option("--new-branch <name>", "New branch name (--mode branch-off)") + .option("--base <ref>", "Base ref (--mode branch-off)") + .option("--branch <name>", "Existing branch (--mode checkout-branch)") + .option("--pr-number <n>", "Pull request or change request number (--mode checkout-pr)") + .option("--forge <forge>", "Forge for --mode checkout-pr (default: source checkout)"), + ).action(withOutput(runCreateCommand)); + + addJsonAndDaemonHostOptions(workspace.command("ls").description("List active workspaces")).action( + withOutput(runLsCommand), + ); + + addJsonAndDaemonHostOptions( + workspace + .command("archive") + .description("Archive a workspace and everything it owns") + .argument("<workspace-id>", "Workspace id"), + ).action(withOutput(runArchiveCommand)); + + return workspace; +} diff --git a/packages/cli/src/commands/workspace/ls.ts b/packages/cli/src/commands/workspace/ls.ts new file mode 100644 index 000000000..7ea9e8f56 --- /dev/null +++ b/packages/cli/src/commands/workspace/ls.ts @@ -0,0 +1,32 @@ +import type { Command } from "commander"; +import { connectToDaemon, getDaemonHost } from "../../utils/client.js"; +import type { CommandError, ListResult } from "../../output/index.js"; +import { toWorkspaceRow, workspaceSchema, type WorkspaceRow } from "./shared.js"; + +export async function runLsCommand( + options: { host?: string }, + _command: Command, +): Promise<ListResult<WorkspaceRow>> { + const host = getDaemonHost({ host: options.host }); + const client = await connectToDaemon({ host: options.host }).catch((error: unknown) => { + const message = error instanceof Error ? error.message : String(error); + throw { + code: "DAEMON_NOT_RUNNING", + message: `Cannot connect to daemon at ${host}: ${message}`, + } satisfies CommandError; + }); + try { + const workspaces: WorkspaceRow[] = []; + let cursor: string | undefined; + do { + const payload = await client.fetchWorkspaces({ + page: { limit: 200, ...(cursor ? { cursor } : {}) }, + }); + workspaces.push(...payload.entries.map(toWorkspaceRow)); + cursor = payload.pageInfo.nextCursor ?? undefined; + } while (cursor); + return { type: "list", data: workspaces, schema: workspaceSchema }; + } finally { + await client.close().catch(() => undefined); + } +} diff --git a/packages/cli/src/commands/workspace/shared.ts b/packages/cli/src/commands/workspace/shared.ts new file mode 100644 index 000000000..cd800c9f1 --- /dev/null +++ b/packages/cli/src/commands/workspace/shared.ts @@ -0,0 +1,31 @@ +import type { WorkspaceDescriptorPayload } from "@getpaseo/protocol/messages"; +import type { OutputSchema } from "../../output/index.js"; + +export interface WorkspaceRow { + workspaceId: string; + project: string; + name: string; + isolation: "local" | "worktree"; + cwd: string; +} + +export const workspaceSchema: OutputSchema<WorkspaceRow> = { + idField: "workspaceId", + columns: [ + { header: "WORKSPACE ID", field: "workspaceId", width: 20 }, + { header: "PROJECT", field: "project", width: 20 }, + { header: "NAME", field: "name", width: 22 }, + { header: "ISOLATION", field: "isolation", width: 10 }, + { header: "CWD", field: "cwd", width: 42 }, + ], +}; + +export function toWorkspaceRow(workspace: WorkspaceDescriptorPayload): WorkspaceRow { + return { + workspaceId: workspace.id, + project: workspace.projectDisplayName, + name: workspace.name, + isolation: workspace.workspaceKind === "worktree" ? "worktree" : "local", + cwd: workspace.workspaceDirectory, + }; +} diff --git a/packages/cli/src/utils/duration.ts b/packages/cli/src/utils/duration.ts index 1041917fd..0fe1dc274 100644 --- a/packages/cli/src/utils/duration.ts +++ b/packages/cli/src/utils/duration.ts @@ -1,6 +1,6 @@ /** * Parse duration string to milliseconds. - * Supports formats like: 5m, 30s, 1h, 2h30m, 90, etc. + * Supports formats like: 5m, 30s, 1h, 2h30m, 1d, 90, etc. * If no unit is specified, assumes seconds. */ export function parseDuration(input: string): number { @@ -11,14 +11,16 @@ export function parseDuration(input: string): number { return parseInt(trimmed, 10) * 1000; } + if (!/^(?:\d+[smhd])+$/.test(trimmed)) { + throw new Error(`Invalid duration format: ${input}. Use formats like: 5m, 30s, 1h, 2h30m, 1d`); + } + // Parse duration with units let totalMs = 0; - const regex = /(\d+)([smh])/g; + const regex = /(\d+)([smhd])/g; let match; - let hasMatch = false; while ((match = regex.exec(trimmed)) !== null) { - hasMatch = true; const value = parseInt(match[1], 10); const unit = match[2]; @@ -32,12 +34,11 @@ export function parseDuration(input: string): number { case "h": totalMs += value * 60 * 60 * 1000; break; + case "d": + totalMs += value * 24 * 60 * 60 * 1000; + break; } } - if (!hasMatch) { - throw new Error(`Invalid duration format: ${input}. Use formats like: 5m, 30s, 1h, 2h30m`); - } - return totalMs; } diff --git a/packages/cli/tests/05-agent-run.test.ts b/packages/cli/tests/05-agent-run.test.ts index 5b1fb97c4..1c2700a50 100644 --- a/packages/cli/tests/05-agent-run.test.ts +++ b/packages/cli/tests/05-agent-run.test.ts @@ -53,7 +53,8 @@ try { const result = await $`npx paseo run --help`.nothrow(); assert.strictEqual(result.exitCode, 0, "run --help should exit 0"); assert(result.stdout.includes("-d"), "help should mention -d flag"); - assert(result.stdout.includes("--detach"), "help should mention --detach flag"); + assert(result.stdout.includes("--background"), "help should mention --background flag"); + assert(!result.stdout.includes("--detach"), "help should hide legacy --detach syntax"); assert(result.stdout.includes("--title"), "help should mention --title option"); assert(result.stdout.includes("--provider"), "help should mention --provider option"); assert(result.stdout.includes("--mode"), "help should mention --mode option"); @@ -174,18 +175,18 @@ try { console.log("✓ run --output-schema flag is accepted\n"); } - // Test 10: run --output-schema cannot be used with --detach + // Test 10: run --output-schema cannot be used with --background { - console.log("Test 10: run --output-schema cannot be used with --detach"); + console.log("Test 10: run --output-schema cannot be used with --background"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo run -d --output-schema ${schemaPath} "test prompt"`.nothrow(); - assert.notStrictEqual(result.exitCode, 0, "should fail with --detach and --output-schema"); + await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo run --background --output-schema ${schemaPath} "test prompt"`.nothrow(); + assert.notStrictEqual(result.exitCode, 0, "should fail with --background and --output-schema"); const output = result.stdout + result.stderr; assert( - output.includes("--output-schema cannot be used with --detach"), - "error should explain detach incompatibility", + output.includes("--output-schema cannot be used with --background"), + "error should explain background incompatibility", ); - console.log("✓ run --output-schema cannot be used with --detach\n"); + console.log("✓ run --output-schema cannot be used with --background\n"); } // Test 11: -q (quiet) flag is accepted with run diff --git a/packages/cli/tests/14-worktree.test.ts b/packages/cli/tests/14-worktree.test.ts index 6e9289326..d1b30a541 100644 --- a/packages/cli/tests/14-worktree.test.ts +++ b/packages/cli/tests/14-worktree.test.ts @@ -153,13 +153,13 @@ try { console.log("✓ --json flag is accepted with worktree ls\n"); } - // Test 11: paseo --help shows worktree subcommand + // Test 11: paseo --help keeps the compatibility command hidden { - console.log("Test 11: paseo --help shows worktree subcommand"); + console.log("Test 11: paseo --help hides worktree compatibility command"); const result = await $`npx paseo --help`.nothrow(); assert.strictEqual(result.exitCode, 0, "paseo --help should exit 0"); - assert(result.stdout.includes("worktree"), "help should mention worktree subcommand"); - console.log("✓ paseo --help shows worktree subcommand\n"); + assert(!result.stdout.includes("worktree"), "help should not advertise worktree subcommand"); + console.log("✓ paseo --help hides worktree compatibility command\n"); } } finally { // Clean up temp directory diff --git a/packages/cli/tests/31-loop-schedule.test.ts b/packages/cli/tests/31-loop-schedule.test.ts index f5513bdcd..ae070f8fc 100644 --- a/packages/cli/tests/31-loop-schedule.test.ts +++ b/packages/cli/tests/31-loop-schedule.test.ts @@ -53,7 +53,7 @@ try { assert.strictEqual(created.exitCode, 0, created.stderr); const createdJson = JSON.parse(created.stdout); assert.strictEqual(createdJson.name, "review-prs"); - assert.strictEqual(createdJson.cadence, "every:5m"); + assert.strictEqual(createdJson.cadence, "cron:*/5 * * * *"); assert( typeof createdJson.target === "string" && (createdJson.target.startsWith("agent:") || createdJson.target === "new-agent:claude"), @@ -144,6 +144,31 @@ try { console.log("schedule rejects provider with self target\n"); } + { + console.log("Test 1d: compatibility agent-target schedules remain deletable"); + const created = await ctx.paseo( + [ + "schedule", + "create", + "Legacy heartbeat", + "--cron", + "0 0 1 1 *", + "--target", + "00000000-0000-4000-8000-000000000001", + "--json", + ], + { timeout: 30000 }, + ); + assert.strictEqual(created.exitCode, 0, created.stderr); + const createdJson = JSON.parse(created.stdout); + assert.strictEqual(createdJson.target, "agent:0000000"); + + const deleted = await ctx.paseo(["schedule", "delete", createdJson.id, "--json"]); + assert.strictEqual(deleted.exitCode, 0, deleted.stderr); + assert.strictEqual(JSON.parse(deleted.stdout).id, createdJson.id); + console.log("compatibility agent-target schedules remain deletable\n"); + } + { console.log("Test 2: loop run/ls/inspect/logs/stop work"); const run = await ctx.paseo( diff --git a/packages/client/src/daemon-client.test.ts b/packages/client/src/daemon-client.test.ts index 37a766d23..b1d6bfcf9 100644 --- a/packages/client/src/daemon-client.test.ts +++ b/packages/client/src/daemon-client.test.ts @@ -723,7 +723,7 @@ test("sends new-agent run options when creating schedules", async () => { const createPromise = client.scheduleCreate({ requestId: "request-1", prompt: "Run the task", - cadence: { type: "every", everyMs: 60_000 }, + cadence: { type: "cron", expression: "* * * * *" }, target: { type: "new-agent", config: { @@ -741,7 +741,7 @@ test("sends new-agent run options when creating schedules", async () => { type: "schedule/create", requestId: "request-1", prompt: "Run the task", - cadence: { type: "every", everyMs: 60_000 }, + cadence: { type: "cron", expression: "* * * * *" }, target: { type: "new-agent", config: { @@ -1991,7 +1991,7 @@ test("normalizes workspace_setup_progress into a workspace-scoped daemon event", }); }); -test("sends create_agent_request with string workspace ids", async () => { +test("sends create_agent_request with workspace and caller identity", async () => { const logger = createMockLogger(); const mock = createMockTransport(); @@ -2012,6 +2012,7 @@ test("sends create_agent_request with string workspace ids", async () => { provider: "codex", cwd: "/tmp/project/.paseo/worktrees/feature-a", workspaceId: "ws-feature-a", + callerAgentId: "parent-agent", title: "Compat agent", modeId: "default", }); @@ -2022,6 +2023,7 @@ test("sends create_agent_request with string workspace ids", async () => { expect.objectContaining({ type: "create_agent_request", workspaceId: "ws-feature-a", + callerAgentId: "parent-agent", }), ); diff --git a/packages/client/src/daemon-client.ts b/packages/client/src/daemon-client.ts index 6eaa0b155..c0ecc10c4 100644 --- a/packages/client/src/daemon-client.ts +++ b/packages/client/src/daemon-client.ts @@ -338,6 +338,7 @@ export interface CreateAgentRequestOptions extends AgentConfigOverrides { cwd?: string; env?: CreateAgentRequestMessage["env"]; workspaceId?: string; + callerAgentId?: string; initialPrompt?: string; clientMessageId?: string; outputSchema?: Record<string, unknown>; @@ -346,6 +347,8 @@ export interface CreateAgentRequestOptions extends AgentConfigOverrides { git?: GitSetupOptions; worktree?: CreateAgentRequestMessage["worktree"]; autoArchive?: CreateAgentRequestMessage["autoArchive"]; + // COMPAT(createAgentWorktree): low-level old callers may still send the + // create-agent worktree field. Added in v0.2.0; remove after 2027-01-17. worktreeName?: string; requestId?: string; labels?: Record<string, string>; @@ -739,16 +742,11 @@ export interface StopLoopOptions { export interface CreateScheduleOptions { prompt: string; name?: string | null; - cadence: - | { - type: "every"; - everyMs: number; - } - | { - type: "cron"; - expression: string; - timezone?: string; - }; + cadence: { + type: "cron"; + expression: string; + timezone?: string; + }; target: | { type: "self"; @@ -800,16 +798,11 @@ export interface UpdateScheduleOptions { id: string; name?: string | null; prompt?: string; - cadence?: - | { - type: "every"; - everyMs: number; - } - | { - type: "cron"; - expression: string; - timezone?: string; - }; + cadence?: { + type: "cron"; + expression: string; + timezone?: string; + }; newAgentConfig?: UpdateScheduleNewAgentConfig; maxRuns?: number | null; expiresAt?: string | null; @@ -2290,6 +2283,7 @@ export class DaemonClient { config, ...(options.env ? { env: options.env } : {}), ...(options.workspaceId !== undefined ? { workspaceId: options.workspaceId } : {}), + ...(options.callerAgentId !== undefined ? { callerAgentId: options.callerAgentId } : {}), ...(options.initialPrompt ? { initialPrompt: options.initialPrompt } : {}), ...(options.clientMessageId ? { clientMessageId: options.clientMessageId } : {}), ...(options.outputSchema ? { outputSchema: options.outputSchema } : {}), diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts index fe9cc1998..d7f21f124 100644 --- a/packages/client/src/index.ts +++ b/packages/client/src/index.ts @@ -168,13 +168,13 @@ export interface PaseoAgentCreateOptions extends PaseoAgentConfigOverrides { provider?: CreateAgentRequestMessage["config"]["provider"]; cwd?: string; workspaceId?: string; + callerAgentId?: string; initialPrompt?: string; clientMessageId?: string; outputSchema?: Record<string, unknown>; images?: CreateAgentRequestMessage["images"]; attachments?: CreateAgentRequestMessage["attachments"]; git?: CreateAgentRequestMessage["git"]; - worktreeName?: string; requestId?: string; labels?: Record<string, string>; } diff --git a/packages/protocol/src/messages.ts b/packages/protocol/src/messages.ts index 48e9f106d..f1338bd45 100644 --- a/packages/protocol/src/messages.ts +++ b/packages/protocol/src/messages.ts @@ -981,7 +981,7 @@ const AgentAttachmentsSchema = z.unknown().transform(normalizeAgentAttachments). export const ChangeRequestCheckoutSourceSchema = z.object({ kind: z.literal("change_request"), - forge: z.string().optional().default("github"), + forge: z.string().optional(), number: z.number().int().positive(), projectPath: z.string().optional(), }); @@ -1249,6 +1249,9 @@ export const CreateAgentRequestMessageSchema = z.object({ config: AgentSessionConfigSchema, env: z.record(z.string(), z.string()).optional(), workspaceId: z.string().optional(), + // Optional caller context lets managed CLI invocations use the same daemon-owned + // workspace and parentage policy as agent-scoped MCP creation. + callerAgentId: z.string().optional(), worktreeName: z.string().optional(), initialPrompt: z.string().optional(), clientMessageId: z.string().optional(), @@ -2061,9 +2064,11 @@ export const WorkspaceCreateRequestSchema = z.object({ cwd: z.string().optional(), projectId: z.string().optional(), action: z.enum(["branch-off", "checkout"]).optional(), - // Target branch name for checkout, or new branch name for branch-off. + // Target branch for checkout, or base ref for branch-off. refName: z.string().min(1).optional(), baseBranch: z.string().optional(), + // New branch name for branch-off. The worktree path may use a different slug. + branchName: z.string().min(1).optional(), checkoutSource: ChangeRequestCheckoutSourceSchema.optional(), // COMPAT(githubPrNumber): added in v0.1.106, remove after 2026-12-28 once // clients send checkoutSource. diff --git a/packages/protocol/src/messages.workspaces.test.ts b/packages/protocol/src/messages.workspaces.test.ts index 59c772874..a1c07a5ca 100644 --- a/packages/protocol/src/messages.workspaces.test.ts +++ b/packages/protocol/src/messages.workspaces.test.ts @@ -1031,6 +1031,25 @@ describe("workspace message schemas", () => { expect(newWorktree.type).toBe("workspace.create.request"); expect(newWorktree.source.kind).toBe("worktree"); + const branchOff = WorkspaceCreateRequestSchema.parse({ + type: "workspace.create.request", + requestId: "req-branch-off", + source: { + kind: "worktree", + cwd: "/tmp/repo", + action: "branch-off", + branchName: "feature/auth", + worktreeSlug: "feature-auth", + }, + }); + expect(branchOff.source).toEqual({ + kind: "worktree", + cwd: "/tmp/repo", + action: "branch-off", + branchName: "feature/auth", + worktreeSlug: "feature-auth", + }); + // Directory source must also be accepted. const newDirectory = WorkspaceCreateRequestSchema.parse({ type: "workspace.create.request", diff --git a/packages/protocol/src/schedule/cadence.test.ts b/packages/protocol/src/schedule/cadence.test.ts new file mode 100644 index 000000000..3fd3a4af5 --- /dev/null +++ b/packages/protocol/src/schedule/cadence.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, it } from "vitest"; +import { everyMsToFiveFieldCron } from "./cadence.js"; + +describe("everyMsToFiveFieldCron", () => { + it.each([ + [60_000, "*/1 * * * *"], + [15 * 60_000, "*/15 * * * *"], + [60 * 60_000, "0 * * * *"], + [6 * 60 * 60_000, "0 */6 * * *"], + [24 * 60 * 60_000, "0 0 * * *"], + ])("converts %i milliseconds", (everyMs, cron) => { + expect(everyMsToFiveFieldCron(everyMs)).toBe(cron); + }); + + it.each([30_000, 7 * 60_000, 5 * 60 * 60_000, 48 * 60 * 60_000])( + "rejects unrepresentable interval %i", + (everyMs) => { + expect(everyMsToFiveFieldCron(everyMs)).toBeNull(); + }, + ); +}); diff --git a/packages/protocol/src/schedule/cadence.ts b/packages/protocol/src/schedule/cadence.ts new file mode 100644 index 000000000..57abb7e7d --- /dev/null +++ b/packages/protocol/src/schedule/cadence.ts @@ -0,0 +1,27 @@ +/** + * Convert an exact rolling interval to an equivalent five-field cron cadence. + * Returns null when cron's calendar boundaries would change the interval. + */ +export function everyMsToFiveFieldCron(everyMs: number): string | null { + const minutes = everyMs / 60_000; + if (!Number.isInteger(minutes) || minutes <= 0) { + return null; + } + if (minutes < 60 && 60 % minutes === 0) { + return `*/${minutes} * * * *`; + } + if (minutes === 60) { + return "0 * * * *"; + } + if (minutes % 60 !== 0) { + return null; + } + const hours = minutes / 60; + if (hours < 24 && 24 % hours === 0) { + return `0 */${hours} * * *`; + } + if (hours === 24) { + return "0 0 * * *"; + } + return null; +} diff --git a/packages/server/src/server/agent/agent-prompt.test.ts b/packages/server/src/server/agent/agent-prompt.test.ts index d726509a8..f42061698 100644 --- a/packages/server/src/server/agent/agent-prompt.test.ts +++ b/packages/server/src/server/agent/agent-prompt.test.ts @@ -38,13 +38,17 @@ function createCapturedLogger(): CapturedLogger { interface FinishNotificationScenarioOptions { childLastAssistantMessage?: string | null; + childParentAgentId?: string | null; + requireParentOwnership?: boolean; parentPromptError?: Error; logger?: Logger; } interface FinishNotificationScenario { startWatchingChild(): void; + finishChild(): void; finishChildAndReadParentPrompt(): Promise<string>; + wasParentPrompted(): boolean; } function createFinishNotificationScenario( @@ -52,6 +56,7 @@ function createFinishNotificationScenario( ): FinishNotificationScenario { let subscriber: ((event: AgentManagerEvent) => void) | null = null; let resolveParentPrompt: ((prompt: string) => void) | null = null; + let parentPrompted = false; const childAgent: ManagedAgent = Object.create(null); Reflect.set(childAgent, "id", "child-agent"); @@ -85,6 +90,7 @@ function createFinishNotificationScenario( Reflect.set(agentManager, "tryRunOutOfBand", () => false); Reflect.set(agentManager, "hasInFlightRun", () => Boolean(options?.parentPromptError)); Reflect.set(agentManager, "streamAgent", (_agentId: string, prompt: string) => { + parentPrompted = true; resolveParentPrompt?.(prompt); return (async function* noop() {})(); }); @@ -96,7 +102,12 @@ function createFinishNotificationScenario( const agentStorage: AgentStorage = Object.create(AgentStorage.prototype); Reflect.set(agentStorage, "get", async (agentId: string) => { if (agentId === "child-agent") { - return { title: "Child Agent" }; + const parentAgentId = + options?.childParentAgentId === undefined ? "caller-agent" : options.childParentAgentId; + return { + title: "Child Agent", + labels: parentAgentId ? { "paseo.parent-agent-id": parentAgentId } : {}, + }; } return null; }); @@ -108,14 +119,11 @@ function createFinishNotificationScenario( agentStorage, childAgentId: "child-agent", callerAgentId: "caller-agent", + requireParentOwnership: options?.requireParentOwnership, logger: options?.logger ?? createTestLogger(), }); }, - async finishChildAndReadParentPrompt() { - const parentPrompt = new Promise<string>((resolve) => { - resolveParentPrompt = resolve; - }); - + finishChild() { childAgent.lifecycle = "running"; subscriber?.({ type: "agent_state", @@ -127,9 +135,18 @@ function createFinishNotificationScenario( type: "agent_state", agent: childAgent, }); + }, + async finishChildAndReadParentPrompt() { + const parentPrompt = new Promise<string>((resolve) => { + resolveParentPrompt = resolve; + }); + this.finishChild(); return parentPrompt; }, + wasParentPrompted() { + return parentPrompted; + }, }; } @@ -192,6 +209,26 @@ test("finish notifications tell the parent the child's last assistant message", ); }); +test("detaching a child ends its parent-owned finish notification", async () => { + const scenario = createFinishNotificationScenario({ + childParentAgentId: null, + requireParentOwnership: true, + }); + scenario.startWatchingChild(); + scenario.finishChild(); + await new Promise((resolve) => setTimeout(resolve, 0)); + expect(scenario.wasParentPrompted()).toBe(false); +}); + +test("follow-up finish notifications do not require a parent relationship", async () => { + const scenario = createFinishNotificationScenario({ childParentAgentId: "another-agent" }); + + scenario.startWatchingChild(); + const parentPrompt = await scenario.finishChildAndReadParentPrompt(); + + expect(parentPrompt).toContain("Agent child-agent (Child Agent) finished."); +}); + test("finish notifications log a rejected parent prompt without an unhandled rejection", async () => { const captured = createCapturedLogger(); const scenario = createFinishNotificationScenario({ diff --git a/packages/server/src/server/agent/agent-prompt.ts b/packages/server/src/server/agent/agent-prompt.ts index 325fa0dcb..96b4c9c29 100644 --- a/packages/server/src/server/agent/agent-prompt.ts +++ b/packages/server/src/server/agent/agent-prompt.ts @@ -4,6 +4,7 @@ import type { AgentPromptInput, AgentRunOptions } from "./agent-sdk-types.js"; import type { AgentManager, ManagedAgent } from "./agent-manager.js"; import type { AgentStorage } from "./agent-storage.js"; import { ensureAgentLoaded } from "./agent-loading.js"; +import { getParentAgentIdFromLabels } from "@getpaseo/protocol/agent-labels"; export type AgentUnarchiveController = Pick<AgentManager, "notifyAgentState" | "unarchiveSnapshot">; @@ -244,6 +245,7 @@ export interface SetupFinishNotificationParams { agentStorage: AgentStorage; childAgentId: string; callerAgentId: string; + requireParentOwnership?: boolean; logger: Logger; } @@ -264,7 +266,14 @@ function formatFinishNotificationBody(params: FinishNotificationBodyInput): stri } export function setupFinishNotification(params: SetupFinishNotificationParams): void { - const { agentManager, agentStorage, childAgentId, callerAgentId, logger } = params; + const { + agentManager, + agentStorage, + childAgentId, + callerAgentId, + requireParentOwnership = false, + logger, + } = params; let hasSeenRunning = false; let fired = false; let unsubscribe: (() => void) | null = null; @@ -282,6 +291,9 @@ export function setupFinishNotification(params: SetupFinishNotificationParams): } const record = await agentStorage.get(childAgentId); + if (requireParentOwnership && getParentAgentIdFromLabels(record?.labels) !== callerAgentId) { + return; + } const title = record?.title ?? childAgentId; const lastAssistantMessage = await agentManager.getLastAssistantMessage(childAgentId); const body = formatFinishNotificationBody({ diff --git a/packages/server/src/server/agent/create-agent/create.ts b/packages/server/src/server/agent/create-agent/create.ts index 539f324d2..182edc833 100644 --- a/packages/server/src/server/agent/create-agent/create.ts +++ b/packages/server/src/server/agent/create-agent/create.ts @@ -1,6 +1,5 @@ import type { Logger } from "pino"; -import { PARENT_AGENT_ID_LABEL } from "@getpaseo/protocol/agent-labels"; import type { TerminalManager } from "../../../terminal/terminal-manager.js"; import type { CreatePaseoWorktreeInput } from "../../paseo-worktree-service.js"; import { expandUserPath, resolvePathFromBase } from "../../path-utils.js"; @@ -26,6 +25,7 @@ import { appendTimelineItemIfAgentKnown, emitLiveTimelineItemIfAgentKnown, } from "../timeline-append.js"; +import { resolveCreateAgentIntent } from "./intent.js"; export interface CreateAgentSessionWorktreeResult { sessionConfig: AgentSessionConfig; @@ -208,6 +208,7 @@ export async function createAgentCommand( agentStorage: dependencies.agentStorage, childAgentId: snapshot.id, callerAgentId: input.callerAgentId, + requireParentOwnership: true, logger: dependencies.logger, }); } @@ -317,13 +318,21 @@ async function resolveMcpCreateAgent( }); if (createdWorktree) input.onWorktreeCreated?.(createdWorktree); - const workspaceId = await resolveMcpWorkspaceId({ - dependencies, - input, - parentAgent, - setupContinuation, - createdWorkspaceId, - resolvedCwd, + const intent = await resolveCreateAgentIntent({ + explicitWorkspaceId: setupContinuation ? createdWorkspaceId : input.workspaceId, + caller: parentAgent + ? { id: parentAgent.id, cwd: parentAgent.cwd, workspaceId: parentAgent.workspaceId } + : null, + labels: input.labels, + childAgentDefaultLabels: input.callerContext?.childAgentDefaultLabels, + legacyDetached: input.detached ?? false, + resolveWorkspace: async (workspaceId) => ({ workspaceId, cwd: resolvedCwd }), + createWorkspace: async () => ({ + workspaceId: requireResolvedWorkspaceId( + await ensureWorkspaceForMcpCreate(dependencies, resolvedCwd, input.initialPrompt ?? ""), + ), + cwd: resolvedCwd, + }), }); const resolvedCreateConfig = await resolveMcpProviderCreateConfig({ dependencies, @@ -333,27 +342,20 @@ async function resolveMcpCreateAgent( parentAgent, }); - const labels = mergeLabels({ - callerAgentId: input.callerAgentId, - detached: input.detached ?? false, - childAgentDefaultLabels: input.callerContext?.childAgentDefaultLabels, - labels: input.labels, - }); - const trimmedPrompt = input.initialPrompt?.trim() ?? ""; return { config: buildMcpSessionConfig({ input, resolvedProviderModel, provider, - resolvedCwd, + resolvedCwd: intent.cwd, trimmedPrompt, resolvedMode: resolvedCreateConfig.modeId, resolvedFeatures: resolvedCreateConfig.featureValues, }), createOptions: { - ...(labels ? { labels } : {}), - workspaceId: requireResolvedWorkspaceId(workspaceId), + ...(Object.keys(intent.labels).length > 0 ? { labels: intent.labels } : {}), + workspaceId: intent.workspaceId, owner: input.owner, env: input.env, }, @@ -380,34 +382,6 @@ function resolveMcpInitialCwd( }); } -async function resolveMcpWorkspaceId(params: { - dependencies: CreateAgentCommandDependencies; - input: CreateAgentFromMcpInput; - parentAgent: ManagedAgent | null; - setupContinuation?: AgentWorktreeSetupContinuation; - createdWorkspaceId?: string; - resolvedCwd: string; -}): Promise<string | undefined> { - // MCP callers resolve workspace ownership before this point. Worktree - // creation wins because the new agent lives in the fresh worktree workspace. - // Otherwise use the explicit workspace id, then the parent workspace for - // direct internal callers. Ownership is never resolved from cwd. - if (params.setupContinuation) { - return params.createdWorkspaceId; - } - if (params.input.workspaceId) { - return params.input.workspaceId; - } - if (params.parentAgent?.workspaceId) { - return params.parentAgent.workspaceId; - } - return ensureWorkspaceForMcpCreate( - params.dependencies, - params.resolvedCwd, - params.input.initialPrompt ?? "", - ); -} - async function resolveMcpProviderCreateConfig(params: { dependencies: CreateAgentCommandDependencies; input: CreateAgentFromMcpInput; @@ -624,22 +598,3 @@ async function createMcpWorktree( throw toWorktreeRequestError(error); } } - -function mergeLabels(params: { - callerAgentId: string | undefined; - detached: boolean; - childAgentDefaultLabels: Record<string, string> | undefined; - labels: Record<string, string> | undefined; -}): Record<string, string> | undefined { - const mergedLabels = { - ...(!params.detached && params.callerAgentId - ? { [PARENT_AGENT_ID_LABEL]: params.callerAgentId } - : {}), - ...params.childAgentDefaultLabels, - ...params.labels, - }; - if (params.detached) { - delete mergedLabels[PARENT_AGENT_ID_LABEL]; - } - return Object.keys(mergedLabels).length > 0 ? mergedLabels : undefined; -} diff --git a/packages/server/src/server/agent/create-agent/intent.test.ts b/packages/server/src/server/agent/create-agent/intent.test.ts new file mode 100644 index 000000000..38f390d3f --- /dev/null +++ b/packages/server/src/server/agent/create-agent/intent.test.ts @@ -0,0 +1,74 @@ +import { describe, expect, it } from "vitest"; +import { PARENT_AGENT_ID_LABEL } from "@getpaseo/protocol/agent-labels"; +import { resolveCreateAgentIntent } from "./intent.js"; + +describe("resolveCreateAgentIntent", () => { + it("keeps caller parentage when an explicit workspace changes placement", async () => { + const intent = await resolveCreateAgentIntent({ + explicitWorkspaceId: "workspace-isolated", + caller: { id: "parent-agent", cwd: "/parent", workspaceId: "workspace-parent" }, + labels: { purpose: "review" }, + resolveWorkspace: async (workspaceId) => ({ workspaceId, cwd: "/isolated" }), + createWorkspace: async () => ({ workspaceId: "workspace-created", cwd: "/created" }), + }); + + expect(intent).toEqual({ + workspaceId: "workspace-isolated", + cwd: "/isolated", + parentAgentId: "parent-agent", + labels: { + purpose: "review", + [PARENT_AGENT_ID_LABEL]: "parent-agent", + }, + }); + }); + + it("defaults an agent caller to its workspace without creating one", async () => { + let createCount = 0; + const intent = await resolveCreateAgentIntent({ + caller: { id: "parent-agent", cwd: "/parent", workspaceId: "workspace-parent" }, + resolveWorkspace: async (workspaceId) => ({ workspaceId, cwd: "/unused" }), + createWorkspace: async () => { + createCount += 1; + return { workspaceId: "workspace-created", cwd: "/created" }; + }, + }); + + expect(intent.workspaceId).toBe("workspace-parent"); + expect(intent.cwd).toBe("/parent"); + expect(intent.parentAgentId).toBe("parent-agent"); + expect(createCount).toBe(0); + }); + + it("creates a workspace for a human caller with no workspace context", async () => { + const intent = await resolveCreateAgentIntent({ + caller: null, + resolveWorkspace: async (workspaceId) => ({ workspaceId, cwd: "/unused" }), + createWorkspace: async () => ({ workspaceId: "workspace-created", cwd: "/created" }), + }); + + expect(intent).toEqual({ + workspaceId: "workspace-created", + cwd: "/created", + parentAgentId: null, + labels: {}, + }); + }); + + it("keeps legacy detached creation independent", async () => { + const intent = await resolveCreateAgentIntent({ + caller: { id: "parent-agent", cwd: "/parent", workspaceId: "workspace-parent" }, + labels: { [PARENT_AGENT_ID_LABEL]: "spoofed-parent" }, + legacyDetached: true, + resolveWorkspace: async (workspaceId) => ({ workspaceId, cwd: "/unused" }), + createWorkspace: async () => ({ workspaceId: "workspace-created", cwd: "/created" }), + }); + + expect(intent).toEqual({ + workspaceId: "workspace-parent", + cwd: "/parent", + parentAgentId: null, + labels: {}, + }); + }); +}); diff --git a/packages/server/src/server/agent/create-agent/intent.ts b/packages/server/src/server/agent/create-agent/intent.ts new file mode 100644 index 000000000..c811144b7 --- /dev/null +++ b/packages/server/src/server/agent/create-agent/intent.ts @@ -0,0 +1,64 @@ +import { PARENT_AGENT_ID_LABEL } from "@getpaseo/protocol/agent-labels"; + +export interface CreateAgentCaller { + id: string; + cwd: string; + workspaceId?: string; +} + +export interface CreateAgentPlacement { + workspaceId: string; + cwd: string; +} + +export interface CreateAgentIntent { + workspaceId: string; + cwd: string; + parentAgentId: string | null; + labels: Record<string, string>; +} + +export async function resolveCreateAgentIntent(input: { + explicitWorkspaceId?: string; + caller: CreateAgentCaller | null; + labels?: Record<string, string>; + childAgentDefaultLabels?: Record<string, string>; + resolveWorkspace: (workspaceId: string) => Promise<CreateAgentPlacement>; + createWorkspace: () => Promise<CreateAgentPlacement>; + legacyDetached?: boolean; +}): Promise<CreateAgentIntent> { + const parentAgentId = input.legacyDetached ? null : (input.caller?.id ?? null); + const placement = await resolvePlacement(input); + const labels = { + ...input.childAgentDefaultLabels, + ...input.labels, + ...(parentAgentId ? { [PARENT_AGENT_ID_LABEL]: parentAgentId } : {}), + }; + + // COMPAT(detachedCreate): legacy callers may still request detached creation. + // Added in v0.2.0; remove after 2027-01-17 once detached creation is outside the floor. + // The delete also strips a parent label injected through input.labels. + if (input.legacyDetached) { + delete labels[PARENT_AGENT_ID_LABEL]; + } + + return { ...placement, parentAgentId, labels }; +} + +async function resolvePlacement(input: { + explicitWorkspaceId?: string; + caller: CreateAgentCaller | null; + resolveWorkspace: (workspaceId: string) => Promise<CreateAgentPlacement>; + createWorkspace: () => Promise<CreateAgentPlacement>; +}): Promise<CreateAgentPlacement> { + if (input.explicitWorkspaceId) { + return input.resolveWorkspace(input.explicitWorkspaceId); + } + if (input.caller) { + if (!input.caller.workspaceId) { + throw new Error(`Caller agent ${input.caller.id} has no workspace`); + } + return { workspaceId: input.caller.workspaceId, cwd: input.caller.cwd }; + } + return input.createWorkspace(); +} diff --git a/packages/server/src/server/agent/mcp-server.test.ts b/packages/server/src/server/agent/mcp-server.test.ts index 66f71f829..532389815 100644 --- a/packages/server/src/server/agent/mcp-server.test.ts +++ b/packages/server/src/server/agent/mcp-server.test.ts @@ -1202,6 +1202,42 @@ describe("create_agent MCP tool", () => { ).toBe(true); }); + it("creates a fresh local workspace for canonical top-level creation", async () => { + const { agentManager, agentStorage, spies } = createTestDeps(); + spies.agentManager.createAgent.mockResolvedValue({ + id: "top-level-agent", + provider: "codex", + cwd: existingCwd, + workspaceId: "workspace-created", + lifecycle: "idle", + currentModeId: null, + availableModes: [], + config: { title: "Top-level agent" }, + } as ManagedAgent); + const ensureWorkspace = vi.fn(async () => "workspace-created"); + const server = await createAgentMcpServer({ + agentManager, + agentStorage, + providerSnapshotManager: createOpenCodeManager().manager, + ensureWorkspaceForCreate: ensureWorkspace, + logger, + }); + + await registeredTool(server, "create_agent").handler({ + title: "Top-level agent", + provider: "codex/gpt-5.4", + initialPrompt: "Do work", + background: true, + }); + + expect(ensureWorkspace).toHaveBeenCalledWith(existingCwd, { prompt: "Do work" }); + expect(spies.agentManager.createAgent).toHaveBeenCalledWith( + expect.objectContaining({ cwd: existingCwd }), + undefined, + { workspaceId: "workspace-created" }, + ); + }); + it("rejects partial explicit workspace shape", async () => { const { agentManager, agentStorage } = createTestDeps(); const server = await createAgentMcpServer({ @@ -1508,7 +1544,7 @@ describe("create_agent MCP tool", () => { expect(parsed.success).toBe(true); }); - it("accepts each create_worktree target kind", async () => { + it("exposes workspace tools instead of worktree tools", async () => { const { agentManager, agentStorage } = createTestDeps(); const server = await createAgentMcpServer({ agentManager, @@ -1516,31 +1552,14 @@ describe("create_agent MCP tool", () => { providerSnapshotManager: createOpenCodeManager().manager, logger, }); - const tool = registeredTool(server, "create_worktree"); - - for (const target of [ - { kind: "branch-off", worktreeSlug: "feature-x", baseBranch: "main" }, - { kind: "branch-off" }, - { kind: "checkout-branch", branch: "head-ref" }, - { kind: "checkout-pr", githubPrNumber: 42 }, - ] as const) { - const parsed = await tool.inputSchema.safeParseAsync({ cwd: existingCwd, target }); - expect(parsed.success).toBe(true); - } - }); - - it("rejects create_worktree without a target", async () => { - const { agentManager, agentStorage } = createTestDeps(); - const server = await createAgentMcpServer({ - agentManager, - agentStorage, - providerSnapshotManager: createOpenCodeManager().manager, - logger, - }); - const tool = registeredTool(server, "create_worktree"); - - const parsed = await tool.inputSchema.safeParseAsync({}); - expect(parsed.success).toBe(false); + expect(lookupTool(server, "create_workspace")).toBeDefined(); + expect(lookupTool(server, "list_workspaces")).toBeDefined(); + expect(lookupTool(server, "archive_workspace")).toBeDefined(); + expect(lookupTool(server, "create_worktree")).toBeUndefined(); + expect(lookupTool(server, "list_worktrees")).toBeUndefined(); + expect(lookupTool(server, "archive_worktree")).toBeUndefined(); + expect(lookupTool(server, "detach_agent")).toBeUndefined(); + expect(lookupTool(server, "update_heartbeat")).toBeUndefined(); }); it("surfaces createAgent validation failures", async () => { @@ -2443,7 +2462,7 @@ describe("create_agent MCP tool", () => { expect(workspaceGitService.getSnapshot).not.toHaveBeenCalled(); }); - it("registers and broadcasts a workspace when create_worktree creates a worktree", async () => { + it("creates a worktree-isolated workspace", async () => { const { agentManager, agentStorage } = createTestDeps(); const tempDir = await mkdtemp(join(tmpdir(), "paseo-mcp-create-worktree-")); const repoDir = join(tempDir, "repo"); @@ -2488,25 +2507,19 @@ describe("create_agent MCP tool", () => { >, logger, }); - const tool = registeredTool(server, "create_worktree"); + const tool = registeredTool(server, "create_workspace"); const response = await tool.handler({ - cwd: repoDir, - target: { - kind: "branch-off", - worktreeSlug: "tool-worktree", - branchName: "feature/tool-worktree", - baseBranch: "main", - }, + isolation: "worktree", + path: repoDir, + worktreeSlug: "tool-worktree", + branchName: "feature/tool-worktree", + baseBranch: "main", }); - expect(response.structuredContent.branchName).toBe("feature/tool-worktree"); - expect(response.structuredContent.worktreePath).toContain("tool-worktree"); + expect(response.structuredContent.isolation).toBe("worktree"); + expect(response.structuredContent.cwd).toContain("tool-worktree"); expect(response.structuredContent.workspaceId).toBe(broadcasts[0]); expect(workspaceGitService.getSnapshot).not.toHaveBeenCalled(); - expect(workspaceGitService.listWorktrees).toHaveBeenCalledWith(repoDir, { - force: true, - reason: "mcp:create-worktree", - }); expect(setupContinuations).toEqual([undefined]); expect(broadcasts).toHaveLength(1); expect(broadcasts[0]).toMatch(/^wks_[0-9a-f]{16}$/); @@ -2515,7 +2528,149 @@ describe("create_agent MCP tool", () => { } }); - it("forces a workspace git snapshot refresh when archive_worktree deletes a worktree", async () => { + it("creates a worktree workspace from a project root without a path", async () => { + const { agentManager, agentStorage } = createTestDeps(); + const project = createPersistedProjectRecord({ + projectId: "project-source", + rootPath: REPO_CWD, + kind: "git", + displayName: "source", + createdAt: "2026-07-18T00:00:00.000Z", + updatedAt: "2026-07-18T00:00:00.000Z", + }); + const receivedInputs: CreatePaseoWorktreeInput[] = []; + const createPaseoWorktree: CreatePaseoWorktreeWorkflowFn = async (input) => { + receivedInputs.push(input); + return { + worktree: { branchName: "project-worktree", worktreePath: TARGET_CWD }, + intent: { kind: "branch-off", branchName: "project-worktree", baseBranch: "main" }, + workspace: createPersistedWorkspaceRecord({ + workspaceId: "ws-project-source", + projectId: project.projectId, + cwd: TARGET_CWD, + kind: "worktree", + displayName: "project-worktree", + title: input.title ?? null, + createdAt: "2026-07-18T00:00:00.000Z", + updatedAt: "2026-07-18T00:00:00.000Z", + }), + repoRoot: REPO_CWD, + created: true, + }; + }; + const server = await createAgentMcpServer({ + agentManager, + agentStorage, + providerSnapshotManager: createOpenCodeManager().manager, + projectRegistry: { + get: async (projectId) => (projectId === project.projectId ? project : null), + }, + createPaseoWorktree, + logger, + }); + + const response = await invokeToolWithParsedInput(registeredTool(server, "create_workspace"), { + isolation: "worktree", + projectId: project.projectId, + worktreeSlug: "project-worktree", + title: "Project workspace", + }); + + expect(response.structuredContent.workspaceId).toBe("ws-project-source"); + expect(receivedInputs).toEqual([ + expect.objectContaining({ + cwd: REPO_CWD, + projectId: project.projectId, + title: "Project workspace", + }), + ]); + }); + + it("preserves branch checkout and pull request checkout workspace modes", async () => { + const { agentManager, agentStorage } = createTestDeps(); + const createPaseoWorktree = vi.fn(async (input: CreatePaseoWorktreeInput) => ({ + worktree: { + branchName: input.refName ?? "pr-42", + worktreePath: "/tmp/worktrees/selected", + }, + intent: { + kind: "checkout-branch" as const, + branchName: input.refName ?? "pr-42", + }, + workspace: createPersistedWorkspaceRecord({ + workspaceId: "ws-selected", + projectId: "project-1", + cwd: "/tmp/worktrees/selected", + kind: "worktree", + displayName: "selected", + createdAt: "2026-07-18T00:00:00.000Z", + updatedAt: "2026-07-18T00:00:00.000Z", + }), + repoRoot: REPO_CWD, + created: true, + })); + const server = await createAgentMcpServer({ + agentManager, + agentStorage, + providerSnapshotManager: createOpenCodeManager().manager, + createPaseoWorktree, + logger, + }); + const tool = registeredTool(server, "create_workspace"); + + await invokeToolWithParsedInput(tool, { + isolation: "worktree", + path: REPO_CWD, + mode: "checkout-branch", + branch: "existing-work", + worktreeSlug: "existing-work-copy", + }); + await invokeToolWithParsedInput(tool, { + isolation: "worktree", + path: REPO_CWD, + mode: "checkout-pr", + prNumber: 42, + forge: "gitlab", + }); + await invokeToolWithParsedInput(tool, { + isolation: "worktree", + path: REPO_CWD, + mode: "checkout-pr", + prNumber: 43, + }); + + expect(createPaseoWorktree).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + action: "checkout", + refName: "existing-work", + worktreeSlug: "existing-work-copy", + }), + ); + expect(createPaseoWorktree).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + action: "checkout", + checkoutSource: { + kind: "change_request", + forge: "gitlab", + number: 42, + }, + }), + ); + expect(createPaseoWorktree).toHaveBeenNthCalledWith( + 3, + expect.objectContaining({ + action: "checkout", + checkoutSource: { + kind: "change_request", + number: 43, + }, + }), + ); + }); + + it("archives a worktree-isolated workspace by workspace id", async () => { const { agentManager, agentStorage } = createTestDeps(); const tempDir = realpathSync.native( await mkdtemp(join(tmpdir(), "paseo-mcp-archive-worktree-")), @@ -2568,13 +2723,15 @@ describe("create_agent MCP tool", () => { github: createGitHubServiceStub(), logger, }); - const createTool = registeredTool(server, "create_worktree"); - const archiveTool = registeredTool(server, "archive_worktree"); + const createTool = registeredTool(server, "create_workspace"); + const archiveTool = registeredTool(server, "archive_workspace"); const created = await createTool.handler({ - cwd: repoDir, - target: { kind: "branch-off", worktreeSlug: "archive-tool-worktree", baseBranch: "main" }, + isolation: "worktree", + path: repoDir, + worktreeSlug: "archive-tool-worktree", + baseBranch: "main", }); - const createdWorktreePath = z.string().parse(created.structuredContent.worktreePath); + const createdWorktreePath = z.string().parse(created.structuredContent.cwd); listActiveWorkspaces.mockImplementation(async () => [ { workspaceId: "ws-archive-tool-worktree", cwd: createdWorktreePath, kind: "worktree" }, ]); @@ -2584,19 +2741,13 @@ describe("create_agent MCP tool", () => { workspaceGitService.getSnapshot.mockClear(); await archiveTool.handler({ - cwd: repoDir, - worktreePath: created.structuredContent.worktreePath, + workspaceId: "ws-archive-tool-worktree", }); expect(workspaceGitService.getSnapshot).toHaveBeenCalledWith(repoDir, { force: true, reason: "archive-worktree", }); - expect(workspaceGitService.resolveRepoRoot).toHaveBeenCalledWith(repoDir); - expect(workspaceGitService.listWorktrees).toHaveBeenCalledWith(repoDir, { - force: true, - reason: "mcp:archive-worktree", - }); expect(archiveWorkspaceRecord).toHaveBeenCalledWith("ws-archive-tool-worktree"); expect(markWorkspaceArchiving).toHaveBeenCalledWith( ["ws-archive-tool-worktree"], @@ -2611,7 +2762,22 @@ describe("create_agent MCP tool", () => { } }); - it("archives every workspace on a directory and removes the directory", async () => { + it("rejects archiving a missing workspace", async () => { + const { agentManager, agentStorage } = createTestDeps(); + const server = await createAgentMcpServer({ + agentManager, + agentStorage, + providerSnapshotManager: createOpenCodeManager().manager, + listActiveWorkspaces: async () => [], + logger, + }); + + await expect( + registeredTool(server, "archive_workspace").handler({ workspaceId: "missing-workspace" }), + ).rejects.toThrow("Workspace not found: missing-workspace"); + }); + + it("keeps an owned worktree while another workspace still references it", async () => { const { agentManager, agentStorage } = createTestDeps(); const tempDir = realpathSync.native( await mkdtemp(join(tmpdir(), "paseo-mcp-archive-worktree-multi-")), @@ -2670,13 +2836,15 @@ describe("create_agent MCP tool", () => { github: createGitHubServiceStub(), logger, }); - const createTool = registeredTool(server, "create_worktree"); - const archiveTool = registeredTool(server, "archive_worktree"); + const createTool = registeredTool(server, "create_workspace"); + const archiveTool = registeredTool(server, "archive_workspace"); const created = await createTool.handler({ - cwd: repoDir, - target: { kind: "branch-off", worktreeSlug: "archive-multi-worktree", baseBranch: "main" }, + isolation: "worktree", + path: repoDir, + worktreeSlug: "archive-multi-worktree", + baseBranch: "main", }); - const worktreePath = z.string().parse(created.structuredContent.worktreePath); + const worktreePath = z.string().parse(created.structuredContent.cwd); // Populate the active workspaces with the real created path so archiveByScope // matches it against the worktree directory. @@ -2686,19 +2854,18 @@ describe("create_agent MCP tool", () => { ]; await archiveTool.handler({ - cwd: repoDir, - worktreePath, + workspaceId: "ws-mcp-A", }); expect(archivedWorkspaceIds).toContain("ws-mcp-A"); - expect(archivedWorkspaceIds).toContain("ws-mcp-B"); - await expect(access(worktreePath)).rejects.toThrow(); + expect(archivedWorkspaceIds).not.toContain("ws-mcp-B"); + await expect(access(worktreePath)).resolves.toBeUndefined(); } finally { await removeTempDir(tempDir); } }); - it("archives a worktree by slug", async () => { + it("does not expose worktree path or slug operations", async () => { const { agentManager, agentStorage } = createTestDeps(); const tempDir = realpathSync.native( await mkdtemp(join(tmpdir(), "paseo-mcp-archive-worktree-slug-")), @@ -2746,71 +2913,41 @@ describe("create_agent MCP tool", () => { github: createGitHubServiceStub(), logger, }); - const createTool = registeredTool(server, "create_worktree"); - const archiveTool = registeredTool(server, "archive_worktree"); - const created = await createTool.handler({ - cwd: repoDir, - target: { kind: "branch-off", worktreeSlug: "archive-slug-worktree", baseBranch: "main" }, - }); - - const response = await archiveTool.handler({ - cwd: repoDir, - worktreeSlug: "archive-slug-worktree", - }); - - expect(response.structuredContent).toEqual({ success: true }); - expect(workspaceGitService.getSnapshot).toHaveBeenCalledWith(repoDir, { - force: true, - reason: "archive-worktree", - }); - expect(workspaceGitService.resolveRepoRoot).toHaveBeenCalledWith(repoDir); - expect(workspaceGitService.listWorktrees).toHaveBeenCalledWith(repoDir, { - force: true, - reason: "mcp:archive-worktree", - }); - await expect( - access(z.string().parse(created.structuredContent.worktreePath)), - ).rejects.toThrow(); + expect(lookupTool(server, "create_worktree")).toBeUndefined(); + expect(lookupTool(server, "archive_worktree")).toBeUndefined(); } finally { await removeTempDir(tempDir); } }); - it("routes list_worktrees through WorkspaceGitService", async () => { + it("lists active workspace descriptors", async () => { const { agentManager, agentStorage } = createTestDeps(); - const workspaceGitService = { - getSnapshot: vi.fn(async () => null), - listWorktrees: vi.fn(async () => [ - { - path: "/tmp/paseo/worktrees/repo/feature", - branchName: "feature", - createdAt: "2026-04-12T00:00:00.000Z", - }, - ]), - }; + const workspace = createPersistedWorkspaceRecord({ + workspaceId: "ws-feature", + projectId: "project-1", + cwd: "/tmp/paseo/worktrees/repo/feature", + kind: "worktree", + displayName: "feature", + createdAt: "2026-07-17T00:00:00.000Z", + updatedAt: "2026-07-17T00:00:00.000Z", + }); const server = await createAgentMcpServer({ agentManager, agentStorage, providerSnapshotManager: createOpenCodeManager().manager, - workspaceGitService: workspaceGitService as unknown as Pick< - WorkspaceGitService, - "getSnapshot" | "listWorktrees" - >, + workspaceRegistry: { + get: vi.fn(async () => workspace), + list: vi.fn(async () => [workspace]), + upsert: vi.fn(async () => undefined), + }, logger, }); - const tool = registeredTool(server, "list_worktrees"); + const tool = registeredTool(server, "list_workspaces"); - const response = await tool.handler({ cwd: REPO_CWD }); + const response = await tool.handler({}); - expect(workspaceGitService.listWorktrees).toHaveBeenCalledWith(REPO_CWD, { - reason: "mcp:list-worktrees", - }); - expect(response.structuredContent.worktrees).toEqual([ - { - path: "/tmp/paseo/worktrees/repo/feature", - branchName: "feature", - createdAt: "2026-04-12T00:00:00.000Z", - }, + expect(response.structuredContent.workspaces).toEqual([ + expect.objectContaining({ workspaceId: "ws-feature", isolation: "worktree" }), ]); }); @@ -3835,11 +3972,11 @@ describe("create_schedule MCP tool", () => { cron: "*/5 * * * *", name: "Default schedule", }), - ).rejects.toThrow("provider is required when target is new-agent"); + ).rejects.toThrow("provider"); expect(createOrReplace).not.toHaveBeenCalled(); }); - it("keeps create_schedule provider overrides compatible with provider and provider/model forms", async () => { + it("keeps provider forms compatible without materializing default schedule isolation", async () => { const { agentManager, agentStorage } = createTestDeps(); const createOrReplace = vi.fn(async (input: CreateScheduleInput) => createStoredSchedule(input), @@ -3863,6 +4000,12 @@ describe("create_schedule MCP tool", () => { cron: "*/10 * * * *", provider: "codex/gpt-5.4", }); + await tool.handler({ + prompt: "say hello in a worktree", + cron: "*/15 * * * *", + provider: "codex", + isolation: "worktree", + }); expect(createOrReplace).toHaveBeenNthCalledWith( 1, @@ -3889,9 +4032,22 @@ describe("create_schedule MCP tool", () => { }, }), ); + expect(createOrReplace).toHaveBeenNthCalledWith( + 3, + expect.objectContaining({ + target: { + type: "new-agent", + config: { + provider: "codex", + cwd: process.cwd(), + isolation: "worktree", + }, + }, + }), + ); }); - it("returns create_schedule structured content with inherited feature values", async () => { + it("inherits the caller provider, model, and features when provider is omitted", async () => { const { agentManager, agentStorage, spies } = createTestDeps(); spies.agentManager.getAgent.mockReturnValue({ id: "parent-agent", @@ -3922,12 +4078,17 @@ describe("create_schedule MCP tool", () => { const response = await tool.handler({ prompt: "say hello", cron: "*/5 * * * *", - provider: "opencode/openai/gpt-5.5", }); - expect(response.structuredContent.target).toMatchObject({ + expect(response.structuredContent.target).toEqual({ type: "new-agent", - config: { featureValues: { auto_accept: true } }, + config: { + provider: "opencode", + cwd: REPO_CWD, + modeId: "build", + model: "openai/gpt-5.5", + featureValues: { auto_accept: true }, + }, }); }); @@ -4103,6 +4264,60 @@ describe("create_heartbeat MCP tool", () => { }); }); +describe("heartbeat ownership MCP tools", () => { + const logger = createTestLogger(); + + it("deletes the caller's heartbeat", async () => { + const { agentManager, agentStorage } = createTestDeps(); + const heartbeat = createStoredSchedule({ + prompt: "check status", + cadence: { type: "cron", expression: "*/15 * * * *" }, + target: { type: "agent", agentId: "parent-agent" }, + }); + const inspect = vi.fn(async () => heartbeat); + const deleteSchedule = vi.fn(async () => undefined); + const server = await createAgentMcpServer({ + agentManager, + agentStorage, + providerSnapshotManager: createOpenCodeManager().manager, + scheduleService: { + inspect, + delete: deleteSchedule, + } as unknown as ScheduleService, + callerAgentId: "parent-agent", + logger, + }); + + await registeredTool(server, "delete_heartbeat").handler({ id: heartbeat.id }); + + expect(deleteSchedule).toHaveBeenCalledWith(heartbeat.id); + }); + + it("rejects another agent's heartbeat", async () => { + const { agentManager, agentStorage } = createTestDeps(); + const foreignHeartbeat = createStoredSchedule({ + prompt: "foreign", + cadence: { type: "cron", expression: "0 * * * *" }, + target: { type: "agent", agentId: "other-agent" }, + }); + const server = await createAgentMcpServer({ + agentManager, + agentStorage, + providerSnapshotManager: createOpenCodeManager().manager, + scheduleService: { + inspect: vi.fn(async () => foreignHeartbeat), + delete: vi.fn(), + } as unknown as ScheduleService, + callerAgentId: "parent-agent", + logger, + }); + + await expect( + registeredTool(server, "delete_heartbeat").handler({ id: foreignHeartbeat.id }), + ).rejects.toThrow("does not belong to caller"); + }); +}); + describe("update_schedule MCP tool", () => { const logger = createTestLogger(); @@ -4125,6 +4340,16 @@ describe("update_schedule MCP tool", () => { }; } + function scheduleServiceWithUpdate( + update: (input: UpdateScheduleInput) => Promise<StoredSchedule>, + stored = makeStoredSchedule(), + ): ScheduleService { + return { + update, + inspect: vi.fn(async () => stored), + } as unknown as ScheduleService; + } + it("calls scheduleService.update with correct input", async () => { const { agentManager, agentStorage } = createTestDeps(); const stored = makeStoredSchedule(); @@ -4138,7 +4363,7 @@ describe("update_schedule MCP tool", () => { agentManager, agentStorage, providerSnapshotManager: createOpenCodeManager().manager, - scheduleService: { update } as unknown as ScheduleService, + scheduleService: scheduleServiceWithUpdate(update, stored), logger, }); const tool = registeredTool(server, "update_schedule"); @@ -4164,7 +4389,7 @@ describe("update_schedule MCP tool", () => { agentManager, agentStorage, providerSnapshotManager: createOpenCodeManager().manager, - scheduleService: { update } as unknown as ScheduleService, + scheduleService: scheduleServiceWithUpdate(update, stored), logger, }); const tool = registeredTool(server, "update_schedule"); @@ -4176,7 +4401,7 @@ describe("update_schedule MCP tool", () => { expect(update).toHaveBeenCalledWith({ id: "schedule-1", - cadence: { type: "every", everyMs: 600000 }, + cadence: { type: "cron", expression: "*/10 * * * *" }, }); }); @@ -4188,7 +4413,7 @@ describe("update_schedule MCP tool", () => { agentManager, agentStorage, providerSnapshotManager: createOpenCodeManager().manager, - scheduleService: { update } as unknown as ScheduleService, + scheduleService: scheduleServiceWithUpdate(update, stored), logger, }); const tool = registeredTool(server, "update_schedule"); @@ -4217,7 +4442,7 @@ describe("update_schedule MCP tool", () => { agentManager, agentStorage, providerSnapshotManager: createOpenCodeManager().manager, - scheduleService: { update } as unknown as ScheduleService, + scheduleService: scheduleServiceWithUpdate(update, stored), logger, }); const tool = registeredTool(server, "update_schedule"); @@ -4230,7 +4455,7 @@ describe("update_schedule MCP tool", () => { expect(update).toHaveBeenCalledWith({ id: "schedule-1", - cadence: { type: "every", everyMs: 600000 }, + cadence: { type: "cron", expression: "*/10 * * * *" }, }); }); @@ -4238,7 +4463,7 @@ describe("update_schedule MCP tool", () => { { label: "whitespace cron field", input: { id: "schedule-1", every: "10m", cron: " " }, - cadence: { type: "every", everyMs: 600000 }, + cadence: { type: "cron", expression: "*/10 * * * *" }, }, { label: "blank every field for cron cadence", @@ -4258,7 +4483,7 @@ describe("update_schedule MCP tool", () => { agentManager, agentStorage, providerSnapshotManager: createOpenCodeManager().manager, - scheduleService: { update } as unknown as ScheduleService, + scheduleService: scheduleServiceWithUpdate(update, stored), logger, }); const tool = registeredTool(server, "update_schedule"); @@ -4278,7 +4503,7 @@ describe("update_schedule MCP tool", () => { agentManager, agentStorage, providerSnapshotManager: createOpenCodeManager().manager, - scheduleService: { update } as unknown as ScheduleService, + scheduleService: scheduleServiceWithUpdate(update), logger, }); const tool = registeredTool(server, "update_schedule"); @@ -4300,7 +4525,7 @@ describe("update_schedule MCP tool", () => { agentManager, agentStorage, providerSnapshotManager: createOpenCodeManager().manager, - scheduleService: { update } as unknown as ScheduleService, + scheduleService: scheduleServiceWithUpdate(update), logger, }); const tool = registeredTool(server, "update_schedule"); @@ -4323,7 +4548,7 @@ describe("update_schedule MCP tool", () => { agentManager, agentStorage, providerSnapshotManager: createOpenCodeManager().manager, - scheduleService: { update } as unknown as ScheduleService, + scheduleService: scheduleServiceWithUpdate(update), logger, }); const tool = registeredTool(server, "update_schedule"); @@ -4347,7 +4572,7 @@ describe("update_schedule MCP tool", () => { agentManager, agentStorage, providerSnapshotManager: createOpenCodeManager().manager, - scheduleService: { update } as unknown as ScheduleService, + scheduleService: scheduleServiceWithUpdate(update, stored), logger, }); const tool = registeredTool(server, "update_schedule"); @@ -4381,7 +4606,7 @@ describe("update_schedule MCP tool", () => { agentManager, agentStorage, providerSnapshotManager: createOpenCodeManager().manager, - scheduleService: { update } as unknown as ScheduleService, + scheduleService: scheduleServiceWithUpdate(update, stored), logger, }); const tool = registeredTool(server, "update_schedule"); @@ -4412,7 +4637,7 @@ describe("update_schedule MCP tool", () => { agentManager, agentStorage, providerSnapshotManager: createOpenCodeManager().manager, - scheduleService: { update } as unknown as ScheduleService, + scheduleService: scheduleServiceWithUpdate(update), logger, }); const tool = registeredTool(server, "update_schedule"); @@ -4455,11 +4680,15 @@ describe("schedule_logs MCP tool", () => { const { agentManager, agentStorage } = createTestDeps(); const runs = [makeRun({ id: "run-1" }), makeRun({ id: "run-2", status: "failed" })]; const logs = vi.fn(async (_id: string) => runs); + const inspect = vi.fn(async () => ({ + id: "schedule-1", + target: { type: "new-agent", config: { provider: "codex", cwd: "/tmp" } }, + })); const server = await createAgentMcpServer({ agentManager, agentStorage, providerSnapshotManager: createOpenCodeManager().manager, - scheduleService: { logs } as unknown as ScheduleService, + scheduleService: { logs, inspect } as unknown as ScheduleService, logger, }); const tool = registeredTool(server, "schedule_logs"); diff --git a/packages/server/src/server/agent/tools/paseo-tools.ts b/packages/server/src/server/agent/tools/paseo-tools.ts index 0cc428193..12aa43079 100644 --- a/packages/server/src/server/agent/tools/paseo-tools.ts +++ b/packages/server/src/server/agent/tools/paseo-tools.ts @@ -23,12 +23,15 @@ import type { AgentStorage } from "../agent-storage.js"; import { ensureAgentLoaded } from "../agent-loading.js"; import { isStoredAgentProviderAvailable } from "../../persistence-hooks.js"; import { + archiveByScope, killTerminalsForWorkspace, + requireActiveWorkspaceForArchive, type ArchiveDependencies, } from "../../workspace-archive-service.js"; import { createAgentCommand, type CreateAgentFromMcpInput } from "../create-agent/create.js"; import type { VoiceCallerContext, VoiceSpeakHandler } from "../../voice-types.js"; import type { FirstAgentContext } from "../../messages.js"; +import { everyMsToFiveFieldCron } from "@getpaseo/protocol/schedule/cadence"; import { expandUserPath, isSameOrDescendantPath, resolvePathFromBase } from "../../path-utils.js"; import type { TerminalManager } from "../../../terminal/terminal-manager.js"; import type { CreatePaseoWorktreeWorkflowFn } from "../../worktree-session.js"; @@ -65,14 +68,16 @@ import { } from "../lifecycle-command.js"; import type { ForgeService } from "../../../services/forge-service.js"; import type { WorkspaceGitService } from "../../workspace-git-service.js"; -import type { WorkspaceRegistry } from "../../workspace-registry.js"; -import { WorktreeRequestError } from "../../worktree-errors.js"; +import type { + PersistedWorkspaceRecord, + ProjectRegistry, + WorkspaceRegistry, +} from "../../workspace-registry.js"; +import { resolveWorktreeSourceCwd } from "../../workspace-source.js"; import { - archiveCommand, type ArchiveCommandDependencies, - createPaseoWorktreeCommand, type CreatePaseoWorktreeCommandInput, - listPaseoWorktreesCommand, + createPaseoWorktreeCommand, } from "../../worktree/commands.js"; import { registerBrowserTools } from "../../browser-tools/tools.js"; import type { BrowserToolsBroker } from "../../browser-tools/broker.js"; @@ -100,7 +105,13 @@ export interface PaseoToolHostDependencies { listActiveWorkspaces?: ArchiveDependencies["listActiveWorkspaces"]; archiveWorkspaceRecord?: ArchiveDependencies["archiveWorkspaceRecord"]; emitWorkspaceUpdatesForWorkspaceIds?: ArchiveDependencies["emitWorkspaceUpdatesForWorkspaceIds"]; - workspaceRegistry?: Pick<WorkspaceRegistry, "get" | "upsert">; + workspaceRegistry?: Pick<WorkspaceRegistry, "get" | "list" | "upsert">; + projectRegistry?: Pick<ProjectRegistry, "get">; + createDirectoryWorkspace?: ( + cwd: string, + title?: string | null, + projectId?: string, + ) => Promise<PersistedWorkspaceRecord>; markWorkspaceArchiving?: ArchiveDependencies["markWorkspaceArchiving"]; clearWorkspaceArchiving?: ArchiveDependencies["clearWorkspaceArchiving"]; createPaseoWorktree?: CreatePaseoWorktreeWorkflowFn; @@ -157,6 +168,105 @@ interface ProviderSummary { error?: string; } +const WorkspaceAutomationSummarySchema = z.object({ + workspaceId: z.string(), + projectId: z.string(), + cwd: z.string(), + isolation: z.enum(["local", "worktree"]), + kind: z.enum(["directory", "local_checkout", "worktree"]), + title: z.string().nullable(), +}); + +function toWorkspaceAutomationSummary(workspace: PersistedWorkspaceRecord) { + return { + workspaceId: workspace.workspaceId, + projectId: workspace.projectId, + cwd: workspace.cwd, + isolation: workspace.kind === "worktree" ? ("worktree" as const) : ("local" as const), + kind: workspace.kind, + title: workspace.title, + }; +} + +type WorkspaceWorktreeMode = "branch-off" | "checkout-branch" | "checkout-pr"; + +interface WorkspaceWorktreeOptions { + mode?: WorkspaceWorktreeMode; + worktreeSlug?: string; + branchName?: string; + baseBranch?: string; + branch?: string; + prNumber?: number; + forge?: string; +} + +type WorkspaceWorktreeTarget = Pick< + CreatePaseoWorktreeCommandInput, + "action" | "branchName" | "refName" | "checkoutSource" +>; + +function assertOptionsAbsent( + options: Array<[name: string, value: unknown]>, + message: string, +): void { + if (options.some(([, value]) => value !== undefined)) { + throw new Error(message); + } +} + +function resolveWorkspaceWorktreeTarget(input: WorkspaceWorktreeOptions): WorkspaceWorktreeTarget { + switch (input.mode ?? "branch-off") { + case "branch-off": + assertOptionsAbsent( + [ + ["branch", input.branch], + ["prNumber", input.prNumber], + ["forge", input.forge], + ], + "branch, prNumber, and forge require a checkout mode", + ); + return { + action: "branch-off", + ...(input.branchName ? { branchName: input.branchName } : {}), + ...(input.baseBranch ? { refName: input.baseBranch } : {}), + }; + case "checkout-branch": + if (!input.branch) { + throw new Error("branch is required for checkout-branch mode"); + } + assertOptionsAbsent( + [ + ["branchName", input.branchName], + ["baseBranch", input.baseBranch], + ["prNumber", input.prNumber], + ["forge", input.forge], + ], + "branchName, baseBranch, prNumber, and forge are not valid for checkout-branch mode", + ); + return { action: "checkout", refName: input.branch }; + case "checkout-pr": + if (input.prNumber === undefined) { + throw new Error("prNumber is required for checkout-pr mode"); + } + assertOptionsAbsent( + [ + ["branchName", input.branchName], + ["baseBranch", input.baseBranch], + ["branch", input.branch], + ], + "branchName, baseBranch, and branch are not valid for checkout-pr mode", + ); + return { + action: "checkout", + checkoutSource: { + kind: "change_request", + ...(input.forge ? { forge: input.forge } : {}), + number: input.prNumber, + }, + }; + } +} + function toProviderSummary(entry: { provider: AgentProvider; label?: string; @@ -307,7 +417,14 @@ function resolveScheduleUpdateCadence(input: ScheduleUpdateToolInput): ScheduleC throw new Error("timezone can only be used with cron"); } if (every !== undefined) { - return { type: "every", everyMs: parseDurationString(every) }; + // COMPAT(scheduleEveryInput): accept the old hidden field and canonicalize it before write. + // Added in v0.2.0; remove after 2027-01-17. + const everyMs = parseDurationString(every); + const expression = everyMsToFiveFieldCron(everyMs); + if (expression) { + return { type: "cron", expression }; + } + throw new Error(`${every} cannot be represented faithfully by five-field cron`); } if (cron !== undefined) { return { @@ -382,13 +499,6 @@ const TerminalSummarySchema = z.object({ cwd: z.string(), }); -const WorktreeSummarySchema = z.object({ - path: z.string(), - createdAt: z.string(), - branchName: z.string().optional(), - head: z.string().optional(), -}); - function resolveTerminalKeyToken(key: string, literal: boolean): string { if (literal) { return key; @@ -592,7 +702,6 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase ...(typeof callerAgent.config.webSearch === "boolean" ? { webSearch: callerAgent.config.webSearch } : {}), - ...(callerAgent.config.title ? { title: callerAgent.config.title } : {}), ...(callerAgent.config.extra ? { extra: callerAgent.config.extra } : {}), ...(callerAgent.config.featureValues ? { featureValues: callerAgent.config.featureValues } @@ -633,19 +742,26 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase }; }; - const resolveNewAgentScheduleTarget = (params?: { provider?: string; cwd?: string }) => { - if (!params?.provider?.trim()) { - throw new Error("provider is required when target is new-agent"); - } - + const resolveNewAgentScheduleTarget = (params?: { + provider?: string; + cwd?: string; + isolation?: "local" | "worktree"; + }) => { const callerAgent = resolveCallerAgent(); if (callerAgent) { return { type: "new-agent" as const, - config: buildCallerAgentScheduleConfig(callerAgent, params), + config: { + ...buildCallerAgentScheduleConfig(callerAgent, params), + ...(params?.isolation ? { isolation: params.isolation } : {}), + }, }; } + if (!params?.provider?.trim()) { + throw new Error("provider is required when target is new-agent"); + } + const resolvedProviderModel = resolveScheduleProviderAndModel({ provider: params?.provider, defaultProvider: params.provider, @@ -656,9 +772,34 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase provider: resolvedProviderModel.provider, cwd: params?.cwd?.trim() ? expandUserPath(params.cwd) : process.cwd(), ...(resolvedProviderModel.model ? { model: resolvedProviderModel.model } : {}), + ...(params?.isolation ? { isolation: params.isolation } : {}), }, }; }; + + async function requireScheduleTarget(id: string, type: "agent" | "new-agent") { + if (!scheduleService) { + throw new Error("Schedule service is not configured"); + } + const schedule = await scheduleService.inspect(id); + if (schedule.target.type !== type) { + throw new Error( + type === "agent" ? `Heartbeat not found: ${id}` : `Schedule not found: ${id}`, + ); + } + return schedule; + } + + async function requireCallerHeartbeat(id: string) { + if (!callerAgentId) { + throw new Error("Heartbeat operations require an agent-scoped session"); + } + const schedule = await requireScheduleTarget(id, "agent"); + if (schedule.target.type !== "agent" || schedule.target.agentId !== callerAgentId) { + throw new Error(`Heartbeat ${id} does not belong to caller ${callerAgentId}`); + } + return schedule; + } const ProviderModelInputSchema = AgentProviderEnum.trim() .refine((value) => value.includes("/"), { message: "provider must be provider/model, for example codex/gpt-5.4", @@ -820,13 +961,7 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase .strict() .describe("Create a new workspace for the agent."), ]); - const commonCreateAgentInputSchema = { - relationship: AgentRelationshipInputSchema.describe( - "Whether the created agent is a subagent under you or a detached root agent.", - ), - workspace: AgentWorkspaceInputSchema.describe( - "Workspace ownership/location for the created agent.", - ), + const commonCreateAgentFields = { title: z .string() .trim() @@ -846,8 +981,26 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase .min(1, "initialPrompt is required") .describe("Required first task to run immediately after creation."), }; + const legacyCreateAgentPlacementFields = { + relationship: AgentRelationshipInputSchema.describe( + "Whether the created agent is a subagent under you or a detached root agent.", + ), + workspace: AgentWorkspaceInputSchema.describe( + "Workspace ownership/location for the created agent.", + ), + }; + const canonicalCreateAgentFields = { + ...commonCreateAgentFields, + workspaceId: z + .string() + .min(1) + .optional() + .describe( + "Existing workspace id. Agent-scoped calls default to the caller workspace; top-level calls create a new local workspace when omitted.", + ), + }; const agentToAgentInputSchema = { - ...commonCreateAgentInputSchema, + ...canonicalCreateAgentFields, notifyOnFinish: z .boolean() .optional() @@ -857,7 +1010,7 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase ), }; const canonicalTopLevelInputSchema = { - ...commonCreateAgentInputSchema, + ...canonicalCreateAgentFields, background: z .boolean() .optional() @@ -873,9 +1026,17 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase "Agent-scoped only: get notified when the created agent finishes, errors, or needs permission.", ), }; + const legacyAgentToAgentInputSchema = { + ...commonCreateAgentFields, + ...legacyCreateAgentPlacementFields, + notifyOnFinish: agentToAgentInputSchema.notifyOnFinish, + }; const legacyTopLevelCreateAgentInputSchema = { - relationship: commonCreateAgentInputSchema.relationship.optional(), - workspace: commonCreateAgentInputSchema.workspace.optional(), + ...commonCreateAgentFields, + relationship: legacyCreateAgentPlacementFields.relationship.optional(), + workspace: legacyCreateAgentPlacementFields.workspace.optional(), + background: canonicalTopLevelInputSchema.background, + notifyOnFinish: canonicalTopLevelInputSchema.notifyOnFinish, cwd: z .string() .optional() @@ -916,15 +1077,15 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase .optional() .describe("Legacy GitHub PR number. Prefer workspace.source.target.githubPrNumber."), }; - const topLevelInputSchema = { - ...canonicalTopLevelInputSchema, - ...legacyTopLevelCreateAgentInputSchema, - }; - - const createAgentInputSchema = callerAgentId ? agentToAgentInputSchema : topLevelInputSchema; + const createAgentInputSchema = z + .object(callerAgentId ? agentToAgentInputSchema : canonicalTopLevelInputSchema) + .passthrough(); const agentToAgentCreateAgentArgsSchema = z.object(agentToAgentInputSchema).strict(); + const legacyAgentToAgentCreateAgentArgsSchema = z.object(legacyAgentToAgentInputSchema).strict(); const canonicalTopLevelCreateAgentArgsSchema = z.object(canonicalTopLevelInputSchema).strict(); - const topLevelCreateAgentArgsSchema = z.object(topLevelInputSchema).strict(); + const legacyTopLevelCreateAgentArgsSchema = z + .object(legacyTopLevelCreateAgentInputSchema) + .strict(); const commonSendAgentPromptInputSchema = { agentId: z.string(), prompt: z.string(), @@ -980,8 +1141,9 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase ), }; type AgentToAgentCreateAgentArgs = z.infer<typeof agentToAgentCreateAgentArgsSchema>; + type LegacyAgentToAgentCreateAgentArgs = z.infer<typeof legacyAgentToAgentCreateAgentArgsSchema>; type TopLevelCreateAgentArgs = z.infer<typeof canonicalTopLevelCreateAgentArgsSchema>; - type TopLevelCreateAgentToolArgs = z.infer<typeof topLevelCreateAgentArgsSchema>; + type LegacyTopLevelCreateAgentArgs = z.infer<typeof legacyTopLevelCreateAgentArgsSchema>; if (options.voiceOnly || options.enableVoiceTools || callerContext?.enableVoiceTools) { registerTool( @@ -1035,12 +1197,201 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase }); } + registerTool( + "create_workspace", + { + title: "Create workspace", + description: + "Create a workspace using an existing local checkout or a new Paseo-managed worktree.", + inputSchema: { + isolation: z.enum(["local", "worktree"]), + path: z + .string() + .optional() + .describe("Local directory or source checkout. Defaults to your current workspace."), + projectId: z.string().optional().describe("Existing project id to own the workspace."), + title: z.string().trim().min(1).optional(), + mode: z + .enum(["branch-off", "checkout-branch", "checkout-pr"]) + .optional() + .describe("Worktree creation mode. Defaults to branch-off."), + worktreeSlug: z.string().trim().min(1).optional(), + branchName: z + .string() + .trim() + .min(1) + .optional() + .describe("New branch name for branch-off mode."), + baseBranch: z.string().trim().min(1).optional().describe("Base ref for branch-off mode."), + branch: z + .string() + .trim() + .min(1) + .optional() + .describe("Existing branch for checkout-branch mode."), + prNumber: z + .number() + .int() + .positive() + .optional() + .describe("Pull request or change request number for checkout-pr mode."), + forge: z + .string() + .trim() + .min(1) + .optional() + .describe("Forge for checkout-pr mode. Defaults to the source checkout."), + }, + outputSchema: WorkspaceAutomationSummarySchema.shape, + }, + async ({ + isolation, + path, + projectId, + title, + mode, + worktreeSlug, + branchName, + baseBranch, + branch, + prNumber, + forge, + }) => { + let workspace: PersistedWorkspaceRecord; + if (isolation === "local") { + const cwd = resolveScopedCwd(path, { required: true }); + assertOptionsAbsent( + [ + ["mode", mode], + ["worktreeSlug", worktreeSlug], + ["branchName", branchName], + ["baseBranch", baseBranch], + ["branch", branch], + ["prNumber", prNumber], + ["forge", forge], + ], + "Worktree options require isolation worktree", + ); + if (!options.createDirectoryWorkspace) { + throw new Error("Workspace provisioning is not configured"); + } + workspace = await options.createDirectoryWorkspace(cwd, title, projectId); + } else { + let cwd = + path !== undefined || !projectId ? resolveScopedCwd(path, { required: true }) : null; + if (!cwd) { + if (!options.projectRegistry) { + throw new Error("Project registry is not configured"); + } + cwd = await resolveWorktreeSourceCwd({ projectId }, options.projectRegistry); + } + const worktreeTarget = resolveWorkspaceWorktreeTarget({ + mode, + worktreeSlug, + branchName, + baseBranch, + branch, + prNumber, + forge, + }); + const result = await createPaseoWorktreeCommand( + { + paseoHome: options.paseoHome, + worktreesRoot: options.worktreesRoot, + createPaseoWorktreeWorkflow: options.createPaseoWorktree, + }, + { + cwd, + ...(projectId ? { projectId } : {}), + ...(worktreeSlug ? { worktreeSlug } : {}), + ...worktreeTarget, + ...(title ? { title } : {}), + }, + ); + if (!result.ok) { + throw result.cause; + } + workspace = result.createdWorktree.workspace; + } + + return { + content: [], + structuredContent: ensureValidJson(toWorkspaceAutomationSummary(workspace)), + }; + }, + ); + + registerTool( + "list_workspaces", + { + title: "List workspaces", + description: "List active workspaces.", + inputSchema: {}, + outputSchema: { workspaces: z.array(WorkspaceAutomationSummarySchema) }, + }, + async () => { + if (!options.workspaceRegistry) { + throw new Error("Workspace registry is not configured"); + } + const workspaces = (await options.workspaceRegistry.list()) + .filter((workspace) => !workspace.archivedAt) + .map(toWorkspaceAutomationSummary); + return { + content: [], + structuredContent: ensureValidJson({ workspaces }), + }; + }, + ); + + registerTool( + "archive_workspace", + { + title: "Archive workspace", + description: "Archive a workspace and everything it owns.", + inputSchema: { workspaceId: z.string().min(1) }, + outputSchema: { + workspaceId: z.string(), + archivedAgentIds: z.array(z.string()), + removedDirectory: z.boolean(), + }, + }, + async ({ workspaceId }) => { + if (!options.listActiveWorkspaces) { + throw new Error("Active workspace lister is required to archive workspaces"); + } + const workspace = await requireActiveWorkspaceForArchive( + { listActiveWorkspaces: options.listActiveWorkspaces }, + workspaceId, + ); + const result = await archiveByScope( + archiveWorktreeDependencies(options, { + agentManager, + agentStorage, + terminalManager: terminalManager ?? null, + logger: childLogger, + }), + { + requestId: "mcp:archive_workspace", + scope: { kind: "workspace", workspaceId: workspace.workspaceId }, + }, + ); + return { + content: [], + structuredContent: ensureValidJson({ + workspaceId, + archivedAgentIds: result.archivedAgentIds, + removedDirectory: result.removedDirectory, + }), + }; + }, + ); + registerTool( "create_agent", { title: "Create agent", description: - "Create an agent. Requires relationship, workspace, provider/model (for example codex/gpt-5.4), and an initial prompt. Do not guess; call list_providers and list_models first if uncertain.", + "Create an agent. Agent-scoped creation defaults to your workspace and creates your subagent. Top-level creation without workspaceId creates a new local workspace. Requires provider/model (for example codex/gpt-5.4) and an initial prompt. Do not guess; call list_providers and list_models first if uncertain.", inputSchema: createAgentInputSchema, outputSchema: { agentId: z.string(), @@ -1060,15 +1411,12 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase const { parsedArgs, worktree } = resolvedArgs; let requestedBackground: boolean; let notifyOnFinish: boolean; - let detached: boolean; if (resolvedArgs.kind === "agent-scoped") { requestedBackground = true; notifyOnFinish = parsedArgs.notifyOnFinish; - detached = resolvedArgs.relationship.kind === "detached"; } else { requestedBackground = resolvedArgs.parsedArgs.background; notifyOnFinish = resolvedArgs.parsedArgs.notifyOnFinish ?? false; - detached = resolvedArgs.parsedArgs.relationship.kind === "detached"; } const { snapshot, @@ -1101,7 +1449,7 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase mode: parsedArgs.settings?.modeId, background: requestedBackground, notifyOnFinish, - detached, + detached: resolvedArgs.detached, callerAgentId, callerContext, worktree, @@ -1167,15 +1515,16 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase type ResolvedCreateAgentToolArgs = | { kind: "agent-scoped"; - parsedArgs: AgentToAgentCreateAgentArgs; - relationship: AgentToAgentCreateAgentArgs["relationship"]; + parsedArgs: AgentToAgentCreateAgentArgs | LegacyAgentToAgentCreateAgentArgs; + detached: boolean; cwd: string | undefined; workspaceId: string | undefined; worktree: CreateAgentFromMcpInput["worktree"]; } | { kind: "top-level"; - parsedArgs: TopLevelCreateAgentArgs; + parsedArgs: TopLevelCreateAgentArgs | LegacyTopLevelCreateAgentArgs; + detached: boolean; cwd: string | undefined; workspaceId: string | undefined; worktree: CreateAgentFromMcpInput["worktree"]; @@ -1183,38 +1532,125 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase async function resolveCreateAgentToolArgs(args: unknown): Promise<ResolvedCreateAgentToolArgs> { if (callerAgentId) { + if (hasLegacyCreateAgentPlacement(args)) { + // COMPAT(nestedCreateAgentPlacement): accept the old relationship/workspace shape without + // advertising it to models. Added in v0.2.0; remove after 2027-01-17. + const parsed = legacyAgentToAgentCreateAgentArgsSchema.parse(args); + const { cwd, workspaceId, worktree } = await resolveCreateAgentWorkspace(parsed.workspace, { + prompt: parsed.initialPrompt, + }); + return { + kind: "agent-scoped", + parsedArgs: parsed, + detached: parsed.relationship.kind === "detached", + cwd, + workspaceId, + worktree, + }; + } const parsed = agentToAgentCreateAgentArgsSchema.parse(args); - const { cwd, workspaceId, worktree } = await resolveCreateAgentWorkspace(parsed.workspace, { + const { cwd, workspaceId } = await resolveCanonicalCreateAgentWorkspace(parsed.workspaceId, { prompt: parsed.initialPrompt, }); return { kind: "agent-scoped", parsedArgs: parsed, - relationship: parsed.relationship, + detached: false, + cwd, + workspaceId, + worktree: undefined, + }; + } + if (hasLegacyCreateAgentPlacement(args)) { + // COMPAT(nestedCreateAgentPlacement): see the agent-scoped branch above. + const parsedArgs = normalizeTopLevelCreateAgentArgs( + legacyTopLevelCreateAgentArgsSchema.parse(args), + ); + if (parsedArgs.relationship?.kind === "subagent") { + throw new Error("relationship subagent requires an agent-scoped tool session"); + } + if (!parsedArgs.workspace) { + throw new Error("Legacy create_agent placement could not be resolved"); + } + const { cwd, workspaceId, worktree } = await resolveCreateAgentWorkspace( + parsedArgs.workspace, + { prompt: parsedArgs.initialPrompt }, + ); + return { + kind: "top-level", + parsedArgs, + detached: true, cwd, workspaceId, worktree, }; } - const parsedArgs = normalizeTopLevelCreateAgentArgs(topLevelCreateAgentArgsSchema.parse(args)); - if (parsedArgs.relationship.kind === "subagent") { - throw new Error("relationship subagent requires an agent-scoped tool session"); - } - const { cwd, workspaceId, worktree } = await resolveCreateAgentWorkspace(parsedArgs.workspace, { - prompt: parsedArgs.initialPrompt, - }); + const parsedArgs = canonicalTopLevelCreateAgentArgsSchema.parse(args); + const { cwd, workspaceId } = await resolveCanonicalCreateAgentWorkspace( + parsedArgs.workspaceId, + { prompt: parsedArgs.initialPrompt }, + ); return { kind: "top-level", parsedArgs, + detached: false, cwd, workspaceId, - worktree, + worktree: undefined, }; } + function hasLegacyCreateAgentPlacement(args: unknown): boolean { + if (!args || typeof args !== "object") { + return false; + } + const input = args as Record<string, unknown>; + return [ + "relationship", + "workspace", + "cwd", + "worktreeName", + "branchName", + "baseBranch", + "refName", + "githubPrNumber", + ].some((key) => input[key] !== undefined); + } + + async function resolveCanonicalCreateAgentWorkspace( + workspaceId?: string, + firstAgentContext?: FirstAgentContext, + ): Promise<{ + cwd: string | undefined; + workspaceId: string; + }> { + if (workspaceId) { + const resolved = await resolveCreateAgentWorkspace( + { kind: "existing", workspaceId }, + undefined, + ); + return { cwd: resolved.cwd, workspaceId }; + } + if (!callerAgentId) { + if (!options.ensureWorkspaceForCreate) { + throw new Error("Workspace creation is not configured"); + } + const cwd = process.cwd(); + return { + cwd, + workspaceId: await options.ensureWorkspaceForCreate(cwd, firstAgentContext), + }; + } + const caller = resolveCallerAgent(); + if (!caller?.workspaceId) { + throw new Error(`Caller agent ${callerAgentId} has no current workspace`); + } + return { cwd: undefined, workspaceId: caller.workspaceId }; + } + function normalizeTopLevelCreateAgentArgs( - args: TopLevelCreateAgentToolArgs, - ): TopLevelCreateAgentArgs { + args: LegacyTopLevelCreateAgentArgs, + ): LegacyTopLevelCreateAgentArgs { const { cwd, mode, @@ -1235,7 +1671,7 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase }; if (canonicalCandidate.relationship && canonicalCandidate.workspace) { - return canonicalTopLevelCreateAgentArgsSchema.parse({ + return legacyTopLevelCreateAgentArgsSchema.parse({ ...canonicalCandidate, ...(Object.keys(settings).length > 0 ? { settings } : {}), }); @@ -1273,7 +1709,7 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase }, }; - return canonicalTopLevelCreateAgentArgsSchema.parse({ + return legacyTopLevelCreateAgentArgsSchema.parse({ ...canonicalCandidate, relationship: { kind: "detached" }, workspace, @@ -1315,7 +1751,9 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase } async function resolveCreateAgentWorkspace( - workspace: AgentToAgentCreateAgentArgs["workspace"] | TopLevelCreateAgentArgs["workspace"], + workspace: + | LegacyAgentToAgentCreateAgentArgs["workspace"] + | NonNullable<LegacyTopLevelCreateAgentArgs["workspace"]>, firstAgentContext: FirstAgentContext | undefined, ): Promise<{ cwd: string | undefined; @@ -1995,16 +2433,17 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase .optional() .describe("IANA time zone for the cron cadence. For example: America/New_York."), name: z.string().optional(), - provider: AgentProviderEnum.optional().describe( - "Provider, or provider/model (for example: codex or codex/gpt-5.4).", + provider: (callerAgentId ? AgentProviderEnum.optional() : AgentProviderEnum).describe( + "Provider, or provider/model (for example: codex or codex/gpt-5.4). Defaults to the caller's provider in an agent-scoped session.", ), cwd: z.string().optional(), + isolation: z.enum(["local", "worktree"]).optional(), maxRuns: z.number().int().positive().optional(), expiresIn: z.string().optional(), }, outputSchema: ScheduleSummarySchema.shape, }, - async ({ prompt, cron, timezone, name, provider, cwd, maxRuns, expiresIn }) => { + async ({ prompt, cron, timezone, name, provider, cwd, isolation, maxRuns, expiresIn }) => { if (!scheduleService) { throw new Error("Schedule service is not configured"); } @@ -2016,7 +2455,7 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase cron, ...(timezone !== undefined ? { timezone } : {}), }), - target: resolveNewAgentScheduleTarget({ provider, cwd }), + target: resolveNewAgentScheduleTarget({ provider, cwd, isolation }), ...(name?.trim() ? { name: name.trim() } : {}), ...(maxRuns === undefined ? {} : { maxRuns }), ...(expiresAt === undefined ? {} : { expiresAt }), @@ -2078,6 +2517,27 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase }, ); + registerTool( + "delete_heartbeat", + { + title: "Delete heartbeat", + description: "Delete one of your heartbeats.", + inputSchema: { id: z.string().min(1) }, + outputSchema: { success: z.boolean() }, + }, + async ({ id }) => { + if (!scheduleService) { + throw new Error("Schedule service is not configured"); + } + await requireCallerHeartbeat(id); + await scheduleService.delete(id); + return { + content: [], + structuredContent: ensureValidJson({ success: true }), + }; + }, + ); + registerTool( "list_schedules", { @@ -2093,9 +2553,9 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase throw new Error("Schedule service is not configured"); } - const schedules = (await scheduleService.list()).map((schedule) => - toScheduleSummary(schedule), - ); + const schedules = (await scheduleService.list()) + .filter((schedule) => schedule.target.type === "new-agent") + .map((schedule) => toScheduleSummary(schedule)); return { content: [], structuredContent: ensureValidJson({ schedules }), @@ -2118,7 +2578,7 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase throw new Error("Schedule service is not configured"); } - const schedule = await scheduleService.inspect(id); + const schedule = await requireScheduleTarget(id, "new-agent"); return { content: [], structuredContent: ensureValidJson(schedule), @@ -2143,6 +2603,7 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase throw new Error("Schedule service is not configured"); } + await requireScheduleTarget(id, "new-agent"); await scheduleService.pause(id); return { content: [], @@ -2168,6 +2629,7 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase throw new Error("Schedule service is not configured"); } + await requireScheduleTarget(id, "new-agent"); await scheduleService.resume(id); return { content: [], @@ -2193,6 +2655,7 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase throw new Error("Schedule service is not configured"); } + await requireScheduleTarget(id, "new-agent"); await scheduleService.delete(id); return { content: [], @@ -2207,54 +2670,55 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase title: "Update schedule", description: "Update an existing schedule. Only provided fields are changed; omitted fields remain unchanged.", - inputSchema: { - id: z.string(), - every: z.string().optional().describe("New interval duration string (e.g. 5m, 1h)."), - cron: z.string().optional().describe("New cron expression."), - timezone: z - .string() - .trim() - .min(1) - .optional() - .describe( - "IANA time zone for cron cadence; requires cron. For example: America/New_York.", - ), - name: z.string().nullable().optional().describe("New name (null to clear)."), - prompt: z.string().trim().min(1).optional().describe("New prompt text."), - maxRuns: z - .number() - .int() - .positive() - .nullable() - .optional() - .describe("New max runs limit (null to clear)."), - provider: z - .string() - .trim() - .min(1) - .optional() - .describe("New provider for new-agent target."), - model: z - .string() - .trim() - .min(1) - .nullable() - .optional() - .describe("New model for new-agent target (null to clear)."), - mode: z - .string() - .trim() - .min(1) - .nullable() - .optional() - .describe("New mode for new-agent target (null to clear)."), - cwd: z.string().trim().min(1).optional().describe("New cwd for new-agent target."), - expiresIn: z - .string() - .optional() - .describe("New relative expiry duration (for example: 1h, 2d)."), - clearExpires: z.boolean().optional().describe("Clear any schedule expiry."), - }, + inputSchema: z + .object({ + id: z.string(), + cron: z.string().optional().describe("New cron expression."), + timezone: z + .string() + .trim() + .min(1) + .optional() + .describe( + "IANA time zone for cron cadence; requires cron. For example: America/New_York.", + ), + name: z.string().nullable().optional().describe("New name (null to clear)."), + prompt: z.string().trim().min(1).optional().describe("New prompt text."), + maxRuns: z + .number() + .int() + .positive() + .nullable() + .optional() + .describe("New max runs limit (null to clear)."), + provider: z + .string() + .trim() + .min(1) + .optional() + .describe("New provider for new-agent target."), + model: z + .string() + .trim() + .min(1) + .nullable() + .optional() + .describe("New model for new-agent target (null to clear)."), + mode: z + .string() + .trim() + .min(1) + .nullable() + .optional() + .describe("New mode for new-agent target (null to clear)."), + cwd: z.string().trim().min(1).optional().describe("New cwd for new-agent target."), + expiresIn: z + .string() + .optional() + .describe("New relative expiry duration (for example: 1h, 2d)."), + clearExpires: z.boolean().optional().describe("Clear any schedule expiry."), + }) + .passthrough(), outputSchema: StoredScheduleSchema.shape, }, async (input) => { @@ -2262,6 +2726,7 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase throw new Error("Schedule service is not configured"); } + await requireScheduleTarget(input.id, "new-agent"); const schedule = await scheduleService.update(buildScheduleUpdateInput(input)); return { @@ -2288,6 +2753,7 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase throw new Error("Schedule service is not configured"); } + await requireScheduleTarget(id, "new-agent"); const runs = await scheduleService.logs(id); return { content: [], @@ -2296,6 +2762,27 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase }, ); + registerTool( + "run_schedule_once", + { + title: "Run schedule once", + description: "Run a schedule immediately without changing its cron cadence.", + inputSchema: { id: z.string().min(1) }, + outputSchema: StoredScheduleSchema.shape, + }, + async ({ id }) => { + if (!scheduleService) { + throw new Error("Schedule service is not configured"); + } + await requireScheduleTarget(id, "new-agent"); + const schedule = await scheduleService.runOnce(id); + return { + content: [], + structuredContent: ensureValidJson(schedule), + }; + }, + ); + registerTool( "list_providers", { @@ -2407,146 +2894,6 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase }, ); - registerTool( - "list_worktrees", - { - title: "List worktrees", - description: "List Paseo-managed git worktrees for a repository.", - inputSchema: { - cwd: z - .string() - .optional() - .describe("Optional repository cwd. Defaults to your current working directory."), - }, - outputSchema: { - worktrees: z.array(WorktreeSummarySchema), - }, - }, - async ({ cwd }) => { - const resolvedCwd = resolveScopedCwd(cwd, { required: true }); - if (!options.workspaceGitService) { - throw new Error("WorkspaceGitService is required to list worktrees"); - } - const worktrees = await listPaseoWorktreesCommand( - { workspaceGitService: options.workspaceGitService }, - { - cwd: resolvedCwd, - reason: "mcp:list-worktrees", - }, - ); - - return { - content: [], - structuredContent: ensureValidJson({ worktrees }), - }; - }, - ); - - registerTool( - "create_worktree", - { - title: "Create worktree", - description: - "Create a Paseo-managed git worktree. Branch off a new branch, check out an existing branch, or check out a GitHub PR.", - inputSchema: { - cwd: z.string().optional().describe("Repository directory. Defaults to the agent's cwd."), - target: AgentCreateWorktreeTargetInputSchema.describe("What the worktree should contain."), - }, - outputSchema: { - branchName: z.string(), - worktreePath: z.string(), - workspaceId: z.string(), - }, - }, - async ({ cwd, target }) => { - const repoRoot = resolveScopedCwd(cwd, { required: true }); - const commandResult = await createPaseoWorktreeCommand( - { - paseoHome: options.paseoHome, - worktreesRoot: options.worktreesRoot, - createPaseoWorktreeWorkflow: options.createPaseoWorktree, - }, - createMcpWorktreeCommandInput(repoRoot, target), - ); - if (!commandResult.ok) { - throw new WorktreeRequestError(commandResult.error); - } - const { worktree, workspace } = commandResult.createdWorktree; - await options.workspaceGitService?.listWorktrees?.(repoRoot, { - force: true, - reason: "mcp:create-worktree", - }); - - return { - content: [], - structuredContent: ensureValidJson({ - branchName: worktree.branchName, - worktreePath: worktree.worktreePath, - workspaceId: workspace.workspaceId, - }), - }; - }, - ); - - registerTool( - "archive_worktree", - { - title: "Archive worktree", - description: "Delete a Paseo-managed git worktree.", - inputSchema: { - cwd: z - .string() - .optional() - .describe("Optional repository cwd. Defaults to your current working directory."), - worktreePath: z.string().optional(), - worktreeSlug: z.string().optional(), - }, - outputSchema: { - success: z.boolean(), - }, - }, - async ({ cwd, worktreePath, worktreeSlug }) => { - const resolvedCwd = resolveScopedCwd(cwd, { required: true }); - if (!worktreePath && !worktreeSlug) { - throw new Error("worktreePath or worktreeSlug is required"); - } - if (!options.workspaceGitService) { - throw new Error("WorkspaceGitService is required to archive worktrees"); - } - const repoRoot = await options.workspaceGitService.resolveRepoRoot(resolvedCwd); - - const result = await archiveCommand( - archiveWorktreeDependencies(options, { - agentManager, - agentStorage, - terminalManager: terminalManager ?? null, - logger: childLogger, - }), - { - requestId: "mcp:archive_worktree", - repoRoot, - worktreePath, - worktreeSlug, - // This tool archives every workspace on the directory, then removes the - // directory. Disk removal is derived from scope + last-reference. - scope: "worktree", - }, - ); - if (!result.ok) { - throw new Error(result.message); - } - await options.workspaceGitService.listWorktrees(repoRoot, { - force: true, - reason: "mcp:archive-worktree", - }); - - return { - content: [], - structuredContent: ensureValidJson({ success: true }), - }; - }, - ); - registerTool( "get_agent_activity", { @@ -2694,11 +3041,6 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase return toCatalog(); } -type McpCreateWorktreeTarget = - | { kind: "branch-off"; worktreeSlug?: string; branchName?: string; baseBranch?: string } - | { kind: "checkout-branch"; branch: string } - | { kind: "checkout-pr"; githubPrNumber: number }; - interface ArchiveWorktreeCommandContext { agentManager: AgentManager; agentStorage: AgentStorage; @@ -2758,26 +3100,3 @@ function archiveWorktreeDependencies( sessionLogger: context.logger, }; } - -function createMcpWorktreeCommandInput( - repoRoot: string, - target: McpCreateWorktreeTarget, -): CreatePaseoWorktreeCommandInput { - const base = { cwd: repoRoot } as const; - switch (target.kind) { - case "branch-off": - return { - ...base, - worktreeSlug: target.worktreeSlug, - branchName: target.branchName, - action: "branch-off", - ...(target.baseBranch ? { refName: target.baseBranch } : {}), - }; - case "checkout-branch": - return { ...base, action: "checkout", refName: target.branch }; - case "checkout-pr": - return { ...base, action: "checkout", githubPrNumber: target.githubPrNumber }; - default: - throw new Error("unreachable"); - } -} diff --git a/packages/server/src/server/bootstrap.ts b/packages/server/src/server/bootstrap.ts index 0157f81e2..c9d27c881 100644 --- a/packages/server/src/server/bootstrap.ts +++ b/packages/server/src/server/bootstrap.ts @@ -1210,6 +1210,16 @@ export async function createPaseoDaemon( archiveWorkspaceRecord: archiveWorkspaceRecordExternal, emitWorkspaceUpdatesForWorkspaceIds: emitWorkspaceUpdatesExternal, workspaceRegistry, + projectRegistry, + createDirectoryWorkspace: async (cwd, title, projectId) => { + const workspace = await workspaceProvisioning.createWorkspaceForDirectory( + cwd, + title, + projectId, + ); + await emitWorkspaceUpdatesExternal([workspace.workspaceId]); + return workspace; + }, markWorkspaceArchiving: markWorkspaceArchivingExternal, clearWorkspaceArchiving: clearWorkspaceArchivingExternal, ensureWorkspaceForCreate: createAgentCommandDependencies.ensureWorkspaceForCreate, diff --git a/packages/server/src/server/cli-run-workspace-precedence.e2e.test.ts b/packages/server/src/server/cli-run-workspace-precedence.e2e.test.ts index b99295e46..e0524c894 100644 --- a/packages/server/src/server/cli-run-workspace-precedence.e2e.test.ts +++ b/packages/server/src/server/cli-run-workspace-precedence.e2e.test.ts @@ -6,6 +6,7 @@ import path from "node:path"; import { DaemonClient } from "./test-utils/index.js"; import { createTestPaseoDaemon } from "./test-utils/paseo-daemon.js"; import { getFullAccessConfig } from "./daemon-e2e/agent-configs.js"; +import { PARENT_AGENT_ID_LABEL } from "@getpaseo/protocol/agent-labels"; // The daemon-level workspace contract that `paseo run` depends on: each // local-backed createWorkspace for a cwd mints a fresh, distinct workspace, @@ -28,9 +29,10 @@ async function mintLocalWorkspace(client: DaemonClient, cwd: string): Promise<st return result.workspace.id; } -test("daemon mints a distinct local workspace per run and stamps agents by id", async () => { +test("daemon resolves human and managed CLI workspace ownership", async () => { const daemon = await createTestPaseoDaemon(); const cwd = mkdtempSync(path.join(tmpdir(), "paseo-cli-run-cwd-")); + const otherCwd = mkdtempSync(path.join(tmpdir(), "paseo-cli-run-other-cwd-")); const client = new DaemonClient({ url: `ws://127.0.0.1:${daemon.port}/ws`, appVersion: "0.1.82", @@ -56,14 +58,13 @@ test("daemon mints a distinct local workspace per run and stamps agents by id", const fetchedFirst = await client.fetchAgent({ agentId: firstAgent.id }); expect(fetchedFirst?.agent.workspaceId).toBe(firstWorkspaceId); - // A second bare run in the SAME cwd mints a DISTINCT workspace; each run - // owns its own workspace rather than reattaching to the first. - const secondWorkspaceId = await mintLocalWorkspace(client, cwd); + // A second bare run mints a distinct workspace with its own authoritative cwd. + const secondWorkspaceId = await mintLocalWorkspace(client, otherCwd); expect(secondWorkspaceId).not.toBe(firstWorkspaceId); const secondAgent = await client.createAgent({ ...getFullAccessConfig("codex"), - cwd, + cwd: otherCwd, workspaceId: secondWorkspaceId, title: "Second run agent", }); @@ -80,15 +81,48 @@ test("daemon mints a distinct local workspace per run and stamps agents by id", const idsBeforeAttach = await workspaceIds(client); const attachedAgent = await client.createAgent({ ...getFullAccessConfig("codex"), - cwd, + cwd: path.join(otherCwd, "stale-client-directory"), workspaceId: firstWorkspaceId, title: "Attached agent", }); expect(attachedAgent.workspaceId).toBe(firstWorkspaceId); + expect(attachedAgent.cwd).toBe(cwd); expect(await workspaceIds(client)).toEqual(idsBeforeAttach); + + await expect( + client.createAgent({ + ...getFullAccessConfig("codex"), + cwd, + workspaceId: "wks_missing", + title: "Missing workspace agent", + }), + ).rejects.toThrow("Workspace wks_missing not found"); + + const sameWorkspaceChild = await client.createAgent({ + ...getFullAccessConfig("codex"), + cwd: otherCwd, + callerAgentId: firstAgent.id, + title: "Same workspace child", + }); + expect(sameWorkspaceChild.workspaceId).toBe(firstWorkspaceId); + expect(sameWorkspaceChild.cwd).toBe(firstAgent.cwd); + expect(sameWorkspaceChild.labels[PARENT_AGENT_ID_LABEL]).toBe(firstAgent.id); + expect(await workspaceIds(client)).toEqual(idsBeforeAttach); + + const crossWorkspaceChild = await client.createAgent({ + ...getFullAccessConfig("codex"), + cwd, + workspaceId: secondWorkspaceId, + callerAgentId: firstAgent.id, + title: "Cross workspace child", + }); + expect(crossWorkspaceChild.workspaceId).toBe(secondWorkspaceId); + expect(crossWorkspaceChild.cwd).toBe(otherCwd); + expect(crossWorkspaceChild.labels[PARENT_AGENT_ID_LABEL]).toBe(firstAgent.id); } finally { await client.close().catch(() => undefined); await daemon.close(); rmSync(cwd, { recursive: true, force: true }); + rmSync(otherCwd, { recursive: true, force: true }); } }, 180000); diff --git a/packages/server/src/server/paseo-worktree-service.test.ts b/packages/server/src/server/paseo-worktree-service.test.ts index bd30a28a1..13bb5ec79 100644 --- a/packages/server/src/server/paseo-worktree-service.test.ts +++ b/packages/server/src/server/paseo-worktree-service.test.ts @@ -57,6 +57,7 @@ test("creates a worktree and registers it in the source workspace project withou { cwd: repoDir, worktreeSlug: "feature-one", + title: "Feature One", runSetup: false, paseoHome: path.join(tempDir, ".paseo"), }, @@ -70,6 +71,7 @@ test("creates a worktree and registers it in the source workspace project withou expect(result.workspace.projectId).toBe("remote:github.com/acme/repo"); expect(result.workspace.displayName).toBe("feature-one"); expect(result.workspace.baseBranch).toBe("main"); + expect(result.workspace.title).toBe("Feature One"); expect(deps.workspaceGitService.getSnapshot).not.toHaveBeenCalled(); expect(deps.projects.get(sourceProject.projectId)).toEqual(sourceProject); expect(events).toEqual([`workspace:${result.workspace.workspaceId}`]); diff --git a/packages/server/src/server/paseo-worktree-service.ts b/packages/server/src/server/paseo-worktree-service.ts index 43a8570d9..818d98753 100644 --- a/packages/server/src/server/paseo-worktree-service.ts +++ b/packages/server/src/server/paseo-worktree-service.ts @@ -31,6 +31,7 @@ import type { FirstAgentContext } from "@getpaseo/protocol/messages"; export interface CreatePaseoWorktreeInput extends CreateWorktreeCoreInput { projectId?: string; + title?: string; } export interface CreatePaseoWorktreeResult { @@ -89,7 +90,7 @@ export async function createPaseoWorktree( worktreeRoot: createdWorktree.worktree.worktreePath, branch: createdWorktree.worktree.branchName || null, baseBranch: resolveIntentBaseBranch(createdWorktree.intent), - title: resolveFirstAgentPromptTitle(input.firstAgentContext), + title: input.title?.trim() || resolveFirstAgentPromptTitle(input.firstAgentContext), }); deps.github.invalidate({ cwd: createdWorktree.worktree.worktreePath }); diff --git a/packages/server/src/server/schedule/cron.ts b/packages/server/src/server/schedule/cron.ts index ab24746a5..2ac2e7897 100644 --- a/packages/server/src/server/schedule/cron.ts +++ b/packages/server/src/server/schedule/cron.ts @@ -87,6 +87,8 @@ export function validateScheduleCadence(cadence: ScheduleCadence): void { export function computeNextRunAt(cadence: ScheduleCadence, after: Date): Date { if (cadence.type === "every") { + // COMPAT(scheduleEveryMs): execute legacy persisted rolling intervals until the + // compatibility floor reaches v0.2.0. Added in v0.2.0; remove after 2027-01-17. return new Date(after.getTime() + cadence.everyMs); } diff --git a/packages/server/src/server/session.ts b/packages/server/src/server/session.ts index 8476973c4..7d7ad185a 100644 --- a/packages/server/src/server/session.ts +++ b/packages/server/src/server/session.ts @@ -77,6 +77,7 @@ import type { ManagedAgent, } from "./agent/agent-manager.js"; import { createAgentCommand } from "./agent/create-agent/create.js"; +import { resolveCreateAgentIntent, type CreateAgentIntent } from "./agent/create-agent/intent.js"; import { archiveAgentCommand, cancelAgentRunCommand, @@ -160,6 +161,7 @@ import { PushTokenStore } from "./push/token-store.js"; import { archivePersistedWorkspaceRecord, archiveWorkspaceContents, + requireActiveWorkspaceForArchive, } from "./workspace-archive-service.js"; import { WorkspaceReconciliationService } from "./workspace-reconciliation-service.js"; import type { ServiceProxySubsystem } from "./service-proxy.js"; @@ -236,6 +238,7 @@ import { } from "./project-directory-service.js"; import { runGitCommand } from "../utils/run-git-command.js"; import { CreateAgentLifecycleDispatch } from "./agent/create-agent-lifecycle-dispatch.js"; +import { resolveWorktreeSourceCwd } from "./workspace-source.js"; // TODO: Remove once all app store clients are on >=0.1.45 and understand arbitrary provider strings. // Clients before 0.1.45 validate providers with z.enum(["claude", "codex", "opencode"]) and reject @@ -330,6 +333,14 @@ type FetchAgentsResponsePayload = Extract< type FetchAgentsResponseEntry = FetchAgentsResponsePayload["entries"][number]; type FetchAgentsResponsePageInfo = FetchAgentsResponsePayload["pageInfo"]; type AgentUpdatesFilter = FetchAgentsRequestFilter; +type CreateAgentRequestMessage = Extract<SessionInboundMessage, { type: "create_agent_request" }>; + +interface ResolvedSessionCreateAgentIntent { + config: AgentSessionConfig; + intent: CreateAgentIntent; + createdDirectoryWorkspace: boolean; +} + type FetchWorkspacesRequestMessage = Extract< SessionInboundMessage, { type: "fetch_workspaces_request" } @@ -2778,9 +2789,7 @@ export class Session { /** * Handle create agent request */ - private async handleCreateAgentRequest( - msg: Extract<SessionInboundMessage, { type: "create_agent_request" }>, - ): Promise<void> { + private async handleCreateAgentRequest(msg: CreateAgentRequestMessage): Promise<void> { const { config, worktreeName, @@ -2793,7 +2802,6 @@ export class Session { autoArchive, images, attachments, - labels, env, } = msg; this.sessionLogger.info( @@ -2807,7 +2815,9 @@ export class Session { let createdAgentId: string | null = null; try { const requestedCwd = resolve(config.cwd); - if (!(await this.filesystem.isDirectory(requestedCwd))) { + const needsRequestedDirectory = + Boolean(worktreeName || git || worktree) || (!msg.workspaceId && !msg.callerAgentId); + if (needsRequestedDirectory && !(await this.filesystem.isDirectory(requestedCwd))) { throw new Error(`Working directory does not exist or is not a directory: ${requestedCwd}`); } const trimmedPrompt = initialPrompt?.trim(); @@ -2828,18 +2838,15 @@ export class Session { hasLegacyGitOptions: Boolean(git), }); createdWorktreeForCleanup = createdWorktree; - const createAgentConfig: AgentSessionConfig = createdWorktree - ? { ...config, cwd: createdWorktree.workspace.cwd } - : config; - const workspaceId = await this.workspaceProvisioning.resolveOrCreateWorkspaceIdForCreateAgent( - { - createdWorktree, - requestedWorkspaceId: msg.workspaceId, - cwd: createAgentConfig.cwd, - initialTitle: workspacePromptTitle, - }, - ); - const createdDirectoryWorkspaceForAgent = !createdWorktree && !msg.workspaceId; + const resolvedIntent = await this.resolveSessionCreateAgentIntent({ + request: msg, + createdWorktree, + workspacePromptTitle, + }); + const resolvedCwd = resolve(resolvedIntent.config.cwd); + if (!(await this.filesystem.isDirectory(resolvedCwd))) { + throw new Error(`Working directory does not exist or is not a directory: ${resolvedCwd}`); + } const { snapshot, liveSnapshot } = await createAgentCommand( { @@ -2852,8 +2859,8 @@ export class Session { }, { kind: "session", - config: createAgentConfig, - workspaceId, + config: resolvedIntent.config, + workspaceId: resolvedIntent.intent.workspaceId, worktreeName, initialPrompt, clientMessageId, @@ -2861,7 +2868,7 @@ export class Session { images, attachments, git, - labels, + labels: resolvedIntent.intent.labels, env, provisionalTitle, firstAgentContext, @@ -2871,14 +2878,14 @@ export class Session { ); createdAgentId = snapshot.id; await this.agentUpdates.forwardLiveAgent(snapshot); - if (createdDirectoryWorkspaceForAgent && trimmedPrompt) { + if (resolvedIntent.createdDirectoryWorkspace && trimmedPrompt) { this.workspaceAutoName.scheduleForDirectory( { - workspaceId, - cwd: createAgentConfig.cwd, + workspaceId: resolvedIntent.intent.workspaceId, + cwd: resolvedIntent.config.cwd, firstAgentContext, }, - { currentSelection: this.getFocusedAgentSelectionForCwd(createAgentConfig.cwd) }, + { currentSelection: this.getFocusedAgentSelectionForCwd(resolvedIntent.config.cwd) }, ); } this.createAgentLifecycleDispatch.registerAutoArchiveIfRequested({ @@ -2933,6 +2940,55 @@ export class Session { } } + private async resolveSessionCreateAgentIntent(input: { + request: CreateAgentRequestMessage; + createdWorktree: CreatePaseoWorktreeWorkflowResult | null; + workspacePromptTitle: string | null; + }): Promise<ResolvedSessionCreateAgentIntent> { + const { request, createdWorktree } = input; + const callerAgent = request.callerAgentId + ? this.agentManager.getAgent(request.callerAgentId) + : null; + if (request.callerAgentId && !callerAgent) { + throw new Error(`Caller agent ${request.callerAgentId} not found`); + } + + let config = request.config; + + const intent = await resolveCreateAgentIntent({ + explicitWorkspaceId: createdWorktree?.workspace.workspaceId ?? request.workspaceId, + caller: callerAgent + ? { id: callerAgent.id, cwd: callerAgent.cwd, workspaceId: callerAgent.workspaceId } + : null, + labels: request.labels, + resolveWorkspace: async (workspaceId) => { + if (createdWorktree?.workspace.workspaceId === workspaceId) { + return { workspaceId, cwd: createdWorktree.workspace.cwd }; + } + const workspace = await this.workspaceRegistry.get(workspaceId); + if (!workspace || workspace.archivedAt) { + throw new Error(`Workspace ${workspaceId} not found`); + } + return { workspaceId, cwd: workspace.cwd }; + }, + createWorkspace: async () => ({ + workspaceId: await this.workspaceProvisioning.resolveOrCreateWorkspaceIdForCreateAgent({ + createdWorktree: null, + cwd: config.cwd, + initialTitle: input.workspacePromptTitle, + }), + cwd: config.cwd, + }), + }); + config = { ...config, cwd: intent.cwd }; + + return { + config, + intent, + createdDirectoryWorkspace: !createdWorktree && !request.workspaceId && !callerAgent, + }; + } + private async handleResumeAgentRequest( msg: Extract<SessionInboundMessage, { type: "resume_agent_request" }>, ): Promise<void> { @@ -4874,10 +4930,7 @@ export class Session { return; } - const sourceCwd = await this.resolveWorktreeSourceCwd({ - cwd: source.cwd, - projectId: source.projectId, - }); + const sourceCwd = await resolveWorktreeSourceCwd(source, this.projectRegistry); const result = await this.createPaseoWorktreeWorkflow( { @@ -4886,24 +4939,17 @@ export class Session { worktreeSlug: source.worktreeSlug, action: source.action, refName: source.refName, + branchName: source.branchName, checkoutSource: source.checkoutSource, githubPrNumber: source.githubPrNumber, firstAgentContext: request.firstAgentContext, + title: request.title, }, source.baseBranch ? { resolveDefaultBranch: async () => source.baseBranch as string } : undefined, ); - if (request.title?.trim()) { - await this.workspaceRegistry.upsert({ - ...result.workspace, - title: request.title.trim(), - updatedAt: new Date().toISOString(), - }); - result.workspace.title = request.title.trim(); - } - const descriptor = await this.describeCreatedWorktreeWorkspace(result); this.emit({ type: "workspace.create.response", @@ -4917,20 +4963,6 @@ export class Session { await this.emitCreatedWorkspaceUpdate(descriptor); } - private async resolveWorktreeSourceCwd(input: { - cwd?: string; - projectId?: string; - }): Promise<string> { - if (input.cwd) { - return expandTilde(input.cwd); - } - const project = await this.projectRegistry.get(input.projectId as string); - if (!project || project.archivedAt) { - throw new Error(`Project not found: ${input.projectId}`); - } - return project.rootPath; - } - private async handleOpenProjectRequest( request: Extract<SessionInboundMessage, { type: "open_project_request" }>, ): Promise<void> { @@ -5399,10 +5431,10 @@ export class Session { request: Extract<SessionInboundMessage, { type: "archive_workspace_request" }>, ): Promise<void> { try { - const existing = await this.workspaceRegistry.get(request.workspaceId); - if (!existing) { - throw new Error(`Workspace not found: ${request.workspaceId}`); - } + const existing = await requireActiveWorkspaceForArchive( + { listActiveWorkspaces: () => this.listActiveWorkspaceRefs() }, + request.workspaceId, + ); await archiveByScope( { diff --git a/packages/server/src/server/workspace-archive-service.ts b/packages/server/src/server/workspace-archive-service.ts index 2d9a33fe2..8b6045a44 100644 --- a/packages/server/src/server/workspace-archive-service.ts +++ b/packages/server/src/server/workspace-archive-service.ts @@ -67,6 +67,19 @@ export interface ArchiveByScopeRequest { requestId: string; } +export async function requireActiveWorkspaceForArchive( + dependencies: Pick<ArchiveDependencies, "listActiveWorkspaces">, + workspaceId: string, +): Promise<ActiveWorkspaceRef> { + const workspace = (await dependencies.listActiveWorkspaces()).find( + (candidate) => candidate.workspaceId === workspaceId, + ); + if (!workspace) { + throw new Error(`Workspace not found: ${workspaceId}`); + } + return workspace; +} + interface BackingDirectory { path: string; isPaseoOwnedWorktree: boolean; diff --git a/packages/server/src/server/workspace-create-worktree-source.e2e.test.ts b/packages/server/src/server/workspace-create-worktree-source.e2e.test.ts index f2b2b299e..2fbdc930c 100644 --- a/packages/server/src/server/workspace-create-worktree-source.e2e.test.ts +++ b/packages/server/src/server/workspace-create-worktree-source.e2e.test.ts @@ -8,7 +8,7 @@ import { DaemonClient } from "./test-utils/index.js"; import { createTestPaseoDaemon } from "./test-utils/paseo-daemon.js"; // The reshaped workspace.create.request forwards its worktree `source` -// (action/refName/githubPrNumber/worktreeSlug) verbatim into createWorktreeCore. +// (action/refName/branchName/githubPrNumber/worktreeSlug) into createWorktreeCore. // The regression these tests guard against is the daemon dropping action/refName // while subsetting the request. We prove forwarding through the real workflow: // the created worktree's observable branch is the only honest evidence the @@ -64,7 +64,7 @@ test("workspace.create worktree source forwards action=checkout + refName into t } }, 180000); -test("workspace.create worktree source forwards branch-off + refName as the new branch", async () => { +test("workspace.create keeps a branch-off name separate from its worktree slug", async () => { const daemon = await createTestPaseoDaemon(); const { repoDir, tempRoot } = createGitRepoWithBranch(); const client = new DaemonClient({ @@ -80,15 +80,15 @@ test("workspace.create worktree source forwards branch-off + refName as the new kind: "worktree", cwd: repoDir, action: "branch-off", - worktreeSlug: "brand-new-branch", + branchName: "feature/auth", + worktreeSlug: "feature-auth", baseBranch: "main", }, }); expect(result.error).toBeNull(); - // branch-off cuts a new branch named after worktreeSlug from baseBranch; the - // worktree landing on that branch proves worktreeSlug/baseBranch forwarded. - expect(result.workspace?.gitRuntime?.currentBranch).toBe("brand-new-branch"); + expect(result.workspace?.gitRuntime?.currentBranch).toBe("feature/auth"); + expect(path.basename(result.workspace?.workspaceDirectory ?? "")).toBe("feature-auth"); } finally { await client.close().catch(() => undefined); await daemon.close(); diff --git a/packages/server/src/server/workspace-source.ts b/packages/server/src/server/workspace-source.ts new file mode 100644 index 000000000..21a53e627 --- /dev/null +++ b/packages/server/src/server/workspace-source.ts @@ -0,0 +1,25 @@ +import { expandTilde } from "../utils/path.js"; +import type { ProjectRegistry } from "./workspace-registry.js"; + +export interface WorktreeWorkspaceSource { + cwd?: string; + projectId?: string; +} + +export async function resolveWorktreeSourceCwd( + source: WorktreeWorkspaceSource, + projectRegistry: Pick<ProjectRegistry, "get">, +): Promise<string> { + if (source.cwd) { + return expandTilde(source.cwd); + } + if (!source.projectId) { + throw new Error("cwd or projectId is required for a worktree-backed workspace"); + } + + const project = await projectRegistry.get(source.projectId); + if (!project || project.archivedAt) { + throw new Error(`Project not found: ${source.projectId}`); + } + return project.rootPath; +} diff --git a/skills/paseo-handoff/SKILL.md b/skills/paseo-handoff/SKILL.md index 45f2ea4ae..102297544 100644 --- a/skills/paseo-handoff/SKILL.md +++ b/skills/paseo-handoff/SKILL.md @@ -17,7 +17,7 @@ Read the **paseo** skill. Before choosing a provider, read `~/.paseo/orchestrati ## Parsing arguments 1. **Provider** — explicit user request first; otherwise resolve from `impl` preference (or `ui` if the task is styling-only). -2. **Worktree** — "in a worktree" / "worktree" → create a worktree via Paseo with a short branch name derived from the task, based on the current branch. +2. **Isolation** — "in a worktree" / "worktree" → create a workspace with `isolation: "worktree"`, using a short branch name derived from the task. 3. **Task description** — anything else the user said. ## The handoff prompt @@ -54,18 +54,14 @@ The receiving agent has zero context. Include: ## Launch -Create the agent via Paseo with a `[Handoff] <task>` title, the briefing as initial prompt, and `relationship: { kind: "detached" }`. +Prepare the handoff in a dedicated workspace: -Use `workspace` for placement: +1. Select the current workspace or call `create_workspace` with the requested isolation. +2. Call `create_agent` with a `[Handoff] <task>` title, the briefing as initial prompt, and the selected `workspaceId` when explicit placement is needed. +3. Return the agent and workspace to the user, explaining that it remains in your subagent track until they detach it manually. -- No worktree: `workspace: { kind: "current" }`. -- Worktree: `workspace: { kind: "create", source: { kind: "worktree", target: { kind: "branch-off", worktreeSlug: "<short-task-slug>", branchName: "fix/<short-task-slug>" } } }`. -- Existing worktree already created by `create_worktree`: `workspace: { kind: "existing", workspaceId: "<returned-workspace-id>" }`. - -Do not use `workspace: { kind: "current", cwd: "<worktreePath>" }` to place a handoff in a worktree; that keeps the agent in the caller's workspace with only a different runtime cwd. +Do not encode independence as a create mode and do not invoke CLI or wire-level detach operations. Detach is a user gesture in the subagents track. Leave `notifyOnFinish` omitted unless the user explicitly wants no callback. -Handoff agents are siblings/root agents, not your subagents. They must survive you being archived and must not appear in your subagent track. - Don't wait by default — the user decides whether to follow along or move on. Tell them the agent ID and how to follow along (the paseo skill explains). diff --git a/skills/paseo-loop/SKILL.md b/skills/paseo-loop/SKILL.md index 966983e8d..2ee7be549 100644 --- a/skills/paseo-loop/SKILL.md +++ b/skills/paseo-loop/SKILL.md @@ -8,6 +8,8 @@ user-invocable: true A loop is a worker/verifier cycle: launch a worker → check verification → repeat until done or limits hit. Use for "keep trying", "babysit", or "watch this until X." +For lightweight recurring checks that should return to this same conversation, prefer a cron heartbeat (`create_heartbeat`, then `delete_heartbeat` when finished). To change it, delete and recreate it. Use a loop when each iteration needs a worker/verifier lifecycle; use a schedule when each firing should create a fresh independent agent and workspace. + **User's arguments:** $ARGUMENTS ## Prerequisites diff --git a/skills/paseo/SKILL.md b/skills/paseo/SKILL.md index 198b0a0fb..c94595b93 100644 --- a/skills/paseo/SKILL.md +++ b/skills/paseo/SKILL.md @@ -1,48 +1,29 @@ --- name: paseo -description: Paseo reference for managing agents and worktrees. Load whenever you need to create agents, send them prompts, or manage worktrees. +description: Paseo reference for managing workspaces, agents, schedules, and heartbeats. --- Paseo is a daemon that supervises AI coding agents on your machine. Control it through tools or a CLI. -## Worktrees +## Workspaces -**`create_worktree`** — same target union as `create_agent.workspace.source.worktree.target`: +**`create_workspace`** — create a workspace independently of any agent. Required: `isolation` (`local` or `worktree`). Worktree isolation supports `mode: "branch-off" | "checkout-branch" | "checkout-pr"`: use `branchName`/`baseBranch` for a new branch, `branch` for an existing branch, or `prNumber` plus optional `forge`/`projectPath` for a change request. `worktreeSlug` controls the managed path. Returns the workspace descriptor centered on `workspaceId`. -- From a PR: `{ target: { kind: "checkout-pr", githubPrNumber: 503 } }`. -- Branch off a base: `{ target: { kind: "branch-off", worktreeSlug: "foo", branchName: "fix/foo", baseBranch: "main" } }`. -- Checkout an existing branch: `{ target: { kind: "checkout-branch", branch: "feat/bar" } }`. +**`list_workspaces`** — list active workspaces. -Returns `{ branchName, worktreePath, workspaceId }`. Pass `cwd` to target a specific repo. +**`archive_workspace`** — `{ workspaceId }`. Archives the workspace, its agents, and its terminals. Local directories remain; Paseo removes an owned worktree only after its final active workspace reference is archived. -In `branch-off`, `worktreeSlug` controls the worktree path slug and `branchName` controls the git branch. If `branchName` is omitted, Paseo defaults it from `worktreeSlug`. The returned `branchName` is authoritative; checkout and PR flows may return a branch name that differs from any requested slug. - -**`list_worktrees`** — current repo (or pass `cwd`). -**`archive_worktree`** — `{ worktreePath }` or `{ worktreeSlug }`. Removes worktree and branch. +Worktree creation and reference accounting are implementation details of `isolation: "worktree"`. ## Agents -**`create_agent`** — required: `relationship`, `workspace`, `title`, `provider` (`claude/opus`, `codex/gpt-5.4`, …), `initialPrompt`. Common: `notifyOnFinish`, `settings`, `labels`. Returns `{ agentId, … }`. +**`create_agent`** — required: `title`, `provider` (`claude/opus`, `codex/gpt-5.4`, …), `initialPrompt`. Optional: `workspaceId`, `notifyOnFinish`, `settings`, `labels`. Returns `{ agentId, workspaceId, … }`. Initial runtime settings live under `settings`: `modeId`, `thinkingOptionId`, and provider-specific `features`. For Codex fast mode, pass `settings: { features: { "fast_mode": true } }` when creating the agent. -To create a new worktree and launch an agent in it, use `create_agent.workspace.source.kind = "worktree"`. Use `create_worktree` separately only when you need a worktree without launching an agent, or when you need a split flow; in a split flow, pass the returned `workspaceId` to `create_agent` with `workspace: { kind: "existing", workspaceId }`. +Agent-scoped creation always creates your subagent. Omit `workspaceId` to use your current workspace; pass a workspace returned by `create_workspace` for isolated delegation. Placement never changes parentage. -### Agent relationships - -`relationship` controls parentage only: - -- `{ kind: "subagent" }` — child under your subagents track. Use for advisors, committee members, planners, implementers, auditors, loop workers, and any agent whose lifetime belongs to your task. -- `{ kind: "detached" }` — root/sibling agent. Use for handoffs and fire-and-forget delegations the user may continue after you are archived. - -`workspace` controls placement only: - -- `{ kind: "current" }` — same workspace as the caller, with optional `cwd`. -- `{ kind: "existing", workspaceId: string, cwd?: string }` — attach to an existing workspace, usually from `create_worktree`. -- `{ kind: "create", source: { kind: "directory", path?: string } }` — new workspace rooted at a directory. -- `{ kind: "create", source: { kind: "worktree", cwd?: string, target: { kind: "branch-off", worktreeSlug?: string, branchName?: string, baseBranch?: string } } }` -- `{ kind: "create", source: { kind: "worktree", cwd?: string, target: { kind: "checkout-branch", branch: string } } }` -- `{ kind: "create", source: { kind: "worktree", cwd?: string, target: { kind: "checkout-pr", githubPrNumber: number } } }` +Detach is an explicit user action in the subagents track, not an agent tool. A cross-workspace child remains your subagent even though it also appears as a normal tab in its workspace. Agent-scoped `create_agent` defaults `notifyOnFinish` to true. Set it to `false` only for truly fire-and-forget agents. @@ -70,6 +51,10 @@ Only set feature IDs returned by `inspect_provider`. For Codex fast mode, look f **`create_heartbeat`** — sends you a prompt on a cron cadence. Required: `prompt`, `cron`. Optional: `timezone`, `name`, `maxRuns`, `expiresIn`. Use for reminders, PR/build babysitting, and status checks that should return to this conversation. +**`delete_heartbeat`** stops it. MCP intentionally exposes no heartbeat update tool; delete and recreate when its task or cadence changes. + +Schedules have the full list/inspect/update/pause/resume/run-once/log/delete surface. Heartbeats deliberately do not. + ## Models `claude/sonnet` (default), `claude/opus` (harder reasoning), `codex/gpt-5.4` (frontier coding), `claude/haiku` (tests only). @@ -110,16 +95,19 @@ For agent-scoped `create_agent` and background `send_agent_prompt`, leave `notif Don't poll `list_agents` or `get_agent_status` to "check on" a running agent. The notification will tell you. -## CLI parity +## CLI semantics -The `paseo` CLI is a thin wrapper over the same daemon. Same surface: +The CLI and tools use the same ownership semantics even where their syntax differs: ```bash -paseo run --provider codex/gpt-5.4 --mode full-access --worktree feat/x "<prompt>" +paseo workspace create --isolation worktree --mode branch-off --new-branch fix-x --base main +paseo workspace create --isolation worktree --mode checkout-branch --branch existing-work +paseo workspace create --isolation worktree --mode checkout-pr --pr-number 42 +paseo run --provider codex/gpt-5.4 --mode full-access --workspace <workspace-id> "<prompt>" paseo send <agent-id> "<follow-up>" paseo ls -paseo worktree ls paseo schedule create --cron "*/15 * * * *" "ping main build" +paseo heartbeat create --cron "*/15 * * * *" "check the build" ``` Discover with `paseo --help` and `paseo <cmd> --help`. From c97f823236107280eefcbff176e7c6676304e4bd Mon Sep 17 00:00:00 2001 From: nllptrx <48022579+nllptrx@users.noreply.github.com> Date: Sat, 18 Jul 2026 14:21:10 +0200 Subject: [PATCH 24/38] Fix commit-aware PR resolution across forges --- docs/forge-providers.md | 14 ++ .../app/src/git/pull-request-panel/pane.tsx | 1 + packages/protocol/src/messages.ts | 6 +- .../session/checkout/checkout-session.ts | 3 +- .../workspace-git-service.primitive.test.ts | 112 ++++++++++- .../src/server/workspace-git-service.ts | 58 +++++- .../src/services/forge-registry.test.ts | 5 +- packages/server/src/services/forge-service.ts | 10 +- .../server/src/services/gitea-service.test.ts | 177 +++++++++++++++--- packages/server/src/services/gitea-service.ts | 107 +++++++++-- .../src/services/github-service.test.ts | 129 ++++++++++++- .../server/src/services/github-service.ts | 50 ++++- .../src/services/gitlab-service.test.ts | 127 ++++++++++--- .../server/src/services/gitlab-service.ts | 42 +++-- .../server/src/utils/checkout-git.test.ts | 96 +++++++++- packages/server/src/utils/checkout-git.ts | 64 ++++++- 16 files changed, 879 insertions(+), 122 deletions(-) diff --git a/docs/forge-providers.md b/docs/forge-providers.md index f633a1922..6cc4c518e 100644 --- a/docs/forge-providers.md +++ b/docs/forge-providers.md @@ -84,6 +84,20 @@ Register the adapter in `defaultForgeRegistry` with: - `matchesHost` from manifest `cloudHosts` - `probeHost` when self-hosted/Enterprise detection is supported +Current change-request lookup uses two identities deliberately: + +- An open PR/MR belongs to the checkout when its head branch and head repository + match. Its remote head SHA may differ because the checkout can be ahead, + behind, or contain commits that have not been pushed yet. +- A merged or closed PR/MR belongs to the checkout only when its recorded head + SHA exactly matches the checkout's current `HEAD`. Branch names are reusable; + selecting the newest terminal request by branch alone can silently attach an + old promotion or feature request to new work. + +Thread the checkout head SHA through adapter cache and poll identities as well +as the lookup itself. Otherwise a commit made on the same branch can inherit the +previous commit's cached terminal status until the cache expires. + Cloud hosts in the manifest are a bounded public-host list, not a self-host allowlist. Self-hosted detection is a trust gate: Paseo only talks to a forge host that is either a known cloud host or one the CLI is already authenticated diff --git a/packages/app/src/git/pull-request-panel/pane.tsx b/packages/app/src/git/pull-request-panel/pane.tsx index 3aba5bc89..582d9543a 100644 --- a/packages/app/src/git/pull-request-panel/pane.tsx +++ b/packages/app/src/git/pull-request-panel/pane.tsx @@ -411,6 +411,7 @@ export function PullRequestPane({ repoName: data.repoName, checkRunId: ref.checkRunId, workflowRunId: ref.workflowRunId, + changeRequestNumber: data.number, }; // COMPAT(githubCheckDetailsRpc): added in v0.1.106, remove after 2026-12-28 once // all supported clients use checkout.forge.get_check_details.*. diff --git a/packages/protocol/src/messages.ts b/packages/protocol/src/messages.ts index f1338bd45..a566f8e5c 100644 --- a/packages/protocol/src/messages.ts +++ b/packages/protocol/src/messages.ts @@ -1762,9 +1762,9 @@ const CheckoutCheckDetailsRequestPayloadSchema = z.object({ checkRunId: z.number().int().positive().optional(), workflowRunId: z.number().int().positive().optional(), // Permanent forge-routing field, optional because only some forges need it: - // GitLab routes the check-details fetch to the change request's head pipeline - // by iid, so a fork/detached MR pipeline (which lives in the source project, - // not the checkout's target project) resolves correctly. GitHub ignores it. + // GitLab routes check details to the MR's head pipeline; Gitea-family adapters + // resolve the PR head SHA by number, including after merge/close. GitHub + // ignores it. changeRequestNumber: z.number().int().positive().optional(), requestId: z.string(), }); diff --git a/packages/server/src/server/session/checkout/checkout-session.ts b/packages/server/src/server/session/checkout/checkout-session.ts index 0b97961a0..3505a5b12 100644 --- a/packages/server/src/server/session/checkout/checkout-session.ts +++ b/packages/server/src/server/session/checkout/checkout-session.ts @@ -1220,7 +1220,8 @@ export class CheckoutSession { try { // The payload schema keeps checkRunId and workflowRunId optional (a Gitea - // Actions run has no check-run id; GitLab routes by changeRequestNumber), + // Actions run has no check-run id; forge adapters may also route by + // changeRequestNumber), // but a request that addresses no check at all is not actionable — reject // it here with a clear message instead of failing deep in an adapter. The // schema itself cannot enforce this: it is a discriminated-union member, so diff --git a/packages/server/src/server/workspace-git-service.primitive.test.ts b/packages/server/src/server/workspace-git-service.primitive.test.ts index eb76955a3..99664c530 100644 --- a/packages/server/src/server/workspace-git-service.primitive.test.ts +++ b/packages/server/src/server/workspace-git-service.primitive.test.ts @@ -1144,7 +1144,10 @@ describe("WorkspaceGitServiceImpl primitive refresh entrypoint", () => { createCheckoutFacts(cwd, { currentBranch: "feature", remoteUrl: "https://forge-self-heal.test/acme/repo.git", - pullRequestLookupTarget: { headRef: "feature" }, + pullRequestLookupTarget: { + headRef: "feature", + headSha: "1111111111111111111111111111111111111111", + }, }), ); const getCheckoutStatus = vi.fn(async (cwd: string) => @@ -1163,12 +1166,13 @@ describe("WorkspaceGitServiceImpl primitive refresh entrypoint", () => { const subscription = service.registerWorkspace({ cwd: REPO_CWD }, listener); await flushPromises(); - await vi.advanceTimersByTimeAsync(60_000); + await vi.advanceTimersByTimeAsync(120_000); await flushPromises(); expect(forge.getCurrentPullRequestStatus).toHaveBeenCalledWith({ cwd: REPO_CWD, headRef: "feature", + headSha: "1111111111111111111111111111111111111111", reason: "self-heal-forge-pr-status", }); expect(listener).toHaveBeenCalledWith( @@ -1189,6 +1193,108 @@ describe("WorkspaceGitServiceImpl primitive refresh entrypoint", () => { } }); + test("generic forge self-heal uses the fast poll window while checks are pending", async () => { + const forge = { + ...createGitHubServiceStub(), + retainCurrentPullRequestStatusPoll: undefined, + getCurrentPullRequestStatus: vi.fn(async () => + createCurrentPullRequestStatus({ checksStatus: "pending" }), + ), + }; + const unregister = defaultForgeRegistry.register("forge-pending-test", { + createService: () => forge, + matchesHost: (host) => host === "forge-pending.test", + }); + const pendingResult = createPullRequestStatusResult(); + if (pendingResult.status) { + pendingResult.status.checksStatus = "pending"; + pendingResult.status.checks = [{ name: "ci", status: "pending" }]; + } + const service = createService({ + getCheckoutSnapshotFacts: vi.fn(async (cwd: string) => + createCheckoutFacts(cwd, { + currentBranch: "feature", + remoteUrl: "https://forge-pending.test/acme/repo.git", + pullRequestLookupTarget: { headRef: "feature" }, + }), + ), + getCheckoutStatus: vi.fn(async (cwd: string) => + createCheckoutStatus(cwd, { + currentBranch: "feature", + remoteUrl: "https://forge-pending.test/acme/repo.git", + }), + ), + getPullRequestStatus: vi.fn(async () => pendingResult), + }); + + try { + const subscription = service.registerWorkspace({ cwd: REPO_CWD }, vi.fn()); + await flushPromises(); + await vi.advanceTimersByTimeAsync(20_000); + await flushPromises(); + + expect(forge.getCurrentPullRequestStatus).toHaveBeenCalledTimes(1); + subscription.unsubscribe(); + } finally { + service.dispose(); + unregister(); + } + }); + + test("generic forge poll refreshes immediately when checkout HEAD changes", async () => { + let nowMs = 0; + let headSha = "1111111111111111111111111111111111111111"; + const forge = { + ...createGitHubServiceStub(), + retainCurrentPullRequestStatusPoll: undefined, + getCurrentPullRequestStatus: vi.fn(async () => createCurrentPullRequestStatus()), + }; + const unregister = defaultForgeRegistry.register("forge-head-change-test", { + createService: () => forge, + matchesHost: (host) => host === "forge-head-change.test", + }); + const service = createService({ + now: () => new Date(nowMs), + getCheckoutSnapshotFacts: vi.fn(async (cwd: string) => + createCheckoutFacts(cwd, { + currentBranch: "feature", + remoteUrl: "https://forge-head-change.test/acme/repo.git", + pullRequestLookupTarget: { headRef: "feature", headSha }, + }), + ), + getCheckoutStatus: vi.fn(async (cwd: string) => + createCheckoutStatus(cwd, { + currentBranch: "feature", + remoteUrl: "https://forge-head-change.test/acme/repo.git", + }), + ), + getPullRequestStatus: vi.fn(async () => createPullRequestStatusResult("Visible PR")), + }); + + try { + await service.getSnapshot(REPO_CWD); + const subscription = service.registerWorkspace({ cwd: REPO_CWD }, vi.fn()); + expect(service.peekSnapshot(REPO_CWD)?.forge.pullRequest?.title).toBe("Visible PR"); + + headSha = "2222222222222222222222222222222222222222"; + nowMs = 3_000; + await service.refresh(REPO_CWD); + + expect(service.peekSnapshot(REPO_CWD)?.forge.pullRequest).toBeNull(); + expect(forge.getCurrentPullRequestStatus).not.toHaveBeenCalled(); + + await vi.advanceTimersByTimeAsync(0); + await flushPromises(); + + expect(forge.getCurrentPullRequestStatus).toHaveBeenCalledTimes(1); + expect(service.peekSnapshot(REPO_CWD)?.forge.pullRequest?.title).toBe("MR self-healed"); + subscription.unsubscribe(); + } finally { + service.dispose(); + unregister(); + } + }); + test("subscription cancels generic forge PR status self-heal polling after unsubscribe", async () => { const forge = { ...createGitHubServiceStub(), @@ -1221,7 +1327,7 @@ describe("WorkspaceGitServiceImpl primitive refresh entrypoint", () => { await flushPromises(); subscription.unsubscribe(); - await vi.advanceTimersByTimeAsync(60_000); + await vi.advanceTimersByTimeAsync(120_000); await flushPromises(); expect(forge.getCurrentPullRequestStatus).not.toHaveBeenCalled(); diff --git a/packages/server/src/server/workspace-git-service.ts b/packages/server/src/server/workspace-git-service.ts index a0c7818c5..71209da34 100644 --- a/packages/server/src/server/workspace-git-service.ts +++ b/packages/server/src/server/workspace-git-service.ts @@ -47,7 +47,9 @@ import { checkoutLiteFromGitSnapshot } from "./workspace-registry-model.js"; const WORKSPACE_GIT_WATCH_DEBOUNCE_MS = 1_000; const BACKGROUND_GIT_FETCH_INTERVAL_MS = 180_000; export const WORKSPACE_GIT_SELF_HEAL_INTERVAL_MS = 60_000; -const FORGE_PR_STATUS_POLL_INTERVAL_MS = 60_000; +const FORGE_PR_STATUS_POLL_FAST_INTERVAL_MS = 20_000; +const FORGE_PR_STATUS_POLL_SLOW_INTERVAL_MS = 120_000; +const FORGE_PR_STATUS_POLL_ERROR_BACKOFF_CAP_MS = 300_000; const WORKING_TREE_WATCH_FALLBACK_REFRESH_MS = 5_000; // Auxiliary reads may reuse cached values within this window; snapshots do not expire on read. const WORKSPACE_GIT_AUXILIARY_READ_TTL_MS = 15_000; @@ -336,6 +338,7 @@ interface WorkspaceGitAuxiliaryReadCacheEntry<T> { interface WorkspaceForgePrStatusPollTarget { headRef: string; + headSha?: string; headRepositoryOwner?: string; } @@ -1206,9 +1209,11 @@ export class WorkspaceGitServiceImpl implements WorkspaceGitService { remoteUrl, target: pollTarget, }); + const previousPollKey = target.forgePrStatusPollKey; if (target.forgePrStatusPollKey === pollKey && target.forgePrStatusPollSubscription) { return; } + const pollImmediately = previousPollKey !== null && previousPollKey !== pollKey; this.stopForgePrStatusPollForTarget(target); target.forgePrStatusPollKey = pollKey; @@ -1216,6 +1221,7 @@ export class WorkspaceGitServiceImpl implements WorkspaceGitService { target.forgePrStatusPollSubscription = resolution.service.retainCurrentPullRequestStatusPoll({ cwd: target.cwd, headRef: pollTarget.headRef, + ...(pollTarget.headSha ? { headSha: pollTarget.headSha } : {}), ...(pollTarget.headRepositoryOwner ? { headRepositoryOwner: pollTarget.headRepositoryOwner } : {}), @@ -1253,6 +1259,7 @@ export class WorkspaceGitServiceImpl implements WorkspaceGitService { forge: resolution.forge, service: resolution.service, pollTarget, + pollImmediately, }); } @@ -1261,23 +1268,28 @@ export class WorkspaceGitServiceImpl implements WorkspaceGitService { forge, service, pollTarget, + pollImmediately, }: { target: WorkspaceGitTarget; forge: string; service: ForgeService; pollTarget: WorkspaceForgePrStatusPollTarget; + pollImmediately: boolean; }): { unsubscribe: () => void } { let closed = false; let timer: NodeJS.Timeout | null = null; + let latestStatus: WorkspaceGitRuntimeSnapshot["forge"]["pullRequest"] = + target.latestForge?.pullRequest ?? null; + let consecutiveErrors = 0; - const schedule = () => { + const schedule = (delayMs: number) => { if (closed) { return; } timer = setTimeout(() => { timer = null; void poll(); - }, FORGE_PR_STATUS_POLL_INTERVAL_MS); + }, delayMs); }; const poll = async () => { @@ -1288,17 +1300,21 @@ export class WorkspaceGitServiceImpl implements WorkspaceGitService { const status = await service.getCurrentPullRequestStatus({ cwd: target.cwd, headRef: pollTarget.headRef, + ...(pollTarget.headSha ? { headSha: pollTarget.headSha } : {}), ...(pollTarget.headRepositoryOwner ? { headRepositoryOwner: pollTarget.headRepositoryOwner } : {}), reason: "self-heal-forge-pr-status", }); if (!closed && this.isActiveObservedWorkspaceTarget(target)) { + latestStatus = status; + consecutiveErrors = 0; this.rememberForgePrStatusSnapshot(target, buildForgeSnapshotFromStatus(status, forge), { notify: true, }); } } catch (error) { + consecutiveErrors += 1; this.logger.warn( { err: error, @@ -1311,11 +1327,16 @@ export class WorkspaceGitServiceImpl implements WorkspaceGitService { "Failed to run forge PR status self-heal refresh", ); } finally { - schedule(); + schedule(computeGenericForgeNextInterval(latestStatus, consecutiveErrors)); } }; - schedule(); + // A git-only refresh clears forge state when the commit-aware poll identity + // changes. Revalidate that new identity immediately instead of leaving the + // PR panel empty for the full stable polling interval. + schedule( + pollImmediately ? 0 : computeGenericForgeNextInterval(latestStatus, consecutiveErrors), + ); return { unsubscribe: () => { closed = true; @@ -2171,7 +2192,32 @@ function buildWorkspaceForgePrStatusPollKey({ remoteUrl: string; target: WorkspaceForgePrStatusPollTarget; }): string { - return JSON.stringify([forge, remoteUrl, target.headRef, target.headRepositoryOwner ?? null]); + return JSON.stringify([ + forge, + remoteUrl, + target.headRef, + target.headSha ?? null, + target.headRepositoryOwner ?? null, + ]); +} + +function computeGenericForgeNextInterval( + status: WorkspaceGitRuntimeSnapshot["forge"]["pullRequest"], + consecutiveErrors: number, +): number { + const isPending = + status?.checksStatus === "pending" || + status?.checks?.some((check) => check.status === "pending") === true; + const baseInterval = isPending + ? FORGE_PR_STATUS_POLL_FAST_INTERVAL_MS + : FORGE_PR_STATUS_POLL_SLOW_INTERVAL_MS; + if (consecutiveErrors <= 1) { + return baseInterval; + } + return Math.min( + baseInterval * 2 ** (consecutiveErrors - 1), + FORGE_PR_STATUS_POLL_ERROR_BACKOFF_CAP_MS, + ); } async function runGitFetch(cwd: string): Promise<void> { diff --git a/packages/server/src/services/forge-registry.test.ts b/packages/server/src/services/forge-registry.test.ts index dd12174ec..8671f51f8 100644 --- a/packages/server/src/services/forge-registry.test.ts +++ b/packages/server/src/services/forge-registry.test.ts @@ -10,10 +10,12 @@ describe("forge registry", () => { const gitlab = createForgeService("gitlab"); const gitea = createForgeService("gitea"); const forgejo = createForgeService("forgejo"); + const codeberg = createForgeService("codeberg"); expect(github?.getCurrentPullRequestStatus).toBeTypeOf("function"); expect(gitlab?.getCurrentPullRequestStatus).toBeTypeOf("function"); expect(gitea?.getCurrentPullRequestStatus).toBeTypeOf("function"); expect(forgejo?.getCurrentPullRequestStatus).toBeTypeOf("function"); + expect(codeberg?.getCurrentPullRequestStatus).toBeTypeOf("function"); }); it("returns null for an unregistered forge", () => { @@ -29,9 +31,10 @@ describe("forge registry", () => { expect(defaultForgeRegistry.has("gitlab")).toBe(true); expect(defaultForgeRegistry.has("gitea")).toBe(true); expect(defaultForgeRegistry.has("forgejo")).toBe(true); + expect(defaultForgeRegistry.has("codeberg")).toBe(true); expect(defaultForgeRegistry.has("bitbucket")).toBe(false); expect(defaultForgeRegistry.ids()).toEqual( - expect.arrayContaining(["github", "gitlab", "gitea", "forgejo"]), + expect.arrayContaining(["github", "gitlab", "gitea", "forgejo", "codeberg"]), ); }); diff --git a/packages/server/src/services/forge-service.ts b/packages/server/src/services/forge-service.ts index b0bf9c2a1..7f6d36465 100644 --- a/packages/server/src/services/forge-service.ts +++ b/packages/server/src/services/forge-service.ts @@ -271,10 +271,10 @@ export type GetCheckDetailsOptions = { checkRunId?: number; workflowRunId?: number; /** - * GitLab-only: the change request iid. GitLab routes the fetch to the change - * request's head pipeline (`glab ci get --merge-request`), so a fork/detached - * MR pipeline living in the source project resolves instead of 404ing against - * the checkout's target project. GitHub ignores it. + * Change request number used when check details must resolve against a + * specific request rather than the current branch. GitLab routes the fetch to + * the MR's head pipeline; Gitea-family adapters resolve the PR head SHA by + * number, including for terminal PRs. GitHub ignores it. */ changeRequestNumber?: number; } & ForgeReadOptions; @@ -456,6 +456,7 @@ export interface ForgeService { options: { cwd: string; headRef: string; + headSha?: string; headRepositoryOwner?: string; } & ForgeReadOptions, ): Promise<CurrentPullRequestStatus | null>; @@ -496,6 +497,7 @@ export interface ForgeService { retainCurrentPullRequestStatusPoll?(options: { cwd: string; headRef: string; + headSha?: string; headRepositoryOwner?: string; onStatus?: (status: CurrentPullRequestStatus | null) => void; onError?: (error: unknown) => void; diff --git a/packages/server/src/services/gitea-service.test.ts b/packages/server/src/services/gitea-service.test.ts index 51e97b88f..6b2028e66 100644 --- a/packages/server/src/services/gitea-service.test.ts +++ b/packages/server/src/services/gitea-service.test.ts @@ -74,6 +74,35 @@ const CONFLICTING_PR = { ci: "failure", }; +function currentPullRequestApi(input: { + number: number; + state: "open" | "closed"; + headRef: string; + headSha: string; + merged?: boolean; +}) { + return { + number: input.number, + html_url: `https://gitea.com/example-user/sample-repo/pulls/${input.number}`, + title: "Historical pull request", + body: "", + state: input.state, + merged: input.merged ?? false, + mergeable: true, + updated_at: "2026-06-26T10:00:00Z", + labels: [], + head: { + ref: input.headRef, + sha: input.headSha, + repo: { id: 1, owner: { login: "example-user" } }, + }, + base: { + ref: "main", + repo: { id: 1, owner: { login: "example-user" } }, + }, + }; +} + const STATUS_PR_VIEW = { id: 161482, index: 5, @@ -1048,6 +1077,43 @@ describe("createGiteaService", () => { ]); }); + it("resolves terminal PR check details by explicit change request number", async () => { + const headSha = "8888888888888888888888888888888888888888"; + const terminalView = { + ...STATUS_PR_VIEW, + index: 8, + state: "closed", + head: "feat/recently-closed", + headSha, + hasMerged: true, + mergedAt: "2026-06-28T17:00:00Z", + }; + const { service, calls } = makeService( + (args) => { + if (args[0] === "pr" && args[1] === "8") return ok(JSON.stringify(terminalView)); + if (args[0] === "api" && args[1].endsWith(`/commits/${headSha}/status`)) { + return ok(JSON.stringify(SAMPLE_COMBINED_STATUS)); + } + throw new Error(`unexpected call: ${args.join(" ")}`); + }, + { resolveCurrentBranch: async () => "feat/recently-closed" }, + ); + + const details = await service.getCheckDetails({ + cwd: "/repo", + repoOwner: "example-user", + repoName: "sample-repo", + checkRunId: 2, + changeRequestNumber: 8, + }); + + expect(details).toMatchObject({ checkRunId: 2, name: "ci/lint", status: "pending" }); + expect(calls).toEqual([ + ["pr", "8", "-o", "json"], + ["api", `repos/example-user/sample-repo/commits/${headSha}/status`], + ]); + }); + it("resolves Gitea Actions check details addressed only by workflowRunId", async () => { const { service } = makeService( (args) => { @@ -1172,6 +1238,9 @@ describe("createGiteaService", () => { const { service, calls } = makeService( (args) => { if (args[0] === "pr" && args[1] === "list") return ok(JSON.stringify([])); + if (args[0] === "api" && args[1].includes("/pulls?state=all")) { + return ok(JSON.stringify([])); + } throw new Error(`unexpected call: ${args.join(" ")}`); }, { @@ -1187,13 +1256,9 @@ describe("createGiteaService", () => { checkRunId: 2, }), ).rejects.toThrow("Gitea pull request for branch feat/nonexistent was not found"); - expect( - calls - .filter((args) => args[0] === "pr" && args[1] === "list") - .map((args) => [argValue(args, "--state"), argValue(args, "--page")]), - ).toEqual([ - ["open", "1"], - ["all", undefined], + expect(calls[1]).toEqual([ + "api", + "repos/example-user/sample-repo/pulls?state=all&sort=recentupdate&page=1&limit=50", ]); }); @@ -1337,7 +1402,7 @@ describe("createGiteaService", () => { expect(status?.checksStatus).toBe("failure"); }); - it("finds the current branch PR on the second open pull-request page", async () => { + it("finds the open current-branch PR even when its remote head SHA differs", async () => { const firstPage = Array.from({ length: 50 }, (_, index) => ({ ...OPEN_PR, index: String(100 + index), @@ -1367,6 +1432,7 @@ describe("createGiteaService", () => { const status = await service.getCurrentPullRequestStatus({ cwd: "/repo", headRef: "feat/sample-change", + headSha: "9999999999999999999999999999999999999999", }); expect(status?.number).toBe(5); @@ -1381,18 +1447,18 @@ describe("createGiteaService", () => { }); it("falls back to the recent all-state PR window for a recently closed current branch PR", async () => { - const closedPr = { - ...OPEN_PR, - index: "8", + const headSha = "8888888888888888888888888888888888888888"; + const closedPr = currentPullRequestApi({ + number: 8, state: "closed", - url: "https://gitea.com/example-user/sample-repo/pulls/8", - head: "feat/recently-closed", - }; + headRef: "feat/recently-closed", + headSha, + }); const { service, calls } = makeService((args) => { if (args[0] === "pr" && args[1] === "list" && argValue(args, "--state") === "open") { return ok(JSON.stringify([])); } - if (args[0] === "pr" && args[1] === "list" && argValue(args, "--state") === "all") { + if (args[0] === "api" && args[1].includes("/pulls?state=all")) { return ok(JSON.stringify([closedPr])); } throw new Error(`unexpected call: ${args.join(" ")}`); @@ -1401,22 +1467,79 @@ describe("createGiteaService", () => { const status = await service.getCurrentPullRequestStatus({ cwd: "/repo", headRef: "feat/recently-closed", + headSha, }); expect(status).toMatchObject({ number: 8, headRefName: "feat/recently-closed" }); - expect( - calls - .filter((args) => args[0] === "pr" && args[1] === "list") - .map((args) => [argValue(args, "--state"), argValue(args, "--page")]), - ).toEqual([ - ["open", "1"], - ["all", undefined], + expect(calls[1]).toEqual([ + "api", + "repos/example-user/sample-repo/pulls?state=all&sort=recentupdate&page=1&limit=50", ]); }); + it("uses tea repository context when origin identity is unavailable", async () => { + const headSha = "8888888888888888888888888888888888888888"; + const closedPr = currentPullRequestApi({ + number: 8, + state: "closed", + headRef: "feat/recently-closed", + headSha, + }); + const { service, calls } = makeService( + (args) => { + if (args[0] === "pr" && args[1] === "list") return ok("[]"); + if (args[0] === "api" && args[1].startsWith("repos/{owner}/{repo}/pulls?")) { + return ok(JSON.stringify([closedPr])); + } + throw new Error(`unexpected call: ${args.join(" ")}`); + }, + { resolveRemoteUrl: async () => null }, + ); + + const status = await service.getCurrentPullRequestStatus({ + cwd: "/repo", + headRef: "feat/recently-closed", + headSha, + }); + + expect(status).toMatchObject({ number: 8, headRefName: "feat/recently-closed" }); + expect(calls[1]).toEqual([ + "api", + "repos/{owner}/{repo}/pulls?state=all&sort=recentupdate&page=1&limit=50", + ]); + }); + + it("does not attach a stale Gitea-family PR after a same-name branch advances", async () => { + const stale = currentPullRequestApi({ + number: 8, + state: "closed", + headRef: "dev", + headSha: "1111111111111111111111111111111111111111", + merged: true, + }); + const { service } = makeService((args) => { + if (args[0] === "pr" && args[1] === "list") return ok("[]"); + if (args[0] === "api" && args[1].includes("/pulls?state=all")) { + return ok(JSON.stringify([stale])); + } + throw new Error(`unexpected call: ${args.join(" ")}`); + }); + + await expect( + service.getCurrentPullRequestStatus({ + cwd: "/repo", + headRef: "dev", + headSha: "2222222222222222222222222222222222222222", + }), + ).resolves.toBeNull(); + }); + it("returns null when no PR matches the current branch", async () => { const { service, calls } = makeService((args) => { if (args[0] === "pr" && args[1] === "list") return ok(JSON.stringify([])); + if (args[0] === "api" && args[1].includes("/pulls?state=all")) { + return ok(JSON.stringify([])); + } throw new Error(`unexpected call: ${args.join(" ")}`); }); @@ -1426,13 +1549,9 @@ describe("createGiteaService", () => { }); expect(status).toBeNull(); - expect( - calls - .filter((args) => args[0] === "pr" && args[1] === "list") - .map((args) => [argValue(args, "--state"), argValue(args, "--page")]), - ).toEqual([ - ["open", "1"], - ["all", undefined], + expect(calls[1]).toEqual([ + "api", + "repos/example-user/sample-repo/pulls?state=all&sort=recentupdate&page=1&limit=50", ]); }); diff --git a/packages/server/src/services/gitea-service.ts b/packages/server/src/services/gitea-service.ts index 372a62810..2d467a26e 100644 --- a/packages/server/src/services/gitea-service.ts +++ b/packages/server/src/services/gitea-service.ts @@ -234,6 +234,36 @@ const GiteaPullRequestApiSchema = z }) .passthrough(); +const GiteaCurrentPullRequestApiSchema = z + .object({ + number: z.number(), + html_url: z.string(), + title: z.string(), + body: z.string().nullable().optional(), + state: z.string(), + merged: z.boolean().optional(), + mergeable: z.boolean().nullable().optional(), + updated_at: z.string().optional(), + labels: z + .array(z.object({ name: z.string() }).passthrough()) + .optional() + .default([]), + head: z + .object({ + ref: z.string(), + sha: z.string(), + repo: GiteaPullRequestRepoSchema.nullable().optional(), + }) + .passthrough(), + base: z + .object({ + ref: z.string(), + repo: GiteaPullRequestRepoSchema.nullable().optional(), + }) + .passthrough(), + }) + .passthrough(); + const GiteaCommitStatusSchema = z .object({ id: z.number(), @@ -324,6 +354,7 @@ const GiteaReviewCommentSchema = z type GiteaPrListItem = z.infer<typeof GiteaPrListItemSchema>; type GiteaIssueListItem = z.infer<typeof GiteaIssueListItemSchema>; type GiteaPullRequestView = z.infer<typeof GiteaPullRequestViewSchema>; +type GiteaCurrentPullRequestApi = z.infer<typeof GiteaCurrentPullRequestApiSchema>; type GiteaCommitStatus = z.infer<typeof GiteaCommitStatusSchema>; type GiteaCombinedCommitStatus = z.infer<typeof GiteaCombinedCommitStatusSchema>; type GiteaActionsRun = z.infer<typeof GiteaActionsRunSchema>; @@ -801,6 +832,25 @@ function parseGiteaRepoFromUrl(url: string): { owner?: string; name?: string } { return {}; } +function currentPullRequestApiToListItem(item: GiteaCurrentPullRequestApi): GiteaPrListItem { + const headOwner = item.head.repo?.owner?.login; + const baseOwner = item.base.repo?.owner?.login; + const head = + headOwner && headOwner !== baseOwner ? `${headOwner}:${item.head.ref}` : item.head.ref; + return { + index: String(item.number), + state: item.merged ? "merged" : item.state, + url: item.html_url, + title: item.title, + body: item.body ?? "", + ...(item.mergeable != null ? { mergeable: String(item.mergeable) } : {}), + base: item.base.ref, + head, + updated: item.updated_at, + labels: item.labels.map((label) => label.name).join(","), + }; +} + function toPullRequestSummary(item: GiteaPrListItem): PullRequestSummary { const { owner, name } = parseGiteaRepoFromUrl(item.url); return { @@ -1347,6 +1397,7 @@ export function createGiteaService(options: CreateGiteaServiceOptions = {}): For async function loadCurrentPullRequestStatus(input: { cwd: string; headRef: string; + headSha?: string; }): Promise<CurrentPullRequestStatus | null> { const match = await findCurrentPullRequestForHeadRef(input); if (!match) { @@ -1356,7 +1407,9 @@ export function createGiteaService(options: CreateGiteaServiceOptions = {}): For return loadCurrentPullRequestChecks(input.cwd, match, status); } - async function resolveExpectedHeadOwner(cwd: string): Promise<string | null> { + async function resolveCurrentRepoIdentity( + cwd: string, + ): Promise<{ owner: string; name: string } | null> { const remoteUrl = await resolveRemoteUrl(cwd); if (!remoteUrl) { return null; @@ -1365,34 +1418,60 @@ export function createGiteaService(options: CreateGiteaServiceOptions = {}): For if (!location) { return null; } - return parseGitHubRemoteIdentity(location.path)?.owner ?? null; + const identity = parseGitHubRemoteIdentity(location.path); + return identity ? { owner: identity.owner, name: identity.name } : null; } async function findCurrentPullRequestForHeadRef(input: { cwd: string; headRef: string; + headSha?: string; }): Promise<GiteaPrListItem | null> { - const expectedHeadOwner = await resolveExpectedHeadOwner(input.cwd); + const repoIdentity = await resolveCurrentRepoIdentity(input.cwd); + const expectedHeadOwner = repoIdentity?.owner ?? null; const openMatch = await findOpenPullRequestForHeadRef(input, expectedHeadOwner); if (openMatch) { return openMatch; } - const recentItems = await listPullRequestItems({ - cwd: input.cwd, - state: "all", - limit: CURRENT_PR_LOOKUP_PAGE_SIZE, - }); - return ( - recentItems.find((item) => matchesCurrentHeadRef(item, input.headRef, expectedHeadOwner)) ?? - null + // tea discovers repository context from any configured git remote, while + // the default remote resolver reads origin only. Preserve that broader + // discovery for repositories whose primary remote has another name. + const repoPath = repoIdentity + ? `repos/${encodeURIComponent(repoIdentity.owner)}/${encodeURIComponent(repoIdentity.name)}` + : "repos/{owner}/{repo}"; + const recentItems = await runJsonArray( + [ + "api", + `${repoPath}/pulls?state=all&sort=recentupdate&page=1&limit=${CURRENT_PR_LOOKUP_PAGE_SIZE}`, + ], + { cwd: input.cwd }, + GiteaCurrentPullRequestApiSchema, ); + const candidates = recentItems.filter((item) => + matchesCurrentHeadRef( + currentPullRequestApiToListItem(item), + input.headRef, + expectedHeadOwner, + ), + ); + const match = + candidates.find((item) => mapGiteaState(item.state) === "open") ?? + candidates.find( + (item) => + mapGiteaState(item.state) !== "open" && + input.headSha !== undefined && + item.head.sha === input.headSha, + ) ?? + null; + return match ? currentPullRequestApiToListItem(match) : null; } async function findOpenPullRequestForHeadRef( input: { cwd: string; headRef: string; + headSha?: string; }, expectedHeadOwner: string | null, ): Promise<GiteaPrListItem | null> { @@ -1668,7 +1747,11 @@ export function createGiteaService(options: CreateGiteaServiceOptions = {}): For }, getCurrentPullRequestStatus(input): Promise<CurrentPullRequestStatus | null> { - return loadCurrentPullRequestStatus({ cwd: input.cwd, headRef: input.headRef }); + return loadCurrentPullRequestStatus({ + cwd: input.cwd, + headRef: input.headRef, + headSha: input.headSha, + }); }, async getPullRequest(input: GetPullRequestOptions): Promise<PullRequestSummary> { diff --git a/packages/server/src/services/github-service.test.ts b/packages/server/src/services/github-service.test.ts index 0b3fa2787..d82defec0 100644 --- a/packages/server/src/services/github-service.test.ts +++ b/packages/server/src/services/github-service.test.ts @@ -21,7 +21,7 @@ const EXPECTED_GITHUB_FAST_POLL_MS = 20_000; const EXPECTED_GITHUB_SLOW_POLL_MS = 120_000; const EXPECTED_GITHUB_ERROR_BACKOFF_CAP_MS = 300_000; const CURRENT_PR_STATUS_BASE_FIELDS = - "number,url,title,state,isDraft,baseRefName,headRefName,mergedAt,reviewDecision,mergeable,headRepositoryOwner"; + "number,url,title,state,isDraft,baseRefName,headRefName,headRefOid,mergedAt,reviewDecision,mergeable,headRepositoryOwner"; const CURRENT_PR_STATUS_FIELDS = `${CURRENT_PR_STATUS_BASE_FIELDS},statusCheckRollup`; interface RunnerCall { @@ -207,6 +207,7 @@ function currentPullRequestJson(overrides: Record<string, unknown> = {}): string isDraft: false, baseRefName: "main", headRefName: "feature/fork", + headRefOid: "1111111111111111111111111111111111111111", mergedAt: null, statusCheckRollup: [], reviewDecision: "REVIEW_REQUIRED", @@ -2246,6 +2247,26 @@ describe("ForgeService", () => { expect(status?.mergeable).toBe("UNKNOWN"); }); + it("keeps an open PR when its remote head SHA differs from the checkout HEAD", async () => { + const runner = createRunner([ + currentPullRequestJson({ + headRefOid: "1111111111111111111111111111111111111111", + }), + ]); + const service = createGitHubService({ + runner: runner.runner, + resolveGhPath: async () => "/usr/bin/gh", + }); + + const status = await service.getCurrentPullRequestStatus({ + cwd: "/repo", + headRef: "feature/fork", + headSha: "2222222222222222222222222222222222222222", + }); + + expect(status).toMatchObject({ number: 42, state: "open" }); + }); + it("loads GitHub merge, auto-merge, permission, policy, and queue facts for PR 993 shape", async () => { const runner = createScriptedRunner([ currentPullRequestJson({ @@ -2313,6 +2334,100 @@ describe("ForgeService", () => { }); }); + it("keeps a merged PR only when headRefOid matches the checkout HEAD", async () => { + const headSha = "2222222222222222222222222222222222222222"; + const runner = createRunner([ + currentPullRequestJson({ + state: "MERGED", + mergedAt: "2026-07-17T12:00:00Z", + headRefOid: headSha, + }), + ]); + const service = createGitHubService({ + runner: runner.runner, + resolveGhPath: async () => "/usr/bin/gh", + }); + + const status = await service.getCurrentPullRequestStatus({ + cwd: "/repo", + headRef: "feature/fork", + headSha, + }); + + expect(status?.number).toBe(42); + expect(status?.state).toBe("merged"); + }); + + it("does not attach a stale merged PR after a same-name branch advances", async () => { + const stale = currentPullRequestJson({ + state: "MERGED", + mergedAt: "2026-07-17T12:00:00Z", + headRefOid: "1111111111111111111111111111111111111111", + }); + const runner = createScriptedRunner([ + stale, + JSON.stringify({ owner: { login: "parentOwner" }, name: "parentRepo", parent: null }), + JSON.stringify([JSON.parse(stale)]), + ]); + const service = createGitHubService({ + runner: runner.runner, + resolveGhPath: async () => "/usr/bin/gh", + }); + + await expect( + service.getCurrentPullRequestStatus({ + cwd: "/repo", + headRef: "feature/fork", + headSha: "2222222222222222222222222222222222222222", + }), + ).resolves.toBeNull(); + expect(runner.calls.some((call) => call.args[0] === "pr" && call.args[1] === "list")).toBe( + true, + ); + }); + + it("prefers an open PR over an exact-SHA merged PR for the same head", async () => { + const checkoutSha = "2222222222222222222222222222222222222222"; + const owner = { login: "forkOwner" }; + const staleView = currentPullRequestJson({ + state: "MERGED", + mergedAt: "2026-07-15T12:00:00Z", + headRefOid: "0000000000000000000000000000000000000000", + headRepositoryOwner: owner, + }); + const open = JSON.parse( + currentPullRequestJson({ + number: 43, + state: "OPEN", + headRefOid: "3333333333333333333333333333333333333333", + headRepositoryOwner: owner, + }), + ); + const exactMerged = JSON.parse( + currentPullRequestJson({ + number: 42, + state: "MERGED", + mergedAt: "2026-07-16T12:00:00Z", + headRefOid: checkoutSha, + headRepositoryOwner: owner, + }), + ); + const runner = createScriptedRunner([staleView, JSON.stringify([exactMerged, open])]); + const service = createGitHubService({ + runner: runner.runner, + resolveGhPath: async () => "/usr/bin/gh", + }); + + const status = await service.getCurrentPullRequestStatus({ + cwd: "/repo", + headRef: "feature/fork", + headSha: checkoutSha, + headRepositoryOwner: "forkOwner", + }); + + expect(status).toMatchObject({ number: 43, state: "open" }); + }); + it("resolves fork PR heads to the parent repository when gh pr view returns a stale branch match", async () => { const runner = createScriptedRunner([ currentPullRequestJson({ @@ -2541,6 +2656,18 @@ describe("ForgeService", () => { expect(calls.map((call) => call.args)).toEqual([ ["pr", "view", "--json", CURRENT_PR_STATUS_FIELDS], ["repo", "view", "--json", "owner,name,parent"], + [ + "pr", + "list", + "--state", + "all", + "--head", + "main", + "--limit", + "10", + "--json", + CURRENT_PR_STATUS_FIELDS, + ], ]); }); diff --git a/packages/server/src/services/github-service.ts b/packages/server/src/services/github-service.ts index 163fb6d30..e2d82db88 100644 --- a/packages/server/src/services/github-service.ts +++ b/packages/server/src/services/github-service.ts @@ -325,6 +325,7 @@ const CurrentPullRequestStatusSchema = z.object({ isDraft: z.boolean().optional().catch(false), baseRefName: z.string().catch(""), headRefName: z.string().catch(""), + headRefOid: z.string().optional(), mergedAt: z.string().nullable().optional(), statusCheckRollup: z.unknown().optional(), reviewDecision: z.unknown().optional(), @@ -493,7 +494,7 @@ query PullRequestCheckoutTarget($owner: String!, $name: String!, $number: Int!) }`; const CURRENT_PR_STATUS_BASE_FIELDS = - "number,url,title,state,isDraft,baseRefName,headRefName,mergedAt,reviewDecision,mergeable,headRepositoryOwner"; + "number,url,title,state,isDraft,baseRefName,headRefName,headRefOid,mergedAt,reviewDecision,mergeable,headRepositoryOwner"; const CURRENT_PR_STATUS_FIELDS = `${CURRENT_PR_STATUS_BASE_FIELDS},statusCheckRollup`; const PULL_REQUEST_STATUS_FACTS_QUERY = ` @@ -716,6 +717,7 @@ interface InFlightCacheEntry { interface GitHubPollTarget { cwd: string; headRef: string; + headSha?: string; headRepositoryOwner?: string; retainCount: number; timer: NodeJS.Timeout | null; @@ -727,6 +729,7 @@ interface GitHubPollTarget { interface ResolvedPullRequestCandidate { status: CurrentPullRequestStatus; + headSha?: string; headRepositoryOwner?: string; } @@ -868,6 +871,7 @@ export function createGitHubService(options: CreateGitHubServiceOptions = {}): G function getPollTargetKey(target: { cwd: string; headRef: string; + headSha?: string; headRepositoryOwner?: string; }): string { return buildCacheKey({ @@ -875,6 +879,7 @@ export function createGitHubService(options: CreateGitHubServiceOptions = {}): G method: "getCurrentPullRequestStatus", args: { headRef: target.headRef, + headSha: target.headSha, headRepositoryOwner: target.headRepositoryOwner, }, }); @@ -883,6 +888,7 @@ export function createGitHubService(options: CreateGitHubServiceOptions = {}): G function updatePollTargetAfterSuccess(update: { cwd: string; headRef: string; + headSha?: string; headRepositoryOwner?: string; status: CurrentPullRequestStatus | null; notify: boolean; @@ -932,6 +938,7 @@ export function createGitHubService(options: CreateGitHubServiceOptions = {}): G await api.getCurrentPullRequestStatus({ cwd: target.cwd, headRef: target.headRef, + headSha: target.headSha, headRepositoryOwner: target.headRepositoryOwner, reason: "self-heal-github", }); @@ -1095,6 +1102,7 @@ export function createGitHubService(options: CreateGitHubServiceOptions = {}): G method: "getCurrentPullRequestStatus", args: { headRef: input.headRef, + headSha: input.headSha, headRepositoryOwner: input.headRepositoryOwner, }, readOptions: input, @@ -1102,6 +1110,7 @@ export function createGitHubService(options: CreateGitHubServiceOptions = {}): G const status = await resolveCurrentPullRequestView({ cwd: input.cwd, headRef: input.headRef, + headSha: input.headSha, headRepositoryOwner: input.headRepositoryOwner, run, }); @@ -1111,6 +1120,7 @@ export function createGitHubService(options: CreateGitHubServiceOptions = {}): G updatePollTargetAfterSuccess({ cwd: input.cwd, headRef: input.headRef, + headSha: input.headSha, headRepositoryOwner: input.headRepositoryOwner, status, notify: input.reason === "self-heal-github", @@ -1502,6 +1512,7 @@ export function createGitHubService(options: CreateGitHubServiceOptions = {}): G target = { cwd: input.cwd, headRef: input.headRef, + headSha: input.headSha, headRepositoryOwner: input.headRepositoryOwner, retainCount: 0, timer: null, @@ -1869,6 +1880,7 @@ function isStatusCheckRollupPermissionError(error: unknown): boolean { async function resolveCurrentPullRequestView(options: { cwd: string; headRef: string; + headSha?: string; headRepositoryOwner?: string; run: (args: string[], options: GitHubCommandRunnerOptions) => Promise<string>; }): Promise<CurrentPullRequestStatus | null> { @@ -1877,6 +1889,7 @@ async function resolveCurrentPullRequestView(options: { ? pickPullRequestCandidate({ candidates: [viewCandidate], headRef: options.headRef, + headSha: options.headSha, headRepositoryOwner: options.headRepositoryOwner, }) : null; @@ -1893,12 +1906,13 @@ async function resolveCurrentPullRequestView(options: { const forkOwner = repo?.owner?.login; const parentOwner = repo?.parent?.owner?.login; const parentName = repo?.parent?.name; - if (!forkOwner || !parentOwner || !parentName) { + if (!forkOwner) { return null; } - - listHeadRef = `${forkOwner}:${options.headRef}`; - listRepo = `${parentOwner}/${parentName}`; + if (parentOwner && parentName) { + listHeadRef = `${forkOwner}:${options.headRef}`; + listRepo = `${parentOwner}/${parentName}`; + } headRepositoryOwner = forkOwner; } @@ -1911,6 +1925,7 @@ async function resolveCurrentPullRequestView(options: { const match = pickPullRequestCandidate({ candidates, headRef: options.headRef, + headSha: options.headSha, headRepositoryOwner, }); return match?.status ?? null; @@ -2173,8 +2188,10 @@ function toCurrentPullRequestCandidate( return null; } const headRepositoryOwner = item.headRepositoryOwner?.login; + const headSha = item.headRefOid; return { status, + ...(headSha ? { headSha } : {}), ...(headRepositoryOwner ? { headRepositoryOwner } : {}), }; } @@ -2190,26 +2207,45 @@ function hasResolvedRepoIdentity(status: CurrentPullRequestStatus): boolean { function pickPullRequestCandidate(options: { candidates: ResolvedPullRequestCandidate[]; headRef: string; + headSha?: string; headRepositoryOwner?: string; }): ResolvedPullRequestCandidate | null { const matching = options.candidates.filter((candidate) => { if (!isCandidateForHeadRef(candidate, options.headRef)) { return false; } + if ( + candidate.status.state !== "open" && + (!options.headSha || candidate.headSha !== options.headSha) + ) { + return false; + } if (!options.headRepositoryOwner) { return true; } return candidate.headRepositoryOwner === options.headRepositoryOwner; }); - matching.sort(comparePullRequestCandidatePreference); + matching.sort((left, right) => + comparePullRequestCandidatePreference(left, right, options.headSha), + ); return matching[0] ?? null; } function comparePullRequestCandidatePreference( left: ResolvedPullRequestCandidate, right: ResolvedPullRequestCandidate, + headSha?: string, ): number { - return getPullRequestStateRank(left.status) - getPullRequestStateRank(right.status); + const stateRank = getPullRequestStateRank(left.status) - getPullRequestStateRank(right.status); + if (stateRank !== 0) { + return stateRank; + } + const leftExact = headSha !== undefined && left.headSha === headSha; + const rightExact = headSha !== undefined && right.headSha === headSha; + if (leftExact !== rightExact) { + return leftExact ? -1 : 1; + } + return 0; } function getPullRequestStateRank(status: CurrentPullRequestStatus): number { diff --git a/packages/server/src/services/gitlab-service.test.ts b/packages/server/src/services/gitlab-service.test.ts index dc9cc5294..3f930a720 100644 --- a/packages/server/src/services/gitlab-service.test.ts +++ b/packages/server/src/services/gitlab-service.test.ts @@ -33,6 +33,24 @@ function makeService(responder: Responder, overrides: Partial<CreateGitLabServic return { service, calls }; } +function currentMrListArgs(headRef: string): string[] { + return [ + "mr", + "list", + "--all", + "--source-branch", + headRef, + "--order", + "updated_at", + "--sort", + "desc", + "--per-page", + "100", + "-F", + "json", + ]; +} + function gitlabAutoMergeStatus( overrides: Partial<GitLabStatusFacts> = {}, ): PullRequestCommandStatus { @@ -60,6 +78,7 @@ const OPEN_MR = { state: "opened", source_branch: "release/v0.4.0", target_branch: "main", + sha: "1111111111111111111111111111111111111111", source_project_id: 101, target_project_id: 101, draft: false, @@ -231,7 +250,9 @@ const DISCUSSIONS = [ describe("createGitLabService", () => { it("maps a glab merge request view to the neutral current PR status", async () => { - const { service, calls } = makeService(() => ok(JSON.stringify(OPEN_MR))); + const { service, calls } = makeService((args) => + ok(JSON.stringify(args[1] === "list" ? [OPEN_MR] : OPEN_MR)), + ); const status = await service.getCurrentPullRequestStatus({ cwd: "/repo", @@ -261,23 +282,26 @@ describe("createGitLabService", () => { pipelineStatus: "success", mergeWhenPipelineSucceeds: false, }); - expect(calls[0]).toEqual(["mr", "view", "release/v0.4.0", "-F", "json"]); + expect(calls[0]).toEqual(currentMrListArgs("release/v0.4.0")); + expect(calls[1]).toEqual(["mr", "view", "14", "-F", "json"]); }); it("reports a conflicting merge request as CONFLICTING", async () => { - const { service } = makeService(() => - ok( - JSON.stringify({ ...OPEN_MR, has_conflicts: true, detailed_merge_status: "broken_status" }), - ), + const conflicting = { + ...OPEN_MR, + source_branch: "x", + has_conflicts: true, + detailed_merge_status: "broken_status", + }; + const { service } = makeService((args) => + ok(JSON.stringify(args[1] === "list" ? [conflicting] : conflicting)), ); const status = await service.getCurrentPullRequestStatus({ cwd: "/repo", headRef: "x" }); expect(status?.mergeable).toBe("CONFLICTING"); }); it("returns null when no merge request exists for the branch", async () => { - const { service } = makeService(() => { - throw { code: 1, stderr: "no open merge request available for 'feature/x'" }; - }); + const { service } = makeService(() => ok("[]")); const status = await service.getCurrentPullRequestStatus({ cwd: "/repo", headRef: "feature/x", @@ -285,6 +309,61 @@ describe("createGitLabService", () => { expect(status).toBeNull(); }); + it("selects a terminal merge request only when its head SHA matches the checkout", async () => { + const branch = "dev"; + const checkoutSha = "2222222222222222222222222222222222222222"; + const newestStale = { + ...OPEN_MR, + iid: 271, + state: "merged", + source_branch: branch, + sha: "1111111111111111111111111111111111111111", + updated_at: "2026-07-17T12:00:00.000Z", + }; + const exactOlder = { + ...OPEN_MR, + iid: 270, + state: "merged", + source_branch: branch, + sha: checkoutSha, + updated_at: "2026-07-16T12:00:00.000Z", + }; + const { service, calls } = makeService((args) => { + if (args[1] === "list") return ok(JSON.stringify([newestStale, exactOlder])); + if (args[1] === "view" && args[2] === "270") return ok(JSON.stringify(exactOlder)); + if (args[0] === "api") return ok("{}"); + throw new Error(`unexpected call: ${args.join(" ")}`); + }); + + const status = await service.getCurrentPullRequestStatus({ + cwd: "/repo", + headRef: branch, + headSha: checkoutSha, + }); + + expect(status?.number).toBe(270); + expect(calls[1]).toEqual(["mr", "view", "270", "-F", "json"]); + }); + + it("does not attach the latest historical merge request after a reused branch advances", async () => { + const stale = { + ...OPEN_MR, + state: "merged", + source_branch: "dev", + sha: "1111111111111111111111111111111111111111", + }; + const { service, calls } = makeService(() => ok(JSON.stringify([stale]))); + + await expect( + service.getCurrentPullRequestStatus({ + cwd: "/repo", + headRef: "dev", + headSha: "2222222222222222222222222222222222222222", + }), + ).resolves.toBeNull(); + expect(calls).toEqual([currentMrListArgs("dev")]); + }); + it("looks up a numeric current branch through the source-branch list filter", async () => { const numericBranchMr = { ...OPEN_MR, @@ -313,6 +392,7 @@ describe("createGitLabService", () => { const status = await service.getCurrentPullRequestStatus({ cwd: "/repo", headRef: "1234", + headSha: "2222222222222222222222222222222222222222", }); expect(status).toMatchObject({ @@ -320,7 +400,7 @@ describe("createGitLabService", () => { title: "Fix numeric branch", headRefName: "1234", }); - expect(calls[0]).toEqual(["mr", "list", "--source-branch", "1234", "-F", "json"]); + expect(calls[0]).toEqual(currentMrListArgs("1234")); expect(calls[1]).toEqual(["mr", "view", "21", "-F", "json"]); expect(calls).not.toContainEqual(["mr", "view", "1234", "-F", "json"]); }); @@ -342,7 +422,7 @@ describe("createGitLabService", () => { }); expect(status).toBeNull(); - expect(calls).toEqual([["mr", "list", "--source-branch", "1234", "-F", "json"]]); + expect(calls).toEqual([currentMrListArgs("1234")]); }); it("lists merge requests as neutral PR summaries", async () => { @@ -549,17 +629,16 @@ describe("createGitLabService", () => { }); it("surfaces the head pipeline id and url on the gitlab status facts", async () => { - const { service } = makeService(() => - ok( - JSON.stringify({ - ...OPEN_MR, - head_pipeline: { - id: 306, - status: "running", - web_url: "https://gitlab.example.com/example-group/example-project/-/pipelines/306", - }, - }), - ), + const pipelineMr = { + ...OPEN_MR, + head_pipeline: { + id: 306, + status: "running", + web_url: "https://gitlab.example.com/example-group/example-project/-/pipelines/306", + }, + }; + const { service } = makeService((args) => + ok(JSON.stringify(args[1] === "list" ? [pipelineMr] : pipelineMr)), ); const status = await service.getCurrentPullRequestStatus({ @@ -682,6 +761,7 @@ describe("createGitLabService", () => { it("populates approval counts from the approvals endpoint", async () => { const { service, calls } = makeService((args) => { + if (args[0] === "mr" && args[1] === "list") return ok(JSON.stringify([OPEN_MR])); if (args[0] === "mr" && args[1] === "view") return ok(JSON.stringify(OPEN_MR)); if (args[0] === "api" && args[1].endsWith("/approvals")) return ok(JSON.stringify(APPROVALS)); throw new Error(`unexpected call: ${args.join(" ")}`); @@ -697,7 +777,7 @@ describe("createGitLabService", () => { approvalsRequired: 2, approvalsGiven: 1, }); - expect(calls[1]).toEqual([ + expect(calls[2]).toEqual([ "api", "projects/example-group%2Fexample-project/merge_requests/14/approvals", ]); @@ -705,6 +785,7 @@ describe("createGitLabService", () => { it("falls back to zero approvals when the approvals endpoint returns an error", async () => { const { service } = makeService((args) => { + if (args[0] === "mr" && args[1] === "list") return ok(JSON.stringify([OPEN_MR])); if (args[0] === "mr" && args[1] === "view") return ok(JSON.stringify(OPEN_MR)); throw { code: 1, stderr: "500 Internal Server Error" }; }); diff --git a/packages/server/src/services/gitlab-service.ts b/packages/server/src/services/gitlab-service.ts index 204c99dab..dae4cb2b4 100644 --- a/packages/server/src/services/gitlab-service.ts +++ b/packages/server/src/services/gitlab-service.ts @@ -149,6 +149,7 @@ const GitLabMergeRequestSchema = z state: z.string(), source_branch: z.string(), target_branch: z.string(), + sha: z.string().optional(), source_project_id: z.number().nullable().optional(), target_project_id: z.number().nullable().optional(), draft: z.boolean().optional(), @@ -861,16 +862,26 @@ export function createGitLabService(options: CreateGitLabServiceOptions = {}): F return runJson(["mr", "view", ref, "-F", "json"], { cwd }, GitLabMergeRequestSchema); } - function isNumericBranchRef(ref: string): boolean { - return /^[0-9]+$/.test(ref); - } - - async function listOpenMergeRequestsBySourceBranch( + async function listMergeRequestsBySourceBranch( cwd: string, sourceBranch: string, ): Promise<GitLabMergeRequest[]> { return runJson( - ["mr", "list", "--source-branch", sourceBranch, "-F", "json"], + [ + "mr", + "list", + "--all", + "--source-branch", + sourceBranch, + "--order", + "updated_at", + "--sort", + "desc", + "--per-page", + "100", + "-F", + "json", + ], { cwd }, z.array(GitLabMergeRequestSchema), ); @@ -879,16 +890,17 @@ export function createGitLabService(options: CreateGitLabServiceOptions = {}): F async function resolveCurrentMergeRequest( cwd: string, headRef: string, + headSha?: string, ): Promise<GitLabMergeRequest | null> { - if (!isNumericBranchRef(headRef)) { - return viewMergeRequest(cwd, headRef); - } - - const mergeRequests = await listOpenMergeRequestsBySourceBranch(cwd, headRef); + const mergeRequests = await listMergeRequestsBySourceBranch(cwd, headRef); + const candidates = mergeRequests.filter((mr) => mr.source_branch === headRef); const match = - mergeRequests.find( - (mr) => mr.source_branch === headRef && mapMergeRequestState(mr.state) === "open", - ) ?? null; + candidates.find((mr) => mapMergeRequestState(mr.state) === "open") ?? + candidates.find( + (mr) => + mapMergeRequestState(mr.state) !== "open" && headSha !== undefined && mr.sha === headSha, + ) ?? + null; return match ? viewMergeRequest(cwd, String(match.iid)) : null; } @@ -1004,7 +1016,7 @@ export function createGitLabService(options: CreateGitLabServiceOptions = {}): F async getCurrentPullRequestStatus(input): Promise<CurrentPullRequestStatus | null> { try { - const mr = await resolveCurrentMergeRequest(input.cwd, input.headRef); + const mr = await resolveCurrentMergeRequest(input.cwd, input.headRef, input.headSha); if (!mr) { return null; } diff --git a/packages/server/src/utils/checkout-git.test.ts b/packages/server/src/utils/checkout-git.test.ts index cf07bfc97..c28586d8a 100644 --- a/packages/server/src/utils/checkout-git.test.ts +++ b/packages/server/src/utils/checkout-git.test.ts @@ -48,7 +48,11 @@ import { startGitCommandMetrics, stopGitCommandMetrics } from "./run-git-command import { createForgeResolver } from "../services/forge-resolver.js"; import { GitHubCommandError, GitHubCliMissingError } from "../services/github-service.js"; import type { CurrentPullRequestStatus, ForgeService } from "../services/forge-service.js"; -import { TeaAuthenticationError, TeaCliMissingError } from "../services/gitea-service.js"; +import { + TeaAuthenticationError, + TeaCliMissingError, + TeaCommandError, +} from "../services/gitea-service.js"; import { createWorktree as createWorktreePrimitive, type CreateWorktreeOptions, @@ -161,6 +165,7 @@ function createPullRequestStatus(overrides?: Partial<CurrentPullRequestStatus>) interface RequestedPullRequestTarget { headRef: string; + headSha?: string; headRepositoryOwner?: string; } @@ -176,6 +181,7 @@ function createGitHubServiceRecordingPullRequestTargets( github.getCurrentPullRequestStatus = async (request) => { options.requestedTargets.push({ headRef: request.headRef, + ...(request.headSha ? { headSha: request.headSha } : {}), ...(request.headRepositoryOwner ? { headRepositoryOwner: request.headRepositoryOwner } : {}), }); return createPullRequestStatus({ @@ -2341,7 +2347,8 @@ const x = 1; const lookupTarget = await readPullRequestLookupTargetFromFacts(repoDir, paseoHome); - expect(lookupTarget).toEqual({ headRef: "refactor/workspace-scripts" }); + expect(lookupTarget).toMatchObject({ headRef: "refactor/workspace-scripts" }); + expect(lookupTarget?.headSha).toMatch(/^[0-9a-f]{40}$/); }); it("keeps the local branch lookup when origin tracking uses the same head name", async () => { @@ -2356,7 +2363,8 @@ const x = 1; const lookupTarget = await readPullRequestLookupTargetFromFacts(repoDir, paseoHome); - expect(lookupTarget).toEqual({ headRef: "feature" }); + expect(lookupTarget).toMatchObject({ headRef: "feature" }); + expect(lookupTarget?.headSha).toMatch(/^[0-9a-f]{40}$/); }); it("does not attach an owner when the tracked remote is the same GitHub repository", async () => { @@ -2378,7 +2386,8 @@ const x = 1; const lookupTarget = await readPullRequestLookupTargetFromFacts(repoDir, paseoHome); - expect(lookupTarget).toEqual({ headRef: "refactor/workspace-scripts" }); + expect(lookupTarget).toMatchObject({ headRef: "refactor/workspace-scripts" }); + expect(lookupTarget?.headSha).toMatch(/^[0-9a-f]{40}$/); }); it("keeps the fork owner when same-repo comparison is indeterminate", async () => { @@ -2396,7 +2405,8 @@ const x = 1; const lookupTarget = await readPullRequestLookupTargetFromFacts(repoDir, paseoHome); - expect(lookupTarget).toEqual({ headRef: "main", headRepositoryOwner: "chethanuk" }); + expect(lookupTarget).toMatchObject({ headRef: "main", headRepositoryOwner: "chethanuk" }); + expect(lookupTarget?.headSha).toMatch(/^[0-9a-f]{40}$/); }); it("uses the configured push remote for fork PR lookup when upstream is absent", async () => { @@ -2425,8 +2435,11 @@ const x = 1; ); expect(getBranchUpstream(repoDir)).toBeNull(); - expect(factsTarget).toEqual({ headRef: "main", headRepositoryOwner: "chethanuk" }); - expect(requestedTargets).toEqual([{ headRef: "main", headRepositoryOwner: "chethanuk" }]); + expect(factsTarget).toMatchObject({ headRef: "main", headRepositoryOwner: "chethanuk" }); + expect(factsTarget?.headSha).toMatch(/^[0-9a-f]{40}$/); + expect(requestedTargets).toEqual([ + expect.objectContaining({ headRef: "main", headRepositoryOwner: "chethanuk" }), + ]); }); it("keeps the local branch lookup when same-repo tracking points at the base branch", async () => { @@ -2441,7 +2454,8 @@ const x = 1; const lookupTarget = await readPullRequestLookupTargetFromFacts(repoDir, paseoHome); - expect(lookupTarget).toEqual({ headRef: "tender-parrot" }); + expect(lookupTarget).toMatchObject({ headRef: "tender-parrot" }); + expect(lookupTarget?.headSha).toMatch(/^[0-9a-f]{40}$/); }); it("derives the same origin tracked head for on-demand PR status reads", async () => { @@ -2495,7 +2509,9 @@ const x = 1; const status = await getPullRequestStatus(repoDir, github); - expect(requestedTargets).toEqual([{ headRef: "main", headRepositoryOwner: "chethanuk" }]); + expect(requestedTargets).toEqual([ + expect.objectContaining({ headRef: "main", headRepositoryOwner: "chethanuk" }), + ]); expect(status.status?.number).toBe(345); expect(status.status?.headRefName).toBe("main"); }); @@ -2544,6 +2560,33 @@ const x = 1; expect(callCount).toBe(1); }); + it("does not reuse a PR status cache entry after HEAD changes on the same branch", async () => { + execFileSync("git", ["checkout", "-b", "feature"], { cwd: repoDir }); + execFileSync("git", ["remote", "add", "origin", "https://github.com/getpaseo/paseo.git"], { + cwd: repoDir, + }); + + const requestedShas: string[] = []; + const github = createGitHubServiceForStatus(null); + github.getCurrentPullRequestStatus = async (options) => { + if (options.headSha) requestedShas.push(options.headSha); + return createPullRequestStatus({ + url: `https://github.com/getpaseo/paseo/pull/${requestedShas.length}`, + }); + }; + + const first = await getPullRequestStatus(repoDir, github); + writeFileSync(join(repoDir, "next.txt"), "next\n"); + execFileSync("git", ["add", "next.txt"], { cwd: repoDir }); + execFileSync("git", ["commit", "-m", "next commit"], { cwd: repoDir }); + const second = await getPullRequestStatus(repoDir, github); + + expect(first.status?.url).toContain("/pull/1"); + expect(second.status?.url).toContain("/pull/2"); + expect(requestedShas).toHaveLength(2); + expect(requestedShas[0]).not.toBe(requestedShas[1]); + }); + it("passes forced PR status reads through to the GitHub service", async () => { execFileSync("git", ["checkout", "-b", "feature"], { cwd: repoDir }); execFileSync("git", ["remote", "add", "origin", "https://github.com/getpaseo/paseo.git"], { @@ -2637,6 +2680,41 @@ const x = 1; } }); + it("keeps stale PR status when a Gitea-family refresh hits a transient command error", async () => { + execFileSync("git", ["checkout", "-b", "feature"], { cwd: repoDir }); + execFileSync("git", ["remote", "add", "origin", "https://gitea.example.com/acme/repo.git"], { + cwd: repoDir, + }); + + __setPullRequestStatusCacheTtlForTests(50); + try { + let callCount = 0; + const service = createGitHubServiceForStatus(null); + service.getCurrentPullRequestStatus = async () => { + callCount += 1; + if (callCount === 1) { + return createPullRequestStatus({ url: "https://gitea.example.com/acme/repo/pulls/7" }); + } + throw new TeaCommandError({ + args: ["pr", "list"], + cwd: repoDir, + exitCode: 1, + stderr: "request timed out", + }); + }; + + const fresh = await getPullRequestStatus(repoDir, service); + await sleep(80); + const stale = await getPullRequestStatus(repoDir, service); + + expect(stale).toEqual(fresh); + expect(stale.status?.url).toContain("/pulls/7"); + expect(callCount).toBe(2); + } finally { + __resetPullRequestStatusCacheForTests(); + } + }); + it("does not use stale PR status fallback for forced GitHub errors", async () => { execFileSync("git", ["checkout", "-b", "feature"], { cwd: repoDir }); execFileSync("git", ["remote", "add", "origin", "https://github.com/getpaseo/paseo.git"], { diff --git a/packages/server/src/utils/checkout-git.ts b/packages/server/src/utils/checkout-git.ts index 331d2413f..1b0752ede 100644 --- a/packages/server/src/utils/checkout-git.ts +++ b/packages/server/src/utils/checkout-git.ts @@ -7,7 +7,7 @@ import type { Logger } from "pino"; import type { ParsedDiffFile } from "../server/utils/diff-highlighter.js"; import { parseAndHighlightDiff } from "../server/utils/diff-highlighter.js"; import { parseGitHubRepoFromRemote } from "../server/workspace-git-metadata.js"; -import { GitHubCommandError, createGitHubService } from "../services/github-service.js"; +import { createGitHubService } from "../services/github-service.js"; import type { CurrentPullRequestStatus, ForgeAuthState, @@ -15,7 +15,11 @@ import type { ForgeSpecificStatusFacts, PullRequestMergeable, } from "../services/forge-service.js"; -import { ForgeAuthenticationError, ForgeCliMissingError } from "../services/forge-cli-command.js"; +import { + ForgeAuthenticationError, + ForgeCliMissingError, + ForgeCommandError, +} from "../services/forge-cli-command.js"; import { parseGitRevParsePath, resolveGitRevParsePath } from "./git-rev-parse-path.js"; import { runGitCommand } from "./run-git-command.js"; import { isPaseoOwnedWorktreeCwd, resolvePaseoWorktreesBaseRoot } from "./worktree.js"; @@ -67,6 +71,7 @@ interface CheckoutReadCacheOptions { interface PullRequestStatusLookupTarget { headRef: string; + headSha?: string; headRepositoryOwner?: string; } @@ -116,8 +121,8 @@ function createShortstatCache(ttlMs: number) { }); } -function getPullRequestStatusCacheKey(cwd: string): string { - return resolve(cwd); +function getPullRequestStatusCacheKey(cwd: string, headSha: string | null): string { + return `${resolve(cwd)}\u0000${headSha ?? ""}`; } function rememberPullRequestStatus(cacheKey: string, status: PullRequestStatusResult): void { @@ -829,6 +834,38 @@ export async function getCurrentBranch(cwd: string): Promise<string | null> { } } +async function getCurrentHeadSha(cwd: string, context?: CheckoutContext): Promise<string | null> { + const knownSha = context?.facts?.isGit + ? context.facts.pullRequestLookupTarget?.headSha + : undefined; + if (knownSha) { + return knownSha; + } + try { + const { stdout } = await runGitCommand(["rev-parse", "HEAD"], { + cwd, + envOverlay: READ_ONLY_GIT_ENV, + logger: context?.logger, + }); + const sha = stdout.trim(); + return sha.length > 0 ? sha : null; + } catch { + return null; + } +} + +async function addHeadShaToPullRequestLookupTarget( + cwd: string, + target: PullRequestStatusLookupTarget | null, + context?: CheckoutContext, +): Promise<PullRequestStatusLookupTarget | null> { + if (!target) { + return null; + } + const headSha = await getCurrentHeadSha(cwd, context); + return headSha ? { ...target, headSha } : target; +} + async function getRebaseHeadBranch(cwd: string): Promise<string | null> { const paths = ["rebase-merge/head-name", "rebase-apply/head-name"]; const results = await Promise.all( @@ -1761,6 +1798,11 @@ export async function getCheckoutSnapshotFacts( context, )) ?? pullRequestLookupTarget; } + pullRequestLookupTarget = await addHeadShaToPullRequestLookupTarget( + cwd, + pullRequestLookupTarget, + context, + ); return { isGit: true, @@ -3402,7 +3444,8 @@ export async function getPullRequestStatus( options?: CheckoutReadCacheOptions, context?: CheckoutContext, ): Promise<PullRequestStatusResult> { - const cacheKey = getPullRequestStatusCacheKey(cwd); + const headSha = await getCurrentHeadSha(cwd, context); + const cacheKey = getPullRequestStatusCacheKey(cwd, headSha); if (!options?.force) { const cached = pullRequestStatusCache.get(cacheKey); if (cached) { @@ -3415,14 +3458,14 @@ export async function getPullRequestStatus( } } - const lookup = getPullRequestStatusUncached(cwd, forgeService, options, context) + const lookup = getPullRequestStatusUncached(cwd, forgeService, options, context, headSha) .then((status) => { pullRequestStatusCache.set(cacheKey, status); rememberPullRequestStatus(cacheKey, status); return status; }) .catch((error) => { - if (!options?.force && error instanceof GitHubCommandError) { + if (!options?.force && error instanceof ForgeCommandError) { const stale = lastSuccessfulPullRequestStatus.get(cacheKey); if (stale) { return stale; @@ -3443,6 +3486,7 @@ async function getPullRequestStatusUncached( forgeService: ForgeService, options?: CheckoutReadCacheOptions, context?: CheckoutContext, + headSha?: string | null, ): Promise<PullRequestStatusResult> { if (context?.facts?.isGit === false) { return buildPullRequestStatusResult(null, "no_remote"); @@ -3455,7 +3499,11 @@ async function getPullRequestStatusUncached( return buildPullRequestStatusResult(null, "no_remote"); } try { - const lookupTarget = await resolvePullRequestStatusLookupTarget(cwd, head, context); + const resolvedLookupTarget = await resolvePullRequestStatusLookupTarget(cwd, head, context); + const lookupTarget = + headSha && !resolvedLookupTarget.headSha + ? { ...resolvedLookupTarget, headSha } + : resolvedLookupTarget; let status: CurrentPullRequestStatus | null; if (options?.force) { const reason = options.reason; From 05d1f838d83a8a6156365438f2a1baf0aac114b5 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra <boudra.moha@gmail.com> Date: Sat, 18 Jul 2026 15:15:25 +0200 Subject: [PATCH 25/38] fix(composer): keep message input visible after dictation (#2194) The voice overlay transition could retain stale native opacity after the app was backgrounded, leaving the mounted composer transparent. Derive both surfaces directly from voice state so they cannot disagree after resume. --- packages/app/src/composer/input/input.tsx | 47 +++++++++---------- packages/app/src/composer/input/state.test.ts | 17 +++++++ packages/app/src/composer/input/state.ts | 26 ++++++++++ 3 files changed, 64 insertions(+), 26 deletions(-) diff --git a/packages/app/src/composer/input/input.tsx b/packages/app/src/composer/input/input.tsx index 6d57619ea..8a58c9e98 100644 --- a/packages/app/src/composer/input/input.tsx +++ b/packages/app/src/composer/input/input.tsx @@ -25,7 +25,6 @@ import { StyleSheet, withUnistyles } from "react-native-unistyles"; import { useTranslation } from "react-i18next"; import { ICON_SIZE, type Theme } from "@/styles/theme"; import { ArrowUp, Mic, MicOff, CornerDownLeft, Plus, Square } from "lucide-react-native"; -import Animated, { useSharedValue, useAnimatedStyle, withTiming } from "react-native-reanimated"; import { useDictation } from "@/hooks/use-dictation"; import { DictationOverlay } from "@/components/dictation-controls"; import { RealtimeVoiceOverlay } from "@/components/realtime-voice-overlay"; @@ -68,6 +67,7 @@ import { } from "./labels"; import { computeCanStartDictation, + resolveComposerSurfacePresentation, runAlternateSendAction, runDefaultSendAction, stopRealtimeVoice, @@ -1290,7 +1290,6 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>( getNativeElement: () => (isWeb ? getTextInputNativeElement(textInputRef.current) : null), })); const inputHeightRef = useRef(MIN_INPUT_HEIGHT); - const overlayTransition = useSharedValue(0); const sendAfterTranscriptRef = useRef(false); const valueRef = useRef(value); const serverInfo = useSessionStore( @@ -1407,6 +1406,7 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>( ); const showRealtimeOverlay = isRealtimeVoiceForCurrentAgent; const showOverlay = showDictationOverlay || showRealtimeOverlay; + const surfacePresentation = resolveComposerSurfacePresentation(showOverlay); useEffect(() => { if (isDictating || isDictationProcessing) { @@ -1427,22 +1427,6 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>( [canStartDictation, dictationUnavailableMessage, startDictation, toast], ); - // Animate overlay - useEffect(() => { - overlayTransition.value = withTiming(showOverlay ? 1 : 0, { - duration: 200, - }); - }, [overlayTransition, showOverlay]); - - const overlayAnimatedStyle = useAnimatedStyle(() => ({ - opacity: overlayTransition.value, - pointerEvents: overlayTransition.value > 0.5 ? "auto" : "none", - })); - - const inputAnimatedStyle = useAnimatedStyle(() => ({ - opacity: 1 - overlayTransition.value, - })); - const handleVoicePress = useCallback( () => handleVoicePressImpl({ @@ -1760,8 +1744,12 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>( }, [handleStopRealtimeVoice]); const inputWrapperCombinedStyle = useMemo( - () => [styles.inputWrapper, inputWrapperStyle, inputAnimatedStyle], - [inputWrapperStyle, inputAnimatedStyle], + () => [ + styles.inputWrapper, + inputWrapperStyle, + { opacity: surfacePresentation.input.opacity }, + ], + [inputWrapperStyle, surfacePresentation.input.opacity], ); const textInputStyle = useMemo( () => [styles.textInput, computeTextInputHeightStyle(inputHeight, maxInputHeight)], @@ -1772,8 +1760,8 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>( [isSendButtonDisabled], ); const overlayContainerStyle = useMemo( - () => [styles.overlayContainer, overlayAnimatedStyle], - [overlayAnimatedStyle], + () => [styles.overlayContainer, { opacity: surfacePresentation.overlay.opacity }], + [surfacePresentation.overlay.opacity], ); const renderAttachButtonIcon = useCallback( @@ -1802,7 +1790,11 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>( return ( <View ref={rootRef} style={styles.container} testID="message-input-root"> {/* Regular input */} - <Animated.View ref={inputWrapperRef} style={inputWrapperCombinedStyle}> + <View + ref={inputWrapperRef} + style={inputWrapperCombinedStyle} + pointerEvents={surfacePresentation.input.pointerEvents} + > {attachmentSlot} {/* Text input */} <View style={styles.textInputScrollWrapper}> @@ -1881,9 +1873,12 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>( /> </View> </View> - </Animated.View> + </View> - <Animated.View style={overlayContainerStyle}> + <View + style={overlayContainerStyle} + pointerEvents={surfacePresentation.overlay.pointerEvents} + > <MessageInputOverlay showDictationOverlay={showDictationOverlay} showRealtimeOverlay={showRealtimeOverlay} @@ -1901,7 +1896,7 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>( onDiscardFailedRecording={handleDiscardFailedRecording} onRealtimeVoiceStop={handleRealtimeVoiceStop} /> - </Animated.View> + </View> </View> ); }, diff --git a/packages/app/src/composer/input/state.test.ts b/packages/app/src/composer/input/state.test.ts index 8a11806ed..5e8420e77 100644 --- a/packages/app/src/composer/input/state.test.ts +++ b/packages/app/src/composer/input/state.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it, vi } from "vitest"; import { computeCanStartDictation, + resolveComposerSurfacePresentation, runAlternateSendAction, runDefaultSendAction, stopRealtimeVoice, @@ -9,6 +10,22 @@ import { const connected = { isConnected: true } as never; const disconnected = { isConnected: false } as never; +describe("composer surface presentation", () => { + it("shows only the input when no voice overlay is active", () => { + expect(resolveComposerSurfacePresentation(false)).toEqual({ + input: { opacity: 1, pointerEvents: "auto" }, + overlay: { opacity: 0, pointerEvents: "none" }, + }); + }); + + it("shows only the voice overlay while voice UI is active", () => { + expect(resolveComposerSurfacePresentation(true)).toEqual({ + input: { opacity: 0, pointerEvents: "none" }, + overlay: { opacity: 1, pointerEvents: "auto" }, + }); + }); +}); + describe("computeCanStartDictation", () => { it("returns false when socket is disconnected", () => { expect( diff --git a/packages/app/src/composer/input/state.ts b/packages/app/src/composer/input/state.ts index e2f714623..716433f4b 100644 --- a/packages/app/src/composer/input/state.ts +++ b/packages/app/src/composer/input/state.ts @@ -3,6 +3,32 @@ import type { MessagePayload } from "@/composer/types"; export type SendBehavior = "interrupt" | "queue"; +interface ComposerSurfaceState { + opacity: 0 | 1; + pointerEvents: "auto" | "none"; +} + +export interface ComposerSurfacePresentation { + input: ComposerSurfaceState; + overlay: ComposerSurfaceState; +} + +const INPUT_PRESENTATION: ComposerSurfacePresentation = { + input: { opacity: 1, pointerEvents: "auto" }, + overlay: { opacity: 0, pointerEvents: "none" }, +}; + +const OVERLAY_PRESENTATION: ComposerSurfacePresentation = { + input: { opacity: 0, pointerEvents: "none" }, + overlay: { opacity: 1, pointerEvents: "auto" }, +}; + +export function resolveComposerSurfacePresentation( + showOverlay: boolean, +): ComposerSurfacePresentation { + return showOverlay ? OVERLAY_PRESENTATION : INPUT_PRESENTATION; +} + interface StopRealtimeVoiceContext { voice: { stopVoice: () => Promise<unknown> } | null | undefined; isRealtimeVoiceForCurrentAgent: boolean; From 72752b7db6e7e98b87ee7e8ef8318a51272b8fb5 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra <boudra.moha@gmail.com> Date: Sat, 18 Jul 2026 15:18:05 +0200 Subject: [PATCH 26/38] Document workspace-first agent automation (#2192) * docs: explain workspace-first automation * docs: make automation examples self-contained --- public-docs/cli.md | 69 ++++++++++++++++++---- public-docs/mcp.md | 81 ++++++++++++++++---------- public-docs/metadata-generation.md | 2 +- public-docs/orchestration-workflows.md | 20 ++++--- public-docs/orchestration.md | 20 ++++--- public-docs/schedules-chat.md | 4 +- public-docs/schedules-cli.md | 28 ++++++--- public-docs/schedules.md | 23 ++++---- public-docs/skills.md | 8 +-- public-docs/why.md | 2 +- public-docs/workspaces.md | 20 ++++++- public-docs/worktrees.md | 56 +++++++++++++++--- 12 files changed, 237 insertions(+), 96 deletions(-) diff --git a/public-docs/cli.md b/public-docs/cli.md index 24df7f4ca..f568df3e0 100644 --- a/public-docs/cli.md +++ b/public-docs/cli.md @@ -1,6 +1,6 @@ --- title: CLI -description: "Paseo CLI reference: manage agents, daemons, permissions, and worktrees from your terminal." +description: "Paseo CLI reference: manage agents, workspaces, schedules, daemons, and permissions from your terminal." nav: CLI order: 3 category: Getting started @@ -10,7 +10,7 @@ category: Getting started The Paseo CLI lets you manage agents from your terminal. It's the same interface exposed by the daemon's API, so anything you can do in the app you can do from the command line. -> **Agent orchestration:** You can tell coding agents to use the Paseo CLI to spawn and manage other agents. This enables multi-agent workflows where one agent delegates subtasks to others and waits for results. +> **Agent orchestration:** You can tell coding agents to use the Paseo CLI to spawn and manage other agents. Paseo recognizes the calling agent, so CLI-created workers get the same workspace and parent defaults as MCP-created workers. ## Quick reference @@ -30,17 +30,59 @@ Use `paseo run` to start a new agent with a task: ```bash paseo run "implement user authentication" paseo run --provider codex "refactor the API layer" -paseo run --detach "run the full test suite" # background -paseo run --worktree feature-x "implement feature X" +paseo run --background "run the focused test suite" +paseo run --isolation worktree --base main "implement feature X" +paseo run --workspace <workspace-id> "review the current diff" paseo run --output-schema schema.json "extract release notes" paseo run --output-schema '{"type":"object","properties":{"summary":{"type":"string"}},"required":["summary"]}' "summarize release notes" ``` -The `--worktree` flag creates the agent in an isolated git worktree, useful for parallel feature development. +From a human shell, a bare `paseo run` creates a new local workspace for the current directory. Use `--workspace <id>` to add the agent to an existing workspace, or `--isolation worktree` to create a new workspace backed by an isolated git worktree. -Use `--output-schema` to return only matching JSON output. You can pass a schema file path or an inline JSON schema object. This mode cannot be used with `--detach`. +When an existing Paseo agent runs the same command, Paseo recognizes it through `PASEO_AGENT_ID`. Without explicit placement, the new agent becomes its subagent in the same workspace. `--workspace` can place that subagent elsewhere without changing its parent. -By default, `paseo run` waits for completion. Use `--detach` to run in the background. +Use `--output-schema` to return only matching JSON output. You can pass a schema file path or an inline JSON schema object. This mode cannot be used with `--background`. + +By default, `paseo run` waits for completion. Use `--background` to return immediately while the agent keeps running. + +## Workspaces + +Create a workspace independently when you want to prepare its files before starting an agent: + +```bash +paseo workspace create --isolation local --path ~/dev/my-app --title main + +paseo workspace create \ + --isolation worktree \ + --path ~/dev/my-app \ + --mode branch-off \ + --new-branch feature/auth \ + --worktree-slug feature-auth \ + --base main + +paseo workspace create \ + --isolation worktree \ + --path ~/dev/my-app \ + --mode checkout-branch \ + --branch feature/existing \ + --worktree-slug existing-copy + +paseo workspace create \ + --isolation worktree \ + --path ~/dev/my-app \ + --mode checkout-pr \ + --pr-number 2186 +``` + +Then list, use, or archive it: + +```bash +paseo workspace ls +paseo run --workspace <workspace-id> "implement authentication" +paseo workspace archive <workspace-id> +``` + +Add `--forge <name>` to PR checkout when Paseo cannot infer the forge from the source checkout. See [Git worktrees](/docs/worktrees) for setup hooks and services. ## Listing agents @@ -93,7 +135,7 @@ Useful in scripts or when one agent needs to wait for another. ## Schedules -Run an agent on an interval or a cron. See [Schedules from the CLI](/docs/schedules-cli) for the full reference. +Run an agent on a cron schedule. The CLI also accepts simple cadence presets and compiles them to cron. See [Schedules from the CLI](/docs/schedules-cli) for the full reference. ```bash paseo schedule create --every 30m --cwd ~/dev/my-app "Continue the refactor and leave a note." @@ -119,8 +161,11 @@ Change an agent's operational mode (provider-specific): paseo agent mode <id> --list # Show available modes paseo agent mode <id> bypass # Set bypass mode paseo agent mode <id> plan # Set plan mode +paseo agent detach <id> # Make a subagent top-level ``` +Detaching is an explicit lifecycle action, not a creation flag. The agent keeps running; only its relationship to its parent changes. + ## Daemon management ```bash @@ -157,11 +202,13 @@ The CLI is designed to be used by agents themselves. You can instruct an agent t ```bash # Agent A spawns Agent B and waits for it -paseo run --detach "implement the API" --name api-agent -paseo wait api-agent -paseo logs api-agent --tail 5 +agent_id=$(paseo run --background --quiet --title api-agent "implement the API") +paseo wait "$agent_id" +paseo logs "$agent_id" --tail 5 ``` +Because Agent A's ID is present in the environment, Agent B is created as its subagent in the same workspace unless `--workspace` is specified. + Simple implement + verify loop: ```bash diff --git a/public-docs/mcp.md b/public-docs/mcp.md index 26ab1b2a8..71ba24f08 100644 --- a/public-docs/mcp.md +++ b/public-docs/mcp.md @@ -16,31 +16,44 @@ Depending on the provider, Paseo delivers the catalog through its native tool in The MCP server itself is controlled by `daemon.mcp.enabled`. Existing agents may need a reload. +## Mental model + +Workspaces decide where work happens; agent parentage decides who owns the work. + +- An agent that calls `create_agent` without a `workspaceId` gets a subagent in its own workspace. +- Passing a `workspaceId` places that subagent in another workspace without detaching it from its parent. +- A top-level MCP caller without a workspace gets a new local workspace. +- Create a workspace first when you need worktree isolation, a specific branch, or a pull request checkout. + +MCP does not expose an agent-detach tool. Detaching is a manual user action in the app or CLI. + ## Tools ### Agents -| Tool | Function | -| -------------------- | ---------------------------------------------------------------------------------------------------- | -| `create_agent` | Create an agent tied to a working directory, optionally with initial settings or a new git worktree. | -| `send_agent_prompt` | Send a task to a running agent. | -| `get_agent_status` | Return the latest snapshot for an agent. | -| `list_agents` | List recent agents as compact metadata. | -| `cancel_agent` | Abort an agent's current run but keep the agent alive. | -| `archive_agent` | Soft-delete an agent and remove it from the active list. | -| `kill_agent` | Terminate an agent session permanently. | -| `update_agent` | Update an agent name, labels, or runtime settings such as mode/model/thinking/features. | -| `get_agent_activity` | Return recent agent timeline entries as a curated summary. | -| `set_agent_mode` | Switch an agent's session mode. | +| Tool | Function | +| -------------------- | --------------------------------------------------------------------------------------- | +| `create_agent` | Create an agent, optionally placing it in an existing workspace with `workspaceId`. | +| `send_agent_prompt` | Send a task to a running agent. | +| `get_agent_status` | Return the latest snapshot for an agent. | +| `list_agents` | List recent agents as compact metadata. | +| `cancel_agent` | Abort an agent's current run but keep the agent alive. | +| `archive_agent` | Soft-delete an agent and remove it from the active list. | +| `kill_agent` | Terminate an agent session permanently. | +| `update_agent` | Update an agent name, labels, or runtime settings such as mode/model/thinking/features. | +| `get_agent_activity` | Return recent agent timeline entries as a curated summary. | +| `set_agent_mode` | Switch an agent's session mode. | -### Workspaces and worktrees +### Workspaces -| Tool | Function | -| ------------------ | ----------------------------------------------------------------------------- | -| `rename_workspace` | Change the user-visible name of the current or specified workspace. | -| `list_worktrees` | List Paseo-managed git worktrees for a repository. | -| `create_worktree` | Create a Paseo-managed git worktree from a branch, base branch, or GitHub PR. | -| `archive_worktree` | Delete a Paseo-managed git worktree. | +| Tool | Function | +| ------------------- | ----------------------------------------------------------------------------------------------------- | +| `create_workspace` | Create a local or worktree-isolated workspace. Worktrees can branch off, check out a branch, or a PR. | +| `list_workspaces` | List active workspaces and their directories and isolation. | +| `rename_workspace` | Change the user-visible name of the current or specified workspace. | +| `archive_workspace` | Archive a workspace and the sessions it owns. | + +For worktree isolation, `create_workspace` accepts the same useful choices as the app: branch off from a base, check out an existing branch, or check out a pull request. The worktree remains an implementation detail of the workspace lifecycle. ### Terminals @@ -52,19 +65,25 @@ The MCP server itself is controlled by `daemon.mcp.enabled`. Existing agents may | `capture_terminal` | Capture plain-text output from a terminal session. | | `send_terminal_keys` | Send text or special key tokens to a terminal session. | -### Schedules +### Schedules and heartbeats -| Tool | Function | -| ------------------ | ----------------------------------------------------------------- | -| `create_schedule` | Create a recurring schedule that runs on an agent or a new agent. | -| `create_heartbeat` | Send a recurring prompt back into the current agent. | -| `list_schedules` | List schedules managed by the daemon. | -| `inspect_schedule` | Inspect a schedule and its run history. | -| `pause_schedule` | Pause an active schedule. | -| `resume_schedule` | Resume a paused schedule. | -| `update_schedule` | Change the cadence, prompt, limits, or other schedule settings. | -| `schedule_logs` | Return recent runs and output for a schedule. | -| `delete_schedule` | Delete a schedule permanently. | +Both use the same cron engine, but they have deliberately different interfaces. + +| Tool | Function | +| ------------------- | ---------------------------------------------------------------------------- | +| `create_schedule` | Create a cron schedule that starts a new agent for each run. | +| `list_schedules` | List new-agent schedules managed by the daemon. | +| `inspect_schedule` | Inspect a schedule and its run history. | +| `pause_schedule` | Pause an active schedule. | +| `resume_schedule` | Resume a paused schedule. | +| `update_schedule` | Change a schedule's cron, prompt, agent settings, limits, or other settings. | +| `schedule_logs` | Return recent runs and output for a schedule. | +| `run_schedule_once` | Start one new-agent schedule run without changing its cron. | +| `delete_schedule` | Delete a new-agent schedule permanently. | +| `create_heartbeat` | Send a recurring cron-backed prompt into the current agent. | +| `delete_heartbeat` | Delete one of the current agent's heartbeats. | + +MCP heartbeats are ephemeral: create or delete them. To change one, delete it and create a replacement. Pause, resume, update, inspect, logs, and run-once apply to new-agent schedules only. ### Providers diff --git a/public-docs/metadata-generation.md b/public-docs/metadata-generation.md index a5ca7fbb8..f9e7a7c21 100644 --- a/public-docs/metadata-generation.md +++ b/public-docs/metadata-generation.md @@ -13,7 +13,7 @@ Paseo asks a language model to write short pieces of text for you so you don't h Paseo generates these kinds of metadata: - **Workspace titles** — a short, task-shaped label for a workspace, shown in the sidebar. -- **Worktree branch names** — a slug for the branch a new worktree agent runs on. +- **Worktree branch names** — a slug for a new worktree-isolated workspace's branch. - **Commit messages** — a concise message for the changes you're committing. - **Pull request title and body** — drafted from the diff when you open a PR. diff --git a/public-docs/orchestration-workflows.md b/public-docs/orchestration-workflows.md index 0fbbefbac..c1b60328a 100644 --- a/public-docs/orchestration-workflows.md +++ b/public-docs/orchestration-workflows.md @@ -16,8 +16,8 @@ Keep a strong planner in the main chat and send implementation to a workhorse: ```text Stay as the orchestrator. Use Paseo to find the available Codex 5.6 model, then -create a subagent in a new worktree. Ask it to implement the parser change and -run the focused tests. +create a worktree-isolated workspace and launch a subagent there. Ask it to +implement the parser change and run the focused tests. ``` Ask the orchestrator to inspect providers first when you are unsure of the exact model ID. Available models come from your own installed and authenticated CLIs. @@ -36,12 +36,13 @@ Each worker appears in the Subagents track, and the orchestrator can keep workin ## Parallelize edits without collisions -Give each independent implementation its own git worktree: +Give each independent implementation its own worktree-isolated workspace: ```text -Split these two issues between two Paseo subagents. Create a separate worktree -from main for each issue, use the best available implementation model, and have -each agent run the focused checks for its change. Summarize both diffs when done. +Split these two issues between two Paseo subagents. Create a separate workspace +with worktree isolation from main for each issue, use the best available +implementation model, and have each agent run the focused checks for its change. +Summarize both diffs when done. ``` Use the current workspace for collaboration on the same files. Use worktrees when agents may edit independently. @@ -51,9 +52,10 @@ Use the current workspace for collaboration on the same files. Use worktrees whe Use different models for making and judging the change: ```text -Create a worker in a new worktree to implement this feature. When it finishes, -create a second subagent on the same worktree to review the diff for correctness, -missing tests, and unnecessary complexity. Bring the review back here. +Create a worktree-isolated workspace and launch a worker there to implement this +feature. When it finishes, create a second subagent in the same workspace to +review the diff for correctness, missing tests, and unnecessary complexity. +Bring the review back here. ``` The second agent sees the worker's files without sharing its conversation context, which makes the review more independent. diff --git a/public-docs/orchestration.md b/public-docs/orchestration.md index 2ab26bfe5..aec9b51f9 100644 --- a/public-docs/orchestration.md +++ b/public-docs/orchestration.md @@ -8,7 +8,7 @@ category: Orchestration # Orchestration -Paseo orchestration gives a coding agent control of the Paseo daemon. The agent can discover every provider and model you have configured, create worktrees, launch other agents, send them follow-ups, and create heartbeats or schedules. The same work stays visible in the Paseo app. +Paseo orchestration gives a coding agent control of the Paseo daemon. The agent can discover every provider and model you have configured, create workspaces, launch other agents, send them follow-ups, and create heartbeats or schedules. The same work stays visible in the Paseo app. ## Native subagents vs Paseo subagents @@ -22,13 +22,13 @@ Cursor => Claude Code (Fable 5) Native subagents belong to one provider. Claude Code launches Claude Code subagents; Codex launches Codex subagents. They are useful when the parent provider can handle the whole task itself. -Paseo subagents are full agents managed by the Paseo daemon. The orchestrator can choose any configured provider and model, place the worker in the current workspace or a new worktree, and keep coordinating it after launch. Use them when you want one model to plan, another to implement, and another to review. +Paseo subagents are full agents managed by the Paseo daemon. The orchestrator can choose any configured provider and model, keep the worker in the current workspace, or place it in another workspace created for the task. Use them when you want one model to plan, another to implement, and another to review. | | Native subagent | Paseo subagent | | -------------------- | ----------------------------------------- | -------------------------------------------------- | | Provider | Same provider as its parent | Any provider configured in Paseo | -| Working directory | Managed by the parent provider | Current workspace, existing workspace, or worktree | -| Lifecycle | Owned by the parent provider | Managed by Paseo; can receive follow-ups or detach | +| Working directory | Managed by the parent provider | Current or explicitly selected workspace | +| Lifecycle | Owned by the parent provider | Managed by Paseo; can receive follow-ups | | Where you inspect it | Read-only timeline in the Subagents track | Full agent session in the Subagents track | | Best for | Fast, provider-native delegation | Cross-provider work and explicit workspace control | @@ -40,26 +40,30 @@ Then ask naturally: ```text Stay as the orchestrator. Use Paseo to find my available Codex models, then -launch a GPT-5.6 subagent in a new worktree. Ask it to implement the parser -change, run the focused tests, and report back here. +create a worktree-isolated workspace, then launch a GPT-5.6 subagent there. Ask +it to implement the parser change, run the focused tests, and report back here. ``` The orchestrator discovers the provider and model IDs, starts the worker, and receives a notification when it finishes. You can keep talking to the orchestrator in the meantime. +Agent creation has one default: when an agent creates another agent without a workspace ID, the new agent is its subagent in the same workspace. Passing a workspace ID changes where the subagent works, not who its parent is. + ## Where the work appears Spawned work appears in the **Subagents track** above the composer. Open a row to read the live conversation. Both kinds of subagent appear there: -- **Paseo subagents** open as full agent sessions. You can talk to them directly, change their settings, detach them, or archive them. +- **Paseo subagents** open as full agent sessions. You can talk to them directly, change their settings, or archive them. - **Native provider subagents** open as read-only timelines. You can inspect their work, but their provider owns their lifecycle. +A cross-workspace subagent still belongs to its parent's Subagents track. Paseo also opens its workspace so the work is not hidden in an otherwise empty workspace. If you want to turn any subagent into a top-level agent, detach it manually in the app or with `paseo agent detach`; detachment is not an agent-creation mode. + If an agent says background work is running but the track is empty, update Paseo. Provider-created subagent timelines require Paseo 0.1.107 or newer. ## Keep an agent working with a heartbeat -A heartbeat sends a prompt back into the same agent on a cadence. Use one when the agent should keep reassessing a live task: continue a refactor, babysit CI, watch a deployment, or retry after an external system changes. +A heartbeat sends a prompt back into the same agent on a cron cadence. Use one when the agent should keep reassessing a live task: continue a refactor, babysit CI, watch a deployment, or retry after an external system changes. Ask the agent directly: diff --git a/public-docs/schedules-chat.md b/public-docs/schedules-chat.md index 807a81b24..dc5e67419 100644 --- a/public-docs/schedules-chat.md +++ b/public-docs/schedules-chat.md @@ -16,6 +16,6 @@ Example prompts: - "Check the release build every 5 minutes until it passes, and fix the cause if it fails." - "Keep working on this refactor — wake yourself every 20 minutes and continue where you left off." -The agent picks the cadence, target, and prompt from what you asked, creates the schedule, and reports back. You can manage it the same way — "pause the triage schedule", "make the build check run every 2 minutes instead", "delete it" — or from the [Schedules view](/docs/schedules) and the [CLI](/docs/schedules-cli). +The agent picks the cron cadence, agent settings, and prompt from what you asked, creates the schedule, and reports back. You can manage a new-agent schedule the same way — "pause the triage schedule", "make the build check run every 2 minutes instead", "run it once now", or "delete it" — or from the [Schedules view](/docs/schedules) and the [CLI](/docs/schedules-cli). -An agent scheduling itself to wake up later is a **heartbeat**. See [Paseo MCP](/docs/mcp) for the underlying tools. +An agent scheduling itself to wake up later is a **heartbeat**. Over MCP, an agent can create or delete its heartbeat; changing one means deleting it and creating a replacement. This small surface prevents a heartbeat from silently becoming a different job or agent. See [Paseo MCP](/docs/mcp) for the underlying tools. diff --git a/public-docs/schedules-cli.md b/public-docs/schedules-cli.md index a1efabc40..48ffbd66c 100644 --- a/public-docs/schedules-cli.md +++ b/public-docs/schedules-cli.md @@ -8,7 +8,7 @@ category: Schedules # Schedules from the CLI -`paseo schedule` creates and manages [schedules](/docs/schedules) from your terminal, useful for headless boxes and scripts. +`paseo schedule` creates and manages new-agent [schedules](/docs/schedules) from your terminal, useful for headless boxes and scripts. Every run starts a fresh agent. ## Create @@ -62,16 +62,28 @@ paseo schedule create \ "Review overnight CI failures and summarize anything urgent." ``` -Heartbeat the current agent: +## Heartbeats + +Inside a running Paseo agent, create a heartbeat for that same conversation: ```bash -paseo schedule create \ - --every 20m \ - --target self \ +paseo heartbeat create \ + --cron "*/20 * * * *" \ --name heartbeat \ "Check the current task state and continue with the next useful step." ``` +The heartbeat interface is deliberately small: + +```bash +paseo heartbeat update <id> --cron "*/10 * * * *" +paseo heartbeat delete <id> +``` + +Updating a heartbeat changes only its cron cadence and optional time zone. Its target and prompt stay fixed. Heartbeat commands require `PASEO_AGENT_ID`, which Paseo sets inside agent sessions. + +Heartbeats require a raw `--cron` expression. The `--every` presets below are available only for new-agent schedules. + ## Manage ```bash @@ -87,8 +99,10 @@ paseo schedule delete <id> ## Cadence -Use `--every <duration>` for intervals and `--cron "<expr>"` for 5-field cron. Cron schedules default to UTC. Pass `--timezone <IANA>` to interpret cron fields in a local wall-clock time zone, for example `--timezone America/New_York`. The persisted `nextRunAt` is still a UTC instant, but it is computed from that local time zone so recurring jobs stay at the same local time across daylight saving time changes. +Use `--cron "<expr>"` for a 5-field cron expression. For common cron-compatible cadences, `--every <duration>` accepts presets such as `5m` or `1h` and compiles them to cron. It does not create a rolling interval anchored to creation time. -Interval schedules run once immediately by default; pass `--no-run-now` to wait for the first interval. Cron schedules wait for the next matching time; pass `--run-now` to fire once immediately. +Schedules default to UTC. Pass `--timezone <IANA>` to interpret cron fields in a local wall-clock time zone, for example `--timezone America/New_York`. The persisted `nextRunAt` is still a UTC instant, but it is computed from that local time zone so recurring jobs stay at the same local time across daylight saving time changes. + +Schedules wait for the next matching cron time by default. Pass `--run-now` to start one immediate run on creation. When targeting a remote daemon with `--host`, pass `--cwd`; your local working directory may not exist on the remote machine. diff --git a/public-docs/schedules.md b/public-docs/schedules.md index 17611df65..5a1130737 100644 --- a/public-docs/schedules.md +++ b/public-docs/schedules.md @@ -1,6 +1,6 @@ --- title: Schedules -description: Run Paseo agents on a schedule — every few minutes or on a cron. +description: Run fresh agents on cron schedules or wake an existing agent with a heartbeat. nav: Overview order: 25 category: Schedules @@ -8,27 +8,28 @@ category: Schedules # Schedules -A schedule runs an agent for you on a cadence: at this interval or cron time, run this prompt, in this repo, with this agent. +A schedule starts a new agent for you on a cron cadence: at this time, run this prompt, in this repo, with these agent settings. -The target can be: +Paseo also has **heartbeats**. A heartbeat sends a recurring prompt back into one existing agent so it can reassess and continue the same conversation. -- A new agent each run — fresh daily jobs and long-running watchers. -- An existing agent — when you want continuity. -- The agent that created the schedule — heartbeats from inside an agent. +Both concepts use the same cron engine, but their product surfaces stay separate: -Cadence is either an interval, like every 30 minutes, or a cron expression, like every weekday morning. Every run is recorded, and you can pause, resume, run once, update, or delete a schedule at any time. +- **Schedules** create a new agent each run. You can inspect, pause, resume, run once, update, or delete them. +- **Heartbeats** target one existing agent. They are intentionally lightweight: create or delete them over MCP; from the CLI you can also update only their cron period. + +Cron is the canonical cadence. The CLI accepts simple presets such as `5m` or `1h`, but compiles them to cron rather than storing a separate interval type. ## What it's for -- **Overnight refactors:** wake an agent every 30 minutes to continue a scoped refactor, run checks, and leave notes. -- **Heartbeats:** have an agent periodically reassess state and keep moving. -- **Build babysitting:** check CI, EAS, Docker, or release builds until they pass. +- **Fresh recurring jobs:** start a clean agent for daily triage, reports, or maintenance. +- **Heartbeats:** have the current agent periodically reassess state and keep moving. +- **Build babysitting:** keep one agent checking CI, EAS, Docker, or release builds until they pass. - **Daily triage:** scan issues, PRs, and failing checks every morning. - **Maintenance sweeps:** refresh dependencies, audit docs, or clean stale branches. ## Ways to create one -- **In the app** — open the Schedules view and create one with an agent, a cadence, a repo, and a prompt. This is the main way to create and manage schedules. +- **In the app** — open the Schedules view and create one with agent settings, a cron cadence, a repo, and a prompt. This is the main way to create and manage schedules. - **[From chat](/docs/schedules-chat)** — ask the agent in a chat and it sets the schedule up for you. - **[From the CLI](/docs/schedules-cli)** — `paseo schedule create`, for headless boxes and scripts. - **[Over MCP](/docs/mcp)** — agents create and manage schedules programmatically. diff --git a/public-docs/skills.md b/public-docs/skills.md index a4509797e..1c4bd6f86 100644 --- a/public-docs/skills.md +++ b/public-docs/skills.md @@ -23,22 +23,22 @@ When the desktop app finds installed Paseo skills, it keeps the bundled skills u ## `/paseo`, Paseo Reference -The foundational skill. Paseo reference for managing agents and worktrees. Load it when an agent needs to create agents, send them prompts, or manage worktrees. +The foundational skill. Paseo reference for managing agents and workspaces. Load it when an agent needs to create agents, send them prompts, or manage workspace isolation. Not typically invoked directly by users, it's a reference that other skills depend on. ``` -/paseo show me the Paseo CLI surface for creating an agent in a worktree +/paseo show me the Paseo CLI surface for creating an agent in a worktree-isolated workspace ``` ## `/paseo-handoff`, Task Handoff Hands off the current task to another agent with full context. Use it when you say "handoff", "hand off", "hand this to", or want to pass work to another agent. -The receiving agent gets a self-contained briefing with the task, context, relevant files, current state, what's been tried, decisions, acceptance criteria, and constraints. Provider comes from orchestration preferences unless you name one. Supports worktrees when you ask for one. +The receiving agent gets a self-contained briefing with the task, context, relevant files, current state, what's been tried, decisions, acceptance criteria, and constraints. Provider comes from orchestration preferences unless you name one. Supports worktree-isolated workspaces when you ask for one. ``` -/paseo-handoff hand off the auth fix to codex in a worktree +/paseo-handoff hand off the auth fix to codex in a worktree-isolated workspace /paseo-handoff hand this to claude opus for review ``` diff --git a/public-docs/why.md b/public-docs/why.md index 89ce141e0..130c720fa 100644 --- a/public-docs/why.md +++ b/public-docs/why.md @@ -37,7 +37,7 @@ Paseo is a self-hostable platform for running and orchestrating coding agents. I ## Automation - The CLI exposes the same surface as the app. Anything in the UI is scriptable. -- [Paseo tools](/docs/orchestration). Agents can drive Paseo themselves: create worktrees, spawn other agents, open terminals, and send prompts. +- [Paseo tools](/docs/orchestration). Agents can drive Paseo themselves: create isolated workspaces, spawn subagents, open terminals, and send prompts. ## What it isn't diff --git a/public-docs/workspaces.md b/public-docs/workspaces.md index 70ed9b8af..f095c890c 100644 --- a/public-docs/workspaces.md +++ b/public-docs/workspaces.md @@ -1,6 +1,6 @@ --- title: Workspaces -description: Understand Paseo's project, workspace, and session model before setting up agents or git worktrees. +description: Understand how Paseo groups working directories, agents, terminals, and browsers into workspaces. nav: Workspaces order: 10 category: Workspaces @@ -35,14 +35,30 @@ That matters because real development rarely fits into one long chat. You might In Paseo, the workspace is the stable container. The sessions are what you run inside it. +## Choose the isolation + +Every workspace has an isolation mode: + +- **Local** uses an existing directory, such as your main checkout. Use it when sessions should share the files already on disk. +- **Worktree** creates or opens a managed git worktree. Use it when a task needs its own directory and branch. + +The workspace is the product concept; a git worktree is one way to isolate its files. More than one workspace can refer to the same managed worktree, and Paseo removes that worktree after its last workspace is archived. + ## Creating a workspace -When you create a new workspace, Paseo creates a working directory for it. If you are using git, this is an isolated git worktree on its own branch, so agents can work in parallel without touching your main checkout. Paseo names the branch from your first prompt. +You can create a workspace in the app or from the CLI: + +```bash +paseo workspace create --isolation local --path ~/dev/my-app --title main +paseo workspace create --isolation worktree --path ~/dev/my-app --base main +``` You can also create a workspace without starting an agent right away. The workspace is still there with its working directory ready; you can open terminals, run services, or browse files, then start an agent later. Either way, once the workspace exists you can add more sessions to it. Open a terminal alongside an agent, start a second agent to review changes, or open a browser tab to check a local service. Every session lives as a tab inside the same workspace. +Creating an agent and creating a workspace are separate actions. Pass a workspace ID when you want an agent in a specific existing workspace. A bare `paseo run` from a human shell creates a new local workspace; when one agent runs it, Paseo recognizes the caller and creates a subagent in the caller's workspace. + ## Worktrees Every workspace in Paseo is backed by a working directory. When that directory is a git worktree, you get a separate branch and isolated environment for each task. diff --git a/public-docs/worktrees.md b/public-docs/worktrees.md index 2911d0d9b..e413202b5 100644 --- a/public-docs/worktrees.md +++ b/public-docs/worktrees.md @@ -16,12 +16,12 @@ This page covers the git-specific details: where worktrees live, how branches ar ## Layout and workflow -Worktrees live under `$PASEO_HOME/worktrees/` by default, grouped by a hash of the source checkout path. You can change the base directory with `worktrees.root` in `config.json`. Each worktree gets a random slug; the branch name is chosen when you first launch an agent. +Worktrees live under `$PASEO_HOME/worktrees/` by default, grouped by a hash of the source checkout path. You can change the base directory with `worktrees.root` in `config.json`. Each worktree gets a slug and a branch when its workspace is created. ``` ~/.paseo/worktrees/ └── 1vnnm9k3/ # hash of source checkout path - ├── tidy-fox/ # worktree slug (branch set on first agent) + ├── tidy-fox/ # worktree slug └── bold-owl/ ``` @@ -35,10 +35,46 @@ With a custom root, Paseo keeps the same hashed layout under that directory: } ``` -1. Create a worktree, Paseo runs your setup hooks -2. Launch an agent, a branch is created or assigned +1. Create a workspace with worktree isolation, Paseo creates the worktree and runs your setup hooks +2. Launch one or more agents in that workspace 3. Review the diff against the base branch -4. Merge or archive, archive runs teardown and removes the directory +4. Merge or archive the workspace; after the last workspace using it is archived, Paseo runs teardown and removes the worktree + +## Create a worktree-backed workspace + +The examples below use the current directory as the source checkout. Pass `--path ~/dev/my-app` to create the workspace from another checkout. + +Branch off from a base branch: + +```bash +paseo workspace create \ + --isolation worktree \ + --mode branch-off \ + --new-branch feature/auth \ + --worktree-slug feature-auth \ + --base main +``` + +Check out an existing branch: + +```bash +paseo workspace create \ + --isolation worktree \ + --mode checkout-branch \ + --branch feature/existing \ + --worktree-slug existing-copy +``` + +Or open a pull request in its own workspace: + +```bash +paseo workspace create \ + --isolation worktree \ + --mode checkout-pr \ + --pr-number 2186 +``` + +Add `--forge <name>` when Paseo cannot infer the forge from the source checkout. ## paseo.json @@ -169,10 +205,12 @@ Services additionally get: - `$PASEO_SERVICE_<NAME>_PORT` / `_URL`, peer service ports and URLs - `$HOST`, `127.0.0.1` for local-only daemons, `0.0.0.0` when the daemon binds all interfaces -## CLI +## Manage the workspace ```bash -paseo run --worktree feature-auth --base main "implement auth" -paseo worktree ls -paseo worktree archive feature-auth +paseo workspace ls +paseo run --workspace <workspace-id> "implement auth" +paseo workspace archive <workspace-id> ``` + +For the common case, `paseo run --isolation worktree --base main "implement auth"` creates both the workspace and its first agent. From b4518cbf3352210cefec1715c00ad000c4abd20f Mon Sep 17 00:00:00 2001 From: "Jason@HND" <mildreins@gmail.com> Date: Sat, 18 Jul 2026 22:20:03 +0900 Subject: [PATCH 27/38] fix(server): remove wall-clock timeout for Pi compact RPC (#2181) Pi compact is a blocking LLM summarization job that often exceeds the default 30s control-plane timeout on long sessions, causing a false UI error while the real compact continues. Wait for the RPC response, process exit, or session close instead. Also cover the no-timeout lifecycle: null timeout still rejects on close, and compact waits past 30s for a late success response. Closes #1946 --- .../agent/providers/jsonl-rpc-process.test.ts | 23 +++ .../agent/providers/jsonl-rpc-process.ts | 41 ++++-- .../agent/providers/pi/cli-runtime.test.ts | 134 ++++++++++++++---- .../server/agent/providers/pi/cli-runtime.ts | 32 ++++- .../src/server/agent/providers/pi/runtime.ts | 7 +- .../agent/providers/pi/test-utils/fake-pi.ts | 2 +- 6 files changed, 192 insertions(+), 47 deletions(-) diff --git a/packages/server/src/server/agent/providers/jsonl-rpc-process.test.ts b/packages/server/src/server/agent/providers/jsonl-rpc-process.test.ts index f8c059307..26e33043f 100644 --- a/packages/server/src/server/agent/providers/jsonl-rpc-process.test.ts +++ b/packages/server/src/server/agent/providers/jsonl-rpc-process.test.ts @@ -140,6 +140,29 @@ describe("JsonlRpcProcess", () => { } }); + test("null timeout waits past short wall-clock limits until the response arrives", async () => { + const transport = startProcess(); + + try { + await expect( + transport.request({ type: "echo", value: "slow", delayMs: 80 }, null), + ).resolves.toMatchObject({ value: "slow" }); + } finally { + await transport.close(); + } + }); + + test("null timeout still rejects when the process is closed", async () => { + const transport = startProcess(); + await transport.request({ type: "echo", value: "ready" }); + const request = transport.request({ type: "hang" }, null); + + const rejection = expect(request).rejects.toThrow("JSONL RPC process is closed"); + await transport.close(); + + await rejection; + }); + test("rejects pending requests and publishes stderr when the child exits", async () => { const transport = startProcess(); const exit = nextExit(transport); diff --git a/packages/server/src/server/agent/providers/jsonl-rpc-process.ts b/packages/server/src/server/agent/providers/jsonl-rpc-process.ts index 2fddf5995..2e56a0659 100644 --- a/packages/server/src/server/agent/providers/jsonl-rpc-process.ts +++ b/packages/server/src/server/agent/providers/jsonl-rpc-process.ts @@ -4,7 +4,14 @@ import type { Logger } from "pino"; import { spawnProcess } from "../../../utils/spawn.js"; import { terminateWithTreeKill } from "../../../utils/tree-kill.js"; -const DEFAULT_TIMEOUT_MS = 30_000; +/** Default wall-clock timeout for control-plane / short RPC calls. */ +export const JSONL_RPC_DEFAULT_TIMEOUT_MS = 30_000; +/** + * Pass as `timeoutMs` to wait only for a response, process death, or `close()`. + * Use for long-running blocking RPCs (e.g. LLM-backed compact). + */ +export const JSONL_RPC_NO_TIMEOUT = null; + const STDERR_BUFFER_LIMIT = 8192; const GRACEFUL_SHUTDOWN_TIMEOUT_MS = 2_000; const FORCE_SHUTDOWN_TIMEOUT_MS = 1_000; @@ -28,7 +35,7 @@ interface JsonlRpcResponse { interface PendingRequest { resolve: (value: unknown) => void; reject: (error: Error) => void; - timer: NodeJS.Timeout; + timer: NodeJS.Timeout | null; } export interface JsonlRpcExit { @@ -116,7 +123,7 @@ export class JsonlRpcProcess { startRequest( command: { type: string; [key: string]: unknown }, - timeoutMs = DEFAULT_TIMEOUT_MS, + timeoutMs: number | null = JSONL_RPC_DEFAULT_TIMEOUT_MS, ): { id: string; promise: Promise<unknown> } { if (this.disposed) { return { @@ -127,14 +134,14 @@ export class JsonlRpcProcess { const id = `req_${this.nextRequestId}`; this.nextRequestId += 1; const promise = new Promise<unknown>((resolve, reject) => { - const timer = setTimeout(() => { + const timer = createRequestTimeout(timeoutMs, () => { this.pending.delete(id); reject( new Error( `${this.diagnosticName} request timed out for ${command.type}\n${this.stderrBuffer}`.trim(), ), ); - }, timeoutMs); + }); this.pending.set(id, { resolve, reject, timer }); this.send({ ...command, id }); }); @@ -143,7 +150,7 @@ export class JsonlRpcProcess { request( command: { type: string; [key: string]: unknown }, - timeoutMs = DEFAULT_TIMEOUT_MS, + timeoutMs: number | null = JSONL_RPC_DEFAULT_TIMEOUT_MS, ): Promise<unknown> { return this.startRequest(command, timeoutMs).promise; } @@ -228,7 +235,9 @@ export class JsonlRpcProcess { if (!pending) { return; } - clearTimeout(pending.timer); + if (pending.timer) { + clearTimeout(pending.timer); + } this.pending.delete(response.id); if (!response.success) { pending.reject( @@ -247,9 +256,25 @@ export class JsonlRpcProcess { } this.disposed = true; for (const pending of this.pending.values()) { - clearTimeout(pending.timer); + if (pending.timer) { + clearTimeout(pending.timer); + } pending.reject(error); } this.pending.clear(); } } + +/** + * Schedule a request timeout, or return null when the call should wait + * indefinitely for a response, process exit, or close(). + */ +function createRequestTimeout( + timeoutMs: number | null, + onTimeout: () => void, +): NodeJS.Timeout | null { + if (timeoutMs == null || !Number.isFinite(timeoutMs) || timeoutMs <= 0) { + return null; + } + return setTimeout(onTimeout, timeoutMs); +} diff --git a/packages/server/src/server/agent/providers/pi/cli-runtime.test.ts b/packages/server/src/server/agent/providers/pi/cli-runtime.test.ts index 07cf1341d..f3b2af84a 100644 --- a/packages/server/src/server/agent/providers/pi/cli-runtime.test.ts +++ b/packages/server/src/server/agent/providers/pi/cli-runtime.test.ts @@ -2,7 +2,7 @@ import type { ChildProcessWithoutNullStreams } from "node:child_process"; import { EventEmitter } from "node:events"; import { PassThrough } from "node:stream"; import pino from "pino"; -import { describe, expect, test } from "vitest"; +import { describe, expect, test, vi } from "vitest"; import { PiCliRuntime } from "./cli-runtime.js"; import type { PiRuntimeLaunch } from "./runtime.js"; @@ -47,10 +47,7 @@ function createRuntime( }); } -function replyToCommands( - child: PiChild, - handler: (command: Record<string, unknown>) => unknown, -): void { +function onPiCommand(child: PiChild, handler: (command: Record<string, unknown>) => void): void { let buffer = ""; child.stdin.on("data", (chunk) => { buffer += chunk.toString(); @@ -59,34 +56,68 @@ function replyToCommands( if (newlineIndex === -1) break; const line = buffer.slice(0, newlineIndex); buffer = buffer.slice(newlineIndex + 1); - const command = JSON.parse(line) as Record<string, unknown>; - try { - const result = handler(command); - child.stdout.write( - `${JSON.stringify({ - id: command.id, - type: "response", - command: command.type, - success: true, - data: result, - })}\n`, - ); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - child.stdout.write( - `${JSON.stringify({ - id: command.id, - type: "response", - command: command.type, - success: false, - error: message, - })}\n`, - ); - } + handler(JSON.parse(line) as Record<string, unknown>); } }); } +function replyToCommands( + child: PiChild, + handler: (command: Record<string, unknown>) => unknown, +): void { + onPiCommand(child, (command) => { + try { + const result = handler(command); + child.stdout.write( + `${JSON.stringify({ + id: command.id, + type: "response", + command: command.type, + success: true, + data: result, + })}\n`, + ); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + child.stdout.write( + `${JSON.stringify({ + id: command.id, + type: "response", + command: command.type, + success: false, + error: message, + })}\n`, + ); + } + }); +} + +function capturePendingCommand(child: PiChild, type: string): Promise<Record<string, unknown>> { + return new Promise((resolve) => { + onPiCommand(child, (command) => { + if (command.type === type) { + resolve(command); + } + }); + }); +} + +function writePiResponse( + child: PiChild, + command: Record<string, unknown>, + data: unknown = {}, +): void { + child.stdout.write( + `${JSON.stringify({ + id: command.id, + type: "response", + command: command.type, + success: true, + data, + })}\n`, + ); +} + describe("PiCliRuntime", () => { test("starts pi in rpc mode and resolves command responses", async () => { const child = createPiChild(); @@ -282,6 +313,51 @@ describe("PiCliRuntime", () => { await rejection; }); + test("compact waits beyond the default control-plane timeout for a late response", async () => { + vi.useFakeTimers(); + const child = createPiChild(); + const pendingCompact = capturePendingCommand(child, "compact"); + const session = await createRuntime(child).startSession({ cwd: "/workspace/project" }); + + try { + const compactPromise = session.compact("focus on tests"); + const compactCommand = await pendingCompact; + await vi.advanceTimersByTimeAsync(35_000); + + expect(compactCommand).toMatchObject({ + type: "compact", + customInstructions: "focus on tests", + id: expect.any(String), + }); + + writePiResponse(child, compactCommand, { + summary: "done", + firstKeptEntryId: "entry-1", + tokensBefore: 120_000, + estimatedTokensAfter: 20_000, + }); + + await expect(compactPromise).resolves.toBeUndefined(); + } finally { + vi.useRealTimers(); + await session.close(); + } + }); + + test("compact without a wall-clock timeout rejects when the session closes", async () => { + const child = createPiChild(); + const pendingCompact = capturePendingCommand(child, "compact"); + const session = await createRuntime(child).startSession({ cwd: "/workspace/project" }); + + const compactPromise = session.compact(); + await pendingCompact; + + const rejection = expect(compactPromise).rejects.toThrow("Pi RPC session is closed"); + await session.close(); + + await rejection; + }); + test("disposes the Pi process", async () => { const child = createPiChild(); const session = await createRuntime(child).startSession({ cwd: "/workspace/project" }); diff --git a/packages/server/src/server/agent/providers/pi/cli-runtime.ts b/packages/server/src/server/agent/providers/pi/cli-runtime.ts index aee7b2a78..66cff0daf 100644 --- a/packages/server/src/server/agent/providers/pi/cli-runtime.ts +++ b/packages/server/src/server/agent/providers/pi/cli-runtime.ts @@ -2,7 +2,11 @@ import type { ChildProcessWithoutNullStreams } from "node:child_process"; import type { Logger } from "pino"; import type { ProviderRuntimeSettings } from "../../provider-launch-config.js"; -import { JsonlRpcProcess, type JsonlRpcLaunch } from "../jsonl-rpc-process.js"; +import { + JSONL_RPC_NO_TIMEOUT, + JsonlRpcProcess, + type JsonlRpcLaunch, +} from "../jsonl-rpc-process.js"; import { buildPiLaunch, type PiRuntime, @@ -26,6 +30,14 @@ const DEFAULT_PI_COMMAND: [string, ...string[]] = [ ]; const DEFAULT_COMMANDS_RPC_NAME = "get_commands"; +/** + * Pi RPC timeout policy: + * - Control-plane / accept-and-stream (`prompt`, `get_state`, `abort`, …): default 30s + * - Long-running blocking LLM jobs (`compact`): no wall-clock timeout — complete on + * response, process death, or session close (`JsonlRpcProcess.failAll` / `close`). + */ +const PI_COMPACT_REQUEST_TIMEOUT_MS = JSONL_RPC_NO_TIMEOUT; + export interface PiCliRuntimeOptions { logger: Logger; runtimeSettings?: ProviderRuntimeSettings; @@ -112,10 +124,16 @@ class PiCliRuntimeSession implements PiRuntimeSession { } async compact(customInstructions?: string): Promise<void> { - await this.request({ - type: "compact", - ...(customInstructions ? { customInstructions } : {}), - }); + // Compact is a blocking LLM summarization job; Pi only returns the RPC + // response after the summary is written. A control-plane 30s timeout falsely + // fails long sessions while the real compact continues (issue #1946). + await this.request( + { + type: "compact", + ...(customInstructions ? { customInstructions } : {}), + }, + PI_COMPACT_REQUEST_TIMEOUT_MS, + ); } async setAutoCompaction(enabled: boolean): Promise<void> { @@ -135,7 +153,7 @@ class PiCliRuntimeSession implements PiRuntimeSession { return data.messages ?? []; } - async getAvailableModels(timeoutMs?: number): Promise<PiModel[]> { + async getAvailableModels(timeoutMs?: number | null): Promise<PiModel[]> { const data = (await this.request({ type: "get_available_models" }, timeoutMs)) as { models?: PiModel[]; }; @@ -208,7 +226,7 @@ class PiCliRuntimeSession implements PiRuntimeSession { await this.process.close(new Error("Pi RPC session is closed")); } - request(command: PiRpcCommand, timeoutMs?: number): Promise<unknown> { + request(command: PiRpcCommand, timeoutMs?: number | null): Promise<unknown> { return this.process.request(command, timeoutMs); } diff --git a/packages/server/src/server/agent/providers/pi/runtime.ts b/packages/server/src/server/agent/providers/pi/runtime.ts index 0b4d38afa..6d50693ed 100644 --- a/packages/server/src/server/agent/providers/pi/runtime.ts +++ b/packages/server/src/server/agent/providers/pi/runtime.ts @@ -51,12 +51,15 @@ export interface PiRuntimeSession { abort(): Promise<void>; getState(): Promise<PiSessionState>; getMessages(): Promise<PiAgentMessage[]>; - getAvailableModels(timeoutMs?: number): Promise<PiModel[]>; + getAvailableModels(timeoutMs?: number | null): Promise<PiModel[]>; setModel(provider: string, modelId: string): Promise<PiModel>; setThinkingLevel(level: string): Promise<void>; getSessionStats(): Promise<PiSessionStats>; getCommands(): Promise<PiRpcSlashCommand[]>; - request(command: { type: string; [key: string]: unknown }, timeoutMs?: number): Promise<unknown>; + request( + command: { type: string; [key: string]: unknown }, + timeoutMs?: number | null, + ): Promise<unknown>; sendRawFrame(frame: object & { type: string }): void; respondToExtensionUiRequest( id: string, diff --git a/packages/server/src/server/agent/providers/pi/test-utils/fake-pi.ts b/packages/server/src/server/agent/providers/pi/test-utils/fake-pi.ts index 8332c3f50..8093baeb2 100644 --- a/packages/server/src/server/agent/providers/pi/test-utils/fake-pi.ts +++ b/packages/server/src/server/agent/providers/pi/test-utils/fake-pi.ts @@ -227,7 +227,7 @@ export class FakePiSession implements PiRuntimeSession { return this.messages; } - async getAvailableModels(_timeoutMs?: number): Promise<PiModel[]> { + async getAvailableModels(_timeoutMs?: number | null): Promise<PiModel[]> { return this.models; } From fcaa84f0e4e8a60486cd7051fc55b979c851c6e9 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <mvanhorn@users.noreply.github.com> Date: Sat, 18 Jul 2026 06:44:34 -0700 Subject: [PATCH 28/38] fix(omp): accept thinkingLevel "max" when importing OMP sessions (#2191) Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> --- .../src/server/agent/providers/omp/agent.test.ts | 7 +++++++ .../server/src/server/agent/providers/omp/agent.ts | 6 ++++-- .../server/src/server/agent/providers/omp/rpc-types.ts | 10 +++++++++- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/packages/server/src/server/agent/providers/omp/agent.test.ts b/packages/server/src/server/agent/providers/omp/agent.test.ts index 7ac3608a7..be6666248 100644 --- a/packages/server/src/server/agent/providers/omp/agent.test.ts +++ b/packages/server/src/server/agent/providers/omp/agent.test.ts @@ -67,6 +67,13 @@ describe("OMP agent client and session", () => { }); }); + test("preserves max as the selected thinking option", async () => { + const omp = new OmpHarness(); + await omp.start({ thinkingOptionId: "max" }); + + expect(omp.launchConfiguration().argv).toEqual(expect.arrayContaining(["--thinking", "max"])); + }); + test("streams a prompt through completion", async () => { const omp = new OmpHarness(); await omp.start(); diff --git a/packages/server/src/server/agent/providers/omp/agent.ts b/packages/server/src/server/agent/providers/omp/agent.ts index 23066b631..a16187096 100644 --- a/packages/server/src/server/agent/providers/omp/agent.ts +++ b/packages/server/src/server/agent/providers/omp/agent.ts @@ -136,7 +136,8 @@ const OMP_THINKING_OPTIONS: ReadonlyArray<{ { id: "low", label: "Low", description: "Faster reasoning" }, { id: "medium", label: "Medium", description: "Balanced reasoning", isDefault: true }, { id: "high", label: "High", description: "Deeper reasoning" }, - { id: "xhigh", label: "XHigh", description: "Maximum reasoning" }, + { id: "xhigh", label: "XHigh", description: "Extra-high reasoning" }, + { id: "max", label: "Max", description: "Maximum reasoning" }, ] as const; export interface OmpAgentClientOptions { @@ -269,7 +270,8 @@ function isOmpThinkingLevel(value: string | null | undefined): value is OmpThink value === "low" || value === "medium" || value === "high" || - value === "xhigh" + value === "xhigh" || + value === "max" ); } diff --git a/packages/server/src/server/agent/providers/omp/rpc-types.ts b/packages/server/src/server/agent/providers/omp/rpc-types.ts index df3c7f9b3..bdd5412a4 100644 --- a/packages/server/src/server/agent/providers/omp/rpc-types.ts +++ b/packages/server/src/server/agent/providers/omp/rpc-types.ts @@ -1,6 +1,14 @@ import { z } from "zod"; -export const OmpThinkingLevelSchema = z.enum(["off", "minimal", "low", "medium", "high", "xhigh"]); +export const OmpThinkingLevelSchema = z.enum([ + "off", + "minimal", + "low", + "medium", + "high", + "xhigh", + "max", +]); export const OmpImageContentSchema = z .object({ type: z.literal("image"), data: z.string(), mimeType: z.string() }) From 791753271665c215c4f15569491d88f27d123edc Mon Sep 17 00:00:00 2001 From: Mohamed Boudra <boudra.moha@gmail.com> Date: Sat, 18 Jul 2026 16:30:59 +0200 Subject: [PATCH 29/38] Keep focused agent timelines live and catch up instantly (#2196) * fix(app): keep focused agent timelines live Window focus was conflated with app visibility, so switching OS windows could remove the selected timeline subscription. Separate those signals, grace every visibility-driven removal, and cover retained history until authoritative catch-up completes. * fix(app): keep timeline catch-up failures non-blocking * fix(app): surface timeline catch-up failures * fix(app): preserve file preview refocus * fix(app): preserve optimistic catch-up flow --- docs/timeline-sync.md | 9 +- packages/app/e2e/helpers/timeline-delivery.ts | 107 ++++++++++++++++++ packages/app/e2e/terminal-stuck-size.spec.ts | 4 +- .../app/e2e/viewed-agent-timelines.spec.ts | 40 +++++++ packages/app/src/components/file-pane.tsx | 8 +- .../terminal-pane-focus-claim.test.ts | 6 +- .../components/terminal-pane-focus-claim.ts | 4 +- packages/app/src/components/terminal-pane.tsx | 10 +- packages/app/src/contexts/session-context.tsx | 4 +- .../use-agent-screen-state-machine.test.ts | 41 ++++++- .../hooks/use-agent-screen-state-machine.ts | 13 ++- packages/app/src/hooks/use-app-visible.ts | 52 ++++++--- packages/app/src/i18n/resources/ar.ts | 1 + packages/app/src/i18n/resources/en.ts | 1 + packages/app/src/i18n/resources/es.ts | 1 + packages/app/src/i18n/resources/fr.ts | 1 + packages/app/src/i18n/resources/ja.ts | 1 + packages/app/src/i18n/resources/pt-BR.ts | 1 + packages/app/src/i18n/resources/ru.ts | 1 + packages/app/src/i18n/resources/zh-CN.ts | 1 + packages/app/src/panels/agent-panel.tsx | 44 ++++++- .../screens/workspace/workspace-screen.tsx | 3 +- .../src/timeline/viewed-timeline-sync.test.ts | 80 +++++++++++-- .../app/src/timeline/viewed-timeline-sync.ts | 81 ++++++++++++- packages/app/src/utils/app-visibility.test.ts | 26 +++++ packages/app/src/utils/app-visibility.ts | 53 +++++++-- 26 files changed, 528 insertions(+), 65 deletions(-) create mode 100644 packages/app/e2e/helpers/timeline-delivery.ts create mode 100644 packages/app/src/utils/app-visibility.test.ts diff --git a/docs/timeline-sync.md b/docs/timeline-sync.md index dfd56a46a..e90abf412 100644 --- a/docs/timeline-sync.md +++ b/docs/timeline-sync.md @@ -56,9 +56,12 @@ When a client resumes without a cursor, it fetches the latest tail page. The app chooses one delivery policy from `server_info.features.selectiveAgentTimeline`: - Selective daemons receive the union of agents visible in every pane. Additions subscribe and - catch up immediately. Removals stay subscribed for a short grace period so quick tab and pane - switches do not repeatedly unsubscribe and catch up. Backgrounding, disconnecting, and disposal - clear that grace immediately. + catch up immediately. Every visibility-driven removal, including app backgrounding, stays + subscribed for a short grace period so brief tab, pane, route, and app switches do not repeatedly + unsubscribe and catch up. Losing window keyboard focus does not make a selected pane invisible. + Disconnecting and disposal clear pending grace because the subscription itself no longer exists. + After grace has expired, a retained timeline stays covered when revisited until authoritative + catch-up completes; cached partial output is never presented as current history. - Legacy daemons keep globally streaming agent timelines. Visibility still triggers the existing authoritative catch-up, but the app does not issue selective-subscription RPCs. diff --git a/packages/app/e2e/helpers/timeline-delivery.ts b/packages/app/e2e/helpers/timeline-delivery.ts new file mode 100644 index 000000000..1ac692b56 --- /dev/null +++ b/packages/app/e2e/helpers/timeline-delivery.ts @@ -0,0 +1,107 @@ +import { expect, type Page } from "@playwright/test"; + +type WebSocketMessage = string | Buffer; + +interface SessionMessage { + type?: unknown; + payload?: unknown; +} + +function readSessionMessage(message: WebSocketMessage): SessionMessage | null { + if (typeof message !== "string") return null; + try { + const envelope = JSON.parse(message) as { + type?: unknown; + message?: SessionMessage; + }; + return envelope.type === "session" ? (envelope.message ?? null) : envelope; + } catch { + return null; + } +} + +export function observeTimelineSubscriptions(page: Page) { + let acknowledgedAgentIds: string[] | null = null; + + page.on("websocket", (socket) => { + socket.on("framereceived", ({ payload }) => { + const message = readSessionMessage(payload); + if (message?.type !== "agent.timeline.set_subscription.response") return; + const response = message.payload as { agentIds?: unknown } | undefined; + if (!Array.isArray(response?.agentIds)) return; + acknowledgedAgentIds = response.agentIds.filter( + (agentId): agentId is string => typeof agentId === "string", + ); + }); + }); + + return { + async waitForSubscribedAgents(agentIds: string[]): Promise<void> { + const expected = [...new Set(agentIds)].sort(); + await expect + .poll(() => acknowledgedAgentIds?.slice().sort() ?? null, { timeout: 15_000 }) + .toEqual(expected); + }, + }; +} + +interface AssistantFrameState { + active: boolean; + lastText: string | null; + snapshots: string[]; +} + +export async function observeLastAssistantFrames(page: Page) { + await page.evaluate(() => { + const state: AssistantFrameState = { + active: true, + lastText: null, + snapshots: [], + }; + const sample = () => { + const hasPaintedHistoryOverlay = Array.from( + document.querySelectorAll('[data-testid="agent-history-overlay"]'), + ).some((element) => { + const rect = element.getBoundingClientRect(); + return rect.width > 0 && rect.height > 0; + }); + if (!hasPaintedHistoryOverlay) { + const messages = Array.from( + document.querySelectorAll('[data-testid="assistant-message"]'), + ).filter((element) => { + const rect = element.getBoundingClientRect(); + return rect.width > 0 && rect.height > 0; + }); + const text = messages.at(-1)?.textContent?.trim() ?? ""; + if (text && text !== state.lastText) { + state.lastText = text; + state.snapshots.push(text); + } + } + if (state.active) requestAnimationFrame(sample); + }; + Object.assign(window, { __assistantFrameState: state }); + requestAnimationFrame(sample); + }); + + return { + async stop(): Promise<string[]> { + return page.evaluate( + () => + new Promise<string[]>((resolve) => { + requestAnimationFrame(() => { + const state = ( + window as typeof window & { __assistantFrameState?: AssistantFrameState } + ).__assistantFrameState; + if (!state) { + resolve([]); + return; + } + state.active = false; + resolve([...state.snapshots]); + }); + }), + ); + }, + }; +} diff --git a/packages/app/e2e/terminal-stuck-size.spec.ts b/packages/app/e2e/terminal-stuck-size.spec.ts index 9c498e44a..9e80f16b5 100644 --- a/packages/app/e2e/terminal-stuck-size.spec.ts +++ b/packages/app/e2e/terminal-stuck-size.spec.ts @@ -15,7 +15,7 @@ import { getServerId } from "./helpers/server-id"; * * The repro is the mundane user flow: with a workspace already showing a terminal, open another * one and switch to a different app while it spawns. We stage the blur deterministically by - * stubbing `document.hasFocus()` (which `useAppVisible` consults on window focus/blur events) + * stubbing `document.hasFocus()` (which `useAppActivelyVisible` consults on focus/blur events) * instead of racing real OS focus. * * The assertion reads the PTY's own opinion of its size (`stty size`) with input written @@ -23,7 +23,7 @@ import { getServerId } from "./helpers/server-id"; */ /** - * Simulates the window losing/regaining OS focus, which is what `useAppVisible` reads through + * Simulates the window losing/regaining OS focus, which is what `useAppActivelyVisible` reads through * `document.hasFocus()` plus the window focus/blur events. * * This has to be stubbed: headless Chromium never actually blurs. Opening a second page and diff --git a/packages/app/e2e/viewed-agent-timelines.spec.ts b/packages/app/e2e/viewed-agent-timelines.spec.ts index 7e4c7d539..67f5b3289 100644 --- a/packages/app/e2e/viewed-agent-timelines.spec.ts +++ b/packages/app/e2e/viewed-agent-timelines.spec.ts @@ -3,6 +3,10 @@ import { buildHostAgentDetailRoute } from "@/utils/host-routes"; import { test } from "./fixtures"; import { seedWorkspace, type SeedDaemonClient } from "./helpers/seed-client"; import { getServerId } from "./helpers/server-id"; +import { + observeLastAssistantFrames, + observeTimelineSubscriptions, +} from "./helpers/timeline-delivery"; import { waitForWorkspaceTabsVisible } from "./helpers/workspace-tabs"; import { installDaemonWebSocketGate } from "./helpers/daemon-websocket-gate"; import { @@ -71,6 +75,42 @@ async function commitMessage(scenario: ViewedTimelineScenario, agentId: string, } test.describe("Viewed agent timelines", () => { + test("a focused turn streams live and resumes its hidden remainder atomically", async ({ + page, + }) => { + test.setTimeout(90_000); + const subscriptions = observeTimelineSubscriptions(page); + const scenario = await seedViewedTimelineScenario(); + try { + await openAgent(page, scenario, scenario.firstAgentId); + await subscriptions.waitForSubscribedAgents([scenario.firstAgentId]); + + await scenario.client.sendAgentMessage( + scenario.firstAgentId, + "Stream while focused, then finish while hidden.", + ); + await expect(page.getByRole("button", { name: /stop|cancel/i }).first()).toBeVisible(); + await expect( + page.getByTestId("assistant-message").filter({ hasText: "Cycle 1" }).first(), + ).toBeVisible(); + await expect(page.getByText("(end of synthetic stream)", { exact: true })).toHaveCount(0); + + await selectAgent(page, "Second viewed chat"); + await subscriptions.waitForSubscribedAgents([scenario.secondAgentId]); + const finish = await scenario.client.waitForFinish(scenario.firstAgentId, 30_000); + expect(finish.status).toBe("idle"); + + const assistantFrames = await observeLastAssistantFrames(page); + await selectAgent(page, "First viewed chat"); + await expect(page.getByText("(end of synthetic stream)", { exact: true })).toBeVisible(); + const snapshots = await assistantFrames.stop(); + + expect(snapshots[0]).toContain("(end of synthetic stream)"); + } finally { + await scenario.cleanup(); + } + }); + test("a hidden retained chat catches up when shown", async ({ page }) => { const scenario = await seedViewedTimelineScenario(); try { diff --git a/packages/app/src/components/file-pane.tsx b/packages/app/src/components/file-pane.tsx index e8cde4141..e78ac348b 100644 --- a/packages/app/src/components/file-pane.tsx +++ b/packages/app/src/components/file-pane.tsx @@ -27,7 +27,7 @@ import { explorerFileFromReadResult } from "@/file-explorer/read-result"; import { resolveFilePreviewReadTarget } from "@/file-explorer/preview-target"; import type { WorkspaceFileLocation } from "@/workspace/file-open"; import { useRetainedPanelActive } from "@/components/retained-panel"; -import { useAppVisible } from "@/hooks/use-app-visible"; +import { useAppActivelyVisible } from "@/hooks/use-app-visible"; import { isFileQueryEnabled } from "@/components/file-pane-enabled"; interface CodeLineProps { @@ -386,10 +386,10 @@ export function FilePane({ ); // Re-read the file when this pane becomes visible again (#445). `isActive` - // covers tab switches, `isAppVisible` the whole-app background/foreground; the - // gate itself lives in isFileQueryEnabled. + // covers tab switches; active app visibility covers backgrounding and returning + // from another window after an external edit. The gate lives in isFileQueryEnabled. const isActive = useRetainedPanelActive(); - const isAppVisible = useAppVisible(); + const isAppVisible = useAppActivelyVisible(); const query = useQuery({ queryKey: ["workspaceFile", serverId, readTarget?.cwd ?? null, readTarget?.path ?? null], diff --git a/packages/app/src/components/terminal-pane-focus-claim.test.ts b/packages/app/src/components/terminal-pane-focus-claim.test.ts index 67e540f52..24edd4a57 100644 --- a/packages/app/src/components/terminal-pane-focus-claim.test.ts +++ b/packages/app/src/components/terminal-pane-focus-claim.test.ts @@ -18,14 +18,14 @@ describe("terminal pane focus claim", () => { it("waits for both the client and renderer before requesting a claim", () => { const withoutClient = canRequestFocusClaim({ isWorkspaceFocused: true, - isAppVisible: true, + isAppActivelyVisible: true, isClientReady: false, isConnected: true, isRendererReady: true, }); const withoutRenderer = canRequestFocusClaim({ isWorkspaceFocused: true, - isAppVisible: true, + isAppActivelyVisible: true, isClientReady: true, isConnected: true, isRendererReady: false, @@ -37,7 +37,7 @@ describe("terminal pane focus claim", () => { it("does not deliver a requested claim after the host disconnects", () => { const disconnected = canRequestFocusClaim({ isWorkspaceFocused: true, - isAppVisible: true, + isAppActivelyVisible: true, isClientReady: true, isConnected: false, isRendererReady: true, diff --git a/packages/app/src/components/terminal-pane-focus-claim.ts b/packages/app/src/components/terminal-pane-focus-claim.ts index 03f9ec2fa..21118ad19 100644 --- a/packages/app/src/components/terminal-pane-focus-claim.ts +++ b/packages/app/src/components/terminal-pane-focus-claim.ts @@ -10,7 +10,7 @@ export interface FocusClaimStep { interface FocusClaimReadiness { isWorkspaceFocused: boolean; - isAppVisible: boolean; + isAppActivelyVisible: boolean; isClientReady: boolean; isConnected: boolean; isRendererReady: boolean; @@ -24,7 +24,7 @@ export const EMPTY_FOCUS_CLAIM_STATE: FocusClaimState = { export function canRequestFocusClaim(input: FocusClaimReadiness): boolean { return ( input.isWorkspaceFocused && - input.isAppVisible && + input.isAppActivelyVisible && input.isClientReady && input.isConnected && input.isRendererReady diff --git a/packages/app/src/components/terminal-pane.tsx b/packages/app/src/components/terminal-pane.tsx index 3eb607857..f5ba2864f 100644 --- a/packages/app/src/components/terminal-pane.tsx +++ b/packages/app/src/components/terminal-pane.tsx @@ -13,7 +13,7 @@ import type { TerminalInputModeState } from "@getpaseo/protocol/terminal-input-m import { useTranslation } from "react-i18next"; import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime"; import { useKeyboardShiftStyle } from "@/hooks/use-keyboard-shift-style"; -import { useAppVisible } from "@/hooks/use-app-visible"; +import { useAppActivelyVisible } from "@/hooks/use-app-visible"; import { useStableEvent } from "@/hooks/use-stable-event"; import { hasPendingTerminalModifiers, @@ -177,7 +177,7 @@ export function TerminalPane({ onOpenWorkspaceFile, }: TerminalPaneProps) { const { t } = useTranslation(); - const isAppVisible = useAppVisible(); + const isAppActivelyVisible = useAppActivelyVisible(); const { theme } = useUnistyles(); const { settings } = useAppSettings(); const xtermTheme = useMemo(() => toXtermTheme(theme.colors.terminal), [theme]); @@ -283,7 +283,7 @@ export function TerminalPane({ useEffect(() => { const canRequest = canRequestFocusClaim({ isWorkspaceFocused, - isAppVisible, + isAppActivelyVisible, isClientReady: client !== null, isConnected, isRendererReady: rendererReadyStreamKey === terminalStreamKey, @@ -299,7 +299,7 @@ export function TerminalPane({ } }, [ client, - isAppVisible, + isAppActivelyVisible, isConnected, isPaneFocused, isWorkspaceFocused, @@ -654,7 +654,7 @@ export function TerminalPane({ let sent = false; const canSend = canRequestFocusClaim({ isWorkspaceFocused, - isAppVisible, + isAppActivelyVisible, isClientReady: client !== null, isConnected, isRendererReady: true, diff --git a/packages/app/src/contexts/session-context.tsx b/packages/app/src/contexts/session-context.tsx index abde80934..ed2f62fb0 100644 --- a/packages/app/src/contexts/session-context.tsx +++ b/packages/app/src/contexts/session-context.tsx @@ -42,7 +42,7 @@ import type { AudioPlaybackSource } from "@/voice/audio-engine-types"; import { useSessionStore, type MessageEntry, type SessionState } from "@/stores/session-store"; import { useWorkspaceSetupStore } from "@/stores/workspace-setup-store"; import { sendOsNotification } from "@/utils/os-notifications"; -import { getIsAppActivelyVisible } from "@/utils/app-visibility"; +import { getIsAppActivelyVisible, getIsAppVisible } from "@/utils/app-visibility"; import { getInitKey, getInitDeferred, @@ -773,7 +773,7 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider }); viewedTimelineSyncRef.current = sync; setViewedTimelineSync(serverId, sync); - sync.setActive(getIsAppActivelyVisible(appStateRef.current)); + sync.setActive(getIsAppVisible(appStateRef.current)); return () => { if (viewedTimelineSyncRef.current === sync) { diff --git a/packages/app/src/hooks/use-agent-screen-state-machine.test.ts b/packages/app/src/hooks/use-agent-screen-state-machine.test.ts index f2da3318b..5422bf63f 100644 --- a/packages/app/src/hooks/use-agent-screen-state-machine.test.ts +++ b/packages/app/src/hooks/use-agent-screen-state-machine.test.ts @@ -63,6 +63,7 @@ function createBaseInput(): AgentScreenMachineInput { isArchivingCurrentAgent: false, isHistorySyncing: false, needsAuthoritativeSync: false, + visibilityCatchUpStatus: "ready", hasHydratedHistoryBefore: false, }; } @@ -189,6 +190,43 @@ describe("deriveAgentScreenViewState", () => { expect(sync.ui).toBe("silent"); }); + it("covers already-hydrated history while a newly visible agent catches up", () => { + const memory = createBaseMemory({ + hasRenderedReady: true, + lastReadyAgent: createAgent("agent-1"), + }); + const input: AgentScreenMachineInput = { + ...createBaseInput(), + agent: createAgent("agent-1"), + hasHydratedHistoryBefore: true, + visibilityCatchUpStatus: "pending", + }; + + const result = deriveAgentScreenViewState({ input, memory }); + const ready = expectReadyState(result.state); + const sync = expectCatchingUpSync(ready); + + expect(sync.ui).toBe("overlay"); + }); + + it("keeps hydrated history readable after a visibility catch-up error", () => { + const memory = createBaseMemory({ + hasRenderedReady: true, + lastReadyAgent: createAgent("agent-1"), + }); + const input: AgentScreenMachineInput = { + ...createBaseInput(), + agent: createAgent("agent-1"), + hasHydratedHistoryBefore: true, + visibilityCatchUpStatus: "error", + }; + + const result = deriveAgentScreenViewState({ input, memory }); + const ready = expectReadyState(result.state); + + expectSyncErrorSync(ready); + }); + it("keeps sync errors non-blocking once the screen was ready", () => { const memory = createBaseMemory({ hasRenderedReady: true, @@ -328,7 +366,7 @@ describe("deriveAgentScreenViewState", () => { expect(result.memory.lastReadyAgent).toBeNull(); }); - it("still allows optimistic create flow to render before authoritative history arrives", () => { + it("keeps optimistic create non-blocking while timeline and authoritative history catch up", () => { const memory = createBaseMemory(); const input: AgentScreenMachineInput = { ...createBaseInput(), @@ -336,6 +374,7 @@ describe("deriveAgentScreenViewState", () => { continuity: { kind: "optimistic-create", agent: createAgent("agent-1") }, needsAuthoritativeSync: true, isHistorySyncing: true, + visibilityCatchUpStatus: "pending", hasHydratedHistoryBefore: false, }; diff --git a/packages/app/src/hooks/use-agent-screen-state-machine.ts b/packages/app/src/hooks/use-agent-screen-state-machine.ts index 9882ff5cf..941e87deb 100644 --- a/packages/app/src/hooks/use-agent-screen-state-machine.ts +++ b/packages/app/src/hooks/use-agent-screen-state-machine.ts @@ -46,6 +46,7 @@ export interface AgentScreenMachineInput { isArchivingCurrentAgent: boolean; isHistorySyncing: boolean; needsAuthoritativeSync: boolean; + visibilityCatchUpStatus: "ready" | "pending" | "error"; continuity: AgentScreenContinuity; hasHydratedHistoryBefore: boolean; } @@ -147,10 +148,12 @@ function resolveAgentScreenSource(args: { function resolveCatchingUpUi(args: { hasOptimisticCreateContinuity: boolean; + isVisibilityCatchUpPending: boolean; hasHydratedHistoryBefore: boolean; hadInitialSyncFailure: boolean; }): "overlay" | "silent" { if (args.hasOptimisticCreateContinuity) return "silent"; + if (args.isVisibilityCatchUpPending) return "overlay"; if (args.hasHydratedHistoryBefore) return "silent"; if (args.hadInitialSyncFailure) return "silent"; return "overlay"; @@ -167,11 +170,19 @@ function resolveAgentScreenSync(args: { if (input.missingAgentState.kind === "error") { return { status: "sync_error" }; } - if (input.needsAuthoritativeSync || input.isHistorySyncing) { + if (input.visibilityCatchUpStatus === "error") { + return { status: "sync_error" }; + } + if ( + input.visibilityCatchUpStatus === "pending" || + input.needsAuthoritativeSync || + input.isHistorySyncing + ) { return { status: "catching_up", ui: resolveCatchingUpUi({ hasOptimisticCreateContinuity: hasOptimisticCreateContinuity(input), + isVisibilityCatchUpPending: input.visibilityCatchUpStatus === "pending", hasHydratedHistoryBefore: input.hasHydratedHistoryBefore, hadInitialSyncFailure, }), diff --git a/packages/app/src/hooks/use-app-visible.ts b/packages/app/src/hooks/use-app-visible.ts index 89a046b3c..2777e18af 100644 --- a/packages/app/src/hooks/use-app-visible.ts +++ b/packages/app/src/hooks/use-app-visible.ts @@ -1,19 +1,24 @@ import { useSyncExternalStore } from "react"; import { AppState } from "react-native"; -import { getIsAppActivelyVisible } from "@/utils/app-visibility"; +import { getIsAppActivelyVisible, getIsAppVisible } from "@/utils/app-visibility"; import { isWeb } from "@/constants/platform"; -let current = getIsAppActivelyVisible(); -const listeners = new Set<() => void>(); +let visible = getIsAppVisible(); +let activelyVisible = getIsAppActivelyVisible(); +const visibilityListeners = new Set<() => void>(); +const activeVisibilityListeners = new Set<() => void>(); function notify(): void { - const next = getIsAppActivelyVisible(); - if (next === current) { - return; + const nextVisible = getIsAppVisible(); + if (nextVisible !== visible) { + visible = nextVisible; + for (const listener of visibilityListeners) listener(); } - current = next; - for (const listener of listeners) { - listener(); + + const nextActivelyVisible = getIsAppActivelyVisible(); + if (nextActivelyVisible !== activelyVisible) { + activelyVisible = nextActivelyVisible; + for (const listener of activeVisibilityListeners) listener(); } } @@ -29,15 +34,32 @@ if (isWeb && typeof document !== "undefined") { window.addEventListener("blur", notify); } -function subscribe(listener: () => void): () => void { - listeners.add(listener); - return () => listeners.delete(listener); +function subscribeToVisibility(listener: () => void): () => void { + visibilityListeners.add(listener); + return () => visibilityListeners.delete(listener); } -function getSnapshot(): boolean { - return current; +function subscribeToActiveVisibility(listener: () => void): () => void { + activeVisibilityListeners.add(listener); + return () => activeVisibilityListeners.delete(listener); +} + +function getVisibilitySnapshot(): boolean { + return visible; +} + +function getActiveVisibilitySnapshot(): boolean { + return activelyVisible; } export function useAppVisible(): boolean { - return useSyncExternalStore(subscribe, getSnapshot, getSnapshot); + return useSyncExternalStore(subscribeToVisibility, getVisibilitySnapshot, getVisibilitySnapshot); +} + +export function useAppActivelyVisible(): boolean { + return useSyncExternalStore( + subscribeToActiveVisibility, + getActiveVisibilitySnapshot, + getActiveVisibilitySnapshot, + ); } diff --git a/packages/app/src/i18n/resources/ar.ts b/packages/app/src/i18n/resources/ar.ts index 3062b5e9c..9ca62a417 100644 --- a/packages/app/src/i18n/resources/ar.ts +++ b/packages/app/src/i18n/resources/ar.ts @@ -190,6 +190,7 @@ export const ar: TranslationResources = { notFound: "لم يتم العثور على Agent", failedToLoad: "فشل تحميل الوكيل", reconnecting: "جارٍ إعادة الاتصال...", + timelineSyncFailed: "تعذر تحديث سجل الوكيل. جارٍ إعادة المحاولة…", archivingTitle: "وكيل الارشيف...", archivingSubtitle: "الرجاء الانتظار بينما نقوم بأرشفة هذا الوكيل.", }, diff --git a/packages/app/src/i18n/resources/en.ts b/packages/app/src/i18n/resources/en.ts index d04201091..147b24beb 100644 --- a/packages/app/src/i18n/resources/en.ts +++ b/packages/app/src/i18n/resources/en.ts @@ -188,6 +188,7 @@ export const en = { notFound: "Agent not found", failedToLoad: "Failed to load agent", reconnecting: "Reconnecting...", + timelineSyncFailed: "Couldn't refresh agent history. Retrying…", archivingTitle: "Archiving agent...", archivingSubtitle: "Please wait while we archive this agent.", }, diff --git a/packages/app/src/i18n/resources/es.ts b/packages/app/src/i18n/resources/es.ts index 58221329b..72a7cef63 100644 --- a/packages/app/src/i18n/resources/es.ts +++ b/packages/app/src/i18n/resources/es.ts @@ -190,6 +190,7 @@ export const es: TranslationResources = { notFound: "Agentno encontrado", failedToLoad: "No se pudo cargar el agente", reconnecting: "Reconectando...", + timelineSyncFailed: "No se pudo actualizar el historial del agente. Reintentando…", archivingTitle: "Agente de archivo...", archivingSubtitle: "Espere mientras archivamos este agente.", }, diff --git a/packages/app/src/i18n/resources/fr.ts b/packages/app/src/i18n/resources/fr.ts index 9083dc98c..ae70cba91 100644 --- a/packages/app/src/i18n/resources/fr.ts +++ b/packages/app/src/i18n/resources/fr.ts @@ -191,6 +191,7 @@ export const fr: TranslationResources = { notFound: "Agentintrouvable", failedToLoad: "Échec du chargement de l'agent", reconnecting: "Reconnexion...", + timelineSyncFailed: "Impossible d’actualiser l’historique de l’agent. Nouvelle tentative…", archivingTitle: "Agent d'archivage...", archivingSubtitle: "Veuillez patienter pendant que nous archivons cet agent.", }, diff --git a/packages/app/src/i18n/resources/ja.ts b/packages/app/src/i18n/resources/ja.ts index ff5c83e9b..61f878696 100644 --- a/packages/app/src/i18n/resources/ja.ts +++ b/packages/app/src/i18n/resources/ja.ts @@ -190,6 +190,7 @@ export const ja: TranslationResources = { notFound: "エージェントが見つかりません", failedToLoad: "エージェントの読み込みに失敗しました", reconnecting: "再接続中...", + timelineSyncFailed: "エージェントの履歴を更新できませんでした。再試行しています…", archivingTitle: "エージェントをアーカイブ中...", archivingSubtitle: "このエージェントをアーカイブするまでお待ちください。", }, diff --git a/packages/app/src/i18n/resources/pt-BR.ts b/packages/app/src/i18n/resources/pt-BR.ts index 3d5f1f20b..45d07d737 100644 --- a/packages/app/src/i18n/resources/pt-BR.ts +++ b/packages/app/src/i18n/resources/pt-BR.ts @@ -190,6 +190,7 @@ export const ptBR: TranslationResources = { notFound: "Agente não encontrado", failedToLoad: "Falha ao carregar agente", reconnecting: "Reconectando...", + timelineSyncFailed: "Não foi possível atualizar o histórico do agente. Tentando novamente…", archivingTitle: "Arquivando agente...", archivingSubtitle: "Aguarde enquanto arquivamos este agente.", }, diff --git a/packages/app/src/i18n/resources/ru.ts b/packages/app/src/i18n/resources/ru.ts index 57f147682..f0f2bb207 100644 --- a/packages/app/src/i18n/resources/ru.ts +++ b/packages/app/src/i18n/resources/ru.ts @@ -190,6 +190,7 @@ export const ru: TranslationResources = { notFound: "Agent не найден", failedToLoad: "Не удалось загрузить агент", reconnecting: "Повторное подключение...", + timelineSyncFailed: "Не удалось обновить историю агента. Повторная попытка…", archivingTitle: "Архивный агент...", archivingSubtitle: "Пожалуйста, подождите, пока мы архивируем этого агента.", }, diff --git a/packages/app/src/i18n/resources/zh-CN.ts b/packages/app/src/i18n/resources/zh-CN.ts index 2feb4684a..24650d1b9 100644 --- a/packages/app/src/i18n/resources/zh-CN.ts +++ b/packages/app/src/i18n/resources/zh-CN.ts @@ -190,6 +190,7 @@ export const zhCN: TranslationResources = { notFound: "未找到 Agent", failedToLoad: "加载 Agent 失败", reconnecting: "正在重连...", + timelineSyncFailed: "无法刷新代理历史记录。正在重试…", archivingTitle: "正在归档 Agent...", archivingSubtitle: "请稍候,我们正在归档这个 Agent。", }, diff --git a/packages/app/src/panels/agent-panel.tsx b/packages/app/src/panels/agent-panel.tsx index d4102d30c..7613dd035 100644 --- a/packages/app/src/panels/agent-panel.tsx +++ b/packages/app/src/panels/agent-panel.tsx @@ -1,7 +1,15 @@ import type { DaemonClient } from "@getpaseo/client/internal/daemon-client"; import type { TFunction } from "i18next"; import { SquarePen } from "lucide-react-native"; -import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"; +import React, { + memo, + useCallback, + useEffect, + useMemo, + useRef, + useState, + useSyncExternalStore, +} from "react"; import { useTranslation } from "react-i18next"; import { ActivityIndicator, Text, View } from "react-native"; import ReanimatedAnimated from "react-native-reanimated"; @@ -14,6 +22,7 @@ import { AgentStreamView, type AgentStreamViewHandle } from "@/agent-stream/view import { ArchivedAgentCallout } from "@/components/archived-agent-callout"; import { FileDropZone } from "@/components/file-drop/file-drop-zone"; import { useRetainedPanelActive } from "@/components/retained-panel"; +import { SidebarCallout } from "@/components/sidebar-callout"; import { Composer } from "@/composer"; import { AgentModeControl } from "@/composer/agent-controls/mode-control"; import { RewindComposerRestoreProvider } from "@/components/rewind/composer-restore"; @@ -751,6 +760,26 @@ function ChatAgentContent({ const agentHistorySyncGeneration = useSessionStore((state) => agentId ? (state.sessions[serverId]?.agentHistorySyncGeneration?.get(agentId) ?? -1) : -1, ); + const viewedTimelineSync = useSessionStore( + (state) => state.sessions[serverId]?.viewedTimelineSync ?? null, + ); + const subscribeToVisibilityCatchUp = useCallback( + (listener: () => void) => viewedTimelineSync?.subscribe(listener) ?? (() => {}), + [viewedTimelineSync], + ); + const readTimelineStatus = useCallback( + () => + !agentId || !viewedTimelineSync + ? ("ready" as const) + : viewedTimelineSync.getAgentTimelineStatus(agentId), + [agentId, viewedTimelineSync], + ); + const timelineStatus = useSyncExternalStore( + subscribeToVisibilityCatchUp, + readTimelineStatus, + readTimelineStatus, + ); + const visibilityCatchUpStatus = isPaneVisible ? timelineStatus : "ready"; const hasActiveCreateHandoff = useCreateFlowStore((state) => findActiveCreateHandoff({ pendingByDraftId: state.pendingByDraftId, serverId, agentId }), ); @@ -862,6 +891,7 @@ function ChatAgentContent({ isArchivingCurrentAgent, isHistorySyncing, needsAuthoritativeSync, + visibilityCatchUpStatus, continuity, hasHydratedHistoryBefore, }, @@ -1004,6 +1034,7 @@ function ChatAgentContent({ viewState.tag === "ready" && viewState.sync.status === "catching_up" && viewState.sync.ui === "overlay"; + const showHistorySyncError = viewState.tag === "ready" && viewState.sync.status === "sync_error"; return ( <ChatAgentReadyContent @@ -1023,6 +1054,7 @@ function ChatAgentContent({ handleComposerHeightChange={handleComposerHeightChange} handleMessageSent={handleMessageSent} showHistorySyncOverlay={showHistorySyncOverlay} + showHistorySyncError={showHistorySyncError} cwd={agentCwd} onAttentionInputFocus={attentionController.clearOnInputFocus} onAttentionPromptSend={attentionController.clearOnPromptSend} @@ -1048,6 +1080,7 @@ const ChatAgentReadyContent = memo(function ChatAgentReadyContent({ handleComposerHeightChange, handleMessageSent, showHistorySyncOverlay, + showHistorySyncError, cwd, onAttentionInputFocus, onAttentionPromptSend, @@ -1069,6 +1102,7 @@ const ChatAgentReadyContent = memo(function ChatAgentReadyContent({ handleComposerHeightChange: (height: number) => void; handleMessageSent: () => void; showHistorySyncOverlay: boolean; + showHistorySyncError: boolean; cwd: string; onAttentionInputFocus: () => void; onAttentionPromptSend: () => void; @@ -1140,6 +1174,14 @@ const ChatAgentReadyContent = memo(function ChatAgentReadyContent({ <FileDropZone style={styles.container} disabled={isArchivingCurrentAgent}> {contentContainer} + {showHistorySyncError ? ( + <SidebarCallout + title={t("agentPanel.states.timelineSyncFailed")} + variant="error" + testID="agent-timeline-sync-error" + /> + ) : null} + {composerSection} {showHistorySyncOverlay ? ( diff --git a/packages/app/src/screens/workspace/workspace-screen.tsx b/packages/app/src/screens/workspace/workspace-screen.tsx index 3de632990..16cf2b0cc 100644 --- a/packages/app/src/screens/workspace/workspace-screen.tsx +++ b/packages/app/src/screens/workspace/workspace-screen.tsx @@ -2,6 +2,7 @@ import { memo, useCallback, useEffect, + useLayoutEffect, useMemo, useRef, useState, @@ -2027,7 +2028,7 @@ function WorkspaceScreenContent({ }), [isFocusModeEnabled, isMobile, isRouteFocused, uiTabs, workspaceLayout], ); - useEffect(() => { + useLayoutEffect(() => { if (!persistenceKey || !viewedTimelineSync) { return; } diff --git a/packages/app/src/timeline/viewed-timeline-sync.test.ts b/packages/app/src/timeline/viewed-timeline-sync.test.ts index 2e7fa6dd1..42f15507f 100644 --- a/packages/app/src/timeline/viewed-timeline-sync.test.ts +++ b/packages/app/src/timeline/viewed-timeline-sync.test.ts @@ -1,4 +1,4 @@ -import { expect, test } from "vitest"; +import { expect, test, vi } from "vitest"; import type { ProjectedTimelineForwardFetchPlan } from "./timeline-sync-plan"; import { createViewedTimelineSync, @@ -191,6 +191,7 @@ test("unchanged visible-set publication does not cancel paged catch-up", async ( const world = new TimelineWorld(); world.sync.setConnected(true); world.sync.replaceVisibleAgentIds("workspace", ["agent-a"]); + expect(world.sync.getAgentTimelineStatus("agent-a")).toBe("pending"); const membership = await world.nextMembership(); membership.succeed(); const firstPage = await world.nextFetch("agent-a"); @@ -198,8 +199,13 @@ test("unchanged visible-set publication does not cancel paged catch-up", async ( world.sync.replaceVisibleAgentIds("workspace", ["agent-a", "agent-a"]); firstPage.respond({ hasNewer: true, seq: 5 }); const secondPage = await world.nextFetch("agent-a"); + expect(world.sync.getAgentTimelineStatus("agent-a")).toBe("pending"); secondPage.respond({ hasNewer: false }); + await vi.waitFor(() => { + expect(world.sync.getAgentTimelineStatus("agent-a")).toBe("ready"); + }); + expect(secondPage.request).toEqual({ direction: "after", cursor: { epoch: "epoch-agent-a", seq: 5 }, @@ -280,6 +286,7 @@ test("disconnect cancels paging and reconnect restores membership before fresh c const stalePage = await world.nextFetch("agent-a"); world.sync.setConnected(false); + expect(world.sync.getAgentTimelineStatus("agent-a")).toBe("pending"); stalePage.respond({ hasNewer: true, seq: 8 }); world.sync.setConnected(true); const restoredMembership = await world.nextMembership(); @@ -287,6 +294,8 @@ test("disconnect cancels paging and reconnect restores membership before fresh c const restoredPage = await world.nextFetch("agent-a"); restoredPage.respond({ hasNewer: false }); + await vi.waitFor(() => expect(world.sync.getAgentTimelineStatus("agent-a")).toBe("ready")); + expect(restoredMembership.agentIds).toEqual(["agent-a"]); world.expectNoPendingFetch(); }); @@ -328,10 +337,13 @@ test("a failed catch-up reports once and retries through the explicit retry poli const failed = await world.nextFetch("agent-a"); failed.fail("timeline unavailable"); const [error, retryCatchUp] = await Promise.all([world.nextError(), world.nextRetry()]); + expect(world.sync.getAgentTimelineStatus("agent-a")).toBe("error"); retryCatchUp(); const retry = await world.nextFetch("agent-a"); + expect(world.sync.getAgentTimelineStatus("agent-a")).toBe("error"); retry.respond({ hasNewer: false }); + await vi.waitFor(() => expect(world.sync.getAgentTimelineStatus("agent-a")).toBe("ready")); expect({ error, retryDirection: retry.request.direction }).toEqual({ error: "timeline unavailable", @@ -396,12 +408,15 @@ test("membership failure autonomously retries without another visibility declara const failed = await world.nextMembership(); failed.fail("subscription unavailable"); const [error, retryMembership] = await Promise.all([world.nextError(), world.nextRetry()]); + expect(world.sync.getAgentTimelineStatus("agent-a")).toBe("error"); retryMembership(); const retry = await world.nextMembership(); retry.succeed(); const catchUp = await world.nextFetch("agent-a"); + expect(world.sync.getAgentTimelineStatus("agent-a")).toBe("error"); catchUp.respond({ hasNewer: false }); + await vi.waitFor(() => expect(world.sync.getAgentTimelineStatus("agent-a")).toBe("ready")); expect({ error, failed: failed.agentIds, retry: retry.agentIds }).toEqual({ error: "subscription unavailable", @@ -410,7 +425,7 @@ test("membership failure autonomously retries without another visibility declara }); }); -test("background sends an empty set and foreground restores visible membership", async () => { +test("background waits for grace before unsubscribing and catches up on return", async () => { const world = new TimelineWorld(); world.sync.setConnected(true); world.sync.replaceVisibleAgentIds("workspace", ["agent-a"]); @@ -420,6 +435,8 @@ test("background sends an empty set and foreground restores visible membership", initialCatchUp.respond({ hasNewer: false }); world.sync.setActive(false); + world.expectNoPendingMembership(); + world.runUnsubscribeGrace(); const background = await world.nextMembership(); background.succeed(); world.sync.setActive(true); @@ -434,6 +451,24 @@ test("background sends an empty set and foreground restores visible membership", }); }); +test("foregrounding within grace preserves the live membership", async () => { + const world = new TimelineWorld(); + world.sync.setConnected(true); + world.sync.replaceVisibleAgentIds("workspace", ["agent-a"]); + const membership = await world.nextMembership(); + membership.succeed(); + const catchUp = await world.nextFetch("agent-a"); + catchUp.respond({ hasNewer: false }); + + world.sync.setActive(false); + world.expectNoPendingMembership(); + world.sync.setActive(true); + + world.expectNoPendingUnsubscribe(); + world.expectNoPendingMembership(); + world.expectNoPendingFetch(); +}); + test("stale membership retry cannot overwrite a newer effective set", async () => { const world = new TimelineWorld(); world.sync.setConnected(true); @@ -483,10 +518,16 @@ test("quickly returning to an agent cancels its pending unsubscribe without anot const initialCatchUp = await world.nextFetch("agent-a"); initialCatchUp.respond({ hasNewer: false }); + await vi.waitFor(() => { + expect(world.sync.getAgentTimelineStatus("agent-a")).toBe("ready"); + }); + world.sync.replaceVisibleAgentIds("workspace", []); world.expectNoPendingMembership(); world.sync.replaceVisibleAgentIds("workspace", ["agent-a"]); + expect(world.sync.getAgentTimelineStatus("agent-a")).toBe("ready"); + world.expectNoPendingUnsubscribe(); world.expectNoPendingMembership(); world.expectNoPendingFetch(); @@ -500,6 +541,12 @@ test("unsubscribe grace expiry removes the agent exactly once", async () => { membership.succeed(); const catchUp = await world.nextFetch("agent-a"); catchUp.respond({ hasNewer: false }); + await vi.waitFor(() => expect(world.sync.getAgentTimelineStatus("agent-a")).toBe("ready")); + + const readinessChanges: string[] = []; + world.sync.subscribe(() => { + readinessChanges.push(world.sync.getAgentTimelineStatus("agent-a")); + }); world.sync.replaceVisibleAgentIds("workspace", []); world.runUnsubscribeGrace(); @@ -507,6 +554,7 @@ test("unsubscribe grace expiry removes the agent exactly once", async () => { unsubscribe.succeed(); expect(unsubscribe.agentIds).toEqual([]); + expect(readinessChanges.at(-1)).toBe("pending"); world.expectNoPendingUnsubscribe(); world.expectNoPendingMembership(); }); @@ -533,7 +581,7 @@ test("a new visible agent subscribes immediately while the previous agent linger expect(settledMembership.agentIds).toEqual(["agent-b"]); }); -test("backgrounding clears pending unsubscribe grace and membership immediately", async () => { +test("backgrounding preserves an existing unsubscribe grace period", async () => { const world = new TimelineWorld(); world.sync.setConnected(true); world.sync.replaceVisibleAgentIds("workspace", ["agent-a"]); @@ -544,25 +592,37 @@ test("backgrounding clears pending unsubscribe grace and membership immediately" world.sync.replaceVisibleAgentIds("workspace", []); world.sync.setActive(false); + world.expectNoPendingMembership(); + world.runUnsubscribeGrace(); const unsubscribe = await world.nextMembership(); unsubscribe.succeed(); expect(unsubscribe.agentIds).toEqual([]); - world.expectNoPendingUnsubscribe(); + world.expectNoPendingMembership(); }); test("disconnecting cancels pending unsubscribe grace without publishing on the closed socket", async () => { const world = new TimelineWorld(); world.sync.setConnected(true); - world.sync.replaceVisibleAgentIds("workspace", ["agent-a"]); + world.sync.replaceVisibleAgentIds("workspace", ["agent-a", "agent-b"]); const membership = await world.nextMembership(); membership.succeed(); - const catchUp = await world.nextFetch("agent-a"); - catchUp.respond({ hasNewer: false }); + const [agentA, agentB] = await Promise.all([ + world.nextFetch("agent-a"), + world.nextFetch("agent-b"), + ]); + agentA.respond({ hasNewer: false }); + agentB.respond({ hasNewer: false }); + await vi.waitFor(() => expect(world.sync.getAgentTimelineStatus("agent-a")).toBe("ready")); - world.sync.replaceVisibleAgentIds("workspace", []); + world.sync.replaceVisibleAgentIds("workspace", ["agent-a"]); + const agentAStatuses: string[] = []; + world.sync.subscribe(() => { + agentAStatuses.push(world.sync.getAgentTimelineStatus("agent-a")); + }); world.sync.setConnected(false); + expect(agentAStatuses).toEqual(["pending"]); world.expectNoPendingUnsubscribe(); world.expectNoPendingMembership(); }); @@ -612,13 +672,17 @@ test("switching from legacy to selective delivery publishes membership and catch world.sync.setConnected(true); const legacyCatchUp = await world.nextFetch("agent-a"); legacyCatchUp.respond({ hasNewer: false }); + await vi.waitFor(() => expect(world.sync.getAgentTimelineStatus("agent-a")).toBe("ready")); world.sync.setDeliveryMode("selective"); + expect(world.sync.getAgentTimelineStatus("agent-a")).toBe("pending"); const membership = await world.nextMembership(); membership.succeed(); const catchUp = await world.nextFetch("agent-a"); catchUp.respond({ hasNewer: false }); + await vi.waitFor(() => expect(world.sync.getAgentTimelineStatus("agent-a")).toBe("ready")); + expect(membership.agentIds).toEqual(["agent-a"]); world.expectNoPendingMembership(); world.expectNoPendingFetch(); diff --git a/packages/app/src/timeline/viewed-timeline-sync.ts b/packages/app/src/timeline/viewed-timeline-sync.ts index 4aa8a26dd..d928d032c 100644 --- a/packages/app/src/timeline/viewed-timeline-sync.ts +++ b/packages/app/src/timeline/viewed-timeline-sync.ts @@ -25,9 +25,12 @@ interface ViewedTimelineSyncPorts { } export type TimelineDeliveryMode = "legacy" | "selective"; +export type ViewedTimelineStatus = "ready" | "pending" | "error"; export interface ViewedTimelineUiBridge { replaceVisibleAgentIds(sourceId: string, agentIds: string[]): void; + subscribe(listener: () => void): () => void; + getAgentTimelineStatus(agentId: string): ViewedTimelineStatus; } export interface ViewedTimelineSync extends ViewedTimelineUiBridge { @@ -88,6 +91,9 @@ export function createViewedTimelineSync(ports: ViewedTimelineSyncPorts): Viewed const catchUpGenerations = new Map<string, number>(); const pendingGaps = new Map<string, ProjectedTimelineForwardFetchPlan>(); const lingeringRemovals = new Map<string, () => void>(); + const visibilityCatchUpPending = new Set<string>(); + const visibilityCatchUpErrors = new Set<string>(); + const listeners = new Set<() => void>(); let active = true; let connected = false; let deliveryMode = ports.initialDeliveryMode; @@ -107,6 +113,26 @@ export function createViewedTimelineSync(ports: ViewedTimelineSyncPorts): Viewed const isAcknowledged = (agentId: string) => acknowledged.includes(agentId); const isDesired = (agentId: string) => desired.includes(agentId); + const notifyListeners = () => { + for (const listener of listeners) listener(); + }; + + const setVisibilityCatchUpReady = (agentId: string) => { + const wasPending = visibilityCatchUpPending.delete(agentId); + const hadError = visibilityCatchUpErrors.delete(agentId); + if (wasPending || hadError) notifyListeners(); + }; + + const setVisibilityCatchUpError = (agentIds: string[]) => { + let changed = false; + for (const agentId of agentIds) { + if (!visibilityCatchUpPending.delete(agentId)) continue; + visibilityCatchUpErrors.add(agentId); + changed = true; + } + if (changed) notifyListeners(); + }; + const cancelCatchUp = (agentId: string) => { catchUpGenerations.set(agentId, (catchUpGenerations.get(agentId) ?? 0) + 1); catchUps.get(agentId)?.cancelRetry?.(); @@ -148,6 +174,7 @@ export function createViewedTimelineSync(ports: ViewedTimelineSyncPorts): Viewed throw new Error(`Timeline page for ${agentId} hasNewer without an end cursor`); } catchUps.set(agentId, { generation, status: "complete" }); + setVisibilityCatchUpReady(agentId); } catch (error) { if (catchUps.get(agentId)?.generation === generation) { const cancelRetry = ports.schedule(() => { @@ -156,6 +183,7 @@ export function createViewedTimelineSync(ports: ViewedTimelineSyncPorts): Viewed startCatchUp(agentId); }, RETRY_DELAY_MS); catchUps.set(agentId, { generation, status: "error", cancelRetry }); + setVisibilityCatchUpError([agentId]); ports.reportError(error); } } @@ -208,6 +236,7 @@ export function createViewedTimelineSync(ports: ViewedTimelineSyncPorts): Viewed await ports.setSubscription(requested); } catch (error) { membershipNeedsRetry = true; + setVisibilityCatchUpError(requested); cancelMembershipRetry?.(); cancelMembershipRetry = ports.schedule(() => { cancelMembershipRetry = null; @@ -268,20 +297,45 @@ export function createViewedTimelineSync(ports: ViewedTimelineSyncPorts): Viewed } }; - const commitDesiredMembership = (nextDesired: string[]) => { + const commitDesiredMembership = ( + nextDesired: string[], + options: { resetCatchUpStatus?: boolean } = {}, + ) => { + let statusChanged = false; + if (options.resetCatchUpStatus) { + for (const agentId of nextDesired) { + if (!visibilityCatchUpPending.has(agentId)) { + visibilityCatchUpPending.add(agentId); + statusChanged = true; + } + if (visibilityCatchUpErrors.delete(agentId)) statusChanged = true; + } + } if (sameAgentIds(nextDesired, desired)) { + if (statusChanged) notifyListeners(); if (deliveryMode === "selective" && membershipNeedsRetry) void reconcileMembership(); retryFailedCatchUps(); return; } for (const agentId of desired) { - if (!nextDesired.includes(agentId)) cancelCatchUp(agentId); + if (!nextDesired.includes(agentId)) { + cancelCatchUp(agentId); + visibilityCatchUpPending.delete(agentId); + visibilityCatchUpErrors.delete(agentId); + } + } + for (const agentId of nextDesired) { + if (!desired.includes(agentId)) { + visibilityCatchUpPending.add(agentId); + visibilityCatchUpErrors.delete(agentId); + } } cancelMembershipRetry?.(); cancelMembershipRetry = null; desired = nextDesired; membershipGeneration += 1; + notifyListeners(); if (deliveryMode === "legacy") { acknowledged = connected ? desired : []; if (connected) startAcknowledgedCatchUps(); @@ -302,7 +356,7 @@ export function createViewedTimelineSync(ports: ViewedTimelineSyncPorts): Viewed lingeringRemovals.delete(agentId); } - if (allowGrace && connected && active && deliveryMode === "selective") { + if (allowGrace && connected && deliveryMode === "selective") { for (const agentId of desired) { if (visible.includes(agentId) || lingeringRemovals.has(agentId)) continue; const cancel = ports.schedule(() => { @@ -319,6 +373,15 @@ export function createViewedTimelineSync(ports: ViewedTimelineSyncPorts): Viewed }; return { + subscribe(listener) { + listeners.add(listener); + return () => listeners.delete(listener); + }, + getAgentTimelineStatus(agentId) { + if (visibilityCatchUpErrors.has(agentId)) return "error"; + if (!isDesired(agentId) || visibilityCatchUpPending.has(agentId)) return "pending"; + return "ready"; + }, replaceVisibleAgentIds(sourceId, agentIds) { const normalized = normalizeAgentIds(agentIds); if (normalized.length === 0) sources.delete(sourceId); @@ -328,14 +391,14 @@ export function createViewedTimelineSync(ports: ViewedTimelineSyncPorts): Viewed setActive(nextActive) { if (active === nextActive) return; active = nextActive; - publishVisibleMembership(false); + publishVisibleMembership(true); }, setConnected(nextConnected) { if (connected === nextConnected) return; connected = nextConnected; if (!connected) { clearLingeringRemovals(); - commitDesiredMembership(visibleAgentIds()); + commitDesiredMembership(visibleAgentIds(), { resetCatchUpStatus: true }); cancelMembershipRetry?.(); cancelMembershipRetry = null; acknowledged = []; @@ -361,7 +424,11 @@ export function createViewedTimelineSync(ports: ViewedTimelineSyncPorts): Viewed membershipGeneration += 1; for (const agentId of desired) cancelCatchUp(agentId); desired = visibleAgentIds(); + visibilityCatchUpPending.clear(); + visibilityCatchUpErrors.clear(); + for (const agentId of desired) visibilityCatchUpPending.add(agentId); acknowledged = deliveryMode === "legacy" && connected ? desired : []; + notifyListeners(); if (deliveryMode === "selective" && connected) void reconcileMembership(); else if (connected) startAcknowledgedCatchUps(); }, @@ -382,6 +449,10 @@ export function createViewedTimelineSync(ports: ViewedTimelineSyncPorts): Viewed for (const agentId of desired) cancelCatchUp(agentId); desired = []; acknowledged = []; + visibilityCatchUpPending.clear(); + visibilityCatchUpErrors.clear(); + notifyListeners(); + listeners.clear(); }, }; } diff --git a/packages/app/src/utils/app-visibility.test.ts b/packages/app/src/utils/app-visibility.test.ts new file mode 100644 index 000000000..eec95463d --- /dev/null +++ b/packages/app/src/utils/app-visibility.test.ts @@ -0,0 +1,26 @@ +import { expect, test } from "vitest"; +import { isAppActivelyVisible, isAppVisible } from "./app-visibility"; + +test("a visible desktop app remains visible when another window has focus", () => { + const input = { + appState: "active", + native: false, + documentVisible: true, + windowFocused: false, + }; + + expect(isAppVisible(input)).toBe(true); + expect(isAppActivelyVisible(input)).toBe(false); +}); + +test("a hidden desktop page is neither visible nor actively visible", () => { + const input = { + appState: "active", + native: false, + documentVisible: false, + windowFocused: true, + }; + + expect(isAppVisible(input)).toBe(false); + expect(isAppActivelyVisible(input)).toBe(false); +}); diff --git a/packages/app/src/utils/app-visibility.ts b/packages/app/src/utils/app-visibility.ts index 896d70eb1..d078f1022 100644 --- a/packages/app/src/utils/app-visibility.ts +++ b/packages/app/src/utils/app-visibility.ts @@ -1,20 +1,49 @@ import { AppState } from "react-native"; import { isNative } from "@/constants/platform"; -export function getIsAppActivelyVisible(appState: string = AppState.currentState): boolean { - if (appState !== "active") { - return false; - } +interface AppVisibilityInput { + appState: string; + native: boolean; + documentVisible: boolean; +} - if (isNative) { - return true; - } +interface ActiveAppVisibilityInput extends AppVisibilityInput { + windowFocused: boolean; +} - const documentVisible = typeof document === "undefined" || document.visibilityState === "visible"; - const windowFocused = +export function isAppVisible(input: AppVisibilityInput): boolean { + return input.appState === "active" && (input.native || input.documentVisible); +} + +export function isAppActivelyVisible(input: ActiveAppVisibilityInput): boolean { + return isAppVisible(input) && (input.native || input.windowFocused); +} + +function getDocumentVisible(): boolean { + return typeof document === "undefined" || document.visibilityState === "visible"; +} + +function getWindowFocused(): boolean { + return ( typeof document === "undefined" || typeof document.hasFocus !== "function" || - document.hasFocus(); - - return documentVisible && windowFocused; + document.hasFocus() + ); +} + +export function getIsAppVisible(appState: string = AppState.currentState): boolean { + return isAppVisible({ + appState, + native: isNative, + documentVisible: getDocumentVisible(), + }); +} + +export function getIsAppActivelyVisible(appState: string = AppState.currentState): boolean { + return isAppActivelyVisible({ + appState, + native: isNative, + documentVisible: getDocumentVisible(), + windowFocused: getWindowFocused(), + }); } From 45bbb973a3a3d4b386b67f219dd2944b19c369c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20K=C4=99drzy=C5=84ski?= <kedrzu@gmail.com> Date: Sat, 18 Jul 2026 16:54:41 +0200 Subject: [PATCH 30/38] Switch models from the Command Center (#2147) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Switch models from the Command Center Add a model switcher to the Command-K Command Center. Typing surfaces a flat, filterable list of "Model › Provider › Name" breadcrumb rows with provider icons: - Running agent: its own provider's models (a live agent can't change provider); selecting calls setAgentModel. - New draft tab: every available provider's models in one flat list; selecting sets provider + model on the draft via a focused-draft controller published to a global store (the draft form state is local to the composer subtree and otherwise unreachable from the global Command Center). Models only appear once the user starts typing, so the default palette view is unchanged. Reuses useProvidersSnapshot and the existing setAgentModel RPC — no protocol changes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Update packages/app/src/components/command-center.tsx Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * Update packages/app/src/hooks/use-command-center.ts Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * refactor(app): make Command Center extensible Let focused features register stable actions while the palette owns search, selection, and a single virtualized result projection. * fix(app): save model preference after agent switch Persist the shared model choice only after the daemon confirms the live agent switched successfully. --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com> --- packages/app/src/app/_layout.tsx | 6 +- .../app/src/command-center/command-center.tsx | 792 ++++++++++++++++++ .../app/src/command-center/contributions.ts | 53 ++ .../model-contributions.test.tsx | 104 +++ .../src/command-center/model-contributions.ts | 51 ++ .../app/src/command-center/provider-icon.tsx | 20 + packages/app/src/command-center/provider.tsx | 56 ++ .../app/src/command-center/registry.test.ts | 73 ++ packages/app/src/command-center/registry.ts | 100 +++ .../app/src/command-center/results.test.ts | 105 +++ packages/app/src/command-center/results.ts | 181 ++++ .../app/src/components/command-center.tsx | 726 ---------------- .../app/src/composer/agent-controls/index.tsx | 56 +- .../app/src/composer/draft/workspace-tab.tsx | 30 + packages/app/src/composer/index.tsx | 7 +- packages/app/src/hooks/use-command-center.ts | 482 ----------- packages/app/src/i18n/resources.test.ts | 4 + packages/app/src/i18n/resources/ar.ts | 2 + packages/app/src/i18n/resources/en.ts | 2 + packages/app/src/i18n/resources/es.ts | 2 + packages/app/src/i18n/resources/fr.ts | 3 + packages/app/src/i18n/resources/ja.ts | 2 + packages/app/src/i18n/resources/pt-BR.ts | 2 + packages/app/src/i18n/resources/ru.ts | 2 + packages/app/src/i18n/resources/zh-CN.ts | 2 + 25 files changed, 1636 insertions(+), 1227 deletions(-) create mode 100644 packages/app/src/command-center/command-center.tsx create mode 100644 packages/app/src/command-center/contributions.ts create mode 100644 packages/app/src/command-center/model-contributions.test.tsx create mode 100644 packages/app/src/command-center/model-contributions.ts create mode 100644 packages/app/src/command-center/provider-icon.tsx create mode 100644 packages/app/src/command-center/provider.tsx create mode 100644 packages/app/src/command-center/registry.test.ts create mode 100644 packages/app/src/command-center/registry.ts create mode 100644 packages/app/src/command-center/results.test.ts create mode 100644 packages/app/src/command-center/results.ts delete mode 100644 packages/app/src/components/command-center.tsx delete mode 100644 packages/app/src/hooks/use-command-center.ts diff --git a/packages/app/src/app/_layout.tsx b/packages/app/src/app/_layout.tsx index 16aa4b0f6..2f7fcd2fd 100644 --- a/packages/app/src/app/_layout.tsx +++ b/packages/app/src/app/_layout.tsx @@ -21,7 +21,8 @@ import { GestureDetector, GestureHandlerRootView } from "react-native-gesture-ha import { KeyboardProvider } from "react-native-keyboard-controller"; import { SafeAreaProvider } from "react-native-safe-area-context"; import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles"; -import { CommandCenter } from "@/components/command-center"; +import { CommandCenter, CommandCenterRootActions } from "@/command-center/command-center"; +import { CommandCenterProvider } from "@/command-center/provider"; import { AddProjectFlowHost } from "@/components/add-project-flow-host"; import { WorktreeSetupCalloutSource } from "@/components/worktree-setup-callout-source"; import { DownloadToast } from "@/components/download-toast"; @@ -553,6 +554,7 @@ function AppContainer({ children, chromeEnabled: chromeEnabledOverride }: AppCon <RosettaCalloutSource /> <UpdateCalloutSource /> <WorktreeSetupCalloutSource /> + <CommandCenterRootActions /> <CommandCenter /> <AddProjectFlowHost /> <HostChooserModal /> @@ -570,7 +572,7 @@ function AppContainer({ children, chromeEnabled: chromeEnabledOverride }: AppCon surface ); - return content; + return <CommandCenterProvider>{content}</CommandCenterProvider>; } function SidebarChrome({ diff --git a/packages/app/src/command-center/command-center.tsx b/packages/app/src/command-center/command-center.tsx new file mode 100644 index 000000000..584c80bfc --- /dev/null +++ b/packages/app/src/command-center/command-center.tsx @@ -0,0 +1,792 @@ +import { + FlatList, + Modal, + Pressable, + Text, + TextInput, + View, + type ListRenderItemInfo, + type PressableStateCallbackType, +} from "react-native"; +import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { router, type Href } from "expo-router"; +import { useTranslation } from "react-i18next"; +import { Check, ChevronRight, Folder, Home, Plus, Settings } from "lucide-react-native"; +import { StyleSheet, withUnistyles } from "react-native-unistyles"; +import { + BottomSheetBackdrop, + BottomSheetFlatList, + BottomSheetTextInput, + type BottomSheetFlatListMethods, +} from "@gorhom/bottom-sheet"; +import { AgentStatusDot } from "@/components/agent-status-dot"; +import { Shortcut } from "@/components/ui/shortcut"; +import { + IsolatedBottomSheetModal, + useIsolatedBottomSheetVisibility, +} from "@/components/ui/isolated-bottom-sheet-modal"; +import { getIsElectronRuntime, useIsCompactFormFactor } from "@/constants/layout"; +import { isNative, isWeb } from "@/constants/platform"; +import { useAggregatedAgents, type AggregatedAgent } from "@/hooks/use-aggregated-agents"; +import { useKeyboardShortcutOverrides } from "@/hooks/use-keyboard-shortcut-overrides"; +import { useOpenAddProject } from "@/hooks/use-open-add-project"; +import { useProjects } from "@/hooks/use-projects"; +import { useHosts } from "@/runtime/host-runtime"; +import { useKeyboardShortcutsStore } from "@/stores/keyboard-shortcuts-store"; +import { navigateToWorkspace } from "@/stores/navigation-active-workspace-store"; +import { getBindingIdForAction, getDefaultKeysForAction } from "@/keyboard/keyboard-shortcuts"; +import { chordStringToShortcutKeys } from "@/keyboard/shortcut-string"; +import { keyboardActionDispatcher } from "@/keyboard/keyboard-action-dispatcher"; +import { + clearCommandCenterFocusRestoreElement, + takeCommandCenterFocusRestoreElement, +} from "@/utils/command-center-focus-restore"; +import { focusWithRetries } from "@/utils/web-focus"; +import { buildOpenProjectRoute, buildSettingsRoute } from "@/utils/host-routes"; +import { navigateToAgent } from "@/utils/navigate-to-agent"; +import { formatTimeAgo } from "@/utils/time"; +import { shortenPath } from "@/utils/shorten-path"; +import { getShortcutOs } from "@/utils/shortcut-platform"; +import type { ShortcutKey } from "@/utils/format-shortcut"; +import type { CommandCenterContribution, CommandCenterIconProps } from "./contributions"; +import { useCommandCenterActions, useCommandCenterContributions } from "./provider"; +import { + buildContributionSections, + moveActiveResultId, + preserveActiveResultId, + projectCommandCenterRows, + type CommandCenterAgentResult, + type CommandCenterListRow, + type CommandCenterResult, + type CommandCenterResultSection, + type CommandCenterWorkspaceResult, +} from "./results"; + +const ThemedBottomSheetTextInput = withUnistyles(BottomSheetTextInput, (theme) => ({ + placeholderTextColor: theme.colors.foregroundMuted, +})); +const ThemedTextInput = withUnistyles(TextInput, (theme) => ({ + placeholderTextColor: theme.colors.foregroundMuted, +})); +const ThemedFolder = withUnistyles(Folder, (theme) => ({ color: theme.colors.foregroundMuted })); +const ThemedCheck = withUnistyles(Check, (theme) => ({ color: theme.colors.foreground })); +const ThemedChevronRight = withUnistyles(ChevronRight, (theme) => ({ + color: theme.colors.foregroundMuted, +})); +const ThemedPlus = withUnistyles(Plus, (theme) => ({ color: theme.colors.foregroundMuted })); +const ThemedSettings = withUnistyles(Settings, (theme) => ({ + color: theme.colors.foregroundMuted, +})); +const ThemedHome = withUnistyles(Home, (theme) => ({ color: theme.colors.foregroundMuted })); +const COMMAND_CENTER_SNAP_POINTS = ["60%", "90%"]; +const KEYBOARD_SHOULD_PERSIST_TAPS = "always" as const; + +function PlusIcon({ size }: CommandCenterIconProps) { + return <ThemedPlus size={size} strokeWidth={2.4} />; +} + +function SettingsIcon({ size }: CommandCenterIconProps) { + return <ThemedSettings size={size} strokeWidth={2.2} />; +} + +function HomeIcon({ size }: CommandCenterIconProps) { + return <ThemedHome size={size} strokeWidth={2.2} />; +} + +function resolveActionShortcutKeys( + actionId: string | undefined, + overrides: Record<string, string>, +): ShortcutKey[][] | undefined { + if (!actionId) return undefined; + const platform = { + isMac: getShortcutOs() === "mac", + isDesktop: getIsElectronRuntime(), + }; + const bindingId = getBindingIdForAction(actionId, platform); + if (!bindingId) return undefined; + const override = overrides[bindingId]; + if (override) return chordStringToShortcutKeys(override); + const defaultKeys = getDefaultKeysForAction(actionId, platform); + return defaultKeys ? [defaultKeys] : undefined; +} + +export function CommandCenterRootActions() { + const { t } = useTranslation(); + const { overrides } = useKeyboardShortcutOverrides(); + const openAddProject = useOpenAddProject(); + const settingsRoute = useMemo<Href>(() => buildSettingsRoute(), []); + const homeRoute = useMemo<Href>(() => buildOpenProjectRoute(), []); + const actions = useMemo<CommandCenterContribution[]>( + () => [ + { + id: "new-agent", + group: "actions", + groupRank: 0, + rank: 0, + keywords: ["open", "project", "folder", "workspace", "repo"], + visibility: "always", + run: () => { + clearCommandCenterFocusRestoreElement(); + openAddProject(); + }, + presentation: { + kind: "action", + title: t("shell.commandCenter.addProject"), + sectionTitle: t("shell.commandCenter.actions"), + icon: PlusIcon, + shortcutKeys: resolveActionShortcutKeys("new-agent", overrides), + }, + }, + { + id: "home", + group: "actions", + groupRank: 0, + rank: 1, + keywords: ["home", "start", "import", "session", "pair", "device", "providers"], + visibility: "always", + run: () => { + clearCommandCenterFocusRestoreElement(); + router.push(homeRoute); + }, + presentation: { + kind: "action", + title: t("shell.commandCenter.home"), + sectionTitle: t("shell.commandCenter.actions"), + icon: HomeIcon, + }, + }, + { + id: "settings", + group: "actions", + groupRank: 0, + rank: 2, + keywords: ["settings", "preferences", "config", "configuration"], + visibility: "always", + run: () => { + clearCommandCenterFocusRestoreElement(); + router.push(settingsRoute); + }, + presentation: { + kind: "action", + title: t("sidebar.actions.settings"), + sectionTitle: t("shell.commandCenter.actions"), + icon: SettingsIcon, + }, + }, + ], + [homeRoute, openAddProject, overrides, settingsRoute, t], + ); + + useCommandCenterActions({ sourceId: "root", enabled: true, actions }); + return null; +} + +function sortAgents(left: AggregatedAgent, right: AggregatedAgent): number { + const leftNeedsInput = (left.pendingPermissionCount ?? 0) > 0 ? 1 : 0; + const rightNeedsInput = (right.pendingPermissionCount ?? 0) > 0 ? 1 : 0; + if (leftNeedsInput !== rightNeedsInput) return rightNeedsInput - leftNeedsInput; + const leftAttention = left.requiresAttention ? 1 : 0; + const rightAttention = right.requiresAttention ? 1 : 0; + if (leftAttention !== rightAttention) return rightAttention - leftAttention; + const leftRunning = left.status === "running" ? 1 : 0; + const rightRunning = right.status === "running" ? 1 : 0; + if (leftRunning !== rightRunning) return rightRunning - leftRunning; + return right.lastActivityAt.getTime() - left.lastActivityAt.getTime(); +} + +function matchesQuery(searchText: string, query: string): boolean { + const normalized = query.trim().toLowerCase(); + return !normalized || searchText.includes(normalized); +} + +function useBuiltInSections(open: boolean, query: string): CommandCenterResultSection[] { + const { t } = useTranslation(); + const { agents } = useAggregatedAgents(); + const { projects } = useProjects({ enabled: open }); + const showAgentHost = useHosts().length > 1; + + return useMemo(() => { + if (!open) return []; + const allWorkspaces: CommandCenterWorkspaceResult[] = []; + for (const project of projects) { + for (const host of project.hosts) { + for (const workspace of host.workspaces) { + if (workspace.archivingAt) continue; + const title = workspace.title ?? workspace.name; + const subtitle = workspace.currentBranch + ? `${host.serverName} · ${workspace.currentBranch}` + : host.serverName; + const searchText = `${title} ${subtitle}`.toLowerCase(); + allWorkspaces.push({ + kind: "workspace", + id: `workspace:${host.serverId}:${workspace.id}`, + title, + subtitle, + searchText, + run: () => { + clearCommandCenterFocusRestoreElement(); + navigateToWorkspace({ serverId: host.serverId, workspaceId: workspace.id }); + }, + }); + } + } + } + allWorkspaces.sort((left, right) => { + const titleDelta = left.title.localeCompare(right.title, undefined, { + numeric: true, + sensitivity: "base", + }); + return titleDelta || left.subtitle.localeCompare(right.subtitle); + }); + const workspaceTitleByKey = new Map( + allWorkspaces.map((workspace) => [workspace.id.slice("workspace:".length), workspace.title]), + ); + const workspaces = allWorkspaces.filter((workspace) => + matchesQuery(workspace.searchText, query), + ); + const agentResults = agents + .map<CommandCenterAgentResult>((agent) => { + const title = agent.title || t("shell.commandCenter.newAgent"); + const workspaceTitle = agent.workspaceId + ? workspaceTitleByKey.get(`${agent.serverId}:${agent.workspaceId}`) + : undefined; + const location = workspaceTitle ?? shortenPath(agent.cwd); + const subtitle = [ + showAgentHost ? agent.serverLabel : null, + location, + formatTimeAgo(agent.lastActivityAt), + ] + .filter((part): part is string => Boolean(part)) + .join(" · "); + return { + kind: "agent", + id: `agent:${agent.serverId}:${agent.id}`, + agent, + title, + subtitle, + searchText: `${title} ${subtitle} ${agent.cwd}`.toLowerCase(), + run: () => { + clearCommandCenterFocusRestoreElement(); + navigateToAgent({ serverId: agent.serverId, agentId: agent.id }); + }, + }; + }) + .filter((agent) => matchesQuery(agent.searchText, query)) + .sort((left, right) => sortAgents(left.agent, right.agent)); + return [ + { + id: "workspaces", + rank: 2, + title: t("shell.commandCenter.workspaces"), + results: workspaces, + }, + { id: "agents", rank: 3, title: t("shell.commandCenter.agents"), results: agentResults }, + ]; + }, [agents, open, projects, query, showAgentHost, t]); +} + +interface CommandCenterState { + open: boolean; + query: string; + setQuery(query: string): void; + activeId: string | null; + rows: readonly CommandCenterListRow[]; + results: readonly CommandCenterResult[]; + rowIndexByResultId: ReadonlyMap<string, number>; + offsets: readonly number[]; + inputRef: React.RefObject<TextInput | null>; + close(): void; + select(result: CommandCenterResult): void; + key(key: string): boolean; +} + +function useCommandCenterState(): CommandCenterState { + const open = useKeyboardShortcutsStore((state) => state.commandCenterOpen); + const setOpen = useKeyboardShortcutsStore((state) => state.setCommandCenterOpen); + const snapshot = useCommandCenterContributions(); + const inputRef = useRef<TextInput>(null); + const previousOpenRef = useRef(open); + const [query, setQuery] = useState(""); + const [activeId, setActiveId] = useState<string | null>(null); + const builtInSections = useBuiltInSections(open, query); + const contributionSections = useMemo( + () => buildContributionSections(snapshot.contributions, query), + [query, snapshot.contributions], + ); + const projection = useMemo( + () => projectCommandCenterRows([...contributionSections, ...builtInSections]), + [builtInSections, contributionSections], + ); + const resolvedActiveId = preserveActiveResultId(activeId, projection.selectableResults); + + const close = useCallback(() => setOpen(false), [setOpen]); + const select = useCallback( + (result: CommandCenterResult) => { + setOpen(false); + void result.run(); + }, + [setOpen], + ); + const key = useCallback( + (pressed: string): boolean => { + if (!open) return false; + const results = projection.selectableResults; + if (pressed === "Escape") { + close(); + return true; + } + if (pressed === "Enter") { + const selected = results.find((result) => result.id === resolvedActiveId); + if (!selected) return false; + select(selected); + return true; + } + if (pressed !== "ArrowDown" && pressed !== "ArrowUp") return false; + if (results.length === 0) return false; + const direction = pressed === "ArrowDown" ? "next" : "previous"; + setActiveId(moveActiveResultId(resolvedActiveId, results, direction)); + return true; + }, + [close, open, projection.selectableResults, resolvedActiveId, select], + ); + + useEffect(() => { + const wasOpen = previousOpenRef.current; + previousOpenRef.current = open; + if (open) { + const timer = setTimeout(() => inputRef.current?.focus(), 0); + return () => clearTimeout(timer); + } + setQuery(""); + setActiveId(null); + if (!wasOpen) return; + const element = takeCommandCenterFocusRestoreElement(); + if (!element) return; + const cancel = focusWithRetries({ + focus: () => element.focus(), + isFocused: () => typeof document !== "undefined" && document.activeElement === element, + onTimeout: () => + keyboardActionDispatcher.dispatch({ id: "message-input.focus", scope: "message-input" }), + }); + return cancel; + }, [open]); + + useEffect(() => { + if (!open || !isWeb) return; + const listener = (event: KeyboardEvent) => { + if (key(event.key)) event.preventDefault(); + }; + window.addEventListener("keydown", listener, true); + return () => window.removeEventListener("keydown", listener, true); + }, [key, open]); + + return { + open, + query, + setQuery, + activeId: resolvedActiveId, + rows: projection.rows, + results: projection.selectableResults, + rowIndexByResultId: projection.rowIndexByResultId, + offsets: projection.offsets, + inputRef, + close, + select, + key, + }; +} + +interface ResultRowProps { + result: CommandCenterResult; + active: boolean; + onSelect(result: CommandCenterResult): void; +} + +const ResultRow = memo(function ResultRow({ result, active, onSelect }: ResultRowProps) { + const press = useCallback(() => onSelect(result), [onSelect, result]); + const style = useCallback( + ({ hovered, pressed }: PressableStateCallbackType & { hovered?: boolean }) => [ + styles.row, + (result.kind === "agent" || + result.kind === "workspace" || + (result.kind === "contribution" && + result.contribution.presentation.kind === "action" && + Boolean(result.contribution.presentation.subtitle))) && + styles.tallRow, + (Boolean(hovered) || pressed || active) && styles.activeRow, + ], + [active, result], + ); + return ( + <Pressable style={style} onPress={press}> + <ResultContent result={result} /> + </Pressable> + ); +}); + +function ResultContent({ result }: { result: CommandCenterResult }) { + if (result.kind === "agent") { + const agent = result.agent; + return ( + <View style={styles.rowContent} testID={`command-center-agent-${agent.serverId}:${agent.id}`}> + <View style={styles.rowMain}> + <View style={styles.iconSlot}> + <AgentStatusDot + status={agent.status} + requiresAttention={agent.requiresAttention} + showInactive + /> + </View> + <View style={styles.textContent}> + <Text style={styles.title} numberOfLines={1}> + {result.title} + </Text> + <Text style={styles.subtitle} numberOfLines={1} testID="command-center-agent-subtitle"> + {result.subtitle} + </Text> + </View> + </View> + </View> + ); + } + if (result.kind === "workspace") { + const key = result.id.slice("workspace:".length); + return ( + <View style={styles.rowContent} testID={`command-center-workspace-${key}`}> + <View style={styles.rowMain}> + <View style={styles.iconSlot}> + <ThemedFolder size={16} strokeWidth={2.2} /> + </View> + <View style={styles.textContent}> + <Text style={styles.title} numberOfLines={1}> + {result.title} + </Text> + <Text style={styles.subtitle} numberOfLines={1}> + {result.subtitle} + </Text> + </View> + </View> + </View> + ); + } + const presentation = result.contribution.presentation; + const Icon = presentation.icon; + if (presentation.kind === "action") { + return ( + <View style={styles.rowContent}> + <View style={styles.rowMain}> + {Icon ? ( + <View style={styles.iconSlot}> + <Icon size={16} /> + </View> + ) : null} + <View style={styles.textContent}> + <Text style={styles.title} numberOfLines={1}> + {presentation.title} + </Text> + {presentation.subtitle ? ( + <Text style={styles.subtitle}>{presentation.subtitle}</Text> + ) : null} + </View> + </View> + {presentation.shortcutKeys ? ( + <Shortcut chord={presentation.shortcutKeys} style={styles.rowShortcut} /> + ) : null} + </View> + ); + } + return ( + <View style={styles.rowContent} testID={presentation.testId}> + <View style={styles.rowMain}> + {Icon ? ( + <View style={styles.iconSlot}> + <Icon size={16} /> + </View> + ) : null} + <View style={styles.breadcrumb}> + {presentation.path.map((part, index) => ( + <View + key={presentation.path.slice(0, index + 1).join("\u0000")} + style={styles.breadcrumbPart} + > + {index > 0 ? <ThemedChevronRight size={13} strokeWidth={2} /> : null} + <Text + style={ + index === presentation.path.length - 1 ? styles.title : styles.breadcrumbGroup + } + numberOfLines={1} + > + {part} + </Text> + </View> + ))} + </View> + </View> + {presentation.selected ? ( + <View style={styles.iconSlot}> + <ThemedCheck size={16} strokeWidth={2.2} /> + </View> + ) : null} + </View> + ); +} + +function SectionRow({ row }: { row: Extract<CommandCenterListRow, { kind: "section" }> }) { + let sizeStyle = styles.dividerSection; + if (row.title && row.divider) sizeStyle = styles.dividedSection; + if (row.title && !row.divider) sizeStyle = styles.titledSection; + return ( + <View style={sizeStyle}> + {row.divider ? <View style={styles.sectionDivider} /> : null} + {row.title ? <Text style={styles.sectionLabel}>{row.title}</Text> : null} + </View> + ); +} + +export function CommandCenter() { + const { t } = useTranslation(); + const state = useCommandCenterState(); + const isCompact = useIsCompactFormFactor(); + const showBottomSheet = isCompact && isNative; + const listRef = useRef<FlatList<CommandCenterListRow>>(null); + const bottomSheetListRef = useRef<BottomSheetFlatListMethods>(null); + const bottomSheetInputRef = useRef<React.ElementRef<typeof BottomSheetTextInput>>(null); + const { sheetRef, handleSheetChange, handleSheetDismiss } = useIsolatedBottomSheetVisibility({ + visible: state.open, + isEnabled: showBottomSheet, + onClose: state.close, + }); + + useEffect(() => { + if (!state.open || !state.activeId) return; + const index = state.rowIndexByResultId.get(state.activeId); + if (index === undefined) return; + const ref = showBottomSheet ? bottomSheetListRef.current : listRef.current; + ref?.scrollToIndex({ index, animated: true, viewPosition: 0.5 }); + }, [showBottomSheet, state.activeId, state.open, state.rowIndexByResultId]); + useEffect(() => { + if (!showBottomSheet || !state.open) return; + const timer = setTimeout(() => bottomSheetInputRef.current?.focus(), 300); + return () => clearTimeout(timer); + }, [showBottomSheet, state.open]); + + const renderItem = useCallback( + ({ item }: ListRenderItemInfo<CommandCenterListRow>) => + item.kind === "section" ? ( + <SectionRow row={item} /> + ) : ( + <ResultRow + result={item.result} + active={item.result.id === state.activeId} + onSelect={state.select} + /> + ), + [state.activeId, state.select], + ); + const getItemLayout = useCallback( + (_data: ArrayLike<CommandCenterListRow> | null | undefined, index: number) => ({ + index, + length: state.rows[index].height, + offset: state.offsets[index], + }), + [state.offsets, state.rows], + ); + const keyExtractor = useCallback((row: CommandCenterListRow) => row.key, []); + const empty = useMemo( + () => <Text style={styles.emptyText}>{t("shell.commandCenter.noMatches")}</Text>, + [t], + ); + const commonListProps = { + data: state.rows, + renderItem, + keyExtractor, + getItemLayout, + ListEmptyComponent: empty, + keyboardShouldPersistTaps: KEYBOARD_SHOULD_PERSIST_TAPS, + showsVerticalScrollIndicator: false, + initialNumToRender: 12, + maxToRenderPerBatch: 10, + windowSize: 5, + }; + const keyPress = useCallback( + ({ nativeEvent: { key } }: { nativeEvent: { key: string } }) => state.key(key), + [state], + ); + const submit = useCallback(() => state.key("Enter"), [state]); + const backdrop = useCallback( + (props: React.ComponentProps<typeof BottomSheetBackdrop>) => ( + <BottomSheetBackdrop {...props} disappearsOnIndex={-1} appearsOnIndex={0} opacity={0.45} /> + ), + [], + ); + + if (showBottomSheet) { + return ( + <IsolatedBottomSheetModal + ref={sheetRef} + snapPoints={COMMAND_CENTER_SNAP_POINTS} + index={0} + enableDynamicSizing={false} + onChange={handleSheetChange} + onDismiss={handleSheetDismiss} + backdropComponent={backdrop} + enablePanDownToClose + backgroundStyle={styles.sheetBackground} + handleIndicatorStyle={styles.sheetHandle} + keyboardBehavior="extend" + keyboardBlurBehavior="restore" + accessible={false} + > + <View style={styles.bottomSheetHeader}> + <ThemedBottomSheetTextInput + testID="command-center-input" + ref={bottomSheetInputRef} + value={state.query} + onChangeText={state.setQuery} + onKeyPress={keyPress} + onSubmitEditing={submit} + placeholder={t("shell.commandCenter.placeholder")} + style={styles.input} + autoCapitalize="none" + autoCorrect={false} + autoFocus + /> + </View> + <BottomSheetFlatList ref={bottomSheetListRef} {...commonListProps} /> + </IsolatedBottomSheetModal> + ); + } + if (!state.open) return null; + return ( + <Modal visible transparent animationType="fade" onRequestClose={state.close}> + <View style={styles.overlay}> + <Pressable style={styles.backdrop} onPress={state.close} /> + <View testID="command-center-panel" style={styles.panel}> + <View style={styles.header}> + <ThemedTextInput + testID="command-center-input" + ref={state.inputRef} + value={state.query} + onChangeText={state.setQuery} + placeholder={t("shell.commandCenter.placeholder")} + style={styles.input} + autoCapitalize="none" + autoCorrect={false} + autoFocus + /> + </View> + <FlatList ref={listRef} style={styles.results} {...commonListProps} /> + </View> + </View> + </Modal> + ); +} + +const styles = StyleSheet.create((theme) => ({ + overlay: { + flex: 1, + justifyContent: "flex-start", + alignItems: "center", + paddingTop: theme.spacing[12], + }, + backdrop: { ...StyleSheet.absoluteFillObject, backgroundColor: "rgba(0, 0, 0, 0.5)" }, + panel: { + width: 640, + maxWidth: "92%", + maxHeight: "80%", + borderWidth: 1, + borderColor: theme.colors.border, + borderRadius: theme.borderRadius.lg, + overflow: "hidden", + backgroundColor: theme.colors.surface0, + ...theme.shadow.lg, + }, + header: { + paddingHorizontal: theme.spacing[4], + paddingVertical: theme.spacing[3], + borderBottomWidth: 1, + borderBottomColor: theme.colors.border, + }, + bottomSheetHeader: { + paddingHorizontal: theme.spacing[4], + paddingVertical: theme.spacing[3], + borderBottomWidth: 1, + borderBottomColor: theme.colors.border, + }, + input: { + fontSize: theme.fontSize.base, + paddingVertical: theme.spacing[1], + color: theme.colors.foreground, + outlineWidth: 0, + }, + results: { flexGrow: 0 }, + sectionLabel: { + paddingHorizontal: theme.spacing[4], + paddingBottom: theme.spacing[2], + fontSize: theme.fontSize.xs, + color: theme.colors.foregroundMuted, + }, + sectionDivider: { + height: 1, + marginTop: theme.spacing[2], + marginBottom: theme.spacing[2], + backgroundColor: theme.colors.border, + }, + row: { height: 36, paddingHorizontal: theme.spacing[4], paddingVertical: theme.spacing[2] }, + tallRow: { height: 56 }, + activeRow: { backgroundColor: theme.colors.surface1 }, + rowContent: { + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + gap: theme.spacing[3], + }, + rowMain: { + flex: 1, + minWidth: 0, + flexDirection: "row", + alignItems: "flex-start", + gap: theme.spacing[3], + }, + textContent: { flex: 1, minWidth: 0 }, + title: { + color: theme.colors.foreground, + fontSize: theme.fontSize.sm, + lineHeight: 20, + flexShrink: 1, + }, + subtitle: { color: theme.colors.foregroundMuted, fontSize: theme.fontSize.xs, lineHeight: 18 }, + iconSlot: { width: 16, height: 20, alignItems: "center", justifyContent: "center" }, + rowShortcut: { flexShrink: 0 }, + breadcrumb: { + flex: 1, + minWidth: 0, + flexDirection: "row", + alignItems: "center", + gap: theme.spacing[1], + }, + breadcrumbPart: { + flexDirection: "row", + alignItems: "center", + gap: theme.spacing[1], + flexShrink: 1, + }, + breadcrumbGroup: { + color: theme.colors.foregroundMuted, + fontSize: theme.fontSize.sm, + lineHeight: 20, + flexShrink: 0, + }, + emptyText: { + paddingHorizontal: theme.spacing[4], + paddingVertical: theme.spacing[6], + textAlign: "center", + color: theme.colors.foregroundMuted, + fontSize: theme.fontSize.sm, + }, + sheetBackground: { backgroundColor: theme.colors.surface0 }, + sheetHandle: { backgroundColor: theme.colors.palette.zinc[600] }, + titledSection: { height: 32, justifyContent: "flex-end" }, + dividedSection: { height: 49, justifyContent: "flex-end" }, + dividerSection: { height: 17, justifyContent: "flex-end" }, +})); diff --git a/packages/app/src/command-center/contributions.ts b/packages/app/src/command-center/contributions.ts new file mode 100644 index 000000000..ab54cbc7e --- /dev/null +++ b/packages/app/src/command-center/contributions.ts @@ -0,0 +1,53 @@ +import type { ComponentType } from "react"; +import type { ShortcutKey } from "@/utils/format-shortcut"; + +export interface CommandCenterIconProps { + size: number; +} + +export type CommandCenterIcon = ComponentType<CommandCenterIconProps>; + +interface CommandCenterContributionBase { + id: string; + group: string; + groupRank: number; + rank: number; + keywords: readonly string[]; + visibility: "always" | "query"; + run(): void | Promise<void>; +} + +export type CommandCenterContribution = + | (CommandCenterContributionBase & { + presentation: { + kind: "action"; + title: string; + subtitle?: string; + sectionTitle?: string; + icon?: CommandCenterIcon; + shortcutKeys?: ShortcutKey[][]; + }; + }) + | (CommandCenterContributionBase & { + presentation: { + kind: "choice"; + path: readonly [string, ...string[]]; + icon?: CommandCenterIcon; + selected: boolean; + testId?: string; + }; + }); + +export interface CommandCenterContributionSnapshot { + contributions: readonly CommandCenterContribution[]; +} + +export interface CommandCenterRegistrationOwner { + sourceId: string; + token: symbol; +} + +export interface CommandCenterRegistration { + owner: CommandCenterRegistrationOwner; + contributions: readonly CommandCenterContribution[]; +} diff --git a/packages/app/src/command-center/model-contributions.test.tsx b/packages/app/src/command-center/model-contributions.test.tsx new file mode 100644 index 000000000..6e38b33e1 --- /dev/null +++ b/packages/app/src/command-center/model-contributions.test.tsx @@ -0,0 +1,104 @@ +import { describe, expect, it } from "vitest"; +import type { ProviderSelectorProvider } from "@/provider-selection/provider-selection"; +import { buildModelChoiceContributions } from "./model-contributions"; + +function TestIcon() { + return null; +} + +function provider(input: { + id: string; + label: string; + state: "models" | "error"; +}): ProviderSelectorProvider { + if (input.state === "error") { + return { + id: input.id, + label: input.label, + modelSelection: { kind: "error", message: "Unavailable" }, + }; + } + return { + id: input.id, + label: input.label, + modelSelection: { + kind: "models", + rows: [ + { + favoriteKey: `${input.id}:model`, + provider: input.id, + providerLabel: input.label, + modelId: "model", + modelLabel: `${input.label} model`, + description: undefined, + }, + ], + }, + }; +} + +describe("Command Center model choices", () => { + it("publishes selectable providers and executes the draft owner's command", () => { + const selections: string[] = []; + const choices = buildModelChoiceContributions({ + serverId: "host", + providers: [ + provider({ id: "claude", label: "Claude", state: "models" }), + provider({ id: "codex", label: "Codex", state: "models" }), + ], + selectedProvider: "codex", + selectedModelId: "model", + groupLabel: "Model", + searchKeywords: "model switch", + getIcon: () => TestIcon, + select: (selectedProvider, modelId) => selections.push(`${selectedProvider}:${modelId}`), + }); + + expect( + choices.map((choice) => ({ + id: choice.id, + selected: choice.presentation.kind === "choice" ? choice.presentation.selected : false, + })), + ).toEqual([ + { id: "host:claude:model", selected: false }, + { id: "host:codex:model", selected: true }, + ]); + choices[0].run(); + choices[1].run(); + expect(selections).toEqual(["claude:model"]); + }); + + it("does not publish unavailable providers or no-op default rows", () => { + const choices = buildModelChoiceContributions({ + serverId: "host", + providers: [ + provider({ id: "unavailable", label: "Unavailable", state: "error" }), + { + id: "empty", + label: "Empty", + modelSelection: { + kind: "models", + rows: [ + { + favoriteKey: "empty:", + provider: "empty", + providerLabel: "Empty", + modelId: "", + modelLabel: "Default", + description: undefined, + }, + ], + }, + }, + ], + selectedProvider: null, + selectedModelId: null, + groupLabel: "Model", + searchKeywords: "model switch", + getIcon: () => TestIcon, + select: () => undefined, + }); + + expect(choices).toEqual([]); + }); +}); diff --git a/packages/app/src/command-center/model-contributions.ts b/packages/app/src/command-center/model-contributions.ts new file mode 100644 index 000000000..182a528c9 --- /dev/null +++ b/packages/app/src/command-center/model-contributions.ts @@ -0,0 +1,51 @@ +import type { AgentProvider } from "@getpaseo/protocol/agent-types"; +import type { ProviderSelectorProvider } from "@/provider-selection/provider-selection"; +import type { CommandCenterContribution, CommandCenterIcon } from "./contributions"; + +interface ModelChoiceInput { + serverId: string; + providers: readonly ProviderSelectorProvider[]; + selectedProvider: string | null; + selectedModelId: string | null; + groupLabel: string; + searchKeywords: string; + getIcon(provider: AgentProvider): CommandCenterIcon; + select(provider: AgentProvider, modelId: string): void; +} + +export function buildModelChoiceContributions( + input: ModelChoiceInput, +): CommandCenterContribution[] { + const contributions: CommandCenterContribution[] = []; + let rank = 0; + for (const provider of input.providers) { + if (provider.modelSelection.kind !== "models") continue; + const agentProvider = provider.id; + const icon = input.getIcon(agentProvider); + for (const model of provider.modelSelection.rows) { + if (!model.modelId) continue; + const modelId = model.modelId; + const selected = input.selectedProvider === provider.id && input.selectedModelId === modelId; + contributions.push({ + id: `${input.serverId}:${provider.id}:${modelId}`, + group: "models", + groupRank: 1, + rank, + keywords: [modelId, input.searchKeywords], + visibility: "query", + run: () => { + if (!selected) input.select(agentProvider, modelId); + }, + presentation: { + kind: "choice", + path: [input.groupLabel, provider.label, model.modelLabel], + icon, + selected, + testId: `command-center-model-${input.serverId}:${provider.id}:${modelId}`, + }, + }); + rank += 1; + } + } + return contributions; +} diff --git a/packages/app/src/command-center/provider-icon.tsx b/packages/app/src/command-center/provider-icon.tsx new file mode 100644 index 000000000..245d5de27 --- /dev/null +++ b/packages/app/src/command-center/provider-icon.tsx @@ -0,0 +1,20 @@ +import { withUnistyles } from "react-native-unistyles"; +import type { AgentProvider } from "@getpaseo/protocol/agent-types"; +import { getProviderIcon } from "@/components/provider-icons"; +import type { CommandCenterIcon, CommandCenterIconProps } from "./contributions"; + +const commandCenterProviderIcons = new Map<AgentProvider, CommandCenterIcon>(); + +export function getCommandCenterProviderIcon(provider: AgentProvider): CommandCenterIcon { + const cached = commandCenterProviderIcons.get(provider); + if (cached) return cached; + + const ProviderIcon = withUnistyles(getProviderIcon(provider), (theme) => ({ + color: theme.colors.foregroundMuted, + })); + function CommandCenterProviderIcon({ size }: CommandCenterIconProps) { + return <ProviderIcon size={size} />; + } + commandCenterProviderIcons.set(provider, CommandCenterProviderIcon); + return CommandCenterProviderIcon; +} diff --git a/packages/app/src/command-center/provider.tsx b/packages/app/src/command-center/provider.tsx new file mode 100644 index 000000000..2f1cc706c --- /dev/null +++ b/packages/app/src/command-center/provider.tsx @@ -0,0 +1,56 @@ +import { + createContext, + useContext, + useEffect, + useRef, + useSyncExternalStore, + type ReactNode, +} from "react"; +import type { CommandCenterContribution } from "./contributions"; +import { createCommandCenterRegistry, type CommandCenterRegistry } from "./registry"; + +const CommandCenterRegistryContext = createContext<CommandCenterRegistry | null>(null); + +export function CommandCenterProvider({ children }: { children: ReactNode }) { + const registryRef = useRef<CommandCenterRegistry | null>(null); + if (!registryRef.current) registryRef.current = createCommandCenterRegistry(); + + return ( + <CommandCenterRegistryContext.Provider value={registryRef.current}> + {children} + </CommandCenterRegistryContext.Provider> + ); +} + +function useCommandCenterRegistry(): CommandCenterRegistry { + const registry = useContext(CommandCenterRegistryContext); + if (!registry) throw new Error("CommandCenterProvider is required"); + return registry; +} + +export function useCommandCenterContributions() { + const registry = useCommandCenterRegistry(); + return useSyncExternalStore(registry.subscribe, registry.getSnapshot, registry.getSnapshot); +} + +export function useCommandCenterActions(input: { + sourceId: string; + enabled: boolean; + actions: readonly CommandCenterContribution[]; +}): void { + const registry = useCommandCenterRegistry(); + const ownerRef = useRef({ sourceId: input.sourceId, token: Symbol(input.sourceId) }); + if (ownerRef.current.sourceId !== input.sourceId) { + ownerRef.current = { sourceId: input.sourceId, token: Symbol(input.sourceId) }; + } + const owner = ownerRef.current; + + useEffect(() => { + if (!input.enabled) { + registry.remove(owner); + return; + } + registry.replace({ owner, contributions: input.actions }); + return () => registry.remove(owner); + }, [input.actions, input.enabled, owner, registry]); +} diff --git a/packages/app/src/command-center/registry.test.ts b/packages/app/src/command-center/registry.test.ts new file mode 100644 index 000000000..cf8d60684 --- /dev/null +++ b/packages/app/src/command-center/registry.test.ts @@ -0,0 +1,73 @@ +import { describe, expect, it } from "vitest"; +import type { CommandCenterContribution, CommandCenterRegistrationOwner } from "./contributions"; +import { createCommandCenterRegistry } from "./registry"; + +function action(id: string, rank: number): CommandCenterContribution { + return { + id, + group: "actions", + groupRank: 0, + rank, + keywords: [], + visibility: "always", + run: () => undefined, + presentation: { kind: "action", title: id }, + }; +} + +function owner(sourceId: string): CommandCenterRegistrationOwner { + return { sourceId, token: Symbol(sourceId) }; +} + +describe("Command Center registry", () => { + it("atomically replaces a source and preserves a no-op snapshot", () => { + const registry = createCommandCenterRegistry(); + const source = owner("root"); + const first = [action("first", 0)]; + let notifications = 0; + registry.subscribe(() => { + notifications += 1; + }); + + registry.replace({ owner: source, contributions: first }); + const snapshot = registry.getSnapshot(); + registry.replace({ owner: source, contributions: first }); + expect(registry.getSnapshot()).toBe(snapshot); + expect(notifications).toBe(1); + + registry.replace({ owner: source, contributions: [action("second", 0)] }); + expect(registry.getSnapshot().contributions.map((item) => item.id)).toEqual(["root:second"]); + expect(notifications).toBe(2); + }); + + it("does not let stale cleanup remove a replacement owner", () => { + const registry = createCommandCenterRegistry(); + const stale = owner("draft:tab"); + const current = owner("draft:tab"); + registry.replace({ owner: stale, contributions: [action("old", 0)] }); + registry.replace({ owner: current, contributions: [action("new", 0)] }); + + registry.remove(stale); + expect(registry.getSnapshot().contributions.map((item) => item.id)).toEqual(["draft:tab:new"]); + registry.remove(current); + expect(registry.getSnapshot().contributions).toEqual([]); + }); + + it("orders independently of registration order and rejects duplicate active ids", () => { + const registry = createCommandCenterRegistry(); + registry.replace({ owner: owner("later"), contributions: [action("z", 2)] }); + registry.replace({ owner: owner("earlier"), contributions: [action("a", 1)] }); + expect(registry.getSnapshot().contributions.map((item) => item.id)).toEqual([ + "earlier:a", + "later:z", + ]); + + const duplicateOwner = owner("duplicate"); + expect(() => + registry.replace({ + owner: duplicateOwner, + contributions: [action("same", 0), action("same", 1)], + }), + ).toThrow("Duplicate Command Center contribution id: duplicate:same"); + }); +}); diff --git a/packages/app/src/command-center/registry.ts b/packages/app/src/command-center/registry.ts new file mode 100644 index 000000000..f4c383621 --- /dev/null +++ b/packages/app/src/command-center/registry.ts @@ -0,0 +1,100 @@ +import type { + CommandCenterContribution, + CommandCenterContributionSnapshot, + CommandCenterRegistration, + CommandCenterRegistrationOwner, +} from "./contributions"; + +export interface CommandCenterRegistry { + getSnapshot(): CommandCenterContributionSnapshot; + subscribe(listener: () => void): () => void; + replace(registration: CommandCenterRegistration): void; + remove(owner: CommandCenterRegistrationOwner): void; +} + +interface ActiveRegistration { + owner: CommandCenterRegistrationOwner; + contributions: readonly CommandCenterContribution[]; +} + +const EMPTY_SNAPSHOT: CommandCenterContributionSnapshot = { contributions: [] }; + +function contributionId(sourceId: string, id: string): string { + return `${sourceId}:${id}`; +} + +function compareContributions( + left: CommandCenterContribution, + right: CommandCenterContribution, +): number { + if (left.groupRank !== right.groupRank) return left.groupRank - right.groupRank; + const groupDelta = left.group.localeCompare(right.group); + if (groupDelta !== 0) return groupDelta; + if (left.rank !== right.rank) return left.rank - right.rank; + return left.id.localeCompare(right.id); +} + +function sameContributions( + left: readonly CommandCenterContribution[], + right: readonly CommandCenterContribution[], +): boolean { + return left.length === right.length && left.every((item, index) => item === right[index]); +} + +export function createCommandCenterRegistry(): CommandCenterRegistry { + const registrations = new Map<string, ActiveRegistration>(); + const listeners = new Set<() => void>(); + let snapshot = EMPTY_SNAPSHOT; + + function publish(): void { + const contributions: CommandCenterContribution[] = []; + const ids = new Set<string>(); + + for (const registration of registrations.values()) { + for (const contribution of registration.contributions) { + const id = contributionId(registration.owner.sourceId, contribution.id); + if (ids.has(id)) { + throw new Error(`Duplicate Command Center contribution id: ${id}`); + } + ids.add(id); + contributions.push({ ...contribution, id }); + } + } + contributions.sort(compareContributions); + + if (sameContributions(snapshot.contributions, contributions)) return; + snapshot = contributions.length === 0 ? EMPTY_SNAPSHOT : { contributions }; + for (const listener of listeners) listener(); + } + + return { + getSnapshot: () => snapshot, + subscribe(listener) { + listeners.add(listener); + return () => listeners.delete(listener); + }, + replace(registration) { + const current = registrations.get(registration.owner.sourceId); + if ( + current?.owner.token === registration.owner.token && + sameContributions(current.contributions, registration.contributions) + ) { + return; + } + const ids = new Set<string>(); + for (const contribution of registration.contributions) { + const id = contributionId(registration.owner.sourceId, contribution.id); + if (ids.has(id)) throw new Error(`Duplicate Command Center contribution id: ${id}`); + ids.add(id); + } + registrations.set(registration.owner.sourceId, registration); + publish(); + }, + remove(owner) { + const current = registrations.get(owner.sourceId); + if (current?.owner.token !== owner.token) return; + registrations.delete(owner.sourceId); + publish(); + }, + }; +} diff --git a/packages/app/src/command-center/results.test.ts b/packages/app/src/command-center/results.test.ts new file mode 100644 index 000000000..3405a6a51 --- /dev/null +++ b/packages/app/src/command-center/results.test.ts @@ -0,0 +1,105 @@ +import { describe, expect, it } from "vitest"; +import type { CommandCenterContribution } from "./contributions"; +import { + buildContributionSections, + moveActiveResultId, + preserveActiveResultId, + projectCommandCenterRows, + type CommandCenterWorkspaceResult, +} from "./results"; + +function contribution(input: { + id: string; + group: string; + groupRank: number; + visibility?: "always" | "query"; +}): CommandCenterContribution { + return { + ...input, + rank: 0, + keywords: [input.id], + visibility: input.visibility ?? "always", + run: () => undefined, + presentation: { + kind: "action", + title: input.id, + sectionTitle: input.group, + }, + }; +} + +function workspace(id: string): CommandCenterWorkspaceResult { + return { + kind: "workspace", + id, + title: id, + subtitle: "host", + searchText: id, + run: () => undefined, + }; +} + +function sectionResultIds(sections: ReturnType<typeof buildContributionSections>): string[] { + const ids: string[] = []; + for (const section of sections) { + for (const result of section.results) ids.push(result.id); + } + return ids; +} + +describe("Command Center result projection", () => { + it("query-gates model choices and creates one flat row index", () => { + const contributions = [ + contribution({ id: "settings", group: "actions", groupRank: 0 }), + contribution({ id: "opus", group: "models", groupRank: 1, visibility: "query" }), + ]; + const emptySections = buildContributionSections(contributions, ""); + expect(sectionResultIds(emptySections)).toEqual(["settings"]); + + const sections = buildContributionSections(contributions, "o"); + const projection = projectCommandCenterRows([ + ...sections, + { id: "workspaces", rank: 2, title: "Workspaces", results: [workspace("workspace:1")] }, + ]); + expect(projection.rows.map((row) => row.key)).toEqual([ + "section:models", + "opus", + "section:workspaces", + "workspace:1", + ]); + expect(projection.rowIndexByResultId.get("workspace:1")).toBe(3); + expect(projection.offsets).toEqual([0, 32, 68, 117]); + }); + + it("preserves active selection by id and falls back to the first result", () => { + const first = workspace("workspace:first"); + const second = workspace("workspace:second"); + expect(preserveActiveResultId(second.id, [first, second])).toBe(second.id); + expect(preserveActiveResultId("missing", [first, second])).toBe(first.id); + expect(preserveActiveResultId(first.id, [])).toBeNull(); + }); + + it("wraps keyboard selection in both directions", () => { + const first = workspace("workspace:first"); + const second = workspace("workspace:second"); + expect(moveActiveResultId(second.id, [first, second], "next")).toBe(first.id); + expect(moveActiveResultId(first.id, [first, second], "previous")).toBe(second.id); + }); + + it("keeps keyboard selection aligned with rows beyond the render window", () => { + const workspaces = Array.from({ length: 200 }, (_, index) => workspace(`workspace:${index}`)); + const projection = projectCommandCenterRows([ + { id: "workspaces", rank: 0, title: "Workspaces", results: workspaces }, + ]); + + let activeId: string | null = workspaces[0].id; + for (let index = 0; index < 150; index += 1) { + activeId = moveActiveResultId(activeId, projection.selectableResults, "next"); + } + + const targetId = workspaces[150].id; + expect(activeId).toBe(targetId); + expect(projection.rowIndexByResultId.get(targetId)).toBe(151); + expect(projection.offsets[151]).toBe(32 + 150 * 56); + }); +}); diff --git a/packages/app/src/command-center/results.ts b/packages/app/src/command-center/results.ts new file mode 100644 index 000000000..ab2f3a837 --- /dev/null +++ b/packages/app/src/command-center/results.ts @@ -0,0 +1,181 @@ +import type { AggregatedAgent } from "@/hooks/use-aggregated-agents"; +import type { CommandCenterContribution } from "./contributions"; + +export interface CommandCenterWorkspaceResult { + kind: "workspace"; + id: string; + title: string; + subtitle: string; + searchText: string; + run(): void; +} + +export interface CommandCenterAgentResult { + kind: "agent"; + id: string; + agent: AggregatedAgent; + title: string; + subtitle: string; + searchText: string; + run(): void; +} + +export interface CommandCenterContributionResult { + kind: "contribution"; + id: string; + contribution: CommandCenterContribution; + searchText: string; + run(): void | Promise<void>; +} + +export type CommandCenterResult = + | CommandCenterWorkspaceResult + | CommandCenterAgentResult + | CommandCenterContributionResult; + +export interface CommandCenterResultSection { + id: string; + rank: number; + title?: string; + results: readonly CommandCenterResult[]; +} + +interface MutableCommandCenterResultSection { + id: string; + rank: number; + title?: string; + results: CommandCenterResult[]; +} + +export type CommandCenterListRow = + | { kind: "section"; key: string; title?: string; divider: boolean; height: number } + | { kind: "result"; key: string; result: CommandCenterResult; height: number }; + +export interface CommandCenterListProjection { + rows: readonly CommandCenterListRow[]; + selectableResults: readonly CommandCenterResult[]; + rowIndexByResultId: ReadonlyMap<string, number>; + offsets: readonly number[]; +} + +function matchesQuery(searchText: string, query: string): boolean { + const normalized = query.trim().toLowerCase(); + return !normalized || searchText.includes(normalized); +} + +function contributionSearchText(contribution: CommandCenterContribution): string { + const presentationText = + contribution.presentation.kind === "action" + ? [contribution.presentation.title, contribution.presentation.subtitle ?? ""] + : contribution.presentation.path; + return [...presentationText, ...contribution.keywords].join(" ").toLowerCase(); +} + +function resultHeight(result: CommandCenterResult): number { + if (result.kind === "workspace" || result.kind === "agent") return 56; + if (result.contribution.presentation.kind === "action") { + return result.contribution.presentation.subtitle ? 56 : 36; + } + return 36; +} + +export function buildContributionSections( + contributions: readonly CommandCenterContribution[], + query: string, +): CommandCenterResultSection[] { + const groups = new Map<string, MutableCommandCenterResultSection>(); + const hasQuery = Boolean(query.trim()); + + for (const contribution of contributions) { + if (contribution.visibility === "query" && !hasQuery) continue; + const searchText = contributionSearchText(contribution); + if (!matchesQuery(searchText, query)) continue; + const existing = groups.get(contribution.group); + const title = + contribution.presentation.kind === "action" + ? contribution.presentation.sectionTitle + : undefined; + const section = existing ?? { + id: contribution.group, + rank: contribution.groupRank, + title, + results: [], + }; + section.results.push({ + kind: "contribution", + id: contribution.id, + contribution, + searchText, + run: contribution.run, + }); + groups.set(contribution.group, section); + } + + return [...groups.values()].sort( + (left, right) => left.rank - right.rank || left.id.localeCompare(right.id), + ); +} + +export function projectCommandCenterRows( + sections: readonly CommandCenterResultSection[], +): CommandCenterListProjection { + const populated = sections + .filter((section) => section.results.length > 0) + .sort((left, right) => left.rank - right.rank || left.id.localeCompare(right.id)); + const rows: CommandCenterListRow[] = []; + const selectableResults: CommandCenterResult[] = []; + const rowIndexByResultId = new Map<string, number>(); + const offsets: number[] = []; + let offset = 0; + + for (const [sectionIndex, section] of populated.entries()) { + const divider = sectionIndex > 0; + let sectionHeight = 0; + if (section.title && divider) sectionHeight = 49; + if (section.title && !divider) sectionHeight = 32; + if (!section.title && divider) sectionHeight = 17; + if (sectionHeight > 0) { + offsets.push(offset); + rows.push({ + kind: "section", + key: `section:${section.id}`, + title: section.title, + divider, + height: sectionHeight, + }); + offset += sectionHeight; + } + for (const result of section.results) { + const height = resultHeight(result); + offsets.push(offset); + rowIndexByResultId.set(result.id, rows.length); + rows.push({ kind: "result", key: result.id, result, height }); + selectableResults.push(result); + offset += height; + } + } + + return { rows, selectableResults, rowIndexByResultId, offsets }; +} + +export function preserveActiveResultId( + activeId: string | null, + results: readonly CommandCenterResult[], +): string | null { + if (activeId && results.some((result) => result.id === activeId)) return activeId; + return results[0]?.id ?? null; +} + +export function moveActiveResultId( + activeId: string | null, + results: readonly CommandCenterResult[], + direction: "next" | "previous", +): string | null { + if (results.length === 0) return null; + const current = results.findIndex((result) => result.id === activeId); + const delta = direction === "next" ? 1 : -1; + let start = current; + if (current < 0 && direction === "previous") start = 0; + const next = (start + delta + results.length) % results.length; + return results[next].id; +} diff --git a/packages/app/src/components/command-center.tsx b/packages/app/src/components/command-center.tsx deleted file mode 100644 index 49cafb624..000000000 --- a/packages/app/src/components/command-center.tsx +++ /dev/null @@ -1,726 +0,0 @@ -import { - Modal, - Pressable, - ScrollView, - Text, - TextInput, - View, - type PressableStateCallbackType, -} from "react-native"; -import { memo, useCallback, useEffect, useMemo, useRef, type ReactNode } from "react"; -import { useTranslation } from "react-i18next"; -import { Folder, Home, Plus, Settings } from "lucide-react-native"; -import { StyleSheet, useUnistyles, withUnistyles } from "react-native-unistyles"; -import { - useCommandCenter, - type CommandCenterActionItem, - type CommandCenterAgentItem, - type CommandCenterItem, - type CommandCenterWorkspaceItem, -} from "@/hooks/use-command-center"; -import { AgentStatusDot } from "@/components/agent-status-dot"; -import { Shortcut } from "@/components/ui/shortcut"; -import { isNative, isWeb } from "@/constants/platform"; -import { useIsCompactFormFactor } from "@/constants/layout"; -import { - IsolatedBottomSheetModal, - useIsolatedBottomSheetVisibility, -} from "@/components/ui/isolated-bottom-sheet-modal"; -import { - BottomSheetBackdrop, - BottomSheetScrollView, - BottomSheetTextInput, -} from "@gorhom/bottom-sheet"; - -const ThemedBottomSheetTextInput = withUnistyles(BottomSheetTextInput, (theme) => ({ - placeholderTextColor: theme.colors.foregroundMuted, -})); -const ThemedFolder = withUnistyles(Folder, (theme) => ({ - color: theme.colors.foregroundMuted, -})); - -interface CommandCenterRowProps { - active: boolean; - children: ReactNode; - onPress: () => void; - registerRow: (el: View | null) => void; - onLayout?: (event: { nativeEvent: { layout: { y: number; height: number } } }) => void; -} - -const CommandCenterRow = memo(function CommandCenterRow({ - active, - children, - onPress, - registerRow, - onLayout, -}: CommandCenterRowProps) { - const { theme } = useUnistyles(); - - const pressableStyle = useCallback( - ({ hovered, pressed }: PressableStateCallbackType & { hovered?: boolean }) => [ - styles.row, - (Boolean(hovered) || pressed || active) && { - backgroundColor: theme.colors.surface1, - }, - ], - [active, theme.colors.surface1], - ); - - return ( - <Pressable ref={registerRow} style={pressableStyle} onPress={onPress} onLayout={onLayout}> - {children} - </Pressable> - ); -}); - -interface CommandCenterRowContainerProps { - item: CommandCenterItem; - rowIndex: number; - active: boolean; - rowRefs: React.MutableRefObject<Map<number, View>>; - onSelect: (item: CommandCenterItem) => void; - onLayout?: (event: { nativeEvent: { layout: { y: number; height: number } } }) => void; - children: ReactNode; -} - -function CommandCenterRowContainer({ - item, - rowIndex, - active, - rowRefs, - onSelect, - onLayout, - children, -}: CommandCenterRowContainerProps) { - const handlePress = useCallback(() => onSelect(item), [onSelect, item]); - const registerRow = useCallback( - (el: View | null) => { - if (el) rowRefs.current.set(rowIndex, el); - else rowRefs.current.delete(rowIndex); - }, - [rowRefs, rowIndex], - ); - return ( - <CommandCenterRow - active={active} - registerRow={registerRow} - onPress={handlePress} - onLayout={onLayout} - > - {children} - </CommandCenterRow> - ); -} - -interface CommandCenterActionRowProps { - item: CommandCenterActionItem; - rowIndex: number; - active: boolean; - rowRefs: React.MutableRefObject<Map<number, View>>; - onLayout?: (event: { nativeEvent: { layout: { y: number; height: number } } }) => void; - onSelect: (item: CommandCenterItem) => void; -} - -function CommandCenterActionRow({ - item, - rowIndex, - active, - rowRefs, - onLayout, - onSelect, -}: CommandCenterActionRowProps) { - const { theme } = useUnistyles(); - let actionIcon: React.ReactNode = null; - if (item.icon === "plus") { - actionIcon = <Plus size={16} strokeWidth={2.4} color={theme.colors.foregroundMuted} />; - } else if (item.icon === "settings") { - actionIcon = <Settings size={16} strokeWidth={2.2} color={theme.colors.foregroundMuted} />; - } else if (item.icon === "home") { - actionIcon = <Home size={16} strokeWidth={2.2} color={theme.colors.foregroundMuted} />; - } - const titleStyle = useMemo( - () => [styles.title, { color: theme.colors.foreground }], - [theme.colors.foreground], - ); - return ( - <CommandCenterRowContainer - item={item} - rowIndex={rowIndex} - active={active} - rowRefs={rowRefs} - onSelect={onSelect} - onLayout={onLayout} - > - <View style={styles.rowContent}> - <View style={styles.rowMain}> - {actionIcon ? <View style={styles.iconSlot}>{actionIcon}</View> : null} - <View style={styles.textContent}> - <Text style={titleStyle} numberOfLines={1}> - {item.title} - </Text> - </View> - </View> - {item.shortcutKeys ? ( - <Shortcut chord={item.shortcutKeys} style={styles.rowShortcut} /> - ) : null} - </View> - </CommandCenterRowContainer> - ); -} - -interface CommandCenterAgentRowContentProps { - item: CommandCenterAgentItem; -} - -function CommandCenterAgentRowContent({ item }: CommandCenterAgentRowContentProps) { - const { theme } = useUnistyles(); - const agent = item.agent; - const titleStyle = useMemo( - () => [styles.title, { color: theme.colors.foreground }], - [theme.colors.foreground], - ); - const subtitleStyle = useMemo( - () => [styles.subtitle, { color: theme.colors.foregroundMuted }], - [theme.colors.foregroundMuted], - ); - return ( - <View style={styles.rowContent} testID={`command-center-agent-${agent.serverId}:${agent.id}`}> - <View style={styles.rowMain}> - <View style={styles.iconSlot}> - <AgentStatusDot - status={agent.status} - requiresAttention={agent.requiresAttention} - showInactive - /> - </View> - <View style={styles.textContent}> - <Text style={titleStyle} numberOfLines={1}> - {item.title} - </Text> - <Text style={subtitleStyle} numberOfLines={1} testID="command-center-agent-subtitle"> - {item.subtitle} - </Text> - </View> - </View> - </View> - ); -} - -interface AgentItemsSectionProps { - agentItems: CommandCenterAgentItem[]; - startIndex: number; - activeIndex: number; - rowRefs: React.MutableRefObject<Map<number, View>>; - onRowLayout: ( - rowIndex: number, - ) => (event: { nativeEvent: { layout: { y: number; height: number } } }) => void; - onSelect: (item: CommandCenterItem) => void; - sectionDividerStyle: React.ComponentProps<typeof View>["style"]; - sectionLabelStyle: React.ComponentProps<typeof Text>["style"]; -} - -function AgentItemsSection({ - agentItems, - startIndex, - activeIndex, - rowRefs, - onRowLayout, - onSelect, - sectionDividerStyle, - sectionLabelStyle, -}: AgentItemsSectionProps) { - const { t } = useTranslation(); - - return ( - <> - {startIndex > 0 ? <View style={sectionDividerStyle} /> : null} - <Text style={sectionLabelStyle}>{t("shell.commandCenter.agents")}</Text> - {agentItems.map((item, index) => { - const rowIndex = startIndex + index; - const agent = item.agent; - return ( - <CommandCenterRowContainer - key={`${agent.serverId}:${agent.id}`} - item={item} - rowIndex={rowIndex} - active={rowIndex === activeIndex} - rowRefs={rowRefs} - onLayout={onRowLayout(rowIndex)} - onSelect={onSelect} - > - <CommandCenterAgentRowContent item={item} /> - </CommandCenterRowContainer> - ); - })} - </> - ); -} - -interface WorkspaceItemsSectionProps { - workspaceItems: CommandCenterWorkspaceItem[]; - startIndex: number; - activeIndex: number; - rowRefs: React.MutableRefObject<Map<number, View>>; - onRowLayout: ( - rowIndex: number, - ) => (event: { nativeEvent: { layout: { y: number; height: number } } }) => void; - onSelect: (item: CommandCenterItem) => void; - sectionDividerStyle: React.ComponentProps<typeof View>["style"]; - sectionLabelStyle: React.ComponentProps<typeof Text>["style"]; -} - -function WorkspaceItemsSection({ - workspaceItems, - startIndex, - activeIndex, - rowRefs, - onRowLayout, - onSelect, - sectionDividerStyle, - sectionLabelStyle, -}: WorkspaceItemsSectionProps) { - const { t } = useTranslation(); - - return ( - <> - {startIndex > 0 ? <View style={sectionDividerStyle} /> : null} - <Text style={sectionLabelStyle}>{t("shell.commandCenter.workspaces")}</Text> - {workspaceItems.map((item, index) => { - const rowIndex = startIndex + index; - return ( - <CommandCenterRowContainer - key={`${item.serverId}:${item.workspaceId}`} - item={item} - rowIndex={rowIndex} - active={rowIndex === activeIndex} - rowRefs={rowRefs} - onSelect={onSelect} - onLayout={onRowLayout(rowIndex)} - > - <View - style={styles.rowContent} - testID={`command-center-workspace-${item.serverId}:${item.workspaceId}`} - > - <View style={styles.rowMain}> - <View style={styles.iconSlot}> - <ThemedFolder size={16} strokeWidth={2.2} /> - </View> - <View style={styles.textContent}> - <Text style={styles.title} numberOfLines={1}> - {item.title} - </Text> - <Text style={styles.subtitle} numberOfLines={1}> - {item.subtitle} - </Text> - </View> - </View> - </View> - </CommandCenterRowContainer> - ); - })} - </> - ); -} - -export function CommandCenter() { - const { theme } = useUnistyles(); - const { t } = useTranslation(); - const { - open, - inputRef, - query, - setQuery, - activeIndex, - items, - handleClose, - handleSelectItem, - handleKeyEvent, - } = useCommandCenter(); - const isCompact = useIsCompactFormFactor(); - const showBottomSheet = isCompact && isNative; - const rowRefs = useRef<Map<number, View>>(new Map()); - const rowLayouts = useRef<Map<number, { y: number; height: number }>>(new Map()); - const resultsRef = useRef<ScrollView>(null); - const nativeScrollY = useRef(0); - const nativeViewHeight = useRef(0); - // BottomSheetTextInput wraps a different TextInput type (from react-native-gesture-handler). - // Use a loose ref to avoid the type mismatch — same pattern as AdaptiveTextInput. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const bottomSheetInputRef = useRef<any>(null); - - const { sheetRef, handleSheetChange, handleSheetDismiss } = useIsolatedBottomSheetVisibility({ - visible: open, - isEnabled: showBottomSheet, - onClose: handleClose, - }); - - // Focus the bottom sheet input when the sheet opens on mobile - useEffect(() => { - if (showBottomSheet && open) { - const id = setTimeout(() => bottomSheetInputRef.current?.focus(), 300); - return () => clearTimeout(id); - } - }, [showBottomSheet, open]); - - const renderBackdrop = useCallback( - (props: React.ComponentProps<typeof BottomSheetBackdrop>) => ( - <BottomSheetBackdrop {...props} disappearsOnIndex={-1} appearsOnIndex={0} opacity={0.45} /> - ), - [], - ); - - // Scroll active row into view - useEffect(() => { - if (!open) return; - - if (isWeb) { - const row = rowRefs.current.get(activeIndex); - if (!row || typeof document === "undefined") return; - const scrollNode = - ( - resultsRef.current as - | (ScrollView & { - getScrollableNode?: () => HTMLElement | null; - }) - | null - )?.getScrollableNode?.() ?? null; - const rowEl = row as unknown as HTMLElement; - - if (!scrollNode) { - rowEl.scrollIntoView?.({ block: "nearest" }); - return; - } - - const rowTop = rowEl.offsetTop; - const rowBottom = rowTop + rowEl.offsetHeight; - const visibleTop = scrollNode.scrollTop; - const visibleBottom = visibleTop + scrollNode.clientHeight; - - if (rowTop < visibleTop) { - scrollNode.scrollTop = rowTop; - return; - } - - if (rowBottom > visibleBottom) { - scrollNode.scrollTop = rowBottom - scrollNode.clientHeight; - } - return; - } - - // Native: use onLayout-measured positions - const layout = rowLayouts.current.get(activeIndex); - if (!layout || !resultsRef.current) return; - - const rowTop = layout.y; - const rowBottom = rowTop + layout.height; - const visibleTop = nativeScrollY.current; - const visibleBottom = visibleTop + nativeViewHeight.current; - - if (rowTop < visibleTop) { - resultsRef.current.scrollTo?.({ y: rowTop, animated: true }); - } else if (rowBottom > visibleBottom) { - resultsRef.current.scrollTo?.({ - y: rowBottom - nativeViewHeight.current, - animated: true, - }); - } - }, [activeIndex, open]); - - const handleRowLayout = useCallback( - (rowIndex: number) => (event: { nativeEvent: { layout: { y: number; height: number } } }) => { - rowLayouts.current.set(rowIndex, { - y: event.nativeEvent.layout.y, - height: event.nativeEvent.layout.height, - }); - }, - [], - ); - - const actionItems = useMemo(() => items.filter((item) => item.kind === "action"), [items]); - const workspaceItems = useMemo(() => items.filter((item) => item.kind === "workspace"), [items]); - const agentItems = useMemo(() => items.filter((item) => item.kind === "agent"), [items]); - - const panelStyle = useMemo( - () => [ - styles.panel, - { borderColor: theme.colors.border, backgroundColor: theme.colors.surface0 }, - ], - [theme.colors.border, theme.colors.surface0], - ); - const headerStyle = useMemo( - () => [styles.header, { borderBottomColor: theme.colors.border }], - [theme.colors.border], - ); - const inputStyle = useMemo( - () => [styles.input, { color: theme.colors.foreground }], - [theme.colors.foreground], - ); - const emptyTextStyle = useMemo( - () => [styles.emptyText, { color: theme.colors.foregroundMuted }], - [theme.colors.foregroundMuted], - ); - const sectionLabelStyle = useMemo( - () => [styles.sectionLabel, { color: theme.colors.foregroundMuted }], - [theme.colors.foregroundMuted], - ); - const sectionDividerStyle = useMemo( - () => [styles.sectionDivider, { backgroundColor: theme.colors.border }], - [theme.colors.border], - ); - const sheetBackgroundStyle = useMemo( - () => ({ backgroundColor: theme.colors.surface0 }), - [theme.colors.surface0], - ); - const sheetHandleStyle = useMemo( - () => ({ backgroundColor: theme.colors.palette.zinc[600] }), - [theme.colors.palette.zinc], - ); - - const handleKeyPress = useCallback( - ({ nativeEvent: { key } }: { nativeEvent: { key: string } }) => { - handleKeyEvent(key); - }, - [handleKeyEvent], - ); - - const handleSubmitEditing = useCallback(() => { - handleKeyEvent("Enter"); - }, [handleKeyEvent]); - - const snapPoints = useMemo(() => ["60%", "90%"], []); - - const resultList = - items.length === 0 ? ( - <Text style={emptyTextStyle}>{t("shell.commandCenter.noMatches")}</Text> - ) : ( - <> - {actionItems.length > 0 ? ( - <> - <Text style={sectionLabelStyle}>{t("shell.commandCenter.actions")}</Text> - {actionItems.map((item, index) => ( - <CommandCenterActionRow - key={`action:${item.id}`} - item={item} - rowIndex={index} - active={index === activeIndex} - rowRefs={rowRefs} - onLayout={handleRowLayout(index)} - onSelect={handleSelectItem} - /> - ))} - </> - ) : null} - - {workspaceItems.length > 0 ? ( - <WorkspaceItemsSection - workspaceItems={workspaceItems} - startIndex={actionItems.length} - activeIndex={activeIndex} - rowRefs={rowRefs} - onRowLayout={handleRowLayout} - onSelect={handleSelectItem} - sectionDividerStyle={sectionDividerStyle} - sectionLabelStyle={sectionLabelStyle} - /> - ) : null} - - {agentItems.length > 0 ? ( - <AgentItemsSection - agentItems={agentItems} - startIndex={actionItems.length + workspaceItems.length} - activeIndex={activeIndex} - rowRefs={rowRefs} - onRowLayout={handleRowLayout} - onSelect={handleSelectItem} - sectionDividerStyle={sectionDividerStyle} - sectionLabelStyle={sectionLabelStyle} - /> - ) : null} - </> - ); - - // Mobile: bottom sheet - if (showBottomSheet) { - return ( - <IsolatedBottomSheetModal - ref={sheetRef} - snapPoints={snapPoints} - index={0} - enableDynamicSizing={false} - onChange={handleSheetChange} - onDismiss={handleSheetDismiss} - backdropComponent={renderBackdrop} - enablePanDownToClose - backgroundStyle={sheetBackgroundStyle} - handleIndicatorStyle={sheetHandleStyle} - keyboardBehavior="extend" - keyboardBlurBehavior="restore" - accessible={false} - > - <View style={styles.bottomSheetHeader}> - <ThemedBottomSheetTextInput - testID="command-center-input" - ref={bottomSheetInputRef as unknown as React.Ref<never>} - value={query} - onChangeText={setQuery} - onKeyPress={handleKeyPress} - onSubmitEditing={handleSubmitEditing} - placeholder={t("shell.commandCenter.placeholder")} - style={inputStyle} - autoCapitalize="none" - autoCorrect={false} - autoFocus - /> - </View> - <BottomSheetScrollView - contentContainerStyle={styles.resultsContent} - keyboardShouldPersistTaps="always" - showsVerticalScrollIndicator={false} - > - {resultList} - </BottomSheetScrollView> - </IsolatedBottomSheetModal> - ); - } - - if (!open) return null; - - // Desktop web: centered overlay panel - return ( - <Modal visible={open} transparent animationType="fade" onRequestClose={handleClose}> - <View style={styles.overlay}> - <Pressable style={styles.backdrop} onPress={handleClose} /> - - <View testID="command-center-panel" style={panelStyle}> - <View style={headerStyle}> - <TextInput - testID="command-center-input" - ref={inputRef} - value={query} - onChangeText={setQuery} - placeholder={t("shell.commandCenter.placeholder")} - placeholderTextColor={theme.colors.foregroundMuted} - style={inputStyle} - autoCapitalize="none" - autoCorrect={false} - autoFocus - /> - </View> - - <ScrollView - ref={resultsRef} - style={styles.results} - contentContainerStyle={styles.resultsContent} - keyboardShouldPersistTaps="always" - showsVerticalScrollIndicator={false} - > - {resultList} - </ScrollView> - </View> - </View> - </Modal> - ); -} - -const styles = StyleSheet.create((theme) => ({ - bottomSheetHeader: { - paddingHorizontal: theme.spacing[4], - paddingVertical: theme.spacing[3], - borderBottomWidth: 1, - borderBottomColor: theme.colors.border, - }, - overlay: { - flex: 1, - justifyContent: "flex-start", - alignItems: "center", - paddingTop: theme.spacing[12], - }, - backdrop: { - ...StyleSheet.absoluteFillObject, - backgroundColor: "rgba(0, 0, 0, 0.5)", - }, - panel: { - width: 640, - maxWidth: "92%", - maxHeight: "80%", - borderWidth: 1, - borderRadius: theme.borderRadius.lg, - overflow: "hidden", - ...theme.shadow.lg, - }, - header: { - paddingHorizontal: theme.spacing[4], - paddingVertical: theme.spacing[3], - borderBottomWidth: 1, - }, - input: { - fontSize: theme.fontSize.base, - paddingVertical: theme.spacing[1], - outlineStyle: "none", - } as object, - results: { - flexGrow: 0, - }, - resultsContent: { - paddingVertical: theme.spacing[2], - }, - sectionLabel: { - paddingHorizontal: theme.spacing[4], - paddingTop: 0, - paddingBottom: theme.spacing[2], - fontSize: theme.fontSize.xs, - }, - sectionDivider: { - height: 1, - marginTop: theme.spacing[2], - marginBottom: theme.spacing[2], - }, - row: { - paddingHorizontal: theme.spacing[4], - paddingVertical: theme.spacing[2], - }, - rowContent: { - flexDirection: "row", - alignItems: "center", - justifyContent: "space-between", - gap: theme.spacing[3], - }, - rowMain: { - flex: 1, - minWidth: 0, - flexDirection: "row", - alignItems: "flex-start", - gap: theme.spacing[3], - }, - iconSlot: { - width: 16, - height: 20, - alignItems: "center", - justifyContent: "center", - }, - textContent: { - flex: 1, - minWidth: 0, - gap: 2, - }, - rowShortcut: { - marginLeft: theme.spacing[2], - flexShrink: 0, - }, - title: { - fontSize: theme.fontSize.sm, - fontWeight: "400", - lineHeight: 20, - color: theme.colors.foreground, - }, - subtitle: { - fontSize: theme.fontSize.xs, - lineHeight: 18, - color: theme.colors.foregroundMuted, - }, - emptyText: { - paddingHorizontal: theme.spacing[4], - paddingVertical: theme.spacing[4], - fontSize: theme.fontSize.base, - }, -})); diff --git a/packages/app/src/composer/agent-controls/index.tsx b/packages/app/src/composer/agent-controls/index.tsx index 6e4024e39..a7ca3f624 100644 --- a/packages/app/src/composer/agent-controls/index.tsx +++ b/packages/app/src/composer/agent-controls/index.tsx @@ -62,6 +62,9 @@ import { useIsCompactFormFactor } from "@/constants/layout"; import { useToast } from "@/contexts/toast-context"; import { toErrorMessage } from "@/utils/error-messages"; import { showProviderNoticeToast } from "@/utils/provider-notice-toast"; +import { useCommandCenterActions } from "@/command-center/provider"; +import { buildModelChoiceContributions } from "@/command-center/model-contributions"; +import { getCommandCenterProviderIcon } from "@/command-center/provider-icon"; interface AgentControlOption { id: string; @@ -130,6 +133,7 @@ export interface DraftAgentControlsProps { interface AgentControlsProps { agentId: string; serverId: string; + isPaneFocused: boolean; onDropdownClose?: () => void; isCompactLayout?: boolean; } @@ -1362,9 +1366,11 @@ function ThinkingComboboxOption({ export const AgentControls = memo(function AgentControls({ agentId, serverId, + isPaneFocused, onDropdownClose, isCompactLayout, }: AgentControlsProps) { + const { t } = useTranslation(); const { preferences, updatePreferences } = useFormPreferences(); const agent = useSessionStore( useShallow((state) => selectAgentControlsSlice(state, serverId, agentId)), @@ -1436,29 +1442,49 @@ export const AgentControls = memo(function AgentControls({ const activeModelId = modelSelection.activeModelId; const handleSelectModel = useCallback( - (modelId: string) => { + async (modelId: string) => { if (!client || !agentProvider) { return; } - void updatePreferences((current) => - mergeProviderPreferences({ - preferences: current, - provider: agentProvider, - updates: { - model: modelId, - }, - }), - ).catch((error) => { - console.warn("[AgentControls] persist model preference failed", error); - }); - void client.setAgentModel(agentId, modelId).catch((error) => { - console.warn("[AgentControls] setAgentModel failed", error); + try { + await client.setAgentModel(agentId, modelId); + await updatePreferences((current) => + mergeProviderPreferences({ + preferences: current, + provider: agentProvider, + updates: { + model: modelId, + }, + }), + ); + } catch (error) { + console.warn("[AgentControls] setAgentModel or persist preference failed", error); toast.error(toErrorMessage(error)); - }); + } }, [agentId, agentProvider, client, toast, updatePreferences], ); + const commandCenterModelActions = useMemo( + () => + buildModelChoiceContributions({ + serverId, + providers: agentModelSelectorProviders, + selectedProvider: agentProvider ?? null, + selectedModelId: activeModelId, + groupLabel: t("shell.commandCenter.modelGroupLabel"), + searchKeywords: t("shell.commandCenter.modelSearchKeywords"), + getIcon: getCommandCenterProviderIcon, + select: (_provider, modelId) => handleSelectModel(modelId), + }), + [activeModelId, agentModelSelectorProviders, agentProvider, handleSelectModel, serverId, t], + ); + useCommandCenterActions({ + sourceId: `agent:${serverId}:${agentId}`, + enabled: isPaneFocused && Boolean(client), + actions: commandCenterModelActions, + }); + const handleToggleFavoriteModel = useCallback( (provider: string, modelId: string) => { void updatePreferences((current) => diff --git a/packages/app/src/composer/draft/workspace-tab.tsx b/packages/app/src/composer/draft/workspace-tab.tsx index 4398448cc..3ac357fb8 100644 --- a/packages/app/src/composer/draft/workspace-tab.tsx +++ b/packages/app/src/composer/draft/workspace-tab.tsx @@ -24,6 +24,9 @@ import { useCreateFlowStore } from "@/stores/create-flow-store"; import type { Agent } from "@/stores/session-store"; import { useWorkspaceFields } from "@/stores/session-store-hooks"; import { useWorkspaceDraftSubmissionStore } from "@/stores/workspace-draft-submission-store"; +import { useCommandCenterActions } from "@/command-center/provider"; +import { buildModelChoiceContributions } from "@/command-center/model-contributions"; +import { getCommandCenterProviderIcon } from "@/command-center/provider-icon"; import { encodeImages } from "@/utils/encode-images"; import type { WorkspaceFileOpenRequest } from "@/workspace/file-open"; import { shouldAutoFocusWorkspaceDraftComposer } from "@/screens/workspace/workspace-draft-pane-focus"; @@ -375,6 +378,28 @@ export function WorkspaceDraftAgentTab({ if (!composerState) { throw new Error("Workspace draft composer state is required"); } + + const draftModelActions = useMemo( + () => + buildModelChoiceContributions({ + serverId, + providers: composerState.modelSelectorProviders, + selectedProvider: composerState.selectedProvider, + selectedModelId: composerState.effectiveModelId || null, + groupLabel: t("shell.commandCenter.modelGroupLabel"), + searchKeywords: t("shell.commandCenter.modelSearchKeywords"), + getIcon: getCommandCenterProviderIcon, + select: composerState.setProviderAndModelFromUser, + }), + [ + composerState.effectiveModelId, + composerState.modelSelectorProviders, + composerState.selectedProvider, + composerState.setProviderAndModelFromUser, + serverId, + t, + ], + ); const clearDraftInput = draftInput.clear; const setDraftText = draftInput.setText; const setDraftAttachments = draftInput.setAttachments; @@ -517,6 +542,11 @@ export function WorkspaceDraftAgentTab({ onCreated(result); }, }); + useCommandCenterActions({ + sourceId: `draft:${serverId}:${tabId}`, + enabled: isPaneFocused && !isSubmitting, + actions: draftModelActions, + }); const isReadyForPendingAutoSubmit = Boolean( pendingAutoSubmit && diff --git a/packages/app/src/composer/index.tsx b/packages/app/src/composer/index.tsx index d214d794b..6faa7a5e5 100644 --- a/packages/app/src/composer/index.tsx +++ b/packages/app/src/composer/index.tsx @@ -258,10 +258,11 @@ interface RenderLeftContentArgs { serverId: string; focusInput: () => void; isCompactLayout: boolean; + isPaneFocused: boolean; } function renderLeftContent(args: RenderLeftContentArgs): ReactElement { - const { agentControls, agentId, serverId, focusInput, isCompactLayout } = args; + const { agentControls, agentId, serverId, focusInput, isCompactLayout, isPaneFocused } = args; if (resolveAgentControlsMode(agentControls) === "draft" && agentControls) { return <DraftAgentControls {...agentControls} isCompactLayout={isCompactLayout} />; } @@ -269,6 +270,7 @@ function renderLeftContent(args: RenderLeftContentArgs): ReactElement { <AgentControls agentId={agentId} serverId={serverId} + isPaneFocused={isPaneFocused} onDropdownClose={focusInput} isCompactLayout={isCompactLayout} /> @@ -1845,8 +1847,9 @@ export function Composer({ serverId, focusInput, isCompactLayout, + isPaneFocused, }), - [agentControls, agentId, focusInput, isCompactLayout, serverId], + [agentControls, agentId, focusInput, isCompactLayout, isPaneFocused, serverId], ); const handleAttachButtonRef = useCallback((node: View | null) => { diff --git a/packages/app/src/hooks/use-command-center.ts b/packages/app/src/hooks/use-command-center.ts deleted file mode 100644 index 682c254ba..000000000 --- a/packages/app/src/hooks/use-command-center.ts +++ /dev/null @@ -1,482 +0,0 @@ -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import type { TextInput } from "react-native"; -import { router, type Href } from "expo-router"; -import { useTranslation } from "react-i18next"; -import { useKeyboardShortcutsStore } from "@/stores/keyboard-shortcuts-store"; -import { keyboardActionDispatcher } from "@/keyboard/keyboard-action-dispatcher"; -import { useAggregatedAgents, type AggregatedAgent } from "@/hooks/use-aggregated-agents"; -import { useOpenAddProject } from "@/hooks/use-open-add-project"; -import { - clearCommandCenterFocusRestoreElement, - takeCommandCenterFocusRestoreElement, -} from "@/utils/command-center-focus-restore"; -import { buildOpenProjectRoute, buildSettingsRoute } from "@/utils/host-routes"; -import type { ShortcutKey } from "@/utils/format-shortcut"; -import { chordStringToShortcutKeys } from "@/keyboard/shortcut-string"; -import { getBindingIdForAction, getDefaultKeysForAction } from "@/keyboard/keyboard-shortcuts"; -import { useKeyboardShortcutOverrides } from "@/hooks/use-keyboard-shortcut-overrides"; -import { getShortcutOs } from "@/utils/shortcut-platform"; -import { getIsElectronRuntime } from "@/constants/layout"; -import { navigateToAgent } from "@/utils/navigate-to-agent"; -import { focusWithRetries } from "@/utils/web-focus"; -import { isWeb } from "@/constants/platform"; -import { useProjects } from "@/hooks/use-projects"; -import { useHosts } from "@/runtime/host-runtime"; -import { navigateToWorkspace } from "@/stores/navigation-active-workspace-store"; -import { formatTimeAgo } from "@/utils/time"; -import { shortenPath } from "@/utils/shorten-path"; - -const EMPTY_ACTION_ITEMS: CommandCenterActionItem[] = []; -const EMPTY_WORKSPACE_ITEMS: CommandCenterWorkspaceItem[] = []; -const EMPTY_AGENT_ITEMS: CommandCenterAgentItem[] = []; -const EMPTY_COMMAND_CENTER_ITEMS: CommandCenterItem[] = []; - -function buildSearchText(...fields: string[]): string { - return fields.join(" ").toLowerCase(); -} - -function matchesQuery(searchText: string, query: string): boolean { - const normalized = query.trim().toLowerCase(); - return !normalized || searchText.includes(normalized); -} - -function sortAgents(left: AggregatedAgent, right: AggregatedAgent): number { - const leftNeedsInput = (left.pendingPermissionCount ?? 0) > 0 ? 1 : 0; - const rightNeedsInput = (right.pendingPermissionCount ?? 0) > 0 ? 1 : 0; - if (leftNeedsInput !== rightNeedsInput) return rightNeedsInput - leftNeedsInput; - - const leftAttention = left.requiresAttention ? 1 : 0; - const rightAttention = right.requiresAttention ? 1 : 0; - if (leftAttention !== rightAttention) return rightAttention - leftAttention; - - const leftRunning = left.status === "running" ? 1 : 0; - const rightRunning = right.status === "running" ? 1 : 0; - if (leftRunning !== rightRunning) return rightRunning - leftRunning; - - return right.lastActivityAt.getTime() - left.lastActivityAt.getTime(); -} - -interface CommandCenterActionDefinition { - id: string; - titleKey: - | "shell.commandCenter.addProject" - | "shell.commandCenter.home" - | "sidebar.actions.settings"; - icon?: "plus" | "settings" | "home"; - actionId?: string; - keywords: string[]; - routeKind: "settings" | "home" | "none"; -} - -const COMMAND_CENTER_ACTIONS: readonly CommandCenterActionDefinition[] = [ - { - id: "new-agent", - titleKey: "shell.commandCenter.addProject", - icon: "plus", - actionId: "new-agent", - keywords: ["open", "project", "folder", "workspace", "repo"], - routeKind: "none", - }, - { - id: "home", - titleKey: "shell.commandCenter.home", - icon: "home", - keywords: ["home", "start", "import", "session", "pair", "device", "providers"], - routeKind: "home", - }, - { - id: "settings", - titleKey: "sidebar.actions.settings", - icon: "settings", - keywords: ["settings", "preferences", "config", "configuration"], - routeKind: "settings", - }, -]; - -export interface CommandCenterActionItem { - kind: "action"; - id: string; - title: string; - icon?: "plus" | "settings" | "home"; - route?: Href; - shortcutKeys?: ShortcutKey[][]; - searchText: string; -} - -export interface CommandCenterWorkspaceItem { - kind: "workspace"; - serverId: string; - workspaceId: string; - title: string; - subtitle: string; - searchText: string; -} - -export interface CommandCenterAgentItem { - kind: "agent"; - agent: AggregatedAgent; - title: string; - subtitle: string; - searchText: string; -} - -export type CommandCenterItem = - | CommandCenterActionItem - | CommandCenterWorkspaceItem - | CommandCenterAgentItem; - -function resolveActionShortcutKeys( - actionId: string | undefined, - overrides: Record<string, string>, -): ShortcutKey[][] | undefined { - if (!actionId) return undefined; - const isMac = getShortcutOs() === "mac"; - const isDesktopApp = getIsElectronRuntime(); - const platform = { isMac, isDesktop: isDesktopApp }; - const bindingId = getBindingIdForAction(actionId, platform); - if (!bindingId) return undefined; - const override = overrides[bindingId]; - if (override) return chordStringToShortcutKeys(override); - const defaultKeys = getDefaultKeysForAction(actionId, platform); - return defaultKeys ? [defaultKeys] : undefined; -} - -export function useCommandCenter() { - const { t } = useTranslation(); - const { overrides } = useKeyboardShortcutOverrides(); - const open = useKeyboardShortcutsStore((s) => s.commandCenterOpen); - const setOpen = useKeyboardShortcutsStore((s) => s.setCommandCenterOpen); - const openAddProject = useOpenAddProject(); - const inputRef = useRef<TextInput>(null); - const didNavigateRef = useRef(false); - const prevOpenRef = useRef(open); - const activeIndexRef = useRef(0); - const itemsRef = useRef<CommandCenterItem[]>([]); - const handleCloseRef = useRef<() => void>(() => undefined); - const handleSelectItemRef = useRef<(item: CommandCenterItem) => void>(() => undefined); - const [query, setQuery] = useState(""); - const [activeIndex, setActiveIndex] = useState(0); - - const { agents } = useAggregatedAgents(); - const { projects } = useProjects({ enabled: open }); - const hosts = useHosts(); - const showAgentHost = hosts.length > 1; - - const allWorkspaceItems = useMemo(() => { - const results: CommandCenterWorkspaceItem[] = []; - for (const project of projects) { - for (const host of project.hosts) { - for (const workspace of host.workspaces) { - if (workspace.archivingAt) continue; - const title = workspace.title ?? workspace.name; - const subtitle = workspace.currentBranch - ? `${host.serverName} · ${workspace.currentBranch}` - : host.serverName; - results.push({ - kind: "workspace", - serverId: host.serverId, - workspaceId: workspace.id, - title, - subtitle, - searchText: buildSearchText(title, subtitle), - }); - } - } - } - results.sort((left, right) => { - const titleDelta = left.title.localeCompare(right.title, undefined, { - numeric: true, - sensitivity: "base", - }); - if (titleDelta !== 0) return titleDelta; - const hostDelta = left.subtitle.localeCompare(right.subtitle, undefined, { - numeric: true, - sensitivity: "base", - }); - if (hostDelta !== 0) return hostDelta; - return `${left.serverId}:${left.workspaceId}`.localeCompare( - `${right.serverId}:${right.workspaceId}`, - ); - }); - return results; - }, [projects]); - - const workspaceTitleByKey = useMemo( - () => - new Map( - allWorkspaceItems.map((workspace) => [ - `${workspace.serverId}:${workspace.workspaceId}`, - workspace.title, - ]), - ), - [allWorkspaceItems], - ); - - const workspaceResults = useMemo(() => { - if (!open || allWorkspaceItems.length === 0) { - return EMPTY_WORKSPACE_ITEMS; - } - return allWorkspaceItems.filter((workspace) => matchesQuery(workspace.searchText, query)); - }, [allWorkspaceItems, open, query]); - - const agentResults = useMemo(() => { - if (!open || agents.length === 0) { - return EMPTY_AGENT_ITEMS; - } - const items = agents.map<CommandCenterAgentItem>((agent) => { - const title = agent.title || t("shell.commandCenter.newAgent"); - const workspaceTitle = agent.workspaceId - ? workspaceTitleByKey.get(`${agent.serverId}:${agent.workspaceId}`) - : undefined; - const location = workspaceTitle ?? shortenPath(agent.cwd); - const subtitle = [ - showAgentHost ? agent.serverLabel : null, - location, - formatTimeAgo(agent.lastActivityAt), - ] - .filter((part): part is string => Boolean(part)) - .join(" · "); - return { - kind: "agent", - agent, - title, - subtitle, - searchText: buildSearchText(title, subtitle, agent.cwd), - }; - }); - const filtered = items.filter((item) => matchesQuery(item.searchText, query)); - filtered.sort((left, right) => sortAgents(left.agent, right.agent)); - return filtered; - }, [agents, open, query, showAgentHost, t, workspaceTitleByKey]); - - const settingsRoute = useMemo<Href>(() => { - return buildSettingsRoute(); - }, []); - - const homeRoute = useMemo<Href>(() => buildOpenProjectRoute() as Href, []); - - const actionItems = useMemo(() => { - if (!open) { - return EMPTY_ACTION_ITEMS; - } - return COMMAND_CENTER_ACTIONS.filter( - (action) => action.routeKind !== "home" || Boolean(homeRoute), - ) - .map<CommandCenterActionItem>((action) => { - let route: Href | undefined; - if (action.routeKind === "settings") route = settingsRoute; - else if (action.routeKind === "home") route = homeRoute; - const title = t(action.titleKey); - return { - kind: "action", - id: action.id, - title, - icon: action.icon, - route, - shortcutKeys: resolveActionShortcutKeys(action.actionId, overrides), - searchText: buildSearchText(title, ...action.keywords), - }; - }) - .filter((action) => matchesQuery(action.searchText, query)); - }, [open, query, settingsRoute, homeRoute, overrides, t]); - - const items = useMemo(() => { - if (!open) { - return EMPTY_COMMAND_CENTER_ITEMS; - } - return [...actionItems, ...workspaceResults, ...agentResults]; - }, [actionItems, workspaceResults, agentResults, open]); - - const handleClose = useCallback(() => { - setOpen(false); - }, [setOpen]); - - const handleSelectAgent = useCallback( - (agent: AggregatedAgent) => { - didNavigateRef.current = true; - - // Don't restore focus back to the prior element after we navigate. - clearCommandCenterFocusRestoreElement(); - setOpen(false); - navigateToAgent({ - serverId: agent.serverId, - agentId: agent.id, - }); - }, - [setOpen], - ); - - const handleSelectWorkspace = useCallback( - (workspace: CommandCenterWorkspaceItem) => { - didNavigateRef.current = true; - clearCommandCenterFocusRestoreElement(); - setOpen(false); - navigateToWorkspace({ - serverId: workspace.serverId, - workspaceId: workspace.workspaceId, - }); - }, - [setOpen], - ); - - const handleSelectAction = useCallback( - (action: CommandCenterActionItem) => { - clearCommandCenterFocusRestoreElement(); - setOpen(false); - if (action.id === "new-agent") { - openAddProject(); - return; - } - if (!action.route) { - return; - } - didNavigateRef.current = true; - router.push(action.route); - }, - [openAddProject, setOpen], - ); - - const handleSelectItem = useCallback( - (item: CommandCenterItem) => { - if (item.kind === "action") { - handleSelectAction(item); - return; - } - if (item.kind === "workspace") { - handleSelectWorkspace(item); - return; - } - handleSelectAgent(item.agent); - }, - [handleSelectAction, handleSelectAgent, handleSelectWorkspace], - ); - - useEffect(() => { - activeIndexRef.current = activeIndex; - }, [activeIndex]); - - useEffect(() => { - itemsRef.current = items; - }, [items]); - - useEffect(() => { - handleCloseRef.current = handleClose; - }, [handleClose]); - - useEffect(() => { - handleSelectItemRef.current = handleSelectItem; - }, [handleSelectItem]); - - useEffect(() => { - const prevOpen = prevOpenRef.current; - prevOpenRef.current = open; - - if (!open) { - setQuery(""); - setActiveIndex(0); - - if (prevOpen && !didNavigateRef.current) { - const el = takeCommandCenterFocusRestoreElement(); - const isFocused = () => - Boolean(el) && typeof document !== "undefined" && document.activeElement === el; - - const cancel = focusWithRetries({ - focus: () => el?.focus(), - isFocused, - onTimeout: () => { - keyboardActionDispatcher.dispatch({ - id: "message-input.focus", - scope: "message-input", - }); - }, - }); - return cancel; - } - - return; - } - - didNavigateRef.current = false; - - const id = setTimeout(() => { - inputRef.current?.focus(); - }, 0); - return () => clearTimeout(id); - }, [open]); - - useEffect(() => { - if (!open) return; - if (activeIndex >= items.length) { - setActiveIndex(items.length > 0 ? items.length - 1 : 0); - } - }, [activeIndex, items.length, open]); - - const handleKeyEvent = useCallback( - (key: string): boolean => { - if (!open) return false; - const currentItems = itemsRef.current; - - if (key === "Escape") { - handleCloseRef.current(); - return true; - } - - if (key === "Enter") { - if (currentItems.length === 0) return false; - const index = Math.max(0, Math.min(activeIndexRef.current, currentItems.length - 1)); - handleSelectItemRef.current(currentItems[index]); - return true; - } - - if (key === "ArrowDown" || key === "ArrowUp") { - if (currentItems.length === 0) return false; - setActiveIndex((current) => { - const delta = key === "ArrowDown" ? 1 : -1; - const next = current + delta; - if (next < 0) return currentItems.length - 1; - if (next >= currentItems.length) return 0; - return next; - }); - return true; - } - - return false; - }, - [open], - ); - - useEffect(() => { - if (!open || !isWeb) return; - - const handler = (event: KeyboardEvent) => { - if ( - event.key !== "ArrowDown" && - event.key !== "ArrowUp" && - event.key !== "Enter" && - event.key !== "Escape" - ) { - return; - } - if (handleKeyEvent(event.key)) { - event.preventDefault(); - } - }; - - // react-native-web can stop propagation on key events, so listen in capture phase. - window.addEventListener("keydown", handler, true); - return () => window.removeEventListener("keydown", handler, true); - }, [open, handleKeyEvent]); - - return { - open, - inputRef, - query, - setQuery, - activeIndex, - setActiveIndex, - items, - handleClose, - handleSelectItem, - handleKeyEvent, - }; -} diff --git a/packages/app/src/i18n/resources.test.ts b/packages/app/src/i18n/resources.test.ts index d412bd8d1..124c6643e 100644 --- a/packages/app/src/i18n/resources.test.ts +++ b/packages/app/src/i18n/resources.test.ts @@ -184,6 +184,10 @@ describe("translation resources", () => { expect(en.shell.commandCenter.newAgent).toBe("New agent"); expect(en.shell.commandCenter.addProject).toBe("Add project"); expect(en.shell.commandCenter.home).toBe("Home"); + expect(en.shell.commandCenter.modelGroupLabel).toBe("Model"); + expect(en.shell.commandCenter.modelSearchKeywords).toBe( + "switch model change model set model select model", + ); }); it("includes composer and agent workflow keys for the Batch 2 migration", () => { diff --git a/packages/app/src/i18n/resources/ar.ts b/packages/app/src/i18n/resources/ar.ts index 9ca62a417..703221824 100644 --- a/packages/app/src/i18n/resources/ar.ts +++ b/packages/app/src/i18n/resources/ar.ts @@ -62,6 +62,8 @@ export const ar: TranslationResources = { newAgent: "وكيل جديد", addProject: "إضافة مشروع", home: "بيت", + modelGroupLabel: "النموذج", + modelSearchKeywords: "تبديل النموذج تغيير النموذج تعيين النموذج اختيار النموذج", }, }, composer: { diff --git a/packages/app/src/i18n/resources/en.ts b/packages/app/src/i18n/resources/en.ts index 147b24beb..acc593ce4 100644 --- a/packages/app/src/i18n/resources/en.ts +++ b/packages/app/src/i18n/resources/en.ts @@ -60,6 +60,8 @@ export const en = { newAgent: "New agent", addProject: "Add project", home: "Home", + modelGroupLabel: "Model", + modelSearchKeywords: "switch model change model set model select model", }, }, composer: { diff --git a/packages/app/src/i18n/resources/es.ts b/packages/app/src/i18n/resources/es.ts index 72a7cef63..73bdc0c7f 100644 --- a/packages/app/src/i18n/resources/es.ts +++ b/packages/app/src/i18n/resources/es.ts @@ -62,6 +62,8 @@ export const es: TranslationResources = { newAgent: "Nuevo agente", addProject: "Agregar proyecto", home: "Hogar", + modelGroupLabel: "Modelo", + modelSearchKeywords: "cambiar modelo modificar modelo establecer modelo seleccionar modelo", }, }, composer: { diff --git a/packages/app/src/i18n/resources/fr.ts b/packages/app/src/i18n/resources/fr.ts index ae70cba91..c3b58ff75 100644 --- a/packages/app/src/i18n/resources/fr.ts +++ b/packages/app/src/i18n/resources/fr.ts @@ -63,6 +63,9 @@ export const fr: TranslationResources = { newAgent: "Nouvel agent", addProject: "Ajouter un projet", home: "Maison", + modelGroupLabel: "Modèle", + modelSearchKeywords: + "changer de modèle modifier le modèle définir le modèle sélectionner le modèle", }, }, composer: { diff --git a/packages/app/src/i18n/resources/ja.ts b/packages/app/src/i18n/resources/ja.ts index 61f878696..b7528bf08 100644 --- a/packages/app/src/i18n/resources/ja.ts +++ b/packages/app/src/i18n/resources/ja.ts @@ -62,6 +62,8 @@ export const ja: TranslationResources = { newAgent: "新しいエージェント", addProject: "プロジェクトを追加", home: "ホーム", + modelGroupLabel: "モデル", + modelSearchKeywords: "モデルを切り替え モデルを変更 モデルを設定 モデルを選択", }, }, composer: { diff --git a/packages/app/src/i18n/resources/pt-BR.ts b/packages/app/src/i18n/resources/pt-BR.ts index 45d07d737..d474e3019 100644 --- a/packages/app/src/i18n/resources/pt-BR.ts +++ b/packages/app/src/i18n/resources/pt-BR.ts @@ -62,6 +62,8 @@ export const ptBR: TranslationResources = { newAgent: "Novo agente", addProject: "Adicionar projeto", home: "Início", + modelGroupLabel: "Modelo", + modelSearchKeywords: "trocar modelo mudar modelo definir modelo selecionar modelo", }, }, composer: { diff --git a/packages/app/src/i18n/resources/ru.ts b/packages/app/src/i18n/resources/ru.ts index f0f2bb207..4ef336f8e 100644 --- a/packages/app/src/i18n/resources/ru.ts +++ b/packages/app/src/i18n/resources/ru.ts @@ -62,6 +62,8 @@ export const ru: TranslationResources = { newAgent: "Новый агент", addProject: "Добавить проект", home: "Дом", + modelGroupLabel: "Модель", + modelSearchKeywords: "сменить модель изменить модель выбрать модель установить модель", }, }, composer: { diff --git a/packages/app/src/i18n/resources/zh-CN.ts b/packages/app/src/i18n/resources/zh-CN.ts index 24650d1b9..347d4585d 100644 --- a/packages/app/src/i18n/resources/zh-CN.ts +++ b/packages/app/src/i18n/resources/zh-CN.ts @@ -62,6 +62,8 @@ export const zhCN: TranslationResources = { newAgent: "新建 Agent", addProject: "添加 project", home: "首页", + modelGroupLabel: "模型", + modelSearchKeywords: "切换模型 更改模型 设置模型 选择模型", }, }, composer: { From c0622a7046651e6b3a9c43718df5f5a09703dd15 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra <boudra.moha@gmail.com> Date: Sat, 18 Jul 2026 17:42:06 +0200 Subject: [PATCH 31/38] feat(website): show real app in homepage hero --- packages/website/public/homepage-hero.png | Bin 0 -> 1325588 bytes .../website/src/components/hero-mockup.tsx | 1016 +---------------- 2 files changed, 13 insertions(+), 1003 deletions(-) create mode 100644 packages/website/public/homepage-hero.png diff --git a/packages/website/public/homepage-hero.png b/packages/website/public/homepage-hero.png new file mode 100644 index 0000000000000000000000000000000000000000..6ab4ada9ff871d6a68d926ab3aaf4ff2146b0ea6 GIT binary patch literal 1325588 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%0DT>-49qD! zedbMS|H^rU(jKTTu|Z}A7#v}8-!d0BB50Y+Lh+Ga0ogKzaFTCC5GEotq4+K_L79lT zx)n%}kO!nH9MSZ&(2e<{NpBspOe9WVnww5Wgc4U|F2Xfk=E4tLvP3*ef}&LE#nQ@m z@jOqn;Br_YWo(!bhk}3z3V@ReNXLdYoDn88TP%(?s0}SNJz-7663(ic@)GCFqdDd# z3P~6v4Mh@4TtoX-QId)RfrCsO+=o_`c!F?^rqDU-6xu*Fskpt!<|uOLl)`R}>$;Li z5htM)pv>^XI9BL!jrv=`6jfED7&fa=buF+ZiJh{?$tZ#LRWd2MCeI+uU->J4<*(O7 z-3AVd6k18~iuzyn&iyIM>x|>s>@KW=!m=Q8afJYq1Q19xmmgxZ@s_HIR--0rMjP*U zR$#IH8`{p;w4G@?Re$PatZAFnTgO=AfQp@Q)W!(AfUsOuZVD{B3qn8V9C&$MtnA<q z?M!*y!L#qam-9a7oadbPd7kh0>Fvez05S;#Imj(GUeZ36nfx6450Za{+;5~PW4^Rm ztynFC>@}FYZ``_pfqr^IVsTM%NwIU^o;_<<ufci(iVhP=a&jVOHE>-W?HxeDI7MYt zYHVu8#*Lj_g@f_;Udui1!tZMJiM<t5G62V2J9iZp3{{3KZrNE`%D2bFgL{I5Nxnkr zHGVDNn1Vw)wrvf!h%F9gEjbO#r*6?dwO1hL`Z2pJ?+Y>MXa9Z<rnti;C0q)|)E_Qc zii=cWD0Lh7CvK`)z5vN74lDk~0dBI{pr?ZZOT&awXl-k)tFN0jZCZ5nR4Nc+@*Cu{ zVE#NnMSJ(`hLDiVQY3j2(l(j(v4n1y8>1d&EehD>s-&Ehl&Cxn`~;8=Gb=UG8{FO1 z)j*C&_(zp^J4jDU{doN+aA1=Ci|mYtoh2{5l*U@XXd1y%w6(S2-{q-?E5M0;FVD&( z;}KbTpfMy{9_fW30v(iV`f=0ru;c+(2b2m7AFONiD6)cW{&W-GJ62sRD@MPeOQ*7d zw1vQQ$RdWs3J*C**M}H^7wF$hTIdEN7FYuzoV*&*Rd`HT>Fi0#INM~RFXM1mF+$_x zsjCwYKHiPCD7|v|<e$F;Y+77YSW;9BTO&sQ<YYS{<<_I^+CWh$-1_c2%*U!G{H5Z6 z5^$o!`9|BtL|YphnxJyk>IT{A-MTvD2BJ42V$!p_n}Sa<c0T*@7IKLl+<%~`kg>BJ z+DCaQnU*8?6fxd0gHnF$%{N8{ox`zh>sCZ92AvJ}!%&@ZrLc_D>W^1~)$GFeez1M( zXZ!Z<=0s<45n0Emzd)QA#*nZP-%RWMqsNbfCEdAw=V7OlJcv*`zW(}aEMWVCJH{ZW zLcEvQUcaxXFq^an6|Vit$_lLfl6dp68q4(_50X@)TlsW}Y3S<eN{ElgKM~YM9Kfi- zJ-L4EdPa@NJ9Xe7X|zae$psOSHb}ABcRlJtm`Zw#3#vqU^{UmR{z3wLI1$cI>MtER zCJqa_S3+VUm#y{GYjniCZ?K0^taX|A>G3F{O1GO1tQ`guNkd~}XJ-dN3?e5h3Xq>< zyBKus((Uz9pzRn(G1eFq8RbozHc&%{tRW2z#VB_~8U~543Nf;in2ck`kK@u-A847O zEya->`e>ql!~}G-x8pz7qzacIe|aJ*pO~0L=b?!SwN?+#P73ZSciEMzsA!HHlgSgH zJ5DuvAfW)7BZNOpc6di9CnbxfQRWq)34LB}qCAeUviA1$nB~T$K=0xD#TAO~g6tPv zL>fp?xmfftXomPUq|%#_DU<wpkg1a9aNXLEKu8=VtmZiI^uVZR5^CH&!wy9%;G~0l z_a8dwC_d!Ko1Mpf&0%KgertyclIVn-**S8?pN!9po^a)AxzQ-mQS}~TadPwWz@f&* z#=@crBptUa^97h!L^>-YosLv?`Lfhn1~nuB%c72++Mv8n0zAd7h!X{$5Vz1~|E9*K zhfflv(lcq+j*gKrh@xf>NKjg5ymkWv1hCLVlYIBEv$&+Nn6<=;Wy^@qz`V#ULf=#X z^5Vry5E^kR2u)Z;*@pSY6oy{qJ;g^$(pLPPFm0mzpWV=e7BOw1j^I#bl_KOOucdxf z<&eA!fj(9&WHBVew9rTe2Y%{24C@2u<8Y#9s<{={AoQWyNa5l=GQLFN@F4u8CIE0H zxLGxX(3yDkgbi_AI1-|fco0qf0`^5GqhT1f5L2U4s6CK77Muk`+NOxFoDvfq6A>Y) z1^*ixO8}y;zW&?Sgp!mLIhk(A5eS<-fjjhnWCem*@RwSxJGbp{XbV&ZU2YzgkBhUZ zHBwY`)T$3x?cBP3-@d&{8<2+>v75u7?Hv*kGQl^`a!XmUK<cxOLeGGj)$|@i>`_<u zmGY}Dca=6rpluqP8=3dDW1xAZfZQe`v&ZKZ_PN@yu1(3Au6V^PDOR&}lF!pYn+j0g zt!Cq7v)9!+KxTxv$jID8Urk$IyH_0id>c1=80e^ZSdlt4!xkRrCvmpG1U!xjkF~rw zX+T1<b8T}MBrPp(`SwKhvF^a54^!61TBgH}KMX(&CtG2dF&tf$Iwb>UQ7A)EP4|0S zT74~fwzne9Q}}8NkIl4ZH+el>eGfzIBqSr8HhFr4X^PPx=8Dh;zF=wt$==@a04Imy z6(94>Ct^}$W?Gi3`ulIMoGUFqUsnFjrOW5;J!r-!gq2YX2ZYZMWt*5_hi_2uy~rxn z)K&6zW^y_W1>O#HQJ5t>lC14xDtZOR@^-^Q0dmm<wo=Ca=<3x^F1_UoKYHiQ_dp+p z0wbd)882ml75hi&>C$WG%dUNM@$%Wb4b_<$*=Cc41BPx2g{7}MIbkMlArm*1PO|E{ z3OqwIlTtCwst17?9&XaXP})i;6TrlHG~-YsMtRLp84Hw=Ts9SDKK#>uMdoHxSoBl@ zrRB2h(o3($*Mmcd2SMQKL72Tfjd#(LB+`39wg^iUlej`OCuol6S!u6+uyozqbLZn; z8dKFX9pXi^C#ThTTo=pEmR&nv`p*j&%T9NByVFuLX}%%B8c>Wr!X;De>GVD7!^81Y zRc$3}FdVl-g6$9RD^abyaD^fu>d%B68#4`5Ymi|10&8YeWYjB}xjC=SAzR_Swq~); zX=&)}Y~vYO9CAmZ$~X%dQX!T71neV%ZD^^xeybdeEOe|;@vDJSTki%om-TW!SLTU$ z3P6(<>GBMzDP<?-t1Pgp8i`wl)YKYf6+FPvwzQd<4UIK^e=nyg1qh!lz1JEY%jv42 z^_N8hP*AHlMz|CL0oHX)=wtvDw;3dKCLJ2shUQuz@6SIC(cdiLme#g=22nO@^<ja? z{&(MAvV756M*Rx|6OM-eM~fyOwqe{Ky;H?nfKgU}Jc$yd=g7SyU}Xw?(B|?q5mCe8 zd{MqTNiQrvn&VHNt5KTDi?N6P4(?9fhhy1CnI6j@c@eIUE)|y@P$#OGJ3cCjA;)o) ztSTdmO{Z1l>Ff{{r6it9){q<Fk(0>@(IV^=r;86mf*ousNJQ~*adf||J_?BO*gqpV zAu>F!-qVVC)?$gICPYmT3kc2+Kq0jMMiZ*o#7pL*?jCU^iOQTDX(m{~7)B1m$JCjN z3Lvl)k3RqrbDz%#!4F?y1g4H*1d9ME?)Cb6d(nC#2R>#Iu1f)ntl&mvQk~$ypV%h0 ziEU#0U)msocxvJS{xcZ``!=R(rV<rP#>N`)!TtLnlOW7Q^#=tea|WUGzxvIuj~qF2 z`}S=>d!Uzg@7{$^OXgP_;EGHNFnci3>KBW0<tc8iP4q#AFs3OLwl5jOGz>nL-zw6v zR8~}A76HJ{F^bPq9l(spy$Hu9la1Hw(<04-{Rg_+aU(!f<l}*SvZ20VhTRS$U*Vxc z2lnnKp`acc!5hL8SJ1^&63a&2L`@9AQFj4f0J)irA^FT?w!H99)~zpe6fT%QA2L!T z?C%x!BmIkkDiIE;YYvGI2n0Bcdm&(?rIJJflM3P{@%w!IN1YKuc6Ls2k#p0A4e1%_ z5T)|sjp;U^QXLGATv>U2^tcBIIZKL*NytRpKTNQ+6sBb4Jf=ndsXy!CA#^{GmvH^j zj_jVDlLMF6Pn^y*tJm;K5ClI`&x=LA1k>B1MS$TjD2r4F9i7xU=xwTRR5#!N%|K%^ z3xQw>)sCS+2MjPA#=j~2Wz#P||D4aAI~6!!!12(#7$dLc=TgT}@)3MEqPBsc_El0+ z{msb60W!K&6P*Yw<D>j-hOzn@w(=6ElO#rDz#@tnFrV;w4{9mDpKit)8g#yqLB~iy zXn=&s2nc<RLSLyNX3)jglIQ}u%c24Yo_LZZ@%D+S7+1YiDj}IL`U3rb`tl@1e6nL0 zFvXIQ5r6iYAFTp5q3;;7^BWKnb?Ea4bRrUU_Q{Z8q@<paGLC0V@%naWEzQ8;9e6Zi zl}`T3%dZqV99uSTCIK5}ViwH!vHVIA1$kiypBSOijU-6^HJ!#m44e}uPh2X!#Mhwl z!46uPLby0FD9MdQI|kzj1;%v{IT*bQBeGF!FKVY_wb9>2A8RaU08@YsFVt}R(t&YZ zpmo7jn}xH%5TeP+?Q)UnkR++YpFhI9*U|A%;|O}{SwY`vK@2*Sf)uX`49E3LIp7x! zItmIFdUzt8L9`1`t-lUM!Xw#llCW_VazX2>Sd-DQ_H-d|LS&-FO9#m$DbWnl!Rht( zhHfX(d<OXQPMtn=>dcwHeD&2IjvhlFl8cv!lhMF6LOipxvT^W#x^WXkrqFjIG|*#y zDHc^fst?sFzL_Np7xP3jX4v!d@=7n4dFnmPq`VyC!@F_gCRix&XZX(f3)v&o!T2OV z;9;wpJ7+Gj|2J>l5U>@)*c6Vuv@(L&OkrVkD~{Dp!5ziuLx}n_`w*X0hWaJJ^8|)7 zcHQo(pZ((Jzy00s0fm$B7?J}rACvShJuRJzg?=e20z`Q0md%U|m)k|-zA<MGDIQ<T z%VR`iy?q^3o|B6f<r!c&AQc>)zoq5=v!UnA&hy@RhxG>7UmA<33$Wt^ZGekKN+hd; zjWZ)XjS(;=Ga^q1E$6Q7+w*7V;*@2spaanNq`*f+${l%)r>I9hXhoCM?LaNqsaJ-6 ziSr%@1sT=ADjGE5Y6-uLiiwo(x{V$Sf*CL+6zB?=@|;X}<Lx|V7=uZ|N+RB>sw!4u zq{KeBZ+~HdV|I2H9h+4Ljf>_Hh?8Muj`AeM<`*YURM*sEI$~no%W8P%j{N-ms3}uf z2lfbU&=W-3s3`XJlN6(ahzAD5s5P@rU(2~nqTco5ov@QWS8wsU9`xL7jD9~eW@)DN zz4X|XulgH0NlA#1vCBOwz3caMM$Jo&S(0g6JKGR$?7UDj;O*ng=f5?qTNj(oV*6X2 zwsB$dvUd{}Ew``vAZ4A+5{o&_9+9>(Wj)CSFEpKL@;3an_6wUOe%Z{`5$4EYqu@Yl z)U2g5RxU_-e|gH<#-93H?blfcb_UunG@i+}=B=2yZeGHoRjC^%8zV}aFMt=&I4X(U z;>iDm&ba=dJSie&MasIR_Em4kze{C9omNHv3`n2Hz?g0r1_2(X__(Ay?thU~48P#L z)+SGLZFOB`YD(t6**o)~D9<~N2asbCj2z1$0p${55l}AW5M>cHF(wm}XwoKiGQH9? z%^#gfr|mzPW~NDJ(sVLuGC60Ov`ICYphk&C6fXop0YPqI5!eOfSdJArcj;%Jhuzr# zSvQ&f(N1{WVb*uw=Xsy!d7tz5eZJqHua8ouK%Na_BaLl!xb(~4x+f=NN1!s8N40h| zx3txzZ_LiwymMnh#^$tb`CA@}3XipR2`Mqn-$j?`eS?hFy5#knfE+SY)wlq(W*7=- zkpHTvh`8W@`!L8lGFd2n10z;PH=7I6O+ol*bscRtn{a>^qzpD0t$gl@+WN|{knkNj z6lCY>kO&sjz)P*|jR=#H6AF@%mb-n+9*C6`#72I6@q+LQ(NHD&`q<>H89TDl^1|<n zG=6jXATH&dHzo%KhT!jz4=hQA!V8CanQ7Z%!1hGI2!6Ax4VQJ9>FVtCJnF|w;bj@d zML{OUYZ@!@xtO1`XG_KobYe-hOWS^}{z`IU#@5W8$%&bpQ?};iJP;e5Ks@UTLFpF+ zk{S{aMq%!{u*3q5Uqiqr3fOZ?+t=U2r?b1gQ`@#W6i~{SHY7JGqqhxBt+nf-*5_qE zNPhhN0?1_JjoKDX<EG@TRBSRWBqdk9W%t@}mGwn)&$sD&Q)R=3p-yc(`Ogs*8@6f< zIhL`)LL>6F?B15OD|=HO+}R6dr-1#i?GFz1l~xqPpv}wPosyW9zF~`+FG-oK6L_fS za<A=M9?agnoy=r!+8z=Vrs=o=agePWp}n)Up{Y70Ve__(oyq2Apx&Ik)kC4+Wmy~T zBK|1^Hf(nKn6ee;o$L$JH)Lm~<;F!P(ZOehh`P6@H;+qA%v5LX;tY0qbqTSoon710 z!L}?n>;9}XHO?A6-4*hn66&~FX}PozUYXBICCO%_Y>QeGLz*2r4V{#qXte3c*|=2f z>+fxDZCJFu2{jxS2YF$ve4GU?IXN@Mg?TVMcr(H`(GhXp%e|Kvgg?YEw-cOW^*T%R z57>`3Czxv{X@wBiC0UPq>E(jDC&4wbjuK;!N%+l~5+)Rr!23-6VB{O$8A7>0bjgJ! z$N_7P^2aj;7$}ce2(=6@@(~@C3;Zb9>m~sZa>XX|B1jgayvh@KE{|P?NGa?;U{5Js zN+9$6JQtb2q$o-w>Of|QE5eFn@yuwP#@>L&=QO%dzW!b<4Y#p_O-&0+AT|rq6@?>T z#F*p<!TX~KsOC_7(sOllVV`3fH%(4c1PlnXa{ENO#+f<KL88V1|0szI_$P|zVrOa; ztVwd>LgLb@NIA$HVM$_iDJ+GhuoV6$f?ZjC<^#i+0fW7}oBKch`4N;FX)UrC`hjhx z1ybU?@T@3K-8wqEPM<xyDmVxWBd6>i-Mg1x5J*IIlhf#+Zs=MJO$}QR4iigcND>&u zzQt3T*{;eHdwY7{c=HWb?J}a6JB`ew91kSJ{ey$dGAMsS&+?Gwa*G@Qfg}7(Sq@4R zAk74I44;UQB*H>NIf=i2-#%DG@4WX8Q>1+l988PQbl~nFg^K3(gwVCKpnV=#TG2lg zx>9$a3PG@9qSwMc!AADZyYK0`bu8A2zAh$a-(!!@v*`y11ylLbR4$nZn5S8aAWH?v z3uMAX>}w7_IMU;~%o4{RQ3R#tp=+>IC)wPboY(&L`lU;kpo3tThEkx4jSEl)*g-L& zXB~LzDVQZhEH5vIL;^8@ce*}4p7PGS-xeZ~>UOLpI8OxW!RkO7o2IQ{choS7+a8Ya zBVURBWPDsazLb9b?6cL?Rd_tI^@7G?h4%%|h629%)?4_>qQ<bubJCwT=%9U&yU>}T z!N8HF4)M~&a>%0f=NYI}evgi3vw!B}qG{Kl>v-z)X&M-Ej!?25dE^m#R88$~X9WY& zwnN>z)yo$YIOkb%=`x%r9iefIjL@x=0Rf)N6?Z1?P-mhluuc1;goZ~bIu>#5i0}FM z`2O<ezo6P*d;PD?EzO9bq0t0`1RqPz3!Khai^AI3+UW|89`<lw*y#qG7VC7ZBqJju zU_^CxcD(n&2hd7*O#oxClXJJJd5VJ;f1;qGSz1;~GYbw5R>i3fJpJ@@zk05!s=B(S z#`cA>6Yd-6Ul+SBBs8Q;r=!itm%^K+!O&UYBhqt)0mw+b!KtD_%1+9!{z`x38R0Nt z#F@uU<nz#;J@@b0g$nVBuTQ|C;vRYp&J<w|*~vf2>IVmLV)xO<ANSnqr5D%^u>2}O z%K2JVxQGM!hCy3aahcXm^WZPsEC3#BarJ|GBEuY&8y5>eJG%mLE10Ib9$bG>H2dQi zaiLcN+4A@O4?koyu+1US*-gOq<Ou@eH1Vtb{$--bA}lo7lful@6yq~k(b()AuF393 zgLLpvfh=I(z#w1N2;#D};C#m|B|a}H&CDD=VF&YeqCN+NUopTja%<a77zP<w2K7UX z%-m`Rav*I+-8{AWiGBMIA3Xw|3RjX17-Qqle)>#iMuy`<hlCa1eg6YSV}@-Dnam^| zoi0jzVHn@8&U@?ax6htGJ2GNmG-jB6`6XY;_myPm8#h|6)zqRBi%4(E#$*Wkg@uKb z9dJ}W;!;`Zll%9_M909G<Vec(y6Zd=Kdd|RcK}l*CMLrC19D?Jo^b$OW1EP`EkfDR zk&&qBB1uix6D`M>kdT0IE6l?0<lf3USvVLZ_(CZd-+}^zf`iR7J5Gz47hoa3@|Rc0 zBlkE65Hb(N#j0Qxb56y{k`;g)q`8@DUwHmET5Z=$e|ni=nG-W;27G$xAnk!7+nw&G zK^f6+K;HI+=bx8`Gt3CM&!oq}NdDvR{{UtD&#%7f;o%~{v&HL)mk4~d@A1cx6X$3Y zvH^&(<MfVck;RyUfEg_W2a{&3F*KBAjP1Gtky4n2m`UzB=Q-YaonQiu8Su76=KuA{ zC%~@r7MxtaGd{s>j!fTF8bLTwSvF}|SCAouOux4vKlKd<y0uMH6ck9~VP%`2znzhv zzKQQfW?>enNc+C|_TR~{%%}7gEOi?AZ|=67A3gEpvBKjVjDQBs%8<ARp8g3h#m<u{ zIIcz+(@+U!rk5l_`8tp?rlbrh*1MNnFZ~w-v+d02#Nd&J6*=MVYkYa7)1#9^MXi(C z5y6SOXS`KEP*CTUw90c`0KHLvyzzFG&T;yn{ea!DUSG4!=}?k?N`!Z`&;WEXObw6C zjIrhM@$l;z?=I^6a%6JY)p=R-aNWty!{IBUAodO4qbK@=aiVUZN~s9;Smu6vYUGUe zc;B6FmwPZV&YbN&LH3o3K*UAyba<lki^_pgXF8_%a0uqgt&$Ey8zhEBd+(**Gw7On zxvubZUG5}pe2yswatvxv-xn1T8y%?{8qq@o!eD4;0AfFKt*)$7*Xksc+vLdB)Le^S zH+MG=D**%pZb?~@AwCV_ou|pf5`yb@?ikKo{3=nE>hFuXhCgYEqgO+6*yO*%Bl_C< ztA^X73rKZR>|x|gNg-GcjswN|K_QFC9|#qm3U4MZO-GAKtkCQS$*%(DY`SsXSp+Vc zT-YWz*ELl$$djn`&hn}Yl=0qseOcIh^eO<i$l;lbh310z1p@X^C#*vZhG9xC3!)zy zsBNr7*OBiF-`3>x6wy^N8=#|cI~bMO&Mi-5iTZ44tsO&I;hV#@!c75bjDqWj`goSF zPXJRJr~Tv&yo@fL=6u;n5L~~N{!9b>D)VFA?37(O%dxT*UMqQ19&QR==J@2;h0;@r z@oBscNj+pafe$%umJelxaId$|)bwO$PdocB=eze#XGYrEhO2EiTi9$$gF35BbY&%I zI!l4)juK8>xLlN^O7r&e_B45clW~!96_*^}5T734A4DUeV$qEs9@P_Lj-q{gxU#l% zLeS|JwoNuH7t4#{)+YL_@a4aK1KqPiMF0&|GZWG4UW6qyH(OfUo4UGhG6fKcUn^=# z*fa$P-M88!LP7`HZZoxx-pB0EPhihRpFoZU6N{uE@X4uUeEfFtr4vb__NXPMw!y)E z%q0?=pe7;OPEJk8)-}(!0%$tlXh_!@u7amhZ%mJJBg*uo>1w5GgoQ?8>M5-_)7)BT zT@`Q=u1nW`zVu{ZKnRJDL$Kjl%lS@UUnRRLbUwKs<|S<v9F)Je={pw+H^isWJZM4& z!w@pR{rX<%%CSXL&$%>-gpgZ`$(bS2Av5;3&z|m{D_8oh35_E2bmWD5STDtL3@js~ z`kp@R(%kg}_CZKnn8V`WrU6DV6fkDt5oN}52v?H+tlBVqgsdlxmmlaK@`BU>F_9oG z)#OAxzKPiU;R!OTzyamp=PXSh!lMWOW{JfQmMyM$mP9Q{LwYIXFB+aQC8;fHB9J=B zBDoP;Yhp^wjbYY|)wvQ6G?NRYA|jJFfpBsg9ow0xa%>@EmO@HiqTnyWnjmj(9;Wbc zZ-0NELd`XI4+ZhY4dX^9qldzkOtGVL7I^|p<iAE2Qgf0SGxCrbe2Kfy%rO2KXM_t1 ziwx^5@ICnz3t$(DM(t637EM=jMj}u#O)^s!&;PIzm$XHf!ctfYOX2?`Fu5SIhe+La zwK~x3jj6=Uo-xzGancroCn23q3vR*1d3zy*6P3z<rlv-nu8Vtw<FWc3`T4lv!Nn1p zhn&A&yLN3#YVygiPg%9=cR!Gynw;`QLBZvU3N9{`6V&x|1E}E5#Z%$g(AXpig~UYh z2{Rvf9!t!+y6Y^#@pOfK9C8Ci5FpBOjBd>JSSOj??s@`=$qs*Jtd%RPtMDeJX>Vu# z!W5pJnFTF?UEBEh*xmLmPFRp}4V9Xjnx2~Sbx{$8lvc&e&70t~T`VaX88tBB%EL$) z4cS04y;3Nq?|GKK(9lo{c<%hUBVQh7;~`fyc%oWpip$E%fZ3_lbz?$eU8D3@Zc0xF z`D<>uK|RRgft%*#sS(^GWfG#g@IDBC0;g*jp%5S2xFKnLV&dRupLcb2kuL~`(a}-L zzyLeSW+d+A<rAk;^$qkD6cm7BVqzxcq{Kwz5N#vM?`YQXba|=@(ir$95}N)><#1~& z!UNO;PVacA`DU?b^7B*1t77psSKrX2*AMZoarza%KAuXzhY!9gkyEneG6EDuMn)EX zRrvYgFI-(*B}M|1l}dKJ`Ll<qud91}?_MY!5Y)f}$S@nZl;jjZCK{W=Dplsdqsa+H zb8|D1c*6R499!brQ>%r_qN}*40#}H;cJAD~X%kQ@D!*jewkK9qUn4&`IXRyfe9_&l z!y-=w;_LC50|!VMA6jUoqF)DRWn<FDlc!F~gJ&#nap?rIb?Q_R)?qu^Laaflb9gFe z&K2V?jW#0+JF{`aMr7ZtJRa$QN9a$w^DE5^awkqedDjsU;W*esrh<sHhvlNvvB)H2 ztX&ta7-Z9cdk-Eubgb|gW?YuHR1{vBXcvwg|Kur3WsoXvold7~Xl%v^UUR(`^a9*s z_x<<NXRhL}(I(J^@zR%<tIpe|uB@yg|FVo=idfzm92y&&A$-iC@@r$L%+kwc62owJ zcLPVE?=-eFWAUeX+carL4EOjr6@C23v7?vED?Ak*W@t7*3Gd1-fy0Tz5Lc_JqNAh3 zR)>{exnkkw8iQ8_C&nje8DRT*=h@xJ;px-G5AS*SvAug6>KkZ6@P=1-uh_L~XJk~Q z^=t=qqG@Z3i;abDTYasDH$^Fyxw>R$ZvpeJs;ZoxzB|i7c=)>o0)%fZfH92V(m^aa z#9AG*b{)>-`e*yeGuRtsalETi$@U-zJ|ZlF$B$2Pb_B)C(9jT$FE9?!cmS>#2OP9| zOSG=7trb**7aA26snN8PVHy<*dr$6vf?)w)aX?huDmhq4b(usUOY;(HZffRO0iJ6a zuHn+r%ZP%3o>_q2f!L;qVn186g!L*FDE^5PCr=j_<K|spv|dq>(cfc6<|u{uINSHU zM~j1qm6VjkyC^t%6c=c;UWRcXz187sXz<oX0;LSMroVgfMY^h5oy&oktCd$7ZLCVj zbQA7jf>5bcmn+H#2M5^n*45S11s>k>5I#z2etrD|=2qbVKnS^$CS82C_|Zr9KK0~} zQH>rN8KFH#tXcEeqmPovcLi)_#O9L$lYJ?H=6cum%n=qIgwN6Tn;n{)H%l&=KyB5z zxv4412&R#agJQSbhIr$_&kiwAk)X{%8TUk!k$%l6-P?Ohay61RB+-7ZT)AQ&K9{|_ zzM=lpga0O3YjLa)H77Q@v|V~cq%8-L*@R0@@<RMFaeV>}@7S^9R5$|{BN7lSKA|0_ zlrj6)H#X3lSjNzJn4v^(GIF4S<1F?jQ~H+>9b%}#G(-Nm*%1_+Ij*+$I&{pS;Gp7j zXUVBtqOktt^aCx8QQC@$+6`vmXu7R0$f}=zXV-t&JM-|Usx*yLOEy4Af)Z$DS2jW1 z0F^+(PP4SYpf*t(pKifc)JLaBTf2K^{+OYi=~mfn6|q4?LD{nsl8}WYB!MiIJwOtY zK$cpvkUgoCHNSh#R6Rvi5;BhZ*pGD+v2yF)bIv{c{oe0=ze=WY7GE53fTl&%v2Vls z$J+_D*xXzl8&Q5!(=1H0!0^mA!-fyLT6vW}aVbEMjV%gwC1-#sQ>Tm{J1#djk1go* z>7+0;I+`uvS$07}es&1#(*FB7q(H(PG6t6~UBZS`I)(5!4BPeV)~2SOCnTiXyz;%- z8&w^prfy@K*53z|%G}stY3LYaqt8MyXg*&F%TPPa+tSu;Hnv%bsq-BHgk(Wbv20UX zdGlpoFF(XWTg|P_T@7tLtssS!Eu}TBRrOs)hM1gu&ez2kHtO2U%@ms-&SaU=6cpKW zcvDwneW!t$LJ%r6^&Q5j%A@{XcYyG<bT!v^8HjhtH}d7B-O_%d{EN<SI<+c~UST^! zdtGGZF(1tkU_zo2$<qU5c$AbCOc*=)ftinB;*U>>A|zp7_hL(gid1W;2CL(<eKTB9 zU21GJfS1Su%dnkX)|3_lXd%3sTGWCV0;{a4xe=vDLZBg+hQ50ncZoYNV{%K}xro;G z7C!e{)Z)u|RoBZ|7s#l+qeapynb!cfM&2>1v8g`0AOlAoOt!ZIdR0ta5wS;`TbfA3 zhOudCjn${)Px3lxe~DHpBr{`DdMw&4G%?^1*K@YIwu<<{!+}42hIE;`!1G#p5L+!( z*Hty%sK=d`&#uiyKLE(o8*A96GKqm&k)8cZ5Gk-#i!f?C@(ydLx|Sv;6@EBaap+J0 ze)byy@Ces;=}G~@GZc~B)i-w%W4%7<{26cW!D3>U52ElgQ_^DrlZbT(8b^4=wvHD2 zo05+w@sh_Q!kLqw&eBQ8zGXz}YuC&5h8mDO9?ib7DJ?G%-G(CB={VqUm4qjh^0sa5 z&63(vTSpIR8a8wo<pG$4ZQ0(@D&;rn6{dPaM9eYxsp>vvW<*Niwt9GYlPT)30^|dZ zjZg{818<8E$^q@{VR$JdZI_;vM3B58LxypNxQqlQq!j=T@WhKv3PXN?N~170TAEs0 zo4MCsrBGU_Qtll*cxaay86h0b+Euo0nU<MAOF2~<MKU$$Q!ge^0@;*OIgxU*M9U4i z-cX&Hd;X5wN8#7l*kmGWuCZqdDtsX)h5FG5g_U;tB<D5SI{o$J)*F(lEH5QlQjk5i zqUwsNQ4f&GfF(jCFtA;U1_+#mPsTCjO}5}t4v-{%d+J*eiv9gZ!ss30KQgx{v(8lO z<SImZ0ReYWIoWw>*J>;I*1dfF1P_}`j6|KVTnJqfpgO*hBT33_DnG)H+G6|6X|;57 zvGeh`gOkcPz<7403(^)aARSD?eHw`qawUJ-ZdCD|l`jOu2vW#dez}(O?Ab-hL^dKv zZslTmFW*T(PlA4=$^HDrKUxi;1c3u3M^WIJA|AC8V3B-8sic^yTBw~e!1&%u8Kim< zIe2NkG|bu7t`Yv8QzqZ#;WZoy3$3TxtRe%28puW5MG~Yav|*Ht*=|x0AE6A!nNG8H zPzU@FQdV;rZ(__2@(C4C)Xe{7%~PY2`)Hy5NR>ai2e&=eKi*}frWXp$WXgR!zk4I= zKHP`<a34Q@bsfa$LPJ9XAA9WiUq8=27YqrfxzPcm*RNkU>%Lh#cke1JDsln{F+_qL z5{M2A?AY-W@GwyMpEhkeCMu9N_!GR^K=1qafBiSYhJh|zy;{LygozxnCcL4K9y{8r zFDeEmxP#&0;lwt>H=p=bwKX*q5iY_rAy2~%`NN<7$cl#KU2b0PlEsTRZF)96Jp&67 z1|9U!);<1sLQ>L&%nK}S0|0caU_$(U_4N?~EHyXZKr?Xi-S<!-0G<Pr(O0x$j-5OX zR`Jq{FOgtbc^S83{(kAj7qNoj)ZeZ6`YD__jHKAn5ShfPh%pX5Psj9mjlqHmO##zh z;Jo>@bv007Pyt=OY^ket{O*Z&KmVKGzyiU-?(cv5qJ_bjkaW4aoID+nhTR6YYQ;)} z!IYDeLyizie!FE0t#c|YRAQF6z)znxeNk|5#>EWyqww2SuUv(t-qIf=rieqDOvXtQ zCsA(~v$CiqB4DBOIb`r)DQjkrT*$u<Km73A*>fpmO>GSZ=#tBqQQX?FZas35_|!vi zT)A>3ws>|Ak_zDGJACVJUjhO8<kP)`;pq{PEJp{7qJ8UywbYeW27`0__zA)DgRWh# zVlzSy{Kcx(1K)he7rNxsl#0sA<;$022hYyQ!Yn>+%s6;{7+xvc0Mns~A^2e~T(Dq1 zO-VdOW4(zQz``CI7Z(v3S>IrSct?mYuBqVZMBGg$=1pE+9u&Rcpr9?Aw=g8J)PpYu z1qTsWF)2Bjamv6vdGgfEduPJQqim(+<?Kj7$_ZzMOk!Ag7@I&+>Z_boA(ht9)HH7F zc-RcEH-MBT-#r-=O2$FqkjM}OAbJtkAngkvL~lvIke;8PzijEUl9EgG8DfjVp<&Ri zef)7m;o!Tl*X^}tuyoTK@0~vVu}2=mw2G+|APS*csr7Noy2a!TDE0D-TPe~f`#!C_ zTG?YkQU6ZF-x@d?Z|WFi$38VKKJJMpHmqB_7K*b@m($+S0Ttw#r$eA-63z|dyV_=J zV@Uq$*=LzpDyu5+fPRSnxqdwm^%-G`RjHhS3bWp5#0DMs=)AVpb`DUb70Z@Mbi7}L zqbDLVeA$vETb|#n0Qr|LQ6YF&5Qvc;vVG?c2rEvubq#2-E*q*MO^@zzZXV2)8(5{m z*YU%EL;SzMp3!jZ)X7)3zD)SBz5Dl3axYKM2kw91nUIYHMvaV$8W<5r&CG+KCB7ZM zec=WEuB@yCkHpUY%O^HWy62v4+qRXKm+?w~|Hpy_3u0no&cvMgRvIB{sx7Y|pRxAj zh9}7sIcPs$-xWV!F>2K4fkPTvEyUakg3J-LjE(_)3R2q8+lHcU{qokAOBLBhGo_<r z7>y>bN3fA;&|qL9s<Yl4B@`1sv{Y0yS#jcGV<E!&`uZ<kxNy<Jg&;kw3#@Cms6Fvh zYtU2vIdf)LR#igt9pUe{U_tQo>C@;K-!8(&0Qd^Z62^lwvgQZQ;}1F&dYG$Mt^V1l z(R}5&ssdZ`fFT=00ALOsK1?^JgV_mW;L};z+2Ci;um5A)Hdv>0a^RLrmoBecw-#r7 z0M~D9;*$gEPIPhx+L$}<g4_lboY@si=!6LqU`8@z2QL38i&t-|XRJOl_tCuk0-`6< zM$)IGAMYPzx*whU2s@C(q$D_z%;Zz2OjTC)s%yLq^I_n*SJzzMv2&NBD+9Uv4Idsj zZ(g0gZr2ANu-IXe)*|XPXD)scWu;}eOdlFV+}U`D@{V1eT@%ak;zf&Ti*0{?hv^iV zc2>z{i<h>vwXuk>*>Dxqt*NV}<Kc0lHyBueh)2k?&zN!Vz(*7Nlk)O1(g(g}lknx? zuMCDdI6Cv@1>SexOps>g!M+@%2*b%JhSj}rVQ_kSIzx<o&H%6&RYXJt9%;Yc^c<8& zuu|4s;$PBrsZ}K3^YU|r{_QPGuEHS(!21_#)-Z3M3=3ruBn0*9)oX~g?5f*5z);Y0 z=FGwMBr57OV!tfq*qZT9V-T@;v1;oL#?Y{E<U<J*{l%d}AjDW}@Na-XjAy{xfBGZ& z8uWE^lG(E#Mn8Z?@$>TynjbV{=1e+Kzge?PL=l+w>5!*sO<i6-;vIPDu^E7fjJ$y` z4SBlXS-!6Wl)9;dUt1s`6=&>Uv9B=KwwUW$<yZWEw{Kx&+0bR+S7nnklW~%6bT#rP zYZ;&Am0gyuy7p?dC?DrNG@f*}x{hjnyGC9qQnSVFGq<&wTV<LQ+F?JS0`xL<>iLS3 zeJZa4?wDJ+SLCG`=#(O`uG0x8M)=*%UQ40h^)nX9rM;!i3YK$I<TFL@oZ4`B9C;pT z6|U#A{VO&?2%rckQJ91|;}C2B0zv)0nPiByCKK;#9c^y8LH#?XiIo*jArOwPFtet% zirWX$iXB}YAS;e}@quu~@bg?BM3z~8=3Dt7oB^cl^+CYuI{SSPdL~d4_u12QbYcQS zRS5{q@fL7IpfcAjjr}r}AZ~K6tRL6hB`!!J&Mng2(P3=2J&4u*6{816nva)Hc0u~J z>I%)kDi*{Si1+5)RbZ%{4bBy!65R=C(6Kx`j_5YMS1e%y>l^ibRxD?gie(fu+bdS# zTsD~;h3y?HR^|u~&)A?hH5eo@&ekF-b|8R){F0kJ*nvaVqV~GlfsLg;qGPLZ`Hki# z=XR9cgZ)9L-nH;j!j<)u$^gKlv8*oLLzaO8H#OJWrw%zQ(WT)Fl8kE)k+)$vX;G{h zH`l6TIc1}?QGq;~b0xc$muQ+bhU*p8xH4C_O03=CP-~Sq8>JN`89B*3G3&B>;rfXx zq6sBUmX;pHGh4ucGA=kLg^sb1H<Ho&1Q@8$-oUjuqn1}HmkOpE2qHa`b&7M!-L_fF zN;(386bq|GV^#B&Z7ul5)|JJ@3bY8SMqYq}g6$_woRW`DA(7T+N3T)~u+kzf;E?6y zUpabIvg}&am`IBvE9`#lAdQR^%U2SNBtglBl*0WMPLhI6QdPx$mciab{D%xR8Eb*- z^oFkb#BB4RZVgVB2zHn~Ra&KZwg*=JVV^H<R!~C^FHcKnCk%SdupF`P<bfyvPJCV- zJc71MfT0@dhVu4UnOk^n8(2v0QcYANJlw4k$&?Y%C4<2&G3q|thx>3JKiDBv9eRC@ zQTO(hWTwmA7060LQsRLxK7V5UhTs3o8vumZzVE#AXGmrdh^VBb1QU`gt*5lK40;N> zTVPs{(g2?b&;^_TV*rf{HgVyRktpviU$XSAH{K*b3%k0BlP3BM9{kyX1DCE`*1)0@ z2PJnb6~^?#N4^Ho3R*B9={!Ouu@nNiA&dsB2-5`tdI$<1e*E#vFKvC{`7OZ<f=vpJ zG7K^h5CT?R8Nr3+R6e`Ywn8fof#`JOEWhgKD=~@%2F?S8!u1}ncHpAWuv0)^6h^|L z&Q>{*GF2xNjERi{X<M^;&F|lQqpYl)Ybh<b*O!M6#V5v71i6qWCM7)l;DgVi&-pL} z%Ht7HQIycpR87@|hJ_I==}&+70|t8P6PYYXfkbX2FGrnNjGoXp$rO7WR7(uRRaI4c z_U_dyp^voksrf2Lr+a4O#^p<w^5y^&Xwt-q(PyGhoeH&zeZleLC#FuG^2*k&!39Cm zJVzs9ZU+3bJ+Bft&5`gyff1xUa_ks)5pb)hxR^Q|ar^DScULPaP~x&*##iJ%clMmO z-+HU4xadFs{7!dwdssvmT*IeBLMBhXhbTz^1HgpPJ3v{aXO_Wetf;D7vS<lvndRl> z5WL`-k^X^$2LYw9z&gbW!tM+>4U+fAx8H(AL}`6}httzUoJG6&yO(ajIWCs9N_V0+ z{(b*`0?WPn$}7di#aKKcArXBoKQA9LUtdB}q$~rr%fC3zB`3pQeCo-kAOQg1es$ys z_3(?;YeGUcLQ0@|eFppR0W@guzWq|x(#fg<Z2ZxlkLl|Fw07<EX;Zmng#QTeKM?xQ z4}MNF@zjw~r{N;3S+nN%Zzw~RN8<v7L%%rmC1^c`V&}?9$EW6lEFvZzp6kewBOx0% zzVW-?(d8JGbbCNwo*FediHfniL%9>AA0bdM11!J0Kl-q$>N;VWw!Zk{3tKj0gEk59 zl75Ju=f#T`q{-eXUk>stEG`ZU59b!bNz%UH4S?UY??VqhsJ1ne%Z>I!b_{(*Rn?M3 zi(raiawQNH{h7~9HJ2`3p<}4T!F(q6)>@dypX}ZH+@|M%&8ThajR<R#CQdqg_UwT# z4l={Y4o=_B&C6qF``W9o((?feq2j>}+IL`oW@Z*0!>Je+^EZ(Z5wJp^+qCJShaRG_ zXdRR}0sKHKNz)!jiY>Pj{D0xX#r+2k{PL+M-+29Zm&>lSx3wc{455~K=5{W2we}F_ z8xIM!RtsPZ^aadGou4>)A|PPYlEq7u>_lKZ1~M}+7#PB{%$D9>%)?WuNhXAxTpc}> z!bm{W)gmD0F)598Iy!3Fl&Py$t$h2<H)(p|RWKIa-LnbtWmn5Jf(9KN6#V3d4WI&N zVq*IegOZC;-&&F104FbBwj7&2dW=X4`wsUdFZu&CQ(HZ#wBm3<&^7kR4j|?GKKl&T zESM!UVnAPdBh=Ws+FBy^;Wr>xH{2$QiVMNH0kaE=3g7v!cj)2td6Bqa&o|T~=?s9h z@yVx<-(<uQD|E_~so;Dc?%9K91L^ct9*syxEMeNX*to#Q=B;15j<&%6gE7S#Sy)gg zrc<@k^?d@L;<)9DG1mwRteV#|FfhQ07oOid@6kt@ROyfmWTZ8LJne-g+w!Yr%ue8` zh_ABNGlK9hwKodxK1s<*%a?5f%*f5pl>-^U(wy8JfFG*pVrC{;ItJ|FS^C*#@GUgo z_jm39eP=afBf&7i{Q}H|)ry9(2Y^);7?-5Doz&!F$9rYw#f!z4O7I6_vBMGJqmTD= z2nklFjwfdTNymro42O90=FM<JaU!5OpxCCQrhaww2-|~xE7{)7)V4T$W>gJSH%I<M z%2fvFt*z4|*-g%)M+e+HZ5p*_YA{ju-WykpHQ9&JkZf#ha_kE{6rmsa06=@K=K2ty zA;3|1a{Tk_uV-atv3ApSU3(yCJzhruvFx>&##snplmfX;oG{TooszF2w&tSNARkl4 z8r=W+XRNj>R{VV2xbYxiEWyBC=TcJW&)nV@dfL+7hUW@RjOzn*!%z3^PfSjdPwx~b zNd|vq=Rvb7Jj<+12=T<u<moP7z6=WlF<1smUOte00e8EM64%vS#w|9VJavi%mz@=i ze>SfEe*XMhSy6#E5_#EcKn}fI4m9eVK-Y*C{A$xC99fVbhYL7!<_ukKE;~99Zjw0k z_2IE&$1Dh(KX%NRs;X;PL||T0^YkvZph&%e#|H+^e;oGhq)A4Dk?u#<%mysn6y9D2 zsetELpV`^jpZ@*t&xC}${@QEAEd~;hVL^`^J(`k|D!8PgH9feox6!9|TV4KtM?Z4h z$}*|?9!*;Ufpg7U#cAh)aGgeKleU^$&<M2*>QSq-y)w<Yz0X4567!R{e><qP$6Qf) zrSe*tGTTccX8*ak);EW1@#L#rKPfQg`%Y)z*>kG3y)`j48l^-$!_@t=-1^T;Y`h1S zt$ey~{UGA#n;Si#0uQL5-@Zlo3|<?ba=N56{~NaA4yF3OdPx5jD^vf!S1es(;GIF$ zR{>T1KQAA8Z^wZ*C0-?FOAq|Z_BKe98kzUNQxNC+ep1KfxZiS$d<WFfAg}yF#0O8H zlAWr62Ovx%(LEv5YSeD};7^wEZ&k~Xdje#$yB`RxP6gA`%D+@nx=eVesFh`i|I}6v zJB!Z(RZk;mdh{)@JDV!0nxP|pl|;A(2U(KVi2Uqxh|It$n6fh5t|*kkl#}^$p~%f5 zghtik52-I|k;(8GoRTm3q1Yrq_F9z&n=2rmg<-%a#9`P!6P<$svT^5^S1=Lz$s+RO zokva?9D)icJl5OQ=FU#bjkZBouGTfxS8KFG25HC*k3S$iQtLrCR83*USoQQ`i>#5< zU`r1_0G%3CuAz4bcvWFPdJ#h&#|H`}nV5`DwN=C;G>c~^9S#q^S6N6+i^W7JP^r8J zdqQmI1r?!EZgw*W-G}>dAMWD^V*%#m<wB&WsjG3?hB4<C6c+FmMlOjS*$tz|PMiS6 zk#-a4U;Fm$hk$~zBFGH}Q-i^nkeEb3xNFy|T|@eDS3zO^fB*H}rAwAT5x|s8d=x?t zCL|=BPfN#p1cNem(!YK9(UsD&2WHIz^8lvO>GCiP!9GDu2{Q){_(zW)&nql&lyPHG zf?u_L=gyLn<)}7c@x%iZ8PALh83b~}Xii~x-$V@(-}B($AjmWUBLmu6ThU*NPdJMy zlZ8@{w5oV&Y)_7sP3-cp`mmo$S3Zz77So`hVEpvQjU9(+HS%;+Ol&Mn1u#(-a(E}m z)<JrpR_#?+eZ88LDUzB)r3kcsymv2BR1eI$AApK=58}n)oUb6bG&D3(T8aNfMnxl< z3Wf?@X;5#Qeuxfx_U@rhFe+g>hv^j?7n_!zPMSJhuH)-Ta!NANrfXNPz9S$2V<;AU z-2W336G>KH#s9Em9sKej=;{6U&%)RTv*BDyGVlhLbRnZ>O-eoomuuc*^8i|49iaz% z?Bof6fqSP<!*Izpc&}4eK#z_~Ip<L`V|z1q?p!SUnCFU%iozo!7t9aDA_;sbks5M& zf5-Nvix%HDY?v_hTCs+ur)5Bv0%O3(pC2HZ(C{!|OY~{$8=7b)^4<Nx2c>1D)Doy6 zTcg^#T6zmL2^izl2}u=svHb@=!>EmtEw*EPJZaF_v++DHG!Cl}g#$97v1oDnn}dgG z2O_r!CrW=usES`7J#ynl6GlB)jJMr36sp{bQztRG!UBZS%D{1LFSsmFAt9)dq$Gu$ zO-P`z?wffZl7^hq3c0zzy!TfiPnbPm-XK+J85!~M@ieP!Dc5APva{qZm}?<)(hZ_x zfMeod%~A~RQXq{_K0&b*b}!jkQFFRfa`9s3nYb9Zp<;AcK}mUPSePV8#TJ(o^C!0; zK8bytP@`<s03b=9COd!rJVsv55W|uEn~>VqAh5RUgWYpy&n6b5in<oQaPaD=I%qDm z4ug(K1C9lFPo@}j*!p3uDTD6H6^GhZ({{W6wwtB|?;j=jpxnZhG(=u*9#>FE0zPpg zJYzohrh(ijI>wc$Q>KyverzB&fDQBwVtfMiLMvh>$<5QrjXir>{sb6+j{@>dZ{q^k zU`!ggH3(aPSx#ag@vx&XucLzsQzTob1MCcsh{Q7MS`tQaYI@px+jjs}ju@fzVL~bb z>W?2k&S2!nL{Y)K&&0%txOCk-puMjW>+!(B&l!dw7JmMIOa-v6lT%YE5@B3r-*(N8 z7CHFEK^iC^;0_uNm0D1i*Is#rZwa`?z@$si!wJ`l)HCQNR7(ups6=LDT!@Z|p)exa zpH`QfbL802I(@CZC(Dqf)H%cyiN{dM1YU(ndH<~YnXVaqbeQnSNJgK@)WD<hmP8%W z%BVxDdSe98)2`QCC(Z0!9fj{BoTLXH1KW3QUl<fTa%2Flqp#D4hJ_-q%+!ZDo{}*0 zFrgvM$INf9Lt$Yr5M)8Uzz8*kMFsD@za4-OfE<}+dLz&m3s7=Os=SM9@YvX~V;AF^ zL9M3HB0NldQX;M)k|R!bscGkVA7UXJbJrLqUa+6jr%$6{%Y-Qr35pRJ71h$*EZWrG zuT|+886BM+tPZ@sfj+*!W5>!BD;O9Y0F6=6(OgGlAUO<42_@T>mR7nY1?x2e8LN+w zYV4C9M>05ca~TgyVd(jZN$0#hz3^jT31bSPKZk{d-97p4nwnZBC_Bp4ZSG;!z&V5F zrv9B;hYQluGMEzazp1RKl;0u_1Dl!fx;zU6Vn&Uv+cbho78DlI6C5WlMm?=lQB@(I z&fC))rC;PX$4?j!)Wkf7W+|5ez{4iz83h-lGb|#UwT#b>&j_`SYg*b`1?cBdP+YWg z*KSBukVHv=S5>F($H_W_{uq0S{5(k*U@Ats7T=oNM~uLkfZWf-#xO8-Ivqoxe^aOI zW+23j3mY!r>|eq>iHi?7G_A&E&~5LVF_SeYJ6lH+(-5w-Mp}(@fE(=?p;IgK3XNM2 zS;L?6LMBca6Y$ew+6;{d2lN7Ya-AD-hIsG&_lOobG9UnbU<MHvduly}6ht`>OXbAH zp9Q1B!+`2^;La+KiLVvImUL*Hzr6P@qlt|WlN+;icw__{58q+KL0fxe<B=Hl6C8%9 z1n8k&O7W~bBW*#-`30gaY=f8buvl2k7CE&7f*v~j6@3ZPEfXou8RSUBK1Pk?wfDL> z%K9O?q$A9RN7s&BJ8=S_t(jMOBK8&cPM?9(62u_75U$QC=g*tS48H*(g-TEG@%H9} z>qBt?YxKMCz6*(%`efB+Uy+n_4p#bwi<z=-vnkrXYX^HvHmGc$@NXz8DPd%>TV~&d zY%<f!_8r?-ty;w@$!vh<Q*?AR0susPD=RG(iC6*b*)%pcw>XOCA(;Q=p+k&ZD510h zgXL;@Mbzo&h^PqGeYq)R5@yfI=Eq*WVgW6sX4v$q-8R#o%)w$&(Q@U`<-Kj@wjW)q z3-?MHM3>M1h@m$c8Vm%VvnqPjZkEz_2!kt<p2-bL#j4zs)elkODweRg<cNkQp&RjI zKV)G0J^@8GB1Tg!|Ek=Pcz?2oRsPaR_?4d8fMUReGQ6MijV<J$Jt_qk@|R$^iPIAM zvpNQ;SCJat{Nn#4M`S%t3WS;;nV*QhqT(F4a9;L8dqhG-liYT#jZ@2c1u&C@T#HgT z3*mvuSdes;<Twc^$0gv15-C*OGlm%2Txdoz8@*9)A0G<R)d|NJGM~l6)5B}<U<kv& zZQ|WTKD6|0RCRZDaZU@K*+T?e!UAal|Ev~>h~6!o?VTdhw--@2U=4Pgy)|0F!?dE9 zuk}#lx2O^C$@mmV!Y_axSfAiWTJdnli=1~D3Jm~>QfUv~2i<1slX{nw6OhXu3@}95 z@$!(;qTsb�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�mjO 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 `J*XX7C^^zmrx&TM|b!1C5YKb^$rn3mU*;m(Ay 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 BTK)h4 literal 0 HcmV?d00001 diff --git a/packages/website/src/components/hero-mockup.tsx b/packages/website/src/components/hero-mockup.tsx index 9545b3d57..f1a0733d3 100644 --- a/packages/website/src/components/hero-mockup.tsx +++ b/packages/website/src/components/hero-mockup.tsx @@ -1,1005 +1,15 @@ -import * as React from "react"; -import { motion } from "framer-motion"; -import { - FolderGit2, - Paperclip, - ArrowUp, - X, - MessagesSquare, - Plus, - Settings, - Monitor, - GitCommitHorizontal, - ChevronDown, - ChevronRight, - ListChevronsUpDown, - Bot, - Check, - Loader, - SquareTerminal, - PanelLeft, - Ellipsis, - Mic, - GitPullRequest, - SquarePen, - Columns2, - Rows2, -} from "lucide-react"; -import { ClaudeIcon, CodexIcon, SourceControlIcon } from "~/components/mockup"; - -const USER_BUBBLE_STYLE = { borderRadius: "16px 2px 16px 16px" }; -const NEW_BADGE_STYLE = { backgroundColor: "rgba(46, 160, 67, 0.2)" }; - -// ── Stagger timing ────────────────────────────────────── - -const D = { - shell: 0, - sidebar: 0.1, - titleBar: 0.15, - tabs: 0.25, - chat: 0.35, - diffPanel: 0.55, -}; - -function fade(delay: number) { - return { - initial: { opacity: 0, y: 8 } as const, - animate: { opacity: 1, y: 0 } as const, - transition: { duration: 0.5, delay, ease: "easeOut" as const }, - }; -} - -// ── Data ──────────────────────────────────────────────── - -type ChatItem = - | { type: "user"; text: string } - | { type: "text"; text: string; bold?: boolean } - | { type: "tool"; label: string; summary?: string; status: "running" | "done" }; - -const CHAT: ChatItem[] = [ - { type: "user", text: "Build me an internal dashboard for tracking customer returns" }, - { type: "text", text: "I'll break this down into planning and implementation.", bold: true }, - { type: "tool", label: "Run plan-technical", summary: "codex · plan-technical", status: "done" }, - { type: "tool", label: "Run plan-design", summary: "claude · plan-design", status: "done" }, - { - type: "tool", - label: "Wait for agents", - summary: "plan-technical plan-design", - status: "done", - }, - { type: "text", text: "Got the plans. Spinning up Codex for implementation.", bold: true }, - { type: "tool", label: "Run implement", summary: "codex · 12 files changed", status: "done" }, - { type: "text", text: "Implementation done. Requesting review from Claude." }, - { type: "tool", label: "Run review", summary: "claude · no issues found", status: "running" }, - { type: "text", text: "All tasks complete. Dashboard is ready.", bold: true }, -]; - -// ── Sidebar data ──────────────────────────────────────── - -interface SidebarWorkspace { - name: string; - kind: "checkout" | "worktree"; - status: "running" | "done" | "idle" | "syncing"; - selected?: boolean; - diffStat?: { additions: number; deletions: number }; - pr?: { number: number; state: "open" | "merged" | "closed" }; -} - -interface SidebarProject { - initial: string; - name: string; - workspaces: SidebarWorkspace[]; -} - -const SIDEBAR_PROJECTS: SidebarProject[] = [ - { - initial: "A", - name: "acme/returns-app", - workspaces: [ - { - name: "main", - kind: "checkout", - status: "syncing", - selected: true, - diffStat: { additions: 247, deletions: 15 }, - }, - { - name: "feat/dashboard", - kind: "worktree", - status: "done", - pr: { number: 142, state: "open" }, - }, - ], - }, - { - initial: "P", - name: "acme/payments", - workspaces: [ - { name: "main", kind: "checkout", status: "idle" }, - { - name: "fix/stripe-webhook", - kind: "worktree", - status: "done", - diffStat: { additions: 38, deletions: 4 }, - pr: { number: 89, state: "merged" }, - }, - ], - }, - { - initial: "I", - name: "acme/infra", - workspaces: [ - { name: "main", kind: "checkout", status: "idle" }, - { - name: "feat/k8s-autoscale", - kind: "worktree", - status: "syncing", - diffStat: { additions: 91, deletions: 3 }, - }, - ], - }, - { - initial: "D", - name: "acme/design-system", - workspaces: [{ name: "main", kind: "checkout", status: "idle" }], - }, -]; - -// ── Tab data ───────────────────────────────────────────── - -interface TabDef { - name: string; - provider: "claude" | "codex" | "terminal"; - done: boolean; - active?: boolean; -} - -const TABS: TabDef[] = [ - { name: "Orchestrator", provider: "claude", done: false, active: true }, - { name: "Implement", provider: "codex", done: true }, -]; - -// ── Diff data ────────────────────────────────────────── - -type DiffLineType = "add" | "remove" | "context" | "header"; -interface DiffLine { - type: DiffLineType; - ln: string | null; - content?: string; - tokens?: Array<{ text: string; cls: string }>; -} - -const DIFF_LINES: DiffLine[] = [ - { - type: "add", - ln: "1", - tokens: [ - { text: "import", cls: "text-syn-keyword" }, - { text: " { ", cls: "text-syn-punctuation" }, - { text: "useState", cls: "text-syn-variable" }, - { text: " } ", cls: "text-syn-punctuation" }, - { text: "from", cls: "text-syn-keyword" }, - { text: ' "react"', cls: "text-syn-string" }, - ], - }, - { - type: "add", - ln: "2", - tokens: [ - { text: "import", cls: "text-syn-keyword" }, - { text: " { ", cls: "text-syn-punctuation" }, - { text: "ReturnTable", cls: "text-syn-variable" }, - { text: " } ", cls: "text-syn-punctuation" }, - { text: "from", cls: "text-syn-keyword" }, - { text: ' "./components"', cls: "text-syn-string" }, - ], - }, - { type: "add", ln: "3", tokens: [] }, - { - type: "add", - ln: "4", - tokens: [ - { text: "export", cls: "text-syn-keyword" }, - { text: " function", cls: "text-syn-keyword" }, - { text: " Dashboard", cls: "text-syn-function" }, - { text: "() {", cls: "text-syn-punctuation" }, - ], - }, - { - type: "add", - ln: "5", - tokens: [ - { text: " const", cls: "text-syn-keyword" }, - { text: " [returns, setReturns]", cls: "text-syn-variable" }, - { text: " = ", cls: "text-syn-operator" }, - { text: "useState", cls: "text-syn-function" }, - { text: "([])", cls: "text-syn-punctuation" }, - ], - }, - { - type: "add", - ln: "6", - tokens: [ - { text: " const", cls: "text-syn-keyword" }, - { text: " [filter, setFilter]", cls: "text-syn-variable" }, - { text: " = ", cls: "text-syn-operator" }, - { text: "useState", cls: "text-syn-function" }, - { text: '("all")', cls: "text-syn-string" }, - ], - }, - { type: "add", ln: "7", tokens: [] }, - { - type: "add", - ln: "8", - tokens: [ - { text: " ", cls: "text-syn-punctuation" }, - { text: "return", cls: "text-syn-keyword" }, - { text: " (", cls: "text-syn-punctuation" }, - ], - }, - { - type: "add", - ln: "9", - tokens: [ - { text: " <", cls: "text-syn-punctuation" }, - { text: "main", cls: "text-syn-tag" }, - { text: " className", cls: "text-syn-property" }, - { text: '="', cls: "text-syn-punctuation" }, - { text: "min-h-screen p-8", cls: "text-syn-string" }, - { text: '">', cls: "text-syn-punctuation" }, - ], - }, - { - type: "add", - ln: "10", - tokens: [ - { text: " <", cls: "text-syn-punctuation" }, - { text: "h1", cls: "text-syn-tag" }, - { text: ">Customer Returns</", cls: "text-syn-variable" }, - { text: "h1", cls: "text-syn-tag" }, - { text: ">", cls: "text-syn-punctuation" }, - ], - }, - { - type: "add", - ln: "11", - tokens: [ - { text: " <", cls: "text-syn-punctuation" }, - { text: "FilterBar", cls: "text-syn-tag" }, - { text: " value", cls: "text-syn-property" }, - { text: "={", cls: "text-syn-punctuation" }, - { text: "filter", cls: "text-syn-variable" }, - { text: "}", cls: "text-syn-punctuation" }, - { text: " onChange", cls: "text-syn-property" }, - { text: "={", cls: "text-syn-punctuation" }, - { text: "setFilter", cls: "text-syn-variable" }, - { text: "} />", cls: "text-syn-punctuation" }, - ], - }, - { - type: "add", - ln: "12", - tokens: [ - { text: " <", cls: "text-syn-punctuation" }, - { text: "ReturnTable", cls: "text-syn-tag" }, - { text: " data", cls: "text-syn-property" }, - { text: "={", cls: "text-syn-punctuation" }, - { text: "returns", cls: "text-syn-variable" }, - { text: "} />", cls: "text-syn-punctuation" }, - ], - }, - { - type: "add", - ln: "13", - tokens: [ - { text: " <", cls: "text-syn-punctuation" }, - { text: "StatusChart", cls: "text-syn-tag" }, - { text: " data", cls: "text-syn-property" }, - { text: "={", cls: "text-syn-punctuation" }, - { text: "returns", cls: "text-syn-variable" }, - { text: "} />", cls: "text-syn-punctuation" }, - ], - }, - { - type: "add", - ln: "14", - tokens: [ - { text: " </", cls: "text-syn-punctuation" }, - { text: "main", cls: "text-syn-tag" }, - { text: ">", cls: "text-syn-punctuation" }, - ], - }, - { type: "add", ln: "15", tokens: [{ text: " )", cls: "text-syn-punctuation" }] }, - { type: "add", ln: "16", tokens: [{ text: "}", cls: "text-syn-punctuation" }] }, -]; - -// ── Implementation agent chat data ────────────────────── - -// ── Terminal log lines ───────────────────────────────── - -const TERMINAL_LINES = [ - { id: "cmd", text: "$ npm run dev", cls: "text-mock-fg" }, - { id: "blank1", text: "", cls: "" }, - { id: "pkg", text: "> acme-returns@0.1.0 dev", cls: "text-mock-fg-muted" }, - { id: "pkg-cmd", text: "> next dev --turbopack", cls: "text-mock-fg-muted" }, - { id: "blank2", text: "", cls: "" }, - { id: "next-ver", text: " ▲ Next.js 15.3.1 (Turbopack)", cls: "text-mock-fg" }, - { id: "next-local", text: " - Local: http://localhost:3000", cls: "text-mock-fg-muted" }, - { id: "blank3", text: "", cls: "" }, - { id: "starting", text: " ✓ Starting...", cls: "text-mock-green" }, - { id: "ready", text: " ✓ Ready in 1.2s", cls: "text-mock-green" }, - { id: "compiling-dashboard", text: " ○ Compiling /dashboard ...", cls: "text-mock-fg-muted" }, - { id: "compiled-dashboard", text: " ✓ Compiled /dashboard in 340ms", cls: "text-mock-green" }, - { id: "compiling-api", text: " ○ Compiling /api/returns ...", cls: "text-mock-fg-muted" }, - { id: "compiled-api", text: " ✓ Compiled /api/returns in 120ms", cls: "text-mock-green" }, -]; - -// ── Sub-components ────────────────────────────────────── - -function TrafficLights() { - return ( - <div className="flex items-center gap-[6px]"> - <div className="w-[11px] h-[11px] rounded-full bg-[#ff5f57]" /> - <div className="w-[11px] h-[11px] rounded-full bg-[#febc2e]" /> - <div className="w-[11px] h-[11px] rounded-full bg-[#28c840]" /> - </div> - ); -} - -function ProviderIcon({ - provider, - muted = false, -}: { - provider: "claude" | "codex" | "terminal"; - muted?: boolean; -}) { - const cls = muted ? "text-mock-fg-muted" : "text-mock-fg"; - if (provider === "terminal") return <SquareTerminal size={13} className={cls} />; - return provider === "claude" ? ( - <ClaudeIcon size={13} className={cls} /> - ) : ( - <CodexIcon size={13} className={cls} /> - ); -} - -function TabBarAction({ - icon: Icon, -}: { - icon: React.ComponentType<{ size?: number; className?: string; strokeWidth?: number }>; -}) { - return ( - <div className="w-4 h-5 flex items-center justify-center flex-shrink-0"> - <Icon size={12} strokeWidth={1.5} className="text-mock-fg-muted" /> - </div> - ); -} - -function PaneTabBar({ tabs, focused = false }: { tabs: TabDef[]; focused?: boolean }) { - return ( - <div className="flex items-stretch h-7 bg-mock-surface0 border-b border-mock-border flex-shrink-0"> - {tabs.map((tab) => ( - <div - key={tab.name} - className="flex items-center gap-1.5 px-2 border-r border-mock-border relative min-w-0" - > - {tab.active && ( - <div - className={`absolute top-0 left-0 right-0 h-0.5 ${focused ? "bg-mock-accent" : "bg-mock-border-accent"}`} - /> - )} - - <div className="relative flex-shrink-0"> - <ProviderIcon provider={tab.provider} muted={!tab.active} /> - </div> - - <span - className={`text-[11px] truncate ${tab.active ? "text-mock-fg" : "text-mock-fg-muted"}`} - > - {tab.name} - </span> - - <X size={11} className="text-mock-zinc500 flex-shrink-0 ml-auto" /> - </div> - ))} - <div className="ml-auto flex items-center px-1.5"> - <TabBarAction icon={SquarePen} /> - <TabBarAction icon={SquareTerminal} /> - <TabBarAction icon={Columns2} /> - <TabBarAction icon={Rows2} /> - </div> - </div> - ); -} - -function Composer({ - provider, - model, - focused = false, -}: { - provider: "claude" | "codex"; - model: string; - focused?: boolean; -}) { - const Icon = provider === "claude" ? ClaudeIcon : CodexIcon; - return ( - <div className="px-3 pb-3 flex-shrink-0"> - <div className="bg-mock-surface1 border border-mock-border-accent rounded-2xl px-3 py-3 flex flex-col gap-[12px]"> - <div className="flex items-center"> - {focused && ( - <span className="inline-block w-[1px] h-[14px] bg-mock-fg animate-pulse mr-[1px] flex-shrink-0" /> - )} - <span className="text-[11px] text-mock-fg-muted/40 leading-[1.4] select-none whitespace-nowrap overflow-hidden text-ellipsis"> - Message the agent, tag @files, or use /commands and /skills - </span> - </div> - <div className="flex items-end justify-between"> - <div className="flex items-center gap-0.5"> - <div className="w-[22px] h-[22px] rounded-full flex items-center justify-center flex-shrink-0"> - <Paperclip size={12} className="text-mock-fg" /> - </div> - <div className="flex items-center gap-[4px] h-[22px] px-[6px] rounded-full"> - <Icon size={12} className="text-mock-fg-muted" /> - <span className="text-[10px] text-mock-fg-muted leading-none">{model}</span> - <ChevronDown size={10} className="text-mock-fg-muted" /> - </div> - </div> - <div className="flex items-center gap-[6px]"> - <div className="w-[22px] h-[22px] rounded-full flex items-center justify-center flex-shrink-0"> - <Mic size={12} className="text-mock-fg" /> - </div> - <div className="w-[22px] h-[22px] rounded-full bg-mock-accent flex items-center justify-center flex-shrink-0"> - <ArrowUp size={12} className="text-white" /> - </div> - </div> - </div> - </div> - </div> - ); -} - -function prStateLabel(state: "open" | "merged" | "closed"): string { - if (state === "open") return "Open"; - if (state === "merged") return "Merged"; - return "Closed"; -} - -function WorkspaceIcon({ workspace }: { workspace: SidebarWorkspace }) { - if (workspace.status === "syncing") return <SyncedLoader size={11} />; - const icon = - workspace.kind === "worktree" ? ( - <FolderGit2 size={14} className="text-mock-fg-muted" /> - ) : ( - <Monitor size={14} className="text-mock-fg-muted" /> - ); - return ( - <> - {icon} - {workspace.status !== "idle" && ( - <div className="absolute bottom-0 right-0"> - <WorkspaceStatusDot status={workspace.status} /> - </div> - )} - </> - ); -} - -function SidebarWorkspaceRow({ workspace }: { workspace: SidebarWorkspace }) { - return ( - <div className={`mb-1 mx-1.5 rounded-lg ${workspace.selected ? "bg-mock-surface1" : ""}`}> - <div className="flex items-center gap-2 min-h-[28px] py-1 pl-[22px] pr-1"> - <div className="relative w-[14px] h-4 flex-shrink-0 flex items-center justify-center"> - <WorkspaceIcon workspace={workspace} /> - </div> - <span className="text-[10px] text-mock-fg/[0.76] font-normal truncate flex-1 min-w-0 leading-[1.4]"> - {workspace.name} - </span> - {workspace.diffStat && ( - <div className="flex items-center gap-1 flex-shrink-0"> - <span className="text-[10px] text-mock-green-400 font-normal leading-none"> - +{workspace.diffStat.additions} - </span> - <span className="text-[10px] text-mock-red font-normal leading-none"> - -{workspace.diffStat.deletions} - </span> - </div> - )} - </div> - {workspace.pr && ( - <div className="flex items-center gap-1 pl-[42px] pr-2 pb-0.5"> - <GitPullRequest size={11} className="text-mock-fg-muted" /> - <span className="text-[10px] text-mock-fg-muted leading-none truncate"> - #{workspace.pr.number} · {prStateLabel(workspace.pr.state)} - </span> - </div> - )} - </div> - ); -} - -function diffLineBg(type: DiffLineType): string { - if (type === "add") return "bg-mock-diff-add"; - if (type === "remove") return "bg-mock-diff-remove"; - return "bg-mock-surface1"; -} - -function diffLineNumCls(type: DiffLineType): string { - if (type === "add") return "text-mock-green-400"; - if (type === "remove") return "text-mock-red"; - return "text-mock-fg-muted"; -} - -function DiffLineRow({ line }: { line: DiffLine }) { - const lineBg = diffLineBg(line.type); - const lineNumCls = diffLineNumCls(line.type); - return ( - <div className={`flex items-stretch ${lineBg}`}> - <div className="w-8 border-r border-mock-border flex-shrink-0 flex items-center justify-end"> - <code className={`text-[10px] font-mono ${lineNumCls} select-none pr-2 py-[1px]`}> - {line.ln ?? ""} - </code> - </div> - <code className="text-[10px] font-mono text-mock-fg pl-3 pr-3 py-[1px] whitespace-pre flex-1 min-w-0"> - {line.tokens?.map((tok) => ( - <span key={`${tok.cls}:${tok.text}`} className={tok.cls}> - {tok.text} - </span> - ))} - </code> - </div> - ); -} - -function chatItemKey(item: ChatItem): string { - if (item.type === "user") return `user-${item.text}`; - if (item.type === "tool") return `tool-${item.label}-${item.summary ?? ""}`; - return `text-${item.text}`; -} - -function ChatMessages({ items }: { items: ChatItem[] }) { - return ( - <div className="flex-1 overflow-hidden px-2 py-2"> - {items.map((item) => { - const key = chatItemKey(item); - if (item.type === "user") { - return ( - <div key={key} className="flex justify-end py-1"> - <div - className="bg-mock-surface2 px-2.5 py-1.5 max-w-[85%] leading-none" - style={USER_BUBBLE_STYLE} - > - <span className="text-[11px] text-mock-fg leading-none">{item.text}</span> - </div> - </div> - ); - } - if (item.type === "tool") { - const isDone = item.status === "done"; - return ( - <div - key={key} - className="flex items-center rounded-lg px-2 py-[1px] -mx-[6px] border border-transparent" - > - <div className="w-[22px] h-[22px] rounded-full flex items-center justify-center flex-shrink-0 mr-1"> - <Bot size={12} className="text-mock-fg-muted" /> - </div> - <span className="text-[11px] text-mock-fg-muted truncate flex-shrink-0"> - {item.label} - </span> - {item.summary && ( - <span className="text-[11px] text-mock-zinc500 truncate flex-1 ml-2"> - {item.summary} - </span> - )} - <div className="ml-auto flex-shrink-0 pl-2"> - {isDone ? ( - <Check size={11} className="text-mock-zinc500" /> - ) : ( - <Loader size={11} className="text-mock-fg-muted animate-spin" /> - )} - </div> - </div> - ); - } - return ( - <p - key={key} - className={`text-[11px] px-2 py-[1px] text-mock-fg leading-[1.5] ${item.bold ? "font-medium" : ""}`} - > - {item.text} - </p> - ); - })} - </div> - ); -} - -function ChatArea() { - return ( - <div className="flex flex-col flex-1 min-w-0 bg-mock-surface0"> - <ChatMessages items={CHAT} /> - <Composer provider="claude" model="Opus 4.6" focused /> - </div> - ); -} - -function TerminalPane() { - return ( - <div className="flex flex-col flex-1 min-w-0 min-h-0 bg-mock-surface0"> - <div className="flex-1 overflow-hidden px-3 py-2"> - {TERMINAL_LINES.map((line) => ( - <div key={line.id} className="leading-[1.15]"> - <code className={`text-[10px] font-mono ${line.cls} whitespace-pre`}>{line.text}</code> - </div> - ))} - </div> - </div> - ); -} - -function ExplorerSidebar() { - return ( - <div className="flex flex-col bg-mock-sidebar border-l border-mock-border w-[28%] flex-shrink-0 overflow-hidden"> - {/* Explorer header — h-12, Changes/Files tabs */} - <div className="flex items-center h-10 px-2 border-b border-mock-border flex-shrink-0"> - <div className="flex items-center gap-1"> - <div className="flex items-center px-3 py-1.5 rounded-md bg-mock-surface1"> - <span className="text-[11px] text-mock-fg font-medium">Changes</span> - </div> - <div className="flex items-center px-3 py-1.5 rounded-md"> - <span className="text-[11px] text-mock-fg-muted">Files</span> - </div> - </div> - </div> - - {/* Secondary header — h-9, Uncommitted dropdown + filter icons */} - <div className="flex items-center justify-between h-9 border-b border-mock-border flex-shrink-0"> - <div className="flex items-center gap-1 ml-2 px-1 h-6 rounded"> - <span className="text-[11px] text-mock-fg-muted">Uncommitted</span> - <ChevronDown size={11} className="text-mock-fg-muted" /> - </div> - <div className="flex items-center pr-2"> - <div className="flex items-center justify-center w-6 h-6 rounded"> - <ListChevronsUpDown size={14} className="text-mock-fg-muted" /> - </div> - </div> - </div> - - {/* File header — expanded */} - <div className="flex items-center justify-between pl-2 pr-2 py-1.5 border-b border-mock-border flex-shrink-0"> - <div className="flex items-center gap-1 flex-1 min-w-0 overflow-hidden"> - <ChevronDown size={10} className="text-mock-fg-muted flex-shrink-0" /> - <span className="text-[11px] text-mock-fg flex-shrink-0">dashboard.tsx</span> - <span className="text-[11px] text-mock-fg-muted truncate min-w-0"> src/pages</span> - <span - className="text-[10px] text-mock-green-400 px-2 py-[2px] rounded-md flex-shrink-0" - style={NEW_BADGE_STYLE} - > - New - </span> - </div> - <div className="flex items-center gap-1 flex-shrink-0 ml-1"> - <span className="text-[11px] text-mock-green-400">+16</span> - <span className="text-[11px] text-mock-red">-0</span> - </div> - </div> - - {/* Diff lines */} - <div className="overflow-hidden bg-mock-surface1"> - {DIFF_LINES.map((line) => ( - <DiffLineRow key={`${line.type}-${line.ln ?? "blank"}`} line={line} /> - ))} - </div> - - {/* Collapsed file rows */} - {[ - { name: "return-table.tsx", dir: "src/components", added: 42, removed: 8 }, - { name: "filter-bar.tsx", dir: "src/components", added: 28, removed: 3 }, - { name: "status-chart.tsx", dir: "src/components", added: 19, removed: 0, isNew: true }, - { name: "returns.ts", dir: "src/api", added: 12, removed: 5 }, - { name: "index.tsx", dir: "src/pages", added: 6, removed: 2 }, - ].map((file) => ( - <div - key={file.name} - className="flex items-center justify-between pl-2 pr-2 py-1.5 border-b border-mock-border" - > - <div className="flex items-center gap-1 flex-1 min-w-0 overflow-hidden"> - <ChevronRight size={10} className="text-mock-fg-muted flex-shrink-0" /> - <span className="text-[11px] text-mock-fg flex-shrink-0">{file.name}</span> - <span className="text-[11px] text-mock-fg-muted truncate min-w-0"> {file.dir}</span> - {file.isNew && ( - <span - className="text-[10px] text-mock-green-400 px-2 py-[2px] rounded-md flex-shrink-0" - style={NEW_BADGE_STYLE} - > - New - </span> - )} - </div> - <div className="flex items-center gap-1 flex-shrink-0 ml-1"> - <span className="text-[11px] text-mock-green-400">+{file.added}</span> - <span className="text-[11px] text-mock-red">-{file.removed}</span> - </div> - </div> - ))} - - <div className="flex-1" /> - </div> - ); -} - -// Snake traversal order matching the real app: [0, 1, 3, 5, 4, 2] (2-col × 3-row grid) -// DOT_SEQUENCE[sequenceIndex] = dotIndex, so sequenceIndex = indexOf(dotIndex) -const DOT_SEQUENCE = [0, 1, 3, 5, 4, 2] as const; -const SYNCED_LOADER_DURATION_MS = 950; -const DOT_COUNT = 6; -const DOT_INDICES = Array.from({ length: DOT_COUNT }, (_, i) => i); -const STEP_MS = SYNCED_LOADER_DURATION_MS / DOT_COUNT; // 158.333…ms per step - -interface SyncedLoaderDotProps { - dotIndex: number; - dotSize: number; - gap: number; -} - -function SyncedLoaderDot({ dotIndex, dotSize, gap }: SyncedLoaderDotProps) { - const col = dotIndex % 2; - const row = Math.floor(dotIndex / 2); - const sequenceIndex = DOT_SEQUENCE.indexOf(dotIndex as (typeof DOT_SEQUENCE)[number]); - const delayMs = -(sequenceIndex * STEP_MS); - const style = React.useMemo( - () => ({ - position: "absolute" as const, - left: col * (dotSize + gap), - top: row * (dotSize + gap), - width: dotSize, - height: dotSize, - borderRadius: "50%", - backgroundColor: "#f59e0b", - animationName: "synced-snake-dot", - animationDuration: `${SYNCED_LOADER_DURATION_MS}ms`, - animationTimingFunction: "linear", - animationIterationCount: "infinite", - animationDelay: `${delayMs}ms`, - }), - [col, row, dotSize, gap, delayMs], - ); - return <div style={style} />; -} - -function SyncedLoader({ size = 11 }: { size?: number }) { - const gap = Math.max(1, Math.round(size * 0.12)); - const dotSize = Math.max(2, Math.floor((size - gap * 2) / 3)); - const gridW = dotSize * 2 + gap; - const gridH = dotSize * 3 + gap * 2; - - const outerStyle = React.useMemo( - () => ({ - width: size, - height: size, - display: "flex", - alignItems: "center", - justifyContent: "center", - flexShrink: 0, - }), - [size], - ); - - const innerStyle = React.useMemo( - () => ({ position: "relative" as const, width: gridW, height: gridH }), - [gridW, gridH], - ); - - return ( - <div style={outerStyle}> - <div style={innerStyle}> - {DOT_INDICES.map((dotIndex) => ( - <SyncedLoaderDot - key={`dot-${dotIndex}`} - dotIndex={dotIndex} - dotSize={dotSize} - gap={gap} - /> - ))} - </div> - </div> - ); -} - -function WorkspaceStatusDot({ status }: { status: SidebarWorkspace["status"] }) { - if (status === "running") return <div className="w-[5px] h-[5px] rounded-full bg-mock-amber" />; - if (status === "done") return <div className="w-[5px] h-[5px] rounded-full bg-mock-green" />; - return null; -} - -function Sidebar() { - // Shared left edge: pl-3 (12px) for traffic lights, icons, footer dot - // Workspace indent: pl-7 (28px) - // PR badge aligns with workspace text: pl-[42px] - return ( - <div className="flex flex-col bg-mock-sidebar border-r border-mock-border w-[200px] flex-shrink-0"> - {/* Traffic lights */} - <div className="flex items-center pl-3 pr-2 pt-3 pb-2"> - <TrafficLights /> - </div> - - {/* Sessions header */} - <div className="flex items-center gap-2 pl-3 pr-2 py-2 border-b border-mock-border"> - <MessagesSquare size={14} className="text-mock-fg-muted flex-shrink-0" /> - <span className="text-[12px] text-mock-fg-muted">Sessions</span> - </div> - - {/* Project list */} - <div className="flex-1 overflow-hidden pt-1 pb-4 min-h-0"> - {SIDEBAR_PROJECTS.map((project) => ( - <div key={project.name} className="mb-2"> - {/* Project row — icon aligns with traffic lights / sessions */} - <div className="flex items-center gap-2 min-h-[32px] py-1.5 pl-3 pr-2"> - <div className="w-4 h-4 rounded-sm border border-mock-border flex items-center justify-center flex-shrink-0"> - <span className="text-[9px] text-mock-fg-muted leading-none"> - {project.initial} - </span> - </div> - <span className="text-[11px] text-mock-fg font-normal truncate flex-1 min-w-0 leading-5"> - {project.name} - </span> - </div> - - {/* Workspace rows — indented one level */} - {project.workspaces.map((workspace) => ( - <SidebarWorkspaceRow key={workspace.name} workspace={workspace} /> - ))} - </div> - ))} - </div> - - {/* Footer — dot aligns with traffic lights / icons */} - <div className="flex items-center justify-between pl-3 pr-2 py-3 border-t border-mock-border"> - <div className="flex items-center gap-2 min-w-0 flex-shrink"> - <div className="w-2 h-2 rounded-full bg-mock-green flex-shrink-0" /> - <span className="text-[11px] text-mock-fg-muted truncate">MacBook Pro</span> - </div> - <div className="flex items-center gap-1 flex-shrink-0"> - <div className="w-6 h-6 flex items-center justify-center"> - <Plus size={18} className="text-mock-fg-muted" /> - </div> - <div className="w-6 h-6 flex items-center justify-center"> - <Settings size={18} className="text-mock-fg-muted" /> - </div> - </div> - </div> - </div> - ); -} - -const TERMINAL_TABS: TabDef[] = [ - { name: "npm run dev", provider: "terminal", done: false, active: true }, -]; - -function TitleBarLeft() { - return ( - <div className="flex items-center gap-2 flex-1 min-w-0"> - <div className="px-2 py-1 rounded-lg flex items-center justify-center flex-shrink-0"> - <PanelLeft size={14} className="text-mock-fg-muted" /> - </div> - <div className="flex items-center gap-2 min-w-0 flex-1"> - <span className="text-[13px] font-light text-mock-fg truncate flex-shrink-0">main</span> - <span className="text-[13px] text-mock-fg-muted truncate flex-shrink min-w-0"> - acme/returns-app - </span> - <div className="px-2 py-1 rounded-lg flex items-center justify-center flex-shrink-0"> - <Ellipsis size={14} className="text-mock-fg-muted" /> - </div> - </div> - </div> - ); -} - -function TitleBarRight() { - return ( - <div className="flex items-center gap-2 ml-auto flex-shrink-0"> - <div className="flex items-stretch rounded-md border border-mock-border-accent overflow-hidden"> - <div className="flex items-center gap-2 px-3 py-1"> - <GitCommitHorizontal size={12} className="text-mock-fg-muted flex-shrink-0" /> - <span className="text-[11px] text-mock-fg font-normal">Commit</span> - </div> - <div className="flex items-center justify-center w-7 border-l border-mock-border-accent"> - <ChevronDown size={12} className="text-mock-fg-muted" /> - </div> - </div> - <div className="flex items-center gap-2 px-3 py-1 rounded-md"> - <SourceControlIcon size={14} className="text-mock-fg-muted" /> - <span className="text-[11px] font-normal text-mock-green-400">+247</span> - <span className="text-[11px] font-normal text-mock-red">-15</span> - </div> - </div> - ); -} - -function DesktopSplitPanes() { - return ( - <div className="flex flex-1 min-h-0"> - {/* Left pane: all agent tabs + chat */} - <div className="flex flex-col flex-1 min-w-0 min-h-0"> - <motion.div {...fade(D.tabs)}> - <PaneTabBar tabs={TABS} focused /> - </motion.div> - <motion.div {...fade(D.chat)} className="flex-1 flex min-h-0"> - <ChatArea /> - </motion.div> - </div> - - {/* Resize handle */} - <div className="w-px bg-mock-border flex-shrink-0" /> - - {/* Right pane: terminal only */} - <motion.div {...fade(D.chat)} className="flex flex-col flex-1 min-w-0 min-h-0"> - <PaneTabBar tabs={TERMINAL_TABS} /> - <TerminalPane /> - </motion.div> - </div> - ); -} - -// ── Desktop Mockup ────────────────────────────────────── - -function DesktopMockup() { - const containerRef = React.useRef<HTMLDivElement>(null); - const [scale, setScale] = React.useState(1); - - React.useEffect(() => { - function updateScale() { - if (!containerRef.current) return; - const parentWidth = containerRef.current.parentElement?.clientWidth ?? 1200; - const designWidth = 1200; - setScale(Math.min(1, parentWidth / designWidth)); - } - updateScale(); - window.addEventListener("resize", updateScale); - return () => window.removeEventListener("resize", updateScale); - }, []); - - const containerStyle = React.useMemo(() => ({ height: `${1200 * (9 / 16) * scale}px` }), [scale]); - const innerStyle = React.useMemo(() => ({ width: 1200, transform: `scale(${scale})` }), [scale]); - - return ( - <div ref={containerRef} className="w-full overflow-hidden" style={containerStyle}> - <div - className="mx-auto rounded-xl overflow-hidden border border-mock-border bg-mock-surface0 shadow-[6px_6px_0_rgba(0,0,0,0.4)] origin-top-left" - style={innerStyle} - > - {/* Top-level: left sidebar | center column | explorer sidebar — all full height */} - <div className="flex aspect-video"> - {/* Left sidebar — full height */} - <motion.div {...fade(D.sidebar)} className="contents"> - <Sidebar /> - </motion.div> - - {/* Center column: title bar + split panes */} - <div className="flex flex-col flex-1 min-w-0 min-h-0"> - {/* Title bar — belongs to center column only */} - <motion.div - {...fade(D.titleBar)} - className="flex items-center h-10 px-2 bg-mock-surface0 border-b border-mock-border flex-shrink-0" - > - <TitleBarLeft /> - <TitleBarRight /> - </motion.div> - - <DesktopSplitPanes /> - </div> - - {/* Explorer sidebar — full height, pushes center column */} - <motion.div {...fade(D.diffPanel)} className="contents"> - <ExplorerSidebar /> - </motion.div> - </div> - </div> - </div> - ); -} - -// ── Export ─────────────────────────────────────────────── - export function HeroMockup() { - return <DesktopMockup />; + return ( + <div className="overflow-hidden rounded-xl bg-mock-surface0 p-2 ring-1 ring-white/10 sm:rounded-2xl sm:p-3"> + <img + src="/homepage-hero.png" + alt="Paseo desktop app with coding agents, a conversation, and a code diff open side by side" + width={2400} + height={1442} + fetchPriority="high" + decoding="async" + className="block h-auto w-full rounded-lg sm:rounded-xl" + /> + </div> + ); } From e0e50c9a8e1b9604a44b3cd57ac6c4ed518da061 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra <boudra.moha@gmail.com> Date: Sat, 18 Jul 2026 17:45:02 +0200 Subject: [PATCH 32/38] fix(app): preserve persisted theme styles after startup Module-level style composites could materialize the temporary adaptive theme before persisted settings loaded. Keep Unistyles reads in render and guard against eager module-scope access. --- .oxlintrc.json | 3 + docs/unistyles.md | 24 ++++++ packages/app/src/agent-stream/view.tsx | 6 +- packages/app/src/app/pair-scan.tsx | 12 +-- .../src/components/adaptive-modal-sheet.tsx | 5 +- packages/app/src/components/agent-list.tsx | 7 +- .../app/src/components/dictation-controls.tsx | 11 +-- .../app/src/components/explorer-sidebar.tsx | 4 +- .../app/src/components/file-explorer-pane.tsx | 4 +- .../src/components/provider-catalog-list.tsx | 4 +- .../components/provider-diagnostic-sheet.tsx | 9 +-- .../src/components/sidebar-resize-handle.tsx | 8 +- .../src/components/sidebar-workspace-list.tsx | 6 +- .../app/src/components/tool-call-details.tsx | 11 +-- .../app/src/components/tree-primitives.tsx | 6 +- packages/app/src/components/ui/combobox.tsx | 6 +- .../src/components/workspace-hover-card.tsx | 14 ++-- packages/app/src/composer/index.tsx | 4 +- .../components/desktop-updates-section.tsx | 12 ++- .../components/integrations-section.tsx | 4 +- packages/app/src/git/diff-pane.tsx | 13 ++-- .../src/provider-usage/settings-section.tsx | 6 +- packages/app/src/screens/settings-screen.tsx | 12 ++- .../appearance/appearance-section.tsx | 3 +- .../app/src/screens/settings/host-page.tsx | 7 +- .../settings/keyboard-shortcuts-section.tsx | 4 +- .../screens/settings/providers-section.tsx | 6 +- .../workspace/workspace-desktop-tabs-row.tsx | 11 +-- .../screens/workspace/workspace-screen.tsx | 7 +- .../workspace/workspace-scripts-button.tsx | 16 ++-- .../src/styles/unistyles-module-scope.test.ts | 76 +++++++++++++++++++ 31 files changed, 188 insertions(+), 133 deletions(-) create mode 100644 packages/app/src/styles/unistyles-module-scope.test.ts diff --git a/.oxlintrc.json b/.oxlintrc.json index 9e389e515..bdcab1dc5 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -87,6 +87,9 @@ { "files": ["packages/app/src/**/*.{ts,tsx}"], "rules": { + // React Native style arrays must read Unistyles proxies during render. Hoisting them to + // satisfy this allocation rule captures the temporary startup theme instead. + "react-perf/jsx-no-new-array-as-prop": "off", "no-restricted-imports": [ "error", { diff --git a/docs/unistyles.md b/docs/unistyles.md index 255d74d44..619ceed3b 100644 --- a/docs/unistyles.md +++ b/docs/unistyles.md @@ -58,6 +58,30 @@ For standard React Native components, the [Unistyles Babel plugin](https://www.u The important detail: the automatic native path tracks `props.style`. It does not generally track every prop that happens to carry style-like values. +### Do Not Materialize Styles At Module Scope + +Never read a Unistyles style property into a module-level constant. This includes cached arrays: + +```tsx +// Wrong: evaluated while the app may still be using the temporary system theme. +const ROW_STYLE = [settingsStyles.row, settingsStyles.rowBorder]; + +// Right: each style proxy is read when this view renders. +<View style={[settingsStyles.row, settingsStyles.rowBorder]} />; +``` + +Paseo starts with adaptive themes, then applies the persisted theme after async settings load. A +module-level read can therefore materialize the light style before a persisted dark theme is +active. If the view mounts after that theme change, React Native receives the stale light object; +Unistyles registers the node for future changes but does not retroactively replace its initial +props. Settings dividers once rendered light `#e4e4e7` inside a dark `#252B2A` card for exactly +this reason. + +Render-time array syntax is intentional and exempt from the app's JSX array-allocation lint rule. +Keep the entries separate so each retains its Unistyles metadata. If composition is needed outside +JSX, create the array inside the component or in a `useMemo` that first runs when the component +mounts—never at module evaluation time. + [`useUnistyles()`](https://www.unistyl.es/v3/references/use-unistyles) is different. It gives React access to the current theme/runtime and can make a component re-render when those values change. Use it for values that must be rendered through React props, such as icon colors or small escape hatches. Do not expect direct reads from `UnistylesRuntime` to re-render a component; [issue #817](https://github.com/jpudysz/react-native-unistyles/issues/817) is a useful reminder of that invariant. ## Dynamic Pixel Styles On Web diff --git a/packages/app/src/agent-stream/view.tsx b/packages/app/src/agent-stream/view.tsx index a1354787f..c67092029 100644 --- a/packages/app/src/agent-stream/view.tsx +++ b/packages/app/src/agent-stream/view.tsx @@ -1234,7 +1234,9 @@ function PermissionActionButton({ onPress, }: PermissionActionButtonProps) { const handlePress = useCallback(() => onPress(action), [onPress, action]); - const optionTextStyle = isPrimary ? optionTextPrimaryStyle : permissionStyles.optionText; + const optionTextStyle = isPrimary + ? [permissionStyles.optionText, permissionStyles.optionTextPrimary] + : permissionStyles.optionText; const colorMapping = isPrimary ? primaryColorMapping : mutedColorMapping; return ( <Pressable testID={testID} style={pressableStyle} onPress={handlePress} disabled={isResponding}> @@ -1622,8 +1624,6 @@ const permissionStyles = StyleSheet.create((theme) => ({ }, })); -const optionTextPrimaryStyle = [permissionStyles.optionText, permissionStyles.optionTextPrimary]; - interface StreamItemWrapperProps { gapBelow: number; children: ReactNode; diff --git a/packages/app/src/app/pair-scan.tsx b/packages/app/src/app/pair-scan.tsx index c73dc6d93..4408ef33c 100644 --- a/packages/app/src/app/pair-scan.tsx +++ b/packages/app/src/app/pair-scan.tsx @@ -257,10 +257,10 @@ export default function PairScanScreen() { /> <View style={styles.overlay} pointerEvents="none"> <View style={styles.scanFrame}> - <View style={CORNER_TL_STYLE} /> - <View style={CORNER_TR_STYLE} /> - <View style={CORNER_BL_STYLE} /> - <View style={CORNER_BR_STYLE} /> + <View style={[styles.corner, styles.cornerTL]} /> + <View style={[styles.corner, styles.cornerTR]} /> + <View style={[styles.corner, styles.cornerBL]} /> + <View style={[styles.corner, styles.cornerBR]} /> </View> {isPairing ? <Text style={helperTextStyle}>{t("pairing.scan.pairing")}</Text> : null} </View> @@ -272,7 +272,3 @@ export default function PairScanScreen() { } const BARCODE_SCANNER_SETTINGS: BarcodeSettings = { barcodeTypes: ["qr"] }; -const CORNER_TL_STYLE = [styles.corner, styles.cornerTL]; -const CORNER_TR_STYLE = [styles.corner, styles.cornerTR]; -const CORNER_BL_STYLE = [styles.corner, styles.cornerBL]; -const CORNER_BR_STYLE = [styles.corner, styles.cornerBR]; diff --git a/packages/app/src/components/adaptive-modal-sheet.tsx b/packages/app/src/components/adaptive-modal-sheet.tsx index 68814e579..9b91a375e 100644 --- a/packages/app/src/components/adaptive-modal-sheet.tsx +++ b/packages/app/src/components/adaptive-modal-sheet.tsx @@ -230,7 +230,6 @@ const styles = StyleSheet.create((theme) => ({ }, })); -const SEARCH_INPUT_STYLE = [styles.searchInput, isWeb && { outlineStyle: "none" }]; const WEB_EXIT_DURATION_MS = 160; function SheetBackground({ style }: BottomSheetBackgroundProps) { @@ -373,7 +372,7 @@ export function SheetHeaderView({ <Search size={theme.iconSize.md} color={theme.colors.foregroundMuted} /> <AdaptiveTextInput // @ts-expect-error - outlineStyle is web-only - style={SEARCH_INPUT_STYLE} + style={[styles.searchInput, isWeb && { outlineStyle: "none" }]} placeholder={search.placeholder ?? t("common.actions.search")} resetKey={search.resetKey} onChangeText={handleSearchChange} @@ -430,7 +429,7 @@ export function InlineHeaderView({ header }: { header: SheetHeader }) { <Search size={theme.iconSize.sm} color={theme.colors.foregroundMuted} /> <AdaptiveTextInput // @ts-expect-error - outlineStyle is web-only - style={SEARCH_INPUT_STYLE} + style={[styles.searchInput, isWeb && { outlineStyle: "none" }]} placeholder={header.search.placeholder ?? t("common.actions.search")} resetKey={header.search.resetKey} onChangeText={header.search.onChange} diff --git a/packages/app/src/components/agent-list.tsx b/packages/app/src/components/agent-list.tsx index ea999652d..3746c4e63 100644 --- a/packages/app/src/components/agent-list.tsx +++ b/packages/app/src/components/agent-list.tsx @@ -544,7 +544,7 @@ export function AgentList({ </Text> <View style={styles.sheetButtonRow}> <Pressable - style={SHEET_CANCEL_BUTTON_STYLE} + style={[styles.sheetButton, styles.sheetCancelButton]} onPress={handleCloseActionSheet} testID="agent-action-cancel" > @@ -552,7 +552,7 @@ export function AgentList({ </Pressable> <Pressable disabled={isActionDaemonUnavailable} - style={SHEET_ARCHIVE_BUTTON_STYLE} + style={[styles.sheetButton, styles.sheetArchiveButton]} onPress={handleArchiveAgent} testID="agent-action-archive" > @@ -789,6 +789,3 @@ const styles = StyleSheet.create((theme) => ({ fontSize: theme.fontSize.base, }, })); - -const SHEET_CANCEL_BUTTON_STYLE = [styles.sheetButton, styles.sheetCancelButton]; -const SHEET_ARCHIVE_BUTTON_STYLE = [styles.sheetButton, styles.sheetArchiveButton]; diff --git a/packages/app/src/components/dictation-controls.tsx b/packages/app/src/components/dictation-controls.tsx index eb2b14a25..477717d4e 100644 --- a/packages/app/src/components/dictation-controls.tsx +++ b/packages/app/src/components/dictation-controls.tsx @@ -105,7 +105,7 @@ export function DictationControls({ <Pressable onPress={onRetry} accessibilityLabel={t("message.dictation.retry")} - style={ACTION_CONFIRM_STYLE} + style={[styles.actionButton, styles.actionButtonConfirm]} > <RefreshCcw size={theme.iconSize.sm} color={theme.colors.surface0} /> </Pressable> @@ -115,14 +115,14 @@ export function DictationControls({ <Pressable onPress={onAccept} accessibilityLabel={t("message.dictation.insert")} - style={ACTION_SECONDARY_STYLE} + style={[styles.actionButton, styles.actionButtonSecondary]} > <Check size={theme.iconSize.sm} color={theme.colors.foreground} /> </Pressable> <Pressable onPress={onAcceptAndSend} accessibilityLabel={t("message.dictation.insertAndSend")} - style={ACTION_CONFIRM_STYLE} + style={[styles.actionButton, styles.actionButtonConfirm]} > <ArrowUp size={theme.iconSize.sm} color={theme.colors.surface0} /> </Pressable> @@ -240,7 +240,7 @@ export function DictationOverlay({ onPress={onAccept} accessibilityRole="button" accessibilityLabel={t("message.dictation.insert")} - style={OVERLAY_ACCEPT_BUTTON_STYLE} + style={[overlayStyles.actionButton, OVERLAY_ACCEPT_BUTTON_BG]} > <Pencil size={theme.iconSize.lg} @@ -400,7 +400,4 @@ const overlayStyles = StyleSheet.create((theme) => ({ }, })); -const ACTION_CONFIRM_STYLE = [styles.actionButton, styles.actionButtonConfirm]; -const ACTION_SECONDARY_STYLE = [styles.actionButton, styles.actionButtonSecondary]; const OVERLAY_ACCEPT_BUTTON_BG = { backgroundColor: "rgba(255, 255, 255, 0.25)" }; -const OVERLAY_ACCEPT_BUTTON_STYLE = [overlayStyles.actionButton, OVERLAY_ACCEPT_BUTTON_BG]; diff --git a/packages/app/src/components/explorer-sidebar.tsx b/packages/app/src/components/explorer-sidebar.tsx index 8b6fa0e49..2d7209ae9 100644 --- a/packages/app/src/components/explorer-sidebar.tsx +++ b/packages/app/src/components/explorer-sidebar.tsx @@ -211,7 +211,7 @@ export function ExplorerSidebar({ return ( <Animated.View style={desktopSidebarStyle}> - <View style={DESKTOP_SIDEBAR_BORDER_STYLE}> + <View style={[styles.desktopSidebarBorder, { flex: 1 }]}> <SidebarResizeHandle edge="left" gesture={resizeGesture} @@ -516,5 +516,3 @@ const styles = StyleSheet.create((theme) => ({ minHeight: 0, }, })); - -const DESKTOP_SIDEBAR_BORDER_STYLE = [styles.desktopSidebarBorder, { flex: 1 }]; diff --git a/packages/app/src/components/file-explorer-pane.tsx b/packages/app/src/components/file-explorer-pane.tsx index 6707b417c..2a6f42464 100644 --- a/packages/app/src/components/file-explorer-pane.tsx +++ b/packages/app/src/components/file-explorer-pane.tsx @@ -575,7 +575,7 @@ function FileExplorerPaneContent(props: FileExplorerPaneContentProps) { } return ( - <View style={TREE_PANE_CONTAINER_STYLE}> + <View style={[styles.treePane, styles.treePaneFill]}> <View style={styles.paneHeader} testID="files-pane-header"> <Pressable onPress={handleSortCycle} style={sortTriggerStyleProp}> <Text style={styles.sortTriggerText}>{currentSortLabel}</Text> @@ -1253,5 +1253,3 @@ const styles = StyleSheet.create((theme) => ({ padding: theme.spacing[4], }, })); - -const TREE_PANE_CONTAINER_STYLE = [styles.treePane, styles.treePaneFill]; diff --git a/packages/app/src/components/provider-catalog-list.tsx b/packages/app/src/components/provider-catalog-list.tsx index f3e0835d0..a31e45be0 100644 --- a/packages/app/src/components/provider-catalog-list.tsx +++ b/packages/app/src/components/provider-catalog-list.tsx @@ -155,7 +155,7 @@ export function ProviderCatalogList({ accessibilityLabel={t("providerCatalog.search")} placeholder={t("providerCatalog.search")} // @ts-expect-error - outlineStyle is web-only - style={SEARCH_INPUT_STYLE} + style={[styles.searchInput, isWeb && { outlineStyle: "none" }]} autoCapitalize="none" autoCorrect={false} /> @@ -284,5 +284,3 @@ const styles = StyleSheet.create((theme) => ({ fontSize: theme.fontSize.sm, }, })); - -const SEARCH_INPUT_STYLE = [styles.searchInput, isWeb && { outlineStyle: "none" }]; diff --git a/packages/app/src/components/provider-diagnostic-sheet.tsx b/packages/app/src/components/provider-diagnostic-sheet.tsx index 9085801e0..b4831acac 100644 --- a/packages/app/src/components/provider-diagnostic-sheet.tsx +++ b/packages/app/src/components/provider-diagnostic-sheet.tsx @@ -225,7 +225,7 @@ function AddCustomModelSubSheet({ autoCorrect={false} returnKeyType="done" // @ts-expect-error - outlineStyle is web-only - style={FORM_INPUT_STYLE} + style={[sheetStyles.formInput, isWeb && { outlineStyle: "none" }]} /> {error ? <Text style={sheetStyles.errorText}>{error}</Text> : null} <View style={sheetStyles.formActions}> @@ -437,7 +437,9 @@ function renderProviderSheetFooter({ const contentStyle = isCompact ? sheetStyles.compactFooterContent : sheetStyles.footerContent; const actionsStyle = isCompact ? sheetStyles.compactFooterActions : sheetStyles.footerActions; const buttonStyle = isCompact ? sheetStyles.compactFooterButton : null; - const metaStyle = isCompact ? COMPACT_FOOTER_META_STYLE : sheetStyles.footerMeta; + const metaStyle = isCompact + ? [sheetStyles.footerMeta, sheetStyles.compactFooterMeta] + : sheetStyles.footerMeta; return ( <View style={contentStyle}> @@ -871,9 +873,6 @@ const sheetStyles = StyleSheet.create((theme) => ({ }, })); -const FORM_INPUT_STYLE = [sheetStyles.formInput, isWeb && { outlineStyle: "none" }]; -const COMPACT_FOOTER_META_STYLE = [sheetStyles.footerMeta, sheetStyles.compactFooterMeta]; - const MAIN_SNAP_POINTS = ["65%", "92%"]; const ADD_SNAP_POINTS = ["40%"]; const DIAGNOSTIC_SNAP_POINTS = ["50%", "85%"]; diff --git a/packages/app/src/components/sidebar-resize-handle.tsx b/packages/app/src/components/sidebar-resize-handle.tsx index 98f4e918b..617d9076b 100644 --- a/packages/app/src/components/sidebar-resize-handle.tsx +++ b/packages/app/src/components/sidebar-resize-handle.tsx @@ -21,7 +21,10 @@ const webResizeCursorStyle = isWeb export function SidebarResizeHandle({ edge, gesture, testID }: SidebarResizeHandleProps) { const [highlighted, setHighlighted] = useState(false); const highlightTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null); - const hitAreaStyle = edge === "left" ? LEFT_HIT_AREA_STYLE : RIGHT_HIT_AREA_STYLE; + const hitAreaStyle = + edge === "left" + ? [styles.hitArea, styles.leftEdge, webResizeCursorStyle] + : [styles.hitArea, styles.rightEdge, webResizeCursorStyle]; const cancelHighlightTimer = useCallback(() => { if (highlightTimerRef.current === null) return; @@ -84,6 +87,3 @@ const styles = StyleSheet.create((theme) => ({ opacity: 0.25, }, })); - -const LEFT_HIT_AREA_STYLE = [styles.hitArea, styles.leftEdge, webResizeCursorStyle]; -const RIGHT_HIT_AREA_STYLE = [styles.hitArea, styles.rightEdge, webResizeCursorStyle]; diff --git a/packages/app/src/components/sidebar-workspace-list.tsx b/packages/app/src/components/sidebar-workspace-list.tsx index ffef8dfd0..44e545ba1 100644 --- a/packages/app/src/components/sidebar-workspace-list.tsx +++ b/packages/app/src/components/sidebar-workspace-list.tsx @@ -315,7 +315,9 @@ export function PrBadge({ hint }: { hint: PrHint }) { const handleHoverIn = useCallback(() => setIsHovered(true), []); const handleHoverOut = useCallback(() => setIsHovered(false), []); - const textStyle = isHovered ? prBadgeTextHoveredCombined : prBadgeStyles.text; + const textStyle = isHovered + ? [prBadgeStyles.text, prBadgeStyles.textHovered] + : prBadgeStyles.text; const iconUniProps = isHovered ? foregroundColorMapping : getPrIconUniMapping(hint.state); const presentation = getForgePresentation(normalizeForge(hint.forge)); @@ -395,8 +397,6 @@ const prBadgeStyles = StyleSheet.create((theme) => ({ }, })); -const prBadgeTextHoveredCombined = [prBadgeStyles.text, prBadgeStyles.textHovered]; - function StatusDotOverlay({ dotColorStyle, size, diff --git a/packages/app/src/components/tool-call-details.tsx b/packages/app/src/components/tool-call-details.tsx index 9025f6a23..72ea48cef 100644 --- a/packages/app/src/components/tool-call-details.tsx +++ b/packages/app/src/components/tool-call-details.tsx @@ -711,7 +711,7 @@ function ErrorSection({ errorText, ds }: { errorText: string; ds: DetailStyles } const { t } = useTranslation(); return ( <View style={styles.section}> - <Text style={SECTION_TITLE_ERROR_STYLE}>{t("toolCallDetails.error")}</Text> + <Text style={[styles.sectionTitle, styles.errorText]}>{t("toolCallDetails.error")}</Text> <ScrollView horizontal nestedScrollEnabled @@ -719,7 +719,11 @@ function ErrorSection({ errorText, ds }: { errorText: string; ds: DetailStyles } contentContainerStyle={styles.jsonContent} showsHorizontalScrollIndicator={true} > - <Text selectable style={SCROLL_TEXT_ERROR_STYLE} dataSet={CODE_SURFACE_DATASET}> + <Text + selectable + style={[styles.scrollText, styles.errorText]} + dataSet={CODE_SURFACE_DATASET} + > {errorText} </Text> </ScrollView> @@ -948,6 +952,3 @@ const styles = StyleSheet.create((theme) => { }, }; }); - -const SECTION_TITLE_ERROR_STYLE = [styles.sectionTitle, styles.errorText]; -const SCROLL_TEXT_ERROR_STYLE = [styles.scrollText, styles.errorText]; diff --git a/packages/app/src/components/tree-primitives.tsx b/packages/app/src/components/tree-primitives.tsx index 26c71f2c2..11949c140 100644 --- a/packages/app/src/components/tree-primitives.tsx +++ b/packages/app/src/components/tree-primitives.tsx @@ -51,7 +51,7 @@ export function TreeIndentGuides({ depth }: { depth: number }) { /** Rotating disclosure chevron for a directory row (points right; rotates down when expanded). */ export function TreeChevron({ expanded }: { expanded: boolean }) { return ( - <View style={expanded ? CHEVRON_EXPANDED_STYLE : styles.chevron}> + <View style={expanded ? [styles.chevron, styles.chevronExpanded] : styles.chevron}> <ThemedChevronRight size={16} uniProps={foregroundMutedIconColorMapping} /> </View> ); @@ -76,7 +76,3 @@ const styles = StyleSheet.create((theme: Theme) => ({ transform: [{ rotate: "90deg" }], }, })); - -// Stable module-level style ref so TreeChevron passes a constant array, not one created -// per render — satisfies react-perf (no inline-array prop) without a per-render useMemo. -const CHEVRON_EXPANDED_STYLE = [styles.chevron, styles.chevronExpanded]; diff --git a/packages/app/src/components/ui/combobox.tsx b/packages/app/src/components/ui/combobox.tsx index 279954404..81c8f0ef0 100644 --- a/packages/app/src/components/ui/combobox.tsx +++ b/packages/app/src/components/ui/combobox.tsx @@ -199,7 +199,7 @@ export function SearchInput({ <AdaptiveTextInput ref={inputRef} // @ts-expect-error - outlineStyle is web-only - style={SEARCH_INPUT_STYLE} + style={[styles.searchInput, IS_WEB && { outlineStyle: "none" }]} placeholder={placeholder} resetKey={resetKey} onChangeText={onChangeText} @@ -212,7 +212,7 @@ export function SearchInput({ key={resetKey} ref={inputRef} // @ts-expect-error - outlineStyle is web-only - style={SEARCH_INPUT_STYLE} + style={[styles.searchInput, IS_WEB && { outlineStyle: "none" }]} placeholder={placeholder} placeholderTextColor={theme.colors.foregroundMuted} onChangeText={onChangeText} @@ -1711,5 +1711,3 @@ const styles = StyleSheet.create((theme) => ({ justifyContent: "flex-end", }, })); - -const SEARCH_INPUT_STYLE = [styles.searchInput, IS_WEB && { outlineStyle: "none" }]; diff --git a/packages/app/src/components/workspace-hover-card.tsx b/packages/app/src/components/workspace-hover-card.tsx index ca2930c25..37cd114fc 100644 --- a/packages/app/src/components/workspace-hover-card.tsx +++ b/packages/app/src/components/workspace-hover-card.tsx @@ -418,7 +418,8 @@ function CopyableInfoRow({ if (copied || isHovered) { iconUniProps = foregroundColorMapping; } - const textStyle = copied || isHovered ? cardInfoTextHoveredCombined : styles.cardInfoText; + const textStyle = + copied || isHovered ? [styles.cardInfoText, styles.cardInfoTextHovered] : styles.cardInfoText; return ( <Pressable @@ -506,7 +507,9 @@ function ChecksSummaryContent({ const { t } = useTranslation(); const { passed, failed, pending } = getChecksSummaryCounts(checks); - const labelStyle = hovered ? checksSummaryLabelHoveredCombined : styles.checksSummaryLabel; + const labelStyle = hovered + ? [styles.checksSummaryLabel, styles.checksSummaryLabelHovered] + : styles.checksSummaryLabel; const iconUniProps = hovered ? foregroundColorMapping : foregroundMutedColorMapping; const icon = getForgePresentation(normalizeForge(forge)).icon; @@ -670,10 +673,3 @@ const styles = StyleSheet.create((theme) => ({ color: theme.colors.statusSuccess, }, })); - -const checksSummaryLabelHoveredCombined = [ - styles.checksSummaryLabel, - styles.checksSummaryLabelHovered, -]; - -const cardInfoTextHoveredCombined = [styles.cardInfoText, styles.cardInfoTextHovered]; diff --git a/packages/app/src/composer/index.tsx b/packages/app/src/composer/index.tsx index 6faa7a5e5..c2e3e5dbf 100644 --- a/packages/app/src/composer/index.tsx +++ b/packages/app/src/composer/index.tsx @@ -570,7 +570,7 @@ function QueuedMessageRow({ </Pressable> <Pressable onPress={handleSendNow} - style={QUEUE_SEND_BUTTON_STYLE} + style={[styles.queueActionButton, styles.queueSendButton]} accessibilityLabel={sendNowLabel} accessibilityRole="button" > @@ -2237,8 +2237,6 @@ const styles = StyleSheet.create((theme: Theme) => ({ }, })) as unknown as Record<string, object>; -const QUEUE_SEND_BUTTON_STYLE = [styles.queueActionButton, styles.queueSendButton]; - const ThemedPencil = withUnistyles(Pencil); const ThemedArrowUp = withUnistyles(ArrowUp); const ThemedGitPullRequest = withUnistyles(GitPullRequest); diff --git a/packages/app/src/desktop/components/desktop-updates-section.tsx b/packages/app/src/desktop/components/desktop-updates-section.tsx index 9725a1848..5fa4685c9 100644 --- a/packages/app/src/desktop/components/desktop-updates-section.tsx +++ b/packages/app/src/desktop/components/desktop-updates-section.tsx @@ -246,7 +246,7 @@ function DaemonInfoCard(props: DaemonInfoCardProps) { <Text style={styles.valueSubtext}>{daemonStatusDetailText}</Text> </View> </View> - <View style={ROW_WITH_BORDER_STYLE}> + <View style={[settingsStyles.row, settingsStyles.rowBorder]}> <View style={settingsStyles.rowContent}> <Text style={settingsStyles.rowTitle}>{t("desktop.daemon.management.title")}</Text> <Text style={settingsStyles.rowHint}>{t("desktop.daemon.management.hint")}</Text> @@ -258,7 +258,7 @@ function DaemonInfoCard(props: DaemonInfoCardProps) { accessibilityLabel={t("desktop.daemon.management.title")} /> </View> - <View style={ROW_WITH_BORDER_STYLE}> + <View style={[settingsStyles.row, settingsStyles.rowBorder]}> <View style={settingsStyles.rowContent}> <Text style={settingsStyles.rowTitle}>{t("desktop.daemon.keepRunning.title")}</Text> <Text style={settingsStyles.rowHint}>{t("desktop.daemon.keepRunning.hint")}</Text> @@ -270,7 +270,7 @@ function DaemonInfoCard(props: DaemonInfoCardProps) { accessibilityLabel={t("desktop.daemon.keepRunning.title")} /> </View> - <View style={ROW_WITH_BORDER_STYLE}> + <View style={[settingsStyles.row, settingsStyles.rowBorder]}> <View style={settingsStyles.rowContent}> <Text style={settingsStyles.rowTitle}>{t("desktop.daemon.logs.title")}</Text> <Text style={settingsStyles.rowHint}> @@ -294,7 +294,7 @@ function DaemonInfoCard(props: DaemonInfoCardProps) { </Button> </View> </View> - <View style={ROW_WITH_BORDER_STYLE}> + <View style={[settingsStyles.row, settingsStyles.rowBorder]}> <View style={settingsStyles.rowContent}> <Text style={settingsStyles.rowTitle}>{t("desktop.daemon.fullStatus.title")}</Text> <Text style={settingsStyles.rowHint}>{t("desktop.daemon.fullStatus.hint")}</Text> @@ -420,7 +420,7 @@ export function LocalDaemonSection() { testID="host-page-daemon-lifecycle-card" > {isLoading || isLoadingSettings ? ( - <View style={LOADING_CARD_STYLE}> + <View style={[settingsStyles.card, styles.loadingCard]}> <ActivityIndicator size="small" color={theme.colors.foregroundMuted} /> </View> ) : ( @@ -524,7 +524,5 @@ const styles = StyleSheet.create((theme) => ({ }, })); -const LOADING_CARD_STYLE = [settingsStyles.card, styles.loadingCard]; -const ROW_WITH_BORDER_STYLE = [settingsStyles.row, settingsStyles.rowBorder]; const LOGS_MODAL_SNAP_POINTS = ["70%", "92%"]; const CLI_STATUS_MODAL_SNAP_POINTS = ["60%", "85%"]; diff --git a/packages/app/src/desktop/components/integrations-section.tsx b/packages/app/src/desktop/components/integrations-section.tsx index 531172eb3..01d0f5a8b 100644 --- a/packages/app/src/desktop/components/integrations-section.tsx +++ b/packages/app/src/desktop/components/integrations-section.tsx @@ -19,8 +19,6 @@ import { useCliInstall, useSkillsStatus } from "@/desktop/hooks/use-install-stat const CLI_DOCS_URL = "https://paseo.sh/docs/cli"; const SKILLS_DOCS_URL = "https://paseo.sh/docs/skills"; -const ROW_WITH_BORDER_STYLE = [settingsStyles.row, settingsStyles.rowBorder]; - const OP_KIND_ORDER: Record<SkillOp["kind"], number> = { add: 0, update: 1, delete: 2 }; const OP_KIND_LABEL_KEY: Record<SkillOp["kind"], string> = { add: "settings.integrations.operations.add", @@ -183,7 +181,7 @@ export function IntegrationsSection() { </Button> )} </View> - <View style={ROW_WITH_BORDER_STYLE}> + <View style={[settingsStyles.row, settingsStyles.rowBorder]}> <View style={settingsStyles.rowContent}> <View style={styles.rowTitleRow}> <Blocks size={theme.iconSize.md} color={theme.colors.foreground} /> diff --git a/packages/app/src/git/diff-pane.tsx b/packages/app/src/git/diff-pane.tsx index 27db92cd4..5a1d2e007 100644 --- a/packages/app/src/git/diff-pane.tsx +++ b/packages/app/src/git/diff-pane.tsx @@ -1091,7 +1091,11 @@ export function DiffFileBody({ ); return ( - <View style={FILE_SECTION_BODY_STYLE} onLayout={handleLayout} testID={testID}> + <View + style={[styles.fileSectionBodyContainer, styles.fileSectionBorder]} + onLayout={handleLayout} + testID={testID} + > {(() => { if (file.status === "too_large" || file.status === "binary") { return ( @@ -1118,7 +1122,7 @@ export function DiffFileBody({ if (layout === "split") { const rows = buildSplitDiffRows(file); return ( - <View style={DIFF_CONTENT_SPLIT_ROW_STYLE} dataSet={CODE_SURFACE_DATASET}> + <View style={[styles.diffContent, styles.splitRow]} dataSet={CODE_SURFACE_DATASET}> <SplitDiffColumn rows={rows} side="left" @@ -1172,7 +1176,7 @@ export function DiffFileBody({ const textViewportWidth = scrollViewWidth > 0 ? scrollViewWidth : Math.max(0, bodyWidth - gutterWidth); return ( - <View style={DIFF_CONTENT_ROW_STYLE} dataSet={CODE_SURFACE_DATASET}> + <View style={[styles.diffContent, styles.diffContentRow]} dataSet={CODE_SURFACE_DATASET}> <View style={styles.gutterColumn}> {computedLines.map(({ line, lineNumber, key, reviewTarget }, index) => ( <View key={key} testID={`diff-gutter-row-${index}`}> @@ -3062,7 +3066,4 @@ const styles = StyleSheet.create((theme) => ({ }, })); -const FILE_SECTION_BODY_STYLE = [styles.fileSectionBodyContainer, styles.fileSectionBorder]; -const DIFF_CONTENT_SPLIT_ROW_STYLE = [styles.diffContent, styles.splitRow]; -const DIFF_CONTENT_ROW_STYLE = [styles.diffContent, styles.diffContentRow]; const DIFF_HEIGHT_CHANGE_EPSILON = 0.5; diff --git a/packages/app/src/provider-usage/settings-section.tsx b/packages/app/src/provider-usage/settings-section.tsx index 58da33fff..51da2d710 100644 --- a/packages/app/src/provider-usage/settings-section.tsx +++ b/packages/app/src/provider-usage/settings-section.tsx @@ -55,7 +55,7 @@ function ProviderUsageBody({ }) { if (view.kind === "loading") { return ( - <View style={EMPTY_CARD_STYLE}> + <View style={[settingsStyles.card, styles.emptyCard]}> <Text style={styles.emptyText}>{providerUsageCopy.loading}</Text> </View> ); @@ -73,7 +73,7 @@ function ProviderUsageBody({ if (view.payload.providers.length === 0) { return ( - <View style={EMPTY_CARD_STYLE}> + <View style={[settingsStyles.card, styles.emptyCard]}> <Text style={styles.emptyText}>{providerUsageCopy.empty}</Text> </View> ); @@ -92,5 +92,3 @@ const styles = StyleSheet.create((theme) => ({ fontSize: theme.fontSize.sm, }, })); - -const EMPTY_CARD_STYLE = [settingsStyles.card, styles.emptyCard]; diff --git a/packages/app/src/screens/settings-screen.tsx b/packages/app/src/screens/settings-screen.tsx index 5eefdf6fa..dcbaf1fa3 100644 --- a/packages/app/src/screens/settings-screen.tsx +++ b/packages/app/src/screens/settings-screen.tsx @@ -210,8 +210,6 @@ function selectedSidebarItemStyle({ hovered }: PressableStateCallbackType & { ho ]; } -const ROW_WITH_BORDER_STYLE = [settingsStyles.row, settingsStyles.rowBorder]; - function getSendBehaviorOptions(t: TFunction) { return [ { value: "interrupt" as const, label: t("settings.general.defaultSend.options.interrupt") }, @@ -360,7 +358,7 @@ function GeneralSection({ options={sendBehaviorOptions} /> </View> - <View style={ROW_WITH_BORDER_STYLE}> + <View style={[settingsStyles.row, settingsStyles.rowBorder]}> <View style={settingsStyles.rowContent}> <Text style={settingsStyles.rowTitle}>{t("settings.general.language.label")}</Text> <Text style={settingsStyles.rowHint}>{t("settings.general.language.description")}</Text> @@ -387,7 +385,7 @@ function GeneralSection({ </DropdownMenu> </View> {isDesktopApp ? ( - <View style={ROW_WITH_BORDER_STYLE}> + <View style={[settingsStyles.row, settingsStyles.rowBorder]}> <View style={settingsStyles.rowContent}> <Text style={settingsStyles.rowTitle}>{t("settings.general.serviceUrls.label")}</Text> <Text style={settingsStyles.rowHint}> @@ -414,7 +412,7 @@ function GeneralSection({ </DropdownMenu> </View> ) : null} - <View style={ROW_WITH_BORDER_STYLE}> + <View style={[settingsStyles.row, settingsStyles.rowBorder]}> <View style={settingsStyles.rowContent}> <Text style={settingsStyles.rowTitle}> {t("settings.general.terminalScrollback.label")} @@ -700,7 +698,7 @@ function DesktopAppUpdateRow() { return ( <> - <View style={ROW_WITH_BORDER_STYLE}> + <View style={[settingsStyles.row, settingsStyles.rowBorder]}> <View style={settingsStyles.rowContent}> <Text style={settingsStyles.rowTitle}>{t("settings.about.releaseChannel.label")}</Text> <Text style={settingsStyles.rowHint}> @@ -714,7 +712,7 @@ function DesktopAppUpdateRow() { options={releaseChannelOptions} /> </View> - <View style={ROW_WITH_BORDER_STYLE}> + <View style={[settingsStyles.row, settingsStyles.rowBorder]}> <View style={settingsStyles.rowContent}> <Text style={settingsStyles.rowTitle}>{t("settings.about.updates.label")}</Text> <Text style={settingsStyles.rowHint}>{statusText}</Text> diff --git a/packages/app/src/screens/settings/appearance/appearance-section.tsx b/packages/app/src/screens/settings/appearance/appearance-section.tsx index 4ae7b2886..3f8a41fe0 100644 --- a/packages/app/src/screens/settings/appearance/appearance-section.tsx +++ b/packages/app/src/screens/settings/appearance/appearance-section.tsx @@ -212,7 +212,6 @@ function AutoExpandReasoningRow({ value, onChange }: AutoExpandReasoningRowProps ); } -const TOOL_CALL_DETAIL_ROW_STYLE = [settingsStyles.row, settingsStyles.rowBorder]; const TOOL_CALL_DETAIL_LEVELS: readonly AppSettings["toolCallDetailLevel"][] = [ "detailed", "overview", @@ -250,7 +249,7 @@ function ToolCallDetailRow({ value, onChange }: ToolCallDetailRowProps) { const { t } = useTranslation(); const selectedLabel = getToolCallDetailLevelLabel(t, value); return ( - <View style={TOOL_CALL_DETAIL_ROW_STYLE}> + <View style={[settingsStyles.row, settingsStyles.rowBorder]}> <View style={settingsStyles.rowContent}> <Text style={settingsStyles.rowTitle}>{t("settings.general.toolCallDetail.label")}</Text> <Text style={settingsStyles.rowHint}> diff --git a/packages/app/src/screens/settings/host-page.tsx b/packages/app/src/screens/settings/host-page.tsx index e2a62970f..4fde68367 100644 --- a/packages/app/src/screens/settings/host-page.tsx +++ b/packages/app/src/screens/settings/host-page.tsx @@ -150,7 +150,7 @@ function HostNotFound() { const { t } = useTranslation(); return ( <View> - <View style={EMPTY_CARD_STYLE}> + <View style={[settingsStyles.card, styles.emptyCard]}> <Text style={styles.emptyText}>{t("settings.host.notFound")}</Text> </View> </View> @@ -276,7 +276,7 @@ export function HostAgentsPage({ serverId }: { serverId: string }) { <AppendSystemPromptCard serverId={serverId} /> </SettingsSection> ) : ( - <View style={EMPTY_CARD_STYLE}> + <View style={[settingsStyles.card, styles.emptyCard]}> <Text style={styles.emptyText}>{t("settings.host.agents.unavailable")}</Text> </View> )} @@ -300,7 +300,7 @@ export function HostWorkspacesPage({ serverId }: { serverId: string }) { <AutoArchiveMergedWorkspacesCard serverId={serverId} /> </SettingsSection> ) : ( - <View style={EMPTY_CARD_STYLE}> + <View style={[settingsStyles.card, styles.emptyCard]}> <Text style={styles.emptyText}>{t("settings.host.workspaces.unavailable")}</Text> </View> )} @@ -1940,4 +1940,3 @@ const styles = StyleSheet.create((theme) => ({ })); const FLEX_1_STYLE = { flex: 1 }; -const EMPTY_CARD_STYLE = [settingsStyles.card, styles.emptyCard]; diff --git a/packages/app/src/screens/settings/keyboard-shortcuts-section.tsx b/packages/app/src/screens/settings/keyboard-shortcuts-section.tsx index de1323df5..b81dcb4ed 100644 --- a/packages/app/src/screens/settings/keyboard-shortcuts-section.tsx +++ b/packages/app/src/screens/settings/keyboard-shortcuts-section.tsx @@ -264,7 +264,7 @@ export function KeyboardShortcutsSection() { if (isNative) { return ( <SettingsSection title={t("settings.sections.shortcuts")}> - <View style={mobileCardStyle}> + <View style={[settingsStyles.card, styles.mobileCard]}> <Text style={styles.mobileText}>{t("settings.shortcuts.unavailableOnMobile")}</Text> </View> </SettingsSection> @@ -362,5 +362,3 @@ const styles = StyleSheet.create((theme) => ({ color: theme.colors.foregroundMuted, }, })); - -const mobileCardStyle = [settingsStyles.card, styles.mobileCard]; diff --git a/packages/app/src/screens/settings/providers-section.tsx b/packages/app/src/screens/settings/providers-section.tsx index a21ac56a8..2d951c954 100644 --- a/packages/app/src/screens/settings/providers-section.tsx +++ b/packages/app/src/screens/settings/providers-section.tsx @@ -410,12 +410,12 @@ export function ProvidersSection({ serverId }: ProvidersSectionProps) { style={styles.sectionSpacing} > {!hasServer || !isConnected ? ( - <View style={EMPTY_CARD_STYLE}> + <View style={[settingsStyles.card, styles.emptyCard]}> <Text style={styles.emptyText}>{t("settings.providers.unavailable")}</Text> </View> ) : null} {hasServer && isConnected && isLoading ? ( - <View style={EMPTY_CARD_STYLE}> + <View style={[settingsStyles.card, styles.emptyCard]}> <Text style={styles.emptyText}>{t("settings.providers.loading")}</Text> </View> ) : null} @@ -543,5 +543,3 @@ const styles = StyleSheet.create((theme) => ({ backgroundColor: theme.colors.surface3, }, })); - -const EMPTY_CARD_STYLE = [settingsStyles.card, styles.emptyCard]; diff --git a/packages/app/src/screens/workspace/workspace-desktop-tabs-row.tsx b/packages/app/src/screens/workspace/workspace-desktop-tabs-row.tsx index 3180bbc44..bee528e26 100644 --- a/packages/app/src/screens/workspace/workspace-desktop-tabs-row.tsx +++ b/packages/app/src/screens/workspace/workspace-desktop-tabs-row.tsx @@ -1165,7 +1165,9 @@ function ResolvedDesktopTabChip({ return ( <View style={styles.tabSlot}> - {showDropIndicatorBefore ? <View style={TAB_DROP_INDICATOR_BEFORE_STYLE} /> : null} + {showDropIndicatorBefore ? ( + <View style={[styles.tabDropIndicator, styles.tabDropIndicatorBefore]} /> + ) : null} <TabChip tab={item.tab} isActive={item.isActive} @@ -1184,7 +1186,9 @@ function ResolvedDesktopTabChip({ onCloseTab={onCloseTab} dragHandleProps={dragHandleProps} /> - {showDropIndicatorAfter ? <View style={TAB_DROP_INDICATOR_AFTER_STYLE} /> : null} + {showDropIndicatorAfter ? ( + <View style={[styles.tabDropIndicator, styles.tabDropIndicatorAfter]} /> + ) : null} </View> ); }} @@ -1375,6 +1379,3 @@ const styles = StyleSheet.create((theme) => ({ height: 14, }, })); - -const TAB_DROP_INDICATOR_BEFORE_STYLE = [styles.tabDropIndicator, styles.tabDropIndicatorBefore]; -const TAB_DROP_INDICATOR_AFTER_STYLE = [styles.tabDropIndicator, styles.tabDropIndicatorAfter]; diff --git a/packages/app/src/screens/workspace/workspace-screen.tsx b/packages/app/src/screens/workspace/workspace-screen.tsx index 16cf2b0cc..828eef184 100644 --- a/packages/app/src/screens/workspace/workspace-screen.tsx +++ b/packages/app/src/screens/workspace/workspace-screen.tsx @@ -3341,7 +3341,7 @@ function WorkspaceScreenContent({ [t], ); - const containerStyle = containerWithWorkspaceBackgroundStyle; + const containerStyle = [styles.container, styles.containerWorkspaceBackground]; const menuNewAgentIcon = MENU_NEW_AGENT_ICON; const menuNewTerminalIcon = MENU_NEW_TERMINAL_ICON; @@ -4090,9 +4090,4 @@ const styles = StyleSheet.create((theme) => ({ }, })); -const containerWithWorkspaceBackgroundStyle = [ - styles.container, - styles.containerWorkspaceBackground, -]; - const EXPLORER_TOGGLE_KEYS: ShortcutKey[] = ["mod", "E"]; diff --git a/packages/app/src/screens/workspace/workspace-scripts-button.tsx b/packages/app/src/screens/workspace/workspace-scripts-button.tsx index 4d3b25ec6..d310d759c 100644 --- a/packages/app/src/screens/workspace/workspace-scripts-button.tsx +++ b/packages/app/src/screens/workspace/workspace-scripts-button.tsx @@ -88,7 +88,9 @@ function ScriptActionButtonChildren({ ) : ( <ThemedPlay size={10} uniProps={colorMapping} {...playFillTransparent} /> ); - const labelStyle = hovered ? actionButtonLabelHoveredStyle : styles.actionButtonLabel; + const labelStyle = hovered + ? [styles.actionButtonLabel, styles.actionButtonLabelHovered] + : styles.actionButtonLabel; return ( <> {iconElement} @@ -156,7 +158,7 @@ function HostLinkChildren({ hovered, disabled, label }: HostLinkChildrenProps): const showIcon = !disabled && (hovered || isNative); const isActive = Boolean(hovered) && !disabled; const colorMapping = isActive ? foregroundColorMapping : mutedColorMapping; - const hostLabelStyle = isActive ? hostLabelActiveStyle : styles.hostLabel; + const hostLabelStyle = isActive ? [styles.hostLabel, styles.hostLabelActive] : styles.hostLabel; return ( <> <Text style={hostLabelStyle} numberOfLines={1}> @@ -207,7 +209,8 @@ function HostLinkRow({ label, url, scriptName, onOpenInBrowserTab }: HostLinkPro function ExitCodeBadge({ code }: { code: number }): ReactElement { const { t } = useTranslation(); - const exitTextStyle = code === 0 ? styles.exitBadgeText : exitBadgeTextErrorStyle; + const exitTextStyle = + code === 0 ? styles.exitBadgeText : [styles.exitBadgeText, styles.exitBadgeTextError]; return ( <View style={styles.exitBadge}> <Text style={exitTextStyle}>{t("workspace.scripts.states.exitCode", { code })}</Text> @@ -305,7 +308,7 @@ function ScriptRow({ }, [onStartScript, script.scriptName]); const scriptNameStyle = useMemo( - () => (isRunning ? scriptNameActiveStyle : styles.scriptName), + () => (isRunning ? [styles.scriptName, styles.scriptNameActive] : styles.scriptName), [isRunning], ); @@ -631,8 +634,3 @@ const styles = StyleSheet.create((theme) => ({ color: theme.colors.foreground, }, })); - -const actionButtonLabelHoveredStyle = [styles.actionButtonLabel, styles.actionButtonLabelHovered]; -const hostLabelActiveStyle = [styles.hostLabel, styles.hostLabelActive]; -const scriptNameActiveStyle = [styles.scriptName, styles.scriptNameActive]; -const exitBadgeTextErrorStyle = [styles.exitBadgeText, styles.exitBadgeTextError]; diff --git a/packages/app/src/styles/unistyles-module-scope.test.ts b/packages/app/src/styles/unistyles-module-scope.test.ts new file mode 100644 index 000000000..714875d97 --- /dev/null +++ b/packages/app/src/styles/unistyles-module-scope.test.ts @@ -0,0 +1,76 @@ +import { readdirSync, readFileSync } from "node:fs"; +import path from "node:path"; +import * as ts from "typescript"; +import { describe, expect, it } from "vitest"; + +const SOURCE_ROOT = path.resolve(__dirname, ".."); + +function listSourceFiles(directory: string): string[] { + return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => { + const entryPath = path.join(directory, entry.name); + if (entry.isDirectory()) { + return listSourceFiles(entryPath); + } + return /\.tsx?$/.test(entry.name) ? [entryPath] : []; + }); +} + +function containsEagerStyleRead(initializer: ts.Expression): boolean { + if (ts.isFunctionLike(initializer)) { + return false; + } + + let found = false; + + function visit(node: ts.Node): void { + if (node !== initializer && ts.isFunctionLike(node)) { + return; + } + if ( + ts.isPropertyAccessExpression(node) && + ts.isIdentifier(node.expression) && + node.expression.text !== "StyleSheet" && + /styles$/i.test(node.expression.text) + ) { + found = true; + return; + } + ts.forEachChild(node, visit); + } + + visit(initializer); + return found; +} + +function findEagerModuleStyleReads(filePath: string): string[] { + const source = readFileSync(filePath, "utf8"); + const sourceFile = ts.createSourceFile( + filePath, + source, + ts.ScriptTarget.Latest, + true, + filePath.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS, + ); + + return sourceFile.statements.flatMap((statement) => { + if (!ts.isVariableStatement(statement)) { + return []; + } + return statement.declarationList.declarations.flatMap((declaration) => { + if (!declaration.initializer || !containsEagerStyleRead(declaration.initializer)) { + return []; + } + const line = + sourceFile.getLineAndCharacterOfPosition(declaration.getStart(sourceFile)).line + 1; + return [`${path.relative(SOURCE_ROOT, filePath)}:${line}`]; + }); + }); +} + +describe("Unistyles module scope", () => { + it("does not materialize style proxies before the persisted theme loads", () => { + const violations = listSourceFiles(SOURCE_ROOT).flatMap(findEagerModuleStyleReads); + + expect(violations).toEqual([]); + }); +}); From 2185779d6c51137006f6f52256c60dce118efae4 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra <boudra.moha@gmail.com> Date: Sat, 18 Jul 2026 18:45:23 +0200 Subject: [PATCH 33/38] fix(settings): highlight changes that apply next turn (#2201) Permission and thinking changes made during an active turn do not affect that turn. Use setting-specific warnings so the timing is harder to miss. --- packages/app/src/components/toast-host.tsx | 18 +++++++++++++++--- .../app/src/utils/provider-notice-toast.ts | 4 ++-- .../src/server/agent/provider-notices.ts | 11 ++++++++--- .../agent/providers/claude/agent.test.ts | 4 ++-- .../src/server/agent/providers/claude/agent.ts | 4 ++-- .../providers/codex-app-server-agent.test.ts | 8 ++++---- .../agent/providers/codex-app-server-agent.ts | 9 ++++++--- .../server/agent/providers/omp/agent.test.ts | 5 ++++- .../src/server/agent/providers/omp/agent.ts | 5 ++--- 9 files changed, 45 insertions(+), 23 deletions(-) diff --git a/packages/app/src/components/toast-host.tsx b/packages/app/src/components/toast-host.tsx index 082a03787..e275f0bb0 100644 --- a/packages/app/src/components/toast-host.tsx +++ b/packages/app/src/components/toast-host.tsx @@ -6,7 +6,7 @@ import { StyleSheet, useUnistyles } from "react-native-unistyles"; import { useTranslation } from "react-i18next"; import { useIsCompactFormFactor } from "@/constants/layout"; import { isWeb } from "@/constants/platform"; -import { AlertTriangle, CheckCircle2 } from "lucide-react-native"; +import { AlertTriangle, CheckCircle2, Info } from "lucide-react-native"; import { getOverlayRoot, OVERLAY_Z } from "@/lib/overlay-root"; import { HEADER_INNER_HEIGHT, @@ -14,7 +14,7 @@ import { HEADER_TOP_PADDING_MOBILE, } from "@/constants/layout"; -export type ToastVariant = "default" | "success" | "error"; +export type ToastVariant = "default" | "info" | "success" | "warning" | "error"; export interface ToastShowOptions { icon?: ReactNode; @@ -230,7 +230,9 @@ export function ToastViewport({ const toastAnimatedStyle = useMemo( () => [ styles.toast, + toastVariant === "info" ? styles.toastInfo : null, toastVariant === "success" ? styles.toastSuccess : null, + toastVariant === "warning" ? styles.toastWarning : null, toastVariant === "error" ? styles.toastError : null, { marginTop: topOffset, @@ -250,8 +252,12 @@ export function ToastViewport({ } let defaultIcon: ReactNode = null; - if (toast.variant === "success") { + if (toast.variant === "info") { + defaultIcon = <Info size={18} color={theme.colors.palette.blue[300]} />; + } else if (toast.variant === "success") { defaultIcon = <CheckCircle2 size={18} color={theme.colors.primary} />; + } else if (toast.variant === "warning") { + defaultIcon = <AlertTriangle size={18} color={theme.colors.palette.amber[500]} />; } else if (toast.variant === "error") { defaultIcon = <AlertTriangle size={18} color={theme.colors.destructive} />; } @@ -313,6 +319,12 @@ const styles = StyleSheet.create((theme) => ({ toastSuccess: { borderColor: theme.colors.border, }, + toastInfo: { + borderColor: theme.colors.palette.blue[300], + }, + toastWarning: { + borderColor: theme.colors.palette.amber[500], + }, toastError: { borderColor: theme.colors.destructive, }, diff --git a/packages/app/src/utils/provider-notice-toast.ts b/packages/app/src/utils/provider-notice-toast.ts index 478f929f8..7c3ace8b5 100644 --- a/packages/app/src/utils/provider-notice-toast.ts +++ b/packages/app/src/utils/provider-notice-toast.ts @@ -13,7 +13,7 @@ export function showProviderNoticeToast( return; } toast.show(notice.message, { - variant: "default", - durationMs: notice.type === "warning" ? 3200 : undefined, + variant: notice.type, + durationMs: notice.type === "warning" ? 5000 : undefined, }); } diff --git a/packages/server/src/server/agent/provider-notices.ts b/packages/server/src/server/agent/provider-notices.ts index 921f70a50..d2a9c3af5 100644 --- a/packages/server/src/server/agent/provider-notices.ts +++ b/packages/server/src/server/agent/provider-notices.ts @@ -1,6 +1,11 @@ import type { AgentProviderNotice } from "./agent-sdk-types.js"; -export const SETTING_APPLIES_NEXT_TURN_NOTICE: AgentProviderNotice = { - type: "info", - message: "This change applies next turn.", +export const MODE_APPLIES_NEXT_TURN_NOTICE: AgentProviderNotice = { + type: "warning", + message: "Permission mode applies next turn", +}; + +export const THINKING_APPLIES_NEXT_TURN_NOTICE: AgentProviderNotice = { + type: "warning", + message: "Thinking level applies next turn", }; diff --git a/packages/server/src/server/agent/providers/claude/agent.test.ts b/packages/server/src/server/agent/providers/claude/agent.test.ts index 20f8f5235..bd535ac13 100644 --- a/packages/server/src/server/agent/providers/claude/agent.test.ts +++ b/packages/server/src/server/agent/providers/claude/agent.test.ts @@ -704,8 +704,8 @@ describe("ClaudeAgentSession features", () => { }); await expect(session.setThinkingOption?.("ultracode")).resolves.toEqual({ - type: "info", - message: "This change applies next turn.", + type: "warning", + message: "Thinking level applies next turn", }); await session.close(); diff --git a/packages/server/src/server/agent/providers/claude/agent.ts b/packages/server/src/server/agent/providers/claude/agent.ts index 99a6acfe2..a0403f2e6 100644 --- a/packages/server/src/server/agent/providers/claude/agent.ts +++ b/packages/server/src/server/agent/providers/claude/agent.ts @@ -50,7 +50,7 @@ import { claudeQuery, type ClaudeOptions, type ClaudeQueryFactory } from "./quer import { realClaudeRewindSdk, revertClaudeConversation, revertClaudeFiles } from "./rewind.js"; import { normalizeProviderReplayTimestamp } from "../../provider-history-timestamps.js"; import { claudeProjectDirSync } from "./project-dir.js"; -import { SETTING_APPLIES_NEXT_TURN_NOTICE } from "../../provider-notices.js"; +import { THINKING_APPLIES_NEXT_TURN_NOTICE } from "../../provider-notices.js"; import { isProviderImageMarkdown, materializeProviderImage, @@ -2209,7 +2209,7 @@ class ClaudeAgentSession implements AgentSession { } this.queryRestartNeeded = true; if (this.activeForegroundTurnId || this.autonomousTurn) { - return SETTING_APPLIES_NEXT_TURN_NOTICE; + return THINKING_APPLIES_NEXT_TURN_NOTICE; } } diff --git a/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts b/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts index 569370cae..7a585f0ce 100644 --- a/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts +++ b/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts @@ -347,12 +347,12 @@ describe("Codex app-server provider", () => { const session = createSession({ modeId: "auto", thinkingOptionId: "medium" }); await expect(session.setMode("full-access")).resolves.toEqual({ - type: "info", - message: "This change applies next turn.", + type: "warning", + message: "Permission mode applies next turn", }); await expect(session.setThinkingOption?.("high")).resolves.toEqual({ - type: "info", - message: "This change applies next turn.", + type: "warning", + message: "Thinking level applies next turn", }); session.activeForegroundTurnId = null; diff --git a/packages/server/src/server/agent/providers/codex-app-server-agent.ts b/packages/server/src/server/agent/providers/codex-app-server-agent.ts index fd6b7b68e..f816e33b0 100644 --- a/packages/server/src/server/agent/providers/codex-app-server-agent.ts +++ b/packages/server/src/server/agent/providers/codex-app-server-agent.ts @@ -92,7 +92,10 @@ import { resolveBinaryVersion, } from "./diagnostic-utils.js"; import { appendOrReplaceGrowingAssistantMessage, runProviderTurn } from "./provider-runner.js"; -import { SETTING_APPLIES_NEXT_TURN_NOTICE } from "../provider-notices.js"; +import { + MODE_APPLIES_NEXT_TURN_NOTICE, + THINKING_APPLIES_NEXT_TURN_NOTICE, +} from "../provider-notices.js"; import type { WorkspaceGitService } from "../../workspace-git-service.js"; function assertChildWithPipes( @@ -3943,7 +3946,7 @@ export class CodexAppServerAgentSession implements AgentSession { this.currentMode = modeId; this.cachedRuntimeInfo = null; if (this.activeForegroundTurnId) { - return SETTING_APPLIES_NEXT_TURN_NOTICE; + return MODE_APPLIES_NEXT_TURN_NOTICE; } } @@ -3961,7 +3964,7 @@ export class CodexAppServerAgentSession implements AgentSession { this.refreshResolvedCollaborationMode(); this.cachedRuntimeInfo = null; if (this.activeForegroundTurnId) { - return SETTING_APPLIES_NEXT_TURN_NOTICE; + return THINKING_APPLIES_NEXT_TURN_NOTICE; } } diff --git a/packages/server/src/server/agent/providers/omp/agent.test.ts b/packages/server/src/server/agent/providers/omp/agent.test.ts index be6666248..032315696 100644 --- a/packages/server/src/server/agent/providers/omp/agent.test.ts +++ b/packages/server/src/server/agent/providers/omp/agent.test.ts @@ -232,7 +232,10 @@ describe("OMP agent client and session", () => { expect.objectContaining({ name: "review", kind: "skill" }), ]), ); - await expect(omp.setMode("ask")).resolves.toEqual(expect.objectContaining({ type: "info" })); + await expect(omp.setMode("ask")).resolves.toEqual({ + type: "warning", + message: "Start a new OMP session to change approval mode", + }); }); test("rewinds natively, interrupts, and shuts down", async () => { diff --git a/packages/server/src/server/agent/providers/omp/agent.ts b/packages/server/src/server/agent/providers/omp/agent.ts index a16187096..1e002c9f4 100644 --- a/packages/server/src/server/agent/providers/omp/agent.ts +++ b/packages/server/src/server/agent/providers/omp/agent.ts @@ -1084,9 +1084,8 @@ export class OmpAgentSession implements AgentSession { throw new Error(`Invalid OMP mode '${modeId}'`); } return { - type: "info", - message: - "OMP approval mode is set when the agent launches. Start a new OMP session to use a different mode.", + type: "warning", + message: "Start a new OMP session to change approval mode", }; } From 5ea311f243649571cf782c6cc1b078a06955b33f Mon Sep 17 00:00:00 2001 From: Mohamed Boudra <boudra.moha@gmail.com> Date: Sat, 18 Jul 2026 21:08:46 +0200 Subject: [PATCH 34/38] feat(app): restore recent chat while reconnecting (#2206) Keep a small non-authoritative view of the last focused chat so the app can paint it before the daemon finishes revalidation. Bound persistence to one focused agent per host and evict whole host entries to limit storage and serialization work. --- docs/architecture.md | 8 + .../use-agent-screen-state-machine.test.ts | 19 + .../hooks/use-agent-screen-state-machine.ts | 3 + packages/app/src/panels/agent-panel.tsx | 17 +- packages/app/src/runtime/host-runtime.test.ts | 69 +++ packages/app/src/runtime/host-runtime.ts | 33 +- .../src/runtime/replica-cache/index.test.ts | 256 +++++++++++ .../app/src/runtime/replica-cache/index.ts | 422 ++++++++++++++++++ packages/app/src/stores/session-store.ts | 68 ++- 9 files changed, 875 insertions(+), 20 deletions(-) create mode 100644 packages/app/src/runtime/replica-cache/index.test.ts create mode 100644 packages/app/src/runtime/replica-cache/index.ts diff --git a/docs/architecture.md b/docs/architecture.md index b4ab8ef21..ce2189853 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -99,12 +99,20 @@ Cross-platform React Native app that connects to one or more daemons. - Expo Router navigation (`/h/[serverId]/workspace/[workspaceId]`, `/h/[serverId]/agent/[agentId]`, etc.). The `workspaceId` URL segment is an opaque workspace id, not a directly meaningful filesystem path. - `HostRuntimeController` manages saved host connections, reconnection, and per-host runtime state +- `runtime/replica-cache` keeps a non-authoritative per-host display replica in AsyncStorage: only the last focused agent, its workspace, and a short timeline tail. It restores before navigation becomes ready, leaves remote hydration flags false, and is atomically replaced by the normal snapshot-plus-delta synchronization path. - `SessionContext` wraps the daemon client for the active session - Composer UI and submit/draft behavior live in `packages/app/src/composer/`; screens and panels should integrate it from there instead of dropping composer internals into `components/`, `hooks/`, or `screens/workspace/` - Timeline reducers in `timeline/session-stream-reducers.ts` handle compaction, gap detection, sequence-based deduplication - Timeline sync correctness is documented in [docs/timeline-sync.md](timeline-sync.md): live streams are for immediacy, `fetch_agent_timeline_request` is authoritative, and catch-up is paged but complete. - Voice features: dictation (STT) and voice agent (realtime) +The replica cache exists only to paint stale data immediately while the host connects. It does not +own mutations, infer deletions, or replace daemon reconciliation. Pending permission requests are +not restored from it. AsyncStorage is not encrypted, so the cached timeline tail may contain source +code, prompts, and tool output; encrypted-at-rest storage is a separate product/security decision. +Its serialized payload has a 1 MiB byte budget and evicts whole host snapshots in least-recently- +written order; a single oversized host is omitted rather than partially restored. + ### `packages/cli` — Command-line client Commander.js CLI with Docker-style commands. Common agent operations are also exposed at the top level (e.g. `paseo ls`, `paseo run`). diff --git a/packages/app/src/hooks/use-agent-screen-state-machine.test.ts b/packages/app/src/hooks/use-agent-screen-state-machine.test.ts index 5422bf63f..9b9b3e4f2 100644 --- a/packages/app/src/hooks/use-agent-screen-state-machine.test.ts +++ b/packages/app/src/hooks/use-agent-screen-state-machine.test.ts @@ -190,6 +190,25 @@ describe("deriveAgentScreenViewState", () => { expect(sync.ui).toBe("silent"); }); + it("keeps hydrated history visible while reconnect revalidation and visibility catch-up overlap", () => { + const memory = createBaseMemory({ + hasRenderedReady: true, + lastReadyAgent: createAgent("agent-1"), + }); + const input: AgentScreenMachineInput = { + ...createBaseInput(), + hasHydratedHistoryBefore: true, + needsAuthoritativeSync: true, + visibilityCatchUpStatus: "pending", + }; + + const result = deriveAgentScreenViewState({ input, memory }); + const ready = expectReadyState(result.state); + const sync = expectCatchingUpSync(ready); + + expect(sync.ui).toBe("silent"); + }); + it("covers already-hydrated history while a newly visible agent catches up", () => { const memory = createBaseMemory({ hasRenderedReady: true, diff --git a/packages/app/src/hooks/use-agent-screen-state-machine.ts b/packages/app/src/hooks/use-agent-screen-state-machine.ts index 941e87deb..daf8562f0 100644 --- a/packages/app/src/hooks/use-agent-screen-state-machine.ts +++ b/packages/app/src/hooks/use-agent-screen-state-machine.ts @@ -150,9 +150,11 @@ function resolveCatchingUpUi(args: { hasOptimisticCreateContinuity: boolean; isVisibilityCatchUpPending: boolean; hasHydratedHistoryBefore: boolean; + needsAuthoritativeSync: boolean; hadInitialSyncFailure: boolean; }): "overlay" | "silent" { if (args.hasOptimisticCreateContinuity) return "silent"; + if (args.hasHydratedHistoryBefore && args.needsAuthoritativeSync) return "silent"; if (args.isVisibilityCatchUpPending) return "overlay"; if (args.hasHydratedHistoryBefore) return "silent"; if (args.hadInitialSyncFailure) return "silent"; @@ -184,6 +186,7 @@ function resolveAgentScreenSync(args: { hasOptimisticCreateContinuity: hasOptimisticCreateContinuity(input), isVisibilityCatchUpPending: input.visibilityCatchUpStatus === "pending", hasHydratedHistoryBefore: input.hasHydratedHistoryBefore, + needsAuthoritativeSync: input.needsAuthoritativeSync, hadInitialSyncFailure, }), }; diff --git a/packages/app/src/panels/agent-panel.tsx b/packages/app/src/panels/agent-panel.tsx index 7613dd035..990527a7b 100644 --- a/packages/app/src/panels/agent-panel.tsx +++ b/packages/app/src/panels/agent-panel.tsx @@ -498,6 +498,13 @@ function AgentPanelContent({ const runtimeIsConnected = useHostRuntimeIsConnected(runtimeServerId); const runtimeConnectionStatus = useHostRuntimeConnectionStatus(runtimeServerId); const runtimeLastError = useHostRuntimeLastError(runtimeServerId); + const hasCachedAgent = useSessionStore((state) => { + if (!resolvedServerId || !resolvedAgentId) return false; + const session = state.sessions[resolvedServerId]; + return Boolean( + session?.agents.has(resolvedAgentId) || session?.agentDetails.has(resolvedAgentId), + ); + }); const connectionServerId = resolvedServerId ?? null; const daemon = connectionServerId @@ -512,7 +519,7 @@ function AgentPanelContent({ : runtimeConnectionStatus; const lastConnectionError = runtimeLastError; - if (!resolvedServerId || !runtimeClient) { + if (!resolvedServerId || (!runtimeClient && !hasCachedAgent)) { return ( <AgentSessionUnavailableState serverLabel={serverLabel} @@ -549,7 +556,7 @@ function AgentPanelBody({ serverId: string; agentId?: string; isPaneFocused: boolean; - client: NonNullable<ReturnType<typeof useHostRuntimeClient>>; + client: ReturnType<typeof useHostRuntimeClient>; isConnected: boolean; connectionStatus: HostRuntimeConnectionStatus; onOpenWorkspaceFile?: (request: WorkspaceFileOpenRequest) => void; @@ -593,7 +600,7 @@ function AgentPanelBody({ } return; } - if (!isConnected || !hasSession) { + if (!client || !isConnected || !hasSession) { return; } if (lookupState.tag === "loading" || lookupState.tag === "not_found") { @@ -710,7 +717,7 @@ function ChatAgentContent({ serverId: string; agentId?: string; isPaneFocused: boolean; - client: NonNullable<ReturnType<typeof useHostRuntimeClient>>; + client: ReturnType<typeof useHostRuntimeClient>; isConnected: boolean; connectionStatus: HostRuntimeConnectionStatus; onOpenWorkspaceFile?: (request: WorkspaceFileOpenRequest) => void; @@ -949,7 +956,7 @@ function ChatAgentContent({ } return; } - if (!isPaneVisible || !isConnected || !hasSession) { + if (!client || !isPaneVisible || !isConnected || !hasSession) { return; } if ( diff --git a/packages/app/src/runtime/host-runtime.test.ts b/packages/app/src/runtime/host-runtime.test.ts index 4de541e34..156f554dc 100644 --- a/packages/app/src/runtime/host-runtime.test.ts +++ b/packages/app/src/runtime/host-runtime.test.ts @@ -20,6 +20,7 @@ import { type HostRuntimeControllerDeps, type HostRuntimeStorage, } from "./host-runtime"; +import { ReplicaCache } from "./replica-cache"; class FakeDaemonClient { private state: ConnectionState = { status: "idle" }; @@ -1364,6 +1365,74 @@ describe("HostRuntimeController", () => { }); describe("HostRuntimeStore", () => { + it("restores the display replica before declaring the host registry loaded", async () => { + const host = makeHost(); + const storage = createMemoryHostRuntimeStorage(); + await storage.setItem("@paseo:daemon-registry", JSON.stringify([host])); + await storage.setItem("@paseo:e2e", "1"); + + const cachedAgent = replicaAgent( + makeFetchAgentsEntry({ + id: "cached-agent", + cwd: "/repo/paseo", + updatedAt: "2026-07-18T08:00:00.000Z", + title: "Cached agent", + }).agent, + host.serverId, + ); + const session = useSessionStore.getState(); + session.initializeSession(host.serverId, null); + session.setAgents(host.serverId, new Map([[cachedAgent.id, cachedAgent]])); + session.setFocusedAgentId(host.serverId, cachedAgent.id); + session.setAgentStreamTail( + host.serverId, + new Map([ + [ + cachedAgent.id, + [ + { + kind: "assistant_message", + id: "cached-message", + text: "Already painted", + timestamp: new Date("2026-07-18T08:01:00.000Z"), + }, + ], + ], + ]), + ); + const cache = new ReplicaCache(storage); + cache.setHosts([host.serverId]); + await cache.flush(); + session.clearSession(host.serverId); + + const store = new HostRuntimeStore({ + storage, + deps: { + createClient: () => { + throw new Error("createClient should not be called"); + }, + connectToDaemon: async () => { + throw new Error("connectToDaemon should not be called"); + }, + getClientId: async () => "cid_test_runtime", + }, + }); + const registryLoaded = onceHostListMatches(store, () => store.isHostRegistryLoaded()); + store.boot(); + await registryLoaded; + + const restored = useSessionStore.getState().sessions[host.serverId]; + expect(restored?.agents.get(cachedAgent.id)?.title).toBe("Cached agent"); + expect(restored?.agentStreamTail.get(cachedAgent.id)?.[0]).toMatchObject({ + text: "Already painted", + timestamp: new Date("2026-07-18T08:01:00.000Z"), + }); + expect(restored?.hasHydratedAgents).toBe(false); + + store.syncHosts([]); + session.clearSession(host.serverId); + }); + it("marks the host registry loaded after boot reads storage", async () => { const previousOverride = process.env.EXPO_PUBLIC_LOCAL_DAEMON; process.env.EXPO_PUBLIC_LOCAL_DAEMON = "not-an-endpoint"; diff --git a/packages/app/src/runtime/host-runtime.ts b/packages/app/src/runtime/host-runtime.ts index 85f4634ec..59fdf9c09 100644 --- a/packages/app/src/runtime/host-runtime.ts +++ b/packages/app/src/runtime/host-runtime.ts @@ -55,6 +55,7 @@ import { } from "@/composer/attachments/submit"; import { encodeImages } from "@/utils/encode-images"; import { DirectorySync, type RefreshAgentDirectoryResult } from "@/runtime/directory-sync"; +import { ReplicaCache } from "@/runtime/replica-cache"; export type HostRuntimeConnectionStatus = "idle" | "connecting" | "online" | "offline" | "error"; export type HostRegistryStatus = "loading" | "ready"; @@ -1355,10 +1356,12 @@ export class HostRuntimeStore { private configuredOverrideBootstrapInFlight: Promise<void> | null = null; private bootStarted = false; private storage: HostRuntimeStorage; + private replicaCache: ReplicaCache; constructor(input?: { deps?: HostRuntimeControllerDeps; storage?: HostRuntimeStorage }) { this.deps = input?.deps ?? createDefaultDeps(); this.storage = input?.storage ?? AsyncStorage; + this.replicaCache = new ReplicaCache(this.storage); } // --- Host registry --- @@ -1432,27 +1435,29 @@ export class HostRuntimeStore { private async loadFromStorage(): Promise<void> { let shouldPersistHosts = false; + let profiles: HostProfile[] = []; try { const stored = await this.storage.getItem(REGISTRY_STORAGE_KEY); - if (!stored) { - return; + if (stored) { + const parsed = JSON.parse(stored) as unknown; + if (Array.isArray(parsed)) { + const normalizedProfiles = parsed + .map((entry) => normalizeStoredHostProfile(entry)) + .filter((entry): entry is HostProfile => entry !== null); + profiles = normalizedProfiles.filter((entry) => !isPlaceholderServerId(entry.serverId)); + if (profiles.length !== normalizedProfiles.length) { + shouldPersistHosts = true; + } + } } - const parsed = JSON.parse(stored) as unknown; - if (!Array.isArray(parsed)) { - return; - } - const normalizedProfiles = parsed - .map((entry) => normalizeStoredHostProfile(entry)) - .filter((entry): entry is HostProfile => entry !== null); - const profiles = normalizedProfiles.filter((entry) => !isPlaceholderServerId(entry.serverId)); this.hosts = profiles; + this.replicaCache.setHosts(profiles.map((profile) => profile.serverId)); + await this.replicaCache.restore(); this.syncHosts(profiles); - if (profiles.length !== normalizedProfiles.length) { - shouldPersistHosts = true; - } } catch (error) { console.error("[HostRuntime] Failed to load host registry from storage", error); } finally { + this.replicaCache.start(); this.hostRegistryStatus = "ready"; this.emitHostList(); if (shouldPersistHosts) { @@ -1582,6 +1587,7 @@ export class HostRuntimeStore { rekeyMap(this.lastConnectionStatusByServer, oldServerId, newServerId); rekeyMap(this.agentDirectoryBootstrapInFlight, oldServerId, newServerId); + this.replicaCache.reconcileServerId(oldServerId, newServerId); this.directorySyncByServer.get(oldServerId)?.dispose(); this.directorySyncByServer.delete(oldServerId); const directory = new DirectorySync(newServerId, { @@ -1885,6 +1891,7 @@ export class HostRuntimeStore { >; }, ): void { + this.replicaCache.setHosts(hosts.map((host) => host.serverId)); const nextIds = new Set(hosts.map((host) => host.serverId)); for (const [serverId, controller] of this.controllers) { if (nextIds.has(serverId)) { diff --git a/packages/app/src/runtime/replica-cache/index.test.ts b/packages/app/src/runtime/replica-cache/index.test.ts new file mode 100644 index 000000000..69800f03a --- /dev/null +++ b/packages/app/src/runtime/replica-cache/index.test.ts @@ -0,0 +1,256 @@ +import { afterEach, describe, expect, it } from "vitest"; +import type { WorkspaceDescriptorPayload } from "@getpaseo/protocol/messages"; +import { normalizeAgentSnapshot } from "@/utils/agent-snapshots"; +import { + normalizeEmptyProjectDescriptor, + normalizeWorkspaceDescriptor, + useSessionStore, +} from "@/stores/session-store"; +import type { StreamItem } from "@/types/stream"; +import { ReplicaCache, type ReplicaCacheStorage } from "."; + +const SERVER_ID = "cached-host"; +const LRU_SERVER_IDS = ["host-a", "host-b", "host-c"] as const; + +class MemoryStorage implements ReplicaCacheStorage { + readonly values = new Map<string, string>(); + + async getItem(key: string): Promise<string | null> { + return this.values.get(key) ?? null; + } + + async setItem(key: string, value: string): Promise<void> { + this.values.set(key, value); + } +} + +function workspace( + id = "workspace-1", + projectId = "project-1", + workspaceDirectory = "/repo/paseo", +): WorkspaceDescriptorPayload { + return { + id, + projectId, + projectDisplayName: "Paseo", + projectRootPath: workspaceDirectory, + workspaceDirectory, + projectKind: "git", + workspaceKind: "local_checkout", + name: "main", + status: "running", + statusEnteredAt: "2026-07-18T08:00:00.000Z", + activityAt: null, + archivingAt: null, + diffStat: null, + scripts: [], + }; +} + +function agent(id: string, workspaceId = "workspace-1", cwd = "/repo/paseo") { + return normalizeAgentSnapshot( + { + id, + provider: "codex", + cwd, + workspaceId, + model: null, + createdAt: "2026-07-18T08:00:00.000Z", + updatedAt: "2026-07-18T08:01:00.000Z", + lastUserMessageAt: "2026-07-18T08:01:00.000Z", + status: "idle", + capabilities: { + supportsStreaming: true, + supportsSessionPersistence: true, + supportsDynamicModes: true, + supportsMcpServers: true, + supportsReasoningStream: true, + supportsToolInvocations: true, + }, + currentModeId: null, + availableModes: [], + pendingPermissions: [], + persistence: null, + title: `Agent ${id}`, + labels: {}, + }, + SERVER_ID, + ); +} + +function message(id: string, text: string): StreamItem { + return { + kind: "assistant_message", + id, + text, + timestamp: new Date("2026-07-18T08:02:00.000Z"), + timelineCursor: { epoch: "epoch-1", seq: 12 }, + }; +} + +function seedSession(): void { + const store = useSessionStore.getState(); + store.initializeSession(SERVER_ID, null); + store.setAgents(SERVER_ID, new Map([["agent-1", agent("agent-1")]])); + store.setWorkspaces( + SERVER_ID, + new Map([["workspace-1", normalizeWorkspaceDescriptor(workspace())]]), + ); + store.setEmptyProjects(SERVER_ID, [ + normalizeEmptyProjectDescriptor({ + projectId: "empty-project", + projectDisplayName: "Empty project", + projectRootPath: "/repo/empty", + projectKind: "directory", + }), + ]); + store.setFocusedAgentId(SERVER_ID, "agent-1"); + store.setAgentStreamTail(SERVER_ID, new Map([["agent-1", [message("message-1", "Cached")]]])); + store.setAgentTimelineCursor( + SERVER_ID, + new Map([["agent-1", { epoch: "epoch-1", startSeq: 1, endSeq: 12 }]]), + ); + store.setAgentTimelineHasOlder(SERVER_ID, new Map([["agent-1", true]])); + store.setAgentAuthoritativeHistoryApplied(SERVER_ID, "agent-1", true); +} + +function seedTimeline(serverId: string, text: string): void { + const agentId = `agent-${serverId}`; + const workspaceId = `workspace-${serverId}`; + const workspaceDirectory = `/repo/${serverId}`; + const store = useSessionStore.getState(); + store.initializeSession(serverId, null); + store.setAgents(serverId, new Map([[agentId, agent(agentId, workspaceId, workspaceDirectory)]])); + store.setWorkspaces( + serverId, + new Map([ + [ + workspaceId, + normalizeWorkspaceDescriptor( + workspace(workspaceId, `project-${serverId}`, workspaceDirectory), + ), + ], + ]), + ); + store.setFocusedAgentId(serverId, agentId); + store.setAgentStreamTail(serverId, new Map([[agentId, [message(`message-${serverId}`, text)]]])); +} + +afterEach(() => { + const store = useSessionStore.getState(); + store.clearSession(SERVER_ID); + for (const serverId of LRU_SERVER_IDS) store.clearSession(serverId); +}); + +describe("ReplicaCache", () => { + it("restores a displayable stale replica without claiming remote hydration", async () => { + const storage = new MemoryStorage(); + const writer = new ReplicaCache(storage); + writer.setHosts([SERVER_ID]); + seedSession(); + await writer.flush(); + + useSessionStore.getState().clearSession(SERVER_ID); + + const reader = new ReplicaCache(storage); + reader.setHosts([SERVER_ID]); + await reader.restore(); + + const session = useSessionStore.getState().sessions[SERVER_ID]; + expect(session?.client).toBeNull(); + expect(session?.hasHydratedAgents).toBe(false); + expect(session?.hasHydratedWorkspaces).toBe(false); + expect(Array.from(session?.agents.keys() ?? [])).toEqual(["agent-1"]); + expect(Array.from(session?.workspaces.keys() ?? [])).toEqual(["workspace-1"]); + expect(Array.from(session?.emptyProjects.keys() ?? [])).toEqual([]); + expect(session?.agents.get("agent-1")?.updatedAt).toBeInstanceOf(Date); + expect(session?.workspaces.get("workspace-1")?.statusEnteredAt).toBeInstanceOf(Date); + expect(session?.agentStreamTail.get("agent-1")).toEqual([message("message-1", "Cached")]); + expect(session?.agentAuthoritativeHistoryApplied.get("agent-1")).toBe(true); + expect(session?.agentTimelineCursor.get("agent-1")).toEqual({ + epoch: "epoch-1", + startSeq: 1, + endSeq: 12, + }); + expect(session?.agentTimelineHasOlder.get("agent-1")).toBe(true); + }); + + it("persists only the focused agent view with a short timeline tail", async () => { + const storage = new MemoryStorage(); + const cache = new ReplicaCache(storage); + cache.setHosts([SERVER_ID]); + seedSession(); + + const store = useSessionStore.getState(); + store.setAgents(SERVER_ID, (agents) => + new Map(agents).set("agent-2", agent("agent-2", "workspace-2", "/repo/other")), + ); + store.setWorkspaces(SERVER_ID, (workspaces) => + new Map(workspaces).set( + "workspace-2", + normalizeWorkspaceDescriptor(workspace("workspace-2", "project-2", "/repo/other")), + ), + ); + const secondTimeline = Array.from({ length: 60 }, (_, index) => + message(`message-${index}`, `Second ${index}`), + ); + store.setAgentStreamTail( + SERVER_ID, + new Map([ + ["agent-1", [message("message-1", "First")]], + ["agent-2", secondTimeline], + ]), + ); + store.setFocusedAgentId(SERVER_ID, "agent-2"); + await cache.flush(); + + store.clearSession(SERVER_ID); + const reader = new ReplicaCache(storage); + reader.setHosts([SERVER_ID]); + await reader.restore(); + + const session = useSessionStore.getState().sessions[SERVER_ID]; + const timelines = session?.agentStreamTail; + expect(Array.from(session?.agents.keys() ?? [])).toEqual(["agent-2"]); + expect(Array.from(session?.workspaces.keys() ?? [])).toEqual(["workspace-2"]); + expect(Array.from(session?.emptyProjects.keys() ?? [])).toEqual([]); + expect(Array.from(timelines?.keys() ?? [])).toEqual(["agent-2"]); + expect(timelines?.get("agent-2")).toEqual(secondTimeline.slice(-50)); + }); + + it("evicts the least recently written host when the cache exceeds its byte budget", async () => { + const storage = new MemoryStorage(); + const cache = new ReplicaCache(storage, { maxBytes: 7_000 }); + cache.setHosts(LRU_SERVER_IDS.slice(0, 2)); + seedTimeline("host-a", "A".repeat(1_200)); + seedTimeline("host-b", "B".repeat(1_200)); + await cache.flush(); + + seedTimeline("host-a", "A".repeat(1_201)); + await cache.flush(); + + cache.setHosts(LRU_SERVER_IDS); + seedTimeline("host-c", "C".repeat(1_200)); + await cache.flush(); + + for (const serverId of LRU_SERVER_IDS) { + useSessionStore.getState().clearSession(serverId); + } + const reader = new ReplicaCache(storage, { maxBytes: 7_000 }); + reader.setHosts(LRU_SERVER_IDS); + await reader.restore(); + + expect(Object.keys(useSessionStore.getState().sessions).sort()).toEqual(["host-a", "host-c"]); + }); + + it("drops malformed or unknown cache versions", async () => { + const storage = new MemoryStorage(); + storage.values.set("@paseo:replica-cache", JSON.stringify({ version: 999, hosts: [] })); + const cache = new ReplicaCache(storage); + cache.setHosts([SERVER_ID]); + + await cache.restore(); + + expect(useSessionStore.getState().sessions[SERVER_ID]).toBeUndefined(); + }); +}); diff --git a/packages/app/src/runtime/replica-cache/index.ts b/packages/app/src/runtime/replica-cache/index.ts new file mode 100644 index 000000000..7f8582884 --- /dev/null +++ b/packages/app/src/runtime/replica-cache/index.ts @@ -0,0 +1,422 @@ +import { Buffer } from "buffer"; +import { z } from "zod"; +import { + AgentSnapshotPayloadSchema, + ProjectPlacementPayloadSchema, + WorkspaceDescriptorPayloadSchema, + WorkspaceProjectDescriptorPayloadSchema, + type AgentSnapshotPayload, + type WorkspaceDescriptorPayload, +} from "@getpaseo/protocol/messages"; +import { + normalizeEmptyProjectDescriptor, + normalizeWorkspaceDescriptor, + useSessionStore, + type Agent, + type SessionReplica, + type SessionState, + type WorkspaceDescriptor, +} from "@/stores/session-store"; +import type { StreamItem } from "@/types/stream"; +import { normalizeAgentSnapshot } from "@/utils/agent-snapshots"; + +const STORAGE_KEY = "@paseo:replica-cache"; +const CACHE_VERSION = 1; +const PERSIST_DELAY_MS = 750; +const MAX_TIMELINE_ITEMS = 50; +const MAX_CACHE_BYTES = 1024 * 1024; +const DATE_TAG = "__paseoDate"; + +const StoredAgentSchema = z.object({ + snapshot: AgentSnapshotPayloadSchema, + projectPlacement: ProjectPlacementPayloadSchema.nullable(), + lastActivityAt: z.string(), +}); + +const StoredTimelineSchema = z.object({ + agentId: z.string(), + items: z.unknown(), + cursor: z + .object({ + epoch: z.string(), + startSeq: z.number().int().nonnegative(), + endSeq: z.number().int().nonnegative(), + }) + .nullable(), + hasOlder: z.boolean(), +}); + +const StoredHostSchema = z.object({ + serverId: z.string(), + agents: z.array(StoredAgentSchema), + workspaces: z.array(WorkspaceDescriptorPayloadSchema), + emptyProjects: z.array(WorkspaceProjectDescriptorPayloadSchema), + timeline: StoredTimelineSchema.nullable(), +}); + +const StoredCacheSchema = z.object({ + version: z.literal(CACHE_VERSION), + hosts: z.array(StoredHostSchema), +}); + +type StoredAgent = z.infer<typeof StoredAgentSchema>; +type StoredHost = z.infer<typeof StoredHostSchema>; + +export interface ReplicaCacheStorage { + getItem: (key: string) => Promise<string | null>; + setItem: (key: string, value: string) => Promise<void>; +} + +interface ReplicaCacheOptions { + maxBytes?: number; +} + +function isRecord(value: unknown): value is Record<string, unknown> { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function hasString(value: Record<string, unknown>, key: string): boolean { + return typeof value[key] === "string"; +} + +function isStreamItem(value: unknown): value is StreamItem { + if (!isRecord(value) || !hasString(value, "id") || !(value.timestamp instanceof Date)) { + return false; + } + switch (value.kind) { + case "user_message": + case "assistant_message": + return hasString(value, "text"); + case "thought": + return hasString(value, "text") && (value.status === "loading" || value.status === "ready"); + case "tool_call": + return isRecord(value.payload) && isRecord(value.payload.data); + case "todo_list": + return hasString(value, "provider") && Array.isArray(value.items); + case "activity_log": + return hasString(value, "message") && hasString(value, "activityType"); + case "compaction": + return value.status === "loading" || value.status === "completed"; + default: + return false; + } +} + +function encodeDates(value: unknown): unknown { + if (value instanceof Date) { + return { [DATE_TAG]: value.toISOString() }; + } + if (Array.isArray(value)) { + return value.map(encodeDates); + } + if (!isRecord(value)) { + return value; + } + return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, encodeDates(entry)])); +} + +function decodeDates(value: unknown): unknown { + if (Array.isArray(value)) { + return value.map(decodeDates); + } + if (!isRecord(value)) { + return value; + } + if (Object.keys(value).length === 1 && typeof value[DATE_TAG] === "string") { + const date = new Date(value[DATE_TAG]); + return Number.isNaN(date.getTime()) ? value : date; + } + return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, decodeDates(entry)])); +} + +function deserializeTimeline(stored: StoredHost["timeline"]): SessionReplica["timeline"] { + if (!stored) { + return null; + } + const decoded = decodeDates(stored.items); + if (!Array.isArray(decoded) || !decoded.every(isStreamItem)) { + return null; + } + return { + agentId: stored.agentId, + items: decoded, + cursor: stored.cursor, + hasOlder: stored.hasOlder, + }; +} + +function serializeAgent(agent: Agent): StoredAgent { + const snapshot: AgentSnapshotPayload = { + id: agent.id, + provider: agent.provider, + cwd: agent.cwd, + ...(agent.workspaceId ? { workspaceId: agent.workspaceId } : {}), + model: agent.model, + ...(agent.features ? { features: agent.features } : {}), + thinkingOptionId: agent.thinkingOptionId ?? null, + createdAt: agent.createdAt.toISOString(), + updatedAt: agent.updatedAt.toISOString(), + lastUserMessageAt: agent.lastUserMessageAt?.toISOString() ?? null, + status: agent.status, + capabilities: agent.capabilities, + currentModeId: agent.currentModeId, + availableModes: agent.availableModes, + pendingPermissions: [], + persistence: agent.persistence, + ...(agent.runtimeInfo ? { runtimeInfo: agent.runtimeInfo } : {}), + ...(agent.lastUsage ? { lastUsage: agent.lastUsage } : {}), + ...(agent.lastError ? { lastError: agent.lastError } : {}), + title: agent.title, + labels: agent.labels, + requiresAttention: agent.requiresAttention ?? false, + attentionReason: agent.attentionReason ?? null, + attentionTimestamp: agent.attentionTimestamp?.toISOString() ?? null, + archivedAt: agent.archivedAt?.toISOString() ?? null, + }; + return { + snapshot, + projectPlacement: agent.projectPlacement ?? null, + lastActivityAt: agent.lastActivityAt.toISOString(), + }; +} + +function deserializeAgent(serverId: string, stored: StoredAgent): Agent { + return { + ...normalizeAgentSnapshot(stored.snapshot, serverId), + lastActivityAt: new Date(stored.lastActivityAt), + projectPlacement: stored.projectPlacement, + }; +} + +function serializeWorkspace(workspace: WorkspaceDescriptor): WorkspaceDescriptorPayload { + return { + id: workspace.id, + projectId: workspace.projectId, + projectDisplayName: workspace.projectDisplayName, + projectCustomName: workspace.projectCustomName ?? null, + projectRootPath: workspace.projectRootPath, + workspaceDirectory: workspace.workspaceDirectory, + projectKind: workspace.projectKind, + workspaceKind: workspace.workspaceKind, + name: workspace.name, + title: workspace.title ?? null, + pinnedAt: workspace.pinnedAt ?? null, + status: workspace.status, + statusEnteredAt: workspace.statusEnteredAt?.toISOString() ?? null, + activityAt: null, + archivingAt: workspace.archivingAt, + diffStat: workspace.diffStat, + scripts: workspace.scripts, + gitRuntime: workspace.gitRuntime, + githubRuntime: workspace.githubRuntime, + forge: workspace.forge, + project: workspace.project, + }; +} + +function deserializeHost(stored: StoredHost): SessionReplica { + const agents = stored.agents.map((entry) => deserializeAgent(stored.serverId, entry)); + const workspaces = stored.workspaces.map(normalizeWorkspaceDescriptor); + const emptyProjects = stored.emptyProjects.map(normalizeEmptyProjectDescriptor); + return { + agents: new Map(agents.map((agent) => [agent.id, agent])), + workspaces: new Map(workspaces.map((workspace) => [workspace.id, workspace])), + emptyProjects: new Map(emptyProjects.map((project) => [project.projectId, project])), + timeline: deserializeTimeline(stored.timeline), + }; +} + +export class ReplicaCache { + private readonly activeServerIds = new Set<string>(); + private readonly storedHosts = new Map<string, StoredHost>(); + private readonly lastFocusedAgentIds = new Map<string, string>(); + private readonly capturedSessions = new Map<string, SessionState>(); + private readonly maxBytes: number; + private needsPersist = false; + private unsubscribe: (() => void) | null = null; + private persistTimer: ReturnType<typeof setTimeout> | null = null; + private writeQueue: Promise<void> = Promise.resolve(); + + constructor( + private readonly storage: ReplicaCacheStorage, + options: ReplicaCacheOptions = {}, + ) { + const emptyPayloadBytes = Buffer.byteLength( + JSON.stringify({ version: CACHE_VERSION, hosts: [] }), + "utf8", + ); + this.maxBytes = Math.max(options.maxBytes ?? MAX_CACHE_BYTES, emptyPayloadBytes); + } + + setHosts(serverIds: Iterable<string>): void { + const next = new Set(serverIds); + this.activeServerIds.clear(); + for (const serverId of next) this.activeServerIds.add(serverId); + let removedStoredHost = false; + for (const serverId of this.storedHosts.keys()) { + if (!next.has(serverId)) { + this.storedHosts.delete(serverId); + removedStoredHost = true; + } + } + for (const serverId of this.lastFocusedAgentIds.keys()) { + if (!next.has(serverId)) this.lastFocusedAgentIds.delete(serverId); + } + for (const serverId of this.capturedSessions.keys()) { + if (!next.has(serverId)) this.capturedSessions.delete(serverId); + } + if (removedStoredHost) this.needsPersist = true; + if (this.unsubscribe && this.needsPersist) this.schedulePersist(); + } + + async restore(): Promise<void> { + let raw: string | null; + try { + raw = await this.storage.getItem(STORAGE_KEY); + } catch { + return; + } + if (!raw) return; + let parsed: unknown; + try { + parsed = JSON.parse(raw); + } catch { + return; + } + const cache = StoredCacheSchema.safeParse(parsed); + if (!cache.success) return; + for (const host of cache.data.hosts) { + if (!this.activeServerIds.has(host.serverId)) { + this.needsPersist = true; + continue; + } + this.storedHosts.set(host.serverId, host); + if (host.timeline) this.lastFocusedAgentIds.set(host.serverId, host.timeline.agentId); + } + if (this.buildBoundedPayload().evicted) this.needsPersist = true; + for (const host of this.storedHosts.values()) { + useSessionStore.getState().restoreSessionReplica(host.serverId, deserializeHost(host)); + const session = useSessionStore.getState().sessions[host.serverId]; + if (session) this.capturedSessions.set(host.serverId, session); + } + } + + start(): void { + if (this.unsubscribe) return; + this.unsubscribe = useSessionStore.subscribe((state) => { + if (this.activeServerIds.size === 0) return; + for (const serverId of this.activeServerIds) { + const focusedAgentId = state.sessions[serverId]?.focusedAgentId; + if (focusedAgentId) this.lastFocusedAgentIds.set(serverId, focusedAgentId); + } + this.schedulePersist(); + }); + if (this.needsPersist) this.schedulePersist(); + } + + reconcileServerId(oldServerId: string, newServerId: string): void { + const stored = this.storedHosts.get(oldServerId); + if (stored) { + this.storedHosts.delete(oldServerId); + this.storedHosts.set(newServerId, { ...stored, serverId: newServerId }); + } + const focusedAgentId = this.lastFocusedAgentIds.get(oldServerId); + if (focusedAgentId) { + this.lastFocusedAgentIds.delete(oldServerId); + this.lastFocusedAgentIds.set(newServerId, focusedAgentId); + } + const capturedSession = this.capturedSessions.get(oldServerId); + if (capturedSession) { + this.capturedSessions.delete(oldServerId); + this.capturedSessions.set(newServerId, capturedSession); + } + if (this.activeServerIds.delete(oldServerId)) this.activeServerIds.add(newServerId); + this.needsPersist = true; + this.schedulePersist(); + } + + async flush(): Promise<void> { + if (this.persistTimer) { + clearTimeout(this.persistTimer); + this.persistTimer = null; + } + this.captureSessions(); + const { payload } = this.buildBoundedPayload(); + this.needsPersist = false; + const write = this.writeQueue + .catch(() => undefined) + .then(() => this.storage.setItem(STORAGE_KEY, payload)); + this.writeQueue = write; + await write.catch(() => undefined); + } + + private captureSessions(): void { + const sessions = useSessionStore.getState().sessions; + for (const serverId of this.activeServerIds) { + const session = sessions[serverId]; + if (!session) continue; + if (this.capturedSessions.get(serverId) === session) continue; + this.capturedSessions.set(serverId, session); + if (session.focusedAgentId) { + this.lastFocusedAgentIds.set(serverId, session.focusedAgentId); + } + const focusedAgentId = this.lastFocusedAgentIds.get(serverId) ?? null; + const focusedAgent = focusedAgentId ? session.agents.get(focusedAgentId) : undefined; + const focusedWorkspace = focusedAgent + ? ((focusedAgent.workspaceId + ? session.workspaces.get(focusedAgent.workspaceId) + : undefined) ?? + Array.from(session.workspaces.values()).find( + (workspace) => workspace.workspaceDirectory === focusedAgent.cwd, + )) + : undefined; + const items = focusedAgentId ? session.agentStreamTail.get(focusedAgentId) : undefined; + const timeline = + focusedAgent && items + ? { + agentId: focusedAgent.id, + items: encodeDates(items.slice(-MAX_TIMELINE_ITEMS)), + cursor: session.agentTimelineCursor.get(focusedAgent.id) ?? null, + hasOlder: session.agentTimelineHasOlder.get(focusedAgent.id) ?? false, + } + : null; + const stored: StoredHost = { + serverId, + agents: focusedAgent ? [serializeAgent(focusedAgent)] : [], + workspaces: focusedWorkspace ? [serializeWorkspace(focusedWorkspace)] : [], + emptyProjects: [], + timeline, + }; + this.storedHosts.delete(serverId); + this.storedHosts.set(serverId, stored); + } + } + + private buildBoundedPayload(): { payload: string; evicted: boolean } { + let evicted = false; + let payload = this.serialize(); + while (Buffer.byteLength(payload, "utf8") > this.maxBytes && this.storedHosts.size > 0) { + const oldestServerId = this.storedHosts.keys().next().value; + if (oldestServerId === undefined) break; + this.storedHosts.delete(oldestServerId); + evicted = true; + payload = this.serialize(); + } + return { payload, evicted }; + } + + private serialize(): string { + return JSON.stringify({ + version: CACHE_VERSION, + hosts: Array.from(this.storedHosts.values()), + }); + } + + private schedulePersist(): void { + if (this.persistTimer) return; + this.persistTimer = setTimeout(() => { + this.persistTimer = null; + void this.flush(); + }, PERSIST_DELAY_MS); + } +} diff --git a/packages/app/src/stores/session-store.ts b/packages/app/src/stores/session-store.ts index baa5d26ba..d6e37a171 100644 --- a/packages/app/src/stores/session-store.ts +++ b/packages/app/src/stores/session-store.ts @@ -320,6 +320,20 @@ export interface AgentTimelineCursorState { endSeq: number; } +export interface SessionReplicaTimeline { + agentId: string; + items: StreamItem[]; + cursor: AgentTimelineCursorState | null; + hasOlder: boolean; +} + +export interface SessionReplica { + agents: Map<string, Agent>; + workspaces: Map<string, WorkspaceDescriptor>; + emptyProjects: Map<string, EmptyProjectDescriptor>; + timeline: SessionReplicaTimeline | null; +} + export type WorkspaceRestoreStatus = "restoring" | "failed" | "needs-host-upgrade"; // Per-session state @@ -398,7 +412,12 @@ interface SessionStoreState { // Action types interface SessionStoreActions { // Session management - initializeSession: (serverId: string, client: DaemonClient, clientGeneration?: number) => void; + initializeSession: ( + serverId: string, + client: DaemonClient | null, + clientGeneration?: number, + ) => void; + restoreSessionReplica: (serverId: string, replica: SessionReplica) => void; clearSession: (serverId: string) => void; getSession: (serverId: string) => SessionState | undefined; updateSessionClient: (serverId: string, client: DaemonClient, clientGeneration?: number) => void; @@ -549,7 +568,7 @@ const agentLastActivityCoalescer = createAgentLastActivityCoalescer(); // Helper to create initial session state function createInitialSessionState( serverId: string, - client: DaemonClient, + client: DaemonClient | null, clientGeneration = 0, ): SessionState { return { @@ -675,6 +694,51 @@ export const useSessionStore = create<SessionStore>()( }); }, + restoreSessionReplica: (serverId, replica) => { + set((prev) => { + if (prev.sessions[serverId]) { + return prev; + } + const session = createInitialSessionState(serverId, null); + const timeline = replica.timeline; + const agentStreamTail = new Map<string, StreamItem[]>(); + const agentTimelineCursor = new Map<string, AgentTimelineCursorState>(); + const agentTimelineHasOlder = new Map<string, boolean>(); + const agentAuthoritativeHistoryApplied = new Map<string, boolean>(); + const agentHistorySyncGeneration = new Map<string, number>(); + if (timeline) { + agentStreamTail.set(timeline.agentId, timeline.items); + agentTimelineHasOlder.set(timeline.agentId, timeline.hasOlder); + agentAuthoritativeHistoryApplied.set(timeline.agentId, true); + agentHistorySyncGeneration.set(timeline.agentId, session.historySyncGeneration); + if (timeline.cursor) agentTimelineCursor.set(timeline.agentId, timeline.cursor); + } + const agentLastActivity = new Map(prev.agentLastActivity); + for (const agent of replica.agents.values()) { + agentLastActivity.set(agent.id, agent.lastActivityAt); + } + return { + ...prev, + sessions: { + ...prev.sessions, + [serverId]: { + ...session, + agents: replica.agents, + workspaceAgentActivity: buildWorkspaceAgentActivityIndex(replica.agents), + workspaces: replica.workspaces, + emptyProjects: replica.emptyProjects, + agentStreamTail, + agentTimelineCursor, + agentTimelineHasOlder, + agentAuthoritativeHistoryApplied, + agentHistorySyncGeneration, + }, + }, + agentLastActivity, + }; + }); + }, + clearSession: (serverId) => { agentLastActivityCoalescer.flushNow(); set((prev) => { From 99dc8ddda5c823fbe1c7d7016840c8275e75e652 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra <boudra.moha@gmail.com> Date: Sat, 18 Jul 2026 21:21:05 +0200 Subject: [PATCH 35/38] Free resources from idle agents automatically (#2203) * fix(server): release resources held by idle agents Keep unarchived agents resumable while closing their provider runtimes after two minutes. Active agent schedules keep runtimes resident. * fix(server): preserve resumable agent state * fix(server): resume agents when listing commands * fix(server): preserve collected agent interactions --- docs/agent-lifecycle.md | 25 +- docs/data-model.md | 2 +- .../server/src/server/agent/agent-loading.ts | 12 +- .../src/server/agent/agent-manager.test.ts | 328 ++++++++++++++++++ .../server/src/server/agent/agent-manager.ts | 157 ++++++++- .../src/server/agent/agent-response-loop.ts | 2 + .../src/server/agent/agent-sdk-types.ts | 5 +- .../opencode-agent-commands.e2e.test.ts | 22 ++ .../agent/providers/opencode-agent.test.ts | 132 +++---- .../server/agent/providers/opencode-agent.ts | 98 ++++-- .../providers/opencode-server-manager.test.ts | 66 ++-- .../providers/opencode/server-manager.ts | 52 +-- .../providers/opencode/test-server-manager.ts | 8 +- .../test-utils/test-opencode-harness.ts | 6 +- packages/server/src/server/bootstrap.ts | 37 ++ .../src/server/hub/daemon-executions.ts | 6 +- packages/server/src/server/loop-service.ts | 15 +- .../src/server/schedule/service.test.ts | 41 +++ .../server/src/server/schedule/service.ts | 13 + packages/server/src/server/session.ts | 33 +- 20 files changed, 879 insertions(+), 181 deletions(-) diff --git a/docs/agent-lifecycle.md b/docs/agent-lifecycle.md index 2be3c3b55..f5e922830 100644 --- a/docs/agent-lifecycle.md +++ b/docs/agent-lifecycle.md @@ -10,7 +10,26 @@ initializing → idle → running → idle (or error → closed) └────────┘ (agent completes a turn, awaits next prompt) ``` -Each agent in `AgentManager` carries a `lastStatus` of `initializing`, `idle`, `running`, `error`, or `closed`. State transitions persist to disk and stream to subscribed clients via WebSocket. +Each live agent in `AgentManager` carries a `lastStatus` of `initializing`, `idle`, `running`, or `error`. `closed` is the persisted, resumable state for an agent record that has no live provider runtime. State transitions persist to disk and stream to subscribed clients via WebSocket. + +## Runtime residency + +An unarchived agent may be `closed` without being deleted or archived. Closing releases its provider +processes and subscriptions while retaining its Paseo identity, persistence handle, timeline, +workspace, labels, title, usage, attention, timestamps, and parent relationship. Opening or prompting +the agent runs through `ensureAgentLoaded()`, which resumes the durable provider session under the +same Paseo agent ID. Provider history is not appended again when the canonical timeline is already +primed. + +The daemon collects an eligible idle runtime after two minutes and sweeps every 15 seconds. Only +unarchived, non-internal agents that are exactly `idle`, have no active or pending run, replacement, +or permission, and have not been activated during the idle window are eligible. `running`, +`initializing`, and `error` agents stay resident. Subagents are considered independently; collection +does not cascade or change parentage. + +Active schedules targeting an existing agent protect that agent from collection. Paused, completed, +and new-agent schedules do not. A pane may remain open after collection; its next prompt resumes the +runtime. ### Cancellation @@ -39,6 +58,10 @@ The provider still owns the underlying runtime. Paseo keeps an agent record so t Archive is a **soft delete**: the agent record stays on disk with `archivedAt` set, the runtime is closed, and the agent disappears from active lists. Archive is **global** — it lives on the server and propagates to every connected client. +Archive is distinct from runtime collection. Archive sets `archivedAt`, invokes the provider's native +archive hook, and cascades to managed children. Runtime collection does none of those things; it only +releases the live runtime and writes `lastStatus: closed` on the still-active record. + `create_agent_request` can opt an agent into `autoArchive`. In that mode the daemon archives the agent after the first terminal turn event (`turn_completed`, `turn_failed`, or `turn_canceled`). When the agent owns an isolated workspace, auto-archive archives that workspace too; the managed worktree is removed when its final workspace reference is gone. Archiving runs through `AgentManager.archiveAgent` (`packages/server/src/server/agent/agent-manager.ts`): diff --git a/docs/data-model.md b/docs/data-model.md index 0d34d49a6..829ca612a 100644 --- a/docs/data-model.md +++ b/docs/data-model.md @@ -83,7 +83,7 @@ Each agent is stored as a separate JSON file, grouped by project directory. | `lastUserMessageAt` | `string?` (ISO 8601) | Last user message timestamp | | `title` | `string?` | User-visible title | | `labels` | `Record<string, string>` | Key-value labels (default `{}`). `paseo.parent-agent-id` is set automatically for agent-scoped creation and removed by detach — see [agent-lifecycle.md](./agent-lifecycle.md) | -| `lastStatus` | `AgentStatus` | One of: `"initializing"`, `"idle"`, `"running"`, `"error"`, `"closed"` | +| `lastStatus` | `AgentStatus` | One of: `"initializing"`, `"idle"`, `"running"`, `"error"`, `"closed"`. `closed` means the record is resumable but has no live provider runtime; archive remains represented separately by `archivedAt`. | | `lastModeId` | `string?` | Last active mode ID | | `config` | `SerializableConfig?` | Agent session configuration (see below) | | `runtimeInfo` | `RuntimeInfo?` | Live runtime state (see below) | diff --git a/packages/server/src/server/agent/agent-loading.ts b/packages/server/src/server/agent/agent-loading.ts index c3a67af2c..01dd9264b 100644 --- a/packages/server/src/server/agent/agent-loading.ts +++ b/packages/server/src/server/agent/agent-loading.ts @@ -20,7 +20,8 @@ export type AgentLoaderManager = Pick< | "getRegisteredProviderIds" | "hydrateTimelineFromProvider" | "resumeAgentFromPersistence" ->; +> & + Partial<Pick<AgentManager, "touchAgentActivity" | "waitForAgentClose">>; export interface EnsureAgentLoadedDeps { agentManager: AgentLoaderManager; @@ -33,11 +34,18 @@ export async function ensureAgentLoaded( agentId: string, deps: EnsureAgentLoadedDeps, ): Promise<ManagedAgent> { - const existing = deps.agentManager.getAgent(agentId); + await deps.agentManager.waitForAgentClose?.(agentId); + const existing = + deps.agentManager.touchAgentActivity?.(agentId) ?? deps.agentManager.getAgent(agentId); if (existing) { return existing; } + // A close may have started after the first barrier observed no in-flight + // work. Once the live lookup is empty, this second barrier closes that gap + // before storage-backed resume begins. + await deps.agentManager.waitForAgentClose?.(agentId); + const inflight = pendingAgentInitializations.get(agentId); if (inflight) { return inflight; diff --git a/packages/server/src/server/agent/agent-manager.test.ts b/packages/server/src/server/agent/agent-manager.test.ts index 48e32643c..12dea4e47 100644 --- a/packages/server/src/server/agent/agent-manager.test.ts +++ b/packages/server/src/server/agent/agent-manager.test.ts @@ -17,6 +17,7 @@ import { AgentStorage } from "./agent-storage.js"; import { toAgentPayload } from "./agent-projections.js"; import { PARENT_AGENT_ID_LABEL } from "@getpaseo/protocol/agent-labels"; import { formatSystemNotificationPrompt } from "./agent-prompt.js"; +import { ensureAgentLoaded } from "./agent-loading.js"; import type { StoredAgentRecord } from "./agent-storage.js"; import type { AgentClient, @@ -7159,6 +7160,333 @@ test("closeAgent persists one final closed snapshot", async () => { } }); +test("collectIdleAgents releases an idle runtime and resumes the same agent and timeline", async () => { + const workdir = mkdtempSync(join(tmpdir(), "agent-manager-idle-collection-")); + const storage = new AgentStorage(join(workdir, "agents"), logger); + let activeSession: TestAgentSession | null = null; + const client = new (class extends NativeArchiveRecordingClient { + override async createSession(config: AgentSessionConfig): Promise<AgentSession> { + activeSession = new TestAgentSession(config); + return activeSession; + } + })(); + const manager = new AgentManager({ + clients: { codex: client }, + registry: storage, + logger, + idFactory: () => "00000000-0000-4000-8000-000000000210", + }); + + try { + const created = await manager.createAgent({ provider: "codex", cwd: workdir }, undefined, { + workspaceId: "workspace-idle-collection", + }); + await manager.appendTimelineItem(created.id, { + type: "user_message", + text: "Keep this timeline", + }); + activeSession?.pushEvent({ + type: "provider_subagent", + provider: "codex", + event: { + type: "upsert", + id: "retained-provider-child", + title: "Retained provider child", + status: "completed", + }, + }); + await manager.flush(); + const timelineBeforeCollection = manager.getTimeline(created.id); + + const collection = await manager.collectIdleAgents({ + cutoff: new Date(Date.now() + 1_000), + protectedAgentIds: new Set(), + }); + + expect(collection).toEqual({ + collected: [ + { + agentId: created.id, + provider: "codex", + sessionId: created.persistence?.sessionId, + }, + ], + failures: [], + }); + expect(manager.getAgent(created.id)).toBeNull(); + expect(client.archivedHandles).toEqual([]); + const stored = await storage.get(created.id); + expect(stored).toMatchObject({ + id: created.id, + lastStatus: "closed", + workspaceId: "workspace-idle-collection", + }); + expect(stored?.archivedAt).toBeFalsy(); + + const resumed = await ensureAgentLoaded(created.id, { + agentManager: manager, + agentStorage: storage, + logger, + }); + + expect(resumed.id).toBe(created.id); + expect(resumed.persistence).toEqual(created.persistence); + expect(manager.getTimeline(created.id)).toEqual(timelineBeforeCollection); + expect(manager.listProviderSubagents(created.id)).toEqual([ + expect.objectContaining({ + id: "retained-provider-child", + title: "Retained provider child", + status: "completed", + }), + ]); + const idleBeforeOpen = resumed.updatedAt; + await ensureAgentLoaded(created.id, { + agentManager: manager, + agentStorage: storage, + logger, + }); + await expect( + manager.collectIdleAgents({ cutoff: idleBeforeOpen, protectedAgentIds: new Set() }), + ).resolves.toMatchObject({ collected: [] }); + await expect(manager.runAgent(created.id, "Continue the same agent")).resolves.toMatchObject({ + finalText: "", + canceled: false, + }); + expect(manager.getAgent(created.id)?.id).toBe(created.id); + } finally { + await manager.flush().catch(() => undefined); + await storage.flush().catch(() => undefined); + rmSync(workdir, { recursive: true, force: true }); + } +}); + +test("archiving an idle-collected parent still cascades to its managed children", async () => { + const workdir = mkdtempSync(join(tmpdir(), "agent-manager-collected-parent-archive-")); + const storage = new AgentStorage(join(workdir, "agents"), logger); + const manager = new AgentManager({ + clients: { codex: new TestAgentClient() }, + registry: storage, + logger, + }); + + try { + const parent = await manager.createAgent( + { provider: "codex", cwd: workdir, title: "Collected parent" }, + undefined, + { workspaceId: undefined }, + ); + const child = await manager.createAgent( + { provider: "codex", cwd: workdir, title: "Managed child" }, + undefined, + { + labels: { [PARENT_AGENT_ID_LABEL]: parent.id }, + workspaceId: undefined, + }, + ); + + await manager.collectIdleAgents({ + cutoff: new Date(Date.now() + 1_000), + protectedAgentIds: new Set([child.id]), + }); + await manager.archiveSnapshot(parent.id, new Date().toISOString()); + + expect((await storage.get(parent.id))?.archivedAt).toEqual(expect.any(String)); + expect((await storage.get(child.id))?.archivedAt).toEqual(expect.any(String)); + expect(manager.getAgent(child.id)).toBeNull(); + } finally { + await manager.flush().catch(() => undefined); + await storage.flush().catch(() => undefined); + rmSync(workdir, { recursive: true, force: true }); + } +}); + +test("collectIdleAgents leaves recent, protected, internal, running, and error agents resident", async () => { + const workdir = mkdtempSync(join(tmpdir(), "agent-manager-idle-eligibility-")); + const client = new (class extends TestAgentClient { + readonly sessions: TestAgentSession[] = []; + + override async createSession(config: AgentSessionConfig): Promise<AgentSession> { + const session = new TestAgentSession(config); + this.sessions.push(session); + return session; + } + })(); + const ids = [ + "00000000-0000-4000-8000-000000000211", + "00000000-0000-4000-8000-000000000212", + "00000000-0000-4000-8000-000000000213", + "00000000-0000-4000-8000-000000000214", + "00000000-0000-4000-8000-000000000215", + ]; + const manager = new AgentManager({ + clients: { codex: client }, + logger, + idFactory: () => ids.shift()!, + }); + + try { + const recent = await manager.createAgent({ provider: "codex", cwd: workdir }, undefined, { + workspaceId: undefined, + }); + const protectedAgent = await manager.createAgent( + { provider: "codex", cwd: workdir }, + undefined, + { workspaceId: undefined }, + ); + const internal = await manager.createAgent( + { provider: "codex", cwd: workdir, internal: true }, + undefined, + { workspaceId: undefined }, + ); + const running = await manager.createAgent({ provider: "codex", cwd: workdir }, undefined, { + workspaceId: undefined, + }); + const failed = await manager.createAgent({ provider: "codex", cwd: workdir }, undefined, { + workspaceId: undefined, + }); + client.sessions[3]!.pushEvent({ + type: "turn_started", + provider: "codex", + turnId: "autonomous-running", + }); + client.sessions[4]!.pushEvent({ + type: "turn_failed", + provider: "codex", + turnId: "autonomous-failed", + error: "provider failed", + }); + await manager.flush(); + + const recentSweep = await manager.collectIdleAgents({ + cutoff: new Date(recent.updatedAt.getTime() - 1), + protectedAgentIds: new Set(), + }); + const protectedSweep = await manager.collectIdleAgents({ + cutoff: new Date(Date.now() + 1_000), + protectedAgentIds: new Set([protectedAgent.id, recent.id]), + }); + + expect(recentSweep.collected).toEqual([]); + expect(protectedSweep.collected).toEqual([]); + expect(manager.getAgent(recent.id)?.lifecycle).toBe("idle"); + expect(manager.getAgent(protectedAgent.id)?.lifecycle).toBe("idle"); + expect(manager.getAgent(internal.id)?.lifecycle).toBe("idle"); + expect(manager.getAgent(running.id)?.lifecycle).toBe("running"); + expect(manager.getAgent(failed.id)?.lifecycle).toBe("error"); + } finally { + await Promise.all(manager.listAgents().map((agent) => manager.closeAgent(agent.id))).catch( + () => undefined, + ); + rmSync(workdir, { recursive: true, force: true }); + } +}); + +test("load waits for an in-flight collection close and creates only one resumed runtime", async () => { + const workdir = mkdtempSync(join(tmpdir(), "agent-manager-idle-close-race-")); + const storage = new AgentStorage(join(workdir, "agents"), logger); + const closeStarted = deferred<void>(); + const closeAllowed = deferred<void>(); + const client = new (class extends TestAgentClient { + resumeCount = 0; + + override async createSession(config: AgentSessionConfig): Promise<AgentSession> { + return new (class extends TestAgentSession { + override async close(): Promise<void> { + closeStarted.resolve(); + await closeAllowed.promise; + } + })(config); + } + + override async resumeSession( + handle: AgentPersistenceHandle, + config?: Partial<AgentSessionConfig>, + ): Promise<AgentSession> { + this.resumeCount += 1; + return super.resumeSession(handle, config); + } + })(); + const manager = new AgentManager({ clients: { codex: client }, registry: storage, logger }); + + try { + const created = await manager.createAgent( + { provider: "codex", cwd: workdir }, + "00000000-0000-4000-8000-000000000216", + { workspaceId: undefined }, + ); + const collection = manager.collectIdleAgents({ + cutoff: new Date(Date.now() + 1_000), + protectedAgentIds: new Set(), + }); + await closeStarted.promise; + const loads = Promise.all([ + ensureAgentLoaded(created.id, { agentManager: manager, agentStorage: storage, logger }), + ensureAgentLoaded(created.id, { agentManager: manager, agentStorage: storage, logger }), + ]); + + expect(client.resumeCount).toBe(0); + closeAllowed.resolve(); + const [first, second] = await loads; + await collection; + + expect(first.id).toBe(created.id); + expect(second.id).toBe(created.id); + expect(client.resumeCount).toBe(1); + } finally { + await manager.closeAgent("00000000-0000-4000-8000-000000000216").catch(() => undefined); + await storage.flush().catch(() => undefined); + rmSync(workdir, { recursive: true, force: true }); + } +}); + +test("provider close failure still persists and emits a resumable closed agent", async () => { + const workdir = mkdtempSync(join(tmpdir(), "agent-manager-close-failure-")); + const storage = new AgentStorage(join(workdir, "agents"), logger); + const client = new (class extends TestAgentClient { + override async createSession(config: AgentSessionConfig): Promise<AgentSession> { + return new (class extends TestAgentSession { + override async close(): Promise<void> { + throw new Error("provider cleanup failed"); + } + })(config); + } + })(); + const manager = new AgentManager({ clients: { codex: client }, registry: storage, logger }); + + try { + const created = await manager.createAgent( + { provider: "codex", cwd: workdir }, + "00000000-0000-4000-8000-000000000217", + { workspaceId: undefined }, + ); + const closed = waitForAgentLifecycle(manager, created.id, "closed"); + + const collection = await manager.collectIdleAgents({ + cutoff: new Date(Date.now() + 1_000), + protectedAgentIds: new Set(), + }); + await closed; + expect(collection.collected).toEqual([]); + expect(collection.failures).toHaveLength(1); + expect(collection.failures[0]).toMatchObject({ + agentId: created.id, + provider: "codex", + error: expect.objectContaining({ message: "provider cleanup failed" }), + }); + const stored = await storage.get(created.id); + expect(stored).toMatchObject({ lastStatus: "closed" }); + expect(stored?.archivedAt).toBeFalsy(); + + await expect( + ensureAgentLoaded(created.id, { agentManager: manager, agentStorage: storage, logger }), + ).resolves.toMatchObject({ id: created.id, lifecycle: "idle" }); + } finally { + await manager.closeAgent("00000000-0000-4000-8000-000000000217").catch(() => undefined); + await storage.flush().catch(() => undefined); + rmSync(workdir, { recursive: true, force: true }); + } +}); + test("hydrateTimeline keeps provider user_message items when no canonical user history exists", async () => { const workdir = mkdtempSync(join(tmpdir(), "agent-manager-history-keep-user-")); const storagePath = join(workdir, "agents"); diff --git a/packages/server/src/server/agent/agent-manager.ts b/packages/server/src/server/agent/agent-manager.ts index 158cdc811..6f0eb9cc0 100644 --- a/packages/server/src/server/agent/agent-manager.ts +++ b/packages/server/src/server/agent/agent-manager.ts @@ -391,6 +391,21 @@ export interface AgentMetricsSnapshot { }; } +export interface IdleAgentCollectionEntry { + agentId: string; + provider: AgentProvider; + sessionId?: string; +} + +export interface IdleAgentCollectionFailure extends IdleAgentCollectionEntry { + error: unknown; +} + +export interface IdleAgentCollectionResult { + collected: IdleAgentCollectionEntry[]; + failures: IdleAgentCollectionFailure[]; +} + type ActiveManagedAgent = | ManagedAgentInitializing | ManagedAgentIdle @@ -568,6 +583,7 @@ export class AgentManager { private readonly previousStatuses = new Map<string, AgentLifecycleStatus>(); private readonly backgroundTasks = new Set<Promise<void>>(); private readonly agentRegistrationTasks = new Set<Promise<void>>(); + private readonly inFlightAgentCloses = new Map<string, Promise<void>>(); private readonly agentStreamCoalescer: AgentStreamCoalescer; private mcpBaseUrl: string | null; private readonly mcpAuthToken: string | null; @@ -949,6 +965,19 @@ export class AgentManager { return agent ? { ...agent } : null; } + touchAgentActivity(id: string): ManagedAgent | null { + const agent = this.agents?.get(id); + if (!agent) { + return null; + } + this.touchUpdatedAt(agent); + return { ...agent }; + } + + async waitForAgentClose(agentId: string): Promise<void> { + await this.inFlightAgentCloses?.get(agentId)?.catch(() => undefined); + } + getTimeline(id: string): AgentTimelineItem[] { this.requireAgent(id); return this.timelineStore.getItems(id); @@ -1004,6 +1033,7 @@ export class AgentManager { ): Promise<ManagedAgent> { this.assertAcceptingAgentRegistrations(); const resolvedAgentId = validateAgentId(agentId ?? this.idFactory(), "createAgent"); + await this.deleteAgentState(resolvedAgentId); const { storedConfig, launchConfig } = await this.prepareSessionConfig(config, resolvedAgentId); this.requireEnabledProvider(storedConfig.provider); const client = await this.requireAvailableClient({ @@ -1088,7 +1118,10 @@ export class AgentManager { const launchContext = await this.buildLaunchContext(resolvedAgentId, client); const providerLaunchConfig = this.resolveProviderLaunchConfig(launchConfig, launchContext); const session = await client.resumeSession(handle, providerLaunchConfig, launchContext); - return this.registerSession(session, storedConfig, resolvedAgentId, options); + return this.registerSession(session, storedConfig, resolvedAgentId, { + ...options, + persistence: handle, + }); } importProviderSession(input: { @@ -1309,7 +1342,24 @@ export class AgentManager { } } - async closeAgent(agentId: string, options: { persistClosedState?: boolean } = {}): Promise<void> { + closeAgent(agentId: string): Promise<void> { + const existing = this.inFlightAgentCloses.get(agentId); + if (existing) { + return existing; + } + + const close = this.closeAgentRuntime(agentId); + this.inFlightAgentCloses.set(agentId, close); + const clearClose = () => { + if (this.inFlightAgentCloses.get(agentId) === close) { + this.inFlightAgentCloses.delete(agentId); + } + }; + void close.then(clearClose, clearClose); + return close; + } + + private async closeAgentRuntime(agentId: string): Promise<void> { const agent = this.requireAgent(agentId); this.logger.trace( { @@ -1324,13 +1374,18 @@ export class AgentManager { "agent.manager.close.start", ); const closedAgent = this.prepareAgentForClosure(agent, "agent closed"); - await agent.session.close(); - this.timelineStore.delete(agentId); - for (const event of this.providerSubagents.deleteParent(agentId)) { - this.dispatch({ type: "provider_subagent", event }); + let closeError: unknown; + try { + await agent.session.close(); + } catch (error) { + closeError = error; } - if (options.persistClosedState !== false) { + + let persistError: unknown; + try { await this.persistSnapshot(closedAgent); + } catch (error) { + persistError = error; } this.emitClosedAgent(closedAgent, { persist: false }); this.logger.trace( @@ -1341,6 +1396,58 @@ export class AgentManager { }, "agent.manager.close.complete", ); + + if (closeError !== undefined) { + throw closeError; + } + if (persistError !== undefined) { + throw persistError; + } + } + + async collectIdleAgents(options: { + cutoff: Date; + protectedAgentIds: ReadonlySet<string>; + }): Promise<IdleAgentCollectionResult> { + const result: IdleAgentCollectionResult = { collected: [], failures: [] }; + + for (const agent of Array.from(this.agents.values())) { + const current = this.agents.get(agent.id); + if (!current || !this.isIdleAgentCollectable(current, options)) { + continue; + } + + const entry: IdleAgentCollectionEntry = { + agentId: current.id, + provider: current.provider, + ...(current.persistence?.sessionId ? { sessionId: current.persistence.sessionId } : {}), + }; + try { + await this.closeAgent(current.id); + result.collected.push(entry); + } catch (error) { + result.failures.push({ ...entry, error }); + } + } + + return result; + } + + private isIdleAgentCollectable( + agent: LiveManagedAgent, + options: { cutoff: Date; protectedAgentIds: ReadonlySet<string> }, + ): agent is ManagedAgentIdle { + return ( + agent.lifecycle === "idle" && + agent.updatedAt.getTime() <= options.cutoff.getTime() && + !agent.internal && + !options.protectedAgentIds.has(agent.id) && + agent.activeForegroundTurnId === null && + !this.runs.hasRun(agent.id) && + !agent.pendingReplacement && + agent.pendingPermissions.size === 0 && + agent.inFlightPermissionResponses.size === 0 + ); } async archiveAgent(agentId: string): Promise<{ archivedAt: string }> { @@ -1360,6 +1467,7 @@ export class AgentManager { const { archivedAt } = await this.markRecordArchived(stored); agent.updatedAt = new Date(archivedAt); await this.closeAgent(agentId); + this.discardRetainedAgentState(agentId); await this.cascadeArchiveChildren(agentId); @@ -1386,8 +1494,7 @@ export class AgentManager { if (this.agents.has(record.id)) { await this.archiveAgent(record.id); } else { - await this.markRecordArchived(record); - await this.cascadeArchiveChildren(record.id); + await this.archiveSnapshot(record.id, new Date().toISOString()); } } } @@ -1674,11 +1781,15 @@ export class AgentManager { if (this.agents.has(agentId)) { this.notifyAgentState(agentId); - } else if (!nextRecord.internal) { - this.dispatchArchivedStoredAgent(nextRecord); + } else { + this.discardRetainedAgentState(agentId); + if (!nextRecord.internal) { + this.dispatchArchivedStoredAgent(nextRecord); + } } await this.fireAgentArchived(agentId); + await this.cascadeArchiveChildren(agentId); return nextRecord; } @@ -2329,6 +2440,11 @@ export class AgentManager { await this.durableTimelineStore.deleteAgent(agentId); } + async deleteAgentState(agentId: string): Promise<void> { + this.discardRetainedAgentState(agentId); + await this.deleteCommittedTimeline(agentId); + } + async getLastAssistantMessage(agentId: string): Promise<string | null> { const agent = this.agents.get(agentId); if (!agent) { @@ -2638,6 +2754,7 @@ export class AgentManager { await this.refreshSessionState(managed, { emit: false }); this.assertAgentRegistrationActive(managed); managed.lifecycle = "idle"; + this.touchUpdatedAt(managed); await this.persistSnapshot(managed); this.assertAgentRegistrationActive(managed); this.emitState(managed, { persist: false }); @@ -2686,6 +2803,7 @@ export class AgentManager { | undefined; }): Promise<{ durableTimelineHasRows: boolean }> { const { agentId, now, options } = params; + const timelineAlreadyPrimed = this.timelineStore.has(agentId); const explicitTimelineSeed = buildExplicitTimelineSeedForRegister(now, options); const shouldSeedFromDurable = !explicitTimelineSeed && @@ -2695,7 +2813,8 @@ export class AgentManager { ? await this.loadCommittedTimelineSeed(agentId, now) : null; const durableTimelineHasRows = - durableTimelineSeed != null && (durableTimelineSeed.nextSeq ?? 1) > 1; + timelineAlreadyPrimed || + (durableTimelineSeed != null && (durableTimelineSeed.nextSeq ?? 1) > 1); const timelineSeed = explicitTimelineSeed ?? durableTimelineSeed; if (timelineSeed || !this.timelineStore.has(agentId)) { this.timelineStore.initialize(agentId, timelineSeed ?? { timestamp: now.toISOString() }); @@ -2803,9 +2922,23 @@ export class AgentManager { lifecycle: "closed", session: null, activeForegroundTurnId: null, + pendingPermissions: new Map(), + bufferedPermissionResolutions: new Map(), + inFlightPermissionResponses: new Set(), + pendingReplacement: false, + foregroundTurnWaiters: new Set(), + finalizedForegroundTurnIds: new Set(), + unsubscribeSession: null, }; } + private discardRetainedAgentState(agentId: string): void { + this.timelineStore.delete(agentId); + for (const event of this.providerSubagents.deleteParent(agentId)) { + this.dispatch({ type: "provider_subagent", event }); + } + } + private emitClosedAgent(agent: ManagedAgentClosed, options?: { persist?: boolean }): void { this.emitState(agent, options); } diff --git a/packages/server/src/server/agent/agent-response-loop.ts b/packages/server/src/server/agent/agent-response-loop.ts index d3a3de010..a76f82fda 100644 --- a/packages/server/src/server/agent/agent-response-loop.ts +++ b/packages/server/src/server/agent/agent-response-loop.ts @@ -382,6 +382,8 @@ export async function generateStructuredAgentResponse<T>( await manager.closeAgent(agent.id); } catch { // ignore cleanup errors + } finally { + await manager.deleteAgentState(agent.id).catch(() => undefined); } } } diff --git a/packages/server/src/server/agent/agent-sdk-types.ts b/packages/server/src/server/agent/agent-sdk-types.ts index 16da95c05..3813e4eec 100644 --- a/packages/server/src/server/agent/agent-sdk-types.ts +++ b/packages/server/src/server/agent/agent-sdk-types.ts @@ -630,6 +630,7 @@ export interface AgentSession { ): Promise<AgentPermissionResult | void>; describePersistence(): AgentPersistenceHandle | null; interrupt(): Promise<void>; + /** Release live runtime resources without archiving or deleting the durable native session. */ close(): Promise<void>; listCommands?(): Promise<AgentSlashCommand[]>; setModel?(modelId: string | null): Promise<void>; @@ -707,12 +708,12 @@ export interface AgentClient { isAvailable(): Promise<boolean>; getDiagnostic?(): Promise<{ diagnostic: string }>; /** - * Archive a persisted session in the native provider (best-effort). + * Archive a durable native session (best-effort). Runtime release belongs to AgentSession.close(). * Called when Paseo archives an agent so the provider's own UI reflects the same state. */ archiveNativeSession?(handle: AgentPersistenceHandle): Promise<void>; /** - * Unarchive a persisted session in the native provider. + * Unarchive a durable native session in the provider. * Called before Paseo clears its archived flag so provider resume can succeed. */ unarchiveNativeSession?(handle: AgentPersistenceHandle): Promise<void>; diff --git a/packages/server/src/server/agent/providers/opencode-agent-commands.e2e.test.ts b/packages/server/src/server/agent/providers/opencode-agent-commands.e2e.test.ts index f712c6034..8dd3832cf 100644 --- a/packages/server/src/server/agent/providers/opencode-agent-commands.e2e.test.ts +++ b/packages/server/src/server/agent/providers/opencode-agent-commands.e2e.test.ts @@ -37,6 +37,28 @@ describe("opencode agent commands E2E", () => { } }, 60_000); + test("listing commands resumes an idle-collected agent", async () => { + const agent = await ctx.client.createAgent({ + ...getFullAccessConfig("opencode"), + cwd: "/tmp", + title: "Collected OpenCode Commands Test Agent", + }); + + const collection = await ctx.daemon.daemon.agentManager.collectIdleAgents({ + cutoff: new Date(Date.now() + 1_000), + protectedAgentIds: new Set(), + }); + expect(collection.failures).toEqual([]); + expect(collection.collected.map((entry) => entry.agentId)).toContain(agent.id); + expect(ctx.daemon.daemon.agentManager.getAgent(agent.id)).toBeNull(); + + const result = await ctx.client.listCommands({ agentId: agent.id }); + + expect(result.error).toBeNull(); + expect(result.commands.length).toBeGreaterThan(0); + expect(ctx.daemon.daemon.agentManager.getAgent(agent.id)?.id).toBe(agent.id); + }, 60_000); + test("sendMessage executes a slash command without arguments", async () => { const agent = await ctx.client.createAgent({ ...getFullAccessConfig("opencode"), diff --git a/packages/server/src/server/agent/providers/opencode-agent.test.ts b/packages/server/src/server/agent/providers/opencode-agent.test.ts index 829422ea7..e5ef7f681 100644 --- a/packages/server/src/server/agent/providers/opencode-agent.test.ts +++ b/packages/server/src/server/agent/providers/opencode-agent.test.ts @@ -194,7 +194,8 @@ describe("OpenCodeAgentClient adapter smoke tests", () => { test("creates a session with valid id and provider", async () => { const cwd = tmpCwd(); const runtime = new TestOpenCodeHarness(); - runtime.enqueueClient(new TestOpenCodeClient()); + const openCode = new TestOpenCodeClient(); + runtime.enqueueClient(openCode); const client = new OpenCodeAgentClient(logger, undefined, { serverManager: runtime, createClient: runtime.createClient, @@ -206,9 +207,49 @@ describe("OpenCodeAgentClient adapter smoke tests", () => { expect(session.provider).toBe("opencode"); await session.close(); + expect(openCode.calls.sessionAbort).toEqual([{ sessionID: "session-1", directory: cwd }]); + expect(openCode.calls.sessionUpdate).toEqual([]); rmSync(cwd, { recursive: true, force: true }); }, 60_000); + test("archives and unarchives the durable native session through client hooks", async () => { + const cwd = tmpCwd(); + const runtime = new TestOpenCodeHarness(); + const archiveClient = new TestOpenCodeClient(); + const unarchiveClient = new TestOpenCodeClient(); + runtime.enqueueClient(archiveClient); + runtime.enqueueClient(unarchiveClient); + const client = new OpenCodeAgentClient(logger, undefined, { + serverManager: runtime, + createClient: runtime.createClient, + }); + const handle = { + provider: "opencode" as const, + sessionId: "session-1", + metadata: { cwd }, + }; + + await client.archiveNativeSession(handle); + await client.unarchiveNativeSession(handle); + + expect(archiveClient.calls.sessionUpdate).toEqual([ + { + sessionID: "session-1", + directory: cwd, + time: { archived: expect.any(Number) }, + }, + ]); + expect(unarchiveClient.calls.sessionUpdate).toEqual([ + { + sessionID: "session-1", + directory: cwd, + time: { archived: null }, + }, + ]); + expect(runtime.acquisitions.every((acquisition) => acquisition.releaseCount === 1)).toBe(true); + rmSync(cwd, { recursive: true, force: true }); + }); + test("single turn completes with streaming deltas", async () => { const cwd = tmpCwd(); const runtime = new TestOpenCodeHarness(); @@ -713,65 +754,6 @@ describe("OpenCodeAgentClient adapter smoke tests", () => { }); describe("OpenCode adapter context-window normalization", () => { - test("close reconciliation aborts then archives upstream session", async () => { - const abort = vi.fn().mockResolvedValue({ data: true, error: undefined }); - const update = vi.fn().mockResolvedValue({ - data: { id: "session-1", time: { archived: Date.now() } }, - error: undefined, - }); - - await __openCodeInternals.reconcileOpenCodeSessionClose({ - client: { - session: { - abort, - update, - }, - } as never, - sessionId: "session-1", - directory: "/tmp/project", - logger: createTestLogger(), - }); - - expect(abort).toHaveBeenCalledWith({ - sessionID: "session-1", - directory: "/tmp/project", - }); - expect(update).toHaveBeenCalledTimes(1); - expect(update).toHaveBeenCalledWith({ - sessionID: "session-1", - directory: "/tmp/project", - time: { - archived: expect.any(Number), - }, - }); - }); - - test("close reconciliation still archives when abort returns an error", async () => { - const abort = vi.fn().mockResolvedValue({ - data: undefined, - error: { data: {}, errors: [], success: false }, - }); - const update = vi.fn().mockResolvedValue({ - data: { id: "session-1", time: { archived: Date.now() } }, - error: undefined, - }); - - await __openCodeInternals.reconcileOpenCodeSessionClose({ - client: { - session: { - abort, - update, - }, - } as never, - sessionId: "session-1", - directory: "/tmp/project", - logger: createTestLogger(), - }); - - expect(abort).toHaveBeenCalledTimes(1); - expect(update).toHaveBeenCalledTimes(1); - }); - test("builds OpenCode file parts for image prompt blocks", () => { expect( __openCodeInternals.buildOpenCodePromptParts([ @@ -2345,6 +2327,7 @@ describe("OpenCode persisted sessions", () => { describe("OpenCode provider subagent contract", () => { async function createAdoptedChildSession(): Promise<{ readonly runtime: TestOpenCodeHarness; + readonly provider: OpenCodeAgentClient; readonly parent: Awaited<ReturnType<OpenCodeAgentClient["createSession"]>>; readonly child: Awaited<ReturnType<OpenCodeAgentClient["resumeSession"]>>; readonly childClient: TestOpenCodeClient; @@ -2387,9 +2370,36 @@ describe("OpenCode provider subagent contract", () => { undefined, { env: { PASEO_AGENT_ID: "child-agent" } }, ); - return { runtime, parent, child, childClient }; + return { runtime, provider: client, parent, child, childClient }; } + test("archives an adopted child on the parent's registered OpenCode server", async () => { + const { runtime, provider, parent, child } = await createAdoptedChildSession(); + const archiveClient = new TestOpenCodeClient(); + runtime.enqueueClient(archiveClient); + + await provider.archiveNativeSession({ + provider: "opencode", + sessionId: "ses_child_external", + metadata: { cwd: "/workspace/repo" }, + }); + + expect(archiveClient.calls.sessionUpdate).toEqual([ + { + sessionID: "ses_child_external", + directory: "/workspace/repo", + time: { archived: expect.any(Number) }, + }, + ]); + expect(runtime.acquisitions.at(-1)).toEqual({ + kind: "existing", + url: runtime.server.url, + releaseCount: 1, + }); + await child.close(); + await parent.close(); + }); + test("resumes an adopted child on the parent's registered OpenCode server", async () => { const runtime = new TestOpenCodeHarness(); const parentClient = new TestOpenCodeClient(); diff --git a/packages/server/src/server/agent/providers/opencode-agent.ts b/packages/server/src/server/agent/providers/opencode-agent.ts index b70fdfa76..aabd1b627 100644 --- a/packages/server/src/server/agent/providers/opencode-agent.ts +++ b/packages/server/src/server/agent/providers/opencode-agent.ts @@ -431,7 +431,7 @@ function isOpenCodeNotFoundError(error: unknown): boolean { ); } -async function reconcileOpenCodeSessionClose(params: { +async function abortOpenCodeSession(params: { client: Pick<OpencodeClient, "session">; sessionId: string; directory: string; @@ -462,31 +462,6 @@ async function reconcileOpenCodeSessionClose(params: { "Failed to abort OpenCode session during close", ); } - - try { - const response = await client.session.update({ - sessionID: sessionId, - directory, - time: { archived: Date.now() }, - }); - if (response.error && !isOpenCodeNotFoundError(response.error)) { - logger.warn( - { - sessionId, - error: toDiagnosticErrorMessage(response.error), - }, - "Failed to archive OpenCode session during close", - ); - } - } catch (error) { - logger.warn( - { - sessionId, - error: toDiagnosticErrorMessage(error), - }, - "Failed to archive OpenCode session during close", - ); - } } function isOpenCodeHeadersTimeoutFailure(error: unknown): boolean { @@ -1254,7 +1229,6 @@ export const __openCodeInternals = { hasNormalizedOpenCodeUsage, mergeOpenCodeStepFinishUsage, parseOpenCodeModelLookupKey, - reconcileOpenCodeSessionClose, resolveOpenCodeModelLookupKeyFromAssistantMessage, resolveOpenCodeSelectedModelContextWindow, isSelectableOpenCodeAgent, @@ -1353,7 +1327,7 @@ export class OpenCodeAgentClient implements AgentClient { url, ); } catch (error) { - acquisition.release(); + await acquisition.release(); throw error; } } @@ -1407,7 +1381,7 @@ export class OpenCodeAgentClient implements AgentClient { registeredAcquisition !== null, ); } catch (error) { - acquisition.release(); + await acquisition.release(); throw error; } } @@ -1439,7 +1413,7 @@ export class OpenCodeAgentClient implements AgentClient { ]); return { models, modes }; } finally { - acquisition.release(); + await acquisition.release(); } } @@ -1455,7 +1429,7 @@ export class OpenCodeAgentClient implements AgentClient { try { return await listOpenCodeCommandsFromSdk(client, openCodeConfig.cwd); } finally { - acquisition.release(); + await acquisition.release(); } } @@ -1476,7 +1450,7 @@ export class OpenCodeAgentClient implements AgentClient { try { return await collectOpenCodeImportableSessionsFromSdk(client, options); } finally { - acquisition.release(); + await acquisition.release(); } } @@ -1512,7 +1486,57 @@ export class OpenCodeAgentClient implements AgentClient { }, }); } finally { - acquisition.release(); + await acquisition.release(); + } + } + + async archiveNativeSession(handle: AgentPersistenceHandle): Promise<void> { + await this.setNativeSessionArchived(handle, Date.now()); + } + + async unarchiveNativeSession(handle: AgentPersistenceHandle): Promise<void> { + await this.setNativeSessionArchived(handle, null); + } + + private async setNativeSessionArchived( + handle: AgentPersistenceHandle, + archivedAt: number | null, + ): Promise<void> { + const metadata = (handle.metadata ?? {}) as Partial<AgentSessionConfig>; + if (!metadata.cwd) { + throw new Error("OpenCode native archive update requires the original working directory"); + } + + const registeredServerUrl = getOpenCodeChildSessionServerUrl(handle.sessionId); + const acquisition = + (registeredServerUrl ? this.serverManager.acquireExisting(registeredServerUrl) : null) ?? + (await this.serverManager.acquireCurrent()); + const client = this.createOpenCodeClient({ + baseUrl: acquisition.server.url, + directory: metadata.cwd, + }); + try { + // OpenCode accepts null to clear the archive timestamp, but this SDK + // release's generated request type still exposes only number. + const updateSession = client.session.update.bind(client.session) as (parameters: { + sessionID: string; + directory?: string; + time?: { archived?: number | null }; + }) => ReturnType<typeof client.session.update>; + const response = readOpenCodeRecord( + await updateSession({ + sessionID: handle.sessionId, + directory: metadata.cwd, + time: { archived: archivedAt }, + }), + ); + if (response?.error) { + throw new Error( + `Failed to ${archivedAt === null ? "unarchive" : "archive"} OpenCode session: ${toDiagnosticErrorMessage(response.error)}`, + ); + } + } finally { + await acquisition.release(); } } @@ -2897,7 +2921,7 @@ class OpenCodeAgentSession implements AgentSession { private childHydrationCompleted = false; private readonly unrelatedSessionIds = new Set<string>(); private selectedModelContextWindowMaxTokens: number | undefined; - private releaseServer: (() => void) | null; + private releaseServer: (() => Promise<void>) | null; private eventStreamAbortController: AbortController | null = null; private eventStreamReady: Deferred<void> | null = null; private eventStreamTask: Promise<void> | null = null; @@ -2911,7 +2935,7 @@ class OpenCodeAgentSession implements AgentSession { sessionId: string, logger: Logger, modelContextWindowsByModelKey: ReadonlyMap<string, number> = new Map(), - releaseServer?: () => void, + releaseServer?: () => Promise<void>, persistSession = true, private readonly agentId?: string, private readonly serverUrl?: string, @@ -3891,7 +3915,7 @@ class OpenCodeAgentSession implements AgentSession { this.eventStreamReady = null; this.eventStreamTask = null; this.subscribers.clear(); - await reconcileOpenCodeSessionClose({ + await abortOpenCodeSession({ client: this.client, sessionId: this.sessionId, directory: this.config.cwd, @@ -3900,7 +3924,7 @@ class OpenCodeAgentSession implements AgentSession { await this.deleteProviderSessionIfEphemeral(); this.activeForegroundTurnId = null; } finally { - this.releaseServer?.(); + await this.releaseServer?.(); this.releaseServer = null; } } diff --git a/packages/server/src/server/agent/providers/opencode-server-manager.test.ts b/packages/server/src/server/agent/providers/opencode-server-manager.test.ts index 60c93c6ef..de094dba6 100644 --- a/packages/server/src/server/agent/providers/opencode-server-manager.test.ts +++ b/packages/server/src/server/agent/providers/opencode-server-manager.test.ts @@ -37,10 +37,10 @@ describe("OpenCodeServerManager generations", () => { expect(newAcquisition.server.url).toBe("http://127.0.0.1:4102"); expect(runtime.terminatedPorts).toEqual([]); - newAcquisition.release(); - oldAcquisition.release(); + await newAcquisition.release(); + await oldAcquisition.release(); - expect(runtime.terminatedPorts).toEqual([4101]); + expect(runtime.terminatedPorts).toEqual([4102, 4101]); }); test("new acquisitions after rotation use the new server", async () => { @@ -48,22 +48,22 @@ describe("OpenCodeServerManager generations", () => { const oldAcquisition = await manager.acquireCurrent(); const rotatedAcquisition = await manager.acquireNew(); - rotatedAcquisition.release(); - const nextAcquisition = await manager.acquireCurrent(); expect(nextAcquisition.server.url).toBe("http://127.0.0.1:4202"); expect(runtime.terminatedPorts).toEqual([]); - nextAcquisition.release(); - oldAcquisition.release(); + await rotatedAcquisition.release(); + expect(runtime.terminatedPorts).toEqual([]); + await nextAcquisition.release(); + await oldAcquisition.release(); }); test("concurrent new-server acquisitions share one fresh generation", async () => { const { manager, runtime } = createTestManager([4251, 4252, 4253]); const initialAcquisition = await manager.acquireCurrent(); - initialAcquisition.release(); + await initialAcquisition.release(); const [modelsAcquisition, modesAcquisition] = await Promise.all([ manager.acquireNew(), @@ -74,8 +74,8 @@ describe("OpenCodeServerManager generations", () => { expect(modesAcquisition.server.url).toBe("http://127.0.0.1:4252"); expect(runtime.launchedPorts).toEqual([4251, 4252]); - modesAcquisition.release(); - modelsAcquisition.release(); + await modesAcquisition.release(); + await modelsAcquisition.release(); }); test("release is idempotent", async () => { @@ -83,12 +83,12 @@ describe("OpenCodeServerManager generations", () => { const oldAcquisition = await manager.acquireCurrent(); const newAcquisition = await manager.acquireNew(); - newAcquisition.release(); + await newAcquisition.release(); - oldAcquisition.release(); - oldAcquisition.release(); + await oldAcquisition.release(); + await oldAcquisition.release(); - expect(runtime.terminatedPorts).toEqual([4301]); + expect(runtime.terminatedPorts).toEqual([4302, 4301]); }); test("shutdown kills current and retired servers", async () => { @@ -152,16 +152,16 @@ describe("OpenCodeServerManager generations", () => { const dedicatedStart = manager.acquireDedicated({ TEST_ENV: "custom" }); await runtime.settle(); - currentAcquisition.release(); - expect(runtime.terminatedPorts).toEqual([]); + await currentAcquisition.release(); + expect(runtime.terminatedPorts).toEqual([4473]); runtime.processForPort(4474).announceListening(); const dedicatedAcquisition = await dedicatedStart; expect(dedicatedAcquisition.server.url).toBe("http://127.0.0.1:4474"); - dedicatedAcquisition.release(); - expect(runtime.terminatedPorts).toEqual([4474]); + await dedicatedAcquisition.release(); + expect(runtime.terminatedPorts).toEqual([4473, 4474]); }); test("acquireExisting keeps a retired dedicated server alive until every reference releases", async () => { @@ -172,10 +172,10 @@ describe("OpenCodeServerManager generations", () => { expect(existingAcquisition?.server.url).toBe("http://127.0.0.1:4475"); - dedicatedAcquisition.release(); + await dedicatedAcquisition.release(); expect(runtime.terminatedPorts).toEqual([]); - existingAcquisition?.release(); + await existingAcquisition?.release(); expect(runtime.terminatedPorts).toEqual([4475]); }); @@ -187,7 +187,7 @@ describe("OpenCodeServerManager generations", () => { expect(manager.acquireExisting("http://127.0.0.1:9999")).toBe(null); - acquisition.release(); + await acquisition.release(); expect(runtime.terminatedPorts).toEqual([4476]); expect(manager.acquireExisting(url)).toBe(null); }); @@ -197,12 +197,26 @@ describe("OpenCodeServerManager generations", () => { const firstAcquisition = await manager.acquireCurrent(); const secondAcquisition = await manager.acquireNew(); - secondAcquisition.release(); + await secondAcquisition.release(); const thirdAcquisition = await manager.acquireNew(); - thirdAcquisition.release(); - firstAcquisition.release(); + await thirdAcquisition.release(); + await firstAcquisition.release(); - expect(runtime.terminatedPorts).toEqual([4502, 4501]); + expect(runtime.terminatedPorts).toEqual([4502, 4503, 4501]); + }); + + test("final release detaches the terminating generation before a concurrent acquire", async () => { + const { manager, runtime } = createTestManager([4551, 4552]); + + const first = await manager.acquireCurrent(); + const release = first.release(); + const next = await manager.acquireCurrent(); + + await release; + expect(next.server.url).toBe("http://127.0.0.1:4552"); + expect(runtime.terminatedPorts).toEqual([4551]); + + await next.release(); }); }); @@ -258,7 +272,7 @@ describe("OpenCodeServerManager managed process ledger", () => { }), ]); - acquisition.release(); + await acquisition.release(); await manager.shutdown(); } finally { rmSync(tempDir, { recursive: true, force: true }); diff --git a/packages/server/src/server/agent/providers/opencode/server-manager.ts b/packages/server/src/server/agent/providers/opencode/server-manager.ts index 3fbb89ad2..d765d3d27 100644 --- a/packages/server/src/server/agent/providers/opencode/server-manager.ts +++ b/packages/server/src/server/agent/providers/opencode/server-manager.ts @@ -21,11 +21,10 @@ const OPENCODE_SERVER_FORCE_SHUTDOWN_TIMEOUT_MS = 1_000; export interface OpenCodeServerAcquisition { server: { port: number; url: string }; - release: () => void; + release: () => Promise<void>; } export interface OpenCodeServerManagerLike { - ensureRunning(): Promise<{ port: number; url: string }>; acquireCurrent(): Promise<OpenCodeServerAcquisition>; acquireNew(): Promise<OpenCodeServerAcquisition>; acquireDedicated(env: Record<string, string>): Promise<OpenCodeServerAcquisition>; @@ -135,12 +134,6 @@ export class OpenCodeServerManager implements OpenCodeServerManagerLike { process.on("SIGINT", cleanup); } - async ensureRunning(): Promise<{ port: number; url: string }> { - const acquisition = await this.acquireCurrent(); - acquisition.release(); - return acquisition.server; - } - async acquireCurrent(): Promise<OpenCodeServerAcquisition> { const server = await this.getCurrentServer(); return this.acquireServer(server); @@ -160,7 +153,7 @@ export class OpenCodeServerManager implements OpenCodeServerManagerLike { await server.ready; return acquisition; } catch (error) { - acquisition.release(); + await acquisition.release(); throw error; } } @@ -188,20 +181,37 @@ export class OpenCodeServerManager implements OpenCodeServerManagerLike { private acquireServer(server: OpenCodeServerGeneration): OpenCodeServerAcquisition { server.refCount += 1; - let released = false; + let releasePromise: Promise<void> | null = null; return { server: { port: server.port, url: server.url }, - release: () => { - if (released) { - return; + release: async () => { + if (releasePromise) { + return releasePromise; } - released = true; - server.refCount -= 1; - this.cleanupRetiredServers(); + releasePromise = this.releaseServer(server); + return releasePromise; }, }; } + private async releaseServer(server: OpenCodeServerGeneration): Promise<void> { + server.refCount = Math.max(0, server.refCount - 1); + if (server.refCount > 0) { + return; + } + + if (this.currentServer === server) { + this.currentServer = null; + server.retired = true; + } + if (!server.retired) { + return; + } + + this.retiredServers.delete(server); + await this.killServer(server); + } + private async getNewServer(): Promise<OpenCodeServerGeneration> { if (this.newServerPromise) { return this.newServerPromise; @@ -261,14 +271,14 @@ export class OpenCodeServerManager implements OpenCodeServerManagerLike { existing.retired = true; this.retiredServers.add(existing); this.currentServer = null; - this.cleanupRetiredServers(); + await this.cleanupRetiredServers(); } if (this.startPromise) { const pending = await this.startPromise; pending.retired = true; this.retiredServers.add(pending); this.currentServer = null; - this.cleanupRetiredServers(); + await this.cleanupRetiredServers(); } } @@ -417,13 +427,15 @@ export class OpenCodeServerManager implements OpenCodeServerManagerLike { this.retiredServers.clear(); } - private cleanupRetiredServers(): void { + private async cleanupRetiredServers(): Promise<void> { + const cleanup: Promise<void>[] = []; for (const server of Array.from(this.retiredServers)) { if (server.refCount === 0) { this.retiredServers.delete(server); - void this.killServer(server); + cleanup.push(this.killServer(server)); } } + await Promise.all(cleanup); } private async killServer(server: OpenCodeServerGeneration): Promise<void> { diff --git a/packages/server/src/server/agent/providers/opencode/test-server-manager.ts b/packages/server/src/server/agent/providers/opencode/test-server-manager.ts index f13a2eb95..e4cc8a65e 100644 --- a/packages/server/src/server/agent/providers/opencode/test-server-manager.ts +++ b/packages/server/src/server/agent/providers/opencode/test-server-manager.ts @@ -10,12 +10,6 @@ export interface TestOpenCodeServerAcquisition { export class TestOpenCodeServerManager implements OpenCodeServerManagerLike { readonly acquisitions: TestOpenCodeServerAcquisition[] = []; readonly server = { port: 1234, url: "http://127.0.0.1:1234" }; - ensureRunningCount = 0; - - async ensureRunning(): Promise<{ port: number; url: string }> { - this.ensureRunningCount += 1; - return this.server; - } async acquireCurrent(): Promise<OpenCodeServerAcquisition> { return this.recordAcquisition({ kind: "current" }); @@ -47,7 +41,7 @@ export class TestOpenCodeServerManager implements OpenCodeServerManagerLike { this.acquisitions.push(acquisition); return { server: this.server, - release: () => { + release: async () => { acquisition.released = true; }, }; diff --git a/packages/server/src/server/agent/providers/opencode/test-utils/test-opencode-harness.ts b/packages/server/src/server/agent/providers/opencode/test-utils/test-opencode-harness.ts index 754b1b86d..3c3ac4dc7 100644 --- a/packages/server/src/server/agent/providers/opencode/test-utils/test-opencode-harness.ts +++ b/packages/server/src/server/agent/providers/opencode/test-utils/test-opencode-harness.ts @@ -53,16 +53,12 @@ export class TestOpenCodeHarness implements OpenCodeServerManagerLike { this.acquisitions.push(acquisition); return { server: this.server, - release: () => { + release: async () => { acquisition.releaseCount += 1; }, }; } - async ensureRunning(): Promise<{ port: number; url: string }> { - return this.server; - } - readonly createClient = (options: { baseUrl: string; directory: string }): OpencodeClient => { this.clientCreations.push(options); const client = this.clients.shift() ?? new TestOpenCodeClient(); diff --git a/packages/server/src/server/bootstrap.ts b/packages/server/src/server/bootstrap.ts index c9d27c881..4d3d48929 100644 --- a/packages/server/src/server/bootstrap.ts +++ b/packages/server/src/server/bootstrap.ts @@ -217,6 +217,8 @@ import { DaemonExecutions } from "./hub/daemon-executions.js"; const MAX_MCP_DEBUG_BATCH_ITEMS = 10; const REDACTED_LOG_VALUE = "[redacted]"; +const IDLE_AGENT_RUNTIME_TTL_MS = 2 * 60 * 1000; +const IDLE_AGENT_RUNTIME_SWEEP_INTERVAL_MS = 15 * 1000; const DOWNLOAD_OPEN_FLAGS = process.platform === "win32" ? constants.O_RDONLY : constants.O_RDONLY | constants.O_NOFOLLOW; @@ -1175,6 +1177,39 @@ export async function createPaseoDaemon( archiveWorkspace: archiveScheduleWorkspaceExternal, }); await scheduleService.start(); + let inFlightIdleAgentCollection: Promise<void> | null = null; + const collectIdleAgentRuntimes = async () => { + const protectedAgentIds = await scheduleService.listActiveAgentTargetIds(); + const cutoff = new Date(Date.now() - IDLE_AGENT_RUNTIME_TTL_MS); + const result = await agentManager.collectIdleAgents({ cutoff, protectedAgentIds }); + for (const collected of result.collected) { + logger.info(collected, "Collected idle agent runtime"); + } + for (const failure of result.failures) { + const { error, ...context } = failure; + logger.warn({ ...context, err: error }, "Failed to collect idle agent runtime"); + } + }; + const runIdleAgentCollection = () => { + if (inFlightIdleAgentCollection) { + return; + } + const collection = collectIdleAgentRuntimes() + .catch((error) => { + logger.warn({ err: error }, "Idle agent runtime sweep failed"); + }) + .finally(() => { + if (inFlightIdleAgentCollection === collection) { + inFlightIdleAgentCollection = null; + } + }); + inFlightIdleAgentCollection = collection; + }; + const idleAgentCollectionTimer = setInterval( + runIdleAgentCollection, + IDLE_AGENT_RUNTIME_SWEEP_INTERVAL_MS, + ); + idleAgentCollectionTimer.unref(); agentManager.setAgentArchivedCallback(async (agentId) => { try { await scheduleService.completeForAgent(agentId); @@ -1564,6 +1599,8 @@ export async function createPaseoDaemon( await hubRelationships.stop(); workspaceReconciliation.dispose(); scriptHealthMonitor.stop(); + clearInterval(idleAgentCollectionTimer); + await inFlightIdleAgentCollection; // Freeze both ingress and registration before taking the agent closure snapshot. wsServer?.prepareForShutdown(); agentManager.prepareForShutdown(); diff --git a/packages/server/src/server/hub/daemon-executions.ts b/packages/server/src/server/hub/daemon-executions.ts index 83b4d0693..712a7ecc2 100644 --- a/packages/server/src/server/hub/daemon-executions.ts +++ b/packages/server/src/server/hub/daemon-executions.ts @@ -177,7 +177,11 @@ export class DaemonExecutions implements HubExecutionAgents { try { await autoArchiveRegistration.cancel(); if (createdAgentId && this.agentManager.getAgent(createdAgentId)) { - await this.agentManager.closeAgent(createdAgentId); + try { + await this.agentManager.closeAgent(createdAgentId); + } finally { + await this.agentManager.deleteAgentState(createdAgentId); + } } } finally { try { diff --git a/packages/server/src/server/loop-service.ts b/packages/server/src/server/loop-service.ts index 99e5f8ff3..43474796c 100644 --- a/packages/server/src/server/loop-service.ts +++ b/packages/server/src/server/loop-service.ts @@ -241,6 +241,7 @@ type LoopAgentManager = Pick< | "archiveAgent" | "cancelAgentRun" | "closeAgent" + | "deleteAgentState" | "getAgent" | "runAgent" | "subscribe" @@ -570,7 +571,7 @@ export class LoopService { await this.options.agentManager.archiveAgent(agentId); return; } - await this.options.agentManager.closeAgent(agentId); + await this.closeInternalAgent(agentId); } catch (error) { if (!isUnknownLoopAgentError(error, agentId)) { throw error; @@ -578,6 +579,14 @@ export class LoopService { } } + private async closeInternalAgent(agentId: string): Promise<void> { + try { + await this.options.agentManager.closeAgent(agentId); + } finally { + await this.options.agentManager.deleteAgentState(agentId); + } + } + private async executeLoop(loopId: string, signal: AbortSignal): Promise<void> { const loop = this.requireLoop(loopId); const deadline = loop.maxTimeMs ? Date.now() + loop.maxTimeMs : null; @@ -775,7 +784,7 @@ export class LoopService { if (loop.archive) { await this.options.agentManager.archiveAgent(agent.id); } else { - await this.options.agentManager.closeAgent(agent.id); + await this.closeInternalAgent(agent.id); } } catch { // Ignore cleanup errors for internal loop workers. @@ -917,7 +926,7 @@ export class LoopService { if (loop.archive) { await this.options.agentManager.archiveAgent(verifierAgent.id); } else { - await this.options.agentManager.closeAgent(verifierAgent.id); + await this.closeInternalAgent(verifierAgent.id); } } catch { // Ignore cleanup errors for internal loop verifiers. diff --git a/packages/server/src/server/schedule/service.test.ts b/packages/server/src/server/schedule/service.test.ts index a7fad87be..b713641e0 100644 --- a/packages/server/src/server/schedule/service.test.ts +++ b/packages/server/src/server/schedule/service.test.ts @@ -376,6 +376,47 @@ describe("ScheduleService", () => { expect(resumed.nextRunAt).toBe("2026-01-01T00:04:00.000Z"); }); + test("lists only active schedules that target existing agents", async () => { + const service = createScheduleService({ + paseoHome: tempDir, + logger: createTestLogger(), + agentManager: new AgentManager({ logger: createTestLogger() }), + agentStorage, + providerSnapshotManager: NO_UNATTENDED_SCHEDULE_POLICY, + now: () => now, + runner: async () => ({ agentId: null, output: "ok" }), + }); + const activeAgentId = "00000000-0000-4000-8000-000000000201"; + const pausedAgentId = "00000000-0000-4000-8000-000000000202"; + const completedAgentId = "00000000-0000-4000-8000-000000000203"; + const cadence = { type: "every" as const, everyMs: 60_000 }; + + await service.create({ + prompt: "Keep active agent resident", + cadence, + target: { type: "agent", agentId: activeAgentId }, + }); + const paused = await service.create({ + prompt: "Paused heartbeat", + cadence, + target: { type: "agent", agentId: pausedAgentId }, + }); + await service.pause(paused.id); + await service.create({ + prompt: "Completed heartbeat", + cadence, + target: { type: "agent", agentId: completedAgentId }, + }); + await service.completeForAgent(completedAgentId); + await service.create({ + prompt: "Fresh agent each run", + cadence, + target: { type: "new-agent", config: { provider: "claude", cwd: tempDir } }, + }); + + await expect(service.listActiveAgentTargetIds()).resolves.toEqual(new Set([activeAgentId])); + }); + test("completes schedules when max runs is reached", async () => { const service = createScheduleService({ paseoHome: tempDir, diff --git a/packages/server/src/server/schedule/service.ts b/packages/server/src/server/schedule/service.ts index d55ecb464..6bcaed724 100644 --- a/packages/server/src/server/schedule/service.ts +++ b/packages/server/src/server/schedule/service.ts @@ -204,7 +204,9 @@ type ScheduleAgentManager = Pick< | "hydrateTimelineFromProvider" | "resumeAgentFromPersistence" | "runAgent" + | "touchAgentActivity" | "waitForAgentEvent" + | "waitForAgentClose" >; interface ScheduleWorkspaceCreateInput { @@ -368,6 +370,17 @@ export class ScheduleService { return this.store.list(); } + async listActiveAgentTargetIds(): Promise<Set<string>> { + const schedules = await this.store.list(); + const agentIds = new Set<string>(); + for (const schedule of schedules) { + if (schedule.status === "active" && schedule.target.type === "agent") { + agentIds.add(schedule.target.agentId); + } + } + return agentIds; + } + async inspect(id: string): Promise<StoredSchedule> { const schedule = await this.store.get(id); if (!schedule) { diff --git a/packages/server/src/server/session.ts b/packages/server/src/server/session.ts index 7d7ad185a..17510db81 100644 --- a/packages/server/src/server/session.ts +++ b/packages/server/src/server/session.ts @@ -2164,7 +2164,7 @@ export class Session { try { await this.agentStorage.remove(agentId); - await this.agentManager.deleteCommittedTimeline(agentId); + await this.agentManager.deleteAgentState(agentId); } catch (error) { this.sessionLogger.error({ err: error, agentId }, `Failed to fully delete agent ${agentId}`); } @@ -3350,6 +3350,15 @@ export class Session { const agentIds = Array.isArray(agentId) ? agentId : [agentId]; try { + await Promise.all( + agentIds.map((id) => + ensureAgentLoaded(id, { + agentManager: this.agentManager, + agentStorage: this.agentStorage, + logger: this.sessionLogger, + }), + ), + ); await Promise.all(agentIds.map((id) => this.agentManager.clearAgentAttention(id))); if (requestId) { const agents = ( @@ -3436,8 +3445,16 @@ export class Session { ); try { - const agents = this.agentManager.listAgents(); - const agent = agents.find((a) => a.id === agentId); + const existing = this.agentManager.getAgent(agentId); + const stored = existing ? null : await this.agentStorage.get(agentId); + const agent = + existing || (stored && !stored.archivedAt) + ? await ensureAgentLoaded(agentId, { + agentManager: this.agentManager, + agentStorage: this.agentStorage, + logger: this.sessionLogger, + }) + : null; if (agent?.session?.listCommands) { const commands = await agent.session.listCommands(); @@ -5847,6 +5864,11 @@ export class Session { msg: Extract<SessionInboundMessage, { type: "agent.provider_subagents.list.request" }>, ): Promise<void> { try { + await ensureAgentLoaded(msg.parentAgentId, { + agentManager: this.agentManager, + agentStorage: this.agentStorage, + logger: this.sessionLogger, + }); this.emit({ type: "agent.provider_subagents.list.response", payload: { @@ -5874,6 +5896,11 @@ export class Session { ): Promise<void> { const direction: AgentTimelineFetchDirection = msg.direction ?? (msg.cursor ? "after" : "tail"); try { + await ensureAgentLoaded(msg.parentAgentId, { + agentManager: this.agentManager, + agentStorage: this.agentStorage, + logger: this.sessionLogger, + }); const descriptor = this.agentManager.getProviderSubagent(msg.parentAgentId, msg.subagentId); if (!descriptor) { throw new Error("Provider subagent not found"); From 8cc2ae0ba434ed4c500a7580fcab94f10cd53523 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra <boudra.moha@gmail.com> Date: Sat, 18 Jul 2026 22:15:27 +0200 Subject: [PATCH 36/38] Resume collected agents before pane actions (#2209) * fix: resume collected agents before pane actions * fix(server): keep archived agents closed during pane actions An archive could win while a collected agent was resuming, then lose when the provider runtime registered. Recheck persisted archive state after registration and close the resumed runtime before any mutation runs. * fix(server): fence archived agents after shared resume Protected pane actions could join a resume started by an ordinary loader and skip the archive fence. Recheck persisted lifecycle state for every protected caller so archive always wins before mutation. --- .../server/src/server/agent/agent-loading.ts | 23 +++ .../src/server/agent/agent-manager.test.ts | 133 +++++++++++++++++- packages/server/src/server/session.test.ts | 37 +++-- packages/server/src/server/session.ts | 13 +- .../agent-config/agent-config-session.test.ts | 45 ++++++ .../agent-config/agent-config-session.ts | 8 +- 6 files changed, 243 insertions(+), 16 deletions(-) diff --git a/packages/server/src/server/agent/agent-loading.ts b/packages/server/src/server/agent/agent-loading.ts index 01dd9264b..e0e806b01 100644 --- a/packages/server/src/server/agent/agent-loading.ts +++ b/packages/server/src/server/agent/agent-loading.ts @@ -30,6 +30,29 @@ export interface EnsureAgentLoadedDeps { logger: Logger; } +export async function ensureUnarchivedAgentLoaded( + agentId: string, + deps: EnsureAgentLoadedDeps & { + agentManager: AgentLoaderManager & Pick<AgentManager, "closeAgent">; + }, +): Promise<ManagedAgent> { + const record = await deps.agentStorage.get(agentId); + if (record?.archivedAt) { + throw new Error(`Agent is archived: ${agentId}`); + } + + const agent = await ensureAgentLoaded(agentId, deps); + const latestRecord = await deps.agentStorage.get(agentId); + if (latestRecord?.archivedAt) { + await deps.agentManager.closeAgent(agentId).catch((error: unknown) => { + deps.logger.warn({ err: error, agentId }, "Failed to close concurrently archived agent"); + }); + throw new Error(`Agent is archived: ${agentId}`); + } + + return agent; +} + export async function ensureAgentLoaded( agentId: string, deps: EnsureAgentLoadedDeps, diff --git a/packages/server/src/server/agent/agent-manager.test.ts b/packages/server/src/server/agent/agent-manager.test.ts index 12dea4e47..161a2b416 100644 --- a/packages/server/src/server/agent/agent-manager.test.ts +++ b/packages/server/src/server/agent/agent-manager.test.ts @@ -17,7 +17,7 @@ import { AgentStorage } from "./agent-storage.js"; import { toAgentPayload } from "./agent-projections.js"; import { PARENT_AGENT_ID_LABEL } from "@getpaseo/protocol/agent-labels"; import { formatSystemNotificationPrompt } from "./agent-prompt.js"; -import { ensureAgentLoaded } from "./agent-loading.js"; +import { ensureAgentLoaded, ensureUnarchivedAgentLoaded } from "./agent-loading.js"; import type { StoredAgentRecord } from "./agent-storage.js"; import type { AgentClient, @@ -7300,6 +7300,137 @@ test("archiving an idle-collected parent still cascades to its managed children" } }); +test("ensureUnarchivedAgentLoaded does not resume an archived agent", async () => { + const workdir = mkdtempSync(join(tmpdir(), "agent-manager-archived-load-")); + const storage = new AgentStorage(join(workdir, "agents"), logger); + const manager = new AgentManager({ + clients: { codex: new TestAgentClient() }, + registry: storage, + logger, + }); + + try { + const agent = await manager.createAgent({ provider: "codex", cwd: workdir }, undefined, { + workspaceId: undefined, + }); + await manager.collectIdleAgents({ + cutoff: new Date(Date.now() + 1_000), + protectedAgentIds: new Set(), + }); + await manager.archiveSnapshot(agent.id, new Date().toISOString()); + + await expect( + ensureUnarchivedAgentLoaded(agent.id, { + agentManager: manager, + agentStorage: storage, + logger, + }), + ).rejects.toThrow(`Agent is archived: ${agent.id}`); + expect(manager.getAgent(agent.id)).toBeNull(); + } finally { + await storage.flush().catch(() => undefined); + rmSync(workdir, { recursive: true, force: true }); + } +}); + +test("ensureUnarchivedAgentLoaded closes a runtime archived while it resumes", async () => { + const workdir = mkdtempSync(join(tmpdir(), "agent-manager-archived-resume-race-")); + const storage = new AgentStorage(join(workdir, "agents"), logger); + const resumeStarted = deferred<void>(); + const resumeAllowed = deferred<void>(); + const client = new (class extends TestAgentClient { + override async resumeSession( + handle: AgentPersistenceHandle, + config?: Partial<AgentSessionConfig>, + launchContext?: AgentLaunchContext, + ): Promise<AgentSession> { + resumeStarted.resolve(); + await resumeAllowed.promise; + return super.resumeSession(handle, config, launchContext); + } + })(); + const manager = new AgentManager({ clients: { codex: client }, registry: storage, logger }); + + try { + const agent = await manager.createAgent({ provider: "codex", cwd: workdir }, undefined, { + workspaceId: undefined, + }); + await manager.collectIdleAgents({ + cutoff: new Date(Date.now() + 1_000), + protectedAgentIds: new Set(), + }); + + const load = ensureUnarchivedAgentLoaded(agent.id, { + agentManager: manager, + agentStorage: storage, + logger, + }); + await resumeStarted.promise; + await manager.archiveSnapshot(agent.id, new Date().toISOString()); + resumeAllowed.resolve(); + + await expect(load).rejects.toThrow(`Agent is archived: ${agent.id}`); + expect(manager.getAgent(agent.id)).toBeNull(); + expect((await storage.get(agent.id))?.archivedAt).toEqual(expect.any(String)); + } finally { + resumeAllowed.resolve(); + await storage.flush().catch(() => undefined); + rmSync(workdir, { recursive: true, force: true }); + } +}); + +test("ensureUnarchivedAgentLoaded fences an archived agent after joining a shared resume", async () => { + const workdir = mkdtempSync(join(tmpdir(), "agent-manager-archived-shared-resume-")); + const storage = new AgentStorage(join(workdir, "agents"), logger); + const resumeStarted = deferred<void>(); + const resumeAllowed = deferred<void>(); + const client = new (class extends TestAgentClient { + override async resumeSession( + handle: AgentPersistenceHandle, + config?: Partial<AgentSessionConfig>, + launchContext?: AgentLaunchContext, + ): Promise<AgentSession> { + resumeStarted.resolve(); + await resumeAllowed.promise; + return super.resumeSession(handle, config, launchContext); + } + })(); + const manager = new AgentManager({ clients: { codex: client }, registry: storage, logger }); + + try { + const agent = await manager.createAgent({ provider: "codex", cwd: workdir }, undefined, { + workspaceId: undefined, + }); + await manager.collectIdleAgents({ + cutoff: new Date(Date.now() + 1_000), + protectedAgentIds: new Set(), + }); + + const sharedLoad = ensureAgentLoaded(agent.id, { + agentManager: manager, + agentStorage: storage, + logger, + }); + await resumeStarted.promise; + const protectedLoad = ensureUnarchivedAgentLoaded(agent.id, { + agentManager: manager, + agentStorage: storage, + logger, + }); + await manager.archiveSnapshot(agent.id, new Date().toISOString()); + resumeAllowed.resolve(); + + await sharedLoad; + await expect(protectedLoad).rejects.toThrow(`Agent is archived: ${agent.id}`); + expect(manager.getAgent(agent.id)).toBeNull(); + expect((await storage.get(agent.id))?.archivedAt).toEqual(expect.any(String)); + } finally { + resumeAllowed.resolve(); + await storage.flush().catch(() => undefined); + rmSync(workdir, { recursive: true, force: true }); + } +}); + test("collectIdleAgents leaves recent, protected, internal, running, and error agents resident", async () => { const workdir = mkdtempSync(join(tmpdir(), "agent-manager-idle-eligibility-")); const client = new (class extends TestAgentClient { diff --git a/packages/server/src/server/session.test.ts b/packages/server/src/server/session.test.ts index 4db2bc7a6..183f02633 100644 --- a/packages/server/src/server/session.test.ts +++ b/packages/server/src/server/session.test.ts @@ -370,6 +370,7 @@ function createSessionForTest(options: SessionForTestOptions = {}): Session { ...options.agentManager, }), agentStorage: asAgentStorage({ + get: vi.fn().mockResolvedValue(undefined), list: vi.fn().mockResolvedValue([]), ...options.agentStorage, }), @@ -4899,12 +4900,22 @@ test("sends project updates only to capable sockets in a retained session", () = }); describe("agent config setters", () => { + function liveAgentManager(overrides: { [K in keyof SessionOptions["agentManager"]]?: unknown }): { + [K in keyof SessionOptions["agentManager"]]?: unknown; + } { + return { + waitForAgentClose: vi.fn().mockResolvedValue(undefined), + touchAgentActivity: vi.fn(() => ({ id: "agent-1" })), + ...overrides, + }; + } + test("set_agent_mode_request: success emits accepted response carrying the notice", async () => { const messages: SessionOutboundMessage[] = []; const notice = { type: "info", message: "Switched to plan mode" } as const; const session = createSessionForTest({ messages, - agentManager: { setAgentMode: vi.fn().mockResolvedValue(notice) }, + agentManager: liveAgentManager({ setAgentMode: vi.fn().mockResolvedValue(notice) }), }); await session.handleMessage({ @@ -4932,7 +4943,9 @@ describe("agent config setters", () => { const messages: SessionOutboundMessage[] = []; const session = createSessionForTest({ messages, - agentManager: { setAgentMode: vi.fn().mockRejectedValue(new Error("mode boom")) }, + agentManager: liveAgentManager({ + setAgentMode: vi.fn().mockRejectedValue(new Error("mode boom")), + }), }); await session.handleMessage({ @@ -4967,7 +4980,7 @@ describe("agent config setters", () => { const messages: SessionOutboundMessage[] = []; const session = createSessionForTest({ messages, - agentManager: { setAgentModel: vi.fn().mockResolvedValue(undefined) }, + agentManager: liveAgentManager({ setAgentModel: vi.fn().mockResolvedValue(undefined) }), }); await session.handleMessage({ @@ -4989,7 +5002,9 @@ describe("agent config setters", () => { const messages: SessionOutboundMessage[] = []; const session = createSessionForTest({ messages, - agentManager: { setAgentModel: vi.fn().mockRejectedValue(new Error("model boom")) }, + agentManager: liveAgentManager({ + setAgentModel: vi.fn().mockRejectedValue(new Error("model boom")), + }), }); await session.handleMessage({ @@ -5024,7 +5039,7 @@ describe("agent config setters", () => { const messages: SessionOutboundMessage[] = []; const session = createSessionForTest({ messages, - agentManager: { setAgentFeature: vi.fn().mockResolvedValue(undefined) }, + agentManager: liveAgentManager({ setAgentFeature: vi.fn().mockResolvedValue(undefined) }), }); await session.handleMessage({ @@ -5047,7 +5062,9 @@ describe("agent config setters", () => { const messages: SessionOutboundMessage[] = []; const session = createSessionForTest({ messages, - agentManager: { setAgentFeature: vi.fn().mockRejectedValue(new Error("feature boom")) }, + agentManager: liveAgentManager({ + setAgentFeature: vi.fn().mockRejectedValue(new Error("feature boom")), + }), }); await session.handleMessage({ @@ -5084,7 +5101,9 @@ describe("agent config setters", () => { const notice = { type: "warning", message: "Thinking budget reduced" } as const; const session = createSessionForTest({ messages, - agentManager: { setAgentThinkingOption: vi.fn().mockResolvedValue(notice) }, + agentManager: liveAgentManager({ + setAgentThinkingOption: vi.fn().mockResolvedValue(notice), + }), }); await session.handleMessage({ @@ -5112,9 +5131,9 @@ describe("agent config setters", () => { const messages: SessionOutboundMessage[] = []; const session = createSessionForTest({ messages, - agentManager: { + agentManager: liveAgentManager({ setAgentThinkingOption: vi.fn().mockRejectedValue(new Error("thinking boom")), - }, + }), }); await session.handleMessage({ diff --git a/packages/server/src/server/session.ts b/packages/server/src/server/session.ts index 17510db81..b974559cb 100644 --- a/packages/server/src/server/session.ts +++ b/packages/server/src/server/session.ts @@ -36,7 +36,7 @@ import { isStoredAgentProviderAvailable, toAgentPersistenceHandle, } from "./persistence-hooks.js"; -import { ensureAgentLoaded } from "./agent/agent-loading.js"; +import { ensureAgentLoaded, ensureUnarchivedAgentLoaded } from "./agent/agent-loading.js"; import { formatSystemNotificationPrompt, sendPromptToAgent, @@ -816,6 +816,13 @@ export class Session { emit: (msg) => this.emit(msg), }, operations: { + ensureLoaded: async (agentId) => { + await ensureUnarchivedAgentLoaded(agentId, { + agentManager, + agentStorage, + logger: this.sessionLogger, + }); + }, setMode: async (agentId, modeId) => (await setAgentModeCommand({ agentManager }, { agentId, modeId })).notice, setModel: (agentId, modelId) => agentManager.setAgentModel(agentId, modelId), @@ -5864,7 +5871,7 @@ export class Session { msg: Extract<SessionInboundMessage, { type: "agent.provider_subagents.list.request" }>, ): Promise<void> { try { - await ensureAgentLoaded(msg.parentAgentId, { + await ensureUnarchivedAgentLoaded(msg.parentAgentId, { agentManager: this.agentManager, agentStorage: this.agentStorage, logger: this.sessionLogger, @@ -5896,7 +5903,7 @@ export class Session { ): Promise<void> { const direction: AgentTimelineFetchDirection = msg.direction ?? (msg.cursor ? "after" : "tail"); try { - await ensureAgentLoaded(msg.parentAgentId, { + await ensureUnarchivedAgentLoaded(msg.parentAgentId, { agentManager: this.agentManager, agentStorage: this.agentStorage, logger: this.sessionLogger, diff --git a/packages/server/src/server/session/agent-config/agent-config-session.test.ts b/packages/server/src/server/session/agent-config/agent-config-session.test.ts index d9ae7470c..d0e14519a 100644 --- a/packages/server/src/server/session/agent-config/agent-config-session.test.ts +++ b/packages/server/src/server/session/agent-config/agent-config-session.test.ts @@ -9,14 +9,21 @@ import type { AgentProviderNotice } from "../../agent/agent-sdk-types.js"; import type { SessionOutboundMessage } from "../../messages.js"; class FakeAgentConfigOperations implements AgentConfigOperations { + readonly loadedAgentIds: string[] = []; readonly modeCalls: Array<{ agentId: string; modeId: string }> = []; readonly modelCalls: Array<{ agentId: string; modelId: string | null }> = []; readonly featureCalls: Array<{ agentId: string; featureId: string; value: unknown }> = []; readonly thinkingCalls: Array<{ agentId: string; thinkingOptionId: string | null }> = []; modeNotice: AgentProviderNotice | null = null; thinkingNotice: AgentProviderNotice | null = null; + loadFailure: Error | null = null; failWith: Error | null = null; + async ensureLoaded(agentId: string): Promise<void> { + this.loadedAgentIds.push(agentId); + if (this.loadFailure) throw this.loadFailure; + } + async setMode(agentId: string, modeId: string): Promise<AgentProviderNotice | null> { this.modeCalls.push({ agentId, modeId }); if (this.failWith) throw this.failWith; @@ -68,6 +75,7 @@ describe("AgentConfigSession", () => { }); expect(operations.modeCalls).toEqual([{ agentId: "agent-1", modeId: "plan" }]); + expect(operations.loadedAgentIds).toEqual(["agent-1"]); expect(emitted).toEqual([ { type: "set_agent_mode_response", @@ -109,6 +117,43 @@ describe("AgentConfigSession", () => { }); }); + test("set mode: a failed load rejects without mutating the collected agent", async () => { + const { subsystem, emitted, operations } = makeSubsystem(); + operations.loadFailure = new Error("agent is archived"); + + await subsystem.handleSetAgentModeRequest({ + type: "set_agent_mode_request", + agentId: "agent-1", + modeId: "plan", + requestId: "req-1", + }); + + expect(operations.loadedAgentIds).toEqual(["agent-1"]); + expect(operations.modeCalls).toEqual([]); + expect(emitted.map((message) => message.type)).toEqual([ + "activity_log", + "set_agent_mode_response", + ]); + expect(emitted[0]).toEqual({ + type: "activity_log", + payload: { + id: expect.any(String), + timestamp: expect.any(Date), + type: "error", + content: "Failed to set agent mode: agent is archived", + }, + }); + expect(emitted[1]).toEqual({ + type: "set_agent_mode_response", + payload: { + requestId: "req-1", + agentId: "agent-1", + accepted: false, + error: "agent is archived", + }, + }); + }); + test("set model: emits an accepted response with no notice", async () => { const { subsystem, emitted, operations } = makeSubsystem(); diff --git a/packages/server/src/server/session/agent-config/agent-config-session.ts b/packages/server/src/server/session/agent-config/agent-config-session.ts index ef8630c57..1cd39856a 100644 --- a/packages/server/src/server/session/agent-config/agent-config-session.ts +++ b/packages/server/src/server/session/agent-config/agent-config-session.ts @@ -19,11 +19,12 @@ export interface AgentConfigSessionHost { /** * The per-agent config mutations this subsystem drives. The shell adapts these - * onto the live AgentManager (mode still routes through setAgentModeCommand); - * tests wire an in-memory fake. Mode and thinking yield a provider notice; model - * and feature do not. + * onto the AgentManager and loads a collected agent before mutation (mode still + * routes through setAgentModeCommand); tests wire an in-memory fake. Mode and + * thinking yield a provider notice; model and feature do not. */ export interface AgentConfigOperations { + ensureLoaded(agentId: string): Promise<void>; setMode(agentId: string, modeId: string): Promise<AgentProviderNotice | null>; setModel(agentId: string, modelId: string | null): Promise<void>; setFeature(agentId: string, featureId: string, value: unknown): Promise<void>; @@ -138,6 +139,7 @@ export class AgentConfigSession { this.logger.info(logFields, `session: ${logLabel}`); try { + await this.operations.ensureLoaded(agentId); const notice = await run(); this.logger.info(logFields, `session: ${logLabel} success`); emitResponse({ requestId, agentId, accepted: true, error: null, notice }); From 0cfb9b6b94429e08d1832fdcaf794904ccf327bc Mon Sep 17 00:00:00 2001 From: Mohamed Boudra <boudra.moha@gmail.com> Date: Sat, 18 Jul 2026 22:26:31 +0200 Subject: [PATCH 37/38] Keep sidebar pins visible while reopening (#2210) * fix(app): keep sidebar pins visible while reopening Hidden sidebars stop consuming live workspace updates but retain their last rendered entries so the opening animation never exposes an empty list. * test(app): reuse sidebar pin flow --- packages/app/e2e/helpers/sidebar.ts | 7 ++++++ packages/app/e2e/sidebar-workspace.spec.ts | 25 +++++++++++++++++++ .../hooks/use-sidebar-workspace-entries.ts | 5 +++- 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/packages/app/e2e/helpers/sidebar.ts b/packages/app/e2e/helpers/sidebar.ts index c6d0fb3ce..dc06f26cc 100644 --- a/packages/app/e2e/helpers/sidebar.ts +++ b/packages/app/e2e/helpers/sidebar.ts @@ -39,6 +39,13 @@ export async function clickArchiveWorkspaceMenuItem( await archiveItem.click(); } +export async function pinWorkspaceFromSidebar(page: Page, workspaceId: string): Promise<void> { + const serverId = await openWorkspaceSidebarKebab(page, workspaceId); + const pinItem = page.getByTestId(`sidebar-workspace-menu-pin-${serverId}:${workspaceId}`); + await expect(pinItem).toBeVisible({ timeout: 10_000 }); + await pinItem.click(); +} + export async function archiveWorkspaceFromSidebar(page: Page, workspaceId: string): Promise<void> { // A clean workspace archives with no prompt. Managed worktree backing may raise // a browser confirm for unsynced work, so accept it when present. diff --git a/packages/app/e2e/sidebar-workspace.spec.ts b/packages/app/e2e/sidebar-workspace.spec.ts index be68e73a7..51fee85cb 100644 --- a/packages/app/e2e/sidebar-workspace.spec.ts +++ b/packages/app/e2e/sidebar-workspace.spec.ts @@ -6,6 +6,7 @@ import { expectMobileAgentSidebarHidden, expectMobileAgentSidebarVisible, openMobileAgentSidebar, + pinWorkspaceFromSidebar, } from "./helpers/sidebar"; import { seedWorkspace } from "./helpers/seed-client"; import { expectWorkspaceHeader } from "./helpers/workspace-ui"; @@ -168,6 +169,30 @@ test.describe("Mobile sidebar panelState transition", () => { await closeMobileAgentSidebar(page); await expectMobileAgentSidebarHidden(page); }); + + test("keeps a pinned workspace rendered while the retained sidebar is closed", async ({ + page, + }) => { + const workspace = await seedWorkspace({ repoPrefix: "sidebar-retained-pin-" }); + + try { + await gotoAppShell(page); + await openMobileAgentSidebar(page); + await expectMobileAgentSidebarVisible(page); + + const row = page.getByTestId(getWorkspaceRowTestId(workspace.workspaceId)); + await expect(row).toBeVisible({ timeout: 30_000 }); + await pinWorkspaceFromSidebar(page, workspace.workspaceId); + await expect(page.getByTestId("sidebar-pinned-section")).toBeVisible(); + + await closeMobileAgentSidebar(page); + await expectMobileAgentSidebarHidden(page); + + await expect(row).toHaveCount(1); + } finally { + await workspace.cleanup(); + } + }); }); test.describe("Half-screen desktop layout", () => { diff --git a/packages/app/src/hooks/use-sidebar-workspace-entries.ts b/packages/app/src/hooks/use-sidebar-workspace-entries.ts index 5f0ccc407..c2eb65ac7 100644 --- a/packages/app/src/hooks/use-sidebar-workspace-entries.ts +++ b/packages/app/src/hooks/use-sidebar-workspace-entries.ts @@ -38,7 +38,10 @@ export function useSidebarWorkspaceEntries( // subscription to structurally shared indexes, never one session-store // subscription per mounted row. return useMemo(() => { - if (!enabled || placements.length === 0 || sessions.length === 0) { + if (!enabled) { + return previousEntriesRef.current; + } + if (placements.length === 0 || sessions.length === 0) { previousEntriesRef.current = EMPTY_ENTRIES; return EMPTY_ENTRIES; } From c9bcfa7638a8d3fb4b29784938d3c0f04e84f79b Mon Sep 17 00:00:00 2001 From: Mohamed Boudra <boudra.moha@gmail.com> Date: Sat, 18 Jul 2026 23:03:39 +0200 Subject: [PATCH 38/38] Use safer automatic approval modes by default (#2213) * feat(providers): default to safer automatic approvals * fix(providers): preserve defaults on older Codex versions * test(providers): cover Claude automatic approval default * fix(providers): preserve Claude defaults on cloud transports * fix(providers): resolve defaults from launch capabilities --- .../resolve-agent-form.test.ts | 19 ++++++ .../provider-selection/resolve-agent-form.ts | 7 ++- packages/protocol/src/provider-manifest.ts | 4 +- .../src/server/agent/agent-manager.test.ts | 59 +++++++++++++++---- .../server/src/server/agent/agent-manager.ts | 32 +++++++--- .../src/server/agent/agent-sdk-types.ts | 7 +++ .../server/agent/provider-registry.test.ts | 27 ++++++++- .../src/server/agent/provider-registry.ts | 19 +++++- .../agent/provider-snapshot-manager.test.ts | 32 +++++++++- .../server/agent/provider-snapshot-manager.ts | 2 + .../providers/claude/agent.redesign.test.ts | 31 ++++++++++ .../server/agent/providers/claude/agent.ts | 24 +++++++- .../agent/providers/codex-app-server-agent.ts | 17 +++++- 13 files changed, 253 insertions(+), 27 deletions(-) diff --git a/packages/app/src/provider-selection/resolve-agent-form.test.ts b/packages/app/src/provider-selection/resolve-agent-form.test.ts index 992c3a8f0..a10de7b12 100644 --- a/packages/app/src/provider-selection/resolve-agent-form.test.ts +++ b/packages/app/src/provider-selection/resolve-agent-form.test.ts @@ -575,6 +575,25 @@ describe("resolveFormState", () => { expect(resolved.modeId).toBe("workspace-write"); }); + it("falls back when the provider cannot advertise its preferred default mode", () => { + const providerMap = makeProviderMap({ + ...TEST_CODEX_DEFINITION, + defaultModeId: "auto-review", + modes: TEST_CODEX_DEFINITION.modes, + }); + + const resolved = resolveFormState( + undefined, + { provider: "codex" }, + CODEX_MODELS, + INITIAL_USER_MODIFIED, + makeState({ provider: "codex" }).form, + providerMap, + ); + + expect(resolved.modeId).toBe("auto"); + }); + it("ignores disabled ready providers when resolving selectable defaults", () => { const entries: ProviderSnapshotEntry[] = [ { diff --git a/packages/app/src/provider-selection/resolve-agent-form.ts b/packages/app/src/provider-selection/resolve-agent-form.ts index 196b16bc2..56e84f5b1 100644 --- a/packages/app/src/provider-selection/resolve-agent-form.ts +++ b/packages/app/src/provider-selection/resolve-agent-form.ts @@ -171,7 +171,12 @@ function resolvePreferredModeId(input: { const preferredModeId = normalizeSelectedModeId(input.preferredModeId); if (preferredModeId) return preferredModeId; - return input.providerDef?.defaultModeId ?? input.providerDef?.modes[0]?.id ?? ""; + const defaultModeId = input.providerDef?.defaultModeId; + const modes = input.providerDef?.modes ?? []; + if (defaultModeId && (modes.length === 0 || modes.some((mode) => mode.id === defaultModeId))) { + return defaultModeId; + } + return modes[0]?.id ?? ""; } export function mergeSelectedComposerPreferences(args: { diff --git a/packages/protocol/src/provider-manifest.ts b/packages/protocol/src/provider-manifest.ts index 37c79e59a..b3e388faa 100644 --- a/packages/protocol/src/provider-manifest.ts +++ b/packages/protocol/src/provider-manifest.ts @@ -185,7 +185,7 @@ export const AGENT_PROVIDER_DEFINITIONS: AgentProviderDefinition[] = [ id: "claude", label: "Claude", description: "Anthropic's multi-tool assistant with MCP support, streaming, and deep reasoning", - defaultModeId: "default", + defaultModeId: "auto", modes: CLAUDE_MODES, voice: { enabled: true, @@ -197,7 +197,7 @@ export const AGENT_PROVIDER_DEFINITIONS: AgentProviderDefinition[] = [ id: "codex", label: "Codex", description: "OpenAI's Codex workspace agent with sandbox controls and optional network access", - defaultModeId: "auto", + defaultModeId: "auto-review", modes: CODEX_MODES, voice: { enabled: true, diff --git a/packages/server/src/server/agent/agent-manager.test.ts b/packages/server/src/server/agent/agent-manager.test.ts index 161a2b416..e5f20da37 100644 --- a/packages/server/src/server/agent/agent-manager.test.ts +++ b/packages/server/src/server/agent/agent-manager.test.ts @@ -35,6 +35,7 @@ import type { AgentStreamEvent, AgentTimelineItem, ImportProviderSessionInput, + ResolveAgentDefaultModeInput, } from "./agent-sdk-types.js"; import type { PaseoToolCatalog } from "./tools/types.js"; import type { ProviderDefinition } from "./provider-registry.js"; @@ -111,11 +112,15 @@ function expectArchivedAgentRecord( } class TestAgentClient implements AgentClient { - readonly provider = "codex" as const; + readonly provider: AgentProvider; readonly capabilities = TEST_CAPABILITIES; readonly createdConfigs: AgentSessionConfig[] = []; readonly resumeOverrides: Array<Partial<AgentSessionConfig> | undefined> = []; + constructor(provider: AgentProvider = "codex") { + this.provider = provider; + } + async isAvailable(): Promise<boolean> { return true; } @@ -129,18 +134,18 @@ class TestAgentClient implements AgentClient { return { models: [ { - provider: "codex", + provider: this.provider, id: "gpt-5.4", label: "GPT-5.4", isDefault: true, }, { - provider: "codex", + provider: this.provider, id: "gpt-5.4-mini", label: "GPT-5.4 Mini", }, { - provider: "codex", + provider: this.provider, id: "gpt-5.2-codex", label: "GPT-5.2 Codex", }, @@ -156,7 +161,7 @@ class TestAgentClient implements AgentClient { ): Promise<AgentSession> { this.resumeOverrides.push(config); return new TestAgentSession({ - provider: "codex", + provider: this.provider, cwd: config?.cwd ?? process.cwd(), daemonAppendSystemPrompt: config?.daemonAppendSystemPrompt, }); @@ -935,9 +940,43 @@ test("normalizeConfig injects the provider default model when omitted", async () ); expect(snapshot.config.model).toBe("gpt-5.4"); + expect(snapshot.config.modeId).toBe("auto-review"); +}); + +test("normalizeConfig injects Claude's automatic approval default when omitted", async () => { + const workdir = mkdtempSync(join(tmpdir(), "agent-manager-claude-default-test-")); + const manager = new AgentManager({ + clients: { claude: new TestAgentClient("claude") }, + logger, + }); + + const snapshot = await manager.createAgent({ provider: "claude", cwd: workdir }, undefined, { + workspaceId: undefined, + }); + expect(snapshot.config.modeId).toBe("auto"); }); +test("normalizeConfig uses a capability-aware provider mode default", async () => { + const workdir = mkdtempSync(join(tmpdir(), "agent-manager-mode-default-test-")); + class CapabilityAwareClient extends TestAgentClient { + override async resolveDefaultModeId(input: ResolveAgentDefaultModeInput): Promise<string> { + return input.env?.CLAUDE_CODE_USE_BEDROCK === "1" ? "default" : "auto"; + } + } + const manager = new AgentManager({ + clients: { codex: new CapabilityAwareClient() }, + logger, + }); + + const snapshot = await manager.createAgent({ provider: "codex", cwd: workdir }, undefined, { + workspaceId: undefined, + env: { CLAUDE_CODE_USE_BEDROCK: "1" }, + }); + + expect(snapshot.config.modeId).toBe("default"); +}); + test("createAgent forwards request env into the spawned provider process", async () => { const workdir = mkdtempSync(join(tmpdir(), "agent-manager-env-test-")); const client = new EnvProbeAgentClient(); @@ -997,7 +1036,7 @@ test("normalizeConfig strips legacy 'default' model id", async () => { ); expect(snapshot.config.model).toBe("gpt-5.4"); - expect(snapshot.config.modeId).toBe("auto"); + expect(snapshot.config.modeId).toBe("auto-review"); }); test("listDraftCommands returns no commands without guessing a missing model", async () => { @@ -1094,7 +1133,7 @@ test("listDraftCommands uses explicit model config without default model fetchin provider: "codex", cwd: workdir, model: "gpt-5.4", - modeId: "auto", + modeId: "auto-review", }, ]); }); @@ -1194,7 +1233,7 @@ test("listDraftFeatures uses explicit model config without default model fetchin provider: "codex", cwd: workdir, model: "gpt-5.4", - modeId: "auto", + modeId: "auto-review", }, ]); }); @@ -1651,7 +1690,7 @@ test("createAgent passes daemon launch env through the provider launch context", provider: "codex", cwd: workdir, model: "gpt-5.4", - modeId: "auto", + modeId: "auto-review", }); expect(client.lastLaunchContext).toEqual({ agentId: snapshot.id, @@ -2448,7 +2487,7 @@ test("resumeAgentFromPersistence keeps metadata config, applies overrides, and p }); expect(client.lastResumeOverrides).toMatchObject({ model: "gpt-5.4", - modeId: "auto", + modeId: "auto-review", systemPrompt: "new prompt", mcpServers: { paseo: { diff --git a/packages/server/src/server/agent/agent-manager.ts b/packages/server/src/server/agent/agent-manager.ts index 6f0eb9cc0..0bcbc2ca8 100644 --- a/packages/server/src/server/agent/agent-manager.ts +++ b/packages/server/src/server/agent/agent-manager.ts @@ -117,6 +117,7 @@ interface PreparedSessionConfig { interface NormalizeConfigOptions { resolveDefaultModel?: boolean; + env?: Record<string, string>; } interface TimeoutOptions { @@ -1034,7 +1035,11 @@ export class AgentManager { this.assertAcceptingAgentRegistrations(); const resolvedAgentId = validateAgentId(agentId ?? this.idFactory(), "createAgent"); await this.deleteAgentState(resolvedAgentId); - const { storedConfig, launchConfig } = await this.prepareSessionConfig(config, resolvedAgentId); + const { storedConfig, launchConfig } = await this.prepareSessionConfig( + config, + resolvedAgentId, + options?.env, + ); this.requireEnabledProvider(storedConfig.provider); const client = await this.requireAvailableClient({ provider: storedConfig.provider, @@ -4075,17 +4080,27 @@ export class AgentManager { } if (!normalized.modeId) { - try { - normalized.modeId = - getAgentProviderDefinition(normalized.provider).defaultModeId ?? undefined; - } catch { - // Unknown provider - } + normalized.modeId = await this.resolveDefaultModeId(normalized, options.env); } return normalized; } + private async resolveDefaultModeId( + config: AgentSessionConfig, + env?: Record<string, string>, + ): Promise<string | undefined> { + const providerDefault = await this.clients + .get(config.provider) + ?.resolveDefaultModeId?.({ config, env }); + if (providerDefault) return providerDefault; + try { + return getAgentProviderDefinition(config.provider).defaultModeId ?? undefined; + } catch { + return undefined; + } + } + private async resolveDefaultModelId(config: AgentSessionConfig): Promise<string | undefined> { const client = this.clients.get(config.provider); if (!client) { @@ -4107,8 +4122,9 @@ export class AgentManager { private async prepareSessionConfig( config: AgentSessionConfig, agentId: string, + env?: Record<string, string>, ): Promise<PreparedSessionConfig> { - const storedConfig = await this.normalizeConfig(stripInternalPaseoMcpServer(config)); + const storedConfig = await this.normalizeConfig(stripInternalPaseoMcpServer(config), { env }); const launchConfig = this.applyDaemonAppendSystemPrompt( withRuntimePaseoMcpServer({ config: storedConfig, diff --git a/packages/server/src/server/agent/agent-sdk-types.ts b/packages/server/src/server/agent/agent-sdk-types.ts index 3813e4eec..c5b8e37b1 100644 --- a/packages/server/src/server/agent/agent-sdk-types.ts +++ b/packages/server/src/server/agent/agent-sdk-types.ts @@ -668,6 +668,12 @@ export type FetchCatalogOptions = export interface ProviderCatalog { models: AgentModelDefinition[]; modes: AgentMode[]; + defaultModeId?: string | null; +} + +export interface ResolveAgentDefaultModeInput { + config: AgentSessionConfig; + env?: Record<string, string>; } export interface AgentClient { @@ -690,6 +696,7 @@ export interface AgentClient { * The registry is responsible for merging configured model overrides. */ fetchCatalog(options: FetchCatalogOptions): Promise<ProviderCatalog>; + resolveDefaultModeId?(input: ResolveAgentDefaultModeInput): Promise<string | undefined>; resolveCreateConfig?(input: ResolveAgentCreateConfigInput): ResolveAgentCreateConfigResult; isCreateConfigUnattended?(input: AgentCreateConfigUnattendedInput): boolean; listCommands?(config: AgentSessionConfig): Promise<AgentSlashCommand[]>; diff --git a/packages/server/src/server/agent/provider-registry.test.ts b/packages/server/src/server/agent/provider-registry.test.ts index 177c55477..4596b6d91 100644 --- a/packages/server/src/server/agent/provider-registry.test.ts +++ b/packages/server/src/server/agent/provider-registry.test.ts @@ -814,7 +814,7 @@ test("enabled: false keeps provider metadata in registry", () => { id: "claude", label: "Claude", description: "Anthropic's multi-tool assistant with MCP support, streaming, and deep reasoning", - defaultModeId: "default", + defaultModeId: "auto", enabled: false, }); expect(registry.claude.modes).toEqual( @@ -1487,6 +1487,31 @@ describe("fetchCatalog", () => { expect(catalog.models.map((model) => model.id)).toEqual(["profile-model", "extra-model"]); }); + test("replacement models still resolve the provider's capability-aware default mode", async () => { + const resolveDefaultModeId = vi.fn(async () => "default"); + const injectedClient = { + provider: "codex", + capabilities: {}, + resolveDefaultModeId, + isAvailable: vi.fn(async () => true), + } satisfies Partial<AgentClient> as AgentClient; + const registry = buildProviderRegistry(logger, { + providerOverrides: { + codex: { models: [{ id: "profile-model", label: "Profile Model" }] }, + }, + }); + + const catalog = await registry.codex.fetchCatalog( + { scope: "workspace", cwd: "/tmp/catalog", force: false }, + injectedClient, + ); + + expect(catalog.defaultModeId).toBe("default"); + expect(resolveDefaultModeId).toHaveBeenCalledWith({ + config: { provider: "codex", cwd: "/tmp/catalog" }, + }); + }); + test("additionalModels can override replacement model fields", async () => { const registry = buildProviderRegistry(logger, { providerOverrides: { diff --git a/packages/server/src/server/agent/provider-registry.ts b/packages/server/src/server/agent/provider-registry.ts index 9d9022f5a..dbdb5c076 100644 --- a/packages/server/src/server/agent/provider-registry.ts +++ b/packages/server/src/server/agent/provider-registry.ts @@ -14,6 +14,7 @@ import type { ProviderCatalog, ResolveAgentCreateConfigInput, ResolveAgentCreateConfigResult, + ResolveAgentDefaultModeInput, } from "./agent-sdk-types.js"; import { isDefaultAgentCreateConfigUnattended, @@ -427,12 +428,20 @@ function wrapClientProvider( fetchCatalog: async (options) => { const catalog = await inner.fetchCatalog(options); return { + ...catalog, models: mergeModels(provider, profileModels, additionalModels, catalog.models, { profileModelsAreAdditive, }), modes: catalog.modes, }; }, + resolveDefaultModeId: inner.resolveDefaultModeId + ? async ({ config, env }: ResolveAgentDefaultModeInput) => + await inner.resolveDefaultModeId?.({ + config: { ...config, provider: inner.provider }, + env, + }) + : undefined, resolveCreateConfig: inner.resolveCreateConfig?.bind(inner), isCreateConfigUnattended: inner.isCreateConfigUnattended?.bind(inner), listFeatures: listFeatures @@ -516,17 +525,25 @@ function createRegistryEntry( // the single catalog API; otherwise use static/empty modes with no runtime. const models = mergeModelAdditions(provider, replacementModels, resolved.additionalModels); if (hasStaticModes) { + const defaultModeId = await catalogClient.resolveDefaultModeId?.({ + config: { + provider, + cwd: options.scope === "workspace" ? options.cwd : process.cwd(), + }, + }); return { models, modes: decorateModes(resolved.definition.modes), + defaultModeId, }; } const catalog = await catalogClient.fetchCatalog(options); - return { models, modes: decorateModes(catalog.modes) }; + return { ...catalog, models, modes: decorateModes(catalog.modes) }; } const catalog = await catalogClient.fetchCatalog(options); return { + ...catalog, models: mergeModels( provider, resolved.profileModels, diff --git a/packages/server/src/server/agent/provider-snapshot-manager.test.ts b/packages/server/src/server/agent/provider-snapshot-manager.test.ts index 4b6a3eac5..980abe088 100644 --- a/packages/server/src/server/agent/provider-snapshot-manager.test.ts +++ b/packages/server/src/server/agent/provider-snapshot-manager.test.ts @@ -117,9 +117,11 @@ describe("ProviderSnapshotManager public surface", () => { try { const snapshot = manager.getSnapshot("/tmp/project"); const claude = snapshot.find((entry) => entry.provider === "claude"); + const codex = snapshot.find((entry) => entry.provider === "codex"); expect(claude?.status).toBe("loading"); expect(claude?.label).toBe("Claude"); - expect(claude?.defaultModeId).toBe("default"); + expect(claude?.defaultModeId).toBe("auto"); + expect(codex?.defaultModeId).toBe("auto-review"); } finally { manager.destroy(); } @@ -220,6 +222,34 @@ describe("ProviderSnapshotManager public surface", () => { } }); + test("ready snapshots publish the catalog's capability-aware default mode", async () => { + const manager = new ProviderSnapshotManager({ + logger: createTestLogger(), + extraClients: { + codex: createExtraClient("codex", { + isAvailable: async () => true, + fetchCatalog: async () => ({ + models: [], + modes: [{ id: "default", label: "Default", description: "Ask before running tools" }], + defaultModeId: "default", + }), + }), + }, + }); + + try { + const entry = await manager.getProvider({ + cwd: "/tmp/project", + provider: "codex", + wait: true, + }); + + expect(entry).toMatchObject({ status: "ready", defaultModeId: "default" }); + } finally { + manager.destroy(); + } + }); + test("explicit refresh re-probes only the requested warm provider", async () => { const cwd = "/tmp/project"; const isAvailableCodex = vi.fn(async () => true); diff --git a/packages/server/src/server/agent/provider-snapshot-manager.ts b/packages/server/src/server/agent/provider-snapshot-manager.ts index f21d91923..771c4d24c 100644 --- a/packages/server/src/server/agent/provider-snapshot-manager.ts +++ b/packages/server/src/server/agent/provider-snapshot-manager.ts @@ -799,6 +799,8 @@ export class ProviderSnapshotManager { setEntry({ ...base, + defaultModeId: + catalog.defaultModeId === undefined ? definition.defaultModeId : catalog.defaultModeId, status: "ready", enabled: true, models: catalog.models, diff --git a/packages/server/src/server/agent/providers/claude/agent.redesign.test.ts b/packages/server/src/server/agent/providers/claude/agent.redesign.test.ts index f43bbacf7..1af10b8a5 100644 --- a/packages/server/src/server/agent/providers/claude/agent.redesign.test.ts +++ b/packages/server/src/server/agent/providers/claude/agent.redesign.test.ts @@ -218,6 +218,37 @@ test("rejects auto mode when Claude Code uses Bedrock", async () => { } }); +test.each([ + { env: {}, expected: "auto" }, + { env: { CLAUDE_CODE_USE_BEDROCK: "1" }, expected: "default" }, + { env: { CLAUDE_CODE_USE_VERTEX: "true" }, expected: "default" }, +])("defaults to $expected for transport environment $env", async ({ env, expected }) => { + const client = new ClaudeAgentClient({ + logger: createTestLogger(), + runtimeSettings: { env }, + }); + + await expect( + client.resolveDefaultModeId({ + config: { provider: "claude", cwd: process.cwd() }, + }), + ).resolves.toBe(expected); +}); + +test("launch environment participates in the Claude default mode", async () => { + const client = new ClaudeAgentClient({ + logger: createTestLogger(), + runtimeSettings: { env: {} }, + }); + + await expect( + client.resolveDefaultModeId({ + config: { provider: "claude", cwd: process.cwd() }, + env: { CLAUDE_CODE_USE_BEDROCK: "1" }, + }), + ).resolves.toBe("default"); +}); + test("allows launch env to disable inherited Bedrock transport for auto mode", async () => { const previousBedrock = process.env.CLAUDE_CODE_USE_BEDROCK; process.env.CLAUDE_CODE_USE_BEDROCK = "1"; diff --git a/packages/server/src/server/agent/providers/claude/agent.ts b/packages/server/src/server/agent/providers/claude/agent.ts index a0403f2e6..4a07fd4fe 100644 --- a/packages/server/src/server/agent/providers/claude/agent.ts +++ b/packages/server/src/server/agent/providers/claude/agent.ts @@ -91,6 +91,7 @@ import { type ListImportableSessionsOptions, type McpServerConfig, type ProviderCatalog, + type ResolveAgentDefaultModeInput, } from "../../agent-sdk-types.js"; import { importSessionFromPersistence } from "../../provider-session-import.js"; import { @@ -1472,7 +1473,28 @@ export class ClaudeAgentClient implements AgentClient { async fetchCatalog(_options: FetchCatalogOptions): Promise<ProviderCatalog> { // Claude exposes a global catalog here; cwd/force are intentionally irrelevant. const models = await getClaudeModelsWithSettings(this.logger, this.configDir); - return { models, modes: DEFAULT_MODES }; + const modes = detectIneligibleAutoModeTransport( + createProviderEnv({ baseEnv: process.env, runtimeSettings: this.runtimeSettings }), + ) + ? DEFAULT_MODES.filter((mode) => mode.id !== "auto") + : DEFAULT_MODES; + return { + models, + modes, + defaultModeId: modes.some((mode) => mode.id === "auto") ? "auto" : "default", + }; + } + + async resolveDefaultModeId({ + config, + env: launchEnv, + }: ResolveAgentDefaultModeInput): Promise<string> { + const env = createProviderEnv({ + baseEnv: process.env, + runtimeSettings: this.runtimeSettings, + overlays: [config.extra?.claude?.env, launchEnv], + }); + return detectIneligibleAutoModeTransport(env) ? "default" : "auto"; } async listFeatures(config: AgentSessionConfig): Promise<AgentFeature[]> { diff --git a/packages/server/src/server/agent/providers/codex-app-server-agent.ts b/packages/server/src/server/agent/providers/codex-app-server-agent.ts index f816e33b0..b1d12194c 100644 --- a/packages/server/src/server/agent/providers/codex-app-server-agent.ts +++ b/packages/server/src/server/agent/providers/codex-app-server-agent.ts @@ -6379,8 +6379,21 @@ export class CodexAppServerAgentClient implements AgentClient { } async fetchCatalog(_options: FetchCatalogOptions): Promise<ProviderCatalog> { - const models = await this.fetchModelsFromAppServer(); - return { models, modes: CODEX_MODES }; + const [models, autoReviewEnabled] = await Promise.all([ + this.fetchModelsFromAppServer(), + this.resolveAutoReviewEnabled(), + ]); + return { + models, + defaultModeId: autoReviewEnabled ? "auto-review" : DEFAULT_CODEX_MODE_ID, + modes: autoReviewEnabled + ? CODEX_MODES + : CODEX_MODES.filter((mode) => mode.id !== "auto-review"), + }; + } + + async resolveDefaultModeId(): Promise<string> { + return (await this.resolveAutoReviewEnabled()) ? "auto-review" : DEFAULT_CODEX_MODE_ID; } private async fetchModelsFromAppServer(): Promise<AgentModelDefinition[]> {