diff --git a/CHANGELOG.md b/CHANGELOG.md index ff1f5fed..cdd1fced 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.14.2] — 2026-08-22 + +### Fixed + +- **`amplifier-agent update` now actually picks up upstream module fixes.** Module + sources are declared with a floating `@main` ref, but foundation resolves a git + source by returning the existing clone directory whenever it is present and + structurally intact — it never fetches into it. A clone is therefore written exactly + once, at first install, and pinned to whatever commit `main` pointed at that day for + the life of the machine. An upstream fix to a module never reached an existing + install, and reinstalling did not help: the reinstall rebuilt from the same frozen + clone, restoring the same stale code *and* its stale dependency pins. The symptom was + a machine that reported the new engine version, passed `doctor`, and still ran the old + module. A one-time migration in the post-install hook now deletes cached + `amplifier-module-*` clones before priming, so the prepare that follows clones afresh. + The migration records a marker under `/migrations/` and runs at most once + per machine; it never raises, so it cannot fail an install. + + This is what makes the version bump load-bearing rather than cosmetic: the post-install + hook returns early when the prepared-bundle cache for the running version already + exists, so the migration and the bump have to ship together — the bump is what forces + the cold prepare that re-creates the clones the migration removes. + ## [0.14.1] — 2026-08-21 ### Fixed diff --git a/pyproject.toml b/pyproject.toml index 4d50b16d..b00ec475 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = 'amplifier-agent' -version = '0.14.1' +version = '0.14.2' requires-python = '>=3.12' license = 'MIT' dependencies = [ diff --git a/src/amplifier_agent_lib/post_install.py b/src/amplifier_agent_lib/post_install.py index 13a4a254..b0401e1a 100644 --- a/src/amplifier_agent_lib/post_install.py +++ b/src/amplifier_agent_lib/post_install.py @@ -13,11 +13,72 @@ from __future__ import annotations import asyncio +import shutil import sys +from pathlib import Path -from amplifier_agent_lib import __version__ +from amplifier_agent_lib import __version__, persistence from amplifier_agent_lib.bundle.cache import cache_dir_for_version, load_and_prepare_cached +#: One-time migrations, keyed by a stable id. A migration runs at most once +#: per machine; the marker file under ``/migrations/`` records it. +_CLONE_REFRESH_MIGRATION_ID = "0.14.2-refresh-stale-module-clones" + + +def _module_clone_root() -> Path: + """Return the directory holding foundation's git clones of modules. + + Mirrors foundation's own default (``~/.amplifier/cache``). This is *not* + :func:`persistence.cache_root`, which is amplifier-agent's own tree -- + module clones belong to foundation and are shared with other Amplifier + apps on the same machine. + """ + return Path.home() / ".amplifier" / "cache" + + +def _refresh_stale_module_clones() -> None: + """Delete cached module clones once, so the next prepare re-clones them. + + Module sources are declared with a floating ``@main`` ref, but foundation + resolves a git source by returning the existing clone directory whenever it + is present and structurally intact -- it never fetches into it. A clone is + therefore written exactly once, at first install, and pinned to whatever + commit ``main`` pointed at that day, for the life of the machine. + + The practical effect is that an upstream fix to a module never reaches an + existing install. Reinstalling does not help: the reinstall rebuilds from + the same frozen clone, restoring the same stale code *and* its stale + dependency pins. Deleting the clone is what breaks the cycle, because the + next prepare has nothing to reuse and clones afresh. + + Scope is deliberately limited to ``amplifier-module-*``. Bundle and + foundation clones are frozen by the same mechanism, but widening this + migration would re-clone roughly a third more repositories to fix a + problem nobody has reported. + + Never raises: a failure here must not fail an install. Callers run before + priming so a wiped clone is immediately re-created. + """ + marker = persistence.state_root() / "migrations" / f"{_CLONE_REFRESH_MIGRATION_ID}.done" + if marker.exists(): + return + + removed = 0 + clone_root = _module_clone_root() + if clone_root.is_dir(): + for child in sorted(clone_root.iterdir()): + if child.is_dir() and child.name.startswith("amplifier-module-"): + shutil.rmtree(child, ignore_errors=True) + removed += 1 + + marker.parent.mkdir(parents=True, exist_ok=True) + marker.write_text("done\n") + + if removed: + sys.stderr.write( + f"amplifier-agent: migration {_CLONE_REFRESH_MIGRATION_ID}: removed {removed} stale module clone(s)\n" + ) + async def main() -> int: """Prime the prepared-bundle cache for the current version. @@ -26,6 +87,13 @@ async def main() -> int: Always 0 — failures are logged to stderr and swallowed so the installer never fails due to this hook. """ + # Must precede the priming below: the migration only clears the clones, + # and it is the prepare that follows which re-creates them. + try: + _refresh_stale_module_clones() + except Exception as exc: # pragma: no cover - defensive + sys.stderr.write(f"amplifier-agent: module clone refresh skipped ({exc})\n") + cache_dir = cache_dir_for_version(__version__) manifest = cache_dir / "manifest.json" diff --git a/uv.lock b/uv.lock index 71f611af..df4a4737 100644 --- a/uv.lock +++ b/uv.lock @@ -16,7 +16,7 @@ members = [ [[package]] name = "amplifier-agent" -version = "0.14.1" +version = "0.14.2" source = { editable = "." } dependencies = [ { name = "amplifier-foundation" },