@oh-my-pi/pi-coding-agent 17.0.1 → 17.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 (126) hide show
  1. package/CHANGELOG.md +92 -20
  2. package/dist/cli.js +3485 -3448
  3. package/dist/types/advisor/config.d.ts +14 -6
  4. package/dist/types/advisor/runtime.d.ts +20 -11
  5. package/dist/types/autolearn/controller.d.ts +1 -0
  6. package/dist/types/config/model-discovery.d.ts +17 -0
  7. package/dist/types/config/model-resolver.d.ts +6 -2
  8. package/dist/types/config/settings-schema.d.ts +32 -7
  9. package/dist/types/config/settings.d.ts +50 -0
  10. package/dist/types/cursor.d.ts +11 -0
  11. package/dist/types/discovery/helpers.d.ts +5 -2
  12. package/dist/types/exec/bash-executor.d.ts +2 -0
  13. package/dist/types/extensibility/extensions/managed-timers.d.ts +15 -0
  14. package/dist/types/extensibility/extensions/runner.d.ts +7 -0
  15. package/dist/types/extensibility/extensions/types.d.ts +18 -0
  16. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +40 -0
  17. package/dist/types/extensibility/shared-events.d.ts +6 -0
  18. package/dist/types/extensibility/utils.d.ts +2 -2
  19. package/dist/types/mcp/transports/stdio.d.ts +13 -1
  20. package/dist/types/modes/components/advisor-config.d.ts +8 -1
  21. package/dist/types/modes/components/hook-editor.d.ts +7 -0
  22. package/dist/types/modes/components/model-hub.d.ts +5 -2
  23. package/dist/types/modes/components/session-selector.d.ts +2 -0
  24. package/dist/types/modes/controllers/command-controller.d.ts +8 -0
  25. package/dist/types/modes/controllers/selector-controller.d.ts +3 -1
  26. package/dist/types/modes/print-mode.d.ts +1 -1
  27. package/dist/types/modes/warp-events.d.ts +24 -0
  28. package/dist/types/modes/warp-events.test.d.ts +1 -0
  29. package/dist/types/plan-mode/model-transition.d.ts +47 -0
  30. package/dist/types/plan-mode/model-transition.test.d.ts +1 -0
  31. package/dist/types/registry/agent-lifecycle.d.ts +26 -1
  32. package/dist/types/sdk.d.ts +13 -2
  33. package/dist/types/session/agent-session.d.ts +34 -10
  34. package/dist/types/session/session-history-format.d.ts +10 -0
  35. package/dist/types/session/session-manager.d.ts +14 -0
  36. package/dist/types/slash-commands/helpers/active-oauth-account.d.ts +9 -0
  37. package/dist/types/task/label.d.ts +1 -1
  38. package/dist/types/telemetry-export.d.ts +34 -9
  39. package/dist/types/tools/approval.d.ts +8 -0
  40. package/dist/types/tools/bash.d.ts +2 -0
  41. package/dist/types/tools/essential-tools.d.ts +29 -0
  42. package/dist/types/tools/image-gen.d.ts +2 -1
  43. package/dist/types/tools/index.d.ts +1 -0
  44. package/dist/types/tools/xdev.d.ts +11 -2
  45. package/dist/types/utils/title-generator.d.ts +2 -1
  46. package/dist/types/web/search/providers/kimi.d.ts +4 -1
  47. package/dist/types/web/search/types.d.ts +1 -1
  48. package/package.json +21 -16
  49. package/src/advisor/__tests__/advisor.test.ts +1304 -42
  50. package/src/advisor/__tests__/config.test.ts +58 -2
  51. package/src/advisor/config.ts +76 -24
  52. package/src/advisor/runtime.ts +445 -92
  53. package/src/autolearn/controller.ts +23 -28
  54. package/src/cli.ts +5 -1
  55. package/src/config/model-discovery.ts +81 -21
  56. package/src/config/model-registry.ts +25 -6
  57. package/src/config/model-resolver.ts +14 -7
  58. package/src/config/settings-schema.ts +42 -6
  59. package/src/config/settings.ts +405 -25
  60. package/src/cursor.ts +20 -3
  61. package/src/debug/report-bundle.ts +40 -4
  62. package/src/discovery/helpers.ts +28 -5
  63. package/src/exec/bash-executor.ts +14 -5
  64. package/src/extensibility/custom-tools/loader.ts +3 -3
  65. package/src/extensibility/custom-tools/wrapper.ts +2 -1
  66. package/src/extensibility/extensions/loader.ts +3 -3
  67. package/src/extensibility/extensions/managed-timers.ts +83 -0
  68. package/src/extensibility/extensions/runner.ts +26 -0
  69. package/src/extensibility/extensions/types.ts +18 -0
  70. package/src/extensibility/extensions/wrapper.ts +2 -1
  71. package/src/extensibility/hooks/loader.ts +3 -3
  72. package/src/extensibility/legacy-pi-coding-agent-shim.ts +96 -1
  73. package/src/extensibility/plugins/legacy-pi-compat.ts +225 -22
  74. package/src/extensibility/plugins/manager.ts +2 -2
  75. package/src/extensibility/shared-events.ts +6 -0
  76. package/src/extensibility/utils.ts +91 -25
  77. package/src/irc/bus.ts +22 -3
  78. package/src/launch/broker.ts +3 -2
  79. package/src/launch/client.ts +2 -2
  80. package/src/launch/presence.ts +2 -2
  81. package/src/lsp/client.ts +1 -1
  82. package/src/main.ts +11 -8
  83. package/src/mcp/manager.ts +9 -3
  84. package/src/mcp/transports/stdio.ts +103 -23
  85. package/src/modes/components/advisor-config.ts +65 -3
  86. package/src/modes/components/ask-dialog.ts +1 -1
  87. package/src/modes/components/hook-editor.ts +18 -3
  88. package/src/modes/components/model-hub.ts +138 -42
  89. package/src/modes/components/session-selector.ts +4 -0
  90. package/src/modes/components/status-line/component.test.ts +1 -0
  91. package/src/modes/components/status-line/segments.ts +21 -6
  92. package/src/modes/controllers/command-controller.ts +167 -47
  93. package/src/modes/controllers/event-controller.ts +5 -0
  94. package/src/modes/controllers/extension-ui-controller.ts +4 -22
  95. package/src/modes/controllers/input-controller.ts +12 -12
  96. package/src/modes/controllers/selector-controller.ts +191 -31
  97. package/src/modes/interactive-mode.ts +139 -54
  98. package/src/modes/print-mode.ts +3 -3
  99. package/src/modes/rpc/host-tools.ts +2 -1
  100. package/src/modes/rpc/rpc-mode.ts +19 -4
  101. package/src/modes/warp-events.test.ts +794 -0
  102. package/src/modes/warp-events.ts +232 -0
  103. package/src/plan-mode/model-transition.test.ts +60 -0
  104. package/src/plan-mode/model-transition.ts +51 -0
  105. package/src/registry/agent-lifecycle.ts +133 -18
  106. package/src/sdk.ts +221 -42
  107. package/src/session/agent-session.ts +1285 -348
  108. package/src/session/session-history-format.ts +20 -5
  109. package/src/session/session-manager.ts +48 -0
  110. package/src/slash-commands/builtin-registry.ts +7 -0
  111. package/src/slash-commands/helpers/active-oauth-account.ts +16 -0
  112. package/src/task/executor.ts +1 -1
  113. package/src/task/label.ts +2 -0
  114. package/src/telemetry-export.ts +453 -97
  115. package/src/tools/approval.ts +11 -0
  116. package/src/tools/bash.ts +71 -38
  117. package/src/tools/essential-tools.ts +45 -0
  118. package/src/tools/gh.ts +169 -2
  119. package/src/tools/image-gen.ts +69 -7
  120. package/src/tools/index.ts +7 -5
  121. package/src/tools/read.ts +48 -3
  122. package/src/tools/write.ts +22 -4
  123. package/src/tools/xdev.ts +14 -3
  124. package/src/utils/title-generator.ts +15 -4
  125. package/src/web/search/providers/kimi.ts +18 -12
  126. package/src/web/search/types.ts +6 -1
@@ -56,8 +56,15 @@ import { reset as resetCapabilities } from "../capability";
56
56
  import type { CollabGuestLink } from "../collab/guest";
57
57
  import type { CollabHost } from "../collab/host";
58
58
  import { KeybindingsManager } from "../config/keybindings";
59
+ import type { ResolvedModelRoleValue } from "../config/model-resolver";
59
60
  import { applyProviderGlobalsFromSettings } from "../config/provider-globals";
60
- import { isSettingsInitialized, onStatusLineSessionAccentChanged, Settings, settings } from "../config/settings";
61
+ import {
62
+ isSettingsInitialized,
63
+ onModelRolesChanged,
64
+ onStatusLineSessionAccentChanged,
65
+ Settings,
66
+ settings,
67
+ } from "../config/settings";
61
68
  import { clearClaudePluginRootsCache } from "../discovery/helpers";
62
69
  import type {
63
70
  AutocompleteProviderFactory,
@@ -88,6 +95,7 @@ import {
88
95
  resolveApprovedPlan,
89
96
  resolvePlanTitle,
90
97
  } from "../plan-mode/approved-plan";
98
+ import { resolvePlanModelTransition } from "../plan-mode/model-transition";
91
99
  import planModeApprovedPrompt from "../prompts/system/plan-mode-approved.md" with { type: "text" };
92
100
  import planModeCompactInstructionsPrompt from "../prompts/system/plan-mode-compact-instructions.md" with {
93
101
  type: "text",
@@ -539,6 +547,8 @@ export class InteractiveMode implements InteractiveModeContext {
539
547
  #goalSuppressNextContinuation = false;
540
548
  #planModePreviousModelState: { model: Model; thinkingLevel?: ConfiguredThinkingLevel } | undefined;
541
549
  #pendingModelSwitch: { model: Model; thinkingLevel?: ConfiguredThinkingLevel } | undefined;
550
+ /** Whether #pendingModelSwitch was queued by the live plan-role reconciler. */
551
+ #pendingPlanModelSwitch = false;
542
552
  #planModeHasEntered = false;
543
553
  #planReviewOverlay: PlanReviewOverlay | undefined;
544
554
  #planReviewOverlayHandle: OverlayHandle | undefined;
@@ -1023,6 +1033,11 @@ export class InteractiveMode implements InteractiveModeContext {
1023
1033
  this.#handleSessionAccentInputsChanged();
1024
1034
  }),
1025
1035
  );
1036
+ this.#eventBusUnsubscribers.push(
1037
+ onModelRolesChanged(() => {
1038
+ void this.#reapplyPlanModeModelOnRoleChange();
1039
+ }),
1040
+ );
1026
1041
  this.#eventBusUnsubscribers.push(
1027
1042
  this.session.subscribeCommandMetadataChanged(() => {
1028
1043
  const retainedCommands = this.#pendingSlashCommands.filter(command => !command.name.startsWith("skill:"));
@@ -2080,35 +2095,81 @@ export class InteractiveMode implements InteractiveModeContext {
2080
2095
  if (!resolved.model) return;
2081
2096
 
2082
2097
  const currentModel = this.session.model;
2083
- const sameModel = modelsAreEqual(currentModel, resolved.model);
2084
- const planThinkingLevel = resolved.explicitThinkingLevel ? resolved.thinkingLevel : undefined;
2085
-
2098
+ // Capture the pre-plan model so #exitPlanMode can restore it. Only the
2099
+ // entry path records this a mid-planning role change (below) leaves the
2100
+ // active model on the plan role, so overwriting here would restore the old
2101
+ // plan model instead of the user's real pre-plan model.
2086
2102
  this.#planModePreviousModelState = currentModel
2087
2103
  ? { model: currentModel, thinkingLevel: this.session.configuredThinkingLevel() }
2088
2104
  : undefined;
2089
2105
 
2090
- if (!sameModel) {
2091
- if (this.session.isStreaming) {
2092
- this.#pendingModelSwitch = { model: resolved.model, thinkingLevel: planThinkingLevel };
2106
+ await this.#applyPlanModelTransition(currentModel, resolved);
2107
+ }
2108
+
2109
+ /**
2110
+ * Re-resolve the `plan` role and move the active model onto it. Fires when
2111
+ * the plan role is reassigned while plan mode is active: the active model IS
2112
+ * the plan model there, so a settings-only change would otherwise leave the
2113
+ * current turn on the model plan mode was entered with (issue #5657). No-op
2114
+ * outside plan mode — role reassignment for an inactive role only touches
2115
+ * settings.
2116
+ */
2117
+ async #reapplyPlanModeModelOnRoleChange(): Promise<void> {
2118
+ if (!this.planModeEnabled) return;
2119
+ const resolved = this.session.resolveRoleModelWithThinking("plan");
2120
+ if (!resolved.model) {
2121
+ this.#clearPendingPlanModelSwitch();
2122
+ return;
2123
+ }
2124
+ await this.#applyPlanModelTransition(this.session.model, resolved);
2125
+ }
2126
+
2127
+ /**
2128
+ * Drop a stale deferred switch that was queued for a previous plan-role
2129
+ * assignment. Other deferred switches (such as restoring the pre-plan
2130
+ * model) remain intact.
2131
+ */
2132
+ #clearPendingPlanModelSwitch(): void {
2133
+ if (!this.#pendingPlanModelSwitch) return;
2134
+ this.#pendingModelSwitch = undefined;
2135
+ this.#pendingPlanModelSwitch = false;
2136
+ }
2137
+
2138
+ /** Apply (or defer) the model/thinking change implied by the resolved plan role. */
2139
+ async #applyPlanModelTransition(currentModel: Model | undefined, resolved: ResolvedModelRoleValue): Promise<void> {
2140
+ const transition = resolvePlanModelTransition(currentModel, resolved, this.session.isStreaming);
2141
+ if (transition.kind !== "apply" || !transition.deferred) {
2142
+ this.#clearPendingPlanModelSwitch();
2143
+ }
2144
+ switch (transition.kind) {
2145
+ case "none":
2146
+ return;
2147
+ case "thinking":
2148
+ this.session.setThinkingLevel(transition.thinkingLevel);
2149
+ return;
2150
+ case "apply":
2151
+ if (transition.deferred) {
2152
+ this.#pendingModelSwitch = { model: transition.model, thinkingLevel: transition.thinkingLevel };
2153
+ this.#pendingPlanModelSwitch = true;
2154
+ return;
2155
+ }
2156
+ try {
2157
+ await this.session.setModelTemporary(transition.model, transition.thinkingLevel);
2158
+ } catch (error) {
2159
+ this.showWarning(
2160
+ `Failed to switch to plan model for plan mode: ${error instanceof Error ? error.message : String(error)}`,
2161
+ );
2162
+ }
2093
2163
  return;
2094
- }
2095
- try {
2096
- await this.session.setModelTemporary(resolved.model, planThinkingLevel);
2097
- } catch (error) {
2098
- this.showWarning(
2099
- `Failed to switch to plan model for plan mode: ${error instanceof Error ? error.message : String(error)}`,
2100
- );
2101
- }
2102
- } else if (planThinkingLevel) {
2103
- this.session.setThinkingLevel(planThinkingLevel);
2104
2164
  }
2105
2165
  }
2106
2166
 
2107
2167
  /** Apply any deferred model switch after the current stream ends. */
2108
2168
  async flushPendingModelSwitch(): Promise<void> {
2109
2169
  const pending = this.#pendingModelSwitch;
2110
- if (!pending) return;
2111
2170
  this.#pendingModelSwitch = undefined;
2171
+ this.#pendingPlanModelSwitch = false;
2172
+ if (!pending) return;
2112
2173
  try {
2113
2174
  await this.session.setModelTemporary(pending.model, pending.thinkingLevel);
2114
2175
  } catch (error) {
@@ -2120,19 +2181,23 @@ export class InteractiveMode implements InteractiveModeContext {
2120
2181
 
2121
2182
  async #clearTransientModeState(): Promise<void> {
2122
2183
  if (this.planModeEnabled || this.planModePaused) {
2123
- if (this.#planModePreviousTools !== undefined) {
2124
- await this.session.setActiveToolsByName(this.#planModePreviousTools);
2125
- }
2126
- this.session.setPlanProposalHandler?.(null);
2127
2184
  this.session.setPlanModeState(undefined);
2128
- this.planModeEnabled = false;
2129
- this.planModePaused = false;
2130
- this.planModePlanFilePath = undefined;
2131
- this.#planModePreviousTools = undefined;
2132
- this.#planModePreviousModelState = undefined;
2133
- this.#pendingModelSwitch = undefined;
2134
- this.#planModeHasEntered = false;
2135
- this.#updatePlanModeStatus();
2185
+ try {
2186
+ if (this.#planModePreviousTools !== undefined) {
2187
+ await this.session.setActiveToolsByName(this.#planModePreviousTools);
2188
+ }
2189
+ } finally {
2190
+ this.session.setPlanProposalHandler?.(null);
2191
+ this.planModeEnabled = false;
2192
+ this.planModePaused = false;
2193
+ this.planModePlanFilePath = undefined;
2194
+ this.#planModePreviousTools = undefined;
2195
+ this.#planModePreviousModelState = undefined;
2196
+ this.#pendingModelSwitch = undefined;
2197
+ this.#pendingPlanModelSwitch = false;
2198
+ this.#planModeHasEntered = false;
2199
+ this.#updatePlanModeStatus();
2200
+ }
2136
2201
  }
2137
2202
 
2138
2203
  if (this.goalModeEnabled || this.goalModePaused) {
@@ -2314,6 +2379,7 @@ export class InteractiveMode implements InteractiveModeContext {
2314
2379
  this.session.setThinkingLevel(prev.thinkingLevel);
2315
2380
  } else if (this.session.isStreaming) {
2316
2381
  this.#pendingModelSwitch = { model: prev.model, thinkingLevel: prev.thinkingLevel };
2382
+ this.#pendingPlanModelSwitch = false;
2317
2383
  } else {
2318
2384
  await this.session.setModelTemporary(prev.model, prev.thinkingLevel);
2319
2385
  }
@@ -2345,33 +2411,30 @@ export class InteractiveMode implements InteractiveModeContext {
2345
2411
  return;
2346
2412
  }
2347
2413
 
2348
- const previousTools = this.#planModePreviousTools;
2349
- if (previousTools && previousTools.length > 0) {
2350
- await this.session.setActiveToolsByName(previousTools);
2351
- }
2352
- if (this.#planModePreviousModelState) {
2353
- if (!options?.deferModelRestore) {
2354
- await this.#restorePlanPreviousModel(this.#planModePreviousModelState);
2414
+ const planModeState = this.session.getPlanModeState();
2415
+ this.session.setPlanModeState(undefined);
2416
+ try {
2417
+ if (this.#planModePreviousTools !== undefined) {
2418
+ await this.session.setActiveToolsByName(this.#planModePreviousTools);
2355
2419
  }
2356
- // If #applyPlanModeModel queued a deferred switch to the plan-role model
2357
- // (because the session was streaming on entry), drop it now: we are
2358
- // leaving plan mode, so flushing it on the next agent_end would land the
2359
- // session on the plan-role model after the user has exited plan mode
2360
- // (issue #816). This runs even when deferModelRestore is set
2361
- // (compact-approval path): otherwise the stale plan switch survives and
2362
- // flushPendingModelSwitch() later clobbers the restored/execution model.
2363
- // Only clear when the pending target matches the plan-role model — leave
2364
- // any unrelated user-queued switch intact.
2365
- const pending = this.#pendingModelSwitch;
2366
- if (pending) {
2367
- const planResolution = this.session.resolveRoleModelWithThinking("plan");
2368
- if (planResolution.model && modelsAreEqual(pending.model, planResolution.model)) {
2369
- this.#pendingModelSwitch = undefined;
2420
+ if (this.#planModePreviousModelState) {
2421
+ if (!options?.deferModelRestore) {
2422
+ await this.#restorePlanPreviousModel(this.#planModePreviousModelState);
2370
2423
  }
2424
+ // If #applyPlanModeModel queued a deferred switch to the plan-role model
2425
+ // (because the session was streaming on entry), drop it now: we are
2426
+ // leaving plan mode, so flushing it on the next agent_end would land the
2427
+ // session on the plan-role model after the user has exited plan mode
2428
+ // (issue #816). This runs even when deferModelRestore is set
2429
+ // (compact-approval path): otherwise the stale plan switch survives and
2430
+ // flushPendingModelSwitch() later clobbers the restored/execution model.
2431
+ this.#clearPendingPlanModelSwitch();
2371
2432
  }
2433
+ } catch (error) {
2434
+ this.session.setPlanModeState(planModeState);
2435
+ throw error;
2372
2436
  }
2373
2437
  this.session.setPlanProposalHandler?.(null);
2374
- this.session.setPlanModeState(undefined);
2375
2438
  this.planModeEnabled = false;
2376
2439
  // Suppress cache-miss marker on the next turn: plan exit changes the system
2377
2440
  // prompt, which predictably invalidates the cache.
@@ -2551,6 +2614,7 @@ export class InteractiveMode implements InteractiveModeContext {
2551
2614
  maxHeight: "100%",
2552
2615
  margin: 0,
2553
2616
  fullscreen: true,
2617
+ mouseTracking: false,
2554
2618
  });
2555
2619
  this.ui.setFocus(overlay);
2556
2620
  this.ui.requestRender();
@@ -2881,6 +2945,18 @@ export class InteractiveMode implements InteractiveModeContext {
2881
2945
  planFilePath: options.planFilePath,
2882
2946
  contextPreserved: options.preserveContext === true,
2883
2947
  });
2948
+ // Close the review overlay only now — after the async title write and plan
2949
+ // prompt are prepared, immediately before the execution turn is queued. The
2950
+ // synthetic prompt below blocks in `session.prompt` for the whole run, so
2951
+ // hiding here (rather than after #approvePlan returns) keeps the operator off
2952
+ // the stale plan-review screen (issue #5688) while #5319's stale-buffer guard
2953
+ // stays intact. Deferring the hide past the awaited `setSessionName` also
2954
+ // prevents restored editor focus from letting operator keystrokes submit a
2955
+ // normal turn ahead of the approved execution turn (PR #5689 review).
2956
+ // `#hidePlanReview` is idempotent, so the caller's trailing `closePlanReview()`
2957
+ // — and the cancelled/error early returns above — stay safe no-ops.
2958
+ this.#hidePlanReview();
2959
+ this.ui.requestRender();
2884
2960
  // A user turn queued during compaction was already fired by
2885
2961
  // `flushCompactionQueue` before we returned from `handleCompactCommand`; the
2886
2962
  // old abort-then-prompt path would have discarded that operator turn AND
@@ -4310,11 +4386,20 @@ export class InteractiveMode implements InteractiveModeContext {
4310
4386
  this.#selectorController.showSessionSelector();
4311
4387
  }
4312
4388
 
4313
- handleResumeSession(sessionPath: string): Promise<void> {
4389
+ async handleResumeSession(sessionPath: string): Promise<void> {
4390
+ // Flush pending settings writes *before* disposing controllers or resetting
4391
+ // observers: a save failure must leave the session, process project dir,
4392
+ // and Settings in the source scope with all UI intact.
4393
+ try {
4394
+ await this.settings.flush();
4395
+ } catch (err) {
4396
+ this.showError(`Failed to save pending settings: ${err instanceof Error ? err.message : String(err)}`);
4397
+ return;
4398
+ }
4314
4399
  this.#btwController.dispose();
4315
4400
  this.#omfgController.dispose();
4316
4401
  this.resetObserverRegistry();
4317
- return this.#selectorController.handleResumeSession(sessionPath);
4402
+ await this.#selectorController.handleResumeSession(sessionPath, { settingsFlushed: true });
4318
4403
  }
4319
4404
 
4320
4405
  handleSessionDeleteCommand(): Promise<void> {
@@ -8,7 +8,7 @@
8
8
  import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
9
9
  import type { AssistantMessage, ImageContent } from "@oh-my-pi/pi-ai";
10
10
  import { logger, sanitizeText } from "@oh-my-pi/pi-utils";
11
- import type { AgentSession, AgentSessionEvent } from "../session/agent-session";
11
+ import { type AgentSession, type AgentSessionEvent, SHUTDOWN_CONSOLIDATE_BUDGET_MS } from "../session/agent-session";
12
12
  import { isSilentAbort } from "../session/messages";
13
13
  import { flushTelemetryExport } from "../telemetry-export";
14
14
  import { initializeExtensions } from "./runtime-init";
@@ -152,7 +152,7 @@ export async function runPrintMode(session: AgentSession, options: PrintModeOpti
152
152
  // OMP-owned Chromium survives this exit (issue #5643). `dispose()`
153
153
  // is idempotent, so the unreachable call below is a harmless no-op.
154
154
  await flushTelemetryExport();
155
- await session.dispose();
155
+ await session.dispose({ mnemopiConsolidateTimeoutMs: SHUTDOWN_CONSOLIDATE_BUDGET_MS });
156
156
  const flushed = process.stderr.write(`${errorLine}\n`);
157
157
  if (flushed) {
158
158
  process.exit(1);
@@ -189,5 +189,5 @@ export async function runPrintMode(session: AgentSession, options: PrintModeOpti
189
189
  });
190
190
  });
191
191
 
192
- await session.dispose();
192
+ await session.dispose({ mnemopiConsolidateTimeoutMs: SHUTDOWN_CONSOLIDATE_BUDGET_MS });
193
193
  }
@@ -3,6 +3,7 @@ import type { Static, TSchema } from "@oh-my-pi/pi-ai";
3
3
  import { Snowflake } from "@oh-my-pi/pi-utils";
4
4
  import { applyToolProxy } from "../../extensibility/tool-proxy";
5
5
  import type { Theme } from "../../modes/theme/theme";
6
+ import { defaultLoadModeForToolName } from "../../tools/essential-tools";
6
7
  import type {
7
8
  RpcHostToolCallRequest,
8
9
  RpcHostToolCancelRequest,
@@ -54,7 +55,7 @@ class RpcHostToolAdapter<TParams extends TSchema = TSchema, TTheme extends Theme
54
55
  this.#definition = definition;
55
56
  this.#bridge = bridge;
56
57
  applyToolProxy(definition, this);
57
- this.loadMode = definition.loadMode ?? "discoverable";
58
+ this.loadMode = defaultLoadModeForToolName(definition.name, definition.loadMode);
58
59
  }
59
60
 
60
61
  execute(
@@ -12,7 +12,7 @@
12
12
  */
13
13
  import { getOAuthProviders } from "@oh-my-pi/pi-ai/oauth";
14
14
  import { isZodSchema, zodToWireSchema } from "@oh-my-pi/pi-ai/utils/schema";
15
- import { $env, isRecord, readJsonl, Snowflake } from "@oh-my-pi/pi-utils";
15
+ import { $env, isRecord, readLines, Snowflake } from "@oh-my-pi/pi-utils";
16
16
  import { reset as resetCapabilities } from "../../capability";
17
17
  import { clearPluginRootsAndCaches, resolveActiveProjectRegistryPath } from "../../discovery/helpers";
18
18
  import {
@@ -29,6 +29,7 @@ import type { AgentSession } from "../../session/agent-session";
29
29
  import { SKILL_PROMPT_MESSAGE_TYPE, USER_INTERRUPT_LABEL } from "../../session/messages";
30
30
  import { executeAcpBuiltinSlashCommand } from "../../slash-commands/acp-builtins";
31
31
  import { buildAvailableSlashCommands } from "../../slash-commands/available-commands";
32
+ import { defaultLoadModeForToolName } from "../../tools/essential-tools";
32
33
  import type { EventBus } from "../../utils/event-bus";
33
34
  import { initializeExtensions } from "../runtime-init";
34
35
  import { isRpcHostToolResult, isRpcHostToolUpdate, RpcHostToolBridge } from "./host-tools";
@@ -507,7 +508,7 @@ function normalizeHostToolDefinitions(tools: RpcHostToolDefinition[]): RpcHostTo
507
508
  description,
508
509
  parameters: tool.parameters,
509
510
  hidden: tool.hidden === true,
510
- loadMode: tool.loadMode ?? "discoverable",
511
+ loadMode: defaultLoadModeForToolName(name, tool.loadMode),
511
512
  };
512
513
  });
513
514
  }
@@ -1375,8 +1376,22 @@ export async function runRpcMode(
1375
1376
 
1376
1377
  // Keep the stdin reader moving: side-channel frames dispatch immediately,
1377
1378
  // ordinary commands serialize through inputDispatcher, and bash remains
1378
- // background-dispatched so abort_bash can overtake it.
1379
- for await (const parsed of readJsonl(Bun.stdin.stream())) {
1379
+ // background-dispatched so abort_bash can overtake it. Frames are read
1380
+ // line-by-line and parsed here (not via readJsonl) so a single malformed
1381
+ // line is reported as an error frame and the loop keeps running instead of
1382
+ // throwing out of the generator and killing the whole process (issue #5194).
1383
+ const decoder = new TextDecoder();
1384
+ for await (const line of readLines(Bun.stdin.stream())) {
1385
+ const text = decoder.decode(line).trim();
1386
+ if (!text) continue;
1387
+ let parsed: unknown;
1388
+ try {
1389
+ parsed = JSON.parse(text);
1390
+ } catch (e: unknown) {
1391
+ const message = e instanceof Error ? e.message : String(e);
1392
+ output(error(undefined, "parse", `Failed to parse command: ${message}`));
1393
+ continue;
1394
+ }
1380
1395
  inputDispatcher.dispatch(parsed);
1381
1396
  }
1382
1397