@pasko70/pibo 1.10.0 → 1.11.0
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/dist/apps/chat/chat-request-normalizers.js +7 -0
- package/dist/apps/chat/data/chat-data-mappers.js +2 -0
- package/dist/apps/chat/loop-api.js +11 -6
- package/dist/apps/chat/web-app.js +30 -10
- package/dist/apps/chat-ui/assets/{dist-wE9nop9V.js → dist-0E9FVJ6k.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-DlATLa-U.js → dist-BvqC0hRM.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-DUlaXAk7.js → dist-C2BRHNXr.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-nOLTkZrJ.js → dist-C57jNmjf.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-Y-AA2omI.js → dist-CBL8UXjd.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-D-cxLQO1.js → dist-CI-LPHjw.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-LHRs1Nhr.js → dist-CMRFkfl5.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-CS7wdk0Z.js → dist-CpZPhD2y.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-BwKObYnX.js → dist-CuCZ-3_K.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-GdEM8UW1.js → dist-CzC5kPWJ.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-CKtT8YGm.js → dist-D9hLnn1T.js} +1 -1
- package/dist/apps/chat-ui/assets/index-BEvjPUor.js +173 -0
- package/dist/apps/chat-ui/assets/{index-C0x9nEcf.css → index-DNeE4HrG.css} +1 -1
- package/dist/apps/chat-ui/index.html +2 -2
- package/dist/apps/chat-vscode-web/assets/index-BLZRi4Ka.js +41 -0
- package/dist/apps/chat-vscode-web/assets/index-Bf2JvJ9z.css +2 -0
- package/dist/apps/chat-vscode-web/index.html +2 -2
- package/dist/apps/vscode-artifacts/latest.vsix +0 -0
- package/dist/apps/vscode-artifacts/pibo-vscode-ext-1.11.0.vsix +0 -0
- package/dist/auth/better-auth.js +49 -1
- package/dist/auth/cli.js +220 -0
- package/dist/auth/machine-keys.js +258 -0
- package/dist/auth/machine-session.js +123 -0
- package/dist/bin/pibo.js +0 -0
- package/dist/bin/rg.js +0 -0
- package/dist/cli-session/localSessionSource.js +34 -12
- package/dist/cli.js +6 -0
- package/dist/compute/cli.js +7 -0
- package/dist/compute/resource-health.js +61 -4
- package/dist/config/config.js +5 -0
- package/dist/core/events.js +6 -1
- package/dist/core/routed-session.js +67 -4
- package/dist/core/session-router.js +40 -6
- package/dist/data/ingest-service.js +3 -1
- package/dist/data/schema.js +4 -0
- package/dist/debug/index.js +4 -0
- package/dist/debug/trace-status.js +25 -0
- package/dist/debug/trace.js +51 -17
- package/dist/index.js +1 -0
- package/dist/loops/accounting.js +19 -0
- package/dist/loops/cli.js +22 -6
- package/dist/loops/prompts.js +4 -1
- package/dist/loops/service.js +97 -8
- package/dist/loops/store.js +128 -17
- package/dist/loops/tools.js +24 -7
- package/dist/reliability/store.js +34 -7
- package/dist/resources/cli.js +15 -0
- package/dist/resources/lifecycle.js +28 -4
- package/dist/resources/reaper.js +1 -0
- package/dist/runs/lifecycle.js +59 -0
- package/dist/runs/registry.js +47 -1
- package/dist/runs/tools.js +31 -13
- package/dist/session-ui/terminalRows.js +66 -2
- package/dist/shared/trace-async-agent-runs.js +4 -4
- package/dist/shared/trace-event-projection.js +59 -19
- package/dist/shared/trace-nodes.js +5 -0
- package/dist/shared/trace-page-merge.js +15 -0
- package/dist/shared/trace-run-notifications.js +3 -1
- package/dist/signals/projector.js +16 -3
- package/dist/tools/browser-pool.js +50 -0
- package/dist/tools/browser-use-leases.js +12 -8
- package/dist/tools/guides.js +8 -1
- package/dist/tools/index.js +1 -0
- package/package.json +2 -1
- package/skills/builtin/loop/SKILL.md +12 -3
- package/dist/apps/chat-ui/assets/index-8W_yMHQI.js +0 -173
- package/dist/apps/chat-vscode-web/assets/index-B5QK07zO.css +0 -2
- package/dist/apps/chat-vscode-web/assets/index-BAMxIaI_.js +0 -41
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
2
|
+
export const PIBO_MACHINE_SESSION_COOKIE = "pibo_machine_session";
|
|
3
|
+
export const DEFAULT_MACHINE_SESSION_TTL_SECONDS = 8 * 60 * 60;
|
|
4
|
+
const MACHINE_SESSION_VERSION = 1;
|
|
5
|
+
const MAX_COOKIE_VALUE_LENGTH = 1024;
|
|
6
|
+
function signPayload(secret, encodedPayload) {
|
|
7
|
+
return createHmac("sha256", secret).update(encodedPayload, "utf8").digest();
|
|
8
|
+
}
|
|
9
|
+
function encodePayload(secret, payload) {
|
|
10
|
+
const encodedPayload = Buffer.from(JSON.stringify(payload), "utf8").toString("base64url");
|
|
11
|
+
const signature = signPayload(secret, encodedPayload).toString("base64url");
|
|
12
|
+
return `${encodedPayload}.${signature}`;
|
|
13
|
+
}
|
|
14
|
+
function decodePayload(secret, value) {
|
|
15
|
+
if (value.length === 0 || value.length > MAX_COOKIE_VALUE_LENGTH)
|
|
16
|
+
return undefined;
|
|
17
|
+
const parts = value.split(".");
|
|
18
|
+
if (parts.length !== 2 || !parts[0] || !parts[1])
|
|
19
|
+
return undefined;
|
|
20
|
+
let providedSignature;
|
|
21
|
+
try {
|
|
22
|
+
providedSignature = Buffer.from(parts[1], "base64url");
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
const expectedSignature = signPayload(secret, parts[0]);
|
|
28
|
+
if (providedSignature.length !== expectedSignature.length ||
|
|
29
|
+
!timingSafeEqual(providedSignature, expectedSignature)) {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
const parsed = JSON.parse(Buffer.from(parts[0], "base64url").toString("utf8"));
|
|
34
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
35
|
+
return undefined;
|
|
36
|
+
const payload = parsed;
|
|
37
|
+
if (payload.v !== MACHINE_SESSION_VERSION ||
|
|
38
|
+
typeof payload.keyId !== "string" ||
|
|
39
|
+
!/^[a-f0-9]{16}$/.test(payload.keyId) ||
|
|
40
|
+
typeof payload.expiresAt !== "number" ||
|
|
41
|
+
!Number.isSafeInteger(payload.expiresAt)) {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
return { v: MACHINE_SESSION_VERSION, keyId: payload.keyId, expiresAt: payload.expiresAt };
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function cookieValue(headers) {
|
|
51
|
+
const cookieHeader = headers.get("cookie");
|
|
52
|
+
if (!cookieHeader)
|
|
53
|
+
return undefined;
|
|
54
|
+
for (const part of cookieHeader.split(";")) {
|
|
55
|
+
const separator = part.indexOf("=");
|
|
56
|
+
if (separator < 0)
|
|
57
|
+
continue;
|
|
58
|
+
const name = part.slice(0, separator).trim();
|
|
59
|
+
if (name !== PIBO_MACHINE_SESSION_COOKIE)
|
|
60
|
+
continue;
|
|
61
|
+
return part.slice(separator + 1).trim();
|
|
62
|
+
}
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
function sessionCookieHeader(value, expiresAt, maxAgeSeconds) {
|
|
66
|
+
return [
|
|
67
|
+
`${PIBO_MACHINE_SESSION_COOKIE}=${value}`,
|
|
68
|
+
"Path=/",
|
|
69
|
+
"HttpOnly",
|
|
70
|
+
"Secure",
|
|
71
|
+
"SameSite=Strict",
|
|
72
|
+
`Max-Age=${maxAgeSeconds}`,
|
|
73
|
+
`Expires=${expiresAt.toUTCString()}`,
|
|
74
|
+
].join("; ");
|
|
75
|
+
}
|
|
76
|
+
export function createMachineSessionManager(options) {
|
|
77
|
+
const ttlSeconds = options.ttlSeconds ?? DEFAULT_MACHINE_SESSION_TTL_SECONDS;
|
|
78
|
+
if (!Number.isInteger(ttlSeconds) || ttlSeconds < 1) {
|
|
79
|
+
throw new Error("Machine session TTL must be a positive integer");
|
|
80
|
+
}
|
|
81
|
+
const now = options.now ?? (() => new Date());
|
|
82
|
+
return {
|
|
83
|
+
create(authentication) {
|
|
84
|
+
const createdAt = now();
|
|
85
|
+
const ttlExpiry = createdAt.getTime() + ttlSeconds * 1000;
|
|
86
|
+
const keyExpiry = authentication.session.expiresAt?.getTime();
|
|
87
|
+
const expiryMs = keyExpiry === undefined ? ttlExpiry : Math.min(ttlExpiry, keyExpiry);
|
|
88
|
+
if (expiryMs <= createdAt.getTime())
|
|
89
|
+
throw new Error("Machine key is expired");
|
|
90
|
+
const expiresAt = new Date(expiryMs);
|
|
91
|
+
const value = encodePayload(options.secret, {
|
|
92
|
+
v: MACHINE_SESSION_VERSION,
|
|
93
|
+
keyId: authentication.id,
|
|
94
|
+
expiresAt: expiryMs,
|
|
95
|
+
});
|
|
96
|
+
return {
|
|
97
|
+
header: sessionCookieHeader(value, expiresAt, Math.max(1, Math.floor((expiryMs - createdAt.getTime()) / 1000))),
|
|
98
|
+
expiresAt,
|
|
99
|
+
session: { ...authentication.session, expiresAt },
|
|
100
|
+
};
|
|
101
|
+
},
|
|
102
|
+
getSession(headers) {
|
|
103
|
+
const value = cookieValue(headers);
|
|
104
|
+
if (!value)
|
|
105
|
+
return undefined;
|
|
106
|
+
const payload = decodePayload(options.secret, value);
|
|
107
|
+
const currentTime = now().getTime();
|
|
108
|
+
if (!payload || payload.expiresAt <= currentTime)
|
|
109
|
+
return undefined;
|
|
110
|
+
const keySession = options.machineKeys.getSessionById(payload.keyId);
|
|
111
|
+
if (!keySession)
|
|
112
|
+
return undefined;
|
|
113
|
+
const keyExpiry = keySession.expiresAt?.getTime();
|
|
114
|
+
const effectiveExpiry = keyExpiry === undefined ? payload.expiresAt : Math.min(payload.expiresAt, keyExpiry);
|
|
115
|
+
if (effectiveExpiry <= currentTime)
|
|
116
|
+
return undefined;
|
|
117
|
+
return { ...keySession, expiresAt: new Date(effectiveExpiry) };
|
|
118
|
+
},
|
|
119
|
+
clearHeader() {
|
|
120
|
+
return sessionCookieHeader("", new Date(0), 0);
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
}
|
package/dist/bin/pibo.js
CHANGED
|
File without changes
|
package/dist/bin/rg.js
CHANGED
|
File without changes
|
|
@@ -29,6 +29,8 @@ export class LocalCliSessionSource {
|
|
|
29
29
|
listeners = new Map();
|
|
30
30
|
openHandles = new Set();
|
|
31
31
|
closed = false;
|
|
32
|
+
closing = false;
|
|
33
|
+
closePromise;
|
|
32
34
|
constructor(options = {}) {
|
|
33
35
|
this.sessionStore =
|
|
34
36
|
options.sessionStore ?? createDefaultPiboDataSessionStore();
|
|
@@ -349,19 +351,39 @@ export class LocalCliSessionSource {
|
|
|
349
351
|
};
|
|
350
352
|
}
|
|
351
353
|
async close() {
|
|
354
|
+
if (this.closePromise)
|
|
355
|
+
return this.closePromise;
|
|
356
|
+
this.closePromise = this.closeUnsafe();
|
|
357
|
+
return this.closePromise;
|
|
358
|
+
}
|
|
359
|
+
async closeUnsafe() {
|
|
352
360
|
if (this.closed)
|
|
353
361
|
return;
|
|
354
|
-
this.
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
this.
|
|
362
|
+
this.closing = true;
|
|
363
|
+
let routerError;
|
|
364
|
+
try {
|
|
365
|
+
if (this.ownsRouter)
|
|
366
|
+
await this.router?.disposeAll?.();
|
|
367
|
+
}
|
|
368
|
+
catch (error) {
|
|
369
|
+
routerError = error;
|
|
370
|
+
}
|
|
371
|
+
try {
|
|
372
|
+
this.closed = true;
|
|
373
|
+
for (const handle of [...this.openHandles])
|
|
374
|
+
handle.close();
|
|
375
|
+
this.listeners.clear();
|
|
376
|
+
this.unsubscribeRouter?.();
|
|
377
|
+
if (this.ownsSessionStore)
|
|
378
|
+
this.sessionStore.close?.();
|
|
379
|
+
if (this.ownsDataStore)
|
|
380
|
+
this.dataStore?.close();
|
|
381
|
+
}
|
|
382
|
+
finally {
|
|
383
|
+
this.closing = false;
|
|
384
|
+
}
|
|
385
|
+
if (routerError)
|
|
386
|
+
throw routerError;
|
|
365
387
|
}
|
|
366
388
|
listenerCount(sessionId) {
|
|
367
389
|
if (sessionId)
|
|
@@ -825,7 +847,7 @@ export class LocalCliSessionSource {
|
|
|
825
847
|
listener(update);
|
|
826
848
|
}
|
|
827
849
|
assertOpen() {
|
|
828
|
-
if (this.closed)
|
|
850
|
+
if (this.closed || this.closing)
|
|
829
851
|
throw new CliSourceError("source_closed", "Local CLI session source is closed");
|
|
830
852
|
}
|
|
831
853
|
}
|
package/dist/cli.js
CHANGED
|
@@ -80,6 +80,11 @@ export async function runPiboCli(argv = process.argv) {
|
|
|
80
80
|
printPiboVersion();
|
|
81
81
|
return;
|
|
82
82
|
}
|
|
83
|
+
if (argv[2] === "auth") {
|
|
84
|
+
const { runAuthCli } = await import("./auth/cli.js");
|
|
85
|
+
await runAuthCli([argv[0] ?? "node", "pibo auth", ...argv.slice(3)]);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
83
88
|
if (argv[2] === "mcp") {
|
|
84
89
|
const { runMcpCli } = await import("./mcp/index.js");
|
|
85
90
|
await runMcpCli([argv[0] ?? "node", "pibo mcp", ...argv.slice(3)]);
|
|
@@ -465,6 +470,7 @@ function printRootDiscoveryText() {
|
|
|
465
470
|
|
|
466
471
|
Commands:
|
|
467
472
|
config Manage local pibo config
|
|
473
|
+
auth Manage Web authentication and machine identities
|
|
468
474
|
mcp Discover and call configured MCP servers
|
|
469
475
|
tools Install and inspect curated external CLI tools
|
|
470
476
|
pi-packages Register Pi Coding Agent packages
|
package/dist/compute/cli.js
CHANGED
|
@@ -123,6 +123,13 @@ export function renderComputeResourceHealthText(health) {
|
|
|
123
123
|
const lines = [`Compute resource health: ${health.severity} (read-only)`];
|
|
124
124
|
lines.push(`Generated at: ${health.generatedAt}`);
|
|
125
125
|
lines.push(`Browser processes: ${health.browserProcesses.totalChromiumMainProcesses} main / ${health.browserProcesses.totalChromiumProcesses} total Chromium processes`);
|
|
126
|
+
if (health.browserProcesses.exemptMainProcessDetails.length > 0) {
|
|
127
|
+
lines.push(`Explicitly exempt browser main processes: ${health.browserProcesses.exemptMainProcessDetails.length}`);
|
|
128
|
+
lines.push("PID\tUSER_DATA_DIR\tCOMMAND");
|
|
129
|
+
for (const process of health.browserProcesses.exemptMainProcessDetails.slice(0, 5)) {
|
|
130
|
+
lines.push(`${process.pid}\t${process.userDataDir ?? "-"}\t${process.commandName}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
126
133
|
if (health.browserProcesses.unassignedMainProcessDetails.length > 0) {
|
|
127
134
|
lines.push(`Unmanaged browser main processes: ${health.browserProcesses.unassignedMainProcessDetails.length}`);
|
|
128
135
|
lines.push("PID\tWORKER\tUSER_DATA_DIR\tCOMMAND");
|
|
@@ -2,7 +2,7 @@ import { execFile } from "node:child_process";
|
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import { readdir, readFile } from "node:fs/promises";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
|
-
import { basename, join } from "node:path";
|
|
5
|
+
import { basename, isAbsolute, join, resolve } from "node:path";
|
|
6
6
|
import { promisify } from "node:util";
|
|
7
7
|
import { readResourceReaperTimerStatus } from "../resources/reaper-state.js";
|
|
8
8
|
import { createEmptyBrowserPoolState, normalizeBrowserPoolState } from "../tools/browser-pool.js";
|
|
@@ -43,6 +43,7 @@ export function buildComputeResourceHealth(options = {}) {
|
|
|
43
43
|
const disk = options.disk;
|
|
44
44
|
const checks = [];
|
|
45
45
|
const mainProcesses = processes.filter((process) => process.isChromium && process.isMainProcess);
|
|
46
|
+
const exemptBrowserUserDataDirs = new Set(normalizeBrowserUserDataDirs(options.exemptBrowserUserDataDirs ?? readExemptBrowserUserDataDirs()));
|
|
46
47
|
const perWorker = browserPools.map(({ state, statePath }) => {
|
|
47
48
|
const activeLeaseCount = state.activeLeaseId ? Math.max(1, state.activeLeaseCount ?? 1) : state.activeLeaseCount ?? 0;
|
|
48
49
|
const matched = mainProcesses.filter((process) => browserProcessMatchesPool(process, state));
|
|
@@ -73,8 +74,16 @@ export function buildComputeResourceHealth(options = {}) {
|
|
|
73
74
|
if (browserProcessMatchesPool(process, pool.state))
|
|
74
75
|
assignedMainPids.add(process.pid);
|
|
75
76
|
}
|
|
76
|
-
const
|
|
77
|
-
.filter((process) =>
|
|
77
|
+
const activeWorkerOwnedMainPids = new Set(mainProcesses
|
|
78
|
+
.filter((process) => Boolean(findOwningActiveWorker(process, processes, workers)))
|
|
79
|
+
.map((process) => process.pid));
|
|
80
|
+
const unmatchedMainProcesses = mainProcesses
|
|
81
|
+
.filter((process) => !assignedMainPids.has(process.pid) && !activeWorkerOwnedMainPids.has(process.pid));
|
|
82
|
+
const exemptMainProcessDetails = unmatchedMainProcesses
|
|
83
|
+
.filter((process) => browserProcessMatchesExemptUserDataDir(process, exemptBrowserUserDataDirs))
|
|
84
|
+
.map((process) => describeUnassignedBrowserProcess(process, workers));
|
|
85
|
+
const unassignedMainProcessDetails = unmatchedMainProcesses
|
|
86
|
+
.filter((process) => !browserProcessMatchesExemptUserDataDir(process, exemptBrowserUserDataDirs))
|
|
78
87
|
.map((process) => describeUnassignedBrowserProcess(process, workers));
|
|
79
88
|
const unassignedChromiumMainProcesses = unassignedMainProcessDetails.length;
|
|
80
89
|
const activePoolIds = perWorker.filter((pool) => pool.activeLeaseCount > 0).map((pool) => `${pool.workerId}/${pool.poolId}`);
|
|
@@ -118,6 +127,8 @@ export function buildComputeResourceHealth(options = {}) {
|
|
|
118
127
|
totalChromiumMainProcesses: mainProcesses.length,
|
|
119
128
|
unassignedChromiumMainProcesses,
|
|
120
129
|
unassignedMainProcessDetails,
|
|
130
|
+
exemptChromiumMainProcesses: exemptMainProcessDetails.length,
|
|
131
|
+
exemptMainProcessDetails,
|
|
121
132
|
perWorker,
|
|
122
133
|
},
|
|
123
134
|
browserLeases: { active: activePoolIds.length, activePoolIds, staleCdpFiles },
|
|
@@ -148,6 +159,28 @@ function browserLeakMessage(unassignedCount) {
|
|
|
148
159
|
return `${unassignedCount} unmanaged Chromium main process(es) are not associated with a managed browser pool.`;
|
|
149
160
|
return "Chromium main-process count exceeds managed pool expectations.";
|
|
150
161
|
}
|
|
162
|
+
function findOwningActiveWorker(process, processes, workers) {
|
|
163
|
+
const containerId = process.containerId;
|
|
164
|
+
if (containerId && containerId.length >= 12) {
|
|
165
|
+
const byContainer = workers.find((worker) => worker.state === "running" && (worker.id === containerId || worker.id.startsWith(containerId) || containerId.startsWith(worker.id)));
|
|
166
|
+
if (byContainer)
|
|
167
|
+
return byContainer;
|
|
168
|
+
}
|
|
169
|
+
const activeWorkersByPid = new Map(workers
|
|
170
|
+
.filter((worker) => worker.state === "running" && worker.hostPid !== undefined && worker.hostPid > 1)
|
|
171
|
+
.map((worker) => [worker.hostPid, worker]));
|
|
172
|
+
const processByPid = new Map(processes.map((candidate) => [candidate.pid, candidate]));
|
|
173
|
+
const visited = new Set();
|
|
174
|
+
let current = process;
|
|
175
|
+
while (current && current.pid > 1 && !visited.has(current.pid)) {
|
|
176
|
+
visited.add(current.pid);
|
|
177
|
+
const direct = activeWorkersByPid.get(current.pid) ?? activeWorkersByPid.get(current.ppid);
|
|
178
|
+
if (direct)
|
|
179
|
+
return direct;
|
|
180
|
+
current = processByPid.get(current.ppid);
|
|
181
|
+
}
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
151
184
|
function describeUnassignedBrowserProcess(process, workers) {
|
|
152
185
|
const worker = workers.find((candidate) => candidate.hostPid !== undefined && candidate.hostPid > 0 && candidate.hostPid === process.ppid);
|
|
153
186
|
const userDataDir = readChromeArg(process.args, "user-data-dir");
|
|
@@ -173,6 +206,21 @@ function readChromeArg(args, name) {
|
|
|
173
206
|
const pattern = new RegExp(`(?:^|\\s)--${name}=([^\\s]+)`);
|
|
174
207
|
return args.match(pattern)?.[1];
|
|
175
208
|
}
|
|
209
|
+
function readExemptBrowserUserDataDirs() {
|
|
210
|
+
return (process.env.PIBO_RESOURCE_REAPER_EXEMPT_BROWSER_USER_DATA_DIRS ?? "").split(",");
|
|
211
|
+
}
|
|
212
|
+
function normalizeBrowserUserDataDirs(values) {
|
|
213
|
+
return [...new Set([...values]
|
|
214
|
+
.map((value) => value.trim())
|
|
215
|
+
.filter((value) => value.length > 0 && isAbsolute(value))
|
|
216
|
+
.map((value) => resolve(value)))];
|
|
217
|
+
}
|
|
218
|
+
function browserProcessMatchesExemptUserDataDir(process, exemptions) {
|
|
219
|
+
const userDataDir = readChromeArg(process.args, "user-data-dir");
|
|
220
|
+
if (!userDataDir || !isAbsolute(userDataDir))
|
|
221
|
+
return false;
|
|
222
|
+
return exemptions.has(resolve(userDataDir));
|
|
223
|
+
}
|
|
176
224
|
function sanitizeArgsPreview(args) {
|
|
177
225
|
const redacted = args
|
|
178
226
|
.replace(/(token|access_token|refresh_token|password|passwd|cookie|secret)=([^\s]+)/gi, "$1=<redacted>")
|
|
@@ -199,6 +247,7 @@ export async function getComputeResourceHealth(options = {}) {
|
|
|
199
247
|
browserPools,
|
|
200
248
|
staleCdpFiles,
|
|
201
249
|
reaperTimers: detectReaperTimerStatus(),
|
|
250
|
+
exemptBrowserUserDataDirs: options.exemptBrowserUserDataDirs,
|
|
202
251
|
});
|
|
203
252
|
}
|
|
204
253
|
export function defaultBrowserUseHome() {
|
|
@@ -248,7 +297,12 @@ async function collectWorkers() {
|
|
|
248
297
|
async function collectProcesses() {
|
|
249
298
|
try {
|
|
250
299
|
const { stdout } = await execFileAsync("ps", ["-eo", "pid=,ppid=,pgid=,etimes=,comm=,args="], { maxBuffer: 10 * 1024 * 1024 });
|
|
251
|
-
|
|
300
|
+
const processes = parseProcessList(stdout);
|
|
301
|
+
await Promise.all(processes.filter((process) => process.isChromium && process.isMainProcess).map(async (process) => {
|
|
302
|
+
const cgroup = await readFile(`/proc/${process.pid}/cgroup`, "utf8").catch(() => "");
|
|
303
|
+
process.containerId = parseDockerContainerIdFromCgroup(cgroup);
|
|
304
|
+
}));
|
|
305
|
+
return { processes };
|
|
252
306
|
}
|
|
253
307
|
catch (error) {
|
|
254
308
|
return { processes: [], error: error instanceof Error ? error.message : String(error) };
|
|
@@ -299,6 +353,9 @@ function detectReaperTimerStatus() {
|
|
|
299
353
|
}
|
|
300
354
|
return readResourceReaperTimerStatus();
|
|
301
355
|
}
|
|
356
|
+
export function parseDockerContainerIdFromCgroup(value) {
|
|
357
|
+
return value.match(/(?:^|\/)docker[-/]([a-f0-9]{12,64})(?:\.scope|\/|$)/im)?.[1];
|
|
358
|
+
}
|
|
302
359
|
function browserProcessMatchesPool(process, state) {
|
|
303
360
|
if (state.pid && process.pid === state.pid)
|
|
304
361
|
return true;
|
package/dist/config/config.js
CHANGED
|
@@ -43,6 +43,11 @@ export const PIBO_CONFIG_KEYS = [
|
|
|
43
43
|
type: "string",
|
|
44
44
|
description: "SQLite path for Better Auth data.",
|
|
45
45
|
},
|
|
46
|
+
{
|
|
47
|
+
key: "auth.machineKeyStorePath",
|
|
48
|
+
type: "string",
|
|
49
|
+
description: "Optional path for revocable machine-key records. Default $PIBO_HOME/machine-keys.json.",
|
|
50
|
+
},
|
|
46
51
|
{
|
|
47
52
|
key: "auth.mode",
|
|
48
53
|
type: "string",
|
package/dist/core/events.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { SessionManager, shouldCompact } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { PiboSteeringUnavailableError } from "./events.js";
|
|
2
3
|
import { getOpenAiCodexProviderUsageForActiveModel } from "../auth/openai-codex-usage.js";
|
|
3
4
|
import { normalizeSessionErrorDetails, runtimeSessionErrorDetails } from "./session-errors.js";
|
|
4
5
|
import { expandInlineSkills } from "./skill-expansion.js";
|
|
@@ -422,6 +423,8 @@ export class RoutedSession {
|
|
|
422
423
|
queue = [];
|
|
423
424
|
processing = false;
|
|
424
425
|
disposed = false;
|
|
426
|
+
disposePromise;
|
|
427
|
+
drainPromise;
|
|
425
428
|
fastMode = false;
|
|
426
429
|
fastModePatchedAgents = new WeakSet();
|
|
427
430
|
activeMessage;
|
|
@@ -683,7 +686,32 @@ export class RoutedSession {
|
|
|
683
686
|
};
|
|
684
687
|
this.emit(output);
|
|
685
688
|
this.onStateChange?.({ processing: this.processing, queuedMessages: this.queue.length, disposed: this.disposed });
|
|
686
|
-
|
|
689
|
+
this.startDrain();
|
|
690
|
+
return output;
|
|
691
|
+
}
|
|
692
|
+
async steerMessage(event) {
|
|
693
|
+
this.assertActive();
|
|
694
|
+
const activeMessage = this.activeMessage;
|
|
695
|
+
if (!activeMessage || !this.processing || !this.runtime.session.isStreaming) {
|
|
696
|
+
throw new PiboSteeringUnavailableError();
|
|
697
|
+
}
|
|
698
|
+
const session = this.runtime.session;
|
|
699
|
+
const expandedText = expandInlineSkills(event.text, session.resourceLoader.getSkills().skills);
|
|
700
|
+
try {
|
|
701
|
+
await session.steer(expandedText);
|
|
702
|
+
}
|
|
703
|
+
catch (error) {
|
|
704
|
+
throw new PiboSteeringUnavailableError(`The active session could not accept steering: ${errorMessage(error)}`, { cause: error });
|
|
705
|
+
}
|
|
706
|
+
const output = {
|
|
707
|
+
type: "message_steered",
|
|
708
|
+
piboSessionId: this.piboSessionId,
|
|
709
|
+
eventId: event.id,
|
|
710
|
+
activeEventId: activeMessage.id,
|
|
711
|
+
text: event.text,
|
|
712
|
+
source: event.source,
|
|
713
|
+
};
|
|
714
|
+
this.emit(output);
|
|
687
715
|
return output;
|
|
688
716
|
}
|
|
689
717
|
async executeAction(event) {
|
|
@@ -875,9 +903,26 @@ export class RoutedSession {
|
|
|
875
903
|
return await this.runtime.session.compact(customInstructions);
|
|
876
904
|
}
|
|
877
905
|
async dispose() {
|
|
906
|
+
if (this.disposePromise)
|
|
907
|
+
return this.disposePromise;
|
|
908
|
+
this.disposePromise = this.disposeUnsafe();
|
|
909
|
+
return this.disposePromise;
|
|
910
|
+
}
|
|
911
|
+
async disposeUnsafe() {
|
|
878
912
|
if (this.disposed)
|
|
879
913
|
return;
|
|
914
|
+
const activeMessage = this.activeMessage;
|
|
880
915
|
this.notifyMessagesInterrupted(this.activeAndQueuedMessages(), "session disposed");
|
|
916
|
+
if (activeMessage) {
|
|
917
|
+
const error = "Session disposed while a message was active.";
|
|
918
|
+
this.emit({
|
|
919
|
+
type: "session_error",
|
|
920
|
+
piboSessionId: this.piboSessionId,
|
|
921
|
+
eventId: activeMessage.id,
|
|
922
|
+
error,
|
|
923
|
+
errorDetails: runtimeSessionErrorDetails(error),
|
|
924
|
+
});
|
|
925
|
+
}
|
|
881
926
|
this.cancelProviderRecovery();
|
|
882
927
|
this.queue.length = 0;
|
|
883
928
|
this.onStateChange?.({ processing: this.processing, queuedMessages: this.queue.length, disposed: true });
|
|
@@ -888,7 +933,15 @@ export class RoutedSession {
|
|
|
888
933
|
this.recoverySession = undefined;
|
|
889
934
|
}
|
|
890
935
|
this.disposed = true;
|
|
891
|
-
|
|
936
|
+
const abort = this.runtime.session.abort;
|
|
937
|
+
if (abort)
|
|
938
|
+
await Promise.allSettled([abort.call(this.runtime.session)]);
|
|
939
|
+
try {
|
|
940
|
+
await this.drainPromise;
|
|
941
|
+
}
|
|
942
|
+
finally {
|
|
943
|
+
await this.runtime.dispose();
|
|
944
|
+
}
|
|
892
945
|
}
|
|
893
946
|
async kill() {
|
|
894
947
|
this.notifyMessagesInterrupted(this.activeAndQueuedMessages(), "session killed");
|
|
@@ -918,6 +971,16 @@ export class RoutedSession {
|
|
|
918
971
|
}
|
|
919
972
|
return false;
|
|
920
973
|
}
|
|
974
|
+
startDrain() {
|
|
975
|
+
if (this.drainPromise)
|
|
976
|
+
return;
|
|
977
|
+
const drain = this.drain();
|
|
978
|
+
this.drainPromise = drain;
|
|
979
|
+
void drain.finally(() => {
|
|
980
|
+
if (this.drainPromise === drain)
|
|
981
|
+
this.drainPromise = undefined;
|
|
982
|
+
});
|
|
983
|
+
}
|
|
921
984
|
async drain() {
|
|
922
985
|
if (this.processing || this.disposed)
|
|
923
986
|
return;
|
|
@@ -974,7 +1037,7 @@ export class RoutedSession {
|
|
|
974
1037
|
}
|
|
975
1038
|
}
|
|
976
1039
|
catch (error) {
|
|
977
|
-
if (error instanceof PiboProviderRecoveryCancelledError)
|
|
1040
|
+
if (error instanceof PiboProviderRecoveryCancelledError || this.disposed)
|
|
978
1041
|
return;
|
|
979
1042
|
const message = errorMessage(error);
|
|
980
1043
|
this.emit({
|
|
@@ -1030,7 +1093,7 @@ export class RoutedSession {
|
|
|
1030
1093
|
};
|
|
1031
1094
|
this.emit(output);
|
|
1032
1095
|
this.onStateChange?.({ processing: this.processing, queuedMessages: this.queue.length, disposed: this.disposed });
|
|
1033
|
-
|
|
1096
|
+
this.startDrain();
|
|
1034
1097
|
return output;
|
|
1035
1098
|
}
|
|
1036
1099
|
async runAction(event) {
|
|
@@ -6,6 +6,7 @@ import { RoutedSession } from "./routed-session.js";
|
|
|
6
6
|
import { runtimeSessionErrorDetails } from "./session-errors.js";
|
|
7
7
|
import { createSubagentToolName } from "../subagents/tool.js";
|
|
8
8
|
import { PiboRunRegistry } from "../runs/registry.js";
|
|
9
|
+
import { PiboRunExecutionTimeoutError } from "../runs/lifecycle.js";
|
|
9
10
|
import { createPiboSignalRegistry } from "../signals/registry.js";
|
|
10
11
|
import { createDefaultPiboReliabilityStore } from "../reliability/store.js";
|
|
11
12
|
import { InMemoryPiboSessionStore, } from "../sessions/store.js";
|
|
@@ -85,6 +86,15 @@ function formatRunReminderMessage(notification) {
|
|
|
85
86
|
toolName: run.toolName,
|
|
86
87
|
summary: run.summary,
|
|
87
88
|
})),
|
|
89
|
+
timedOut: notification.timedOut.map((run) => ({
|
|
90
|
+
runId: run.runId,
|
|
91
|
+
kind: run.kind,
|
|
92
|
+
status: run.status,
|
|
93
|
+
toolName: run.toolName,
|
|
94
|
+
summary: run.summary,
|
|
95
|
+
timeoutMs: run.timeoutMs,
|
|
96
|
+
timeoutPhase: run.timeoutPhase,
|
|
97
|
+
})),
|
|
88
98
|
cancelled: notification.cancelled.map((run) => ({
|
|
89
99
|
runId: run.runId,
|
|
90
100
|
kind: run.kind,
|
|
@@ -99,7 +109,7 @@ function formatRunReminderMessage(notification) {
|
|
|
99
109
|
toolName: run.toolName,
|
|
100
110
|
summary: run.summary,
|
|
101
111
|
})),
|
|
102
|
-
instruction: "Use pibo_run_read for completed or
|
|
112
|
+
instruction: "Use pibo_run_read for completed, failed, or timed_out runs. Use pibo_run_wait, pibo_run_status, pibo_run_cancel, or pibo_run_ack for runs you still need to manage.",
|
|
103
113
|
}),
|
|
104
114
|
"</pibo_run_notification>",
|
|
105
115
|
].join("\n");
|
|
@@ -108,7 +118,7 @@ function isRunReminderServiceMessage(event) {
|
|
|
108
118
|
return event.source === "service" && event.text.startsWith("<pibo_run_notification>");
|
|
109
119
|
}
|
|
110
120
|
function isTerminalRunStatus(status) {
|
|
111
|
-
return status === "completed" || status === "failed" || status === "cancelled";
|
|
121
|
+
return status === "completed" || status === "failed" || status === "timed_out" || status === "cancelled";
|
|
112
122
|
}
|
|
113
123
|
function asJsonObject(value) {
|
|
114
124
|
return value ?? {};
|
|
@@ -212,7 +222,9 @@ export class PiboSessionRouter {
|
|
|
212
222
|
this.clearIdleSessionTimer(event.piboSessionId);
|
|
213
223
|
try {
|
|
214
224
|
if (event.type === "message") {
|
|
215
|
-
return
|
|
225
|
+
return event.delivery === "steer"
|
|
226
|
+
? await session.steerMessage(event)
|
|
227
|
+
: session.enqueueMessage(event);
|
|
216
228
|
}
|
|
217
229
|
if (event.action === "abort") {
|
|
218
230
|
this.signalRegistry.project({ type: "session_interrupted", piboSessionId: event.piboSessionId, reason: "abort action" });
|
|
@@ -232,6 +244,23 @@ export class PiboSessionRouter {
|
|
|
232
244
|
}
|
|
233
245
|
return output;
|
|
234
246
|
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
if (event.type === "message" && event.id) {
|
|
249
|
+
this.signalRegistry.project({
|
|
250
|
+
type: "message_rejected",
|
|
251
|
+
piboSessionId: event.piboSessionId,
|
|
252
|
+
eventId: event.id,
|
|
253
|
+
});
|
|
254
|
+
const status = session.getStatus();
|
|
255
|
+
this.signalRegistry.project({
|
|
256
|
+
type: "session_processing_changed",
|
|
257
|
+
piboSessionId: event.piboSessionId,
|
|
258
|
+
processing: status.processing,
|
|
259
|
+
queuedMessages: status.queuedMessages,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
throw error;
|
|
263
|
+
}
|
|
235
264
|
finally {
|
|
236
265
|
this.scheduleIdleSessionEvictionIfIdle(event.piboSessionId);
|
|
237
266
|
}
|
|
@@ -667,7 +696,7 @@ export class PiboSessionRouter {
|
|
|
667
696
|
}
|
|
668
697
|
createRunToolController(parentPiboSessionId) {
|
|
669
698
|
return {
|
|
670
|
-
startToolRun: ({ toolName, params, completionPolicy, retryable, maxAttempts, execute }) => {
|
|
699
|
+
startToolRun: ({ toolName, params, completionPolicy, retryable, maxAttempts, timeoutMs, serviceWarning, execute }) => {
|
|
671
700
|
assertGatewayResourceAvailableForWork(`yielded run ${toolName}`);
|
|
672
701
|
const run = this.runRegistry.startToolRun({
|
|
673
702
|
controllerPiboSessionId: parentPiboSessionId,
|
|
@@ -676,6 +705,8 @@ export class PiboSessionRouter {
|
|
|
676
705
|
completionPolicy,
|
|
677
706
|
retryable,
|
|
678
707
|
maxAttempts,
|
|
708
|
+
timeoutMs,
|
|
709
|
+
serviceWarning,
|
|
679
710
|
});
|
|
680
711
|
void (async () => {
|
|
681
712
|
try {
|
|
@@ -685,8 +716,11 @@ export class PiboSessionRouter {
|
|
|
685
716
|
this.scheduleRunReminder(parentPiboSessionId, false);
|
|
686
717
|
}
|
|
687
718
|
catch (error) {
|
|
688
|
-
const
|
|
689
|
-
|
|
719
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
720
|
+
const terminalRun = error instanceof PiboRunExecutionTimeoutError
|
|
721
|
+
? this.runRegistry.timeOut(run.runId, message, error.timeoutPhase)
|
|
722
|
+
: this.runRegistry.fail(run.runId, message);
|
|
723
|
+
if (terminalRun)
|
|
690
724
|
this.scheduleRunReminder(parentPiboSessionId, false);
|
|
691
725
|
}
|
|
692
726
|
})();
|
|
@@ -303,7 +303,7 @@ function payloadForOutputEvent(event) {
|
|
|
303
303
|
function previewTextForOutputEvent(event) {
|
|
304
304
|
if (event.type === "assistant_message" || event.type === "assistant_delta" || event.type === "thinking_delta" || event.type === "thinking_finished")
|
|
305
305
|
return previewText(event.text ?? "");
|
|
306
|
-
if (event.type === "message_queued" || event.type === "message_started")
|
|
306
|
+
if (event.type === "message_queued" || event.type === "message_steered" || event.type === "message_started")
|
|
307
307
|
return previewText(event.text);
|
|
308
308
|
if (event.type === "tool_call" || event.type === "tool_execution_started" || event.type === "tool_execution_updated" || event.type === "tool_execution_finished")
|
|
309
309
|
return event.toolName;
|
|
@@ -320,6 +320,8 @@ function previewTextForOutputEvent(event) {
|
|
|
320
320
|
function attributesForOutputEvent(event) {
|
|
321
321
|
if (event.type === "message_queued")
|
|
322
322
|
return { inlineText: event.text, source: event.source, queuedMessages: event.queuedMessages };
|
|
323
|
+
if (event.type === "message_steered")
|
|
324
|
+
return { inlineText: event.text, source: event.source, activeEventId: event.activeEventId };
|
|
323
325
|
if (event.type === "assistant_message" || event.type === "assistant_delta")
|
|
324
326
|
return { assistantIndex: event.assistantIndex, contentIndex: event.contentIndex };
|
|
325
327
|
if (event.type === "thinking_started" || event.type === "thinking_delta" || event.type === "thinking_finished")
|
package/dist/data/schema.js
CHANGED
|
@@ -354,6 +354,10 @@ export function applyPiboDataSchema(db) {
|
|
|
354
354
|
ON sessions(channel, kind, updated_at DESC);
|
|
355
355
|
CREATE INDEX IF NOT EXISTS idx_event_log_session_stream
|
|
356
356
|
ON event_log(session_id, stream_id);
|
|
357
|
+
CREATE INDEX IF NOT EXISTS idx_event_log_unread_session_stream
|
|
358
|
+
ON event_log(session_id, stream_id)
|
|
359
|
+
WHERE (retention_class = 'chat_message' AND type IN ('user.message.accepted', 'assistant_message'))
|
|
360
|
+
OR type = 'session_error';
|
|
357
361
|
CREATE INDEX IF NOT EXISTS idx_event_log_session_sequence_stream
|
|
358
362
|
ON event_log(session_id, session_sequence DESC, stream_id DESC);
|
|
359
363
|
CREATE INDEX IF NOT EXISTS idx_event_log_session_type_sequence_stream
|
package/dist/debug/index.js
CHANGED
|
@@ -985,6 +985,10 @@ function compactRunRow(run) {
|
|
|
985
985
|
policy: run.completionPolicy,
|
|
986
986
|
consumed: run.consumed,
|
|
987
987
|
updatedAt: run.updatedAt,
|
|
988
|
+
timeoutMs: run.timeoutMs,
|
|
989
|
+
timeoutAt: run.timeoutAt,
|
|
990
|
+
timeoutPhase: run.timeoutPhase,
|
|
991
|
+
serviceWarning: run.serviceWarning,
|
|
988
992
|
summary: run.summary,
|
|
989
993
|
};
|
|
990
994
|
}
|