@code-yeongyu/senpi 2026.10.5 → 2026.10.6

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 (115) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/bundle/chunks/{anthropic-messages-G5236QNK.js → anthropic-messages-4MNFPQ66.js} +1 -1
  3. package/dist/bundle/chunks/{app-server-command-3Q5G4CAF.js → app-server-command-GNTX4MZT.js} +1 -1
  4. package/dist/bundle/chunks/{azure-openai-responses-NTA2HGG4.js → azure-openai-responses-A5OEHSJM.js} +1 -1
  5. package/dist/bundle/chunks/bedrock-converse-stream.js +1 -1
  6. package/dist/bundle/chunks/{chunk-EEOKVSNF.js → chunk-3EE6G4G7.js} +180 -176
  7. package/dist/bundle/chunks/{chunk-2WV426OJ.js → chunk-4MJKBXI3.js} +1 -1
  8. package/dist/bundle/chunks/{chunk-ECDLITEY.js → chunk-6EH7QXOG.js} +2 -2
  9. package/dist/bundle/chunks/chunk-6KQGOVQG.js +2 -0
  10. package/dist/bundle/chunks/{chunk-JVSOW4QU.js → chunk-7A3WRAKP.js} +1 -1
  11. package/dist/bundle/chunks/{chunk-WTJU5B2C.js → chunk-BE66QJ3Z.js} +1 -1
  12. package/dist/bundle/chunks/{chunk-UHRXXTFZ.js → chunk-D2W3MXRF.js} +2 -2
  13. package/dist/bundle/chunks/{chunk-U7CI2JGX.js → chunk-DDEAZX22.js} +1 -1
  14. package/dist/bundle/chunks/{chunk-UXIQK5NQ.js → chunk-ESSQUSRC.js} +1 -1
  15. package/dist/bundle/chunks/{chunk-KLBKUBDV.js → chunk-GQ3YLXVZ.js} +1 -1
  16. package/dist/bundle/chunks/{chunk-CTQU2XNX.js → chunk-MLNJNJFE.js} +1 -1
  17. package/dist/bundle/chunks/{chunk-BPX6QWPD.js → chunk-PELNNWGO.js} +1 -1
  18. package/dist/bundle/chunks/{chunk-UUAKEDVS.js → chunk-QLIW3YMI.js} +1 -1
  19. package/dist/bundle/chunks/{chunk-BBK6SSYT.js → chunk-TAV6QZFM.js} +1 -1
  20. package/dist/bundle/chunks/{chunk-QUROHJLP.js → chunk-TFO5JGLR.js} +1 -1
  21. package/dist/bundle/chunks/{cli-main-VZWSUHFB.js → cli-main-2C3YPHRE.js} +1 -1
  22. package/dist/bundle/chunks/{cloudflare-workers-ai-system-one-5ZUHUKMU.js → cloudflare-workers-ai-system-one-TE3OEJEL.js} +1 -1
  23. package/dist/bundle/chunks/{google-generative-ai-OTMG6ZXI.js → google-generative-ai-JBC53AMK.js} +1 -1
  24. package/dist/bundle/chunks/{google-vertex-2LXS3ICC.js → google-vertex-46ODNPM2.js} +1 -1
  25. package/dist/bundle/chunks/{help-fast-path-SHZJ3QWQ.js → help-fast-path-FUMH5NSA.js} +1 -1
  26. package/dist/bundle/chunks/{host-command-7JIGGOWR.js → host-command-N6XS5O67.js} +1 -1
  27. package/dist/bundle/chunks/{interactive-mode-JA7AHYYL.js → interactive-mode-CM4C4ZFY.js} +1 -1
  28. package/dist/bundle/chunks/{llama-cpp-classify-U5XAMKZE.js → llama-cpp-classify-AD6TTKNL.js} +1 -1
  29. package/dist/bundle/chunks/{multi-session-host-34WOINP4.js → multi-session-host-QLLTPWIM.js} +5 -5
  30. package/dist/bundle/chunks/{openai-codex-responses-WNTYIPFZ.js → openai-codex-responses-Q4OWIK7Y.js} +1 -1
  31. package/dist/bundle/chunks/{openai-completions-VSH22FCN.js → openai-completions-6HU7GIGD.js} +1 -1
  32. package/dist/bundle/chunks/{openai-images-XBTUJXPR.js → openai-images-M73YANTR.js} +1 -1
  33. package/dist/bundle/chunks/{openai-responses-WLBHCFYL.js → openai-responses-S4QYZXF3.js} +1 -1
  34. package/dist/bundle/chunks/{openrouter-images-3LVJZY3G.js → openrouter-images-PCJFX2KO.js} +1 -1
  35. package/dist/bundle/chunks/{package-manager-cli-2QLVGQSC.js → package-manager-cli-VRCAO7RV.js} +1 -1
  36. package/dist/bundle/chunks/{rotation-stream-JUCEXHWU.js → rotation-stream-7E37KOCX.js} +1 -1
  37. package/dist/bundle/chunks/{rpc-mode-QEBSN2OH.js → rpc-mode-2OPE6J7P.js} +1 -1
  38. package/dist/bundle/chunks/{session-control-endpoint-UGQIP6ZN.js → session-control-endpoint-SOG4XFF7.js} +1 -1
  39. package/dist/bundle/chunks/{session-picker-HX4LOJZ7.js → session-picker-VTTRFGE7.js} +1 -1
  40. package/dist/bundle/chunks/session-worker.js +370 -366
  41. package/dist/bundle/chunks/{typesafe-system-one-FNI3UASE.js → typesafe-system-one-TF23RUVG.js} +1 -1
  42. package/dist/bundle/cli.js +1 -1
  43. package/dist/bundle/index.js +1 -1
  44. package/dist/bundle/rpc-entry.js +1 -1
  45. package/dist/bundle/runtime-manifest.json +1 -1
  46. package/dist/core/agent-session-runtime.d.ts +12 -0
  47. package/dist/core/agent-session.d.ts +18 -0
  48. package/dist/core/agent-session.js +55 -19
  49. package/dist/core/dynamic-prompt/handoff.d.ts +4 -2
  50. package/dist/core/dynamic-prompt/handoff.js +1 -1
  51. package/dist/core/extensions/builtin/diff.js +14 -9
  52. package/dist/core/extensions/builtin/files.js +6 -2
  53. package/dist/core/extensions/builtin/gpt-apply-patch/parser.js +5 -1
  54. package/dist/core/extensions/builtin/gpt-apply-patch/streaming-parser.d.ts +8 -0
  55. package/dist/core/extensions/builtin/gpt-apply-patch/streaming-parser.js +26 -2
  56. package/dist/core/extensions/builtin/gpt-apply-patch/streaming-render.js +82 -11
  57. package/dist/core/extensions/builtin/gpt-apply-patch/types.d.ts +9 -1
  58. package/dist/core/extensions/builtin/permission-system/auto-credentials.d.ts +2 -0
  59. package/dist/core/extensions/builtin/permission-system/auto-credentials.js +17 -0
  60. package/dist/core/extensions/builtin/permission-system/auto-paths.d.ts +14 -0
  61. package/dist/core/extensions/builtin/permission-system/auto-paths.js +93 -0
  62. package/dist/core/extensions/builtin/permission-system/auto-policy.d.ts +20 -0
  63. package/dist/core/extensions/builtin/permission-system/auto-policy.js +126 -0
  64. package/dist/core/extensions/builtin/permission-system/auto-program-rules.d.ts +6 -0
  65. package/dist/core/extensions/builtin/permission-system/auto-program-rules.js +139 -0
  66. package/dist/core/extensions/builtin/permission-system/auto-shell-grammar.d.ts +29 -0
  67. package/dist/core/extensions/builtin/permission-system/auto-shell-grammar.js +74 -0
  68. package/dist/core/extensions/builtin/permission-system/auto-shell-segments.d.ts +13 -0
  69. package/dist/core/extensions/builtin/permission-system/auto-shell-segments.js +23 -0
  70. package/dist/core/extensions/builtin/permission-system/cli.d.ts +1 -0
  71. package/dist/core/extensions/builtin/permission-system/cli.js +9 -10
  72. package/dist/core/extensions/builtin/permission-system/config.d.ts +4 -1
  73. package/dist/core/extensions/builtin/permission-system/config.js +13 -1
  74. package/dist/core/extensions/builtin/permission-system/index.js +20 -12
  75. package/dist/core/extensions/builtin/permission-system/non-interactive.d.ts +11 -6
  76. package/dist/core/extensions/builtin/permission-system/non-interactive.js +8 -33
  77. package/dist/core/extensions/builtin/permission-system/service.d.ts +20 -1
  78. package/dist/core/extensions/builtin/permission-system/service.js +43 -7
  79. package/dist/core/extensions/builtin/permission-system/settings.d.ts +1 -0
  80. package/dist/core/extensions/builtin/permission-system/settings.js +4 -3
  81. package/dist/core/extensions/builtin/permission-system/types.d.ts +1 -1
  82. package/dist/core/extensions/builtin/prompt-preset/claude-fable-5-1.js +16 -3
  83. package/dist/core/retry-fallback/usage-limit.js +5 -1
  84. package/dist/core/tools/diff-render.d.ts +4 -0
  85. package/dist/core/tools/diff-render.js +13 -0
  86. package/dist/core/tools/renderers/edit.js +6 -4
  87. package/dist/main.js +12 -0
  88. package/dist/modes/interactive/interactive-mode.d.ts +1 -0
  89. package/dist/modes/interactive/interactive-mode.js +55 -11
  90. package/dist/modes/interactive/provider-error-presentation.d.ts +25 -0
  91. package/dist/modes/interactive/provider-error-presentation.js +42 -0
  92. package/dist/modes/interactive/theme/dark.json +5 -5
  93. package/dist/modes/interactive/theme/light.json +5 -5
  94. package/dist/modes/rpc/custom-capability.d.ts +6 -0
  95. package/dist/modes/rpc/custom-capability.js +6 -0
  96. package/dist/modes/rpc/rpc-client.d.ts +3 -0
  97. package/dist/modes/rpc/rpc-input-validation.d.ts +7 -0
  98. package/dist/modes/rpc/rpc-input-validation.js +37 -0
  99. package/dist/modes/rpc/rpc-types.d.ts +9 -0
  100. package/dist/modes/rpc/session-command-activity.d.ts +2 -0
  101. package/dist/modes/rpc/session-command-activity.js +25 -0
  102. package/dist/modes/rpc/session-command-router.js +11 -4
  103. package/dist/modes/rpc/session-registry-attach.js +1 -0
  104. package/dist/modes/rpc/session-registry-types.d.ts +7 -0
  105. package/dist/modes/rpc/session-registry-types.js +11 -0
  106. package/dist/modes/rpc/session-registry.js +6 -3
  107. package/dist/modes/rpc/session-sweep.d.ts +12 -2
  108. package/dist/modes/rpc/session-sweep.js +36 -3
  109. package/dist/modes/rpc/session-teardown.d.ts +2 -0
  110. package/dist/modes/rpc/session-teardown.js +1 -0
  111. package/dist/modes/rpc/worker-session-registry.js +5 -1
  112. package/docs/rpc.md +39 -5
  113. package/docs/settings.md +3 -1
  114. package/package.json +6 -6
  115. package/dist/bundle/chunks/chunk-K3BXFPWZ.js +0 -2
@@ -79,6 +79,19 @@ function renderToolDiffLine(line, filePath, theme, contentOverride) {
79
79
  const rendered = `${theme.fg(diffColor, line.sign)}${lineNumber} ${content}`;
80
80
  return theme.bg(background, rendered);
81
81
  }
82
+ export function countDiffChanges(diffText) {
83
+ let added = 0;
84
+ let removed = 0;
85
+ for (const line of diffText.split("\n")) {
86
+ if (line.startsWith("+++") || line.startsWith("---"))
87
+ continue;
88
+ if (line.startsWith("+"))
89
+ added++;
90
+ else if (line.startsWith("-"))
91
+ removed++;
92
+ }
93
+ return { added, removed };
94
+ }
82
95
  export function renderToolDiff(diffText, options) {
83
96
  const parsedLines = diffText.split("\n").map(parseRenderableDiffLine);
84
97
  const rendered = [];
@@ -6,7 +6,7 @@
6
6
  * definition, so the tool's public shape is unchanged.
7
7
  */
8
8
  import { Box, Container, Spacer, Text } from "@earendil-works/pi-tui";
9
- import { renderToolDiff } from "../diff-render.js";
9
+ import { countDiffChanges, renderToolDiff } from "../diff-render.js";
10
10
  import { computeEditsDiff } from "../edit-diff.js";
11
11
  import { renderToolPath, str } from "../render-utils.js";
12
12
  function createEditCallRenderComponent() {
@@ -48,9 +48,11 @@ function getRenderablePreviewInput(args) {
48
48
  }
49
49
  return null;
50
50
  }
51
- function formatEditCall(args, theme, cwd) {
51
+ function formatEditCall(args, theme, cwd, diffText) {
52
52
  const pathDisplay = renderToolPath(str(args?.file_path ?? args?.path), theme, cwd);
53
- return `${theme.fg("toolTitle", theme.bold("edit"))} ${pathDisplay}`;
53
+ const counts = diffText === undefined ? undefined : countDiffChanges(diffText);
54
+ const countTag = counts === undefined ? "" : theme.fg("muted", ` (+${counts.added}/-${counts.removed})`);
55
+ return `${theme.fg("toolTitle", theme.bold("edit"))} ${pathDisplay}${countTag}`;
54
56
  }
55
57
  function formatEditResult(args, preview, result, theme, isError) {
56
58
  const rawPath = str(args?.file_path ?? args?.path);
@@ -87,7 +89,7 @@ function getEditHeaderBg(preview, settledError, theme) {
87
89
  function buildEditCallComponent(component, args, theme, cwd) {
88
90
  component.setBgFn(getEditHeaderBg(component.preview, component.settledError, theme));
89
91
  component.detachAll();
90
- component.addChild(new Text(formatEditCall(args, theme, cwd), 0, 0));
92
+ component.addChild(new Text(formatEditCall(args, theme, cwd, component.preview && !("error" in component.preview) ? component.preview.diff : undefined), 0, 0));
91
93
  if (!component.preview) {
92
94
  return component;
93
95
  }
package/dist/main.js CHANGED
@@ -677,6 +677,18 @@ export function createCliRuntimeFactory(configuration, local = {}) {
677
677
  parsed.projectTrustOverride ??
678
678
  (!hasTrustRequiringResources || trustStore.get(cwd) === true));
679
679
  const runtimeSettingsManager = SettingsManager.create(cwd, agentDir, { projectTrusted });
680
+ // The opener's per-session fallback policy is an in-memory override: never saved, never shared.
681
+ if (launchProfile?.retryFallback) {
682
+ runtimeSettingsManager.applyOverrides({
683
+ retry: {
684
+ modelFallback: launchProfile.retryFallback.modelFallback,
685
+ fallbackChains: Object.fromEntries(Object.entries(launchProfile.retryFallback.fallbackChains).map(([key, entries]) => [
686
+ key,
687
+ [...entries],
688
+ ])),
689
+ },
690
+ });
691
+ }
680
692
  const services = await createAgentSessionServices({
681
693
  cwd,
682
694
  agentDir,
@@ -108,6 +108,7 @@ export declare class InteractiveMode {
108
108
  private readonly sessionShownTipIds;
109
109
  private shortcutOverlay;
110
110
  private lastEditorText;
111
+ private startupProviderGuidanceShown;
111
112
  private lastInputWasPaste;
112
113
  private sessionLogger;
113
114
  private readonly continuityNotices;
@@ -13,6 +13,7 @@ import { spawn, spawnSync } from "child_process";
13
13
  import { APP_COMMAND, APP_NAME, APP_TITLE, BRAND, CONFIG_DIR_NAME, DISPLAY_VERSION, expandTildePath, getAgentDir, getAuthPath, getDebugLogPath, getDocsPath, getShareViewerUrl, } from "../../config.js";
14
14
  import { parseSkillBlock, } from "../../core/agent-session.js";
15
15
  import { SessionImportFileNotFoundError } from "../../core/agent-session-runtime.js";
16
+ import { formatNoModelsAvailableMessage } from "../../core/auth-guidance.js";
16
17
  import { isApiKeyLoginProvider } from "../../core/auth-providers.js";
17
18
  import { envValue } from "../../core/brand.js";
18
19
  import { CACHE_TTL_MS, collectCacheMisses, computeCacheWaste, detectCacheMiss, } from "../../core/cache-stats.js";
@@ -113,7 +114,7 @@ import { buildResourceScopeGroups, formatResourceScopeGroups, getDisplaySourceIn
113
114
  import { describeLoginFailure } from "./login-outcome.js";
114
115
  import { refreshModelCatalogs } from "./model-catalog-refresh.js";
115
116
  import { getModelSearchText } from "./model-search.js";
116
- import { isNetworkProviderError, isNetworkProviderMessage, ProviderErrorPresentation, } from "./provider-error-presentation.js";
117
+ import { isNetworkProviderError, isNetworkProviderMessage, isRetryableProviderError, ProviderErrorPresentation, } from "./provider-error-presentation.js";
117
118
  import { replayAssistantTools } from "./replay-assistant-tools.js";
118
119
  import { allScopeSessions, chooseResumePath, currentScopeSessions } from "./resume-rebind.js";
119
120
  import { isRiskyMainModel, RISKY_MAIN_MODEL_WARNING } from "./risky-main-model-warning.js";
@@ -554,6 +555,7 @@ export class InteractiveMode {
554
555
  constructor(runtimeHost, options = {}) {
555
556
  this.sessionShownTipIds = new Set();
556
557
  this.lastEditorText = "";
558
+ this.startupProviderGuidanceShown = false;
557
559
  this.lastInputWasPaste = false;
558
560
  this.continuityNotices = new ContinuityNoticeTracker();
559
561
  this.turnWorkingTip = new WorkingTipCache();
@@ -1323,11 +1325,31 @@ export class InteractiveMode {
1323
1325
  if (modelsJsonError) {
1324
1326
  this.showError(`models.json error: ${modelsJsonError}`);
1325
1327
  }
1326
- for (const warning of this.session.modelRuntime.getWarnings()) {
1327
- this.showWarning(warning);
1328
+ const modelRuntimeWarnings = this.session.modelRuntime.getWarnings();
1329
+ // Fold repeated warnings only in quiet startup; a hand-built context without getQuietStartup
1330
+ // is treated as not-quiet so full detail shows (the safe default, and what the old path did).
1331
+ const getQuietStartup = this.settingsManager?.getQuietStartup?.bind(this.settingsManager);
1332
+ const quietStartup = getQuietStartup ? getQuietStartup() : false;
1333
+ if (showsStartupDetails(this.options.verbose, quietStartup)) {
1334
+ for (const warning of modelRuntimeWarnings) {
1335
+ this.showWarning(warning);
1336
+ }
1337
+ }
1338
+ else if (modelRuntimeWarnings.length === 1) {
1339
+ this.showWarning(modelRuntimeWarnings[0]);
1340
+ }
1341
+ else if (modelRuntimeWarnings.length > 1) {
1342
+ this.showNoticeBox({
1343
+ title: `${modelRuntimeWarnings.length} model warnings`,
1344
+ tone: "warning",
1345
+ why: modelRuntimeWarnings[0],
1346
+ extra: modelRuntimeWarnings.slice(1).map((text) => ({ text })),
1347
+ });
1328
1348
  }
1329
1349
  if (modelFallbackMessage) {
1330
1350
  this.showWarning(modelFallbackMessage);
1351
+ if (modelFallbackMessage === formatNoModelsAvailableMessage())
1352
+ this.startupProviderGuidanceShown = true;
1331
1353
  }
1332
1354
  for (const warning of this.session.fallbackValidationWarnings) {
1333
1355
  this.showWarning(warning);
@@ -1805,7 +1827,13 @@ export class InteractiveMode {
1805
1827
  if (options?.sort !== false) {
1806
1828
  labels.sort((a, b) => a.localeCompare(b));
1807
1829
  }
1808
- return theme.fg("dim", ` ${labels.join(", ")}`);
1830
+ // A short listing fits on one line and shows in full; a long one (dozens of skills) is
1831
+ // what flooded the first screen on a narrow terminal, so it truncates to a few names
1832
+ // with a +N more hint. The full list is one Ctrl+O away.
1833
+ const shown = labels.length <= 8 ? labels : labels.slice(0, 3);
1834
+ const hidden = labels.length - shown.length;
1835
+ const more = hidden > 0 ? theme.fg("muted", ` +${hidden} more (${keyText("app.tools.expand")})`) : "";
1836
+ return theme.fg("dim", ` ${shown.join(", ")}`) + more;
1809
1837
  };
1810
1838
  // System resources are left out of the compact body; a section with nothing else to show stays
1811
1839
  // hidden until the listing is expanded, where the system group lists them. Bodies are built on
@@ -4185,6 +4213,15 @@ export class InteractiveMode {
4185
4213
  case "resume_context_reduced":
4186
4214
  this.showWarning(event.notice);
4187
4215
  break;
4216
+ case "provider_required":
4217
+ // The first notice repeats the startup "No models available" warning (same /login guidance), so it
4218
+ // is absorbed by it; the session emits again only after a turn was admitted in between.
4219
+ if (this.startupProviderGuidanceShown) {
4220
+ this.startupProviderGuidanceShown = false;
4221
+ break;
4222
+ }
4223
+ this.showWarning(event.notice);
4224
+ break;
4188
4225
  case "settings_source_selected":
4189
4226
  this.showSettingsSourceSelected(event);
4190
4227
  break;
@@ -4475,6 +4512,9 @@ export class InteractiveMode {
4475
4512
  }
4476
4513
  }
4477
4514
  else if (event.result) {
4515
+ // A successful compaction ends the retry episode: no finish is owed, so a later
4516
+ // never-retried terminal failure must not reopen the banner.
4517
+ this.providerErrors?.clear();
4478
4518
  // Compaction event consumers in the fork are session-backed and do not
4479
4519
  // necessarily expose InteractiveMode's SessionManager convenience getter.
4480
4520
  // Keep the structural fallback for focused handler consumers while using
@@ -4520,7 +4560,11 @@ export class InteractiveMode {
4520
4560
  }
4521
4561
  else if (event.errorMessage) {
4522
4562
  const errorMessage = sanitizeTerminalLabel(event.errorMessage);
4523
- if (isNetworkProviderError(errorMessage)) {
4563
+ // The quiet provider-retry banner's finish() closes out a retry recorded in the CURRENT
4564
+ // episode. A terminal compaction failure after an episode that already finished (or one
4565
+ // that never retried) must always surface as an error, even when its message looks
4566
+ // transient ("timeout", "truncated generator").
4567
+ if (isRetryableProviderError(errorMessage) && this.providerErrors?.awaitingRetryFinish === true) {
4524
4568
  this.getProviderErrors().finish(errorMessage);
4525
4569
  }
4526
4570
  else if (event.reason === "manual") {
@@ -4632,7 +4676,7 @@ export class InteractiveMode {
4632
4676
  this.setExtensionStatus(FALLBACK_STATUS_KEY, undefined);
4633
4677
  break;
4634
4678
  case "retry_fallback_exhausted":
4635
- if (isNetworkProviderError(event.lastError)) {
4679
+ if (isRetryableProviderError(event.lastError)) {
4636
4680
  this.getProviderErrors().finish(event.lastError);
4637
4681
  this.setExtensionStatus(FALLBACK_STATUS_KEY, undefined);
4638
4682
  break;
@@ -4654,7 +4698,7 @@ export class InteractiveMode {
4654
4698
  });
4655
4699
  break;
4656
4700
  case "auto_retry_start": {
4657
- if (isNetworkProviderError(event.errorMessage)) {
4701
+ if (isRetryableProviderError(event.errorMessage)) {
4658
4702
  this.getProviderErrors().retrying(event.errorMessage, this.toolOutputExpanded);
4659
4703
  }
4660
4704
  // During retry waits, isStreaming flips false between attempts. The main Esc handler
@@ -4694,7 +4738,7 @@ export class InteractiveMode {
4694
4738
  if (event.success || event.finalError === "Retry cancelled") {
4695
4739
  this.providerErrors?.clear();
4696
4740
  }
4697
- else if (isNetworkProviderError(event.finalError)) {
4741
+ else if (isRetryableProviderError(event.finalError)) {
4698
4742
  this.getProviderErrors().finish(event.finalError, event.attempt);
4699
4743
  }
4700
4744
  else {
@@ -4704,7 +4748,7 @@ export class InteractiveMode {
4704
4748
  break;
4705
4749
  }
4706
4750
  case "summarization_retry_scheduled": {
4707
- if (isNetworkProviderError(event.errorMessage)) {
4751
+ if (isRetryableProviderError(event.errorMessage)) {
4708
4752
  this.getProviderErrors().retrying(event.errorMessage, this.toolOutputExpanded);
4709
4753
  }
4710
4754
  else {
@@ -4725,7 +4769,7 @@ export class InteractiveMode {
4725
4769
  break;
4726
4770
  }
4727
4771
  case "summarization_retry_finished": {
4728
- this.providerErrors?.clear();
4772
+ this.providerErrors?.clearTransient();
4729
4773
  this.clearStatusIndicator("retry");
4730
4774
  this.ui.requestRender();
4731
4775
  break;
@@ -4760,7 +4804,7 @@ export class InteractiveMode {
4760
4804
  showRetryStatusIndicatorWithCadence(event) {
4761
4805
  const refreshIntervalMs = largeSessionWorkingStatusInterval(this.sessionManager.getEntryCount(), DEFAULT_RETRY_STATUS_REFRESH_INTERVAL_MS, LARGE_SESSION_RETRY_STATUS_REFRESH_INTERVAL_MS);
4762
4806
  const indicator = refreshIntervalMs === DEFAULT_RETRY_STATUS_REFRESH_INTERVAL_MS ? undefined : { intervalMs: refreshIntervalMs };
4763
- this.showStatusIndicator(new RetryStatusIndicator(this.ui, event.attempt, event.maxAttempts, event.delayMs, indicator, isNetworkProviderError(event.errorMessage)));
4807
+ this.showStatusIndicator(new RetryStatusIndicator(this.ui, event.attempt, event.maxAttempts, event.delayMs, indicator, isRetryableProviderError(event.errorMessage)));
4764
4808
  this.ui.requestRender();
4765
4809
  }
4766
4810
  /** Extract text content from a user message */
@@ -2,6 +2,15 @@ import type { AssistantMessage } from "@earendil-works/pi-ai";
2
2
  import { type Container } from "@earendil-works/pi-tui";
3
3
  /** Presentation classification only. Retry eligibility remains owned by AgentSession. */
4
4
  export declare function isNetworkProviderError(raw: string | undefined, envelopeOnly?: boolean): boolean;
5
+ /**
6
+ * True when a provider failure should take the quiet presentation path (one banner, a
7
+ * status-line countdown while retrying, raw JSON only on expand): any transient failure,
8
+ * which is a network drop, a 429 rate-limit, or a 5xx. Hard quota/auth/billing failures
9
+ * classify non-retryable here and stay verbose, because they need a credential or plan
10
+ * change, not a wait. Delegates to the shared classifier so the transcript decision and
11
+ * the retry engine never disagree about what is transient.
12
+ */
13
+ export declare function isRetryableProviderError(raw: string | undefined): boolean;
5
14
  export declare function isNetworkProviderMessage(message: AssistantMessage): boolean;
6
15
  /** Owns one display episode, not session messages or retry/fallback policy. */
7
16
  export declare class ProviderErrorPresentation {
@@ -11,7 +20,23 @@ export declare class ProviderErrorPresentation {
11
20
  constructor(chat: Container);
12
21
  record(raw: string, expanded?: boolean): void;
13
22
  retrying(raw: string, expanded: boolean): void;
23
+ /** A success or cleanup ends the episode: no finish is owed afterward. */
14
24
  clear(): void;
25
+ /**
26
+ * The retry engine finishing (summarization_retry_finished): clears the transient row but keeps
27
+ * the episode marker, because the terminal compaction_end that follows still closes the
28
+ * exhausted episode out via finish().
29
+ */
30
+ clearTransient(): void;
31
+ /** True while a retry episode is active (a retry was recorded and not yet finished/cleared). */
32
+ get hasActiveRetry(): boolean;
33
+ /**
34
+ * True only while a retry has been recorded in the CURRENT episode and not yet closed out by
35
+ * finish(). A fresh terminal failure after an episode that already finished returns false, so
36
+ * it surfaces as a plain error instead of reopening the stale banner.
37
+ */
38
+ get awaitingRetryFinish(): boolean;
39
+ private retryAwaitingFinish;
15
40
  finish(raw?: string, attempts?: number): void;
16
41
  newTurn(): void;
17
42
  }
@@ -1,3 +1,4 @@
1
+ import { isRetryableErrorMessage } from "@earendil-works/pi-ai";
1
2
  import { Text } from "@earendil-works/pi-tui";
2
3
  import { z } from "zod";
3
4
  import { keyText } from "./components/keybinding-hints.js";
@@ -25,6 +26,21 @@ export function isNetworkProviderError(raw, envelopeOnly = false) {
25
26
  throw error;
26
27
  }
27
28
  }
29
+ /**
30
+ * True when a provider failure should take the quiet presentation path (one banner, a
31
+ * status-line countdown while retrying, raw JSON only on expand): any transient failure,
32
+ * which is a network drop, a 429 rate-limit, or a 5xx. Hard quota/auth/billing failures
33
+ * classify non-retryable here and stay verbose, because they need a credential or plan
34
+ * change, not a wait. Delegates to the shared classifier so the transcript decision and
35
+ * the retry engine never disagree about what is transient.
36
+ */
37
+ export function isRetryableProviderError(raw) {
38
+ if (!raw)
39
+ return false;
40
+ if (isNetworkProviderError(raw))
41
+ return true;
42
+ return isRetryableErrorMessage(raw);
43
+ }
28
44
  export function isNetworkProviderMessage(message) {
29
45
  return ((message.stopReason === "error" || message.stopReason === "aborted") &&
30
46
  isNetworkProviderError(message.errorMessage));
@@ -62,10 +78,12 @@ class ProviderFailureNotice {
62
78
  export class ProviderErrorPresentation {
63
79
  constructor(chat) {
64
80
  this.pending = false;
81
+ this.retryAwaitingFinish = false;
65
82
  this.chat = chat;
66
83
  }
67
84
  record(raw, expanded = false) {
68
85
  this.pending = true;
86
+ this.retryAwaitingFinish = true;
69
87
  if (!this.notice) {
70
88
  this.notice = new ProviderFailureNotice();
71
89
  this.notice.setExpanded(expanded);
@@ -78,17 +96,41 @@ export class ProviderErrorPresentation {
78
96
  // RetryStatusIndicator owns the single visible transient row.
79
97
  this.notice?.setSummary(undefined);
80
98
  }
99
+ /** A success or cleanup ends the episode: no finish is owed afterward. */
81
100
  clear() {
82
101
  this.pending = false;
102
+ this.retryAwaitingFinish = false;
83
103
  // Keep diagnostics reachable through the existing expansion affordance.
84
104
  this.notice?.setSummary(undefined);
85
105
  }
106
+ /**
107
+ * The retry engine finishing (summarization_retry_finished): clears the transient row but keeps
108
+ * the episode marker, because the terminal compaction_end that follows still closes the
109
+ * exhausted episode out via finish().
110
+ */
111
+ clearTransient() {
112
+ this.pending = false;
113
+ this.notice?.setSummary(undefined);
114
+ }
115
+ /** True while a retry episode is active (a retry was recorded and not yet finished/cleared). */
116
+ get hasActiveRetry() {
117
+ return this.pending;
118
+ }
119
+ /**
120
+ * True only while a retry has been recorded in the CURRENT episode and not yet closed out by
121
+ * finish(). A fresh terminal failure after an episode that already finished returns false, so
122
+ * it surfaces as a plain error instead of reopening the stale banner.
123
+ */
124
+ get awaitingRetryFinish() {
125
+ return this.retryAwaitingFinish;
126
+ }
86
127
  finish(raw, attempts) {
87
128
  if (raw)
88
129
  this.record(raw);
89
130
  if (!this.pending)
90
131
  return;
91
132
  this.pending = false;
133
+ this.retryAwaitingFinish = false;
92
134
  const count = attempts === undefined ? "" : ` after ${attempts} retries`;
93
135
  this.notice?.setSummary(`The model provider could not complete the request${count}. Try again or choose another model with /model.`);
94
136
  }
@@ -34,9 +34,9 @@
34
34
  "customMessageBg": "okhsl(295 42% 24%)",
35
35
  "customMessageText": "muted",
36
36
  "customMessageLabel": "violet",
37
- "toolPendingBg": "okhsl(229 5% 24%)",
38
- "toolSuccessBg": "okhsl(158 46% 25%)",
39
- "toolErrorBg": "okhsl(19 54% 25%)",
37
+ "toolPendingBg": "okhsl(229 5% 13%)",
38
+ "toolSuccessBg": "okhsl(158 26% 13%)",
39
+ "toolErrorBg": "okhsl(19 28% 14%)",
40
40
  "toolTitle": "text",
41
41
  "toolOutput": "muted",
42
42
  "mdHeading": "yellow",
@@ -49,8 +49,8 @@
49
49
  "mdQuoteBorder": "muted",
50
50
  "mdHr": "muted",
51
51
  "mdListBullet": "violet",
52
- "toolDiffAdded": "green",
53
- "toolDiffRemoved": "red",
52
+ "toolDiffAdded": "okhsl(159 58% 76%)",
53
+ "toolDiffRemoved": "okhsl(20 70% 77%)",
54
54
  "toolDiffContext": "muted",
55
55
  "syntaxComment": "muted",
56
56
  "syntaxKeyword": "blue",
@@ -34,9 +34,9 @@
34
34
  "customMessageBg": "okhsl(295 25% 91%)",
35
35
  "customMessageText": "muted",
36
36
  "customMessageLabel": "violet",
37
- "toolPendingBg": "okhsl(248 3% 91%)",
38
- "toolSuccessBg": "okhsl(156 21% 91%)",
39
- "toolErrorBg": "okhsl(24 23% 91%)",
37
+ "toolPendingBg": "okhsl(248 3% 93%)",
38
+ "toolSuccessBg": "okhsl(156 25% 91%)",
39
+ "toolErrorBg": "okhsl(24 28% 91%)",
40
40
  "toolTitle": "text",
41
41
  "toolOutput": "muted",
42
42
  "mdHeading": "yellow",
@@ -49,8 +49,8 @@
49
49
  "mdQuoteBorder": "muted",
50
50
  "mdHr": "muted",
51
51
  "mdListBullet": "violet",
52
- "toolDiffAdded": "green",
53
- "toolDiffRemoved": "red",
52
+ "toolDiffAdded": "okhsl(159 85% 28%)",
53
+ "toolDiffRemoved": "okhsl(20 100% 30%)",
54
54
  "toolDiffContext": "muted",
55
55
  "syntaxComment": "muted",
56
56
  "syntaxKeyword": "blue",
@@ -74,6 +74,12 @@ export declare const PROMPT_SURFACE_CAPABILITY = "prompt_surface";
74
74
  * value reaches every consumer, so a client may rely on it the moment it sees this name.
75
75
  */
76
76
  export declare const BROWSER_ENGINE_CAPABILITY = "browser_engine";
77
+ /**
78
+ * HOST capability: this host honors `open_session.retryFallback` (`{ modelFallback, fallbackChains }`). The
79
+ * policy is that session's own: it is applied as an in-memory settings override for that session only,
80
+ * never written to a settings file and never seen by another session on the host.
81
+ */
82
+ export declare const RETRY_FALLBACK_PROFILE_CAPABILITY = "retry_fallback_profile";
77
83
  /**
78
84
  * HOST capability: `open_session.promptSurface` also accepts `chat` (a chat bridge: no routing line,
79
85
  * no handoff block, no todo cues). A host without it refuses `chat` with `invalid_launch_profile`,
@@ -74,6 +74,12 @@ export const PROMPT_SURFACE_CAPABILITY = "prompt_surface";
74
74
  * value reaches every consumer, so a client may rely on it the moment it sees this name.
75
75
  */
76
76
  export const BROWSER_ENGINE_CAPABILITY = "browser_engine";
77
+ /**
78
+ * HOST capability: this host honors `open_session.retryFallback` (`{ modelFallback, fallbackChains }`). The
79
+ * policy is that session's own: it is applied as an in-memory settings override for that session only,
80
+ * never written to a settings file and never seen by another session on the host.
81
+ */
82
+ export const RETRY_FALLBACK_PROFILE_CAPABILITY = "retry_fallback_profile";
77
83
  /**
78
84
  * HOST capability: `open_session.promptSurface` also accepts `chat` (a chat bridge: no routing line,
79
85
  * no handoff block, no todo cues). A host without it refuses `chat` with `invalid_launch_profile`,
@@ -6,6 +6,7 @@
6
6
  import type { AgentMessage, ThinkingLevel } from "@earendil-works/pi-agent-core";
7
7
  import type { ImageContent } from "@earendil-works/pi-ai";
8
8
  import type { PromptDisposition, QueuedInputDisposition, SessionStats } from "../../core/agent-session.ts";
9
+ import type { SessionRetryFallbackProfile } from "../../core/agent-session-runtime.ts";
9
10
  import type { BashResult } from "../../core/bash-executor.ts";
10
11
  import type { BrowserEngine } from "../../core/browser-engine.ts";
11
12
  import { type ClientMessageIdentity } from "../../core/client-message-identity.ts";
@@ -136,6 +137,8 @@ export declare class RpcClient {
136
137
  promptSurface?: PromptSurface;
137
138
  /** Which browser this session's skills drive; needs the host's `browser_engine`. */
138
139
  browserEngine?: BrowserEngine;
140
+ /** This session's own fallback policy, in memory only; needs the host's `retry_fallback_profile`. */
141
+ retryFallback?: SessionRetryFallbackProfile;
139
142
  }): Promise<{
140
143
  sessionId: string;
141
144
  state: RpcSessionState;
@@ -40,6 +40,13 @@ export declare function sessionPromptSurfaceError(value: unknown): string | unde
40
40
  * own browser must not silently run without one.
41
41
  */
42
42
  export declare function sessionBrowserEngineError(value: unknown): string | undefined;
43
+ /**
44
+ * Detail for an `open_session.retryFallback` the host refuses, or undefined when it is absent or well
45
+ * formed: `{ modelFallback: boolean, fallbackChains: Record<string, string[]> }`, at most 32 chains of at
46
+ * most 32 non-empty selectors. A malformed profile is refused rather than ignored, so a caller that asked
47
+ * for a chain never runs without one unknowingly.
48
+ */
49
+ export declare function sessionRetryFallbackError(value: unknown): string | undefined;
43
50
  export declare function rpcCommandShapeError(command: unknown): string | undefined;
44
51
  export declare function rpcMessageLengthError(command: unknown): string | undefined;
45
52
  //# sourceMappingURL=rpc-input-validation.d.ts.map
@@ -161,6 +161,43 @@ export function sessionBrowserEngineError(value) {
161
161
  return undefined;
162
162
  return `browserEngine must be "connected", "builtin" or "none".`;
163
163
  }
164
+ const MAX_RETRY_FALLBACK_CHAINS = 32;
165
+ const MAX_RETRY_FALLBACK_ENTRIES = 32;
166
+ const MAX_RETRY_FALLBACK_SELECTOR_LENGTH = 512;
167
+ /**
168
+ * Detail for an `open_session.retryFallback` the host refuses, or undefined when it is absent or well
169
+ * formed: `{ modelFallback: boolean, fallbackChains: Record<string, string[]> }`, at most 32 chains of at
170
+ * most 32 non-empty selectors. A malformed profile is refused rather than ignored, so a caller that asked
171
+ * for a chain never runs without one unknowingly.
172
+ */
173
+ export function sessionRetryFallbackError(value) {
174
+ if (value === undefined)
175
+ return undefined;
176
+ const shape = "retryFallback must be { modelFallback: boolean, fallbackChains: Record<string, string[]> }.";
177
+ if (typeof value !== "object" || value === null || Array.isArray(value))
178
+ return shape;
179
+ const record = value;
180
+ if (typeof record.modelFallback !== "boolean")
181
+ return shape;
182
+ const chains = record.fallbackChains;
183
+ if (typeof chains !== "object" || chains === null || Array.isArray(chains))
184
+ return shape;
185
+ const entries = Object.entries(chains);
186
+ if (entries.length > MAX_RETRY_FALLBACK_CHAINS)
187
+ return `retryFallback allows at most ${MAX_RETRY_FALLBACK_CHAINS} chains.`;
188
+ for (const [key, selectors] of entries) {
189
+ if (!isSelector(key))
190
+ return shape;
191
+ if (!Array.isArray(selectors) || !selectors.every(isSelector))
192
+ return shape;
193
+ if (selectors.length > MAX_RETRY_FALLBACK_ENTRIES)
194
+ return `retryFallback allows at most ${MAX_RETRY_FALLBACK_ENTRIES} entries per chain.`;
195
+ }
196
+ return undefined;
197
+ }
198
+ function isSelector(value) {
199
+ return typeof value === "string" && value.trim().length > 0 && value.length <= MAX_RETRY_FALLBACK_SELECTOR_LENGTH;
200
+ }
164
201
  export function rpcCommandShapeError(command) {
165
202
  if (typeof command !== "object" || command === null || Array.isArray(command)) {
166
203
  return "RPC command must be a JSON object.";
@@ -9,6 +9,7 @@ import type { ImageContent, Model, ProviderDiagnostic, ThinkingSelection } from
9
9
  import type { SessionRuntimeKind } from "../../cli/args.ts";
10
10
  import type { AgentAbortSource } from "../../core/agent-abort-provenance.ts";
11
11
  import type { PromptDisposition, QueuedInput, QueuedInputDisposition, SessionStats } from "../../core/agent-session.ts";
12
+ import type { SessionRetryFallbackProfile } from "../../core/agent-session-runtime.ts";
12
13
  import type { BashResult } from "../../core/bash-executor.ts";
13
14
  import type { BrowserEngine } from "../../core/browser-engine.ts";
14
15
  import type { ClientMessageIdentity } from "../../core/client-message-identity.ts";
@@ -504,6 +505,14 @@ export type RpcCommand = (RpcSessionCommand & {
504
505
  * Requires the host capability `browser_engine`. Any other value is refused with `invalid_launch_profile`.
505
506
  */
506
507
  browserEngine?: BrowserEngine;
508
+ /**
509
+ * The fallback policy THIS session runs with (e.g. a task child's own chain): applied as an
510
+ * in-memory override of this session's `retry.modelFallback` / `retry.fallbackChains`, never written
511
+ * to a settings file and never seen by another session. Requires the host capability
512
+ * `retry_fallback_profile`; a malformed value is refused with `invalid_launch_profile`. Applied
513
+ * when the open creates the session; an attach keeps the session's existing policy.
514
+ */
515
+ retryFallback?: SessionRetryFallbackProfile;
507
516
  } | {
508
517
  id?: string;
509
518
  type: "close_session";
@@ -0,0 +1,2 @@
1
+ export declare function refreshesSessionActivity(command: string): boolean;
2
+ //# sourceMappingURL=session-command-activity.d.ts.map
@@ -0,0 +1,25 @@
1
+ /** Whether the command itself refreshes activity, apart from attached-client keepalives. */
2
+ const observations = new Set([
3
+ "get_state",
4
+ "get_messages",
5
+ "get_entries",
6
+ "get_tree",
7
+ "get_fork_messages",
8
+ "get_last_assistant_text",
9
+ "get_session_stats",
10
+ "get_steering_messages",
11
+ "get_follow_up_messages",
12
+ "get_available_models",
13
+ "get_available_thinking_levels",
14
+ "get_fast_mode",
15
+ "get_commands",
16
+ "get_loaded_surfaces",
17
+ "get_media",
18
+ "get_auth_providers",
19
+ "get_provider_accounts",
20
+ "memory_report",
21
+ ]);
22
+ export function refreshesSessionActivity(command) {
23
+ return !observations.has(command);
24
+ }
25
+ //# sourceMappingURL=session-command-activity.js.map
@@ -1,11 +1,11 @@
1
1
  import { VERSION } from "../../config.js";
2
- import { ACCEPT_EDITS_PERMISSION_PRESET_CAPABILITY } from "../../core/extensions/builtin/permission-system/config.js";
2
+ import { ACCEPT_EDITS_PERMISSION_PRESET_CAPABILITY, AUTO_PERMISSION_PRESET_CAPABILITY, } from "../../core/extensions/builtin/permission-system/config.js";
3
3
  import { DURABLE_CLIENT_MESSAGE_ID_CAPABILITY } from "./client-admission-record.js";
4
4
  import { buildRpcSessionState } from "./connection-handler.js";
5
- import { AUTO_TITLE_PER_SESSION_CAPABILITY, AUTO_TITLE_SESSIONS_CAPABILITY, BROWSER_ENGINE_CAPABILITY, CONTINUE_FROM_LEAF_CAPABILITY, DURABLE_SESSION_ID_CAPABILITY, MEDIA_PLACEHOLDERS_CAPABILITY, PROMPT_SURFACE_CAPABILITY, PROMPT_SURFACE_CHAT_CAPABILITY, RETAIN_ON_DISCONNECT_CAPABILITY, SESSION_CONTEXT_CAPABILITY, SESSION_KIND_CAPABILITY, WARM_CAPABILITY, } from "./custom-capability.js";
5
+ import { AUTO_TITLE_PER_SESSION_CAPABILITY, AUTO_TITLE_SESSIONS_CAPABILITY, BROWSER_ENGINE_CAPABILITY, CONTINUE_FROM_LEAF_CAPABILITY, DURABLE_SESSION_ID_CAPABILITY, MEDIA_PLACEHOLDERS_CAPABILITY, PROMPT_SURFACE_CAPABILITY, PROMPT_SURFACE_CHAT_CAPABILITY, RETAIN_ON_DISCONNECT_CAPABILITY, RETRY_FALLBACK_PROFILE_CAPABILITY, SESSION_CONTEXT_CAPABILITY, SESSION_KIND_CAPABILITY, WARM_CAPABILITY, } from "./custom-capability.js";
6
6
  import { answerWarm } from "./host-warm.js";
7
7
  import { protocolIdentity } from "./protocol-identity.js";
8
- import { sessionAutoTitleError, sessionBrowserEngineError, sessionContextError, sessionKindError, sessionPromptSurfaceError, } from "./rpc-input-validation.js";
8
+ import { sessionAutoTitleError, sessionBrowserEngineError, sessionContextError, sessionKindError, sessionPromptSurfaceError, sessionRetryFallbackError, } from "./rpc-input-validation.js";
9
9
  import { RPC_ERROR_INVALID_LAUNCH_PROFILE, RPC_ERROR_INVALID_SESSION_CONTEXT, RPC_ERROR_INVALID_SESSION_KIND, RPC_ERROR_MISSING_SESSION_ID, RPC_ERROR_OPEN_FAILED, RPC_ERROR_SESSION_CLOSING, RPC_ERROR_UNKNOWN_SESSION, } from "./rpc-types.js";
10
10
  import { runWithSessionAttribution } from "./session-attribution.js";
11
11
  import { createRpcSessionBinding } from "./session-binding.js";
@@ -213,7 +213,10 @@ export class SessionCommandRouter {
213
213
  PROMPT_SURFACE_CHAT_CAPABILITY,
214
214
  // Each session's tool subprocesses and eval kernel get its own OMO_BROWSER_ENGINE from its launch profile.
215
215
  BROWSER_ENGINE_CAPABILITY,
216
+ // Each session's fallback chain is its own in-memory settings override, never the host's file.
217
+ RETRY_FALLBACK_PROFILE_CAPABILITY,
216
218
  ACCEPT_EDITS_PERMISSION_PRESET_CAPABILITY,
219
+ AUTO_PERMISSION_PRESET_CAPABILITY,
217
220
  // Only an in-process runtime shares the loop a warm loads into (senpi#2314).
218
221
  ...(this.registry.warm ? [WARM_CAPABILITY] : []),
219
222
  ...(this.connectionOptions?.capabilities ?? []),
@@ -317,7 +320,7 @@ export class SessionCommandRouter {
317
320
  sweepIdleSessions() {
318
321
  const now = this.idleNow();
319
322
  const idleEvictionMs = this.memoryPressure ? this.idleEvictionMs / 2 : this.idleEvictionMs;
320
- const verdicts = selectSweepEvictions(this.registry, now, idleEvictionMs);
323
+ const verdicts = selectSweepEvictions(this.registry, now, idleEvictionMs, (sessionId) => this.activeRequests.has(sessionId) || (this.bindings.get(sessionId)?.pendingPrompts?.().length ?? 0) > 0);
321
324
  for (const sessionId of verdicts.orphaned)
322
325
  void this.evictIdleSession(sessionId, "session_dir_removed");
323
326
  for (const sessionId of verdicts.idle)
@@ -486,6 +489,9 @@ export class SessionCommandRouter {
486
489
  const browserEngineError = sessionBrowserEngineError(command.browserEngine);
487
490
  if (browserEngineError)
488
491
  return error(command.id, "open_session", `${RPC_ERROR_INVALID_LAUNCH_PROFILE}: ${browserEngineError}`);
492
+ const retryFallbackError = sessionRetryFallbackError(command.retryFallback);
493
+ if (retryFallbackError)
494
+ return error(command.id, "open_session", `${RPC_ERROR_INVALID_LAUNCH_PROFILE}: ${retryFallbackError}`);
489
495
  let opened;
490
496
  try {
491
497
  opened = await this.registry.openSession({
@@ -504,6 +510,7 @@ export class SessionCommandRouter {
504
510
  ...(typeof command.auto_title === "boolean" ? { autoTitle: command.auto_title } : {}),
505
511
  ...(command.promptSurface !== undefined ? { promptSurface: command.promptSurface } : {}),
506
512
  ...(command.browserEngine !== undefined ? { browserEngine: command.browserEngine } : {}),
513
+ ...(command.retryFallback !== undefined ? { retryFallback: command.retryFallback } : {}),
507
514
  },
508
515
  // Host lifecycle policy, deliberately outside the immutable launch profile.
509
516
  { retainOnDisconnect: command.retain_on_disconnect === true });
@@ -6,6 +6,7 @@ export async function attachToOpenSession(entries, sessionPath, profile, options
6
6
  const [handle, entry] = existing;
7
7
  const wasParked = entry.retainOnDisconnect === true && entry.attachments === 0;
8
8
  entry.attachments += 1;
9
+ entry.detachedAt = undefined;
9
10
  // The claim carries the attachment state another generation decides on: a path this host is
10
11
  // actively serving a client on is never reclaimable from it.
11
12
  pathReservations?.setAttached(sessionPath, true);