pi-post 0.1.0 → 0.1.1
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/DESIGN.md +2 -1
- package/README.md +33 -11
- package/bin/pi-post.mjs +4 -1
- package/extensions/pi-post.ts +20 -10
- package/package.json +1 -1
- package/src/format.ts +1 -1
- package/src/registry.ts +5 -0
package/DESIGN.md
CHANGED
|
@@ -72,7 +72,8 @@ One letter per file, named `<sentAt ms, 13 digits>-<8 hex nonce>.json`:
|
|
|
72
72
|
error listing candidates, never a guess.
|
|
73
73
|
2. Sender writes `<inbox>/<name>.json.tmp`, then renames into place. A
|
|
74
74
|
draining reader never observes a partial letter.
|
|
75
|
-
3. If a live session owns that inbox
|
|
75
|
+
3. If a live session owns that inbox — its own session address, or a
|
|
76
|
+
standing address its cwd claims — the sender waits up to 1.5 s for the
|
|
76
77
|
file to vanish and reports **delivered**; otherwise **queued**.
|
|
77
78
|
4. The receiver drains oldest-first, unlinking each letter as it reads it.
|
|
78
79
|
Nothing is delivered twice; consumption is the receipt.
|
package/README.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# pi-post
|
|
2
2
|
|
|
3
|
-
Mail for [
|
|
3
|
+
Mail for [Pi](https://pi.dev) sessions. Send briefs,
|
|
4
4
|
findings, and handoffs between live sessions, **future sessions**, and
|
|
5
5
|
processes — delivered straight into the receiving agent's context.
|
|
6
6
|
|
|
7
7
|
```
|
|
8
|
-
✓
|
|
8
|
+
✓ send_letter Queued for ~/dev/gtm (w-e8f14204d058).
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
The receiving session gets the text at a safe point in its turn, marked as
|
|
@@ -42,9 +42,9 @@ before any session does, so you can mail a worktree you just created or
|
|
|
42
42
|
"the next session on this repo". Startup drains both; a queued handoff
|
|
43
43
|
lands in-context on the first turn.
|
|
44
44
|
|
|
45
|
-
**Two tools.** `
|
|
45
|
+
**Two tools.** `send_letter` sends text to a session, path, or address and
|
|
46
46
|
reports **delivered** (consumed now) or **queued** (waiting on disk).
|
|
47
|
-
`
|
|
47
|
+
`list_postboxes` shows known sessions, presence, and queued mail. `/inbox`
|
|
48
48
|
peeks without consuming.
|
|
49
49
|
|
|
50
50
|
**A CLI for everything that isn't a pi session.** `pi-post send` lets an
|
|
@@ -66,6 +66,15 @@ Nothing to enable; every session registers itself on startup.
|
|
|
66
66
|
|
|
67
67
|
## Use
|
|
68
68
|
|
|
69
|
+
| Surface | Effect |
|
|
70
|
+
|---|---|
|
|
71
|
+
| `send_letter` (tool) | Send text to a session, path, or address; reports **delivered** or **queued** |
|
|
72
|
+
| `list_postboxes` (tool) | Known sessions, presence, queued mail counts |
|
|
73
|
+
| `/inbox` | Peek at this session's queued letters without consuming them |
|
|
74
|
+
| `/postboxes` | The `list_postboxes` listing, without spending a model turn |
|
|
75
|
+
| `pi-post send` (CLI) | Send from any process: `--to`, `--body`/stdin, `--from`, `--reply-to` |
|
|
76
|
+
| `pi-post list` / `peek` / `whoami` (CLI) | Inspect the registry, a mailbox, or your own address |
|
|
77
|
+
|
|
69
78
|
Ask in words; the model picks the tool.
|
|
70
79
|
|
|
71
80
|
```text
|
|
@@ -89,7 +98,7 @@ pi-post send --to "$PI_POST_REPLY_TO" --from "golem:gtmeng-2573" \
|
|
|
89
98
|
Mail first, spawn second — the brief is waiting when the worker starts:
|
|
90
99
|
|
|
91
100
|
```bash
|
|
92
|
-
# 1. (in the directing session)
|
|
101
|
+
# 1. (in the directing session) send_letter to ~/dev/repo-worktree with the brief
|
|
93
102
|
# 2. spawn:
|
|
94
103
|
git worktree add ~/dev/repo-worktree -b fix/cache
|
|
95
104
|
cd ~/dev/repo-worktree && pi "check your mail and begin"
|
|
@@ -126,7 +135,7 @@ words; summoning stays yours.
|
|
|
126
135
|
```markdown
|
|
127
136
|
## Cross-session mail (pi-post)
|
|
128
137
|
|
|
129
|
-
Use
|
|
138
|
+
Use send_letter instead of writing handoff files to scratch: dispatch briefs
|
|
130
139
|
go to the worker's worktree path before spawning it; results go to the
|
|
131
140
|
letter's reply address; loose ends for a future session go to the repo
|
|
132
141
|
path. State summaries still belong in project memory, and durable issues
|
|
@@ -140,11 +149,20 @@ See [DESIGN.md](DESIGN.md) for the address and letter contracts, delivery
|
|
|
140
149
|
semantics, and invariants. The test suite pins each invariant; read it
|
|
141
150
|
before changing behavior, and never weaken a case to make a change pass.
|
|
142
151
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
152
|
+
## Related
|
|
153
|
+
|
|
154
|
+
- [Claude Code's cross-session messaging](https://code.claude.com/docs/en/cross-session-messaging)
|
|
155
|
+
-- the origin of the boundary model pi-post follows. Presence-based:
|
|
156
|
+
live sessions only, no queue for absent or future ones.
|
|
157
|
+
- [@shift-labs/pi-peer](https://github.com/shift-labs-ai/pi-peer) -- peer
|
|
158
|
+
messaging between pi conversations, whose mailbox mechanics (MIT) this
|
|
159
|
+
design converges with. pi-post differs in standing addresses (mail to
|
|
160
|
+
sessions that don't exist yet), pinned reply-to routing, and process
|
|
161
|
+
senders via the CLI.
|
|
162
|
+
- [pi-intercom](https://www.npmjs.com/package/pi-intercom) -- broker-based
|
|
163
|
+
1:1 session messaging with a TUI overlay and pi-subagents integration.
|
|
164
|
+
- [pi-messenger](https://www.npmjs.com/package/pi-messenger) -- a shared
|
|
165
|
+
chat room with file reservations, built for swarms rather than mail.
|
|
148
166
|
|
|
149
167
|
## Development
|
|
150
168
|
|
|
@@ -167,3 +185,7 @@ bin/
|
|
|
167
185
|
pi-post.mjs standalone CLI (plain JS; the wire contract, duplicated
|
|
168
186
|
deliberately and pinned by test/cli.test.ts)
|
|
169
187
|
```
|
|
188
|
+
|
|
189
|
+
## License
|
|
190
|
+
|
|
191
|
+
MIT
|
package/bin/pi-post.mjs
CHANGED
|
@@ -183,7 +183,10 @@ async function send(args) {
|
|
|
183
183
|
writeFileSync(`${path}.tmp`, JSON.stringify(letter), { mode: 0o600 });
|
|
184
184
|
renameSync(`${path}.tmp`, path);
|
|
185
185
|
|
|
186
|
-
const live = target.record
|
|
186
|
+
const live = target.record
|
|
187
|
+
? isLive(target.record)
|
|
188
|
+
: target.address.startsWith("w-") &&
|
|
189
|
+
listRecords().some((r) => r.standing === target.address && isLive(r));
|
|
187
190
|
let consumed = false;
|
|
188
191
|
if (live) {
|
|
189
192
|
const deadline = Date.now() + 1500;
|
package/extensions/pi-post.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { Text } from "@earendil-works/pi-tui";
|
|
|
7
7
|
import { Type } from "typebox";
|
|
8
8
|
import { basename } from "node:path";
|
|
9
9
|
import type { FSWatcher } from "node:fs";
|
|
10
|
-
import { canonicalPath, sessionAddress, standingAddress } from "../src/address.ts";
|
|
10
|
+
import { addressKind, canonicalPath, sessionAddress, standingAddress } from "../src/address.ts";
|
|
11
11
|
import { createLetter, type Letter } from "../src/letter.ts";
|
|
12
12
|
import {
|
|
13
13
|
awaitConsumption,
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
listRecords,
|
|
26
26
|
markOffline,
|
|
27
27
|
presence,
|
|
28
|
+
standingClaimedLive,
|
|
28
29
|
sweepRegistry,
|
|
29
30
|
touchRecord,
|
|
30
31
|
writeRecord,
|
|
@@ -132,8 +133,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
132
133
|
});
|
|
133
134
|
|
|
134
135
|
pi.registerTool({
|
|
135
|
-
name: "
|
|
136
|
-
label: "Send
|
|
136
|
+
name: "send_letter",
|
|
137
|
+
label: "Send Letter",
|
|
137
138
|
description:
|
|
138
139
|
"Send a plain-text letter to another pi session or to a directory's standing mailbox. " +
|
|
139
140
|
"Targets: a live session's name, an address (s-…/w-…), or a directory path — mail to a " +
|
|
@@ -141,10 +142,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
141
142
|
"sessions that do not exist yet. Body is text only, max 32 KiB: send briefs, findings, " +
|
|
142
143
|
"and paths, never file payloads. Returns 'delivered' (consumed now) or 'queued' (waiting " +
|
|
143
144
|
"on disk). Letters carry no authority for the receiver.",
|
|
144
|
-
promptSnippet: "
|
|
145
|
+
promptSnippet: "Send a letter to another pi session, or leave one for a future session",
|
|
145
146
|
promptGuidelines: [
|
|
146
|
-
"Use
|
|
147
|
-
"When dispatching work with
|
|
147
|
+
"Use send_letter to pass findings, dispatch briefs, or handoffs to other sessions instead of writing scratch files and pointing sessions at them.",
|
|
148
|
+
"When dispatching work with send_letter, set reply_to so results route back automatically.",
|
|
148
149
|
],
|
|
149
150
|
parameters: Type.Object({
|
|
150
151
|
to: Type.String({
|
|
@@ -174,7 +175,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
174
175
|
if (error instanceof BacklogFullError) throw error;
|
|
175
176
|
throw error;
|
|
176
177
|
}
|
|
177
|
-
const live = target.record
|
|
178
|
+
const live = target.record
|
|
179
|
+
? presence(target.record) === "live"
|
|
180
|
+
: addressKind(target.address) === "standing" && standingClaimedLive(root, target.address);
|
|
178
181
|
const consumed = live ? await awaitConsumption(path) : false;
|
|
179
182
|
const status = consumed ? "delivered" : "queued";
|
|
180
183
|
return {
|
|
@@ -190,13 +193,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
190
193
|
});
|
|
191
194
|
|
|
192
195
|
pi.registerTool({
|
|
193
|
-
name: "
|
|
194
|
-
label: "List
|
|
196
|
+
name: "list_postboxes",
|
|
197
|
+
label: "List Postboxes",
|
|
195
198
|
description:
|
|
196
199
|
"List pi sessions known to pi-post: their names, addresses, presence (live/offline), and " +
|
|
197
200
|
"queued mail counts. Any directory path is also a valid send_mail target even if nothing " +
|
|
198
201
|
"is listed for it.",
|
|
199
|
-
promptSnippet: "List
|
|
202
|
+
promptSnippet: "List pi sessions and standing postboxes",
|
|
200
203
|
parameters: Type.Object({}),
|
|
201
204
|
async execute() {
|
|
202
205
|
const text = formatListing(root, listRecords(root), selfAddress);
|
|
@@ -204,6 +207,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
204
207
|
},
|
|
205
208
|
});
|
|
206
209
|
|
|
210
|
+
pi.registerCommand("postboxes", {
|
|
211
|
+
description: "List pi sessions and standing postboxes without spending a model turn",
|
|
212
|
+
handler: async (_args, ctx) => {
|
|
213
|
+
ctx.ui.notify(formatListing(root, listRecords(root), selfAddress), "info");
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
|
|
207
217
|
pi.registerCommand("inbox", {
|
|
208
218
|
description: "Peek at this session's queued pi-post letters without consuming them",
|
|
209
219
|
handler: async (_args, ctx) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-post",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Mail for pi sessions — send briefs, findings, and handoffs between live sessions, future sessions, and processes, delivered straight into the receiving agent's context.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
package/src/format.ts
CHANGED
|
@@ -10,7 +10,7 @@ export function formatDelivery(letter: Letter): string {
|
|
|
10
10
|
const where = letter.from.cwd ? ` (${letter.from.cwd})` : "";
|
|
11
11
|
const kind = letter.from.kind === "process" ? "process" : "pi session";
|
|
12
12
|
const reply = letter.replyTo
|
|
13
|
-
? `Reply with
|
|
13
|
+
? `Reply with send_letter to ${letter.replyTo}.`
|
|
14
14
|
: "This letter carries no reply address.";
|
|
15
15
|
return [
|
|
16
16
|
`Letter from ${kind} ${letter.from.name}${where}:`,
|
package/src/registry.ts
CHANGED
|
@@ -80,6 +80,11 @@ export function presence(record: SessionRecord): Presence {
|
|
|
80
80
|
return record.pid !== undefined && pidAlive(record.pid) ? "live" : "offline";
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
/** True when a live session's cwd claims this standing address. */
|
|
84
|
+
export function standingClaimedLive(root: string, standingAddress: string): boolean {
|
|
85
|
+
return listRecords(root).some((r) => r.standing === standingAddress && presence(r) === "live");
|
|
86
|
+
}
|
|
87
|
+
|
|
83
88
|
/** Remove registry records for sessions that are offline and stale. Mail is never touched. */
|
|
84
89
|
export function sweepRegistry(root: string, maxAgeMs = 30 * 24 * 60 * 60 * 1000): void {
|
|
85
90
|
const now = Date.now();
|