Meta-contract: transportable (bit-for-bit identical across projects), high-density (zero fluff), constant (evolve by crystallizing, not speculating), optimal minimum (add only when it hurts).
locks.json is a shared registry for singleton pi extensions.
Path:
~/.pi/agent/locks.json
{
"@scope/pi-singleton": {
"pid": 2590864,
"cwd": "/home/user/project"
}
}Top-level keys are extension identities. Values are JSON objects owned by that extension.
Use the most stable available identity:
package.json/namefor npm-style pi packages- Directory name when the extension entrypoint is
index.tsbut there is no package name - File basename when the extension is a single file
For npm-style package extensions, the canonical value is the package.json name. Implementations may keep that value as a small local constant when it is clearer than runtime package introspection. The fallback rules are only for unpackaged extensions.
Examples:
extensions/pi-singleton/package.json name=@scope/pi-singleton -> @scope/pi-singleton
extensions/pi-singleton/index.ts without package.json -> pi-singleton
extensions/pi-singleton.ts -> pi-singleton
{
"pid": 2590864
}pid is the process that currently owns the singleton runtime. cwd should be stored when ownership is tied to a pi session directory.
During a user-initiated start/connect event, an extension should:
- Read its lock entry
- If
pidis stale, replace the entry - If
pidandcwdmatch the current pi instance, refresh or keep the entry - If a live polling owner exists, ask interactively whether to move singleton ownership here
Lock writes must be caused by an explicit user-initiated runtime event, such as a start/connect command or a confirmed takeover prompt.
Extension initialization and session-start hooks may read locks.json, update local status, install ownership watchers, and resume local work when the existing lock already points at the current pid/cwd. After a full process restart, a session-start hook may replace a stale lock from the same cwd to restore explicitly requested ownership. They must not create ownership from an inactive lock, take over a live polling owner, or replace a stale lock from another directory by themselves. Such locks should stay visible as state until the user runs the start/connect command. Session replacement should suspend local runtime work and ownership watchers without releasing the lock, so the next session in the same pid/cwd can resume from explicit ownership.
Extensions may add compact fields when useful:
{
"pid": 2590864,
"cwd": "/repo/project",
"mode": "connected",
"updatedAt": "2026-04-28T00:00:00.000Z"
}Do not print optional fields in normal UI unless they help the user act.
- One top-level key per singleton extension
- An extension may only mutate its own key
- Other keys must be preserved exactly
- If
cwdis present, active-here ownership means bothpidandcwdmatch the current pi instance - Human-readable diagnostics should say
active here,active elsewhere, orstale - Debug data belongs in
locks.json, not in normal status output
Singleton extensions with footer/status presence should expose quiet but explicit local state:
offwhen this pi instance does not own the singleton runtimeonwhen this pi instance owns the runtime but has no pending runtime detail to show[16:32:39]when the runtime owns scheduled work and can show the next countdown
Extensions may prefix those states with their own compact name, such as wakeup off or telegram on.
Start/connect commands should make singleton moves easy:
- If no live owner exists, take ownership without an extra prompt
- If a live polling owner exists, ask whether to move singleton ownership to this pi instance
- On confirmation, write the current
{ "pid": ..., "cwd": ... }to this extension's key inlocks.json - The previous owner must notice that
locks.jsonno longer points at its ownpid/cwdand stop singleton-owned work such as polling/watchers without deleting the new lock or unrelated session-local queues
Takeover prompts should use the extension name as the dialog title, then the question, a blank line, and source/target lines:
pi-singleton
move singleton lock here?
from: pid 2590864, cwd /old
to: /new
Avoid repeating the extension name in the body. Color is encouraged: extension title/name accent, question warning, from:/to: muted.
The previous owner may use fs.watch, mtime polling, or an existing status/timer tick. Long-lived watchers should compare against a snapshotted pid/cwd identity rather than a live pi context object, because session replacement such as /new makes captured contexts stale. The important contract is graceful singleton-runtime shutdown after ownership mismatch while session-local state that does not require polling remains owned by its original instance.
Delete ~/.pi/agent/locks.json to reset singleton runtime ownership for all participating extensions without deleting their configuration files.
Current baseline is read-modify-write JSON. This is enough for interactive pi singleton starts.
If multiple instances may start concurrently, use an atomic helper later:
- Lock file around
locks.json, or - Temp file + rename with conflict checks, or
- OS-level exclusive open for a short critical section
Migrations from legacy lock files or legacy keys should be one-off cleanup work. Runtime ownership should read and write only locks.json under the canonical identity key.