@pasko70/pibo 3.4.3 → 3.5.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/agent-runtime/routed-session.js +39 -12
- package/dist/agent-runtimes/codex-native/adapter.js +14 -1
- package/dist/agent-runtimes/codex-native/models.js +4 -4
- package/dist/agent-runtimes/codex-native/process.js +32 -13
- package/dist/agent-runtimes/codex-native/provider-usage.js +115 -0
- package/dist/agent-runtimes/pi/adapter.js +1 -0
- package/dist/agent-runtimes/pi/routed-session.js +4 -2
- package/dist/apps/chat/bounded-event-stream.js +98 -0
- package/dist/apps/chat/chat-settings-routes.js +3 -3
- package/dist/apps/chat/data/chat-data-mappers.js +32 -13
- package/dist/apps/chat/data/event-command-service.js +30 -21
- package/dist/apps/chat/data/history-query-service.js +34 -25
- package/dist/apps/chat/data/read-state-service.js +13 -0
- package/dist/apps/chat/data/session-query-service.js +19 -11
- package/dist/apps/chat/data/timeline-query-service.js +19 -7
- package/dist/apps/chat/message-command-dispatcher.js +134 -0
- package/dist/apps/chat/output-compactor.js +9 -0
- package/dist/apps/chat/output-event-policy.js +9 -1
- package/dist/apps/chat/stream.js +21 -3
- package/dist/apps/chat/telemetry-retention-service.js +113 -8
- package/dist/apps/chat/trace-response-cache.js +46 -0
- package/dist/apps/chat/trace-v2.js +12 -6
- package/dist/apps/chat/trace.js +1 -1
- package/dist/apps/chat/web-app.js +608 -281
- package/dist/apps/chat-ui/assets/{dist-DeMKnZR8.js → dist-BG0n7zLd.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-C8GzUMPk.js → dist-D6TjFhAm.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-BkUu8WPA.js → dist-D79vyxSX.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-B4YhOxh0.js → dist-DFZ8cwh0.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-Cyb4rVa6.js → dist-cOjokPrK.js} +1 -1
- package/dist/apps/chat-ui/assets/index-RMHUTJ62.js +229 -0
- package/dist/apps/chat-ui/assets/{index-BOceJ0jM.css → index-hEkrlRk-.css} +1 -1
- package/dist/apps/chat-ui/index.html +2 -2
- package/dist/apps/chat-vscode-web/assets/index-xacbCyTx.js +44 -0
- package/dist/apps/chat-vscode-web/index.html +1 -1
- package/dist/compute/pool/seeds.js +25 -3
- package/dist/core/events.js +4 -0
- package/dist/core/output-render-sequence.js +2 -0
- package/dist/core/provider-capacity.js +33 -0
- package/dist/core/provider-telemetry.js +21 -6
- package/dist/core/runtime-capacity.js +174 -0
- package/dist/core/runtime-telemetry.js +40 -12
- package/dist/core/session-router.js +79 -2
- package/dist/data/async-chat-reads.js +38 -0
- package/dist/data/async-chat-storage.js +91 -0
- package/dist/data/async-telemetry-maintenance.js +9 -0
- package/dist/data/bounded-worker-client.js +256 -0
- package/dist/data/chat-read-projections.js +159 -0
- package/dist/data/chat-read-worker.js +73 -0
- package/dist/data/chat-storage-worker.js +146 -0
- package/dist/data/ingest-service.js +79 -14
- package/dist/data/message-command-store.js +148 -0
- package/dist/data/payload-store.js +92 -11
- package/dist/data/pibo-store.js +10 -8
- package/dist/data/schema.js +16 -2
- package/dist/data/session-store.js +3 -1
- package/dist/data/storage-backup.js +278 -0
- package/dist/data/telemetry-capture.js +188 -0
- package/dist/data/telemetry-command.js +3 -0
- package/dist/data/telemetry-maintenance-worker.js +40 -0
- package/dist/data/telemetry-maintenance.js +110 -0
- package/dist/data/telemetry-retention.js +16 -7
- package/dist/data/telemetry-worker.js +111 -0
- package/dist/data/telemetry-writer.js +150 -83
- package/dist/data/telemetry.js +5 -0
- package/dist/debug/index.js +52 -0
- package/dist/debug/storage-backup.js +33 -0
- package/dist/debug/telemetry-capture.js +66 -0
- package/dist/gateway/cli.js +104 -16
- package/dist/gateway/server.js +2 -0
- package/dist/providers/openai-gpt56.js +11 -6
- package/dist/session-ui/terminalRows.js +55 -13
- package/dist/sessions/pibo-data-store.js +30 -10
- package/dist/shared/debug-features.js +4 -0
- package/dist/shared/model-inference-metrics.js +23 -0
- package/dist/shared/trace-event-projection.js +59 -2
- package/dist/shared/trace-history.js +9 -0
- package/dist/shared/trace-live-reducer.js +1 -0
- package/dist/shared/trace-patch-nodes.js +19 -0
- package/dist/web/channel.js +8 -2
- package/dist/web/http.js +36 -3
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/dist/apps/chat-ui/assets/index-Dk4mbXAB.js +0 -228
- package/dist/apps/chat-vscode-web/assets/index-0oTGFHni.js +0 -43
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { parseArgs } from "node:util";
|
|
2
|
+
import { createStorageBackup, readStorageBackupManifest, verifyStorageBackup, restoreStorageBackup } from "../data/storage-backup.js";
|
|
3
|
+
export async function runStorageBackupCli(args) {
|
|
4
|
+
const action = args[0];
|
|
5
|
+
if (!action || args.includes("--help") || args.includes("-h")) {
|
|
6
|
+
console.log(`pibo debug backup - explicit SQLite snapshot and payload backup
|
|
7
|
+
Commands:
|
|
8
|
+
create --source <sqlite> --payload-root <dir> --destination <new-dir>
|
|
9
|
+
[--resume] [--max-bytes <n>] [--max-payloads <n>] [--max-ms <n>] [--max-wal-growth <n>]
|
|
10
|
+
inspect --archive <dir> Read the manifest only
|
|
11
|
+
verify --archive <dir> Verify database and all payload hashes
|
|
12
|
+
restore --archive <dir> --destination <new-dir>
|
|
13
|
+
Defaults: 1 GiB data, 100000 payloads, 60000 ms and 64 MiB extra source WAL per create attempt. Resume preserves the original snapshot.
|
|
14
|
+
Each archive contains one database snapshot. Product and reliability snapshots are separate cuts.
|
|
15
|
+
No source deletion, vacuum, or overwrite of an existing restore destination.`);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const { values } = parseArgs({ args: args.slice(1), options: { source: { type: "string" }, "payload-root": { type: "string" }, destination: { type: "string" }, archive: { type: "string" }, resume: { type: "boolean" }, "max-bytes": { type: "string" }, "max-payloads": { type: "string" }, "max-ms": { type: "string" }, "max-wal-growth": { type: "string" }, json: { type: "boolean" } } });
|
|
19
|
+
const required = (name) => { const value = values[name]; if (typeof value !== "string" || !value)
|
|
20
|
+
throw Error(`Missing --${name}`); return value; };
|
|
21
|
+
let result;
|
|
22
|
+
if (action === "create")
|
|
23
|
+
result = await createStorageBackup({ source: required("source"), payloadRoot: required("payload-root"), destination: required("destination"), resume: values.resume, maxBytes: values["max-bytes"] ? Number(values["max-bytes"]) : undefined, maxPayloads: values["max-payloads"] ? Number(values["max-payloads"]) : undefined, maxMilliseconds: values["max-ms"] ? Number(values["max-ms"]) : undefined, maxWalGrowthBytes: values["max-wal-growth"] ? Number(values["max-wal-growth"]) : undefined });
|
|
24
|
+
else if (action === "inspect")
|
|
25
|
+
result = await readStorageBackupManifest(required("archive"));
|
|
26
|
+
else if (action === "verify")
|
|
27
|
+
result = await verifyStorageBackup(required("archive"), AbortSignal.timeout(60000));
|
|
28
|
+
else if (action === "restore")
|
|
29
|
+
result = await restoreStorageBackup(required("archive"), required("destination"), AbortSignal.timeout(60000));
|
|
30
|
+
else
|
|
31
|
+
throw Error("Unknown backup action; use backup --help");
|
|
32
|
+
console.log(JSON.stringify(result, null, 2));
|
|
33
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { parseArgs } from "node:util";
|
|
2
|
+
import { join, dirname } from "node:path";
|
|
3
|
+
import { opendir } from "node:fs/promises";
|
|
4
|
+
import { resolveDebugStore } from "./stores.js";
|
|
5
|
+
import { startTelemetryCapture, inspectTelemetryCapture, finalizeTelemetryCapture, readTelemetryCapturePage } from "../data/telemetry-capture.js";
|
|
6
|
+
export async function runTelemetryCaptureCli(args) {
|
|
7
|
+
const action = args[0];
|
|
8
|
+
if (!action || args.includes("--help") || args.includes("-h")) {
|
|
9
|
+
console.log(`pibo debug telemetry capture - scoped provider metadata in isolated stores
|
|
10
|
+
Commands:
|
|
11
|
+
start --session <ps_...> --owner <name> --duration-ms <n> --max-bytes <n> --max-rows <n>
|
|
12
|
+
stop <capture-id> Fence appends and finalize an independent closed archive
|
|
13
|
+
list List up to 100 manifests without opening databases
|
|
14
|
+
inspect <capture-id> Read one manifest without opening its database
|
|
15
|
+
page <capture-id> [--after <sequence>] [--limit <1..100>]
|
|
16
|
+
Limits: one active capture, at most one hour, 64 MiB logical data and 100000 rows per run.
|
|
17
|
+
Detail is allow-listed provider metadata only. Arbitrary provider bodies, credentials and prompts are excluded.
|
|
18
|
+
Stopping/finalizing is explicit; archived data is never opened by normal gateway reads.`);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const { values, positionals } = parseArgs({ args: args.slice(1), allowPositionals: true, options: { session: { type: "string" }, owner: { type: "string" }, "duration-ms": { type: "string" }, "max-bytes": { type: "string" }, "max-rows": { type: "string" }, after: { type: "string" }, limit: { type: "string" }, json: { type: "boolean" } } });
|
|
22
|
+
const root = join(dirname(resolveDebugStore("pibo-data").path), "telemetry-captures");
|
|
23
|
+
let result;
|
|
24
|
+
if (action === "start")
|
|
25
|
+
result = startTelemetryCapture(root, { sessionId: values.session ?? "", owner: values.owner ?? "", durationMs: Number(values["duration-ms"]), maxBytes: Number(values["max-bytes"]), maxRows: Number(values["max-rows"]) });
|
|
26
|
+
else if (action === "list") {
|
|
27
|
+
const entries = [];
|
|
28
|
+
let scanned = 0, truncated = false;
|
|
29
|
+
try {
|
|
30
|
+
const directory = await opendir(root);
|
|
31
|
+
for await (const entry of directory) {
|
|
32
|
+
if (++scanned > 1000 || entries.length >= 100) {
|
|
33
|
+
truncated = true;
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
if (!entry.isDirectory() || !entry.name.startsWith("capture_"))
|
|
37
|
+
continue;
|
|
38
|
+
try {
|
|
39
|
+
entries.push(inspectTelemetryCapture(root, entry.name));
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
entries.push({ id: entry.name, status: "manifest_unavailable" });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
if (error.code !== "ENOENT")
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
result = { entries, truncated };
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
const id = positionals[0];
|
|
54
|
+
if (!id)
|
|
55
|
+
throw Error("Capture id is required");
|
|
56
|
+
if (action === "stop")
|
|
57
|
+
result = await finalizeTelemetryCapture(root, id);
|
|
58
|
+
else if (action === "inspect")
|
|
59
|
+
result = inspectTelemetryCapture(root, id);
|
|
60
|
+
else if (action === "page")
|
|
61
|
+
result = await readTelemetryCapturePage(root, id, Number(values.after ?? 0), Number(values.limit ?? 50));
|
|
62
|
+
else
|
|
63
|
+
throw Error("Unknown capture action; use capture --help");
|
|
64
|
+
}
|
|
65
|
+
console.log(JSON.stringify(result, null, 2));
|
|
66
|
+
}
|
package/dist/gateway/cli.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawn, execFile } from "node:child_process";
|
|
2
|
-
import {
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { appendFileSync, existsSync, mkdirSync, openSync, readFileSync, readdirSync } from "node:fs";
|
|
3
4
|
import { homedir } from "node:os";
|
|
4
5
|
import { join } from "node:path";
|
|
5
6
|
import { promisify } from "node:util";
|
|
@@ -205,11 +206,14 @@ function runtimeTelemetryHint(value) {
|
|
|
205
206
|
}
|
|
206
207
|
function runtimeStatus(value) {
|
|
207
208
|
const obj = objectValue(value);
|
|
208
|
-
if (!obj)
|
|
209
|
+
if (!obj || !stringValue(obj.piboSessionId) || typeof obj.processing !== "boolean" || typeof obj.streaming !== "boolean"
|
|
210
|
+
|| !Number.isInteger(obj.queuedMessages) || Number(obj.queuedMessages) < 0)
|
|
209
211
|
return undefined;
|
|
210
212
|
return {
|
|
211
213
|
piboSessionId: stringValue(obj.piboSessionId),
|
|
212
214
|
queuedMessages: numberValue(obj.queuedMessages),
|
|
215
|
+
activeEventId: stringValue(obj.activeEventId),
|
|
216
|
+
queuedEventIds: Array.isArray(obj.queuedEventIds) && obj.queuedEventIds.every((id) => typeof id === "string") ? obj.queuedEventIds : undefined,
|
|
213
217
|
processing: booleanValue(obj.processing),
|
|
214
218
|
streaming: booleanValue(obj.streaming),
|
|
215
219
|
activeTelemetry: runtimeTelemetryHint(obj.activeTelemetry),
|
|
@@ -217,13 +221,14 @@ function runtimeStatus(value) {
|
|
|
217
221
|
}
|
|
218
222
|
function activeRun(value) {
|
|
219
223
|
const obj = objectValue(value);
|
|
220
|
-
if (!obj)
|
|
224
|
+
if (!obj || !stringValue(obj.runId) || !stringValue(obj.status)
|
|
225
|
+
|| !(stringValue(obj.controllerPiboSessionId) || stringValue(obj.piboSessionId)))
|
|
221
226
|
return undefined;
|
|
222
227
|
return {
|
|
223
228
|
runId: stringValue(obj.runId),
|
|
224
229
|
status: stringValue(obj.status),
|
|
225
230
|
toolName: stringValue(obj.toolName),
|
|
226
|
-
piboSessionId: stringValue(obj.piboSessionId),
|
|
231
|
+
piboSessionId: stringValue(obj.controllerPiboSessionId) ?? stringValue(obj.piboSessionId),
|
|
227
232
|
};
|
|
228
233
|
}
|
|
229
234
|
function parseGatewaySafetyPayload(payload, reachable) {
|
|
@@ -231,7 +236,9 @@ function parseGatewaySafetyPayload(payload, reachable) {
|
|
|
231
236
|
const mode = obj && (obj.mode === "dev" || obj.mode === "prod" || obj.mode === "fallback") ? obj.mode : "unknown";
|
|
232
237
|
const runtimeStatuses = Array.isArray(obj?.runtimeStatuses) ? obj.runtimeStatuses.map(runtimeStatus).filter((item) => Boolean(item)) : [];
|
|
233
238
|
const activeRuns = Array.isArray(obj?.activeRuns) ? obj.activeRuns.map(activeRun).filter((item) => Boolean(item)) : [];
|
|
234
|
-
|
|
239
|
+
const incomplete = !Array.isArray(obj?.runtimeStatuses) || !Array.isArray(obj?.activeRuns)
|
|
240
|
+
|| runtimeStatuses.length !== obj.runtimeStatuses.length || activeRuns.length !== obj.activeRuns.length;
|
|
241
|
+
return { reachable, mode, generation: stringValue(obj?.generation), health: obj?.health, runtimeStatuses, activeRuns, ambiguous: incomplete || booleanValue(obj?.ambiguous) };
|
|
235
242
|
}
|
|
236
243
|
export function checkActiveWork(status, target = "web") {
|
|
237
244
|
const reasons = [];
|
|
@@ -249,7 +256,7 @@ export function checkActiveWork(status, target = "web") {
|
|
|
249
256
|
reasons.push(`${id} is processing`);
|
|
250
257
|
if (session.streaming === true)
|
|
251
258
|
reasons.push(`${id} is streaming`);
|
|
252
|
-
if ((session.queuedMessages ?? 0) > 0)
|
|
259
|
+
if ((session.queuedMessages ?? 0) > 0 || (session.activeTelemetry?.queueDepth ?? 0) > 0)
|
|
253
260
|
reasons.push(`${id} has queued messages`);
|
|
254
261
|
if (session.activeTelemetry?.isStale === true) {
|
|
255
262
|
const phase = session.activeTelemetry.activePhase ? ` in ${session.activeTelemetry.activePhase}` : "";
|
|
@@ -260,6 +267,58 @@ export function checkActiveWork(status, target = "web") {
|
|
|
260
267
|
reasons.push(`${run.runId ?? "yielded run"} is ${run.status ?? "active"}`);
|
|
261
268
|
return { unsafe: reasons.length > 0, reasons };
|
|
262
269
|
}
|
|
270
|
+
function blockingSessions(status) {
|
|
271
|
+
return status.runtimeStatuses.filter((session) => session.processing || session.streaming
|
|
272
|
+
|| (session.queuedMessages ?? 0) > 0 || (session.activeTelemetry?.queueDepth ?? 0) > 0 || session.activeTelemetry?.isStale);
|
|
273
|
+
}
|
|
274
|
+
export function restartConfirmationToken(status) {
|
|
275
|
+
if (!status.reachable || status.error || status.ambiguous || status.mode !== "prod" || !status.generation)
|
|
276
|
+
return undefined;
|
|
277
|
+
const sessions = blockingSessions(status);
|
|
278
|
+
if (status.runtimeStatuses.some((session) => !session.piboSessionId || typeof session.processing !== "boolean"
|
|
279
|
+
|| typeof session.streaming !== "boolean" || !Number.isInteger(session.queuedMessages) || session.queuedMessages < 0))
|
|
280
|
+
return undefined;
|
|
281
|
+
if (sessions.some((session) => ((session.processing || session.streaming || session.activeTelemetry?.isStale)
|
|
282
|
+
&& !(session.activeEventId || session.activeTelemetry?.activeTurnId))
|
|
283
|
+
|| (session.queuedMessages > 0 && (session.queuedEventIds?.length !== session.queuedMessages || session.queuedEventIds?.some((id) => !id)))
|
|
284
|
+
|| (session.activeTelemetry?.queueDepth ?? 0) > (session.queuedMessages ?? 0)))
|
|
285
|
+
return undefined;
|
|
286
|
+
if (status.activeRuns.some((run) => !run.runId || !run.piboSessionId || !run.status))
|
|
287
|
+
return undefined;
|
|
288
|
+
// Exclude clock/progress counters: approval describes work identities, not polling time.
|
|
289
|
+
const snapshot = {
|
|
290
|
+
generation: status.generation,
|
|
291
|
+
target: "web", port: targetPort("web"), service: gatewayServiceName("web"), home: managedGatewayHome("web"),
|
|
292
|
+
sessions: sessions.map((session) => ({
|
|
293
|
+
id: session.piboSessionId, activeEventId: session.activeEventId,
|
|
294
|
+
processing: session.processing, streaming: session.streaming,
|
|
295
|
+
queuedEventIds: [...(session.queuedEventIds ?? [])], queuedMessages: session.queuedMessages,
|
|
296
|
+
telemetryTurnId: session.activeTelemetry?.activeTurnId,
|
|
297
|
+
stale: session.activeTelemetry?.isStale === true,
|
|
298
|
+
})).sort((a, b) => a.id.localeCompare(b.id)),
|
|
299
|
+
runs: [...status.activeRuns].sort((a, b) => a.runId.localeCompare(b.runId)),
|
|
300
|
+
};
|
|
301
|
+
return `${RESTART_CONFIRMATION_TOKEN}:${createHash("sha256").update(JSON.stringify(snapshot)).digest("hex")}`;
|
|
302
|
+
}
|
|
303
|
+
function printRestartApproval(status) {
|
|
304
|
+
const token = restartConfirmationToken(status);
|
|
305
|
+
if (token) {
|
|
306
|
+
console.log("Review the listed work and obtain user approval before running:");
|
|
307
|
+
console.log(` pibo gateway web restart --force --confirm ${token}`);
|
|
308
|
+
}
|
|
309
|
+
else
|
|
310
|
+
console.log("Snapshot approval unavailable: status must identify the gateway generation and all active work. Inspect: pibo gateway web doctor");
|
|
311
|
+
}
|
|
312
|
+
function auditRestart(status, decision, force, reason) {
|
|
313
|
+
const home = managedGatewayHome("web");
|
|
314
|
+
mkdirSync(home, { recursive: true });
|
|
315
|
+
appendFileSync(join(home, "gateway-restart-audit.jsonl"), `${JSON.stringify({
|
|
316
|
+
at: new Date().toISOString(), decision, force, reason, snapshot: restartConfirmationToken(status),
|
|
317
|
+
generation: status.generation,
|
|
318
|
+
sessionIds: [...new Set([...blockingSessions(status).map((session) => session.piboSessionId), ...status.activeRuns.map((run) => run.piboSessionId)].filter(Boolean))],
|
|
319
|
+
runIds: status.activeRuns.map((run) => run.runId), reasons: checkActiveWork(status).reasons,
|
|
320
|
+
})}\n`, { mode: 0o600 });
|
|
321
|
+
}
|
|
263
322
|
async function readGatewaySafetyStatus(target) {
|
|
264
323
|
const port = targetPort(target);
|
|
265
324
|
const reachable = await isPortReachable("127.0.0.1", port, 1500);
|
|
@@ -281,6 +340,10 @@ function printSafetyStatus(target, status) {
|
|
|
281
340
|
console.log(` runtime sessions: ${status.runtimeStatuses.length}`);
|
|
282
341
|
for (const session of status.runtimeStatuses) {
|
|
283
342
|
console.log(` ${session.piboSessionId ?? "unknown"}: processing=${session.processing === true} streaming=${session.streaming === true} queued=${session.queuedMessages ?? 0}`);
|
|
343
|
+
if (session.activeEventId)
|
|
344
|
+
console.log(` active event: ${session.activeEventId}`);
|
|
345
|
+
if (session.queuedEventIds?.length)
|
|
346
|
+
console.log(` queued events: ${session.queuedEventIds.join(", ")}`);
|
|
284
347
|
if (session.activeTelemetry) {
|
|
285
348
|
const parts = [
|
|
286
349
|
session.activeTelemetry.activePhase ? `phase=${session.activeTelemetry.activePhase}` : undefined,
|
|
@@ -295,7 +358,7 @@ function printSafetyStatus(target, status) {
|
|
|
295
358
|
}
|
|
296
359
|
console.log(` active yielded runs: ${status.activeRuns.length}`);
|
|
297
360
|
for (const run of status.activeRuns)
|
|
298
|
-
console.log(` ${run.runId ?? "unknown"}: ${run.status ?? "active"}${run.toolName ? ` (${run.toolName})` : ""}`);
|
|
361
|
+
console.log(` ${run.runId ?? "unknown"}: ${run.status ?? "active"}${run.toolName ? ` (${run.toolName})` : ""} session=${run.piboSessionId ?? "unknown"}`);
|
|
299
362
|
}
|
|
300
363
|
function managerRequiresShell(command) {
|
|
301
364
|
return process.platform === "win32" && /\.(?:cmd|bat)$/i.test(command);
|
|
@@ -332,6 +395,7 @@ async function runManagedGatewayCommand(target, command, args, argv = process.ar
|
|
|
332
395
|
}
|
|
333
396
|
else
|
|
334
397
|
console.log(" restart safety: idle");
|
|
398
|
+
printRestartApproval(status);
|
|
335
399
|
}
|
|
336
400
|
if (command === "doctor")
|
|
337
401
|
process.exitCode = status.reachable && !status.error && status.mode === expectedMode(target) ? 0 : 1;
|
|
@@ -380,19 +444,43 @@ async function runManagedGatewayCommand(target, command, args, argv = process.ar
|
|
|
380
444
|
const force = args.includes("--force");
|
|
381
445
|
const confirmIndex = args.indexOf("--confirm");
|
|
382
446
|
const confirmation = confirmIndex >= 0 ? args[confirmIndex + 1] : undefined;
|
|
383
|
-
if (force && confirmation !== RESTART_CONFIRMATION_TOKEN) {
|
|
447
|
+
if (target === "dev" && force && confirmation !== RESTART_CONFIRMATION_TOKEN) {
|
|
384
448
|
console.error(`Force restart requires: --confirm ${RESTART_CONFIRMATION_TOKEN}`);
|
|
385
449
|
process.exitCode = 1;
|
|
386
450
|
return true;
|
|
387
451
|
}
|
|
388
|
-
if (target === "web"
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
452
|
+
if (target === "web") {
|
|
453
|
+
// Both normal and delayed/forced commands inspect again at execution time.
|
|
454
|
+
let current = await readGatewaySafetyStatus(target);
|
|
455
|
+
for (let inspection = 0; inspection < 2; inspection += 1) {
|
|
456
|
+
const active = checkActiveWork(current, target);
|
|
457
|
+
const token = restartConfirmationToken(current);
|
|
458
|
+
printSafetyStatus(target, current);
|
|
394
459
|
for (const reason of active.reasons)
|
|
395
460
|
console.error(`- ${reason}`);
|
|
461
|
+
const blocked = force ? !token || confirmation !== token : active.unsafe;
|
|
462
|
+
if (blocked) {
|
|
463
|
+
const reason = force ? "Snapshot approval is missing, unavailable, or changed." : "Active work or unavailable gateway status.";
|
|
464
|
+
console.error(`Restart blocked: ${reason}`);
|
|
465
|
+
console.error("Ask the user before interrupting active sessions. Obtain fresh approval with: pibo gateway web status");
|
|
466
|
+
printRestartApproval(current);
|
|
467
|
+
try {
|
|
468
|
+
auditRestart(current, "blocked", force, reason);
|
|
469
|
+
}
|
|
470
|
+
catch (error) {
|
|
471
|
+
console.error(`Restart audit failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
472
|
+
}
|
|
473
|
+
process.exitCode = 1;
|
|
474
|
+
return true;
|
|
475
|
+
}
|
|
476
|
+
if (inspection === 0)
|
|
477
|
+
current = await readGatewaySafetyStatus(target);
|
|
478
|
+
}
|
|
479
|
+
try {
|
|
480
|
+
auditRestart(current, "approved", force, force ? "Unchanged snapshot explicitly approved" : "Gateway idle");
|
|
481
|
+
}
|
|
482
|
+
catch (error) {
|
|
483
|
+
console.error(`Restart blocked: cannot record restart audit: ${error instanceof Error ? error.message : String(error)}`);
|
|
396
484
|
process.exitCode = 1;
|
|
397
485
|
return true;
|
|
398
486
|
}
|
|
@@ -642,8 +730,8 @@ Commands:
|
|
|
642
730
|
dev doctor Check dev gateway health
|
|
643
731
|
|
|
644
732
|
Options:
|
|
645
|
-
--force --confirm
|
|
646
|
-
|
|
733
|
+
--force --confirm <snapshot-token>
|
|
734
|
+
Restart only the work explicitly approved from web status
|
|
647
735
|
|
|
648
736
|
Next:
|
|
649
737
|
pibo gateway web status
|
package/dist/gateway/server.js
CHANGED
|
@@ -360,6 +360,7 @@ export class PiboGatewayServer {
|
|
|
360
360
|
},
|
|
361
361
|
findSessions: (input) => this.requireSessionStore().find(input),
|
|
362
362
|
listSessions: () => this.requireSessionStore().list?.() ?? [],
|
|
363
|
+
getSessionStructureRevision: () => this.requireSessionStore().getStructureRevision?.(),
|
|
363
364
|
getSessionRuntimeBinding: (piboSessionId) => this.requireRouter().getSessionRuntimeBinding(piboSessionId),
|
|
364
365
|
getSessionRuntimeProfile: (piboSessionId) => this.requireRouter().getSessionRuntimeProfile(piboSessionId),
|
|
365
366
|
inspectSessionRuntimeHistory: async (piboSessionId) => {
|
|
@@ -424,6 +425,7 @@ export class PiboGatewayServer {
|
|
|
424
425
|
getSessionStatusSnapshot: (piboSessionId, options) => this.requireRouter().getSessionStatusSnapshot(piboSessionId, options),
|
|
425
426
|
getSessionForkCandidates: (piboSessionId) => this.requireRouter().getSessionForkCandidates(piboSessionId),
|
|
426
427
|
listSessionRuntimeStatuses: () => this.requireRouter().listSessionRuntimeStatuses(),
|
|
428
|
+
getRuntimeCapacityStatus: () => this.requireRouter().getRuntimeCapacityStatus(),
|
|
427
429
|
listRuns: (options) => this.requireRouter().listRuns(options),
|
|
428
430
|
snapshotSignalSession: (piboSessionId) => this.requireRouter().snapshotSignalSession(piboSessionId),
|
|
429
431
|
snapshotSignalTree: (rootPiboSessionId) => this.requireRouter().snapshotSignalTree(rootPiboSessionId),
|
|
@@ -38,6 +38,9 @@ export const OPENAI_CODEX_GPT_6_ASTRA_MODEL = {
|
|
|
38
38
|
name: "GPT-6-Astra",
|
|
39
39
|
cost: { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 },
|
|
40
40
|
};
|
|
41
|
+
// Codex serves the Reserve quota alias from the Luna-class model, so Reserve borrows Luna's metadata.
|
|
42
|
+
export const OPENAI_CODEX_RESERVE_MODEL = { id: "gpt-reserve", name: "Luna Reserve" };
|
|
43
|
+
const OPENAI_CODEX_RESERVE_BASE_MODEL_ID = "gpt-5.6-luna";
|
|
41
44
|
const OPENAI_GPT_56_MODEL_IDS = new Set(OPENAI_GPT_56_MODELS.map((model) => model.id));
|
|
42
45
|
export function getBuiltInOpenAiModels() {
|
|
43
46
|
return getBuiltInProviderModels(OPENAI_PROVIDER_ID);
|
|
@@ -69,6 +72,10 @@ export function buildOpenAiCodexGpt56Models(baseModels = getBuiltInOpenAiCodexMo
|
|
|
69
72
|
}
|
|
70
73
|
export function buildOpenAiCodexSupplementalModels(baseModels = getBuiltInOpenAiCodexModels()) {
|
|
71
74
|
const models = buildOpenAiCodexGpt56Models(baseModels);
|
|
75
|
+
const reserveBase = models.find((model) => model.id === OPENAI_CODEX_RESERVE_BASE_MODEL_ID);
|
|
76
|
+
if (reserveBase && !models.some((model) => model.id === OPENAI_CODEX_RESERVE_MODEL.id)) {
|
|
77
|
+
models.push({ ...cloneModel(reserveBase), ...OPENAI_CODEX_RESERVE_MODEL });
|
|
78
|
+
}
|
|
72
79
|
if (models.some((model) => model.id === OPENAI_CODEX_GPT_6_ASTRA_MODEL.id))
|
|
73
80
|
return models;
|
|
74
81
|
return [...models, openAiCodexAstraModelToRegistryModel()];
|
|
@@ -76,7 +83,7 @@ export function buildOpenAiCodexSupplementalModels(baseModels = getBuiltInOpenAi
|
|
|
76
83
|
export function registerOpenAiSupplementalModels(modelRegistry, options = {}) {
|
|
77
84
|
const baseOpenAiModels = options.baseOpenAiModels ?? getBuiltInOpenAiModels();
|
|
78
85
|
const openAiModels = buildOpenAiGpt56Models(baseOpenAiModels);
|
|
79
|
-
const openAiAdded =
|
|
86
|
+
const openAiAdded = countAddedModels(baseOpenAiModels, OPENAI_PROVIDER_ID, openAiModels);
|
|
80
87
|
modelRegistry.registerProvider(OPENAI_PROVIDER_ID, {
|
|
81
88
|
baseUrl: OPENAI_BASE_URL,
|
|
82
89
|
api: OPENAI_RESPONSES_API,
|
|
@@ -85,9 +92,7 @@ export function registerOpenAiSupplementalModels(modelRegistry, options = {}) {
|
|
|
85
92
|
});
|
|
86
93
|
const baseOpenAiCodexModels = options.baseOpenAiCodexModels ?? getBuiltInOpenAiCodexModels();
|
|
87
94
|
const openAiCodexModels = buildOpenAiCodexSupplementalModels(baseOpenAiCodexModels);
|
|
88
|
-
const
|
|
89
|
-
const openAiCodexAdded = countMissingGpt56Models(baseOpenAiCodexModels, OPENAI_CODEX_PROVIDER_ID)
|
|
90
|
-
+ (hasBuiltInAstra ? 0 : 1);
|
|
95
|
+
const openAiCodexAdded = countAddedModels(baseOpenAiCodexModels, OPENAI_CODEX_PROVIDER_ID, openAiCodexModels);
|
|
91
96
|
modelRegistry.registerProvider(OPENAI_CODEX_PROVIDER_ID, {
|
|
92
97
|
baseUrl: OPENAI_CODEX_BASE_URL,
|
|
93
98
|
api: OPENAI_CODEX_RESPONSES_API,
|
|
@@ -128,9 +133,9 @@ function buildProviderGpt56Models(options) {
|
|
|
128
133
|
.map((model) => openAiGpt56ModelToRegistryModel(model, options));
|
|
129
134
|
return [...providerBaseModels, ...additions];
|
|
130
135
|
}
|
|
131
|
-
function
|
|
136
|
+
function countAddedModels(baseModels, providerId, registeredModels) {
|
|
132
137
|
const existingIds = new Set(baseModels.filter((model) => model.provider === providerId).map((model) => model.id));
|
|
133
|
-
return
|
|
138
|
+
return registeredModels.filter((model) => !existingIds.has(model.id)).length;
|
|
134
139
|
}
|
|
135
140
|
function cloneModel(model) {
|
|
136
141
|
return {
|
|
@@ -16,13 +16,14 @@ export function buildCompactTerminalRows(traceView, options) {
|
|
|
16
16
|
if (!traceView)
|
|
17
17
|
return [];
|
|
18
18
|
const turnById = mapTurnNodes(traceView.nodes);
|
|
19
|
+
const showToolDebugMetrics = Boolean(options.debugMode && (options.debugFeatures?.toolMetrics ?? true));
|
|
19
20
|
const flatNodes = flattenTraceNodes(traceView.nodes)
|
|
20
21
|
.sort((left, right) => compareTraceNodes(left.node, right.node))
|
|
21
22
|
.filter((item) => item.node.type !== "agent.turn" && (options.showThinking || item.node.type !== "model.reasoning"));
|
|
22
23
|
const candidates = syncThinkingToolRows(flatNodes.map((item) => createRowCandidate(item.node, item.turnId)));
|
|
23
24
|
applyCompletedTurnTiming(candidates, turnById);
|
|
24
25
|
const reconciled = reconcileConceptualRowCandidates(candidates);
|
|
25
|
-
const rows = !
|
|
26
|
+
const rows = !showToolDebugMetrics && (options.toolDisplayMode ?? "default") === "default"
|
|
26
27
|
? groupRelatedToolCandidates(reconciled).map((candidate) => candidate.row)
|
|
27
28
|
: reconciled.map((candidate) => candidate.row);
|
|
28
29
|
return applyToolDisplayMode(rows, options.toolDisplayMode ?? "default");
|
|
@@ -41,14 +42,7 @@ function applyToolDisplayMode(rows, mode) {
|
|
|
41
42
|
const slimRow = {
|
|
42
43
|
...row,
|
|
43
44
|
lines: row.lines.slice(0, 1),
|
|
44
|
-
input: undefined,
|
|
45
|
-
output: undefined,
|
|
46
|
-
error: undefined,
|
|
47
|
-
markdown: undefined,
|
|
48
|
-
expandable: false,
|
|
49
45
|
singleLine: true,
|
|
50
|
-
previewOmission: undefined,
|
|
51
|
-
detailItems: undefined,
|
|
52
46
|
};
|
|
53
47
|
if (mode !== "intent")
|
|
54
48
|
return [slimRow];
|
|
@@ -60,6 +54,13 @@ function applyToolDisplayMode(rows, mode) {
|
|
|
60
54
|
prefix: "bullet",
|
|
61
55
|
tokens: [token(intent, row.status === "error" ? "red" : row.status === "done" ? "green" : "cyan", "semibold")],
|
|
62
56
|
}],
|
|
57
|
+
input: undefined,
|
|
58
|
+
output: undefined,
|
|
59
|
+
error: undefined,
|
|
60
|
+
markdown: undefined,
|
|
61
|
+
expandable: false,
|
|
62
|
+
previewOmission: undefined,
|
|
63
|
+
detailItems: undefined,
|
|
63
64
|
}];
|
|
64
65
|
});
|
|
65
66
|
}
|
|
@@ -159,18 +160,34 @@ function createRowCandidate(node, turnId) {
|
|
|
159
160
|
};
|
|
160
161
|
break;
|
|
161
162
|
}
|
|
163
|
+
const isToolCall = node.type === "tool.call" || node.type === "tool.result" || (node.type === "agent.delegation" && Boolean(node.toolCallId));
|
|
164
|
+
const reference = isToolCall ? toolCallReference(node) : undefined;
|
|
162
165
|
return {
|
|
163
166
|
...candidate,
|
|
164
167
|
row: {
|
|
165
168
|
...candidate.row,
|
|
166
169
|
id: compactTerminalRowIdentity(node),
|
|
167
170
|
intent: node.intent,
|
|
168
|
-
isToolCall
|
|
171
|
+
isToolCall,
|
|
172
|
+
toolCallReference: reference,
|
|
173
|
+
expandable: reference || Object.values(node.payloadRefs ?? {}).some(Boolean) ? true : candidate.row.expandable,
|
|
169
174
|
toolMetrics: node.toolMetrics,
|
|
175
|
+
modelInferences: node.modelInferences,
|
|
170
176
|
...debugFields(node),
|
|
171
177
|
},
|
|
172
178
|
};
|
|
173
179
|
}
|
|
180
|
+
function toolCallReference(node) {
|
|
181
|
+
if (!node.toolCallId)
|
|
182
|
+
return undefined;
|
|
183
|
+
const parsed = parseTraceToolNodeIdentity(node.id);
|
|
184
|
+
return {
|
|
185
|
+
traceNodeId: node.id,
|
|
186
|
+
toolCallId: node.toolCallId,
|
|
187
|
+
eventId: node.eventId ?? parsed?.qualifier?.eventId,
|
|
188
|
+
invocationOrdinal: node.toolInvocationOrdinal ?? parsed?.qualifier?.invocationOrdinal,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
174
191
|
export function compactTerminalRowIdentity(node) {
|
|
175
192
|
if (node.type === "tool.call" || node.type === "tool.result" || node.type === "agent.delegation") {
|
|
176
193
|
if (node.toolCallId) {
|
|
@@ -226,6 +243,7 @@ function reconcileConceptualRowCandidates(candidates) {
|
|
|
226
243
|
output: candidate.row.output ?? existing.row.output,
|
|
227
244
|
error: candidate.row.error ?? existing.row.error,
|
|
228
245
|
payloadRefs: { ...existing.row.payloadRefs, ...candidate.row.payloadRefs },
|
|
246
|
+
modelInferences: mergeModelInferences(existing.row.modelInferences, candidate.row.modelInferences),
|
|
229
247
|
imagePreviews: mergeImagePreviews(existing.row.imagePreviews, candidate.row.imagePreviews),
|
|
230
248
|
},
|
|
231
249
|
};
|
|
@@ -310,6 +328,16 @@ function assistantPartIndex(rowId) {
|
|
|
310
328
|
const index = Number(match[1]);
|
|
311
329
|
return Number.isSafeInteger(index) ? index : undefined;
|
|
312
330
|
}
|
|
331
|
+
function mergeModelInferences(existing, candidate) {
|
|
332
|
+
if (!existing?.length)
|
|
333
|
+
return candidate;
|
|
334
|
+
if (!candidate?.length)
|
|
335
|
+
return existing;
|
|
336
|
+
const merged = new Map(existing.map((record) => [record.id, record]));
|
|
337
|
+
for (const record of candidate)
|
|
338
|
+
merged.set(record.id, record);
|
|
339
|
+
return [...merged.values()];
|
|
340
|
+
}
|
|
313
341
|
function mergeImagePreviews(existing, candidate) {
|
|
314
342
|
if (!existing?.length)
|
|
315
343
|
return candidate?.slice(0, MAX_COMPACT_TERMINAL_IMAGE_PREVIEWS);
|
|
@@ -329,6 +357,9 @@ function applyCompletedTurnTiming(candidates, turnById) {
|
|
|
329
357
|
continue;
|
|
330
358
|
const turnCandidates = candidates.filter((candidate) => candidate.turnId === turn.id);
|
|
331
359
|
const finalCandidate = turnCandidates.at(-1);
|
|
360
|
+
if (finalCandidate && turn.modelInferences?.length) {
|
|
361
|
+
finalCandidate.row.modelInferences = mergeModelInferences(finalCandidate.row.modelInferences, turn.modelInferences);
|
|
362
|
+
}
|
|
332
363
|
if (finalCandidate?.row.kind !== "message.assistant" || finalCandidate.row.status === "running")
|
|
333
364
|
continue;
|
|
334
365
|
finalCandidate.row.startedAt = turn.startedAt;
|
|
@@ -362,7 +393,8 @@ function createUserMessageRow(node) {
|
|
|
362
393
|
lines: [{ prefix: "prompt", tokens: [token(text)] }],
|
|
363
394
|
sourceNodeIds: [node.id],
|
|
364
395
|
forkEntryId: node.entryId,
|
|
365
|
-
pendingMessageDelivery: pendingUserMessageDelivery(node),
|
|
396
|
+
pendingMessageDelivery: pendingUserMessageDelivery(node) ?? (node.status === "running" && node.messageDeliveryState ? "queue" : undefined),
|
|
397
|
+
messageDeliveryState: node.messageDeliveryState,
|
|
366
398
|
startedAt: node.startedAt,
|
|
367
399
|
output: text,
|
|
368
400
|
payloadRefs: node.payloadRefs,
|
|
@@ -657,9 +689,16 @@ function createCompactionRow(node) {
|
|
|
657
689
|
input: node.input,
|
|
658
690
|
output: node.output,
|
|
659
691
|
error: node.error,
|
|
660
|
-
|
|
692
|
+
compactionStats: node.compactionStats,
|
|
693
|
+
compactionMarkdown: compactionMarkdown(node.output),
|
|
694
|
+
expandable: node.status === "error" && (node.input !== undefined || node.output !== undefined || Boolean(node.error)),
|
|
661
695
|
};
|
|
662
696
|
}
|
|
697
|
+
function compactionMarkdown(value) {
|
|
698
|
+
if (!isRecord(value))
|
|
699
|
+
return undefined;
|
|
700
|
+
return stringValue(value.summary);
|
|
701
|
+
}
|
|
663
702
|
function createExecutionCommandRow(node) {
|
|
664
703
|
if (node.title === "status") {
|
|
665
704
|
return createStatusToolRow(node);
|
|
@@ -966,13 +1005,14 @@ function createExploringGroup(candidates) {
|
|
|
966
1005
|
}] : []),
|
|
967
1006
|
],
|
|
968
1007
|
sourceNodeIds: candidates.flatMap((candidate) => candidate.row.sourceNodeIds),
|
|
1008
|
+
modelInferences: candidates.reduce((records, candidate) => mergeModelInferences(records, candidate.row.modelInferences), undefined),
|
|
969
1009
|
eventId: firstRow?.eventId,
|
|
970
1010
|
runId: firstRow?.runId,
|
|
971
1011
|
orderSource: firstRow?.orderSource,
|
|
972
1012
|
orderStreamId: firstRow?.orderStreamId,
|
|
973
1013
|
orderStreamFrameIndex: firstRow?.orderStreamFrameIndex,
|
|
974
1014
|
detailItems,
|
|
975
|
-
expandable: detailItems.some((item) => item.input !== undefined || item.output !== undefined || Boolean(item.error)),
|
|
1015
|
+
expandable: detailItems.some((item) => item.toolCallReference || item.input !== undefined || item.output !== undefined || Boolean(item.error)),
|
|
976
1016
|
previewOmission: omittedDetailCount > 0 ? {
|
|
977
1017
|
source: "details",
|
|
978
1018
|
visibleLineCount: visibleDetailItems.length,
|
|
@@ -1014,13 +1054,14 @@ function createImageGroup(candidates) {
|
|
|
1014
1054
|
}] : []),
|
|
1015
1055
|
],
|
|
1016
1056
|
sourceNodeIds: candidates.flatMap((candidate) => candidate.row.sourceNodeIds),
|
|
1057
|
+
modelInferences: candidates.reduce((records, candidate) => mergeModelInferences(records, candidate.row.modelInferences), undefined),
|
|
1017
1058
|
eventId: firstRow?.eventId,
|
|
1018
1059
|
runId: firstRow?.runId,
|
|
1019
1060
|
orderSource: firstRow?.orderSource,
|
|
1020
1061
|
orderStreamId: firstRow?.orderStreamId,
|
|
1021
1062
|
orderStreamFrameIndex: firstRow?.orderStreamFrameIndex,
|
|
1022
1063
|
detailItems,
|
|
1023
|
-
expandable: detailItems.some((item) => item.input !== undefined || item.output !== undefined || Boolean(item.error)),
|
|
1064
|
+
expandable: detailItems.some((item) => item.toolCallReference || item.input !== undefined || item.output !== undefined || Boolean(item.error)),
|
|
1024
1065
|
imagePreviews: detailItems.flatMap((item) => item.imagePreviews ?? []).slice(0, MAX_COMPACT_TERMINAL_IMAGE_PREVIEWS),
|
|
1025
1066
|
previewOmission: omittedDetailCount > 0 ? {
|
|
1026
1067
|
source: "details",
|
|
@@ -1043,6 +1084,7 @@ function detailItemsForGroup(candidates, kind) {
|
|
|
1043
1084
|
error: candidate.row.error,
|
|
1044
1085
|
payloadRefs: candidate.row.payloadRefs,
|
|
1045
1086
|
linkedPiboSessionId: candidate.row.linkedPiboSessionId,
|
|
1087
|
+
toolCallReference: candidate.row.toolCallReference,
|
|
1046
1088
|
previewOmission: candidate.row.previewOmission,
|
|
1047
1089
|
imagePreviews: candidate.row.imagePreviews,
|
|
1048
1090
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ChatDataIngestService } from "../data/ingest-service.js";
|
|
1
|
+
import { ChatDataIngestService, outputIdempotencyKey } from "../data/ingest-service.js";
|
|
2
2
|
import { PiboDataStore } from "../data/pibo-store.js";
|
|
3
3
|
import { PIBO_AGENT_OBSERVATION_AUTO_CURSOR_MAX_SCOPES, createPiboSession, matchesFindInput, } from "./store.js";
|
|
4
4
|
import { createLegacyPiRuntimeSessionBinding, nextRuntimeSessionBinding, RuntimeSessionBindingConflictError, } from "./runtime-binding.js";
|
|
@@ -49,6 +49,7 @@ export class PiboDataSessionStore {
|
|
|
49
49
|
const row = this.db.prepare(`${SESSION_SELECT} WHERE s.id = ? AND s.deleted_at IS NULL`).get(id);
|
|
50
50
|
return row ? sessionFromRow(row) : undefined;
|
|
51
51
|
}
|
|
52
|
+
getStructureRevision() { return this.db.prepare("SELECT revision FROM chat_navigation_clock WHERE id=1").get().revision; }
|
|
52
53
|
list() {
|
|
53
54
|
return this.db.prepare(`${SESSION_SELECT} WHERE s.deleted_at IS NULL ORDER BY s.updated_at DESC`).all().map(sessionFromRow);
|
|
54
55
|
}
|
|
@@ -469,9 +470,26 @@ export class PiboDataSessionStore {
|
|
|
469
470
|
const at = input.at ?? new Date().toISOString();
|
|
470
471
|
const runsBySession = groupRunsByController(input.recoveredRuns ?? []);
|
|
471
472
|
return this.dataStore.transaction(() => {
|
|
473
|
+
// Durable terminal output wins over stale telemetry. Do not send a different
|
|
474
|
+
// recovery payload through the same output identity during startup.
|
|
475
|
+
const terminalErrors = new Map();
|
|
472
476
|
const recoveredTurns = this.dataStore.telemetry.recoverInterruptedTurns({
|
|
473
477
|
at,
|
|
474
|
-
resolveOutcome: (turn) =>
|
|
478
|
+
resolveOutcome: (turn) => {
|
|
479
|
+
const identity = { type: "session_error", piboSessionId: turn.piboSessionId, eventId: recoveryEventId(turn), error: "" };
|
|
480
|
+
const existing = this.dataStore.eventLog.findByIdempotencyKey(outputIdempotencyKey(identity));
|
|
481
|
+
if (existing) {
|
|
482
|
+
const event = {
|
|
483
|
+
...identity,
|
|
484
|
+
error: typeof existing.attributes.error === "string" ? existing.attributes.error : existing.previewText ?? "Runtime error",
|
|
485
|
+
errorDetails: existing.attributes.errorDetails,
|
|
486
|
+
};
|
|
487
|
+
terminalErrors.set(turn.turnId, event);
|
|
488
|
+
const details = event.errorDetails;
|
|
489
|
+
return { status: details?.code === "timeout" ? "timeout" : details?.errorClass === "runtime_abort" ? "aborted" : "error", summary: event.error };
|
|
490
|
+
}
|
|
491
|
+
return recoveryOutcomeForTurn(turn, runsBySession.get(turn.piboSessionId) ?? []);
|
|
492
|
+
},
|
|
475
493
|
});
|
|
476
494
|
if (recoveredTurns.length === 0)
|
|
477
495
|
return [];
|
|
@@ -482,7 +500,8 @@ export class PiboDataSessionStore {
|
|
|
482
500
|
if (!session)
|
|
483
501
|
continue;
|
|
484
502
|
const row = this.db.prepare("SELECT room_id FROM sessions WHERE id = ?").get(session.id);
|
|
485
|
-
const
|
|
503
|
+
const existingError = terminalErrors.get(recovered.turn.turnId);
|
|
504
|
+
const event = existingError ?? {
|
|
486
505
|
type: "session_error",
|
|
487
506
|
piboSessionId: session.id,
|
|
488
507
|
eventId: recoveryEventId(recovered.turn),
|
|
@@ -512,13 +531,14 @@ export class PiboDataSessionStore {
|
|
|
512
531
|
updated_at = ?
|
|
513
532
|
WHERE session_id = ?
|
|
514
533
|
`).run(at, at, at, session.id);
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
534
|
+
if (!existingError)
|
|
535
|
+
ingest.ingestOutputEvent({
|
|
536
|
+
session,
|
|
537
|
+
roomId: row?.room_id ?? undefined,
|
|
538
|
+
actorId: session.id,
|
|
539
|
+
event,
|
|
540
|
+
createdAt: at,
|
|
541
|
+
});
|
|
522
542
|
results.push({
|
|
523
543
|
turnId: recovered.turn.turnId,
|
|
524
544
|
piboSessionId: session.id,
|