fix: refresh stale module clones so amplifier-agent update delivers upstream fixes (v0.14.2) - #141
fix: refresh stale module clones so amplifier-agent update delivers upstream fixes (v0.14.2)#141Salil Das (sadlilas) wants to merge 1 commit into
amplifier-agent update delivers upstream fixes (v0.14.2)#141Conversation
… upstream 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. The clone path is derived from sha256(git_url@ref), so a floating-ref clone owns one stable directory that is written exactly once, at first install, and stays pinned to whatever commit `main` pointed at that day for the life of the machine. Reinstalling does not help. `uv tool install --reinstall --force` empties the tool venv and every module is genuinely reinstalled, but each rebuilds from that same frozen clone, restoring the same stale code and its stale dependency pins. The fix is a one-time migration in the post-install hook that deletes cached `amplifier-module-*` clones before priming, so the cold prepare that follows has nothing to reuse and clones afresh. It records a marker under `<state_root>/migrations/` so it runs at most once per machine, and it is wrapped in try/except at the call site so it can never fail an install. It is ordered before priming deliberately: the migration only removes clones; the prepare that follows re-creates them. The version bump to 0.14.2 is load-bearing, not cosmetic: `post_install.main()` returns early when the prepared-bundle cache for the running version already exists, so shipping the migration without a bump would skip priming entirely — deleting the clones and never re-cloning them. The bump is what forces the cold prepare. That coupling is why the release bump and the fix travel in one commit. Verified in an isolated container from a faithful pre-fix state: running exactly what `amplifier-agent update` runs produced "removed 20 stale module clone(s)", moved the module clone to current main, updated its dependency pin, resolved the transitive dependency to the new floor, kept 29 modules installed, and left `doctor` all green. A second post-install run printed "cache already prepared" and re-wiped nothing. 🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier) Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
The bump is not cosmetic. `update` compares versions and exits early with "Already up to date" when they match, so without a new release tag the reinstall never runs and no user receives this fix. Same coupling PR #141 identified, for a different reason: #141 needed the bump because post-install would early-return on a warm cache; this needs it because `update` never gets that far.
|
Flagging an overlap rather than acting on it — this is your PR, so the call is yours. #142 supersedes the outcome this PR is after, by a different route. Instead of deleting stale clones from Two reasons it may be worth closing this rather than merging both:
Your diagnosis is what made #142 possible — the One thing from your "Follow-up, not in this PR" section is still open and unaddressed by #142: |
Completes the absorption of #141 so it can be closed: - uv.lock version bump, which #141 carried and this branch had missed - #141's stale-clone diagnosis folded into the 0.14.2 changelog entry - legacy clones in ~/.amplifier/cache surfaced by doctor and removable via cache clear --legacy #141 deleted those clones unconditionally. That was right while the directory was the one amplifier-agent used; after the relocation it is foundation's default for every Amplifier app, keyed with no per-app namespacing, so on a machine running app-cli they are its live clones and indistinguishable from our leftovers. Reported and offered rather than deleted.
|
Follow-up: #142 now absorbs this PR completely, so it can be closed without losing anything. Element by element:
The refresh behaviour is now permanent rather than one-shot. #142 relocates the clone root, so the new root starts empty on every existing machine and the first cold prepare clones from current The one thing #142 deliberately does not copy is the unconditional delete. Your migration was correct while Your underlying concern is kept. Verified in a clean container: advisory never fails Your Still unaddressed by either PR, from your follow-up section: |
|
Correction to my previous comment: the The reasoning I gave was wrong in one respect: I framed the clones in So an affordance to delete them is, for a substantial share of the people who would see it, a button that breaks a different application. The caveat I had put in the help text documented the damage rather than preventing it. Reporting alone is the same footgun with extra steps, because the only actionable follow-up is a manual Everything else in my previous comment stands: the version bump, the Net effect for a user upgrading: your intent — never leave someone silently running stale modules — is fully achieved by the relocation, since the new clone root starts empty and everything re-clones from current |
…f deleting clones (#142) amplifier-agent wrote its module clones into ~/.amplifier, a tree owned by amplifier-app-cli, and those clones were frozen at first install forever. Nothing in this repository caused the coupling: foundation resolves its storage root from AMPLIFIER_HOME and falls back to ~/.amplifier, and this app never set it. AMPLIFIER_HOME is now bound to <agent home>/foundation before amplifier_foundation is imported. Foundation keys each clone on sha256(git_url@ref) and reuses any directory that exists. With a floating @main the key never changes while the commit it names does. Rather than deleting directories to work around that, floating refs are now resolved to their current commit before prepare(), so the key identifies the content: an unmoved branch downloads nothing, a moved one clones fresh, and an unreachable remote falls back to the commit already on disk. Not pinning: bundle.md still declares @main and is never rewritten, so module fixes still reach users without an engine release. Also: relocated recipe session state, the context-intelligence reader root, provider rate-limit state and the ChatGPT OAuth token (with copy-forward) out of app-cli's tree; made update check module drift when the engine is already current; and made every storage root honour $AMPLIFIER_AGENT_HOME. Supersedes #141. Requires microsoft/amplifier-bundle-skills#61. Release: 0.15.0
Summary
amplifier-agent updatecurrently cannot deliver an upstream module fix to an existinginstall. This ships the missing piece as v0.14.2.
Module sources are declared with a floating
@mainref, but foundation resolves a gitsource by returning the existing clone directory whenever it is present and structurally
intact — it never fetches into it (
amplifier_foundation/sources/git.py,resolve()):No fetch, no ref comparison, no commit check. The clone path is
sha256(git_url@ref)[:16],so a floating-ref clone owns one stable directory that is written exactly once — at first
install — and pinned to whatever commit
mainpointed at that day, for the life of themachine.
Reinstalling does not help.
uv tool install --reinstall --forceempties the tool venv andevery module is genuinely reinstalled, but each one rebuilds from the same frozen clone,
restoring the same stale code and its stale dependency pins.
Why a version bump alone does not fix it
Worth stating explicitly, because it is the intuitive fix and it does not work. I measured a
bare version bump against a machine in the pre-fix state:
amplifier-agent --versionThe machine reports the new version, passes
doctor, reinstalls everything — and still runsthe old module. That is worse than an obvious failure, because every surface says healthy.
The fix
A one-time migration in the post-install hook deletes cached
amplifier-module-*clonesbefore priming, so the cold prepare that follows has nothing to reuse and clones afresh.
<state_root>/migrations/<id>.done; runs at most once per machine.try/exceptat the call site and never raises — it cannot fail an install.follows is what re-creates them.
The bump and the migration must ship together.
post_install.main()returns early whenthe prepared-bundle cache for the running version already exists. Ship the migration without
the bump and post-install skips priming entirely — deleting the clones and never re-cloning
them, which is strictly worse. The bump is what forces the cold prepare. That coupling is
the reason this is a release PR rather than a plain fix.
Verification
Measured in an isolated container, from a faithful pre-fix state (module clone checked out at
a pre-fix commit and editable-installed, matching dependency pin installed, warm
prepared-bundle cache generated in that state), then running exactly what
amplifier-agent updateruns —uv tool install --reinstall --forcefollowed byamplifier-agent-post-install:mainamplifier-module-*distributions in venvamplifier-agent doctorpost-installruncache already prepared; clone untouched; nothing re-wipedRepo gate (
make checkequivalent —makeis unavailable on this machine, so the threecommands were run directly):
No test file was added:
tests/in this repo holds e2e DTU suites and nothing else, by theexplicit convention recorded in
[tool.pytest.ini_options].Scope and limits
Deliberately narrow to
amplifier-module-*. Bundle and foundation clones are frozen bythe same mechanism. Widening the filter to
amplifier-would re-clone roughly a third morerepositories to fix a problem nobody has reported. Easy to widen later if it ever bites.
Only reaches users who run the post-install hook — that is
install.shandamplifier-agent update. A user who runsuv tool installdirectly bypasses it and staysstale.
The offline path is untested. If the network drops between the wipe and the re-prime, the
user is left with no module clones and a cache miss.
post_installswallows the exception andthe next run re-prepares and re-clones, so it self-heals — but that first run is slow and will
fail hard if still offline.
Follow-up, not in this PR
This is a workaround at the app layer. The underlying behaviour — a floating
@mainclonewritten once and never re-fetched — lives in foundation and affects every app that consumes
modules this way, so the same class of staleness will recur until it is addressed there.
A second foundation-level contributor:
ModuleActivator._install_dependencies()skipsreinstalling when a distribution of the same name is already present, checked before the
pyproject.tomlfingerprint comparison on the following line. The fingerprint check wouldotherwise have caught a changed dependency constraint. That guard is why deleting the clone,
rather than merely reinstalling, is the only reliable repair today.