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).