@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,12 +1,15 @@
1
1
  import { createHash } from "node:crypto";
2
+ import { boundedMessageBytes } from "../../../data/bounded-worker-client.js";
2
3
  import { messageTurnTimingsFromEvents } from "../../../shared/trace-event-projection.js";
3
4
  import { storedChatEventFromV2Row, storedPiboEventFromV2Row } from "./chat-data-mappers.js";
4
5
  import { TRACE_RECONCILIATION_ENTRY_CAP, TRACE_RECONCILIATION_TIMING_CAP } from "../../../shared/trace-limits.js";
5
6
  import { parseTraceToolNodeIdentity } from "../../../shared/trace-tool-identity.js";
6
7
  export class ChatTimelineQueryService {
7
8
  store;
8
- constructor(store) {
9
+ hydrationBytes;
10
+ constructor(store, hydrationBytes = 1024 * 1024) {
9
11
  this.store = store;
12
+ this.hydrationBytes = hydrationBytes;
10
13
  }
11
14
  listEvents(input = {}) {
12
15
  const clauses = [];
@@ -25,8 +28,17 @@ export class ChatTimelineQueryService {
25
28
  }
26
29
  const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
27
30
  const limit = Math.max(1, Math.min(input.limit ?? 1000, 5000));
28
- const rows = this.store.db.prepare(`SELECT * FROM event_log ${where} ORDER BY stream_id ASC LIMIT ?`).all(...values, limit);
29
- return rows.map((row) => storedChatEventFromV2Row(row, this.store.payloads));
31
+ const rows = this.store.db.prepare(`SELECT * FROM event_log ${where} ORDER BY stream_id ASC LIMIT ?`).iterate(...values, limit);
32
+ const events = [];
33
+ let remainingBytes = 4 * 1024 * 1024 - 16;
34
+ for (const row of rows) {
35
+ const event = storedChatEventFromV2Row(row, this.store.payloads, this.hydrationBytes);
36
+ // Stop hydration as soon as the page exceeds IPC capacity. Callers can retry a
37
+ // smaller page without first allocating every large body in the requested range.
38
+ remainingBytes -= boundedMessageBytes(event, remainingBytes - 32) + 32;
39
+ events.push(event);
40
+ }
41
+ return events;
30
42
  }
31
43
  listSessionEvents(piboSessionId, limit = 1000) {
32
44
  return this.listTraceEvents({ piboSessionId, limit, includeLive: true });
@@ -39,7 +51,7 @@ export class ChatTimelineQueryService {
39
51
  }
40
52
  scanMessageTurnTimings(piboSessionId) {
41
53
  const rows = this.store.db.prepare(`
42
- SELECT * FROM event_log
54
+ SELECT * FROM event_log INDEXED BY idx_event_log_timing_sequence
43
55
  WHERE session_id = ?
44
56
  AND type IN ('message_queued', 'message_steered', 'message_started', 'message_finished', 'session_error', 'thinking_finished', 'assistant_message')
45
57
  ORDER BY session_sequence ASC, stream_id ASC
@@ -47,7 +59,7 @@ export class ChatTimelineQueryService {
47
59
  `).all(piboSessionId, TRACE_RECONCILIATION_TIMING_CAP + 1);
48
60
  if (rows.length > TRACE_RECONCILIATION_TIMING_CAP)
49
61
  return { timings: [], overflow: true };
50
- const events = rows.map((row) => storedPiboEventFromV2Row(row, this.store.payloads)).filter((event) => event !== undefined);
62
+ const events = rows.map((row) => storedPiboEventFromV2Row(row, this.store.payloads, this.hydrationBytes)).filter((event) => event !== undefined);
51
63
  return { timings: messageTurnTimingsFromEvents(events), overflow: false };
52
64
  }
53
65
  listTraceEvents(input) {
@@ -70,14 +82,14 @@ export class ChatTimelineQueryService {
70
82
  }
71
83
  const rows = this.store.db.prepare(`
72
84
  SELECT * FROM (
73
- SELECT * FROM event_log
85
+ SELECT * FROM event_log ${includeLive ? "" : "INDEXED BY idx_event_log_semantic_sequence"}
74
86
  WHERE ${clauses.join(" AND ")}
75
87
  ORDER BY session_sequence DESC, stream_id DESC
76
88
  LIMIT ?
77
89
  )
78
90
  ORDER BY session_sequence ASC, stream_id ASC
79
91
  `).all(...values, limit);
80
- return rows.map((row) => storedPiboEventFromV2Row(row, this.store.payloads)).filter((event) => event !== undefined);
92
+ return rows.map((row) => storedPiboEventFromV2Row(row, this.store.payloads, this.hydrationBytes)).filter((event) => event !== undefined);
81
93
  }
82
94
  isPayloadAttachedToTraceNode(input) {
83
95
  const nodeIdentity = parseTraceToolNodeIdentity(input.nodeId);
@@ -0,0 +1,134 @@
1
+ import { PiboSteeringUnavailableError } from "../../core/events.js";
2
+ import { randomUUID } from "node:crypto";
3
+ /** Owns bounded dispatches; durable claims, not this map, own accepted work. */
4
+ export class MessageCommandDispatcher {
5
+ storage;
6
+ context;
7
+ owner = `message-dispatch:${randomUUID()}`;
8
+ claims = new Map();
9
+ lastRenewed = new Map();
10
+ timer;
11
+ disposed = false;
12
+ pumping;
13
+ wakePending = false;
14
+ lastFailureReportAt = 0;
15
+ leaseMs = 30_000;
16
+ constructor(storage, context) {
17
+ this.storage = storage;
18
+ this.context = context;
19
+ this.wake();
20
+ }
21
+ wake() {
22
+ if (this.disposed)
23
+ return;
24
+ if (this.pumping) {
25
+ this.wakePending = true;
26
+ return;
27
+ }
28
+ if (this.timer)
29
+ clearTimeout(this.timer);
30
+ this.timer = undefined;
31
+ let nextPollMs = 50;
32
+ this.pumping = this.pump().catch(error => {
33
+ nextPollMs = 1000;
34
+ this.wakePending = false;
35
+ if (Date.now() - this.lastFailureReportAt >= 5000) {
36
+ this.lastFailureReportAt = Date.now();
37
+ console.warn("[pibo] durable message dispatcher unavailable", { code: error && typeof error === "object" && "code" in error ? String(error.code) : "dispatch_failed" });
38
+ }
39
+ }).finally(() => {
40
+ this.pumping = undefined;
41
+ if (!this.disposed) {
42
+ if (this.wakePending) {
43
+ this.wakePending = false;
44
+ queueMicrotask(() => this.wake());
45
+ }
46
+ else {
47
+ this.timer = setTimeout(() => this.wake(), nextPollMs);
48
+ this.timer.unref?.();
49
+ }
50
+ }
51
+ });
52
+ }
53
+ outputPersisted(event) {
54
+ if (event.type !== "message_finished" && event.type !== "session_error" && event.type !== "message_steered")
55
+ return;
56
+ if (!event.eventId)
57
+ return;
58
+ for (const [id, claim] of this.claims)
59
+ if (claim.sessionId === event.piboSessionId && claim.eventId === event.eventId)
60
+ this.forget(id);
61
+ this.wake();
62
+ }
63
+ forget(id) { this.claims.delete(id); this.lastRenewed.delete(id); }
64
+ async pump() {
65
+ for (const [id, claim] of this.claims) {
66
+ if (this.disposed)
67
+ return;
68
+ if (Date.now() - (this.lastRenewed.get(id) ?? 0) < this.leaseMs / 3)
69
+ continue;
70
+ if (!await this.storage.heartbeatCommand(id, this.owner, claim.token, this.leaseMs))
71
+ this.forget(id);
72
+ else
73
+ this.lastRenewed.set(id, Date.now());
74
+ }
75
+ while (!this.disposed && this.claims.size < 12) {
76
+ const claim = await this.storage.claimCommand(this.owner, this.leaseMs);
77
+ if (!claim)
78
+ break;
79
+ this.claims.set(claim.id, claim);
80
+ this.lastRenewed.set(claim.id, Date.now());
81
+ // A slow cold runtime must not serialize unrelated session admission or dispatch.
82
+ void this.dispatch(claim);
83
+ }
84
+ }
85
+ async dispatch(claim) {
86
+ try {
87
+ if (this.disposed)
88
+ return;
89
+ if (!this.context.getSession(claim.sessionId)) {
90
+ await this.storage.transitionCommand(claim.id, this.owner, claim.token, "failed", "Target session no longer exists.");
91
+ this.forget(claim.id);
92
+ this.wake();
93
+ return;
94
+ }
95
+ if (!await this.storage.transitionCommand(claim.id, this.owner, claim.token, "initializing")) {
96
+ this.forget(claim.id);
97
+ this.wake();
98
+ return;
99
+ }
100
+ if (this.disposed)
101
+ return;
102
+ const output = await this.context.emit({ type: "message", piboSessionId: claim.sessionId, id: claim.eventId, text: claim.text, delivery: claim.delivery, source: "user" });
103
+ if (output.type === "session_error") {
104
+ await this.storage.transitionCommand(claim.id, this.owner, claim.token, "failed", "Runtime rejected the accepted message.");
105
+ this.forget(claim.id);
106
+ this.wake();
107
+ }
108
+ // Output ingest advances queued/running/terminal state. No late emit result may downgrade it.
109
+ }
110
+ catch (error) {
111
+ if (!this.disposed) {
112
+ const cancelled = Boolean(error && typeof error === "object" && "code" in error && error.code === "runtime_start_cancelled");
113
+ const steering = error instanceof PiboSteeringUnavailableError;
114
+ const capacity = Boolean(error && typeof error === "object" && "code" in error && error.code === "runtime_capacity_unavailable");
115
+ try {
116
+ await this.storage.transitionCommand(claim.id, this.owner, claim.token, cancelled || steering || capacity ? "failed" : "interrupted", cancelled ? "Message cancelled before runtime dispatch." : capacity ? "Runtime capacity was unavailable before dispatch; the message did not run." : steering ? "Steering is unavailable; the message was not queued as a normal turn." : "Runtime dispatch outcome is unclear; inspect the session before retrying.");
117
+ }
118
+ catch { /* Lease expiry retains the uncertain outcome. */ }
119
+ this.forget(claim.id);
120
+ this.wake();
121
+ }
122
+ }
123
+ }
124
+ async dispose() {
125
+ this.disposed = true;
126
+ if (this.timer)
127
+ clearTimeout(this.timer);
128
+ this.timer = undefined;
129
+ await this.pumping;
130
+ this.claims.clear();
131
+ this.lastRenewed.clear();
132
+ // Do not release dispatched claims for replay: the router may still own side effects.
133
+ }
134
+ }
@@ -1,6 +1,9 @@
1
1
  import { assistantOutputKey, isLiveOnlyOutputEvent, thinkingOutputKey, toolOutputKey } from "./output-event-policy.js";
2
2
  const MAX_TRACKED_COMPACTOR_SESSIONS = 1_024;
3
3
  export class OutputCompactor {
4
+ liveVersion = 0;
5
+ liveVersions = new Map();
6
+ versionForSession(id) { return this.liveVersions.get(id) ?? this.liveVersion; }
4
7
  assistantBuffers = new Map();
5
8
  thinkingBuffers = new Map();
6
9
  toolSnapshots = new Map();
@@ -100,6 +103,10 @@ export class OutputCompactor {
100
103
  settled = true;
101
104
  for (const mutate of mutations)
102
105
  mutate();
106
+ this.liveVersions.delete(event.piboSessionId);
107
+ this.liveVersions.set(event.piboSessionId, ++this.liveVersion);
108
+ while (this.liveVersions.size > MAX_TRACKED_COMPACTOR_SESSIONS)
109
+ this.liveVersions.delete(this.liveVersions.keys().next().value);
103
110
  this.touchSession(event.piboSessionId);
104
111
  };
105
112
  return {
@@ -148,8 +155,10 @@ export class OutputCompactor {
148
155
  }
149
156
  this.sessionBufferCounts.delete(piboSessionId);
150
157
  this.sessionRecency.delete(piboSessionId);
158
+ this.liveVersions.delete(piboSessionId);
151
159
  }
152
160
  disposeAll() {
161
+ this.liveVersions.clear();
153
162
  this.assistantBuffers.clear();
154
163
  this.thinkingBuffers.clear();
155
164
  this.toolSnapshots.clear();
@@ -84,7 +84,8 @@ export function isPiboOutputEvent(value) {
84
84
  return typeof value.reason === "string"
85
85
  && typeof value.aborted === "boolean"
86
86
  && isOptionalNonNegativeSafeInteger(value.compactionIndex)
87
- && isOptionalString(value.errorMessage);
87
+ && isOptionalString(value.errorMessage)
88
+ && (value.compactionStats === undefined || isCompactionStats(value.compactionStats));
88
89
  case "approval_requested":
89
90
  return isApprovalRequest(value.request);
90
91
  case "approval_resolved":
@@ -117,6 +118,13 @@ function isOptionalFiniteNumber(value) {
117
118
  function isOptionalString(value) {
118
119
  return value === undefined || typeof value === "string";
119
120
  }
121
+ function isCompactionStats(value) {
122
+ if (!isRecord(value) || !isNonNegativeSafeInteger(value.toolCallCount))
123
+ return false;
124
+ if (!isOptionalFiniteNumber(value.maxToolOutputTokens) || !isOptionalFiniteNumber(value.compactionTokens))
125
+ return false;
126
+ return value.maxToolOutputTokenBasis === undefined || typeof value.maxToolOutputTokenBasis === "string";
127
+ }
120
128
  function isOptionalNonNegativeSafeInteger(value) {
121
129
  return value === undefined || isNonNegativeSafeInteger(value);
122
130
  }
@@ -1,8 +1,26 @@
1
+ class BoundedStreamIdentitySet extends Set {
2
+ bytes = 0;
3
+ add(key) {
4
+ if (this.has(key))
5
+ return this;
6
+ const bytes = key.length * 2;
7
+ if (bytes > 256 * 1024)
8
+ return this;
9
+ super.add(key);
10
+ this.bytes += bytes;
11
+ while (this.size > 2048 || this.bytes > 256 * 1024)
12
+ this.delete(this.values().next().value);
13
+ return this;
14
+ }
15
+ delete(key) { if (!super.delete(key))
16
+ return false; this.bytes -= key.length * 2; return true; }
17
+ clear() { super.clear(); this.bytes = 0; }
18
+ }
1
19
  export function createChatStreamState() {
2
20
  return {
3
- textMessageIds: new Set(),
4
- reasoningMessageIds: new Set(),
5
- toolCallIds: new Set(),
21
+ textMessageIds: new BoundedStreamIdentitySet(),
22
+ reasoningMessageIds: new BoundedStreamIdentitySet(),
23
+ toolCallIds: new BoundedStreamIdentitySet(),
6
24
  transientFrameIndex: 0,
7
25
  };
8
26
  }
@@ -1,3 +1,5 @@
1
+ import { AsyncTelemetryMaintenance } from "../../data/async-telemetry-maintenance.js";
2
+ import { TelemetryMaintenance } from "../../data/telemetry-maintenance.js";
1
3
  import { telemetryRetentionCutoff, } from "../../core/telemetry-retention-settings.js";
2
4
  export const TELEMETRY_RETENTION_CLASSES = [
3
5
  "live",
@@ -7,14 +9,33 @@ export const TELEMETRY_RETENTION_CLASSES = [
7
9
  "incident",
8
10
  ];
9
11
  export const DEFAULT_TELEMETRY_RETENTION_MAINTENANCE_INTERVAL_MS = 24 * 60 * 60 * 1000;
12
+ /** Bounded recovery: back off a failing maintenance job, then leave it visible instead of respawning. */
13
+ export const TELEMETRY_RETENTION_MAINTENANCE_FAILURE_LIMIT = 8;
14
+ export const TELEMETRY_RETENTION_MAINTENANCE_MAX_RETRY_MS = 30_000;
15
+ export async function disposeTelemetryRetentionMaintenance(state) {
16
+ state.disposed = true;
17
+ if (state.timer)
18
+ clearTimeout(state.timer);
19
+ state.timer = undefined;
20
+ state.running = false;
21
+ await state.worker?.close();
22
+ state.worker = undefined;
23
+ }
10
24
  export function pruneTelemetryOlderThan(input) {
11
25
  const cutoff = telemetryRetentionCutoff(input.days, input.now);
12
- const results = TELEMETRY_RETENTION_CLASSES.map((retentionClass) => input.dataStore.telemetry.prune({ retentionClass, before: cutoff, apply: input.apply }));
26
+ const results = [];
27
+ for (const retentionClass of TELEMETRY_RETENTION_CLASSES) {
28
+ const result = input.dataStore.telemetry.prune({ retentionClass, before: cutoff, apply: input.apply });
29
+ results.push(result);
30
+ if (result.completed === false)
31
+ break;
32
+ }
13
33
  return {
14
34
  cutoff,
15
35
  days: input.days,
16
36
  applied: input.apply === true,
17
37
  results,
38
+ completed: results.every(result => result.completed !== false),
18
39
  rowsDeleted: results.reduce((sum, result) => sum + result.rowsDeleted, 0),
19
40
  bytesMatched: results.reduce((sum, result) => sum + result.bytesMatched, 0),
20
41
  };
@@ -27,6 +48,8 @@ export function isTelemetryRetentionMaintenanceDue(input) {
27
48
  return !(input.state.lastCheckedAt && now.getTime() - input.state.lastCheckedAt < intervalMs);
28
49
  }
29
50
  export function maybeRunTelemetryRetentionMaintenance(input) {
51
+ if (input.state.disposed)
52
+ return;
30
53
  if (!input.settings.enabled)
31
54
  return;
32
55
  if (input.state.running)
@@ -39,19 +62,70 @@ export function maybeRunTelemetryRetentionMaintenance(input) {
39
62
  input.state.lastCheckedAt = now.getTime();
40
63
  return;
41
64
  }
42
- if (hasActiveRuntimeWork(input.context))
43
- return;
44
65
  input.state.lastCheckedAt = now.getTime();
45
66
  input.state.running = true;
46
- setTimeout(() => {
67
+ const cutoff = telemetryRetentionCutoff(input.settings.days, now);
68
+ const local = input.dataStore.path === ":memory:" ? new TelemetryMaintenance(input.dataStore.db) : undefined;
69
+ let consecutiveFailures = 0;
70
+ const run = async () => {
71
+ let failed = false;
47
72
  try {
48
- pruneTelemetryOlderThan({ dataStore: input.dataStore, days: input.settings.days, now, apply: true });
49
- input.onPruned?.(now.toISOString());
73
+ if (input.state.disposed)
74
+ return;
75
+ if (local) {
76
+ if (!local.status() || local.status()?.status === "completed" || local.status()?.status === "cancelled")
77
+ local.start(cutoff);
78
+ local.step();
79
+ }
80
+ else if (!input.state.worker) {
81
+ input.state.worker = new AsyncTelemetryMaintenance(input.dataStore.path);
82
+ await input.state.worker.command("start", cutoff);
83
+ }
84
+ const status = local?.status() ?? await input.state.worker.command("status");
85
+ if (input.state.disposed)
86
+ return;
87
+ if (status?.status === "completed") {
88
+ input.onPruned?.(now.toISOString());
89
+ input.state.running = false;
90
+ await input.state.worker?.close();
91
+ input.state.worker = undefined;
92
+ return;
93
+ }
94
+ if (status?.status === "cancelled" || status?.status === "paused") {
95
+ input.state.running = false;
96
+ await input.state.worker?.close();
97
+ input.state.worker = undefined;
98
+ return;
99
+ }
100
+ }
101
+ catch {
102
+ failed = true;
103
+ input.state.failures = (input.state.failures ?? 0) + 1;
104
+ await input.state.worker?.close();
105
+ input.state.worker = undefined;
106
+ }
107
+ if (input.state.disposed)
108
+ return;
109
+ if (!failed) {
110
+ consecutiveFailures = 0;
111
+ input.state.timer = setTimeout(() => { input.state.timer = undefined; void run(); }, local ? 25 : 250);
112
+ input.state.timer.unref?.();
113
+ return;
50
114
  }
51
- finally {
115
+ // A persistent failure must not respawn a worker thread every 250 ms forever. Back off, then stop
116
+ // with the failure count visible so a later due check retries instead of burning one worker per attempt.
117
+ consecutiveFailures++;
118
+ if (consecutiveFailures >= TELEMETRY_RETENTION_MAINTENANCE_FAILURE_LIMIT) {
52
119
  input.state.running = false;
120
+ input.state.timer = undefined;
121
+ return;
53
122
  }
54
- }, 0).unref?.();
123
+ const backoffMs = Math.min(TELEMETRY_RETENTION_MAINTENANCE_MAX_RETRY_MS, 250 * 2 ** (consecutiveFailures - 1));
124
+ input.state.timer = setTimeout(() => { input.state.timer = undefined; void run(); }, backoffMs);
125
+ input.state.timer.unref?.();
126
+ };
127
+ input.state.timer = setTimeout(() => { input.state.timer = undefined; void run(); }, 0);
128
+ input.state.timer.unref?.();
55
129
  }
56
130
  export function isPersistentRetentionDue(lastPrunedAt, now, intervalMs) {
57
131
  if (!lastPrunedAt)
@@ -67,3 +141,34 @@ export function hasActiveRuntimeWork(context) {
67
141
  ?? [];
68
142
  return statuses.some((status) => Boolean(status?.processing || status?.streaming || (status?.queuedMessages ?? 0) > 0));
69
143
  }
144
+ /** Explicit HTTP maintenance keeps SQL off the gateway and yields between every bounded batch. */
145
+ export async function pruneTelemetryOlderThanAsync(input) {
146
+ if (input.dataStore.path === ":memory:")
147
+ return pruneTelemetryOlderThan(input);
148
+ const cutoff = telemetryRetentionCutoff(input.days), worker = new AsyncTelemetryMaintenance(input.dataStore.path);
149
+ const abort = () => { void worker.close(); };
150
+ input.signal?.addEventListener("abort", abort, { once: true });
151
+ try {
152
+ if (input.signal?.aborted)
153
+ throw Error("Maintenance request cancelled");
154
+ if (!input.apply) {
155
+ const results = await worker.preview(cutoff);
156
+ return { cutoff, days: input.days, applied: false, results, rowsDeleted: 0, bytesMatched: results.reduce((n, r) => n + r.bytesMatched, 0), completed: true };
157
+ }
158
+ let status = await worker.command("start", cutoff);
159
+ if (status?.status === "paused")
160
+ status = await worker.command("resume");
161
+ const until = Date.now() + 30000;
162
+ while (status?.status === "running" && Date.now() < until) {
163
+ await new Promise(resolve => setTimeout(resolve, 100));
164
+ status = await worker.command("status");
165
+ }
166
+ if (status?.status === "running")
167
+ status = await worker.command("pause");
168
+ return { cutoff: status?.cutoff ?? cutoff, days: input.days, applied: true, results: [], rowsDeleted: status?.deleted ?? 0, bytesMatched: 0, completed: status?.status === "completed" };
169
+ }
170
+ finally {
171
+ input.signal?.removeEventListener("abort", abort);
172
+ await worker.close();
173
+ }
174
+ }
@@ -0,0 +1,46 @@
1
+ /** Exact encoded responses; bounded independently from reconstructed trace caches. */
2
+ export class TraceResponseCache {
3
+ maximumBytes;
4
+ maximumEntries;
5
+ entries = new Map();
6
+ bytes = 0;
7
+ constructor(maximumBytes = 8 * 1024 * 1024, maximumEntries = 32) {
8
+ this.maximumBytes = maximumBytes;
9
+ this.maximumEntries = maximumEntries;
10
+ }
11
+ get(key, request) {
12
+ const entry = this.entries.get(key);
13
+ if (!entry)
14
+ return;
15
+ this.entries.delete(key);
16
+ this.entries.set(key, entry);
17
+ const headers = new Headers(entry.headers);
18
+ headers.set("server-timing", 'trace_cache;desc="watermark-hit"');
19
+ const etag = headers.get("etag");
20
+ const requested = request.headers.get("if-none-match");
21
+ if (etag && requested?.split(',').some(v => v.trim() === '*' || v.trim() === etag || v.trim() === 'W/' + etag))
22
+ return new Response(null, { status: 304, headers });
23
+ return new Response(entry.body.slice(), { status: entry.status, headers });
24
+ }
25
+ async set(key, response) {
26
+ if (response.status !== 200 || !response.headers.get("content-type")?.includes("json"))
27
+ return;
28
+ const body = new Uint8Array(await response.clone().arrayBuffer());
29
+ const bytes = body.byteLength + Buffer.byteLength(key);
30
+ if (bytes > Math.min(this.maximumBytes, 1024 * 1024))
31
+ return;
32
+ const previous = this.entries.get(key);
33
+ if (previous)
34
+ this.bytes -= previous.bytes;
35
+ this.entries.delete(key);
36
+ this.entries.set(key, { body, headers: [...response.headers.entries()], status: response.status, bytes });
37
+ this.bytes += bytes;
38
+ while (this.bytes > this.maximumBytes || this.entries.size > this.maximumEntries) {
39
+ const oldest = this.entries.keys().next().value;
40
+ this.bytes -= this.entries.get(oldest).bytes;
41
+ this.entries.delete(oldest);
42
+ }
43
+ }
44
+ clear() { this.entries.clear(); this.bytes = 0; }
45
+ status() { return { entries: this.entries.size, bytes: this.bytes, maximumBytes: this.maximumBytes, maximumEntries: this.maximumEntries }; }
46
+ }
@@ -148,17 +148,21 @@ export function looksLikeImagePayloadPreview(value) {
148
148
  return /"type"\s*:\s*"(?:image|input_image|output_image|image_url)"/i.test(value)
149
149
  || /"mime(?:Type|_type)"\s*:\s*"image\//i.test(value);
150
150
  }
151
- export function readTracePayloadChunk(input) {
151
+ export async function readTracePayloadChunk(input) {
152
152
  const parsed = parseTracePayloadRef(input.ref);
153
153
  if (!parsed)
154
154
  return undefined;
155
155
  const payload = input.payloadStore.getPayload(parsed.payloadId);
156
156
  if (!payload)
157
157
  return undefined;
158
- const bytes = Buffer.from(input.payloadStore.readPayloadBytes(parsed.payloadId));
159
- const offset = Math.max(0, Math.min(input.offset, bytes.byteLength));
158
+ const offset = Math.max(0, Math.min(input.offset, payload.byteSize));
160
159
  const limit = Math.max(1, Math.min(input.limit, TRACE_V2_PAYLOAD_MAX_LIMIT_BYTES));
161
- const chunk = bytes.subarray(offset, Math.min(bytes.byteLength, offset + limit));
160
+ const bytes = await input.payloadStore.readPayloadRange(parsed.payloadId, offset, limit + 3);
161
+ let length = Math.min(limit, bytes.length);
162
+ // Include a complete UTF-8 code point at the boundary; cursors remain byte offsets.
163
+ while (length < bytes.length && (bytes[length] & 0xc0) === 0x80)
164
+ length++;
165
+ const chunk = bytes.subarray(0, length);
162
166
  const preview = payload.previewText ?? "";
163
167
  const traceRef = {
164
168
  ref: input.ref,
@@ -168,7 +172,7 @@ export function readTracePayloadChunk(input) {
168
172
  truncatedPreview: Buffer.byteLength(preview, "utf8") < payload.byteSize,
169
173
  hash: payload.sha256,
170
174
  };
171
- const nextOffset = offset + chunk.byteLength < bytes.byteLength ? offset + chunk.byteLength : undefined;
175
+ const nextOffset = offset + chunk.byteLength < payload.byteSize ? offset + chunk.byteLength : undefined;
172
176
  return {
173
177
  ref: traceRef,
174
178
  offset,
@@ -248,6 +252,8 @@ function compactTraceNode(node, payloadStore, piboSessionId, depth) {
248
252
  completedAt: node.completedAt,
249
253
  durationMs: node.durationMs,
250
254
  toolMetrics: node.toolMetrics,
255
+ modelInferences: node.modelInferences,
256
+ compactionStats: node.compactionStats,
251
257
  orderKey: node.orderKey,
252
258
  depth,
253
259
  hasChildren: node.children.length > 0,
@@ -268,7 +274,7 @@ function compactTraceNode(node, payloadStore, piboSessionId, depth) {
268
274
  });
269
275
  }
270
276
  function inlinePayloadForNodeValue(node, kind, value) {
271
- return inlinePayloadForValue(value, inlinePayloadByteLimit(node, kind));
277
+ return node.payloadRefs?.[kind] ? undefined : inlinePayloadForValue(value, inlinePayloadByteLimit(node, kind));
272
278
  }
273
279
  function inlinePayloadByteLimit(node, kind) {
274
280
  if ((node.type === "user.message" || node.type === "assistant.message" || node.type === "model.reasoning") && kind !== "error") {
@@ -5,7 +5,7 @@ import { isBuiltInHistoryReconciliationProof } from "../../agent-runtimes/histor
5
5
  import { buildTraceViewFromEvents, traceNodesFromHistoryEntries } from "../../shared/trace-engine.js";
6
6
  import { chatWebSessionSidebarOrder, isChatWebSessionArchived, isChatWebSessionPinned } from "./session-metadata.js";
7
7
  import { workflowSessionKindFromMetadata } from "../../sessions/workflow-session-kind.js";
8
- export const TRACE_PROJECTION_VERSION = "runtime-history-v2-incomplete-status";
8
+ export const TRACE_PROJECTION_VERSION = "runtime-history-v2-debug-metrics";
9
9
  export { compareTraceNodes, sortTraceNodes, nestTraceNodes, flattenTraceNodes, mapTraceNodesById, buildTraceViewFromEvents, traceNodesFromHistoryEntries, } from "../../shared/trace-engine.js";
10
10
  export const TRACE_TRANSCRIPT_TAIL_MAX_BYTES = PI_HISTORY_TAIL_MAX_BYTES;
11
11
  export const TRACE_TRANSCRIPT_HISTORY_PAGE_MAX_BYTES = PI_HISTORY_PAGE_MAX_BYTES;