@pasko70/pibo 1.12.0 → 1.12.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 (43) hide show
  1. package/dist/apps/chat/chat-trace-helpers.js +59 -2
  2. package/dist/apps/chat/data/timeline-query-service.js +1 -1
  3. package/dist/apps/chat/web-app.js +16 -8
  4. package/dist/apps/chat-ui/assets/{dist-DiPvo4rq.js → dist-3bD-92Wo.js} +1 -1
  5. package/dist/apps/chat-ui/assets/{dist-DxJWicji.js → dist-BFjy5Y59.js} +1 -1
  6. package/dist/apps/chat-ui/assets/{dist-D1U9ck8z.js → dist-BKwQlbhS.js} +1 -1
  7. package/dist/apps/chat-ui/assets/{dist-B2Pozq2c.js → dist-Cp68y_h5.js} +1 -1
  8. package/dist/apps/chat-ui/assets/{dist-DYdgxFZX.js → dist-DE0y_EI7.js} +1 -1
  9. package/dist/apps/chat-ui/assets/{dist-Ci4lft6y.js → dist-DPb0yDi4.js} +1 -1
  10. package/dist/apps/chat-ui/assets/{dist-CkLit_da.js → dist-D_dQ5GaA.js} +1 -1
  11. package/dist/apps/chat-ui/assets/{dist-Bgv5Fm3v.js → dist-Daq2KRpq.js} +1 -1
  12. package/dist/apps/chat-ui/assets/{dist-D4qbF8wy.js → dist-aZnuCezL.js} +1 -1
  13. package/dist/apps/chat-ui/assets/{dist-la7ZfDGk.js → dist-q6Vpm5yH.js} +1 -1
  14. package/dist/apps/chat-ui/assets/index-DXPKuGfs.js +237 -0
  15. package/dist/apps/chat-ui/index.html +1 -1
  16. package/dist/apps/chat-vscode-web/assets/index-B3dSrp-L.js +41 -0
  17. package/dist/apps/chat-vscode-web/index.html +1 -1
  18. package/dist/apps/vscode-artifacts/latest.vsix +0 -0
  19. package/dist/apps/vscode-artifacts/pibo-vscode-ext-1.12.1.vsix +0 -0
  20. package/dist/core/gateway-resource-guard.js +46 -15
  21. package/dist/core/routed-session.js +40 -1
  22. package/dist/core/runtime.js +2 -0
  23. package/dist/core/session-errors.js +3 -0
  24. package/dist/core/session-router.js +25 -2
  25. package/dist/core/transcript-integrity.js +431 -0
  26. package/dist/data/pibo-store.js +2 -0
  27. package/dist/data/telemetry.js +148 -0
  28. package/dist/debug/index.js +11 -0
  29. package/dist/gateway/server.js +1 -0
  30. package/dist/reliability/store.js +11 -6
  31. package/dist/runs/registry.js +38 -1
  32. package/dist/runs/resource-isolation.js +437 -0
  33. package/dist/runs/tools.js +6 -3
  34. package/dist/sessions/pibo-data-store.js +114 -0
  35. package/dist/shared/trace-engine.js +3 -3
  36. package/dist/shared/trace-event-projection.js +165 -36
  37. package/dist/shared/trace-nodes.js +6 -4
  38. package/dist/shared/trace-page-merge.js +106 -2
  39. package/dist/shared/trace-transcript.js +194 -36
  40. package/package.json +1 -1
  41. package/dist/apps/chat-ui/assets/index-CYxPvrxL.js +0 -237
  42. package/dist/apps/chat-vscode-web/assets/index-CFSHKXsQ.js +0 -41
  43. package/dist/apps/vscode-artifacts/pibo-vscode-ext-1.12.0.vsix +0 -0
@@ -0,0 +1,431 @@
1
+ import { sessionEntryToContextMessages, } from "@earendil-works/pi-coding-agent";
2
+ export const PIBO_TRANSCRIPT_INTEGRITY_RESUME_MESSAGE_TYPE = "pibo-transcript-integrity-resume";
3
+ export const PIBO_TRANSCRIPT_INTEGRITY_RESUME_PROMPT = "Continue the interrupted task autonomously from the repaired transcript. Do not rerun completed tools, wait for additional user input, or mention transcript repair unless it affects the result.";
4
+ export const PIBO_TRANSCRIPT_INTEGRITY_ENTRY_TYPE = "pibo-transcript-integrity";
5
+ const installedStates = new WeakMap();
6
+ export class PiboTranscriptIntegrityError extends Error {
7
+ constructor(message) {
8
+ super(message);
9
+ this.name = "PiboTranscriptIntegrityError";
10
+ }
11
+ }
12
+ export function validatePiboTranscriptIntegrityMessages(messages) {
13
+ return validateProjectedMessages(messages.map((message) => ({ message })));
14
+ }
15
+ export function installPiboTranscriptIntegrity(session) {
16
+ const existing = installedStates.get(session);
17
+ if (existing)
18
+ return [...existing.reports];
19
+ const manager = session.sessionManager;
20
+ const state = {
21
+ session,
22
+ originalAppendMessage: manager.appendMessage.bind(manager),
23
+ restoredAssistants: new Map(),
24
+ reports: [],
25
+ continuationPending: false,
26
+ continuationInProgress: false,
27
+ };
28
+ installedStates.set(session, state);
29
+ manager.appendMessage = ((message) => appendMessageWithIntegrity(state, message));
30
+ patchProviderBoundary(state);
31
+ patchCompactionBoundaries(state);
32
+ reconcilePiboTranscriptIntegrity(session, "load");
33
+ return [...state.reports];
34
+ }
35
+ export function reconcilePiboTranscriptIntegrity(session, boundary) {
36
+ const state = installedStates.get(session);
37
+ if (!state)
38
+ throw new PiboTranscriptIntegrityError("Transcript integrity is not installed for this session");
39
+ const manager = session.sessionManager;
40
+ const activeEntries = manager.buildContextEntries();
41
+ const projected = projectEntries(activeEntries);
42
+ const [issue] = validateProjectedMessages(projected, manager.getEntries());
43
+ if (!issue)
44
+ return undefined;
45
+ const invalidEntry = issue.entryId ? manager.getEntry(issue.entryId) : undefined;
46
+ if (!invalidEntry) {
47
+ return quarantineRuntimeTranscript(state, boundary, issue);
48
+ }
49
+ const invalidEntryIndex = activeEntries.findIndex((entry) => entry.id === invalidEntry.id);
50
+ const removedEntryIds = invalidEntryIndex >= 0
51
+ ? activeEntries.slice(invalidEntryIndex).map((entry) => entry.id)
52
+ : [invalidEntry.id];
53
+ const resultMessage = isToolResultMessageEntry(invalidEntry) ? invalidEntry.message : undefined;
54
+ const authoritativeAssistant = resultMessage
55
+ ? uniqueAuthoritativeAssistant(manager.getEntries(), issue.toolCallId)
56
+ : undefined;
57
+ let report;
58
+ branchTo(manager, invalidEntry.parentId);
59
+ if (authoritativeAssistant && resultMessage && (issue.relation === "orphan_result" || issue.relation === "wrong_branch_result")) {
60
+ const repairId = manager.appendCustomEntry(PIBO_TRANSCRIPT_INTEGRITY_ENTRY_TYPE, repairMetadata({
61
+ boundary,
62
+ relation: issue.relation,
63
+ action: "restored_pair",
64
+ toolCallIds: assistantToolCalls(authoritativeAssistant.message).map((call) => call.id),
65
+ removedEntryIds,
66
+ restoredEntryIds: [authoritativeAssistant.id, invalidEntry.id],
67
+ phase: "journal_started",
68
+ }));
69
+ const restoredAssistantEntryId = state.originalAppendMessage(authoritativeAssistant.message);
70
+ rememberRestoredAssistant(state, authoritativeAssistant.message, restoredAssistantEntryId);
71
+ const restoredResultEntryId = state.originalAppendMessage(resultMessage);
72
+ manager.appendCustomEntry(PIBO_TRANSCRIPT_INTEGRITY_ENTRY_TYPE, {
73
+ repairId,
74
+ phase: "journal_committed",
75
+ restoredAssistantEntryId,
76
+ restoredResultEntryId,
77
+ });
78
+ report = {
79
+ repairId,
80
+ boundary,
81
+ relation: issue.relation,
82
+ action: "restored_pair",
83
+ toolCallIds: assistantToolCalls(authoritativeAssistant.message).map((call) => call.id),
84
+ removedEntryIds,
85
+ restoredEntryIds: [authoritativeAssistant.id, invalidEntry.id],
86
+ continuationRequired: boundary === "load",
87
+ };
88
+ }
89
+ else {
90
+ const repairId = manager.appendCustomEntry(PIBO_TRANSCRIPT_INTEGRITY_ENTRY_TYPE, repairMetadata({
91
+ boundary,
92
+ relation: issue.relation,
93
+ action: "quarantined_tail",
94
+ toolCallIds: [issue.toolCallId],
95
+ removedEntryIds,
96
+ restoredEntryIds: [],
97
+ phase: "quarantined",
98
+ }));
99
+ report = {
100
+ repairId,
101
+ boundary,
102
+ relation: issue.relation,
103
+ action: "quarantined_tail",
104
+ toolCallIds: [issue.toolCallId],
105
+ removedEntryIds,
106
+ restoredEntryIds: [],
107
+ continuationRequired: boundary === "load",
108
+ };
109
+ }
110
+ syncAgentTranscript(session);
111
+ assertSessionTranscriptValid(session, boundary);
112
+ recordReport(state, report);
113
+ return report;
114
+ }
115
+ export function claimPiboTranscriptIntegrityContinuation(session) {
116
+ const state = installedStates.get(session);
117
+ if (!state || !state.continuationPending || state.continuationInProgress)
118
+ return [];
119
+ const reports = state.reports.filter((report) => report.continuationRequired);
120
+ if (reports.length === 0) {
121
+ state.continuationPending = false;
122
+ return [];
123
+ }
124
+ state.continuationPending = false;
125
+ state.continuationInProgress = true;
126
+ session.sessionManager.appendCustomEntry(PIBO_TRANSCRIPT_INTEGRITY_ENTRY_TYPE, {
127
+ phase: "continuation_started",
128
+ repairIds: reports.map((report) => report.repairId),
129
+ });
130
+ return reports;
131
+ }
132
+ export function settlePiboTranscriptIntegrityContinuation(session, outcome, error) {
133
+ const state = installedStates.get(session);
134
+ if (!state?.continuationInProgress)
135
+ return;
136
+ state.continuationInProgress = false;
137
+ session.sessionManager.appendCustomEntry(PIBO_TRANSCRIPT_INTEGRITY_ENTRY_TYPE, {
138
+ phase: `continuation_${outcome}`,
139
+ ...(error ? { errorClass: error instanceof Error ? error.name : "Error" } : {}),
140
+ });
141
+ }
142
+ function patchProviderBoundary(state) {
143
+ const agent = state.session.agent;
144
+ const previous = agent.transformContext?.bind(agent);
145
+ const repairedProviderMessages = async (signal) => {
146
+ const repaired = state.session.sessionManager.buildSessionContext().messages;
147
+ if (!previous)
148
+ return repaired;
149
+ const transformed = await previous(repaired, signal);
150
+ return validatePiboTranscriptIntegrityMessages(transformed).length === 0 ? transformed : repaired;
151
+ };
152
+ agent.transformContext = async (messages, signal) => {
153
+ const transformed = previous ? await previous(messages, signal) : messages;
154
+ const durableRepair = reconcilePiboTranscriptIntegrity(state.session, "before_provider");
155
+ if (durableRepair)
156
+ return repairedProviderMessages(signal);
157
+ const [runtimeIssue] = validatePiboTranscriptIntegrityMessages(transformed);
158
+ if (!runtimeIssue)
159
+ return transformed;
160
+ quarantineRuntimeTranscript(state, "before_provider", runtimeIssue);
161
+ return repairedProviderMessages(signal);
162
+ };
163
+ }
164
+ function patchCompactionBoundaries(state) {
165
+ const session = state.session;
166
+ const originalCompact = session.compact.bind(session);
167
+ session.compact = (async (...args) => {
168
+ reconcilePiboTranscriptIntegrity(session, "before_compaction");
169
+ const result = await originalCompact(...args);
170
+ reconcilePiboTranscriptIntegrity(session, "after_compaction");
171
+ return result;
172
+ });
173
+ }
174
+ function appendMessageWithIntegrity(state, message) {
175
+ if (isAssistantMessage(message)) {
176
+ const restoredEntryId = restoredAssistantEntryId(state, message);
177
+ return restoredEntryId ?? state.originalAppendMessage(message);
178
+ }
179
+ if (!isToolResultMessage(message))
180
+ return state.originalAppendMessage(message);
181
+ const activeMessages = state.session.sessionManager.buildSessionContext().messages;
182
+ if (activeToolCall(activeMessages, message.toolCallId))
183
+ return state.originalAppendMessage(message);
184
+ const authoritativeAssistant = uniqueAssistantMessage(state.session.agent.state.messages, message.toolCallId);
185
+ if (authoritativeAssistant) {
186
+ const toolCallIds = assistantToolCalls(authoritativeAssistant).map((call) => call.id);
187
+ const repairId = state.session.sessionManager.appendCustomEntry(PIBO_TRANSCRIPT_INTEGRITY_ENTRY_TYPE, repairMetadata({
188
+ boundary: "persistence",
189
+ relation: "orphan_result",
190
+ action: "restored_pair",
191
+ toolCallIds,
192
+ removedEntryIds: [],
193
+ restoredEntryIds: [],
194
+ phase: "journal_started",
195
+ }));
196
+ const assistantEntryId = state.originalAppendMessage(authoritativeAssistant);
197
+ rememberRestoredAssistant(state, authoritativeAssistant, assistantEntryId);
198
+ const resultEntryId = state.originalAppendMessage(message);
199
+ state.session.sessionManager.appendCustomEntry(PIBO_TRANSCRIPT_INTEGRITY_ENTRY_TYPE, {
200
+ repairId,
201
+ phase: "journal_committed",
202
+ assistantEntryId,
203
+ resultEntryId,
204
+ });
205
+ recordReport(state, {
206
+ repairId,
207
+ boundary: "persistence",
208
+ relation: "orphan_result",
209
+ action: "restored_pair",
210
+ toolCallIds,
211
+ removedEntryIds: [],
212
+ restoredEntryIds: [assistantEntryId, resultEntryId],
213
+ continuationRequired: false,
214
+ });
215
+ return resultEntryId;
216
+ }
217
+ const repairId = state.session.sessionManager.appendCustomEntry(PIBO_TRANSCRIPT_INTEGRITY_ENTRY_TYPE, repairMetadata({
218
+ boundary: "persistence",
219
+ relation: "orphan_result",
220
+ action: "quarantined_runtime_result",
221
+ toolCallIds: [message.toolCallId],
222
+ removedEntryIds: [],
223
+ restoredEntryIds: [],
224
+ phase: "quarantined",
225
+ }));
226
+ recordReport(state, {
227
+ repairId,
228
+ boundary: "persistence",
229
+ relation: "orphan_result",
230
+ action: "quarantined_runtime_result",
231
+ toolCallIds: [message.toolCallId],
232
+ removedEntryIds: [],
233
+ restoredEntryIds: [],
234
+ continuationRequired: false,
235
+ });
236
+ syncAgentTranscript(state.session);
237
+ return repairId;
238
+ }
239
+ function quarantineRuntimeTranscript(state, boundary, issue) {
240
+ const repairId = state.session.sessionManager.appendCustomEntry(PIBO_TRANSCRIPT_INTEGRITY_ENTRY_TYPE, repairMetadata({
241
+ boundary,
242
+ relation: issue.relation,
243
+ action: "quarantined_runtime_result",
244
+ toolCallIds: [issue.toolCallId],
245
+ removedEntryIds: [],
246
+ restoredEntryIds: [],
247
+ phase: "quarantined",
248
+ }));
249
+ const report = {
250
+ repairId,
251
+ boundary,
252
+ relation: issue.relation,
253
+ action: "quarantined_runtime_result",
254
+ toolCallIds: [issue.toolCallId],
255
+ removedEntryIds: [],
256
+ restoredEntryIds: [],
257
+ continuationRequired: boundary === "load",
258
+ };
259
+ syncAgentTranscript(state.session);
260
+ recordReport(state, report);
261
+ return report;
262
+ }
263
+ function validateProjectedMessages(projected, allEntries = []) {
264
+ const calls = new Map();
265
+ const results = new Map();
266
+ const issues = [];
267
+ const allCalls = allAssistantCallIds(allEntries);
268
+ for (let messageIndex = 0; messageIndex < projected.length; messageIndex += 1) {
269
+ const { message, entry } = projected[messageIndex];
270
+ if (isAssistantMessage(message)) {
271
+ for (const call of assistantToolCalls(message)) {
272
+ const existing = calls.get(call.id);
273
+ if (existing) {
274
+ issues.push({ relation: "duplicate_call", toolCallId: call.id, toolName: call.name, messageIndex, entryId: entry?.id });
275
+ continue;
276
+ }
277
+ calls.set(call.id, { name: call.name, messageIndex });
278
+ }
279
+ continue;
280
+ }
281
+ if (!isToolResultMessage(message))
282
+ continue;
283
+ if (results.has(message.toolCallId)) {
284
+ issues.push({ relation: "duplicate_result", toolCallId: message.toolCallId, toolName: message.toolName, messageIndex, entryId: entry?.id });
285
+ continue;
286
+ }
287
+ results.set(message.toolCallId, messageIndex);
288
+ const call = calls.get(message.toolCallId);
289
+ if (!call) {
290
+ issues.push({
291
+ relation: allCalls.has(message.toolCallId) ? "wrong_branch_result" : "orphan_result",
292
+ toolCallId: message.toolCallId,
293
+ toolName: message.toolName,
294
+ messageIndex,
295
+ entryId: entry?.id,
296
+ });
297
+ continue;
298
+ }
299
+ if (call.name !== message.toolName) {
300
+ issues.push({
301
+ relation: "tool_name_mismatch",
302
+ toolCallId: message.toolCallId,
303
+ toolName: message.toolName,
304
+ expectedToolName: call.name,
305
+ messageIndex,
306
+ entryId: entry?.id,
307
+ });
308
+ }
309
+ }
310
+ return issues;
311
+ }
312
+ function projectEntries(entries) {
313
+ return entries.flatMap((entry) => sessionEntryToContextMessages(entry).map((message) => ({ entry, message })));
314
+ }
315
+ function allAssistantCallIds(entries) {
316
+ const ids = new Set();
317
+ for (const entry of entries) {
318
+ if (!isAssistantMessageEntry(entry))
319
+ continue;
320
+ for (const call of assistantToolCalls(entry.message))
321
+ ids.add(call.id);
322
+ }
323
+ return ids;
324
+ }
325
+ function uniqueAuthoritativeAssistant(entries, toolCallId) {
326
+ const candidates = entries.filter((entry) => isAssistantMessageEntry(entry) && assistantToolCalls(entry.message).some((call) => call.id === toolCallId));
327
+ return candidates.length === 1 ? candidates[0] : undefined;
328
+ }
329
+ function uniqueAssistantMessage(messages, toolCallId) {
330
+ const candidates = messages.filter((message) => isAssistantMessage(message) && assistantToolCalls(message).some((call) => call.id === toolCallId));
331
+ return candidates.length === 1 ? candidates[0] : undefined;
332
+ }
333
+ function activeToolCall(messages, toolCallId) {
334
+ for (const message of messages) {
335
+ if (!isAssistantMessage(message))
336
+ continue;
337
+ const call = assistantToolCalls(message).find((candidate) => candidate.id === toolCallId);
338
+ if (call)
339
+ return call;
340
+ }
341
+ return undefined;
342
+ }
343
+ function assistantToolCalls(message) {
344
+ if (!isAssistantMessage(message))
345
+ return [];
346
+ return message.content.flatMap((part) => {
347
+ if (!part || typeof part !== "object")
348
+ return [];
349
+ const candidate = part;
350
+ return candidate.type === "toolCall" && typeof candidate.id === "string" && typeof candidate.name === "string"
351
+ ? [{ type: "toolCall", id: candidate.id, name: candidate.name, arguments: candidate.arguments }]
352
+ : [];
353
+ });
354
+ }
355
+ function isAssistantMessage(message) {
356
+ return Boolean(message && typeof message === "object" && message.role === "assistant" && Array.isArray(message.content));
357
+ }
358
+ function isToolResultMessage(message) {
359
+ return Boolean(message
360
+ && typeof message === "object"
361
+ && message.role === "toolResult"
362
+ && typeof message.toolCallId === "string"
363
+ && typeof message.toolName === "string");
364
+ }
365
+ function isAssistantMessageEntry(entry) {
366
+ return entry.type === "message" && isAssistantMessage(entry.message);
367
+ }
368
+ function isToolResultMessageEntry(entry) {
369
+ return entry.type === "message" && isToolResultMessage(entry.message);
370
+ }
371
+ function branchTo(manager, parentId) {
372
+ if (parentId)
373
+ manager.branch(parentId);
374
+ else
375
+ manager.resetLeaf();
376
+ }
377
+ function syncAgentTranscript(session) {
378
+ session.agent.state.messages = session.sessionManager.buildSessionContext().messages;
379
+ }
380
+ function assertSessionTranscriptValid(session, boundary) {
381
+ const issues = validateProjectedMessages(projectEntries(session.sessionManager.buildContextEntries()), session.sessionManager.getEntries());
382
+ if (issues.length === 0)
383
+ return;
384
+ const issue = issues[0];
385
+ throw new PiboTranscriptIntegrityError(`Transcript integrity repair failed at ${boundary}: ${issue.relation} for tool call ${issue.toolCallId}`);
386
+ }
387
+ function rememberRestoredAssistant(state, message, entryId) {
388
+ const fingerprint = assistantFingerprint(message);
389
+ for (const call of assistantToolCalls(message))
390
+ state.restoredAssistants.set(call.id, { entryId, fingerprint });
391
+ }
392
+ function restoredAssistantEntryId(state, message) {
393
+ const calls = assistantToolCalls(message);
394
+ if (calls.length === 0)
395
+ return undefined;
396
+ const fingerprint = assistantFingerprint(message);
397
+ const restored = calls.map((call) => state.restoredAssistants.get(call.id));
398
+ if (restored.some((item) => !item || item.fingerprint !== fingerprint))
399
+ return undefined;
400
+ const entryIds = new Set(restored.map((item) => item.entryId));
401
+ return entryIds.size === 1 ? restored[0].entryId : undefined;
402
+ }
403
+ function assistantFingerprint(message) {
404
+ return JSON.stringify({
405
+ content: message.content,
406
+ api: message.api,
407
+ provider: message.provider,
408
+ model: message.model,
409
+ responseId: message.responseId,
410
+ timestamp: message.timestamp,
411
+ });
412
+ }
413
+ function repairMetadata(input) {
414
+ return {
415
+ version: 1,
416
+ boundary: input.boundary,
417
+ relation: input.relation,
418
+ action: input.action,
419
+ phase: input.phase,
420
+ toolCallIds: input.toolCallIds,
421
+ removedEntryIds: input.removedEntryIds,
422
+ restoredEntryIds: input.restoredEntryIds,
423
+ removedCount: input.removedEntryIds.length,
424
+ restoredCount: input.restoredEntryIds.length,
425
+ };
426
+ }
427
+ function recordReport(state, report) {
428
+ state.reports.push(report);
429
+ if (report.continuationRequired)
430
+ state.continuationPending = true;
431
+ }
@@ -39,6 +39,8 @@ export class PiboDataStore {
39
39
  this.telemetry = new TelemetryStore(this.db);
40
40
  }
41
41
  transaction(action) {
42
+ if (this.db.isTransaction)
43
+ return action();
42
44
  this.db.exec("BEGIN IMMEDIATE");
43
45
  try {
44
46
  const result = action();
@@ -86,6 +86,154 @@ export class TelemetryStore {
86
86
  `).all(turnId);
87
87
  return rows.map(toolCallFromRow);
88
88
  }
89
+ listActiveTurns() {
90
+ const rows = this.db.prepare(`
91
+ SELECT * FROM telemetry_turns
92
+ WHERE status IN ('queued', 'running')
93
+ ORDER BY queued_at ASC, created_at ASC
94
+ `).all();
95
+ return rows.map(turnFromRow);
96
+ }
97
+ recoverInterruptedTurns(input = {}) {
98
+ const at = input.at ?? new Date().toISOString();
99
+ return this.transaction(() => this.listActiveTurns().map((turn) => {
100
+ const outcome = input.resolveOutcome?.(turn) ?? {
101
+ status: "aborted",
102
+ summary: "Turn was interrupted by gateway restart.",
103
+ };
104
+ const phaseStatus = outcome.status;
105
+ const providerStatus = outcome.status;
106
+ const toolStatus = outcome.status;
107
+ const phaseName = outcome.status === "timeout"
108
+ ? "timeout"
109
+ : outcome.status === "error"
110
+ ? "error"
111
+ : "abort";
112
+ for (const phase of this.listOpenPhasesForTurn(turn.turnId)) {
113
+ this.finishPhase(phase.phaseId, {
114
+ status: phaseStatus,
115
+ endedAt: at,
116
+ lastProgressAt: at,
117
+ summary: outcome.summary,
118
+ });
119
+ }
120
+ for (const request of this.listActiveProviderRequestsForTurn(turn.turnId)) {
121
+ this.upsertProviderRequest({
122
+ providerRequestId: request.providerRequestId,
123
+ piboSessionId: request.piboSessionId,
124
+ rootSessionId: request.rootSessionId,
125
+ roomId: request.roomId,
126
+ turnId: request.turnId,
127
+ phaseId: request.phaseId,
128
+ provider: request.provider,
129
+ api: request.api,
130
+ model: request.model,
131
+ transport: request.transport,
132
+ serviceTier: request.serviceTier,
133
+ status: providerStatus,
134
+ responseHeadersAt: request.responseHeadersAt,
135
+ firstByteAt: request.firstByteAt,
136
+ lastRawEventAt: request.lastRawEventAt,
137
+ lastNormalizedEventAt: request.lastNormalizedEventAt,
138
+ completedAt: at,
139
+ httpStatus: request.httpStatus,
140
+ upstreamResponseId: request.upstreamResponseId,
141
+ rawEventCount: request.rawEventCount,
142
+ normalizedEventCount: request.normalizedEventCount,
143
+ parseErrorCount: request.parseErrorCount,
144
+ unknownEventCount: request.unknownEventCount,
145
+ bytesReceived: request.bytesReceived,
146
+ eventTypeCounts: request.eventTypeCounts,
147
+ eventStreamId: request.eventStreamId,
148
+ eventId: request.eventId,
149
+ payloadRef: request.payloadRef,
150
+ errorCategory: "runtime_restart",
151
+ errorMessage: outcome.summary,
152
+ captureMode: request.captureMode,
153
+ retentionClass: request.retentionClass,
154
+ updatedAt: at,
155
+ });
156
+ }
157
+ for (const toolCall of this.listActiveToolCallsForTurn(turn.turnId)) {
158
+ const executionEndedAt = toolCall.executionStartedAt ? at : toolCall.executionEndedAt;
159
+ this.upsertToolCall({
160
+ toolCallId: toolCall.toolCallId,
161
+ piboSessionId: toolCall.piboSessionId,
162
+ rootSessionId: toolCall.rootSessionId,
163
+ roomId: toolCall.roomId,
164
+ turnId: toolCall.turnId,
165
+ providerRequestId: toolCall.providerRequestId,
166
+ providerItemId: toolCall.providerItemId,
167
+ outputIndex: toolCall.outputIndex,
168
+ toolName: toolCall.toolName,
169
+ status: toolStatus,
170
+ argsStartedAt: toolCall.argsStartedAt,
171
+ firstDeltaAt: toolCall.firstDeltaAt,
172
+ lastDeltaAt: toolCall.lastDeltaAt,
173
+ argsCompletedAt: toolCall.argsCompletedAt,
174
+ executionStartedAt: toolCall.executionStartedAt,
175
+ executionEndedAt,
176
+ durationMs: toolCall.executionStartedAt && executionEndedAt
177
+ ? Math.max(0, Date.parse(executionEndedAt) - Date.parse(toolCall.executionStartedAt))
178
+ : toolCall.durationMs,
179
+ argsBytes: toolCall.argsBytes,
180
+ parseStatus: toolCall.parseStatus,
181
+ safeArgKeys: toolCall.safeArgKeys,
182
+ eventStreamId: toolCall.eventStreamId,
183
+ eventId: toolCall.eventId,
184
+ payloadRef: toolCall.payloadRef,
185
+ runId: toolCall.runId,
186
+ errorCategory: "runtime_restart",
187
+ errorMessage: outcome.summary,
188
+ retentionClass: toolCall.retentionClass,
189
+ updatedAt: at,
190
+ });
191
+ }
192
+ this.upsertPhase({
193
+ phaseId: `${turn.turnId}:runtime_restart`,
194
+ turnId: turn.turnId,
195
+ piboSessionId: turn.piboSessionId,
196
+ rootSessionId: turn.rootSessionId,
197
+ roomId: turn.roomId,
198
+ name: phaseName,
199
+ status: phaseStatus,
200
+ startedAt: at,
201
+ endedAt: at,
202
+ lastProgressAt: at,
203
+ eventId: turn.eventId,
204
+ runId: turn.runId,
205
+ summary: outcome.summary,
206
+ retentionClass: turn.retentionClass,
207
+ updatedAt: at,
208
+ });
209
+ const recovered = this.upsertTurn({
210
+ turnId: turn.turnId,
211
+ piboSessionId: turn.piboSessionId,
212
+ rootSessionId: turn.rootSessionId,
213
+ roomId: turn.roomId,
214
+ inputEventId: turn.inputEventId,
215
+ eventId: turn.eventId,
216
+ eventStreamId: turn.eventStreamId,
217
+ payloadRef: turn.payloadRef,
218
+ runId: turn.runId,
219
+ source: turn.source,
220
+ status: outcome.status,
221
+ currentPhase: phaseName,
222
+ queuedAt: turn.queuedAt,
223
+ startedAt: turn.startedAt,
224
+ completedAt: at,
225
+ lastProgressAt: at,
226
+ queuedBehind: turn.queuedBehind,
227
+ queueDepth: 0,
228
+ summary: outcome.summary,
229
+ retentionClass: turn.retentionClass,
230
+ createdAt: turn.createdAt,
231
+ updatedAt: at,
232
+ metadata: turn.metadata,
233
+ });
234
+ return { turn: recovered, outcome };
235
+ }));
236
+ }
89
237
  listProviderEventsPage(providerRequestId, input = {}) {
90
238
  return listTelemetryProviderEventsPage(this.db, providerRequestId, input);
91
239
  }
@@ -989,6 +989,17 @@ function compactRunRow(run) {
989
989
  timeoutAt: run.timeoutAt,
990
990
  timeoutPhase: run.timeoutPhase,
991
991
  serviceWarning: run.serviceWarning,
992
+ isolation: run.resources?.isolationMode,
993
+ resourceUnit: run.resources?.unitName,
994
+ resourceLimitReason: run.resources?.limitReason,
995
+ minimumHostAvailableBytes: run.resources?.minimumHostAvailableBytes,
996
+ peakMemoryFullPsiAvg10: run.resources?.peakMemoryFullPsiAvg10,
997
+ peakIoFullPsiAvg10: run.resources?.peakIoFullPsiAvg10,
998
+ memoryPeakBytes: run.resources?.cgroup?.memoryPeakBytes,
999
+ memoryMaxBytes: run.resources?.cgroup?.memoryMaxBytes,
1000
+ tasksPeak: run.resources?.cgroup?.tasksPeak,
1001
+ ioReadBytes: run.resources?.cgroup?.ioReadBytes,
1002
+ ioWriteBytes: run.resources?.cgroup?.ioWriteBytes,
992
1003
  summary: run.summary,
993
1004
  };
994
1005
  }
@@ -149,6 +149,7 @@ export class PiboGatewayServer {
149
149
  pluginRegistry: this.pluginRegistry,
150
150
  sessionStore: this.sessionStore,
151
151
  messagePreflight: createLoopMessagePreflight({ path: this.options.loopStorePath }),
152
+ recoverInterruptedRuntimeState: true,
152
153
  });
153
154
  this.unsubscribe = this.router.subscribe((event) => this.broadcastRouterEvent(event));
154
155
  this.server = createServer((socket) => this.handleSocket(socket));
@@ -144,7 +144,8 @@ export class PiboReliabilityStore {
144
144
  timeout_ms INTEGER,
145
145
  timeout_at TEXT,
146
146
  timeout_phase TEXT,
147
- service_warning TEXT
147
+ service_warning TEXT,
148
+ resource_json TEXT
148
149
  );
149
150
  CREATE INDEX IF NOT EXISTS idx_pibo_runs_controller_updated
150
151
  ON pibo_runs(controller_pibo_session_id, updated_at);
@@ -155,6 +156,7 @@ export class PiboReliabilityStore {
155
156
  ensurePiboRunColumn(this.db, "timeout_at", "TEXT");
156
157
  ensurePiboRunColumn(this.db, "timeout_phase", "TEXT");
157
158
  ensurePiboRunColumn(this.db, "service_warning", "TEXT");
159
+ ensurePiboRunColumn(this.db, "resource_json", "TEXT");
158
160
  this.appendEventStatement = this.db.prepare(`
159
161
  INSERT INTO pibo_event_stream (topic, key, event_id, idempotency_key, created_at, retention_class, payload_json)
160
162
  VALUES (?, ?, ?, ?, ?, ?, ?)
@@ -496,10 +498,10 @@ export class PiboReliabilityStore {
496
498
  INSERT INTO pibo_runs (
497
499
  run_id, kind, controller_pibo_session_id, status, completion_policy, consumed, tool_name,
498
500
  summary, result_json, error, notified_status, acknowledged_status, created_at, updated_at,
499
- completed_at, job_id, retryable, max_attempts, timeout_ms, timeout_at, timeout_phase, service_warning
500
- ) VALUES (?, 'tool', ?, 'running', ?, 0, ?, ?, NULL, NULL, NULL, NULL, ?, ?, NULL, ?, ?, ?, ?, ?, NULL, ?)
501
+ completed_at, job_id, retryable, max_attempts, timeout_ms, timeout_at, timeout_phase, service_warning, resource_json
502
+ ) VALUES (?, 'tool', ?, 'running', ?, 0, ?, ?, NULL, NULL, NULL, NULL, ?, ?, NULL, ?, ?, ?, ?, ?, NULL, ?, ?)
501
503
  `)
502
- .run(runId, input.controllerPiboSessionId, input.completionPolicy, input.toolName, `${input.toolName} run is running.`, timestamp, timestamp, job.jobId, input.retryable ? 1 : 0, maxAttempts, input.timeoutMs ?? null, timeoutAt ?? null, input.serviceWarning ?? null);
504
+ .run(runId, input.controllerPiboSessionId, input.completionPolicy, input.toolName, `${input.toolName} run is running.`, timestamp, timestamp, job.jobId, input.retryable ? 1 : 0, maxAttempts, input.timeoutMs ?? null, timeoutAt ?? null, input.serviceWarning ?? null, input.resources ? JSON.stringify(input.resources) : null);
503
505
  this.claimJob(job.jobId, input.workerId ?? `run-registry:${process.pid}`, 24 * 60 * 60 * 1000);
504
506
  return this.requireRun(runId);
505
507
  }
@@ -527,10 +529,11 @@ export class PiboReliabilityStore {
527
529
  timeout_ms = ?,
528
530
  timeout_at = ?,
529
531
  timeout_phase = ?,
530
- service_warning = ?
532
+ service_warning = ?,
533
+ resource_json = ?
531
534
  WHERE run_id = ?
532
535
  `)
533
- .run(next.status, next.completionPolicy, next.consumed ? 1 : 0, next.summary ?? null, next.result ? JSON.stringify(next.result) : null, next.error ?? null, next.notifiedStatus ?? null, next.acknowledgedStatus ?? null, next.updatedAt, next.completedAt ?? null, next.jobId ?? null, next.retryable ? 1 : 0, next.maxAttempts, next.timeoutMs ?? null, next.timeoutAt ?? null, next.timeoutPhase ?? null, next.serviceWarning ?? null, runId);
536
+ .run(next.status, next.completionPolicy, next.consumed ? 1 : 0, next.summary ?? null, next.result ? JSON.stringify(next.result) : null, next.error ?? null, next.notifiedStatus ?? null, next.acknowledgedStatus ?? null, next.updatedAt, next.completedAt ?? null, next.jobId ?? null, next.retryable ? 1 : 0, next.maxAttempts, next.timeoutMs ?? null, next.timeoutAt ?? null, next.timeoutPhase ?? null, next.serviceWarning ?? null, next.resources ? JSON.stringify(next.resources) : null, runId);
534
537
  return this.requireRun(runId);
535
538
  }
536
539
  getRun(runId) {
@@ -801,6 +804,8 @@ function runFromRow(row) {
801
804
  output.timeoutPhase = row.timeout_phase;
802
805
  if (row.service_warning)
803
806
  output.serviceWarning = row.service_warning;
807
+ if (row.resource_json)
808
+ output.resources = JSON.parse(row.resource_json);
804
809
  return output;
805
810
  }
806
811
  function retryDelayMs(attempts, input) {