@pasko70/pibo 3.1.3 → 3.1.4

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 (29) hide show
  1. package/dist/apps/chat/data/timeline-query-service.js +5 -2
  2. package/dist/apps/chat/output-compactor.js +33 -30
  3. package/dist/apps/chat/trace-v2.js +1 -0
  4. package/dist/apps/chat/trace.js +3 -1
  5. package/dist/apps/chat/web-app.js +106 -65
  6. package/dist/apps/chat-ui/assets/{dist-BNzch-mL.js → dist-BOqsX6_s.js} +1 -1
  7. package/dist/apps/chat-ui/assets/{dist-CoA9zNTP.js → dist-BcUbdOKJ.js} +1 -1
  8. package/dist/apps/chat-ui/assets/{dist-liw1S7HS.js → dist-DkpIJ_Pp.js} +1 -1
  9. package/dist/apps/chat-ui/assets/{dist-7DhHpLCA.js → dist-a0mykCz7.js} +1 -1
  10. package/dist/apps/chat-ui/assets/{dist-DDRlEGPR.js → dist-qDZ-CdlK.js} +1 -1
  11. package/dist/apps/chat-ui/assets/{index-BKp6zKIe.js → index-G2ic-FSG.js} +89 -89
  12. package/dist/apps/chat-ui/index.html +1 -1
  13. package/dist/apps/chat-vscode-web/assets/{index-CKC-46jZ.js → index-zQ1fNz5K.js} +1 -1
  14. package/dist/apps/chat-vscode-web/index.html +1 -1
  15. package/dist/cli-session/localSessionSource.js +26 -20
  16. package/dist/core/output-persistence-retry.js +23 -1
  17. package/dist/core/output-render-sequence.js +33 -9
  18. package/dist/data/ingest-service.js +8 -0
  19. package/dist/debug/index.js +294 -2
  20. package/dist/debug/output-integrity.js +608 -0
  21. package/dist/debug/output-repair.js +584 -0
  22. package/dist/debug/trace.js +2 -0
  23. package/dist/reliability/store.js +2 -2
  24. package/dist/sessions/pibo-data-store.js +26 -20
  25. package/dist/shared/trace-engine.js +5 -2
  26. package/dist/shared/trace-event-projection.js +74 -0
  27. package/dist/shared/trace-page-merge.js +31 -5
  28. package/npm-shrinkwrap.json +2 -2
  29. package/package.json +1 -1
@@ -0,0 +1,584 @@
1
+ import { DatabaseSync } from "node:sqlite";
2
+ import { isPiboOutputEvent } from "../apps/chat/output-event-policy.js";
3
+ import { ChatDataIngestService } from "../data/ingest-service.js";
4
+ import { PiboDataStore } from "../data/pibo-store.js";
5
+ import { createDefaultPiboPluginRegistry } from "../plugins/builtin.js";
6
+ import { PiboDataSessionStore } from "../sessions/pibo-data-store.js";
7
+ const EVENT_SOURCES = new Set(["user", "ui", "service", "actor"]);
8
+ const REPAIR_LIMIT_MAX = 100;
9
+ const RELIABILITY_SCAN_LIMIT = 5000;
10
+ export function inspectOutputTurnRepair(input) {
11
+ return buildRepairPlan(input).inspection;
12
+ }
13
+ export function repairOutputTurn(input) {
14
+ const mode = input.apply ? "apply" : "dry-run";
15
+ const reliabilityEvidence = collectReliabilityEvidence(input.reliabilityStore, input.piboSessionId, input.eventId);
16
+ const initial = buildRepairPlan(input, reliabilityEvidence);
17
+ const base = resultBase(input.store, input.piboSessionId, mode);
18
+ if (!input.apply || !initial.inspection.repairable || !initial.terminalEvent) {
19
+ return { ...base, applied: false, inspection: initial.inspection };
20
+ }
21
+ const data = new PiboDataStore(input.store.path);
22
+ try {
23
+ return data.transaction(() => {
24
+ const currentReliabilityEvidence = collectReliabilityEvidence(input.reliabilityStore, input.piboSessionId, input.eventId);
25
+ const current = buildRepairPlanFromDb(data.db, input.piboSessionId, input.eventId, currentReliabilityEvidence, input.adapterEvidence);
26
+ if (!current.inspection.repairable || !current.terminalEvent) {
27
+ return { ...base, applied: false, inspection: current.inspection };
28
+ }
29
+ const sessionStore = new PiboDataSessionStore(data);
30
+ const session = sessionStore.get(input.piboSessionId);
31
+ if (!session) {
32
+ return {
33
+ ...base,
34
+ applied: false,
35
+ inspection: { ...current.inspection, repairable: false, reason: "session_not_found" },
36
+ };
37
+ }
38
+ const room = data.db.prepare("SELECT room_id AS roomId FROM sessions WHERE id = ?").get(input.piboSessionId);
39
+ const createdAt = input.now?.() ?? new Date().toISOString();
40
+ const persisted = new ChatDataIngestService(data).ingestOutputEvent({
41
+ session,
42
+ ...(room?.roomId ? { roomId: room.roomId } : {}),
43
+ actorId: "pibo-debug-repair",
44
+ event: current.terminalEvent,
45
+ createdAt,
46
+ });
47
+ const audit = data.eventLog.appendEvent({
48
+ sessionId: input.piboSessionId,
49
+ sessionSequence: nextEventSequence(data.db, input.piboSessionId),
50
+ ...(room?.roomId ? { roomId: room.roomId } : {}),
51
+ topic: "pibo.audit",
52
+ type: "pibo.output.repair_applied",
53
+ source: "pibo-debug-repair",
54
+ actorType: "system",
55
+ actorId: "pibo-debug-repair",
56
+ eventId: input.eventId,
57
+ idempotencyKey: `pibo.output.repair:${input.piboSessionId}:${input.eventId}:${current.terminalEvent.type}`,
58
+ retentionClass: "audit_event",
59
+ previewText: `Output repair applied ${current.terminalEvent.type}`,
60
+ attributes: {
61
+ repairVersion: 1,
62
+ targetEventId: input.eventId,
63
+ terminalType: current.terminalEvent.type,
64
+ terminalStreamId: persisted.streamId,
65
+ evidenceSources: current.inspection.plannedEvent?.evidenceSources ?? [],
66
+ evidenceReferences: current.evidenceReferences,
67
+ },
68
+ createdAt,
69
+ indexedAt: createdAt,
70
+ });
71
+ return {
72
+ ...base,
73
+ applied: true,
74
+ inspection: current.inspection,
75
+ persisted: {
76
+ type: current.terminalEvent.type,
77
+ streamId: persisted.streamId,
78
+ duplicate: persisted.duplicate,
79
+ auditStreamId: audit.streamId,
80
+ },
81
+ };
82
+ });
83
+ }
84
+ finally {
85
+ data.close();
86
+ }
87
+ }
88
+ export function repairOutputTurns(input) {
89
+ if (!input.store.exists)
90
+ throw new Error(`Debug store "pibo-data" not found at ${input.store.path}`);
91
+ const limit = normalizeRepairLimit(input.limit);
92
+ const db = new DatabaseSync(input.store.path, { readOnly: true });
93
+ let candidates;
94
+ try {
95
+ const clauses = ["startedAt IS NOT NULL", "messageStarted > 0", "terminalEvents = 0"];
96
+ const params = [input.piboSessionId];
97
+ if (input.since) {
98
+ clauses.push("startedAt >= ?");
99
+ params.push(input.since);
100
+ }
101
+ if (input.before) {
102
+ clauses.push("startedAt < ?");
103
+ params.push(input.before);
104
+ }
105
+ candidates = db.prepare(`
106
+ WITH turn_lifecycle AS (
107
+ SELECT event_id AS eventId,
108
+ MIN(CASE WHEN type = 'message_started' THEN created_at END) AS startedAt,
109
+ SUM(type = 'message_started') AS messageStarted,
110
+ SUM(type IN ('message_finished', 'session_error')) AS terminalEvents
111
+ FROM event_log
112
+ WHERE session_id = ? AND event_id IS NOT NULL
113
+ GROUP BY event_id
114
+ )
115
+ SELECT eventId, startedAt
116
+ FROM turn_lifecycle
117
+ WHERE ${clauses.join(" AND ")}
118
+ ORDER BY startedAt ASC
119
+ LIMIT ?
120
+ `).all(...params, limit);
121
+ }
122
+ finally {
123
+ db.close();
124
+ }
125
+ const results = candidates.map((candidate) => repairOutputTurn({
126
+ store: input.store,
127
+ reliabilityStore: input.reliabilityStore,
128
+ piboSessionId: input.piboSessionId,
129
+ eventId: candidate.eventId,
130
+ adapterEvidence: input.adapterEvidence,
131
+ apply: input.apply,
132
+ now: input.now,
133
+ }));
134
+ const mode = input.apply ? "apply" : "dry-run";
135
+ return {
136
+ resultType: "debug.repair.output.scope",
137
+ mode,
138
+ scope: {
139
+ piboSessionId: input.piboSessionId,
140
+ ...(input.since ? { since: input.since } : {}),
141
+ ...(input.before ? { before: input.before } : {}),
142
+ limit,
143
+ },
144
+ candidateCount: results.length,
145
+ repairableCount: results.filter((result) => result.inspection.repairable).length,
146
+ appliedCount: results.filter((result) => result.applied).length,
147
+ results,
148
+ warnings: resultWarnings(),
149
+ nextCommands: resultNextCommands(input.piboSessionId),
150
+ };
151
+ }
152
+ export async function readOutputRepairAdapterEvidence(input) {
153
+ if (!input.store.exists)
154
+ return { available: false, entries: [] };
155
+ const db = new DatabaseSync(input.store.path, { readOnly: true });
156
+ try {
157
+ const session = db.prepare("SELECT status, workspace, created_at, updated_at, pi_session_id FROM sessions WHERE id = ?").get(input.piboSessionId);
158
+ if (!session)
159
+ return { available: false, entries: [] };
160
+ const binding = readRuntimeBinding(db, input.piboSessionId, session);
161
+ if (!binding)
162
+ return { available: false, entries: [] };
163
+ const adapter = createDefaultPiboPluginRegistry().getAgentRuntimeAdapter(binding.runtimeInstanceId);
164
+ if (!adapter?.descriptor.capabilities.maintenance.history || !adapter.readHistory) {
165
+ return { available: false, entries: [] };
166
+ }
167
+ try {
168
+ const page = await adapter.readHistory({ binding, workspace: session.workspace ?? process.cwd(), limit: 500 });
169
+ return { available: true, entries: page.entries };
170
+ }
171
+ catch (error) {
172
+ return { available: false, entries: [], error: redactError(error) };
173
+ }
174
+ }
175
+ finally {
176
+ db.close();
177
+ }
178
+ }
179
+ export function formatOutputTurnRepair(result) {
180
+ if (result.resultType === "debug.repair.output.scope") {
181
+ const lines = [
182
+ "pibo debug repair output",
183
+ `mode\t${result.mode}`,
184
+ `session\t${result.scope.piboSessionId}`,
185
+ ...(result.scope.since ? [`since\t${result.scope.since}`] : []),
186
+ ...(result.scope.before ? [`before\t${result.scope.before}`] : []),
187
+ `candidates\t${result.candidateCount}`,
188
+ `repairable\t${result.repairableCount}`,
189
+ `applied\t${result.appliedCount}`,
190
+ ...result.results.map((item) => `${item.inspection.eventId}\t${item.applied ? "applied" : item.inspection.repairable ? "repairable" : item.inspection.reason ?? "not-repairable"}`),
191
+ "",
192
+ ...result.warnings.map((warning) => `warning\t${warning}`),
193
+ "",
194
+ "Next:",
195
+ ...result.nextCommands.map((command) => ` ${command}`),
196
+ ];
197
+ return lines.join("\n");
198
+ }
199
+ const inspection = result.inspection;
200
+ const lines = [
201
+ "pibo debug repair output",
202
+ `mode\t${result.mode}`,
203
+ `applied\t${result.applied}`,
204
+ `session\t${inspection.piboSessionId}`,
205
+ `event\t${inspection.eventId}`,
206
+ `repairable\t${inspection.repairable}`,
207
+ ...(inspection.reason ? [`reason\t${inspection.reason}`] : []),
208
+ ...(inspection.plannedEvent ? [`plannedEvent\t${inspection.plannedEvent.type}`, `evidenceSources\t${inspection.plannedEvent.evidenceSources.join(",")}`] : []),
209
+ `messageStarted\t${inspection.observed.messageStarted}`,
210
+ `assistantMessages\t${inspection.observed.assistantMessages}`,
211
+ `productAssistantMessages\t${inspection.observed.productAssistantMessages}`,
212
+ `messageFinished\t${inspection.observed.messageFinished}`,
213
+ `sessionErrors\t${inspection.observed.sessionErrors}`,
214
+ `openThinkingParts\t${inspection.observed.openThinkingParts}`,
215
+ `openToolInvocations\t${inspection.observed.openToolInvocations}`,
216
+ `identityCollisions\t${inspection.observed.identityCollisions}`,
217
+ `reliabilityTerminalCandidates\t${inspection.observed.reliabilityTerminalCandidates}`,
218
+ `adapterCompletedAssistantMessages\t${inspection.observed.adapterCompletedAssistantMessages}`,
219
+ ...(result.persisted ? [`persisted\t${result.persisted.type}@${result.persisted.streamId}`, `audit\t${result.persisted.auditStreamId}`] : []),
220
+ "",
221
+ ...result.warnings.map((warning) => `warning\t${warning}`),
222
+ "",
223
+ "Next:",
224
+ ...result.nextCommands.map((command) => ` ${command}`),
225
+ ];
226
+ return lines.join("\n");
227
+ }
228
+ function buildRepairPlan(input, reliabilityEvidence = collectReliabilityEvidence(input.reliabilityStore, input.piboSessionId, input.eventId)) {
229
+ if (!input.store.exists)
230
+ throw new Error(`Debug store "pibo-data" not found at ${input.store.path}`);
231
+ const db = new DatabaseSync(input.store.path, { readOnly: true });
232
+ db.exec("BEGIN");
233
+ try {
234
+ const plan = buildRepairPlanFromDb(db, input.piboSessionId, input.eventId, reliabilityEvidence, input.adapterEvidence);
235
+ db.exec("COMMIT");
236
+ return plan;
237
+ }
238
+ catch (error) {
239
+ if (db.isTransaction)
240
+ db.exec("ROLLBACK");
241
+ throw error;
242
+ }
243
+ finally {
244
+ db.close();
245
+ }
246
+ }
247
+ function buildRepairPlanFromDb(db, piboSessionId, eventId, reliability, adapterEvidence) {
248
+ const session = db.prepare("SELECT status, workspace, created_at, updated_at, pi_session_id FROM sessions WHERE id = ? AND deleted_at IS NULL").get(piboSessionId);
249
+ const counts = db.prepare(`
250
+ SELECT
251
+ SUM(type = 'message_started') AS messageStarted,
252
+ SUM(type = 'assistant_message') AS assistantMessages,
253
+ SUM(type = 'message_finished') AS messageFinished,
254
+ SUM(type = 'session_error') AS sessionErrors,
255
+ SUM(type = 'pibo.output.identity_collision') AS identityCollisions
256
+ FROM event_log
257
+ WHERE session_id = ? AND event_id = ?
258
+ `).get(piboSessionId, eventId);
259
+ const productAssistantMessages = tableExists(db, "chat_messages")
260
+ ? Number(db.prepare("SELECT COUNT(*) AS count FROM chat_messages WHERE session_id = ? AND turn_id = ? AND role = 'assistant' AND status = 'complete'").get(piboSessionId, eventId).count)
261
+ : 0;
262
+ const productRunningMessages = tableExists(db, "chat_messages")
263
+ ? Number(db.prepare("SELECT COUNT(*) AS count FROM chat_messages WHERE session_id = ? AND turn_id = ? AND role = 'assistant' AND status IN ('running', 'streaming')").get(piboSessionId, eventId).count)
264
+ : 0;
265
+ const adapterEntries = (adapterEvidence?.entries ?? []).filter((entry) => entry.type === "message" && entry.turnId === eventId);
266
+ const adapterCompleted = adapterEntries.filter((entry) => entry.type === "message" && entry.role === "assistant" && entry.status !== "running" && entry.status !== "error");
267
+ const adapterRunning = adapterEntries.filter((entry) => entry.type === "message" && entry.status === "running");
268
+ const observed = {
269
+ ...(session?.status ? { sessionStatus: session.status } : {}),
270
+ messageStarted: Number(counts.messageStarted ?? 0),
271
+ assistantMessages: Number(counts.assistantMessages ?? 0),
272
+ productAssistantMessages,
273
+ messageFinished: Number(counts.messageFinished ?? 0),
274
+ sessionErrors: Number(counts.sessionErrors ?? 0),
275
+ openThinkingParts: countOpenThinkingParts(db, piboSessionId, eventId),
276
+ openToolInvocations: countOpenToolInvocations(db, piboSessionId, eventId),
277
+ identityCollisions: Number(counts.identityCollisions ?? 0),
278
+ reliabilityTerminalCandidates: reliability.terminalEvents.length,
279
+ adapterCompletedAssistantMessages: adapterCompleted.length,
280
+ adapterRunningEntries: adapterRunning.length,
281
+ };
282
+ const evidence = [];
283
+ if (reliability.terminalEvents.length)
284
+ evidence.push({ source: "reliability_payload", kind: "exact_terminal", count: reliability.terminalEvents.length, references: reliability.references });
285
+ const productCompleted = observed.assistantMessages + productAssistantMessages;
286
+ if (productCompleted)
287
+ evidence.push({ source: "pibo_product_history", kind: "completed_assistant", count: productCompleted, references: productReferences(db, piboSessionId, eventId) });
288
+ if (adapterCompleted.length)
289
+ evidence.push({ source: "adapter_history", kind: "completed_assistant", count: adapterCompleted.length, references: adapterCompleted.map((entry) => entry.id).slice(0, 20) });
290
+ if (adapterRunning.length)
291
+ evidence.push({ source: "adapter_history", kind: "running_entry", count: adapterRunning.length, references: adapterRunning.map((entry) => entry.id).slice(0, 20) });
292
+ if (productRunningMessages)
293
+ evidence.push({ source: "pibo_product_history", kind: "running_entry", count: productRunningMessages, references: [] });
294
+ const base = { piboSessionId, eventId, sessionExists: Boolean(session), observed, evidence };
295
+ const refuse = (reason) => ({
296
+ inspection: { ...base, repairable: false, reason },
297
+ evidenceReferences: [],
298
+ });
299
+ if (!session)
300
+ return refuse("session_not_found");
301
+ if (session.status === "running")
302
+ return refuse("session_active");
303
+ if (observed.messageStarted === 0)
304
+ return refuse("message_start_missing");
305
+ if (observed.messageStarted !== 1)
306
+ return refuse("message_start_duplicated");
307
+ if (observed.messageFinished + observed.sessionErrors > 0)
308
+ return refuse("already_terminal");
309
+ if (adapterRunning.length || productRunningMessages)
310
+ return refuse("session_active");
311
+ const exactTerminal = uniqueReliabilityTerminal(reliability.terminalEvents.map((item) => item.event));
312
+ if (exactTerminal === "conflict")
313
+ return refuse("evidence_conflict");
314
+ if (observed.openThinkingParts || observed.openToolInvocations)
315
+ return refuse("lifecycle_open");
316
+ if (exactTerminal) {
317
+ const sources = ["reliability_payload"];
318
+ return {
319
+ inspection: {
320
+ ...base,
321
+ repairable: true,
322
+ plannedEvent: {
323
+ type: exactTerminal.type,
324
+ ...(exactTerminal.type === "message_finished" && "source" in exactTerminal && exactTerminal.source ? { source: exactTerminal.source } : {}),
325
+ evidenceSources: sources,
326
+ },
327
+ },
328
+ terminalEvent: exactTerminal,
329
+ evidenceReferences: reliability.references,
330
+ };
331
+ }
332
+ const completedSources = [];
333
+ if (productCompleted)
334
+ completedSources.push("pibo_product_history");
335
+ if (adapterCompleted.length)
336
+ completedSources.push("adapter_history");
337
+ if (!completedSources.length)
338
+ return refuse("evidence_missing");
339
+ const source = messageSource(db, piboSessionId, eventId);
340
+ const terminalEvent = { type: "message_finished", piboSessionId, eventId, ...source };
341
+ return {
342
+ inspection: {
343
+ ...base,
344
+ repairable: true,
345
+ plannedEvent: { type: "message_finished", ...source, evidenceSources: completedSources },
346
+ },
347
+ terminalEvent,
348
+ evidenceReferences: evidence.flatMap((item) => item.references).slice(0, 50),
349
+ };
350
+ }
351
+ function collectReliabilityEvidence(store, piboSessionId, eventId) {
352
+ if (!store?.exists)
353
+ return { terminalEvents: [], references: [] };
354
+ const db = new DatabaseSync(store.path, { readOnly: true });
355
+ db.exec("BEGIN");
356
+ try {
357
+ const rows = [];
358
+ for (const table of ["pibo_jobs", "pibo_dead_jobs"]) {
359
+ if (!tableExists(db, table))
360
+ continue;
361
+ const tableRows = db.prepare(`
362
+ SELECT job_id AS jobId, payload_json AS payloadJson
363
+ FROM ${table}
364
+ WHERE queue IN ('output-persistence', 'output-persistence-cli')
365
+ ORDER BY updated_at DESC
366
+ LIMIT ?
367
+ `).all(RELIABILITY_SCAN_LIMIT);
368
+ rows.push(...tableRows.map((row) => ({ ...row, table })));
369
+ }
370
+ const terminalEvents = [];
371
+ for (const row of rows) {
372
+ const payload = parseJson(row.payloadJson);
373
+ for (const event of outputEventsInPayload(payload)) {
374
+ if (event.piboSessionId !== piboSessionId || !("eventId" in event) || event.eventId !== eventId)
375
+ continue;
376
+ if (event.type !== "message_finished" && event.type !== "session_error")
377
+ continue;
378
+ terminalEvents.push({ event, reference: `${row.table}:${row.jobId}` });
379
+ }
380
+ }
381
+ const evidence = {
382
+ terminalEvents,
383
+ references: [...new Set(terminalEvents.map((item) => item.reference))].slice(0, 50),
384
+ };
385
+ db.exec("COMMIT");
386
+ return evidence;
387
+ }
388
+ catch (error) {
389
+ if (db.isTransaction)
390
+ db.exec("ROLLBACK");
391
+ throw error;
392
+ }
393
+ finally {
394
+ db.close();
395
+ }
396
+ }
397
+ function outputEventsInPayload(value) {
398
+ const found = [];
399
+ let visited = 0;
400
+ const visit = (candidate, depth) => {
401
+ if (depth > 8 || visited >= 2000 || candidate === null || typeof candidate !== "object")
402
+ return;
403
+ visited += 1;
404
+ if (isPiboOutputEvent(candidate)) {
405
+ found.push(candidate);
406
+ return;
407
+ }
408
+ if (Array.isArray(candidate)) {
409
+ for (const item of candidate.slice(0, 500))
410
+ visit(item, depth + 1);
411
+ return;
412
+ }
413
+ const record = candidate;
414
+ for (const key of ["event", "deliveries", "state", "payload"]) {
415
+ if (key in record)
416
+ visit(record[key], depth + 1);
417
+ }
418
+ };
419
+ visit(value, 0);
420
+ return found;
421
+ }
422
+ function uniqueReliabilityTerminal(events) {
423
+ if (!events.length)
424
+ return undefined;
425
+ const bySignature = new Map();
426
+ for (const event of events)
427
+ bySignature.set(stableJson(event), event);
428
+ if (bySignature.size !== 1)
429
+ return "conflict";
430
+ return [...bySignature.values()][0];
431
+ }
432
+ function productReferences(db, piboSessionId, eventId) {
433
+ const eventRows = db.prepare("SELECT stream_id AS id FROM event_log WHERE session_id = ? AND event_id = ? AND type = 'assistant_message' ORDER BY stream_id LIMIT 20").all(piboSessionId, eventId);
434
+ const messageRows = tableExists(db, "chat_messages")
435
+ ? db.prepare("SELECT id FROM chat_messages WHERE session_id = ? AND turn_id = ? AND role = 'assistant' AND status = 'complete' ORDER BY sequence LIMIT 20").all(piboSessionId, eventId)
436
+ : [];
437
+ return [...eventRows.map((row) => `event_log:${row.id}`), ...messageRows.map((row) => `chat_messages:${row.id}`)];
438
+ }
439
+ function countOpenThinkingParts(db, piboSessionId, eventId) {
440
+ const row = db.prepare(`
441
+ SELECT COUNT(*) AS count FROM (
442
+ SELECT CASE WHEN json_valid(attributes_json)
443
+ THEN COALESCE(json_extract(attributes_json, '$.thinkingIndex'), json_extract(attributes_json, '$.contentIndex'), 0)
444
+ ELSE 0 END AS thinking_index
445
+ FROM event_log
446
+ WHERE session_id = ? AND event_id = ? AND type IN ('thinking_started', 'thinking_finished')
447
+ GROUP BY thinking_index
448
+ HAVING SUM(type = 'thinking_started') != 1
449
+ OR SUM(type = 'thinking_finished') != 1
450
+ )
451
+ `).get(piboSessionId, eventId);
452
+ return Number(row.count);
453
+ }
454
+ function countOpenToolInvocations(db, piboSessionId, eventId) {
455
+ const row = db.prepare(`
456
+ SELECT COUNT(*) AS count FROM (
457
+ SELECT
458
+ CASE WHEN tool_call_id IS NOT NULL THEN tool_call_id
459
+ WHEN json_valid(attributes_json) THEN json_extract(attributes_json, '$.toolCallId') END AS tool_call_id,
460
+ CASE WHEN json_valid(attributes_json) THEN COALESCE(json_extract(attributes_json, '$.toolInvocationOrdinal'), 0) ELSE 0 END AS ordinal
461
+ FROM event_log
462
+ WHERE session_id = ? AND event_id = ?
463
+ AND type IN ('tool_call', 'tool_execution_started', 'tool_execution_finished')
464
+ GROUP BY tool_call_id, ordinal
465
+ HAVING SUM(type = 'tool_call') != 1
466
+ OR SUM(type = 'tool_execution_started') != 1
467
+ OR SUM(type = 'tool_execution_finished') != 1
468
+ )
469
+ `).get(piboSessionId, eventId);
470
+ return Number(row.count);
471
+ }
472
+ function messageSource(db, piboSessionId, eventId) {
473
+ const row = db.prepare(`
474
+ SELECT CASE WHEN json_valid(attributes_json)
475
+ THEN COALESCE(json_extract(attributes_json, '$.source'), json_extract(attributes_json, '$.inlinePayload.source')) END AS source
476
+ FROM event_log
477
+ WHERE session_id = ? AND event_id = ? AND type = 'message_started'
478
+ ORDER BY stream_id ASC
479
+ LIMIT 1
480
+ `).get(piboSessionId, eventId);
481
+ return row?.source && EVENT_SOURCES.has(row.source) ? { source: row.source } : {};
482
+ }
483
+ function readRuntimeBinding(db, piboSessionId, session) {
484
+ if (!tableExists(db, "session_runtime_bindings")) {
485
+ return {
486
+ piboSessionId,
487
+ runtimeInstanceId: "pi",
488
+ adapterId: "pi",
489
+ nativeSessionId: session.pi_session_id ?? undefined,
490
+ state: session.pi_session_id ? "bound" : "unbound",
491
+ protocol: "pi-sdk",
492
+ metadata: { source: "legacy-synthesized" },
493
+ revision: 1,
494
+ createdAt: session.created_at,
495
+ updatedAt: session.updated_at,
496
+ };
497
+ }
498
+ const row = db.prepare("SELECT * FROM session_runtime_bindings WHERE pibo_session_id = ?").get(piboSessionId);
499
+ if (!row)
500
+ return undefined;
501
+ const locator = parseObject(typeof row.locator_json === "string" ? row.locator_json : null);
502
+ return {
503
+ piboSessionId,
504
+ runtimeInstanceId: String(row.runtime_instance_id),
505
+ adapterId: String(row.runtime_adapter_id),
506
+ nativeSessionId: typeof row.native_session_id === "string" ? row.native_session_id : undefined,
507
+ state: row.binding_state,
508
+ protocol: typeof row.protocol === "string" ? row.protocol : undefined,
509
+ protocolVersion: typeof row.protocol_version === "string" ? row.protocol_version : undefined,
510
+ adapterVersion: typeof row.adapter_version === "string" ? row.adapter_version : undefined,
511
+ locator: typeof locator.kind === "string" ? locator : undefined,
512
+ metadata: parseObject(typeof row.metadata_json === "string" ? row.metadata_json : null),
513
+ revision: Number(row.revision),
514
+ createdAt: String(row.created_at),
515
+ updatedAt: String(row.updated_at),
516
+ };
517
+ }
518
+ function resultBase(store, piboSessionId, mode) {
519
+ return {
520
+ resultType: "debug.repair.output",
521
+ mode,
522
+ store: { path: store.path, exists: store.exists },
523
+ warnings: resultWarnings(),
524
+ nextCommands: resultNextCommands(piboSessionId),
525
+ };
526
+ }
527
+ function resultWarnings() {
528
+ return [
529
+ "Repair refuses active or ambiguous turns and never invents assistant content.",
530
+ "Every applied terminal event is paired with a pibo.output.repair_applied audit event.",
531
+ "This repair does not delete or replay pending or dead output-persistence jobs.",
532
+ ];
533
+ }
534
+ function resultNextCommands(piboSessionId) {
535
+ return [
536
+ `pibo debug trace ${piboSessionId} --check`,
537
+ `pibo debug events ${piboSessionId} --limit 50`,
538
+ `pibo debug jobs dead --queue output-persistence`,
539
+ ];
540
+ }
541
+ function normalizeRepairLimit(value) {
542
+ const parsed = typeof value === "number" ? value : value === undefined ? 20 : Number.parseInt(value, 10);
543
+ if (!Number.isSafeInteger(parsed) || parsed < 1)
544
+ throw new Error("--limit must be a positive integer");
545
+ return Math.min(parsed, REPAIR_LIMIT_MAX);
546
+ }
547
+ function nextEventSequence(db, piboSessionId) {
548
+ return Number(db.prepare("SELECT COALESCE(MAX(session_sequence), 0) + 1 AS nextSequence FROM event_log WHERE session_id = ?").get(piboSessionId).nextSequence);
549
+ }
550
+ function tableExists(db, table) {
551
+ return db.prepare("SELECT name FROM sqlite_schema WHERE type = 'table' AND name = ?").get(table) !== undefined;
552
+ }
553
+ function parseJson(value) {
554
+ try {
555
+ return JSON.parse(value);
556
+ }
557
+ catch {
558
+ return undefined;
559
+ }
560
+ }
561
+ function parseObject(value) {
562
+ if (!value)
563
+ return {};
564
+ try {
565
+ const parsed = JSON.parse(value);
566
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
567
+ }
568
+ catch {
569
+ return {};
570
+ }
571
+ }
572
+ function stableJson(value) {
573
+ if (value === null || typeof value !== "object")
574
+ return JSON.stringify(value);
575
+ if (Array.isArray(value))
576
+ return `[${value.map(stableJson).join(",")}]`;
577
+ return `{${Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, child]) => `${JSON.stringify(key)}:${stableJson(child)}`).join(",")}}`;
578
+ }
579
+ function redactError(error) {
580
+ return (error instanceof Error ? error.message : String(error))
581
+ .replace(/(bearer|token|secret|password|api[_-]?key)\s*[:=]\s*[^\s,;]+/gi, "$1=[redacted]")
582
+ .replace(/\/\/[^\s/@:]+:[^\s/@]+@/g, "//[redacted]@")
583
+ .slice(0, 500);
584
+ }
@@ -86,6 +86,7 @@ export async function inspectDebugTrace(piboSessionId, stores, options = {}) {
86
86
  errorNodeCount: statusSummary.errorNodeCount,
87
87
  nodes: filtered,
88
88
  rawNodeCount: rows.length,
89
+ ...(view.integrityStatus ? { integrityStatus: view.integrityStatus } : {}),
89
90
  ...(options.check ? { checks: checkTraceView(view, adapterIssues) } : {}),
90
91
  nextCommands: buildTraceNextCommands(view.piboSessionId, filtered),
91
92
  };
@@ -121,6 +122,7 @@ export function formatDebugTrace(result, options = {}) {
121
122
  ...(result.nativeSessionId ? [`nativeSessionId: ${result.nativeSessionId}`] : []),
122
123
  ...(result.runtimeBindingState ? [`runtimeBindingState: ${result.runtimeBindingState}`] : []),
123
124
  `historySource: ${result.historySource}`,
125
+ ...(result.integrityStatus ? [`integrityStatus: ${result.integrityStatus}`] : []),
124
126
  `title: ${result.title}`,
125
127
  `status: ${result.status}`,
126
128
  `statusSource: ${result.statusSource}`,
@@ -514,13 +514,13 @@ export class PiboReliabilityStore {
514
514
  return Number(result.changes ?? 0) > 0;
515
515
  });
516
516
  }
517
- fail(jobId, workerId, error, claimToken) {
517
+ fail(jobId, workerId, error, claimToken, reason = "failed") {
518
518
  const timestamp = now();
519
519
  return this.inImmediateTransaction(() => {
520
520
  const row = this.getLiveWorkerJob(jobId, workerId, timestamp, claimToken);
521
521
  if (!row)
522
522
  return false;
523
- this.moveJobToDead(row, error, "failed", timestamp);
523
+ this.moveJobToDead(row, error, reason, timestamp);
524
524
  return true;
525
525
  });
526
526
  }