@fitchmultz/pi-subagents 0.52.21 → 0.52.22
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
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## 0.52.22 - 2026-11-07
|
|
6
|
+
|
|
7
|
+
<!-- npm-release:unreleased:start -->
|
|
8
|
+
|
|
9
|
+
- Bind each durable wake reservation to one POST and its required settlement, preserving retryable settlement failures and crash uncertainty.
|
|
10
|
+
|
|
11
|
+
<!-- npm-release:unreleased:end -->
|
|
12
|
+
|
|
13
|
+
- refactor: bind wake reservations to one settled attempt (#366) ([9bd60f9](https://github.com/fitchmultz/pi-subagents/commit/9bd60f92a4630ca9300ed9c4c06bc48758077a40))
|
|
14
|
+
|
|
15
|
+
### What's Changed
|
|
16
|
+
|
|
17
|
+
- refactor: bind wake reservations to one settled attempt by @fitchmultz in https://github.com/fitchmultz/pi-subagents/pull/366
|
|
18
|
+
|
|
19
|
+
**Full Changelog**: https://github.com/fitchmultz/pi-subagents/compare/v0.52.21...v0.52.22
|
|
20
|
+
|
|
5
21
|
## 0.52.21 - 2026-11-07
|
|
6
22
|
|
|
7
23
|
<!-- npm-release:unreleased:start -->
|
package/dist/build-info.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const EXTENSION_BUILD = Object.freeze({"version":"0.52.
|
|
1
|
+
export const EXTENSION_BUILD = Object.freeze({"version":"0.52.22","sha256":"fc7e48d1fb96f143f56be9757d30a98bb138b94edd1f5388438ddeb3eed54638"});
|
|
@@ -28,7 +28,7 @@ export class WakeState {
|
|
|
28
28
|
inode;
|
|
29
29
|
device;
|
|
30
30
|
owner;
|
|
31
|
-
|
|
31
|
+
phase = { kind: "idle" };
|
|
32
32
|
constructor(stateDir) {
|
|
33
33
|
this.stateDir = stateDir;
|
|
34
34
|
this.path = join(stateDir, FILE);
|
|
@@ -194,13 +194,37 @@ export class WakeState {
|
|
|
194
194
|
this.validate();
|
|
195
195
|
this.db.prepare("DELETE FROM wake_batches WHERE bucket=?").run(bucket);
|
|
196
196
|
}
|
|
197
|
-
/** Persist quota and uncertainty
|
|
197
|
+
/** Persist quota and uncertainty before exposing a single-use POST reservation. */
|
|
198
198
|
reserve(bucket) {
|
|
199
199
|
this.retrySettlement();
|
|
200
|
-
|
|
200
|
+
if (this.phase.kind !== "idle") {
|
|
201
|
+
throw new Error("A wake reservation is already active or its owner is closed.");
|
|
202
|
+
}
|
|
203
|
+
const until = this.transaction(() => this.reservation(bucket));
|
|
204
|
+
if (until !== undefined) {
|
|
205
|
+
return { kind: "deferred", until };
|
|
206
|
+
}
|
|
207
|
+
const reservation = {
|
|
208
|
+
kind: "reserved",
|
|
209
|
+
post: async (action) => {
|
|
210
|
+
if (this.phase !== reservation) {
|
|
211
|
+
throw new Error("This wake reservation is no longer available.");
|
|
212
|
+
}
|
|
213
|
+
this.phase = { kind: "posting" };
|
|
214
|
+
try {
|
|
215
|
+
await action();
|
|
216
|
+
}
|
|
217
|
+
finally {
|
|
218
|
+
// Completion, rejection and abort all start the durable expiry window here.
|
|
219
|
+
this.settle();
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
this.phase = reservation;
|
|
224
|
+
return reservation;
|
|
201
225
|
}
|
|
202
226
|
retrySettlement() {
|
|
203
|
-
if (this.
|
|
227
|
+
if (this.phase.kind === "settlementPending") {
|
|
204
228
|
this.settle();
|
|
205
229
|
}
|
|
206
230
|
}
|
|
@@ -219,9 +243,9 @@ export class WakeState {
|
|
|
219
243
|
}
|
|
220
244
|
/** A failed settlement stays open and must be retried before this owner can dispatch again. */
|
|
221
245
|
settle() {
|
|
222
|
-
this.
|
|
246
|
+
this.phase = { kind: "settlementPending" };
|
|
223
247
|
this.transaction(() => this.finishOwner(this.owner.name));
|
|
224
|
-
this.
|
|
248
|
+
this.phase = { kind: "idle" };
|
|
225
249
|
}
|
|
226
250
|
finishOwner(owner) {
|
|
227
251
|
const now = Math.max(Date.now(), integer(this.db.prepare("SELECT clock FROM wake_meta").get()?.clock));
|
|
@@ -286,6 +310,7 @@ export class WakeState {
|
|
|
286
310
|
this.settle();
|
|
287
311
|
}
|
|
288
312
|
finally {
|
|
313
|
+
this.phase = { kind: "closed" };
|
|
289
314
|
this.db.close();
|
|
290
315
|
this.owner.close();
|
|
291
316
|
}
|
|
@@ -171,20 +171,14 @@ export class WakeDeliveries {
|
|
|
171
171
|
next = Math.min(next, due);
|
|
172
172
|
continue;
|
|
173
173
|
}
|
|
174
|
-
const
|
|
175
|
-
if (
|
|
176
|
-
next = Math.min(next,
|
|
174
|
+
const reservation = state.reserve(key);
|
|
175
|
+
if (reservation.kind === "deferred") {
|
|
176
|
+
next = Math.min(next, reservation.until);
|
|
177
177
|
continue;
|
|
178
178
|
}
|
|
179
179
|
// ponytail: two cold Macs can each admit one wake. Literal cross-Mac single admission
|
|
180
180
|
// requires a neutral quota authority governing every writer, not a URL hash or HTTP 2xx.
|
|
181
|
-
|
|
182
|
-
await this.post(candidate);
|
|
183
|
-
}
|
|
184
|
-
finally {
|
|
185
|
-
// Expiry starts after response/error/abort, never before COMMIT or connect/TLS.
|
|
186
|
-
state.settle();
|
|
187
|
-
}
|
|
181
|
+
await reservation.post(() => this.post(candidate));
|
|
188
182
|
// Re-read ACKs and all groups after transport I/O; a snapshot must not wake already consumed mail.
|
|
189
183
|
return Date.now();
|
|
190
184
|
}
|
package/package.json
CHANGED
|
@@ -24,6 +24,13 @@ function integer(value: unknown): number {
|
|
|
24
24
|
return value;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
type WakeReservation =
|
|
28
|
+
| { readonly kind: "deferred"; readonly until: number }
|
|
29
|
+
| { readonly kind: "reserved"; readonly post: (action: () => Promise<void>) => Promise<void> };
|
|
30
|
+
type AttemptPhase =
|
|
31
|
+
| { readonly kind: "idle" | "posting" | "settlementPending" | "closed" }
|
|
32
|
+
| Extract<WakeReservation, { readonly kind: "reserved" }>;
|
|
33
|
+
|
|
27
34
|
/** One helper ledger, independent of identity, certificate, URL and credential rotation. */
|
|
28
35
|
export class WakeState {
|
|
29
36
|
private readonly db: DatabaseSync;
|
|
@@ -32,7 +39,7 @@ export class WakeState {
|
|
|
32
39
|
private readonly inode: number;
|
|
33
40
|
private readonly device: number;
|
|
34
41
|
private readonly owner: WakeOwner;
|
|
35
|
-
private
|
|
42
|
+
private phase: AttemptPhase = { kind: "idle" };
|
|
36
43
|
|
|
37
44
|
constructor(stateDir: string) {
|
|
38
45
|
this.stateDir = stateDir;
|
|
@@ -212,14 +219,37 @@ export class WakeState {
|
|
|
212
219
|
this.db.prepare("DELETE FROM wake_batches WHERE bucket=?").run(bucket);
|
|
213
220
|
}
|
|
214
221
|
|
|
215
|
-
/** Persist quota and uncertainty
|
|
216
|
-
reserve(bucket: string):
|
|
222
|
+
/** Persist quota and uncertainty before exposing a single-use POST reservation. */
|
|
223
|
+
reserve(bucket: string): WakeReservation {
|
|
217
224
|
this.retrySettlement();
|
|
218
|
-
|
|
225
|
+
if (this.phase.kind !== "idle") {
|
|
226
|
+
throw new Error("A wake reservation is already active or its owner is closed.");
|
|
227
|
+
}
|
|
228
|
+
const until = this.transaction(() => this.reservation(bucket));
|
|
229
|
+
if (until !== undefined) {
|
|
230
|
+
return { kind: "deferred", until };
|
|
231
|
+
}
|
|
232
|
+
const reservation: Extract<WakeReservation, { readonly kind: "reserved" }> = {
|
|
233
|
+
kind: "reserved",
|
|
234
|
+
post: async (action) => {
|
|
235
|
+
if (this.phase !== reservation) {
|
|
236
|
+
throw new Error("This wake reservation is no longer available.");
|
|
237
|
+
}
|
|
238
|
+
this.phase = { kind: "posting" };
|
|
239
|
+
try {
|
|
240
|
+
await action();
|
|
241
|
+
} finally {
|
|
242
|
+
// Completion, rejection and abort all start the durable expiry window here.
|
|
243
|
+
this.settle();
|
|
244
|
+
}
|
|
245
|
+
},
|
|
246
|
+
};
|
|
247
|
+
this.phase = reservation;
|
|
248
|
+
return reservation;
|
|
219
249
|
}
|
|
220
250
|
|
|
221
251
|
retrySettlement(): void {
|
|
222
|
-
if (this.
|
|
252
|
+
if (this.phase.kind === "settlementPending") {
|
|
223
253
|
this.settle();
|
|
224
254
|
}
|
|
225
255
|
}
|
|
@@ -238,10 +268,10 @@ export class WakeState {
|
|
|
238
268
|
}
|
|
239
269
|
|
|
240
270
|
/** A failed settlement stays open and must be retried before this owner can dispatch again. */
|
|
241
|
-
settle(): void {
|
|
242
|
-
this.
|
|
271
|
+
private settle(): void {
|
|
272
|
+
this.phase = { kind: "settlementPending" };
|
|
243
273
|
this.transaction(() => this.finishOwner(this.owner.name));
|
|
244
|
-
this.
|
|
274
|
+
this.phase = { kind: "idle" };
|
|
245
275
|
}
|
|
246
276
|
|
|
247
277
|
private finishOwner(owner: string): void {
|
|
@@ -314,6 +344,7 @@ export class WakeState {
|
|
|
314
344
|
try {
|
|
315
345
|
this.settle();
|
|
316
346
|
} finally {
|
|
347
|
+
this.phase = { kind: "closed" };
|
|
317
348
|
this.db.close();
|
|
318
349
|
this.owner.close();
|
|
319
350
|
}
|
|
@@ -211,19 +211,14 @@ export class WakeDeliveries {
|
|
|
211
211
|
next = Math.min(next, due);
|
|
212
212
|
continue;
|
|
213
213
|
}
|
|
214
|
-
const
|
|
215
|
-
if (
|
|
216
|
-
next = Math.min(next,
|
|
214
|
+
const reservation = state.reserve(key);
|
|
215
|
+
if (reservation.kind === "deferred") {
|
|
216
|
+
next = Math.min(next, reservation.until);
|
|
217
217
|
continue;
|
|
218
218
|
}
|
|
219
219
|
// ponytail: two cold Macs can each admit one wake. Literal cross-Mac single admission
|
|
220
220
|
// requires a neutral quota authority governing every writer, not a URL hash or HTTP 2xx.
|
|
221
|
-
|
|
222
|
-
await this.post(candidate);
|
|
223
|
-
} finally {
|
|
224
|
-
// Expiry starts after response/error/abort, never before COMMIT or connect/TLS.
|
|
225
|
-
state.settle();
|
|
226
|
-
}
|
|
221
|
+
await reservation.post(() => this.post(candidate));
|
|
227
222
|
// Re-read ACKs and all groups after transport I/O; a snapshot must not wake already consumed mail.
|
|
228
223
|
return Date.now();
|
|
229
224
|
}
|