pi-session-mail 0.6.0 → 0.6.2

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.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,23 @@ follows [Semantic Versioning](https://semver.org/).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.6.2] - 2026-11-01
10
+
11
+ ### Changed
12
+
13
+ - Shorter declarations for the four session-mail tools reduce model context use.
14
+ Delivery, ask waits, reply rules, result fields, and errors are unchanged.
15
+
16
+ ## [0.6.1] - 2026-10-31
17
+
18
+ ### Changed
19
+
20
+ - The label on the answer to an ask now calls it an ask: "It is a reply to your
21
+ ask …" and "[mailbox] Your ask …, quoted from …" (and "Ask cut at 2 KiB").
22
+ It said "request" before, which now names a different kind of mail. A reply
23
+ to a request, or to an id with no copy in `sent/`, keeps the request wording.
24
+ The test that pinned the old wording for an ask's answer is updated.
25
+
9
26
  ## [0.6.0] - 2026-10-31
10
27
 
11
28
  ### Added
@@ -240,7 +257,9 @@ author's dotfiles; the changes below are against that copy.
240
257
  - CI on Node 22.19 and 24: lint, typecheck, tests, and a check of the
241
258
  published file list.
242
259
 
243
- [Unreleased]: https://github.com/gvanderclay/pi-session-mail/compare/v0.6.0...HEAD
260
+ [Unreleased]: https://github.com/gvanderclay/pi-session-mail/compare/v0.6.2...HEAD
261
+ [0.6.2]: https://github.com/gvanderclay/pi-session-mail/compare/v0.6.1...v0.6.2
262
+ [0.6.1]: https://github.com/gvanderclay/pi-session-mail/compare/v0.6.0...v0.6.1
244
263
  [0.6.0]: https://github.com/gvanderclay/pi-session-mail/compare/v0.5.0...v0.6.0
245
264
  [0.5.0]: https://github.com/gvanderclay/pi-session-mail/compare/v0.4.0...v0.5.0
246
265
  [0.4.0]: https://github.com/gvanderclay/pi-session-mail/compare/v0.3.0...v0.4.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-session-mail",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "A file mailbox between Pi sessions on the same machine",
5
5
  "keywords": [
6
6
  "pi-package",
package/src/index.ts CHANGED
@@ -75,14 +75,15 @@ function cap(text: string, max: number): string | undefined {
75
75
  */
76
76
  const plain = (text: string): string => text.replace(/[\p{Cc}\u2028\u2029]/gu, " ");
77
77
 
78
- /** A reply quotes each request it answers: the id, the copy's path, then the capped body. */
78
+ /** A reply quotes each request or ask it answers: the id, the copy's path, then the capped body. */
79
79
  function quoteRequest(request: SentCopy): string {
80
+ const ask = request.envelope.kind === "ask";
80
81
  const cut = cap(request.envelope.body, QUOTE_CAP);
81
82
  const body =
82
83
  cut === undefined
83
84
  ? request.envelope.body
84
- : `${cut}\n[mailbox] Request cut at 2 KiB; the full copy is ${plain(request.path)}`;
85
- return `[mailbox] Your request ${plain(request.envelope.id)}, quoted from ${plain(request.path)}:\n${body
85
+ : `${cut}\n[mailbox] ${ask ? "Ask" : "Request"} cut at 2 KiB; the full copy is ${plain(request.path)}`;
86
+ return `[mailbox] Your ${ask ? "ask" : "request"} ${plain(request.envelope.id)}, quoted from ${plain(request.path)}:\n${body
86
87
  .split("\n")
87
88
  .map((line) => `> ${line}`)
88
89
  .join("\n")}`;
@@ -136,10 +137,10 @@ function inboundText(
136
137
  envelope: Envelope,
137
138
  path: string,
138
139
  quotes: readonly string[],
139
- opts: { lateAnswer: boolean; byUser: boolean },
140
+ opts: { lateAnswer: boolean; byUser: boolean; answersAsks: boolean },
140
141
  senderRecord: RunningRecord | undefined,
141
142
  ): string {
142
- const { lateAnswer, byUser } = opts;
143
+ const { lateAnswer, byUser, answersAsks } = opts;
143
144
  const sender = senderLine(envelope.from, senderRecord);
144
145
  const id = plain(envelope.id);
145
146
  const replyIds = envelope.in_reply_to.map(plain);
@@ -152,7 +153,9 @@ function inboundText(
152
153
  if (envelope.kind === "message")
153
154
  header.push(`It expects no answer; if one is wanted, send it with session_mail_send to ${envelope.from}.`);
154
155
  if (envelope.in_reply_to.length > 0)
155
- header.push(`It is a reply to your request${envelope.in_reply_to.length > 1 ? "s" : ""} ${replyIds.join(", ")}.`);
156
+ header.push(
157
+ `It is a reply to your ${answersAsks ? "ask" : "request"}${envelope.in_reply_to.length > 1 ? "s" : ""} ${replyIds.join(", ")}.`,
158
+ );
156
159
  if (byUser)
157
160
  header.push("The user made that request, typing it with /mailbox or through an extension such as delegate.");
158
161
  if (envelope.in_reply_to.length > 0 && envelope.status !== "done")
@@ -395,6 +398,9 @@ export default function mailbox(pi: ExtensionAPI) {
395
398
  const lateAnswer = envelope.kind === "reply" && requests.some((copy) => copy.envelope.kind === "ask");
396
399
  const wakes = envelope.kind !== "reply" || lateAnswer;
397
400
  const byUser = envelope.kind === "reply" && requests.some((copy) => copy.envelope.kind === "request");
401
+ // The label says "ask" only when every id it answers is a known ask; otherwise "request", as before.
402
+ const answersAsks =
403
+ requests.length === envelope.in_reply_to.length && requests.every((copy) => copy.envelope.kind === "ask");
398
404
  // Listeners run synchronously inside emit, so `handled` is final when it returns.
399
405
  const inbound: InboundPayload = { envelope, path, requests, handled: false };
400
406
  pi.events.emit(INBOUND, inbound);
@@ -415,7 +421,7 @@ export default function mailbox(pi: ExtensionAPI) {
415
421
  envelope,
416
422
  path,
417
423
  quotes,
418
- { lateAnswer, byUser },
424
+ { lateAnswer, byUser, answersAsks },
419
425
  listRunning().find((r) => r.address === envelope.from),
420
426
  ),
421
427
  display: true,
package/src/tools.ts CHANGED
@@ -126,49 +126,54 @@ const SENDS_MAIL = { readOnlyHint: false, destructiveHint: false, idempotentHint
126
126
  const SessionList = Type.Object({
127
127
  sessions: Type.Array(
128
128
  Type.Object({
129
- id: Type.String({ description: "The session's full id; pass it as `to`" }),
130
- name: Type.Optional(Type.String({ description: "The session name; absent when none is set" })),
129
+ id: Type.String({ description: "Full session id; usable as `to`" }),
130
+ name: Type.Optional(Type.String()),
131
131
  cwd: Type.String(),
132
132
  state: Type.Union([Type.Literal("idle"), Type.Literal("busy")]),
133
133
  waitingOn: Type.Optional(
134
- Type.String({ description: "The full id of the session this one's ask waits on; absent when none" }),
134
+ Type.String({ description: "Full session id this session's ask waits on; absent if none" }),
135
135
  ),
136
- self: Type.Boolean({ description: "True for the calling session" }),
136
+ self: Type.Boolean({ description: "True for the caller" }),
137
137
  }),
138
138
  ),
139
139
  });
140
140
 
141
141
  const SendResult = Type.Object({
142
- id: Type.String({ description: "The message's id" }),
143
- to: Type.String({ description: "The recipient's full id" }),
144
- running: Type.Boolean({ description: "False when the message waits in a closed session's inbox" }),
142
+ id: Type.String({ description: "Message id" }),
143
+ to: Type.String({ description: "Recipient's full session id" }),
144
+ running: Type.Boolean({ description: "False if queued in a closed session's inbox" }),
145
145
  });
146
146
 
147
147
  const asked = {
148
- id: Type.String({ description: "The ask's id" }),
149
- to: Type.String({ description: "The asked session's full id" }),
148
+ id: Type.String({ description: "Ask id" }),
149
+ to: Type.String({ description: "Asked session's full id" }),
150
150
  };
151
151
 
152
- /** One variant per {@link AskOutcome}, told apart by `outcome`. */
153
- const AskResult = Type.Union([
154
- Type.Object({
155
- ...asked,
156
- outcome: Type.Literal("answered"),
157
- status: Type.String({
158
- description: '"done" for an answer; any other status means the run ended otherwise, and body is not an answer',
152
+ const AskResult = Type.Intersect([
153
+ Type.Object(asked),
154
+ Type.Union([
155
+ Type.Object({
156
+ outcome: Type.Literal("answered"),
157
+ status: Type.String({
158
+ description: '"done" means an answer; otherwise the run ended without an answer and body is not one',
159
+ }),
160
+ body: Type.String(),
161
+ }),
162
+ Type.Object({
163
+ outcome: Type.Union([
164
+ Type.Literal("timed-out"),
165
+ Type.Literal("stopped-running"),
166
+ Type.Literal("stopped-by-user"),
167
+ Type.Literal("background"),
168
+ ]),
159
169
  }),
160
- body: Type.String(),
161
- }),
162
- Type.Object({ ...asked, outcome: Type.Literal("timed-out") }),
163
- Type.Object({ ...asked, outcome: Type.Literal("stopped-running") }),
164
- Type.Object({ ...asked, outcome: Type.Literal("stopped-by-user") }),
165
- Type.Object({ ...asked, outcome: Type.Literal("background") }),
170
+ ]),
166
171
  ]);
167
172
 
168
173
  const ReplyResult = Type.Object({
169
- id: Type.String({ description: "The reply's id" }),
170
- ask: Type.String({ description: "The id of the ask it answers" }),
171
- to: Type.String({ description: "The asker's full id" }),
174
+ id: Type.String({ description: "Reply id" }),
175
+ ask: Type.String({ description: "Answered ask id" }),
176
+ to: Type.String({ description: "Asker's full session id" }),
172
177
  });
173
178
 
174
179
  /** One running session as the list shows it. */
@@ -215,7 +220,7 @@ export function registerTools(pi: ExtensionAPI, hooks: ToolHooks): Tools {
215
220
  name: "session_mail_list",
216
221
  label: "Session mail: list",
217
222
  description:
218
- "List the Pi sessions running on this machine, in every agent root: each one's name (or short id when it has none), full id, working directory, idle or busy state, and whom it is waiting on. The calling session is marked. A session is addressed by its name, its full id, or an id prefix of at least 8 characters.",
223
+ "List running Pi sessions across all agent roots on this machine, with name (short id if unnamed), full id, cwd, idle/busy state and whom each waits on. Marks the caller. Address running sessions by name, full id or 8+ character id prefix.",
219
224
  parameters: Type.Object({}),
220
225
  outputSchema: SessionList,
221
226
  // Listing deletes the records of sessions whose process is gone, which is housekeeping, not a change.
@@ -249,13 +254,13 @@ export function registerTools(pi: ExtensionAPI, hooks: ToolHooks): Tools {
249
254
  name: "session_mail_send",
250
255
  label: "Session mail: send",
251
256
  description:
252
- "Send a message to another Pi session on this machine. A running recipient that is idle starts a turn on it; a busy one reads it at its next gap between tool calls. The message expects no answer and nothing is sent back automatically; the recipient answers, if at all, with its own session_mail_send. `to` is a running session's name, an id prefix of at least 8 characters, or a full session id; a closed session is reached only by its full id, and the message waits in its inbox until it starts. session_mail_list shows who is running.",
257
+ "Send mail to a Pi session on this machine. An idle recipient starts a turn; a busy one reads it at the next tool-call gap. No answer is expected or automatic; any answer uses session_mail_send. Address running sessions by name, full id or 8+ character id prefix. Closed sessions require a full id; mail queues until they start. Discover running sessions with session_mail_list.",
253
258
  parameters: Type.Object({
254
259
  to: Type.String({
255
- description: "The recipient: a running session's name, an id prefix of 8+ characters, or a full session id.",
260
+ description: "Recipient's name or 8+ character id prefix if running; full session id otherwise.",
256
261
  }),
257
262
  message: Type.String({
258
- description: "The message text. The recipient sees it labelled as coming from this session.",
263
+ description: "Message text, labelled as from this session.",
259
264
  }),
260
265
  }),
261
266
  outputSchema: SendResult,
@@ -286,13 +291,13 @@ export function registerTools(pi: ExtensionAPI, hooks: ToolHooks): Tools {
286
291
  name: "session_mail_ask",
287
292
  label: "Session mail: ask",
288
293
  description:
289
- "Ask another running Pi session on this machine a question. The answer is the other session's final message when its run settles, or an earlier reply it sends. In a session a person is watching, the ask returns at once and the answer arrives later as a message that starts a turn. In an unattended session, such as a delegate, the ask waits up to 10 minutes and the answer comes back as this tool's result. `to` is a running session's name, an id prefix of at least 8 characters, or a full session id; session_mail_list shows who is running. For a note that needs no answer, use session_mail_send.",
294
+ "Ask a running Pi session on this machine. Its final message at run settle or an early reply answers. In a watched TUI, returns at once; the later answer starts a turn. Unattended sessions, including delegates, wait up to 10 minutes for the answer as this tool's result. Address by name, full id or 8+ character id prefix; discover with session_mail_list. Use session_mail_send for notes needing no answer.",
290
295
  parameters: Type.Object({
291
296
  to: Type.String({
292
- description: "The running session to ask: its name, an id prefix of 8+ characters, or its full session id.",
297
+ description: "Running recipient's name, full id or 8+ character id prefix.",
293
298
  }),
294
299
  message: Type.String({
295
- description: "The question. The recipient sees it labelled as coming from this session.",
300
+ description: "Question, labelled as from this session.",
296
301
  }),
297
302
  }),
298
303
  outputSchema: AskResult,
@@ -354,10 +359,10 @@ export function registerTools(pi: ExtensionAPI, hooks: ToolHooks): Tools {
354
359
  name: "session_mail_reply",
355
360
  label: "Session mail: reply",
356
361
  description:
357
- "Answer an ask another Pi session sent this session, at once, without ending this run. The asker is waiting, and gets the message as the answer to its ask; the answer this run's last message would otherwise send is then skipped for that ask. `ask` is the ask id its label gives. Each ask is answered once; requests are answered by the extension that consumes them, and messages expect no answer.",
362
+ "Answer a received ask at once without ending this run. Each ask allows one reply, replacing this run's final-settle answer for it. Use the ask id from its label. Only asks qualify; requests are answered by their consuming extension, and messages expect no answer.",
358
363
  parameters: Type.Object({
359
- ask: Type.String({ description: "The ask id, as given in the ask's label." }),
360
- message: Type.String({ description: "The answer." }),
364
+ ask: Type.String({ description: "Ask id from its label." }),
365
+ message: Type.String({ description: "Answer text." }),
361
366
  }),
362
367
  outputSchema: ReplyResult,
363
368
  // A repeated reply to the same ask is refused and sends nothing.