Spawns session-owl's background stage agents with a clean origin so they can use its memory tools (a Claude Code platform quirk companion).

Session memory as a background-maintained append-only knowledge graph; its roots become the compaction summary; the agent traverses and regex-searches it to recall verbatim messages, tool calls and results.

/owl:ls, /owl:cat, /owl:find let you list, inspect, and search your memory graph; the owl_recall tool lets the agent recall on demand.pi install npm:avtc-pi-session-owl
A full port to Claude Code's plugin (mods) system lives in claude/README.md.
session-owl runs three maintenance stages:
At compaction, the active set is rendered into the compaction summary that pi injects afterward — the agent's post-compaction memory:
# Memory
Your session memory — the top level of a tree; each id opens deeper detail via owl_recall.
Legend: 📁 n.. node · 📄 o.. observation (obs) · importance crit high med low (how much it matters if lost) · 🆕new (not yet processed) 📦archived 🪦obsolete · 2nodes 3obs (direct children) · 34lines 412tokens (direct children observations full details size)
## Memory use
The root view is a navigation index into retained session memory: the lines name what exists; the detail, evidence, rationale, and results live in the tree behind them, across the whole session (see the totals). Recall before relying on session-derived understanding: search memory, or expand a visible related id — a complete-looking label still summarizes only the surface, and relevant context often sits deeper than the root. Use {"ids":["n23"]} to expand, {"query":"…"} to search, and add "fullDetails":true when exact messages, tool output, or rationale matter.
## Initial prompt
Redesign the auth flow: move JWT validation to middleware and drop the legacy login form.
## Active set
📁 nGoal · crit · Redesign auth flow (JWT middleware, drop legacy login) · 3nodes 1obs · 2lines 45tokens · Jul 28 14:30 — Jul 29 09:15
📁 n12 · high · Auth flow redesign · 1obs · 2lines 15tokens · Jul 28 14:30 — Jul 29 09:15
📁 n8 · high · Decisions · 5obs · 214lines 5100tokens · Jul 28 14:30 — Jul 29 09:15
📁 n6 · 📦low · Old login form · 2obs · 96lines 2300tokens · Jul 27 09:00 — Jul 27 18:00
📁 nIrrelevant · med · Irrelevant · 4obs · 12lines 260tokens · Jul 28 14:30 — Jul 29 09:15
---
Source tree total: 237 nodes (4 levels) · 273 observations · 10k lines 576k tokens of details · 3 compactions
## Recently touched
Jul 29 09:10 read src/auth/middleware.ts:1-40,120-180
Jul 29 09:12 edit src/auth/jwt.ts
Jul 29 09:15 write src/auth/index.ts
After compaction the agent continues with this summary alongside pi's own recent-context tail.
Self-run benchmarks (one authored scenario, N=3 continuations per arm, LLM-judge scored) against bare pi and pi-observational-memory, driven by our compaction benchmark harness. Agent glm-5.3; session-owl-builder is the default configuration:
| Recall accuracy | bare pi | om-default | session-owl-builder | session-owl-selector | om-full |
|---|---|---|---|---|---|
| Facts the user asked to remember (direct probes) | 100% | 87% | 100% | 100% | 90% |
| Topics cued from summary, agent has to call tools to recall details | 21% | 23% | 78% | 77% | 43% |
| No trace in the summary or kept tail, agent has to search through session memory or deduct | 11% | 22% | 100% | 56% | 22% |
Arms: bare pi — native compaction summary, no memory extension; om-default — pi-observational-memory (v2 fork) at the default threshold, observations fit — Reflector/Pruner stay idle; om-full — observations exceed the threshold, Reflector + Pruner run.
Full reports: agent glm-5.3 · background qwen3.8-27B · agent qwen3.8-27B fp16 · xhigh
The graph is a containment tree of nodes (folders) holding observations (leaves). It renders the same way everywhere — the agent's owl_recall, the Builder and Selector tools, and the /owl:ls//owl:cat//owl:find commands:
📁 nGoal · crit · The session goal · 4nodes 1obs · 2lines 45tokens · Jul 28 14:30
📁 n12 · high · Auth flow redesign · 1obs · 2lines 15tokens · Jul 28 14:30 — Jul 29 09:15
📄 o31 · med · JWT validation moved to middleware · 2lines 15tokens · Jul 28 14:30
📁 n8 · high · Decisions · 5obs · 214lines 5100tokens · Jul 28 14:30 — Jul 29 09:15
📁 n6 · 📦low · Old login form · 2obs · 96lines 2300tokens · Jul 27 09:00 — Jul 27 18:00
📁 n3 · 🪦med · YAML config · → n8 · 1obs · 1line 12tokens · Jul 27 09:00
📁 n5 · med · Scratch · 2obs · 38lines 900tokens · Jul 28 14:30 — Jul 29 09:15
(📁 n.. node · 📄 o.. observation (obs); importance *crit*/*high*/*med*/*low* (how much it matters if lost); state glyphs 🆕 new (not yet processed) · 📦 archived · 🪦 obsolete; 2nodes 3obs (direct children) · 34lines 412tokens (direct children observations full details size).)
How the agent operates it. The Builder and Selector navigate and edit the graph with filesystem-style tools — ls, cat, find to read; mkdir, mv, merge, supersede (Builder-only), set_meta to reorganize; try_finish to converge the root view on its budget. The Builder maintains the source graph; the Selector builds a curated copy (the active set) for the summary. The agent itself uses the read-only owl_recall to fetch and search on demand.
While session-owl works, a footer line shows the active stage, its progress, and its token cost:
🦉 1005(+11) obs 150/371 → 95 roots 35k/40k · 8.0k/262k · 3.4k tok · +3 obs
(observation/node counts with deltas since the stage started · chunk batch N/M during an observe run · root-view tokens vs budget · context-window usage · streamed output tokens · observations accepted in the current chunk but not yet persisted — appears only while a chunk is actively recording).
| Tool | Description |
|---|---|
owl_recall | Recall from memory — fetch by id, filter by time range, or regex search. Targets the rendered tree (selected-root or observations-root). |
| Command | Description |
|---|---|
/owl:status | Show memory stats and per-phase token/cost usage (since last compaction and since session start) |
/owl:ls [nodeId] | List root nodes, or a node's children |
/owl:cat <id> | Show a node (with its observations) or a single observation in full |
/owl:find <query> | Search memory (regex; current items) |
/owl:find-all <query> | Search memory (regex; everything, including superseded) |
/owl:rescan | Discard the current memory graph and re-observe the entire session from the start (asks confirmation). With --reuse-observations: rebuild the graph structure from the collected observations without re-observing |
/owl:reobserve-0-obs-chunks | Re-observe session ranges that were skipped with zero observations (repair after a degraded model run) |
/owl:settings | Open the settings UI |
/owl:status output:

Five independent mode axes, all live-toggleable mid-session:
| Setting | Options | Default |
|---|---|---|
observerMode | on-threshold · on-compaction | on-threshold |
builderMode | on-compaction · each-N-observations · on-session-context-threshold · on-root-view-threshold | each-N-observations |
selectorMode | on-compaction · on-session-context-threshold | on-compaction |
renderMode | selected-root (Selector curates) · observations-root (the Builder's root view) | observations-root |
subagentMode | inherited (same as the main conversation) · on-compaction (captured only when the subagent itself compacts) · disabled | on-compaction |
The defaults keep the observations graph in shape, so when a compaction is triggered the summary is immediately provided — tune the modes and thresholds to your model and workload. Two shape knobs — rootViewTargetNodes (how many roots to aim for; no target = the agent shapes the root view on its own) and rootViewStrategy (task · category · recency · importance · topic) — advise the Builder and Selector on the root view's organization. See CONFIGURATION.md for the full schema reference (every knob, defaults, per-component model presets).
Pi's compaction hook is last-registration-wins: when two extensions customize compaction, only the last one registered has an effect — the other silently does nothing. Pi has no mechanism for extensions to veto each other.
Session Owl handles this by checking, at every start, whether another compaction-handling extension is installed (from ~/.pi/agent/settings.json, <project>/.pi/settings.json, and the pi extension dirs). When it finds one, it stays dormant — it collects no observations, compaction falls through to pi's native summary or the other extension's, and the widget shows a paused line:
🦉 ⚠ paused — pi-blackhole also handles compaction (/owl:status)
The pause is runtime-only (nothing is written to your settings): remove the other extension and restart pi, or toggle ignoreConflicts in /owl:settings — session-owl resumes immediately, even mid-session.
Unknown or future packages are caught by a source scan for the override-shaped compaction-hook registration in installed package dirs (best effort — passive listeners that only observe compaction events never trigger it). Known compaction-handling packages are also checked by name — a curated list maintained in session-owl's source — so forks and renamed copies are caught even when their code shape changes.
If you deliberately run session-owl alongside another compaction handler (e.g. for benchmarking), set ignoreConflicts: true in the session-owl settings (/owl:settings). The last-registered extension wins — with this enabled you are choosing that fight knowingly.
Check out the full suite of related extensions, avtc-pi — deterministic feature development, subagent delegation, working-memory, behavioral learning, parallel-work guardrails, durable decisions, notifications, and more.
Developed with Z.ai — get 10% off your subscription via this referral link.
MIT
hooks/register.js 141 lines1// SPDX-License-Identifier: MIT
2// SPDX-FileCopyrightText: 2026 avtc <tarasenkov@gmail.com>
3//
4// session-owl-subagent-support — the stage spawner companion.
5//
6// Claude Code tags every tool call an agent makes with the plugin whose
7// code spawned it, and then re-entry-skips that plugin's own tool.call
8// hooks for those calls — session-owl's spawned stage agents could not
9// reliably reach their memory tools. This companion exists to be a
10// DIFFERENT spawner: session-owl's orchestration calls the `launch` tool
11// here from its ambient code, the hook below returns at once, and an
12// ambient timer (fired after the hook's frame closed, so the spawn's
13// origin roots HERE, never at the requester) spawns the agent and reports
14// the handle back through session-owl's `stage_launched` callback tool.
15//
16// Stateless by design: no storage, no configuration, nothing to own. The
17// stage agent types are registered by session-owl itself; only the spawn
18// is routed through here.
19
20async function onSessionStart($, e, next) {
21 try {
22 await $.tool.register({
23 name: "launch",
24 description: "Spawn a session-owl stage agent and report its handle back.",
25 inputSchema: {
26 type: "object",
27 required: ["launchId", "subagentType", "prompt"],
28 properties: {
29 launchId: {
30 type: "string",
31 description: "The launch id the requester parks its wait under.",
32 },
33 subagentType: {
34 type: "string",
35 description: "The namespaced agent type to spawn (session-owl:observe and kin).",
36 },
37 prompt: {
38 type: "string",
39 description: "The agent's prompt.",
40 },
41 model: {
42 type: "string",
43 description: "An explicit model for the agent, when the requester pins one.",
44 },
45 },
46 },
47 });
48 } catch (err) {
49 // a failed registration surfaces as a refused launch on the caller's
50 // side (its broker degrades to the direct spawn) — and the reason is
51 // said here too, through the engine's transport first, never silent
52 const detail = err instanceof Error ? err.stack || err.message : String(err);
53 const text = `session-owl-subagent-support: launch tool registration failed: ${detail}`;
54 try {
55 $.ui.log(text);
56 } catch {
57 try {
58 console.error(text);
59 } catch {
60 // no console in this sandbox
61 }
62 }
63 }
64 return next(e);
65}
66
67async function onToolCall($, e, next) {
68 if (e && e.tool === "mcp__session-owl-subagent-support__launch") {
69 const launchId = typeof e.launchId === "string" ? e.launchId : "";
70 const subagentType = typeof e.subagentType === "string" ? e.subagentType : "";
71 const prompt = typeof e.prompt === "string" ? e.prompt : "";
72 const model = typeof e.model === "string" ? e.model : undefined;
73 // the capsules: closures spelling the engine calls literally, FIRED
74 // from the ambient timer below — never inside this hook's frame (a
75 // frame-open spawn keeps the requester in the origin chain; probed:
76 // only the after-return ambient spawn roots the origin here)
77 const spawnCapsule = () =>
78 $.agent.spawn({
79 subagentType,
80 prompt,
81 ...(model !== undefined ? { model } : {}),
82 background: true,
83 });
84 const reportCapsule = (payload) => $.tool.call({ tool: "mcp__session-owl__stage_launched", launchId, ...payload });
85 // Never a silent failure: a failed spawn reports launchError back
86 // through the SAME handshake (the requester rejects the parked launch
87 // with this reason and logs it); a failed report says itself on the
88 // console if one exists (this companion has no logger of its own).
89 // The engine's own transport first: $.ui.log writes a dim transcript
90 // row the USER sees (the engine's debug log carries it either way) —
91 // the console is only the final fallback for a sandbox without one.
92 const sayError = (prefix, err) => {
93 const detail = err instanceof Error ? err.stack || err.message : String(err);
94 const text = `session-owl-subagent-support: ${prefix}: ${detail}`;
95 try {
96 $.ui.log(text);
97 return;
98 } catch {
99 // the engine transport refused — fall through
100 }
101 try {
102 console.error(text);
103 } catch {
104 // no console in this sandbox — nothing exists to say it with
105 }
106 };
107 setTimeout(() => {
108 spawnCapsule()
109 .then((handle) => {
110 const hasId = handle != null && typeof handle.agentId === "string" && handle.agentId.length > 0;
111 const payload = hasId
112 ? {
113 spawnAgentId: handle.agentId,
114 spawnModel: typeof handle.model === "string" ? handle.model : "",
115 }
116 : {
117 // an id-less resolve is a spawn failure: report it as one so
118 // the requester falls back at once (never a silent timeout);
119 // a permission deny (auto mode) resolves rather than rejects,
120 // and the raw shape names it
121 launchError: `spawn resolved without an agentId: ${JSON.stringify(handle) ?? String(handle)}`,
122 };
123 return reportCapsule(payload).catch((err) => sayError("stage_launched report failed", err));
124 })
125 .catch((err) => {
126 sayError("stage agent spawn failed", err);
127 reportCapsule({ launchError: err instanceof Error ? err.stack || err.message : String(err) }).catch(
128 (reportErr) => sayError("launchError report failed", reportErr),
129 );
130 });
131 }, 30);
132 return { result: "launching" };
133 }
134 return next(e);
135}
136
137export function register(on) {
138 on("session.start", onSessionStart);
139 on("tool.call", onToolCall);
140}
141