@prjct.app/pi-team 0.5.4 → 0.5.6
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 +8 -0
- package/CONTRIBUTING.md +2 -1
- package/README.md +119 -296
- package/docs/architecture.md +160 -0
- package/docs/package.md +26 -43
- package/package.json +3 -3
- package/src/mailbox.ts +18 -4
- package/src/store.ts +17 -7
- package/docs/cover.png +0 -0
- package/docs/reference.md +0 -42
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
## [0.5.6](https://github.com/prjct-app/pi-team/compare/v0.5.5...v0.5.6) (2026-09-10)
|
|
2
|
+
|
|
3
|
+
## [0.5.5](https://github.com/prjct-app/pi-team/compare/v0.5.4...v0.5.5) (2026-09-10)
|
|
4
|
+
|
|
5
|
+
### Performance Improvements
|
|
6
|
+
|
|
7
|
+
* carry the canonical payload serialization instead of recomputing it ([#24](https://github.com/prjct-app/pi-team/issues/24)) ([340c495](https://github.com/prjct-app/pi-team/commit/340c4954358e715c6aed5e37e9f0a78f33100877))
|
|
8
|
+
|
|
1
9
|
## [0.5.4](https://github.com/prjct-app/pi-team/compare/v0.5.3...v0.5.4) (2026-09-10)
|
|
2
10
|
|
|
3
11
|
### Performance Improvements
|
package/CONTRIBUTING.md
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
- Deliver changes through a pull request using `.github/pull_request_template.md`.
|
|
5
5
|
- Use English for code, documentation, tests, issues, and pull requests.
|
|
6
6
|
- Use strict TypeScript and only APIs documented by Pi 0.85.1.
|
|
7
|
+
- Use immutable values: `npm run check` fails on any `let` under `src/`. See [AGENTS.md](AGENTS.md).
|
|
7
8
|
- Do not import host internals, monkey-patch prototypes, or access real credentials, sessions, or user configuration in tests.
|
|
8
9
|
- Keep runtime dependencies in `dependencies`; list Pi-provided packages in `peerDependencies` with a `*` range.
|
|
9
10
|
- Run `npm run check`, `npm test`, and `npm pack --dry-run` before review.
|
|
@@ -11,7 +12,7 @@
|
|
|
11
12
|
|
|
12
13
|
## Package documentation
|
|
13
14
|
|
|
14
|
-
Follow [docs/package.md](docs/package.md) and its versioned official references. Keep README examples consistent with registered commands, distinguish tested behavior from unverified compatibility, and verify `npm run check:package` before release.
|
|
15
|
+
Follow [docs/package.md](docs/package.md) and its versioned official references. Keep README examples consistent with registered commands, distinguish tested behavior from unverified compatibility, and verify `npm run check:package` before release. Design detail belongs in [docs/architecture.md](docs/architecture.md), not the README.
|
|
15
16
|
|
|
16
17
|
## Releases
|
|
17
18
|
|
package/README.md
CHANGED
|
@@ -2,37 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://pi.dev)
|
|
4
4
|
|
|
5
|
-
Coordinate independent PI Agent sessions with local team messaging, queued tasks,
|
|
5
|
+
Coordinate independent PI Agent sessions with local team messaging, queued tasks,
|
|
6
|
+
and shared results.
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
You open the terminals; `pi-team` lets those sessions send each other work, wake a
|
|
9
|
+
free teammate, and return a result the requester can verify. Everything stays on
|
|
10
|
+
this machine, in local files.
|
|
8
11
|
|
|
9
|
-
|
|
10
|
-
- Concurrent mailbox storage: many agents write at the same time without lock failures.
|
|
11
|
-
- Automatic delivery when a teammate is idle; pending work survives restarts.
|
|
12
|
-
- Automatic results verified against the original request, plus periodic review turns that chase unresolved work.
|
|
13
|
-
- One `/team wake [message]` command requests an actionable status check-in from every teammate.
|
|
14
|
-
- Automatic task-boundary compaction before the next team turn, keeping independent sessions focused and reusable.
|
|
15
|
-
- Minimal live session widget, on-demand requester → assignee status, folded transcript previews, and one `/team` command surface.
|
|
16
|
-
|
|
17
|
-
## Demo
|
|
18
|
-
|
|
19
|
-
[](https://github.com/prjct-app/pi-team/raw/refs/heads/main/media/pi-team-demo/pi-team-demo.mp4)
|
|
20
|
-
|
|
21
|
-
[Watch or download the 40-second demo](https://github.com/prjct-app/pi-team/raw/refs/heads/main/media/pi-team-demo/pi-team-demo.mp4). It shows the request flow, persistent local coordination, correlated results, and the two-command session setup. The soundtrack is an original instrumental composition with no voice-over or external samples.
|
|
12
|
+
[](https://github.com/prjct-app/pi-team/raw/refs/heads/main/media/pi-team-demo/pi-team-demo.mp4)
|
|
22
13
|
|
|
23
14
|
## Install
|
|
24
15
|
|
|
25
|
-
Requires Pi installed separately and Node.js **22.19 or later**.
|
|
26
|
-
|
|
27
|
-
Install with Pi's package manager:
|
|
16
|
+
Requires Pi installed separately and Node.js **22.19 or later**. Tested against
|
|
17
|
+
Pi **0.85.1**; newer versions are not yet verified. Independent community package.
|
|
28
18
|
|
|
29
19
|
```sh
|
|
30
20
|
pi install npm:@prjct.app/pi-team
|
|
31
21
|
```
|
|
32
22
|
|
|
33
|
-
|
|
23
|
+
Add `-l` for project-only installation, and restart Pi afterwards. Manage it with
|
|
24
|
+
the usual `pi list` / `pi update` / `pi remove` and `pi config`. Do not install the
|
|
25
|
+
same extension from both GitHub and npm: Pi treats those as different packages.
|
|
34
26
|
|
|
35
|
-
##
|
|
27
|
+
## Quickstart
|
|
36
28
|
|
|
37
29
|
Open two interactive Pi terminals. In the first:
|
|
38
30
|
|
|
@@ -47,22 +39,28 @@ In the second:
|
|
|
47
39
|
/team join demo reviewer
|
|
48
40
|
```
|
|
49
41
|
|
|
50
|
-
Back in the first
|
|
42
|
+
Back in the first:
|
|
51
43
|
|
|
52
44
|
```text
|
|
53
45
|
/team note reviewer Please review the current README.
|
|
54
|
-
/team
|
|
46
|
+
/team send reviewer Add a limits table to the README.
|
|
55
47
|
```
|
|
56
48
|
|
|
57
|
-
|
|
49
|
+
The note appears in the reviewer's transcript without starting any model work.
|
|
50
|
+
The request wakes the reviewer once it is idle, and its result comes back to the
|
|
51
|
+
coordinator. Installing alone never joins a team.
|
|
58
52
|
|
|
59
|
-
Supported on Linux
|
|
53
|
+
Supported on Linux and macOS with local disk storage. Network filesystems,
|
|
54
|
+
cross-machine messaging, and native Windows are not supported.
|
|
60
55
|
|
|
61
56
|
## Concepts
|
|
62
57
|
|
|
63
|
-
A **team** is a named local mailbox
|
|
64
|
-
|
|
65
|
-
|
|
58
|
+
A **team** is a named local mailbox. A session joins under an **alias**: an
|
|
59
|
+
address, not a privileged role or an automatic persona, and shared rather than
|
|
60
|
+
private — anyone using this OS account can rejoin an offline alias and see its
|
|
61
|
+
history. Each session keeps its own model, cwd, instructions, permissions, and
|
|
62
|
+
conversation. Two agents editing the same files can still overwrite each other:
|
|
63
|
+
this package does not manage file ownership.
|
|
66
64
|
|
|
67
65
|
| Kind | Meaning |
|
|
68
66
|
| --- | --- |
|
|
@@ -70,66 +68,14 @@ identities. Messages come in three kinds:
|
|
|
70
68
|
| `note` | Display-only FYI. Appears in the transcript; never starts a model turn. |
|
|
71
69
|
| `result` | The automatic reply to a request: outcome, final text, and observed files. Delivered to the emitter for verification. |
|
|
72
70
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
recipient is idle → worked on → result persisted and made available to the
|
|
77
|
-
emitter → the recipient compacts before accepting another team turn → the
|
|
78
|
-
emitter verifies the result against the original request and, if anything is
|
|
79
|
-
missing, replies in the same thread with what remains to finish → the emitter
|
|
80
|
-
compacts that result-review turn before accepting another team turn.
|
|
81
|
-
|
|
82
|
-
### Status widget
|
|
83
|
-
|
|
84
|
-
While joined, a minimal live widget above the editor shows only the session
|
|
85
|
-
header:
|
|
86
|
-
|
|
87
|
-
```text
|
|
88
|
-
shop · pm · connected
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
The state is `connected`, `working`, `compacting`, `paused`, or `select a model`.
|
|
92
|
-
A pending count is appended when work addressed to you is still queued. Run
|
|
93
|
-
`/team status` to show the complete unresolved requester → assignee flow,
|
|
94
|
-
including each task subject, assignee presence, and whether the request is queued
|
|
95
|
-
or active. Subjects are visible team-wide for coordination, while request bodies
|
|
96
|
-
remain limited to their sender and recipient.
|
|
97
|
-
|
|
98
|
-
## Three terminals
|
|
99
|
-
|
|
100
|
-
In the planning terminal:
|
|
101
|
-
|
|
102
|
-
```text
|
|
103
|
-
/team create shop
|
|
104
|
-
/team join shop pm
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
In a backend worktree or repository:
|
|
108
|
-
|
|
109
|
-
```text
|
|
110
|
-
/team join shop backend
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
In a frontend worktree or repository:
|
|
114
|
-
|
|
115
|
-
```text
|
|
116
|
-
/team join shop frontend
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
Then tell PM: "Coordinate the login feature with backend and frontend. Agree on the
|
|
120
|
-
API contract before implementation. Ask me before any push or deployment."
|
|
71
|
+
While joined, a minimal widget above the editor shows `team · alias · state`,
|
|
72
|
+
where state is `connected`, `working`, `compacting`, `paused`, or `select a
|
|
73
|
+
model`, plus a pending count when work is queued for you.
|
|
121
74
|
|
|
122
75
|
Membership is restored automatically when the same Pi session is resumed or
|
|
123
|
-
reloaded
|
|
124
|
-
`/new` and `/fork` start unaffiliated sessions on purpose.
|
|
125
|
-
|
|
126
|
-
`pm` is an address, not a privileged role or an automatic persona. Give each agent
|
|
127
|
-
its responsibilities in its own session. Each retains its own model, cwd,
|
|
128
|
-
instructions, permissions and conversation. Team names connect separate worktrees;
|
|
129
|
-
they are not inferred from directory names. Two agents using the same files can
|
|
130
|
-
still overwrite each other's edits: this package does not manage file ownership.
|
|
76
|
+
reloaded. `/new` and `/fork` start unaffiliated sessions on purpose.
|
|
131
77
|
|
|
132
|
-
|
|
78
|
+
## Commands
|
|
133
79
|
|
|
134
80
|
| Command | Meaning |
|
|
135
81
|
| --- | --- |
|
|
@@ -137,233 +83,102 @@ still overwrite each other's edits: this package does not manage file ownership.
|
|
|
137
83
|
| `/team join shop backend` | Register this session and enable automatic reception |
|
|
138
84
|
| `/team list` | List teams; refresh team-name completion |
|
|
139
85
|
| `/team members` | Show aliases, cwd, and idle/busy/paused/offline status |
|
|
140
|
-
| `/team status` | Show every unresolved requester → assignee relationship
|
|
141
|
-
| `/team wake [message]` | Queue an actionable check-in
|
|
86
|
+
| `/team status` | Show every unresolved requester → assignee relationship |
|
|
87
|
+
| `/team wake [message]` | Queue an actionable check-in for every other teammate |
|
|
142
88
|
| `/team send backend Implement login` | Queue a request that can start work |
|
|
143
89
|
| `/team note frontend API contract changed` | Display an FYI; never starts a model turn |
|
|
144
|
-
| `/team inbox` | Show the most recent 20
|
|
90
|
+
| `/team inbox` | Show the most recent 20 records and their states |
|
|
145
91
|
| `/team pause` | Pause new work, without cancelling current work |
|
|
146
|
-
| `/team resume` | Resume reception and reset the
|
|
147
|
-
| `/team leave` | Leave; if processing a
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
including offline aliases. Its standard prompt asks each agent to report current
|
|
161
|
-
work, remaining work, blockers, and the next concrete step; request missing input
|
|
162
|
-
through `team_send`; and finish authorized pending work instead of waiting. An
|
|
163
|
-
optional message is appended as sender-provided context and does not replace the
|
|
164
|
-
standard safety and authorization boundaries. Each recipient returns its own
|
|
165
|
-
correlated result. If any teammate cannot be queued, the command reports the
|
|
166
|
-
successful count and each failed alias instead of claiming complete delivery.
|
|
167
|
-
|
|
168
|
-
### Agent tools
|
|
169
|
-
|
|
170
|
-
- `team_members`: discover the current team, without leaking lease tokens.
|
|
171
|
-
- `team_send`: send `{ to, kind: "request" | "note", subject, body }`.
|
|
172
|
-
- `team_status`: read-only view of outstanding work: the team-wide unresolved
|
|
173
|
-
requester → assignee flow, requests you emitted (with recipient presence and
|
|
174
|
-
age), work queued for you, results awaiting your review, your currently
|
|
175
|
-
claimed task, and teammate presence.
|
|
92
|
+
| `/team resume` | Resume reception and reset the automatic turn budget |
|
|
93
|
+
| `/team leave` | Leave; if processing a request, report its result first |
|
|
94
|
+
|
|
95
|
+
Names and aliases are 1–48 lowercase letters, digits, or hyphens, starting with a
|
|
96
|
+
letter. Unknown teams are rejected, never implicitly created; duplicate live
|
|
97
|
+
aliases are rejected. Sending to an offline **known** alias queues until someone
|
|
98
|
+
rejoins it. Tab completion covers subcommands, discovered teams, and teammates.
|
|
99
|
+
|
|
100
|
+
## Agent tools
|
|
101
|
+
|
|
102
|
+
- `team_members` — discover teammates and their status.
|
|
103
|
+
- `team_send` — send `{ to, kind: "request" | "note", subject, body }`.
|
|
104
|
+
- `team_status` — outstanding work: what you emitted and is unresolved, what is
|
|
105
|
+
queued for you, results awaiting your review, and third-party team activity.
|
|
176
106
|
|
|
177
107
|
Tools cannot create teams, join, resume reception, change permissions, or launch
|
|
178
|
-
terminals
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
## Delivery and results
|
|
182
|
-
|
|
183
|
-
Requests and correlated results start a new turn only when the recipient is idle,
|
|
184
|
-
has a selected model, no pending user messages or open extension prompt, an empty editor,
|
|
185
|
-
and no task-boundary compaction in progress.
|
|
186
|
-
A second readiness check handles a user starting work during a filesystem read.
|
|
187
|
-
No running tool is interrupted. Notes are transcript-only; view them with
|
|
188
|
-
`/team inbox`. They are not injected into the model's context.
|
|
189
|
-
|
|
190
|
-
Incoming/outgoing messages show a folded preview. Use Pi's tool-output expansion
|
|
191
|
-
shortcut (Ctrl+O by default) to read the full content. `/team inbox` refreshes the
|
|
192
|
-
persisted state; an old transcript preview is an event snapshot, not a live receipt.
|
|
193
|
-
|
|
194
|
-
For each processed request, the extension automatically sends **one** correlated
|
|
195
|
-
result after `agent_settled`, not merely after an individual model/tool turn:
|
|
196
|
-
|
|
197
|
-
- Last assistant **text**, capped at 3,000 characters. No thinking blocks.
|
|
198
|
-
- Up to 50 absolute file paths observed in successful `edit`/`write` results.
|
|
199
|
-
- An execution outcome: completed, failed, or interrupted.
|
|
200
|
-
|
|
201
|
-
The assistant is instructed to include actual test results and blockers in its final
|
|
202
|
-
text. The extension does **not** infer test success from a shell command or model
|
|
203
|
-
claim. It does not automatically enumerate files changed through bash, custom tools,
|
|
204
|
-
or other processes. The observed file list is not a Git diff or a complete change
|
|
205
|
-
inventory. A completed run is not proof of task success; review the reported outcome
|
|
206
|
-
and changes in the recipient worktree.
|
|
207
|
-
|
|
208
|
-
Results may wake the requester so it can continue coordinating, but processing a
|
|
209
|
-
result never produces another automatic reply. Delivered results quote the
|
|
210
|
-
original request, and agents are instructed to verify the deliverable against it
|
|
211
|
-
and reply in-thread with exactly what is missing when a result is incomplete or
|
|
212
|
-
failed. Notes/acknowledgements never wake a model.
|
|
213
|
-
|
|
214
|
-
### Task-boundary compaction
|
|
215
|
-
|
|
216
|
-
After a request or correlated result-review turn settles and its mailbox outcome
|
|
217
|
-
is safely persisted, the extension calls Pi's documented `ctx.compact()` API.
|
|
218
|
-
That session claims no other peer message while compaction is running. The focused
|
|
219
|
-
instructions preserve user-authored goals and constraints, team identity,
|
|
220
|
-
unresolved requester → assignee relationships, concrete outcomes, blockers,
|
|
221
|
-
files, tests, and next actions while asking Pi to discard verbose tool output,
|
|
222
|
-
duplicated task payloads, completed traces, and private reasoning.
|
|
223
|
-
|
|
224
|
-
Compaction changes model context, not extension registration or mailbox state:
|
|
225
|
-
`/team` commands and team tools remain available, and each terminal continues as
|
|
226
|
-
an independent Pi session rather than a spawned subagent. Pi still applies its
|
|
227
|
-
configured `keepRecentTokens`, so this is compaction rather than a hard context
|
|
228
|
-
reset. It uses a summarization model call now to reduce repeated context on later
|
|
229
|
-
tasks. If compaction fails, the TUI warns and reception continues; Pi's normal
|
|
230
|
-
context-threshold compaction remains available. User takeover skips this automatic
|
|
231
|
-
step because the resulting turn is no longer an isolated team task.
|
|
232
|
-
|
|
233
|
-
While you have emitted requests that stay unresolved past five minutes, an
|
|
234
|
-
automatic review turn asks your agent every minute to chase the responsible
|
|
235
|
-
teammate in-thread or report the blockage to you. Reviews quiet down after three
|
|
236
|
-
turns without mailbox progress and re-arm on any change; they share the
|
|
237
|
-
five-turn automatic budget, never start new work, and never retry interrupted
|
|
238
|
-
work on their own. The one-minute cadence and five-minute threshold are fixed
|
|
239
|
-
defaults; they are not user-configurable yet.
|
|
240
|
-
|
|
241
|
-
User takeover during a peer task pauses reception and sends an interrupted
|
|
242
|
-
notice instead of forwarding the unrelated final answer. Files observed after the
|
|
243
|
-
takeover are not included. `/team leave` does not cancel the current run; quitting
|
|
244
|
-
Pi or reloading while working produces an interrupted record, not a success report.
|
|
245
|
-
|
|
246
|
-
## Limits and safety
|
|
247
|
-
|
|
248
|
-
- At most five automatic peer turns per session before reception pauses. Use
|
|
249
|
-
`/team resume` to continue. No daily token or monetary budget is enforced.
|
|
250
|
-
- At most eight non-result messages in an automatically linked conversation.
|
|
251
|
-
- Duplicate sender/recipient/subject/body messages within one minute are refused.
|
|
252
|
-
- Maximum 16 KB per outgoing body, 20 KB per serialized outgoing message, and
|
|
253
|
-
50 inbox slots per recipient. Pending/claimed deliveries occupy a slot until
|
|
254
|
-
settled, and each outgoing request reserves another slot for its automatic reply.
|
|
255
|
-
A send is rejected if the recipient is full, or if a request's sender cannot
|
|
256
|
-
reserve its reply. Notes need no reply reservation. Automatic replies are capped
|
|
257
|
-
at 32 KB; their observed file list is shortened with a notice when necessary.
|
|
258
|
-
Older teams may already be overcommitted; allow them to drain before sending
|
|
259
|
-
more work. Results for previously accepted requests are never discarded.
|
|
260
|
-
- Maximum 100 aliases and 500 message records per team, reserving result capacity
|
|
261
|
-
for outstanding requests. History is not silently deleted. Create a fresh team
|
|
262
|
-
when full. Inbox display is limited to the latest 20; persisted records remain.
|
|
263
|
-
- Peer messages explicitly identify their origin and are not user consent. Rules
|
|
264
|
-
instruct agents not to relay denied work, alter configuration, or evade plan mode.
|
|
265
|
-
- Text is delivered as a custom message, never executed as a slash command or used
|
|
266
|
-
for automatic `@file` expansion. The session's normal tool policy still applies.
|
|
267
|
-
- This is **not a sandbox or an authorization system**. Agents and processes under
|
|
268
|
-
the same OS user already have filesystem access. Prompt-level rules are not a
|
|
269
|
-
hard guarantee against a model that ignores instructions. Do not place untrusted
|
|
270
|
-
agents in a team or rely on team boundaries to protect secrets from that OS user.
|
|
271
|
-
- Messages stay in local mailbox files but, when processed, their text is sent to
|
|
272
|
-
the recipient's configured model provider like normal prompt content. Results
|
|
273
|
-
are also shared with the requester. Do not send credentials or unrelated secrets.
|
|
274
|
-
|
|
275
|
-
## Persistence and recovery
|
|
276
|
-
|
|
277
|
-
Storage: `~/.pi/agent/teams/<team>/state.json` (respects `PI_CODING_AGENT_DIR`).
|
|
278
|
-
Each team is one small JSON record stored with optimistic concurrency: readers
|
|
279
|
-
never wait on a lock, and writers compare-and-swap a monotonically increasing
|
|
280
|
-
revision, retrying against a fresh read on conflict. Many agents can therefore
|
|
281
|
-
write at the same time instead of queueing for a team-wide lock. Every
|
|
282
|
-
publication is written to a private temporary file, synced, hard-linked into a
|
|
283
|
-
bounded `revisions/` history, and atomically renamed into place; the history
|
|
284
|
-
doubles as recovery evidence for interrupted writes. Files are 0600 and team
|
|
285
|
-
folders 0700; envelopes carry a content hash. Unsafe/symlinked roots or mailbox
|
|
286
|
-
files and invalid schemas fail closed; corrupt files are preserved for manual
|
|
287
|
-
recovery, not erased. Mailboxes written before envelope records migrate
|
|
288
|
-
transparently on their first write.
|
|
289
|
-
|
|
290
|
-
Presence lives outside the shared record: each member renews its own
|
|
291
|
-
`presence/<alias>.json` every two seconds, so heartbeats add no write
|
|
292
|
-
contention. Presence expires after 30 seconds, or sooner when the recorded
|
|
293
|
-
process has exited. A lock abandoned by a crashed writer is reclaimed after ten
|
|
294
|
-
seconds. Transient storage errors are reported but never pause reception; the
|
|
295
|
-
next tick retries.
|
|
296
|
-
|
|
297
|
-
Directory watchers provide prompt delivery; periodic polling recovers missed
|
|
298
|
-
notifications. Watchers and timers only run for joined interactive sessions and are
|
|
299
|
-
closed on shutdown. Teammates observing a disconnected peer holding a claim
|
|
300
|
-
sweep it so its requester receives an interrupted result instead of waiting
|
|
301
|
-
forever.
|
|
302
|
-
|
|
303
|
-
Ownership tokens fence out replaced sessions. Pending messages survive disconnection.
|
|
304
|
-
Claimed work is marked interrupted on disconnect/rejoin; it is **not automatically
|
|
305
|
-
replayed**, since edits may already have happened. This favors avoiding duplicate
|
|
306
|
-
side effects over guaranteed execution: a crash after claiming but before starting
|
|
307
|
-
can also leave an interrupted task. There is no exactly-once guarantee for filesystem
|
|
308
|
-
changes or model actions. If storage cannot record a result, reception pauses and
|
|
309
|
-
reports an error; review before retrying.
|
|
310
|
-
|
|
311
|
-
Membership, pause state, and pending task-boundary compaction are recorded in Pi
|
|
312
|
-
session entries. Resuming the same session can rejoin; `/new` and `/fork` do not
|
|
313
|
-
inherit membership. Explicit leave clears restoration. Before attempting to claim
|
|
314
|
-
work, the extension records that restoration must pause, without pausing the live
|
|
315
|
-
session. Successful result persistence clears this recovery-only pause; failures
|
|
316
|
-
and interruptions retain it. If shutdown interrupts a post-task compaction, the
|
|
317
|
-
same session retries compaction before claiming queued peer work.
|
|
318
|
-
|
|
319
|
-
Thus even an abrupt process death during a task restores paused and requires
|
|
320
|
-
`/team resume` before pending work starts. A crash just before a claim can
|
|
321
|
-
conservatively require resume too. History and pending work remain in the team
|
|
322
|
-
until explicitly managed outside this prototype.
|
|
323
|
-
|
|
324
|
-
Local disks only: shared network filesystems, containers with separate home
|
|
325
|
-
directories, cross-machine transport, and native Windows are not supported here.
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
## Manage the package
|
|
329
|
-
|
|
330
|
-
For an npm installation:
|
|
108
|
+
terminals; they require membership you established. A request returns **queued**,
|
|
109
|
+
never "task completed".
|
|
331
110
|
|
|
332
|
-
|
|
333
|
-
pi list
|
|
334
|
-
pi update npm:@prjct.app/pi-team
|
|
335
|
-
pi remove npm:@prjct.app/pi-team
|
|
336
|
-
```
|
|
111
|
+
## How delivery works
|
|
337
112
|
|
|
338
|
-
|
|
113
|
+
A request or result starts a turn only when the recipient is idle, has a selected
|
|
114
|
+
model, no pending user message or open prompt, an empty editor, and no
|
|
115
|
+
task-boundary compaction running. No running tool is interrupted.
|
|
339
116
|
|
|
340
|
-
|
|
117
|
+
For each processed request the extension sends **one** result after the agent
|
|
118
|
+
settles: the last assistant text capped at 3,000 characters (no thinking), up to
|
|
119
|
+
50 absolute paths observed in successful `edit`/`write` calls, and an outcome of
|
|
120
|
+
completed, failed, or interrupted.
|
|
341
121
|
|
|
342
|
-
|
|
122
|
+
That file list is **not** a Git diff. Files changed through bash, custom tools, or
|
|
123
|
+
other processes are not enumerated, and the extension never infers test success
|
|
124
|
+
from a shell command or a model claim. **A completed run is not proof of success**:
|
|
125
|
+
review the reported outcome and the recipient worktree.
|
|
343
126
|
|
|
344
|
-
|
|
127
|
+
After each task the session compacts before accepting another, so independent
|
|
128
|
+
sessions stay focused. While a request you emitted stays unresolved past five
|
|
129
|
+
minutes, a review turn asks your agent to chase the teammate or tell you what is
|
|
130
|
+
blocked. See [Architecture](docs/architecture.md) for both mechanisms.
|
|
345
131
|
|
|
346
|
-
|
|
347
|
-
| --- | --- |
|
|
348
|
-
| A request stays queued | Run `/team status` to identify its requester, assignee, subject, and assignee presence. The recipient may be busy, paused, offline, missing a selected model, or typing in its editor. After five minutes, automatic review turns chase the teammate or surface the blockage to you. |
|
|
349
|
-
| `Team auto-turn limit reached` | Five automatic peer turns ran without user input. Review the transcript, then `/team resume`. |
|
|
350
|
-
| Automatic context compaction failed | The mailbox result was already persisted. Reception continues, and Pi can retry through its normal threshold compaction or `/compact`. |
|
|
351
|
-
| `Membership expired or replaced` | Another live session took your alias, or your membership was fenced out. Rejoin with `/team join <team> <alias>`; choose a new alias if the old one is in use. |
|
|
352
|
-
| `Recipient inbox full` / `Sender inbox full` | Fifty unsettled deliveries per member, with one slot reserved per outstanding request. Let the teammate drain its queue; notes are exempt from reply reservations. |
|
|
353
|
-
| `Team history full (500 records)` | The team is at capacity; history is never silently deleted. Create a fresh team and rejoin. |
|
|
354
|
-
| Repeated storage warnings | Transient read/write conflicts are retried automatically and never pause reception. If the same warning persists, check that the teams directory is a local disk and report the issue. |
|
|
355
|
-
| A teammate went offline mid-task | Its claimed work is interrupted and the emitter receives that result; it is not replayed automatically. Review the worktree, then resend explicitly if still needed. |
|
|
132
|
+
## Safety
|
|
356
133
|
|
|
357
|
-
|
|
134
|
+
- **This is not a sandbox or an authorization system.** Agents and processes under
|
|
135
|
+
the same OS user already have filesystem access. Prompt-level rules are not a
|
|
136
|
+
hard guarantee against a model that ignores them. Do not place untrusted agents
|
|
137
|
+
in a team or rely on team boundaries to protect secrets from that OS user.
|
|
138
|
+
- Peer messages are identified as untrusted data and **are not user consent**.
|
|
139
|
+
Agents are instructed not to relay denied work, change configuration, or evade
|
|
140
|
+
plan mode. Peer text is never executed as a slash command or expanded as a file
|
|
141
|
+
mention.
|
|
142
|
+
- Messages stay in local files, but when processed their text goes to the
|
|
143
|
+
recipient's configured **model provider** like any prompt, and results go to the
|
|
144
|
+
requester. Do not send credentials or unrelated secrets.
|
|
145
|
+
- User takeover during a peer task pauses reception and reports an interrupted
|
|
146
|
+
result instead of forwarding your unrelated work.
|
|
147
|
+
|
|
148
|
+
## Limits
|
|
149
|
+
|
|
150
|
+
| Limit | Value |
|
|
151
|
+
| --- | --- |
|
|
152
|
+
| Automatic peer turns before reception pauses | 5, then `/team resume` |
|
|
153
|
+
| Messages in one automatically linked conversation | 8 non-result |
|
|
154
|
+
| Unsettled deliveries per member | 50 slots, one reserved per outstanding request |
|
|
155
|
+
| Records per team | 500, including reserved result capacity |
|
|
156
|
+
| Members per team | 100 |
|
|
157
|
+
| Outgoing body / serialized message | 16 KB / 20 KB |
|
|
158
|
+
| Automatic result | 32 KB, file list shortened with a notice |
|
|
159
|
+
| Presence lease | 30 s, renewed every 2 s |
|
|
160
|
+
| Review threshold / cadence | 5 min unresolved, checked every 1 min |
|
|
161
|
+
| Duplicate suppression | identical message within 1 min is refused |
|
|
162
|
+
|
|
163
|
+
No daily token or monetary budget is enforced. History is never silently deleted:
|
|
164
|
+
create a fresh team when one is full. Review cadence and the turn budget are fixed
|
|
165
|
+
defaults, not user-configurable yet.
|
|
358
166
|
|
|
359
|
-
|
|
167
|
+
## Troubleshooting
|
|
360
168
|
|
|
361
|
-
|
|
169
|
+
| Symptom | Cause and action |
|
|
170
|
+
| --- | --- |
|
|
171
|
+
| A request stays queued | Run `/team status`. The recipient may be busy, paused, offline, missing a model, or typing. After five minutes, review turns chase it or surface the blockage. |
|
|
172
|
+
| `Team auto-turn limit reached` | Five automatic turns ran without user input. Review the transcript, then `/team resume`. |
|
|
173
|
+
| Automatic compaction failed | The result was already persisted. Reception continues; Pi can still compact normally or via `/compact`. |
|
|
174
|
+
| `Membership expired or replaced` | Another live session took your alias. Rejoin, choosing a new alias if the old one is in use. |
|
|
175
|
+
| `Recipient inbox full` / `Sender inbox full` | Fifty unsettled deliveries per member, one slot reserved per outstanding request. Let the teammate drain; notes need no reservation. |
|
|
176
|
+
| `Team history full (500 records)` | At capacity; history is never deleted. Create a fresh team and rejoin. |
|
|
177
|
+
| Repeated storage warnings | Conflicts retry automatically and never pause reception. If one persists, check that the teams directory is on a local disk and report it. |
|
|
178
|
+
| A teammate went offline mid-task | Its claimed work is interrupted and the emitter receives that result; it is not replayed. Review the worktree, then resend explicitly. |
|
|
362
179
|
|
|
363
180
|
## Development
|
|
364
181
|
|
|
365
|
-
From a repository checkout:
|
|
366
|
-
|
|
367
182
|
```sh
|
|
368
183
|
npm ci --ignore-scripts
|
|
369
184
|
npm run check
|
|
@@ -371,8 +186,16 @@ npm test
|
|
|
371
186
|
npm run check:package
|
|
372
187
|
```
|
|
373
188
|
|
|
374
|
-
Pi loads the TypeScript entry point directly; no build step
|
|
189
|
+
Pi loads the TypeScript entry point directly; there is no build step. Use `pi -e .`
|
|
190
|
+
to try a checkout. Tests use isolated temporary state and never call model APIs.
|
|
191
|
+
|
|
192
|
+
`npm run check` also enforces that `src/` contains no `let`: session state is a
|
|
193
|
+
single immutable record updated functionally, and reads go through its accessor at
|
|
194
|
+
the point of use rather than being captured across an `await`.
|
|
195
|
+
|
|
196
|
+
## More
|
|
375
197
|
|
|
376
|
-
|
|
198
|
+
- [Architecture](docs/architecture.md) — storage, concurrency, recovery, context budget
|
|
199
|
+
- [Package structure](docs/package.md) · [Releases](docs/releases.md) · [Contributing](CONTRIBUTING.md) · [Changelog](CHANGELOG.md)
|
|
377
200
|
|
|
378
201
|
[MIT](LICENSE).
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
How pi-team stores state, coordinates concurrent sessions, and recovers from
|
|
4
|
+
failure. For everyday use see the [README](../README.md).
|
|
5
|
+
|
|
6
|
+
## Storage model
|
|
7
|
+
|
|
8
|
+
Each team is one JSON record at `~/.pi/agent/teams/<team>/state.json`, honouring
|
|
9
|
+
`PI_CODING_AGENT_DIR`. The record holds the member roster and the full message
|
|
10
|
+
history. Team folders are `0700` and files `0600`; an unsafe, symlinked, or
|
|
11
|
+
foreign-owned directory or record fails closed rather than being repaired.
|
|
12
|
+
|
|
13
|
+
Records are wrapped in an envelope carrying a schema marker, a monotonically
|
|
14
|
+
increasing revision, and a SHA-256 of the payload. A corrupt record is preserved
|
|
15
|
+
for manual recovery, never erased. Mailboxes written before envelopes existed
|
|
16
|
+
are read as revision 0 and rewritten as envelope records on their first write.
|
|
17
|
+
|
|
18
|
+
## Concurrency
|
|
19
|
+
|
|
20
|
+
Readers never take a lock. Every publication writes a private temporary file,
|
|
21
|
+
hard-links it into a bounded `revisions/` history, and atomically renames it into
|
|
22
|
+
place, so a concurrent read sees either the whole previous record or the whole
|
|
23
|
+
next one. The history doubles as recovery evidence for an interrupted write.
|
|
24
|
+
|
|
25
|
+
Writers compare-and-swap on the revision under a short-lived sibling lock.
|
|
26
|
+
A conflict fails fast and the caller retries against a fresh read, so many agents
|
|
27
|
+
write concurrently instead of queueing behind a team-wide lock. A lock abandoned
|
|
28
|
+
by a crashed writer is reclaimed after ten seconds.
|
|
29
|
+
|
|
30
|
+
Because every publication renames a **new inode** into place, readers can safely
|
|
31
|
+
cache a parsed record keyed on `(inode, size, mtime)`: a write by any process
|
|
32
|
+
changes the inode. Cached reads serve the polling loop; mutations always read
|
|
33
|
+
uncached, so they never operate on the shared cached object.
|
|
34
|
+
|
|
35
|
+
## Presence
|
|
36
|
+
|
|
37
|
+
Presence lives outside the shared record. Each member rewrites only its own
|
|
38
|
+
`presence/<alias>.json` every two seconds, so heartbeats add no write contention
|
|
39
|
+
and never touch `state.json`.
|
|
40
|
+
|
|
41
|
+
These files are deliberately non-durable: the atomic rename is kept, both fsyncs
|
|
42
|
+
are not. Presence expires after 30 seconds and is rewritten every 2, so a write
|
|
43
|
+
lost to a crash only makes a member look offline sooner — never alive longer.
|
|
44
|
+
A member is also considered gone as soon as its recorded process has exited.
|
|
45
|
+
|
|
46
|
+
## Request lifecycle
|
|
47
|
+
|
|
48
|
+
A request is queued, claimed when the recipient is idle, worked on, and settled
|
|
49
|
+
with a result delivered back to the emitter. The recipient compacts before
|
|
50
|
+
accepting another team turn; the emitter verifies the result against its original
|
|
51
|
+
request and replies in-thread only if something is missing.
|
|
52
|
+
|
|
53
|
+
States are `pending`, `processing`, `completed`, `interrupted`, and `seen`
|
|
54
|
+
(notes already displayed).
|
|
55
|
+
|
|
56
|
+
## Sweeping orphaned claims
|
|
57
|
+
|
|
58
|
+
A session that dies holding a claim would otherwise leave its requester waiting
|
|
59
|
+
forever, so peers interrupt the claim on its behalf and the requester receives an
|
|
60
|
+
`interrupted` result.
|
|
61
|
+
|
|
62
|
+
Members are never removed from the record — leaving only marks them offline — so
|
|
63
|
+
"someone is offline" is permanently true once anyone has ever left and cannot be
|
|
64
|
+
used to decide when to sweep. A snapshot instead reports `sweepable`, true only
|
|
65
|
+
when a member the record still counts as connected is actually dead **and** still
|
|
66
|
+
holds a claim, which is the only case where sweeping changes anything. Everything
|
|
67
|
+
else degrades correctly without it: rejoining re-admits a stale alias on its own,
|
|
68
|
+
and displayed status comes from presence rather than the record.
|
|
69
|
+
|
|
70
|
+
## Context budget
|
|
71
|
+
|
|
72
|
+
What an extension puts in the model context is paid on every later turn and
|
|
73
|
+
stays in the session branch, so injected values are bounded and any elision is
|
|
74
|
+
stated rather than silent.
|
|
75
|
+
|
|
76
|
+
| Carrier | In LLM context | Used for |
|
|
77
|
+
| --- | --- | --- |
|
|
78
|
+
| `before_agent_start` system prompt | yes, every turn | peer rules and team identity, carried exactly once |
|
|
79
|
+
| `sendMessage` content | yes, and it persists | peer messages and review turns |
|
|
80
|
+
| Tool results | yes, and they persist | `team_members`, `team_send`, `team_status` |
|
|
81
|
+
| `appendEntry` | **no**, TUI only | notes, `/team inbox`, sent-message previews |
|
|
82
|
+
|
|
83
|
+
Consequences worth knowing:
|
|
84
|
+
|
|
85
|
+
- Peer rules are **not** repeated inside each message; they arrive once per turn
|
|
86
|
+
through the system prompt.
|
|
87
|
+
- A delivered result quotes the original request excerpted to 500 characters,
|
|
88
|
+
with the id kept. Verification of a very long request works from an extract.
|
|
89
|
+
- `team_status` lists are capped and report an `omitted` count. Its
|
|
90
|
+
`otherTeamWork` list carries only third-party work; anything addressed to or
|
|
91
|
+
emitted by this session is already in the other lists.
|
|
92
|
+
- Each `team_status` call is a point-in-time snapshot. Earlier results in the
|
|
93
|
+
same conversation are stale but cannot be retracted, which is why each one is
|
|
94
|
+
kept small.
|
|
95
|
+
|
|
96
|
+
## Task-boundary compaction
|
|
97
|
+
|
|
98
|
+
Once a result is safely persisted, the extension calls Pi's `ctx.compact()`. That
|
|
99
|
+
session claims no other peer message while compaction runs. The instructions
|
|
100
|
+
preserve user-authored goals and constraints, team identity, unresolved
|
|
101
|
+
requester → assignee relationships, outcomes, blockers, files, tests, and next
|
|
102
|
+
actions, and discard verbose tool output, duplicated payloads, completed traces,
|
|
103
|
+
and private reasoning.
|
|
104
|
+
|
|
105
|
+
Compaction changes model context, not extension registration or mailbox state:
|
|
106
|
+
`/team` commands and tools stay available, and each terminal remains an
|
|
107
|
+
independent Pi session rather than a spawned subagent. Pi still applies its
|
|
108
|
+
configured `keepRecentTokens`. If compaction fails, the TUI warns and reception
|
|
109
|
+
continues. User takeover skips it, because the turn is no longer an isolated team
|
|
110
|
+
task.
|
|
111
|
+
|
|
112
|
+
## Recovery and guarantees
|
|
113
|
+
|
|
114
|
+
Ownership tokens fence out replaced sessions. Pending messages survive
|
|
115
|
+
disconnection. Claimed work is marked interrupted on disconnect or rejoin and is
|
|
116
|
+
**not automatically replayed**, since edits may already have happened.
|
|
117
|
+
|
|
118
|
+
This favours avoiding duplicate side effects over guaranteed execution. **There is
|
|
119
|
+
no exactly-once guarantee** for filesystem changes or model actions: a crash after
|
|
120
|
+
claiming but before starting also leaves an interrupted task. If storage cannot
|
|
121
|
+
record a result, reception pauses and reports an error.
|
|
122
|
+
|
|
123
|
+
Membership, pause state, and pending compaction are recorded in Pi session
|
|
124
|
+
entries. Resuming the same session rejoins; `/new` and `/fork` do not inherit
|
|
125
|
+
membership. Before claiming work the extension records that restoration must
|
|
126
|
+
pause, without pausing the live session, so an abrupt process death during a task
|
|
127
|
+
restores paused and requires `/team resume`. A crash just before a claim can
|
|
128
|
+
conservatively require resume too.
|
|
129
|
+
|
|
130
|
+
Directory watchers provide prompt delivery; polling every two seconds recovers
|
|
131
|
+
missed notifications. Both run only for joined interactive sessions and close on
|
|
132
|
+
shutdown. Transient storage errors are reported but never pause reception.
|
|
133
|
+
|
|
134
|
+
## Design decisions and non-goals
|
|
135
|
+
|
|
136
|
+
| Concern | Choice |
|
|
137
|
+
| --- | --- |
|
|
138
|
+
| Session creation | The user opens every terminal; nothing is spawned |
|
|
139
|
+
| Discovery | Explicit named teams and aliases, never inferred from directories |
|
|
140
|
+
| Transport | Local filesystem records; no broker, socket server, or daemon |
|
|
141
|
+
| Presence | Per-member files outside the record; heartbeats never lock |
|
|
142
|
+
| Concurrent writes | Compare-and-swap with retry; no team-wide lock |
|
|
143
|
+
| Follow-up | Periodic review turns delegated to the agent, not programmatic retries |
|
|
144
|
+
| Active recipient | Wait until fully idle; no steering between tools |
|
|
145
|
+
| Offline recipient | Persist to a known alias until it rejoins |
|
|
146
|
+
| Approval | Never supplied by peers; local policies always win |
|
|
147
|
+
| Coordination | Direct messages and bulk check-ins; no task board or worktree manager |
|
|
148
|
+
| Scope | Local disks only: no network filesystems, cross-machine transport, or native Windows |
|
|
149
|
+
|
|
150
|
+
Not provided: file ownership between agents, a sandbox, an authorization system,
|
|
151
|
+
or protection of secrets from other processes under the same OS user.
|
|
152
|
+
|
|
153
|
+
## Pi interfaces used
|
|
154
|
+
|
|
155
|
+
`registerCommand`, `registerTool`, `sendMessage`, `appendEntry`, custom entry and
|
|
156
|
+
message renderers, `setWidget`, `getEditorText`, `isIdle`, `hasPendingMessages`,
|
|
157
|
+
`compact`, session lifecycle events, UI prompt events, `tool_result`,
|
|
158
|
+
`message_end`, and `agent_settled`. All public and documented; no host internals
|
|
159
|
+
are imported, no prototypes patched, and peer text is never shell-evaluated or
|
|
160
|
+
expanded as file mentions.
|
package/docs/package.md
CHANGED
|
@@ -2,57 +2,40 @@
|
|
|
2
2
|
|
|
3
3
|
## Identity
|
|
4
4
|
|
|
5
|
-
- npm name: `@prjct.app/pi-team
|
|
6
|
-
-
|
|
7
|
-
-
|
|
8
|
-
- Tested host: Pi `0.85.1`; Node.js `22.19+`.
|
|
9
|
-
|
|
10
|
-
The npm name and repository name may differ. Repository URLs remain unchanged. Existing runtime command names, event names, persisted entry types, and settings keys are unchanged by the package rename.
|
|
11
|
-
|
|
12
|
-
## Resource manifest
|
|
13
|
-
|
|
14
|
-
```json
|
|
15
|
-
{
|
|
16
|
-
"name": "@prjct.app/pi-team",
|
|
17
|
-
"keywords": [
|
|
18
|
-
"pi-package"
|
|
19
|
-
],
|
|
20
|
-
"pi": {
|
|
21
|
-
"extensions": [
|
|
22
|
-
"./index.ts"
|
|
23
|
-
]
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
The `pi-package` keyword makes the package discoverable. Manifest paths are relative to the package root. The extension entry point is shipped as TypeScript because Pi loads it directly. There is no CLI binary or JavaScript build artifact to install separately.
|
|
5
|
+
- npm name: `@prjct.app/pi-team`
|
|
6
|
+
- Repository: [prjct-app/pi-team](https://github.com/prjct-app/pi-team)
|
|
7
|
+
- Tested host: Pi `0.85.1`; Node.js `22.19+`
|
|
29
8
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
Pi-provided libraries imported by this package are declared in `peerDependencies` with `*`, as required by Pi's package guide. They are not bundled. Exact Pi 0.85.1 development dependencies establish the tested baseline; the peer wildcard is not a claim that every Pi release is supported.
|
|
33
|
-
|
|
34
|
-
Third-party runtime dependencies belong in `dependencies`. Companion extensions are installed separately only when communication uses Pi's event bus; this package does not import code from a separately installed companion. A package that directly imports another Pi package's resources must instead bundle it following the official guide.
|
|
9
|
+
The npm name and repository name may differ. The package rename changed no
|
|
10
|
+
runtime command names, event names, persisted entry types, or settings keys.
|
|
35
11
|
|
|
36
|
-
##
|
|
12
|
+
## Dependencies
|
|
37
13
|
|
|
38
|
-
|
|
14
|
+
Pi-provided libraries are declared in `peerDependencies` with `*`, as the Pi
|
|
15
|
+
package guide requires, and are not bundled. The exact Pi 0.85.1 devDependencies
|
|
16
|
+
establish the tested baseline; the peer wildcard is not a claim that every Pi
|
|
17
|
+
release is supported. This package has **no runtime dependencies**: storage is
|
|
18
|
+
self-contained.
|
|
39
19
|
|
|
40
|
-
|
|
20
|
+
Third-party runtime dependencies would belong in `dependencies`. A package that
|
|
21
|
+
directly imports another Pi package's resources must bundle it instead.
|
|
41
22
|
|
|
42
|
-
|
|
23
|
+
## Publishing
|
|
43
24
|
|
|
44
|
-
|
|
25
|
+
The `files` allowlist ships runtime resources, user documentation, and licence
|
|
26
|
+
files; tests, dependencies, and repository settings are excluded. Run
|
|
27
|
+
`npm run check:package` to inspect the exact prospective tarball, and confirm every
|
|
28
|
+
manifest entry and referenced local document is present. Only claim npm
|
|
29
|
+
availability after verifying a successful registry publication.
|
|
45
30
|
|
|
46
31
|
## Official references
|
|
47
32
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
- [Packages: manifest, sources, dependencies, filtering, and deduplication](https://github.com/earendil-works/pi/blob/v0.85.1/packages/coding-agent/docs/packages.md).
|
|
51
|
-
- [Extensions: lifecycle, commands, tools, messages, and UI APIs](https://github.com/earendil-works/pi/blob/v0.85.1/packages/coding-agent/docs/extensions.md).
|
|
52
|
-
- [TUI: components, rendering, terminal widths, and image support](https://github.com/earendil-works/pi/blob/v0.85.1/packages/coding-agent/docs/tui.md).
|
|
53
|
-
|
|
54
|
-
The installed `@earendil-works/pi-coding-agent@0.85.1` package ships the same guides under `docs/`. The [current official guide](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/packages.md) may describe changes beyond this tested baseline.
|
|
33
|
+
Pinned to the tested version rather than the moving main branch:
|
|
55
34
|
|
|
56
|
-
|
|
35
|
+
- [Packages](https://github.com/earendil-works/pi/blob/v0.85.1/packages/coding-agent/docs/packages.md) — manifest, sources, dependencies, deduplication
|
|
36
|
+
- [Extensions](https://github.com/earendil-works/pi/blob/v0.85.1/packages/coding-agent/docs/extensions.md) — lifecycle, commands, tools, messages, UI
|
|
37
|
+
- [TUI](https://github.com/earendil-works/pi/blob/v0.85.1/packages/coding-agent/docs/tui.md) — components, rendering, widths
|
|
57
38
|
|
|
58
|
-
The
|
|
39
|
+
The installed `@earendil-works/pi-coding-agent@0.85.1` ships the same guides under
|
|
40
|
+
`docs/`. The [current guide](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/packages.md)
|
|
41
|
+
may describe changes beyond this tested baseline.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prjct.app/pi-team",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.6",
|
|
4
4
|
"description": "Coordinate independent PI Agent sessions with local team messaging, queued tasks, and shared results.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -63,10 +63,10 @@
|
|
|
63
63
|
"index.ts",
|
|
64
64
|
"src",
|
|
65
65
|
"docs",
|
|
66
|
+
"!docs/cover.png",
|
|
66
67
|
"README.md",
|
|
67
68
|
"CONTRIBUTING.md",
|
|
68
69
|
"LICENSE",
|
|
69
|
-
"CHANGELOG.md"
|
|
70
|
-
"docs/package.md"
|
|
70
|
+
"CHANGELOG.md"
|
|
71
71
|
]
|
|
72
72
|
}
|
package/src/mailbox.ts
CHANGED
|
@@ -150,15 +150,22 @@ export class Mailbox {
|
|
|
150
150
|
for (const attempt of ATTEMPTS) {
|
|
151
151
|
const record = await this.readState(team);
|
|
152
152
|
const state = record.payload;
|
|
153
|
-
|
|
153
|
+
// The parser already produced this for the content hash. A pre-envelope
|
|
154
|
+
// record carries none, so that path serializes as before. A mismatch
|
|
155
|
+
// could only ever cause one redundant publication, never a lost write:
|
|
156
|
+
// `before` comes from the pre-action object and `after` from the
|
|
157
|
+
// post-action one, so they cannot coincide by accident.
|
|
158
|
+
const before = record.payloadJson ?? JSON.stringify(state);
|
|
154
159
|
const presence = await this.readPresence(team);
|
|
155
160
|
const swept = state.members.filter(member => member.status !== 'offline' && !this.alive(member, presence));
|
|
156
161
|
for (const member of swept) this.disconnect(state, member);
|
|
157
162
|
const result = action(state);
|
|
158
|
-
|
|
163
|
+
const after = JSON.stringify(state);
|
|
164
|
+
if (before === after) return result;
|
|
159
165
|
if (!Value.Check(StateSchema, state)) throw new Error('Invalid mailbox format; refusing to write');
|
|
160
166
|
try {
|
|
161
|
-
await publish(this.recordPath(team), record.revision, state, this.normalize(team),
|
|
167
|
+
await publish(this.recordPath(team), record.revision, state, this.normalize(team),
|
|
168
|
+
{ maxBytes: MAX_BYTES, payloadJson: after });
|
|
162
169
|
await Promise.all(swept.map(member => unlink(this.presencePath(team, member.alias)).catch(() => {})));
|
|
163
170
|
return result;
|
|
164
171
|
} catch (error) {
|
|
@@ -223,7 +230,14 @@ export class Mailbox {
|
|
|
223
230
|
await this.writePresence(member, status);
|
|
224
231
|
}
|
|
225
232
|
|
|
226
|
-
/**
|
|
233
|
+
/**
|
|
234
|
+
* Lock-free consistent view of the record with presence-based statuses.
|
|
235
|
+
*
|
|
236
|
+
* The cached read returns a shared record, and the message objects below are
|
|
237
|
+
* the record's own, not copies. Never mutate them: it would corrupt both the
|
|
238
|
+
* process-wide cache and the `payloadJson` taken alongside it. `mutate` is
|
|
239
|
+
* safe because it reads uncached.
|
|
240
|
+
*/
|
|
227
241
|
async snapshot(member: Membership): Promise<Snapshot> {
|
|
228
242
|
const record = await this.readState(member.team, true);
|
|
229
243
|
this.owner(record.payload, member);
|
package/src/store.ts
CHANGED
|
@@ -14,7 +14,13 @@ import { dirname, join } from 'node:path';
|
|
|
14
14
|
* hard-links its envelope into a bounded revisions/ history, which doubles as
|
|
15
15
|
* recovery evidence for interrupted writes.
|
|
16
16
|
*/
|
|
17
|
-
|
|
17
|
+
/**
|
|
18
|
+
* `payloadJson` is the canonical `JSON.stringify(payload)`, carried when the
|
|
19
|
+
* parser already had to compute it for the content hash. It is consistent by
|
|
20
|
+
* construction, never derived from the raw file text, and always optional: a
|
|
21
|
+
* pre-envelope record has none and callers fall back to serializing.
|
|
22
|
+
*/
|
|
23
|
+
export type Record<T> = { revision: number; payload: T; payloadJson?: string };
|
|
18
24
|
/** Parse raw file bytes into a record, throwing on corruption. Never deletes. */
|
|
19
25
|
export type Normalize<T> = (raw: string) => Record<T>;
|
|
20
26
|
/**
|
|
@@ -46,10 +52,11 @@ export function envelope<T>(raw: string): Record<T> {
|
|
|
46
52
|
if (typeof parsed.revision !== 'number' || !Number.isSafeInteger(parsed.revision) || parsed.revision < 1) {
|
|
47
53
|
throw Object.assign(new Error('Invalid record revision.'), { code: 'CORRUPT_RECORD' });
|
|
48
54
|
}
|
|
49
|
-
|
|
55
|
+
const payloadJson = JSON.stringify(parsed.payload);
|
|
56
|
+
if (parsed.contentHash !== sha256(payloadJson)) {
|
|
50
57
|
throw Object.assign(new Error('Record hash mismatch; preserved for manual recovery.'), { code: 'CORRUPT_RECORD' });
|
|
51
58
|
}
|
|
52
|
-
return { revision: parsed.revision, payload: parsed.payload as T };
|
|
59
|
+
return { revision: parsed.revision, payload: parsed.payload as T, payloadJson };
|
|
53
60
|
}
|
|
54
61
|
|
|
55
62
|
function assertSafeFile(path: string, info: { isFile(): boolean; size: number; mode: number; uid: number }, maxBytes: number): void {
|
|
@@ -162,7 +169,7 @@ async function pruneRevisions(dir: string, latest: number): Promise<void> {
|
|
|
162
169
|
*/
|
|
163
170
|
export async function publish<T>(
|
|
164
171
|
path: string, expectedRevision: number, payload: T, normalize: Normalize<T>,
|
|
165
|
-
options: { maxBytes: number; durability?: Durability },
|
|
172
|
+
options: { maxBytes: number; durability?: Durability; payloadJson?: string },
|
|
166
173
|
): Promise<Record<T>> {
|
|
167
174
|
const durability = options.durability ?? 'full';
|
|
168
175
|
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
@@ -175,12 +182,15 @@ export async function publish<T>(
|
|
|
175
182
|
throw Object.assign(new Error(`Record changed before the write; current revision is ${revision}.`), { code: 'STALE_REVISION' });
|
|
176
183
|
}
|
|
177
184
|
const next = revision + 1;
|
|
178
|
-
|
|
185
|
+
// Supplied by callers that already serialized this exact object; it must
|
|
186
|
+
// equal JSON.stringify(payload). The record stays self-consistent either
|
|
187
|
+
// way, because the hash is taken over the string that gets embedded.
|
|
188
|
+
const payloadJson = options.payloadJson ?? JSON.stringify(payload);
|
|
179
189
|
const text = `{"schemaVersion":1,"revision":${next},"contentHash":"${sha256(payloadJson)}","payload":${payloadJson}}`;
|
|
180
190
|
if (Buffer.byteLength(text) > options.maxBytes) throw new Error('Record size limit exceeded.');
|
|
181
191
|
const historyPath = join(dirname(path), 'revisions', `${next}.json`);
|
|
182
192
|
const previous = await readRecord(historyPath, (raw: string) => envelope<T>(raw), options.maxBytes);
|
|
183
|
-
if (previous && (previous.revision !== next ||
|
|
193
|
+
if (previous && (previous.revision !== next || (previous.payloadJson ?? JSON.stringify(previous.payload)) !== payloadJson)) {
|
|
184
194
|
throw new Error('An interrupted publication owns this revision; explicit recovery is required.');
|
|
185
195
|
}
|
|
186
196
|
if (!previous) await writeAtomic(historyPath, text, durability);
|
|
@@ -195,7 +205,7 @@ export async function publish<T>(
|
|
|
195
205
|
cache.delete(path);
|
|
196
206
|
counters.publishes++;
|
|
197
207
|
await pruneRevisions(dirname(path), next).catch(() => {});
|
|
198
|
-
return { revision: next, payload };
|
|
208
|
+
return { revision: next, payload, payloadJson };
|
|
199
209
|
} finally {
|
|
200
210
|
await lock.close();
|
|
201
211
|
await unlink(lockPath).catch(() => {});
|
package/docs/cover.png
DELETED
|
Binary file
|
package/docs/reference.md
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
# Reference and deliberate differences
|
|
2
|
-
|
|
3
|
-
Verified against the live official Markdown documentation during implementation:
|
|
4
|
-
|
|
5
|
-
- https://code.claude.com/docs/en/cross-session-messaging.md
|
|
6
|
-
- https://code.claude.com/docs/en/agent-teams.md
|
|
7
|
-
|
|
8
|
-
The closest reference is **cross-session messaging**: independently started sessions,
|
|
9
|
-
discovery with `ListAgents`, communication with `SendMessage`, compact expandable
|
|
10
|
-
previews, and per-session permission boundaries. Current documentation says local
|
|
11
|
-
Claude Code sessions use per-session sockets/named pipes. The team feature adds a
|
|
12
|
-
lead that spawns teammates, optional shared task coordination, and JSON mailboxes.
|
|
13
|
-
These are distinct features; historical `TeamCreate` examples are not current.
|
|
14
|
-
|
|
15
|
-
Our user-selected scope differs deliberately:
|
|
16
|
-
|
|
17
|
-
| Concern | pi-team |
|
|
18
|
-
| --- | --- |
|
|
19
|
-
| Session creation | User opens all terminals |
|
|
20
|
-
| Discovery | Explicit named team and aliases |
|
|
21
|
-
| Transport | Local filesystem records with optimistic concurrency, no broker or socket server |
|
|
22
|
-
| Presence | Per-member files outside the shared record; heartbeats never lock |
|
|
23
|
-
| Concurrent writes | Compare-and-swap revisions with retry; no team-wide lock |
|
|
24
|
-
| Follow-up | Periodic review turns delegated to the agent, not programmatic retries |
|
|
25
|
-
| Active recipient | Wait until fully idle; no between-tool steering |
|
|
26
|
-
| Offline recipient | Persist to a known alias until rejoin |
|
|
27
|
-
| Results | Automatic last-text reply to requests, quoted against the original request |
|
|
28
|
-
| Approval | Never supplied by peers; preserve local policies |
|
|
29
|
-
| Coordination | Direct messages, `/team wake [message]` bulk check-ins, and task-boundary context compaction; no task board or worktree manager |
|
|
30
|
-
| Limits | Bounded conversations, inboxes and automatic turns |
|
|
31
|
-
| UI | Existing Pi loader plus a minimal session widget, on-demand requester → assignee flow through `/team status`, and expandable messages |
|
|
32
|
-
|
|
33
|
-
Pi APIs used: `registerCommand`, `registerTool`, `sendMessage`, custom entry/message
|
|
34
|
-
renderers, `setWidget`, `getEditorText`, `isIdle`, `hasPendingMessages`, `compact`,
|
|
35
|
-
session lifecycle, UI prompt events, `tool_result`, `message_end`, and `agent_settled`.
|
|
36
|
-
No monkey-patching of Pi internals, shell evaluation of peer messages, forwarding
|
|
37
|
-
of thinking, or modifications to existing local extensions are required.
|
|
38
|
-
|
|
39
|
-
Integration tests exercise the extension through a simulated Pi API, backed by real
|
|
40
|
-
filesystem operations. The three-process scenario hosts PM/backend/frontend in
|
|
41
|
-
separate Node processes, checks queued delivery and correlated responses, kills the
|
|
42
|
-
backend, and checks recovery. This is not a live three-model behavioral evaluation.
|