@patimweb/pi-lanes

Lanes for pi: every prompt an extension triggers runs in its own pi process with its own session, so background work never blocks the session you type in

Packages

Package details

extension

Install @patimweb/pi-lanes from npm and Pi will load the resources declared by the package manifest.

$ pi install npm:@patimweb/pi-lanes
Package
@patimweb/pi-lanes
Version
1.1.1
Published
Oct 14, 2026
Downloads
312/mo · 10/wk
Author
patimwep
License
MIT
Types
extension
Size
64.9 KB
Dependencies
0 dependencies · 2 peers
Pi manifest JSON
{
  "extensions": [
    "./index.ts"
  ]
}

Security note

Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.

README

pi-lanes

Background work never blocks the session you type in.

pi runs one turn at a time. Everything an extension triggers (a Teams message in listen mode, a mail hook, a scheduled prompt) is queued into the session you are working in, and waits for whatever runs there. A twenty-minute task for one colleague means twenty minutes of silence for everyone else.

pi-lanes takes those prompts off your session and runs each in a lane: a pi --mode rpc process with a session of its own.

you ──typing──▶ front session (pi in the terminal)
                   │
extension prompt ──┼──▶ lane "teams:Anna"  pi --mode rpc, own session ──▶ subagents …
                   ├──▶ lane "teams:Bob"   pi --mode rpc, own session
                   └──▶ one-off lane        pi --mode rpc, ends when done

Installation

pi install npm:@patimweb/pi-lanes

No configuration needed. Settings go in ~/.pi/agent/pi-lanes.json.

What goes where

Prompt Goes to
Typed by you the front session
Sent by an extension right after you pressed a key (a slash command asking the agent for something) the front session
Sent by an extension with a hint naming a lane that lane: same lane, same session
Sent by an extension without a hint a one-off lane with a fresh session

Within a lane, order is kept: a prompt for a lane that is busy is steered into the running turn and read after the current step. Different lanes run in parallel, up to maxConcurrent; more wait in line.

Lanes do not chain

Only the interactive terminal session routes. Every lane is started with PI_LANE in its environment, and every process a lane starts (a subagent, a tool, another pi) inherits it. A prompt an extension sends inside a lane stays in that lane. So the depth is fixed: front session, lane, and what the lane itself starts.

For extension authors: hints

Emit a hint on pi's event bus right before sending the prompt:

const prompt = buildPrompt(message);
pi.events.emit("pi-lanes:route", {
  text: prompt,                       // exactly what goes to sendUserMessage
  lane: `mychat:${conversationId}`,   // same key, same lane and session
  label: "Anna Schmidt",
  env: { MY_EXTENSION_CONVERSATION: conversationId },
  command: normalisedMessageText,     // for stop and reset words
  request: shortFormForTheJournal,
  from: "Anna Schmidt",
  trusted: false,                     // may this lane read lanes_journal?
});
pi.sendUserMessage(prompt, { deliverAs: "followUp" });

Nothing listens when pi-lanes is not installed, and the prompt takes the usual way into the session: the hint needs no dependency on this package.

Field Meaning
text the prompt, exactly (required)
lane lane key; absent means a one-off lane
label name in /lanes and the journal
env extra environment for the lane process, e.g. which conversation it may answer
command the triggering text, compared to stopWords and resetWords
request, from for the journal
trusted the lane may read what other lanes were asked (lanes_journal)
session true keeps this prompt in the front session after all

env is how an extension tells the process in the lane what it is for. pi-teams sets PI_TEAMS_WORKER_CHAT, and in a process with that variable it refuses to write to any other chat and never starts its own watcher.

Control words

When a hint carries a command that is exactly one of these words:

Word (default) Effect
stop, stopp, abbrechen, abbruch, cancel the lane's running turn is aborted; the prompt still reaches the model, so it confirms
neues thema, new topic, reset the lane process ends and starts over with a fresh session

Settings

~/.pi/agent/pi-lanes.json, every key optional:

Key Default Meaning
enabled true route at all
maxConcurrent 3 lanes working at the same time (1-16)
maxLanes 6 lane processes alive, busy or idle; the longest-idle makes room
idleMinutes 30 an idle lane exits; its session stays on disk
freshAfterHours 72 a lane quiet for longer starts a fresh session (0 = always continue)
command "pi" the pi executable
args [] extra arguments for every lane, e.g. ["--model", "provider/id"]
env {} extra environment for every lane
stopWords, resetWords see above control words
unhinted "lane" "session" keeps prompts without a hint in the front session
userGraceSeconds 10 a prompt without a hint this soon after a key press stays in the front session
isolation see below what a lane, and everything it starts, may reach

Isolation

Separate sessions keep a lane from knowing what other lanes talked about. Isolation keeps it from looking: a lane asked "what did Bob want?" can say that Bob was around, but not what about.

Always on, without configuration:

  • <agent dir>/pi-lanes is closed to every lane and every process it starts: sessions, journal, nothing of it can be read through a tool.
  • lanes_journal shows who and when in a lane: which lane, for whom, at what time, how long. What was asked and answered only in the front session and in lanes started as trusted.

Configurable in isolation:

"isolation": {
  "lane":     { "allowedTools": ["subagent", "teams_send_chat_message", "teams_read_chat", "lanes_journal"] },
  "children": { "blockedTools": ["teams_*", "subagent"] },
  "ownLane":  { "teams_read_chat": { "param": "chat", "env": "PI_TEAMS_WORKER_CHAT" } },
  "protectedPaths": ["~/.pi"],
  "workspace": "/workspace/lanes"
}
Key Meaning
lane.allowedTools tools the lane process may call (* globs); empty = all
lane.blockedTools tools the lane process may not call
children.blockedTools tools closed to what the lane starts (subagents, pi -p, …)
ownLane a tool argument must equal a variable from the lane's environment (the hint's env). The refusal names the right value, so the model corrects itself. A lane without the variable cannot use the tool at all.
protectedPaths more places closed to lanes and their children, e.g. ~/.pi for all state and credentials
workspace every lane works in <workspace>/<hash>, one-off lanes too. The workspaces of other lanes are closed.

A lane and its children are told apart by PI_LANE_CHILD: the lane puts its pid there when it loads, and everything it starts inherits it. A hint's env cannot set any PI_LANE* variable.

How far it goes. Tool lists and ownLane are exact. Paths are checked in two ways: path arguments are resolved (symlinks included), and every other string argument, a bash command above all, is searched for the protected paths in their usual spellings (absolute, ~/, $HOME/). A command that assembles a path at run time gets past that search. It is a guard rail against a model that was talked into looking, not a sandbox. Where that matters, run lanes as an OS user of their own.

Commands and tools

/lanes which lanes work, idle or wait
/lanes stop <name> abort a lane's running turn
lanes_journal which lanes were active, for whom and when; what they were asked and answered only in the front session and in trusted lanes

Files, all below ~/.pi/agent/pi-lanes/:

Path Content
sessions/<hash>/ sessions of a lane
sessions/once/ sessions of one-off lanes
journal.jsonl start, steer, queue, stop, reset, result, duration, errors

With isolation.workspace set, lanes work in <workspace>/<hash>; the hash is the same as for the sessions.

Things to know

  • Nobody sits in front of a lane. Dialogs (confirm, select, …) are cancelled. An extension that asks before acting has to be configured to act on its own, or it will refuse in a lane.
  • Lanes are RPC processes, so ctx.hasUI is true in them. An extension that starts background loops on hasUI (a scheduler, a poller) would start them in every lane. Check ctx.mode === "tui" instead, or pass an environment variable through env that switches the loop off.
  • Context is per lane. Nothing from one lane reaches another by itself. Shared memory extensions still work across lanes, since they read the same files: close their tools with lane.allowedTools (and children.blockedTools) when lanes belong to different people.

License

MIT