fix: ignore stale desktop app markers

This commit is contained in:
codebanditssss 2026-07-04 20:44:19 +05:30
parent 5d7ad6137d
commit 057624cfef
4 changed files with 123 additions and 16 deletions

View File

@ -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

View File

@ -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

View File

@ -44,12 +44,29 @@ If you plan to use GitLab or Linear, install and authenticate their CLIs or cred
## Install AO
<Tabs items={["npm", "pnpm", "yarn", "from source"]}>
<Tab value="npm">```bash npm install -g @aoagents/ao ```</Tab>
<Tab value="pnpm">```bash pnpm add -g @aoagents/ao ```</Tab>
<Tab value="yarn">```bash yarn global add @aoagents/ao ```</Tab>
<Tab value="npm">
```bash
npm install -g @aoagents/ao
```
</Tab>
<Tab value="pnpm">
```bash
pnpm add -g @aoagents/ao
```
</Tab>
<Tab value="yarn">
```bash
yarn global add @aoagents/ao
```
</Tab>
<Tab value="from source">
```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
```
</Tab>
</Tabs>

View File

@ -94,6 +94,9 @@ Covers: install health, plugin resolution, notifier connectivity, stale temp fil
## Windows-specific
<Accordions>
<Accordion title="`ao start` opens a broken old dashboard with `file:///private/tmp/ao-release-0.9.5`">
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.
</Accordion>
<Accordion title="`ao open` prints a URL instead of opening a tab">
Expected on Windows. The iTerm2 helper only runs on macOS. Use the dashboard's built-in terminal (the printed URL takes you there).
</Accordion>