@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
package/README.md CHANGED
@@ -24,6 +24,19 @@ pibo --help
24
24
 
25
25
  The current package declares Node.js 24+. Some Linux distributions still ship Node 22 through `apt`, so install a recent Node release first when needed. A user-local Node/npm setup through `nvm`, `fnm`, or a user-writable npm prefix works well.
26
26
 
27
+ Package installation has no host-level side effects. For a self-hosted server, choose a supported setup profile after installing the CLI:
28
+
29
+ ```bash
30
+ # Recommended complete workstation
31
+ pibo setup plan --profile batteries-included --domain pibo.example.com
32
+ sudo pibo setup install --profile batteries-included --domain pibo.example.com --apply --yes
33
+
34
+ # Minimal gateway and Chat Web only
35
+ pibo setup plan --profile vanilla --domain pibo.example.com
36
+ ```
37
+
38
+ See [Installation profiles](./docs/project/installation-profiles.md) for components, resource and security costs, staging, upgrades, and rollback.
39
+
27
40
  For a user-local global npm prefix:
28
41
 
29
42
  ```bash
@@ -67,17 +80,18 @@ A fresh server install usually needs only three decisions:
67
80
  2. Install Node.js 24+ for that user or system-wide.
68
81
  3. Install Pibo through npm.
69
82
 
70
- Use the setup planner to keep the first run simple:
83
+ Use an installation profile for the normal first run. Batteries Included is recommended; Vanilla is the minimal controlled base:
71
84
 
72
85
  ```bash
73
86
  pibo setup doctor --domain pibo.example.com --expected-ip <server-ip>
74
- pibo setup user-host --domain pibo.example.com --print-files
75
- pibo setup user-host --domain pibo.example.com --write-to /tmp/pibo-setup
87
+ pibo setup plan --profile batteries-included --domain pibo.example.com
88
+ pibo setup install --profile batteries-included --domain pibo.example.com --write-to /tmp/pibo-setup
89
+ sudo pibo setup install --profile batteries-included --domain pibo.example.com --apply --yes
76
90
  ```
77
91
 
78
- This is the normal user path: one gateway, one `PIBO_HOME`, no required Docker, no dev gateway, and no GitHub App setup. After reviewing staged files, use `--apply --yes` to write the generated systemd/Caddy files.
92
+ The package install remains side-effect free. The explicit setup command records ownership, checks pinned downloads, configures the selected components, and exposes status, upgrade, and uninstall plans. See [Installation profiles](./docs/project/installation-profiles.md).
79
93
 
80
- Developer hosts are opt-in and add production/dev separation plus Docker compute workers:
94
+ Core developers can still use the topology-specific developer-host planner to add production/dev separation plus Docker compute workers:
81
95
 
82
96
  ```bash
83
97
  pibo setup developer-host \
@@ -159,7 +173,7 @@ pibo mcp # discover and call configured MCP servers
159
173
  pibo tools # install and inspect curated external CLI tools
160
174
  pibo pi-packages # register Pi Coding Agent packages
161
175
  pibo debug # inspect local Pibo data stores
162
- pibo setup # plan user-host installs and developer-host upgrades
176
+ pibo setup # plan and manage supported host installation profiles
163
177
  pibo profile # inspect runtime profiles
164
178
  pibo tui # start the direct Pi TUI
165
179
  pibo tui:routed # start the routed Pibo TUI
@@ -17,6 +17,7 @@ export function profileWithRuntimeInstance(profile, runtimeInstanceId) {
17
17
  parentSessionId: profile.parentSessionId,
18
18
  model: undefined,
19
19
  mainModel: undefined,
20
+ mainModelFallbacks: [],
20
21
  subagentModel: undefined,
21
22
  thinkingLevel: profile.thinkingLevel,
22
23
  mainThinkingLevel: profile.mainThinkingLevel,
@@ -30,6 +31,7 @@ export function profileWithRuntimeInstance(profile, runtimeInstanceId) {
30
31
  mcpServers: profile.mcpServers,
31
32
  piPackages: profile.piPackages,
32
33
  contextFiles: profile.contextFiles,
34
+ diagnostics: profile.diagnostics,
33
35
  builtinTools: profile.builtinTools,
34
36
  builtinToolNames: profile.builtinToolNames,
35
37
  autoContextFiles: profile.autoContextFiles,
@@ -229,6 +231,7 @@ export function buildPortableRuntimeContextSnapshot(input) {
229
231
  metadata: {
230
232
  activeModel: runtimeManifest.effectiveModel,
231
233
  mainModel: profile.mainModel,
234
+ mainModelFallbacks: profile.mainModelFallbacks,
232
235
  subagentModel: profile.subagentModel,
233
236
  mainThinkingLevel: profile.mainThinkingLevel ?? profile.thinkingLevel,
234
237
  subagentThinkingLevel: profile.subagentThinkingLevel ?? profile.thinkingLevel,
@@ -240,6 +243,21 @@ export function buildPortableRuntimeContextSnapshot(input) {
240
243
  ?? input.runtime.capabilities.nativeSubagents.enabledByDefault,
241
244
  },
242
245
  });
246
+ for (const [index, diagnostic] of profile.diagnostics.entries()) {
247
+ addNode({
248
+ id: `profile-diagnostic-${index}`,
249
+ kind: "diagnostic",
250
+ title: diagnostic.code,
251
+ source: "profile",
252
+ state: diagnostic.severity === "error" ? "error" : "warning",
253
+ badges: [diagnostic.severity.toUpperCase(), "PROFILE"],
254
+ hydratedText: diagnostic.message,
255
+ metadata: diagnostic.resourceName ? {
256
+ resourceKind: diagnostic.resourceKind,
257
+ resourceName: diagnostic.resourceName,
258
+ } : undefined,
259
+ });
260
+ }
243
261
  for (const [index, diagnostic] of input.runtime.diagnostics.entries()) {
244
262
  addNode({
245
263
  id: `runtime-diagnostic-${index}`,
@@ -265,9 +283,11 @@ export function buildPortableRuntimeContextSnapshot(input) {
265
283
  });
266
284
  }
267
285
  const resourceDiagnostics = input.resources?.diagnostics ?? [];
268
- const warnings = input.runtime.diagnostics.filter((diagnostic) => diagnostic.severity === "warning").length
286
+ const warnings = profile.diagnostics.filter((diagnostic) => diagnostic.severity === "warning").length
287
+ + input.runtime.diagnostics.filter((diagnostic) => diagnostic.severity === "warning").length
269
288
  + resourceDiagnostics.filter((diagnostic) => diagnostic.severity === "warning").length;
270
- const errors = input.runtime.diagnostics.filter((diagnostic) => diagnostic.severity === "error").length
289
+ const errors = profile.diagnostics.filter((diagnostic) => diagnostic.severity === "error").length
290
+ + input.runtime.diagnostics.filter((diagnostic) => diagnostic.severity === "error").length
271
291
  + resourceDiagnostics.filter((diagnostic) => diagnostic.severity === "error").length;
272
292
  return {
273
293
  version: 1,
@@ -287,6 +307,10 @@ export function buildPortableRuntimeContextSnapshot(input) {
287
307
  },
288
308
  nodes,
289
309
  diagnostics: [
310
+ ...profile.diagnostics.map((diagnostic) => ({
311
+ type: diagnostic.severity,
312
+ message: `[${diagnostic.code}] ${diagnostic.message}`,
313
+ })),
290
314
  ...input.runtime.diagnostics.map((diagnostic) => ({ type: diagnostic.severity, message: diagnostic.message })),
291
315
  ...resourceDiagnostics.map((diagnostic) => ({ type: diagnostic.severity, message: diagnostic.message })),
292
316
  ],
@@ -1,5 +1,6 @@
1
1
  import { PiboSteeringUnavailableError, } from "../core/events.js";
2
- import { runtimeSessionErrorDetails } from "../core/session-errors.js";
2
+ import { PIBO_PROVIDER_RECOVERY_PROMPT, isPiboProviderFallbackError } from "../core/provider-recovery.js";
3
+ import { normalizeSessionErrorDetails, runtimeSessionErrorDetails } from "../core/session-errors.js";
3
4
  import { isPiboThinkingLevel } from "../core/thinking.js";
4
5
  import { AgentRuntimeCapabilityUnavailableError } from "./errors.js";
5
6
  const RUN_REMINDER_CAPABILITY_TOOLS = new Set([
@@ -29,6 +30,23 @@ function errorMessage(error) {
29
30
  function promptSource(source) {
30
31
  return source === "user" || source === "ui" ? "interactive" : "rpc";
31
32
  }
33
+ function sameModel(left, right) {
34
+ return Boolean(left && right && left.provider === right.provider && left.id === right.id);
35
+ }
36
+ function uniqueModels(models) {
37
+ const seen = new Set();
38
+ return models.flatMap((model) => {
39
+ const provider = model.provider.trim();
40
+ const id = model.id.trim();
41
+ if (!provider || !id)
42
+ return [];
43
+ const key = `${provider}\u0000${id}`;
44
+ if (seen.has(key))
45
+ return [];
46
+ seen.add(key);
47
+ return [{ provider, id }];
48
+ });
49
+ }
32
50
  function runtimeCapabilityError(session, capability) {
33
51
  return new AgentRuntimeCapabilityUnavailableError(capability, session.runtimeInstanceId, `Runtime instance "${session.runtimeInstanceId}" does not support ${capability}.`);
34
52
  }
@@ -120,6 +138,10 @@ export class RuntimeRoutedSession {
120
138
  nextThinkingIndex = 0;
121
139
  sessionIdentityOperationInFlight = false;
122
140
  forkCandidatesRequest;
141
+ primaryModel;
142
+ modelFallbacks;
143
+ suppressProviderFailures = false;
144
+ pendingProviderFailure;
123
145
  unsubscribe;
124
146
  constructor(piboSessionId, runtimeSession, emit, pluginRegistry, options = {}) {
125
147
  this.piboSessionId = piboSessionId;
@@ -128,6 +150,10 @@ export class RuntimeRoutedSession {
128
150
  this.pluginRegistry = pluginRegistry;
129
151
  this.options = options;
130
152
  this.runtime = runtimeSession.getNativeCompatibilityHandle?.() ?? runtimeSession;
153
+ this.primaryModel = runtimeSession.getStatus().activeModel
154
+ ? { ...runtimeSession.getStatus().activeModel }
155
+ : undefined;
156
+ this.modelFallbacks = uniqueModels(options.modelFallbacks ?? []);
131
157
  this.unsubscribe = runtimeSession.subscribe((event) => this.handleRuntimeEvent(event));
132
158
  }
133
159
  enqueueMessage(event, onAccepted = () => { }) {
@@ -407,7 +433,9 @@ export class RuntimeRoutedSession {
407
433
  const setModel = this.runtimeSession.controls?.setModel;
408
434
  if (!setModel)
409
435
  throw runtimeCapabilityError(this.runtimeSession, "in-session model switching");
410
- return await setModel(model);
436
+ const selected = await setModel(model);
437
+ this.primaryModel = { ...selected };
438
+ return selected;
411
439
  }
412
440
  setThinkingLevel(level) {
413
441
  const setReasoning = this.runtimeSession.controls?.setReasoning;
@@ -642,10 +670,10 @@ export class RuntimeRoutedSession {
642
670
  }));
643
671
  return;
644
672
  case "error":
645
- this.emitRuntimeError(event.message, event.details);
673
+ this.handleRuntimeFailure(event.message, event.details);
646
674
  return;
647
675
  case "turn_failed":
648
- this.emitRuntimeError(event.message, event.details);
676
+ this.handleRuntimeFailure(event.message, event.details);
649
677
  return;
650
678
  case "native_event":
651
679
  this.options.onNativeEventTelemetry?.(this.piboSessionId, event.event, {
@@ -661,6 +689,14 @@ export class RuntimeRoutedSession {
661
689
  return;
662
690
  }
663
691
  }
692
+ handleRuntimeFailure(message, details) {
693
+ const normalized = normalizeSessionErrorDetails(message, details);
694
+ if (this.suppressProviderFailures && isPiboProviderFallbackError(message, normalized)) {
695
+ this.pendingProviderFailure = { message, details: normalized };
696
+ return;
697
+ }
698
+ this.emitRuntimeError(message, normalized);
699
+ }
664
700
  emitRuntimeError(message, details) {
665
701
  if (this.activeMessageFailed)
666
702
  return;
@@ -706,6 +742,71 @@ export class RuntimeRoutedSession {
706
742
  this.notifyState();
707
743
  }
708
744
  }
745
+ async promptWithModelFallbacks(event, inFlight) {
746
+ const setModel = this.runtimeSession.controls?.setModel;
747
+ const fallbackModels = this.modelFallbacks.filter((model) => !sameModel(model, this.primaryModel));
748
+ if (!setModel || fallbackModels.length === 0) {
749
+ await this.runtimeSession.prompt({
750
+ text: event.text,
751
+ source: promptSource(event.source),
752
+ capabilityScope: event.capabilityScope,
753
+ });
754
+ return;
755
+ }
756
+ this.suppressProviderFailures = true;
757
+ let switchedModel = false;
758
+ let finalFailure;
759
+ try {
760
+ for (let attempt = 0; attempt <= fallbackModels.length; attempt += 1) {
761
+ if (this.disposed || inFlight.cancelled)
762
+ return;
763
+ if (attempt > 0) {
764
+ try {
765
+ await setModel(fallbackModels[attempt - 1]);
766
+ switchedModel = true;
767
+ }
768
+ catch {
769
+ continue;
770
+ }
771
+ }
772
+ this.pendingProviderFailure = undefined;
773
+ let promptError;
774
+ try {
775
+ await this.runtimeSession.prompt({
776
+ text: attempt === 0 ? event.text : PIBO_PROVIDER_RECOVERY_PROMPT,
777
+ source: attempt === 0 ? promptSource(event.source) : "rpc",
778
+ capabilityScope: event.capabilityScope,
779
+ });
780
+ }
781
+ catch (error) {
782
+ promptError = error;
783
+ }
784
+ if (this.disposed || inFlight.cancelled)
785
+ return;
786
+ const failure = this.pendingProviderFailure;
787
+ if (!failure) {
788
+ if (promptError)
789
+ throw promptError;
790
+ return;
791
+ }
792
+ finalFailure = failure;
793
+ }
794
+ if (finalFailure)
795
+ this.emitRuntimeError(finalFailure.message, finalFailure.details);
796
+ }
797
+ finally {
798
+ this.suppressProviderFailures = false;
799
+ this.pendingProviderFailure = undefined;
800
+ if (switchedModel && this.primaryModel && !this.disposed) {
801
+ try {
802
+ await setModel(this.primaryModel);
803
+ }
804
+ catch (error) {
805
+ console.warn(`Failed to restore primary model for Pibo session "${this.piboSessionId}": ${errorMessage(error)}`);
806
+ }
807
+ }
808
+ }
809
+ }
709
810
  async processQueuedMessage(event) {
710
811
  const inFlight = this.beginInFlightMessage(event);
711
812
  try {
@@ -743,11 +844,7 @@ export class RuntimeRoutedSession {
743
844
  });
744
845
  if (inFlight.cancelled)
745
846
  return;
746
- await this.runtimeSession.prompt({
747
- text: event.text,
748
- source: promptSource(event.source),
749
- capabilityScope: event.capabilityScope,
750
- });
847
+ await this.promptWithModelFallbacks(event, inFlight);
751
848
  if (this.disposed || inFlight.cancelled)
752
849
  return;
753
850
  if (!this.activeMessageFailed) {
@@ -776,6 +873,8 @@ export class RuntimeRoutedSession {
776
873
  }
777
874
  }
778
875
  finally {
876
+ this.suppressProviderFailures = false;
877
+ this.pendingProviderFailure = undefined;
779
878
  this.activeMessage = undefined;
780
879
  this.activeMessageFailed = false;
781
880
  this.resetContentIndices();
@@ -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,
@@ -1213,6 +1238,11 @@ export class RoutedSession {
1213
1238
  this.assertSessionWorkIdle("fork");
1214
1239
  const previous = this.createSessionSnapshot();
1215
1240
  const result = await this.runtime.fork(entryId);
1241
+ if (!result.cancelled) {
1242
+ // Pi defers persistence until an assistant message. Materialize an empty
1243
+ // first-message branch so history readers can use its advertised path.
1244
+ materializeForkedSession(this.runtime.session.sessionManager);
1245
+ }
1216
1246
  return {
1217
1247
  piboSessionId: this.piboSessionId,
1218
1248
  previous,
@@ -1518,6 +1548,9 @@ export class RoutedSession {
1518
1548
  this.nextAssistantIndex = 0;
1519
1549
  this.activeThinkingIndex = undefined;
1520
1550
  this.nextThinkingIndex = 0;
1551
+ this.nextUsageIndex = 0;
1552
+ this.activeCompactionIndex = undefined;
1553
+ this.nextCompactionIndex = 0;
1521
1554
  this.emit({
1522
1555
  type: "message_started",
1523
1556
  piboSessionId: this.piboSessionId,
@@ -1539,7 +1572,8 @@ export class RoutedSession {
1539
1572
  await this.resumeContextGuardRecovery(session);
1540
1573
  if (this.disposed || inFlight.cancelled)
1541
1574
  return;
1542
- await this.recoverTransientProviderErrors(session);
1575
+ if (!this.providerFallbacksEnabled)
1576
+ await this.recoverTransientProviderErrors(session);
1543
1577
  if (this.disposed || inFlight.cancelled)
1544
1578
  return;
1545
1579
  this.flushPendingAssistantError();
@@ -1578,6 +1612,9 @@ export class RoutedSession {
1578
1612
  this.nextAssistantIndex = 0;
1579
1613
  this.activeThinkingIndex = undefined;
1580
1614
  this.nextThinkingIndex = 0;
1615
+ this.nextUsageIndex = 0;
1616
+ this.activeCompactionIndex = undefined;
1617
+ this.nextCompactionIndex = 0;
1581
1618
  this.providerWebSearchLifecycle.clear();
1582
1619
  if (this.inFlightMessage === inFlight)
1583
1620
  this.inFlightMessage = undefined;
@@ -1817,8 +1854,12 @@ export class RoutedSession {
1817
1854
  this.activeThinkingIndex = undefined;
1818
1855
  return output;
1819
1856
  }
1820
- if (event.type === "assistant_usage" ||
1821
- event.type === "compaction_start" ||
1857
+ if (event.type === "assistant_usage") {
1858
+ const usageIndex = this.nextUsageIndex;
1859
+ this.nextUsageIndex += 1;
1860
+ return { ...event, ...correlation, usageIndex };
1861
+ }
1862
+ if (event.type === "compaction_start" ||
1822
1863
  event.type === "compaction_end" ||
1823
1864
  event.type === "tool_call" ||
1824
1865
  event.type === "tool_execution_started" ||