pi-sessions 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +29 -20
  2. package/dist/session-messaging/broker/client-frame.js +62 -0
  3. package/dist/session-messaging/broker/process.js +234 -0
  4. package/dist/shared/session-broker/framing.js +59 -0
  5. package/dist/shared/session-broker/socket-path.js +19 -0
  6. package/extensions/pi-sessions.ts +14 -1
  7. package/extensions/session-auto-title/generate.ts +56 -66
  8. package/extensions/session-auto-title/install.ts +2 -0
  9. package/extensions/session-auto-title/runs.ts +50 -0
  10. package/extensions/session-handoff/board-view-model.ts +2 -4
  11. package/extensions/session-handoff/bootstrap.ts +14 -16
  12. package/extensions/session-handoff/extract.ts +34 -16
  13. package/extensions/session-handoff/install.ts +1 -1
  14. package/extensions/session-handoff/kickoff.ts +4 -3
  15. package/extensions/session-handoff/launch-target.ts +1 -1
  16. package/extensions/session-handoff/metadata.ts +72 -23
  17. package/extensions/session-messaging/broker/client-frame.ts +113 -0
  18. package/extensions/session-messaging/broker/process.ts +12 -21
  19. package/extensions/session-messaging/broker/spawn.ts +12 -7
  20. package/extensions/session-messaging/pi/reachable-contract.ts +59 -0
  21. package/extensions/session-messaging/pi/reachable-presenter.ts +49 -0
  22. package/extensions/session-messaging/pi/reachable-tool.ts +190 -0
  23. package/extensions/session-messaging/pi/reachable-view-model.ts +59 -0
  24. package/extensions/session-messaging/pi/renderer.ts +1 -1
  25. package/extensions/session-messaging/pi/tools.ts +8 -8
  26. package/extensions/session-search/hooks.ts +45 -52
  27. package/extensions/session-search/install.ts +3 -158
  28. package/extensions/session-search/reindex.ts +3 -3
  29. package/extensions/session-search/renderer.ts +1 -32
  30. package/extensions/session-search/tool-contract.ts +1 -21
  31. package/extensions/shared/model-runtime.ts +8 -1
  32. package/extensions/shared/session-broker/client.ts +3 -4
  33. package/extensions/shared/session-broker/framing.ts +6 -7
  34. package/extensions/shared/session-index/lineage.ts +6 -2
  35. package/extensions/shared/session-index/sqlite.ts +44 -17
  36. package/extensions/shared/settings.ts +25 -9
  37. package/extensions/shared/tmux.ts +22 -0
  38. package/extensions/subagents/classify.ts +6 -0
  39. package/extensions/subagents/install.ts +22 -137
  40. package/extensions/subagents/report-message-renderer.ts +2 -2
  41. package/extensions/subagents/settle.ts +182 -0
  42. package/package.json +18 -26
package/README.md CHANGED
@@ -34,7 +34,7 @@
34
34
 
35
35
  ## Install
36
36
 
37
- Requires Pi `0.80.10` or newer and Node `>=24 <26`.
37
+ Requires Pi `0.82.1` or newer and Node `>=24 <26`.
38
38
 
39
39
  **From npm** (recommended):
40
40
 
@@ -65,15 +65,15 @@ Open the frontend implementation task in a session to the right.
65
65
 
66
66
  ## Features
67
67
 
68
- | Extension | Surface | What it does |
69
- | ------------------ | ------------------------------------------------- | ------------------------------------------------------- |
70
- | Session Search | `session_search` pi tool | Search through old sessions |
71
- | Session Ask | `session_ask` pi tool | Ask questions about old sessions |
72
- | Session Handoff | `session_handoff` pi tool, `/handoff` board | Start and manage focused child sessions |
73
- | Session Messaging | `session_send_message`, `session_cancel` pi tools | Coordinate between live Pi sessions and own subagents |
74
- | Session Picker | `Alt+O` | Reference old sessions in your prompt |
75
- | Session Index | `/session-index` slash command | Shows index status and rebuilds the local session index |
76
- | Session Auto Title | in background, `/title` slash command | Give sessions titles |
68
+ | Extension | Surface | What it does |
69
+ | ------------------ | ---------------------------------------------------------------------- | ------------------------------------------------------- |
70
+ | Session Search | `session_search` pi tool | Search through old sessions |
71
+ | Session Ask | `session_ask` pi tool | Ask questions about old sessions |
72
+ | Session Handoff | `session_handoff` pi tool, `/handoff` board | Start and manage focused child sessions |
73
+ | Session Messaging | `session_reachable`, `session_send_message`, `session_cancel` pi tools | Coordinate between live Pi sessions and own subagents |
74
+ | Session Picker | `Alt+O` | Reference old sessions in your prompt |
75
+ | Session Index | `/session-index` slash command | Shows index status and rebuilds the local session index |
76
+ | Session Auto Title | in background, `/title` slash command | Give sessions titles |
77
77
 
78
78
  Every feature is on by default. Turn one off with `enable: false` under its own settings namespace, which unregisters its tools and hooks.
79
79
 
@@ -93,16 +93,16 @@ Every feature is on by default. Turn one off with `enable: false` under its own
93
93
 
94
94
  ## Session Search
95
95
 
96
- `session_search` searches the local session index by text, repo, cwd, time range, file evidence, and whether a session is currently running.
96
+ `session_search` searches the local session index by text, repo, cwd, time range, and file evidence.
97
97
 
98
- Queries support regular text for normal usage, quoted phrases, `AND` / `OR` / `NOT`, parentheses, and `-term` negation when matching needs to be stricter. Unquoted terms use prefix matching, quoted terms are exact. A search with no query returns matching sessions chronologically, newest first. Use `live: true` to restrict results to currently running sessions.
98
+ Queries support regular text for normal usage, quoted phrases, `AND` / `OR` / `NOT`, parentheses, and `-term` negation when matching needs to be stricter. Unquoted terms use prefix matching, quoted terms are exact. A search with no query returns matching sessions chronologically, newest first.
99
99
 
100
100
  File filters distinguish read-or-write evidence from write-only evidence:
101
101
 
102
102
  - `files.touched`: sessions that read or changed a path
103
103
  - `files.changed`: sessions that changed a path
104
104
 
105
- Use `kind: "user"` or `kind: "subagent"` to filter by session type across the whole index. Use `relationScope: "branch"` to search subagents launched from the current conversation branch, or `relationScope: "tree"` to include subagents launched from abandoned branches as well.
105
+ Use `kind: "user"` or `kind: "subagent"` to filter by session type across the whole index. Searching for sessions that can be addressed right now is a different question, answered by `session_reachable`.
106
106
 
107
107
  ## Session Handoff
108
108
 
@@ -116,7 +116,7 @@ Run `/handoff` to open the **Handoffs** board. The Subagents and User sessions t
116
116
 
117
117
  Agents can coordinate with live Pi sessions and their own subagents:
118
118
 
119
- - `session_search` with `live: true` lists live sessions
119
+ - `session_reachable` lists the sessions this session can send messages to
120
120
  - `session_send_message` sends a message to a live session or own subagent
121
121
  - `session_cancel` aborts another live session's current turn
122
122
 
@@ -140,6 +140,8 @@ If you want to override the shortcut, put this in your `~/.pi/agent/settings.jso
140
140
  "sessions": {
141
141
  "handoff": {
142
142
  "pickerShortcut": "alt+p",
143
+ "model": "openai-codex/gpt-5.6-terra",
144
+ "thinkingLevel": "low",
143
145
  "deferred": {
144
146
  "copyToClipboard": true
145
147
  }
@@ -148,6 +150,8 @@ If you want to override the shortcut, put this in your `~/.pi/agent/settings.jso
148
150
  }
149
151
  ```
150
152
 
153
+ `model` and `thinkingLevel` configure the agent that builds handoff prompts. They default to the new session's values when absent.
154
+
151
155
  `deferred.copyToClipboard` (default `true`) controls whether deferred handoffs copy the resume command to the clipboard. When off, the resume command is only shown in the tool call.
152
156
 
153
157
  Subagents require the handoff and messaging features. Limit recursive delegation depth with `sessions.subagents.maxDepth` (default `2`):
@@ -210,9 +214,10 @@ Note that generating titles for all sessions can take some time, and will hit yo
210
214
  - if you manually rename a session with `/name`, automatic retitling pauses for that session
211
215
  - Regenerate the title for the current session to resume automatic retitling
212
216
  - if unconfigured, it will attempt to use these models in order, first one that is available:
213
- - `google/gemini-flash-lite-latest`
217
+ - `openai-codex/gpt-5.6-luna`
218
+ - `openai/gpt-5.6-luna`
214
219
  - `anthropic/claude-haiku-4-5`
215
- - `openai/gpt-5.4-mini`
220
+ - `google/gemini-flash-lite-latest`
216
221
  - your currently configured model
217
222
 
218
223
  To change auto-titling settings, edit `~/.pi/agent/settings.json`:
@@ -223,20 +228,24 @@ To change auto-titling settings, edit `~/.pi/agent/settings.json`:
223
228
  "autoTitle": {
224
229
  "refreshTurns": 4,
225
230
  "timeoutSecs": 15,
231
+ "tokenBudget": 64,
226
232
  "model": "anthropic/claude-haiku-4-5",
227
233
  "thinkingLevel": "off",
228
- "prompt": "Custom prompt that overrides the default."
234
+ "prompt": "Custom prompt that overrides the default.",
235
+ "persistRuns": false
229
236
  }
230
237
  }
231
238
  }
232
239
  ```
233
240
 
241
+ `persistRuns` records each title request as its own session file under `~/.pi/agent/pi-sessions/session-auto-title/`, holding the exact prompt and response. Open one with `pi --session <file>` to see what the titling model was sent.
242
+
234
243
  ## Development
235
244
 
236
245
  ```bash
237
- npm install
238
- npm run check
239
- npm test
246
+ mise trust && mise bootstrap
247
+ mise run check
248
+ mise run test
240
249
  ```
241
250
 
242
251
  For an end-to-end manual flow, see [SMOKE.md](./SMOKE.md).
@@ -0,0 +1,62 @@
1
+ // The broker runs as a detached raw-Node process with nothing installed next to
2
+ // it, so client frames are validated by hand rather than against the TypeBox
3
+ // schemas in shared/session-broker/protocol.ts. The broker only routes
4
+ // envelopes, so it validates the routing fields and treats `envelope` as an
5
+ // opaque object; the receiving client validates the envelope body in full.
6
+ const CONTEXT = "Invalid session messaging client frame";
7
+ export function parseClientFrame(value) {
8
+ const frame = requireObject(value, "/");
9
+ switch (frame.type) {
10
+ case "register":
11
+ return { type: "register", sessionId: requireString(frame.sessionId, "/sessionId") };
12
+ case "unregister":
13
+ return { type: "unregister" };
14
+ case "list":
15
+ return { type: "list", requestId: requireString(frame.requestId, "/requestId") };
16
+ case "send":
17
+ return {
18
+ type: "send",
19
+ requestId: requireString(frame.requestId, "/requestId"),
20
+ target: requireString(frame.target, "/target"),
21
+ envelope: requireObject(frame.envelope, "/envelope"),
22
+ };
23
+ case "incoming_ack": {
24
+ const error = optionalString(frame.error, "/error");
25
+ return {
26
+ type: "incoming_ack",
27
+ requestId: requireString(frame.requestId, "/requestId"),
28
+ delivered: requireBoolean(frame.delivered, "/delivered"),
29
+ ...(error === undefined ? {} : { error }),
30
+ };
31
+ }
32
+ default:
33
+ throw invalid("/type", "expected a known client frame type");
34
+ }
35
+ }
36
+ function requireObject(value, path) {
37
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
38
+ throw invalid(path, "expected an object");
39
+ }
40
+ return value;
41
+ }
42
+ function requireString(value, path) {
43
+ if (typeof value !== "string") {
44
+ throw invalid(path, "expected a string");
45
+ }
46
+ return value;
47
+ }
48
+ function requireBoolean(value, path) {
49
+ if (typeof value !== "boolean") {
50
+ throw invalid(path, "expected a boolean");
51
+ }
52
+ return value;
53
+ }
54
+ function optionalString(value, path) {
55
+ if (value === undefined) {
56
+ return undefined;
57
+ }
58
+ return requireString(value, path);
59
+ }
60
+ function invalid(path, message) {
61
+ return new Error(`${CONTEXT}: ${path} ${message}`);
62
+ }
@@ -0,0 +1,234 @@
1
+ import { mkdirSync, unlinkSync, writeFileSync } from "node:fs";
2
+ import { createServer } from "node:net";
3
+ import { join } from "node:path";
4
+ import { readFrames, writeFrame } from "../../shared/session-broker/framing.js";
5
+ import { getSessionMessagingDir, getSessionMessagingSocketPath, } from "../../shared/session-broker/socket-path.js";
6
+ import { parseClientFrame, } from "./client-frame.js";
7
+ const SEND_TIMEOUT_MS = 10_000;
8
+ const EXIT_IDLE_MS = 5_000;
9
+ const KEEPALIVE_DELAY_MS = 5_000;
10
+ const messagingDir = getSessionMessagingDir();
11
+ const socketPath = getSessionMessagingSocketPath();
12
+ const pidPath = join(messagingDir, "broker.pid");
13
+ const sessions = new Map();
14
+ const pendingSends = new Map();
15
+ let idleTimer;
16
+ let server;
17
+ function closeWithError(socket, message) {
18
+ try {
19
+ writeFrame(socket, { type: "error", message });
20
+ }
21
+ catch { }
22
+ socket.destroy();
23
+ }
24
+ function scheduleIdleExit() {
25
+ if (idleTimer)
26
+ clearTimeout(idleTimer);
27
+ idleTimer = setTimeout(() => {
28
+ if (sessions.size === 0)
29
+ shutdown();
30
+ }, EXIT_IDLE_MS);
31
+ }
32
+ function clearIdleExit() {
33
+ if (!idleTimer)
34
+ return;
35
+ clearTimeout(idleTimer);
36
+ idleTimer = undefined;
37
+ }
38
+ function unregister(sessionId) {
39
+ if (!sessionId)
40
+ return;
41
+ sessions.delete(sessionId);
42
+ failPendingSendsTo(sessionId);
43
+ if (sessions.size === 0)
44
+ scheduleIdleExit();
45
+ }
46
+ function resolveTarget(target) {
47
+ const socket = sessions.get(target);
48
+ if (socket)
49
+ return { sessionId: target, socket };
50
+ return { error: `No live session found for id: ${target}` };
51
+ }
52
+ function handleFrame(socket, message, getSessionId, setSessionId) {
53
+ const currentSessionId = getSessionId();
54
+ if (!currentSessionId && message.type !== "register") {
55
+ closeWithError(socket, `Received ${message.type} before register.`);
56
+ return;
57
+ }
58
+ switch (message.type) {
59
+ case "register": {
60
+ const existing = sessions.get(message.sessionId);
61
+ if (existing && existing !== socket && !existing.destroyed) {
62
+ writeFrame(socket, {
63
+ type: "register_failed",
64
+ reason: "Session messaging unavailable: this session is already registered by another live Pi process.",
65
+ });
66
+ socket.end();
67
+ return;
68
+ }
69
+ clearIdleExit();
70
+ setSessionId(message.sessionId);
71
+ sessions.set(message.sessionId, socket);
72
+ socket.setKeepAlive(true, KEEPALIVE_DELAY_MS);
73
+ writeFrame(socket, { type: "registered", sessionId: message.sessionId });
74
+ break;
75
+ }
76
+ case "unregister":
77
+ unregister(currentSessionId);
78
+ setSessionId(undefined);
79
+ socket.end();
80
+ break;
81
+ case "list":
82
+ writeFrame(socket, {
83
+ type: "sessions",
84
+ requestId: message.requestId,
85
+ sessionIds: [...sessions.keys()],
86
+ });
87
+ break;
88
+ case "send":
89
+ handleSend(socket, currentSessionId, message);
90
+ break;
91
+ case "incoming_ack":
92
+ handleIncomingAck(socket, message);
93
+ break;
94
+ }
95
+ }
96
+ function handleSend(socket, currentSessionId, message) {
97
+ const source = currentSessionId ? sessions.get(currentSessionId) : undefined;
98
+ if (!currentSessionId || !source) {
99
+ writeFrame(socket, {
100
+ type: "send_result",
101
+ requestId: message.requestId,
102
+ delivered: false,
103
+ error: "Sender session is not registered.",
104
+ });
105
+ return;
106
+ }
107
+ const resolved = resolveTarget(message.target);
108
+ if ("error" in resolved) {
109
+ writeFrame(socket, {
110
+ type: "send_result",
111
+ requestId: message.requestId,
112
+ delivered: false,
113
+ reason: "no_session",
114
+ error: resolved.error,
115
+ });
116
+ return;
117
+ }
118
+ if (resolved.sessionId === currentSessionId) {
119
+ writeFrame(socket, {
120
+ type: "send_result",
121
+ requestId: message.requestId,
122
+ delivered: false,
123
+ error: "Cannot send a session envelope to the current session.",
124
+ });
125
+ return;
126
+ }
127
+ const timeout = setTimeout(() => {
128
+ pendingSends.delete(message.requestId);
129
+ writeFrame(socket, {
130
+ type: "send_result",
131
+ requestId: message.requestId,
132
+ delivered: false,
133
+ error: "Timed out waiting for target session to accept the envelope.",
134
+ });
135
+ }, SEND_TIMEOUT_MS);
136
+ pendingSends.set(message.requestId, {
137
+ source: socket,
138
+ target: resolved.socket,
139
+ timeout,
140
+ targetSessionId: resolved.sessionId,
141
+ });
142
+ writeFrame(resolved.socket, {
143
+ type: "incoming",
144
+ requestId: message.requestId,
145
+ envelope: stampEnvelope(message.envelope, currentSessionId, resolved.sessionId),
146
+ });
147
+ }
148
+ function failPendingSendsTo(targetSessionId) {
149
+ for (const [requestId, pending] of pendingSends) {
150
+ if (pending.targetSessionId !== targetSessionId) {
151
+ continue;
152
+ }
153
+ clearTimeout(pending.timeout);
154
+ pendingSends.delete(requestId);
155
+ writeFrame(pending.source, {
156
+ type: "send_result",
157
+ requestId,
158
+ delivered: false,
159
+ reason: "disconnected",
160
+ error: "Target disconnected before accepting the envelope.",
161
+ });
162
+ }
163
+ }
164
+ function handleIncomingAck(socket, message) {
165
+ const pending = pendingSends.get(message.requestId);
166
+ if (!pending || pending.target !== socket)
167
+ return;
168
+ clearTimeout(pending.timeout);
169
+ pendingSends.delete(message.requestId);
170
+ writeFrame(pending.source, {
171
+ type: "send_result",
172
+ requestId: message.requestId,
173
+ delivered: message.delivered,
174
+ ...(message.error === undefined ? {} : { error: message.error }),
175
+ });
176
+ }
177
+ function stampEnvelope(envelope, source, target) {
178
+ return { ...envelope, source, target };
179
+ }
180
+ function handleConnection(socket) {
181
+ let sessionId;
182
+ socket.setKeepAlive(true, KEEPALIVE_DELAY_MS);
183
+ void readConnection(socket, () => sessionId, (nextSessionId) => {
184
+ sessionId = nextSessionId;
185
+ });
186
+ socket.on("close", () => {
187
+ unregister(sessionId);
188
+ });
189
+ socket.on("error", () => { });
190
+ }
191
+ async function readConnection(socket, getSessionId, setSessionId) {
192
+ try {
193
+ for await (const frame of readFrames(socket, parseClientFrame)) {
194
+ handleFrame(socket, frame, getSessionId, setSessionId);
195
+ }
196
+ }
197
+ catch (error) {
198
+ closeWithError(socket, error instanceof Error ? error.message : String(error));
199
+ }
200
+ }
201
+ function shutdown() {
202
+ for (const socket of sessions.values())
203
+ socket.end();
204
+ sessions.clear();
205
+ for (const pending of pendingSends.values())
206
+ clearTimeout(pending.timeout);
207
+ pendingSends.clear();
208
+ if (server)
209
+ server.close();
210
+ if (process.platform !== "win32") {
211
+ try {
212
+ unlinkSync(socketPath);
213
+ }
214
+ catch { }
215
+ }
216
+ try {
217
+ unlinkSync(pidPath);
218
+ }
219
+ catch { }
220
+ process.exit(0);
221
+ }
222
+ mkdirSync(messagingDir, { recursive: true, mode: 0o700 });
223
+ if (process.platform !== "win32") {
224
+ try {
225
+ unlinkSync(socketPath);
226
+ }
227
+ catch { }
228
+ }
229
+ server = createServer(handleConnection);
230
+ server.listen(socketPath, () => {
231
+ writeFileSync(pidPath, String(process.pid));
232
+ });
233
+ process.on("SIGTERM", shutdown);
234
+ process.on("SIGINT", shutdown);
@@ -0,0 +1,59 @@
1
+ import { createInterface } from "node:readline";
2
+ const MAX_FRAME_BYTES = 256 * 1024;
3
+ export function writeFrame(socket, frame) {
4
+ socket.write(`${JSON.stringify(frame)}\n`);
5
+ }
6
+ // The parser is injected so the detached broker can run with no dependencies
7
+ // installed alongside it, while Pi-side callers keep using TypeBox schemas.
8
+ export async function* readFrames(socket, parseFrame) {
9
+ const reader = createInterface({ input: socket, crlfDelay: Number.POSITIVE_INFINITY });
10
+ const lines = [];
11
+ const waiters = [];
12
+ let error;
13
+ let closed = false;
14
+ const wake = () => {
15
+ const waiter = waiters.shift();
16
+ waiter?.();
17
+ };
18
+ reader.on("line", (line) => {
19
+ lines.push(line);
20
+ wake();
21
+ });
22
+ reader.on("error", (nextError) => {
23
+ error = nextError instanceof Error ? nextError : new Error(String(nextError));
24
+ closed = true;
25
+ wake();
26
+ });
27
+ reader.on("close", () => {
28
+ closed = true;
29
+ wake();
30
+ });
31
+ try {
32
+ while (true) {
33
+ while (lines.length === 0 && !closed) {
34
+ await new Promise((resolve) => waiters.push(resolve));
35
+ }
36
+ const line = lines.shift();
37
+ if (line === undefined) {
38
+ if (error) {
39
+ throw error;
40
+ }
41
+ return;
42
+ }
43
+ if (Buffer.byteLength(line, "utf8") > MAX_FRAME_BYTES) {
44
+ throw new Error("Session messaging frame exceeds maximum size.");
45
+ }
46
+ let parsed;
47
+ try {
48
+ parsed = JSON.parse(line);
49
+ }
50
+ catch (parseError) {
51
+ throw parseError instanceof Error ? parseError : new Error(String(parseError));
52
+ }
53
+ yield parseFrame(parsed);
54
+ }
55
+ }
56
+ finally {
57
+ reader.close();
58
+ }
59
+ }
@@ -0,0 +1,19 @@
1
+ import { homedir } from "node:os";
2
+ import { join } from "node:path";
3
+ function sanitizePipeSegment(value) {
4
+ return (value
5
+ .replace(/[^a-zA-Z0-9]+/g, "-")
6
+ .replace(/^-+|-+$/g, "")
7
+ .toLowerCase() || "default");
8
+ }
9
+ export function getSessionMessagingDir(homeDir = homedir()) {
10
+ return (process.env.PI_SESSIONS_MESSAGING_DIR ??
11
+ join(homeDir, ".pi", "agent", "pi-sessions", "messaging"));
12
+ }
13
+ export function getSessionMessagingSocketPath(platform = process.platform, homeDir = homedir()) {
14
+ const messagingDir = getSessionMessagingDir(homeDir);
15
+ if (platform === "win32") {
16
+ return `\\\\.\\pipe\\pi-sessions-messaging-${sanitizePipeSegment(messagingDir)}`;
17
+ }
18
+ return join(messagingDir, "broker.sock");
19
+ }
@@ -9,6 +9,7 @@ import { installHandoff } from "./session-handoff/install.ts";
9
9
  import { installHooks } from "./session-hooks/install.ts";
10
10
  import { installIndex } from "./session-index/install.ts";
11
11
  import { installMessaging } from "./session-messaging/install.ts";
12
+ import { createSessionReachableTool } from "./session-messaging/pi/reachable-tool.ts";
12
13
  import {
13
14
  createSessionCancelTool,
14
15
  createSessionSendMessageTool,
@@ -67,6 +68,18 @@ export default function piSessions(pi: ExtensionAPI): void {
67
68
  }),
68
69
  );
69
70
  pi.registerTool(createSessionCancelTool(subagents ?? messaging));
71
+ pi.registerTool(
72
+ createSessionReachableTool({
73
+ indexPath: index.path,
74
+ listSessions: () => messaging.listSessions(),
75
+ getRelationTo: messaging.getCachedRelationTo,
76
+ ...(subagents
77
+ ? {
78
+ listSubagents: async (scope) => (await subagents.roster.resolve(scope)).entries,
79
+ }
80
+ : {}),
81
+ }),
82
+ );
70
83
  }
71
84
  if (settings.features.handoff) {
72
85
  const board = {
@@ -88,7 +101,7 @@ export default function piSessions(pi: ExtensionAPI): void {
88
101
  );
89
102
  }
90
103
  if (settings.features.search) {
91
- installSearch(pi, { settings, index, messaging, roster: subagents?.roster });
104
+ installSearch(pi, { settings, index });
92
105
  }
93
106
  if (settings.features.ask) {
94
107
  installAsk(pi, { settings, index, getModelRuntime });