@pasko70/pibo 3.4.4 → 3.5.1

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 (91) hide show
  1. package/dist/agent-runtime/routed-session.js +111 -25
  2. package/dist/agent-runtimes/codex-native/turn.js +27 -3
  3. package/dist/agent-runtimes/pi/adapter.js +1 -0
  4. package/dist/agent-runtimes/pi/routed-session.js +2 -2
  5. package/dist/apps/chat/bounded-event-stream.js +98 -0
  6. package/dist/apps/chat/chat-settings-routes.js +3 -3
  7. package/dist/apps/chat/data/chat-data-mappers.js +40 -14
  8. package/dist/apps/chat/data/event-command-service.js +30 -21
  9. package/dist/apps/chat/data/history-query-service.js +34 -25
  10. package/dist/apps/chat/data/read-state-service.js +37 -3
  11. package/dist/apps/chat/data/session-query-service.js +19 -11
  12. package/dist/apps/chat/data/timeline-query-service.js +19 -7
  13. package/dist/apps/chat/message-command-dispatcher.js +149 -0
  14. package/dist/apps/chat/output-compactor.js +9 -0
  15. package/dist/apps/chat/output-event-policy.js +9 -1
  16. package/dist/apps/chat/stream.js +21 -3
  17. package/dist/apps/chat/telemetry-retention-service.js +113 -8
  18. package/dist/apps/chat/trace-response-cache.js +46 -0
  19. package/dist/apps/chat/trace-v2.js +12 -6
  20. package/dist/apps/chat/trace.js +1 -1
  21. package/dist/apps/chat/web-app.js +645 -297
  22. package/dist/apps/chat-ui/assets/{dist-V06sfuZa.js → dist-B-auLrzD.js} +1 -1
  23. package/dist/apps/chat-ui/assets/{dist-CH3SvpYV.js → dist-BA_dsINH.js} +1 -1
  24. package/dist/apps/chat-ui/assets/{dist-23lt_7qm.js → dist-eJZar_0-.js} +1 -1
  25. package/dist/apps/chat-ui/assets/{dist-Bf2KScPo.js → dist-wNNR2Bci.js} +1 -1
  26. package/dist/apps/chat-ui/assets/{dist-DsgL8w-W.js → dist-zcmEsIEp.js} +1 -1
  27. package/dist/apps/chat-ui/assets/index-DEkbN5Vo.js +229 -0
  28. package/dist/apps/chat-ui/assets/index-DZK6Tzil.css +1 -0
  29. package/dist/apps/chat-ui/index.html +2 -2
  30. package/dist/apps/chat-vscode-web/assets/index-DZgW1fCB.js +44 -0
  31. package/dist/apps/chat-vscode-web/index.html +1 -1
  32. package/dist/cli-session/localSessionSource.js +3 -2
  33. package/dist/compute/pool/seeds.js +25 -3
  34. package/dist/core/events.js +4 -0
  35. package/dist/core/output-render-sequence.js +65 -6
  36. package/dist/core/provider-capacity.js +33 -0
  37. package/dist/core/provider-telemetry.js +21 -6
  38. package/dist/core/runtime-capacity.js +174 -0
  39. package/dist/core/runtime-telemetry.js +40 -12
  40. package/dist/core/session-router.js +178 -13
  41. package/dist/data/async-chat-reads.js +38 -0
  42. package/dist/data/async-chat-storage.js +96 -0
  43. package/dist/data/async-telemetry-maintenance.js +9 -0
  44. package/dist/data/bounded-worker-client.js +256 -0
  45. package/dist/data/chat-read-projections.js +159 -0
  46. package/dist/data/chat-read-worker.js +73 -0
  47. package/dist/data/chat-storage-worker.js +167 -0
  48. package/dist/data/ingest-service.js +152 -18
  49. package/dist/data/message-command-store.js +290 -0
  50. package/dist/data/payload-store.js +92 -11
  51. package/dist/data/pibo-store.js +10 -8
  52. package/dist/data/schema.js +39 -4
  53. package/dist/data/session-store.js +3 -1
  54. package/dist/data/storage-backup.js +278 -0
  55. package/dist/data/storage-maintenance.js +344 -0
  56. package/dist/data/storage-verification-worker.js +25 -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 +207 -1
  66. package/dist/debug/message-queue.js +108 -0
  67. package/dist/debug/output-collision-repair.js +140 -0
  68. package/dist/debug/output-integrity.js +38 -2
  69. package/dist/debug/output-repair.js +1 -0
  70. package/dist/debug/storage-backup.js +41 -0
  71. package/dist/debug/storage-maintenance.js +78 -0
  72. package/dist/debug/telemetry-capture.js +66 -0
  73. package/dist/gateway/cli.js +71 -7
  74. package/dist/gateway/server.js +3 -0
  75. package/dist/providers/openai-gpt56.js +11 -6
  76. package/dist/reliability/store.js +119 -23
  77. package/dist/session-ui/terminalRows.js +54 -13
  78. package/dist/sessions/pibo-data-store.js +19 -14
  79. package/dist/shared/debug-features.js +4 -0
  80. package/dist/shared/model-inference-metrics.js +23 -0
  81. package/dist/shared/trace-event-projection.js +69 -2
  82. package/dist/shared/trace-history.js +9 -0
  83. package/dist/shared/trace-live-reducer.js +1 -0
  84. package/dist/shared/trace-patch-nodes.js +19 -0
  85. package/dist/web/channel.js +117 -12
  86. package/dist/web/http.js +135 -41
  87. package/npm-shrinkwrap.json +2 -2
  88. package/package.json +1 -1
  89. package/dist/apps/chat-ui/assets/index-BOceJ0jM.css +0 -1
  90. package/dist/apps/chat-ui/assets/index-BOemYq-V.js +0 -228
  91. package/dist/apps/chat-vscode-web/assets/index-CMwTB8o8.js +0 -43
@@ -1,5 +1,5 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
- import { outputIdentityFingerprint, outputPartFingerprint } from "../core/output-render-sequence.js";
2
+ import { legacyOutputIdentityFingerprintCandidates, OUTPUT_IDENTITY_FINGERPRINT_VERSION, outputIdentityFieldDigests, outputIdentityFingerprint, outputPartFingerprint } from "../core/output-render-sequence.js";
3
3
  import { rootSessionId } from "./session-store.js";
4
4
  export class PiboOutputIdentityCollisionError extends Error {
5
5
  idempotencyKey;
@@ -17,6 +17,8 @@ export class PiboOutputIdentityCollisionError extends Error {
17
17
  export function outputPersistenceErrorIsRetryable(error) {
18
18
  if (error instanceof PiboOutputIdentityCollisionError)
19
19
  return false;
20
+ if (error && typeof error === "object" && "code" in error && error.code === "pibo_output_identity_collision")
21
+ return false;
20
22
  if (error instanceof AggregateError) {
21
23
  return error.errors.length === 0 || error.errors.some(outputPersistenceErrorIsRetryable);
22
24
  }
@@ -24,6 +26,7 @@ export function outputPersistenceErrorIsRetryable(error) {
24
26
  }
25
27
  const INLINE_MESSAGE_PAYLOAD_THRESHOLD_BYTES = 16 * 1024;
26
28
  const INLINE_JSON_PAYLOAD_THRESHOLD_BYTES = 16 * 1024;
29
+ const MAX_FINGERPRINT_FIELD_DIFFERENCES = 24;
27
30
  export class ChatDataIngestService {
28
31
  store;
29
32
  constructor(store) {
@@ -43,10 +46,11 @@ export class ChatDataIngestService {
43
46
  duplicate: true,
44
47
  };
45
48
  }
49
+ const now = input.legacyEvent?.createdAt ?? new Date().toISOString();
50
+ const preparedPayload = input.preparedPayload ?? this.prepareUserMessagePayload(input.text, now);
46
51
  return this.store.transaction(() => {
47
- const now = input.legacyEvent?.createdAt ?? new Date().toISOString();
48
- this.store.sessions.upsertSession({ session: input.session, roomId: input.roomId, firstMessagePreview: input.text, lastActivityAt: now });
49
- const payloadRef = this.writeTextPayloadIfLarge(input.text, now, "chat_message");
52
+ this.store.sessions.upsertSession({ session: input.session, roomId: input.roomId, firstMessagePreview: input.text, lastActivityAt: now, preserveRuntimeBinding: true });
53
+ const payloadRef = preparedPayload ? this.store.payloads.commitPreparedPayload(preparedPayload).id : undefined;
50
54
  const event = this.store.eventLog.appendEvent({
51
55
  sessionId: input.session.id,
52
56
  sessionSequence: this.nextEventSequence(input.session.id),
@@ -79,6 +83,7 @@ export class ChatDataIngestService {
79
83
  roomId: input.roomId,
80
84
  sequence: this.nextMessageSequence(input.session.id),
81
85
  role: "user",
86
+ turnId: input.eventId,
82
87
  actorId: input.actorId,
83
88
  status: "complete",
84
89
  createdAt: now,
@@ -91,23 +96,41 @@ export class ChatDataIngestService {
91
96
  inlineText: payloadRef ? undefined : input.text,
92
97
  }),
93
98
  });
94
- this.upsertNavigation(input.session, input.roomId, previewText(input.text), now, "running");
99
+ this.upsertNavigation(input.session, input.roomId, previewText(input.text), now, input.eventId ? undefined : "running");
95
100
  return { streamId: event.streamId, messageId, duplicate: false };
96
101
  });
97
102
  }
103
+ prepareUserMessagePayload(text, createdAt) {
104
+ return this.prepareTextPayloadIfLarge(text, createdAt, "chat_message");
105
+ }
98
106
  ingestOutputEvent(input) {
99
107
  const event = input.event;
100
108
  const idempotencyKey = outputIdempotencyKey(event);
101
109
  const identityFingerprint = outputIdentityFingerprint(event);
110
+ const identityFieldDigests = outputIdentityFieldDigests(event);
111
+ const incomingProvenance = input.persistenceProvenance ?? {
112
+ producer: "direct-ingest",
113
+ projection: "product-history",
114
+ phase: "direct",
115
+ };
102
116
  const partFingerprint = isOutputPartEvent(event) ? outputPartFingerprint(event) : undefined;
103
117
  if (idempotencyKey) {
104
- const existing = this.store.eventLog.findByIdempotencyKey(idempotencyKey);
118
+ const directExisting = this.store.eventLog.findByIdempotencyKey(idempotencyKey);
119
+ const legacyExisting = !directExisting && event.type === "execution_result"
120
+ ? this.store.eventLog.findByIdempotencyKey(legacyOutputIdempotencyKey(event))
121
+ : undefined;
122
+ const legacyPhaseMatches = legacyExisting && event.type === "execution_result"
123
+ ? legacyExecutionResultPhase(legacyExisting.attributes) === executionResultPhase(event)
124
+ : false;
125
+ const existing = directExisting ?? (legacyExisting && (storedFingerprintMatches(legacyExisting.attributes, event, identityFingerprint) || legacyPhaseMatches) ? legacyExisting : undefined);
105
126
  if (existing) {
106
127
  const existingFingerprint = typeof existing.attributes.identityFingerprint === "string"
107
128
  ? existing.attributes.identityFingerprint
108
129
  : undefined;
109
- if (existingFingerprint && existingFingerprint !== identityFingerprint) {
130
+ if (existingFingerprint && !storedFingerprintMatches(existing.attributes, event, identityFingerprint)) {
110
131
  const now = input.createdAt ?? new Date().toISOString();
132
+ const existingFieldDigests = stringMap(existing.attributes.identityFieldDigests);
133
+ const fieldDifferences = diffFieldDigests(existingFieldDigests, identityFieldDigests);
111
134
  this.store.eventLog.appendEvent({
112
135
  sessionId: input.session.id,
113
136
  sessionSequence: this.nextEventSequence(input.session.id),
@@ -126,6 +149,10 @@ export class ChatDataIngestService {
126
149
  existingFingerprint,
127
150
  incomingFingerprint: identityFingerprint,
128
151
  incomingType: event.type,
152
+ existingProvenance: redactedPersistenceProvenance(existing.attributes.persistenceProvenance),
153
+ incomingProvenance,
154
+ fieldDifferences,
155
+ fieldDifferencesTruncated: fieldDifferences.length >= MAX_FINGERPRINT_FIELD_DIFFERENCES,
129
156
  },
130
157
  createdAt: now,
131
158
  indexedAt: now,
@@ -140,13 +167,17 @@ export class ChatDataIngestService {
140
167
  };
141
168
  }
142
169
  }
170
+ const now = input.createdAt ?? new Date().toISOString();
171
+ const payload = payloadForOutputEvent(event);
172
+ const preparedPayload = payload ? this.preparePayloadIfLarge(payload.value, payload.contentType, now, retentionClassForOutputEvent(event)) : undefined;
143
173
  return this.store.transaction(() => {
144
- const now = input.createdAt ?? new Date().toISOString();
174
+ if (event.type === "compaction_end" && !event.aborted && !event.errorMessage && !event.compactionStats) {
175
+ event.compactionStats = this.compactionStats(input.session.id, event.result);
176
+ }
145
177
  if (input.roomId) {
146
- this.store.sessions.upsertSession({ session: input.session, roomId: input.roomId, lastActivityAt: now, status: outputSessionStatus(event) });
178
+ this.store.sessions.upsertSession({ session: input.session, roomId: input.roomId, lastActivityAt: now, status: outputSessionStatus(event), preserveRuntimeBinding: true });
147
179
  }
148
- const payload = payloadForOutputEvent(event);
149
- const payloadRef = payload ? this.writePayloadIfLarge(payload.value, payload.contentType, now, retentionClassForOutputEvent(event)) : undefined;
180
+ const payloadRef = preparedPayload ? this.store.payloads.commitPreparedPayload(preparedPayload).id : undefined;
150
181
  const storedEvent = this.store.eventLog.appendEvent({
151
182
  sessionId: input.session.id,
152
183
  sessionSequence: this.nextEventSequence(input.session.id),
@@ -166,6 +197,9 @@ export class ChatDataIngestService {
166
197
  previewText: previewTextForOutputEvent(event),
167
198
  attributes: compactObject({
168
199
  identityFingerprint,
200
+ identityFingerprintVersion: OUTPUT_IDENTITY_FINGERPRINT_VERSION,
201
+ identityFieldDigests,
202
+ persistenceProvenance: incomingProvenance,
169
203
  outputPartFingerprint: partFingerprint,
170
204
  eventIdentityScoped: eventIdForOutputEvent(event) !== undefined,
171
205
  semanticEventId: eventIdForOutputEvent(event),
@@ -233,6 +267,34 @@ export class ChatDataIngestService {
233
267
  return { streamId: storedEvent.streamId, duplicate: false, messageId, observationId };
234
268
  });
235
269
  }
270
+ compactionStats(sessionId, result) {
271
+ const boundary = this.store.db.prepare(`
272
+ SELECT COALESCE(MAX(session_sequence), 0) AS sequence
273
+ FROM event_log
274
+ WHERE session_id = ?
275
+ AND type = 'compaction_end'
276
+ AND COALESCE(json_extract(attributes_json, '$.aborted'), 0) = 0
277
+ AND json_extract(attributes_json, '$.errorMessage') IS NULL
278
+ `).get(sessionId);
279
+ const segmentWhere = `session_id = ? AND type = 'tool_execution_finished' AND session_sequence > ?`;
280
+ const count = this.store.db.prepare(`SELECT COUNT(*) AS count FROM event_log WHERE ${segmentWhere}`)
281
+ .get(sessionId, boundary.sequence);
282
+ const maxRow = this.store.db.prepare(`
283
+ SELECT attributes_json
284
+ FROM event_log
285
+ WHERE ${segmentWhere}
286
+ AND json_type(attributes_json, '$.toolMetrics.outputTokens') IN ('integer', 'real')
287
+ ORDER BY json_extract(attributes_json, '$.toolMetrics.outputTokens') DESC
288
+ LIMIT 1
289
+ `).get(sessionId, boundary.sequence);
290
+ const maxToolOutput = maxRow ? toolMetricsFromAttributes(maxRow.attributes_json) : undefined;
291
+ return compactObject({
292
+ toolCallCount: count.count,
293
+ maxToolOutputTokens: maxToolOutput?.outputTokens,
294
+ maxToolOutputTokenBasis: maxToolOutput?.tokenBasis,
295
+ compactionTokens: compactionTokenCount(result),
296
+ });
297
+ }
236
298
  nextEventSequence(sessionId) {
237
299
  const row = this.store.db.prepare("SELECT COALESCE(MAX(session_sequence), 0) + 1 AS next_sequence FROM event_log WHERE session_id = ?").get(sessionId);
238
300
  return row.next_sequence;
@@ -241,22 +303,22 @@ export class ChatDataIngestService {
241
303
  const row = this.store.db.prepare("SELECT COALESCE(MAX(sequence), 0) + 1 AS next_sequence FROM chat_messages WHERE session_id = ?").get(sessionId);
242
304
  return row.next_sequence;
243
305
  }
244
- writeTextPayloadIfLarge(text, createdAt, retentionClass) {
306
+ prepareTextPayloadIfLarge(text, createdAt, retentionClass) {
245
307
  if (Buffer.byteLength(text, "utf8") <= INLINE_MESSAGE_PAYLOAD_THRESHOLD_BYTES)
246
308
  return undefined;
247
- return this.store.payloads.writePayload({
309
+ return this.store.payloads.preparePayload({
248
310
  value: text,
249
311
  contentType: "text/plain; charset=utf-8",
250
312
  retentionClass,
251
313
  createdAt,
252
- }).id;
314
+ });
253
315
  }
254
- writePayloadIfLarge(value, contentType, createdAt, retentionClass) {
316
+ preparePayloadIfLarge(value, contentType, createdAt, retentionClass) {
255
317
  const bytes = Buffer.byteLength(typeof value === "string" ? value : JSON.stringify(value), "utf8");
256
318
  const threshold = typeof value === "string" ? INLINE_MESSAGE_PAYLOAD_THRESHOLD_BYTES : INLINE_JSON_PAYLOAD_THRESHOLD_BYTES;
257
319
  if (bytes <= threshold)
258
320
  return undefined;
259
- return this.store.payloads.writePayload({ value, contentType, retentionClass, createdAt }).id;
321
+ return this.store.payloads.preparePayload({ value, contentType, retentionClass, createdAt });
260
322
  }
261
323
  upsertNavigation(session, roomId, lastMessagePreview, now, status) {
262
324
  this.store.navigation.upsertSession({
@@ -275,6 +337,69 @@ export class ChatDataIngestService {
275
337
  });
276
338
  }
277
339
  }
340
+ function toolMetricsFromAttributes(attributesJson) {
341
+ try {
342
+ const attributes = JSON.parse(attributesJson);
343
+ if (!isRecord(attributes) || !isRecord(attributes.toolMetrics))
344
+ return undefined;
345
+ const outputTokens = nonNegativeFiniteNumber(attributes.toolMetrics.outputTokens);
346
+ const tokenBasis = typeof attributes.toolMetrics.tokenBasis === "string"
347
+ ? attributes.toolMetrics.tokenBasis
348
+ : undefined;
349
+ return { outputTokens, tokenBasis };
350
+ }
351
+ catch {
352
+ return undefined;
353
+ }
354
+ }
355
+ function compactionTokenCount(result) {
356
+ if (!isRecord(result))
357
+ return undefined;
358
+ return nonNegativeFiniteNumber(result.tokensBefore);
359
+ }
360
+ function nonNegativeFiniteNumber(value) {
361
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
362
+ }
363
+ function isRecord(value) {
364
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
365
+ }
366
+ function stringMap(value) {
367
+ if (!isRecord(value))
368
+ return {};
369
+ return Object.fromEntries(Object.entries(value).filter((entry) => typeof entry[1] === "string").slice(0, 64));
370
+ }
371
+ function diffFieldDigests(existing, incoming) {
372
+ return [...new Set([...Object.keys(existing), ...Object.keys(incoming)])]
373
+ .sort()
374
+ .filter((field) => existing[field] !== incoming[field])
375
+ .slice(0, MAX_FINGERPRINT_FIELD_DIFFERENCES)
376
+ .map((field) => ({
377
+ field,
378
+ change: existing[field] === undefined ? "added" : incoming[field] === undefined ? "removed" : "changed",
379
+ }));
380
+ }
381
+ function redactedPersistenceProvenance(value) {
382
+ if (!isRecord(value))
383
+ return { producer: "unknown", projection: "product-history", phase: "unknown" };
384
+ const bounded = (field) => typeof value[field] === "string" ? value[field].slice(0, 64) : "unknown";
385
+ return { producer: bounded("producer"), projection: bounded("projection"), phase: bounded("phase") };
386
+ }
387
+ function executionResultPhase(event) {
388
+ return isRecord(event.result) && event.result.queued === true ? "queued" : "complete";
389
+ }
390
+ function legacyExecutionResultPhase(attributes) {
391
+ return isRecord(attributes.inlinePayload) && attributes.inlinePayload.queued === true ? "queued" : "complete";
392
+ }
393
+ function storedFingerprintMatches(attributes, event, currentFingerprint) {
394
+ const fingerprint = attributes.identityFingerprint;
395
+ if (typeof fingerprint !== "string")
396
+ return true;
397
+ if (attributes.identityFingerprintVersion === OUTPUT_IDENTITY_FINGERPRINT_VERSION)
398
+ return fingerprint === currentFingerprint;
399
+ // Versionless fingerprints were produced by v1. Compare with the exact old
400
+ // algorithm instead of comparing incompatible hash formats.
401
+ return legacyOutputIdentityFingerprintCandidates(event).includes(fingerprint);
402
+ }
278
403
  function deterministicId(prefix, value) {
279
404
  return `${prefix}_${createHash("sha256").update(value).digest("hex").slice(0, 32)}`;
280
405
  }
@@ -313,6 +438,13 @@ export function outputPersistenceDeliveryKey(event) {
313
438
  return outputIdempotencyKey(event)
314
439
  ?? `pibo.output:${event.piboSessionId}:${event.type}:render:${event.renderSequence ?? "unpositioned"}`;
315
440
  }
441
+ /** Accepted only while recovering pre-phase execution-result envelopes. */
442
+ export function legacyOutputIdempotencyKey(event) {
443
+ if (event.type !== "execution_result")
444
+ return outputIdempotencyKey(event);
445
+ const base = event.eventId;
446
+ return base ? `pibo.output:${event.piboSessionId}:${event.type}:${base}:${event.action}` : undefined;
447
+ }
316
448
  function outputPartKey(event) {
317
449
  if (event.type === "tool_call")
318
450
  return `${event.toolCallId}:${event.toolInvocationOrdinal ?? 0}:${event.argsComplete ? "complete" : "partial"}:${hashJson(event.args)}`;
@@ -329,7 +461,7 @@ function outputPartKey(event) {
329
461
  if (event.type === "compaction_start" || event.type === "compaction_end")
330
462
  return String(event.compactionIndex ?? 0);
331
463
  if (event.type === "execution_result")
332
- return event.action;
464
+ return `${event.action}:${executionResultPhase(event)}`;
333
465
  return "main";
334
466
  }
335
467
  function messageIdForOutputEvent(event) {
@@ -414,6 +546,8 @@ function specificAttributesForOutputEvent(event) {
414
546
  if (event.type === "assistant_usage")
415
547
  return {
416
548
  usageIndex: event.usageIndex,
549
+ inferenceId: event.inferenceId,
550
+ inferenceTarget: event.inferenceTarget,
417
551
  inputTokens: event.inputTokens,
418
552
  outputTokens: event.outputTokens,
419
553
  cacheReadTokens: event.cacheReadTokens,
@@ -435,7 +569,7 @@ function specificAttributesForOutputEvent(event) {
435
569
  if (event.type === "session_error")
436
570
  return { error: event.error, ...(event.errorDetails ? { errorDetails: event.errorDetails } : {}) };
437
571
  if (event.type === "compaction_start" || event.type === "compaction_end")
438
- return { compactionIndex: event.compactionIndex, reason: event.reason, aborted: "aborted" in event ? event.aborted : undefined, errorMessage: "errorMessage" in event ? event.errorMessage : undefined };
572
+ return { compactionIndex: event.compactionIndex, reason: event.reason, aborted: "aborted" in event ? event.aborted : undefined, errorMessage: "errorMessage" in event ? event.errorMessage : undefined, compactionStats: "compactionStats" in event ? event.compactionStats : undefined };
439
573
  return {};
440
574
  }
441
575
  function observationKindForOutputEvent(event) {
@@ -0,0 +1,290 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ export const MESSAGE_COMMAND_SCHEMA = `
3
+ CREATE TABLE IF NOT EXISTS message_commands (
4
+ id TEXT PRIMARY KEY, request_key TEXT NOT NULL UNIQUE, fingerprint TEXT NOT NULL,
5
+ session_id TEXT NOT NULL, room_id TEXT NOT NULL, event_id TEXT NOT NULL,
6
+ stream_id INTEGER NOT NULL, payload_ref TEXT NOT NULL REFERENCES payloads(id), payload_bytes INTEGER NOT NULL,
7
+ delivery TEXT NOT NULL CHECK(delivery IN ('queue','steer')),
8
+ state TEXT NOT NULL CHECK(state IN ('accepted','waiting_slot','initializing','session_queue','running','completed','failed','interrupted')),
9
+ owner TEXT, token INTEGER NOT NULL DEFAULT 0, lease_until INTEGER NOT NULL DEFAULT 0,
10
+ created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, error TEXT,
11
+ UNIQUE(session_id,event_id)
12
+ );
13
+ CREATE INDEX IF NOT EXISTS message_commands_dispatch_order ON message_commands(session_id,state,stream_id,delivery);
14
+ CREATE INDEX IF NOT EXISTS message_commands_recent ON message_commands(session_id,stream_id DESC);
15
+ CREATE INDEX IF NOT EXISTS message_commands_pending ON message_commands(state,created_at,id);
16
+ CREATE INDEX IF NOT EXISTS message_commands_session ON message_commands(session_id,state,created_at,id);
17
+ CREATE INDEX IF NOT EXISTS message_commands_lease ON message_commands(lease_until) WHERE owner IS NOT NULL;
18
+ CREATE INDEX IF NOT EXISTS message_commands_health_scope ON message_commands(state,delivery,room_id,session_id,stream_id,created_at);
19
+ CREATE INDEX IF NOT EXISTS event_log_message_command_terminal ON event_log(session_id,event_id,type,stream_id)
20
+ WHERE event_id IS NOT NULL AND type IN ('message_finished','session_error','message_steered');
21
+ CREATE TABLE IF NOT EXISTS message_command_stats (
22
+ state TEXT NOT NULL, delivery TEXT NOT NULL, command_count INTEGER NOT NULL, payload_bytes INTEGER NOT NULL,
23
+ PRIMARY KEY(state,delivery)
24
+ );
25
+ CREATE TRIGGER IF NOT EXISTS message_command_stats_insert AFTER INSERT ON message_commands BEGIN
26
+ INSERT INTO message_command_stats(state,delivery,command_count,payload_bytes) VALUES(NEW.state,NEW.delivery,1,NEW.payload_bytes)
27
+ ON CONFLICT(state,delivery) DO UPDATE SET command_count=command_count+1,payload_bytes=payload_bytes+NEW.payload_bytes;
28
+ END;
29
+ CREATE TRIGGER IF NOT EXISTS message_command_stats_delete AFTER DELETE ON message_commands BEGIN
30
+ UPDATE message_command_stats SET command_count=command_count-1,payload_bytes=payload_bytes-OLD.payload_bytes WHERE state=OLD.state AND delivery=OLD.delivery;
31
+ END;
32
+ CREATE TRIGGER IF NOT EXISTS message_command_stats_update AFTER UPDATE OF state,delivery,payload_bytes ON message_commands
33
+ WHEN OLD.state<>NEW.state OR OLD.delivery<>NEW.delivery OR OLD.payload_bytes<>NEW.payload_bytes BEGIN
34
+ UPDATE message_command_stats SET command_count=command_count-1,payload_bytes=payload_bytes-OLD.payload_bytes WHERE state=OLD.state AND delivery=OLD.delivery;
35
+ INSERT INTO message_command_stats(state,delivery,command_count,payload_bytes) VALUES(NEW.state,NEW.delivery,1,NEW.payload_bytes)
36
+ ON CONFLICT(state,delivery) DO UPDATE SET command_count=command_count+1,payload_bytes=payload_bytes+NEW.payload_bytes;
37
+ END;
38
+ CREATE TABLE IF NOT EXISTS message_dispatch_clock (id INTEGER PRIMARY KEY CHECK(id=1), sequence INTEGER NOT NULL);
39
+ INSERT OR IGNORE INTO message_dispatch_clock VALUES (1,0);
40
+ CREATE TABLE IF NOT EXISTS message_dispatch_rooms (room_id TEXT PRIMARY KEY, sequence INTEGER NOT NULL);
41
+ INSERT INTO message_command_stats(state,delivery,command_count,payload_bytes)
42
+ SELECT state,delivery,count(*),COALESCE(sum(payload_bytes),0) FROM message_commands
43
+ WHERE NOT EXISTS (SELECT 1 FROM message_command_stats) GROUP BY state,delivery;
44
+ `;
45
+ export const MESSAGE_COMMAND_LIMITS = Object.freeze({
46
+ queue: { count: 1000, roomCount: 256, sessionCount: 64, bytes: 64 * 1024 * 1024, roomBytes: 16 * 1024 * 1024, sessionBytes: 4 * 1024 * 1024, ageMs: 60 * 60 * 1000, roomAgeMs: 15 * 60 * 1000, sessionAgeMs: 10 * 60 * 1000 },
47
+ steer: { count: 64, roomCount: 16, sessionCount: 4, bytes: 4 * 1024 * 1024, roomBytes: 1024 * 1024, sessionBytes: 1024 * 1024, ageMs: 60 * 1000, roomAgeMs: 60 * 1000, sessionAgeMs: 60 * 1000 },
48
+ dispatch: { queue: 10, roomQueue: 5, steer: 2, roomSteer: 1 },
49
+ });
50
+ const active = "'accepted','waiting_slot','initializing','session_queue','running'";
51
+ const unstarted = "'accepted','waiting_slot'";
52
+ const terminalEvidenceSql = `(SELECT CASE
53
+ WHEN count(DISTINCT CASE WHEN e.type='session_error' THEN 'failed' ELSE 'completed' END)=1
54
+ THEN max(CASE WHEN e.type='session_error' THEN 'failed' ELSE 'completed' END) END
55
+ FROM event_log e WHERE e.session_id=message_commands.session_id AND e.event_id=message_commands.event_id
56
+ AND e.type IN ('message_finished','session_error','message_steered'))`;
57
+ /** Durable receipts are independent of optional trace/telemetry retention. Only the storage worker owns this store. */
58
+ export class MessageCommandStore {
59
+ store;
60
+ constructor(store) {
61
+ this.store = store;
62
+ }
63
+ fingerprint(input) {
64
+ if (Buffer.byteLength(input.text) > 1024 * 1024)
65
+ throw domainError("command_too_large", "Message exceeds the durable command byte limit.");
66
+ return createHash("sha256").update(JSON.stringify(input)).digest("hex");
67
+ }
68
+ prepare(input) {
69
+ return {
70
+ fingerprint: this.fingerprint(input),
71
+ payload: this.store.payloads.preparePayload({ value: input.text, contentType: "text/plain", retentionClass: "message_command" }),
72
+ };
73
+ }
74
+ find(key, fingerprint) {
75
+ const row = this.store.db.prepare("SELECT * FROM message_commands WHERE request_key = ?").get(key);
76
+ if (row && fingerprint && row.fingerprint !== fingerprint)
77
+ throw domainError("command_conflict", "Client transaction ID is already bound to different message content, delivery, or session.");
78
+ return row && receipt(row);
79
+ }
80
+ list(sessionId) {
81
+ const pending = this.store.db.prepare(`SELECT * FROM message_commands WHERE session_id=? AND state IN (${active},'interrupted') ORDER BY stream_id DESC LIMIT 70`).all(sessionId);
82
+ const terminal = this.store.db.prepare("SELECT * FROM message_commands WHERE session_id=? AND state IN ('completed','failed') ORDER BY stream_id DESC LIMIT 64").all(sessionId);
83
+ return [...pending, ...terminal].sort((a, b) => b.stream_id - a.stream_id).map(receipt);
84
+ }
85
+ queueStatus(sessionId) {
86
+ const rows = this.store.db.prepare(`SELECT state,delivery,payload_bytes,created_at FROM message_commands WHERE session_id=? AND state IN (${active}) LIMIT 69`).all(sessionId);
87
+ const blocker = this.store.db.prepare("SELECT id,created_at FROM message_commands WHERE session_id=? AND state='interrupted' ORDER BY stream_id LIMIT 1").get(sessionId);
88
+ const summarize = (delivery) => {
89
+ const scoped = rows.filter(row => row.delivery === delivery), limits = MESSAGE_COMMAND_LIMITS[delivery];
90
+ return { count: scoped.length, bytes: scoped.reduce((sum, row) => sum + row.payload_bytes, 0), oldestWaitMs: Math.max(0, ...scoped.filter(row => row.state === "accepted" || row.state === "waiting_slot").map(row => Date.now() - row.created_at)), limits: { count: limits.sessionCount, bytes: limits.sessionBytes, waitMs: limits.sessionAgeMs } };
91
+ };
92
+ return { queue: summarize("queue"), steer: summarize("steer"), ...(blocker ? { reconciliation: { required: true, retryable: false, scope: "session", blockingCommandId: blocker.id, blockedSince: blocker.created_at, nextAction: `pibo debug message-queue inspect --session ${sessionId}` } } : {}) };
93
+ }
94
+ get(id) {
95
+ const row = this.store.db.prepare("SELECT * FROM message_commands WHERE id = ?").get(id);
96
+ return row && receipt(row);
97
+ }
98
+ terminalOutcome(sessionId, eventId) {
99
+ const row = this.store.db.prepare(`SELECT CASE WHEN count(DISTINCT CASE WHEN type='session_error' THEN 'failed' ELSE 'completed' END)=1 THEN max(CASE WHEN type='session_error' THEN 'failed' ELSE 'completed' END) END state FROM event_log WHERE session_id=? AND event_id=? AND type IN ('message_finished','session_error','message_steered')`).get(sessionId, eventId);
100
+ return row?.state;
101
+ }
102
+ terminalEvidence(sessionId, eventId) {
103
+ return this.store.db.prepare(`SELECT stream_id,type,CASE WHEN type='session_error' THEN 'failed' ELSE 'completed' END state FROM event_log
104
+ WHERE session_id=? AND event_id=? AND type IN ('message_finished','session_error','message_steered') ORDER BY stream_id LIMIT 8`).all(sessionId, eventId)
105
+ .map(row => ({ streamId: row.stream_id, type: row.type, state: row.state }));
106
+ }
107
+ assertAdmissionUnblocked(sessionId, delivery, beforeStreamId = Number.MAX_SAFE_INTEGER) {
108
+ if (delivery !== "queue")
109
+ return;
110
+ const blocker = this.store.db.prepare("SELECT id,created_at FROM message_commands WHERE session_id=? AND state='interrupted' AND stream_id<? ORDER BY stream_id LIMIT 1").get(sessionId, beforeStreamId);
111
+ if (blocker)
112
+ throw domainError("command_reconciliation_required", "A previous interrupted message requires operator review before this session can accept more messages.", { retryable: false, scope: "session", blockingCommandId: blocker.id, blockedSince: blocker.created_at, oldestWaitAgeMs: Math.max(0, Date.now() - blocker.created_at), nextAction: `pibo debug message-queue inspect --session ${sessionId}` });
113
+ }
114
+ insert(input) {
115
+ const prior = this.find(input.key, input.fingerprint);
116
+ if (prior)
117
+ return prior;
118
+ this.assertAdmissionUnblocked(input.sessionId, input.delivery, input.streamId);
119
+ const limit = MESSAGE_COMMAND_LIMITS[input.delivery];
120
+ const rows = this.store.db.prepare(`SELECT c.session_id,c.room_id,c.payload_bytes,c.created_at,c.state,
121
+ NOT EXISTS(SELECT 1 FROM message_commands p WHERE p.session_id=c.session_id AND p.state='interrupted' AND p.stream_id<c.stream_id) dispatchable
122
+ FROM message_commands c WHERE c.state IN (${active}) AND c.delivery=? LIMIT ?`).all(input.delivery, limit.count + 1);
123
+ const now = Date.now();
124
+ const exceeds = (scope, count, bytes, ageMs) => scope.length >= count
125
+ || scope.reduce((n, r) => n + r.payload_bytes, input.payload.byteSize) > bytes
126
+ || scope.some(r => r.dispatchable === 1 && (r.state === "accepted" || r.state === "waiting_slot") && now - r.created_at >= ageMs);
127
+ if (exceeds(rows, limit.count, limit.bytes, limit.ageMs)
128
+ || exceeds(rows.filter(r => r.room_id === input.roomId), limit.roomCount, limit.roomBytes, limit.roomAgeMs)
129
+ || exceeds(rows.filter(r => r.session_id === input.sessionId), limit.sessionCount, limit.sessionBytes, limit.sessionAgeMs)) {
130
+ throw domainError("command_overloaded", "Durable message queue count, byte or dispatchable wait-age capacity reached; retry the same transaction later.", { retryable: true });
131
+ }
132
+ const id = `cmd_${randomUUID()}`;
133
+ const payload = this.store.payloads.commitPreparedPayload(input.payload);
134
+ this.store.db.prepare(`INSERT INTO message_commands (id,request_key,fingerprint,session_id,room_id,event_id,stream_id,payload_ref,payload_bytes,delivery,state,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,'accepted',?,?)`).run(id, input.key, input.fingerprint, input.sessionId, input.roomId, input.eventId, input.streamId, payload.id, payload.byteSize, input.delivery, now, now);
135
+ return this.get(id);
136
+ }
137
+ terminalizeBlockedSuccessors(now, limit = 100) {
138
+ const bounded = Math.max(1, Math.min(1000, Math.trunc(limit) || 1));
139
+ return Number(this.store.db.prepare(`UPDATE message_commands SET state='failed',error='Not dispatched: an earlier interrupted message requires reconciliation.',owner = NULL,lease_until=0,updated_at=?
140
+ WHERE id IN (SELECT c.id FROM message_commands c WHERE c.state IN (${unstarted}) AND c.delivery='queue' AND c.owner IS NULL AND EXISTS(
141
+ SELECT 1 FROM message_commands p WHERE p.session_id=c.session_id AND p.state='interrupted' AND p.stream_id<c.stream_id
142
+ ) ORDER BY c.created_at,c.id LIMIT ?)`).run(now, bounded).changes);
143
+ }
144
+ /** Bounded, idempotent startup repair. Ambiguous rows cannot starve later evidence-backed rows. */
145
+ reconcileInterrupted(limit = 1000) {
146
+ const bounded = Math.max(1, Math.min(1000, Math.trunc(limit) || 1));
147
+ let rows = [];
148
+ try {
149
+ rows = this.store.db.prepare(`SELECT id FROM message_commands WHERE state='interrupted' AND ${terminalEvidenceSql} IN ('completed','failed') ORDER BY updated_at,id LIMIT ?`).all(bounded);
150
+ }
151
+ catch {
152
+ return { examined: 0, reconciled: 0, blockedSuccessorsFailed: 0, ambiguous: 0, errors: 1 };
153
+ }
154
+ let reconciled = 0, errors = 0;
155
+ for (const row of rows) {
156
+ try {
157
+ reconciled += this.store.transaction(() => Number(this.store.db.prepare(`UPDATE message_commands SET state=${terminalEvidenceSql},error=NULL,owner = NULL,lease_until=0,updated_at=? WHERE id=? AND state='interrupted' AND ${terminalEvidenceSql} IN ('completed','failed')`).run(Date.now(), row.id).changes));
158
+ }
159
+ catch {
160
+ errors++;
161
+ }
162
+ }
163
+ let blockedSuccessorsFailed = 0;
164
+ try {
165
+ blockedSuccessorsFailed = this.store.transaction(() => this.terminalizeBlockedSuccessors(Date.now(), bounded));
166
+ }
167
+ catch {
168
+ errors++;
169
+ }
170
+ const ambiguous = Number(this.store.db.prepare("SELECT count(*) n FROM message_commands WHERE state='interrupted'").get().n);
171
+ return { examined: rows.length, reconciled, blockedSuccessorsFailed, ambiguous, errors };
172
+ }
173
+ recoverExpiredLeasesInTransaction(now, afterSelection) {
174
+ const rows = this.store.db.prepare("SELECT id FROM message_commands WHERE owner IS NOT NULL AND lease_until<=? ORDER BY lease_until,id LIMIT 100").all(now);
175
+ afterSelection?.();
176
+ let changed = 0;
177
+ for (const row of rows) {
178
+ changed += Number(this.store.db.prepare(`UPDATE message_commands SET
179
+ state=CASE WHEN ${terminalEvidenceSql} IN ('completed','failed') THEN ${terminalEvidenceSql} WHEN state='waiting_slot' THEN 'accepted' ELSE 'interrupted' END,
180
+ error=CASE WHEN ${terminalEvidenceSql} IN ('completed','failed') OR state='waiting_slot' THEN NULL ELSE 'Runtime ownership expired; execution requires reconciliation.' END,
181
+ owner = NULL,lease_until=0,updated_at=? WHERE id=? AND owner IS NOT NULL AND lease_until<=? AND state IN (${active})`).run(now, row.id, now).changes);
182
+ }
183
+ this.terminalizeBlockedSuccessors(now);
184
+ return changed;
185
+ }
186
+ /** Public for deterministic recovery tests; production callers use claim(). */
187
+ recoverExpiredLeases(now = Date.now(), barrier) {
188
+ return this.store.transaction(() => this.recoverExpiredLeasesInTransaction(now, barrier));
189
+ }
190
+ claim(owner, leaseMs) {
191
+ const limits = MESSAGE_COMMAND_LIMITS.dispatch;
192
+ const candidateSql = `WITH occupied AS (
193
+ SELECT room_id,delivery FROM message_commands WHERE owner IS NOT NULL AND state IN (${active})
194
+ ) SELECT c.* FROM message_commands c LEFT JOIN message_dispatch_rooms r ON r.room_id=c.room_id
195
+ WHERE c.state='accepted'
196
+ AND (SELECT count(*) FROM occupied o WHERE o.delivery=c.delivery) < CASE c.delivery WHEN 'steer' THEN ${limits.steer} ELSE ${limits.queue} END
197
+ AND (SELECT count(*) FROM occupied o WHERE o.delivery=c.delivery AND o.room_id=c.room_id) < CASE c.delivery WHEN 'steer' THEN ${limits.roomSteer} ELSE ${limits.roomQueue} END
198
+ AND NOT EXISTS (SELECT 1 FROM message_commands p WHERE p.session_id=c.session_id AND p.state IN (${active},'interrupted') AND p.stream_id<c.stream_id AND (c.delivery='queue' OR p.delivery='steer'))
199
+ ORDER BY CASE c.delivery WHEN 'steer' THEN 0 ELSE 1 END,COALESCE(r.sequence,0),c.stream_id LIMIT 1`;
200
+ if (!this.store.db.prepare(candidateSql).get() && !this.store.db.prepare("SELECT 1 FROM message_commands WHERE owner IS NOT NULL AND lease_until <= ? LIMIT 1").get(Date.now()))
201
+ return undefined;
202
+ const row = this.store.transaction(() => {
203
+ const now = Date.now();
204
+ this.recoverExpiredLeasesInTransaction(now);
205
+ const candidate = this.store.db.prepare(candidateSql).get();
206
+ if (!candidate)
207
+ return undefined;
208
+ this.store.db.prepare("UPDATE message_dispatch_clock SET sequence=sequence+1 WHERE id=1").run();
209
+ this.store.db.prepare("INSERT INTO message_dispatch_rooms(room_id,sequence) SELECT ?,sequence FROM message_dispatch_clock WHERE id=1 ON CONFLICT(room_id) DO UPDATE SET sequence=excluded.sequence").run(candidate.room_id);
210
+ this.store.db.prepare("UPDATE message_commands SET state='waiting_slot',owner = ?,token=token+1,lease_until=?,updated_at=? WHERE id=?").run(owner, now + leaseMs, now, candidate.id);
211
+ return this.store.db.prepare("SELECT * FROM message_commands WHERE id=?").get(candidate.id);
212
+ });
213
+ if (!row)
214
+ return undefined;
215
+ try {
216
+ const text = Buffer.from(this.store.payloads.readPayloadBytesBounded(row.payload_ref, 1024 * 1024)).toString("utf8");
217
+ return { ...receipt(row), token: row.token, text, delivery: row.delivery };
218
+ }
219
+ catch {
220
+ this.transition(row.id, owner, row.token, "failed", "Durable message payload is unavailable or corrupt.");
221
+ return undefined;
222
+ }
223
+ }
224
+ cancelPending(sessionId) {
225
+ return Number(this.store.db.prepare("UPDATE message_commands SET state='failed',error='Cancelled before runtime dispatch.',owner = NULL,lease_until=0,updated_at=? WHERE session_id=? AND state IN ('accepted','waiting_slot')").run(Date.now(), sessionId).changes);
226
+ }
227
+ transition(id, owner, token, state, error) {
228
+ return Number(this.store.db.prepare(`UPDATE message_commands SET state=?,error=?,updated_at=?,owner = CASE WHEN ? IN ('completed','failed','interrupted') THEN NULL ELSE owner END,lease_until=CASE WHEN ? IN ('completed','failed','interrupted') THEN 0 ELSE lease_until END WHERE id=? AND owner = ? AND token=? AND lease_until>? AND state IN (${active})`).run(state, error?.slice(0, 500) ?? null, Date.now(), state, state, id, owner, token, Date.now()).changes) === 1;
229
+ }
230
+ heartbeat(id, owner, token, leaseMs) {
231
+ return Number(this.store.db.prepare(`UPDATE message_commands SET lease_until=? WHERE id=? AND owner = ? AND token=? AND lease_until>? AND state IN (${active})`).run(Date.now() + leaseMs, id, owner, token, Date.now()).changes) === 1;
232
+ }
233
+ recordOutput(sessionId, eventId, type) {
234
+ if (!eventId)
235
+ return;
236
+ const states = { message_queued: "session_queue", message_started: "running", message_finished: "completed", session_error: "failed", message_steered: "completed" };
237
+ const state = states[type];
238
+ if (!state)
239
+ return;
240
+ const eligible = state === "session_queue" ? "'initializing','waiting_slot'" : state === "running" ? "'initializing','waiting_slot','session_queue'" : `${active},'interrupted'`;
241
+ this.store.db.prepare(`UPDATE message_commands SET state=?,error=NULL,updated_at=?,owner = CASE WHEN ? IN ('completed','failed') THEN NULL ELSE owner END,lease_until=CASE WHEN ? IN ('completed','failed') THEN 0 ELSE lease_until END WHERE session_id=? AND event_id=? AND state IN (${eligible})`).run(state, Date.now(), state, state, sessionId, eventId);
242
+ }
243
+ health(now = Date.now(), scopeLimit = 20) {
244
+ const bounded = Math.max(1, Math.min(100, Math.trunc(scopeLimit) || 20));
245
+ const activeRowsLimit = MESSAGE_COMMAND_LIMITS.queue.count + MESSAGE_COMMAND_LIMITS.steer.count + 1;
246
+ const counts = this.store.db.prepare("SELECT state,delivery,command_count count,payload_bytes bytes FROM message_command_stats WHERE command_count>0 ORDER BY state,delivery").all();
247
+ const interrupted = Number(this.store.db.prepare("SELECT count(*) n FROM message_commands WHERE state='interrupted'").get().n);
248
+ const expired = Number(this.store.db.prepare("SELECT count(*) n FROM message_commands WHERE owner IS NOT NULL AND lease_until<=?").get(now).n);
249
+ const blockedRows = this.store.db.prepare(`SELECT c.session_id,c.room_id,c.created_at,p.id blocking_id,p.created_at blocked_since
250
+ FROM message_commands c JOIN message_commands p ON p.id=(SELECT p2.id FROM message_commands p2 WHERE p2.session_id=c.session_id AND p2.state='interrupted' AND p2.stream_id<c.stream_id ORDER BY p2.stream_id LIMIT 1)
251
+ WHERE c.state IN (${unstarted}) AND c.delivery='queue' ORDER BY c.created_at,c.id LIMIT ?`).all(bounded);
252
+ const blockedCount = Number(this.store.db.prepare(`SELECT count(*) n FROM message_commands c WHERE c.state IN (${unstarted}) AND c.delivery='queue' AND EXISTS(SELECT 1 FROM message_commands p WHERE p.session_id=c.session_id AND p.state='interrupted' AND p.stream_id<c.stream_id)`).get().n);
253
+ const oldestDispatchable = this.store.db.prepare(`SELECT created_at FROM message_commands c WHERE c.state IN (${unstarted}) AND NOT EXISTS(SELECT 1 FROM message_commands p WHERE p.session_id=c.session_id AND p.state='interrupted' AND p.stream_id<c.stream_id) ORDER BY created_at LIMIT 1`).get();
254
+ const scopes = new Map();
255
+ for (const row of blockedRows) {
256
+ const prior = scopes.get(row.session_id);
257
+ if (prior)
258
+ prior.blockedSuccessors++;
259
+ else
260
+ scopes.set(row.session_id, { sessionId: row.session_id, roomId: row.room_id, blockingCommandId: row.blocking_id, blockedSince: row.blocked_since, blockedSuccessors: 1 });
261
+ }
262
+ if (interrupted > 0) {
263
+ for (const row of this.store.db.prepare("SELECT session_id,room_id,id,created_at FROM message_commands WHERE state='interrupted' ORDER BY created_at LIMIT ?").all(bounded)) {
264
+ if (!scopes.has(row.session_id))
265
+ scopes.set(row.session_id, { sessionId: row.session_id, roomId: row.room_id, blockingCommandId: row.id, blockedSince: row.created_at, blockedSuccessors: 0 });
266
+ }
267
+ }
268
+ const activeRows = this.store.db.prepare(`SELECT session_id,room_id,delivery,payload_bytes,created_at,state,NOT EXISTS(SELECT 1 FROM message_commands p WHERE p.session_id=c.session_id AND p.state='interrupted' AND p.stream_id<c.stream_id) dispatchable FROM message_commands c WHERE state IN (${active}) ORDER BY created_at,id LIMIT ?`).all(activeRowsLimit);
269
+ const capacityReasons = [];
270
+ const activeStates = new Set(["accepted", "waiting_slot", "initializing", "session_queue", "running"]);
271
+ const globalTotals = (delivery) => counts.filter(row => row.delivery === delivery && activeStates.has(row.state)).reduce((total, row) => ({ count: total.count + row.count, bytes: total.bytes + row.bytes }), { count: 0, bytes: 0 });
272
+ const summarizeCapacity = (rows, delivery, scope) => { const selected = rows.filter(row => row.delivery === delivery), limits = MESSAGE_COMMAND_LIMITS[delivery], countLimit = scope === "global" ? limits.count : scope === "room" ? limits.roomCount : limits.sessionCount, byteLimit = scope === "global" ? limits.bytes : scope === "room" ? limits.roomBytes : limits.sessionBytes, ageLimit = scope === "global" ? limits.ageMs : scope === "room" ? limits.roomAgeMs : limits.sessionAgeMs, totals = scope === "global" ? globalTotals(delivery) : { count: selected.length, bytes: selected.reduce((sum, row) => sum + row.payload_bytes, 0) }, count = totals.count, bytes = totals.bytes, oldestDispatchableWaitMs = Math.max(0, ...selected.filter(row => row.dispatchable === 1 && (row.state === "accepted" || row.state === "waiting_slot")).map(row => now - row.created_at)), reasons = [...(count >= countLimit ? [`count ${count}/${countLimit}`] : []), ...(bytes >= byteLimit ? [`bytes ${bytes}/${byteLimit}`] : []), ...(oldestDispatchableWaitMs >= ageLimit ? [`dispatchable wait ${oldestDispatchableWaitMs}ms/${ageLimit}ms`] : [])]; return { delivery, count, bytes, oldestDispatchableWaitMs, available: reasons.length === 0, degradedReasons: reasons }; };
273
+ const globalCapacity = ["queue", "steer"].map(delivery => summarizeCapacity(activeRows, delivery, "global"));
274
+ const roomKeys = [...new Set(activeRows.map(row => row.room_id))], sessionKeys = [...new Set(activeRows.map(row => row.session_id))];
275
+ const roomCapacity = roomKeys.flatMap(roomId => ["queue", "steer"].map(delivery => ({ roomId, ...summarizeCapacity(activeRows.filter(row => row.room_id === roomId), delivery, "room") }))).filter(row => !row.available).slice(0, bounded);
276
+ const sessionCapacity = sessionKeys.flatMap(sessionId => ["queue", "steer"].map(delivery => ({ sessionId, roomId: activeRows.find(row => row.session_id === sessionId)?.room_id ?? "unknown", ...summarizeCapacity(activeRows.filter(row => row.session_id === sessionId), delivery, "session") }))).filter(row => !row.available).slice(0, bounded);
277
+ for (const row of globalCapacity.filter(row => !row.available))
278
+ capacityReasons.push(`global ${row.delivery} admission degraded: ${row.degradedReasons.join(", ")}`);
279
+ for (const row of roomCapacity)
280
+ capacityReasons.push(`room ${row.roomId} ${row.delivery} admission degraded: ${row.degradedReasons.join(", ")}`);
281
+ for (const row of sessionCapacity)
282
+ capacityReasons.push(`session ${row.sessionId} ${row.delivery} admission degraded: ${row.degradedReasons.join(", ")}`);
283
+ const degradedReasons = [...(interrupted ? [`${interrupted} interrupted predecessor(s) require reconciliation`] : []), ...(blockedCount ? [`${blockedCount} successor(s) are blocked by FIFO`] : []), ...(expired ? [`${expired} owned lease(s) are expired`] : []), ...capacityReasons];
284
+ return { status: degradedReasons.length ? "degraded" : "healthy", storage: { available: true }, counts, interruptedPredecessors: interrupted, blockedSuccessors: blockedCount, expiredOwnedLeases: expired, oldestDispatchableWaitMs: oldestDispatchable ? Math.max(0, now - oldestDispatchable.created_at) : 0, oldestBlockedWaitMs: blockedRows.length ? Math.max(...blockedRows.map(row => Math.max(0, now - row.created_at))) : 0, affectedScopes: [...scopes.values()].slice(0, bounded), limits: MESSAGE_COMMAND_LIMITS, degradedReasons, admissionCapacity: { global: globalCapacity, rooms: roomCapacity, sessions: sessionCapacity }, bounded: { scopeLimit: bounded, activeRowsLimit, activeRowsTruncated: activeRows.length === activeRowsLimit, affectedScopesTruncated: interrupted > bounded || scopes.size > bounded || blockedCount > blockedRows.length } };
285
+ }
286
+ }
287
+ function receipt(row) {
288
+ return { id: row.id, sessionId: row.session_id, roomId: row.room_id, eventId: row.event_id, streamId: row.stream_id, state: row.state, createdAt: row.created_at, updatedAt: row.updated_at, ...(row.error ? { error: row.error } : {}) };
289
+ }
290
+ function domainError(code, message, details = {}) { return Object.assign(new Error(message), { code, ...details }); }