Add cached image quickstart #336

Open
didericis-codex wants to merge 9 commits from issue-330-cached-images into smolmachines-sidecar-vm
Collaborator

Closes #330

Summary

  • add a fresh/cached image startup policy to the start flow
  • add --cached-images for headless quickstart
  • reuse existing local Docker images and matching smolmachine artifacts without rebuilding
  • add a SQLite-backed cached_image_stale_warning_days setting, defaulting to 1 day, and warn when selected cached images/artifacts exceed it

Tests

  • python3 -m unittest discover -t . -s tests/unit -v
  • python3 -m unittest tests.unit.test_cli_start_headless tests.unit.test_cli_start_selector tests.unit.test_compose tests.unit.test_docker_util_image tests.unit.test_docker_launch_committed_image tests.unit.test_smolmachines_launch_image tests.unit.test_config_store -v
  • python3 -m compileall -q bot_bottle tests/unit/test_config_store.py tests/unit/test_cli_start_headless.py tests/unit/test_cli_start_selector.py tests/unit/test_compose.py tests/unit/test_docker_util_image.py tests/unit/test_docker_launch_committed_image.py tests/unit/test_smolmachines_launch_image.py
  • git diff --check
Closes #330 ## Summary - add a fresh/cached image startup policy to the start flow - add --cached-images for headless quickstart - reuse existing local Docker images and matching smolmachine artifacts without rebuilding - add a SQLite-backed cached_image_stale_warning_days setting, defaulting to 1 day, and warn when selected cached images/artifacts exceed it ## Tests - python3 -m unittest discover -t . -s tests/unit -v - python3 -m unittest tests.unit.test_cli_start_headless tests.unit.test_cli_start_selector tests.unit.test_compose tests.unit.test_docker_util_image tests.unit.test_docker_launch_committed_image tests.unit.test_smolmachines_launch_image tests.unit.test_config_store -v - python3 -m compileall -q bot_bottle tests/unit/test_config_store.py tests/unit/test_cli_start_headless.py tests/unit/test_cli_start_selector.py tests/unit/test_compose.py tests/unit/test_docker_util_image.py tests/unit/test_docker_launch_committed_image.py tests/unit/test_smolmachines_launch_image.py - git diff --check
didericis reviewed 2026-07-09 13:33:04 -04:00
@@ -0,0 +28,4 @@
CREATE TABLE IF NOT EXISTS bot_bottle_config (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
)
Owner

Will be easier to surface migration issues or problems with config changes if we make the specific config keys explicit. Think we should just put them in the schema here rather than having a generic key.

Will be easier to surface migration issues or problems with config changes if we make the specific config keys explicit. Think we should just put them in the schema here rather than having a generic `key`.
didericis-codex marked this conversation as resolved
didericis-codex reviewed 2026-07-09 13:36:35 -04:00
@@ -0,0 +28,4 @@
id INTEGER PRIMARY KEY CHECK (id = 1),
cached_image_stale_warning_days INTEGER NOT NULL DEFAULT 1
)
""",
Author
Collaborator

Done in 0c70de0e07. The config store schema now uses explicit settings columns with cached_image_stale_warning_days directly on the table, and the generic key/value accessors are gone. I also added a schema assertion test so this stays explicit.

Done in 0c70de0e07efdd7dca22bf85860c6b2d00198b52. The config store schema now uses explicit settings columns with `cached_image_stale_warning_days` directly on the table, and the generic key/value accessors are gone. I also added a schema assertion test so this stays explicit.
didericis force-pushed issue-330-cached-images from 0c70de0e07 to 83bd20f9b3 2026-07-09 14:00:16 -04:00 Compare
Owner

@didericis-claude there's a similar shape to all of the stale checks in the backends. Also need to make sure we're handling macos. Given the similar shape and fact that this should work on all backend, think we should add default steps similar to what we're doing in prepare in the base BottleBackend class for the launch method to enforce a similar procedure across backends.

@didericis-claude there's a similar shape to all of the stale checks in the backends. Also need to make sure we're handling macos. Given the similar shape and fact that this should work on all backend, think we should add default steps similar to what we're doing in `prepare` in the base `BottleBackend` class for the `launch` method to enforce a similar procedure across backends.
Collaborator

Good call. The pattern is: check image_policy, warn_if_stale* if cached, die if missing — docker and smolmachines both do this independently, and macos-container silently ignores image_policy entirely.

Plan: add an optional _image_policy_preflight(plan: PlanT) -> None hook to BottleBackend (no-op default), call it from a template launch wrapper before delegating to _launch_impl. Each backend overrides it with its own stale-check logic. macos-container gets the hook with a die if cached is requested (since it doesn't support cached images yet), which is more honest than silently ignoring the flag. Will update this PR.

Good call. The pattern is: check `image_policy`, `warn_if_stale*` if cached, die if missing — docker and smolmachines both do this independently, and macos-container silently ignores `image_policy` entirely. Plan: add an optional `_image_policy_preflight(plan: PlanT) -> None` hook to `BottleBackend` (no-op default), call it from a template `launch` wrapper before delegating to `_launch_impl`. Each backend overrides it with its own stale-check logic. macos-container gets the hook with a `die` if `cached` is requested (since it doesn't support cached images yet), which is more honest than silently ignoring the flag. Will update this PR.
didericis force-pushed issue-330-cached-images from 60231d9070 to 62365d527a 2026-07-10 15:28:50 -04:00 Compare
didericis requested changes 2026-07-10 16:02:53 -04:00
didericis left a comment
Owner

Didn't finish reading PR, but needs some refactoring/will reread once addressed.

Didn't finish reading PR, but needs some refactoring/will reread once addressed.
@@ -435,0 +441,4 @@
) -> Generator[Bottle, None, None]:
"""Template: optionally check for stale cached images, then delegate
to `_launch_impl`. Pass `skip_stale=True` to bypass the stale check
(used by the interactive CLI after the operator confirms)."""
Owner

Actually this was a bad idea, the check for a stale image should be in prelaunch, not here. remove the skip_stale option and move the logic where we check for a stale image (we also probably shouldn't throw an error: instead, in the plan, we should include a field that says the image is stale, and have the cli check the plan during the preflight confirmation for confirming whether to continue using the cached image or to rebuild it)

Actually this was a bad idea, the check for a stale image should be in prelaunch, not here. remove the `skip_stale` option and move the logic where we check for a stale image (we also probably shouldn't throw an error: instead, in the plan, we should include a field that says the image is stale, and have the cli check the plan during the preflight confirmation for confirming whether to continue using the cached image or to rebuild it)
@@ -435,0 +445,4 @@
if not skip_stale:
self._image_stale_checks(plan)
with self._launch_impl(plan) as bottle:
yield bottle
Owner

Implementation should I think have canonical logic for deciding whether to build the image based on the plan. Something like this (forget what build policy flag was called/is probably wrong, use the correct one)

image = None

if plan.use_cached_images:
    images = self._load_cached_images(plan)
else:
    images = self._build_images(plan)

with self._launch_impl(plan, images) as bottle:
    yield bottle
Implementation should I think have canonical logic for deciding whether to build the image based on the plan. Something like this (forget what build policy flag was called/is probably wrong, use the correct one) ```py image = None if plan.use_cached_images: images = self._load_cached_images(plan) else: images = self._build_images(plan) with self._launch_impl(plan, images) as bottle: yield bottle ```
@@ -154,3 +153,1 @@
_REPO_DIR,
dockerfile=SIDECAR_BUNDLE_DOCKERFILE,
)
cached = plan.spec.image_policy == "cached"
Owner

This is a smell: you shouldn't call _build_images when the policy is cached, you should skip the _build_images function (you're not actually building the image). Keep this in mind when refactoring for this: https://gitea.dideric.is/didericis/bot-bottle/pulls/336/files#issuecomment-3100

This is a smell: you shouldn't call `_build_images` when the policy is `cached`, you should skip the `_build_images` function (you're not actually building the image). Keep this in mind when refactoring for this: https://gitea.dideric.is/didericis/bot-bottle/pulls/336/files#issuecomment-3100
Collaborator

Addressing review 271 (comments 3098, 3099, 3100):

3098 (_build_images called on cached path): Splitting the function — _build_fresh_images / _load_cached_images are now separate paths. build_or_load_images chooses one; launch never calls a build helper when the policy is cached.

3099 (skip_stale in launch): Removing skip_stale from launch. Stale checks move to prelaunch_checks(plan) (public, replaces _image_stale_checks). CLI now calls backend.prelaunch_checks(plan) before entering the launch context; if StaleImageError is raised and the operator confirms, we fall through to launch without re-checking.

3100 (canonical build-or-load pattern): Adopting the pattern. One minor amendment — since each backend builds both an agent image and a sidecar image, image becomes images: BottleImages where BottleImages is a dataclass with agent and sidecar fields. Docker/macos-container use str refs; smolmachines uses Path artifacts. The base-class launch template becomes:

@contextmanager
def launch(self, plan: PlanT) -> Generator[Bottle, None, None]:
    images = self._build_or_load_images(plan)
    with self._launch_impl(plan, images) as bottle:
        yield bottle

Each backend implements _build_or_load_images(plan) -> BottleImages and _launch_impl(plan, images). Implementing now.

Addressing review 271 (comments 3098, 3099, 3100): **3098** (`_build_images` called on cached path): Splitting the function — `_build_fresh_images` / `_load_cached_images` are now separate paths. `build_or_load_images` chooses one; `launch` never calls a build helper when the policy is `cached`. **3099** (`skip_stale` in `launch`): Removing `skip_stale` from `launch`. Stale checks move to `prelaunch_checks(plan)` (public, replaces `_image_stale_checks`). CLI now calls `backend.prelaunch_checks(plan)` before entering the launch context; if `StaleImageError` is raised and the operator confirms, we fall through to `launch` without re-checking. **3100** (canonical build-or-load pattern): Adopting the pattern. One minor amendment — since each backend builds both an agent image and a sidecar image, `image` becomes `images: BottleImages` where `BottleImages` is a dataclass with `agent` and `sidecar` fields. Docker/macos-container use `str` refs; smolmachines uses `Path` artifacts. The base-class `launch` template becomes: ```python @contextmanager def launch(self, plan: PlanT) -> Generator[Bottle, None, None]: images = self._build_or_load_images(plan) with self._launch_impl(plan, images) as bottle: yield bottle ``` Each backend implements `_build_or_load_images(plan) -> BottleImages` and `_launch_impl(plan, images)`. Implementing now.
didericis added 9 commits 2026-07-10 16:58:05 -04:00
- image_cache: StaleImageError exception + check_stale/check_stale_path (raise instead of warn)
- BottleBackend.launch: template method (skip_stale flag) that calls _image_stale_checks then _launch_impl
- Each backend: _image_stale_checks delegates to a stale_checks() function in its launch module; _launch_impl replaces launch override
- macos_container: adds image_created_at to util, cached-image support in _build_images, stale_checks
- cli/start.py: catches StaleImageError, prompts interactively, retries with skip_stale=True; headless mode dies on it
Covers StaleImageError / check_stale / check_stale_path, the
BottleBackend.launch template method (skip_stale flag), stale_checks
functions across docker / smolmachines / macos-container backends,
_build_images cached paths in the macOS backend, image_created_at edge
cases in both docker and container util modules, the CLI stale loop
(headless die, interactive decline, interactive confirm + retry), and
ConfigStore.cached_image_stale_warning_days fallback paths.

Diff-coverage: 488/507 changed lines covered (96.3%).
Remove unused imports, add missing type annotations, fix Die()
constructor calls (int code, not str), replace fake backend
subclass with patch.object approach to avoid reportMissingParameterType
and override-incompatibility errors in strict pyright mode.
Addresses review comments 3098, 3099, 3100 on PR #336:

- Add BottleImages(agent, sidecar) dataclass to backend/__init__.py.
  Docker/macOS backends use str image refs; smolmachines uses Path
  artifacts. Replaces the singular `image` variable from the canonical
  pattern in comment 3100.

- Replace _image_stale_checks/skip_stale with public prelaunch_checks().
  CLI now calls backend.prelaunch_checks(plan) before backend.launch(plan);
  if StaleImageError is raised and the operator confirms, launch proceeds
  without re-checking. Removes the while-True/skip_stale retry loop.

- Add abstract _build_or_load_images(plan) -> BottleImages to
  BottleBackend. launch() calls it then passes images to _launch_impl.
  Each backend implements both methods.

- Fix comment 3098 (macos-container): _build_images is removed.
  build_or_load_images() has separate fresh/cached code paths — the
  cached path never calls a build helper.

- Update _start_bundle (smolmachines) to accept sidecar_artifact: Path
  directly. Sidecar artifact resolution moves to _sidecar_from_path(),
  called by build_or_load_images alongside _agent_from_path().
`from __future__ import annotations` already defers all annotation
evaluation, so quoting `str | Path`, `BottleImages` inside the same
module was redundant and tripped pyright strict mode.
fix(lint): remove unused BottleImages imports from two test files
lint / lint (push) Successful in 2m5s
test / unit (pull_request) Failing after 1m0s
test / integration (pull_request) Successful in 18s
test / coverage (pull_request) Failing after 1m2s
784c7ac152
pyright strict reportUnusedImport flagged BottleImages in
test_docker_launch_committed_image.py and test_macos_container_launch.py;
neither file references the type by name (they only use the returned
value's attributes).
didericis force-pushed issue-330-cached-images from 3327a42922 to 784c7ac152 2026-07-10 16:58:05 -04:00 Compare
Some checks are pending
lint / lint (push) Successful in 2m5s
test / unit (pull_request) Failing after 1m0s
test / integration (pull_request) Successful in 18s
test / coverage (pull_request) Failing after 1m2s
This pull request has changes conflicting with the target branch.
  • bot_bottle/backend/smolmachines/launch.py
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin issue-330-cached-images:issue-330-cached-images
git checkout issue-330-cached-images
Sign in to join this conversation.