Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<state_root>/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
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = 'amplifier-agent'
version = '0.14.1'
version = '0.14.2'
requires-python = '>=3.12'
license = 'MIT'
dependencies = [
Expand Down
70 changes: 69 additions & 1 deletion src/amplifier_agent_lib/post_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 ``<state_root>/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.
Expand All @@ -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"

Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.