@prjct.app/pi-team 0.6.0 → 0.6.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/CHANGELOG.md +6 -0
- package/README.md +1 -0
- package/docs/architecture.md +8 -3
- package/package.json +1 -1
- package/src/store.ts +21 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
## [0.6.1](https://github.com/prjct-app/pi-team/compare/v0.6.0...v0.6.1) (2026-09-11)
|
|
2
|
+
|
|
3
|
+
### Bug Fixes
|
|
4
|
+
|
|
5
|
+
* preserve mailbox locks across rolling upgrades ([3509903](https://github.com/prjct-app/pi-team/commit/3509903026205a0a3d00fcd52eecb3b970ba4d7b))
|
|
6
|
+
|
|
1
7
|
## [0.6.0](https://github.com/prjct-app/pi-team/compare/v0.5.7...v0.6.0) (2026-09-10)
|
|
2
8
|
|
|
3
9
|
### Features
|
package/README.md
CHANGED
|
@@ -180,6 +180,7 @@ defaults, not user-configurable yet.
|
|
|
180
180
|
| --- | --- |
|
|
181
181
|
| 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. |
|
|
182
182
|
| `Team auto-turn limit reached` | Five automatic turns ran without user input. Review the transcript, then `/team resume`. |
|
|
183
|
+
| `Message not claimed by this session` | The durable claim changed before settlement. Review for partial effects, then `/reload` or leave and rejoin before `/team resume`. After updating pi-team, reload every live teammate so all sessions use the same runtime. |
|
|
183
184
|
| `Membership expired or replaced` | Another live session took your alias. Rejoin, choosing a new alias if the old one is in use. |
|
|
184
185
|
| `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. |
|
|
185
186
|
| `Team history full (500 records)` | At capacity; history is never deleted. Create a fresh team and rejoin. |
|
package/docs/architecture.md
CHANGED
|
@@ -25,9 +25,14 @@ next one. The history doubles as recovery evidence for an interrupted write.
|
|
|
25
25
|
Writers compare-and-swap on the revision under a short-lived per-team lock in
|
|
26
26
|
`~/.pi/agent/teams/.locks/`. Keeping the lock outside the team directory lets
|
|
27
27
|
rename and deletion fence stale publishers without allowing them to recreate a
|
|
28
|
-
moved directory.
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
moved directory. Normal publications also acquire the pre-0.6 compatibility
|
|
29
|
+
lock beside `state.json`, after the stable lock. This overlap is required while
|
|
30
|
+
sessions from both sides of the lock migration remain alive during a rolling
|
|
31
|
+
reload; without it, two versions could publish the same next revision and lose
|
|
32
|
+
a claim. The compatibility lock is opened without creating its parent, so a
|
|
33
|
+
stale writer still cannot resurrect a deleted team. A conflict fails fast and
|
|
34
|
+
the caller retries against a fresh read. A lock abandoned by a crashed writer
|
|
35
|
+
is reclaimed after ten seconds.
|
|
31
36
|
|
|
32
37
|
Because every publication renames a **new inode** into place, readers can safely
|
|
33
38
|
cache a parsed record keyed on `(inode, size, mtime)`: a write by any process
|
package/package.json
CHANGED
package/src/store.ts
CHANGED
|
@@ -151,8 +151,8 @@ async function acquireLock(lockPath: string) {
|
|
|
151
151
|
}
|
|
152
152
|
|
|
153
153
|
/** Run one storage operation while holding a caller-chosen private lock. */
|
|
154
|
-
export async function withFileLock<T>(lockPath: string, action: () => Promise<T
|
|
155
|
-
await mkdir(dirname(lockPath), { recursive: true, mode: 0o700 });
|
|
154
|
+
export async function withFileLock<T>(lockPath: string, action: () => Promise<T>, createParent = true): Promise<T> {
|
|
155
|
+
if (createParent) await mkdir(dirname(lockPath), { recursive: true, mode: 0o700 });
|
|
156
156
|
const lock = await acquireLock(lockPath);
|
|
157
157
|
try { return await action(); }
|
|
158
158
|
finally {
|
|
@@ -225,6 +225,23 @@ export async function publish<T>(
|
|
|
225
225
|
path: string, expectedRevision: number, payload: T, normalize: Normalize<T>,
|
|
226
226
|
options: { maxBytes: number; durability?: Durability; payloadJson?: string; lockPath?: string },
|
|
227
227
|
): Promise<Record<T>> {
|
|
228
|
-
|
|
229
|
-
|
|
228
|
+
const legacyLock = `${path}.lock`;
|
|
229
|
+
const primaryLock = options.lockPath ?? legacyLock;
|
|
230
|
+
// Releases before 0.6.0 lock the record path, while lifecycle-safe writers
|
|
231
|
+
// use the stable external team lock. During a rolling reload both versions
|
|
232
|
+
// can be alive, so new normal publications must intersect both lock sets or
|
|
233
|
+
// two writers can pass the revision check and publish the same next revision.
|
|
234
|
+
return withFileLock(primaryLock, async () => {
|
|
235
|
+
if (primaryLock === legacyLock) return publishLocked(path, expectedRevision, payload, normalize, options);
|
|
236
|
+
// Acquiring an in-directory compatibility lock must not recreate a team
|
|
237
|
+
// that a lifecycle operation deleted while this writer was waiting for the
|
|
238
|
+
// stable lock. Revision-zero creation is the only intentional exception.
|
|
239
|
+
const current = await readRecord(path, normalize, options.maxBytes);
|
|
240
|
+
if (!current && expectedRevision !== 0) {
|
|
241
|
+
throw Object.assign(new Error('Record changed before the write; current revision is 0.'), { code: 'STALE_REVISION' });
|
|
242
|
+
}
|
|
243
|
+
if (!current) await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
244
|
+
return withFileLock(legacyLock,
|
|
245
|
+
() => publishLocked(path, expectedRevision, payload, normalize, options), false);
|
|
246
|
+
});
|
|
230
247
|
}
|