@pasko70/pibo 3.0.0 → 3.0.2

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 (77) hide show
  1. package/README.md +20 -6
  2. package/dist/agent-runtime/context-build.js +26 -2
  3. package/dist/agent-runtime/routed-session.js +108 -9
  4. package/dist/agent-runtimes/codex-native/adapter.js +12 -0
  5. package/dist/agent-runtimes/codex-native/resource-delivery.js +35 -7
  6. package/dist/agent-runtimes/omp/adapter.js +3 -7
  7. package/dist/agent-runtimes/pi/adapter.js +5 -3
  8. package/dist/agent-runtimes/pi/routed-session.js +49 -8
  9. package/dist/agent-runtimes/pi/runtime.js +6 -0
  10. package/dist/apps/chat/agent-profiles.js +31 -2
  11. package/dist/apps/chat/agent-store.js +43 -3
  12. package/dist/apps/chat/chat-request-normalizers.js +31 -0
  13. package/dist/apps/chat/data/chat-data-mappers.js +61 -11
  14. package/dist/apps/chat/data/session-query-service.js +33 -10
  15. package/dist/apps/chat/data/timeline-query-service.js +60 -35
  16. package/dist/apps/chat/output-compactor.js +144 -21
  17. package/dist/apps/chat/output-event-policy.js +179 -2
  18. package/dist/apps/chat/stream.js +16 -7
  19. package/dist/apps/chat/trace-v2.js +1 -0
  20. package/dist/apps/chat/web-app.js +236 -64
  21. package/dist/apps/chat-ui/assets/{dist-BBDMeU5-.js → dist-B6GZgWSj.js} +1 -1
  22. package/dist/apps/chat-ui/assets/{dist-mqCviI-c.js → dist-BPotHecb.js} +1 -1
  23. package/dist/apps/chat-ui/assets/{dist-JnW4v8UE.js → dist-BzqQKeVO.js} +1 -1
  24. package/dist/apps/chat-ui/assets/{dist-BDIATCQt.js → dist-CvaPTBTN.js} +1 -1
  25. package/dist/apps/chat-ui/assets/{dist-lRiLP9rr.js → dist-paagejNj.js} +1 -1
  26. package/dist/apps/chat-ui/assets/index-6JJV-eic.js +228 -0
  27. package/dist/apps/chat-ui/assets/index-CeL9JPP5.css +1 -0
  28. package/dist/apps/chat-ui/index.html +2 -2
  29. package/dist/apps/chat-vscode-web/assets/{index-Dtw2Z7jz.js → index-U-MSErGa.js} +4 -4
  30. package/dist/apps/chat-vscode-web/index.html +1 -1
  31. package/dist/apps/cli-ui/cliSessionsCommand.js +34 -8
  32. package/dist/cli-session/localSessionSource.js +153 -161
  33. package/dist/cli.js +7 -11
  34. package/dist/core/context-build.js +7 -16
  35. package/dist/core/output-persistence-retry.js +484 -0
  36. package/dist/core/output-render-sequence.js +331 -0
  37. package/dist/core/profiles.js +24 -0
  38. package/dist/core/provider-recovery.js +7 -1
  39. package/dist/core/session-model.js +37 -1
  40. package/dist/core/session-router.js +67 -20
  41. package/dist/data/ingest-service.js +91 -9
  42. package/dist/data/navigation-store.js +5 -5
  43. package/dist/data/schema.js +122 -2
  44. package/dist/data/session-store.js +3 -3
  45. package/dist/debug/index.js +44 -2
  46. package/dist/gateway/web.js +8 -8
  47. package/dist/local/client.js +3 -2
  48. package/dist/plugins/context-files-store.js +30 -0
  49. package/dist/plugins/context-files.js +52 -1
  50. package/dist/plugins/registry.js +2 -0
  51. package/dist/plugins/user-profile-resources.js +22 -0
  52. package/dist/reliability/store.js +246 -37
  53. package/dist/session-ui/terminalRows.js +6 -3
  54. package/dist/sessions/pibo-data-store.js +145 -0
  55. package/dist/sessions/store.js +42 -0
  56. package/dist/setup/cli.js +422 -6
  57. package/dist/setup/installation-profiles.js +464 -0
  58. package/dist/shared/deterministic-digest.js +94 -0
  59. package/dist/shared/trace-engine.js +54 -0
  60. package/dist/shared/trace-event-projection.js +124 -67
  61. package/dist/shared/trace-history.js +47 -12
  62. package/dist/shared/trace-live-reducer.js +23 -4
  63. package/dist/shared/trace-nodes.js +17 -28
  64. package/dist/shared/trace-order.js +41 -5
  65. package/dist/shared/trace-page-merge.js +44 -14
  66. package/dist/shared/trace-patch-nodes.js +2 -0
  67. package/dist/shared/trace-subagent-links.js +4 -1
  68. package/dist/shared/trace-tool-identity.js +15 -8
  69. package/dist/signals/projector.js +28 -6
  70. package/dist/subagents/tool.js +7 -3
  71. package/docs/ops/vscode-extension-release.md +9 -1
  72. package/npm-shrinkwrap.json +2 -2
  73. package/package.json +2 -1
  74. package/dist/apps/chat-ui/assets/index-0XOOVxLP.js +0 -228
  75. package/dist/apps/chat-ui/assets/index-DV7_CgsC.css +0 -1
  76. package/dist/apps/vscode-artifacts/latest.vsix +0 -0
  77. package/dist/apps/vscode-artifacts/pibo-vscode-ext-3.0.0.vsix +0 -0
@@ -0,0 +1,331 @@
1
+ import { createHash } from "node:crypto";
2
+ const RENDER_SEQUENCE_SLOTS_PER_MILLISECOND = 1_000;
3
+ const MAX_TRACKED_SESSIONS = 1_024;
4
+ const MAX_RECENT_SEGMENTS_PER_SESSION = 1_024;
5
+ const MAX_RECENT_TOOL_INVOCATIONS_PER_SESSION = 1_024;
6
+ export function outputRenderHighWaterStore(value) {
7
+ if (!value || typeof value !== "object")
8
+ return undefined;
9
+ const candidate = value;
10
+ return typeof candidate.claimOutputRenderSequence === "function"
11
+ && typeof candidate.observeOutputRenderSequence === "function"
12
+ ? candidate
13
+ : undefined;
14
+ }
15
+ /**
16
+ * Assigns one monotonic position to each conceptual output segment. Deltas and
17
+ * lifecycle events for the same segment reuse the first position.
18
+ */
19
+ export class OutputRenderSequencer {
20
+ sessions = new Map();
21
+ now;
22
+ highWaterStore;
23
+ constructor(nowOrOptions = Date.now) {
24
+ if (typeof nowOrOptions === "function") {
25
+ this.now = nowOrOptions;
26
+ }
27
+ else {
28
+ this.now = nowOrOptions.now ?? Date.now;
29
+ this.highWaterStore = nowOrOptions.highWaterStore;
30
+ }
31
+ }
32
+ position(event) {
33
+ const state = this.sessionState(event.piboSessionId);
34
+ this.trackActiveTurn(state, event);
35
+ const eventWithToolIdentity = this.positionToolInvocation(state, event);
36
+ const key = outputRenderSegmentKey(eventWithToolIdentity, state.activeEventId);
37
+ const supplied = validRenderSequence(eventWithToolIdentity.renderSequence)
38
+ ? eventWithToolIdentity.renderSequence
39
+ : undefined;
40
+ const existing = supplied ?? (key ? state.positions.get(key) : undefined);
41
+ const renderSequence = existing ?? this.nextSequence(event.piboSessionId, state);
42
+ state.lastSequence = Math.max(state.lastSequence, renderSequence);
43
+ if (supplied !== undefined)
44
+ this.highWaterStore?.observeOutputRenderSequence(event.piboSessionId, supplied);
45
+ if (key && !state.positions.has(key))
46
+ state.positions.set(key, renderSequence);
47
+ const positioned = eventWithToolIdentity.renderSequence === renderSequence
48
+ ? eventWithToolIdentity
49
+ : { ...eventWithToolIdentity, renderSequence };
50
+ if (event.type === "message_finished" || event.type === "session_error") {
51
+ const eventId = "eventId" in event ? event.eventId : undefined;
52
+ if ((eventId && state.activeEventId === eventId) || (event.type === "session_error" && !eventId)) {
53
+ state.activeEventId = undefined;
54
+ }
55
+ }
56
+ const segmentEventId = "eventId" in positioned && positioned.eventId ? positioned.eventId : state.activeEventId;
57
+ this.updateSegmentLifecycle(state, positioned, key, segmentEventId);
58
+ this.trimSessions();
59
+ return positioned;
60
+ }
61
+ disposeSession(piboSessionId) {
62
+ this.sessions.delete(piboSessionId);
63
+ }
64
+ disposeAll() {
65
+ this.sessions.clear();
66
+ }
67
+ debugState() {
68
+ let positionCount = 0;
69
+ let toolInvocationCount = 0;
70
+ let completedSessionCount = 0;
71
+ for (const state of this.sessions.values()) {
72
+ positionCount += state.positions.size;
73
+ for (const invocations of state.toolInvocations.values())
74
+ toolInvocationCount += invocations.length;
75
+ if (sessionCanBeEvicted(state))
76
+ completedSessionCount += 1;
77
+ }
78
+ return { sessionCount: this.sessions.size, completedSessionCount, sessions: [...this.sessions.keys()], positionCount, toolInvocationCount };
79
+ }
80
+ sessionState(piboSessionId) {
81
+ const existing = this.sessions.get(piboSessionId);
82
+ if (existing) {
83
+ this.sessions.delete(piboSessionId);
84
+ this.sessions.set(piboSessionId, existing);
85
+ return existing;
86
+ }
87
+ const state = {
88
+ positions: new Map(),
89
+ segmentEventIds: new Map(),
90
+ activeSegments: new Set(),
91
+ completedSegments: new Set(),
92
+ toolInvocations: new Map(),
93
+ lastSequence: 0,
94
+ };
95
+ this.sessions.set(piboSessionId, state);
96
+ return state;
97
+ }
98
+ positionToolInvocation(state, event) {
99
+ if (!isToolIdentityEvent(event) || !event.toolCallId)
100
+ return event;
101
+ const eventId = event.eventId ?? state.activeEventId;
102
+ const counterKey = toolInvocationCounterKey(eventId, event.toolCallId);
103
+ let invocations = state.toolInvocations.get(counterKey);
104
+ if (!invocations) {
105
+ invocations = [];
106
+ state.toolInvocations.set(counterKey, invocations);
107
+ }
108
+ let invocation;
109
+ const transition = eventId ? {
110
+ piboSessionId: event.piboSessionId,
111
+ eventId,
112
+ toolCallId: event.toolCallId,
113
+ eventType: event.type,
114
+ ...(event.type === "tool_call" ? { callFingerprint: toolCallFingerprint(event) } : {}),
115
+ } : undefined;
116
+ if (validToolInvocationOrdinal(event.toolInvocationOrdinal)) {
117
+ if (eventId)
118
+ this.highWaterStore?.observeOutputToolInvocationOrdinal?.(event.piboSessionId, eventId, event.toolCallId, event.toolInvocationOrdinal);
119
+ if (transition)
120
+ this.highWaterStore?.observeOutputToolInvocation?.({ ...transition, ordinal: event.toolInvocationOrdinal });
121
+ invocation = invocations.find((candidate) => candidate.ordinal === event.toolInvocationOrdinal);
122
+ if (!invocation) {
123
+ invocation = { ordinal: event.toolInvocationOrdinal, seen: new Set(), closed: false, persistedIdentity: true };
124
+ invocations.push(invocation);
125
+ }
126
+ invocation.persistedIdentity = true;
127
+ }
128
+ else {
129
+ const latest = invocations.at(-1);
130
+ if (!latest) {
131
+ invocation = this.createToolInvocation(invocations, event.piboSessionId, eventId, event.toolCallId, transition);
132
+ }
133
+ else if (event.type === "tool_call" && (latest.seen.has("tool_call") || (latest.closed && latest.persistedIdentity))) {
134
+ invocation = this.createToolInvocation(invocations, event.piboSessionId, eventId, event.toolCallId, transition);
135
+ }
136
+ else if (event.type === "tool_execution_started" && latest.closed && latest.seen.has("tool_execution_started")) {
137
+ invocation = this.createToolInvocation(invocations, event.piboSessionId, eventId, event.toolCallId, transition);
138
+ }
139
+ else {
140
+ invocation = latest;
141
+ if (transition)
142
+ this.highWaterStore?.observeOutputToolInvocation?.({ ...transition, ordinal: latest.ordinal });
143
+ }
144
+ }
145
+ invocation.seen.add(event.type);
146
+ if (event.type === "tool_execution_finished")
147
+ invocation.closed = true;
148
+ this.trimToolInvocations(state);
149
+ const needsEventId = !event.eventId && eventId !== undefined;
150
+ return event.toolInvocationOrdinal === invocation.ordinal && !needsEventId
151
+ ? event
152
+ : { ...event, ...(needsEventId ? { eventId } : {}), toolInvocationOrdinal: invocation.ordinal };
153
+ }
154
+ trimToolInvocations(state) {
155
+ let total = 0;
156
+ for (const invocations of state.toolInvocations.values())
157
+ total += invocations.length;
158
+ while (total > MAX_RECENT_TOOL_INVOCATIONS_PER_SESSION) {
159
+ let removed = false;
160
+ for (const [key, invocations] of state.toolInvocations) {
161
+ const closedIndex = invocations.findIndex((invocation) => invocation.closed);
162
+ if (closedIndex === -1)
163
+ continue;
164
+ invocations.splice(closedIndex, 1);
165
+ total -= 1;
166
+ if (!invocations.length)
167
+ state.toolInvocations.delete(key);
168
+ removed = true;
169
+ break;
170
+ }
171
+ if (!removed)
172
+ break;
173
+ }
174
+ }
175
+ createToolInvocation(invocations, piboSessionId, eventId, toolCallId, transition) {
176
+ const localNext = invocations.reduce((maximum, invocation) => Math.max(maximum, invocation.ordinal), -1) + 1;
177
+ const ordinal = transition
178
+ ? this.highWaterStore?.claimOrAttachOutputToolInvocation?.(transition)
179
+ ?? this.highWaterStore?.claimOutputToolInvocationOrdinal?.(piboSessionId, eventId, toolCallId)
180
+ ?? localNext
181
+ : localNext;
182
+ return createInvocation(invocations, ordinal);
183
+ }
184
+ updateSegmentLifecycle(state, event, key, segmentEventId) {
185
+ if (key && !state.segmentEventIds.has(key)) {
186
+ state.segmentEventIds.set(key, segmentEventId);
187
+ }
188
+ if (event.type === "message_finished" || event.type === "session_error") {
189
+ const boundaryEventId = event.eventId;
190
+ for (const segmentKey of state.positions.keys()) {
191
+ if (event.type === "session_error" && !boundaryEventId)
192
+ this.completeSegment(state, segmentKey);
193
+ else if (boundaryEventId && state.segmentEventIds.get(segmentKey) === boundaryEventId)
194
+ this.completeSegment(state, segmentKey);
195
+ }
196
+ return;
197
+ }
198
+ if (!key)
199
+ return;
200
+ if (segmentCompletesWith(event))
201
+ this.completeSegment(state, key);
202
+ else if (segmentRemainsActive(event)) {
203
+ state.completedSegments.delete(key);
204
+ state.activeSegments.add(key);
205
+ }
206
+ }
207
+ completeSegment(state, key) {
208
+ state.activeSegments.delete(key);
209
+ state.completedSegments.delete(key);
210
+ state.completedSegments.add(key);
211
+ while (state.completedSegments.size > MAX_RECENT_SEGMENTS_PER_SESSION) {
212
+ const oldest = state.completedSegments.values().next().value;
213
+ if (oldest === undefined)
214
+ break;
215
+ state.completedSegments.delete(oldest);
216
+ if (!state.activeSegments.has(oldest)) {
217
+ state.positions.delete(oldest);
218
+ state.segmentEventIds.delete(oldest);
219
+ }
220
+ }
221
+ }
222
+ trimSessions() {
223
+ let removable = [...this.sessions.entries()].filter(([, state]) => sessionCanBeEvicted(state));
224
+ while (removable.length > MAX_TRACKED_SESSIONS) {
225
+ const [sessionId] = removable.shift();
226
+ this.sessions.delete(sessionId);
227
+ }
228
+ }
229
+ trackActiveTurn(state, event) {
230
+ if (event.type === "message_started" && event.eventId)
231
+ state.activeEventId = event.eventId;
232
+ }
233
+ nextSequence(piboSessionId, state) {
234
+ const wallClockSequence = Math.floor(this.now()) * RENDER_SEQUENCE_SLOTS_PER_MILLISECOND;
235
+ const minimum = Math.max(wallClockSequence, state.lastSequence + 1);
236
+ const next = this.highWaterStore?.claimOutputRenderSequence(piboSessionId, minimum) ?? minimum;
237
+ state.lastSequence = Math.max(state.lastSequence, next);
238
+ return next;
239
+ }
240
+ }
241
+ function createInvocation(invocations, ordinal) {
242
+ const invocation = { ordinal, seen: new Set(), closed: false, persistedIdentity: false };
243
+ invocations.push(invocation);
244
+ return invocation;
245
+ }
246
+ function segmentRemainsActive(event) {
247
+ return event.type === "message_started"
248
+ || event.type === "assistant_delta"
249
+ || event.type === "thinking_started"
250
+ || event.type === "thinking_delta"
251
+ || event.type === "tool_call"
252
+ || event.type === "tool_execution_started"
253
+ || event.type === "tool_execution_updated";
254
+ }
255
+ function segmentCompletesWith(event) {
256
+ return event.type === "assistant_message"
257
+ || event.type === "thinking_finished"
258
+ || event.type === "tool_execution_finished"
259
+ || event.type === "execution_result"
260
+ || event.type === "compaction_end"
261
+ || event.type === "approval_resolved"
262
+ || event.type === "user_input_resolved";
263
+ }
264
+ function sessionCanBeEvicted(state) {
265
+ return state.activeEventId === undefined && state.activeSegments.size === 0;
266
+ }
267
+ export function validToolInvocationOrdinal(value) {
268
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
269
+ }
270
+ function isToolIdentityEvent(event) {
271
+ return event.type === "tool_call"
272
+ || event.type === "tool_execution_started"
273
+ || event.type === "tool_execution_updated"
274
+ || event.type === "tool_execution_finished"
275
+ || event.type === "subagent_session";
276
+ }
277
+ function toolInvocationCounterKey(eventId, toolCallId) {
278
+ return JSON.stringify([eventId ?? "unscoped", toolCallId]);
279
+ }
280
+ function toolCallFingerprint(event) {
281
+ return createHash("sha256")
282
+ .update(JSON.stringify([event.toolName, event.argsComplete, event.args]))
283
+ .digest("hex");
284
+ }
285
+ export function validRenderSequence(value) {
286
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
287
+ }
288
+ function outputRenderSegmentKey(event, activeEventId) {
289
+ const eventId = "eventId" in event && event.eventId ? event.eventId : activeEventId;
290
+ switch (event.type) {
291
+ case "message_queued":
292
+ case "message_steered":
293
+ return eventId ? `user:${eventId}` : undefined;
294
+ case "message_started":
295
+ case "message_finished":
296
+ return eventId ? `turn:${eventId}` : undefined;
297
+ case "assistant_delta":
298
+ case "assistant_message":
299
+ return `assistant:${eventId ?? "active"}:${event.assistantIndex ?? event.contentIndex ?? 0}`;
300
+ case "thinking_started":
301
+ case "thinking_delta":
302
+ case "thinking_finished":
303
+ return `reasoning:${eventId ?? "active"}:${event.thinkingIndex ?? event.contentIndex ?? 0}`;
304
+ case "tool_call":
305
+ case "tool_execution_started":
306
+ case "tool_execution_updated":
307
+ case "tool_execution_finished":
308
+ return `tool:${eventId ?? "active"}:${event.toolCallId}:${event.toolInvocationOrdinal ?? 0}`;
309
+ case "subagent_session":
310
+ return event.toolCallId ? `tool:${eventId ?? "active"}:${event.toolCallId}:${event.toolInvocationOrdinal ?? 0}` : undefined;
311
+ case "compaction_start":
312
+ case "compaction_end":
313
+ return `compaction:${eventId ?? "active"}:${event.compactionIndex ?? 0}`;
314
+ case "approval_requested":
315
+ return `approval:${eventId ?? "active"}:${event.request.requestId}`;
316
+ case "approval_resolved":
317
+ return `approval:${eventId ?? "active"}:${event.requestId}`;
318
+ case "user_input_requested":
319
+ return `user-input:${eventId ?? "active"}:${event.request.requestId}`;
320
+ case "user_input_resolved":
321
+ return `user-input:${eventId ?? "active"}:${event.requestId}`;
322
+ case "assistant_usage":
323
+ return `usage:${eventId ?? "active"}:${event.usageIndex ?? 0}`;
324
+ case "session_error":
325
+ return `error:${eventId ?? "active"}`;
326
+ case "execution_result":
327
+ return `execution:${eventId ?? event.action}`;
328
+ default:
329
+ return undefined;
330
+ }
331
+ }
@@ -13,10 +13,14 @@ export function normalizeToolProfile(tool) {
13
13
  }
14
14
  export const DEFAULT_AGENT_RUNTIME_INSTANCE_ID = "pi";
15
15
  export const DEFAULT_BUILTIN_TOOL_NAMES = ["read", "bash", "edit", "write"];
16
+ function cloneModelProfiles(models) {
17
+ return models.map((model) => ({ ...model }));
18
+ }
16
19
  function cloneSubagentProfile(subagent) {
17
20
  return {
18
21
  ...subagent,
19
22
  ...(subagent.model ? { model: { ...subagent.model } } : {}),
23
+ ...(subagent.modelFallbacks ? { modelFallbacks: cloneModelProfiles(subagent.modelFallbacks) } : {}),
20
24
  ...(subagent.runtimeOptions ? { runtimeOptions: structuredClone(subagent.runtimeOptions) } : {}),
21
25
  };
22
26
  }
@@ -28,6 +32,7 @@ export class InitialSessionContext {
28
32
  parentSessionId;
29
33
  model;
30
34
  mainModel;
35
+ mainModelFallbacks;
31
36
  subagentModel;
32
37
  thinkingLevel;
33
38
  mainThinkingLevel;
@@ -41,6 +46,7 @@ export class InitialSessionContext {
41
46
  mcpServers;
42
47
  piPackages;
43
48
  contextFiles;
49
+ diagnostics;
44
50
  builtinTools;
45
51
  builtinToolNames;
46
52
  autoContextFiles;
@@ -54,6 +60,7 @@ export class InitialSessionContext {
54
60
  this.parentSessionId = options.parentSessionId;
55
61
  this.model = options.model ? { ...options.model } : undefined;
56
62
  this.mainModel = options.mainModel ? { ...options.mainModel } : undefined;
63
+ this.mainModelFallbacks = cloneModelProfiles(options.mainModelFallbacks ?? []);
57
64
  this.subagentModel = options.subagentModel ? { ...options.subagentModel } : undefined;
58
65
  this.thinkingLevel = options.thinkingLevel;
59
66
  this.mainThinkingLevel = options.mainThinkingLevel;
@@ -67,6 +74,7 @@ export class InitialSessionContext {
67
74
  this.mcpServers = [...(options.mcpServers ?? [])];
68
75
  this.piPackages = [...(options.piPackages ?? [])];
69
76
  this.contextFiles = [...(options.contextFiles ?? [])];
77
+ this.diagnostics = (options.diagnostics ?? []).map((diagnostic) => ({ ...diagnostic }));
70
78
  this.builtinTools = options.builtinTools ?? "default";
71
79
  this.builtinToolNames = [...(options.builtinToolNames ?? DEFAULT_BUILTIN_TOOL_NAMES)];
72
80
  this.autoContextFiles = options.autoContextFiles ?? true;
@@ -82,6 +90,7 @@ export class InitialSessionContextBuilder {
82
90
  parentSessionId;
83
91
  model;
84
92
  mainModel;
93
+ mainModelFallbacks = [];
85
94
  subagentModel;
86
95
  thinkingLevel;
87
96
  mainThinkingLevel;
@@ -95,6 +104,7 @@ export class InitialSessionContextBuilder {
95
104
  mcpServers = [];
96
105
  piPackages = [];
97
106
  contextFiles = [];
107
+ diagnostics = [];
98
108
  builtinTools = "default";
99
109
  builtinToolNames = [...DEFAULT_BUILTIN_TOOL_NAMES];
100
110
  autoContextFiles = true;
@@ -124,6 +134,10 @@ export class InitialSessionContextBuilder {
124
134
  this.mainModel = { ...model };
125
135
  return this;
126
136
  }
137
+ withMainModelFallbacks(models) {
138
+ this.mainModelFallbacks = cloneModelProfiles(models);
139
+ return this;
140
+ }
127
141
  withSubagentModel(model) {
128
142
  this.subagentModel = { ...model };
129
143
  return this;
@@ -220,6 +234,14 @@ export class InitialSessionContextBuilder {
220
234
  this.contextFiles.push(...contextFiles);
221
235
  return this;
222
236
  }
237
+ addDiagnostic(diagnostic) {
238
+ this.diagnostics.push({ ...diagnostic });
239
+ return this;
240
+ }
241
+ addDiagnostics(diagnostics) {
242
+ this.diagnostics.push(...diagnostics.map((diagnostic) => ({ ...diagnostic })));
243
+ return this;
244
+ }
223
245
  createSession() {
224
246
  return new InitialSessionContext({
225
247
  profileName: this.profileName,
@@ -229,6 +251,7 @@ export class InitialSessionContextBuilder {
229
251
  parentSessionId: this.parentSessionId,
230
252
  model: this.model,
231
253
  mainModel: this.mainModel,
254
+ mainModelFallbacks: this.mainModelFallbacks,
232
255
  subagentModel: this.subagentModel,
233
256
  thinkingLevel: this.thinkingLevel,
234
257
  mainThinkingLevel: this.mainThinkingLevel,
@@ -242,6 +265,7 @@ export class InitialSessionContextBuilder {
242
265
  mcpServers: this.mcpServers,
243
266
  piPackages: this.piPackages,
244
267
  contextFiles: this.contextFiles,
268
+ diagnostics: this.diagnostics,
245
269
  builtinTools: this.builtinTools,
246
270
  builtinToolNames: this.builtinToolNames,
247
271
  autoContextFiles: this.autoContextFiles,
@@ -1,5 +1,5 @@
1
1
  import { isRetryableAssistantError } from "@earendil-works/pi-ai";
2
- import { classifySessionErrorMessage } from "./session-errors.js";
2
+ import { classifySessionErrorMessage, normalizeSessionErrorDetails } from "./session-errors.js";
3
3
  export const PIBO_PROVIDER_RECOVERY_MESSAGE_TYPE = "pibo-provider-recovery-resume";
4
4
  export const PIBO_PROVIDER_RECOVERY_PROMPT = "Continue the interrupted task autonomously from the existing session state. The previous provider request failed transiently. Do not wait for more user input, ask the user to repeat the request, or mention this recovery message unless the failure affects the result.";
5
5
  const DEFAULT_RECOVERY_BASE_DELAY_MS = 2_000;
@@ -44,6 +44,12 @@ export function isRetryablePiboProviderError(error) {
44
44
  const errorMessage = error instanceof Error ? error.message : String(error);
45
45
  return isRetryablePiboAssistantError({ stopReason: "error", errorMessage });
46
46
  }
47
+ export function isPiboProviderFallbackError(message, details) {
48
+ const normalized = normalizeSessionErrorDetails(message, details);
49
+ return normalized.origin === "provider"
50
+ && normalized.category !== "context_overflow"
51
+ && normalized.category !== "runtime_abort";
52
+ }
47
53
  export async function waitForPiboProviderRecovery(attempt, settings, signal) {
48
54
  if (signal?.aborted)
49
55
  throw new PiboProviderRecoveryCancelledError();
@@ -1,5 +1,6 @@
1
- import { selectRequestedModelProfile } from "./model-defaults.js";
1
+ import { selectRequestedModelProfile, sanitizeModelProfile } from "./model-defaults.js";
2
2
  import { InitialSessionContext } from "./profiles.js";
3
+ export const PIBO_INITIAL_MODEL_FALLBACKS_METADATA_KEY = "initialModelFallbacks";
3
4
  export function resolvePiboSessionActiveModel(input) {
4
5
  if (input.piboSession.activeModel)
5
6
  return cloneModelProfile(input.piboSession.activeModel);
@@ -8,6 +9,21 @@ export function resolvePiboSessionActiveModel(input) {
8
9
  : profileWithSessionIds(input.profile, input.piboSession.piSessionId);
9
10
  return selectRequestedModelProfile(sessionProfile, input.modelDefaults ?? {});
10
11
  }
12
+ export function resolvePiboSessionModelFallbacks(input) {
13
+ const persisted = input.piboSession.metadata?.[PIBO_INITIAL_MODEL_FALLBACKS_METADATA_KEY];
14
+ const configured = persisted !== undefined
15
+ ? sanitizeModelFallbacks(persisted)
16
+ : input.parentPiSessionId
17
+ ? []
18
+ : sanitizeModelFallbacks(input.profile.mainModelFallbacks);
19
+ return configured.filter((model) => !sameModel(model, input.activeModel));
20
+ }
21
+ export function withPiboSessionModelFallbacksMetadata(metadata, models) {
22
+ return {
23
+ ...(metadata ?? {}),
24
+ [PIBO_INITIAL_MODEL_FALLBACKS_METADATA_KEY]: sanitizeModelFallbacks(models),
25
+ };
26
+ }
11
27
  function profileWithSessionIds(profile, piSessionId, parentPiSessionId) {
12
28
  return new InitialSessionContext({
13
29
  profileName: profile.profileName,
@@ -15,6 +31,7 @@ function profileWithSessionIds(profile, piSessionId, parentPiSessionId) {
15
31
  parentSessionId: parentPiSessionId,
16
32
  model: profile.model,
17
33
  mainModel: profile.mainModel,
34
+ mainModelFallbacks: profile.mainModelFallbacks,
18
35
  subagentModel: profile.subagentModel,
19
36
  thinkingLevel: profile.thinkingLevel,
20
37
  mainThinkingLevel: profile.mainThinkingLevel,
@@ -27,6 +44,7 @@ function profileWithSessionIds(profile, piSessionId, parentPiSessionId) {
27
44
  subagents: profile.subagents,
28
45
  mcpServers: profile.mcpServers,
29
46
  contextFiles: profile.contextFiles,
47
+ diagnostics: profile.diagnostics,
30
48
  piPackages: profile.piPackages,
31
49
  builtinTools: profile.builtinTools,
32
50
  builtinToolNames: profile.builtinToolNames,
@@ -38,3 +56,21 @@ function profileWithSessionIds(profile, piSessionId, parentPiSessionId) {
38
56
  function cloneModelProfile(model) {
39
57
  return { provider: model.provider, id: model.id };
40
58
  }
59
+ function sanitizeModelFallbacks(value) {
60
+ if (!Array.isArray(value))
61
+ return [];
62
+ const seen = new Set();
63
+ return value.flatMap((item) => {
64
+ const model = sanitizeModelProfile(item);
65
+ if (!model)
66
+ return [];
67
+ const key = `${model.provider}\u0000${model.id}`;
68
+ if (seen.has(key))
69
+ return [];
70
+ seen.add(key);
71
+ return [model];
72
+ });
73
+ }
74
+ function sameModel(left, right) {
75
+ return Boolean(right && left.provider === right.provider && left.id === right.id);
76
+ }