@pasko70/pibo 3.4.4 → 3.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/dist/agent-runtime/routed-session.js +37 -12
  2. package/dist/agent-runtimes/pi/adapter.js +1 -0
  3. package/dist/agent-runtimes/pi/routed-session.js +2 -2
  4. package/dist/apps/chat/bounded-event-stream.js +98 -0
  5. package/dist/apps/chat/chat-settings-routes.js +3 -3
  6. package/dist/apps/chat/data/chat-data-mappers.js +32 -13
  7. package/dist/apps/chat/data/event-command-service.js +30 -21
  8. package/dist/apps/chat/data/history-query-service.js +34 -25
  9. package/dist/apps/chat/data/read-state-service.js +13 -0
  10. package/dist/apps/chat/data/session-query-service.js +19 -11
  11. package/dist/apps/chat/data/timeline-query-service.js +19 -7
  12. package/dist/apps/chat/message-command-dispatcher.js +134 -0
  13. package/dist/apps/chat/output-compactor.js +9 -0
  14. package/dist/apps/chat/output-event-policy.js +9 -1
  15. package/dist/apps/chat/stream.js +21 -3
  16. package/dist/apps/chat/telemetry-retention-service.js +113 -8
  17. package/dist/apps/chat/trace-response-cache.js +46 -0
  18. package/dist/apps/chat/trace-v2.js +12 -6
  19. package/dist/apps/chat/trace.js +1 -1
  20. package/dist/apps/chat/web-app.js +608 -281
  21. package/dist/apps/chat-ui/assets/{dist-Bf2KScPo.js → dist-BG0n7zLd.js} +1 -1
  22. package/dist/apps/chat-ui/assets/{dist-23lt_7qm.js → dist-D6TjFhAm.js} +1 -1
  23. package/dist/apps/chat-ui/assets/{dist-V06sfuZa.js → dist-D79vyxSX.js} +1 -1
  24. package/dist/apps/chat-ui/assets/{dist-CH3SvpYV.js → dist-DFZ8cwh0.js} +1 -1
  25. package/dist/apps/chat-ui/assets/{dist-DsgL8w-W.js → dist-cOjokPrK.js} +1 -1
  26. package/dist/apps/chat-ui/assets/index-RMHUTJ62.js +229 -0
  27. package/dist/apps/chat-ui/assets/{index-BOceJ0jM.css → index-hEkrlRk-.css} +1 -1
  28. package/dist/apps/chat-ui/index.html +2 -2
  29. package/dist/apps/chat-vscode-web/assets/index-xacbCyTx.js +44 -0
  30. package/dist/apps/chat-vscode-web/index.html +1 -1
  31. package/dist/compute/pool/seeds.js +25 -3
  32. package/dist/core/events.js +4 -0
  33. package/dist/core/output-render-sequence.js +2 -0
  34. package/dist/core/provider-capacity.js +33 -0
  35. package/dist/core/provider-telemetry.js +21 -6
  36. package/dist/core/runtime-capacity.js +174 -0
  37. package/dist/core/runtime-telemetry.js +40 -12
  38. package/dist/core/session-router.js +79 -2
  39. package/dist/data/async-chat-reads.js +38 -0
  40. package/dist/data/async-chat-storage.js +91 -0
  41. package/dist/data/async-telemetry-maintenance.js +9 -0
  42. package/dist/data/bounded-worker-client.js +256 -0
  43. package/dist/data/chat-read-projections.js +159 -0
  44. package/dist/data/chat-read-worker.js +73 -0
  45. package/dist/data/chat-storage-worker.js +146 -0
  46. package/dist/data/ingest-service.js +79 -14
  47. package/dist/data/message-command-store.js +148 -0
  48. package/dist/data/payload-store.js +92 -11
  49. package/dist/data/pibo-store.js +10 -8
  50. package/dist/data/schema.js +16 -2
  51. package/dist/data/session-store.js +3 -1
  52. package/dist/data/storage-backup.js +278 -0
  53. package/dist/data/telemetry-capture.js +188 -0
  54. package/dist/data/telemetry-command.js +3 -0
  55. package/dist/data/telemetry-maintenance-worker.js +40 -0
  56. package/dist/data/telemetry-maintenance.js +110 -0
  57. package/dist/data/telemetry-retention.js +16 -7
  58. package/dist/data/telemetry-worker.js +111 -0
  59. package/dist/data/telemetry-writer.js +150 -83
  60. package/dist/data/telemetry.js +5 -0
  61. package/dist/debug/index.js +52 -0
  62. package/dist/debug/storage-backup.js +33 -0
  63. package/dist/debug/telemetry-capture.js +66 -0
  64. package/dist/gateway/server.js +2 -0
  65. package/dist/providers/openai-gpt56.js +11 -6
  66. package/dist/session-ui/terminalRows.js +48 -6
  67. package/dist/sessions/pibo-data-store.js +1 -0
  68. package/dist/shared/debug-features.js +4 -0
  69. package/dist/shared/model-inference-metrics.js +23 -0
  70. package/dist/shared/trace-event-projection.js +59 -2
  71. package/dist/shared/trace-history.js +9 -0
  72. package/dist/shared/trace-live-reducer.js +1 -0
  73. package/dist/shared/trace-patch-nodes.js +19 -0
  74. package/dist/web/channel.js +3 -0
  75. package/dist/web/http.js +36 -3
  76. package/npm-shrinkwrap.json +2 -2
  77. package/package.json +1 -1
  78. package/dist/apps/chat-ui/assets/index-BOemYq-V.js +0 -228
  79. package/dist/apps/chat-vscode-web/assets/index-CMwTB8o8.js +0 -43
@@ -1,3 +1,4 @@
1
+ import { previouslyClearedMessages } from "../core/events.js";
1
2
  import { PiboSteeringUnavailableError, } from "../core/events.js";
2
3
  import { PIBO_PROVIDER_RECOVERY_PROMPT, isPiboProviderFallbackError } from "../core/provider-recovery.js";
3
4
  import { normalizeSessionErrorDetails, runtimeSessionErrorDetails } from "../core/session-errors.js";
@@ -166,8 +167,14 @@ export class RuntimeRoutedSession {
166
167
  if (this.sessionIdentityOperationInFlight && !this.forkCandidatesRequest) {
167
168
  throw new Error("Pibo session cannot accept messages while a session identity operation is in progress.");
168
169
  }
170
+ const messages = this.queue.filter(item => item.kind === "message");
171
+ const bytes = Buffer.byteLength(event.text);
172
+ if (bytes > 1024 * 1024 || messages.length >= 64 || messages.reduce((total, item) => total + Buffer.byteLength(item.event.text), bytes) > 4 * 1024 * 1024
173
+ || messages.some(item => Date.now() - item.acceptedAt >= 10 * 60 * 1000)) {
174
+ throw Object.assign(new Error("Session runtime queue count, byte or wait-age capacity reached."), { code: "runtime_capacity_unavailable" });
175
+ }
169
176
  onAccepted();
170
- this.queue.push({ kind: "message", event });
177
+ this.queue.push({ kind: "message", event, acceptedAt: Date.now() });
171
178
  const output = {
172
179
  type: "message_queued",
173
180
  piboSessionId: this.piboSessionId,
@@ -593,6 +600,7 @@ export class RuntimeRoutedSession {
593
600
  if (inFlight?.event.id === eventId) {
594
601
  if (!inFlight.cancellation) {
595
602
  inFlight.cancelled = true;
603
+ inFlight.capacityAbort.abort(new Error("Message cancelled while waiting for capacity."));
596
604
  this.notifyMessagesInterrupted([inFlight.event], "message cancelled");
597
605
  const active = this.activeMessage?.id === eventId;
598
606
  inFlight.cancellation = (async () => {
@@ -710,6 +718,7 @@ export class RuntimeRoutedSession {
710
718
  result: event.result,
711
719
  aborted: event.aborted,
712
720
  errorMessage: event.errorMessage,
721
+ compactionStats: event.compactionStats,
713
722
  }));
714
723
  return;
715
724
  case "approval_requested":
@@ -815,11 +824,29 @@ export class RuntimeRoutedSession {
815
824
  this.notifyState();
816
825
  }
817
826
  }
827
+ async promptWithinCapacity(event, inFlight, input) {
828
+ const provider = this.runtimeSession.getStatus().activeModel?.provider ?? this.runtimeSession.runtimeInstanceId;
829
+ const lease = await this.options.acquireProviderCapacity?.(provider, inFlight.capacityAbort.signal);
830
+ try {
831
+ if (this.disposed || inFlight.cancelled)
832
+ return;
833
+ if (!inFlight.started) {
834
+ inFlight.started = true;
835
+ this.emit({ type: "message_started", piboSessionId: this.piboSessionId, eventId: event.id, text: event.text, source: event.source, provenance: event.provenance });
836
+ }
837
+ if (this.disposed || inFlight.cancelled)
838
+ return;
839
+ await this.runtimeSession.prompt(input);
840
+ }
841
+ finally {
842
+ lease?.release();
843
+ }
844
+ }
818
845
  async promptWithModelFallbacks(event, inFlight) {
819
846
  const canSetModel = Boolean(this.runtimeSession.controls?.setModel);
820
847
  const fallbackModels = this.modelFallbacks.filter((model) => !sameModel(model, this.primaryModel));
821
848
  if (!canSetModel || fallbackModels.length === 0) {
822
- await this.runtimeSession.prompt({
849
+ await this.promptWithinCapacity(event, inFlight, {
823
850
  text: event.text,
824
851
  source: promptSource(event.source),
825
852
  capabilityScope: event.capabilityScope,
@@ -845,7 +872,7 @@ export class RuntimeRoutedSession {
845
872
  this.pendingProviderFailure = undefined;
846
873
  let promptError;
847
874
  try {
848
- await this.runtimeSession.prompt({
875
+ await this.promptWithinCapacity(event, inFlight, {
849
876
  text: attempt === 0 ? event.text : PIBO_PROVIDER_RECOVERY_PROMPT,
850
877
  source: attempt === 0 ? promptSource(event.source) : "rpc",
851
878
  capabilityScope: event.capabilityScope,
@@ -907,14 +934,6 @@ export class RuntimeRoutedSession {
907
934
  this.activeMessageFailed = false;
908
935
  this.beginRunReminderTurnGuard(event);
909
936
  this.resetContentIndices();
910
- this.emit({
911
- type: "message_started",
912
- piboSessionId: this.piboSessionId,
913
- eventId: event.id,
914
- text: event.text,
915
- source: event.source,
916
- provenance: event.provenance,
917
- });
918
937
  if (inFlight.cancelled)
919
938
  return;
920
939
  await this.promptWithModelFallbacks(event, inFlight);
@@ -963,6 +982,7 @@ export class RuntimeRoutedSession {
963
982
  const inFlight = {
964
983
  event,
965
984
  cancelled: false,
985
+ capacityAbort: new AbortController(),
966
986
  settled: new Promise((resolve) => { resolveSettled = resolve; }),
967
987
  resolveSettled: () => { resolveSettled?.(); },
968
988
  };
@@ -1129,8 +1149,12 @@ export class RuntimeRoutedSession {
1129
1149
  ? await this.options.logoutRuntimeAuth(input)
1130
1150
  : await this.pluginRegistry.logoutAgentRuntimeAuth(this.runtimeSession.runtimeInstanceId, input),
1131
1151
  getProviderUsage: () => this.getActionProviderUsage(),
1132
- clearQueue: () => this.clearQueue(),
1152
+ clearQueue: () => this.clearQueue() + previouslyClearedMessages(event),
1133
1153
  abort: async () => {
1154
+ if (this.inFlightMessage && !this.inFlightMessage.started) {
1155
+ await this.cancelMessage(this.inFlightMessage.event.id);
1156
+ return;
1157
+ }
1134
1158
  if (this.activeMessage)
1135
1159
  this.notifyMessagesInterrupted([this.activeMessage], "abort requested");
1136
1160
  await this.runtimeSession.abort();
@@ -1187,6 +1211,7 @@ export class RuntimeRoutedSession {
1187
1211
  if (this.disposed)
1188
1212
  return false;
1189
1213
  const activeMessage = this.activeMessage;
1214
+ this.inFlightMessage?.capacityAbort.abort(new Error(reason));
1190
1215
  this.notifyMessagesInterrupted(this.activeAndQueuedMessages(), reason);
1191
1216
  if (activeMessage) {
1192
1217
  const error = `Session disposed while a message was active: ${reason}`;
@@ -253,6 +253,7 @@ export function semanticEventFromPibo(event) {
253
253
  result: event.result,
254
254
  aborted: event.aborted,
255
255
  errorMessage: event.errorMessage,
256
+ compactionStats: event.compactionStats,
256
257
  };
257
258
  case "session_error":
258
259
  return { type: "error", message: event.error, details: event.errorDetails };
@@ -863,8 +863,6 @@ export class RoutedSession {
863
863
  if (assistantMessageEnded && !assistantMessageFailed)
864
864
  this.flushPendingProviderWebSearchFinishes();
865
865
  const usageEvent = assistantMessageEnded ? normalizeAssistantUsageEvent(this.piboSessionId, candidate?.message) : undefined;
866
- if (usageEvent)
867
- this.emit(this.withActiveMessage(usageEvent));
868
866
  // Pi gets the first chance to recover through its short retry/compaction loop.
869
867
  // Keep the final error pending so the routed turn can continue durable recovery.
870
868
  if (assistantMessageEnded && normalized?.type === "session_error") {
@@ -889,6 +887,8 @@ export class RoutedSession {
889
887
  this.emit(this.withActiveMessage(normalized));
890
888
  }
891
889
  }
890
+ if (usageEvent)
891
+ this.emit(this.withActiveMessage(usageEvent));
892
892
  if (this.forwardPiEvents) {
893
893
  this.emit({ type: "pi_event", piboSessionId: this.piboSessionId, event });
894
894
  }
@@ -0,0 +1,98 @@
1
+ /** Owns the only application queue; HWM zero prevents a second hidden stream queue. */
2
+ export class BoundedEventStream {
3
+ onClose;
4
+ maximumBytes;
5
+ maximumAgeMs;
6
+ maximumFrames;
7
+ stream;
8
+ writer;
9
+ controller;
10
+ queue = [];
11
+ bytes = 0;
12
+ stopped = false;
13
+ finishing = false;
14
+ pendingPull;
15
+ capacityWaiter;
16
+ timer;
17
+ constructor(onClose, maximumBytes = 2 * 1024 * 1024, maximumAgeMs = 5000, maximumFrames = 4096) {
18
+ this.onClose = onClose;
19
+ this.maximumBytes = maximumBytes;
20
+ this.maximumAgeMs = maximumAgeMs;
21
+ this.maximumFrames = maximumFrames;
22
+ this.stream = new ReadableStream({
23
+ start: controller => { this.controller = controller; },
24
+ pull: () => {
25
+ const frame = this.queue.shift();
26
+ if (frame) {
27
+ this.bytes -= frame.value.byteLength;
28
+ this.controller.enqueue(frame.value);
29
+ this.releaseCapacity();
30
+ if (this.finishing && !this.queue.length)
31
+ this.stop("complete");
32
+ return;
33
+ }
34
+ if (this.finishing || this.stopped) {
35
+ this.stop("complete");
36
+ return;
37
+ }
38
+ return new Promise(resolve => { this.pendingPull = resolve; });
39
+ },
40
+ cancel: () => this.stop("cancel"),
41
+ }, { highWaterMark: 0 });
42
+ const self = this;
43
+ this.writer = { enqueue: value => { this.enqueue(value); }, close: () => this.finish(), error: () => this.stop("error"), get desiredSize() { return self.stopped ? null : self.maximumBytes - self.bytes; } };
44
+ this.timer = setInterval(() => { if (this.queue.length && performance.now() - this.queue[0].at > this.maximumAgeMs)
45
+ this.stop("slow_age"); }, Math.min(1000, Math.max(5, maximumAgeMs / 2)));
46
+ this.timer.unref();
47
+ }
48
+ get closed() { return this.stopped || this.finishing; }
49
+ enqueue(value) {
50
+ if (this.closed)
51
+ return;
52
+ if (value.byteLength > this.maximumBytes || this.bytes + value.byteLength > this.maximumBytes || this.queue.length >= this.maximumFrames) {
53
+ this.stop("slow_bytes");
54
+ return;
55
+ }
56
+ if (this.pendingPull) {
57
+ const resolve = this.pendingPull;
58
+ this.pendingPull = undefined;
59
+ this.controller.enqueue(value);
60
+ resolve();
61
+ return;
62
+ }
63
+ this.queue.push({ value, at: performance.now() });
64
+ this.bytes += value.byteLength;
65
+ }
66
+ async waitForCapacity() { if (this.closed)
67
+ return false; if (this.bytes < this.maximumBytes / 2)
68
+ return true; return new Promise(resolve => { this.capacityWaiter = resolve; }); }
69
+ releaseCapacity() { if (this.bytes < this.maximumBytes / 2) {
70
+ this.capacityWaiter?.(!this.closed);
71
+ this.capacityWaiter = undefined;
72
+ } }
73
+ finish() { if (this.stopped)
74
+ return; this.finishing = true; if (!this.queue.length)
75
+ this.stop("complete"); }
76
+ fail() { this.stop("error"); }
77
+ stop(reason) {
78
+ if (this.stopped)
79
+ return;
80
+ this.stopped = true;
81
+ clearInterval(this.timer);
82
+ this.queue.length = 0;
83
+ this.bytes = 0;
84
+ try {
85
+ if (reason === "complete")
86
+ this.controller.close();
87
+ else if (reason !== "cancel")
88
+ this.controller.error(Error("Event stream budget exhausted; reconnect using the last received cursor."));
89
+ }
90
+ catch { }
91
+ this.pendingPull?.();
92
+ this.pendingPull = undefined;
93
+ this.capacityWaiter?.(false);
94
+ this.capacityWaiter = undefined;
95
+ this.onClose(reason);
96
+ }
97
+ status() { return { frames: this.queue.length, bytes: this.bytes, oldestAgeMs: this.queue.length ? performance.now() - this.queue[0].at : 0, closed: this.closed }; }
98
+ }
@@ -8,7 +8,7 @@ import { parseToolMetricTokenCalculation } from "../../shared/tool-call-token-se
8
8
  import { PiboWebHttpError, readJsonBody, responseJson } from "../../web/http.js";
9
9
  import { CHAT_WEB_API_PREFIX } from "./chat-api-routes.js";
10
10
  import { normalizeBasePromptMarkdown, normalizeBasePromptMode, normalizeCompactionPromptMarkdown, normalizeCompactionPromptMode, updateChatModelDefaults, } from "./chat-request-normalizers.js";
11
- import { pruneTelemetryOlderThan } from "./telemetry-retention-service.js";
11
+ import { pruneTelemetryOlderThanAsync } from "./telemetry-retention-service.js";
12
12
  export function chatSettingsRoute(pathname, method) {
13
13
  if (pathname === `${CHAT_WEB_API_PREFIX}/model-defaults` && method === "PATCH")
14
14
  return { kind: "model-defaults" };
@@ -70,8 +70,8 @@ export async function handleChatSettingsRoute(input) {
70
70
  const days = sanitizeTelemetryRetentionDays(body.days);
71
71
  if (!days)
72
72
  throw new PiboWebHttpError("Invalid telemetry retention days", 400);
73
- const result = pruneTelemetryOlderThan({ dataStore: input.dataStore, days, apply: body.dryRun !== true });
74
- if (result.applied)
73
+ const result = await pruneTelemetryOlderThanAsync({ dataStore: input.dataStore, days, apply: body.dryRun !== true, signal: request.signal });
74
+ if (result.applied && result.completed !== false)
75
75
  updateTelemetryRetentionLastPrunedAt(new Date().toISOString());
76
76
  return responseJson({ telemetryRetention: result });
77
77
  }
@@ -3,13 +3,31 @@ import { roomWorkspaceFromMetadata } from "../types/rooms.js";
3
3
  import { tracePayloadRefForStoredPayload } from "../trace-v2.js";
4
4
  import { qualifiedToolNodeId } from "../../../shared/trace-tool-identity.js";
5
5
  const MAX_TRACE_EVENT_HYDRATION_BYTES = 1024 * 1024;
6
- export function storedPiboEventFromV2Row(row, payloadStore) {
6
+ export function storedPiboEventFromV2Row(row, payloadStore, hydrationBytes = MAX_TRACE_EVENT_HYDRATION_BYTES) {
7
+ if (typeof hydrationBytes !== "number" || !Number.isFinite(hydrationBytes))
8
+ hydrationBytes = MAX_TRACE_EVENT_HYDRATION_BYTES;
7
9
  const attributes = parseJsonObject(row.attributes_json);
8
- const payload = outputPayloadFromV2Row(row, attributes, readPersistedPayload(row, payloadStore));
10
+ if (hydrationBytes < MAX_TRACE_EVENT_HYDRATION_BYTES && typeof attributes.inlineText === "string" && Buffer.byteLength(attributes.inlineText) > hydrationBytes)
11
+ attributes.inlineText = row.preview_text ?? "";
12
+ const payload = outputPayloadFromV2Row(row, attributes, readPersistedPayload(row, payloadStore, hydrationBytes));
9
13
  if (!payload)
10
14
  return undefined;
15
+ const storedPayloadRef = storedPayloadReference(row, attributes, payloadStore);
16
+ const renderSequence = numberAttribute(attributes, "renderSequence") ?? payload.renderSequence;
17
+ return { id: String(row.stream_id), piboSessionId: row.session_id ?? undefined, eventSequence: row.session_sequence ?? undefined, renderSequence, eventId: row.event_id ?? undefined, streamId: row.stream_id, storedPayloadRef, type: row.type, createdAt: row.created_at, payload };
18
+ }
19
+ export function storedChatEventFromV2Row(row, payloadStore, hydrationBytes = MAX_TRACE_EVENT_HYDRATION_BYTES) {
20
+ if (typeof hydrationBytes !== "number" || !Number.isFinite(hydrationBytes))
21
+ hydrationBytes = MAX_TRACE_EVENT_HYDRATION_BYTES;
22
+ const attributes = parseJsonObject(row.attributes_json);
23
+ if (hydrationBytes < MAX_TRACE_EVENT_HYDRATION_BYTES && typeof attributes.inlineText === "string" && Buffer.byteLength(attributes.inlineText) > hydrationBytes)
24
+ attributes.inlineText = row.preview_text ?? "";
25
+ const storedPayloadRef = storedPayloadReference(row, attributes, payloadStore);
26
+ return { storedPayloadRef, streamId: row.stream_id, roomId: row.room_id ?? undefined, piboSessionId: row.session_id ?? undefined, eventId: row.event_id ?? `evt_${row.stream_id}`, eventType: row.type, actorType: actorTypeValue(row.actor_type), actorId: row.actor_id ?? undefined, clientTxnId: typeof attributes.clientTxnId === "string" ? attributes.clientTxnId : undefined, createdAt: row.created_at, retentionClass: retentionClassValue(row.retention_class), payload: (outputPayloadFromV2Row(row, attributes, readPersistedPayload(row, payloadStore, hydrationBytes)) ?? null) };
27
+ }
28
+ function storedPayloadReference(row, attributes, payloadStore) {
11
29
  const payloadIdentity = tracePayloadIdentityForEvent(row, attributes);
12
- const storedPayloadRef = row.payload_ref && row.session_id && payloadStore && payloadIdentity
30
+ return row.payload_ref && row.session_id && payloadStore && payloadIdentity
13
31
  ? tracePayloadRefForStoredPayload({
14
32
  payloadStore,
15
33
  piboSessionId: row.session_id,
@@ -17,12 +35,6 @@ export function storedPiboEventFromV2Row(row, payloadStore) {
17
35
  ...payloadIdentity,
18
36
  })
19
37
  : undefined;
20
- const renderSequence = numberAttribute(attributes, "renderSequence") ?? payload.renderSequence;
21
- return { id: String(row.stream_id), piboSessionId: row.session_id ?? undefined, eventSequence: row.session_sequence ?? undefined, renderSequence, eventId: row.event_id ?? undefined, streamId: row.stream_id, storedPayloadRef, type: row.type, createdAt: row.created_at, payload };
22
- }
23
- export function storedChatEventFromV2Row(row, payloadStore) {
24
- const attributes = parseJsonObject(row.attributes_json);
25
- return { streamId: row.stream_id, roomId: row.room_id ?? undefined, piboSessionId: row.session_id ?? undefined, eventId: row.event_id ?? `evt_${row.stream_id}`, eventType: row.type, actorType: actorTypeValue(row.actor_type), actorId: row.actor_id ?? undefined, clientTxnId: typeof attributes.clientTxnId === "string" ? attributes.clientTxnId : undefined, createdAt: row.created_at, retentionClass: retentionClassValue(row.retention_class), payload: (outputPayloadFromV2Row(row, attributes, readPersistedPayload(row, payloadStore)) ?? null) };
26
38
  }
27
39
  function outputPayloadFromV2Row(row, attributes, persistedPayload) {
28
40
  const inlinePayload = attributes.inlinePayload ?? persistedPayload;
@@ -112,7 +124,7 @@ function outputPayloadFromV2Row(row, attributes, persistedPayload) {
112
124
  if (row.type === "compaction_start")
113
125
  return { ...base, type: "compaction_start", compactionIndex: numberAttribute(attributes, "compactionIndex"), reason: stringAttribute(attributes, "reason") ?? row.preview_text ?? "unknown" };
114
126
  if (row.type === "compaction_end")
115
- return { ...base, type: "compaction_end", compactionIndex: numberAttribute(attributes, "compactionIndex"), reason: stringAttribute(attributes, "reason") ?? row.preview_text ?? "unknown", result: inlinePayload, aborted: booleanAttribute(attributes, "aborted") ?? false, errorMessage: stringAttribute(attributes, "errorMessage") };
127
+ return { ...base, type: "compaction_end", compactionIndex: numberAttribute(attributes, "compactionIndex"), reason: stringAttribute(attributes, "reason") ?? row.preview_text ?? "unknown", result: inlinePayload, aborted: booleanAttribute(attributes, "aborted") ?? false, errorMessage: stringAttribute(attributes, "errorMessage"), compactionStats: isRecord(attributes.compactionStats) ? attributes.compactionStats : undefined };
116
128
  if (row.type === "session_error") {
117
129
  const error = stringAttribute(attributes, "error") ?? row.preview_text ?? "Error";
118
130
  return { ...base, type: "session_error", error, errorDetails: normalizeSessionErrorDetails(error, isRecord(attributes.errorDetails) ? attributes.errorDetails : undefined) };
@@ -121,14 +133,14 @@ function outputPayloadFromV2Row(row, attributes, persistedPayload) {
121
133
  return { type: "user.message.accepted", piboSessionId, roomId: row.room_id ?? undefined, text: stringAttribute(attributes, "inlineText") ?? row.preview_text ?? "", clientTxnId: stringAttribute(attributes, "clientTxnId") };
122
134
  return { ...base, type: row.type };
123
135
  }
124
- function readPersistedPayload(row, payloadStore) {
136
+ function readPersistedPayload(row, payloadStore, hydrationBytes = MAX_TRACE_EVENT_HYDRATION_BYTES) {
125
137
  if (!row.payload_ref || !payloadStore)
126
138
  return undefined;
127
139
  try {
128
140
  const metadata = payloadStore.getPayload(row.payload_ref);
129
- if (!metadata || metadata.byteSize > MAX_TRACE_EVENT_HYDRATION_BYTES)
141
+ if (!metadata || metadata.byteSize > hydrationBytes)
130
142
  return undefined;
131
- const text = Buffer.from(payloadStore.readPayloadBytesBounded(row.payload_ref, MAX_TRACE_EVENT_HYDRATION_BYTES)).toString("utf8");
143
+ const text = Buffer.from(payloadStore.readPayloadBytesBounded(row.payload_ref, hydrationBytes)).toString("utf8");
132
144
  return metadata.contentType.includes("json") ? JSON.parse(text) : text;
133
145
  }
134
146
  catch {
@@ -136,6 +148,13 @@ function readPersistedPayload(row, payloadStore) {
136
148
  }
137
149
  }
138
150
  function tracePayloadIdentityForEvent(row, attributes) {
151
+ const semanticId = stringAttribute(attributes, "semanticEventId") ?? row.event_id;
152
+ if (semanticId && (row.type === "assistant_message" || row.type === "thinking_finished")) {
153
+ const thinking = row.type === "thinking_finished";
154
+ const index = numberAttribute(attributes, thinking ? "thinkingIndex" : "assistantIndex") ?? numberAttribute(attributes, "contentIndex");
155
+ const identity = index === undefined ? semanticId : `${semanticId}:${thinking ? "thinking" : "assistant"}:${index}`;
156
+ return { nodeId: `${thinking ? "reasoning" : "assistant"}:${identity}`, payloadKind: thinking ? "reasoning" : "output" };
157
+ }
139
158
  const toolCallId = stringAttribute(attributes, "toolCallId");
140
159
  if (!toolCallId)
141
160
  return undefined;
@@ -1,6 +1,9 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { isLiveOnlyOutputEvent } from "../output-event-policy.js";
3
3
  import { compactObject, nextSessionSequence, previewForPayload, storedChatEventFromV2Row } from "./chat-data-mappers.js";
4
+ export function chatClientTransactionKey(roomId, actorId, clientTxnId) {
5
+ return `chat:user.accepted:${roomId ?? ""}:${actorId ?? ""}:${clientTxnId}`;
6
+ }
4
7
  export class ChatEventCommandService {
5
8
  store;
6
9
  constructor(store) {
@@ -9,10 +12,7 @@ export class ChatEventCommandService {
9
12
  appendEvent(input) {
10
13
  const createdAt = input.createdAt ?? new Date().toISOString();
11
14
  const eventId = input.eventId ?? `ce_${randomUUID()}`;
12
- const idempotencyKey = input.clientTxnId ? `chat:user.accepted:${input.roomId ?? ""}:${input.actorId ?? ""}:${input.clientTxnId}` : `chat:event:${eventId}`;
13
- const existing = input.clientTxnId ? this.findByClientTxn(input.roomId, input.actorId, input.clientTxnId) : undefined;
14
- if (existing)
15
- return existing;
15
+ const idempotencyKey = input.clientTxnId ? chatClientTransactionKey(input.roomId, input.actorId, input.clientTxnId) : `chat:event:${eventId}`;
16
16
  const stored = this.store.eventLog.appendEvent({
17
17
  sessionId: input.piboSessionId,
18
18
  sessionSequence: input.piboSessionId ? nextSessionSequence(this.store, input.piboSessionId) : undefined,
@@ -30,21 +30,7 @@ export class ChatEventCommandService {
30
30
  createdAt,
31
31
  indexedAt: createdAt,
32
32
  });
33
- return storedChatEventFromV2Row({
34
- stream_id: stored.streamId,
35
- session_id: stored.sessionId ?? null,
36
- session_sequence: stored.sessionSequence ?? null,
37
- room_id: stored.roomId ?? null,
38
- type: stored.type,
39
- actor_type: stored.actorType ?? null,
40
- actor_id: stored.actorId ?? null,
41
- event_id: stored.eventId ?? null,
42
- idempotency_key: stored.idempotencyKey ?? null,
43
- retention_class: stored.retentionClass,
44
- preview_text: stored.previewText ?? null,
45
- attributes_json: JSON.stringify(stored.attributes),
46
- created_at: stored.createdAt,
47
- });
33
+ return chatEventFromStoredRow(stored);
48
34
  }
49
35
  appendOutputEvent(event, _input = {}) {
50
36
  // Runtime output ingestion is owned by ChatDataIngestService. Avoid a second event_log insert here.
@@ -55,8 +41,14 @@ export class ChatEventCommandService {
55
41
  findByClientTxn(roomId, actorId, clientTxnId) {
56
42
  if (!roomId || !actorId)
57
43
  return undefined;
58
- const row = this.store.db.prepare("SELECT * FROM event_log WHERE room_id = ? AND actor_id = ? AND json_extract(attributes_json, '$.clientTxnId') = ? ORDER BY stream_id ASC LIMIT 1").get(roomId, actorId, clientTxnId);
59
- return row ? storedChatEventFromV2Row(row) : undefined;
44
+ // Empty IDs are not valid HTTP transaction keys. Retain the legacy direct-call
45
+ // lookup contract without adding a scan fallback to ordinary indexed misses.
46
+ if (clientTxnId === "") {
47
+ const row = this.store.db.prepare("SELECT * FROM event_log WHERE room_id = ? AND actor_id = ? AND json_extract(attributes_json, '$.clientTxnId') = ? ORDER BY stream_id ASC LIMIT 1").get(roomId, actorId, clientTxnId);
48
+ return row ? storedChatEventFromV2Row(row) : undefined;
49
+ }
50
+ const row = this.store.eventLog.findByIdempotencyKey(chatClientTransactionKey(roomId, actorId, clientTxnId));
51
+ return row ? chatEventFromStoredRow(row) : undefined;
60
52
  }
61
53
  deleteSessions(piboSessionIds) {
62
54
  if (!piboSessionIds.length)
@@ -73,3 +65,20 @@ export class ChatEventCommandService {
73
65
  return Number(result.changes ?? 0);
74
66
  }
75
67
  }
68
+ function chatEventFromStoredRow(stored) {
69
+ return storedChatEventFromV2Row({
70
+ stream_id: stored.streamId,
71
+ session_id: stored.sessionId ?? null,
72
+ session_sequence: stored.sessionSequence ?? null,
73
+ room_id: stored.roomId ?? null,
74
+ type: stored.type,
75
+ actor_type: stored.actorType ?? null,
76
+ actor_id: stored.actorId ?? null,
77
+ event_id: stored.eventId ?? null,
78
+ idempotency_key: stored.idempotencyKey ?? null,
79
+ retention_class: stored.retentionClass,
80
+ preview_text: stored.previewText ?? null,
81
+ attributes_json: JSON.stringify(stored.attributes),
82
+ created_at: stored.createdAt,
83
+ });
84
+ }
@@ -1,7 +1,11 @@
1
+ import { tracePayloadRefForStoredPayload } from "../trace-v2.js";
2
+ import { ChatReadProjectionStore } from "../../../data/chat-read-projections.js";
1
3
  export class ChatHistoryQueryService {
2
4
  store;
3
- constructor(store) {
5
+ pageByteBudget;
6
+ constructor(store, pageByteBudget = Number.POSITIVE_INFINITY) {
4
7
  this.store = store;
8
+ this.pageByteBudget = pageByteBudget;
5
9
  }
6
10
  listProductHistoryEntries(input) {
7
11
  const limit = Math.max(1, Math.min(input.limit ?? 200, 1000));
@@ -11,7 +15,12 @@ export class ChatHistoryQueryService {
11
15
  clauses.push("COALESCE(e.session_sequence, m.sequence) < ?");
12
16
  values.push(input.beforeSequence);
13
17
  }
14
- const rows = this.store.db.prepare(`
18
+ const complete = new ChatReadProjectionStore(this.store.db).status().historyComplete;
19
+ const rows = complete ? this.store.db.prepare(`
20
+ SELECT m.*,h.event_sequence FROM chat_history_index h JOIN chat_messages m ON m.id=h.message_id
21
+ WHERE h.session_id=? AND h.role IN ('user','assistant','system') ${input.beforeSequence !== undefined ? "AND h.event_sequence < ?" : ""}
22
+ ORDER BY h.event_sequence DESC,h.message_id DESC LIMIT ?
23
+ `).all(input.piboSessionId, ...(input.beforeSequence !== undefined ? [input.beforeSequence] : []), limit) : this.store.db.prepare(`
15
24
  SELECT m.*, e.session_sequence AS event_sequence
16
25
  FROM chat_messages m
17
26
  LEFT JOIN event_log e ON e.stream_id = m.source_stream_id
@@ -24,7 +33,9 @@ export class ChatHistoryQueryService {
24
33
  if (!role)
25
34
  return [];
26
35
  const attributes = parseObject(row.attributes_json);
27
- const text = this.readMessageText(row, attributes);
36
+ const text = this.readMessageText(row, attributes, Math.min(64 * 1024, Math.floor(this.pageByteBudget / limit)));
37
+ const contentBytes = row.content_payload_ref ? this.store.payloads.getPayload(row.content_payload_ref)?.byteSize : typeof attributes.inlineText === "string" ? Buffer.byteLength(attributes.inlineText) : Buffer.byteLength(row.content_preview ?? "");
38
+ const contentTruncated = contentBytes !== undefined && Buffer.byteLength(text) < contentBytes;
28
39
  return [{
29
40
  id: `product:${row.id}`,
30
41
  type: "message",
@@ -42,42 +53,40 @@ export class ChatHistoryQueryService {
42
53
  sourceStreamId: row.source_stream_id ?? undefined,
43
54
  completedAt: row.completed_at ?? undefined,
44
55
  payloadRef: row.content_payload_ref ?? undefined,
56
+ tracePayloadRef: contentTruncated && row.content_payload_ref ? tracePayloadRefForStoredPayload({ payloadStore: this.store.payloads, piboSessionId: row.session_id, payloadId: row.content_payload_ref, nodeId: `product:${row.id}`, payloadKind: "output" }) : undefined,
57
+ contentTruncated: contentTruncated || undefined,
58
+ contentBytes: contentTruncated ? contentBytes : undefined,
45
59
  }),
46
60
  }];
47
61
  });
48
62
  }
49
63
  getProductHistoryCoverage(piboSessionId) {
50
- const row = this.store.db.prepare(`
51
- SELECT
52
- COUNT(*) AS message_count,
53
- MIN(e.session_sequence) AS first_event_sequence,
54
- MAX(e.session_sequence) AS last_event_sequence,
55
- MIN(m.created_at) AS first_created_at,
56
- MAX(m.created_at) AS last_created_at
57
- FROM chat_messages m
58
- LEFT JOIN event_log e ON e.stream_id = m.source_stream_id
59
- WHERE m.session_id = ?
60
- `).get(piboSessionId);
61
- return {
62
- messageCount: Number(row.message_count ?? 0),
63
- firstEventSequence: row.first_event_sequence ?? undefined,
64
- lastEventSequence: row.last_event_sequence ?? undefined,
65
- firstCreatedAt: row.first_created_at ?? undefined,
66
- lastCreatedAt: row.last_created_at ?? undefined,
67
- };
64
+ const complete = new ChatReadProjectionStore(this.store.db).status().historyComplete;
65
+ const count = this.store.db.prepare("SELECT message_count,revision FROM chat_history_counts WHERE session_id=?").get(piboSessionId);
66
+ const edge = (column, order) => this.store.db.prepare(`SELECT ${column} AS value FROM chat_history_index WHERE session_id=? ORDER BY ${column} ${order},message_id ${order} LIMIT 1`).get(piboSessionId);
67
+ return { messageCount: count?.message_count ?? 0, revision: count?.revision ?? 0, complete,
68
+ firstEventSequence: edge("event_sequence", "ASC")?.value,
69
+ lastEventSequence: edge("event_sequence", "DESC")?.value,
70
+ firstCreatedAt: edge("created_at", "ASC")?.value,
71
+ lastCreatedAt: edge("created_at", "DESC")?.value };
68
72
  }
69
- readMessageText(row, attributes) {
73
+ readMessageText(row, attributes, maximum) {
70
74
  if (row.content_payload_ref) {
71
75
  try {
72
- return this.store.payloads.readPayloadText(row.content_payload_ref);
76
+ if (!Number.isFinite(this.pageByteBudget))
77
+ return this.store.payloads.readPayloadText(row.content_payload_ref);
78
+ const payload = this.store.payloads.getPayload(row.content_payload_ref);
79
+ if (!payload || payload.byteSize > maximum)
80
+ return (row.content_preview ?? "").slice(0, Math.min(2048, maximum / 4));
81
+ return Buffer.from(this.store.payloads.readPayloadBytesBounded(row.content_payload_ref, maximum)).toString("utf8");
73
82
  }
74
83
  catch {
75
84
  // Retain the durable preview if the external payload was removed or corrupted.
76
85
  }
77
86
  }
78
87
  if (typeof attributes.inlineText === "string")
79
- return attributes.inlineText;
80
- return row.content_preview ?? "";
88
+ return Number.isFinite(this.pageByteBudget) ? attributes.inlineText.slice(0, Math.floor(maximum / 4)) : attributes.inlineText;
89
+ return Number.isFinite(this.pageByteBudget) ? (row.content_preview ?? "").slice(0, Math.floor(maximum / 4)) : (row.content_preview ?? "");
81
90
  }
82
91
  }
83
92
  function historyRole(role) {
@@ -1,3 +1,5 @@
1
+ import { ChatSessionQueryService } from "./session-query-service.js";
2
+ import { ChatReadProjectionStore } from "../../../data/chat-read-projections.js";
1
3
  export class ChatReadStateService {
2
4
  store;
3
5
  constructor(store) {
@@ -14,11 +16,22 @@ export class ChatReadStateService {
14
16
  updated_at = excluded.updated_at
15
17
  `).run(piboSessionId, lastReadStreamId, now, now);
16
18
  }
19
+ sessionIndexPage(input) { return new ChatSessionQueryService(this.store).listSessionIndexPage(input); }
20
+ unreadCountsPage(input) { return [...this.countUnreadMessagesBySession(input)]; }
17
21
  countUnreadMessagesBySession(input) {
18
22
  const counts = new Map();
19
23
  const uniqueIds = [...new Set(input.piboSessionIds)];
20
24
  if (!uniqueIds.length)
21
25
  return counts;
26
+ if (new ChatReadProjectionStore(this.store.db).status().unreadComplete) {
27
+ for (let offset = 0; offset < uniqueIds.length; offset += 400) {
28
+ const ids = uniqueIds.slice(offset, offset + 400);
29
+ const rows = this.store.db.prepare(`SELECT session_id,unread_count FROM chat_unread_counts WHERE session_id IN (${ids.map(() => "?").join(",")}) AND unread_count>0`).all(...ids);
30
+ for (const row of rows)
31
+ counts.set(row.session_id, row.unread_count);
32
+ }
33
+ return counts;
34
+ }
22
35
  for (let offset = 0; offset < uniqueIds.length; offset += 400) {
23
36
  const ids = uniqueIds.slice(offset, offset + 400);
24
37
  const requested = ids.map(() => "(?)").join(", ");
@@ -6,20 +6,21 @@ export class ChatSessionQueryService {
6
6
  constructor(store) {
7
7
  this.store = store;
8
8
  }
9
- upsertSession(session, status = "idle", lastActivityAt = session.updatedAt) {
9
+ upsertSession(session, status = "idle", lastActivityAt = session.updatedAt, options = {}) {
10
10
  const roomId = chatRoomIdFromMetadata(session.metadata) ?? "room_default";
11
- this.store.sessions.upsertSession({ session, roomId, status, lastActivityAt });
11
+ this.store.sessions.upsertSession({ session, roomId, status, lastActivityAt, preserveRuntimeBinding: options.preserveRuntimeBinding });
12
12
  this.upsertNavigation(session, roomId, status, lastActivityAt);
13
13
  }
14
14
  upsertSessionsIfChanged(sessions) {
15
15
  let written = 0;
16
16
  let skipped = 0;
17
17
  for (const session of sessions) {
18
- if (this.sessionIndexMatches(session)) {
18
+ const status = this.getSession(session.id)?.status ?? "idle";
19
+ if (this.sessionIndexMatches(session, status)) {
19
20
  skipped++;
20
21
  continue;
21
22
  }
22
- this.upsertSession(session);
23
+ this.upsertSession(session, status);
23
24
  written++;
24
25
  }
25
26
  return { checked: sessions.length, written, skipped };
@@ -28,7 +29,7 @@ export class ChatSessionQueryService {
28
29
  if (session) {
29
30
  const status = statusFromOutputEvent(event);
30
31
  if (status)
31
- this.upsertSession(session, status, createdAt);
32
+ this.upsertSession(session, status, createdAt, { preserveRuntimeBinding: true });
32
33
  else
33
34
  this.touchSession(session, createdAt);
34
35
  }
@@ -36,7 +37,7 @@ export class ChatSessionQueryService {
36
37
  }
37
38
  touchSession(session, lastActivityAt) {
38
39
  const roomId = chatRoomIdFromMetadata(session.metadata) ?? "room_default";
39
- this.store.sessions.upsertSession({ session, roomId, lastActivityAt });
40
+ this.store.sessions.upsertSession({ session, roomId, lastActivityAt, preserveRuntimeBinding: true });
40
41
  this.store.navigation.upsertSession({
41
42
  roomId,
42
43
  sessionId: session.id,
@@ -50,19 +51,26 @@ export class ChatSessionQueryService {
50
51
  updatedAt: lastActivityAt,
51
52
  });
52
53
  }
53
- listSessions() {
54
+ listSessions(roomId) {
54
55
  const rows = this.store.db.prepare(`
55
- SELECT s.*, b.runtime_instance_id, b.runtime_adapter_id, b.native_session_id, b.binding_state
56
+ SELECT s.id,s.pi_session_id,s.parent_id,s.channel,s.kind,s.profile,s.created_at,s.updated_at,s.last_activity_at,s.status,b.runtime_instance_id,b.runtime_adapter_id,b.native_session_id,b.binding_state
56
57
  FROM sessions s
57
58
  LEFT JOIN session_runtime_bindings b ON b.pibo_session_id = s.id
58
- WHERE s.deleted_at IS NULL
59
+ WHERE s.deleted_at IS NULL ${roomId ? "AND s.room_id=?" : ""}
59
60
  ORDER BY s.last_activity_at DESC, s.created_at DESC
60
- `).all();
61
+ `).all(...(roomId ? [roomId] : []));
62
+ return rows.map(sessionFromRow);
63
+ }
64
+ listSessionIndexPage(input) {
65
+ const limit = Math.max(1, Math.min(input.limit ?? 500, 500));
66
+ const rows = this.store.db.prepare(`SELECT s.id,s.pi_session_id,s.parent_id,s.channel,s.kind,s.profile,s.created_at,s.updated_at,s.last_activity_at,s.status,b.runtime_instance_id,b.runtime_adapter_id,b.native_session_id,b.binding_state
67
+ FROM sessions s LEFT JOIN session_runtime_bindings b ON b.pibo_session_id=s.id
68
+ WHERE s.deleted_at IS NULL AND s.room_id=? AND s.id>? ORDER BY s.id LIMIT ?`).all(input.roomId, input.afterId ?? "", limit);
61
69
  return rows.map(sessionFromRow);
62
70
  }
63
71
  getSession(piboSessionId) {
64
72
  const row = this.store.db.prepare(`
65
- SELECT s.*, b.runtime_instance_id, b.runtime_adapter_id, b.native_session_id, b.binding_state
73
+ SELECT s.id,s.pi_session_id,s.parent_id,s.channel,s.kind,s.profile,s.created_at,s.updated_at,s.last_activity_at,s.status,b.runtime_instance_id,b.runtime_adapter_id,b.native_session_id,b.binding_state
66
74
  FROM sessions s
67
75
  LEFT JOIN session_runtime_bindings b ON b.pibo_session_id = s.id
68
76
  WHERE s.id = ? AND s.deleted_at IS NULL