feat(state): clean up per-bottle state on session end (except capability-block)
Previously every bottle launch left ~/.claude-bottle/state/<identity>/ behind forever — metadata.json on every run, plus per-bottle Dockerfile + transcript snapshot on capability-block rebuilds. The metadata accumulated debris across launches; the only state worth keeping was the capability-block rebuild bundle. Make cleanup the default; preserve only on capability-block. - bottle_state.py: .preserve marker helpers (mark_preserved, is_preserved, clear_preserve_marker, preserve_marker_path) + cleanup_state(identity) that rm -rf's the per-bottle dir. - capability_apply.apply_capability_change writes mark_preserved before teardown so cli.py's session-end cleanup keeps the dir. - prepare.py clears any leftover marker at launch (start or resume), so a marker from a prior capability-block doesn't keep state alive past a subsequent normal session-end. - cli/start.py runs the cleanup decision AFTER the launch context closes: if is_preserved → print resume hint; else cleanup_state. The resume hint moves out of the launch with-block (was previously printed unconditionally — would have misled the operator about whether state was actually kept). Future-proof: cli.py never persists state speculatively. If the agent wants to be resumable, it has to go through capability-block. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -114,6 +114,60 @@ class TestBottleIdentity(unittest.TestCase):
|
||||
self.assertTrue(identity.startswith("my-agent-"))
|
||||
|
||||
|
||||
class TestPreserveMarker(_FakeHomeMixin, unittest.TestCase):
|
||||
"""The .preserve marker is how capability_apply tells cli.py's
|
||||
session-end cleanup to keep the state dir instead of removing it."""
|
||||
|
||||
def setUp(self):
|
||||
self._setup_fake_home()
|
||||
|
||||
def tearDown(self):
|
||||
self._teardown_fake_home()
|
||||
|
||||
def test_default_is_unpreserved(self):
|
||||
self.assertFalse(bottle_state.is_preserved("dev-x"))
|
||||
|
||||
def test_mark_then_is_preserved(self):
|
||||
bottle_state.mark_preserved("dev-x")
|
||||
self.assertTrue(bottle_state.is_preserved("dev-x"))
|
||||
|
||||
def test_clear_removes_marker(self):
|
||||
bottle_state.mark_preserved("dev-x")
|
||||
bottle_state.clear_preserve_marker("dev-x")
|
||||
self.assertFalse(bottle_state.is_preserved("dev-x"))
|
||||
|
||||
def test_clear_is_idempotent(self):
|
||||
# No marker present — should not raise.
|
||||
bottle_state.clear_preserve_marker("never-existed")
|
||||
self.assertFalse(bottle_state.is_preserved("never-existed"))
|
||||
|
||||
def test_marker_path_under_state_dir(self):
|
||||
path = bottle_state.preserve_marker_path("dev-x")
|
||||
self.assertTrue(str(path).endswith("/.claude-bottle/state/dev-x/.preserve"))
|
||||
|
||||
|
||||
class TestCleanupState(_FakeHomeMixin, unittest.TestCase):
|
||||
"""cleanup_state removes the entire per-bottle state dir.
|
||||
Called by cli.py when a session ends without the preserve marker."""
|
||||
|
||||
def setUp(self):
|
||||
self._setup_fake_home()
|
||||
|
||||
def tearDown(self):
|
||||
self._teardown_fake_home()
|
||||
|
||||
def test_removes_state_dir_and_contents(self):
|
||||
bottle_state.write_per_bottle_dockerfile("dev-x", "FROM x\n")
|
||||
d = bottle_state.bottle_state_dir("dev-x")
|
||||
self.assertTrue(d.is_dir())
|
||||
bottle_state.cleanup_state("dev-x")
|
||||
self.assertFalse(d.exists())
|
||||
|
||||
def test_idempotent_when_dir_missing(self):
|
||||
# Never created — should not raise.
|
||||
bottle_state.cleanup_state("never-existed")
|
||||
|
||||
|
||||
class TestBottleMetadata(_FakeHomeMixin, unittest.TestCase):
|
||||
def setUp(self):
|
||||
self._setup_fake_home()
|
||||
|
||||
@@ -107,6 +107,14 @@ class TestApplyCapabilityChange(_FakeHomeMixin, unittest.TestCase):
|
||||
self._calls,
|
||||
)
|
||||
|
||||
def test_marks_preserved_before_teardown(self):
|
||||
# cli.py's session-end cleanup reads the marker after the
|
||||
# bottle is torn down. The marker must therefore be written
|
||||
# before teardown — otherwise the cleanup would see no
|
||||
# marker and rm the state dir we just populated.
|
||||
apply_capability_change("dev", "FROM new\n")
|
||||
self.assertTrue(bottle_state.is_preserved("dev"))
|
||||
|
||||
def test_first_change_falls_back_to_repo_dockerfile_for_before(self):
|
||||
# No per-bottle override yet — before-diff comes from the
|
||||
# repo's Dockerfile.
|
||||
|
||||
Reference in New Issue
Block a user