"The PR workflow skill is unavailable in this checkout"
- Dominant language
- TypeScript
- Stars
- 193k
- Forks
- 42.4k
- PR merge metrics
- PR metrics pending
Description
## Type
Bug
## Summary
An active Agent Host session can retain skill paths under one synchronized-customization nonce while a later `session/activeClientSet` publication materializes a new nonce for the same plugin identity. `AgentPluginManager` immediately deletes the older nonce directory, after which invoking an advertised skill fails with `ENOENT`.
This is the underlying cache-lifetime problem also visible in #328464, but it is not specific to `/act-on-feedback`. The latest reproduction involved the built-in synchronized `create-pr` skill.
## Reproduction observed
1. An Agent Host session was using a synchronized plugin revision under nonce A.
2. The user requested that the agent create a pull request.
3. During that turn, the client published an updated customization set for the session's workspace-scoped bundle with nonce B.
4. The host materialized nonce B and immediately logged `Evicting stale nonce for plugin` for nonce A.
5. Less than one second later, the model invoked `skill("create-pr")` using the path that had been advertised from nonce A.
6. The skill tool failed with an error shaped like:
```text
Failed to read skill file: ENOENT: no such file or directory, open '/agentPlugins///skills/create-pr/SKILL.md'
```
The plugin subsequently moved through additional nonces and eventually recreated nonce A, explaining why the missing file can exist again when the cache is inspected after the incident.
## Actual behavior
`AgentPluginManager._syncPlugin` materializes the new revision and then calls `_cleanupStaleNoncesFor`, which deletes every revision except the most recently used one. Active sessions retain concrete paths into those nonce directories, but the cache has no explicit ownership or lease tying those paths to session lifetime.
The implementation relies on deletion failing when an old revision is still in use. That does not work as a lifetime mechanism on macOS/Unix: retaining or reading a pathname does not keep its directory from being deleted.
The current unit test explicitly expects the previous nonce directory to be deleted immediately after syncing a new nonce, so it does not cover an active session retaining the returned path.
## Expected behavior
- A customization path advertised to an active session remains readable for as long as that session can invoke it.
- Installing a new nonce for the same plugin identity does not invalidate paths retained by other active sessions or turns.
- Cache cleanup uses explicit ownership/lifetime information rather than filesystem deletion failure as a proxy for whether a revision is in use.
- If an advertised synchronized skill becomes unavailable anyway, the host refreshes or re-resolves it rather than returning an unrecoverable `ENOENT`.
## Related changes
- #320854 introduced per-nonce plugin directories and immediate stale-nonce eviction. The stated intent was to allow long-running sessions to keep referencing older revisions, but the lock-based retention mechanism does not provide that guarantee.
- #330378 introduced workspace-scoped synchronized bundle identities and was intended to stop cross-workspace nonce ping-pong. In this reproduction, its scoped active-client reconciler published the nonce replacement that exposed the unsafe eviction. The same scoped bundle still changed nonces repeatedly, so the expected stable-nonce invariant did not hold.
- #330413 and #330803 made adjacent cache fixes for missing entries and initialization serialization, but do not change the active-revision lifetime problem.
- #328464 reports the same missing-backing-file symptom for `/act-on-feedback`.
## Suggested fix
Track explicit leases/reference counts for materialized `(plugin URI, nonce)` revisions:
1. Acquire a lease when a synced revision is handed to a session/provider.
2. Release it when that session is disposed or switches revisions.
3. Evict only unreferenced revisions.
4. Add a regression test that retains nonce A's returned directory, syncs nonce B, verifies nonce A remains readable, then releases A and verifies it becomes eligible for cleanup.
As a short-term mitigation, avoid immediate stale-nonce deletion until cleanup can run at a lifecycle boundary that knows no active session retains the revision.
(Written by Copilot)
Contributor guide
Assessment
This issue has not been assessed yet.