@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 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. |
@@ -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. A conflict fails fast and the caller retries against a fresh
29
- read, so many agents write concurrently instead of queueing behind a team-wide
30
- lock. A lock abandoned by a crashed writer is reclaimed after ten seconds.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prjct.app/pi-team",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "Coordinate independent PI Agent sessions with local team messaging, queued tasks, and shared results.",
5
5
  "type": "module",
6
6
  "keywords": [
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>): 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
- return withFileLock(options.lockPath ?? `${path}.lock`, () =>
229
- publishLocked(path, expectedRevision, payload, normalize, options));
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
  }