@prjct.app/pi-team 0.5.3 → 0.5.4
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 +24 -8
- package/src/mailbox.ts +11 -2
- package/src/store.ts +14 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
## [0.5.4](https://github.com/prjct-app/pi-team/compare/v0.5.3...v0.5.4) (2026-09-10)
|
|
2
|
+
|
|
3
|
+
### Performance Improvements
|
|
4
|
+
|
|
5
|
+
* drop the presence fsync, the quadratic file cap and a repeated mkdir ([#23](https://github.com/prjct-app/pi-team/issues/23)) ([75e5849](https://github.com/prjct-app/pi-team/commit/75e584965bd861cea96942497618f8c261c64d8d))
|
|
6
|
+
|
|
1
7
|
## [0.5.3](https://github.com/prjct-app/pi-team/compare/v0.5.2...v0.5.3) (2026-09-10)
|
|
2
8
|
|
|
3
9
|
### Performance Improvements
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -96,6 +96,24 @@ function excerptPath(path: string, limit: number): string {
|
|
|
96
96
|
return path.length <= limit ? path : `…${path.slice(-limit)}`;
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Fill a result's file list up to the serialized size cap. Each accepted path
|
|
101
|
+
* grows the encoded report by exactly its own encoding plus a separating
|
|
102
|
+
* comma, so a running total lands on the same boundary as re-serializing the
|
|
103
|
+
* whole report once per candidate, without the quadratic cost.
|
|
104
|
+
*/
|
|
105
|
+
export function fitFiles(base: Result, candidates: Iterable<string>): { files: string[]; truncated: boolean } {
|
|
106
|
+
const files: string[] = [];
|
|
107
|
+
const size = { bytes: Buffer.byteLength(JSON.stringify({ ...base, files: [] })) };
|
|
108
|
+
for (const file of candidates) {
|
|
109
|
+
const addition = Buffer.byteLength(JSON.stringify(file)) + (files.length ? 1 : 0);
|
|
110
|
+
if (files.length >= 50 || file.length > 4096 || size.bytes + addition > 31000) return { files, truncated: true };
|
|
111
|
+
files.push(file);
|
|
112
|
+
size.bytes += addition;
|
|
113
|
+
}
|
|
114
|
+
return { files, truncated: false };
|
|
115
|
+
}
|
|
116
|
+
|
|
99
117
|
/** Bound a list injected into the prompt, reporting what was left out. */
|
|
100
118
|
function bounded<T>(items: T[], limit = STATUS_ITEMS): { items: T[]; omitted?: number } {
|
|
101
119
|
return items.length <= limit ? { items } : { items: items.slice(0, limit), omitted: items.length - limit };
|
|
@@ -651,14 +669,12 @@ export function installTeam(pi: ExtensionAPI, options: { root?: string; pollMs?:
|
|
|
651
669
|
set(() => ({ outcome: 'interrupted', finalText: 'User took over the session. Subsequent output was not forwarded. Review before continuing.' }));
|
|
652
670
|
}
|
|
653
671
|
const { outcome, finalText, files } = get();
|
|
654
|
-
const
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
report.files.push(file);
|
|
661
|
-
}
|
|
672
|
+
const body = finalText.slice(0, 3000) || `Agent turn ${outcome}; no final text. Review the recipient session.`;
|
|
673
|
+
const fitted = fitFiles({ outcome, body, files: [], tests: [] }, files);
|
|
674
|
+
const report: Result = {
|
|
675
|
+
outcome, files: fitted.files, tests: [],
|
|
676
|
+
body: fitted.truncated ? `${body}\nFile list truncated; review the recipient session.` : body,
|
|
677
|
+
};
|
|
662
678
|
await box.complete(member, finished.id, report);
|
|
663
679
|
set(() => ({ active: undefined, ...(outcome !== 'completed' ? { paused: true } : {}) }));
|
|
664
680
|
if (get().leaving) { await detach(); return false; }
|
package/src/mailbox.ts
CHANGED
|
@@ -36,13 +36,22 @@ export function identifier(value: string): string {
|
|
|
36
36
|
return value;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Directories known to have been created by this process. Only the `mkdir` is
|
|
41
|
+
* skipped: the `lstat` check runs every time, because detecting a directory
|
|
42
|
+
* swapped after the fact is the entire point of the check.
|
|
43
|
+
*/
|
|
44
|
+
const created = new Set<string>();
|
|
45
|
+
|
|
39
46
|
async function privateDirectory(path: string): Promise<void> {
|
|
40
|
-
await mkdir(path, { mode: 0o700, recursive: true });
|
|
47
|
+
if (!created.has(path)) await mkdir(path, { mode: 0o700, recursive: true });
|
|
41
48
|
const stat = await lstat(path);
|
|
42
49
|
if (!stat.isDirectory() || stat.isSymbolicLink() || (stat.mode & 0o077) !== 0 ||
|
|
43
50
|
(process.getuid && stat.uid !== process.getuid())) {
|
|
51
|
+
created.delete(path);
|
|
44
52
|
throw new Error(`Unsafe directory: ${path}. Expected a private directory owned by this user.`);
|
|
45
53
|
}
|
|
54
|
+
created.add(path);
|
|
46
55
|
}
|
|
47
56
|
|
|
48
57
|
/**
|
|
@@ -203,7 +212,7 @@ export class Mailbox {
|
|
|
203
212
|
|
|
204
213
|
private async writePresence(member: Membership, status: 'idle' | 'busy' | 'paused'): Promise<void> {
|
|
205
214
|
const text = JSON.stringify({ token: member.token, status, seen: Date.now() } satisfies Presence);
|
|
206
|
-
await writeAtomic(this.presencePath(member.team, member.alias), text, '
|
|
215
|
+
await writeAtomic(this.presencePath(member.team, member.alias), text, 'none');
|
|
207
216
|
}
|
|
208
217
|
|
|
209
218
|
/** Lock-free heartbeat: touches only this member's own presence file. */
|
package/src/store.ts
CHANGED
|
@@ -17,8 +17,13 @@ import { dirname, join } from 'node:path';
|
|
|
17
17
|
export type Record<T> = { revision: number; payload: T };
|
|
18
18
|
/** Parse raw file bytes into a record, throwing on corruption. Never deletes. */
|
|
19
19
|
export type Normalize<T> = (raw: string) => Record<T>;
|
|
20
|
-
/**
|
|
21
|
-
|
|
20
|
+
/**
|
|
21
|
+
* 'full' fsyncs file and directory; 'light' fsyncs the file only; 'none'
|
|
22
|
+
* fsyncs nothing and relies on the atomic rename alone. Use 'none' only for
|
|
23
|
+
* records that are rewritten on a timer and safe to lose, such as presence:
|
|
24
|
+
* a lost write there makes a member look offline sooner, never alive longer.
|
|
25
|
+
*/
|
|
26
|
+
export type Durability = 'full' | 'light' | 'none';
|
|
22
27
|
|
|
23
28
|
const STALE_LOCK_MS = 10_000;
|
|
24
29
|
const KEEP_REVISIONS = 32;
|
|
@@ -99,17 +104,22 @@ async function syncDirectory(path: string): Promise<void> {
|
|
|
99
104
|
|
|
100
105
|
/** Atomic last-writer-wins write for records without revision history (presence). */
|
|
101
106
|
export async function writeAtomic(path: string, text: string, durability: Durability = 'full'): Promise<void> {
|
|
107
|
+
// Always: the presence directory does not exist before its first write.
|
|
102
108
|
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
103
109
|
const tmp = `${path}.${randomUUID()}.tmp`;
|
|
104
110
|
const handle = await open(tmp, 'wx', 0o600);
|
|
105
111
|
try {
|
|
106
112
|
await handle.writeFile(text, 'utf8');
|
|
107
|
-
await handle.sync();
|
|
113
|
+
if (durability !== 'none') await handle.sync();
|
|
108
114
|
} finally { await handle.close(); }
|
|
109
115
|
try {
|
|
110
116
|
await rename(tmp, path);
|
|
111
117
|
if (durability === 'full') await syncDirectory(path);
|
|
112
|
-
|
|
118
|
+
return;
|
|
119
|
+
} catch (error) {
|
|
120
|
+
await unlink(tmp).catch(() => {});
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
113
123
|
}
|
|
114
124
|
|
|
115
125
|
const locked = () => Object.assign(new Error('Another writer holds this record.'), { code: 'RECORD_LOCKED' });
|