@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
Package details
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-lanesis closed to every lane and every process it starts: sessions, journal, nothing of it can be read through a tool.lanes_journalshows 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 astrusted.
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.hasUIis true in them. An extension that starts background loops onhasUI(a scheduler, a poller) would start them in every lane. Checkctx.mode === "tui"instead, or pass an environment variable throughenvthat 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(andchildren.blockedTools) when lanes belong to different people.
License
MIT