diff --git a/backend/internal/cli/start.go b/backend/internal/cli/start.go index b5a255125..80619d19c 100644 --- a/backend/internal/cli/start.go +++ b/backend/internal/cli/start.go @@ -9,6 +9,8 @@ import ( "os" "path/filepath" "runtime" + "strconv" + "strings" "github.com/spf13/cobra" @@ -116,12 +118,22 @@ func (c *commandContext) runStart(ctx context.Context, cmd *cobra.Command, opts // resolveApp returns the path to a usable desktop bundle, or "" when none is // found (spec ยง6.2). Resolution order is fixed: marker path -> stat -> known -// location scan. It never compares versions (invariant 5). +// location scan. Known-broken desktop versions are treated as stale so `ao +// start` can fetch a current app instead of relaunching a bad bundle forever. func (c *commandContext) resolveApp() string { - if p := c.markerAppPath(); p != "" && isUsableBundle(p) { - return p + marker, hasMarker := c.markerAppState() + var blockedPath string + if hasMarker { + if knownBrokenAppVersion(marker.Version) { + blockedPath = filepath.Clean(marker.AppPath) + } else if isUsableBundle(marker.AppPath) { + return marker.AppPath + } } for _, p := range appScanLocations() { + if blockedPath != "" && filepath.Clean(p) == blockedPath { + continue + } if isUsableBundle(p) { return p } @@ -133,22 +145,59 @@ func (c *commandContext) resolveApp() string { // tests can point the scan at a temp bundle instead of real system paths. var appScanLocations = knownAppLocations -// markerAppPath reads ~/.ao/app-state.json and returns its recorded appPath, or -// "" if the marker is missing/unreadable. It does not stat the path; callers do. -func (c *commandContext) markerAppPath() string { +// markerAppState reads ~/.ao/app-state.json and returns its recorded app state, +// or ok=false if the marker is missing/unreadable. +func (c *commandContext) markerAppState() (appState, bool) { dir, err := aoStateDir() if err != nil { - return "" + return appState{}, false } data, err := os.ReadFile(filepath.Join(dir, appStateFileName)) if err != nil { - return "" + return appState{}, false } var st appState if err := json.Unmarshal(data, &st); err != nil { - return "" + return appState{}, false } - return st.AppPath + if st.AppPath == "" { + return appState{}, false + } + return st, true +} + +// knownBrokenAppVersion reports desktop app versions that should not be reused +// by `ao start`. Pre-0.10.0 Next.js builds could bake the build machine's +// absolute `file://` path into the server bundle (for example +// /private/tmp/ao-release-0.9.5), which breaks Windows launches. Treating those +// markers as stale forces the bootstrapper to fetch a current release. +func knownBrokenAppVersion(version string) bool { + major, minor, _, ok := parseAppVersion(version) + if !ok { + return false + } + return major == 0 && minor < 10 +} + +func parseAppVersion(version string) (major, minor, patch int, ok bool) { + v := strings.TrimSpace(version) + v = strings.TrimPrefix(v, "v") + if i := strings.IndexAny(v, "-+"); i >= 0 { + v = v[:i] + } + parts := strings.Split(v, ".") + if len(parts) < 2 { + return 0, 0, 0, false + } + nums := [3]int{} + for i := 0; i < len(parts) && i < len(nums); i++ { + n, err := strconv.Atoi(parts[i]) + if err != nil { + return 0, 0, 0, false + } + nums[i] = n + } + return nums[0], nums[1], nums[2], true } // aoStateDir resolves the canonical ~/.ao home, honoring AO_DATA_DIR exactly as diff --git a/backend/internal/cli/start_test.go b/backend/internal/cli/start_test.go index d818db106..ce0aa6646 100644 --- a/backend/internal/cli/start_test.go +++ b/backend/internal/cli/start_test.go @@ -20,7 +20,12 @@ import ( // configured state dir (AO_RUN_FILE's directory). func writeMarker(t *testing.T, cfg testConfig, appPath string) { t.Helper() - st := appState{SchemaVersion: 1, AppPath: appPath, InstallSource: "npm-bootstrap"} + writeMarkerVersion(t, cfg, appPath, "") +} + +func writeMarkerVersion(t *testing.T, cfg testConfig, appPath, version string) { + t.Helper() + st := appState{SchemaVersion: 1, AppPath: appPath, Version: version, InstallSource: "npm-bootstrap"} data, err := json.Marshal(st) if err != nil { t.Fatal(err) @@ -80,6 +85,20 @@ func TestResolveApp_MarkerMissThenScanHit(t *testing.T) { } } +func TestResolveApp_IgnoresKnownBrokenMarkerVersion(t *testing.T) { + cfg := setConfigEnv(t) + brokenBundle := makeBundle(t, appBundleName) + replacementBundle := makeBundle(t, "replacement-"+appBundleName) + writeMarkerVersion(t, cfg, brokenBundle, "0.9.5") + t.Cleanup(swapScanLocations(func() []string { return []string{brokenBundle, replacementBundle} })) + + c := &commandContext{deps: Deps{}.withDefaults()} + got := c.resolveApp() + if got != replacementBundle { + t.Fatalf("resolveApp = %q, want replacement path %q", got, replacementBundle) + } +} + func TestResolveApp_ScanMissReturnsEmpty(t *testing.T) { setConfigEnv(t) // no marker written t.Cleanup(swapScanLocations(func() []string { @@ -93,6 +112,25 @@ func TestResolveApp_ScanMissReturnsEmpty(t *testing.T) { } } +func TestKnownBrokenAppVersion(t *testing.T) { + tests := map[string]bool{ + "": false, + "bogus": false, + "0.9.5": true, + "v0.9.5": true, + "0.9.5-nightly.20260519": true, + "0.10.0": false, + "0.10.2": false, + "0.10.3-nightly.202607041403": false, + "1.0.0": false, + } + for version, want := range tests { + if got := knownBrokenAppVersion(version); got != want { + t.Errorf("knownBrokenAppVersion(%q) = %v, want %v", version, got, want) + } + } +} + func TestAssetArchMapping(t *testing.T) { cases := map[string]struct { want string diff --git a/frontend/src/landing/content/docs/installation.mdx b/frontend/src/landing/content/docs/installation.mdx index 1fffc19c0..e82347c23 100644 --- a/frontend/src/landing/content/docs/installation.mdx +++ b/frontend/src/landing/content/docs/installation.mdx @@ -44,12 +44,29 @@ If you plan to use GitLab or Linear, install and authenticate their CLIs or cred ## Install AO - ```bash npm install -g @aoagents/ao ``` - ```bash pnpm add -g @aoagents/ao ``` - ```bash yarn global add @aoagents/ao ``` + + ```bash + npm install -g @aoagents/ao + ``` + + + ```bash + pnpm add -g @aoagents/ao + ``` + + + ```bash + yarn global add @aoagents/ao + ``` + - ```bash git clone https://github.com/ComposioHQ/agent-orchestrator cd agent-orchestrator pnpm install pnpm build - pnpm --filter @aoagents/ao link --global ``` + ```bash + git clone https://github.com/AgentWrapper/agent-orchestrator + cd agent-orchestrator + pnpm install + pnpm build + pnpm --filter @aoagents/ao link --global + ``` diff --git a/frontend/src/landing/content/docs/troubleshooting.mdx b/frontend/src/landing/content/docs/troubleshooting.mdx index e51cf5f1c..20a4f4509 100644 --- a/frontend/src/landing/content/docs/troubleshooting.mdx +++ b/frontend/src/landing/content/docs/troubleshooting.mdx @@ -94,6 +94,9 @@ Covers: install health, plugin resolution, notifier connectivity, stale temp fil ## Windows-specific + + That is a stale pre-0.10 desktop build. Upgrade `@aoagents/ao`, stop AO, then run `ao start` again so the launcher can fetch the current desktop app. If the stale app still opens, delete `%USERPROFILE%\.ao\app-state.json` and `%USERPROFILE%\.ao\staging`, then retry. + Expected on Windows. The iTerm2 helper only runs on macOS. Use the dashboard's built-in terminal (the printed URL takes you there).