pi-roundtable-sandbox 0.7.11 → 0.7.12
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 +5 -0
- package/README.md +1 -1
- package/package.json +2 -2
- package/src/pi-broker.ts +72 -4
- package/src/pi-runtime.ts +4 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.7.12
|
|
4
|
+
|
|
5
|
+
- A host compactor gets at most half the time the turn has left (the runtime passes the broker its `deadline`, `PiHostContext.deadline`), so Pi's own summary still fits after it runs out; with under two seconds left the broker falls back to Pi's summary at once.
|
|
6
|
+
- The broker logs the first `error` event of a server-sent event stream that started with a 200 (`sandbox upstream call failed`, with the event's data, credentials removed), which the status alone never showed; the worker still receives the whole stream.
|
|
7
|
+
|
|
3
8
|
## 0.7.11
|
|
4
9
|
|
|
5
10
|
- `precheckScriptRunner` runs agents' precheck scripts (pi-roundtable 0.7.11) in a sealed container per run: no network, a read-only root, no capabilities, a non-root user, an empty workspace, and a per-run broker (`precheckBroker`) that forwards only single `tools/call` requests of the tools `grant(scope)` allows, with the host's credential (`PrecheckMcpServer`). The container runs `worker/precheck-main.ts` (`PRECHECK_ENTRYPOINT`, `PrecheckWorkerInput`), which gives the script `mcp.call`, `mcp.json`, `firedAt`, `timeZone`, `today`, and `schedule`.
|
package/README.md
CHANGED
|
@@ -262,7 +262,7 @@ new PiSandboxRuntime({
|
|
|
262
262
|
|
|
263
263
|
`PiCompactRequest` carries Pi's preparation: `reason`, `tokensBefore`, `firstKeptEntryId`, `isSplitTurn`, `messagesToSummarize`, `turnPrefixMessages`, `keptMessages` (the messages from `firstKeptEntryId` on), `previousSummary?`, `customInstructions?`, `readFiles` and `modifiedFiles`.
|
|
264
264
|
A `PiCompaction` is Pi's `CompactionResult`: `summary`, `firstKeptEntryId` (the request's), `tokensBefore`, `estimatedTokensAfter?` and `details?` (an object; the broker sets its `engine`).
|
|
265
|
-
A compactor that returns `undefined`, throws, answers out of shape, outlasts `timeoutMs` (its `signal` aborts), or gets a request over `maxRequestBytes` falls back to Pi's summary, and the host logs the reason.
|
|
265
|
+
A compactor that returns `undefined`, throws, answers out of shape, outlasts `timeoutMs` or half the time the turn has left, whichever is shorter (its `signal` aborts), or gets a request over `maxRequestBytes` falls back to Pi's summary, and the host logs the reason.
|
|
266
266
|
The timeout may be at most half of `turnTimeoutMs`, so Pi's summary keeps time to run.
|
|
267
267
|
A turn may ask the host for three compactions and send sixteen compaction reports; later compactions fall back to Pi's summary, and while a compactor that ignored its signal still runs, a new request falls back too.
|
|
268
268
|
Without `compaction` the worker registers no compaction handler; the tiers and Pi's summary still apply.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-roundtable-sandbox",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.12",
|
|
4
4
|
"description": "Sealed guest channels and an allow-listed credential broker for pi-roundtable",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"@biomejs/biome": "2.5.15",
|
|
53
53
|
"@types/bun": "1.4.2",
|
|
54
54
|
"discord.js": "14.27.0",
|
|
55
|
-
"pi-roundtable": "0.7.
|
|
55
|
+
"pi-roundtable": "0.7.12",
|
|
56
56
|
"typebox": "1.3.34",
|
|
57
57
|
"typescript": "7.0.2"
|
|
58
58
|
}
|
package/src/pi-broker.ts
CHANGED
|
@@ -27,6 +27,8 @@ export interface PiHostContext {
|
|
|
27
27
|
/** The host-judged level for this turn; the worker can request less but never more. */
|
|
28
28
|
thinking: PiThinkingLevel;
|
|
29
29
|
signal: AbortSignal;
|
|
30
|
+
/** When the turn must end, in epoch milliseconds; a host compactor gets at most half the time left. */
|
|
31
|
+
deadline?: number;
|
|
30
32
|
}
|
|
31
33
|
export interface PiMcpServer {
|
|
32
34
|
name: string;
|
|
@@ -127,12 +129,55 @@ function endpoint(raw: string, allowHttp = false): URL {
|
|
|
127
129
|
throw new Error("Invalid trusted endpoint");
|
|
128
130
|
return url;
|
|
129
131
|
}
|
|
132
|
+
/**
|
|
133
|
+
* Watches a server-sent event stream as it passes and reports its first `error` event: an
|
|
134
|
+
* upstream can fail mid-answer after a 200, which the status alone never shows.
|
|
135
|
+
*/
|
|
136
|
+
function sseErrorWatcher(
|
|
137
|
+
onError: (data: string) => void,
|
|
138
|
+
): (chunk: Uint8Array, done: boolean) => void {
|
|
139
|
+
const decoder = new TextDecoder();
|
|
140
|
+
let pending = "";
|
|
141
|
+
let reported = false;
|
|
142
|
+
const scan = (event: string) => {
|
|
143
|
+
const lines = event.split(/\r?\n/);
|
|
144
|
+
const type = lines
|
|
145
|
+
.find((line) => line.startsWith("event:"))
|
|
146
|
+
?.slice(6)
|
|
147
|
+
.trim();
|
|
148
|
+
const data = lines
|
|
149
|
+
.filter((line) => line.startsWith("data:"))
|
|
150
|
+
.map((line) => line.slice(5).replace(/^ /, ""))
|
|
151
|
+
.join("\n");
|
|
152
|
+
let typed: unknown;
|
|
153
|
+
try {
|
|
154
|
+
typed = JSON.parse(data);
|
|
155
|
+
} catch {
|
|
156
|
+
typed = undefined;
|
|
157
|
+
}
|
|
158
|
+
if (type === "error" || (isRecord(typed) && typed.type === "error")) {
|
|
159
|
+
reported = true;
|
|
160
|
+
onError(data);
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
return (chunk, done) => {
|
|
164
|
+
if (reported) return;
|
|
165
|
+
pending += decoder.decode(chunk, { stream: !done });
|
|
166
|
+
const events = pending.split(/\r?\n\r?\n/);
|
|
167
|
+
pending = done ? "" : (events.pop() ?? "");
|
|
168
|
+
// An event this long is no error report; keep scanning without holding it.
|
|
169
|
+
if (pending.length > 64 * 1024) pending = "";
|
|
170
|
+
for (const event of events) if (!reported) scan(event);
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
130
174
|
function streamBounded(
|
|
131
175
|
response: Response,
|
|
132
176
|
signal: AbortSignal,
|
|
133
177
|
maxBytes: number,
|
|
134
178
|
secret: string,
|
|
135
179
|
onFailure: (error: unknown) => void,
|
|
180
|
+
watch?: (chunk: Uint8Array, done: boolean) => void,
|
|
136
181
|
): ReadableStream<Uint8Array> {
|
|
137
182
|
const reader = response.body?.getReader();
|
|
138
183
|
let total = 0;
|
|
@@ -157,6 +202,7 @@ function streamBounded(
|
|
|
157
202
|
throw new Error("Credential reflected");
|
|
158
203
|
total += result?.value?.byteLength ?? 0;
|
|
159
204
|
if (total > maxBytes) throw new Error("Response too large");
|
|
205
|
+
watch?.(result?.value ?? new Uint8Array(), !result || result.done);
|
|
160
206
|
if (!result || result.done) {
|
|
161
207
|
if (data.length) controller.enqueue(data);
|
|
162
208
|
controller.close();
|
|
@@ -632,6 +678,17 @@ export class PiSandboxBroker {
|
|
|
632
678
|
PI_MEDIA_LIMITS.totalFileBytes,
|
|
633
679
|
secret,
|
|
634
680
|
(error) => failed(secret, { status: upstream.status, error }),
|
|
681
|
+
upstream.status < 400 &&
|
|
682
|
+
(upstream.headers.get("content-type") ?? "").includes(
|
|
683
|
+
"text/event-stream",
|
|
684
|
+
)
|
|
685
|
+
? sseErrorWatcher((body) =>
|
|
686
|
+
failed(secret, {
|
|
687
|
+
status: upstream.status,
|
|
688
|
+
body: `stream error event: ${body}`,
|
|
689
|
+
}),
|
|
690
|
+
)
|
|
691
|
+
: undefined,
|
|
635
692
|
),
|
|
636
693
|
{ headers: outgoing, status: upstream.status },
|
|
637
694
|
);
|
|
@@ -725,11 +782,22 @@ export class PiSandboxBroker {
|
|
|
725
782
|
compactor.maxRequestBytes ?? PI_COMPACT_LIMITS.requestBytes;
|
|
726
783
|
if (Number(request.headers.get("content-length") ?? 0) > maxBytes)
|
|
727
784
|
return fallback(`the request is over ${maxBytes} bytes`);
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
785
|
+
// Pi's own summary must still fit after a compactor that runs out of time.
|
|
786
|
+
const left =
|
|
787
|
+
turn.context.deadline === undefined
|
|
788
|
+
? Number.POSITIVE_INFINITY
|
|
789
|
+
: turn.context.deadline - Date.now();
|
|
790
|
+
const timeoutMs = Math.min(
|
|
731
791
|
compactor.timeoutMs ?? PI_COMPACT_LIMITS.timeoutMs,
|
|
792
|
+
Math.floor(left / 2),
|
|
732
793
|
);
|
|
794
|
+
if (timeoutMs < 1000)
|
|
795
|
+
return fallback(
|
|
796
|
+
"the turn has too little time left for the host compactor",
|
|
797
|
+
);
|
|
798
|
+
let running: Promise<unknown> = Promise.resolve();
|
|
799
|
+
this.#compacting = running;
|
|
800
|
+
const timeout = AbortSignal.timeout(timeoutMs);
|
|
733
801
|
const signal = AbortSignal.any([
|
|
734
802
|
turn.context.signal,
|
|
735
803
|
request.signal,
|
|
@@ -797,7 +865,7 @@ export class PiSandboxBroker {
|
|
|
797
865
|
} catch (error) {
|
|
798
866
|
return fallback(
|
|
799
867
|
timeout.aborted
|
|
800
|
-
? `the compactor took over ${
|
|
868
|
+
? `the compactor took over ${timeoutMs} ms`
|
|
801
869
|
: signal.aborted
|
|
802
870
|
? "the compaction was aborted"
|
|
803
871
|
: `the compactor failed: ${scrubDiagnostic(errorText(error), 2000)}`,
|
package/src/pi-runtime.ts
CHANGED
|
@@ -351,10 +351,12 @@ export class PiSandboxRuntime {
|
|
|
351
351
|
return { ok: false, error: new AgentRunError("Channel is busy") };
|
|
352
352
|
const controller = new AbortController();
|
|
353
353
|
this.#active.set(turn.channel, controller);
|
|
354
|
+
const turnTimeoutMs = this.#options.turnTimeoutMs ?? 600_000;
|
|
355
|
+
const deadline = Date.now() + turnTimeoutMs;
|
|
354
356
|
const signal = AbortSignal.any([
|
|
355
357
|
controller.signal,
|
|
356
358
|
...(turn.signal ? [turn.signal] : []),
|
|
357
|
-
AbortSignal.timeout(
|
|
359
|
+
AbortSignal.timeout(turnTimeoutMs),
|
|
358
360
|
]);
|
|
359
361
|
signal.addEventListener("abort", () => controller.abort(), { once: true });
|
|
360
362
|
const timedOut = () =>
|
|
@@ -408,6 +410,7 @@ export class PiSandboxRuntime {
|
|
|
408
410
|
speaker: turn.author,
|
|
409
411
|
thinking,
|
|
410
412
|
signal,
|
|
413
|
+
deadline,
|
|
411
414
|
});
|
|
412
415
|
const body = await entry.broker.execute(request, signal);
|
|
413
416
|
signal.throwIfAborted();
|