@pasko70/pibo 3.4.3 → 3.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,7 +5,7 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <meta name="theme-color" content="#101d22" />
7
7
  <title>Pibo</title>
8
- <script type="module" crossorigin src="/apps/chat-vscode/assets/index-0oTGFHni.js"></script>
8
+ <script type="module" crossorigin src="/apps/chat-vscode/assets/index-CMwTB8o8.js"></script>
9
9
  <link rel="stylesheet" crossorigin href="/apps/chat-vscode/assets/index-b18ZkEo0.css">
10
10
  </head>
11
11
  <body>
@@ -1,5 +1,6 @@
1
1
  import { spawn, execFile } from "node:child_process";
2
- import { existsSync, mkdirSync, openSync, readFileSync, readdirSync } from "node:fs";
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
- return { reachable, mode, health: obj?.health, runtimeStatuses, activeRuns, ambiguous: booleanValue(obj?.ambiguous) };
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" && !force) {
389
- const active = checkActiveWork(await readGatewaySafetyStatus(target), target);
390
- if (active.unsafe) {
391
- console.error("Restart blocked: active agent work is running.");
392
- console.error("Do not restart the gateway now.");
393
- console.error("Ask the user before interrupting active sessions.");
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 ${RESTART_CONFIRMATION_TOKEN}
646
- Force a production restart after explicit confirmation
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
@@ -41,14 +41,7 @@ function applyToolDisplayMode(rows, mode) {
41
41
  const slimRow = {
42
42
  ...row,
43
43
  lines: row.lines.slice(0, 1),
44
- input: undefined,
45
- output: undefined,
46
- error: undefined,
47
- markdown: undefined,
48
- expandable: false,
49
44
  singleLine: true,
50
- previewOmission: undefined,
51
- detailItems: undefined,
52
45
  };
53
46
  if (mode !== "intent")
54
47
  return [slimRow];
@@ -60,6 +53,13 @@ function applyToolDisplayMode(rows, mode) {
60
53
  prefix: "bullet",
61
54
  tokens: [token(intent, row.status === "error" ? "red" : row.status === "done" ? "green" : "cyan", "semibold")],
62
55
  }],
56
+ input: undefined,
57
+ output: undefined,
58
+ error: undefined,
59
+ markdown: undefined,
60
+ expandable: false,
61
+ previewOmission: undefined,
62
+ detailItems: undefined,
63
63
  }];
64
64
  });
65
65
  }
@@ -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";
@@ -469,9 +469,26 @@ export class PiboDataSessionStore {
469
469
  const at = input.at ?? new Date().toISOString();
470
470
  const runsBySession = groupRunsByController(input.recoveredRuns ?? []);
471
471
  return this.dataStore.transaction(() => {
472
+ // Durable terminal output wins over stale telemetry. Do not send a different
473
+ // recovery payload through the same output identity during startup.
474
+ const terminalErrors = new Map();
472
475
  const recoveredTurns = this.dataStore.telemetry.recoverInterruptedTurns({
473
476
  at,
474
- resolveOutcome: (turn) => recoveryOutcomeForTurn(turn, runsBySession.get(turn.piboSessionId) ?? []),
477
+ resolveOutcome: (turn) => {
478
+ const identity = { type: "session_error", piboSessionId: turn.piboSessionId, eventId: recoveryEventId(turn), error: "" };
479
+ const existing = this.dataStore.eventLog.findByIdempotencyKey(outputIdempotencyKey(identity));
480
+ if (existing) {
481
+ const event = {
482
+ ...identity,
483
+ error: typeof existing.attributes.error === "string" ? existing.attributes.error : existing.previewText ?? "Runtime error",
484
+ errorDetails: existing.attributes.errorDetails,
485
+ };
486
+ terminalErrors.set(turn.turnId, event);
487
+ const details = event.errorDetails;
488
+ return { status: details?.code === "timeout" ? "timeout" : details?.errorClass === "runtime_abort" ? "aborted" : "error", summary: event.error };
489
+ }
490
+ return recoveryOutcomeForTurn(turn, runsBySession.get(turn.piboSessionId) ?? []);
491
+ },
475
492
  });
476
493
  if (recoveredTurns.length === 0)
477
494
  return [];
@@ -482,7 +499,8 @@ export class PiboDataSessionStore {
482
499
  if (!session)
483
500
  continue;
484
501
  const row = this.db.prepare("SELECT room_id FROM sessions WHERE id = ?").get(session.id);
485
- const event = {
502
+ const existingError = terminalErrors.get(recovered.turn.turnId);
503
+ const event = existingError ?? {
486
504
  type: "session_error",
487
505
  piboSessionId: session.id,
488
506
  eventId: recoveryEventId(recovered.turn),
@@ -512,13 +530,14 @@ export class PiboDataSessionStore {
512
530
  updated_at = ?
513
531
  WHERE session_id = ?
514
532
  `).run(at, at, at, session.id);
515
- ingest.ingestOutputEvent({
516
- session,
517
- roomId: row?.room_id ?? undefined,
518
- actorId: session.id,
519
- event,
520
- createdAt: at,
521
- });
533
+ if (!existingError)
534
+ ingest.ingestOutputEvent({
535
+ session,
536
+ roomId: row?.room_id ?? undefined,
537
+ actorId: session.id,
538
+ event,
539
+ createdAt: at,
540
+ });
522
541
  results.push({
523
542
  turnId: recovered.turn.turnId,
524
543
  piboSessionId: session.id,
@@ -1,3 +1,4 @@
1
+ import { randomUUID } from "node:crypto";
1
2
  import { createServer } from "node:http";
2
3
  import { PiboAuthError } from "../auth/types.js";
3
4
  import { handleSimpleAgentApiRequest } from "../api/simple-agent-api.js";
@@ -190,11 +191,12 @@ function createGatewayRuntimeStatuses(channelContext) {
190
191
  }
191
192
  });
192
193
  }
193
- function createGatewayStatusResponse(channelContext, options) {
194
+ function createGatewayStatusResponse(channelContext, options, generation) {
194
195
  const mode = gatewayMode(options);
195
196
  return responseJson({
196
197
  status: "ok",
197
198
  mode,
199
+ generation,
198
200
  health: { status: "ok", mode },
199
201
  runtimeStatuses: createGatewayRuntimeStatuses(channelContext),
200
202
  activeRuns: collectActiveRuns(channelContext),
@@ -238,6 +240,7 @@ async function waitForServerClose(closePromise, timeoutMs) {
238
240
  });
239
241
  }
240
242
  export function createWebHostChannel(options = {}) {
243
+ const generation = randomUUID();
241
244
  const host = options.host ?? DEFAULT_WEB_CHANNEL_HOST;
242
245
  const port = options.port ?? DEFAULT_WEB_CHANNEL_PORT;
243
246
  const shutdownDrainTimeoutMs = options.shutdownDrainTimeoutMs ?? DEFAULT_SHUTDOWN_DRAIN_TIMEOUT_MS;
@@ -319,7 +322,7 @@ export function createWebHostChannel(options = {}) {
319
322
  return;
320
323
  }
321
324
  if (url.pathname === "/gateway/status") {
322
- await sendResponse(nodeResponse, createGatewayStatusResponse(requireContext(), options));
325
+ await sendResponse(nodeResponse, createGatewayStatusResponse(requireContext(), options, generation));
323
326
  return;
324
327
  }
325
328
  if (url.pathname.startsWith("/api/auth/")) {
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@pasko70/pibo",
3
- "version": "3.4.3",
3
+ "version": "3.4.4",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@pasko70/pibo",
9
- "version": "3.4.3",
9
+ "version": "3.4.4",
10
10
  "workspaces": [
11
11
  "packages/workflows"
12
12
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pasko70/pibo",
3
- "version": "3.4.3",
3
+ "version": "3.4.4",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",