Repository navigation
Packages: a failed package replacement overwrites the working installation before activation is verified #1545
Description
Observed (pm 2026.10.22, plain directory, no git)
Replacing a working project-layer package with a version whose activation throws returns ok:false, but the replacement has already been written:
{"ok": false, "details": {"extension": {"version": "0.2.0"}, "overwritten": true,
"runtime_activation_status": "failed", "verification": {"status": "degraded", "activation_status": "failed"},
"activation_diagnostics": {"failed_count": 1, "failed": [{"name": "fixture-good", "error": "upgrade activation rejected: catalog schema missing"}]}}}After the failed install, pm package inventory fixture-good --json reports version: "0.2.0", configured_enabled: true, status: "installed". The command that worked before the attempt (pm fixture good ping) now exits 2 in every new process. The failure names diagnostic and reinstall commands, but there is no rollback, no "previous state preserved: false" field, and no command that restores the previous version. Only reinstalling the original source by hand restores the command.
Expected
Verify a replacement in staging (load and activate it) before swapping it in, or restore the previous installation and its managed metadata when verification fails. A refused replacement should leave the existing command working, and the result should say whether the previous state was preserved. If restoration cannot complete, it should name the exact recovery command.
Why it matters for agents
An agent upgrading a package gets ok:false and reasonably assumes nothing changed. In fact it has lost a working command, which it discovers only on the next call. Recovery took 5 calls and about 1,600 output tokens (failure, failed invocation, inventory, manual reinstall, verification). Following the suggested doctor detour adds about 1,900 tokens more. In a shared tracker, every other agent loses the command too.
Minimal repro
work=$(mktemp -d); export PM_GLOBAL_PATH="$work/global" GIT_CEILING_DIRECTORIES="$work"
mkdir "$work/project" && cd "$work/project"
pm init --prefix ux --defaults --agent-guidance skip
pm package init ../fixture-good && pm package install ../fixture-good && pm fixture good ping # works
cp -R ../fixture-good ../fixture-upgrade
node --input-type=module <<'JS'
import { readFileSync, writeFileSync } from 'node:fs';
for (const n of ['manifest.json', 'package.json']) { const p = `../fixture-upgrade/${n}`; const m = JSON.parse(readFileSync(p, 'utf8')); m.version = '0.2.0'; writeFileSync(p, JSON.stringify(m)); }
const p = '../fixture-upgrade/index.ts';
writeFileSync(p, readFileSync(p, 'utf8').replace(' api.registerCommand({', ' throw new Error("upgrade activation rejected");\n api.registerCommand({'));
JS
pm package install ../fixture-upgrade --json # ok:false, overwritten:true
pm fixture good ping; echo $? # 2: the working command is gone
pm package inventory fixture-good --json # version 0.2.0, configured_enabled trueSuggested fix
Install into a staging directory, activate it in a child process, and swap atomically only on success. Alternatively, keep the previous tree and its metadata and restore them on failure, reporting previous_state_preserved: true|false and a recovery_command.
Activity
unbraind commentedon Oct 22, 2026
Tracked by pm-c9wz5ef4ulnv under the existing capability lineage.