pi-sessions 0.8.0 → 0.10.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 (111) hide show
  1. package/README.md +74 -37
  2. package/dist/session-messaging/broker/process.js +234 -0
  3. package/dist/shared/session-broker/framing.js +58 -0
  4. package/dist/shared/session-broker/protocol.js +163 -0
  5. package/dist/shared/session-broker/socket-path.js +19 -0
  6. package/dist/shared/typebox.js +36 -0
  7. package/extensions/pi-sessions.ts +125 -0
  8. package/extensions/session-ask/agent.ts +34 -14
  9. package/extensions/{session-ask.ts → session-ask/install.ts} +73 -90
  10. package/extensions/session-ask/navigate.ts +3 -1
  11. package/extensions/session-ask/presenter.ts +23 -0
  12. package/extensions/session-ask/renderer.ts +63 -0
  13. package/extensions/session-ask/tool-contract.ts +27 -0
  14. package/extensions/session-ask/view-model.ts +18 -0
  15. package/extensions/session-auto-title/generate.ts +18 -18
  16. package/extensions/{session-auto-title.ts → session-auto-title/install.ts} +60 -53
  17. package/extensions/session-auto-title/model.ts +25 -17
  18. package/extensions/session-auto-title/retitle.ts +17 -18
  19. package/extensions/session-auto-title/wizard.ts +12 -11
  20. package/extensions/session-handoff/board-view-model.ts +179 -0
  21. package/extensions/session-handoff/board.ts +663 -0
  22. package/extensions/session-handoff/bootstrap.ts +172 -0
  23. package/extensions/session-handoff/extract.ts +129 -132
  24. package/extensions/session-handoff/install.ts +225 -0
  25. package/extensions/session-handoff/kickoff.ts +92 -0
  26. package/extensions/session-handoff/launch/backend.ts +16 -0
  27. package/extensions/session-handoff/launch/deferred.ts +20 -0
  28. package/extensions/session-handoff/launch/ghostty.ts +99 -0
  29. package/extensions/session-handoff/launch/resolution.ts +57 -0
  30. package/extensions/session-handoff/launch/shell.ts +7 -0
  31. package/extensions/session-handoff/launch/tmux.ts +31 -0
  32. package/extensions/session-handoff/launch-options.ts +61 -0
  33. package/extensions/session-handoff/launch-target.ts +91 -0
  34. package/extensions/session-handoff/metadata.ts +194 -56
  35. package/extensions/session-handoff/model.ts +58 -0
  36. package/extensions/session-handoff/query.ts +0 -1
  37. package/extensions/session-handoff/receipt.ts +96 -0
  38. package/extensions/session-handoff/review.ts +2 -28
  39. package/extensions/session-handoff/spawn.ts +76 -181
  40. package/extensions/session-handoff/strong-modal.ts +13 -0
  41. package/extensions/session-handoff/tool-contract.ts +12 -0
  42. package/extensions/session-handoff/tool-presenter.ts +35 -0
  43. package/extensions/session-handoff/tool-renderer.ts +66 -0
  44. package/extensions/session-handoff/tool-schema.ts +54 -0
  45. package/extensions/session-handoff/tool-view-model.ts +49 -0
  46. package/extensions/session-handoff/tool.ts +206 -0
  47. package/extensions/session-handoff/ui.ts +66 -0
  48. package/extensions/session-hooks/install.ts +75 -0
  49. package/extensions/{session-index.ts → session-index/install.ts} +9 -7
  50. package/extensions/session-messaging/broker/process.ts +21 -28
  51. package/extensions/session-messaging/broker/spawn.ts +12 -7
  52. package/extensions/session-messaging/install.ts +78 -0
  53. package/extensions/session-messaging/pi/cancel-session-presenter.ts +64 -0
  54. package/extensions/session-messaging/pi/incoming-message-presenter.ts +32 -0
  55. package/extensions/session-messaging/pi/incoming-message-view-model.ts +20 -0
  56. package/extensions/session-messaging/pi/incoming-runtime.ts +27 -23
  57. package/extensions/session-messaging/pi/message-contracts.ts +32 -0
  58. package/extensions/session-messaging/pi/renderer.ts +19 -12
  59. package/extensions/session-messaging/pi/send-message-presenter.ts +39 -0
  60. package/extensions/session-messaging/pi/send-message-view-model.ts +54 -0
  61. package/extensions/session-messaging/pi/service.ts +262 -81
  62. package/extensions/session-messaging/pi/tools.ts +184 -40
  63. package/extensions/session-search/extract.ts +60 -14
  64. package/extensions/session-search/hooks.ts +10 -13
  65. package/extensions/session-search/install.ts +373 -0
  66. package/extensions/session-search/renderer.ts +195 -0
  67. package/extensions/session-search/tool-contract.ts +51 -0
  68. package/extensions/shared/composition.ts +20 -0
  69. package/extensions/shared/errors.ts +3 -0
  70. package/extensions/shared/model-resolution.ts +73 -0
  71. package/extensions/shared/model-runtime.ts +33 -0
  72. package/extensions/shared/model.ts +7 -10
  73. package/extensions/shared/rendering/collapsible-text.ts +89 -0
  74. package/extensions/shared/rendering/expandable-content-layout.ts +84 -0
  75. package/extensions/shared/rendering/theme.ts +5 -0
  76. package/extensions/shared/session-broker/client.ts +30 -29
  77. package/extensions/shared/session-broker/protocol.ts +128 -10
  78. package/extensions/shared/session-index/common.ts +5 -5
  79. package/extensions/shared/session-index/lineage.ts +0 -3
  80. package/extensions/shared/session-index/schema.ts +0 -1
  81. package/extensions/shared/session-index/scoring.ts +1 -2
  82. package/extensions/shared/session-index/search.ts +11 -4
  83. package/extensions/shared/session-index/store.ts +4 -9
  84. package/extensions/shared/settings.ts +83 -40
  85. package/extensions/shared/text.ts +5 -0
  86. package/extensions/shared/thinking-levels.ts +22 -0
  87. package/extensions/shared/tmux.ts +205 -0
  88. package/extensions/subagents/cancel.ts +111 -0
  89. package/extensions/subagents/classify.ts +58 -0
  90. package/extensions/subagents/install.ts +229 -0
  91. package/extensions/subagents/launch-target.ts +75 -0
  92. package/extensions/subagents/ledger.ts +217 -0
  93. package/extensions/subagents/reconcile.ts +386 -0
  94. package/extensions/subagents/report-message-presenter.ts +36 -0
  95. package/extensions/subagents/report-message-renderer.ts +21 -0
  96. package/extensions/subagents/report-message-view-model.ts +29 -0
  97. package/extensions/subagents/report.ts +159 -0
  98. package/extensions/subagents/roster.ts +262 -0
  99. package/extensions/subagents/settle.ts +182 -0
  100. package/extensions/subagents/wake.ts +173 -0
  101. package/images/handoff-board-subagents.png +0 -0
  102. package/images/handoff-board-user-sessions.png +0 -0
  103. package/images/handoff-subagent-report.png +0 -0
  104. package/images/session-handoff-tool.png +0 -0
  105. package/package.json +16 -18
  106. package/extensions/session-handoff.ts +0 -741
  107. package/extensions/session-hooks.ts +0 -75
  108. package/extensions/session-messaging/pi/message-view.ts +0 -131
  109. package/extensions/session-messaging.ts +0 -30
  110. package/extensions/session-search.ts +0 -419
  111. package/extensions/shared/session-broker/active.ts +0 -26
package/README.md CHANGED
@@ -8,7 +8,21 @@
8
8
 
9
9
  ![session picker](images/session_picker.png)
10
10
 
11
- ### Handoffs
11
+ ### Handoff board
12
+
13
+ ![handoff board showing subagents](images/handoff-board-subagents.png)
14
+
15
+ ![handoff board showing user sessions](images/handoff-board-user-sessions.png)
16
+
17
+ ### Subagent report
18
+
19
+ ![subagent report card in the parent session](images/handoff-subagent-report.png)
20
+
21
+ ### Session handoff
22
+
23
+ ![session handoff tool call](images/session-handoff-tool.png)
24
+
25
+ ### Handoff prompt review
12
26
 
13
27
  ![handoff preview](images/handoff.png)
14
28
 
@@ -20,7 +34,7 @@
20
34
 
21
35
  ## Install
22
36
 
23
- Requires Pi `0.80.2` or newer and Node `>=24 <26`.
37
+ Requires Pi `0.80.10` or newer and Node `>=24 <26`.
24
38
 
25
39
  **From npm** (recommended):
26
40
 
@@ -46,20 +60,36 @@ What session did I implement the db layer?
46
60
  ```
47
61
 
48
62
  ```text
49
- /handoff i want to implement the frontend component now
63
+ Open the frontend implementation task in a session to the right.
50
64
  ```
51
65
 
52
66
  ## Features
53
67
 
54
- | Extension | Surface | What it does |
55
- | ------------------ | ------------------------------------- | ------------------------------------------------------- |
56
- | Session Search | `session_search` pi tool | Search through old sessions |
57
- | Session Ask | `session_ask` pi tool | Ask questions about old sessions |
58
- | Session Handoff | `/handoff`, `session_handoff` pi tool | Start a focused new session; alternative to compaction |
59
- | Session Messaging | `session_send_message` pi tool | Send messages between running Pi sessions |
60
- | Session Picker | `Alt+O` | Reference old sessions in your prompt |
61
- | Session Index | `/session-index` slash command | Shows index status and rebuilds the local session index |
62
- | 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_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
+
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
+
80
+ ```json
81
+ {
82
+ "sessions": {
83
+ "messaging": { "enable": true },
84
+ "subagents": { "enable": true },
85
+ "handoff": { "enable": true },
86
+ "search": { "enable": true },
87
+ "ask": { "enable": true },
88
+ "autoTitle": { "enable": true },
89
+ "hooks": { "enable": true }
90
+ }
91
+ }
92
+ ```
63
93
 
64
94
  ## Session Search
65
95
 
@@ -72,40 +102,25 @@ File filters distinguish read-or-write evidence from write-only evidence:
72
102
  - `files.touched`: sessions that read or changed a path
73
103
  - `files.changed`: sessions that changed a path
74
104
 
75
- ## Session Handoff
76
-
77
- `/handoff <goal>` starts a focused new session. Give pi a goal, and it will generate a prompt for you to review before kicking it off.
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.
78
106
 
79
- You can either start a new session directly in your current one, or if you have Ghostty on macOS, you can spawn a new one in a split-pane and continue in your current:
80
-
81
- - `/handoff --left <goal>`
82
- - `/handoff --right <goal>`
83
- - `/handoff --up <goal>`
84
- - `/handoff --down <goal>`
85
-
86
- The flag indicates the Ghostty split direction.
87
-
88
- Flow:
89
-
90
- - run `/handoff [--<direction>] <goal>`
91
- - review the generated prompt preview
92
- - optionally edit the prompt
93
- - start the new session
107
+ ## Session Handoff
94
108
 
95
- If you do nothing, the preview autostarts after a short countdown.
109
+ The `session_handoff` tool lets the agent create a child session with a self-contained task. Ask for a direction when you want a visible split, ask for a deferred handoff when you only want the prepared session, or let the agent delegate suitable independent work to a background subagent.
96
110
 
97
- When running in Ghostty on macOS, pi-sessions also exposes a `session_handoff` tool. This lets the agent start a background handoff after you choose a split direction. The current session keeps running, while the child session opens in the requested split, gathers context, and shows the same review countdown before starting.
111
+ Directional launches use tmux when the current terminal is inside tmux, or Ghostty on macOS. Deferred launches create the child without starting it and copy its resume command to the clipboard. A background subagent runs in a detached tmux window and reports back when finished.
98
112
 
99
- If background handoffs ever target the wrong pane, run `/handoff --identify` from the intended source pane to refresh the in-memory Ghostty terminal binding.
113
+ Run `/handoff` to open the **Handoffs** board. The Subagents and User sessions tabs show status, age, launch details, and the actions currently available: stop, copy an observation command, or copy a resume command.
100
114
 
101
115
  ## Session Messaging
102
116
 
103
- Agents can coordinate with other currently running Pi sessions:
117
+ Agents can coordinate with live Pi sessions and their own subagents:
104
118
 
105
119
  - `session_search` with `live: true` lists live sessions
106
- - `session_send_message` sends a message to another live session
120
+ - `session_send_message` sends a message to a live session or own subagent
121
+ - `session_cancel` aborts another live session's current turn
107
122
 
108
- Incoming messages start the recipient agent when idle and steer it when already running. Inactive sessions cannot receive messages, but you can still use `session_search` and `session_ask` with them.
123
+ Incoming messages start the recipient agent when idle and steer it when already running. Messaging a dormant owned subagent resumes it automatically; other inactive sessions cannot receive messages, but you can still use `session_search` and `session_ask` with them.
109
124
 
110
125
  ## Session picker
111
126
 
@@ -124,7 +139,28 @@ If you want to override the shortcut, put this in your `~/.pi/agent/settings.jso
124
139
  {
125
140
  "sessions": {
126
141
  "handoff": {
127
- "pickerShortcut": "alt+p"
142
+ "pickerShortcut": "alt+p",
143
+ "model": "openai-codex/gpt-5.6-terra",
144
+ "thinkingLevel": "low",
145
+ "deferred": {
146
+ "copyToClipboard": true
147
+ }
148
+ }
149
+ }
150
+ }
151
+ ```
152
+
153
+ `model` and `thinkingLevel` configure the agent that builds handoff prompts. They default to the new session's values when absent.
154
+
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.
156
+
157
+ Subagents require the handoff and messaging features. Limit recursive delegation depth with `sessions.subagents.maxDepth` (default `2`):
158
+
159
+ ```json
160
+ {
161
+ "sessions": {
162
+ "subagents": {
163
+ "maxDepth": 2
128
164
  }
129
165
  }
130
166
  }
@@ -192,6 +228,7 @@ To change auto-titling settings, edit `~/.pi/agent/settings.json`:
192
228
  "refreshTurns": 4,
193
229
  "timeoutSecs": 15,
194
230
  "model": "anthropic/claude-haiku-4-5",
231
+ "thinkingLevel": "off",
195
232
  "prompt": "Custom prompt that overrides the default."
196
233
  }
197
234
  }
@@ -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 { CLIENT_FRAME_SCHEMA, } from "../../shared/session-broker/protocol.js";
6
+ import { getSessionMessagingDir, getSessionMessagingSocketPath, } from "../../shared/session-broker/socket-path.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, CLIENT_FRAME_SCHEMA, "Invalid session messaging client frame")) {
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,58 @@
1
+ import { createInterface } from "node:readline";
2
+ import { parseTypeBoxValue } from "../typebox.js";
3
+ const MAX_FRAME_BYTES = 256 * 1024;
4
+ export function writeFrame(socket, frame) {
5
+ socket.write(`${JSON.stringify(frame)}\n`);
6
+ }
7
+ export async function* readFrames(socket, schema, context) {
8
+ const reader = createInterface({ input: socket, crlfDelay: Number.POSITIVE_INFINITY });
9
+ const lines = [];
10
+ const waiters = [];
11
+ let error;
12
+ let closed = false;
13
+ const wake = () => {
14
+ const waiter = waiters.shift();
15
+ waiter?.();
16
+ };
17
+ reader.on("line", (line) => {
18
+ lines.push(line);
19
+ wake();
20
+ });
21
+ reader.on("error", (nextError) => {
22
+ error = nextError instanceof Error ? nextError : new Error(String(nextError));
23
+ closed = true;
24
+ wake();
25
+ });
26
+ reader.on("close", () => {
27
+ closed = true;
28
+ wake();
29
+ });
30
+ try {
31
+ while (true) {
32
+ while (lines.length === 0 && !closed) {
33
+ await new Promise((resolve) => waiters.push(resolve));
34
+ }
35
+ const line = lines.shift();
36
+ if (line === undefined) {
37
+ if (error) {
38
+ throw error;
39
+ }
40
+ return;
41
+ }
42
+ if (Buffer.byteLength(line, "utf8") > MAX_FRAME_BYTES) {
43
+ throw new Error("Session messaging frame exceeds maximum size.");
44
+ }
45
+ let parsed;
46
+ try {
47
+ parsed = JSON.parse(line);
48
+ }
49
+ catch (parseError) {
50
+ throw parseError instanceof Error ? parseError : new Error(String(parseError));
51
+ }
52
+ yield parseTypeBoxValue(schema, parsed, context);
53
+ }
54
+ }
55
+ finally {
56
+ reader.close();
57
+ }
58
+ }
@@ -0,0 +1,163 @@
1
+ import { Type } from "typebox";
2
+ export const TASK_REPORT_REFERENCE_SCHEMA = Type.Object({
3
+ reference: Type.String({
4
+ description: "A precise reference such as a file path and line range, symbol name, command, URL, session id, or external artifact.",
5
+ }),
6
+ description: Type.Optional(Type.String({
7
+ description: "Why this reference matters or what evidence the parent should take from it.",
8
+ })),
9
+ });
10
+ export const TASK_REPORT_SCHEMA = Type.Object({
11
+ status: Type.Union([
12
+ Type.Literal("done", {
13
+ description: "The delegated task or requested follow-up is complete.",
14
+ }),
15
+ Type.Literal("blocked", {
16
+ description: "The task cannot continue without a specific decision, clarification, permission, dependency, or other action outside the scope of the task.",
17
+ }),
18
+ Type.Literal("incomplete", {
19
+ description: "Useful work was performed, but the requested result could not be completed and it's uncertain what should be done to reach completion.",
20
+ }),
21
+ ], { description: "The subagent's assessment of the task at the end of this turn." }),
22
+ summary: Type.String({
23
+ description: "A concise, 3-4 sentence self-contained account of the outcome and its most important evidence.",
24
+ }),
25
+ details: Type.Optional(Type.String({
26
+ description: "Optional supporting context such as reasoning, implementation notes, validation performed, limitations, unexpected behavior or roadblocks, or failed approaches.",
27
+ })),
28
+ references: Type.Optional(Type.Array(TASK_REPORT_REFERENCE_SCHEMA, {
29
+ description: "Supporting material for the report.",
30
+ })),
31
+ nextSteps: Type.Optional(Type.Array(Type.String(), {
32
+ description: "Optional concrete actions that would extend or build on the state of the work. If blocked or incomplete, call out specifically what is causing the early return.",
33
+ })),
34
+ });
35
+ const OUTBOUND_MESSAGE_ENVELOPE_SCHEMA = Type.Object({
36
+ kind: Type.Literal("message"),
37
+ messageId: Type.String(),
38
+ body: Type.String(),
39
+ requestResponse: Type.Optional(Type.Boolean()),
40
+ sentAt: Type.String(),
41
+ sourceToolCallId: Type.Optional(Type.String()),
42
+ });
43
+ const OUTBOUND_CANCEL_ENVELOPE_SCHEMA = Type.Object({
44
+ kind: Type.Literal("cancel"),
45
+ cancelId: Type.String(),
46
+ sentAt: Type.String(),
47
+ });
48
+ const OUTBOUND_SUBAGENT_REPORT_ENVELOPE_SCHEMA = Type.Intersect([
49
+ Type.Object({
50
+ kind: Type.Literal("subagent_report"),
51
+ reportId: Type.String(),
52
+ sentAt: Type.String(),
53
+ }),
54
+ TASK_REPORT_SCHEMA,
55
+ ]);
56
+ export const OUTBOUND_SESSION_ENVELOPE_SCHEMA = Type.Union([
57
+ OUTBOUND_MESSAGE_ENVELOPE_SCHEMA,
58
+ OUTBOUND_CANCEL_ENVELOPE_SCHEMA,
59
+ OUTBOUND_SUBAGENT_REPORT_ENVELOPE_SCHEMA,
60
+ ]);
61
+ export const SESSION_MESSAGE_ENVELOPE_SCHEMA = Type.Object({
62
+ kind: Type.Literal("message"),
63
+ messageId: Type.String(),
64
+ source: Type.String(),
65
+ target: Type.String(),
66
+ body: Type.String(),
67
+ requestResponse: Type.Optional(Type.Boolean()),
68
+ sentAt: Type.String(),
69
+ sourceToolCallId: Type.Optional(Type.String()),
70
+ });
71
+ export const SESSION_CANCEL_ENVELOPE_SCHEMA = Type.Object({
72
+ kind: Type.Literal("cancel"),
73
+ cancelId: Type.String(),
74
+ source: Type.String(),
75
+ target: Type.String(),
76
+ sentAt: Type.String(),
77
+ });
78
+ export const SESSION_SUBAGENT_REPORT_ENVELOPE_SCHEMA = Type.Intersect([
79
+ Type.Object({
80
+ kind: Type.Literal("subagent_report"),
81
+ reportId: Type.String(),
82
+ source: Type.String(),
83
+ target: Type.String(),
84
+ sentAt: Type.String(),
85
+ }),
86
+ TASK_REPORT_SCHEMA,
87
+ ]);
88
+ export const SESSION_ENVELOPE_SCHEMA = Type.Union([
89
+ SESSION_MESSAGE_ENVELOPE_SCHEMA,
90
+ SESSION_CANCEL_ENVELOPE_SCHEMA,
91
+ SESSION_SUBAGENT_REPORT_ENVELOPE_SCHEMA,
92
+ ]);
93
+ const REGISTER_CLIENT_FRAME_SCHEMA = Type.Object({
94
+ type: Type.Literal("register"),
95
+ sessionId: Type.String(),
96
+ });
97
+ const UNREGISTER_CLIENT_FRAME_SCHEMA = Type.Object({
98
+ type: Type.Literal("unregister"),
99
+ });
100
+ const LIST_CLIENT_FRAME_SCHEMA = Type.Object({
101
+ type: Type.Literal("list"),
102
+ requestId: Type.String(),
103
+ });
104
+ const SEND_CLIENT_FRAME_SCHEMA = Type.Object({
105
+ type: Type.Literal("send"),
106
+ requestId: Type.String(),
107
+ target: Type.String(),
108
+ envelope: OUTBOUND_SESSION_ENVELOPE_SCHEMA,
109
+ });
110
+ const INCOMING_ACK_CLIENT_FRAME_SCHEMA = Type.Object({
111
+ type: Type.Literal("incoming_ack"),
112
+ requestId: Type.String(),
113
+ delivered: Type.Boolean(),
114
+ error: Type.Optional(Type.String()),
115
+ });
116
+ const REGISTERED_BROKER_FRAME_SCHEMA = Type.Object({
117
+ type: Type.Literal("registered"),
118
+ sessionId: Type.String(),
119
+ });
120
+ const REGISTER_FAILED_BROKER_FRAME_SCHEMA = Type.Object({
121
+ type: Type.Literal("register_failed"),
122
+ reason: Type.String(),
123
+ });
124
+ const SESSIONS_BROKER_FRAME_SCHEMA = Type.Object({
125
+ type: Type.Literal("sessions"),
126
+ requestId: Type.String(),
127
+ sessionIds: Type.Array(Type.String()),
128
+ });
129
+ const INCOMING_BROKER_FRAME_SCHEMA = Type.Object({
130
+ type: Type.Literal("incoming"),
131
+ requestId: Type.String(),
132
+ envelope: SESSION_ENVELOPE_SCHEMA,
133
+ });
134
+ export const SESSION_ENVELOPE_SEND_FAILURE_REASON_SCHEMA = Type.Union([
135
+ Type.Literal("no_session"),
136
+ Type.Literal("disconnected"),
137
+ ]);
138
+ const SEND_RESULT_BROKER_FRAME_SCHEMA = Type.Object({
139
+ type: Type.Literal("send_result"),
140
+ requestId: Type.String(),
141
+ delivered: Type.Boolean(),
142
+ reason: Type.Optional(SESSION_ENVELOPE_SEND_FAILURE_REASON_SCHEMA),
143
+ error: Type.Optional(Type.String()),
144
+ });
145
+ const ERROR_BROKER_FRAME_SCHEMA = Type.Object({
146
+ type: Type.Literal("error"),
147
+ message: Type.String(),
148
+ });
149
+ export const CLIENT_FRAME_SCHEMA = Type.Union([
150
+ REGISTER_CLIENT_FRAME_SCHEMA,
151
+ UNREGISTER_CLIENT_FRAME_SCHEMA,
152
+ LIST_CLIENT_FRAME_SCHEMA,
153
+ SEND_CLIENT_FRAME_SCHEMA,
154
+ INCOMING_ACK_CLIENT_FRAME_SCHEMA,
155
+ ]);
156
+ export const BROKER_FRAME_SCHEMA = Type.Union([
157
+ REGISTERED_BROKER_FRAME_SCHEMA,
158
+ REGISTER_FAILED_BROKER_FRAME_SCHEMA,
159
+ SESSIONS_BROKER_FRAME_SCHEMA,
160
+ INCOMING_BROKER_FRAME_SCHEMA,
161
+ SEND_RESULT_BROKER_FRAME_SCHEMA,
162
+ ERROR_BROKER_FRAME_SCHEMA,
163
+ ]);
@@ -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
+ }
@@ -0,0 +1,36 @@
1
+ import { Value } from "typebox/value";
2
+ export function isTypeBoxValue(schema, value) {
3
+ return Value.Check(schema, value);
4
+ }
5
+ export function parseTypeBoxValue(schema, value, context) {
6
+ if (Value.Check(schema, value)) {
7
+ return value;
8
+ }
9
+ throw new Error(formatTypeBoxError(schema, value, context));
10
+ }
11
+ export function safeParseTypeBoxValue(schema, value) {
12
+ return Value.Check(schema, value) ? value : undefined;
13
+ }
14
+ export function parseTypeBoxRows(schema, value, context) {
15
+ if (!Array.isArray(value)) {
16
+ throw new Error(`${context}: expected an array of rows.`);
17
+ }
18
+ return value.map((row, index) => parseTypeBoxValue(schema, row, `${context} at row ${index + 1}`));
19
+ }
20
+ export function safeParseTypeBoxJson(schema, raw) {
21
+ try {
22
+ const parsed = JSON.parse(raw);
23
+ return safeParseTypeBoxValue(schema, parsed);
24
+ }
25
+ catch {
26
+ return undefined;
27
+ }
28
+ }
29
+ function formatTypeBoxError(schema, value, context) {
30
+ const firstError = Value.Errors(schema, value)[0];
31
+ if (!firstError) {
32
+ return `${context}: invalid value.`;
33
+ }
34
+ const path = firstError.instancePath.length > 0 ? firstError.instancePath : "/";
35
+ return `${context}: ${path} ${firstError.message}`;
36
+ }