Use ~/.ao as canonical state home (#233)

* Use ~/.ao as canonical state home

* chore: format with prettier [skip ci]

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Harshit Singh Bhandari 2026-06-14 22:52:25 +05:30 committed by GitHub
parent 17df922652
commit 724b9a2d81
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 37 additions and 51 deletions

View File

@ -87,8 +87,8 @@ func (c Config) Addr() string {
// AO_PORT bind port (default 3001)
// AO_REQUEST_TIMEOUT per-request timeout (Go duration > 0, default 60s)
// AO_SHUTDOWN_TIMEOUT shutdown deadline (Go duration > 0, default 10s)
// AO_RUN_FILE running.json path (default <state-dir>/running.json)
// AO_DATA_DIR durable state dir (default <state-dir>/data)
// AO_RUN_FILE running.json path (default ~/.ao/running.json)
// AO_DATA_DIR durable state dir (default ~/.ao/data)
// AO_AGENT agent adapter id (default claude-code)
// AO_ALLOWED_ORIGINS CORS origins, comma-separated (default DefaultAllowedOrigins)
//
@ -183,7 +183,7 @@ func parsePositiveDuration(name, raw string) (time.Duration, error) {
}
// resolveRunFilePath picks where running.json lives. An explicit AO_RUN_FILE
// wins; otherwise it sits under the per-user config directory so the CLI and
// wins; otherwise it sits under the canonical AO home directory so the CLI and
// Electron supervisor share one handshake location.
func resolveRunFilePath() (string, error) {
if p, ok := os.LookupEnv("AO_RUN_FILE"); ok && p != "" {
@ -197,7 +197,7 @@ func resolveRunFilePath() (string, error) {
}
// resolveDataDir picks where durable state (the SQLite DB) lives. An explicit
// AO_DATA_DIR wins; otherwise it defaults under the same per-user config
// AO_DATA_DIR wins; otherwise it defaults under the same canonical AO home
// directory as the run-file.
func resolveDataDir() (string, error) {
if p, ok := os.LookupEnv("AO_DATA_DIR"); ok && p != "" {
@ -211,9 +211,9 @@ func resolveDataDir() (string, error) {
}
func defaultStateDir() (string, error) {
configDir, err := os.UserConfigDir()
homeDir, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("resolve state dir: %w", err)
}
return filepath.Join(configDir, "agent-orchestrator"), nil
return filepath.Join(homeDir, ".ao"), nil
}

View File

@ -33,18 +33,18 @@ func TestLoadDefaults(t *testing.T) {
if cfg.RunFilePath == "" {
t.Error("RunFilePath is empty, want a resolved default path")
}
configDir, err := os.UserConfigDir()
homeDir, err := os.UserHomeDir()
if err != nil {
t.Fatalf("UserConfigDir: %v", err)
t.Fatalf("UserHomeDir: %v", err)
}
wantRunFilePath := filepath.Join(configDir, "agent-orchestrator", "running.json")
wantRunFilePath := filepath.Join(homeDir, ".ao", "running.json")
if cfg.RunFilePath != wantRunFilePath {
t.Errorf("RunFilePath = %q, want %q", cfg.RunFilePath, wantRunFilePath)
}
if cfg.DataDir == "" {
t.Error("DataDir is empty, want a resolved default path")
}
wantDataDir := filepath.Join(configDir, "agent-orchestrator", "data")
wantDataDir := filepath.Join(homeDir, ".ao", "data")
if cfg.DataDir != wantDataDir {
t.Errorf("DataDir = %q, want %q", cfg.DataDir, wantDataDir)
}

View File

@ -54,10 +54,10 @@ commands yet.
The CLI and daemon share the same environment-driven config:
| Var | Default | Purpose |
| --------------------- | ------------------------------------------------- | ---------------------- |
| --------------------- | -------------------- | ---------------------- |
| `AO_PORT` | `3001` | Loopback daemon port. |
| `AO_RUN_FILE` | `<UserConfigDir>/agent-orchestrator/running.json` | PID/port handshake. |
| `AO_DATA_DIR` | `<UserConfigDir>/agent-orchestrator/data` | SQLite data directory. |
| `AO_RUN_FILE` | `~/.ao/running.json` | PID/port handshake. |
| `AO_DATA_DIR` | `~/.ao/data` | SQLite data directory. |
| `AO_REQUEST_TIMEOUT` | `60s` | REST request timeout. |
| `AO_SHUTDOWN_TIMEOUT` | `10s` | Graceful shutdown cap. |

View File

@ -367,7 +367,7 @@ A few env vars affect every command:
| Variable | Purpose |
| ----------------------------------- | ------------------------------------------------------------------------------------ |
| `AO_DATA_DIR` | Override the `~/.agent-orchestrator` base directory. |
| `AO_DATA_DIR` | Override the `~/.ao` base directory. |
| `AO_LOG_LEVEL` | `error` · `warn` · `info` · `debug` · `trace` |
| `AO_CONFIG_PATH` | Absolute path to a specific `agent-orchestrator.yaml`. Overrides CWD-based search. |
| `AO_SESSION_ID` · `AO_SESSION_NAME` | Auto-set inside spawned sessions; used by `claim-pr` default |

View File

@ -76,9 +76,6 @@ export function SessionsBoard({ projectId }: SessionsBoardProps) {
const workspaceQuery = useWorkspaceQuery();
const all = workspaceQuery.data ?? [];
const workspaces = projectId ? all.filter((w) => w.id === projectId) : all;
// Project board leads with the project's name; the cross-project board
// (home) keeps the generic title.
const boardTitle = (projectId && workspaces[0]?.name) || "Board";
const sessions = workspaces.flatMap((w) => workerSessions(w.sessions));
const byZone = new Map<AttentionZone, WorkspaceSession[]>();

View File

@ -91,28 +91,25 @@ describe("parseRunFile", () => {
});
describe("defaultRunFilePath", () => {
it("mirrors Go os.UserConfigDir on macOS", () => {
expect(defaultRunFilePath("darwin", {}, "/Users/me")).toBe(
"/Users/me/Library/Application Support/agent-orchestrator/running.json",
);
it("matches Go's canonical AO home default on macOS", () => {
expect(defaultRunFilePath("darwin", {}, "/Users/me")).toBe("/Users/me/.ao/running.json");
});
it("prefers XDG_CONFIG_HOME on linux, falling back to ~/.config", () => {
expect(defaultRunFilePath("linux", { XDG_CONFIG_HOME: "/xdg" }, "/home/me")).toBe(
"/xdg/agent-orchestrator/running.json",
);
expect(defaultRunFilePath("linux", {}, "/home/me")).toBe("/home/me/.config/agent-orchestrator/running.json");
it("ignores XDG_CONFIG_HOME on linux", () => {
expect(defaultRunFilePath("linux", { XDG_CONFIG_HOME: "/xdg" }, "/home/me")).toBe("/home/me/.ao/running.json");
expect(defaultRunFilePath("linux", {}, "/home/me")).toBe("/home/me/.ao/running.json");
});
it("uses APPDATA on windows and returns null when it is unset", () => {
expect(defaultRunFilePath("win32", { APPDATA: "C:\\Users\\me\\AppData\\Roaming" }, "C:\\Users\\me")).toContain(
"agent-orchestrator",
it("ignores APPDATA on windows", () => {
expect(defaultRunFilePath("win32", { APPDATA: "C:\\Users\\me\\AppData\\Roaming" }, "C:\\Users\\me")).toBe(
"C:\\Users\\me/.ao/running.json",
);
expect(defaultRunFilePath("win32", {}, "C:\\Users\\me")).toBeNull();
expect(defaultRunFilePath("win32", {}, "C:\\Users\\me")).toBe("C:\\Users\\me/.ao/running.json");
});
it("returns null when no config root can be resolved", () => {
it("returns null when no home directory can be resolved", () => {
expect(defaultRunFilePath("linux", {}, "")).toBeNull();
expect(defaultRunFilePath("darwin", {}, "")).toBeNull();
expect(defaultRunFilePath("win32", {}, "")).toBeNull();
});
});

View File

@ -88,25 +88,17 @@ export function parseRunFile(contents: string): RunFileInfo | null {
}
/**
* Where the daemon writes running.json when AO_RUN_FILE is unset. Mirrors Go's
* os.UserConfigDir() + "agent-orchestrator/running.json" resolution in
* backend/internal/config so the supervisor reads the same file the daemon
* writes. Returns null when the platform's config root cannot be resolved.
* Where the daemon writes running.json when AO_RUN_FILE is unset. Matches
* backend/internal/config's canonical AO home default so the supervisor reads
* the same file the daemon writes. Returns null when the user home directory
* cannot be resolved.
*/
export function defaultRunFilePath(
platform: NodeJS.Platform,
env: Record<string, string | undefined>,
_env: Record<string, string | undefined>,
homeDir: string,
): string | null {
if (platform === "darwin") {
void platform;
if (!homeDir) return null;
return joinPath(homeDir, "Library", "Application Support", "agent-orchestrator", "running.json");
}
if (platform === "win32") {
if (!env.APPDATA) return null;
return joinPath(env.APPDATA, "agent-orchestrator", "running.json");
}
const configRoot = env.XDG_CONFIG_HOME || (homeDir ? joinPath(homeDir, ".config") : "");
if (!configRoot) return null;
return joinPath(configRoot, "agent-orchestrator", "running.json");
return joinPath(homeDir, ".ao", "running.json");
}