BottleMetadata gains a backend field (default ""). Docker prepare writes
"docker"; smolmachines prepare writes "smolmachines". read_metadata
deserialises it with "" as the backward-compatible default.
resume now passes metadata.backend to _launch_bottle so a preserved
smolmachines bottle is resumed on the right backend without requiring
BOT_BOTTLE_BACKEND to be set manually.
_bottle_for_slug now reads metadata.backend and constructs a
SmolmachinesBottle for smolmachines slugs instead of always defaulting
to DockerBottle. No-metadata slugs still fall back to Docker.
Closes#137
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>
Replaces the cwd-hash identity with a random 5-char base36 suffix
per launch, so two simultaneous `start <agent>` invocations against
the same cwd no longer collide on container names. Each launch is
its own bottle.
State carries metadata: every prepare step writes
~/.claude-bottle/state/<identity>/metadata.json with the
(agent_name, cwd, copy_cwd, started_at) the bottle was launched
with. The new `cli.py resume <identity>` reads this metadata and
re-launches a bottle pinned to the same identity — picking up the
per-bottle Dockerfile (from a prior capability-block apply) and
the transcript snapshot under the same state dir.
- bottle_state.py: bottle_identity(agent_name) drops the cwd param
and gains a random suffix; BottleMetadata dataclass +
read/write/metadata_path helpers.
- BottleSpec gains an optional identity field — resume sets it to
pin the identity; start leaves it empty so prepare mints fresh.
- prepare.py: writes metadata at launch time; uses spec.identity if
provided (resume) else bottle_identity(agent_name) (fresh start).
- start.py: extracted _launch_bottle from cmd_start so resume can
share the launch core; prints `./cli.py resume <identity>` hint
at session end.
- cli/resume.py (new): reads metadata, reconstructs BottleSpec
with the recorded identity + cwd, delegates to _launch_bottle.
Errors clearly when no state exists for the given identity.
- cli/__init__.py: registers `resume` in COMMANDS + usage.
- dashboard.py: capability-block approval status line now appends
the `resume <identity>` hint so the operator can copy-paste the
rebuild command without leaving the TUI.
Closes the rebuild loop in PRD 0016: agent calls capability-block →
operator approves → bottle torn down with state preserved → status
line shows resume command → operator runs it → replacement bottle
boots with the new Dockerfile and prior transcript.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The single point that computed `slug = slugify(agent_name)` in
prepare.py is now `slug = bottle_identity(agent_name, cwd)`. With
--cwd the identity has a sha256(resolved-cwd)[:12] suffix, so the
same agent against different projects gets distinct container
names, network names, queue dir, audit log paths, and per-bottle
state (Dockerfile + transcript). Without --cwd the identity is
just slugify(agent_name), unchanged from before — no-cwd bottles
look the same as today.
The downstream `slug` field on DockerBottlePlan keeps its name —
every module already threads it under "slug" and the value flowing
through is now the bottle's full identity. A comment in prepare.py
flags the change.
Fixes the bug surfaced in PR #22 review: running the same agent
against project-A's cwd then project-B's would silently share
project-A's per-bottle Dockerfile + transcript snapshot, container
name (forcing serialized runs), and queue/audit history.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Phase 1 of PRD 0016. Lays the per-bottle state plumbing that
capability-block remediation will write into:
- claude_bottle/backend/docker/bottle_state.py: bottle_state_dir,
per_bottle_dockerfile (read), write_per_bottle_dockerfile,
per_bottle_image_tag (unique per slug), transcript_snapshot_dir.
Stores under ~/.claude-bottle/state/<slug>/.
- prepare.py: when a per-bottle Dockerfile exists, use
per_bottle_image_tag(slug) as the base image and pass the
per-bottle Dockerfile path through DockerBottlePlan.dockerfile_path.
--cwd still layers a derived image on top.
- launch.py: passes plan.dockerfile_path to build_image so the
per-bottle Dockerfile is what docker build reads.
- DockerBottlePlan gains dockerfile_path field; print() surfaces it
in the preflight summary so the operator can see at-a-glance that
this bottle is running on a rebuilt image.
Phase 2 will write to write_per_bottle_dockerfile (capability-block
approval); Phase 3 wires it into the dashboard.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>