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 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, the sender waits up to 1.5 s for the
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 [pi](https://github.com/badlogic/pi-mono) sessions. Send briefs,
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
- send_mail Queued for ~/dev/gtm (w-e8f14204d058).
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.** `send_mail` sends text to a session, path, or address and
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
- `list_mail` shows known sessions, presence, and queued mail. `/inbox`
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) send_mail to ~/dev/repo-worktree with the brief
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 send_mail instead of writing handoff files to scratch: dispatch briefs
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
- Prior art: the mailbox mechanics converge with
144
- [pi-peer](https://github.com/shift-labs-ai/pi-peer) (MIT), and the
145
- boundary model follows Claude Code's cross-session messaging. pi-post
146
- differs in standing addresses (mail to sessions that don't exist yet),
147
- pinned reply-to routing, and process senders.
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 ? isLive(target.record) : false;
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;
@@ -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: "send_mail",
136
- label: "Send Mail",
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: "Message another pi session, or leave a letter for a future one",
145
+ promptSnippet: "Send a letter to another pi session, or leave one for a future session",
145
146
  promptGuidelines: [
146
- "Use send_mail to pass findings, dispatch briefs, or handoffs to other sessions instead of writing scratch files and pointing sessions at them.",
147
- "When dispatching work with send_mail, set reply_to so results route back automatically.",
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 ? presence(target.record) === "live" : false;
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: "list_mail",
194
- label: "List Mail",
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 reachable pi sessions and their mailboxes",
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.0",
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 send_mail to ${letter.replyTo}.`
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();