From d1432d3ba26cd2891a866c732b30a14836092db1 Mon Sep 17 00:00:00 2001 From: Junjie Wang Date: Thu, 16 Jul 2026 16:14:05 -0700 Subject: [PATCH 1/3] Resume from durable dir for interactions harness. --- cmd/ax/harness.go | 3 +++ cmd/ax/harness_test.go | 12 ++++++++---- internal/config/config.go | 3 +++ internal/config/config_test.go | 31 +++++++++++++++++++++++++++++++ manifests/ax-deployment.yaml | 10 ++++++++++ 5 files changed, 55 insertions(+), 4 deletions(-) diff --git a/cmd/ax/harness.go b/cmd/ax/harness.go index c028007b..1edf14db 100644 --- a/cmd/ax/harness.go +++ b/cmd/ax/harness.go @@ -70,6 +70,9 @@ func setHarnessWorkDir() error { if dir == "" { return nil } + if err := os.MkdirAll(dir, 0755); err != nil { + return fmt.Errorf("create harness working directory %q: %w", dir, err) + } if err := os.Chdir(dir); err != nil { return fmt.Errorf("set harness working directory %q: %w", dir, err) } diff --git a/cmd/ax/harness_test.go b/cmd/ax/harness_test.go index 599f5abb..cabe160f 100644 --- a/cmd/ax/harness_test.go +++ b/cmd/ax/harness_test.go @@ -65,10 +65,14 @@ func TestSetHarnessWorkDir(t *testing.T) { } }) - t.Run("missing directory returns an error", func(t *testing.T) { - t.Setenv("AX_HARNESS_WORKDIR", filepath.Join(t.TempDir(), "does-not-exist")) - if err := setHarnessWorkDir(); err == nil { - t.Error("expected an error for a missing directory, got nil") + t.Run("missing directory is created", func(t *testing.T) { + dir := filepath.Join(t.TempDir(), "created", "workdir") + t.Setenv("AX_HARNESS_WORKDIR", dir) + if err := setHarnessWorkDir(); err != nil { + t.Fatalf("setHarnessWorkDir: %v", err) + } + if _, err := os.Stat(dir); err != nil { + t.Errorf("working directory %q was not created: %v", dir, err) } }) } diff --git a/internal/config/config.go b/internal/config/config.go index 58ec5008..ec89b8b1 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -324,6 +324,9 @@ func (c *Config) Validate() error { } func AXAssetsDir() (string, error) { + if dir := os.Getenv("AX_DURABLE_DIR"); dir != "" { + return filepath.Join(dir, ".ax"), nil + } home, err := os.UserHomeDir() if err != nil { return "", fmt.Errorf("resolving home directory: %w", err) diff --git a/internal/config/config_test.go b/internal/config/config_test.go index baebb037..cdcda582 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -15,6 +15,8 @@ package config import ( + "os" + "path/filepath" "strings" "testing" @@ -345,3 +347,32 @@ func TestValidate_SkillsSelectionOneof(t *testing.T) { } }) } + +func TestAXAssetsDir(t *testing.T) { + t.Run("durable-dir env roots the .ax tree on the volume", func(t *testing.T) { + t.Setenv("AX_DURABLE_DIR", "/mnt/durable") + got, err := AXAssetsDir() + if err != nil { + t.Fatalf("AXAssetsDir: %v", err) + } + want := filepath.Join("/mnt/durable", ".ax") + if got != want { + t.Errorf("AXAssetsDir() = %q, want %q", got, want) + } + }) + t.Run("unset roots under the home directory", func(t *testing.T) { + t.Setenv("AX_DURABLE_DIR", "") + home, err := os.UserHomeDir() + if err != nil { + t.Skipf("no home directory: %v", err) + } + got, err := AXAssetsDir() + if err != nil { + t.Fatalf("AXAssetsDir: %v", err) + } + want := filepath.Join(home, ".ax") + if got != want { + t.Errorf("AXAssetsDir() = %q, want %q", got, want) + } + }) +} diff --git a/manifests/ax-deployment.yaml b/manifests/ax-deployment.yaml index fda42868..d1a76bbb 100644 --- a/manifests/ax-deployment.yaml +++ b/manifests/ax-deployment.yaml @@ -98,6 +98,8 @@ spec: value: "${GOOGLE_CLOUD_PROJECT}" - name: AX_HARNESS_WORKDIR value: "/workspace" + - name: AX_DURABLE_DIR + value: "/workspace" # Content of manifests/ax.yaml - name: AX_CONFIG_CONTENT value: "${AX_CONFIG_CONTENT}" @@ -105,8 +107,16 @@ spec: httpGet: path: /readyz port: 8081 + volumeMounts: + - name: workspace + mountPath: /workspace + volumes: + - name: workspace + durableDir: {} snapshotsConfig: location: gs://${AX_SNAPSHOTS_BUCKET}/axinteractions/ + onPause: Data + onCommit: Data --- # --------------------------------------------------------------------------- From 3c19e5aeafbd0e64150ea6cb58623cf051a081c3 Mon Sep 17 00:00:00 2001 From: Junjie Wang Date: Fri, 17 Jul 2026 11:06:08 -0700 Subject: [PATCH 2/3] Use the existing workdir for durable dir. --- internal/config/config.go | 2 +- internal/config/config_test.go | 6 +++--- .../antigravityinteractions.go | 12 ++++++------ manifests/ax-deployment.yaml | 2 -- 4 files changed, 10 insertions(+), 12 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index ec89b8b1..5ebbcd4b 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -324,7 +324,7 @@ func (c *Config) Validate() error { } func AXAssetsDir() (string, error) { - if dir := os.Getenv("AX_DURABLE_DIR"); dir != "" { + if dir := os.Getenv("AX_HARNESS_WORKDIR"); dir != "" { return filepath.Join(dir, ".ax"), nil } home, err := os.UserHomeDir() diff --git a/internal/config/config_test.go b/internal/config/config_test.go index cdcda582..5a5ae562 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -349,8 +349,8 @@ func TestValidate_SkillsSelectionOneof(t *testing.T) { } func TestAXAssetsDir(t *testing.T) { - t.Run("durable-dir env roots the .ax tree on the volume", func(t *testing.T) { - t.Setenv("AX_DURABLE_DIR", "/mnt/durable") + t.Run("workdir env roots the .ax tree alongside the workspace", func(t *testing.T) { + t.Setenv("AX_HARNESS_WORKDIR", "/mnt/durable") got, err := AXAssetsDir() if err != nil { t.Fatalf("AXAssetsDir: %v", err) @@ -361,7 +361,7 @@ func TestAXAssetsDir(t *testing.T) { } }) t.Run("unset roots under the home directory", func(t *testing.T) { - t.Setenv("AX_DURABLE_DIR", "") + t.Setenv("AX_HARNESS_WORKDIR", "") home, err := os.UserHomeDir() if err != nil { t.Skipf("no home directory: %v", err) diff --git a/internal/harness/antigravityinteractions/antigravityinteractions.go b/internal/harness/antigravityinteractions/antigravityinteractions.go index cb84faf8..7cb85ad8 100644 --- a/internal/harness/antigravityinteractions/antigravityinteractions.go +++ b/internal/harness/antigravityinteractions/antigravityinteractions.go @@ -159,12 +159,12 @@ func cloudLocation() string { return defaultLocation } -// DefaultStateDir returns the default resume-cursor directory, ~/.ax/antigravityinteractions/cursors, -// used when a caller does not set StateDir explicitly. It lives outside the -// agent's working directory on purpose: the working directory is the agent's -// operating surface (it reads and edits files there), so AX's internal state is -// kept separate to avoid the agent seeing or clobbering it. New still requires a -// non-empty StateDir; callers apply this default. +// DefaultStateDir returns the default resume-cursor directory, / +// antigravityinteractions/cursors, used when a caller does not set StateDir +// explicitly. Locally that is ~/.ax/...; on a substrate actor AXAssetsDir roots +// under the workspace, so the cursor lives in a hidden ".ax" subtree on the same +// durableDir as the agent's files. New still requires a non-empty StateDir; +// callers apply this default. func DefaultStateDir() (string, error) { axDir, err := config.AXAssetsDir() if err != nil { diff --git a/manifests/ax-deployment.yaml b/manifests/ax-deployment.yaml index d1a76bbb..5b5c791c 100644 --- a/manifests/ax-deployment.yaml +++ b/manifests/ax-deployment.yaml @@ -98,8 +98,6 @@ spec: value: "${GOOGLE_CLOUD_PROJECT}" - name: AX_HARNESS_WORKDIR value: "/workspace" - - name: AX_DURABLE_DIR - value: "/workspace" # Content of manifests/ax.yaml - name: AX_CONFIG_CONTENT value: "${AX_CONFIG_CONTENT}" From 21cfaaeaa1ceebd0586825306eede0db7bc0b16e Mon Sep 17 00:00:00 2001 From: Junjie Wang Date: Fri, 17 Jul 2026 17:58:00 -0700 Subject: [PATCH 3/3] Update mount path. --- internal/config/config.go | 2 +- internal/config/config_test.go | 6 +++--- .../antigravityinteractions.go | 12 ++++++------ manifests/ax-deployment.yaml | 10 ++++++---- 4 files changed, 16 insertions(+), 14 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index 5ebbcd4b..ec89b8b1 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -324,7 +324,7 @@ func (c *Config) Validate() error { } func AXAssetsDir() (string, error) { - if dir := os.Getenv("AX_HARNESS_WORKDIR"); dir != "" { + if dir := os.Getenv("AX_DURABLE_DIR"); dir != "" { return filepath.Join(dir, ".ax"), nil } home, err := os.UserHomeDir() diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 5a5ae562..cdcda582 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -349,8 +349,8 @@ func TestValidate_SkillsSelectionOneof(t *testing.T) { } func TestAXAssetsDir(t *testing.T) { - t.Run("workdir env roots the .ax tree alongside the workspace", func(t *testing.T) { - t.Setenv("AX_HARNESS_WORKDIR", "/mnt/durable") + t.Run("durable-dir env roots the .ax tree on the volume", func(t *testing.T) { + t.Setenv("AX_DURABLE_DIR", "/mnt/durable") got, err := AXAssetsDir() if err != nil { t.Fatalf("AXAssetsDir: %v", err) @@ -361,7 +361,7 @@ func TestAXAssetsDir(t *testing.T) { } }) t.Run("unset roots under the home directory", func(t *testing.T) { - t.Setenv("AX_HARNESS_WORKDIR", "") + t.Setenv("AX_DURABLE_DIR", "") home, err := os.UserHomeDir() if err != nil { t.Skipf("no home directory: %v", err) diff --git a/internal/harness/antigravityinteractions/antigravityinteractions.go b/internal/harness/antigravityinteractions/antigravityinteractions.go index 7cb85ad8..56c9f3fb 100644 --- a/internal/harness/antigravityinteractions/antigravityinteractions.go +++ b/internal/harness/antigravityinteractions/antigravityinteractions.go @@ -159,12 +159,12 @@ func cloudLocation() string { return defaultLocation } -// DefaultStateDir returns the default resume-cursor directory, / -// antigravityinteractions/cursors, used when a caller does not set StateDir -// explicitly. Locally that is ~/.ax/...; on a substrate actor AXAssetsDir roots -// under the workspace, so the cursor lives in a hidden ".ax" subtree on the same -// durableDir as the agent's files. New still requires a non-empty StateDir; -// callers apply this default. +// DefaultStateDir returns the default resume-cursor directory, +// /antigravityinteractions/cursors, used when a caller does not set +// StateDir explicitly. Locally that is ~/.ax/...; on a substrate actor +// AX_DURABLE_DIR points AXAssetsDir at a durable volume (e.g. /durable/.ax), kept +// outside the agent's working directory so the agent does not see or modify it. +// New still requires a non-empty StateDir; callers apply this default. func DefaultStateDir() (string, error) { axDir, err := config.AXAssetsDir() if err != nil { diff --git a/manifests/ax-deployment.yaml b/manifests/ax-deployment.yaml index 5b5c791c..072682ff 100644 --- a/manifests/ax-deployment.yaml +++ b/manifests/ax-deployment.yaml @@ -97,7 +97,9 @@ spec: - name: GOOGLE_CLOUD_PROJECT value: "${GOOGLE_CLOUD_PROJECT}" - name: AX_HARNESS_WORKDIR - value: "/workspace" + value: "/durable/workspace" + - name: AX_DURABLE_DIR + value: "/durable" # Content of manifests/ax.yaml - name: AX_CONFIG_CONTENT value: "${AX_CONFIG_CONTENT}" @@ -106,10 +108,10 @@ spec: path: /readyz port: 8081 volumeMounts: - - name: workspace - mountPath: /workspace + - name: durable + mountPath: /durable volumes: - - name: workspace + - name: durable durableDir: {} snapshotsConfig: location: gs://${AX_SNAPSHOTS_BUCKET}/axinteractions/