@pasko70/pibo 3.0.0 → 3.0.2

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.
Files changed (77) hide show
  1. package/README.md +20 -6
  2. package/dist/agent-runtime/context-build.js +26 -2
  3. package/dist/agent-runtime/routed-session.js +108 -9
  4. package/dist/agent-runtimes/codex-native/adapter.js +12 -0
  5. package/dist/agent-runtimes/codex-native/resource-delivery.js +35 -7
  6. package/dist/agent-runtimes/omp/adapter.js +3 -7
  7. package/dist/agent-runtimes/pi/adapter.js +5 -3
  8. package/dist/agent-runtimes/pi/routed-session.js +49 -8
  9. package/dist/agent-runtimes/pi/runtime.js +6 -0
  10. package/dist/apps/chat/agent-profiles.js +31 -2
  11. package/dist/apps/chat/agent-store.js +43 -3
  12. package/dist/apps/chat/chat-request-normalizers.js +31 -0
  13. package/dist/apps/chat/data/chat-data-mappers.js +61 -11
  14. package/dist/apps/chat/data/session-query-service.js +33 -10
  15. package/dist/apps/chat/data/timeline-query-service.js +60 -35
  16. package/dist/apps/chat/output-compactor.js +144 -21
  17. package/dist/apps/chat/output-event-policy.js +179 -2
  18. package/dist/apps/chat/stream.js +16 -7
  19. package/dist/apps/chat/trace-v2.js +1 -0
  20. package/dist/apps/chat/web-app.js +236 -64
  21. package/dist/apps/chat-ui/assets/{dist-BBDMeU5-.js → dist-B6GZgWSj.js} +1 -1
  22. package/dist/apps/chat-ui/assets/{dist-mqCviI-c.js → dist-BPotHecb.js} +1 -1
  23. package/dist/apps/chat-ui/assets/{dist-JnW4v8UE.js → dist-BzqQKeVO.js} +1 -1
  24. package/dist/apps/chat-ui/assets/{dist-BDIATCQt.js → dist-CvaPTBTN.js} +1 -1
  25. package/dist/apps/chat-ui/assets/{dist-lRiLP9rr.js → dist-paagejNj.js} +1 -1
  26. package/dist/apps/chat-ui/assets/index-6JJV-eic.js +228 -0
  27. package/dist/apps/chat-ui/assets/index-CeL9JPP5.css +1 -0
  28. package/dist/apps/chat-ui/index.html +2 -2
  29. package/dist/apps/chat-vscode-web/assets/{index-Dtw2Z7jz.js → index-U-MSErGa.js} +4 -4
  30. package/dist/apps/chat-vscode-web/index.html +1 -1
  31. package/dist/apps/cli-ui/cliSessionsCommand.js +34 -8
  32. package/dist/cli-session/localSessionSource.js +153 -161
  33. package/dist/cli.js +7 -11
  34. package/dist/core/context-build.js +7 -16
  35. package/dist/core/output-persistence-retry.js +484 -0
  36. package/dist/core/output-render-sequence.js +331 -0
  37. package/dist/core/profiles.js +24 -0
  38. package/dist/core/provider-recovery.js +7 -1
  39. package/dist/core/session-model.js +37 -1
  40. package/dist/core/session-router.js +67 -20
  41. package/dist/data/ingest-service.js +91 -9
  42. package/dist/data/navigation-store.js +5 -5
  43. package/dist/data/schema.js +122 -2
  44. package/dist/data/session-store.js +3 -3
  45. package/dist/debug/index.js +44 -2
  46. package/dist/gateway/web.js +8 -8
  47. package/dist/local/client.js +3 -2
  48. package/dist/plugins/context-files-store.js +30 -0
  49. package/dist/plugins/context-files.js +52 -1
  50. package/dist/plugins/registry.js +2 -0
  51. package/dist/plugins/user-profile-resources.js +22 -0
  52. package/dist/reliability/store.js +246 -37
  53. package/dist/session-ui/terminalRows.js +6 -3
  54. package/dist/sessions/pibo-data-store.js +145 -0
  55. package/dist/sessions/store.js +42 -0
  56. package/dist/setup/cli.js +422 -6
  57. package/dist/setup/installation-profiles.js +464 -0
  58. package/dist/shared/deterministic-digest.js +94 -0
  59. package/dist/shared/trace-engine.js +54 -0
  60. package/dist/shared/trace-event-projection.js +124 -67
  61. package/dist/shared/trace-history.js +47 -12
  62. package/dist/shared/trace-live-reducer.js +23 -4
  63. package/dist/shared/trace-nodes.js +17 -28
  64. package/dist/shared/trace-order.js +41 -5
  65. package/dist/shared/trace-page-merge.js +44 -14
  66. package/dist/shared/trace-patch-nodes.js +2 -0
  67. package/dist/shared/trace-subagent-links.js +4 -1
  68. package/dist/shared/trace-tool-identity.js +15 -8
  69. package/dist/signals/projector.js +28 -6
  70. package/dist/subagents/tool.js +7 -3
  71. package/docs/ops/vscode-extension-release.md +9 -1
  72. package/npm-shrinkwrap.json +2 -2
  73. package/package.json +2 -1
  74. package/dist/apps/chat-ui/assets/index-0XOOVxLP.js +0 -228
  75. package/dist/apps/chat-ui/assets/index-DV7_CgsC.css +0 -1
  76. package/dist/apps/vscode-artifacts/latest.vsix +0 -0
  77. package/dist/apps/vscode-artifacts/pibo-vscode-ext-3.0.0.vsix +0 -0
@@ -4,6 +4,7 @@ import { InitialSessionContext, } from "./profiles.js";
4
4
  import { createDefaultPiboPluginRegistry, createPiboProfileFromRegistryOrDefault, resolvePiboProfileNameFromRegistryOrDefault, selectDefaultPiboProfileName } from "../plugins/builtin.js";
5
5
  import { RuntimeRoutedSession as RoutedSession, } from "../agent-runtime/routed-session.js";
6
6
  import { runtimeSessionErrorDetails } from "./session-errors.js";
7
+ import { OutputRenderSequencer, outputRenderHighWaterStore } from "./output-render-sequence.js";
7
8
  import { normalizePiboAgentSessionName, } from "../subagents/tool.js";
8
9
  import { PIBO_AGENT_OBSERVATION_DEFAULT_EVENT_TYPES, PIBO_AGENT_OBSERVATION_DEFAULT_TOOL_EVENT_TYPES, normalizePiboAgentObservationCursor, normalizePiboAgentObservationLimit, normalizePiboAgentObservationOrder, normalizePiboAgentObservationToolDetail, parsePiboAgentObservationTimestamp, piboAgentObservationDetails, piboAgentObservationKind, piboAgentObservationRole, piboAgentObservationSourceFromEvent, piboAgentObservationText, piboAgentObservationToolSummary, } from "../subagents/observations.js";
9
10
  import { PiboRunRegistry } from "../runs/registry.js";
@@ -20,7 +21,7 @@ import { getDefaultPiboWorkspace } from "./workspace.js";
20
21
  import { loadPiboModelDefaults, selectRequestedFastMode } from "./model-defaults.js";
21
22
  import { loadPiboGatewaySettings } from "./gateway-settings.js";
22
23
  import { loadPiboUserSettings } from "./user-settings.js";
23
- import { resolvePiboSessionActiveModel } from "./session-model.js";
24
+ import { PIBO_INITIAL_MODEL_FALLBACKS_METADATA_KEY, resolvePiboSessionActiveModel, resolvePiboSessionModelFallbacks, withPiboSessionModelFallbacksMetadata, } from "./session-model.js";
24
25
  import { isPiboThinkingLevel } from "./thinking.js";
25
26
  import { RuntimeSessionRegistry } from "../tools/runtime/registry.js";
26
27
  import { GatewayWorkAdmissionController } from "./gateway-resource-guard.js";
@@ -93,6 +94,7 @@ function profileForSession(baseProfile, runtimeInstanceId, nativeSessionId, pare
93
94
  parentSessionId: parentNativeSessionId,
94
95
  model: usesProfileRuntime ? baseProfile.model : undefined,
95
96
  mainModel: usesProfileRuntime ? baseProfile.mainModel : undefined,
97
+ mainModelFallbacks: usesProfileRuntime ? baseProfile.mainModelFallbacks : undefined,
96
98
  subagentModel: usesProfileRuntime ? baseProfile.subagentModel : undefined,
97
99
  thinkingLevel: baseProfile.thinkingLevel,
98
100
  mainThinkingLevel: baseProfile.mainThinkingLevel,
@@ -329,6 +331,7 @@ export class PiboSessionRouter {
329
331
  sessions = new Map();
330
332
  pendingSessions = new Map();
331
333
  listeners = new Set();
334
+ outputRenderSequencer;
332
335
  runRegistry;
333
336
  gatewayWorkAdmission = new GatewayWorkAdmissionController();
334
337
  signalRegistry;
@@ -374,6 +377,9 @@ export class PiboSessionRouter {
374
377
  // Preserve that composition contract during the adapter migration without branching on adapter ids.
375
378
  this.compatibilityRuntimeRegistry = options.pluginRegistry ? createDefaultPiboPluginRegistry() : undefined;
376
379
  this.sessionStore = options.sessionStore ?? new InMemoryPiboSessionStore();
380
+ this.outputRenderSequencer = new OutputRenderSequencer({
381
+ highWaterStore: outputRenderHighWaterStore(this.sessionStore),
382
+ });
377
383
  this.telemetryStore = options.telemetryStore ?? telemetryStoreFromSessionStore(this.sessionStore);
378
384
  this.telemetryWriter = this.telemetryStore ? new AsyncTelemetryWriter(this.telemetryStore) : undefined;
379
385
  this.telemetryRecorder = this.telemetryStore
@@ -700,6 +706,7 @@ export class PiboSessionRouter {
700
706
  if (this.disposingSessions.get(id) === operation)
701
707
  this.disposingSessions.delete(id);
702
708
  this.quiescingSessions.delete(id);
709
+ this.outputRenderSequencer.disposeSession(id);
703
710
  this.signalRegistry.project({ type: "session_disposed", piboSessionId: id, reason });
704
711
  }
705
712
  await this.telemetryWriter?.flush();
@@ -1123,6 +1130,7 @@ export class PiboSessionRouter {
1123
1130
  this.runtimeResourceSessions.clear();
1124
1131
  this.activeSubagentRequests.clear();
1125
1132
  this.subagentRequestIdsByEvent.clear();
1133
+ this.outputRenderSequencer.disposeAll();
1126
1134
  this.agentObservations.length = 0;
1127
1135
  this.agentObservationEvictedThroughByParent.clear();
1128
1136
  await this.telemetryWriter?.dispose();
@@ -1229,6 +1237,9 @@ export class PiboSessionRouter {
1229
1237
  const activeModel = changesRuntimeModelNamespace
1230
1238
  ? undefined
1231
1239
  : this.ensureSessionActiveModel(piboSession, sessionProfile, parentModelScopeId, modelDefaults);
1240
+ const modelFallbacks = changesRuntimeModelNamespace
1241
+ ? []
1242
+ : this.ensureSessionModelFallbacks(piboSession, sessionProfile, parentModelScopeId, activeModel);
1232
1243
  const userSettings = loadPiboUserSettings();
1233
1244
  const telemetryExtension = this.telemetryStore
1234
1245
  ? createPiboProviderTelemetryExtension({ store: this.telemetryStore, writer: this.telemetryWriter, session: piboSession, model: activeModel })
@@ -1373,6 +1384,7 @@ export class PiboSessionRouter {
1373
1384
  ],
1374
1385
  modelDefaults,
1375
1386
  initialFastMode,
1387
+ providerFallbacksEnabled: modelFallbacks.length > 0,
1376
1388
  },
1377
1389
  },
1378
1390
  });
@@ -1472,6 +1484,7 @@ export class PiboSessionRouter {
1472
1484
  this.handleInterruptedRunReminders(messages);
1473
1485
  },
1474
1486
  messagePreflight: this.options.messagePreflight,
1487
+ modelFallbacks,
1475
1488
  getRuntimeAuthStatus: () => runtimeRegistry.getAgentRuntimeAuthStatus(binding.runtimeInstanceId),
1476
1489
  startRuntimeAuth: async (input) => {
1477
1490
  const { runtimeInstanceId: _runtimeInstanceId, ...result } = await runtimeRegistry.startAgentRuntimeAuth(binding.runtimeInstanceId, input);
@@ -1612,6 +1625,20 @@ export class PiboSessionRouter {
1612
1625
  }
1613
1626
  return activeModel;
1614
1627
  }
1628
+ ensureSessionModelFallbacks(piboSession, profile, parentPiSessionId, activeModel) {
1629
+ const modelFallbacks = resolvePiboSessionModelFallbacks({
1630
+ profile,
1631
+ piboSession,
1632
+ parentPiSessionId,
1633
+ activeModel,
1634
+ });
1635
+ if (piboSession.metadata?.[PIBO_INITIAL_MODEL_FALLBACKS_METADATA_KEY] === undefined) {
1636
+ this.sessionStore.update(piboSession.id, {
1637
+ metadata: withPiboSessionModelFallbacksMetadata(piboSession.metadata, modelFallbacks),
1638
+ });
1639
+ }
1640
+ return modelFallbacks;
1641
+ }
1615
1642
  resolveModelDefaults() {
1616
1643
  if (typeof this.options.modelDefaults === "function")
1617
1644
  return this.options.modelDefaults();
@@ -2052,6 +2079,10 @@ export class PiboSessionRouter {
2052
2079
  throw new Error("Agent observation since must not be after until.");
2053
2080
  }
2054
2081
  const requestIds = input.requestIds ? new Set(input.requestIds) : undefined;
2082
+ if (input.toolCallIds && input.toolCallIds.length > 50) {
2083
+ throw new Error("Agent observation toolCallIds must contain at most 50 entries.");
2084
+ }
2085
+ const toolCallIds = input.toolCallIds ? new Set(input.toolCallIds) : undefined;
2055
2086
  const agentIds = input.agentIds ? new Set(input.agentIds) : undefined;
2056
2087
  if (agentIds) {
2057
2088
  for (const agentId of agentIds)
@@ -2065,7 +2096,8 @@ export class PiboSessionRouter {
2065
2096
  const textContains = input.textContains?.toLowerCase();
2066
2097
  const defaultMessageView = eventTypes === undefined && kinds === undefined;
2067
2098
  const explicitlySelectsTools = input.eventTypes?.some((eventType) => piboAgentObservationKind(eventType) === "tool") === true
2068
- || input.kinds?.includes("tool") === true;
2099
+ || input.kinds?.includes("tool") === true
2100
+ || toolCallIds !== undefined;
2069
2101
  const includeTools = input.includeTools === true || (input.includeTools === undefined && explicitlySelectsTools);
2070
2102
  const defaultEventTypes = includeTools
2071
2103
  ? [...PIBO_AGENT_OBSERVATION_DEFAULT_EVENT_TYPES, ...PIBO_AGENT_OBSERVATION_DEFAULT_TOOL_EVENT_TYPES]
@@ -2076,6 +2108,8 @@ export class PiboSessionRouter {
2076
2108
  return false;
2077
2109
  if (requestIds && (!observation.requestId || !requestIds.has(observation.requestId)))
2078
2110
  return false;
2111
+ if (toolCallIds && (!observation.toolCallId || !toolCallIds.has(observation.toolCallId)))
2112
+ return false;
2079
2113
  if (agentIds && !agentIds.has(observation.agentId))
2080
2114
  return false;
2081
2115
  if (names && !names.has(observation.name))
@@ -2346,13 +2380,13 @@ export class PiboSessionRouter {
2346
2380
  const parentChatRoomId = typeof parent.metadata?.chatRoomId === "string" ? parent.metadata.chatRoomId : undefined;
2347
2381
  if (parentChatRoomId)
2348
2382
  metadata.chatRoomId = parentChatRoomId;
2349
- const newSessionMetadata = {
2383
+ const newSessionMetadata = withPiboSessionModelFallbacksMetadata({
2350
2384
  ...metadata,
2351
2385
  ...(subagent.thinkingLevel ? { initialThinkingLevel: subagent.thinkingLevel } : {}),
2352
2386
  ...(subagent.runtimeOptions && Object.keys(subagent.runtimeOptions).length > 0
2353
2387
  ? { initialRuntimeOptions: structuredClone(subagent.runtimeOptions) }
2354
2388
  : {}),
2355
- };
2389
+ }, subagent.modelFallbacks ?? []);
2356
2390
  const existing = this.sessionStore.find({
2357
2391
  channel: "pibo.subagents",
2358
2392
  kind: "subagent",
@@ -2435,17 +2469,23 @@ export class PiboSessionRouter {
2435
2469
  }
2436
2470
  }
2437
2471
  emitOutput = (event) => {
2438
- const session = this.sessionStore.get(event.piboSessionId);
2439
- this.recordAgentObservation(event, session);
2440
- this.telemetryRecorder?.recordOutput(event, { session, status: this.sessions.get(event.piboSessionId)?.getStatus() });
2441
- this.signalRegistry.project({ type: "pibo_output", event, session });
2442
- this.pluginRegistry.notifyEvent(event);
2472
+ const positionedEvent = this.outputRenderSequencer.position(event);
2473
+ const session = this.sessionStore.get(positionedEvent.piboSessionId);
2474
+ this.recordAgentObservation(positionedEvent, session);
2475
+ this.telemetryRecorder?.recordOutput(positionedEvent, { session, status: this.sessions.get(positionedEvent.piboSessionId)?.getStatus() });
2476
+ this.signalRegistry.project({ type: "pibo_output", event: positionedEvent, session });
2477
+ this.pluginRegistry.notifyEvent(positionedEvent);
2443
2478
  for (const listener of this.listeners) {
2444
- listener(event);
2479
+ try {
2480
+ listener(positionedEvent);
2481
+ }
2482
+ catch (error) {
2483
+ console.error("[pibo] output listener failed", error);
2484
+ }
2445
2485
  }
2446
- this.handleRunReminderOutput(event);
2447
- if (event.type === "message_finished" && event.source !== "service") {
2448
- this.scheduleRunReminder(event.piboSessionId, true);
2486
+ this.handleRunReminderOutput(positionedEvent);
2487
+ if (positionedEvent.type === "message_finished" && positionedEvent.source !== "service") {
2488
+ this.scheduleRunReminder(positionedEvent.piboSessionId, true);
2449
2489
  }
2450
2490
  };
2451
2491
  handleRunReminderOutput(event) {
@@ -2467,6 +2507,10 @@ export class PiboSessionRouter {
2467
2507
  if (!delivery)
2468
2508
  return;
2469
2509
  this.runReminderDeliveries.delete(eventId);
2510
+ if (event.errorDetails?.code === "loop_continuation_invalidated") {
2511
+ this.suppressRunReminderDelivery(delivery);
2512
+ return;
2513
+ }
2470
2514
  if (!isRunReminderContextPressureError(event)
2471
2515
  || this.closing
2472
2516
  || this.quiescingSessions.has(delivery.piboSessionId)
@@ -2475,13 +2519,7 @@ export class PiboSessionRouter {
2475
2519
  return;
2476
2520
  }
2477
2521
  if (this.hasRunReminderRecovery(delivery)) {
2478
- const released = this.runRegistry.releaseNotification(delivery.piboSessionId, delivery.notification);
2479
- for (const run of released)
2480
- this.runRegistry.suppressNotification(delivery.piboSessionId, run.runId);
2481
- this.clearRunReminderRecovery(delivery);
2482
- this.deferredRunReminders.delete(delivery.piboSessionId);
2483
- this.sessions.get(delivery.piboSessionId)?.removeQueuedMessages(isRunReminderServiceMessage);
2484
- this.scheduleRunReminder(delivery.piboSessionId, false, delivery.generation);
2522
+ this.suppressRunReminderDelivery(delivery);
2485
2523
  return;
2486
2524
  }
2487
2525
  this.runRegistry.releaseNotification(delivery.piboSessionId, delivery.notification);
@@ -2507,6 +2545,15 @@ export class PiboSessionRouter {
2507
2545
  return;
2508
2546
  this.scheduleRunReminder(event.piboSessionId, true, generation);
2509
2547
  }
2548
+ suppressRunReminderDelivery(delivery) {
2549
+ const released = this.runRegistry.releaseNotification(delivery.piboSessionId, delivery.notification);
2550
+ for (const run of released)
2551
+ this.runRegistry.suppressNotification(delivery.piboSessionId, run.runId);
2552
+ this.clearRunReminderRecovery(delivery);
2553
+ this.deferredRunReminders.delete(delivery.piboSessionId);
2554
+ this.sessions.get(delivery.piboSessionId)?.removeQueuedMessages(isRunReminderServiceMessage);
2555
+ this.scheduleRunReminder(delivery.piboSessionId, false, delivery.generation);
2556
+ }
2510
2557
  hasRunReminderRecovery(delivery) {
2511
2558
  const recovery = this.runReminderRecoveries.get(delivery.piboSessionId);
2512
2559
  return recovery?.generation === delivery.generation
@@ -1,5 +1,18 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import { rootSessionId } from "./session-store.js";
3
+ export class PiboOutputIdentityCollisionError extends Error {
4
+ idempotencyKey;
5
+ existingFingerprint;
6
+ incomingFingerprint;
7
+ code = "pibo_output_identity_collision";
8
+ constructor(idempotencyKey, existingFingerprint, incomingFingerprint) {
9
+ super(`Pibo output identity collision for "${idempotencyKey}"`);
10
+ this.idempotencyKey = idempotencyKey;
11
+ this.existingFingerprint = existingFingerprint;
12
+ this.incomingFingerprint = incomingFingerprint;
13
+ this.name = "PiboOutputIdentityCollisionError";
14
+ }
15
+ }
3
16
  const INLINE_MESSAGE_PAYLOAD_THRESHOLD_BYTES = 16 * 1024;
4
17
  const INLINE_JSON_PAYLOAD_THRESHOLD_BYTES = 16 * 1024;
5
18
  export class ChatDataIngestService {
@@ -76,9 +89,39 @@ export class ChatDataIngestService {
76
89
  ingestOutputEvent(input) {
77
90
  const event = input.event;
78
91
  const idempotencyKey = outputIdempotencyKey(event);
92
+ const identityFingerprint = outputEventFingerprint(event);
79
93
  if (idempotencyKey) {
80
94
  const existing = this.store.eventLog.findByIdempotencyKey(idempotencyKey);
81
95
  if (existing) {
96
+ const existingFingerprint = typeof existing.attributes.identityFingerprint === "string"
97
+ ? existing.attributes.identityFingerprint
98
+ : undefined;
99
+ if (existingFingerprint && existingFingerprint !== identityFingerprint) {
100
+ const now = input.createdAt ?? new Date().toISOString();
101
+ this.store.eventLog.appendEvent({
102
+ sessionId: input.session.id,
103
+ sessionSequence: this.nextEventSequence(input.session.id),
104
+ roomId: input.roomId,
105
+ topic: "pibo.diagnostic",
106
+ type: "pibo.output.identity_collision",
107
+ source: "pibo-ingest",
108
+ actorType: "system",
109
+ actorId: input.actorId,
110
+ eventId: eventIdForOutputEvent(event),
111
+ idempotencyKey: `${idempotencyKey}:collision:${identityFingerprint}`,
112
+ retentionClass: "audit_event",
113
+ previewText: `Output identity collision for ${event.type}`,
114
+ attributes: {
115
+ outputIdempotencyKey: idempotencyKey,
116
+ existingFingerprint,
117
+ incomingFingerprint: identityFingerprint,
118
+ incomingType: event.type,
119
+ },
120
+ createdAt: now,
121
+ indexedAt: now,
122
+ });
123
+ throw new PiboOutputIdentityCollisionError(idempotencyKey, existingFingerprint, identityFingerprint);
124
+ }
82
125
  return {
83
126
  streamId: existing.streamId,
84
127
  duplicate: true,
@@ -112,6 +155,9 @@ export class ChatDataIngestService {
112
155
  payloadRef,
113
156
  previewText: previewTextForOutputEvent(event),
114
157
  attributes: compactObject({
158
+ identityFingerprint,
159
+ eventIdentityScoped: eventIdForOutputEvent(event) !== undefined,
160
+ semanticEventId: eventIdForOutputEvent(event),
115
161
  legacyStreamId: input.legacyStreamId,
116
162
  inlinePayload: payloadRef ? undefined : toPiboJsonValue(payload?.value),
117
163
  ...attributesForOutputEvent(event),
@@ -222,7 +268,24 @@ function deterministicId(prefix, value) {
222
268
  return `${prefix}_${createHash("sha256").update(value).digest("hex").slice(0, 32)}`;
223
269
  }
224
270
  function hashJson(value) {
225
- return createHash("sha256").update(JSON.stringify(value) ?? "null").digest("hex").slice(0, 16);
271
+ return createHash("sha256").update(canonicalJson(value)).digest("hex").slice(0, 16);
272
+ }
273
+ function outputEventFingerprint(event) {
274
+ const identity = { ...event };
275
+ delete identity.renderSequence;
276
+ return createHash("sha256").update(canonicalJson(identity)).digest("hex");
277
+ }
278
+ function canonicalJson(value) {
279
+ if (value === null || typeof value !== "object")
280
+ return JSON.stringify(value) ?? "null";
281
+ if (Array.isArray(value))
282
+ return `[${value.map((item) => canonicalJson(item)).join(",")}]`;
283
+ const record = value;
284
+ const entries = Object.keys(record)
285
+ .sort()
286
+ .filter((key) => record[key] !== undefined)
287
+ .map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`);
288
+ return `{${entries.join(",")}}`;
226
289
  }
227
290
  function previewText(text) {
228
291
  const normalized = text.replace(/\s+/g, " ").trim();
@@ -231,23 +294,34 @@ function previewText(text) {
231
294
  function compactObject(value) {
232
295
  return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined));
233
296
  }
234
- function outputIdempotencyKey(event) {
297
+ export function outputIdempotencyKey(event) {
235
298
  const base = eventIdForOutputEvent(event) ?? ("toolCallId" in event ? event.toolCallId : undefined);
236
- if (!base)
237
- return undefined;
238
- return `pibo.output:${event.piboSessionId}:${event.type}:${base}:${outputPartKey(event)}`;
299
+ if (base)
300
+ return `pibo.output:${event.piboSessionId}:${event.type}:${base}:${outputPartKey(event)}`;
301
+ if (Number.isSafeInteger(event.renderSequence) && (event.renderSequence ?? 0) > 0) {
302
+ return `pibo.output:${event.piboSessionId}:${event.type}:render:${event.renderSequence}`;
303
+ }
304
+ return undefined;
305
+ }
306
+ export function outputPersistenceDeliveryKey(event) {
307
+ return outputIdempotencyKey(event)
308
+ ?? `pibo.output:${event.piboSessionId}:${event.type}:render:${event.renderSequence ?? "unpositioned"}`;
239
309
  }
240
310
  function outputPartKey(event) {
241
311
  if (event.type === "tool_call")
242
- return `${event.toolCallId}:${event.argsComplete ? "complete" : "partial"}:${hashJson(event.args)}`;
312
+ return `${event.toolCallId}:${event.toolInvocationOrdinal ?? 0}:${event.argsComplete ? "complete" : "partial"}:${hashJson(event.args)}`;
243
313
  if ("toolCallId" in event)
244
- return event.toolCallId ?? "main";
314
+ return `${event.toolCallId ?? "main"}:${event.toolInvocationOrdinal ?? 0}`;
245
315
  if (event.type === "assistant_message")
246
316
  return String(event.assistantIndex ?? event.contentIndex ?? 0);
247
317
  if (event.type === "assistant_delta")
248
318
  return String(event.assistantIndex ?? event.contentIndex ?? 0);
319
+ if (event.type === "assistant_usage")
320
+ return String(event.usageIndex ?? hashJson({ inputTokens: event.inputTokens, outputTokens: event.outputTokens, cacheReadTokens: event.cacheReadTokens, cacheWriteTokens: event.cacheWriteTokens, reasoningTokens: event.reasoningTokens, totalTokens: event.totalTokens, costUsd: event.costUsd }));
249
321
  if (event.type === "thinking_started" || event.type === "thinking_delta" || event.type === "thinking_finished")
250
322
  return String(event.thinkingIndex ?? event.contentIndex ?? 0);
323
+ if (event.type === "compaction_start" || event.type === "compaction_end")
324
+ return String(event.compactionIndex ?? 0);
251
325
  if (event.type === "execution_result")
252
326
  return event.action;
253
327
  return "main";
@@ -318,6 +392,9 @@ function previewTextForOutputEvent(event) {
318
392
  return event.type;
319
393
  }
320
394
  function attributesForOutputEvent(event) {
395
+ return { renderSequence: event.renderSequence, toolInvocationOrdinal: event.toolInvocationOrdinal, ...specificAttributesForOutputEvent(event) };
396
+ }
397
+ function specificAttributesForOutputEvent(event) {
321
398
  if (event.type === "message_queued")
322
399
  return { inlineText: event.text, source: event.source, queuedMessages: event.queuedMessages };
323
400
  if (event.type === "message_steered")
@@ -330,11 +407,14 @@ function attributesForOutputEvent(event) {
330
407
  return { assistantIndex: event.assistantIndex, contentIndex: event.contentIndex };
331
408
  if (event.type === "assistant_usage")
332
409
  return {
410
+ usageIndex: event.usageIndex,
333
411
  inputTokens: event.inputTokens,
334
412
  outputTokens: event.outputTokens,
335
413
  cacheReadTokens: event.cacheReadTokens,
336
414
  cacheWriteTokens: event.cacheWriteTokens,
415
+ reasoningTokens: event.reasoningTokens,
337
416
  totalTokens: event.totalTokens,
417
+ costUsd: event.costUsd,
338
418
  };
339
419
  if (event.type === "thinking_started" || event.type === "thinking_delta" || event.type === "thinking_finished")
340
420
  return { thinkingIndex: event.thinkingIndex, contentIndex: event.contentIndex };
@@ -349,7 +429,7 @@ function attributesForOutputEvent(event) {
349
429
  if (event.type === "session_error")
350
430
  return { error: event.error, ...(event.errorDetails ? { errorDetails: event.errorDetails } : {}) };
351
431
  if (event.type === "compaction_start" || event.type === "compaction_end")
352
- return { reason: event.reason, aborted: "aborted" in event ? event.aborted : undefined, errorMessage: "errorMessage" in event ? event.errorMessage : undefined };
432
+ return { compactionIndex: event.compactionIndex, reason: event.reason, aborted: "aborted" in event ? event.aborted : undefined, errorMessage: "errorMessage" in event ? event.errorMessage : undefined };
353
433
  return {};
354
434
  }
355
435
  function observationKindForOutputEvent(event) {
@@ -398,7 +478,9 @@ function outputSessionStatus(event) {
398
478
  return "error";
399
479
  if (event.type === "message_finished")
400
480
  return "idle";
401
- return "running";
481
+ if (event.type === "message_queued" || event.type === "message_steered" || event.type === "message_started")
482
+ return "running";
483
+ return undefined;
402
484
  }
403
485
  function toPiboJsonValue(value) {
404
486
  if (value === undefined)
@@ -14,19 +14,19 @@ export class NavigationStore {
14
14
  "origin_id = excluded.origin_id",
15
15
  "title = excluded.title",
16
16
  "profile = excluded.profile",
17
- "status = excluded.status",
17
+ ...(input.status === undefined ? [] : ["status = excluded.status"]),
18
18
  "archived_at = excluded.archived_at",
19
- "last_activity_at = excluded.last_activity_at",
19
+ "last_activity_at = MAX(session_navigation.last_activity_at, excluded.last_activity_at)",
20
20
  "last_message_preview = COALESCE(excluded.last_message_preview, session_navigation.last_message_preview)",
21
21
  "child_count = excluded.child_count",
22
- "sort_key = excluded.sort_key",
23
- "updated_at = excluded.updated_at",
22
+ "sort_key = MAX(session_navigation.sort_key, excluded.sort_key)",
23
+ "updated_at = MAX(session_navigation.updated_at, excluded.updated_at)",
24
24
  ];
25
25
  this.db.prepare(`
26
26
  INSERT INTO session_navigation (${insertColumns.join(", ")})
27
27
  VALUES (${insertColumns.map(() => "?").join(", ")})
28
28
  ON CONFLICT(session_id) DO UPDATE SET ${assignments.join(", ")}
29
- `).run(input.roomId ?? null, input.sessionId, input.rootSessionId ?? null, input.parentId ?? null, input.originId ?? null, input.title, input.profile, input.status, input.archivedAt ?? null, input.lastActivityAt, input.lastMessagePreview ?? null, input.childCount ?? 0, input.sortKey, input.updatedAt);
29
+ `).run(input.roomId ?? null, input.sessionId, input.rootSessionId ?? null, input.parentId ?? null, input.originId ?? null, input.title, input.profile, input.status ?? "running", input.archivedAt ?? null, input.lastActivityAt, input.lastMessagePreview ?? null, input.childCount ?? 0, input.sortKey, input.updatedAt);
30
30
  const stored = this.getSession(input.sessionId);
31
31
  if (!stored)
32
32
  throw new Error(`Failed to upsert session navigation \"${input.sessionId}\"`);
@@ -1,5 +1,31 @@
1
- export const PIBO_DATA_SCHEMA_VERSION = 6;
2
- export function applyPiboDataSchema(db) {
1
+ export const PIBO_DATA_SCHEMA_VERSION = 7;
2
+ export const PIBO_DATA_SCHEMA_MIGRATION_STEPS = [
3
+ "schema",
4
+ "runtime-bindings",
5
+ "render-high-water",
6
+ "sequence-repair-selection",
7
+ "sequence-repair-temporary",
8
+ "sequence-repair-backfill",
9
+ "sequence-repair-cleanup",
10
+ "runtime-binding-metadata",
11
+ "user-version",
12
+ ];
13
+ export function applyPiboDataSchema(db, hooks = {}) {
14
+ const ownsTransaction = !db.isTransaction;
15
+ if (ownsTransaction)
16
+ db.exec("BEGIN IMMEDIATE");
17
+ try {
18
+ applyPiboDataSchemaInTransaction(db, hooks);
19
+ if (ownsTransaction)
20
+ db.exec("COMMIT");
21
+ }
22
+ catch (error) {
23
+ if (ownsTransaction && db.isTransaction)
24
+ db.exec("ROLLBACK");
25
+ throw error;
26
+ }
27
+ }
28
+ function applyPiboDataSchemaInTransaction(db, hooks) {
3
29
  const previousVersion = Number(db.prepare("PRAGMA user_version").get()?.user_version ?? 0);
4
30
  const existingSessionCount = db.prepare("SELECT COUNT(*) AS count FROM sqlite_schema WHERE type = 'table' AND name = 'sessions'").get();
5
31
  const hadSessionsBeforeMigration = existingSessionCount.count > 0
@@ -46,6 +72,42 @@ export function applyPiboDataSchema(db) {
46
72
  FOREIGN KEY (pibo_session_id) REFERENCES sessions(id) ON DELETE CASCADE
47
73
  );
48
74
 
75
+ CREATE TABLE IF NOT EXISTS session_output_render_high_water (
76
+ pibo_session_id TEXT PRIMARY KEY,
77
+ high_water INTEGER NOT NULL CHECK(high_water >= 0),
78
+ updated_at TEXT NOT NULL,
79
+ FOREIGN KEY (pibo_session_id) REFERENCES sessions(id) ON DELETE CASCADE
80
+ );
81
+
82
+ CREATE TABLE IF NOT EXISTS session_tool_invocation_counters (
83
+ pibo_session_id TEXT NOT NULL,
84
+ event_id TEXT NOT NULL,
85
+ tool_call_id TEXT NOT NULL,
86
+ next_ordinal INTEGER NOT NULL CHECK(next_ordinal >= 0),
87
+ updated_at TEXT NOT NULL,
88
+ PRIMARY KEY (pibo_session_id, event_id, tool_call_id),
89
+ FOREIGN KEY (pibo_session_id) REFERENCES sessions(id) ON DELETE CASCADE
90
+ );
91
+
92
+ CREATE TABLE IF NOT EXISTS session_tool_invocations (
93
+ pibo_session_id TEXT NOT NULL,
94
+ event_id TEXT NOT NULL,
95
+ tool_call_id TEXT NOT NULL,
96
+ invocation_ordinal INTEGER NOT NULL CHECK(invocation_ordinal >= 0),
97
+ call_fingerprint TEXT,
98
+ status TEXT NOT NULL CHECK(status IN ('open', 'closed')),
99
+ seen_call INTEGER NOT NULL DEFAULT 0 CHECK(seen_call IN (0, 1)),
100
+ seen_started INTEGER NOT NULL DEFAULT 0 CHECK(seen_started IN (0, 1)),
101
+ seen_updated INTEGER NOT NULL DEFAULT 0 CHECK(seen_updated IN (0, 1)),
102
+ seen_finished INTEGER NOT NULL DEFAULT 0 CHECK(seen_finished IN (0, 1)),
103
+ created_at TEXT NOT NULL,
104
+ updated_at TEXT NOT NULL,
105
+ PRIMARY KEY (pibo_session_id, event_id, tool_call_id, invocation_ordinal),
106
+ FOREIGN KEY (pibo_session_id) REFERENCES sessions(id) ON DELETE CASCADE
107
+ );
108
+ CREATE INDEX IF NOT EXISTS idx_session_tool_invocations_open
109
+ ON session_tool_invocations(pibo_session_id, event_id, tool_call_id, status, invocation_ordinal DESC);
110
+
49
111
  CREATE UNIQUE INDEX IF NOT EXISTS idx_session_runtime_bindings_native
50
112
  ON session_runtime_bindings(runtime_adapter_id, native_session_id)
51
113
  WHERE native_session_id IS NOT NULL;
@@ -516,6 +578,7 @@ export function applyPiboDataSchema(db) {
516
578
  CREATE INDEX IF NOT EXISTS idx_telemetry_tool_calls_retention_updated
517
579
  ON telemetry_tool_calls(retention_class, updated_at);
518
580
  `);
581
+ hooks.afterStep?.("schema");
519
582
  db.exec(`
520
583
  INSERT OR IGNORE INTO session_runtime_bindings (
521
584
  pibo_session_id, runtime_instance_id, runtime_adapter_id, native_session_id,
@@ -536,6 +599,61 @@ export function applyPiboDataSchema(db) {
536
599
  WHERE existing.pibo_session_id = sessions.id
537
600
  );
538
601
  `);
602
+ hooks.afterStep?.("runtime-bindings");
603
+ db.exec(`
604
+ INSERT INTO session_output_render_high_water (pibo_session_id, high_water, updated_at)
605
+ SELECT
606
+ id,
607
+ CAST(json_extract(metadata_json, '$.outputRenderSequenceHighWater') AS INTEGER),
608
+ updated_at
609
+ FROM sessions
610
+ WHERE json_type(metadata_json, '$.outputRenderSequenceHighWater') IN ('integer', 'real')
611
+ AND CAST(json_extract(metadata_json, '$.outputRenderSequenceHighWater') AS INTEGER) >= 0
612
+ ON CONFLICT(pibo_session_id) DO UPDATE SET
613
+ high_water = MAX(session_output_render_high_water.high_water, excluded.high_water),
614
+ updated_at = CASE
615
+ WHEN excluded.high_water > session_output_render_high_water.high_water THEN excluded.updated_at
616
+ ELSE session_output_render_high_water.updated_at
617
+ END;
618
+ `);
619
+ hooks.afterStep?.("render-high-water");
620
+ // Always inspect for interrupted pre-atomic v7 repairs. A previous process
621
+ // may have written negative temporary values before setting user_version.
622
+ db.exec(`
623
+ DROP TABLE IF EXISTS temp.pibo_v7_sequence_repair_sessions;
624
+ CREATE TEMP TABLE pibo_v7_sequence_repair_sessions (
625
+ session_id TEXT PRIMARY KEY
626
+ );
627
+ INSERT INTO pibo_v7_sequence_repair_sessions (session_id)
628
+ SELECT DISTINCT session_id
629
+ FROM event_log
630
+ WHERE session_id IS NOT NULL
631
+ AND (session_sequence IS NULL OR session_sequence <= 0);
632
+ `);
633
+ hooks.afterStep?.("sequence-repair-selection");
634
+ db.exec(`
635
+ UPDATE event_log
636
+ SET session_sequence = -stream_id
637
+ WHERE session_id IN (SELECT session_id FROM pibo_v7_sequence_repair_sessions);
638
+ `);
639
+ hooks.afterStep?.("sequence-repair-temporary");
640
+ db.exec(`
641
+ WITH ranked AS (
642
+ SELECT
643
+ stream_id,
644
+ ROW_NUMBER() OVER (PARTITION BY session_id ORDER BY stream_id ASC) AS repaired_sequence
645
+ FROM event_log
646
+ WHERE session_id IN (SELECT session_id FROM pibo_v7_sequence_repair_sessions)
647
+ )
648
+ UPDATE event_log
649
+ SET session_sequence = (
650
+ SELECT repaired_sequence FROM ranked WHERE ranked.stream_id = event_log.stream_id
651
+ )
652
+ WHERE stream_id IN (SELECT stream_id FROM ranked);
653
+ `);
654
+ hooks.afterStep?.("sequence-repair-backfill");
655
+ db.exec("DROP TABLE pibo_v7_sequence_repair_sessions");
656
+ hooks.afterStep?.("sequence-repair-cleanup");
539
657
  if (previousVersion < PIBO_DATA_SCHEMA_VERSION && hadSessionsBeforeMigration) {
540
658
  const rows = db.prepare("SELECT pibo_session_id, metadata_json FROM session_runtime_bindings").all();
541
659
  const update = db.prepare("UPDATE session_runtime_bindings SET metadata_json = ? WHERE pibo_session_id = ?");
@@ -558,5 +676,7 @@ export function applyPiboDataSchema(db) {
558
676
  }), row.pibo_session_id);
559
677
  }
560
678
  }
679
+ hooks.afterStep?.("runtime-binding-metadata");
561
680
  db.exec(`PRAGMA user_version = ${PIBO_DATA_SCHEMA_VERSION}`);
681
+ hooks.afterStep?.("user-version");
562
682
  }
@@ -43,10 +43,10 @@ export class SessionStore {
43
43
  "workspace = excluded.workspace",
44
44
  "title = excluded.title",
45
45
  "first_message_preview = COALESCE(sessions.first_message_preview, excluded.first_message_preview)",
46
- "status = excluded.status",
46
+ ...(input.status === undefined ? [] : ["status = excluded.status"]),
47
47
  "metadata_json = excluded.metadata_json",
48
- "updated_at = excluded.updated_at",
49
- "last_activity_at = excluded.last_activity_at",
48
+ "updated_at = MAX(sessions.updated_at, excluded.updated_at)",
49
+ "last_activity_at = MAX(sessions.last_activity_at, excluded.last_activity_at)",
50
50
  ];
51
51
  this.db.prepare(`
52
52
  INSERT INTO sessions (${baseColumns.join(", ")})