Skip to content

fix(uninstall): PLUGIN_DATA ancestor validation, remnant cleanup once preserved data is gone, honest AB7307 (follow-up to #452) - #519

Merged
ScriptedAlchemy merged 4 commits into
mainfrom
fix/101-plugin-data-followups
Sep 4, 2026
Merged

fix(uninstall): PLUGIN_DATA ancestor validation, remnant cleanup once preserved data is gone, honest AB7307 (follow-up to #452)#519
ScriptedAlchemy merged 4 commits into
mainfrom
fix/101-plugin-data-followups

Conversation

@ScriptedAlchemy

@ScriptedAlchemy ScriptedAlchemy commented Sep 4, 2026

Copy link
Copy Markdown
Owner

Summary

Follow-up to the review threads opened on #452 after its 96d31e0 push (PLUGIN_DATA as receipt-owned durable state), which were still open when #452 was squash-merged as e647336. Per AGENTS.md, threads on an already-merged PR are answered in a follow-up PR; this is it. The fix is applied to both the core uninstallBundle (packages/agent-bundle/src/install/uninstall.ts) and the emitted standalone installer (install/surface.ts), plus Doctor.

What changes

  • Symlinked PLUGIN_DATA ancestors are refused (feat(install): receipt-owned uninstall lifecycle, format/2 receipts, and doctor activation states (#101) #452 thread on uninstall.ts:457, P1). Before the recorded PLUGIN_DATA directory is read or purged, ~/.cursor/agent-bundle, agent-bundle/plugin-data, and the leaf itself must each be a real directory; a symlink on the way is AB7007 (core) / Refusing unsupported filesystem entry (emitted installer), so a recursive purge of the leaf can no longer follow a link outside the Cursor home. Previously only the leaf was lstat-checked.
  • A remnant whose preserved data is gone is consumed on a default rerun (thread on uninstall.ts:564, P2). The --keep-data remnant no-op now applies only while the preserved state/ or written PLUGIN_DATA (or an unowned entry the uninstall retained) is still there. Once removed or emptied by hand, the next run — with or without --purge-data — consumes the remnant: receipt, empty plugin root, and the host and plugin-data directories it recorded, leaving the home byte-identical. This also changes the pre-existing state/-removed-by-hand case (documented in docs/diagnostics.md), which previously stayed a not-installed no-op until an explicit purge.
  • Doctor's AB7307 names PLUGIN_DATA only when real (thread on doctor.ts:537, P2). The remnant diagnostic reports the PLUGIN_DATA directory as preserved state only when the recorded path is this home's agent-bundle/plugin-data/<plugin>, reached through real directories, existing, and non-empty — the same directory uninstall would treat as its own. A path removed by hand, emptied, or recorded for another home is not claimed.
  • The remaining feat(install): receipt-owned uninstall lifecycle, format/2 receipts, and doctor activation states (#101) #452 thread (public docs for PLUGIN_DATA retention/purge, cli.mdx) was already addressed in 9e69e8a before the merge (INSTALL.md, README, site en+zh, changeset).

Test plan

  • tests/uninstall.test.ts: symlinked agent-bundle and plugin-data ancestors refused with AB7007 and the outside tree untouched; a kept PLUGIN_DATA emptied by hand consumed on the next default run with its parents pruned; the state/-removed-by-hand remnant consumed by a default rerun (--plan first), home byte-identical.
  • tests/install-surface.test.ts: the same three flows through the emitted install.mjs --uninstall.
  • tests/doctor.test.ts: AB7307 names PLUGIN_DATA only while it exists and holds something, and never for a path recorded for another home.
  • pnpm typecheck, pnpm lint, full pnpm test:unit (3169 passing on the feat(install): receipt-owned uninstall lifecycle, format/2 receipts, and doctor activation states (#101) #452 head this was cut from), pnpm docs:site:build (diagnostics reference renders from docs/diagnostics.md).

Review status

  • Merge gate: all checks green. Review findings are addressed by code changes in this PR; no review requests are posted.
  • Threads on uninstall.ts ("Treat an emptied state directory as an exhausted remnant") and doctor.ts ("Avoid inventing state for an empty PLUGIN_DATA remnant"), both raised on cd9cc03: addressed in 571373f. An emptied state/ directory is now pruned with the exhausted remnant like an emptied PLUGIN_DATA (core uninstall and the emitted installer, with tests), and AB7307 only names state/ or PLUGIN_DATA when that path still holds data — otherwise it reports the remnant as exhausted and points at the default uninstall that consumes it. docs/diagnostics.md and the changeset describe the rule.
  • Full pnpm test:unit on 571373f: 3202 passing.

…efore touching PLUGIN_DATA, consume a remnant whose preserved data is gone on a default rerun, and have Doctor name PLUGIN_DATA only when it is this home's real non-empty directory
@changeset-bot

changeset-bot Bot commented Sep 4, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 571373f

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
agent-bundle Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 4, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-04T03:49:55.750076Z cd9cc03 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cd9cc03f18

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// nothing to remove, so the remnant receipt stays in place and the run is the documented no-op. An explicit
// `--purge-data --confirm-purge` never takes this path: it consumes the remnant (and its recorded host
// directories) even when state/ has since been removed by hand.
if (remnantOnly && policy !== 'purge' && (data.present || retained.length > 0) && files.length === 1 && files[0] === receiptPath) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Treat an emptied state directory as an exhausted remnant

When a user deletes every entry under a preserved state/ but leaves the directory itself, cursorLocalData still sets data.present solely because the directory exists, so this condition continues returning the not-installed no-op indefinitely. The updated diagnostics documentation and changeset promise that emptied preserved data is consumed, and the emitted installer mirrors the same mistake; treat an empty state/ as absent, as is already done for PLUGIN_DATA.

AGENTS.md reference: AGENTS.md:L78-L81

Useful? React with 👍 / 👎.

const remnantDiagnostic = async (subject: string, path: string, stateOnly: boolean, receipt: InstallReceipt | undefined): Promise<Diagnostic> => {
const entries = stateOnly ? [] : (await readdir(path)).filter((name) => name !== installReceiptFile);
const extras = entries.filter((name) => !isPreservedRuntimeRoot(name)).sort((left, right) => left.localeCompare(right));
const pluginData = await preservedPluginData(path, receipt);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid inventing state for an empty PLUGIN_DATA remnant

When a remnant root contains only its receipt and the recorded PLUGIN_DATA is empty, missing, symlinked, or belongs to another home, preservedPluginData() now returns undefined; preserved is therefore empty, but the existing fallback still renders it as state/. Doctor consequently emits AB7307 claiming the root holds preserved state/ even though no such directory exists; handle the empty preserved set without substituting nonexistent state.

AGENTS.md reference: AGENTS.md:L78-L81

Useful? React with 👍 / 👎.

@pkg-pr-new

pkg-pr-new Bot commented Sep 4, 2026

Copy link
Copy Markdown
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle@519
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle/create-agent-bundle@519
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle/@agent-bundle/runtime@519

commit: 571373f

…hausted remnant) and stop Doctor's AB7307 from inventing state/ for a remnant whose preserved data is gone
@ScriptedAlchemy
ScriptedAlchemy merged commit 8b94bab into main Sep 4, 2026
13 checks passed
@ScriptedAlchemy
ScriptedAlchemy deleted the fix/101-plugin-data-followups branch September 4, 2026 04:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant