pi-post 0.6.1 → 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/DESIGN.md +6 -6
- package/README.md +7 -6
- package/bin/pi-post.mjs +3 -3
- package/extensions/pi-post.ts +5 -5
- package/package.json +1 -1
- 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` |
|
|
@@ -195,7 +195,8 @@ before changing behavior, and never weaken a case to make a change pass.
|
|
|
195
195
|
- [pi-intercom](https://www.npmjs.com/package/pi-intercom) -- broker-based
|
|
196
196
|
1:1 session messaging with a TUI overlay and pi-subagents integration.
|
|
197
197
|
- [pi-messenger](https://www.npmjs.com/package/pi-messenger) -- a shared
|
|
198
|
-
chat room with file reservations, built for swarms rather than
|
|
198
|
+
chat room with file reservations, built for swarms rather than
|
|
199
|
+
point-to-point messaging.
|
|
199
200
|
|
|
200
201
|
## Development
|
|
201
202
|
|
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
|
@@ -103,11 +103,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
103
103
|
sweepRegistry(root);
|
|
104
104
|
sweepInboxes(root);
|
|
105
105
|
|
|
106
|
-
// Queued
|
|
106
|
+
// Queued messages wait in context for the first prompt; they never start a turn.
|
|
107
107
|
await drainAll(ctx, "nextTurn");
|
|
108
108
|
|
|
109
|
-
const
|
|
110
|
-
watchers = [watchInbox(root, selfAddress,
|
|
109
|
+
const onMessage = () => void drainAll(ctx, "steer");
|
|
110
|
+
watchers = [watchInbox(root, selfAddress, onMessage)];
|
|
111
111
|
heartbeat = setInterval(() => selfAddress && touchRecord(root, selfAddress), HEARTBEAT_MS);
|
|
112
112
|
heartbeat.unref?.();
|
|
113
113
|
});
|
|
@@ -209,12 +209,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
209
209
|
label: "List Sessions",
|
|
210
210
|
description:
|
|
211
211
|
"List pi sessions known to pi-post: their names, addresses, presence (live/offline, " +
|
|
212
|
-
"with age), queued
|
|
212
|
+
"with age), queued message counts, and each session's resume handle ([pi --session …], run " +
|
|
213
213
|
"from the listed directory). Live sessions come first; offline sessions unseen for over " +
|
|
214
214
|
"a day are collapsed into a count unless all is set. Any directory path is also a valid " +
|
|
215
215
|
"send_message target even if nothing is listed for it.",
|
|
216
216
|
promptSnippet:
|
|
217
|
-
"List pi sessions reachable by message, with presence, queued
|
|
217
|
+
"List pi sessions reachable by message, with presence, queued messages, and resume handles",
|
|
218
218
|
parameters: Type.Object({
|
|
219
219
|
all: Type.Optional(
|
|
220
220
|
Type.Boolean({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-post",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2",
|
|
4
4
|
"description": "Messages between pi sessions — delivered mid-task or queued until they return. Briefs, findings, and handoffs straight into the receiving agent's context.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
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));
|