pi-post 0.6.1 → 0.6.3
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 +6 -6
- package/README.md +12 -6
- package/bin/pi-post.mjs +3 -3
- package/extensions/pi-post.ts +9 -5
- package/package.json +2 -2
- package/src/format.ts +4 -4
- package/src/registry.ts +4 -4
package/DESIGN.md
CHANGED
|
@@ -51,9 +51,9 @@ is directory information, not lifecycle management — pi-post still
|
|
|
51
51
|
never spawns or resumes anything itself.
|
|
52
52
|
|
|
53
53
|
v0.2.0 had a second kind — standing addresses, one per directory, so
|
|
54
|
-
|
|
54
|
+
messages could wait for sessions that did not exist yet. Removed in v0.3.0:
|
|
55
55
|
in a busy repository, directory identity is not task identity, so
|
|
56
|
-
standing
|
|
56
|
+
standing messages raced among concurrent sessions, delivered to the wrong
|
|
57
57
|
successor, and — because consumption is the receipt — misdelivered
|
|
58
58
|
*silently and destructively*. The lesson is recorded as a non-goal
|
|
59
59
|
below: how sessions come to exist is not the transport's business.
|
|
@@ -93,11 +93,11 @@ One message per file, named `<sentAt ms, 13 digits>-<8 hex nonce>.json`:
|
|
|
93
93
|
it. Nothing is delivered twice; consumption is the receipt.
|
|
94
94
|
5. Each message passes the inbound guard (mode + loop caps), then enters
|
|
95
95
|
context wrapped in the boundary preamble:
|
|
96
|
-
- live
|
|
96
|
+
- live messages → `deliverAs: "steer"`, `triggerTurn: true` — land between
|
|
97
97
|
tool calls; **wakes an idle session**, so a freshly spawned worker's
|
|
98
98
|
first turn can be the brief itself
|
|
99
99
|
- startup/resume drain → `deliverAs: "nextTurn"` — waits in context for
|
|
100
|
-
the next prompt; queued
|
|
100
|
+
the next prompt; a queued message never starts a turn on its own
|
|
101
101
|
|
|
102
102
|
## The boundary
|
|
103
103
|
|
|
@@ -116,8 +116,8 @@ Each is pinned by a test.
|
|
|
116
116
|
- **A reader never sees half a message.** Rename-into-place; only
|
|
117
117
|
`.json` is read.
|
|
118
118
|
- **Nothing is delivered twice.** Unlink before handling.
|
|
119
|
-
- **
|
|
120
|
-
queued
|
|
119
|
+
- **Messages outrank tidiness.** No sweep deletes a non-empty mailbox, and
|
|
120
|
+
queued messages pin the target's registry record: a record is swept only
|
|
121
121
|
when it is offline, stale (7 days), *and* its mailbox is empty. Empty
|
|
122
122
|
inbox directories no record names are removed.
|
|
123
123
|
- **Loops terminate structurally.** Identical body from one sender inside
|
package/README.md
CHANGED
|
@@ -39,7 +39,7 @@ smuggling state between sessions.
|
|
|
39
39
|
|
|
40
40
|
**An address that outlives the process.** A session address names a
|
|
41
41
|
conversation, not a process: the same session resumed tomorrow answers to
|
|
42
|
-
the same address, and
|
|
42
|
+
the same address, and messages queued while it was closed land in-context on
|
|
43
43
|
resume. A directory path as a target is a *query* — it resolves to the
|
|
44
44
|
session registered in that directory, live sessions first, ambiguity
|
|
45
45
|
refused.
|
|
@@ -52,15 +52,15 @@ you is a dead end: anything you can see, you can message *and* reopen.
|
|
|
52
52
|
|
|
53
53
|
**Wake-on-idle delivery.** A message to an idle session starts its turn.
|
|
54
54
|
Spawn a worker in its worktree, send the brief — the brief *is* the
|
|
55
|
-
worker's first turn. No "check your
|
|
55
|
+
worker's first turn. No "check your messages" incantations.
|
|
56
56
|
|
|
57
57
|
**Two tools.** `send_message` sends text to a session, path, address, or
|
|
58
58
|
session id and reports **delivered** (consumed now) or **queued** (waiting
|
|
59
|
-
on disk). `list_sessions` shows known sessions, presence, queued
|
|
59
|
+
on disk). `list_sessions` shows known sessions, presence, queued messages, and
|
|
60
60
|
resume handles. `/inbox` peeks without consuming.
|
|
61
61
|
|
|
62
62
|
**A CLI for everything that isn't a pi session.** `pi-post send` lets an
|
|
63
|
-
autonomous run's exit hook, a Claude Code hook, or any script
|
|
63
|
+
autonomous run's exit hook, a Claude Code hook, or any script message a
|
|
64
64
|
session. `--reply-to` defaults from `PI_SESSION_ID`, so a message sent from
|
|
65
65
|
inside a pi bash tool routes replies home automatically.
|
|
66
66
|
|
|
@@ -81,7 +81,7 @@ Nothing to enable; every session registers itself on startup.
|
|
|
81
81
|
| Surface | Effect |
|
|
82
82
|
|---|---|
|
|
83
83
|
| `send_message` (tool) | Send text to one or more sessions (name, path, address, or session id); reports **delivered** or **queued** per target |
|
|
84
|
-
| `list_sessions` (tool) | Known sessions, live first with ages, queued
|
|
84
|
+
| `list_sessions` (tool) | Known sessions, live first with ages, queued message counts, resume handles; stale offline rows collapse unless `all` |
|
|
85
85
|
| `/inbox` | Peek at this session's queued messages without consuming them |
|
|
86
86
|
| `/sessions` | The `list_sessions` listing, without spending a model turn |
|
|
87
87
|
| `pi-post send` (CLI) | Send from any process: `--to` (repeatable), `--body`/stdin, `--from`, `--reply-to` |
|
|
@@ -145,6 +145,11 @@ sessions can read state; only one can consume a message.
|
|
|
145
145
|
| `PI_POST_FROM` | — | Default `--from` label for the CLI |
|
|
146
146
|
| `PI_POST_REPLY_TO` | — | Default `--reply-to` address for the CLI |
|
|
147
147
|
|
|
148
|
+
The extension also *sets* one variable: `PI_SESSION_ADDRESS`, the session's
|
|
149
|
+
own `s-…` address, exported at session start so child processes (bash tools,
|
|
150
|
+
spawned scripts) can capture a reply target without shelling out to
|
|
151
|
+
`pi-post whoami`.
|
|
152
|
+
|
|
148
153
|
The directory is created `0700` and messages `0600`.
|
|
149
154
|
|
|
150
155
|
## Limits
|
|
@@ -195,7 +200,8 @@ before changing behavior, and never weaken a case to make a change pass.
|
|
|
195
200
|
- [pi-intercom](https://www.npmjs.com/package/pi-intercom) -- broker-based
|
|
196
201
|
1:1 session messaging with a TUI overlay and pi-subagents integration.
|
|
197
202
|
- [pi-messenger](https://www.npmjs.com/package/pi-messenger) -- a shared
|
|
198
|
-
chat room with file reservations, built for swarms rather than
|
|
203
|
+
chat room with file reservations, built for swarms rather than
|
|
204
|
+
point-to-point messaging.
|
|
199
205
|
|
|
200
206
|
## Development
|
|
201
207
|
|
package/bin/pi-post.mjs
CHANGED
|
@@ -277,15 +277,15 @@ function list(args) {
|
|
|
277
277
|
for (const record of records) {
|
|
278
278
|
const live = isLive(record);
|
|
279
279
|
const queued = queuedCount(record.address);
|
|
280
|
-
// Stale offline rows collapse into a count — unless they hold
|
|
280
|
+
// Stale offline rows collapse into a count — unless they hold messages.
|
|
281
281
|
if (!args.all && !live && queued === 0 && now - record.lastSeen > DAY_MS) {
|
|
282
282
|
hidden++;
|
|
283
283
|
continue;
|
|
284
284
|
}
|
|
285
285
|
const state = live ? "live" : `offline ${relativeAge(record.lastSeen, now)}`;
|
|
286
|
-
const
|
|
286
|
+
const queuedNote = queued > 0 ? `, ${queued} queued` : "";
|
|
287
287
|
console.log(
|
|
288
|
-
`${record.name} — ${record.address} (${state}${
|
|
288
|
+
`${record.name} — ${record.address} (${state}${queuedNote}) ${record.cwd} ` +
|
|
289
289
|
`[pi --session ${resumeHandle(record.sessionId)}]`,
|
|
290
290
|
);
|
|
291
291
|
}
|
package/extensions/pi-post.ts
CHANGED
|
@@ -88,6 +88,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
88
88
|
const canonical = canonicalPath(ctx.cwd);
|
|
89
89
|
selfAddress = sessionAddress(sessionId);
|
|
90
90
|
selfName = pi.getSessionName() ?? defaultSessionName(canonical, selfAddress);
|
|
91
|
+
// Expose the address to child processes (bash tools inherit process.env),
|
|
92
|
+
// so scripts can capture a send_message target without shelling out to
|
|
93
|
+
// `pi-post whoami` or re-deriving the hash from PI_SESSION_ID.
|
|
94
|
+
process.env.PI_SESSION_ADDRESS = selfAddress;
|
|
91
95
|
|
|
92
96
|
ensureDirs(root, selfAddress);
|
|
93
97
|
writeRecord(root, {
|
|
@@ -103,11 +107,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
103
107
|
sweepRegistry(root);
|
|
104
108
|
sweepInboxes(root);
|
|
105
109
|
|
|
106
|
-
// Queued
|
|
110
|
+
// Queued messages wait in context for the first prompt; they never start a turn.
|
|
107
111
|
await drainAll(ctx, "nextTurn");
|
|
108
112
|
|
|
109
|
-
const
|
|
110
|
-
watchers = [watchInbox(root, selfAddress,
|
|
113
|
+
const onMessage = () => void drainAll(ctx, "steer");
|
|
114
|
+
watchers = [watchInbox(root, selfAddress, onMessage)];
|
|
111
115
|
heartbeat = setInterval(() => selfAddress && touchRecord(root, selfAddress), HEARTBEAT_MS);
|
|
112
116
|
heartbeat.unref?.();
|
|
113
117
|
});
|
|
@@ -209,12 +213,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
209
213
|
label: "List Sessions",
|
|
210
214
|
description:
|
|
211
215
|
"List pi sessions known to pi-post: their names, addresses, presence (live/offline, " +
|
|
212
|
-
"with age), queued
|
|
216
|
+
"with age), queued message counts, and each session's resume handle ([pi --session …], run " +
|
|
213
217
|
"from the listed directory). Live sessions come first; offline sessions unseen for over " +
|
|
214
218
|
"a day are collapsed into a count unless all is set. Any directory path is also a valid " +
|
|
215
219
|
"send_message target even if nothing is listed for it.",
|
|
216
220
|
promptSnippet:
|
|
217
|
-
"List pi sessions reachable by message, with presence, queued
|
|
221
|
+
"List pi sessions reachable by message, with presence, queued messages, and resume handles",
|
|
218
222
|
parameters: Type.Object({
|
|
219
223
|
all: Type.Optional(
|
|
220
224
|
Type.Boolean({
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-post",
|
|
3
|
-
"version": "0.6.
|
|
4
|
-
"description": "Messages between pi sessions — delivered mid-task or queued until they return.
|
|
3
|
+
"version": "0.6.3",
|
|
4
|
+
"description": "Messages between pi sessions — delivered mid-task or queued until they return.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
7
7
|
"pi-extension",
|
package/src/format.ts
CHANGED
|
@@ -68,16 +68,16 @@ export function formatListing(
|
|
|
68
68
|
const live = presence(record) === "live";
|
|
69
69
|
const queued = queuedCount(root, record.address);
|
|
70
70
|
const self = record.address === selfAddress;
|
|
71
|
-
// Stale offline rows collapse into a count — unless they hold
|
|
72
|
-
// (
|
|
71
|
+
// Stale offline rows collapse into a count — unless they hold messages
|
|
72
|
+
// (messages outrank tidiness) or the caller asked for everything.
|
|
73
73
|
if (!options.all && !live && !self && queued === 0 && now - record.lastSeen > DAY_MS) {
|
|
74
74
|
hidden++;
|
|
75
75
|
continue;
|
|
76
76
|
}
|
|
77
77
|
const state = live ? "live" : `offline ${relativeAge(record.lastSeen, now)}`;
|
|
78
|
-
const
|
|
78
|
+
const queuedNote = queued > 0 ? `, ${queued} queued` : "";
|
|
79
79
|
lines.push(
|
|
80
|
-
`${record.name} — ${record.address} (${state}${
|
|
80
|
+
`${record.name} — ${record.address} (${state}${queuedNote})${self ? " [self]" : ""} ${record.cwd} ` +
|
|
81
81
|
`[pi --session ${resumeHandle(record.sessionId)}]`,
|
|
82
82
|
);
|
|
83
83
|
}
|
package/src/registry.ts
CHANGED
|
@@ -93,7 +93,7 @@ export function presence(record: SessionRecord): Presence {
|
|
|
93
93
|
|
|
94
94
|
/**
|
|
95
95
|
* Remove registry records for sessions that are offline and stale. A record
|
|
96
|
-
* whose mailbox holds
|
|
96
|
+
* whose mailbox holds messages is never swept: queued messages would still deliver on
|
|
97
97
|
* resume (the address derives from the session id), but losing the record
|
|
98
98
|
* hides the queued count and breaks name/path resolution to the target.
|
|
99
99
|
*/
|
|
@@ -102,7 +102,7 @@ export function sweepRegistry(root: string, maxAgeMs = 7 * 24 * 60 * 60 * 1000):
|
|
|
102
102
|
for (const record of listRecords(root)) {
|
|
103
103
|
if (presence(record) !== "offline") continue;
|
|
104
104
|
if (now - record.lastSeen <= maxAgeMs) continue;
|
|
105
|
-
if (queuedCount(root, record.address) > 0) continue; //
|
|
105
|
+
if (queuedCount(root, record.address) > 0) continue; // queued messages keep the record alive
|
|
106
106
|
try {
|
|
107
107
|
unlinkSync(join(registryDir(root), `${record.address}.json`));
|
|
108
108
|
} catch {
|
|
@@ -114,8 +114,8 @@ export function sweepRegistry(root: string, maxAgeMs = 7 * 24 * 60 * 60 * 1000):
|
|
|
114
114
|
/**
|
|
115
115
|
* Remove empty inbox directories that no registry record names (orphans from
|
|
116
116
|
* swept records or removed address schemes). `rmdirSync` refuses non-empty
|
|
117
|
-
* directories, so queued
|
|
118
|
-
* structurally —
|
|
117
|
+
* directories, so a queued message or a mid-flight `.tmp` deposit blocks removal
|
|
118
|
+
* structurally — messages outrank tidiness.
|
|
119
119
|
*/
|
|
120
120
|
export function sweepInboxes(root: string): void {
|
|
121
121
|
const known = new Set(listRecords(root).map((r) => r.address));
|