jeopi-cli 16.4.2 → 16.4.4

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 (148) hide show
  1. package/CHANGELOG.md +74 -0
  2. package/dist/cli.js +3033 -2991
  3. package/dist/types/advisor/advise-tool.d.ts +5 -1
  4. package/dist/types/advisor/config.d.ts +4 -3
  5. package/dist/types/advisor/runtime.d.ts +15 -0
  6. package/dist/types/cli/args.d.ts +2 -0
  7. package/dist/types/cli/update-cli.d.ts +4 -1
  8. package/dist/types/cli/usage-error.d.ts +4 -0
  9. package/dist/types/commit/agentic/agent.d.ts +1 -0
  10. package/dist/types/config/model-registry.d.ts +3 -3
  11. package/dist/types/config/model-resolver.d.ts +7 -1
  12. package/dist/types/config/models-config.d.ts +1 -1
  13. package/dist/types/config/settings-schema.d.ts +3 -13
  14. package/dist/types/eval/py/tool-bridge.d.ts +1 -0
  15. package/dist/types/extensibility/custom-tools/types.d.ts +3 -0
  16. package/dist/types/extensibility/extensions/runner.d.ts +3 -1
  17. package/dist/types/extensibility/extensions/types.d.ts +3 -0
  18. package/dist/types/internal-urls/memory-protocol.d.ts +6 -6
  19. package/dist/types/internal-urls/types.d.ts +7 -0
  20. package/dist/types/irc/bus.d.ts +4 -0
  21. package/dist/types/lsp/deferred-diagnostics.d.ts +11 -0
  22. package/dist/types/modes/components/move-overlay.d.ts +1 -1
  23. package/dist/types/modes/components/plan-review-overlay.d.ts +2 -0
  24. package/dist/types/modes/components/welcome.d.ts +4 -0
  25. package/dist/types/modes/magic-keyword-boundary.d.ts +9 -0
  26. package/dist/types/modes/orchestrate.d.ts +1 -1
  27. package/dist/types/modes/rpc/host-tools.d.ts +2 -0
  28. package/dist/types/modes/rpc/rpc-mode.d.ts +27 -7
  29. package/dist/types/modes/ultrathink.d.ts +1 -1
  30. package/dist/types/modes/utils/transcript-render-helpers.d.ts +9 -0
  31. package/dist/types/modes/workflow.d.ts +1 -1
  32. package/dist/types/session/agent-session.d.ts +6 -0
  33. package/dist/types/session/auth-broker-config.d.ts +2 -2
  34. package/dist/types/tools/bash-skill-urls.d.ts +3 -2
  35. package/dist/types/tools/browser/cmux/cmux-tab.d.ts +3 -1
  36. package/dist/types/tools/browser/cmux/rpc.d.ts +20 -0
  37. package/dist/types/tools/browser/launch.d.ts +1 -0
  38. package/dist/types/tools/browser/run-cancellation.d.ts +17 -2
  39. package/dist/types/tools/browser/run-output.d.ts +25 -0
  40. package/dist/types/tools/browser/tab-worker.d.ts +22 -0
  41. package/dist/types/utils/changelog.d.ts +35 -1
  42. package/package.json +12 -12
  43. package/src/advisor/__tests__/advisor.test.ts +13 -0
  44. package/src/advisor/__tests__/config.test.ts +36 -0
  45. package/src/advisor/advise-tool.ts +6 -1
  46. package/src/advisor/config.ts +11 -8
  47. package/src/advisor/runtime.ts +127 -0
  48. package/src/autolearn/controller.ts +15 -3
  49. package/src/cli/args.ts +12 -0
  50. package/src/cli/flag-tables.ts +21 -7
  51. package/src/cli/update-cli.ts +63 -13
  52. package/src/cli/usage-error.ts +7 -0
  53. package/src/commands/acp.ts +11 -2
  54. package/src/commands/launch.ts +12 -3
  55. package/src/commit/agentic/agent.ts +4 -0
  56. package/src/commit/agentic/index.ts +46 -21
  57. package/src/config/config-file.ts +30 -12
  58. package/src/config/model-registry.ts +7 -7
  59. package/src/config/model-resolver.ts +24 -6
  60. package/src/config/models-config.ts +1 -1
  61. package/src/config/settings-schema.ts +3 -15
  62. package/src/edit/index.ts +10 -76
  63. package/src/edit/renderer.ts +13 -10
  64. package/src/eval/__tests__/agent-bridge.test.ts +19 -14
  65. package/src/eval/__tests__/bridge-timeout.test.ts +60 -0
  66. package/src/eval/executor-base.ts +84 -7
  67. package/src/eval/js/shared/rewrite-imports.ts +31 -13
  68. package/src/eval/py/tool-bridge.ts +13 -14
  69. package/src/exec/non-interactive-env.ts +0 -1
  70. package/src/export/ttsr.ts +0 -3
  71. package/src/extensibility/custom-tools/loader.ts +70 -37
  72. package/src/extensibility/custom-tools/types.ts +3 -0
  73. package/src/extensibility/extensions/runner.ts +3 -0
  74. package/src/extensibility/extensions/types.ts +3 -0
  75. package/src/extensibility/plugins/legacy-pi-bundled-keys.ts +9 -1
  76. package/src/extensibility/plugins/legacy-pi-bundled-registry.ts +30 -3
  77. package/src/extensibility/plugins/manager.ts +21 -0
  78. package/src/internal-urls/docs-index.generated.txt +1 -1
  79. package/src/internal-urls/memory-protocol.ts +13 -8
  80. package/src/internal-urls/skill-protocol.ts +1 -1
  81. package/src/internal-urls/types.ts +7 -0
  82. package/src/irc/bus.ts +48 -18
  83. package/src/lsp/client.ts +7 -1
  84. package/src/lsp/deferred-diagnostics.ts +66 -0
  85. package/src/main.ts +13 -13
  86. package/src/mcp/tool-bridge.ts +57 -6
  87. package/src/mcp/transports/stdio.test.ts +45 -1
  88. package/src/mcp/transports/stdio.ts +6 -3
  89. package/src/modes/acp/acp-agent.ts +73 -2
  90. package/src/modes/acp/acp-event-mapper.ts +5 -0
  91. package/src/modes/acp/acp-mode.ts +11 -0
  92. package/src/modes/components/__tests__/move-overlay.test.ts +16 -1
  93. package/src/modes/components/advisor-config.ts +15 -7
  94. package/src/modes/components/chat-transcript-builder.ts +31 -11
  95. package/src/modes/components/move-overlay.ts +2 -3
  96. package/src/modes/components/plan-review-overlay.ts +7 -0
  97. package/src/modes/components/tips.txt +1 -1
  98. package/src/modes/components/welcome.ts +13 -14
  99. package/src/modes/controllers/command-controller.ts +8 -10
  100. package/src/modes/controllers/event-controller.ts +58 -42
  101. package/src/modes/controllers/input-controller.ts +9 -6
  102. package/src/modes/controllers/selector-controller.ts +3 -2
  103. package/src/modes/controllers/tan-command-controller.ts +40 -1
  104. package/src/modes/interactive-mode.ts +13 -0
  105. package/src/modes/magic-keyword-boundary.ts +23 -0
  106. package/src/modes/orchestrate.ts +6 -5
  107. package/src/modes/print-mode.ts +9 -0
  108. package/src/modes/rpc/host-tools.ts +13 -0
  109. package/src/modes/rpc/rpc-mode.ts +130 -56
  110. package/src/modes/ultrathink.ts +6 -5
  111. package/src/modes/utils/transcript-render-helpers.ts +44 -0
  112. package/src/modes/utils/ui-helpers.ts +16 -1
  113. package/src/modes/workflow.ts +5 -4
  114. package/src/prompts/system/eager-task.md +2 -2
  115. package/src/prompts/system/tan-context-switch.md +17 -0
  116. package/src/prompts/tools/browser.md +5 -5
  117. package/src/sdk.ts +3 -0
  118. package/src/session/agent-session.ts +267 -36
  119. package/src/session/auth-broker-config.ts +4 -4
  120. package/src/session/snapcompact-inline.ts +3 -19
  121. package/src/session/streaming-output.ts +40 -12
  122. package/src/slash-commands/builtin-registry.ts +8 -8
  123. package/src/task/executor.ts +8 -0
  124. package/src/task/render.ts +51 -44
  125. package/src/tools/bash-skill-urls.ts +7 -4
  126. package/src/tools/bash.ts +30 -33
  127. package/src/tools/browser/cmux/cmux-tab.ts +49 -50
  128. package/src/tools/browser/cmux/rpc.ts +50 -0
  129. package/src/tools/browser/launch.ts +8 -4
  130. package/src/tools/browser/run-cancellation.ts +44 -5
  131. package/src/tools/browser/run-output.ts +76 -0
  132. package/src/tools/browser/tab-worker.ts +280 -133
  133. package/src/tools/eval-render.ts +16 -14
  134. package/src/tools/glob.ts +20 -6
  135. package/src/tools/grep.ts +1 -0
  136. package/src/tools/image-gen.ts +3 -4
  137. package/src/tools/irc.ts +27 -13
  138. package/src/tools/path-utils.ts +6 -1
  139. package/src/tools/read.ts +14 -7
  140. package/src/tools/write.ts +22 -5
  141. package/src/tools/yield.ts +29 -1
  142. package/src/utils/changelog.ts +106 -9
  143. package/src/utils/git.ts +0 -1
  144. package/src/utils/title-generator.ts +72 -7
  145. package/src/web/search/index.ts +14 -6
  146. package/src/web/search/providers/perplexity.ts +18 -2
  147. package/dist/types/tools/bash-command-fixup.d.ts +0 -11
  148. package/src/tools/bash-command-fixup.ts +0 -37
@@ -1,5 +1,6 @@
1
1
  import workflowNotice from "../prompts/system/workflow-notice.md" with { type: "text" };
2
2
  import { createGradientHighlighter, type KeywordHighlighter } from "./gradient-highlight";
3
+ import { magicKeywordRegex } from "./magic-keyword-boundary";
3
4
  import { keywordInProse } from "./markdown-prose";
4
5
 
5
6
  /**
@@ -14,15 +15,15 @@ import { keywordInProse } from "./markdown-prose";
14
15
  * "workflowz.ts" never do.
15
16
  */
16
17
 
17
- // Detection: lowercase keyword flanked by whitespace or a string edge. Non-global so `.test` stays stateless.
18
- const WORKFLOW_WORD = /(?<!\S)workflowz(?!\S)/;
18
+ // Detection: lowercase keyword flanked by prose punctuation, whitespace, or a string edge.
19
+ const WORKFLOW_WORD = magicKeywordRegex("workflowz");
19
20
 
20
21
  /** Hidden system notice appended after a user message that mentions "workflowz". */
21
22
  export const WORKFLOW_NOTICE: string = workflowNotice.trim();
22
23
 
23
24
  /**
24
25
  * Whether `text` contains the standalone keyword "workflowz"
25
- * (lowercase, whitespace-delimited) in prose — never inside a code block, inline
26
+ * (lowercase, prose-delimited) in prose — never inside a code block, inline
26
27
  * code span, or XML/HTML section.
27
28
  */
28
29
  export function containsWorkflow(text: string): boolean {
@@ -36,7 +37,7 @@ export function containsWorkflow(text: string): boolean {
36
37
  */
37
38
  export const highlightWorkflow: KeywordHighlighter = createGradientHighlighter({
38
39
  probe: /workflowz/,
39
- highlight: /(?<!\S)workflowz(?!\S)/g,
40
+ highlight: magicKeywordRegex("workflowz", "g"),
40
41
  stops: 14,
41
42
  hue: t => 30 + t * 120,
42
43
  });
@@ -1,7 +1,7 @@
1
1
  <system-reminder>
2
2
  Task delegation is enabled — subagents are the default for this request.
3
3
 
4
- Explore and settle the approach FIRST. Once the design is settled, you MUST fan the work out to `{{toolRefs.task}}` subagents instead of implementing it yourself.{{#if taskBatch}} Batch independent slices into ONE parallel `{{toolRefs.task}}` call; never serialize work that can run concurrently.{{/if}}
4
+ Explore and settle the approach FIRST — scoping, top-level decomposition, and cross-slice contracts are YOUR job; NEVER spawn a subagent to produce the overall plan (per-slice design travels with its executor). Once the design is settled, you MUST fan the work out to `{{toolRefs.task}}` subagents instead of implementing it yourself.{{#if taskBatch}} Batch independent slices into ONE parallel `{{toolRefs.task}}` call; never serialize work that can run concurrently.{{/if}}
5
5
 
6
- Work alone only for: a single-file edit under ~30 lines, a direct answer requiring no code changes, or a command the user explicitly asked you to run.
6
+ Work alone for: a single-file edit under ~30 lines, a direct answer requiring no code changes, a command the user explicitly asked you to run, or when only ONE runnable slice exists — a lone subagent is a lossy handoff, not parallelism.
7
7
  </system-reminder>
@@ -0,0 +1,17 @@
1
+ <system-notice cause="fork">
2
+ The conversation above belongs to your parent session.
3
+ You are a fork created solely to handle the user's request below.
4
+
5
+ Your parent agent is still working on the original task — that responsibility is
6
+ NOT yours. You have no obligations from the prior conversation.
7
+
8
+ - Focus EXCLUSIVELY on the user's immediate request. Nothing else.
9
+ - NEVER continue, follow up on, or intervene in anything discussed before this
10
+ message. Those belong to the parent session.
11
+ - Your parent is CONCURRENTLY editing this same working directory. Files may
12
+ change between your reads, look mid-refactor, or fail to compile. That is the
13
+ parent's live work — NEVER fix, audit, or build on it, even if it looks broken.
14
+ - Any todo list, plan, or unfinished checklist from the prior conversation is
15
+ the parent's. NEVER resume or update it.
16
+ - After addressing the user's request, STOP. Do not work on ANY OTHER TASK.
17
+ </system-notice>
@@ -5,7 +5,7 @@ Drives real Chromium tab; full puppeteer access via JS.
5
5
  - Three actions:
6
6
  - `open` — acquire/reuse named tab (`name` defaults `"main"`). Optional `url` (navigate once ready), `viewport`, `dialogs: "accept" | "dismiss"` (auto-handle `alert`/`confirm`/`beforeunload`; else page hangs till you wire `page.on('dialog', …)`).
7
7
  - `close` — release tab by `name`, or all with `all: true`. `kill: true` also kills spawned-app process trees.
8
- - `run` — execute JS in existing tab. `code` = async function body; `page`, `browser`, `tab`, `display`, `assert`, `wait` in scope. Return value JSON-stringified into result; `display(value)` accumulates text/images.
8
+ - `run` — execute JS in existing tab. `code` = async function body; `page`, `browser`, `tab`, `display`, `assert`, `wait` in scope. Return value JSON-stringified into result; `display(value)` accumulates text/images. `wait(ms)` sleeps; `wait(fn, { timeout?, interval? })` polls `fn` (sync or async) until truthy and resolves with that value (default 30s timeout, 100ms interval; named error on timeout) — use it instead of in-page polling Promises inside `tab.evaluate`.
9
9
  - Tabs survive `run` calls and in-process subagents — open once, reuse.
10
10
  - Browser kinds (`app` on `open`):
11
11
  - default (no `app`) → headless Chromium with stealth patches.
@@ -17,9 +17,9 @@ Drives real Chromium tab; full puppeteer access via JS.
17
17
  - `tab.observe({ includeAll?, viewportOnly? })` — accessibility snapshot: `{ url, title, viewport, scroll, elements: [{ id, role, name, value, states, … }] }`. Ids stable until next observe/goto.
18
18
  - `tab.ariaSnapshot(selector?, { depth?, boxes? })` — Playwright-format ARIA-tree YAML (nested roles + accessible names + `/url`/`/placeholder`), scoped to `selector` or the whole document. Every node carries a `[ref=eN]` id; `[cursor=pointer]` flags clickables. Captures dense, hierarchical structure/text that `observe()`'s flat list flattens away. Refs renumber from e1 each call and stay valid until the next `ariaSnapshot()`.
19
19
  - `tab.ref("e5")` — `[ref=eN]` from the last ariaSnapshot → element handle with the common action methods (`.click()`, `.type()`, `.fill()`, `.hover()`, `.evaluate()`, …); the primary way to act on a ref. For convenience `aria-ref=e5` also works inline in `tab.click`/`type`/`fill`/`waitFor`/`scrollIntoView` (e.g. `tab.click("aria-ref=e5")`).
20
- - `tab.id(n)` — id from last observe → `ElementHandle` (`.click()`, `.type()`, …).
20
+ - `tab.id(n)` — id from last observe → element handle with the same action methods (`.click()`, `.type()`, `.fill()`, …).
21
21
  - `tab.click(selector)` / `tab.type(selector, text)` / `tab.fill(selector, value)` / `tab.press(key, { selector? })` / `tab.scroll(dx, dy)`.
22
- - `tab.waitFor(selector, { timeout? })` / `tab.waitForSelector(selector, { timeout?, visible?, hidden? })` — wait until attached (optionally visible/hidden); returns the `ElementHandle`.
22
+ - `tab.waitFor(selector, { timeout? })` / `tab.waitForSelector(selector, { timeout?, visible?, hidden? })` — wait until attached (optionally visible/hidden); returns an action-method handle.
23
23
  - `tab.drag(from, to)` — endpoints: selector (center-to-center) or `{ x, y }` viewport point (canvases, sliders).
24
24
  - `tab.scrollIntoView(selector)` — center in viewport; before clicking off-screen elements.
25
25
  - `tab.select(selector, …values)` — set `<select>` option(s); returns selection. `tab.fill` NEVER works for selects.
@@ -27,10 +27,10 @@ Drives real Chromium tab; full puppeteer access via JS.
27
27
  - `tab.waitForUrl(pattern, { timeout? })` — substring or `RegExp` (matches SPA pushState nav); returns matched URL.
28
28
  - `tab.waitForResponse(pattern, { timeout? })` — substring, `RegExp`, or `(response) => boolean`; returns puppeteer `HTTPResponse` (`.text()`/`.json()`/`.status()`/`.headers()`).
29
29
  - `tab.waitForNavigation({ waitUntil?, timeout? })` — resolves on the next navigation. Start it BEFORE the click/submit that triggers it; after `tab.goto` (which already waits) use `tab.waitForUrl`/`tab.waitForSelector` instead.
30
- - `tab.evaluate(fn, …args)` — `page.evaluate` for ad-hoc DOM reads.
30
+ - `tab.evaluate(fn, …args)` — run ad-hoc code in the page's MAIN world. DOM and page-defined globals (`window.myFlag`) are visible; mutations affect the page.
31
31
  - `tab.screenshot({ selector?, fullPage?, save?, silent? })` — capture + attach for viewing (`silent: true` skips). Pass `save` only when a later step needs the file.
32
32
  - `tab.extract(format = "markdown")` — readable page content (`"markdown"` | `"text"`); throws when nothing readable.
33
- - Selectors: CSS + puppeteer handlers `aria/Sign in`, `text/Continue`, `xpath/…`, `pierce/…`; also Playwright-style `p-aria/…`, `p-text/…`. Playwright-only engines/pseudos (`:has-text()`, `:visible`, …) are rejected — use `text/…` or `aria/…`. A stalled action/wait fails fast with a named `tab.<op> timed out` error, never the whole-cell timeout.
33
+ - Selectors: CSS + puppeteer handlers `aria/Sign in`, `text/Continue`, `xpath/…`, `pierce/…`; also Playwright-style `p-aria/…`, `p-text/…`. Playwright-only engines/pseudos (`:has-text()`, `:visible`, …) are rejected — use `text/…` or `aria/…`. A stalled action/wait fails fast with a named `tab.<op>` error carrying a match-count diagnosis, never the whole-cell timeout; a selector matching nothing fails in ~2s (pass an explicit `{ timeout }` to `waitFor`/`waitForSelector` to wait out slow-appearing elements).
34
34
  </instruction>
35
35
 
36
36
  <critical>
package/src/sdk.ts CHANGED
@@ -847,6 +847,7 @@ function createCustomToolContext(ctx: ExtensionContext): CustomToolContext {
847
847
  isIdle: ctx.isIdle,
848
848
  hasQueuedMessages: ctx.hasPendingMessages,
849
849
  abort: ctx.abort,
850
+ localProtocolOptions: ctx.localProtocolOptions,
850
851
  };
851
852
  }
852
853
 
@@ -2053,6 +2054,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2053
2054
  modelRegistry,
2054
2055
  () => (hasSession ? createSessionMemoryRuntimeContext(session, agentDir, cwd) : undefined),
2055
2056
  settings,
2057
+ localProtocolOptions,
2056
2058
  );
2057
2059
 
2058
2060
  credentialDisabledTarget = extensionRunner;
@@ -2071,6 +2073,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2071
2073
  session.abort({ reason: USER_INTERRUPT_LABEL });
2072
2074
  },
2073
2075
  settings,
2076
+ localProtocolOptions,
2074
2077
  autoApprove: options.autoApprove ?? false,
2075
2078
  });
2076
2079
  const toolContextStore = new ToolContextStore(getSessionContext);
@@ -144,13 +144,16 @@ import {
144
144
  AdvisorEmissionGuard,
145
145
  type AdvisorMessageDetails,
146
146
  type AdvisorNote,
147
+ AdvisorOutputQuarantinedError,
147
148
  AdvisorRuntime,
148
149
  type AdvisorSeverity,
149
150
  AdvisorTranscriptRecorder,
150
151
  advisorTranscriptFilename,
152
+ buildAdvisorQuarantineSourceText,
151
153
  formatAdvisorBatchContent,
152
154
  isAdvisorInterruptImmuneTurnActive,
153
155
  isInterruptingSeverity,
156
+ quarantineAdvisorUnsafeOutput,
154
157
  resolveAdvisorDeliveryChannel,
155
158
  slugifyAdvisorName,
156
159
  } from "../advisor";
@@ -173,7 +176,7 @@ import {
173
176
  resolveModelOverride,
174
177
  resolveModelRoleValue,
175
178
  } from "../config/model-resolver";
176
- import { MODEL_ROLE_IDS, MODEL_ROLES } from "../config/model-roles";
179
+ import { getKnownRoleIds, MODEL_ROLE_IDS, MODEL_ROLES } from "../config/model-roles";
177
180
  import { expandPromptTemplate, type PromptTemplate } from "../config/prompt-templates";
178
181
  import { applyProviderGlobalsFromSettings } from "../config/provider-globals";
179
182
  import { buildServiceTierByFamily, serviceTierForAllFamilies, serviceTierSettingToTier } from "../config/service-tier";
@@ -387,6 +390,64 @@ const MID_RUN_TODO_NUDGE_MUTATING_TOOLS: Record<string, true> = {
387
390
  write: true,
388
391
  ast_edit: true,
389
392
  };
393
+ const MARKDOWN_PROMPT_PREFIX_RE = /^(?:>\s*)?(?:(?:[-*+]|\d+[.)])\s+)*/;
394
+ const PROMPT_LABEL_RE = /^(?:q(?:uestion)?|ask)\s*\d*\s*[:.)-]\s*/i;
395
+ const QUESTION_PROMPT_RE =
396
+ /^(?:what|which|when|where|why|how|who|whom|whose|do|does|did|can|could|would|will|should|is|are|am|may|shall)\b/i;
397
+ const USER_DIRECTED_PROMPT_RE = /\b(?:you|your|we|our)\b/i;
398
+ const USER_RESPONSE_CUE_RE =
399
+ /^(?:please\s+)?(?:confirm|reply|choose|pick|decide|advise)\b|^(?:please\s+)?answer\b|^(?:please\s+)?(?:let\s+me\s+know|tell\s+me)\b/i;
400
+
401
+ function assistantText(message: AssistantMessage): string {
402
+ return message.content
403
+ .filter((content): content is TextContent => content.type === "text")
404
+ .map(content => content.text)
405
+ .join("\n")
406
+ .trim();
407
+ }
408
+
409
+ interface PromptLine {
410
+ text: string;
411
+ hadPromptLabel: boolean;
412
+ }
413
+
414
+ function promptLine(line: string): PromptLine {
415
+ const withoutMarkdownPrefix = line.trim().replace(MARKDOWN_PROMPT_PREFIX_RE, "").trim();
416
+ const withoutPromptLabel = withoutMarkdownPrefix.replace(PROMPT_LABEL_RE, "").trim();
417
+ return {
418
+ text: withoutPromptLabel,
419
+ hadPromptLabel: withoutPromptLabel !== withoutMarkdownPrefix,
420
+ };
421
+ }
422
+
423
+ function isQuestionPromptLine(line: string): boolean {
424
+ const candidate = promptLine(line);
425
+ if (!/[??]\s*$/.test(candidate.text)) return false;
426
+ return (
427
+ candidate.hadPromptLabel ||
428
+ QUESTION_PROMPT_RE.test(candidate.text) ||
429
+ USER_DIRECTED_PROMPT_RE.test(candidate.text)
430
+ );
431
+ }
432
+
433
+ function isResponseCueLine(line: string): boolean {
434
+ const candidate = promptLine(line)
435
+ .text.replace(/[.!?。!?]+$/, "")
436
+ .trim();
437
+ return USER_RESPONSE_CUE_RE.test(candidate);
438
+ }
439
+
440
+ function isAwaitingUserAnswer(message: AssistantMessage): boolean {
441
+ const text = assistantText(message);
442
+ if (!text) return false;
443
+ const tailLines = text
444
+ .split(/\r?\n/)
445
+ .map(line => line.trim())
446
+ .filter(Boolean)
447
+ .slice(-6);
448
+ const lastLine = tailLines.at(-1);
449
+ return tailLines.some(isQuestionPromptLine) || (lastLine !== undefined && isResponseCueLine(lastLine));
450
+ }
390
451
  /** `customType` for the hidden mid-run todo nudge; `display: false`, so it reaches
391
452
  * the model but never renders in the TUI or transcript. */
392
453
  const MID_RUN_TODO_NUDGE_MESSAGE_TYPE = "mid-run-todo-nudge";
@@ -1387,6 +1448,20 @@ function isDisplayableQueuedMessage(message: AgentMessage): boolean {
1387
1448
  function isAdvisorCard(message: AgentMessage): message is CustomMessage {
1388
1449
  return message.role === "custom" && message.customType === "advisor";
1389
1450
  }
1451
+ function isTerminalTextAssistantAnswer(message: AgentMessage | undefined): message is AssistantMessage {
1452
+ if (message?.role !== "assistant" || message.stopReason !== "stop") return false;
1453
+ let hasText = false;
1454
+ for (const part of message.content) {
1455
+ if (part.type === "toolCall") return false;
1456
+ if (part.type === "text") {
1457
+ if (part.text.trim().length > 0) hasText = true;
1458
+ continue;
1459
+ }
1460
+ if (part.type === "thinking") continue;
1461
+ return false;
1462
+ }
1463
+ return hasText;
1464
+ }
1390
1465
 
1391
1466
  /**
1392
1467
  * A queued message the user can restore to the editor / pull back as a draft.
@@ -1829,6 +1904,7 @@ export class AgentSession {
1829
1904
  // Cursor exec, TUI listeners) is held back. Without this, a client that resumes
1830
1905
  // on `agent_end` can fire its next `prompt` before #promptWithMessage's finally
1831
1906
  #emptyStopRetryCount = 0;
1907
+ #acceptTerminalEmptyStopForPrompt = false;
1832
1908
  #unexpectedStopRetryCount = 0;
1833
1909
  #promptGeneration = 0;
1834
1910
  #pendingAgentEndEmit: AgentSessionEvent | undefined;
@@ -2415,8 +2491,16 @@ export class AgentSession {
2415
2491
  if (this.#advisorSharedInstructions) systemPrompt.push(this.#advisorSharedInstructions);
2416
2492
  if (config.instructions?.trim()) systemPrompt.push(config.instructions.trim());
2417
2493
 
2418
- const names = config.tools?.length ? new Set(config.tools) : ADVISOR_DEFAULT_TOOL_NAMES;
2494
+ const names = config.tools === undefined ? ADVISOR_DEFAULT_TOOL_NAMES : new Set(config.tools);
2419
2495
  const tools = (this.#advisorTools ?? []).filter(t => names.has(t.name));
2496
+ const availableAdvisorToolNames = new Set<string>();
2497
+ availableAdvisorToolNames.add(adviseTool.name);
2498
+ for (const tool of tools) {
2499
+ availableAdvisorToolNames.add(tool.name);
2500
+ if (tool.customWireName !== undefined) availableAdvisorToolNames.add(tool.customWireName);
2501
+ }
2502
+ let quarantinedAdvisorOutput: string | undefined;
2503
+ let currentAdvisorInput = "";
2420
2504
 
2421
2505
  const advisorSessionId = this.#advisorSessionId(slug);
2422
2506
  const appendOnlyContext = new AppendOnlyContextManager();
@@ -2461,6 +2545,13 @@ export class AgentSession {
2461
2545
  streamFn: this.#advisorStreamFn,
2462
2546
  onPayload: this.#onPayload,
2463
2547
  onResponse: this.#onResponse,
2548
+ transformAssistantMessage: message => {
2549
+ quarantinedAdvisorOutput = quarantineAdvisorUnsafeOutput(
2550
+ message,
2551
+ availableAdvisorToolNames,
2552
+ buildAdvisorQuarantineSourceText(currentAdvisorInput, advisorAgent.state.messages),
2553
+ );
2554
+ },
2464
2555
  onSseEvent: this.#onSseEvent,
2465
2556
  transformProviderContext: this.#transformProviderContext,
2466
2557
  intentTracing: false,
@@ -2471,7 +2562,19 @@ export class AgentSession {
2471
2562
  advisorAgent.setDisableReasoning(shouldDisableReasoning(advisorThinkingLevel));
2472
2563
 
2473
2564
  const advisorAgentFacade: AdvisorAgent = {
2474
- prompt: input => advisorAgent.prompt(input),
2565
+ prompt: async input => {
2566
+ let quarantined: string | undefined;
2567
+ try {
2568
+ quarantinedAdvisorOutput = undefined;
2569
+ currentAdvisorInput = input;
2570
+ await advisorAgent.prompt(input);
2571
+ quarantined = quarantinedAdvisorOutput;
2572
+ } finally {
2573
+ quarantinedAdvisorOutput = undefined;
2574
+ currentAdvisorInput = "";
2575
+ }
2576
+ if (quarantined) throw new AdvisorOutputQuarantinedError(quarantined);
2577
+ },
2475
2578
  abort: reason => advisorAgent.abort(reason),
2476
2579
  reset: () => {
2477
2580
  advisorAgent.reset();
@@ -2577,15 +2680,23 @@ export class AgentSession {
2577
2680
  return slug ? `${this.sessionId}-advisor-${slug}` : `${this.sessionId}-advisor`;
2578
2681
  }
2579
2682
 
2683
+ #hasTerminalTextAnswerWithoutQueuedWork(): boolean {
2684
+ if (this.agent.hasQueuedMessages() || this.#pendingNextTurnMessages.length > 0) return false;
2685
+ return isTerminalTextAssistantAnswer(this.agent.state.messages.at(-1));
2686
+ }
2687
+
2580
2688
  /**
2581
2689
  * Route one accepted advice note from `advisor` to the primary. Concern and
2582
2690
  * blocker interrupt the running agent through the steering channel; once the
2583
- * loop has yielded, `triggerTurn` resumes it. After a deliberate user interrupt
2584
- * auto-resume is suppressed while idle/unwinding (the note becomes a preserved
2585
- * card re-entering on resume); a live-streaming turn is steered in directly. A
2586
- * plain nit always rides the non-interrupting YieldQueue aside. Suppression by
2587
- * the per-advisor emission guard drops the note silently — the model still saw
2588
- * `Recorded.`, so it isn't tempted to rephrase the same note past the dedupe.
2691
+ * loop has yielded, `triggerTurn` resumes it. If the loop already ended with a
2692
+ * terminal text answer and no queued work remains, the note is preserved as an
2693
+ * advisor card instead of waking a duplicate completion turn. After a deliberate
2694
+ * user interrupt auto-resume is suppressed while idle/unwinding (the note
2695
+ * becomes a preserved card re-entering on resume); a live-streaming turn is
2696
+ * steered in directly. A plain nit always rides the non-interrupting YieldQueue
2697
+ * aside. Suppression by the per-advisor emission guard drops the note silently —
2698
+ * the model still saw `Recorded.`, so it isn't tempted to rephrase the same note
2699
+ * past the dedupe.
2589
2700
  */
2590
2701
  #routeAdvice(advisor: ActiveAdvisor, note: string, severity?: AdvisorSeverity): void {
2591
2702
  if (!advisor.emissionGuard.accept(note)) {
@@ -2605,6 +2716,7 @@ export class AgentSession {
2605
2716
  streaming: this.agent.state.isStreaming,
2606
2717
  aborting: this.#abortInProgress,
2607
2718
  interruptImmuneTurnActive: interrupting && this.#isAdvisorInterruptImmuneTurnActive(),
2719
+ terminalAnswerNoQueuedWork: this.#hasTerminalTextAnswerWithoutQueuedWork(),
2608
2720
  });
2609
2721
  if (channel === "aside") {
2610
2722
  this.yieldQueue.enqueue("advisor", { note, severity, advisor: source });
@@ -3946,6 +4058,17 @@ export class AgentSession {
3946
4058
  await emitAgentEndNotification();
3947
4059
  return;
3948
4060
  }
4061
+ } else if (this.#isHardErrorFallbackEligible(msg)) {
4062
+ // A non-retryable hard error on a model covered by a configured
4063
+ // fallback chain: retrying the SAME model is pointless, but a
4064
+ // DIFFERENT model is a fresh chance — consult the chain before
4065
+ // surfacing the failure. #handleRetryableError bails out (no
4066
+ // backoff-retry of the failing model) when no switch happens.
4067
+ const didRetry = await this.#handleRetryableError(msg, { hardErrorFallback: true });
4068
+ if (didRetry) {
4069
+ await emitAgentEndNotification();
4070
+ return;
4071
+ }
3949
4072
  }
3950
4073
  // Classifier refusals are persisted-skipped above; also prune the trailing
3951
4074
  // stub from active context so the next turn's prompt does not replay it.
@@ -3990,7 +4113,7 @@ export class AgentSession {
3990
4113
  await emitAgentEndNotification();
3991
4114
  return;
3992
4115
  }
3993
- const todoContinuationScheduled = await this.#checkTodoCompletion();
4116
+ const todoContinuationScheduled = await this.#checkTodoCompletion(msg);
3994
4117
  if (todoContinuationScheduled) {
3995
4118
  await emitAgentEndNotification();
3996
4119
  return;
@@ -6518,6 +6641,8 @@ export class AgentSession {
6518
6641
  abort: () => {
6519
6642
  this.agent.abort();
6520
6643
  },
6644
+ settings: this.settings,
6645
+ localProtocolOptions: this.#localProtocolOptions(),
6521
6646
  });
6522
6647
 
6523
6648
  for (const customTool of mcpTools) {
@@ -7003,6 +7128,30 @@ export class AgentSession {
7003
7128
  return this.#resolveRoleModelFull(role, this.#modelRegistry.getAvailable(), this.model);
7004
7129
  }
7005
7130
 
7131
+ /**
7132
+ * Resolve the explicit thinking suffix that should apply when a temporary
7133
+ * picker selects a model already assigned to a configured role.
7134
+ */
7135
+ resolveTemporaryModelThinkingLevel(model: Model): ConfiguredThinkingLevel | undefined {
7136
+ const availableModels = this.#modelRegistry.getAvailable();
7137
+ if (availableModels.length === 0) return undefined;
7138
+
7139
+ const matchPreferences = getModelMatchPreferences(this.settings);
7140
+ for (const role of getKnownRoleIds(this.settings)) {
7141
+ const roleValue = this.settings.getModelRole(role);
7142
+ if (!roleValue) continue;
7143
+
7144
+ const resolved = resolveModelRoleValue(roleValue, availableModels, {
7145
+ settings: this.settings,
7146
+ matchPreferences,
7147
+ });
7148
+ if (!resolved.explicitThinkingLevel || resolved.thinkingLevel === undefined || !resolved.model) continue;
7149
+ if (modelsAreEqual(resolved.model, model)) return resolved.thinkingLevel;
7150
+ }
7151
+
7152
+ return undefined;
7153
+ }
7154
+
7006
7155
  get promptTemplates(): ReadonlyArray<PromptTemplate> {
7007
7156
  return this.#promptTemplates;
7008
7157
  }
@@ -7476,6 +7625,7 @@ export class AgentSession {
7476
7625
  options?: Pick<PromptOptions, "toolChoice" | "images" | "skipCompactionCheck"> & {
7477
7626
  prependMessages?: AgentMessage[];
7478
7627
  skipPostPromptRecoveryWait?: boolean;
7628
+ acceptTerminalEmptyStop?: boolean;
7479
7629
  },
7480
7630
  ): Promise<void> {
7481
7631
  this.#beginInFlight();
@@ -7493,6 +7643,7 @@ export class AgentSession {
7493
7643
  this.#midRunNudgeCount = 0;
7494
7644
  this.#emptyStopRetryCount = 0;
7495
7645
  this.#unexpectedStopRetryCount = 0;
7646
+ this.#acceptTerminalEmptyStopForPrompt = options?.acceptTerminalEmptyStop === true;
7496
7647
  // A new prompt cycle starts: drop any sticky yield-termination from the
7497
7648
  // previous run so empty-stop / unexpected-stop / compaction maintenance
7498
7649
  // can evaluate this turn normally.
@@ -7997,12 +8148,25 @@ export class AgentSession {
7997
8148
  }
7998
8149
  }
7999
8150
 
8000
- async #promptAgentInitiatedMessage(message: CustomMessage): Promise<void> {
8151
+ async #promptAgentInitiatedMessage(
8152
+ message: CustomMessage,
8153
+ options?: { acceptTerminalEmptyStop?: boolean },
8154
+ ): Promise<void> {
8001
8155
  this.#beginInFlight();
8156
+ const acceptTerminalEmptyStop = options?.acceptTerminalEmptyStop === true;
8002
8157
  try {
8158
+ if (acceptTerminalEmptyStop) {
8159
+ this.#emptyStopRetryCount = 0;
8160
+ this.#unexpectedStopRetryCount = 0;
8161
+ this.#yieldTerminationPending = false;
8162
+ this.#acceptTerminalEmptyStopForPrompt = true;
8163
+ } else {
8164
+ this.#acceptTerminalEmptyStopForPrompt = false;
8165
+ }
8003
8166
  await this.agent.prompt(message);
8004
8167
  await this.#waitForPostPromptRecovery();
8005
8168
  } finally {
8169
+ this.#acceptTerminalEmptyStopForPrompt = false;
8006
8170
  this.#endInFlight();
8007
8171
  }
8008
8172
  }
@@ -8057,7 +8221,12 @@ export class AgentSession {
8057
8221
  */
8058
8222
  async sendCustomMessage<T = unknown>(
8059
8223
  message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details" | "attribution">,
8060
- options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn"; queueChipText?: string },
8224
+ options?: {
8225
+ triggerTurn?: boolean;
8226
+ deliverAs?: "steer" | "followUp" | "nextTurn";
8227
+ queueChipText?: string;
8228
+ acceptTerminalEmptyStop?: boolean;
8229
+ },
8061
8230
  ): Promise<boolean> {
8062
8231
  const details =
8063
8232
  options?.queueChipText && options.deliverAs !== "nextTurn"
@@ -8100,7 +8269,9 @@ export class AgentSession {
8100
8269
  this.#queueHiddenNextTurnMessage(normalizedAppMessage, false);
8101
8270
  return false;
8102
8271
  }
8103
- await this.#promptAgentInitiatedMessage(normalizedAppMessage);
8272
+ await this.#promptAgentInitiatedMessage(normalizedAppMessage, {
8273
+ acceptTerminalEmptyStop: options.acceptTerminalEmptyStop === true,
8274
+ });
8104
8275
  return true;
8105
8276
  }
8106
8277
  this.agent.appendMessage(normalizedAppMessage);
@@ -8119,7 +8290,9 @@ export class AgentSession {
8119
8290
  this.#queueHiddenNextTurnMessage(normalizedAppMessage, false);
8120
8291
  return false;
8121
8292
  }
8122
- await this.#promptAgentInitiatedMessage(normalizedAppMessage);
8293
+ await this.#promptAgentInitiatedMessage(normalizedAppMessage, {
8294
+ acceptTerminalEmptyStop: options.acceptTerminalEmptyStop === true,
8295
+ });
8123
8296
  return true;
8124
8297
  }
8125
8298
 
@@ -9263,6 +9436,11 @@ export class AgentSession {
9263
9436
  * candidate is small or the session has been idle long enough that the
9264
9437
  * provider prompt cache is cold), so it is cheap to run every turn. Gated
9265
9438
  * on the `compaction.supersedeReads` and `compaction.dropUseless` settings.
9439
+ *
9440
+ * Persists via `rewriteEntries` like every other history rewrite — the
9441
+ * session file must match the live (pruned) context or file-based forks
9442
+ * (`/fork`, `/tan`) and resume rebuild a divergent prefix and cold-miss the
9443
+ * provider prompt cache.
9266
9444
  */
9267
9445
  async #pruneStaleToolResults(): Promise<{ prunedCount: number; tokensSaved: number } | undefined> {
9268
9446
  const { supersedeReads, dropUseless } = this.settings.getGroup("compaction");
@@ -9285,6 +9463,7 @@ export class AgentSession {
9285
9463
  return undefined;
9286
9464
  }
9287
9465
 
9466
+ await this.sessionManager.rewriteEntries();
9288
9467
  const sessionContext = this.buildDisplaySessionContext();
9289
9468
  this.agent.replaceMessages(sessionContext.messages);
9290
9469
  this.#resetAllAdvisorRuntimes();
@@ -10381,23 +10560,29 @@ export class AgentSession {
10381
10560
  return false;
10382
10561
  }
10383
10562
 
10563
+ if (this.#acceptTerminalEmptyStopForPrompt && assistantMessage.stopReason === "stop") {
10564
+ this.#acceptTerminalEmptyStopForPrompt = false;
10565
+ this.#emptyStopRetryCount = 0;
10566
+ return false;
10567
+ }
10568
+
10384
10569
  this.#emptyStopRetryCount++;
10385
10570
  if (this.#emptyStopRetryCount > EMPTY_STOP_MAX_RETRIES) {
10386
- logger.warn("Assistant returned empty stop after retry cap", {
10387
- attempts: this.#emptyStopRetryCount - 1,
10571
+ const attempts = this.#emptyStopRetryCount - 1;
10572
+ const finalError = "Assistant returned empty stop after retry cap";
10573
+ logger.warn(finalError, {
10574
+ attempts,
10388
10575
  model: assistantMessage.model,
10389
10576
  provider: assistantMessage.provider,
10390
10577
  });
10391
- if (this.#retryAttempt > 0) {
10392
- await this.#emitSessionEvent({
10393
- type: "auto_retry_end",
10394
- success: false,
10395
- attempt: this.#retryAttempt,
10396
- finalError: "Assistant returned empty stop after retry cap",
10397
- });
10398
- this.#retryAttempt = 0;
10399
- this.#refusalBackoff = undefined;
10400
- }
10578
+ await this.#emitSessionEvent({
10579
+ type: "auto_retry_end",
10580
+ success: false,
10581
+ attempt: this.#retryAttempt > 0 ? this.#retryAttempt : attempts,
10582
+ finalError,
10583
+ });
10584
+ this.#retryAttempt = 0;
10585
+ this.#refusalBackoff = undefined;
10401
10586
  this.#resolveRetry();
10402
10587
  // Tool-use orphans corrupt Anthropic message history (tool_result without
10403
10588
  // matching tool_use). Always remove them even when the retry cap is hit.
@@ -10891,7 +11076,7 @@ export class AgentSession {
10891
11076
  /**
10892
11077
  * Check if agent stopped with incomplete todos and prompt to continue.
10893
11078
  */
10894
- async #checkTodoCompletion(): Promise<boolean> {
11079
+ async #checkTodoCompletion(message: AssistantMessage): Promise<boolean> {
10895
11080
  // Skip todo reminders when the most recent turn was driven by an explicit user force —
10896
11081
  // the user wanted exactly that tool, not a follow-up nag about incomplete todos.
10897
11082
  const lastServedLabel = this.#toolChoiceQueue.consumeLastServedLabel();
@@ -10949,6 +11134,13 @@ export class AgentSession {
10949
11134
  return false;
10950
11135
  }
10951
11136
 
11137
+ if (isAwaitingUserAnswer(message)) {
11138
+ logger.debug("Todo completion: assistant is waiting for user input; skipping reminder", {
11139
+ incomplete: incomplete.length,
11140
+ });
11141
+ return false;
11142
+ }
11143
+
10952
11144
  // Build reminder message
10953
11145
  this.#todoReminderCount++;
10954
11146
  const todoList = incompleteByPhase
@@ -13078,6 +13270,33 @@ export class AgentSession {
13078
13270
  if (AIError.is(id, AIError.Flag.AuthFailed)) return false;
13079
13271
  return this.#modelRegistry.find("fireworks", toFireworksBaseModelId(model.id)) !== undefined;
13080
13272
  }
13273
+ /**
13274
+ * True when a turn failed with a hard (non-retryable) provider error but a
13275
+ * configured `retry.fallbackChains` entry covers the active model: the same
13276
+ * model is not worth retrying, yet a DIFFERENT model is a fresh chance, so
13277
+ * the chain is consulted before the error becomes final. Skips failures a
13278
+ * model switch cannot fix or must not replay: cancellations (abort-flavored
13279
+ * errors are not model faults), context overflow (compaction's job),
13280
+ * classifier refusals (chain consult is handled on the retryable path with
13281
+ * `pinFallback`), and turns that already emitted a tool call (replaying
13282
+ * could duplicate work).
13283
+ */
13284
+ #isHardErrorFallbackEligible(message: AssistantMessage): boolean {
13285
+ if (message.stopReason !== "error") return false;
13286
+ const model = this.model;
13287
+ if (!model) return false;
13288
+ const retrySettings = this.settings.getGroup("retry");
13289
+ if (!retrySettings.enabled || !retrySettings.modelFallback) return false;
13290
+ if (this.#isClassifierRefusal(message)) return false;
13291
+ const id = this.#classifyRetryMessage(message);
13292
+ if (AIError.is(id, AIError.Flag.Abort) || AIError.is(id, AIError.Flag.UserInterrupt)) return false;
13293
+ if (AIError.isContextOverflow(message, model.contextWindow ?? 0)) return false;
13294
+ if (this.#hasReplayUnsafeToolOutput(message)) return false;
13295
+ const currentSelector = formatRetryFallbackSelector(model, this.thinkingLevel);
13296
+ const role = this.#activeRetryFallback?.role ?? this.#resolveRetryFallbackRole(currentSelector);
13297
+ if (!role) return false;
13298
+ return this.#findRetryFallbackCandidates(role, currentSelector).length > 0;
13299
+ }
13081
13300
 
13082
13301
  /**
13083
13302
  * Switch the active model from a Fireworks Fast (`-fast`) variant to its base
@@ -13208,12 +13427,16 @@ export class AgentSession {
13208
13427
  }
13209
13428
 
13210
13429
  /**
13211
- * Handle retryable errors with exponential backoff.
13430
+ * Handle retryable errors with exponential backoff, credential rotation, and
13431
+ * model-fallback chains. Also entered for NON-retryable errors when a switch
13432
+ * is the recovery (`fireworksFastFallback`, `hardErrorFallback`): then a
13433
+ * successful model switch retries immediately, and a failed switch surfaces
13434
+ * the error without a same-model backoff retry.
13212
13435
  * @returns true if retry was initiated, false if max retries exceeded or disabled
13213
13436
  */
13214
13437
  async #handleRetryableError(
13215
13438
  message: AssistantMessage,
13216
- options?: { allowModelFallback?: boolean; fireworksFastFallback?: boolean },
13439
+ options?: { allowModelFallback?: boolean; fireworksFastFallback?: boolean; hardErrorFallback?: boolean },
13217
13440
  ): Promise<boolean> {
13218
13441
  const retrySettings = this.settings.getGroup("retry");
13219
13442
  // The Fireworks Fast→base degrade is an intrinsic model-selection safety net,
@@ -13399,11 +13622,16 @@ export class AgentSession {
13399
13622
  this.#retryAttempt = Math.min(this.#retryAttempt, 1);
13400
13623
  delayMs = refusalBackoffDelayMs(backoff.attempt);
13401
13624
  }
13402
- // Fast→base was requested but the base switch could not happen (e.g. the
13403
- // base model has no credential). Don't fall through to backing-off and
13404
- // retrying the failing fast model for a hard router error that the generic
13405
- // classifier wouldn't retry — surface it instead.
13406
- if (options?.fireworksFastFallback && !switchedModel && !this.#isRetryableError(message)) {
13625
+ // A fallback switch was the whole reason we entered (Fast→base degrade or
13626
+ // a hard-error chain consult) but it could not happen (e.g. no candidate
13627
+ // has a credential). Don't fall through to backing-off and retrying the
13628
+ // failing model for an error the generic classifier wouldn't retry —
13629
+ // surface it instead.
13630
+ if (
13631
+ (options?.fireworksFastFallback || options?.hardErrorFallback) &&
13632
+ !switchedModel &&
13633
+ !this.#isRetryableError(message)
13634
+ ) {
13407
13635
  this.#retryAttempt = 0;
13408
13636
  this.#resolveRetry();
13409
13637
  return false;
@@ -14102,10 +14330,13 @@ export class AgentSession {
14102
14330
  // Side-channel turns must not share OpenAI/Codex append-only
14103
14331
  // conversation state with the main agent turn: IRC and /btw can run
14104
14332
  // while the main turn is mid-tool-call. Keep the prompt-cache key
14105
- // stable, but give provider routing a unique request lineage.
14333
+ // stable, but give provider routing a unique request lineage. The
14334
+ // shared provider state map is still required so Codex can allocate
14335
+ // websocket state under that side-channel session id.
14106
14336
  sessionId: `${cacheSessionId}:side:${Snowflake.next()}`,
14107
14337
  promptCacheKey: cacheSessionId,
14108
- preferWebsockets: false,
14338
+ preferWebsockets: this.#preferWebsockets,
14339
+ providerSessionState: this.#providerSessionState,
14109
14340
  reasoning: toReasoningEffort(this.thinkingLevel),
14110
14341
  disableReasoning: shouldDisableReasoning(this.thinkingLevel),
14111
14342
  hideThinkingSummary: this.agent.hideThinkingSummary,