@pasko70/pibo 3.0.1 → 3.1.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.
Files changed (122) hide show
  1. package/README.md +20 -6
  2. package/compute-image/Dockerfile +47 -0
  3. package/compute-image/Dockerfile.dockerignore +5 -0
  4. package/dist/agent-runtime/context-build.js +26 -2
  5. package/dist/agent-runtime/contract.js +78 -28
  6. package/dist/agent-runtime/portable-history.js +19 -9
  7. package/dist/agent-runtime/registry.js +8 -2
  8. package/dist/agent-runtime/routed-session.js +120 -15
  9. package/dist/agent-runtimes/codex-native/adapter.js +12 -0
  10. package/dist/agent-runtimes/codex-native/resource-delivery.js +35 -7
  11. package/dist/agent-runtimes/omp/adapter.js +3 -7
  12. package/dist/agent-runtimes/pi/adapter.js +5 -3
  13. package/dist/agent-runtimes/pi/routed-session.js +59 -12
  14. package/dist/agent-runtimes/pi/runtime.js +6 -0
  15. package/dist/apps/chat/agent-profiles.js +31 -2
  16. package/dist/apps/chat/agent-store.js +98 -6
  17. package/dist/apps/chat/chat-request-normalizers.js +31 -0
  18. package/dist/apps/chat/data/chat-data-mappers.js +61 -11
  19. package/dist/apps/chat/data/project-service.js +119 -15
  20. package/dist/apps/chat/data/room-service.js +21 -0
  21. package/dist/apps/chat/data/session-query-service.js +33 -10
  22. package/dist/apps/chat/data/timeline-query-service.js +60 -35
  23. package/dist/apps/chat/loop-api.js +10 -2
  24. package/dist/apps/chat/output-compactor.js +144 -21
  25. package/dist/apps/chat/output-event-policy.js +179 -2
  26. package/dist/apps/chat/stream.js +16 -7
  27. package/dist/apps/chat/trace-v2.js +1 -0
  28. package/dist/apps/chat/web-app.js +317 -86
  29. package/dist/apps/chat-ui/assets/{dist-BDIATCQt.js → dist-CUQJqggN.js} +1 -1
  30. package/dist/apps/chat-ui/assets/{dist-mqCviI-c.js → dist-ClUlQWYN.js} +1 -1
  31. package/dist/apps/chat-ui/assets/{dist-JnW4v8UE.js → dist-Djul9BmZ.js} +1 -1
  32. package/dist/apps/chat-ui/assets/{dist-lRiLP9rr.js → dist-GD0JGdCi.js} +1 -1
  33. package/dist/apps/chat-ui/assets/{dist-BBDMeU5-.js → dist-W5HOqHym.js} +1 -1
  34. package/dist/apps/chat-ui/assets/index-BcjkX-iP.js +228 -0
  35. package/dist/apps/chat-ui/assets/index-CmqRSbBU.css +1 -0
  36. package/dist/apps/chat-ui/index.html +2 -2
  37. package/dist/apps/chat-vscode-web/assets/index-JvrPUGvI.js +43 -0
  38. package/dist/apps/chat-vscode-web/index.html +1 -1
  39. package/dist/apps/cli-ui/InkSessionApp.js +111 -12
  40. package/dist/apps/cli-ui/cliSessionsCommand.js +34 -8
  41. package/dist/cli-session/localSessionSource.js +153 -161
  42. package/dist/cli.js +7 -11
  43. package/dist/compute/cli.js +35 -16
  44. package/dist/compute/docker.js +341 -94
  45. package/dist/config/config.js +3 -0
  46. package/dist/core/context-build.js +7 -16
  47. package/dist/core/output-persistence-retry.js +484 -0
  48. package/dist/core/output-render-sequence.js +331 -0
  49. package/dist/core/profiles.js +24 -0
  50. package/dist/core/provider-recovery.js +7 -1
  51. package/dist/core/session-model.js +37 -1
  52. package/dist/core/session-router.js +78 -110
  53. package/dist/data/ingest-service.js +91 -9
  54. package/dist/data/navigation-store.js +5 -5
  55. package/dist/data/pibo-store.js +8 -1
  56. package/dist/data/schema.js +243 -3
  57. package/dist/data/session-store.js +3 -3
  58. package/dist/debug/agents.js +91 -89
  59. package/dist/debug/index.js +46 -2
  60. package/dist/debug/pty.js +127 -48
  61. package/dist/gateway/cli.js +4 -0
  62. package/dist/gateway/web.js +8 -8
  63. package/dist/local/client.js +3 -2
  64. package/dist/loops/channel.js +3 -1
  65. package/dist/loops/cli.js +2 -1
  66. package/dist/loops/service.js +90 -11
  67. package/dist/loops/store.js +100 -74
  68. package/dist/mcp/agent-context.js +13 -26
  69. package/dist/mcp/commands/info.js +3 -1
  70. package/dist/mcp/config-command.js +46 -2
  71. package/dist/mcp/config.js +18 -4
  72. package/dist/plugins/context-files-store.js +36 -6
  73. package/dist/plugins/context-files.js +52 -1
  74. package/dist/plugins/registry.js +2 -0
  75. package/dist/plugins/user-profile-resources.js +22 -0
  76. package/dist/previews/base-url.js +34 -0
  77. package/dist/previews/config.js +2 -32
  78. package/dist/ralph/store.js +5 -0
  79. package/dist/reliability/store.js +378 -106
  80. package/dist/session-ui/terminalRows.js +8 -4
  81. package/dist/sessions/pibo-data-store.js +145 -0
  82. package/dist/sessions/store.js +42 -0
  83. package/dist/setup/cli.js +422 -6
  84. package/dist/setup/installation-profiles.js +464 -0
  85. package/dist/shared/deterministic-digest.js +94 -0
  86. package/dist/shared/trace-engine.js +54 -0
  87. package/dist/shared/trace-event-projection.js +124 -67
  88. package/dist/shared/trace-history.js +47 -12
  89. package/dist/shared/trace-live-reducer.js +23 -4
  90. package/dist/shared/trace-nodes.js +17 -28
  91. package/dist/shared/trace-order.js +41 -5
  92. package/dist/shared/trace-page-merge.js +44 -14
  93. package/dist/shared/trace-patch-nodes.js +2 -0
  94. package/dist/shared/trace-subagent-links.js +4 -1
  95. package/dist/shared/trace-tool-identity.js +15 -8
  96. package/dist/signals/projector.js +28 -6
  97. package/dist/subagents/observation-query.js +121 -0
  98. package/dist/subagents/tool.js +7 -3
  99. package/dist/tools/agent-browser-leases.js +58 -23
  100. package/dist/tools/agent-browser-wrapper.js +1 -1
  101. package/dist/tools/browser-use-leases.js +129 -16
  102. package/dist/tools/browser-use-wrapper.js +1 -1
  103. package/dist/tools/index.js +27 -11
  104. package/dist/tools/python-runtime.js +10 -3
  105. package/dist/tools/registry.js +1 -1
  106. package/dist/tools/runtime/node-backend.js +16 -5
  107. package/dist/tools/runtime/node-worker-source.js +112 -33
  108. package/dist/tools/runtime/python-backend.js +16 -5
  109. package/dist/tools/runtime/python-worker-source.js +167 -16
  110. package/dist/tools/runtime/registry.js +23 -10
  111. package/dist/user-skills/store.js +0 -1
  112. package/docs/ops/vscode-extension-release.md +9 -1
  113. package/npm-shrinkwrap.json +9 -2
  114. package/package.json +9 -2
  115. package/scripts/docker-entrypoint.sh +46 -0
  116. package/scripts/prepare-agent-browser-wrapper.sh +70 -0
  117. package/scripts/prepare-browser-use-wrapper.sh +255 -0
  118. package/dist/apps/chat-ui/assets/index-0XOOVxLP.js +0 -228
  119. package/dist/apps/chat-ui/assets/index-DV7_CgsC.css +0 -1
  120. package/dist/apps/chat-vscode-web/assets/index-Dtw2Z7jz.js +0 -43
  121. package/dist/apps/vscode-artifacts/latest.vsix +0 -0
  122. package/dist/apps/vscode-artifacts/pibo-vscode-ext-3.0.1.vsix +0 -0
@@ -2,10 +2,12 @@ import { randomUUID } from "node:crypto";
2
2
  import { isDeepStrictEqual } from "node:util";
3
3
  import { InitialSessionContext, } from "./profiles.js";
4
4
  import { createDefaultPiboPluginRegistry, createPiboProfileFromRegistryOrDefault, resolvePiboProfileNameFromRegistryOrDefault, selectDefaultPiboProfileName } from "../plugins/builtin.js";
5
- import { RuntimeRoutedSession as RoutedSession, } from "../agent-runtime/routed-session.js";
5
+ import { RUN_REMINDER_MAX_DURATION_MS, 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
- 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
+ import { piboAgentObservationDetails, piboAgentObservationKind, piboAgentObservationRole, piboAgentObservationSourceFromEvent, piboAgentObservationText, } from "../subagents/observations.js";
10
+ import { preparePiboAgentObservationQuery, selectPiboAgentObservationPage, } from "../subagents/observation-query.js";
9
11
  import { PiboRunRegistry } from "../runs/registry.js";
10
12
  import { PiboRunCancellationError, PiboRunCancelledError, PiboRunExecutionTimeoutError, waitForRunCancellationSettlement } from "../runs/lifecycle.js";
11
13
  import { PiboRunResourceLimitError } from "../runs/resource-isolation.js";
@@ -20,7 +22,7 @@ import { getDefaultPiboWorkspace } from "./workspace.js";
20
22
  import { loadPiboModelDefaults, selectRequestedFastMode } from "./model-defaults.js";
21
23
  import { loadPiboGatewaySettings } from "./gateway-settings.js";
22
24
  import { loadPiboUserSettings } from "./user-settings.js";
23
- import { resolvePiboSessionActiveModel } from "./session-model.js";
25
+ import { PIBO_INITIAL_MODEL_FALLBACKS_METADATA_KEY, resolvePiboSessionActiveModel, resolvePiboSessionModelFallbacks, withPiboSessionModelFallbacksMetadata, } from "./session-model.js";
24
26
  import { isPiboThinkingLevel } from "./thinking.js";
25
27
  import { RuntimeSessionRegistry } from "../tools/runtime/registry.js";
26
28
  import { GatewayWorkAdmissionController } from "./gateway-resource-guard.js";
@@ -93,6 +95,7 @@ function profileForSession(baseProfile, runtimeInstanceId, nativeSessionId, pare
93
95
  parentSessionId: parentNativeSessionId,
94
96
  model: usesProfileRuntime ? baseProfile.model : undefined,
95
97
  mainModel: usesProfileRuntime ? baseProfile.mainModel : undefined,
98
+ mainModelFallbacks: usesProfileRuntime ? baseProfile.mainModelFallbacks : undefined,
96
99
  subagentModel: usesProfileRuntime ? baseProfile.subagentModel : undefined,
97
100
  thinkingLevel: baseProfile.thinkingLevel,
98
101
  mainThinkingLevel: baseProfile.mainThinkingLevel,
@@ -157,7 +160,11 @@ function formatRunReminderMessage(notification) {
157
160
  toolName: run.toolName,
158
161
  summary: run.summary,
159
162
  })),
160
- instruction: "Use pibo_run_read for completed, failed, or timed_out runs. Use pibo_run_wait, pibo_run_status, pibo_run_cancel, or pibo_run_ack for runs you still need to manage.",
163
+ instruction: [
164
+ `This autonomous run-reminder turn stops after ${RUN_REMINDER_MAX_DURATION_MS / 60_000} minutes of wall-clock time.`,
165
+ "Handle the listed runs promptly, then finish the turn. Do not start new subagents, yielded runs, or other long-running work from this reminder; leave larger follow-up work for a separate user-initiated or Goal continuation turn.",
166
+ "Use pibo_run_read for completed, failed, or timed_out runs. Use pibo_run_wait, pibo_run_status, pibo_run_cancel, or pibo_run_ack for runs you still need to manage.",
167
+ ].join(" "),
161
168
  }),
162
169
  "</pibo_run_notification>",
163
170
  ].join("\n");
@@ -329,6 +336,7 @@ export class PiboSessionRouter {
329
336
  sessions = new Map();
330
337
  pendingSessions = new Map();
331
338
  listeners = new Set();
339
+ outputRenderSequencer;
332
340
  runRegistry;
333
341
  gatewayWorkAdmission = new GatewayWorkAdmissionController();
334
342
  signalRegistry;
@@ -374,6 +382,9 @@ export class PiboSessionRouter {
374
382
  // Preserve that composition contract during the adapter migration without branching on adapter ids.
375
383
  this.compatibilityRuntimeRegistry = options.pluginRegistry ? createDefaultPiboPluginRegistry() : undefined;
376
384
  this.sessionStore = options.sessionStore ?? new InMemoryPiboSessionStore();
385
+ this.outputRenderSequencer = new OutputRenderSequencer({
386
+ highWaterStore: outputRenderHighWaterStore(this.sessionStore),
387
+ });
377
388
  this.telemetryStore = options.telemetryStore ?? telemetryStoreFromSessionStore(this.sessionStore);
378
389
  this.telemetryWriter = this.telemetryStore ? new AsyncTelemetryWriter(this.telemetryStore) : undefined;
379
390
  this.telemetryRecorder = this.telemetryStore
@@ -419,9 +430,15 @@ export class PiboSessionRouter {
419
430
  this.signalRegistry.project({ type: "session_created", session });
420
431
  this.signalRegistry.project({ type: "pibo_output", event: recovery.event });
421
432
  }
433
+ const recoveredRunReminderControllers = new Set();
422
434
  for (const run of this.runRegistry.listAll({ includeConsumed: true, includeDetached: true })) {
423
435
  this.signalRegistry.project({ type: "run_changed", run, reason: "recovered" });
436
+ if (options.recoverInterruptedRuntimeState && isTerminalRunStatus(run.status) && this.sessionStore.get(run.controllerPiboSessionId)) {
437
+ recoveredRunReminderControllers.add(run.controllerPiboSessionId);
438
+ }
424
439
  }
440
+ for (const piboSessionId of recoveredRunReminderControllers)
441
+ this.scheduleRunReminder(piboSessionId, false);
425
442
  }
426
443
  subscribe(listener) {
427
444
  this.listeners.add(listener);
@@ -700,6 +717,7 @@ export class PiboSessionRouter {
700
717
  if (this.disposingSessions.get(id) === operation)
701
718
  this.disposingSessions.delete(id);
702
719
  this.quiescingSessions.delete(id);
720
+ this.outputRenderSequencer.disposeSession(id);
703
721
  this.signalRegistry.project({ type: "session_disposed", piboSessionId: id, reason });
704
722
  }
705
723
  await this.telemetryWriter?.flush();
@@ -916,8 +934,12 @@ export class PiboSessionRouter {
916
934
  expectedRevision: input.expectedRevision,
917
935
  mode,
918
936
  });
919
- if (switchingRuntime)
920
- this.sessionStore.update(piboSessionId, { activeModel: null });
937
+ if (switchingRuntime) {
938
+ this.sessionStore.update(piboSessionId, {
939
+ activeModel: null,
940
+ metadata: withPiboSessionModelFallbacksMetadata(session.metadata, []),
941
+ });
942
+ }
921
943
  return structuredClone(persisted);
922
944
  }
923
945
  getSessionRuntimeStatus(piboSessionId) {
@@ -1123,6 +1145,7 @@ export class PiboSessionRouter {
1123
1145
  this.runtimeResourceSessions.clear();
1124
1146
  this.activeSubagentRequests.clear();
1125
1147
  this.subagentRequestIdsByEvent.clear();
1148
+ this.outputRenderSequencer.disposeAll();
1126
1149
  this.agentObservations.length = 0;
1127
1150
  this.agentObservationEvictedThroughByParent.clear();
1128
1151
  await this.telemetryWriter?.dispose();
@@ -1229,6 +1252,9 @@ export class PiboSessionRouter {
1229
1252
  const activeModel = changesRuntimeModelNamespace
1230
1253
  ? undefined
1231
1254
  : this.ensureSessionActiveModel(piboSession, sessionProfile, parentModelScopeId, modelDefaults);
1255
+ const modelFallbacks = changesRuntimeModelNamespace
1256
+ ? []
1257
+ : this.ensureSessionModelFallbacks(piboSession, sessionProfile, parentModelScopeId, activeModel);
1232
1258
  const userSettings = loadPiboUserSettings();
1233
1259
  const telemetryExtension = this.telemetryStore
1234
1260
  ? createPiboProviderTelemetryExtension({ store: this.telemetryStore, writer: this.telemetryWriter, session: piboSession, model: activeModel })
@@ -1373,6 +1399,7 @@ export class PiboSessionRouter {
1373
1399
  ],
1374
1400
  modelDefaults,
1375
1401
  initialFastMode,
1402
+ providerFallbacksEnabled: modelFallbacks.length > 0,
1376
1403
  },
1377
1404
  },
1378
1405
  });
@@ -1472,6 +1499,7 @@ export class PiboSessionRouter {
1472
1499
  this.handleInterruptedRunReminders(messages);
1473
1500
  },
1474
1501
  messagePreflight: this.options.messagePreflight,
1502
+ modelFallbacks,
1475
1503
  getRuntimeAuthStatus: () => runtimeRegistry.getAgentRuntimeAuthStatus(binding.runtimeInstanceId),
1476
1504
  startRuntimeAuth: async (input) => {
1477
1505
  const { runtimeInstanceId: _runtimeInstanceId, ...result } = await runtimeRegistry.startAgentRuntimeAuth(binding.runtimeInstanceId, input);
@@ -1612,6 +1640,20 @@ export class PiboSessionRouter {
1612
1640
  }
1613
1641
  return activeModel;
1614
1642
  }
1643
+ ensureSessionModelFallbacks(piboSession, profile, parentPiSessionId, activeModel) {
1644
+ const modelFallbacks = resolvePiboSessionModelFallbacks({
1645
+ profile,
1646
+ piboSession,
1647
+ parentPiSessionId,
1648
+ activeModel,
1649
+ });
1650
+ if (piboSession.metadata?.[PIBO_INITIAL_MODEL_FALLBACKS_METADATA_KEY] === undefined) {
1651
+ this.sessionStore.update(piboSession.id, {
1652
+ metadata: withPiboSessionModelFallbacksMetadata(piboSession.metadata, modelFallbacks),
1653
+ });
1654
+ }
1655
+ return modelFallbacks;
1656
+ }
1615
1657
  resolveModelDefaults() {
1616
1658
  if (typeof this.options.modelDefaults === "function")
1617
1659
  return this.options.modelDefaults();
@@ -2042,98 +2084,15 @@ export class PiboSessionRouter {
2042
2084
  return session;
2043
2085
  }
2044
2086
  observeManagedAgents(parentPiboSessionId, input) {
2045
- const order = normalizePiboAgentObservationOrder(input.order);
2046
- const limit = normalizePiboAgentObservationLimit(input.limit);
2047
- const toolDetail = normalizePiboAgentObservationToolDetail(input.toolDetail);
2048
- const afterSequence = normalizePiboAgentObservationCursor(input.afterSequence);
2049
- const since = parsePiboAgentObservationTimestamp(input.since, "since");
2050
- const until = parsePiboAgentObservationTimestamp(input.until, "until");
2051
- if (since !== undefined && until !== undefined && since > until) {
2052
- throw new Error("Agent observation since must not be after until.");
2053
- }
2054
- const requestIds = input.requestIds ? new Set(input.requestIds) : undefined;
2055
- const agentIds = input.agentIds ? new Set(input.agentIds) : undefined;
2056
- if (agentIds) {
2057
- for (const agentId of agentIds)
2058
- this.requireManagedAgent(parentPiboSessionId, agentId);
2059
- }
2060
- const names = input.names ? new Set(input.names) : undefined;
2061
- const threadKeys = input.threadKeys ? new Set(input.threadKeys) : undefined;
2062
- const eventTypes = input.eventTypes ? new Set(input.eventTypes) : undefined;
2063
- const kinds = input.kinds ? new Set(input.kinds) : undefined;
2064
- const roles = input.roles ? new Set(input.roles) : undefined;
2065
- const textContains = input.textContains?.toLowerCase();
2066
- const defaultMessageView = eventTypes === undefined && kinds === undefined;
2067
- const explicitlySelectsTools = input.eventTypes?.some((eventType) => piboAgentObservationKind(eventType) === "tool") === true
2068
- || input.kinds?.includes("tool") === true;
2069
- const includeTools = input.includeTools === true || (input.includeTools === undefined && explicitlySelectsTools);
2070
- const defaultEventTypes = includeTools
2071
- ? [...PIBO_AGENT_OBSERVATION_DEFAULT_EVENT_TYPES, ...PIBO_AGENT_OBSERVATION_DEFAULT_TOOL_EVENT_TYPES]
2072
- : [...PIBO_AGENT_OBSERVATION_DEFAULT_EVENT_TYPES];
2073
- const defaultEventTypeSet = new Set(defaultEventTypes);
2074
- const matches = this.agentObservations.filter((observation) => {
2075
- if (observation.managingParentId !== parentPiboSessionId)
2076
- return false;
2077
- if (requestIds && (!observation.requestId || !requestIds.has(observation.requestId)))
2078
- return false;
2079
- if (agentIds && !agentIds.has(observation.agentId))
2080
- return false;
2081
- if (names && !names.has(observation.name))
2082
- return false;
2083
- if (threadKeys && (!observation.threadKey || !threadKeys.has(observation.threadKey)))
2084
- return false;
2085
- if (observation.kind === "tool" && !includeTools)
2086
- return false;
2087
- if (defaultMessageView && !defaultEventTypeSet.has(observation.eventType))
2088
- return false;
2089
- if (eventTypes && !eventTypes.has(observation.eventType))
2090
- return false;
2091
- if (kinds && !kinds.has(observation.kind))
2092
- return false;
2093
- if (roles && (!observation.role || !roles.has(observation.role)))
2094
- return false;
2095
- if (afterSequence !== undefined && observation.sequence <= afterSequence)
2096
- return false;
2097
- const createdAt = Date.parse(observation.createdAt);
2098
- if (since !== undefined && createdAt < since)
2099
- return false;
2100
- if (until !== undefined && createdAt > until)
2101
- return false;
2102
- if (textContains && !(observation.text ?? "").toLowerCase().includes(textContains))
2103
- return false;
2104
- return true;
2105
- });
2106
- const cursorPolling = afterSequence !== undefined;
2107
- const ordered = cursorPolling
2108
- ? matches.slice(0, limit)
2109
- : [...matches].sort((left, right) => order === "asc" ? left.sequence - right.sequence : right.sequence - left.sequence).slice(0, limit);
2110
- if (cursorPolling && order === "desc")
2111
- ordered.reverse();
2112
- const selected = ordered.map((observation) => {
2113
- const { managingParentId: _managingParentId, details, ...visible } = observation;
2114
- const compact = visible.kind === "tool" && toolDetail === "summary"
2115
- ? { ...visible, text: piboAgentObservationToolSummary(visible.text, visible.isError, details) }
2116
- : visible;
2117
- return input.includeDetails === true && details !== undefined ? { ...compact, details } : compact;
2118
- });
2119
- const evictedThrough = this.agentObservationEvictedThroughByParent.get(parentPiboSessionId) ?? 0;
2120
- const retentionTruncated = afterSequence === undefined ? evictedThrough > 0 : afterSequence < evictedThrough;
2121
- const nextAfterSequence = selected.reduce((maximum, observation) => Math.max(maximum, observation.sequence), afterSequence === undefined ? 0 : Math.max(afterSequence, evictedThrough));
2122
- return {
2123
- filters: {
2124
- ...input,
2125
- ...(defaultMessageView ? { eventTypes: defaultEventTypes } : {}),
2126
- ...(afterSequence !== undefined ? { afterSequence } : {}),
2127
- order,
2128
- limit,
2129
- includeTools,
2130
- toolDetail,
2131
- includeDetails: input.includeDetails === true,
2132
- },
2133
- observations: selected,
2134
- nextAfterSequence,
2135
- truncated: retentionTruncated || matches.length > selected.length,
2136
- };
2087
+ for (const agentId of input.agentIds ?? [])
2088
+ this.requireManagedAgent(parentPiboSessionId, agentId);
2089
+ const query = preparePiboAgentObservationQuery(input);
2090
+ const ordered = query.scanOrder === "asc"
2091
+ ? this.agentObservations
2092
+ : [...this.agentObservations].reverse();
2093
+ return selectPiboAgentObservationPage(ordered
2094
+ .filter((observation) => observation.managingParentId === parentPiboSessionId)
2095
+ .map(({ managingParentId: _managingParentId, ...observation }) => observation), query, { evictedThrough: this.agentObservationEvictedThroughByParent.get(parentPiboSessionId) ?? 0 });
2137
2096
  }
2138
2097
  async killManagedAgent(parentPiboSessionId, agentId) {
2139
2098
  const child = this.requireManagedAgent(parentPiboSessionId, agentId);
@@ -2346,13 +2305,13 @@ export class PiboSessionRouter {
2346
2305
  const parentChatRoomId = typeof parent.metadata?.chatRoomId === "string" ? parent.metadata.chatRoomId : undefined;
2347
2306
  if (parentChatRoomId)
2348
2307
  metadata.chatRoomId = parentChatRoomId;
2349
- const newSessionMetadata = {
2308
+ const newSessionMetadata = withPiboSessionModelFallbacksMetadata({
2350
2309
  ...metadata,
2351
2310
  ...(subagent.thinkingLevel ? { initialThinkingLevel: subagent.thinkingLevel } : {}),
2352
2311
  ...(subagent.runtimeOptions && Object.keys(subagent.runtimeOptions).length > 0
2353
2312
  ? { initialRuntimeOptions: structuredClone(subagent.runtimeOptions) }
2354
2313
  : {}),
2355
- };
2314
+ }, subagent.modelFallbacks ?? []);
2356
2315
  const existing = this.sessionStore.find({
2357
2316
  channel: "pibo.subagents",
2358
2317
  kind: "subagent",
@@ -2435,17 +2394,23 @@ export class PiboSessionRouter {
2435
2394
  }
2436
2395
  }
2437
2396
  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);
2397
+ const positionedEvent = this.outputRenderSequencer.position(event);
2398
+ const session = this.sessionStore.get(positionedEvent.piboSessionId);
2399
+ this.recordAgentObservation(positionedEvent, session);
2400
+ this.telemetryRecorder?.recordOutput(positionedEvent, { session, status: this.sessions.get(positionedEvent.piboSessionId)?.getStatus() });
2401
+ this.signalRegistry.project({ type: "pibo_output", event: positionedEvent, session });
2402
+ this.pluginRegistry.notifyEvent(positionedEvent);
2443
2403
  for (const listener of this.listeners) {
2444
- listener(event);
2404
+ try {
2405
+ listener(positionedEvent);
2406
+ }
2407
+ catch (error) {
2408
+ console.error("[pibo] output listener failed", error);
2409
+ }
2445
2410
  }
2446
- this.handleRunReminderOutput(event);
2447
- if (event.type === "message_finished" && event.source !== "service") {
2448
- this.scheduleRunReminder(event.piboSessionId, true);
2411
+ this.handleRunReminderOutput(positionedEvent);
2412
+ if (positionedEvent.type === "message_finished" && positionedEvent.source !== "service") {
2413
+ this.scheduleRunReminder(positionedEvent.piboSessionId, true);
2449
2414
  }
2450
2415
  };
2451
2416
  handleRunReminderOutput(event) {
@@ -2575,7 +2540,10 @@ export class PiboSessionRouter {
2575
2540
  }
2576
2541
  }
2577
2542
  projectKnownSessionSignals() {
2578
- for (const session of this.sessionStore.list?.() ?? []) {
2543
+ const sessions = this.sessionStore.list?.() ?? [];
2544
+ const depthBySessionId = new Map(sessions.map((session) => [session.id, this.getSubagentDepth(session.id)]));
2545
+ sessions.sort((left, right) => (depthBySessionId.get(left.id) ?? 0) - (depthBySessionId.get(right.id) ?? 0));
2546
+ for (const session of sessions) {
2579
2547
  this.signalRegistry.project({ type: "session_created", session });
2580
2548
  }
2581
2549
  }
@@ -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}\"`);
@@ -8,7 +8,7 @@ import { MessageStore } from "./message-store.js";
8
8
  import { NavigationStore } from "./navigation-store.js";
9
9
  import { ObservationStore } from "./observation-store.js";
10
10
  import { PayloadStore } from "./payload-store.js";
11
- import { applyPiboDataSchema } from "./schema.js";
11
+ import { applyPiboDataSchema, assertSupportedPiboDataSchemaVersion } from "./schema.js";
12
12
  import { TelemetryStore } from "./telemetry.js";
13
13
  import { SessionStore } from "./session-store.js";
14
14
  export class PiboDataStore {
@@ -27,6 +27,13 @@ export class PiboDataStore {
27
27
  if (this.path !== ":memory:")
28
28
  mkdirSync(dirname(this.path), { recursive: true });
29
29
  this.db = new DatabaseSync(this.path);
30
+ try {
31
+ assertSupportedPiboDataSchemaVersion(this.db);
32
+ }
33
+ catch (error) {
34
+ this.db.close();
35
+ throw error;
36
+ }
30
37
  if (insidePiboHome)
31
38
  protectPrivateFileSync(this.path);
32
39
  this.db.exec("PRAGMA busy_timeout = 5000");