pi-session-mail 0.4.0 → 0.5.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.
- package/CHANGELOG.md +14 -1
- package/README.md +7 -4
- package/package.json +1 -1
- package/src/index.ts +2 -2
- package/src/tools.ts +39 -26
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,18 @@ follows [Semantic Versioning](https://semver.org/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.5.0] - 2026-10-23
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- `session_mail_ask` no longer holds the turn of a session a person is
|
|
14
|
+
watching (a TUI without `PI_UNATTENDED=1`): it returns at once with outcome
|
|
15
|
+
`background`, and the answer arrives as a message that starts a turn, so the
|
|
16
|
+
user can keep talking to the asker. Unattended sessions, such as delegates,
|
|
17
|
+
still wait for the answer. The label on an answer to an ask that is not
|
|
18
|
+
waiting now reads "is not waiting for it" instead of "has stopped waiting",
|
|
19
|
+
and the test pinning it changed with it.
|
|
20
|
+
|
|
9
21
|
## [0.4.0] - 2026-10-20
|
|
10
22
|
|
|
11
23
|
### Added
|
|
@@ -176,7 +188,8 @@ author's dotfiles; the changes below are against that copy.
|
|
|
176
188
|
- CI on Node 22.19 and 24: lint, typecheck, tests, and a check of the
|
|
177
189
|
published file list.
|
|
178
190
|
|
|
179
|
-
[Unreleased]: https://github.com/gvanderclay/pi-session-mail/compare/v0.
|
|
191
|
+
[Unreleased]: https://github.com/gvanderclay/pi-session-mail/compare/v0.5.0...HEAD
|
|
192
|
+
[0.5.0]: https://github.com/gvanderclay/pi-session-mail/compare/v0.4.0...v0.5.0
|
|
180
193
|
[0.4.0]: https://github.com/gvanderclay/pi-session-mail/compare/v0.3.0...v0.4.0
|
|
181
194
|
[0.3.0]: https://github.com/gvanderclay/pi-session-mail/compare/v0.2.2...v0.3.0
|
|
182
195
|
[0.2.2]: https://github.com/gvanderclay/pi-session-mail/compare/v0.2.1...v0.2.2
|
package/README.md
CHANGED
|
@@ -131,7 +131,7 @@ The model gets four tools:
|
|
|
131
131
|
| Tool | Parameters | What it does |
|
|
132
132
|
| --- | --- | --- |
|
|
133
133
|
| `session_mail_list` | none | Lists every running session, in every agent directory: its name (or short id, the first 8 characters of its id, when it has no name), full id, working directory, idle or busy state and whom it is waiting on, and marks the calling session. |
|
|
134
|
-
| `session_mail_ask` | `to`, `message` | Writes an ask (`kind: "ask"`) and waits for the answer, which is the result: its status and body, labelled as not an answer when the status is `stopped` or `failed`. The wait ends when the answer arrives, after 10 minutes ("no answer yet"), when the target's running record disappears ("stopped running"), or when the user stops the run; `waitingOn` is set while it waits and cleared in every case. Refused at once, writing nothing, when another ask of this session is waiting, when the target is not running, when the target is waiting on this session, at the [hop limit](#hop-limit), and when `to` does not resolve, is ambiguous or is this session. |
|
|
134
|
+
| `session_mail_ask` | `to`, `message` | Writes an ask (`kind: "ask"`). In a TUI session without `PI_UNATTENDED=1` it returns at once with outcome `background`, sets no `waitingOn`, and the answer arrives as a message. Everywhere else (print, JSON and RPC modes, and a TUI marked unattended, such as a pi-squire delegate) it waits for the answer, which is the result: its status and body, labelled as not an answer when the status is `stopped` or `failed`. The wait ends when the answer arrives, after 10 minutes ("no answer yet"), when the target's running record disappears ("stopped running"), or when the user stops the run; `waitingOn` is set while it waits and cleared in every case. Refused at once, writing nothing, when another ask of this session is waiting, when the target is not running, when the target is waiting on this session, at the [hop limit](#hop-limit), and when `to` does not resolve, is ambiguous or is this session. |
|
|
135
135
|
| `session_mail_reply` | `ask`, `message` | Answers one open ask this session received, at once, with status `done` and the turn's hop count, without ending the run; the asker's `session_mail_ask` returns with it. The answer at settle then leaves that ask out, while the sender's other requests and asks are still answered. Refused, sending nothing, for an ask already answered (by this tool or at settle), for a request (answered at settle), a message or a reply, for an id that never reached this session, and for an empty text. |
|
|
136
136
|
| `session_mail_send` | `to`, `message` | Writes a message (`kind: "message"`) and returns its id. To a running session it is delivered at once; to a closed session, by full id, it waits in that session's inbox and the result says so. Refused when `to` does not resolve, is ambiguous or is this session, when the text is empty, and at the [hop limit](#hop-limit). |
|
|
137
137
|
|
|
@@ -168,9 +168,12 @@ listener takes it over, as pi-squire's `delegate` does for its tasks.
|
|
|
168
168
|
|
|
169
169
|
A reply to this session's waiting ask goes to the waiting tool call as its
|
|
170
170
|
result: it is neither injected nor emitted on `message:inbound`. A reply to an
|
|
171
|
-
ask that is
|
|
172
|
-
session restarted) is delivered like a message: it
|
|
173
|
-
says the ask
|
|
171
|
+
ask that is not waiting (a background ask, or one that timed out, that the
|
|
172
|
+
user stopped, or whose session restarted) is delivered like a message: it
|
|
173
|
+
starts a turn, and its label says the ask is not waiting for it. An
|
|
174
|
+
unattended session waits rather than asking in the background because ending
|
|
175
|
+
its turn ends its work: a delegate's settle is its result, and print mode
|
|
176
|
+
exits.
|
|
174
177
|
|
|
175
178
|
Every injected message opens with a neutral label: `[mailbox] From <name>
|
|
176
179
|
(<full id>, working in <cwd>), another Pi session on this machine.` The short
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -147,7 +147,7 @@ function inboundText(
|
|
|
147
147
|
header.push(
|
|
148
148
|
`It is waiting for your answer to its ask ${id}. Answer with session_mail_reply (ask ${id}); otherwise this run's last message is sent as the answer.`,
|
|
149
149
|
);
|
|
150
|
-
if (lateAnswer) header.push("It answers an ask of yours that
|
|
150
|
+
if (lateAnswer) header.push("It answers an ask of yours that is not waiting for it, so it arrives as a message.");
|
|
151
151
|
if (envelope.kind === "message")
|
|
152
152
|
header.push(`It expects no answer; if one is wanted, send it with session_mail_send to ${envelope.from}.`);
|
|
153
153
|
if (envelope.in_reply_to.length > 0)
|
|
@@ -388,7 +388,7 @@ export default function mailbox(pi: ExtensionAPI) {
|
|
|
388
388
|
// The answer to a waiting ask is that tool call's result, and nothing else.
|
|
389
389
|
if (tools?.takeAnswer(envelope)) return;
|
|
390
390
|
const { requests, quotes } = requestQuotes(me, envelope.in_reply_to);
|
|
391
|
-
// An answer to an ask that
|
|
391
|
+
// An answer to an ask that is not waiting (a background ask, or one that stopped) is delivered like a message.
|
|
392
392
|
const lateAnswer = envelope.kind === "reply" && requests.some((copy) => copy.envelope.kind === "ask");
|
|
393
393
|
const wakes = envelope.kind !== "reply" || lateAnswer;
|
|
394
394
|
const byUser = envelope.kind === "reply" && requests.some((copy) => copy.envelope.kind === "request");
|
package/src/tools.ts
CHANGED
|
@@ -5,8 +5,11 @@
|
|
|
5
5
|
// `session_mail_list` reports every running Pi session on this machine, in
|
|
6
6
|
// every agent directory, and marks the calling one. `session_mail_send` leaves a plain
|
|
7
7
|
// message, which wakes or steers its recipient and expects no answer.
|
|
8
|
-
// `session_mail_ask` leaves an ask
|
|
9
|
-
//
|
|
8
|
+
// `session_mail_ask` leaves an ask. In a session a person is watching it
|
|
9
|
+
// returns at once and the answer arrives as a message; elsewhere it waits, one
|
|
10
|
+
// at a time, for the answer, which comes back as its result. An unattended
|
|
11
|
+
// session must wait because ending its turn ends its work: a delegate's settle
|
|
12
|
+
// is its result, and print mode exits.
|
|
10
13
|
// `session_mail_reply` answers an ask this session received, mid-run; the asks
|
|
11
14
|
// this session owes, and the kind of every envelope it has received, are kept
|
|
12
15
|
// here for it. Mail a
|
|
@@ -53,8 +56,14 @@ const ASK_TIMEOUT_MS = 10 * 60 * 1000;
|
|
|
53
56
|
/** How often a waiting ask checks that its target still runs. */
|
|
54
57
|
const TARGET_CHECK_MS = 1000;
|
|
55
58
|
|
|
56
|
-
/**
|
|
59
|
+
/** Whether a person is watching this session, so an ask need not hold its turn; `PI_UNATTENDED=1` marks a TUI nobody watches. */
|
|
60
|
+
function attended(ctx: ExtensionContext): boolean {
|
|
61
|
+
return ctx.mode === "tui" && process.env.PI_UNATTENDED !== "1";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** How an ask's tool call ended: at once in the background, or how its wait ended. */
|
|
57
65
|
type AskOutcome =
|
|
66
|
+
| { outcome: "background" }
|
|
58
67
|
| { outcome: "answered"; status: string; body: string }
|
|
59
68
|
| { outcome: "timed-out" }
|
|
60
69
|
| { outcome: "stopped-running" }
|
|
@@ -66,6 +75,8 @@ type WaitingAsk = { id: string; to: string; finish: (outcome: AskOutcome) => voi
|
|
|
66
75
|
function askText(ask: Envelope, target: string, outcome: AskOutcome): string {
|
|
67
76
|
const later = "A later answer arrives as a message.";
|
|
68
77
|
switch (outcome.outcome) {
|
|
78
|
+
case "background":
|
|
79
|
+
return `Sent ask ${ask.id} to ${target}. This session is not waiting: the answer arrives as a message and starts a turn on its own, so go on with other work or end your turn.`;
|
|
69
80
|
case "answered":
|
|
70
81
|
return outcome.status === "done"
|
|
71
82
|
? `${target} answered your ask ${ask.id}:\n\n${outcome.body}`
|
|
@@ -166,6 +177,7 @@ const AskResult = Type.Union([
|
|
|
166
177
|
Type.Object({ ...asked, outcome: Type.Literal("timed-out") }),
|
|
167
178
|
Type.Object({ ...asked, outcome: Type.Literal("stopped-running") }),
|
|
168
179
|
Type.Object({ ...asked, outcome: Type.Literal("stopped-by-user") }),
|
|
180
|
+
Type.Object({ ...asked, outcome: Type.Literal("background") }),
|
|
169
181
|
]);
|
|
170
182
|
|
|
171
183
|
const ReplyResult = Type.Object({
|
|
@@ -292,14 +304,13 @@ export function registerTools(pi: ExtensionAPI, hooks: ToolHooks): Tools {
|
|
|
292
304
|
name: "session_mail_ask",
|
|
293
305
|
label: "Session mail: ask",
|
|
294
306
|
description:
|
|
295
|
-
"Ask another running Pi session on this machine a question
|
|
307
|
+
"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.",
|
|
296
308
|
parameters: Type.Object({
|
|
297
309
|
to: Type.String({
|
|
298
310
|
description: "The running session to ask: its name, an id prefix of 8+ characters, or its full session id.",
|
|
299
311
|
}),
|
|
300
312
|
message: Type.String({
|
|
301
|
-
description:
|
|
302
|
-
"The question. The recipient sees it labelled as coming from this session, with this session waiting.",
|
|
313
|
+
description: "The question. The recipient sees it labelled as coming from this session.",
|
|
303
314
|
}),
|
|
304
315
|
}),
|
|
305
316
|
outputSchema: AskResult,
|
|
@@ -332,26 +343,28 @@ export function registerTools(pi: ExtensionAPI, hooks: ToolHooks): Tools {
|
|
|
332
343
|
const hops = hooks.hops();
|
|
333
344
|
checkHops(hops, hooks);
|
|
334
345
|
const ask = send(me, to, body, { kind: "ask", hops });
|
|
335
|
-
const outcome =
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
346
|
+
const outcome: AskOutcome = attended(ctx)
|
|
347
|
+
? { outcome: "background" }
|
|
348
|
+
: await new Promise<AskOutcome>((resolve) => {
|
|
349
|
+
const finish = (result: AskOutcome) => {
|
|
350
|
+
if (waiting?.id !== ask.id) return;
|
|
351
|
+
waiting = undefined;
|
|
352
|
+
clearTimeout(timer);
|
|
353
|
+
clearInterval(check);
|
|
354
|
+
signal?.removeEventListener("abort", onAbort);
|
|
355
|
+
hooks.waitingOn("");
|
|
356
|
+
resolve(result);
|
|
357
|
+
};
|
|
358
|
+
const onAbort = () => finish({ outcome: "stopped-by-user" });
|
|
359
|
+
const timer = setTimeout(() => finish({ outcome: "timed-out" }), ASK_TIMEOUT_MS);
|
|
360
|
+
const check = setInterval(() => {
|
|
361
|
+
if (!listRunning().some((r) => r.address === to)) finish({ outcome: "stopped-running" });
|
|
362
|
+
}, TARGET_CHECK_MS);
|
|
363
|
+
waiting = { id: ask.id, to, finish };
|
|
364
|
+
hooks.waitingOn(to);
|
|
365
|
+
if (signal?.aborted) onAbort();
|
|
366
|
+
else signal?.addEventListener("abort", onAbort, { once: true });
|
|
367
|
+
});
|
|
355
368
|
const result = { id: ask.id, to, ...outcome };
|
|
356
369
|
return toolResult(askText(ask, target, outcome), result, result);
|
|
357
370
|
},
|