pi-collision-guard

Fail-closed local path leases for PI built-in edit/write across participating processes

Packages

Package details

extension

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 edit and write tools 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 bash commands 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_call handler that rewrites event.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

  1. On the first built-in edit or write for a path, the extension canonicalizes the path and tries to create a per-path lease.
  2. If another participating PI runtime has an active lease, the tool call is blocked before the built-in tool executes.
  3. The runtime refreshes its held leases every 5 seconds.
  4. Normally, a claim lives from that first protected tool call until PI emits agent_settled. All claims owned by that runtime are then released.
  5. session_shutdown stops 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

  1. Run /collision status.
  2. Let the owning agent reach agent_settled, or cleanly exit the owning PI process.
  3. Retry the protected operation. A dead owner is reclaimed on acquire; an unknown owner needs a heartbeat older than 30 seconds.
  4. For a damaged claim, wait until the 30-second corrupt grace period has elapsed, then retry.
  5. 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