@pasko70/pibo 3.0.1 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/README.md +20 -6
  2. package/compute-image/Dockerfile +47 -0
  3. package/compute-image/Dockerfile.dockerignore +5 -0
  4. package/dist/agent-runtime/context-build.js +26 -2
  5. package/dist/agent-runtime/contract.js +78 -28
  6. package/dist/agent-runtime/portable-history.js +19 -9
  7. package/dist/agent-runtime/registry.js +8 -2
  8. package/dist/agent-runtime/routed-session.js +120 -15
  9. package/dist/agent-runtimes/codex-native/adapter.js +12 -0
  10. package/dist/agent-runtimes/codex-native/resource-delivery.js +35 -7
  11. package/dist/agent-runtimes/omp/adapter.js +3 -7
  12. package/dist/agent-runtimes/pi/adapter.js +5 -3
  13. package/dist/agent-runtimes/pi/routed-session.js +59 -12
  14. package/dist/agent-runtimes/pi/runtime.js +6 -0
  15. package/dist/apps/chat/agent-profiles.js +31 -2
  16. package/dist/apps/chat/agent-store.js +98 -6
  17. package/dist/apps/chat/chat-request-normalizers.js +31 -0
  18. package/dist/apps/chat/data/chat-data-mappers.js +61 -11
  19. package/dist/apps/chat/data/project-service.js +119 -15
  20. package/dist/apps/chat/data/room-service.js +21 -0
  21. package/dist/apps/chat/data/session-query-service.js +33 -10
  22. package/dist/apps/chat/data/timeline-query-service.js +60 -35
  23. package/dist/apps/chat/loop-api.js +10 -2
  24. package/dist/apps/chat/output-compactor.js +144 -21
  25. package/dist/apps/chat/output-event-policy.js +179 -2
  26. package/dist/apps/chat/stream.js +16 -7
  27. package/dist/apps/chat/trace-v2.js +1 -0
  28. package/dist/apps/chat/web-app.js +317 -86
  29. package/dist/apps/chat-ui/assets/{dist-BDIATCQt.js → dist-CUQJqggN.js} +1 -1
  30. package/dist/apps/chat-ui/assets/{dist-mqCviI-c.js → dist-ClUlQWYN.js} +1 -1
  31. package/dist/apps/chat-ui/assets/{dist-JnW4v8UE.js → dist-Djul9BmZ.js} +1 -1
  32. package/dist/apps/chat-ui/assets/{dist-lRiLP9rr.js → dist-GD0JGdCi.js} +1 -1
  33. package/dist/apps/chat-ui/assets/{dist-BBDMeU5-.js → dist-W5HOqHym.js} +1 -1
  34. package/dist/apps/chat-ui/assets/index-BcjkX-iP.js +228 -0
  35. package/dist/apps/chat-ui/assets/index-CmqRSbBU.css +1 -0
  36. package/dist/apps/chat-ui/index.html +2 -2
  37. package/dist/apps/chat-vscode-web/assets/index-JvrPUGvI.js +43 -0
  38. package/dist/apps/chat-vscode-web/index.html +1 -1
  39. package/dist/apps/cli-ui/InkSessionApp.js +111 -12
  40. package/dist/apps/cli-ui/cliSessionsCommand.js +34 -8
  41. package/dist/cli-session/localSessionSource.js +153 -161
  42. package/dist/cli.js +7 -11
  43. package/dist/compute/cli.js +35 -16
  44. package/dist/compute/docker.js +341 -94
  45. package/dist/config/config.js +3 -0
  46. package/dist/core/context-build.js +7 -16
  47. package/dist/core/output-persistence-retry.js +484 -0
  48. package/dist/core/output-render-sequence.js +331 -0
  49. package/dist/core/profiles.js +24 -0
  50. package/dist/core/provider-recovery.js +7 -1
  51. package/dist/core/session-model.js +37 -1
  52. package/dist/core/session-router.js +78 -110
  53. package/dist/data/ingest-service.js +91 -9
  54. package/dist/data/navigation-store.js +5 -5
  55. package/dist/data/pibo-store.js +8 -1
  56. package/dist/data/schema.js +243 -3
  57. package/dist/data/session-store.js +3 -3
  58. package/dist/debug/agents.js +91 -89
  59. package/dist/debug/index.js +46 -2
  60. package/dist/debug/pty.js +127 -48
  61. package/dist/gateway/cli.js +4 -0
  62. package/dist/gateway/web.js +8 -8
  63. package/dist/local/client.js +3 -2
  64. package/dist/loops/channel.js +3 -1
  65. package/dist/loops/cli.js +2 -1
  66. package/dist/loops/service.js +90 -11
  67. package/dist/loops/store.js +100 -74
  68. package/dist/mcp/agent-context.js +13 -26
  69. package/dist/mcp/commands/info.js +3 -1
  70. package/dist/mcp/config-command.js +46 -2
  71. package/dist/mcp/config.js +18 -4
  72. package/dist/plugins/context-files-store.js +36 -6
  73. package/dist/plugins/context-files.js +52 -1
  74. package/dist/plugins/registry.js +2 -0
  75. package/dist/plugins/user-profile-resources.js +22 -0
  76. package/dist/previews/base-url.js +34 -0
  77. package/dist/previews/config.js +2 -32
  78. package/dist/ralph/store.js +5 -0
  79. package/dist/reliability/store.js +378 -106
  80. package/dist/session-ui/terminalRows.js +8 -4
  81. package/dist/sessions/pibo-data-store.js +145 -0
  82. package/dist/sessions/store.js +42 -0
  83. package/dist/setup/cli.js +422 -6
  84. package/dist/setup/installation-profiles.js +464 -0
  85. package/dist/shared/deterministic-digest.js +94 -0
  86. package/dist/shared/trace-engine.js +54 -0
  87. package/dist/shared/trace-event-projection.js +124 -67
  88. package/dist/shared/trace-history.js +47 -12
  89. package/dist/shared/trace-live-reducer.js +23 -4
  90. package/dist/shared/trace-nodes.js +17 -28
  91. package/dist/shared/trace-order.js +41 -5
  92. package/dist/shared/trace-page-merge.js +44 -14
  93. package/dist/shared/trace-patch-nodes.js +2 -0
  94. package/dist/shared/trace-subagent-links.js +4 -1
  95. package/dist/shared/trace-tool-identity.js +15 -8
  96. package/dist/signals/projector.js +28 -6
  97. package/dist/subagents/observation-query.js +121 -0
  98. package/dist/subagents/tool.js +7 -3
  99. package/dist/tools/agent-browser-leases.js +58 -23
  100. package/dist/tools/agent-browser-wrapper.js +1 -1
  101. package/dist/tools/browser-use-leases.js +129 -16
  102. package/dist/tools/browser-use-wrapper.js +1 -1
  103. package/dist/tools/index.js +27 -11
  104. package/dist/tools/python-runtime.js +10 -3
  105. package/dist/tools/registry.js +1 -1
  106. package/dist/tools/runtime/node-backend.js +16 -5
  107. package/dist/tools/runtime/node-worker-source.js +112 -33
  108. package/dist/tools/runtime/python-backend.js +16 -5
  109. package/dist/tools/runtime/python-worker-source.js +167 -16
  110. package/dist/tools/runtime/registry.js +23 -10
  111. package/dist/user-skills/store.js +0 -1
  112. package/docs/ops/vscode-extension-release.md +9 -1
  113. package/npm-shrinkwrap.json +9 -2
  114. package/package.json +9 -2
  115. package/scripts/docker-entrypoint.sh +46 -0
  116. package/scripts/prepare-agent-browser-wrapper.sh +70 -0
  117. package/scripts/prepare-browser-use-wrapper.sh +255 -0
  118. package/dist/apps/chat-ui/assets/index-0XOOVxLP.js +0 -228
  119. package/dist/apps/chat-ui/assets/index-DV7_CgsC.css +0 -1
  120. package/dist/apps/chat-vscode-web/assets/index-Dtw2Z7jz.js +0 -43
  121. package/dist/apps/vscode-artifacts/latest.vsix +0 -0
  122. package/dist/apps/vscode-artifacts/pibo-vscode-ext-3.0.1.vsix +0 -0
@@ -290,6 +290,7 @@ export class CodexNativeThreadSession {
290
290
  recycleProcessAfterInterruptedTurn = false;
291
291
  selectedToolNames = new Set();
292
292
  observedToolNames = new Set();
293
+ emittedResourceWarnings = new Set();
293
294
  toolInventoryWarning;
294
295
  constructor(runtimeInstanceId, process, threads, settings, resourceDelivery, binding, structuredUserInput, reloadProcess, productContext, bindingPersistence, firstUseTestFailpoints) {
295
296
  this.runtimeInstanceId = runtimeInstanceId;
@@ -390,6 +391,7 @@ export class CodexNativeThreadSession {
390
391
  this.assertActive();
391
392
  await this.ensureFreshResourcesForTurn();
392
393
  this.assertIdle();
394
+ this.emitPendingResourceWarnings();
393
395
  this.operationInFlight = true;
394
396
  let ownedFirstUseAttempt;
395
397
  try {
@@ -473,6 +475,7 @@ export class CodexNativeThreadSession {
473
475
  contextUsage: this.settings.currentContextUsage,
474
476
  warnings: [
475
477
  ...diagnostics.filter((entry) => entry.level === "warning").map((entry) => entry.message),
478
+ ...this.resourceDelivery.warnings.map((entry) => entry.message),
476
479
  ...(this.resourceWarning ? [this.resourceWarning] : []),
477
480
  ...(this.toolInventoryWarning ? [this.toolInventoryWarning] : []),
478
481
  ],
@@ -659,6 +662,15 @@ export class CodexNativeThreadSession {
659
662
  throw new Error(`Native Codex portable-resource credential refresh failed while ${phase}; Pibo will retry while the session remains idle.`);
660
663
  }
661
664
  }
665
+ emitPendingResourceWarnings() {
666
+ for (const warning of this.resourceDelivery.warnings) {
667
+ const key = `${warning.code}\0${warning.message}`;
668
+ if (this.emittedResourceWarnings.has(key))
669
+ continue;
670
+ this.emittedResourceWarnings.add(key);
671
+ this.emit({ type: "warning", message: warning.message, details: { code: warning.code } });
672
+ }
673
+ }
662
674
  updateSelectedToolNames(resourceDelivery) {
663
675
  const names = [...new Set(resourceDelivery.enabledToolNames)].sort();
664
676
  this.selectedToolNames = new Set(names.slice(0, MAX_INSPECTED_SELECTED_TOOL_NAMES));
@@ -256,21 +256,25 @@ export class CodexNativeResourceDelivery {
256
256
  skillRoots;
257
257
  selectedSkills;
258
258
  expectedMcpServers;
259
+ resources;
259
260
  portableTools;
260
261
  mcpAccess;
261
262
  credentialIssuedAtMs;
262
263
  credentialExpiresAtMs;
263
- constructor(environment, threadConfig, developerInstructions, skillRoots, selectedSkills, expectedMcpServers, portableTools, mcpAccess, credentialIssuedAtMs, credentialExpiresAtMs) {
264
+ resourceWarnings;
265
+ constructor(environment, threadConfig, developerInstructions, skillRoots, selectedSkills, expectedMcpServers, resources, portableTools, mcpAccess, credentialIssuedAtMs, credentialExpiresAtMs, resourceWarnings = []) {
264
266
  this.environment = environment;
265
267
  this.threadConfig = threadConfig;
266
268
  this.developerInstructions = developerInstructions;
267
269
  this.skillRoots = skillRoots;
268
270
  this.selectedSkills = selectedSkills;
269
271
  this.expectedMcpServers = expectedMcpServers;
272
+ this.resources = resources;
270
273
  this.portableTools = portableTools;
271
274
  this.mcpAccess = mcpAccess;
272
275
  this.credentialIssuedAtMs = credentialIssuedAtMs;
273
276
  this.credentialExpiresAtMs = credentialExpiresAtMs;
277
+ this.resourceWarnings = resourceWarnings;
274
278
  }
275
279
  static async prepare(input) {
276
280
  const resourceEnvironment = { ...(input.resources?.getAdapterEnvironment() ?? {}) };
@@ -323,7 +327,7 @@ export class CodexNativeResourceDelivery {
323
327
  const selectedSkills = input.resources
324
328
  ? await Promise.all(input.resources.getInspection().skills.flatMap((skill) => {
325
329
  const path = skill.materializedPath;
326
- return path ? [{ name: skill.name, path }] : [];
330
+ return path ? [{ contributionId: skill.contributionId, name: skill.name, path }] : [];
327
331
  }).map(async (skill) => ({ ...skill, path: await realpath(skill.path) })))
328
332
  : [];
329
333
  const materializedSkillsRoot = input.resources?.getInspection().paths?.skills;
@@ -339,7 +343,7 @@ export class CodexNativeResourceDelivery {
339
343
  : { multi_agent: false, multi_agent_v2: false };
340
344
  threadConfig.agents = { enabled: input.nativeSubagentsEnabled };
341
345
  }
342
- return new CodexNativeResourceDelivery(resourceEnvironment, Object.keys(threadConfig).length > 0 ? threadConfig : undefined, buildDeveloperInstructions(input.resources), skillRoots, selectedSkills, expectedMcpServers, input.portableTools, mcpAccess, credentialIssuedAtMs, credentialExpiresAtMs);
346
+ return new CodexNativeResourceDelivery(resourceEnvironment, Object.keys(threadConfig).length > 0 ? threadConfig : undefined, buildDeveloperInstructions(input.resources), skillRoots, selectedSkills, expectedMcpServers, input.resources, input.portableTools, mcpAccess, credentialIssuedAtMs, credentialExpiresAtMs);
343
347
  }
344
348
  catch (error) {
345
349
  if (mcpAccess && input.portableTools)
@@ -350,6 +354,9 @@ export class CodexNativeResourceDelivery {
350
354
  get hasMcpServers() {
351
355
  return this.expectedMcpServers.length > 0;
352
356
  }
357
+ get warnings() {
358
+ return this.resourceWarnings;
359
+ }
353
360
  get enabledToolNames() {
354
361
  return this.expectedMcpServers
355
362
  .flatMap((server) => server.toolNames.map((toolName) => `${server.name}/${toolName}`))
@@ -379,6 +386,7 @@ export class CodexNativeResourceDelivery {
379
386
  this.credentialExpiresAtMs = expiresAt;
380
387
  }
381
388
  async configureProcess(client, workspace) {
389
+ this.resourceWarnings = [];
382
390
  if (this.skillRoots.length === 0)
383
391
  return;
384
392
  let response;
@@ -411,15 +419,35 @@ export class CodexNativeResourceDelivery {
411
419
  }
412
420
  }
413
421
  }
422
+ const collisions = new Set();
414
423
  for (const skill of this.selectedSkills) {
415
424
  const sameNamePaths = loadedSkills.get(skill.name) ?? new Set();
416
425
  const conflictingPath = [...sameNamePaths].find((path) => path !== skill.path);
417
- if (conflictingPath) {
418
- throw new Error(`Selected Pibo skill "${skill.name}" conflicts with a native Codex skill; explicit Pibo-skill precedence cannot be guaranteed.`);
419
- }
420
- if (!loadedPaths.has(skill.path))
426
+ if (conflictingPath)
427
+ collisions.add(skill.name);
428
+ if (!loadedPaths.has(skill.path) && !conflictingPath)
421
429
  throw new Error(`Codex did not load selected Pibo skill "${skill.name}".`);
422
430
  }
431
+ let collisionMessage;
432
+ if (collisions.size > 0) {
433
+ const names = [...collisions].sort().map((name) => `"${name}"`).join(", ");
434
+ collisionMessage = `Codex native skill name collision for ${names}: the selected Pibo skill remains materialized, but precedence is ambiguous. Rename or disable one source to remove the collision.`;
435
+ this.resourceWarnings = [{
436
+ code: "codex_native_skill_name_collision",
437
+ message: collisionMessage,
438
+ }];
439
+ }
440
+ this.resources?.recordAdapterDelivery?.(this.selectedSkills.map((skill) => {
441
+ const collided = collisions.has(skill.name);
442
+ return {
443
+ contributionId: skill.contributionId,
444
+ status: collided ? "degraded" : "delivered",
445
+ mode: "codex-extra-roots",
446
+ fidelity: collided ? "lossy" : "exact",
447
+ target: skill.path,
448
+ ...(collided && collisionMessage ? { diagnostic: collisionMessage } : {}),
449
+ };
450
+ }));
423
451
  }
424
452
  async verifyThread(client, threadId) {
425
453
  if (this.expectedMcpServers.length === 0)
@@ -631,13 +631,9 @@ class OmpAgentRuntimeAdapter {
631
631
  }
632
632
  return emptyOmpHistoryPage(this.instanceId);
633
633
  }
634
- async resolveBinding() {
635
- return {
636
- piboSessionId: "",
637
- runtimeInstanceId: this.instanceId,
638
- adapterId: OMP_ADAPTER_ID,
639
- state: "unbound",
640
- };
634
+ async resolveBinding(input) {
635
+ // OMP can verify and resume the transcript only after startup via switch_session.
636
+ return structuredClone(input.binding);
641
637
  }
642
638
  }
643
639
  export const OMP_AGENT_RUNTIME_DRIVER = {
@@ -135,6 +135,7 @@ function cloneProfileForPiSession(input) {
135
135
  parentSessionId: profile.parentSessionId,
136
136
  model: profile.model,
137
137
  mainModel: profile.mainModel,
138
+ mainModelFallbacks: profile.mainModelFallbacks,
138
139
  subagentModel: profile.subagentModel,
139
140
  thinkingLevel: profile.thinkingLevel,
140
141
  mainThinkingLevel: profile.mainThinkingLevel,
@@ -148,6 +149,7 @@ function cloneProfileForPiSession(input) {
148
149
  mcpServers: profile.mcpServers,
149
150
  piPackages: profile.piPackages,
150
151
  contextFiles: profile.contextFiles,
152
+ diagnostics: profile.diagnostics,
151
153
  builtinTools: profile.builtinTools,
152
154
  builtinToolNames: profile.builtinToolNames,
153
155
  autoContextFiles: profile.autoContextFiles,
@@ -276,7 +278,7 @@ class PiAgentRuntimeSession {
276
278
  bindingNativeSessionId;
277
279
  nativePresenceExpected;
278
280
  disposed = false;
279
- constructor(runtimeInstanceId, piboSessionId, runtime, binding, initialFastMode, providerWebSearchEnabled) {
281
+ constructor(runtimeInstanceId, piboSessionId, runtime, binding, initialFastMode, providerWebSearchEnabled, providerFallbacksEnabled) {
280
282
  this.runtimeInstanceId = runtimeInstanceId;
281
283
  this.piboSessionId = piboSessionId;
282
284
  this.runtime = runtime;
@@ -284,7 +286,7 @@ class PiAgentRuntimeSession {
284
286
  this.cwd = runtime.cwd;
285
287
  this.bindingNativeSessionId = runtime.session.sessionId;
286
288
  this.nativePresenceExpected = runtime.session.sessionManager.buildSessionContext().messages.length > 0;
287
- this.routed = new PiRoutedSession(piboSessionId, runtime, (event) => this.handlePiboEvent(event), PiboPluginRegistry.create(), true, undefined, initialFastMode, undefined, undefined, (state) => this.handleEngineState(state));
289
+ this.routed = new PiRoutedSession(piboSessionId, runtime, (event) => this.handlePiboEvent(event), PiboPluginRegistry.create(), true, undefined, initialFastMode, undefined, undefined, (state) => this.handleEngineState(state), undefined, undefined, providerFallbacksEnabled);
288
290
  if (providerWebSearchEnabled)
289
291
  this.routed.enableProviderWebSearchObservation();
290
292
  this.controls = this.createControls();
@@ -685,7 +687,7 @@ class PiAgentRuntimeAdapter {
685
687
  && runtime.session.sessionManager.buildSessionContext().messages.length > 0,
686
688
  },
687
689
  };
688
- return new PiAgentRuntimeSession(this.instanceId, input.piboSession.id, runtime, binding, compatibility?.initialFastMode ?? false, profile.tools.some((tool) => tool.enabled !== false && isWebSearchProviderTool(tool)));
690
+ return new PiAgentRuntimeSession(this.instanceId, input.piboSession.id, runtime, binding, compatibility?.initialFastMode ?? false, profile.tools.some((tool) => tool.enabled !== false && isWebSearchProviderTool(tool)), compatibility?.providerFallbacksEnabled ?? false);
689
691
  }
690
692
  loadModelCatalog() {
691
693
  const now = Date.now();
@@ -1,3 +1,5 @@
1
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
2
+ import { dirname } from "node:path";
1
3
  import { SessionManager, shouldCompact } from "@earendil-works/pi-coding-agent";
2
4
  import { PiboSteeringUnavailableError } from "../../core/events.js";
3
5
  import { getOpenAiCodexProviderUsageForActiveModel } from "../../auth/openai-codex-usage.js";
@@ -18,6 +20,16 @@ const RUN_REMINDER_CAPABILITY_TOOLS = new Set([
18
20
  "pibo_run_cancel",
19
21
  "pibo_run_ack",
20
22
  ]);
23
+ function materializeForkedSession(sessionManager) {
24
+ const sessionFile = sessionManager.getSessionFile();
25
+ const header = sessionManager.getHeader();
26
+ if (!sessionManager.isPersisted() || !sessionFile || !header || existsSync(sessionFile))
27
+ return;
28
+ mkdirSync(dirname(sessionFile), { recursive: true });
29
+ writeFileSync(sessionFile, `${[header, ...sessionManager.getEntries()].map((entry) => JSON.stringify(entry)).join("\n")}\n`, { encoding: "utf8", flag: "wx" });
30
+ // Reload through the public API so Pi also records that the file is flushed.
31
+ sessionManager.setSessionFile(sessionFile);
32
+ }
21
33
  function modelSupportsFastServiceTier(model) {
22
34
  if (!model)
23
35
  return false;
@@ -587,6 +599,7 @@ export class RoutedSession {
587
599
  onStateChange;
588
600
  onMessagesInterrupted;
589
601
  messagePreflight;
602
+ providerFallbacksEnabled;
590
603
  queue = [];
591
604
  processing = false;
592
605
  disposed = false;
@@ -605,6 +618,9 @@ export class RoutedSession {
605
618
  nextAssistantIndex = 0;
606
619
  activeThinkingIndex;
607
620
  nextThinkingIndex = 0;
621
+ nextUsageIndex = 0;
622
+ activeCompactionIndex;
623
+ nextCompactionIndex = 0;
608
624
  sessionIdentityOperationInFlight = false;
609
625
  pendingAssistantError;
610
626
  pendingAssistantErrorRetryable = false;
@@ -616,7 +632,7 @@ export class RoutedSession {
616
632
  recoverySession;
617
633
  isContinuePatched = false;
618
634
  compatibilityAuthController;
619
- constructor(piboSessionId, runtime, emit, pluginRegistry, forwardPiEvents, onPiEventTelemetry, initialFastMode, onSessionOperation, onKillChildren, onStateChange, onMessagesInterrupted, messagePreflight) {
635
+ constructor(piboSessionId, runtime, emit, pluginRegistry, forwardPiEvents, onPiEventTelemetry, initialFastMode, onSessionOperation, onKillChildren, onStateChange, onMessagesInterrupted, messagePreflight, providerFallbacksEnabled = false) {
620
636
  this.piboSessionId = piboSessionId;
621
637
  this.runtime = runtime;
622
638
  this.emit = emit;
@@ -628,6 +644,7 @@ export class RoutedSession {
628
644
  this.onStateChange = onStateChange;
629
645
  this.onMessagesInterrupted = onMessagesInterrupted;
630
646
  this.messagePreflight = messagePreflight;
647
+ this.providerFallbacksEnabled = providerFallbacksEnabled;
631
648
  this.fastMode = initialFastMode && this.fastModeSupported();
632
649
  this.bindRuntimeSession();
633
650
  this.patchProviderRequest();
@@ -1005,26 +1022,34 @@ export class RoutedSession {
1005
1022
  const candidate = event;
1006
1023
  const eventId = this.activeMessage?.id ?? this.activeExecutionEvent?.id;
1007
1024
  if (candidate.type === "compaction_start") {
1025
+ const compactionIndex = this.activeCompactionIndex ?? this.nextCompactionIndex;
1026
+ if (this.activeCompactionIndex === undefined)
1027
+ this.nextCompactionIndex += 1;
1028
+ this.activeCompactionIndex = compactionIndex;
1008
1029
  this.emit(this.withActiveMessage({
1009
1030
  type: "compaction_start",
1010
1031
  piboSessionId: this.piboSessionId,
1011
1032
  eventId,
1033
+ compactionIndex,
1012
1034
  reason: typeof candidate.reason === "string" ? candidate.reason : "unknown",
1013
1035
  }));
1014
1036
  }
1015
1037
  if (candidate.type === "compaction_end") {
1038
+ const compactionIndex = this.activeCompactionIndex ?? this.nextCompactionIndex;
1039
+ if (this.activeCompactionIndex === undefined)
1040
+ this.nextCompactionIndex += 1;
1041
+ this.activeCompactionIndex = undefined;
1016
1042
  if (candidate.result && candidate.aborted !== true) {
1017
- // Reset assistant message indices so the next assistant response starts
1018
- // fresh after compaction, matching the reduced agent context.
1043
+ // End any open output block, but keep durable part indices monotonic for
1044
+ // the lifetime of the routed event across context-guard continuation.
1019
1045
  this.activeAssistantIndex = undefined;
1020
- this.nextAssistantIndex = 0;
1021
1046
  this.activeThinkingIndex = undefined;
1022
- this.nextThinkingIndex = 0;
1023
1047
  }
1024
1048
  this.emit(this.withActiveMessage({
1025
1049
  type: "compaction_end",
1026
1050
  piboSessionId: this.piboSessionId,
1027
1051
  eventId,
1052
+ compactionIndex,
1028
1053
  reason: typeof candidate.reason === "string" ? candidate.reason : "unknown",
1029
1054
  result: candidate.result,
1030
1055
  aborted: candidate.aborted === true,
@@ -1082,9 +1107,15 @@ export class RoutedSession {
1082
1107
  if (event.action === "compact") {
1083
1108
  return this.enqueueCompactAction(event);
1084
1109
  }
1085
- const changesSessionIdentity = event.action === "session.fork" || event.action === "session.clone";
1086
- if (changesSessionIdentity) {
1087
- this.assertSessionIdentityOperationIdle(event.action === "session.fork" ? "fork" : "clone");
1110
+ const sessionIdentityOperation = event.action === "session.fork"
1111
+ ? "fork"
1112
+ : event.action === "session.clone"
1113
+ ? "clone"
1114
+ : event.action === "session.switch"
1115
+ ? "switch"
1116
+ : undefined;
1117
+ if (sessionIdentityOperation) {
1118
+ this.assertSessionIdentityOperationIdle(sessionIdentityOperation);
1088
1119
  this.sessionIdentityOperationInFlight = true;
1089
1120
  }
1090
1121
  try {
@@ -1102,7 +1133,7 @@ export class RoutedSession {
1102
1133
  return output;
1103
1134
  }
1104
1135
  finally {
1105
- if (changesSessionIdentity)
1136
+ if (sessionIdentityOperation)
1106
1137
  this.sessionIdentityOperationInFlight = false;
1107
1138
  }
1108
1139
  }
@@ -1213,6 +1244,11 @@ export class RoutedSession {
1213
1244
  this.assertSessionWorkIdle("fork");
1214
1245
  const previous = this.createSessionSnapshot();
1215
1246
  const result = await this.runtime.fork(entryId);
1247
+ if (!result.cancelled) {
1248
+ // Pi defers persistence until an assistant message. Materialize an empty
1249
+ // first-message branch so history readers can use its advertised path.
1250
+ materializeForkedSession(this.runtime.session.sessionManager);
1251
+ }
1216
1252
  return {
1217
1253
  piboSessionId: this.piboSessionId,
1218
1254
  previous,
@@ -1518,6 +1554,9 @@ export class RoutedSession {
1518
1554
  this.nextAssistantIndex = 0;
1519
1555
  this.activeThinkingIndex = undefined;
1520
1556
  this.nextThinkingIndex = 0;
1557
+ this.nextUsageIndex = 0;
1558
+ this.activeCompactionIndex = undefined;
1559
+ this.nextCompactionIndex = 0;
1521
1560
  this.emit({
1522
1561
  type: "message_started",
1523
1562
  piboSessionId: this.piboSessionId,
@@ -1539,7 +1578,8 @@ export class RoutedSession {
1539
1578
  await this.resumeContextGuardRecovery(session);
1540
1579
  if (this.disposed || inFlight.cancelled)
1541
1580
  return;
1542
- await this.recoverTransientProviderErrors(session);
1581
+ if (!this.providerFallbacksEnabled)
1582
+ await this.recoverTransientProviderErrors(session);
1543
1583
  if (this.disposed || inFlight.cancelled)
1544
1584
  return;
1545
1585
  this.flushPendingAssistantError();
@@ -1578,6 +1618,9 @@ export class RoutedSession {
1578
1618
  this.nextAssistantIndex = 0;
1579
1619
  this.activeThinkingIndex = undefined;
1580
1620
  this.nextThinkingIndex = 0;
1621
+ this.nextUsageIndex = 0;
1622
+ this.activeCompactionIndex = undefined;
1623
+ this.nextCompactionIndex = 0;
1581
1624
  this.providerWebSearchLifecycle.clear();
1582
1625
  if (this.inFlightMessage === inFlight)
1583
1626
  this.inFlightMessage = undefined;
@@ -1817,8 +1860,12 @@ export class RoutedSession {
1817
1860
  this.activeThinkingIndex = undefined;
1818
1861
  return output;
1819
1862
  }
1820
- if (event.type === "assistant_usage" ||
1821
- event.type === "compaction_start" ||
1863
+ if (event.type === "assistant_usage") {
1864
+ const usageIndex = this.nextUsageIndex;
1865
+ this.nextUsageIndex += 1;
1866
+ return { ...event, ...correlation, usageIndex };
1867
+ }
1868
+ if (event.type === "compaction_start" ||
1822
1869
  event.type === "compaction_end" ||
1823
1870
  event.type === "tool_call" ||
1824
1871
  event.type === "tool_execution_started" ||
@@ -332,6 +332,10 @@ export async function createPiboRuntime(options = {}) {
332
332
  }
333
333
  const resourceLoader = services.resourceLoader;
334
334
  const diagnostics = [
335
+ ...profile.diagnostics.map((diagnostic) => ({
336
+ type: diagnostic.severity,
337
+ message: `[${diagnostic.code}] ${diagnostic.message}`,
338
+ })),
335
339
  ...piPackageOptions.diagnostics,
336
340
  ...services.diagnostics,
337
341
  ...collectResourceDiagnostics(resourceLoader.getSkills().diagnostics),
@@ -416,6 +420,7 @@ export async function inspectPiboProfile(options = {}) {
416
420
  mcpServers: profile.mcpServers,
417
421
  piPackages: profile.piPackages,
418
422
  contextFiles: profile.contextFiles,
423
+ diagnostics: profile.diagnostics,
419
424
  builtinTools: profile.builtinTools,
420
425
  builtinToolNames: profile.builtinToolNames,
421
426
  autoContextFiles: profile.autoContextFiles,
@@ -456,6 +461,7 @@ export async function inspectPiboProfile(options = {}) {
456
461
  runtimeOptions: structuredClone(profile.runtimeOptions),
457
462
  ...(profile.model ? { model: { ...profile.model } } : {}),
458
463
  ...(profile.mainModel ? { mainModel: { ...profile.mainModel } } : {}),
464
+ mainModelFallbacks: profile.mainModelFallbacks.map((model) => ({ ...model })),
459
465
  ...(profile.subagentModel ? { subagentModel: { ...profile.subagentModel } } : {}),
460
466
  ...(profile.thinkingLevel ? { thinkingLevel: profile.thinkingLevel } : {}),
461
467
  ...(profile.mainThinkingLevel ? { mainThinkingLevel: profile.mainThinkingLevel } : {}),
@@ -14,6 +14,14 @@ export function createCustomAgentProfileDefinition(agent, options = {}) {
14
14
  catch (error) {
15
15
  if (!isUnknownSkillError(error, skillName))
16
16
  throw error;
17
+ const message = `Unknown skill "${skillName}" referenced by custom agent "${agent.profileName}"`;
18
+ builder.addDiagnostic({
19
+ severity: "warning",
20
+ code: "custom_agent_unknown_skill",
21
+ message,
22
+ resourceKind: "skill",
23
+ resourceName: skillName,
24
+ });
17
25
  if (shouldWarnMissingReferences)
18
26
  console.warn(`Skipping unknown skill "${skillName}" for custom agent "${agent.profileName}"`);
19
27
  }
@@ -25,12 +33,29 @@ export function createCustomAgentProfileDefinition(agent, options = {}) {
25
33
  catch (error) {
26
34
  if (!isUnknownContextFileError(error, contextFileKey))
27
35
  throw error;
36
+ const message = `Unknown context file "${contextFileKey}" referenced by custom agent "${agent.profileName}"`;
37
+ builder.addDiagnostic({
38
+ severity: "warning",
39
+ code: "custom_agent_unknown_context_file",
40
+ message,
41
+ resourceKind: "context-file",
42
+ resourceName: contextFileKey,
43
+ });
28
44
  if (shouldWarnMissingReferences)
29
45
  console.warn(`Skipping unknown context file "${contextFileKey}" for custom agent "${agent.profileName}"`);
30
46
  }
31
47
  }
32
- for (const toolName of agent.nativeTools)
33
- builder.addTool(context.getTool(toolName));
48
+ for (const toolName of agent.nativeTools) {
49
+ try {
50
+ builder.addTool(context.getTool(toolName));
51
+ }
52
+ catch (error) {
53
+ if (!isUnknownToolError(error, toolName))
54
+ throw error;
55
+ if (shouldWarnMissingReferences)
56
+ console.warn(`Skipping unknown tool "${toolName}" for custom agent "${agent.profileName}"`);
57
+ }
58
+ }
34
59
  for (const subagent of agent.subagents)
35
60
  builder.addSubagent(subagent);
36
61
  return builder.createSession();
@@ -61,6 +86,7 @@ function createCustomAgentBuilder(agent) {
61
86
  .withToolPackages({ runControl: agent.runControl, goalControl: agent.goalControl ?? true });
62
87
  if (agent.mainModel)
63
88
  builder.withMainModel(agent.mainModel);
89
+ builder.withMainModelFallbacks(agent.mainModelFallbacks ?? []);
64
90
  if (agent.subagentModel)
65
91
  builder.withSubagentModel(agent.subagentModel);
66
92
  if (agent.thinkingLevel)
@@ -80,6 +106,9 @@ function createCustomAgentBuilder(agent) {
80
106
  function uniqueAliases(aliases, profileName) {
81
107
  return [...new Set(aliases.filter((alias) => alias && alias !== profileName))];
82
108
  }
109
+ function isUnknownToolError(error, toolName) {
110
+ return error instanceof Error && error.message === `Unknown tool "${toolName}"`;
111
+ }
83
112
  function isUnknownContextFileError(error, contextFileKey) {
84
113
  return error instanceof Error && error.message === `Unknown context file "${contextFileKey}"`;
85
114
  }