@prjct.app/pi-team 0.5.2 → 0.5.3
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/package.json +1 -1
- package/src/index.ts +29 -12
- package/src/mailbox.ts +10 -1
- package/src/store.ts +9 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
## [0.5.3](https://github.com/prjct-app/pi-team/compare/v0.5.2...v0.5.3) (2026-09-10)
|
|
2
|
+
|
|
3
|
+
### Performance Improvements
|
|
4
|
+
|
|
5
|
+
* stop opening a mailbox transaction on every idle tick ([#22](https://github.com/prjct-app/pi-team/issues/22)) ([561001f](https://github.com/prjct-app/pi-team/commit/561001f1fe8bb3048c005d74ef45d97e75b7ec09))
|
|
6
|
+
|
|
1
7
|
## [0.5.2](https://github.com/prjct-app/pi-team/compare/v0.5.1...v0.5.2) (2026-09-10)
|
|
2
8
|
|
|
3
9
|
### Performance Improvements
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -140,6 +140,8 @@ type Session = Readonly<{
|
|
|
140
140
|
lastReview: number;
|
|
141
141
|
lastRevision: number;
|
|
142
142
|
quietReviews: number;
|
|
143
|
+
widgetText?: string;
|
|
144
|
+
widgetCtx?: ExtensionContext;
|
|
143
145
|
}>;
|
|
144
146
|
|
|
145
147
|
const INITIAL: Session = {
|
|
@@ -182,6 +184,20 @@ export function installTeam(pi: ExtensionAPI, options: { root?: string; pollMs?:
|
|
|
182
184
|
needsCompaction, compactionSubject: needsCompaction ? compactionSubject : undefined,
|
|
183
185
|
} : null);
|
|
184
186
|
}
|
|
187
|
+
/**
|
|
188
|
+
* The widget is rebuilt on every tick otherwise. Keyed on context identity
|
|
189
|
+
* as well as text: `ctx` is replaced on session start and by the command
|
|
190
|
+
* handler, and a new context needs its own registration.
|
|
191
|
+
*/
|
|
192
|
+
function showWidget(text: string | undefined) {
|
|
193
|
+
const { ctx, widgetText, widgetCtx } = get();
|
|
194
|
+
if (text === widgetText && ctx === widgetCtx) return;
|
|
195
|
+
set(() => ({ widgetText: text, widgetCtx: ctx }));
|
|
196
|
+
ctx?.ui.setWidget('team', text === undefined ? undefined : () => ({
|
|
197
|
+
invalidate() {},
|
|
198
|
+
render(width: number) { return [truncateToWidth(text, width)]; },
|
|
199
|
+
}));
|
|
200
|
+
}
|
|
185
201
|
function stop() {
|
|
186
202
|
const { timer, watcher } = get();
|
|
187
203
|
if (timer) clearInterval(timer);
|
|
@@ -195,7 +211,7 @@ export function installTeam(pi: ExtensionAPI, options: { root?: string; pollMs?:
|
|
|
195
211
|
needsCompaction: false, compactionSubject: '', compactionGeneration: session.compactionGeneration + 1,
|
|
196
212
|
}));
|
|
197
213
|
persist();
|
|
198
|
-
|
|
214
|
+
showWidget(undefined);
|
|
199
215
|
}
|
|
200
216
|
async function detach() {
|
|
201
217
|
stop();
|
|
@@ -288,19 +304,16 @@ export function installTeam(pi: ExtensionAPI, options: { root?: string; pollMs?:
|
|
|
288
304
|
}
|
|
289
305
|
const snap = await box.snapshot(member);
|
|
290
306
|
set(() => ({ aliases: snap.members.map(m => m.alias) }));
|
|
291
|
-
const
|
|
307
|
+
const inbox = snap.messages.filter(m => m.to === member.alias && m.state === 'pending');
|
|
308
|
+
const pending = inbox.length;
|
|
292
309
|
const { compacting, needsCompaction, paused, active } = get();
|
|
293
310
|
const status = `${member.team} · ${member.alias} · ${compacting || needsCompaction ? 'compacting' : paused ? 'paused' : !ctx.model ? 'select a model' : active ? 'working' : 'connected'}${pending ? ` · ${pending} pending` : ''}`;
|
|
294
|
-
|
|
295
|
-
invalidate() {},
|
|
296
|
-
render(width: number) { return [truncateToWidth(status, width)]; },
|
|
297
|
-
}));
|
|
311
|
+
showWidget(status);
|
|
298
312
|
if (get().leaving) return;
|
|
299
313
|
// A disconnected peer holding a claim must be interrupted so its
|
|
300
|
-
// requester receives a result instead of waiting forever.
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
}
|
|
314
|
+
// requester receives a result instead of waiting forever. Sweeping is a
|
|
315
|
+
// full mailbox transaction, so it runs only when it would change something.
|
|
316
|
+
if (snap.sweepable) await box.sweep(member);
|
|
304
317
|
// Keep the session branch stable while Pi summarizes it. Team commands stay
|
|
305
318
|
// registered, but no new peer content is appended or claimed until callback.
|
|
306
319
|
if (get().compacting) return;
|
|
@@ -308,7 +321,11 @@ export function installTeam(pi: ExtensionAPI, options: { root?: string; pollMs?:
|
|
|
308
321
|
compactPendingContext(ctx);
|
|
309
322
|
return;
|
|
310
323
|
}
|
|
311
|
-
|
|
324
|
+
// Consuming notes is a mailbox transaction too. The snapshot already lists
|
|
325
|
+
// every message addressed to this member, so it decides whether to open one.
|
|
326
|
+
if (inbox.some(m => m.kind === 'note')) {
|
|
327
|
+
for (const message of await box.notes(member)) pi.appendEntry('team-event', message);
|
|
328
|
+
}
|
|
312
329
|
if (!ready()) return;
|
|
313
330
|
if (get().budget >= 5) {
|
|
314
331
|
if (pending) {
|
|
@@ -670,7 +687,7 @@ export function installTeam(pi: ExtensionAPI, options: { root?: string; pollMs?:
|
|
|
670
687
|
await box.leave(member).catch(notice);
|
|
671
688
|
}
|
|
672
689
|
set(() => ({ member: undefined, active: undefined }));
|
|
673
|
-
|
|
690
|
+
showWidget(undefined);
|
|
674
691
|
});
|
|
675
692
|
});
|
|
676
693
|
}
|
package/src/mailbox.ts
CHANGED
|
@@ -15,7 +15,7 @@ export type Message = {
|
|
|
15
15
|
};
|
|
16
16
|
export type Outgoing = { to: string; kind: 'request' | 'note'; subject: string; body: string; parentId?: string };
|
|
17
17
|
export type FlowItem = Pick<Message, 'id' | 'from' | 'to' | 'subject' | 'state' | 'created'>;
|
|
18
|
-
export type Snapshot = { revision: number; members: Member[]; messages: Message[]; flow: FlowItem[] };
|
|
18
|
+
export type Snapshot = { revision: number; sweepable: boolean; members: Member[]; messages: Message[]; flow: FlowItem[] };
|
|
19
19
|
type State = { version: 1; members: Member[]; messages: Message[] };
|
|
20
20
|
type Presence = { token: string; status: 'idle' | 'busy' | 'paused'; seen: number };
|
|
21
21
|
export const LEASE_MS = 30_000;
|
|
@@ -219,8 +219,17 @@ export class Mailbox {
|
|
|
219
219
|
const record = await this.readState(member.team, true);
|
|
220
220
|
this.owner(record.payload, member);
|
|
221
221
|
const presence = await this.readPresence(member.team);
|
|
222
|
+
// Members the record still counts as connected but whose session is gone.
|
|
223
|
+
// Collected here so liveness is probed once per member, not twice.
|
|
224
|
+
const stale = record.payload.members.filter(m => m.status !== 'offline' && !this.alive(m, presence));
|
|
222
225
|
return {
|
|
223
226
|
revision: record.revision,
|
|
227
|
+
// A sweep only has an observable effect when a dead member still holds a
|
|
228
|
+
// claim: `disconnect` interrupts it so its requester gets a result.
|
|
229
|
+
// Members are never removed from the record, so "someone is offline" is
|
|
230
|
+
// permanently true once anyone leaves and cannot gate the sweep.
|
|
231
|
+
sweepable: stale.some(dead => record.payload.messages.some(
|
|
232
|
+
m => m.state === 'processing' && m.claim === dead.token && m.to === dead.alias)),
|
|
224
233
|
members: record.payload.members.map(m => this.withStatus(m, presence)),
|
|
225
234
|
messages: record.payload.messages.filter(m => m.from === member.alias || m.to === member.alias),
|
|
226
235
|
// Expose only the metadata needed to understand team-wide request flow;
|
package/src/store.ts
CHANGED
|
@@ -25,6 +25,13 @@ const KEEP_REVISIONS = 32;
|
|
|
25
25
|
|
|
26
26
|
export const sha256 = (value: string): string => createHash('sha256').update(value).digest('hex');
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Diagnostics for the polling loop. `reads` counts full record parses, which
|
|
30
|
+
* happen once per attempted mutation and on a cache miss, never on a cache hit.
|
|
31
|
+
* A joined but idle session should leave both of these flat.
|
|
32
|
+
*/
|
|
33
|
+
export const counters = { reads: 0, publishes: 0 };
|
|
34
|
+
|
|
28
35
|
/** Standard envelope parser: schema marker, revision, and content hash. */
|
|
29
36
|
export function envelope<T>(raw: string): Record<T> {
|
|
30
37
|
const parsed = JSON.parse(raw) as { schemaVersion?: unknown; revision?: unknown; contentHash?: unknown; payload?: unknown };
|
|
@@ -62,6 +69,7 @@ export async function readRecord<T>(path: string, normalize: Normalize<T>, maxBy
|
|
|
62
69
|
if (!handle) return undefined;
|
|
63
70
|
try {
|
|
64
71
|
assertSafeFile(path, await handle.stat(), maxBytes);
|
|
72
|
+
counters.reads++;
|
|
65
73
|
return normalize(await handle.readFile('utf8'));
|
|
66
74
|
} finally { await handle.close(); }
|
|
67
75
|
}
|
|
@@ -175,6 +183,7 @@ export async function publish<T>(
|
|
|
175
183
|
if (durability === 'full') await syncDirectory(path);
|
|
176
184
|
} finally { await unlink(tmp).catch(() => {}); }
|
|
177
185
|
cache.delete(path);
|
|
186
|
+
counters.publishes++;
|
|
178
187
|
await pruneRevisions(dirname(path), next).catch(() => {});
|
|
179
188
|
return { revision: next, payload };
|
|
180
189
|
} finally {
|