@elvinw/pi-qq0.5.7
/qq — quick side questions for pi that don't enter the conversation context (Claude Code's /btw, for pi)
Untested on omp
This is a pi package. No one has reported whether it works on omp yet. If you try it, add your result below.
Reports 0
No reports yet.
Readme
pi-qq — /qq quick side question
Ask a one-off "by the way" question without letting the Q&A enter the ongoing
context — the same idea as Claude Code's /btw.
True deletion: dismissed threads are really gone
Mainline pi's session is append-only: dismissing a /qq thread can at best
move the leaf back, leaving the Q&A as a dead branch in the file (visible
in /tree). This extension closes that gap. At load time it additively
installs pruneBranches(keepLeafId?) on SessionManager.prototype —
keeping only the root→leaf path, rebuilding pi's own index, and rewriting the
session file. Never overriding an existing method, and only when the running
pi version is in the verified allowlist (config.patchTargetPiVersions,
currently 1.1.0 and 1.2.0 — the patch touches private-by-convention
internals, so a version is added only after confirming SessionManager is
byte-identical).
Dismiss and /nvm then drop the thread through a ladder — the first tier
that works wins:
- prune (pin matches the running pi): the thread is physically removed
from the session file — no dead branch, same session id. (Other orphan
branches are tidied too.) The thread's billed usage is preserved as a
context-invisible marker at the branch point, so the tokens it really
spent still show in the session stats (
/cost, footer) even though the entries are gone. - rewind (any other pi): the baseline — the thread is orphaned in the
tree (visible in
/tree). There is deliberately no fork tier: a fork would create a duplicate session file in/resumeon every dismiss. A one-time notice makes the degradation visible.
Transient failure paths (no answer, timeouts) still use a plain rewind: they discard one small failed question and must not churn session files.
Good to know: the prune tier rewrites your session files in place (the thread's nodes are removed from the JSONL). The patch itself is in-memory only and additive — it refuses to install on any pi other than the exact pinned one, and uninstalling the extension leaves pi completely untouched. Still, if you run valuable sessions, keep your usual
~/.pibackups.
Commands
| Command | Tool policy during the question |
|---|---|
/qq <question> |
prompt asks the model to use no tools |
/qqro <question> |
prompt asks the model to use only read-only tools (read, grep, find, ls) |
/ro |
Side thread only: toggle the follow-up policy between no-tools and read-only |
/sub |
Side thread only: start a sub-thread from the current position. It stays part of the SAME side session (a context-invisible boundary marker shows it in /tree and on resume); /nvm and /merge always act on the whole session. (Named /sub because pi's built-in /fork — fork the session — wins the bare /fork exact match.) |
/merge |
Side thread only: keep everything — initial /qq + every follow-up + every sub-thread — as conversation. The side session becomes mainline, the mode ends, and the next message continues right after the last answer. |
/nvm |
Side thread only: never mind — exit the mode and rewind the whole side session (initial /qq + follow-ups + sub-threads) to its first question, as if it never happened |
The side thread is visible everywhere:
- Input box: while the thread is open, the editor's top border becomes a
"qq side thread — … /ro read-only · /sub sub-thread · /merge to main · /nvm back"
title (live: the sub-thread depth updates on
/sub) and the footer status shows the live tool policy and depth. - Command list: hidden == not dispatchable. Inside the thread
/qqand/qqrodisappear from the list and do not run either (plain text is the question now)./ro,/sub,/mergeand/nvmare not registered commands at all — an input-level handler owns them while the thread is open, and outside it they pass through to the model as ordinary messages, exactly like any unknown slash text. A mistyped mode command (/nvm',/subx, …) is a typo, not a command: inside the thread it runs as a follow-up question (the box IS the side conversation), with a note telling you the exact spelling.
Neither command touches the active tool set: the side request is a byte-identical prefix extension of your last turn, so on llama.cpp/vLLM it costs zero extra prefill (the server's prefix cache only grows, and the cache is still intact for the next main turn). Tool policy is two-layer:
- the prompt asks the model to comply (no tools / read-only only), and
- a hard guardrail blocks any out-of-policy tool call before it executes
— the model receives the block reason as a tool error and falls back to
answering in text. A rogue
write/bashcan never run during a side question.
How it works
The Q&A takes over the transcript like a normal turn and stays there until you decide its fate.
- First answer → a keypress gate: f open the side thread, m merge it into the conversation, d dismiss it. Any other key is ignored (the gate stays armed, so a stray press can't dismiss by accident).
- In the thread → no keypress. Plain text is the next follow-up;
/rotoggles policy,/substarts a sub-thread,/mergekeeps it all,/nvmdrops it. A normal (non-/qq) message absorbs the thread — it can never be rewound away again. - Mechanically — a pi session is an append-only tree and the model context
is the root→leaf path.
/qqsends the question onto a temporary side branch, waits for the run to settle, then on dismiss//nvmdrops the whole branch at its root (prune on a pinned pi, rewind otherwise); on merge it simply doesn't rewind. - Exit mid-thread → the thread survives in the session file and is re-detected on the next start (answered → re-enters the mode; merged or absorbed → not re-opened).
Compaction safety
A side question must never compact the main conversation. During a side
turn pi-qq cancels auto-compaction, waits for true idle before rewinding
(navigateTree refuses while busy), warns up front at ≥ 90% of the context
window, and notifies if a rewind fails. The side request still sends the full
current context (the model needs it) — what /qq saves is the recurring cost:
the exchange never rides along in every later turn.
Install
pi install npm:@elvinw/pi-qq
Works on any pi: on pi 1.1.0 or 1.2.0, dismissed threads are physically pruned from the session file (see above); on other pi versions the extension automatically degrades to the plain dead-branch behavior.
or from a local clone:
pi --extension ./pi-qq
or place this directory in ~/.pi/agent/extensions/ (personal) or
.pi/extensions/ (project), then /reload.
No npm install is needed to run it: the only runtime import
(CustomEditor, for the mode's input-box title) is provided by pi itself
when it loads the extension.
Development
npm install # pulls the published pi-coding-agent 1.2.0 types + tsc
npm run typecheck
npm test # 57 scenario tests (node:test, no extra deps)
The pi-coding-agent dev-dependency is exactly pinned (no ^) because
the self-patch — and the tests around it — target a verified pi version
(1.2.0, the newest in the allowlist); it also exists for local typechecking
against the published API.
node_modules/ is git-ignored.
Tests
test/ runs the extension against a mock pi environment (the extension
imports pi's types only — no runtime dependency — so a mock ExtensionAPI
plus an in-memory session tree exercise the real logic, no pi process
needed). The scenario matrix covers:
- lifecycle — send → settle → PENDING → dismiss/merge, key semantics (case, d dismisses vs other keys ignored and the gate stays armed), status line, prompt notes
- threads — follow-up chains, context inheritance, thread absorption by
a normal message, orphaning via
/tree - side-thread mode — plain-text follow-ups, no keypress gate after a
follow-up answer (the next plain text is the next follow-up),
/rotoggling (and non-reset across follow-ups),/nvm(with and without a thread, in-flight guard), auto-exit on/compact//tree//neweffects, recursion guard, slash pass-through, in-flight guard, full e2e loop, near-miss (a mistyped mode command like/nvm'runs as a follow-up question, thread intact, and passes through as an ordinary message outside the thread) - sub-threads & merge —
/subappends a context-invisible boundary (depth shown live in the status and the editor title) and the session continues;/nvmdrops the WHOLE session (initial/qq+ sub-threads);/mergekeeps the whole chain in order + close marker and ends the mode; both pass through as ordinary messages outside the thread; both are refused while a question is in flight; resume re-enters at the right depth and the walker treats fork boundaries as transparent (unit-tested, incl. merged/absorbed-after-sub and unanswered-after-sub) - mode surface — autocomplete hides
/qq+/qqroinside the thread and hidden == not dispatched (/ro,/sub,/mergeand/nvmare input-level, not commands, and pass through as ordinary messages outside the thread); the editor border carries the live mode title in mode only - failures — no answer, settle timeout, failed question mid-thread, stuck-busy rewind
- queue — busy-on-call, re-settle loop, queue timeout, in-flight guard
- invariants — zero leaked subscriptions on every path (including mode exit), compaction cancellation, tool guardrail (block/allow matrix), session swap, empty session, preflight warning
- resume — pi exited mid-thread: answered thread re-enters the mode
(follow-up continues,
/nvmdrops the whole session, incl. sub-threads), merged/absorbed threads are not re-opened, unanswered (mid-stream) restore only the thread for/nvm, plain sessions untouched - true deletion —
installSessionPruneon the exact pinned pi version (idempotent, version-mismatch refusal) and a realSessionManagerintegration test (prune keeps root→target, re-pins the leaf, rewrites the file, reopens cleanly); the drop ladder on dismiss and/nvm(prune / rewind tiers — never fork)
Safety
- Tool policy is two-layer while the tool set itself stays untouched
(deliberate, for cache preservation): the prompt asks the model to comply,
and a
tool_callguardrail blocks any out-of-policy call before it executes — the model sees the block reason as a tool error and answers in text instead. Nothing can write files or run commands during/qq. - If an answer is aborted or errors before producing text, only that question is rewound away immediately (nothing to read); the rest of an in-progress thread survives.
- Exitting pi mid-thread never loses the thread: it is re-detected from the session file on the next start (see exit mid-thread above). Merge leaves a context-invisible close marker so a resumed session can tell a merged thread from an open one.
/new,/fork, or quit mid-question: the wait bails out; the side branch is just an orphan branch (harmless). Inside an open side thread these commands (and/compact,/treenavigation) end the mode automatically — the thread is absorbed or orphaned and plain text goes back to the main conversation.- In side-thread mode, plain text submitted while a question is still streaming is dropped with a warning (strictly serial — one question at a time, single-slot llama.cpp safe).
- Double
/qq//qqrois rejected while one is running or queued. - If the agent is running, the question is queued and runs automatically
when the flow settles (aborting the flow unblocks it). Still strictly
serial — compatible with single-slot llama.cpp (
-np 1). A 2h wait cap drops the question rather than wedging the command.