Skip to content

Packages: a failed package replacement overwrites the working installation before activation is verified #1545

Closed

Description

@unbraind

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 true

Suggested 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

  1. unbraind commented on Oct 22, 2026

    @unbraind
    OwnerAuthor

    Tracked by pm-c9wz5ef4ulnv under the existing capability lineage.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions