@pasko70/pibo 3.4.3 → 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 (84) hide show
  1. package/dist/agent-runtime/routed-session.js +39 -12
  2. package/dist/agent-runtimes/codex-native/adapter.js +14 -1
  3. package/dist/agent-runtimes/codex-native/models.js +4 -4
  4. package/dist/agent-runtimes/codex-native/process.js +32 -13
  5. package/dist/agent-runtimes/codex-native/provider-usage.js +115 -0
  6. package/dist/agent-runtimes/pi/adapter.js +1 -0
  7. package/dist/agent-runtimes/pi/routed-session.js +4 -2
  8. package/dist/apps/chat/bounded-event-stream.js +98 -0
  9. package/dist/apps/chat/chat-settings-routes.js +3 -3
  10. package/dist/apps/chat/data/chat-data-mappers.js +32 -13
  11. package/dist/apps/chat/data/event-command-service.js +30 -21
  12. package/dist/apps/chat/data/history-query-service.js +34 -25
  13. package/dist/apps/chat/data/read-state-service.js +13 -0
  14. package/dist/apps/chat/data/session-query-service.js +19 -11
  15. package/dist/apps/chat/data/timeline-query-service.js +19 -7
  16. package/dist/apps/chat/message-command-dispatcher.js +134 -0
  17. package/dist/apps/chat/output-compactor.js +9 -0
  18. package/dist/apps/chat/output-event-policy.js +9 -1
  19. package/dist/apps/chat/stream.js +21 -3
  20. package/dist/apps/chat/telemetry-retention-service.js +113 -8
  21. package/dist/apps/chat/trace-response-cache.js +46 -0
  22. package/dist/apps/chat/trace-v2.js +12 -6
  23. package/dist/apps/chat/trace.js +1 -1
  24. package/dist/apps/chat/web-app.js +608 -281
  25. package/dist/apps/chat-ui/assets/{dist-DeMKnZR8.js → dist-BG0n7zLd.js} +1 -1
  26. package/dist/apps/chat-ui/assets/{dist-C8GzUMPk.js → dist-D6TjFhAm.js} +1 -1
  27. package/dist/apps/chat-ui/assets/{dist-BkUu8WPA.js → dist-D79vyxSX.js} +1 -1
  28. package/dist/apps/chat-ui/assets/{dist-B4YhOxh0.js → dist-DFZ8cwh0.js} +1 -1
  29. package/dist/apps/chat-ui/assets/{dist-Cyb4rVa6.js → dist-cOjokPrK.js} +1 -1
  30. package/dist/apps/chat-ui/assets/index-RMHUTJ62.js +229 -0
  31. package/dist/apps/chat-ui/assets/{index-BOceJ0jM.css → index-hEkrlRk-.css} +1 -1
  32. package/dist/apps/chat-ui/index.html +2 -2
  33. package/dist/apps/chat-vscode-web/assets/index-xacbCyTx.js +44 -0
  34. package/dist/apps/chat-vscode-web/index.html +1 -1
  35. package/dist/compute/pool/seeds.js +25 -3
  36. package/dist/core/events.js +4 -0
  37. package/dist/core/output-render-sequence.js +2 -0
  38. package/dist/core/provider-capacity.js +33 -0
  39. package/dist/core/provider-telemetry.js +21 -6
  40. package/dist/core/runtime-capacity.js +174 -0
  41. package/dist/core/runtime-telemetry.js +40 -12
  42. package/dist/core/session-router.js +79 -2
  43. package/dist/data/async-chat-reads.js +38 -0
  44. package/dist/data/async-chat-storage.js +91 -0
  45. package/dist/data/async-telemetry-maintenance.js +9 -0
  46. package/dist/data/bounded-worker-client.js +256 -0
  47. package/dist/data/chat-read-projections.js +159 -0
  48. package/dist/data/chat-read-worker.js +73 -0
  49. package/dist/data/chat-storage-worker.js +146 -0
  50. package/dist/data/ingest-service.js +79 -14
  51. package/dist/data/message-command-store.js +148 -0
  52. package/dist/data/payload-store.js +92 -11
  53. package/dist/data/pibo-store.js +10 -8
  54. package/dist/data/schema.js +16 -2
  55. package/dist/data/session-store.js +3 -1
  56. package/dist/data/storage-backup.js +278 -0
  57. package/dist/data/telemetry-capture.js +188 -0
  58. package/dist/data/telemetry-command.js +3 -0
  59. package/dist/data/telemetry-maintenance-worker.js +40 -0
  60. package/dist/data/telemetry-maintenance.js +110 -0
  61. package/dist/data/telemetry-retention.js +16 -7
  62. package/dist/data/telemetry-worker.js +111 -0
  63. package/dist/data/telemetry-writer.js +150 -83
  64. package/dist/data/telemetry.js +5 -0
  65. package/dist/debug/index.js +52 -0
  66. package/dist/debug/storage-backup.js +33 -0
  67. package/dist/debug/telemetry-capture.js +66 -0
  68. package/dist/gateway/cli.js +104 -16
  69. package/dist/gateway/server.js +2 -0
  70. package/dist/providers/openai-gpt56.js +11 -6
  71. package/dist/session-ui/terminalRows.js +55 -13
  72. package/dist/sessions/pibo-data-store.js +30 -10
  73. package/dist/shared/debug-features.js +4 -0
  74. package/dist/shared/model-inference-metrics.js +23 -0
  75. package/dist/shared/trace-event-projection.js +59 -2
  76. package/dist/shared/trace-history.js +9 -0
  77. package/dist/shared/trace-live-reducer.js +1 -0
  78. package/dist/shared/trace-patch-nodes.js +19 -0
  79. package/dist/web/channel.js +8 -2
  80. package/dist/web/http.js +36 -3
  81. package/npm-shrinkwrap.json +2 -2
  82. package/package.json +1 -1
  83. package/dist/apps/chat-ui/assets/index-Dk4mbXAB.js +0 -228
  84. package/dist/apps/chat-vscode-web/assets/index-0oTGFHni.js +0 -43
@@ -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
@@ -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
  }