@pasko70/pibo 1.10.1 → 1.11.1
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/static-assets.js +10 -2
- package/dist/apps/chat/web-app.js +18 -2
- package/dist/apps/chat/workflow-manual-trigger-runtime.js +46 -149
- package/dist/apps/chat-ui/assets/{dist-BA6mhAec.js → dist-BBVpFHAq.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-hU-2oAb6.js → dist-BXyVMdHv.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-BPl-fzRB.js → dist-BiGfVaXN.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-BpfBSMzK.js → dist-CCGKu-Wj.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-uF6UXzI7.js → dist-CE0MvPLM.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-Cvx5M97R.js → dist-CuGiEm5l.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-BPnn9e2B.js → dist-DBnh8gXR.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-r31x5Fcc.js → dist-DeOnZ-pw.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-4NlLjLs7.js → dist-DnQYnLQS.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-DnXWNQRm.js → dist-VT4x40uL.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-3Sts0Afa.js → dist-xtnVygdr.js} +1 -1
- package/dist/apps/chat-ui/assets/{index-al8DeEjA.css → index-DNeE4HrG.css} +1 -1
- package/dist/apps/chat-ui/assets/{index-CaTGYBOS.js → index-vcg8JNj9.js} +69 -69
- package/dist/apps/chat-ui/favicon.svg +4 -0
- package/dist/apps/chat-ui/index.html +4 -2
- package/dist/apps/chat-ui/sw.js +12 -1
- package/dist/apps/chat-vscode-web/assets/index-Bf2JvJ9z.css +2 -0
- package/dist/apps/chat-vscode-web/assets/index-DVlCO7v_.js +41 -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.1.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.js +6 -0
- package/dist/compute/cli.js +7 -0
- package/dist/compute/resource-health.js +27 -3
- package/dist/config/config.js +5 -0
- package/dist/core/session-router.js +17 -0
- package/dist/data/schema.js +4 -0
- package/dist/debug/trace-status.js +25 -0
- package/dist/debug/trace.js +9 -10
- package/dist/resources/cli.js +15 -0
- package/dist/resources/lifecycle.js +28 -4
- package/dist/resources/reaper.js +1 -0
- package/dist/session-ui/terminalRows.js +10 -0
- package/dist/shared/trace-event-projection.js +20 -3
- package/dist/shared/trace-transcript.js +4 -0
- package/dist/signals/projector.js +10 -1
- package/package.json +3 -5
- package/dist/apps/chat-vscode-web/assets/index-B5QK07zO.css +0 -2
- package/dist/apps/chat-vscode-web/assets/index-CK4SMZuu.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
|
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));
|
|
@@ -76,8 +77,13 @@ export function buildComputeResourceHealth(options = {}) {
|
|
|
76
77
|
const activeWorkerOwnedMainPids = new Set(mainProcesses
|
|
77
78
|
.filter((process) => Boolean(findOwningActiveWorker(process, processes, workers)))
|
|
78
79
|
.map((process) => process.pid));
|
|
79
|
-
const
|
|
80
|
-
.filter((process) => !assignedMainPids.has(process.pid) && !activeWorkerOwnedMainPids.has(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))
|
|
81
87
|
.map((process) => describeUnassignedBrowserProcess(process, workers));
|
|
82
88
|
const unassignedChromiumMainProcesses = unassignedMainProcessDetails.length;
|
|
83
89
|
const activePoolIds = perWorker.filter((pool) => pool.activeLeaseCount > 0).map((pool) => `${pool.workerId}/${pool.poolId}`);
|
|
@@ -121,6 +127,8 @@ export function buildComputeResourceHealth(options = {}) {
|
|
|
121
127
|
totalChromiumMainProcesses: mainProcesses.length,
|
|
122
128
|
unassignedChromiumMainProcesses,
|
|
123
129
|
unassignedMainProcessDetails,
|
|
130
|
+
exemptChromiumMainProcesses: exemptMainProcessDetails.length,
|
|
131
|
+
exemptMainProcessDetails,
|
|
124
132
|
perWorker,
|
|
125
133
|
},
|
|
126
134
|
browserLeases: { active: activePoolIds.length, activePoolIds, staleCdpFiles },
|
|
@@ -198,6 +206,21 @@ function readChromeArg(args, name) {
|
|
|
198
206
|
const pattern = new RegExp(`(?:^|\\s)--${name}=([^\\s]+)`);
|
|
199
207
|
return args.match(pattern)?.[1];
|
|
200
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
|
+
}
|
|
201
224
|
function sanitizeArgsPreview(args) {
|
|
202
225
|
const redacted = args
|
|
203
226
|
.replace(/(token|access_token|refresh_token|password|passwd|cookie|secret)=([^\s]+)/gi, "$1=<redacted>")
|
|
@@ -224,6 +247,7 @@ export async function getComputeResourceHealth(options = {}) {
|
|
|
224
247
|
browserPools,
|
|
225
248
|
staleCdpFiles,
|
|
226
249
|
reaperTimers: detectReaperTimerStatus(),
|
|
250
|
+
exemptBrowserUserDataDirs: options.exemptBrowserUserDataDirs,
|
|
227
251
|
});
|
|
228
252
|
}
|
|
229
253
|
export function defaultBrowserUseHome() {
|
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",
|
|
@@ -244,6 +244,23 @@ export class PiboSessionRouter {
|
|
|
244
244
|
}
|
|
245
245
|
return output;
|
|
246
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
|
+
}
|
|
247
264
|
finally {
|
|
248
265
|
this.scheduleIdleSessionEvictionIfIdle(event.piboSessionId);
|
|
249
266
|
}
|
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
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export function resolveDebugTraceSessionStatus(sessionStatus, eventTypes) {
|
|
2
|
+
for (let index = eventTypes.length - 1; index >= 0; index -= 1) {
|
|
3
|
+
switch (eventTypes[index]) {
|
|
4
|
+
case "session_error":
|
|
5
|
+
return { status: "error", source: "event-log" };
|
|
6
|
+
case "message_started":
|
|
7
|
+
return { status: "running", source: "event-log" };
|
|
8
|
+
case "message_finished":
|
|
9
|
+
return { status: "idle", source: "event-log" };
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
return {
|
|
13
|
+
status: sessionStatus === "running" || sessionStatus === "error" ? sessionStatus : "idle",
|
|
14
|
+
source: "session-store",
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export function summarizeDebugTraceStatus(sessionStatus, nodeStatuses) {
|
|
18
|
+
const errorNodeCount = nodeStatuses.filter((status) => status === "error").length;
|
|
19
|
+
if (sessionStatus === "error")
|
|
20
|
+
return { status: "error", errorNodeCount };
|
|
21
|
+
if (sessionStatus === "running" || nodeStatuses.some((status) => status === "running")) {
|
|
22
|
+
return { status: "running", errorNodeCount };
|
|
23
|
+
}
|
|
24
|
+
return { status: "done", errorNodeCount };
|
|
25
|
+
}
|
package/dist/debug/trace.js
CHANGED
|
@@ -3,6 +3,7 @@ import { normalizeSessionErrorDetails } from "../core/session-errors.js";
|
|
|
3
3
|
import { compareTraceNodes } from "../shared/trace-nodes.js";
|
|
4
4
|
import { openReadOnlyDebugDatabase, withStorePath } from "./sql.js";
|
|
5
5
|
import { formatNextCommands } from "./next-commands.js";
|
|
6
|
+
import { resolveDebugTraceSessionStatus, summarizeDebugTraceStatus } from "./trace-status.js";
|
|
6
7
|
export async function inspectDebugTrace(piboSessionId, stores, options = {}) {
|
|
7
8
|
if (!stores.sessions.exists)
|
|
8
9
|
throw new Error(`Debug store "sessions" not found at ${stores.sessions.path}`);
|
|
@@ -22,19 +23,23 @@ export async function inspectDebugTrace(piboSessionId, stores, options = {}) {
|
|
|
22
23
|
.prepare("SELECT stream_id, session_id, session_sequence, event_id, type, created_at, preview_text, attributes_json FROM event_log WHERE session_id = ? ORDER BY stream_id ASC")
|
|
23
24
|
.all(piboSessionId).map((row) => eventFromRow(row, adapterIssues)).filter((event) => event !== undefined)
|
|
24
25
|
: [];
|
|
26
|
+
const sessionStatus = resolveDebugTraceSessionStatus(sessionRow.status, events.map((event) => event.type));
|
|
25
27
|
const view = await buildTraceView({
|
|
26
28
|
session,
|
|
27
29
|
sessions,
|
|
28
30
|
events,
|
|
29
|
-
status:
|
|
31
|
+
status: sessionStatus.status,
|
|
30
32
|
});
|
|
31
33
|
const rows = flattenTraceNodes(view.nodes);
|
|
34
|
+
const statusSummary = summarizeDebugTraceStatus(sessionStatus.status, rows.map((node) => node.status));
|
|
32
35
|
const filtered = options.runningOnly ? rows.filter((node) => node.status === "running") : rows;
|
|
33
36
|
return {
|
|
34
37
|
piboSessionId: view.piboSessionId,
|
|
35
38
|
piSessionId: view.piSessionId,
|
|
36
39
|
title: view.title,
|
|
37
|
-
status:
|
|
40
|
+
status: statusSummary.status,
|
|
41
|
+
statusSource: sessionStatus.source,
|
|
42
|
+
errorNodeCount: statusSummary.errorNodeCount,
|
|
38
43
|
nodes: filtered,
|
|
39
44
|
rawNodeCount: rows.length,
|
|
40
45
|
...(options.check ? { checks: checkTraceView(view, adapterIssues) } : {}),
|
|
@@ -69,6 +74,8 @@ export function formatDebugTrace(result, options = {}) {
|
|
|
69
74
|
`piSessionId: ${result.piSessionId}`,
|
|
70
75
|
`title: ${result.title}`,
|
|
71
76
|
`status: ${result.status}`,
|
|
77
|
+
`statusSource: ${result.statusSource}`,
|
|
78
|
+
`nodeErrors: ${result.errorNodeCount}`,
|
|
72
79
|
"",
|
|
73
80
|
];
|
|
74
81
|
if (result.nodes.length === 0) {
|
|
@@ -161,14 +168,6 @@ function flattenTraceNodes(nodes, depth = 0) {
|
|
|
161
168
|
...flattenTraceNodes(node.children, depth + 1),
|
|
162
169
|
]);
|
|
163
170
|
}
|
|
164
|
-
function traceStatus(view) {
|
|
165
|
-
const rows = flattenTraceNodes(view.nodes);
|
|
166
|
-
if (rows.some((node) => node.status === "error"))
|
|
167
|
-
return "error";
|
|
168
|
-
if (rows.some((node) => node.status === "running"))
|
|
169
|
-
return "running";
|
|
170
|
-
return "done";
|
|
171
|
-
}
|
|
172
171
|
export function checkTraceView(view, adapterIssues = []) {
|
|
173
172
|
const issues = [...adapterIssues];
|
|
174
173
|
const all = flattenPiboTraceNodes(view.nodes);
|
package/dist/resources/cli.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isAbsolute, resolve } from "node:path";
|
|
1
2
|
import { Command } from "commander";
|
|
2
3
|
import { getComputeResourceHealth } from "../compute/resource-health.js";
|
|
3
4
|
import { renderComputeResourceHealthText } from "../compute/cli.js";
|
|
@@ -17,6 +18,15 @@ function parsePidList(value) {
|
|
|
17
18
|
throw new Error("PIDs must be positive integers separated by commas");
|
|
18
19
|
return [...new Set(pids)];
|
|
19
20
|
}
|
|
21
|
+
function parseAbsolutePathList(value) {
|
|
22
|
+
const paths = value.split(",").map((item) => item.trim());
|
|
23
|
+
if (paths.some((path) => !path || !isAbsolute(path)))
|
|
24
|
+
throw new Error("Browser user-data directories must be absolute paths separated by commas");
|
|
25
|
+
return [...new Set(paths.map((path) => resolve(path)))];
|
|
26
|
+
}
|
|
27
|
+
function shellQuote(value) {
|
|
28
|
+
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
29
|
+
}
|
|
20
30
|
export function renderResourceLeasesText(leases) {
|
|
21
31
|
if (leases.length === 0)
|
|
22
32
|
return "No active managed browser-pool leases.\nNext: pibo resources status";
|
|
@@ -53,6 +63,10 @@ export function renderResourceReapText(value) {
|
|
|
53
63
|
];
|
|
54
64
|
if (plan.options.includeDev)
|
|
55
65
|
args.push("--include-dev");
|
|
66
|
+
if (plan.options.exemptBrowserPids.length > 0)
|
|
67
|
+
args.push(`--exempt-browser-pids ${plan.options.exemptBrowserPids.join(",")}`);
|
|
68
|
+
if (plan.options.exemptBrowserUserDataDirs.length > 0)
|
|
69
|
+
args.push(`--exempt-browser-user-data-dirs ${shellQuote(plan.options.exemptBrowserUserDataDirs.join(","))}`);
|
|
56
70
|
if (plan.options.browserPoolRoot)
|
|
57
71
|
args.push(`--browser-pool-root ${plan.options.browserPoolRoot}`);
|
|
58
72
|
if (plan.options.browserUseHome)
|
|
@@ -118,6 +132,7 @@ export async function runResourcesCli(argv) {
|
|
|
118
132
|
.option("--idle-timeout-minutes <n>", "Select browser pools idle for this many minutes", parseNonNegativeNumber, 10)
|
|
119
133
|
.option("--unmanaged-browser-grace-minutes <n>", "Select unmanaged Chromium older than this many minutes", parseNonNegativeNumber, 10)
|
|
120
134
|
.option("--exempt-browser-pids <list>", "Comma-separated browser PIDs or process groups to preserve", parsePidList)
|
|
135
|
+
.option("--exempt-browser-user-data-dirs <list>", "Comma-separated absolute browser profile directories to preserve", parseAbsolutePathList)
|
|
121
136
|
.option("--browser-pool-root <path>", "Browser pool root directory to scan")
|
|
122
137
|
.option("--browser-use-home <path>", "Browser-use home directory to scan for stale CDP files")
|
|
123
138
|
.option("--json", "Print machine-readable cleanup plan or result")
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import { readdir, readFile, rm } from "node:fs/promises";
|
|
4
|
-
import { dirname, join } from "node:path";
|
|
4
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
5
5
|
import { promisify } from "node:util";
|
|
6
6
|
import { applyComputeWorkerReapPlan, buildComputeWorkerReapPlan, planReapWorkers, } from "../compute/docker.js";
|
|
7
7
|
import { defaultBrowserPoolRoot, defaultBrowserUseHome, getComputeResourceHealth, parseProcessList, } from "../compute/resource-health.js";
|
|
@@ -60,9 +60,14 @@ export async function planResourceReap(options = {}) {
|
|
|
60
60
|
collectManagedBrowserPools(resolved.browserPoolRoot),
|
|
61
61
|
planStaleCdpFiles(resolved.browserUseHome),
|
|
62
62
|
planComputeReapSafely({ includeDev: resolved.includeDev, maxAgeMinutes: resolved.maxAgeMinutes, now }),
|
|
63
|
-
getComputeResourceHealth({
|
|
63
|
+
getComputeResourceHealth({
|
|
64
|
+
now,
|
|
65
|
+
browserPoolRoot: resolved.browserPoolRoot,
|
|
66
|
+
browserUseHome: resolved.browserUseHome,
|
|
67
|
+
exemptBrowserUserDataDirs: [],
|
|
68
|
+
}),
|
|
64
69
|
]);
|
|
65
|
-
const unmanagedBrowsers = buildUnmanagedBrowserPlanItems(health.browserProcesses.unassignedMainProcessDetails, resolved.unmanagedBrowserGraceMinutes, new Set(resolved.exemptBrowserPids));
|
|
70
|
+
const unmanagedBrowsers = buildUnmanagedBrowserPlanItems(health.browserProcesses.unassignedMainProcessDetails, resolved.unmanagedBrowserGraceMinutes, new Set(resolved.exemptBrowserPids), new Set(resolved.exemptBrowserUserDataDirs));
|
|
66
71
|
return buildResourceReapPlan({ now, options: resolved, records, staleFiles, unmanagedBrowsers, compute });
|
|
67
72
|
}
|
|
68
73
|
export function buildResourceReapPlan(input) {
|
|
@@ -126,9 +131,10 @@ function resolveReapOptions(options) {
|
|
|
126
131
|
browserPoolRoot: options.browserPoolRoot ?? defaultBrowserPoolRoot(),
|
|
127
132
|
browserUseHome: options.browserUseHome ?? defaultBrowserUseHome(),
|
|
128
133
|
exemptBrowserPids: options.exemptBrowserPids ?? readExemptBrowserPids(),
|
|
134
|
+
exemptBrowserUserDataDirs: normalizeBrowserUserDataDirs(options.exemptBrowserUserDataDirs ?? readExemptBrowserUserDataDirs()),
|
|
129
135
|
};
|
|
130
136
|
}
|
|
131
|
-
export function buildUnmanagedBrowserPlanItems(processes, graceMinutes, exemptPids = new Set()) {
|
|
137
|
+
export function buildUnmanagedBrowserPlanItems(processes, graceMinutes, exemptPids = new Set(), exemptUserDataDirs = new Set()) {
|
|
132
138
|
const graceSeconds = graceMinutes * 60;
|
|
133
139
|
return processes.map((process) => {
|
|
134
140
|
let action = "terminate";
|
|
@@ -141,6 +147,10 @@ export function buildUnmanagedBrowserPlanItems(processes, graceMinutes, exemptPi
|
|
|
141
147
|
action = "skip";
|
|
142
148
|
reason = "explicitly exempted pid or process group";
|
|
143
149
|
}
|
|
150
|
+
else if (browserUserDataDirIsExempt(process.userDataDir, exemptUserDataDirs)) {
|
|
151
|
+
action = "skip";
|
|
152
|
+
reason = "explicitly exempted browser user-data-dir";
|
|
153
|
+
}
|
|
144
154
|
else if (process.elapsedSeconds !== undefined && process.elapsedSeconds < graceSeconds) {
|
|
145
155
|
action = "skip";
|
|
146
156
|
reason = `process age ${process.elapsedSeconds}s is within ${graceSeconds}s grace period`;
|
|
@@ -211,6 +221,20 @@ function readExemptBrowserPids() {
|
|
|
211
221
|
.map((value) => Number.parseInt(value.trim(), 10))
|
|
212
222
|
.filter((value) => Number.isInteger(value) && value > 0);
|
|
213
223
|
}
|
|
224
|
+
function readExemptBrowserUserDataDirs() {
|
|
225
|
+
return (process.env.PIBO_RESOURCE_REAPER_EXEMPT_BROWSER_USER_DATA_DIRS ?? "").split(",");
|
|
226
|
+
}
|
|
227
|
+
function normalizeBrowserUserDataDirs(values) {
|
|
228
|
+
return [...new Set([...values]
|
|
229
|
+
.map((value) => value.trim())
|
|
230
|
+
.filter((value) => value.length > 0 && isAbsolute(value))
|
|
231
|
+
.map((value) => resolve(value)))];
|
|
232
|
+
}
|
|
233
|
+
function browserUserDataDirIsExempt(userDataDir, exemptions) {
|
|
234
|
+
if (!userDataDir || !isAbsolute(userDataDir))
|
|
235
|
+
return false;
|
|
236
|
+
return exemptions.has(resolve(userDataDir));
|
|
237
|
+
}
|
|
214
238
|
function buildBrowserReapPlanItem(record, now, idleTimeoutMinutes) {
|
|
215
239
|
const { state } = record;
|
|
216
240
|
let action = "skip";
|
package/dist/resources/reaper.js
CHANGED
|
@@ -76,6 +76,7 @@ export class ResourceReaperService {
|
|
|
76
76
|
browserPoolRoot: this.options.browserPoolRoot,
|
|
77
77
|
browserUseHome: this.options.browserUseHome,
|
|
78
78
|
exemptBrowserPids: this.options.exemptBrowserPids,
|
|
79
|
+
exemptBrowserUserDataDirs: this.options.exemptBrowserUserDataDirs,
|
|
79
80
|
now: runAt,
|
|
80
81
|
});
|
|
81
82
|
result = await this.apply(plan);
|
|
@@ -205,6 +205,7 @@ function createUserMessageRow(node) {
|
|
|
205
205
|
lines: [{ prefix: "prompt", tokens: [token(text)] }],
|
|
206
206
|
sourceNodeIds: [node.id],
|
|
207
207
|
forkEntryId: node.entryId,
|
|
208
|
+
pendingMessageDelivery: pendingUserMessageDelivery(node),
|
|
208
209
|
startedAt: node.startedAt,
|
|
209
210
|
output: text,
|
|
210
211
|
payloadRefs: node.payloadRefs,
|
|
@@ -1193,6 +1194,15 @@ function delegationVerb(status) {
|
|
|
1193
1194
|
return "Spawn failed";
|
|
1194
1195
|
return "Spawned";
|
|
1195
1196
|
}
|
|
1197
|
+
function pendingUserMessageDelivery(node) {
|
|
1198
|
+
if (node.status !== "running")
|
|
1199
|
+
return undefined;
|
|
1200
|
+
if (node.id.startsWith("event:message_steered:"))
|
|
1201
|
+
return "steer";
|
|
1202
|
+
if (node.id.startsWith("event:message_queued:"))
|
|
1203
|
+
return "queue";
|
|
1204
|
+
return undefined;
|
|
1205
|
+
}
|
|
1196
1206
|
function asyncVerb(status) {
|
|
1197
1207
|
if (status === "running")
|
|
1198
1208
|
return "Waiting for";
|
|
@@ -42,6 +42,14 @@ export function applySingleEventToNodes(nodes, byId, piboSessionId, storedEvent,
|
|
|
42
42
|
const node = traceNodeFromEvent(piboSessionId, payload, childByParent, linkedChildByToolCallId, sessionStatus, storedEvent.createdAt, storedEvent.eventSequence, storedEvent.streamId, storedEvent.streamFrameIndex);
|
|
43
43
|
if (!node)
|
|
44
44
|
return;
|
|
45
|
+
if (node.type === "user.message" &&
|
|
46
|
+
node.status === "running" &&
|
|
47
|
+
!node.parentId &&
|
|
48
|
+
payload.type === "message_steered") {
|
|
49
|
+
const activeTurn = [...byId.values()].reverse().find((candidate) => candidate.type === "agent.turn" && candidate.status === "running");
|
|
50
|
+
if (activeTurn)
|
|
51
|
+
node.parentId = activeTurn.id;
|
|
52
|
+
}
|
|
45
53
|
if (node.type === "agent.turn" && node.eventId) {
|
|
46
54
|
const existingTurn = [...byId.values()].find((candidate) => candidate.type === "agent.turn" && candidate.eventId === node.eventId);
|
|
47
55
|
if (existingTurn) {
|
|
@@ -94,8 +102,12 @@ export function applySingleEventToNodes(nodes, byId, piboSessionId, storedEvent,
|
|
|
94
102
|
if (node.eventId) {
|
|
95
103
|
const existing = byId.get(node.id);
|
|
96
104
|
if (existing) {
|
|
97
|
-
if (node.type === "user.message"
|
|
98
|
-
existing.
|
|
105
|
+
if (node.type === "user.message") {
|
|
106
|
+
existing.status = node.status;
|
|
107
|
+
existing.parentId = node.parentId ?? existing.parentId;
|
|
108
|
+
existing.summary = node.summary ?? existing.summary;
|
|
109
|
+
existing.output = node.output ?? existing.output;
|
|
110
|
+
}
|
|
99
111
|
return;
|
|
100
112
|
}
|
|
101
113
|
}
|
|
@@ -297,7 +309,7 @@ function traceNodeFromEvent(piboSessionId, event, childByParent, linkedChildByTo
|
|
|
297
309
|
...(event.type === "message_steered" && event.activeEventId ? { parentId: messageTurnNodeId(event.activeEventId) } : {}),
|
|
298
310
|
type: "user.message",
|
|
299
311
|
title: "User Message",
|
|
300
|
-
status: "done",
|
|
312
|
+
status: isOptimisticUserMessageEvent(event) ? "running" : "done",
|
|
301
313
|
summary: event.text,
|
|
302
314
|
output: event.text,
|
|
303
315
|
};
|
|
@@ -538,6 +550,11 @@ function mergeReasoningEvent(target, update) {
|
|
|
538
550
|
target.output = update.output ?? target.output;
|
|
539
551
|
target.completedAt = update.completedAt ?? target.completedAt;
|
|
540
552
|
}
|
|
553
|
+
function isOptimisticUserMessageEvent(event) {
|
|
554
|
+
return ((event.type === "message_queued" || event.type === "message_steered") &&
|
|
555
|
+
"clientTxnId" in event &&
|
|
556
|
+
typeof event.clientTxnId === "string");
|
|
557
|
+
}
|
|
541
558
|
function isInternalSessionOperation(action) {
|
|
542
559
|
return action === "session.fork" || action === "session.clone" || action === "session.switch";
|
|
543
560
|
}
|
|
@@ -181,6 +181,7 @@ function createAssistantTurnNodes(piboSessionId, entries, timing) {
|
|
|
181
181
|
return [];
|
|
182
182
|
const orderedNodes = [];
|
|
183
183
|
const toolsByCallId = new Map();
|
|
184
|
+
let assistantIndex = 0;
|
|
184
185
|
for (const { entry, index: entryIndex } of entries) {
|
|
185
186
|
if (messageRole(entry) === "toolResult") {
|
|
186
187
|
mergePersistedToolResult(toolsByCallId, orderedNodes, entry, piboSessionId, entryIndex);
|
|
@@ -224,6 +225,9 @@ function createAssistantTurnNodes(piboSessionId, entries, timing) {
|
|
|
224
225
|
if (responseNode) {
|
|
225
226
|
responseNode.status = responseStatus;
|
|
226
227
|
responseNode.error = responseError;
|
|
228
|
+
if (timing?.eventId)
|
|
229
|
+
responseNode.stableKey = `assistant:${timing.eventId}:assistant:${assistantIndex}`;
|
|
230
|
+
assistantIndex += 1;
|
|
227
231
|
}
|
|
228
232
|
}
|
|
229
233
|
const finalNode = orderedNodes.at(-1);
|
|
@@ -28,7 +28,7 @@ function settleActiveSessionNodes(piboSessionId, context, status, terminalSource
|
|
|
28
28
|
}
|
|
29
29
|
export const sessionLifecycleSignalProducer = {
|
|
30
30
|
name: "session-lifecycle",
|
|
31
|
-
accepts: (input) => ["session_created", "session_disposed", "session_processing_changed", "message_accepted", "queue_changed", "recovery", "session_interrupted", "signal_node_pruned"].includes(input.type),
|
|
31
|
+
accepts: (input) => ["session_created", "session_disposed", "session_processing_changed", "message_accepted", "message_rejected", "queue_changed", "recovery", "session_interrupted", "signal_node_pruned"].includes(input.type),
|
|
32
32
|
project(input, context) {
|
|
33
33
|
const data = input;
|
|
34
34
|
if (data.type === "session_created") {
|
|
@@ -78,6 +78,15 @@ export const sessionLifecycleSignalProducer = {
|
|
|
78
78
|
}
|
|
79
79
|
return mutations;
|
|
80
80
|
}
|
|
81
|
+
if (data.type === "message_rejected") {
|
|
82
|
+
const acceptedTurn = context.getNode(`turn:${data.piboSessionId}:${data.eventId}`);
|
|
83
|
+
return [
|
|
84
|
+
{ type: "remove_node", nodeId: `message:${data.piboSessionId}:${data.eventId}` },
|
|
85
|
+
...(acceptedTurn?.metadata?.accepted === true
|
|
86
|
+
? [{ type: "remove_node", nodeId: acceptedTurn.id }]
|
|
87
|
+
: []),
|
|
88
|
+
];
|
|
89
|
+
}
|
|
81
90
|
if (data.type === "session_disposed") {
|
|
82
91
|
return [
|
|
83
92
|
...settleActiveSessionNodes(data.piboSessionId, context, "cancelled", "session_disposed"),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pasko70/pibo",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"imports": {
|
|
6
6
|
"vscode": "./src/apps/chat-vscode/extension/src/vscode-shim.js"
|
|
@@ -57,8 +57,6 @@
|
|
|
57
57
|
"@earendil-works/pi-ai": "0.80.6",
|
|
58
58
|
"@earendil-works/pi-coding-agent": "0.80.6",
|
|
59
59
|
"@earendil-works/pi-tui": "0.80.6",
|
|
60
|
-
"@langchain/core": "^1.2.3",
|
|
61
|
-
"@langchain/langgraph": "^1.4.8",
|
|
62
60
|
"@mdxeditor/editor": "^3.55.0",
|
|
63
61
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
64
62
|
"@tailwindcss/vite": "^4.2.4",
|
|
@@ -79,8 +77,7 @@
|
|
|
79
77
|
"react-markdown": "^10.1.0",
|
|
80
78
|
"react-virtuoso": "^4.18.6",
|
|
81
79
|
"remark-gfm": "^4.0.1",
|
|
82
|
-
"tailwindcss": "^4.2.4"
|
|
83
|
-
"zod": "4.4.1"
|
|
80
|
+
"tailwindcss": "^4.2.4"
|
|
84
81
|
},
|
|
85
82
|
"devDependencies": {
|
|
86
83
|
"@tanstack/router-plugin": "^1.167.28",
|
|
@@ -91,6 +88,7 @@
|
|
|
91
88
|
"@vitejs/plugin-react": "^6.0.1",
|
|
92
89
|
"@vscode/vsce": "^3.6.0",
|
|
93
90
|
"esbuild": "^0.27.7",
|
|
91
|
+
"react-test-renderer": "19.2.5",
|
|
94
92
|
"tsx": "^4.21.0",
|
|
95
93
|
"typescript": "^6.0.3",
|
|
96
94
|
"vite": "^8.0.10",
|