pi-collision-guard
Fail-closed local path leases for PI built-in edit/write across participating processes
Package details
Install pi-collision-guard from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:pi-collision-guard- Package
pi-collision-guard- Version
0.1.0- Published
- Aug 22, 2026
- Downloads
- 171/mo · 32/wk
- Author
- matuan_cat
- License
- MIT
- Types
- extension
- Size
- 128.6 KB
- Dependencies
- 0 dependencies · 1 peer
Pi manifest JSON
{
"extensions": [
"./dist/extensions/collision-guard.js"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
pi-collision-guard
Fail-closed local path leases that stop participating PI processes from running
built-in edit or write against the same canonical file path at the same
time.
This is a narrow coordination guard, not a promise to prevent every file conflict. It adds no model calls and makes no network requests.
Scope
The guard coordinates:
- PI processes on the same machine
- running as the same OS user
- that have this extension loaded
- before PI's built-in
editandwritetools execute
Paths are resolved from the PI session working directory. Existing targets and
their existing parent directories are resolved through realpath, so common
relative-path and symlink aliases converge on one canonical path.
It does not cover:
- agent-issued
bashcommands or user-issued shell commands - custom tools, or custom tools that override built-in
edit/write - Git operations
- IDEs, editors, formatters, build tools, or any external process
- PI processes that did not load this extension
- different OS users or different machines
- NFS or other network filesystems
- hardlink aliases
- time-of-check/time-of-use (TOCTOU) changes after preflight
- a later extension
tool_callhandler that rewritesevent.input.path
PI identifies tool events by tool name, so this extension cannot prove that a
tool named edit or write is still the built-in implementation. If another
extension mutates the path, load collision guard after that handler where
possible.
Install
Requirements:
- Node.js 22 or later
- PI with package support
PI extensions execute with your user account's full filesystem permissions. Review the package before installing it.
Install for the current OS user:
pi install npm:pi-collision-guard
For a project-local PI setting instead:
pi install npm:pi-collision-guard -l
Try once without installing
Load the npm package only for the current PI run:
pi -e npm:pi-collision-guard
What happens during a run
- On the first built-in
editorwritefor a path, the extension canonicalizes the path and tries to create a per-path lease. - If another participating PI runtime has an active lease, the tool call is blocked before the built-in tool executes.
- The runtime refreshes its held leases every 5 seconds.
- Normally, a claim lives from that first protected tool call until PI emits
agent_settled. All claims owned by that runtime are then released. session_shutdownstops the heartbeat and performs an idempotent fallback release.
agent_settled means PI has no automatic retry, compaction retry, or queued
continuation left. A later agent run can acquire new claims again.
Stale and damaged state
The default stale threshold is 30 seconds:
- A lease whose PID is known to be alive remains active, even if its heartbeat is old.
- A lease whose PID is known to be dead is reclaimable on the next acquire.
- If process state cannot be determined, the lease is reclaimable only when its heartbeat is more than 30 seconds old.
A malformed or unreadable claim fails closed for a 30-second corrupt grace period, measured from the claim file modification time. After that grace period, the next acquire may atomically replace it.
Recovery is demand-driven: there is no background janitor deleting stale claims.
Conflict example
PI A and PI B run as the same user and both load this extension:
PI A: built-in edit src/main/java/example/App.java
-> lease acquired; edit proceeds
PI B: built-in write src/main/java/example/App.java
-> blocked: path is held by PI A
PI A: agent_settled
-> PI A's claims are released
PI B: retry write
-> lease acquired; write proceeds
Different canonical paths remain independent.
Commands
/collision status
Lists visible claims, including:
[self]or[other]- a path relative to the current working directory when possible
- a shortened session ID
- claim age
- active/stale status
- damaged claim IDs and whether their grace period has expired
/collision with no arguments is also treated as status.
/collision force-release <path>
Deletes the claim for one canonical path.
This is dangerous: another PI process may still be writing the file. The command is allowed only when PI reports that UI is available and the user accepts the confirmation prompt. It is refused in non-UI contexts, and the extension does not expose force release as an agent-callable tool.
Use it only after independently confirming that no active writer owns the path.
Failure behavior
Protected edit and write calls fail closed. If canonicalization, claim
storage, operation locking, or lease acquisition fails, the tool call is
blocked rather than allowed through. A fresh damaged claim also blocks
takeover.
Heartbeat, release, status, and force-release errors are reported in PI's UI when UI is available. A failed release can leave a claim for stale recovery.
State files and permissions
Default state root:
~/.pi/agent/collision-guard/
├── claims/
└── locks/
- New state directories and operation-lock directories use mode
0700. - New claim files and lock owner-token files use mode
0600. - Claim filenames are
SHA-256(canonicalPath).json; the original canonical path is stored inside the JSON record. - Records also contain the PI session ID, a random runtime owner token, PID, claim time, heartbeat time, and schema version.
Existing directories are not re-permissioned. Filesystem and OS semantics still apply.
Recovery procedure
- Run
/collision status. - Let the owning agent reach
agent_settled, or cleanly exit the owning PI process. - Retry the protected operation. A dead owner is reclaimed on acquire; an unknown owner needs a heartbeat older than 30 seconds.
- For a damaged claim, wait until the 30-second corrupt grace period has elapsed, then retry.
- Only if you have verified that no writer is active, use
/collision force-release <path>and approve the UI prompt.
Uninstall and state cleanup
First let participating agent runs settle and close every PI process still using the extension. Then uninstall:
pi remove npm:pi-collision-guard
PI package removal does not delete collision-guard state. After confirming no loaded PI process remains, remove it explicitly:
rm -rf ~/.pi/agent/collision-guard
Deleting that directory while a participating PI process is active disables coordination until state is recreated and can allow conflicting writes.
Performance and cost
Each protected tool call performs path canonicalization plus local filesystem locking and claim I/O. Heartbeat work runs every 5 seconds only while the runtime holds paths; it scans visible claim files and rewrites this runtime's claims. Status and release operations also scan the claim directory.
The extension itself uses:
- no model tokens
- no model requests
- no network requests
- no remote service
Cost therefore scales with the number of local claim files, not repository size or prompt size.
Security and privacy
Coordination data stays in the current user's local PI state directory. The extension sends no telemetry and does not transmit paths, session IDs, or claim data. Canonical paths are hidden from claim filenames but remain plaintext inside user-readable claim JSON.
This is cooperative coordination, not a security boundary. A process with the same user permissions can modify or delete the state, and unprotected tools can write the target file directly.
Local development
From a checkout:
npm install --ignore-scripts
npm run typecheck
npm test
npm run test:local
npm run build
npm exec -- pi -e .
npm run test:local needs no model or network access. It runs the automated
suite, then starts separate Node processes against a real temporary claim
store to verify acquire, conflict, release, and retry behavior. Loading PI and
having an agent invoke built-in edit / write is an optional final
end-to-end check that does require a configured model.
To run that real PI/model check:
PI_E2E_MODEL=provider/model npm run test:pi-e2e
The scenario starts three real PI processes. PI A edits and holds a path, PI B must receive a collision error for that path, and PI C must edit it after PI A settles and releases the claim.
The final command loads the built dist/extensions/collision-guard.js entry
declared by the package's pi.extensions manifest. Exit PI normally to
exercise session_shutdown.
Useful validation:
npm run format:check
npm pack --dry-run
The test suite contains 22 tests covering lease policy, path canonicalization, concurrent acquisition, stale/corrupt recovery, PI event wiring, commands, heartbeat, release, and fail-closed behavior.
Architecture
The public core is split into domain, application, and infrastructure;
the PI integration is a thin adapter under src/extension, loaded through
extensions/collision-guard.ts. See
docs/architecture-zh.md for a Java-oriented mapping
and a tool-call-to-release trace.
License
MIT